plan-agent 8.7.0 · merged 1b4f657
The implementation-plan skill now decides whether a plan
should force its tests to fail before any implementation exists — and shapes the
plan's steps into RED / GREEN / VERIFY / SHIP phases when it should.
A plan could name its tests and still be implemented backwards: build the feature, then write a test that passes against whatever got built. That test proves the implementation matches itself, not the objective. Nothing in the spec format forced the failure to come first.
No renderer, parser, or build change was needed.
### Phase: groupings, per-step Verify: markers, and
build stopping at each phase boundary all shipped in 8.6.0. This
release is the guidance that uses them — five markdown files and a version bump.
That is why the diff is 291 lines and touches no .mjs.
Nothing executes here. The deliverable is text the model reads at a specific point in a workflow, so the "code path" is the read order — and the contract it has to keep with machinery that already exists.
Detection lives in SKILL.md Step 2, sub-step 1 — after
right-sizing classifies the work, before any step is drafted. It has to be there:
phases are headings inside ## Steps, so the shape must be settled
before the steps exist.
flowchart TD
A["Step 2.1
classify the work"] --> B{"--tdd or --no-tdd
passed?"}
B -->|"--no-tdd"| S["single-pass steps"]
B -->|"--tdd"| F["force phases
RED step 1 stands up the runner"]
B -->|neither| C{"steps touch
application source?"}
C -->|"no — Tier 2"| S
C -->|yes| D{"Step 0b ran?"}
D -->|"no — --quick"| E{"cheap runner probe
test script / pytest.ini / *_test.go"}
E -->|hit| G["red-green-verify"]
E -->|miss| S
D -->|yes| H{"runner found?"}
H -->|yes| G
H -->|"no / borderline"| I["AskUserQuestion once"]
I --> G
I --> S
--quick probe and the --tdd override. Both were review findings.Four ### Phase: headings inside ## Steps, over
flat global numbering. That flatness is load-bearing: it is what
keeps [x] markers valid when phases are added to an in-progress plan,
and why build resumes at the first unmarked step rather than at a
phase start.
Test files only, no implementation source. The Verify: line
demands the failure output, not a claim. Failing for the right
reason is the assertion — a test that errors on a missing import has not
gone red, it has not run.
Minimum change that passes, re-running after every edit. The 8-iteration cap is written into the phase's last step — a cap that lives only in the guideline has nothing to stop the loop. That step says report the exact blocker, never success.
Full suite, lint, and typecheck where the project has one. The browser pass — layout, targets ≥ 44×44px, zero hydration warnings — is UI-only. A backend or library plan omits it rather than inventing an affected page.
Conditional on the user having asked to ship. build Step 6
commits only on request; a SHIP phase that committed unconditionally would
override that from inside the plan. When in doubt, omit — a missing SHIP costs
one prompt, an unwanted one costs a commit nobody asked for.
Start at SKILL.md Step 2.1 for the trigger, then
guidelines/red-green-verify.md for everything the phases contain.
right-sizing.md only arbitrates a collision (below). Do not start in
the guideline — it reads as unconditional until you have seen the gate.
Every phase mechanism already existed. Adding a renderer flag or a
build mode would have duplicated rules in two places and bumped two
skills. The plan document carries the discipline, and build enforces
it for free by walking steps and stopping at boundaries.
An opt-in flag would be forgotten. Always-on would force a RED phase onto
docs-only plans, where a grep -q check is the only thing that could
fail. Detection keys on the Tier 1 signal Step 5c already computes, and asks once
when the call is genuinely close.
A reviewer asked to drop mcp__Claude_Browser__* for
mcp__claude-in-chrome__*, on the premise the former appears nowhere
else in the repo. It does — git-agent/skills/ship-autonomous/SKILL.md:4
declares five of its tools. The repo uses both. The real point inside the finding
was precision: bare read_page is ambiguous with two servers connected,
so the names now carry their prefix and the guideline says to write whichever the
target repo has.
The first draft said "a script driving the MCP tools." Those are model-side
calls; no .mjs file can invoke read_page. The RED bullet
now splits explicitly: an agent-driven pass (the step names the
calls, the Verify: line is the reported measurement) or a
committed script (Playwright/Puppeteer, which is then an ordinary
RED test with a Run: command).
Phases now have two unrelated rationales — context budget (the Phased profile)
and discipline (this shape). They cannot share one heading run:
### Phase: Parse beside ### Phase: RED leaves a reader
unable to tell what a boundary means. RGV takes the headings; context seams live
inside it. If that makes GREEN too large for one window, the objective was two
plans.
| Rejected | Why | What would revisit it |
|---|---|---|
Enforce in build — refuse GREEN before a recorded RED failure |
Duplicates the rules in two skills and bumps both. build already walks steps and stops at boundaries. |
Evidence that plans get restructured mid-implementation to skip RED. |
| Always-on for every plan | Forces a RED phase onto docs-only work with nothing runnable to fail. | Never — Tier 2 has no failure to author. |
Opt-in --tdd only |
MINOR bump and zero risk, but a flag nobody remembers is a feature nobody uses. | If detection proves noisy in practice. |
Align the GREEN cap to tdd-loop's five iterations |
Eight is what this workflow specified; the two skills are independent. | A deliberate cross-plugin reconciliation. |
AbortController + setTimeout in the driver sample |
Guards against Node <18, which reached EOL in April 2025. Three extra lines for no live runtime. | A consumer pinned to an EOL Node. |
Three things were walked, not weighed — they only surfaced by running the thing.
The shipped Node driver went through two wrong versions. First it declared
res with var inside a try in the loop — it
worked by hoisting, but on timeout printed
server never came up: undefined. Fixing the scoping exposed the real
bug underneath: fetch resolves for 503, so the loop broke on the first
transient response and reported "unhealthy" against a server that was merely still
booting.
A poll loop that stops at the first answer is not polling. Both versions looked correct on the page and only failed against a dev server with a realistic boot sequence.
let res
for (let i = 0; i < 60; i++) {
// A booting server answers 503 before it answers 200, and fetch does not
// throw on either — so poll until res.ok, not until the first response.
try { res = await fetch(url, { signal: AbortSignal.timeout(1000) }) } catch {}
if (res?.ok) break
await new Promise(r => setTimeout(r, 500)) // also bounds a hung request
}
if (!res?.ok) throw new Error(`never healthy in 30s (last: ${res?.status ?? 'no response'})`)
Two bots independently claimed mcp__Claude_Browser__* appears
nowhere else in the repo. One grep disproved it. The finding still had
a real point inside it — but acting on the stated premise would have switched the
guideline to the wrong surface. Verify the premise before acting on the conclusion.
Copilot, Codex, and CodeRabbit each re-reviewed on every push, re-firing against older commits after fixes had landed. CodeRabbit hit its own rate limit. The productive shape was one triage pass on merit — four of ten findings were real defects, the rest polish — then a single fix commit, then stop. Copilot's last three runs all returned "no new comments."
No test files ship in this diff — it is guidance. Verification drove the three real surfaces instead.
Pulled out of the shipped markdown with awk on the fence — not
retyped — and run against a dev server that binds after 1.2s then 503s three times:
> dev
> node server.mjs
[dev] listening on 3000
[dev] 503 #1
[dev] 503 #2
[dev] 503 #3
[dev] 200 #4
EXIT=0
Failure paths: permanently-unhealthy server →
never healthy in 30s (last: 503) at 31s elapsed, child killed, port
freed. Crashing dev script → (last: no response). Exit codes
deterministic at 1 / 0, which is what lets the guideline
claim the exit code is the Verify: line.
status: in-progress → four data-phase wrappers, 3 completed step cards matching 3 [x] markers.browser greps are all renderer JS comments.### Phase: SHIP heading → phase "SHIP" has no steps under it, exit 1.Verify: → step 1 is missing its "Why:" or "Verify:" part.Loaded the plugin headlessly and asked it to read back the three rules the review round added. It returned them unprompted by their wording — the strongest available evidence that a guidance change works.
SHIP: Only when all three prior phases are green **and** the user explicitly
asked to ship — otherwise omit it and let VERIFY be last.
VERIFY-BROWSER: Only when the plan touches UI — backend/CLI/library plans omit it.
QUICK: One cheap runner check; no hit means no RGV — unless --tdd was passed,
which overrides and makes RED step 1 stand up the runner.
implementation-plan producing an RGV plan from a bare objective — the
interactive path needs AskUserQuestion. Detection was verified by
reading back the rules, not by observing a plan get classified. Also unrun:
test-skill-behavior-baselines.sh, which spawns headless
claude sessions and exceeded the timeout; it covers five unrelated
skills.
1. extract-plan-spec.mjs silently drops all progress state.
A spec with 1. [x] … and - [x] … renders correctly to
HTML, but extracting back returns bare 1. and - bullets.
Reproduced identically on an unphased plan, so it is pre-existing, not a
regression from the phase work. Impact is narrow today — the .md spec
is the source of truth and extract only runs for legacy HTML-only plans — but
build resumes at the first unmarked step, so a legacy in-progress plan
re-specced this way silently redoes finished work. Phased RGV plans are explicitly
designed to span context windows, which is exactly when resumption happens.
2. RED evidence does not survive a phase checkpoint.
references/phase-checkpoints.md persists step markers and the Decisions
ledger at a boundary, not captured stdout — but SHIP requires the exact RED failure
output in the PR body. A session that compacts after RED cannot produce it.
tests/plugins/test-build-plan-html.mjs reports "87 plans round-tripped cleanly." That phrasing reads as fidelity it does not assert for progress markers — tighten the assertion or the wording.npm run dev. Fine for an illustration, but every copy needs two edits and the prose does not say so.implementation-plan Step 7 still backgrounds its preview server with python3 -m http.server <port> & — the exact pattern the new guideline says permissions block. Left alone as out of scope; it now contradicts guidance shipped in the same file.test script that has not passed in months
still reads as RGV-eligible. Upgrade path: probe exit status rather than existence —
but that costs a subprocess in Step 2, which is why it was not done.
| Path | Why |
|---|---|
guidelines/red-green-verify.md |
new 172 lines. The four phases, the applies/skip/ask rules, the foreground Node driver, the UI-only scoping. Everything else points here. |
skills/implementation-plan/SKILL.md |
Detection in Step 2.1; --tdd/--no-tdd in the flag list and argument-hint; guidelines-library entry; Step 5c note tying ## Tests to the RED steps. |
guidelines/right-sizing.md |
Calibration row, and the precedence rule for when both phasing rationales apply. |
kit/plugins/plan-agent/README.md |
Flag table rows, the detection paragraph, and both argument summaries — the second one at line 586 was missed in the first commit. |
kit/plugins/plan-agent/CHANGELOG.md |
8.7.0 entry. |
.claude-plugin/marketplace.json |
8.6.0 → 8.7.0. Version lives only here for relative-path plugins; a CI guard fails the PR if it does not exceed base. |