skills/finish/SKILL.md
Use when implementation is complete, all tests pass, and you need to decide how to integrate the work - guides completion of development work by presenting structured options for merge, PR, or cleanup
npx skillsauth add raddue/crucible finishInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
All subagent dispatches use disk-mediated dispatch. See shared/dispatch-convention.md for the full protocol.
Guide completion of development work by presenting clear options and handling chosen workflow.
Core principle: Verify tests -> Code review -> Red-team -> Present options -> Execute choice -> Clean up.
Announce at start: "I'm using the finish skill to complete this work."
Before presenting options, verify tests pass:
# Run project's test suite
npm test / cargo test / pytest / go test ./...
If tests fail:
Tests failing (<N> failures). Must fix before completing:
[Show failures]
Cannot proceed with merge/PR until tests pass.
Stop. Don't proceed to Step 2.
If tests pass: Continue to Step 2.
Before presenting options, run a full code review.
REQUIRED SUB-SKILL: Use crucible:temper
BASE_SHA=$(git merge-base HEAD main 2>/dev/null || git merge-base HEAD master)
HEAD_SHA=$(git rev-parse HEAD)
git diff --stat $(git merge-base HEAD main 2>/dev/null || git merge-base HEAD master)...HEAD
Dispatch a code review subagent (general-purpose) using the temper/temper-reviewer.md template with:
--stat summary and key files list, let the reviewer pull targeted diffs rather than receiving the entire diff. Consider splitting into multiple focused reviewers -- one per subsystem.Act on feedback:
If fixes were made, re-run tests to confirm nothing broke.
Do NOT skip this step. The orchestrator did lightweight review during execution -- this is the comprehensive review before integration.
RECOMMENDED SUB-SKILL: Use crucible:test-coverage — audit whether existing tests are still aligned with the changes on this branch. Invoke with:
git diff <base-branch>..HEADThe test-coverage skill handles its own fix dispatch and revert-on-failure logic.
Skip this step when:
.md, .json, .yaml, config files)RECOMMENDED SUB-SKILL: Use crucible:forge (retrospective mode) — capture what happened vs what was planned while execution context is still fresh. Run this BEFORE red-team so the retrospective has access to the full execution state.
After code review passes, red-team the full implementation.
REQUIRED SUB-SKILL: Use crucible:red-team
crucible:red-team on the full implementation:
git diff --stat and key files)Do NOT skip this step. Code review checks quality; red-teaming checks whether the system will actually work and survive real use.
Check for docs/plans/*-noticed.md files matching the current pipeline (date + ticket-slug). If one exists and contains entries, prompt:
Found <N> noticed-but-not-touching entries in <noticed.md path>. Convert any to GitHub issues?
On confirmation, display a numbered list of entries and ask which to convert. For each selected entry, create an issue via gh issue create using the entry's noticed, why it matters, and suggested follow-up fields. Skip silently if no matching -noticed.md file exists.
Resolve via git symbolic-ref refs/remotes/origin/HEAD first — that's the upstream default and works for any name (main/master/trunk). If unset, check which of main / master actually exist locally:
git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null \
|| { for ref in main master; do git rev-parse --verify "$ref" >/dev/null 2>&1 && echo "$ref"; done; }
If exactly one main-like ref exists, use it and narrate the choice ("[finish] base branch: main (only main-like ref found)"). If multiple exist (legacy repos with both main and master, or unusual naming), ask before picking — these are usually load-bearing differences (one is real default, the other is leftover) and the model has no signal which one this branch was cut from. Format:
Multiple base-branch candidates found: main, master. Which did this branch split from?
Do not silently pick the first match.
Present exactly these 4 options:
Implementation complete. What would you like to do?
1. Merge back to <base-branch> locally
2. Push and create a Pull Request
3. Keep the branch as-is (I'll handle it later)
4. Discard this work
Which option?
Don't add explanation - keep options concise.
BLOCK semantics: you CANNOT proceed to Option 1 (merge) or Option 2 (push + PR) until local validation passes. A failing check is a hard stop. Do not push "then fix in CI"; do not merge "then fix on main."
Detect the project's toolchain first by reading manifest files at repo root: package.json, Cargo.toml, pyproject.toml / requirements.txt, go.mod, *.csproj / *.sln, Gemfile, build.gradle / pom.xml. Run only the checks that actually apply.
git diff --name-only <base>...HEAD). If ambiguous, ask the user before running the full matrix.Silently-missing tools are NOT a pass — they are an "unknown." Either run the real tool, or narrate the skip ("no type-check configured — skipping"), or ask the user. Never mask a failure with || true or 2>/dev/null.
Validation matrix (run every applicable check; each must exit 0 unless its documented exit-code contract says otherwise):
| Ecosystem | Type-check | Lint | Format | Tests |
|---|---|---|---|---|
| TypeScript/Node | npx tsc --noEmit (if tsconfig.json) | npm run lint / pnpm lint / biome check (whichever the repo configures) | prettier --check . / biome format --check (whichever configured) | npm test / pnpm test / vitest run / jest |
| Rust | cargo check --all-targets --all-features | cargo clippy --all-targets --all-features -- -D warnings | cargo fmt -- --check | cargo test --workspace --all-features |
| Python | mypy or pyright (if configured) | ruff check / flake8 (whichever configured) | ruff format --check / black --check (whichever configured) | pytest |
| Go | (compiler via go build ./...) | go vet ./... (add golangci-lint run if configured) | out=$(gofmt -l .) && [ -z "$out" ] (preserves gofmt exit code AND fails non-zero on drift) | go test ./... |
| .NET | dotnet build -p:TreatWarningsAsErrors=true (portable across Linux/macOS/Windows; covers type + warnings-as-errors) | Roslyn analyzers via the same -p:TreatWarningsAsErrors=true + any configured analyzer package | dotnet format --verify-no-changes | dotnet test |
| Ruby | (runtime only) | bundle exec rubocop (covers Layout + Style + Lint) | covered by Lint (or bundle exec standardrb if configured instead of rubocop) | bundle exec rspec / bundle exec rake test |
| Java/Kotlin | (compiler via ./gradlew build) | ./gradlew checkstyleMain spotbugsMain or ./mvnw spotbugs:check (if configured) | ./gradlew spotlessCheck (if configured) | ./gradlew test or ./mvnw test |
For ecosystems not in this matrix, extend it: manifest → type-check → lint → format-check → test. Do not skip an ecosystem because it isn't listed.
Exit-code interpretation: treat each tool's documented exit-code contract authoritatively, not just 0 vs non-0. Example: gh pr checks exits 8 for "checks pending" — a legitimate non-terminal state, not a failure. The rule is "never mask an exit code without interpreting it," not "non-zero is always failure." If a tool's contract is unclear, treat non-zero as failure and ask the user.
On ANY unhandled non-zero exit (after exit-code interpretation): STOP. Report the failure, dispatch a fix, and re-run the full matrix from scratch. Do not partially re-run — a fix in one layer can regress another.
# Switch to base branch
git checkout <base-branch>
# Pull latest
git pull
# Merge feature branch
git merge <feature-branch>
# Verify tests on merged result
<test command>
# If tests pass
git branch -d <feature-branch>
<!-- CANONICAL: shared/compass-protocol.md -->
Compass emit (after local merge + tests pass):
Run compass update (atomic multi-field) to emit last_meaningful_commit plus arc-closure. Capture the merge commit SHA and subject first. The next_move value comes from the caller's --value argument if provided, otherwise preserve the existing next_move (omit --set next_move entirely), or pass an empty string if no prior value exists.
MERGE_SHA=$(git rev-parse HEAD)
MERGE_SUBJECT=$(git log -1 --pretty=%s)
# If caller provided a next_move value:
python scripts/compass.py update \
--set last_meaningful_commit --value "${MERGE_SHA}:${MERGE_SUBJECT}" \
--set current_arc --value '' \
--set next_move --value '<caller-provided-or-empty>'
# If no next_move value was provided by caller, omit --set next_move
# so the existing value is preserved:
python scripts/compass.py update \
--set last_meaningful_commit --value "${MERGE_SHA}:${MERGE_SUBJECT}" \
--set current_arc --value ''
Then: If using a worktree, clean it up (Step 7)
Repository Safety Check (before push):
# Check if repo is public
IS_PRIVATE=$(gh repo view --json isPrivate -q .isPrivate)
If the repo is public: scan the PR title, body, and commit messages for proprietary company information, internal names, internal URLs, or sensitive data. STOP and confirm with the user if anything looks sensitive. This check is mandatory — a prior incident involved filing proprietary information to a public repo.
# Push branch
git push -u origin <feature-branch>
# Create PR and capture the URL gh emits (the PR it just created).
# This is the only deterministic PR reference — using `gh pr view`
# instead would use branch-to-PR mapping, which breaks on repos
# with multiple open PRs per branch.
PR_URL=$(gh pr create --title "<title>" --body "$(cat <<'EOF'
## Summary
<2-3 bullets of what changed>
## Test Plan
- [ ] <verification steps>
EOF
)")
PR_NUMBER="${PR_URL##*/}"
# Guard: if gh pr create failed (e.g., a PR already exists for this branch),
# PR_URL is empty — surface the real diagnostic instead of falling through
# into CI monitoring and emitting a misleading "CI failed" message.
if [ -z "$PR_URL" ] || [ -z "$PR_NUMBER" ]; then
echo "gh pr create did not return a PR URL — possible duplicate PR. Inspect: gh pr list --head <feature-branch>"
exit 1
fi
Post-Push CI Monitoring (Non-Negotiable): after gh pr create returns, you CANNOT report success to the user until CI has finished AND passed. "Pushed" is not "done." BLOCK on the watch + empty-check assertion below. Fail closed on any non-zero result that isn't the "no CI configured" case:
# Primary: --watch streams status and returns aggregate exit code.
# Exit 0 = all terminal passes OR no checks configured (must disambiguate).
# Non-zero = at least one check failed/cancelled.
gh pr checks "$PR_NUMBER" --watch
WATCH_RC=$?
# Disambiguate the no-CI case from real success.
CHECK_COUNT=$(gh pr checks "$PR_NUMBER" --json bucket --jq 'length')
if [ "$CHECK_COUNT" = "0" ]; then
echo "No CI checks configured — record in final report and recommend adding CI"
elif [ "$WATCH_RC" -ne 0 ]; then
echo "CI failed (watch exit $WATCH_RC)"; exit 1
else
echo "CI green"
fi
Fallback (if --watch cannot run): poll until all checks reach a terminal state, then assert the bucket set is a subset of {pass, skipping} (allow-list, not deny-list — an unknown future bucket value must fail closed). Use gh's normalized bucket field, not raw state values, to avoid missing edge states like NEUTRAL, ACTION_REQUIRED, or lowercase legacy commit-status values.
gh exit-code contract: per gh's own documentation, gh pr checks exits 0 when all checks terminal-passed, 1 when at least one check terminal-failed, 8 when at least one is still pending, and other values for tool errors (auth, network, rate-limit). Both 0 and 1 are terminal — the fallback's assertion block disambiguates pass from fail via the bucket set. 8 means continue polling. Anything else means gh itself couldn't determine state.
while true; do
BUCKETS=$(gh pr checks "$PR_NUMBER" --json bucket --jq '[.[].bucket] | unique')
RC=$?
echo "CI buckets: $BUCKETS"
case "$RC" in
0|1) break ;; # 0 = all pass, 1 = at least one failed; both terminal — assertion classifies
8) sleep 20 ;; # at least one pending — keep polling
*) echo "gh pr checks errored (rc=$RC) — cannot determine CI state"; exit 1 ;;
esac
done
# Three terminal cases: empty (no CI), all allow-list (green), or anything else (red).
if [ "$BUCKETS" = "[]" ]; then
echo "No CI checks configured — record in final report and recommend adding CI"
elif echo "$BUCKETS" | jq -e 'all(. == "pass" or . == "skipping")' >/dev/null; then
echo "CI green"
else
echo "CI not all-green: $BUCKETS"; exit 1
fi
If the exit is non-zero (either via --watch or the explicit assertion): diagnose from CI logs (gh run view <run-id> --log-failed or gh pr checks "$PR_NUMBER"), dispatch a fix, re-run Step 5.5's full validation matrix (the fix can regress local checks), push, and re-watch. Do NOT report success on a red PR. Do NOT leave the watch running while moving on to another task — CI failure is an actionable blocker that takes precedence.
If the block exits with the "No CI checks configured" message, record that in the final report so the user knows local validation was the only gate, and recommend they add CI.
<!-- CANONICAL: shared/compass-protocol.md -->Compass emit (after gh pr checks --watch returns green):
Run compass update (provisional arc-closure only). Do NOT emit last_meaningful_commit — CI green does not mean merged; the subsequent /merge-pr invocation writes the SHA. The next_move value comes from the caller's --value argument if provided, otherwise preserve the existing next_move (omit --set next_move entirely), or pass an empty string if neither.
# If caller provided a next_move value:
python scripts/compass.py update \
--set current_arc --value '' \
--set next_move --value '<caller-provided-or-empty>'
# If no next_move value was provided by caller, omit --set next_move
# so the existing value is preserved:
python scripts/compass.py update \
--set current_arc --value ''
Then: If using a worktree, clean it up (Step 7)
Report: "Keeping branch <name>."
If using a worktree: "Worktree preserved at <path>."
Confirm first:
This will permanently delete:
- Branch <name>
- All commits: <commit-list>
Type 'discard' to confirm.
Wait for exact confirmation.
If confirmed:
git checkout <base-branch>
git branch -D <feature-branch>
Then: If using a worktree, clean it up (Step 7)
Skip this step if not using git worktrees.
For Options 1, 2, and 4:
Check if in worktree:
git worktree list | grep $(git branch --show-current)
If yes:
git worktree remove <worktree-path>
For Option 3: Keep worktree.
| Option | Merge | Push | Cleanup Branch | Cleanup Worktree (if applicable) | |--------|-------|------|----------------|----------------------------------| | 1. Merge locally | Yes | - | Yes | Yes | | 2. Create PR | - | Yes | - | Yes | | 3. Keep as-is | - | - | - | - | | 4. Discard | - | - | Yes (force) | Yes |
Skipping test verification
Skipping code review
Open-ended questions
Automatic worktree cleanup
No confirmation for discard
Skipping pre-push validation
Silencing validation failures
2>/dev/null || true patterns hide tool failures and make "no output" indistinguishable from "tool missing" — a failing tsc looks identical to a repo without TypeScript.Pushing and moving on
git push returns, gh pr create returns a URL, task feels done. CI runs later, fails, and nobody notices until the next review session.gh pr checks <pr-number> --watch (or equivalent poll loop). Treat a red PR as a hard stop — never report success to the user on a failing PR.Never:
|| true, 2>/dev/null). Known non-failure non-zero codes must be matched to their meaning — gh pr checks exit 8 is "checks pending," not failure. When a tool's contract is unclear, treat non-zero as failure.gh pr create without confirming all CI checks passAlways:
gh pr checks --watch) before declaring Option 2 doneAfter the gate run terminates (any verdict — PASS, ESCALATED, ARCHITECTURAL, etc.), scan the quality-gate defer-ledger handoff directories the gate preserved before deleting its scratch directory: glob ~/.claude/projects/<project-hash>/memory/quality-gate/defer-ledger-*/ and read every round-N-ledger.md within (chunked gates flatten their per-chunk ledgers as chunk-<K>-round-<N>-ledger.md — match those too). The gate writes these handoff dirs on all exit paths because its own scratch dir is deleted at cleanup and would not survive to finish-time (see the Defer-ledger handoff (#303) paragraph in skills/quality-gate/SKILL.md for the producer side and its ## Cost-Cap and Diminishing-Return Signals section for ledger format). Use Write/Read/Glob, NOT Bash — safety hooks block Bash commands referencing .claude/ paths.
For each ledger file found, create-or-append the ## Accepted section to docs/retrospectives/defer-ledger/<issue-number>.md (where <issue-number> is the GitHub issue number associated with this run; derive from branch name <prefix>/<NNN>-... or from the PR's linked issue). If the target file does not exist, create it (creating the docs/retrospectives/defer-ledger/ directory first if absent) with a top-level # Defer ledger — issue #<N> header.
Each appended entry preserves the finding summary verbatim and adds empty Lik:, Cost:, and Outcome: fields for v1.0 hand-fill or auto-fill. Source the per-entry header fields as follows (one entry per defer-ledger-<run-id>/ dir): <gate-run-timestamp> is the <run-id> parsed from the handoff directory name (the run-id is a start-of-gate timestamp); <artifact-type> is the Artifact-type: field from any of the dir's round-N-ledger.md files (constant across rounds of one run); <rounds> is the count of round-N-ledger.md files in the dir. The per-finding ## Accepted lines are concatenated from every ledger's ## Accepted section, deduplicated by <finding-id> (a finding accepted across multiple rounds appears once).
## <gate-run-timestamp> — <artifact-type> (<rounds> rounds)
### Accepted
- [<severity>] <finding-id>: <one-line summary>
Lik:
Cost:
Outcome:
Consume-once: After seeding the corpus from a defer-ledger-<run-id>/ directory, delete that directory so a later finish run does not re-seed the same ledgers. (A build pipeline may leave several handoff dirs — one per gate: design, plan, per-task code gates — all keyed to the same issue; processing and consuming all of them is correct, since they share the one defer-ledger/<issue-number>.md corpus file.)
Graceful skip: If no defer-ledger-*/ handoff directories exist (gate did not run, crashed before round 1, or the handoff was reclaimed by the 24h stale-cleanup pass before finish ran), or no ledger files are present within them, skip silently. Corpus seeding is best-effort — failures here MUST NOT block finish-skill completion.
Why this exists: v0.1 ships visibility-only (no binding deferral). v1.0 will activate binding triage gated on a calibration corpus. This step seeds the corpus from v0.1 ledger emissions so v1.0 has data to learn from. See issue #305 for v1.0 follow-up work.
Called by:
Pairs with:
crucible:red-team directly rather than crucible:quality-gate because it doesn't produce a typed artifact — it's a pre-completion sanity check, not an iterative gate.Recommended:
Before completing this skill, confirm every mandatory checkpoint was executed:
If any checkbox is unchecked, STOP. Go back and execute the missed gate.
testing
Standalone instance-bug reviewer — runs a parallel finder fan-out + verify gate over a diff or a path and prints ranked, verified findings. Use when the user says "delve", "find bugs in this diff", "review this for bugs", "scan this file/subsystem for defects", "instance-bug sweep", or wants concrete reproducible defects (not a merge verdict, not systemic health). Works on a PR id, a base..head range, or a path, on any forge (GitHub, GitLab, Bitbucket, self-hosted).
testing
Render the Crucible calibration ledger weekly report — the honest "Crucible caught N silent bugs" headline, verdict breakdown, per-skill severity rates, and the inflation detector. Triggers on "/ledger", "weekly report", "weekly ledger", "caught N", "quality ledger", "calibration report", "render the ledger".
development
The Book of Grudges — cross-session bug graveyard. Every fixed bug is recorded as a structured "grudge"; before touching code, skills query the grudgebook for the files in scope and surface past regressions as forced "DO NOT REPEAT" context. Read mode (pre-flight) and write mode (on bug resolution / fix(*) PR). Machine-local, per-repo, never committed. Triggers on /grudge, "check grudges", "record a grudge", "any past bugs here", "regression oracle", "bug graveyard".
testing
Reconcile the Crucible calibration ledger — walk merged fix/hotfix branches to falsify the originating gating-verdicts, compute per-skill Brier calibration scores, and append a falsification log. Triggers on "/calibration-reconcile", "reconcile ledger", "reconcile calibration", "falsify verdicts", "brier score", "calibration reconcile", "compute brier".