| name | push-pr |
| description | Test (verify + smoke), commit, push, and open a draft PR with a CI report. Pass test to run only the test phase, ready for a non-draft PR, or a target branch to merge into. |
| argument-hint | [test [MODIFIER...]|ready|target-branch] |
Push — Test, Commit, Push, Draft PR, and Merge Workflow
Test the outstanding work (verification plus smoke tests), commit it with a smart conventional commit, branch off the default branch first if you're still on it, push to the remote repository, and open a draft PR by default. Drafts are the default because the author should run hero-skills:review-pr (which calls all pr-review-toolkit agents plus a security pass, applies fixes, and asks for confirmation) before promoting the PR to ready-for-review. After a successful push, this skill also prints a brief CI status summary.
The test phase (Step 2) absorbed the former hero-skills:test-changes skill — run hero-skills:push-pr test for a test-only run that stops before any commit.
Arguments
$ARGUMENTS - Optional mode keyword or target branch. Only the first whitespace-separated token is matched against the keywords below (exact match, not prefix) — a branch literally named test or ready cannot be targeted this way and needs a rename or a manual git merge instead:
- (none, default) - Test, commit if dirty, push, and create a draft PR
test [MODIFIER...] - Run only Step 2 (verification + smoke tests) and stop — no commit, no push. Optional trailing tokens narrow the run: verify (static checks + unit tests only), smoke (skip verification), backend, frontend [routes...] (routes must start with /), cli, mcp, or free text (a test description to focus on)
ready - Test, commit if dirty, push, and create a non-draft PR (ready for review immediately) — only use when you have already self-reviewed or for trivial changes
- Any other first token - Treated as a target branch name (e.g.,
main, develop): test, commit if dirty, push, then merge into that branch (no PR)
Instructions
Step 0: Load Hero Configuration
HERO_LIB="${CLAUDE_PLUGIN_ROOT:-$HOME/.claude/plugins/hero-skills}/scripts/hero-lib.sh"
[ -r "$HERO_LIB" ] || HERO_LIB="$(git rev-parse --show-toplevel)/scripts/hero-lib.sh"
. "$HERO_LIB" || { echo "ERROR: cannot source hero-lib.sh — reinstall the plugin."; exit 1; }
ROOT=$(hero_root)
cat "$ROOT/HERO.md" 2>/dev/null || echo "NO_HERO_CONFIG"
hero_check_staleness
Each bash block below runs in a fresh shell, so re-source hero-lib.sh at the top of any block that calls a hero_* function.
Read HERO.md if it exists. This skill uses:
- Repository → default branch (for branching and PR base), branch convention, commit convention
- Code Quality → linters, formatters, type checkers (test phase + pre-commit steps)
- Projects → language, framework, install/test/dev commands, ports (test phase; skips auto-detection)
- CI/CD → platform name for PR description context and CI status reporting
- Project Management → issue prefix for branch names,
Fixes:/Relates to: trailers, and linking PRs to issues
If HERO.md is missing, suggest hero-skills:init-hero but proceed with defaults (the test phase falls back to auto-detection). If the stale-HERO hint fired, mention it once to the user but do not block.
Step 1: Branch if on Default Branch
FIRST_ARG=$(printf '%s' "$ARGUMENTS" | awk '{print $1}')
Parse only the first whitespace-separated token — a target branch that happens to start with test (e.g. testing, test-staging) must not be misread as the test keyword.
If $FIRST_ARG is exactly test, skip this step — a test-only run commits nothing, so it may run on any branch, including the default.
Never commit or push directly to the default branch.
. "${CLAUDE_PLUGIN_ROOT:-$HOME/.claude/plugins/hero-skills}/scripts/hero-lib.sh"
BRANCH=$(git branch --show-current)
DEFAULT_BRANCH=$(hero_default_branch_verbose)
echo "Current branch: $BRANCH (default: $DEFAULT_BRANCH)"
If $BRANCH is not $DEFAULT_BRANCH, skip this step entirely and proceed to Step 2 on the current branch.
If $BRANCH equals $DEFAULT_BRANCH: pull it fresh before branching off it — a stale local default branch means the new feature branch (and later, the PR's base diff) silently misses recent commits.
if ! git pull --ff-only origin "$DEFAULT_BRANCH"; then
echo "STOP: 'git pull --ff-only origin $DEFAULT_BRANCH' failed — local $DEFAULT_BRANCH may be divergent or dirty."
echo "Resolve manually (check 'git status'; a diverged local $DEFAULT_BRANCH needs 'git fetch' + reconciling)."
echo "Then re-run hero-skills:push-pr. Do not branch off a base that failed to update."
fi
If the pull failed, stop here — do not proceed to branch creation below. Only continue once it succeeds.
Then derive a feature-branch name from the diff and check out a new branch. Uncommitted changes follow the checkout automatically — do not stash.
Generate BRANCH_NAME by applying hero_branch_policy — the shared naming rules, also used by one-shot's auto-branch step so the two cannot drift:
. "${CLAUDE_PLUGIN_ROOT:-$HOME/.claude/plugins/hero-skills}/scripts/hero-lib.sh"
hero_branch_policy
Deriving the name is a model task, not a shell one — read the diff, then apply the policy. Unlike one-shot (which derives and proceeds), push-pr proposes and waits for confirmation.
Present the proposed name and let the user confirm or modify:
You are on '$BRANCH', which is the default branch. push-pr never commits or pushes directly to the default branch.
Proposed branch: BRANCH_NAME
Options:
1. Use the proposed name
2. Provide your own branch name
3. Cancel
Wait for confirmation, then:
git checkout -b "$BRANCH_NAME"
git branch --show-current
git status --porcelain
Step 2: Test the Changes (Verification + Smoke)
Verify the implementation works end to end before anything is committed: static checks (lint, typecheck) and unit tests against changed files, then auto-detect project type and run smoke tests against running services. Works for standalone projects, monorepo subprojects, or full-stack apps with multiple layers.
Mode selection from $ARGUMENTS (after the leading test, when present): verify runs only Step 2b; smoke, backend, frontend [routes...], cli, and mcp skip Step 2b and run only the matching part of Step 2c; anything else (including no modifier) runs both. Free text that is not a mode keyword is a test description to focus on.
Failure semantics: if a check fails with a quick, mechanical fix (lint, typo, import order), apply the fix and re-run. If it fails in a way that needs design judgment (test asserting wrong behavior, integration breakage, flaky CI), or the UI smoke flags a regression on a changed route, STOP — report the failure and hand back to the user. Never carry a known regression into a commit.
2a: Detect Project Structure
Use HERO.md's Projects section when present. Otherwise scan the current directory (and immediate subdirectories) for indicators:
| Indicator | Type | Default Port |
|---|
pyproject.toml + fastmcp/mcp dep | MCP Server | 8000 |
pyproject.toml + FastAPI/Flask import in app/ | Backend API | 8000 |
pyproject.toml + [project.scripts] or __main__.py | CLI/Library | - |
package.json + next.config.* | Frontend (Next.js) | 3000 |
package.json + vite.config.* | Frontend (Vite) | 5173 |
backend/ + frontend/ subdirs | Full-stack | both |
ls pyproject.toml package.json next.config.* vite.config.* 2>/dev/null
ls backend/pyproject.toml frontend/package.json 2>/dev/null
Check the project's CLAUDE.md for specific run instructions. Report what was detected; if nothing, ask the user. Install dependencies first when needed (uv sync, npm install — per project for full-stack).
2b: Verify Implementation (Lint, Typecheck, Unit Tests)
Capture the list of changed files first (uncommitted, then last commit, fallback to empty). Read the dedupe back into the array via mapfile so filenames containing spaces, tabs, or globs survive intact — ($(...)) would word-split and corrupt them:
mapfile -t CHANGED_FILES < <(git diff --name-only; git diff --name-only HEAD~1 HEAD 2>/dev/null)
mapfile -t CHANGED_FILES < <(printf "%s\n" "${CHANGED_FILES[@]}" | sort -u)
If CHANGED_FILES is empty, run the checks on the whole project (replace "${CHANGED_FILES[@]}" with . or the project root).
Use commands from HERO.md Code Quality and Projects sections when available. Otherwise auto-detect:
- Lint:
uv run ruff check "${CHANGED_FILES[@]}" (Python), npx eslint "${CHANGED_FILES[@]}" (TS/JS), go vet ./... (Go)
- Typecheck:
uv run mypy "${CHANGED_FILES[@]}" (Python), npx tsc --noEmit (TS)
- Unit tests: the
test-command from HERO.md per project; else uv run pytest / npm test. If a test file maps directly to a changed source file, prefer running just those tests for speed.
Then, a scoped pre-commit dry-run — this is the only place a push-pr test run (which stops before Step 3) ever exercises pre-commit, so skipping it here would mean commit-hook regressions surface only in a real commit:
if command -v pre-commit > /dev/null 2>&1; then
pre-commit run --files "${CHANGED_FILES[@]}"
else
echo "NO_PRECOMMIT"
fi
Report the verification result — NO_PRECOMMIT means pre-commit isn't installed; report that case as SKIPPED:
Verification
============
Lint: PASSED (0 issues)
Typecheck: PASSED (0 errors)
Unit tests: 42 passed, 0 failed
Pre-commit: PASSED (or SKIPPED if NO_PRECOMMIT)
If any check fails, apply the failure semantics above — mechanical fixes are fixed and re-run; judgment calls stop the skill before the smoke tests.
2c: Run Smoke Tests by Type
Skip entirely in verify mode.
CLI / Library — find entry points in pyproject.toml ([project.scripts]) or __main__.py and run with --help or a basic invocation (uv run SCRIPT_NAME --help); for libraries with no CLI, uv run python -c "import PACKAGE; print('OK')".
Backend API — start the server in the background (e.g., uv run uvicorn app.main:app --reload --port 8000), wait for ready, then smoke:
curl -s http://localhost:8000/health
curl -s http://localhost:8000/openapi.json | head -50
MCP Server — start the server, launch npx @modelcontextprotocol/inspector, connect via Playwright at http://localhost:6274 (Streamable HTTP → server URL → Connect), and exercise the available tools through the Inspector UI.
Full-stack — backend first (APIs must be ready), frontend second (may proxy to backend), then smoke each layer.
Frontend App — the full recipe below: detect whether this is a UI project, confirm/start its dev server under .test-output/, derive up to 5 routes from the diff (or use explicit /-routes passed after frontend), drive each route with Playwright MCP, and apply the console-noise allowlist and failure rules. If no UI project is detected, skip gracefully — expected on backend-only diffs, not a failure.
Detect UI project
If Step 2a detected no frontend indicator at all (no next.config.*, vite.config.*, and no HERO.md project with a UI-ish framework), skip this section and print:
(–) frontend: no UI project detected — skipping frontend smoke.
This is expected on backend-only PRs.
Otherwise, confirm which project to drive using HERO.md's ## Projects section (already loaded in Step 0). UI detection there is heuristic, not closed-enum — init-hero does not constrain the framework value, so treat the list below as a hint and fall back to asking the user when nothing matches.
Known-UI frameworks (auto-detected as UI):
next nextjs nuxt remix astro vite svelte sveltekit solid solid-start qwik
gatsby angular react cra create-react-app
Known-non-UI frameworks (auto-detected as backend, skip silently):
fastapi flask django starlette express nestjs hono fiber gin echo actix axum
rails sinatra laravel
Decision:
-
If any project's framework is in the known-UI set → that's the UI project. Continue.
-
If every project's framework is in the known-non-UI set OR there are no HERO.md projects with a framework field → fall back to Step 2a's file-based detection (next.config.*, vite.config.*). If that also found nothing, skip as above.
-
If a project's framework is in neither list (custom value, typo, or a UI framework not yet on the list), ask the user once:
Project 'PROJECT_NAME' declares framework: FRAMEWORK_VALUE.
Treat as a UI project for smoke testing?
[y] Yes — drive the dev server with Playwright MCP
[n] No — skip (recommended for non-UI frameworks)
[a] Add 'FRAMEWORK_VALUE' to the known-UI list in skills/push-pr/SKILL.md and continue (asks once per session, not durable)
Default to n if the user answers ambiguously — silently smoking a backend project is worse than silently skipping a UI one.
If multiple UI projects exist, ask the user which one to smoke-test (or pass it explicitly via the project's path). One per run keeps the dev-server lifecycle simple.
Record UI_PORT, UI_DEV_COMMAND, UI_PATH from the matched project. Validate that UI_PATH resolves under $ROOT:
if [ ! -d "$ROOT/$UI_PATH" ]; then
echo "ERROR: UI project path '$ROOT/$UI_PATH' does not exist."
echo " Check the 'path:' field for this project in HERO.md, or run"
echo " hero-skills:init-hero --update to re-detect."
exit 1
fi
Confirm or start the dev server
Check whether the dev server is already up on UI_PORT:
DEV_URL="http://localhost:$UI_PORT"
if curl -sf -o /dev/null -m 3 "$DEV_URL"; then
echo "Dev server already running at $DEV_URL — using it."
STARTED_BY_US=false
else
echo "Dev server is not responding at $DEV_URL."
STARTED_BY_US=true
fi
If STARTED_BY_US=true, ask the user before starting it:
The dev server is not running. Start it now?
[y] Start `UI_DEV_COMMAND` in the background — leaves it running after this skill finishes.
[n] Cancel — start it yourself, then re-run this skill.
On y, start the dev server with output captured to a log under .test-output/ and PID tracked:
mkdir -p "$ROOT/.test-output"
hero_exclude_add .test-output/
DEV_LOG="$ROOT/.test-output/dev-server.log"
: > "$DEV_LOG"
( cd "$ROOT/$UI_PATH" && $UI_DEV_COMMAND > "$DEV_LOG" 2>&1 ) &
DEV_PID=$!
echo "Started dev server (pid $DEV_PID, log $DEV_LOG)."
for i in $(seq 1 30); do
if ! kill -0 "$DEV_PID" 2>/dev/null; then
echo "Dev server process died before responding. Last 30 log lines:"
tail -30 "$DEV_LOG"
exit 1
fi
if curl -sf -o /dev/null -m 2 "$DEV_URL"; then
echo "Dev server is up after ${i}x2s."
break
fi
sleep 2
done
if ! curl -sf -o /dev/null -m 2 "$DEV_URL"; then
ALT_URL=""
for HOST in 127.0.0.1 ::1; do
case "$HOST" in
::*) PROBE_URL="http://[$HOST]:$UI_PORT" ;;
*) PROBE_URL="http://$HOST:$UI_PORT" ;;
esac
if curl -sf -o /dev/null -m 2 "$PROBE_URL"; then
ALT_URL="$PROBE_URL"
break
fi
done
LOG_URL=$(grep -Eom1 'https?://[a-zA-Z0-9.:-]+' "$DEV_LOG" 2>/dev/null || true)
if [ -n "$ALT_URL" ] || [ -n "$LOG_URL" ]; then
echo "Dev server is up but not on $DEV_URL."
[ -n "$ALT_URL" ] && echo " Reachable at: $ALT_URL"
[ -n "$LOG_URL" ] && echo " Server reports: $LOG_URL"
echo " Update HERO.md 'port:' (or 'host:' if your config supports it)"
echo " for project '$PROJECT_NAME' and re-run."
else
echo "Dev server did not come up within 60s. Last 30 log lines:"
tail -30 "$DEV_LOG"
fi
echo ""
echo "Cleaning up the process we started:"
kill "$DEV_PID" 2>/dev/null || true
sleep 1
if curl -sf -o /dev/null -m 2 "$DEV_URL"; then
echo "WARN: stale process still bound to :$UI_PORT after kill."
echo " Investigate: lsof -i :$UI_PORT"
fi
exit 1
fi
Note the log path so the user can tail -f it in another terminal if a smoke-test failure needs deeper diagnosis. Do NOT auto-tail it into this conversation — it will flood the context.
Identify routes
If /-routes were passed after frontend (e.g., hero-skills:push-pr test frontend /dashboard /settings/api), use those verbatim.
Otherwise, derive from the diff. For each changed file under the UI project, map to its owning route(s):
- Next.js App Router:
app/foo/bar/page.tsx → /foo/bar; app/(group)/x/page.tsx → /x (route groups are URL-invisible); route handlers (route.ts) excluded.
- Next.js dynamic / catch-all segments:
app/posts/[slug]/page.tsx, app/[...slug]/page.tsx, app/[[...slug]]/page.tsx — there is no canonical URL for these. Ask the user once for a sample value (e.g., a real slug from the dev DB), or skip the route with (–) and a note. Do not invent values like /posts/example — those usually 404.
- Next.js parallel and intercepted routes:
app/@modal/..., app/(.)photo/..., app/(..)settings/... — exclude entirely. They have no free-standing URL; navigating to a literal @modal returns 404 and pollutes the smoke result.
- Next.js Pages Router:
pages/foo/bar.tsx → /foo/bar; pages/index.tsx → /; pages/[slug].tsx → ask for a sample value or skip.
- Vite + React Router / SvelteKit / Remix / etc.: walk the routing config (
routes.tsx, +page.svelte, routes/) and emit the canonical paths. Apply the same dynamic-segment rule (ask for a sample or skip).
- Shared components (
components/Button.tsx, lib/, hooks/): no direct route. Pick the landing page (/) plus the most-changed page as a fallback so we exercise the rendering path at all.
If the diff touches no UI files at all (despite the project being a UI project — e.g., the change was server actions only), exercise just the landing page / so we still detect a hard regression like a build break.
Cap the route list at 5 routes for a smoke test. More than that and the user should run a real E2E suite.
Print the route list before driving so the user can object:
Smoke routes (N):
- /
- /dashboard
- /settings/api
Drive the browser
Mark BROWSER_OPENED=true after the first successful browser_navigate so the cleanup step knows whether to call browser_close.
For each route in order, run the same recipe via Playwright MCP:
-
mcp__playwright__browser_navigate to $DEV_URL$ROUTE. Set expectedStatus to 200-399 if the tool supports it; otherwise check status from a follow-up browser_network_requests call.
-
mcp__playwright__browser_wait_for until the page is interactive (look for a stable selector — body, the route's <h1>, or a known landmark from the snapshot).
-
mcp__playwright__browser_snapshot — capture the accessibility tree as the canonical "did it render" check.
-
mcp__playwright__browser_console_messages — read messages emitted since the last navigate.
-
mcp__playwright__browser_take_screenshot — save a PNG named smoke-ROUTE_SLUG.png under $ROOT/.test-output/playwright-mcp/. Before the first screenshot of this run, do the three-step setup once:
. "${CLAUDE_PLUGIN_ROOT:-$HOME/.claude/plugins/hero-skills}/scripts/hero-lib.sh"
mkdir -p "$ROOT/.test-output/playwright-mcp"
hero_exclude_add .test-output/
rm -f "$ROOT/.test-output/playwright-mcp"/smoke-*.png
$ROOT/.test-output/ is the canonical local-only test-artifacts directory for hero-skills. All disposable outputs from any hero skill — Playwright screenshots, traces, videos, network logs, dev-server logs, coverage reports — land somewhere under it so the repo root stays clean and a single exclude entry covers them all.
For routes that involve a form change (detected by reading the diff: <form> / useForm / onSubmit added or modified), additionally:
mcp__playwright__browser_fill_form with placeholder-but-plausible values for the visible inputs (keep it under 5 inputs — refuse if the form is huge; that's a real E2E test, not a smoke test).
- Click the submit control,
browser_wait_for the success state, browser_console_messages again.
Console noise allowlist
Dev-mode frameworks emit benign warnings on every page load. The allowlist below is the only set of console messages this skill ignores; everything else (including any console message of type=error) is treated as a failure. Do not invent additional patterns at runtime.
Next.js / React (development mode):
- "[Fast Refresh]"
- "[HMR]"
- "Download the React DevTools"
- "Warning: ReactDOM.render is no longer supported"
- any message whose body starts with "Warning:" AND contains "in development"
Vite:
- "[vite] connecting…"
- "[vite] connected."
- "[vite] hot updated:"
SvelteKit / Svelte:
- "[vite] connecting…" (same as Vite — Kit uses Vite under the hood)
General:
- any message whose URL is a `chrome-extension://` source (browser extensions
emitting in the page context — not the app's fault).
If a future framework has its own benign-warnings set, the user must update this list explicitly via a follow-up edit to this skill — the frontend smoke does not silently expand its filter set.
Failure rules
A route fails the smoke if any of:
-
The HTTP status of the document request is 4xx or 5xx.
-
An entry in browser_console_messages has type=error AND its body does NOT match an allowlist entry from the section above. Match the allowlist conservatively: if you are not sure whether a message is benign, treat it as a failure and let the user decide.
-
An uncaught exception appears in the dev server log (covers a broad set of common failures and ignores nothing). This check only applies when this skill started the dev server — $DEV_LOG is only set on that path; when the server was already running there is no log to grep, so gate on $DEV_LOG being set and the file existing:
if [ -n "$DEV_LOG" ] && [ -f "$DEV_LOG" ]; then
grep -Ei '\b(Error|Warning|Exception|Traceback|Unhandled[A-Z][a-zA-Z]*Rejection):' "$DEV_LOG"
grep -E '\bat [A-Za-z_$][A-Za-z0-9_$.]* \(.*:[0-9]+:[0-9]+\)' "$DEV_LOG"
grep -E '\bModule(Not)?Found|SyntaxError|RangeError|TypeError|ReferenceError' "$DEV_LOG"
fi
-
browser_wait_for times out — the page never became interactive.
-
A form submission's wait_for fails — the success state never rendered.
On any failure: stop driving further routes, surface the failing route + the console message + the screenshot path, and treat the run as failed. Do not auto-retry; the model is a poor judge of "transient vs real" for UI bugs.
2d: Test Cleanup and Report
if [ "${BROWSER_OPENED:-false}" = "true" ]; then
mcp__playwright__browser_close
fi
if [ "${STARTED_BY_US:-false}" = "true" ]; then
echo ""
echo "Dev server is still running (pid $DEV_PID, log $DEV_LOG)."
echo "Stop it now? [y/N]"
fi
If the user says yes, kill "$DEV_PID" and remove the log. Otherwise leave both in place. Stop any other background servers (backend, MCP inspector) started for this run via TaskStop, then report:
Test Results
=================
Project: {name}
Mode: all (verification + smoke)
Verification:
Lint: PASSED (0 issues)
Typecheck: PASSED (0 errors)
Unit tests: 42 passed, 0 failed
Smoke Tests:
Backend (FastAPI on :8000):
GET /health -> 200 OK
Frontend (Next.js on :3000):
Routes: N tested (/, /dashboard, /settings/api)
Console: X errors, Y warnings (filtered framework noise)
Screenshots: $ROOT/.test-output/playwright-mcp/*.png
Result: OK | FAILED at ROUTE — REASON
If $FIRST_ARG (from Step 1) is exactly test, STOP here — the test-only run is complete. Suggest /simplify and a plain hero-skills:push-pr as next steps. Otherwise continue to Step 3.
Step 3: Commit Dirty Changes (Smart Commit)
git status --porcelain
If the tree is clean (no output): the work is already committed — skip straight to Step 4.
If the tree is dirty, run the following before pushing.
3a: Run Pre-commit (if available)
if command -v pre-commit > /dev/null 2>&1; then
pre-commit run --all-files
else
echo "NO_PRECOMMIT"
fi
If pre-commit is installed and checks fail: report errors, offer to auto-fix, do not proceed until passing. If not installed, skip and continue.
3b: Analyze Changes
git status --porcelain
git diff
git diff --cached
git diff --stat
For each changed file: read the diff, understand its purpose, assess quality.
3c: Simplify Code
Invoke the simplify skill via the Skill tool. simplify is not part of this plugin — it ships separately (see the user-invocable skills list in the current session). It reviews the current diff for reuse, quality, and efficiency and fixes any issues found before the commit lands. Step 3g below handles the post-fix pre-push dry-run.
If the simplify skill is unavailable in this environment, report NO_SIMPLIFY_SKILL — falling back to inline checklist and apply this check before continuing:
3d: Ruthless Code Review
Additional checks beyond simplify:
Naming Consistency
- Same concepts use same names throughout
- Imports match exports
Code Quality
Completeness
Report:
Code Review Summary
===================
Files Changed: N
Lines Added: A, Removed: D
Issues Found:
- CRITICAL: FILE:LINE — description
- WARNING: FILE:LINE — description
Suggestions:
- IMPROVEMENT
Fix any CRITICAL or WARNING issues found. Re-run pre-commit after fixes (if available).
3e: Group into Changesets
Group logically related changes:
- Same feature/component together
- Same type of change together
- Dependency updates separate
- Documentation separate
3f: Commit Each Changeset
git add file1 file2 ...
git diff --cached --stat
git commit -m "$(cat <<'EOF'
{type}({scope}): {description}
{body if needed}
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
EOF
)"
Types: feat, fix, refactor, docs, style, test, chore, perf
If issue ID in branch name: Add Fixes: PROJ-123 or Relates to: PROJ-123.
3g: Post-Commit Pre-Push Dry-Run
Dry-run any pre-push hooks now so failures surface before the actual push (Workflow A1 / B1):
if command -v pre-commit > /dev/null 2>&1; then
pre-commit run --hook-stage pre-push --all-files
else
echo "NO_PRECOMMIT"
fi
3h: Commit Summary
Commit Summary
======================
Branch: {branch-name}
Commits Created: N
1. {type}({scope}): {description}
Files: file1, file2 (+X -Y)
Pre-commit: PASSED (or SKIPPED)
Proceed to Step 4.
Step 4: Determine Workflow
| Argument | Workflow |
|---|
| (none, default) | Push + Draft PR |
test | Already stopped after Step 2 (test-only) |
ready | Push + non-draft PR |
main/master | Push + Merge to main |
| Other branch | Push + Merge to target |
Workflow A: Push and Create PR (No Target)
A1: Push to Remote
git push -u origin $(git branch --show-current)
Handle push failures:
| Error | Action |
|---|
rejected (non-fast-forward) | Suggest git pull --rebase |
permission denied | Suggest gh auth login |
remote not found | Check remote configuration |
A2: Check for Existing PR
gh pr list --head $(git branch --show-current) --json number,url,title,state
If PR exists: Report it and skip to A5 (CI status).
A3: Create Pull Request
. "${CLAUDE_PLUGIN_ROOT:-$HOME/.claude/plugins/hero-skills}/scripts/hero-lib.sh"
ROOT=$(hero_root)
DEFAULT_BRANCH=$(hero_default_branch_verbose)
FETCH_FOR_DIFF_OK=true
git fetch origin "$DEFAULT_BRANCH" || FETCH_FOR_DIFF_OK=false
git log origin/$DEFAULT_BRANCH..HEAD --pretty=format:"%s%n%b" --reverse
git diff origin/$DEFAULT_BRANCH..HEAD --stat
git diff origin/$DEFAULT_BRANCH..HEAD --name-only
If $FETCH_FOR_DIFF_OK is false: this doesn't block the PR (GitHub computes the actual base/diff server-side regardless of local staleness), but the title and changeset list generated below are built from this possibly-stale local log — prepend a note to the generated PR body: ⚠️ Generated against a possibly-stale local view of $DEFAULT_BRANCH (fetch failed) — verify the changeset list against GitHub's own diff.
Determine the draft flag (drafts are the default). Parse the first whitespace-separated token of $ARGUMENTS so trailing whitespace or extra arguments don't silently fall through:
FIRST_ARG=$(printf '%s' "$ARGUMENTS" | awk '{print $1}')
DRAFT_FLAG="--draft"
if [ "$FIRST_ARG" = "ready" ]; then
DRAFT_FLAG=""
fi
Generate the PR title from commit history (use the most descriptive commit, or summarize if multiple):
PR_TITLE="$(git log origin/$DEFAULT_BRANCH..HEAD --pretty=%s | head -1)"
Generate PR content by listing each commit as a changeset with its files and description. Keep the title unbranded (no "Hero"/"hero-skills"). End the body with exactly one attribution line, _Generated using hero-skills._:
gh pr create $DRAFT_FLAG --base "$DEFAULT_BRANCH" --title "$PR_TITLE" --body "$(cat <<'EOF'
## Summary
[1-3 sentence overview of what this PR accomplishes]
## Changesets
### 1. `commit-type(scope): commit-message`
**Files:** `file1.ts`, `file2.ts` (+A -D)
Brief description of what this commit does and why
### 2. `commit-type(scope): commit-message`
**Files:** `file3.py` (+A -D)
Brief description of what this commit does and why
[...repeat for each commit on the branch]
## Test Plan
- [ ] [Test step 1]
- [ ] [Test step 2]
## Related Issues
[Link issues if mentioned in commits]
_Generated using hero-skills._
EOF
)"
A4: Report Success
Push Summary
=================
Branch: {branch-name}
Action: Push + Create Draft PR
Commits pushed: N
Draft PR created: #{number}
URL: {pr-url}
Next step: hero-skills:review-pr — self-review, runs pr-review-toolkit agents plus a security pass, applies fixes, marks ready (offer to auto-run: ask "Run it now? [y/N]", invoke via Skill tool on yes)
If the PR was created with ready (non-draft), report PR created instead of Draft PR created, skip the self-review hint, and pick exactly one next step instead:
- This PR touched dependency files (
package.json, pyproject.toml, lockfiles, .github/workflows/*.yml version pins, or Dockerfile* — harden covers Docker image hardening too): Next step: hero-skills:harden (print only — model-invocation-restricted, cannot auto-run).
- Otherwise:
Next step: hero-skills:ship-pr — once green, @auto-approve, merge, verify deploy, reset (offer to auto-run).
A5: Report CI Status
Give a brief, non-blocking CI summary after the push. Skip this step entirely if gh is unavailable.
BRANCH=$(git branch --show-current)
if ! gh repo view --json nameWithOwner -q .nameWithOwner >/dev/null 2>&1; then
echo "CI status unavailable (gh not authenticated or no remote) — skipping CI block."
else
RUNS_JSON=$(gh run list --branch "$BRANCH" --limit 5 \
--json databaseId,name,status,conclusion,headBranch,createdAt,url 2>&1)
if [ $? -ne 0 ]; then
echo "CI status unavailable (gh run list failed) — skipping CI block: $RUNS_JSON"
elif [ "$(printf '%s' "$RUNS_JSON" | tr -d '[:space:]')" = "[]" ]; then
echo "Overall: NO RUNS YET"
else
printf '%s\n' "$RUNS_JSON"
fi
fi
For each run, report: workflow name, status (queued/in_progress/completed), conclusion (success/failure/cancelled/skipped).
If any run failed, surface the failing job/step:
gh run view RUN_ID --json jobs \
--jq '.jobs[] | select(.conclusion=="failure") | {name, steps: [.steps[] | select(.conclusion=="failure") | .name]}'
Do not poll or block on long-running CI. If runs are still queued/in_progress, say so once and note that re-running this command later will show updated status.
Print a compact summary:
CI Status
=========
Branch: {branch-name}
Workflow Runs (latest 5):
1. Build & Test SUCCESS 2m 15s
2. Lint SUCCESS 45s
3. Docker Build FAILURE 1m 48s
Overall: PASSING | FAILING | IN PROGRESS | NO RUNS YET
If gh is unavailable, or gh run list errors (no workflows, no auth, etc.), skip this step silently and omit the CI Status block from the report.
Workflow B: Merge to Target Branch
B1: Push Feature Branch
git push -u origin $(git branch --show-current)
B2: Switch to Target and Pull
Before switching, verify the working tree is clean (Step 3 should have committed everything, but double-check):
FEATURE_BRANCH=$(git branch --show-current)
git status --porcelain
If uncommitted changes exist at this point, STOP. Do not switch branches — go back and commit them (re-run Step 3) before continuing.
git checkout $TARGET_BRANCH
git pull origin $TARGET_BRANCH
B3: Merge Feature Branch
git merge $FEATURE_BRANCH --no-ff -m "Merge branch '$FEATURE_BRANCH' into $TARGET_BRANCH"
If merge conflicts: Stop and let user resolve.
B4: Push Target
git push origin $TARGET_BRANCH
B5: Report and Suggest Cleanup
Push Summary
=================
Source: {feature-branch}
Target: {target-branch}
Merged successfully!
Suggestion: Delete the feature branch?
git branch -d {feature-branch}
git push origin --delete {feature-branch}
Safety Checks
Pre-push Hook Awareness
If .pre-commit-config.yaml exists, check for pre-push stage hooks:
if [ -f .pre-commit-config.yaml ]; then
grep -B2 "pre-push" .pre-commit-config.yaml
fi
Pre-push hooks often run tests, builds, and security scans which can take minutes. If heavy hooks are detected, warn the user before pushing:
Note: Pre-push hooks will run before push completes.
Detected: [pytest, eslint, build, semgrep, trivy, etc.]
This may take a few minutes.
Never Do
- Force push to main/master without explicit confirmation
- Auto-resolve merge conflicts
- Skip hooks with
--no-verify
- Push secrets or sensitive files
- Stash changes to work around a dirty tree — commit them instead (Step 3)
Large PR Warning
If diff >1000 lines or >50 files, warn and suggest breaking into smaller PRs.
Examples
hero-skills:push-pr # Test, commit, push, draft PR
hero-skills:push-pr ready # Test, commit, push, non-draft PR
hero-skills:push-pr test # Test only (verification + smoke), no commit
hero-skills:push-pr test verify # Only lint, typecheck, unit tests
hero-skills:push-pr test frontend /dashboard /settings # Smoke-test specific routes (verbatim)
hero-skills:push-pr test cli run the export command # Smoke-test a specific CLI command
hero-skills:push-pr develop # Test, commit, push, merge into develop
Notes
- Uses GitHub CLI (
gh) for PR, branch, and CI operations
- Respects repository PR templates if they exist
- Always creates merge commits for traceability
- Never commits or pushes directly to the default branch — Step 1 branches off first
- Always check the project's CLAUDE.md first for custom run instructions
- The frontend smoke is a smoke test, not a full E2E: cap routes at 5, skip large forms, do not chase flaky tests. If a real E2E suite already exists in the repo (Playwright config, Cypress, etc.), prefer running it directly instead.
- The test phase never modifies tracked source files. It only reads, drives, and reports, but writes disposable local artifacts under
$ROOT/.test-output/ (screenshots in .test-output/playwright-mcp/, the dev-server log at .test-output/dev-server.log). The ignore rule lives in .git/info/exclude (repo-local, untracked) — not .gitignore — so the working tree never gets dirtied.
- Use
browser_snapshot (not screenshots) for reliable element interaction; screenshots are captured separately as evidence for the report
- When testing completes, stop the background servers the test phase started — except the frontend dev server, which is left running by default and only stopped when the user opts in