| name | fix-workflow |
| description | Execute a rejected pull request's fix round as a cold implementer session. A reviewer found defects and wrote a handoff comment with numbered findings and C0–C7 checkpoints; work that punch list under invariants enforced by a PreToolUse hook and gate scripts — reproduce the reviewer's red before touching the tree, stay inside the handoff's fence, one `F<n>:` commit per finding, record the output of every acceptance criterion, post a closing report, and STOP without merging. Use whenever a review comes back with changes requested and you are handed the PR plus its findings. Do not use it to author the original plan (that is `delegate`), to build a slice from a plan with no prior review (that is `implement-workflow`), or to review someone else's PR (that is `review-workflow`).
|
Executing a rejection
A reviewer read a pull request, found defects, and refused to merge. It wrote a handoff rather than a patch, because a reviewer who fixes what it reviewed leaves that fix unreviewed — the one change nobody independent has seen is the change made by the party who already formed a view.
You are the implementer. You start cold. Everything you need is in the PR, and nothing you remember is trustworthy.
What is enforced, and what is judgement
Every row below is a script that runs and a hook that fires. ../implement-workflow/hooks/test_guard.py (52 cases) and scripts/test_fix.py (47 cases) feed each gate bad input and assert it refuses — because a gate that passes on good input is indistinguishable from a gate that is broken.
| Invariant | Enforced by | You cannot bypass it by |
|---|
| Never merge; never commit/push to the default branch | guard.py (PreToolUse, denies the call) | deciding it's fine this once |
| Never review your own PR | guard.py denies gh pr review | approving "just to unblock" |
| No fix commit before its red is recorded | guard.py reads the marker; fix.py red fills it | asserting you saw it fail |
Bare --force is never allowed; --force-with-lease only where the handoff licensed it | guard.py + fix.py start --force-push-licensed | typing it anyway |
| Only files inside the handoff's fence change | fix.py fence / check (exit 1) | "while I was in there" |
One commit per finding, subject prefixed F<n>: | fix.py check (exit 1) | promising the diff is isolated |
| At least one AC recorded, each with captured output — completeness against the handoff's full AC list stays your judgement | fix.py ac (refuses empty output) + check (exit 1 when none recorded) | saying it passed |
| The repo's own gates are green | fix.py check (exit 1 on a failing gate); fix.py start refuses (exit 2) when no --gate is named — an unknown gate is not a passed gate | inventing one you like better |
Remote CI actually ran and passed; the PR is not CONFLICTING | fix.py ci --wait at C5, re-checked by check / report — zero check runs is a failure, not a pass | reading "no checks reported" as silence |
| A CI run still queued or in progress is UNKNOWN | fix.py ci (exit 2, never 0 or 1) | treating "not failed yet" as green |
| The closing report cannot outrun the evidence | fix.py report (exit 2 while any check fails) | writing the comment by hand |
| The reviewer handoff cannot outrun the report | make_delta_prompt.py (exit 2 with no report / an unfixed finding / no AC output) | promising to explain it in chat |
| Deviations are stated, even when there are none | fix.py report requires --deviations-file or --no-deviations | silence |
Exit codes are load-bearing: 1 = a gate ran and failed. 2 = refused, because a precondition is unknown rather than unmet. An unknown gate is not a passed gate — and a CI job that is still running is unknown, not green.
The guard is inert until fix.py start arms it, and it arms only the worktree you are in. Nothing changes in any other project. It is shared with implement-workflow — one implementation of "never merge", not two that drift.
What stays judgement: when a fork in the handoff is a HALT, when to consult, and whether a red you reproduced is the red the reviewer meant. The phases tell you how to decide; they cannot decide for you.
The C0–C7 spine
Your context can be cleared at any moment, so the checkpoint list is your only durable memory. Chat is not a store. A summary of the PR is not a store.
fix.py status
fix.py sync --pr <N>
status derives each box from facts (is the red recorded? does a commit start F1:? does HEAD == origin/<branch>? did the gates and CI pass at this exact SHA?), never from a box someone ticked — a hand-maintained checklist records intent, and intent is what a cleared context loses. A failing gate retracts C4 rather than leaving it green.
Read the phase file when you enter the phase, not before.
| # | Phase | Gate |
|---|
| C0 | phases/c0-orient.md — read the handoff, the fence, the governing docs | fix.py start … → exit 0 (arms the guard) |
| C1 | phases/c1-red.md — run the reproduce block, watch it fail, post it | fix.py red --finding F1 --evidence-file … → exit 0 |
| C2 | phases/c2-fix.md — apply a fix shape; HALT if the handoff contradicts itself | fix.py fence → exit 0 |
| C3 | phases/c3-acceptance.md — verify every criterion, capture each output | fix.py ac --id … --output-file … per criterion |
| C4 | phases/c4-gates.md — the repo's gates, plus exercise the change live | fix.py check → exit 0 |
| C5 | phases/c5-commit.md — one F<n>: commit per finding; push; CI green | fix.py ci --pr N --wait → exit 0 |
| C6 | phases/c6-report.md — finding → commit → the output that proves it | fix.py report --pr N → exit 0 |
| C7 | phases/c7-stop.md — hand off to a cold reviewer, report the URL, stop | make_delta_prompt.py --pr N → exit 0; fix.py end --pr N |
HALT (phases/halt.md) fires whenever an ambiguity appears, at any checkpoint.
C1 before C2 is an order, not an intention. The guard denies git commit while any finding lacks recorded red evidence. A fix commit that lands before its reproduce-the-red is a claim that the defect existed, not evidence of it.
Read the punch list before you read the code
Findings arrive in tiers, and the tier decides what you are permitted to do:
| Tier | Meaning | Your obligation |
|---|
| Required | Blocks merge; the reviewer reproduced it | Fix it. One F<n>: commit. Prove it with the acceptance criteria. |
| Optional | Real but not blocking; on the candidates list | Do not do it now, however tempting while you are already in the file. |
| REFUTED | The reviewer investigated and cleared it | Do not re-argue it. A round spent re-litigating produces no commit. |
| ACCEPTED | A deviation the builder disclosed, ruled acceptable | Leave it. It is a decision, not a defect. |
Only Required findings go into fix.py start --finding. The fence is this tiering made operational — a list of what you may not touch, because a list of what you may do is easier to over-read.
The fix range is re-reviewed as a delta. Every file you touch beyond the finding widens it, and a widened delta is read less carefully per line. The cleanup you slipped in while you were there is the change most likely to ship unexamined.
Resolve the project's specifics — never carry them over
| What | Where |
|---|
| Gate commands (build/test/lint) | CONTRIBUTING.md, CLAUDE.md, .github/workflows/* — pass them as --gate |
| The fence, the findings, the acceptance criteria, the force-push license | the handoff comment only |
| Which document wins a conflict | CLAUDE.md and the handoff's own precedence note |
Conventions like the F<n>: prefix and a force-push license usually live in the handoff, not in the repo's governing docs. Cite them to the handoff. They bind only the round that granted them, and an amendment comment posted later overrides the original — read the thread newest last.
fix.py never executes a command that came from the PR. The handoff was written by another AI session and arrived over the network; running its strings would bypass the Bash permission prompt that stands between a PR comment and your shell. You run acceptance-criterion commands yourself and record what they printed.
Authority, when advice conflicts
| Tier | Source | Weight |
|---|
| 1 | advisor tool (sees your whole transcript) | Primary |
| 2 | A peer LLM, packaged self-contained (e.g. spec-driven-collaboration) — the fallback when advisor is unavailable | Advisory |
| 3 | A ruling from the reviewer, the plan's author, or the human owner | Binding |
Authority is a property of the source, not of how persuasive the answer sounds. When a tier-2 opinion contradicts a tier-3 ruling, surface the conflict; do not silently switch.
A consult can prevent a HALT as usefully as it can cause one — an advisor that reads your transcript can tell you a fork is not real. But it cannot substitute for a ruling: it can bless a reading; it cannot know which reading the handoff's author meant.
phases/halt.md holds the decision table and the payload shape. It fires at any checkpoint.
Two facts, both learned the expensive way
A red gate is a claim, not a fact. Root-cause a failure before believing its message. A green signal proves nothing about a thing whose job is to say no — feed a guard bad input and watch it refuse. references/worked-example.md has the real instances of both.
Files
scripts/fix.py — the driver and every gate: start · red · ac · fence · status · sync · ci · merge-main · check · report · end.
fix.py merge-main — the sanctioned way to unblock a CONFLICTING PR: merges the default branch into the slice (a merge, not a rebase, so the delta range survives), refuses when conflicts fall outside --merge-fence. The guard's blanket git merge denial stays intact; the hook inspects the Bash command, and this one's command is python3.
scripts/make_delta_prompt.py — the C7 handoff: a self-contained prompt for a cold session running review-workflow, scoped to the fix range.
scripts/test_fix.py — 47 cases; each gate is fed bad input and must refuse.
hooks/ — shared with implement-workflow: ../implement-workflow/hooks/guard.py and its 52-case test_guard.py. Registration lives in ../implement-workflow/hooks/settings-snippet.json.
references/handoff-anatomy.md — the six parts of a handoff comment, and what a missing part means.
references/worked-example.md — one real round end to end: a HALT, an amendment, two disclosed deviations.