Let `build` author a plan when none is specified

High completed
2026-07-27 agentics feature High effort

Replace plan-agent:build's no-plan dead end with an entry into the plan pipeline that already exists — proposal, plan, review, implement — and stop discovery from silently adopting a stale spec when the user named no plan.

At a glance

Today a bare /plan-agent:build either dead-ends or silently adopts whatever stale spec it finds; after this it offers what it found and, when you want something new, walks proposal to plan to review to implementation without you re-invoking anything. Done when tests/plugins/test-build-skill.sh passes with the new checks and a no-argument run in a repo with no plans reaches the proposal gate instead of stopping.

Implement Read and implement all steps in the plan at docs/plans/add-plan-authoring-to-build.md — Let `build` author a plan when none is specified. Verify against the plan's Tests, Verification, and Acceptance Criteria before reporting done. If everything passed, mark completion in docs/plans/add-plan-authoring-to-build.md — tick each step's [x] marker and each criterion's - [x], set status: completed — and re-render the HTML from the spec. If any check failed, leave status: in-progress and say which.
More ways to run this plan — goal & workflow prompts, file path
Pursue as goal — optimize for the outcome
Achieve this goal: Let `build` author a plan when none is specified. The plan at docs/plans/add-plan-authoring-to-build.md describes one approach — use it as reference, but optimize for the outcome. Verify against the plan's Tests, Verification, and Acceptance Criteria before reporting done. If everything passed, mark completion in docs/plans/add-plan-authoring-to-build.md — tick each step's [x] marker and each criterion's - [x], set status: completed — and re-render the HTML from the spec. If any check failed, leave status: in-progress and say which.
File add-plan-authoring-to-build.html
Path docs/plans/add-plan-authoring-to-build.html
Spec docs/plans/add-plan-authoring-to-build.md
Definition of done 16 / 16 done

Context

The story behind this plan — what prompted the work and why it matters now.

build resolves a plan three ways and every failure branch stops, with the body instructing the user to go run /plan-agent:implementation-plan by hand. The pipeline that instruction points at is already wired top-down: build-proposal hands off to implementation-plan, whose Step 8 menu invokes review-plan and build as real Skill() calls. Only the proposal-to-plan seam is still a printed prompt rather than a call, so this work is a second entry point into an existing pipeline rather than a new one.

The design decisions were settled in docs/proposals/chain-plan-authoring-into-build.md. Three matter here. Option A — delegate to the existing head and let control return through the Step 8 menu — was chosen over having build sequence the stages itself, which would need a new suppress-menu flag on implementation-plan plus a re-entrancy guard across three skills. The proposal stage is gated by one question, because build-proposal triages a Tier 0 idea by answering directly and producing no document, which would leave the chain holding nothing. The trigger is an absent plan argument, not an empty plans directory, and discovery therefore offers its result instead of adopting it.

The chain is reachable only from the slash command. /plan-agent:build a todo app treats the objective as a command parameter and enters the chain; the same words typed as plain text do not. Ambient activation keeps exactly the behaviour it has today — it requires a plan that already exists and routes elsewhere when there is none. This is a deliberate narrowing: build is the most overloaded verb in software, and any ambient trigger wide enough to catch "build a todo app" also catches "build fails on CI", "build the docker image", and "rebuild the index", none of which belong in a proposal loop. Making the objective a parameter removes that whole failure class instead of trying to word around it, and it keeps build from competing with implementation-plan for free-text routing.

The objective still forces the discovery skip in step 3: whether it arrives as a parameter or not, an objective means unrelated todo specs are noise.

One risk is accepted rather than solved: the chained run crosses roughly ten interactive gates at floor, and suppressing the redundant ones requires flags on sibling skills, which is the boundary this plan deliberately does not cross.

One risk is solved here: skill model: overrides apply for the rest of the turn and do not unwind when the skill finishes, so a chained run would leave the source-writing stage on whichever model the last planning skill declared. Step 8 fixes that.

Files that change

Every file this plan touches, and what happens to each one.

agentics/
  • kit/plugins/plan-agent/skills/build/SKILL.md modified chain entry, discovery offer, hoisted guard, frontmatter
  • kit/plugins/plan-agent/README.md modified the build row and section both describe the old no-chain behaviour
  • tests/plugins/test-build-skill.sh modified checks covering the new behaviour
  • .claude-plugin/marketplace.json modified plan-agent MAJOR version bump
  • kit/plugins/plan-agent/CHANGELOG.md modified entry for the new activation path
  • CLAUDE.md modified the build clause in the plugin table

Steps

The step-by-step work, in order — each step says what to do, why it matters, and how to check it worked.

1
done Hoist the dirty-working-tree precondition out of the Step 1 preconditions block in kit/plugins/plan-agent/skills/build/SKILL.md so it runs before any chain stage is invoked.
Why
left where it is, it fires only after a full proposal loop and plan interview have already run, asking about uncommitted files at the worst possible moment — git-agent:ship-autonomous runs every pre-flight guard before any mutation and this should match.
Verify
the dirty-tree paragraph appears above the Step 1b chain entry in the file, and the completed-plan and resume-from-unmarked preconditions are still present and unmoved.
2
done Split Step 1's three no-plan branches so only the empty-discovery branch reaches the chain: keep the named-but-missing-path branch stopping with the paths it tried, and keep the HTML-with-no-sibling-spec branch stopping with its existing message.
Why
chaining on a mistyped filename would author a whole plan because of a typo, and an HTML-only legacy plan is a plan that needs a spec reconstructed rather than a new plan authored on top of it.
Verify
reading Step 1 top to bottom shows exactly one branch routing to Step 1b, and the other two still end in a stop.
3
done Change discovery from a pickup to an offer, and run the offer only when no objective was supplied: with an objective present, skip discovery entirely and go to Step 1b, because the user has already said what they want and unrelated todo specs are noise. When the offer does run, rank candidates newest-created: first, show at most the top three plus None of these — author a new plan, and say how many were suppressed.
Why
discovery selects on status: alone with no notion of subject, so in a repo carrying ten todo specs an objective like "a todo app" would be answered with a menu of unrelated plans — and AskUserQuestion caps at four options, so the unbounded offer cannot render at all; gating on objective-absence removes the relevance problem rather than trying to solve it, leaving the offer for the case it was designed for, a bare build resuming interrupted work.
Verify
Step 1 states the objective-present skip before describing the offer, the offer names its three-candidate cap, and the explicit-path branch is untouched.
4
done Widen the argument grammar to [<plan path>] [<objective>] [--dir <path>], treating a leading token as an objective only when it has no .md or .html suffix and no /, and add Skill to the allowed-tools frontmatter line.
Why
build has no objective input today, and authoring a plan needs one — while a path-shaped token must keep hitting the stop from step 2 rather than being read as prose; note that the no-slash rule misreads an objective containing one (add A/B testing support) as a path, so the stop message must name the misparse rather than only listing paths tried.
Verify
grep -m1 '^allowed-tools:' kit/plugins/plan-agent/skills/build/SKILL.md includes Skill, the argument-hint frontmatter shows the objective slot, and the rule states what happens to a slash-bearing objective.
5
done State in the Invocation section that Step 1b is reachable only from the slash command: the objective is a command parameter read from $ARGUMENTS, and the model-invocation path keeps its current contract — it requires a plan that already exists and routes to /plan-agent:implementation-plan when there is none, never entering the chain.
Why
$ARGUMENTS is empty on the model path, so an ambient chain would have to infer an objective from conversation, and the trigger word build is overloaded enough that inference would pull in compile and CI requests; scoping the chain to the command makes the objective explicit by construction and leaves ambient routing untouched, so no existing invocation changes meaning.
Verify
the Invocation section says the chain is command-only, and the Model-invocation bullet still carries its route-away instruction.
6
done Add a new Step 1b to build/SKILL.md carrying the chain: an objective check that runs first — when no objective was supplied (the bare-build path through step 3's None of these — author a new plan), ask for one with AskUserQuestion before anything else, because both the proposal-versus-direct gate and the delegated skills are meaningless without it; then the proposal-versus-direct gate asked on every chained entry, a proposal path that invokes build-proposal then implementation-plan with objective-led text naming the proposal path, a direct path that invokes implementation-plan with the objective, and a return path that re-resolves the produced spec by path.
Why
leading with a bare .md token would drop implementation-plan into conversion mode and produce a plan with no actionable steps, and re-running discovery on return would ask the user about the plan they just watched being authored; --dir must not be forwarded to build-proposal, which resolves its own proposals directory.
Verify
the step names both Skill(skill: "plan-agent:build-proposal" and Skill(skill: "plan-agent:implementation-plan", and its return path says the spec is resolved by path rather than by discovery. The step must also state the abandonment contract: if the chain is abandoned between stages — a tool error, a session drop, or the user backing out after a proposal is written but before a plan exists — the proposal file is left in place uncommitted and build reports its path rather than cleaning it up.
7
done Make every non-implementing Step 8 choice terminate the outer chain, not just one: Exit — I'll implement later stops with the plan at status: todo, and Run as workflow stops because implementation-plan has already emitted the workflow prompt and marked the plan in-progress — the return path reports the produced plan's path and stops without implementing in both cases.
Why
Step 8 is the only point at which the user is asked how to execute, so treating any of those answers as declining merely the inner skill's offer would implement work they just routed elsewhere — Exit would build what they declined, and Run as workflow would run an ordinary in-session build racing the workflow they launched. This overrides the proposal's Appendix A, which as written has the outer build proceed.
Verify
Step 1b's return path names both the Exit and Run as workflow cases and says it stops on each, and the three resume cases from Appendix A are still handled.
8
done Add model: opus to build/SKILL.md's frontmatter.
Why
a skill's model override applies for the rest of the turn and does not unwind when the skill ends, so a chained run would leave the source-writing stage on whatever review-plan or implementation-plan last set; pinning it makes build re-assert on activation and gives the stage that writes source files a deterministic model.
Verify
grep -c '^model: opus$' kit/plugins/plan-agent/skills/build/SKILL.md returns 1.
9
done Leave the description frontmatter unchanged, and rewrite only the Overview's "the execution half of implementation-plan, which authors a plan and stops" so it says the command form can author through the chain while ambient activation still requires an existing plan. Scope — do not delete — the Model-invocation bullet's "Requires a plan that already exists — if there is no plan file, stop and route to /plan-agent:implementation-plan <objective>"; add that this applies to the model path only.
Why
the description governs ambient routing, and since the chain is command-only there is nothing new to advertise — leaving it narrow is what keeps build from claiming free-text "build X" requests and colliding with compile and CI phrasing. The route-away instruction is not stale prose but the ambient contract itself, so it must survive; only the Overview, which describes the skill as a whole, is now wrong.
Verify
grep -c 'stop and route to' kit/plugins/plan-agent/skills/build/SKILL.md returns 1, git diff shows the description: line untouched, and the Overview names the command-versus-ambient split.
10
done Update kit/plugins/plan-agent/README.md: the build row in the component table, which reads "implements an existing plan and runs its gates", and the build section body, which opens "Implements a plan that already exists", plus its usage examples so at least one shows a no-plan invocation.
Why
the README is the plugin's published documentation and describes exactly the dead end this plan removes, so shipping without it leaves the user-facing docs contradicting the skill.
Verify
grep -c 'a plan that already exists' kit/plugins/plan-agent/README.md returns 0, and the usage block shows an objective-only invocation.
11
done Bump the plan-agent version in .claude-plugin/marketplace.json by one major, add a CHANGELOG.md entry under kit/plugins/plan-agent/, and update the build clause in the root CLAUDE.md plugin table.
Why
.claude/rules/marketplace.md classifies "changing argument format or activation behavior" as MAJOR, and step 4 changes the argument format while step 3 changes what an existing no-argument invocation does — ambient activation is deliberately unchanged, so the argument grammar and the discovery behaviour are what carry the bump; a MINOR would understate a breaking change to a published plugin.
Verify
BASE_REF=main node scripts/check-plugin-versions.mjs exits 0 and the new version's major component is exactly one greater than the value on main.
12
done Extend tests/plugins/test-build-skill.sh with numbered checks asserting the chain entry, the discovery offer, the hoisted guard, the Exit-terminates-chain rule, the pinned model, step 4's objective-versus-path grammar rule, step 2's two surviving stop branches, and step 5's command-only scoping alongside the retained route-away instruction, following the existing FAILURES counter convention and check 6's section-scoped pattern: sed the owning section, squeeze newlines, then grep within it.
Why
every behaviour above is prose in a hard-wrapped markdown file, and check 6's own comment records a file-wide grep passing against a mutation that deleted all five rules it guarded and appended a decoy paragraph; the grammar rule and the stop branches are what keep a typo from authoring a whole plan, so leaving them unasserted guards the new path while abandoning the old one.
Verify
bash tests/plugins/test-build-skill.sh exits 0, and reverting any one of steps 1, 2, 3, 4, 6, 7, or 8 makes it exit 1 naming that check.

Tests

The tests that prove the change does what it promises.

Tier 1 — This plan changes shipped skill behaviour and an executable test script The objective test is static, not behavioural: it asserts the chain is authored correctly in build/SKILL.md, not that it executes correctly at runtime. No harness in this repo can drive a skill's interactive gates, so acceptance criteria 2, 3, and 5 are proved only by the manual walkthrough in Verification. Treat the gap as known rather than covered.
Objective: a bare build with no plan argument enters the chain instead of dead-ending. File: tests/plugins/test-build-skill.sh; Type: smoke (static); Asserts: build/SKILL.md carries the Step 1b chain entry with both delegating Skill calls, presents discovery as an offer, places the dirty-tree guard ahead of the chain, terminates on every non-implementing Step 8 choice, pins model opus, keeps both stop branches, and retains the model-invocation route-away instruction exactly once; Run: bash tests/plugins/test-build-skill.sh
Unit: plugin version regression guard. File: scripts/check-plugin-versions.mjs; Targets: plan-agent entry in .claude-plugin/marketplace.json; Key cases: version strictly greater than origin/main, valid semver, single source of truth in marketplace.json

Definition of done

The plan counts as done when every statement below is true — check each one off as you verify it.

Final check

One last pass to confirm the whole change works end to end.

Run bash tests/plugins/test-build-skill.sh and confirm it exits 0 with the new checks reported as PASS alongside the nine existing ones. Then confirm the checks are load-bearing rather than decorative: temporarily delete the Step 1b heading from build/SKILL.md, re-run, and confirm the script exits 1 naming that check, then restore the file.

Exercise the headline scenario in this repo, not a scratch one, because it is the case the design nearly failed: with a dozen unrelated todo specs present, run /plan-agent:build a todo app. Confirm it shows no discovery offer at all and reaches the proposal-versus-direct gate. Then type build a todo app as plain text with no command and confirm it does not enter the chain — ambient activation must still require an existing plan and route away without one. Then run a bare /plan-agent:build with no objective and confirm the offer appears capped at three candidates with the suppressed count stated.

Exercise the objective end-to-end in a scratch directory with an empty plans directory. Invoke /plan-agent:build add a health check endpoint and confirm it asks the proposal-versus-direct question rather than stopping with a routing message. Answer Straight to plan authoring and confirm implementation-plan opens with that objective — not in conversion mode, which would be visible as a plan whose steps restate proposal headings instead of naming real actions. Then repeat in a directory holding exactly one todo spec and confirm that spec is offered with an author-a-new-plan option rather than being adopted silently. Run the chain once more and answer Exit — I'll implement later at the Step 8 menu; confirm the run stops with the plan at status: todo and git status --porcelain showing no source files written. Repeat with an objective broad enough to trip the renderer's workflow heuristic (five or more files across three or more directories, so the Run as workflow option appears) and answer it; confirm build reports the plan path and stops rather than starting an in-session build alongside the emitted workflow prompt. Then run a bare /plan-agent:build in the empty directory and confirm it asks for an objective before the proposal-versus-direct gate rather than invoking a planning skill with an empty one. Finally invoke /plan-agent:build add A/B testing support and confirm the slash-bearing objective either reaches the chain or stops with a message naming the misparse — never a bare list of paths tried, which would leave the user with no idea their objective was read as a filename.

Finally run BASE_REF=main node scripts/check-plugin-versions.mjs and confirm it exits 0, proving the marketplace bump landed and did not regress against main.

Wrapping up

Three gates that must all pass before this plan is marked completed.

Required

Completion Report

Criterion 3, the discovery offer
bare build with one todo spec stopped, offered the candidate alongside an author-a-new-plan option, stated that nothing was suppressed, wrote no files, and left the spec at status: todo.
Criteria 4 and 6, objective suppresses discovery
/plan-agent:build a todo app with an unrelated todo spec present showed no offer at all and reached the proposal-versus-direct gate.
Criterion 5, ambient activation unchanged
plain-text build a todo app wrote an app directly and created no plan file, so the chain was never entered.
Criterion 7, the typo stop
/plan-agent:build docs/plans/does-not-exist.md stopped naming both resolution attempts and refused the chain.
The chain is productive, not merely reachable
/plan-agent:build add a health check endpoint in an empty repo authored a plan and implemented a working node:http server end to end. Two defects were found by running it, both fixed and re-verified:
Gates had no stated behaviour when AskUserQuestion is unavailable
two headless runs resolved the same missing tool in opposite ways, one adopting the lone discovery candidate (the exact silent pickup this plan removes) and one stopping; build/SKILL.md now states the fallback, check 18 guards it, and re-running both scenarios produced a clean stop-and-report.
The misparse example was wrong
the rule tests the leading token, so this plan's own example add A/B testing support parses cleanly as an objective; only a slash in the first token misparses, the example is now A/B testing for checkout, and a live run confirms the stop names the misparse and offers a reworded form. Three further defects were found by automated review on PR #470, all fixed and guarded by checks 10, 12, and 13:
Implement now re-entered the preconditions
Skill() is synchronous, so the nested build had already finished; the branch would have asked whether to redo just-completed work, or restarted a run stopped via Mark in-progress and stop. It now reports the nested result and stops, overriding this plan's step 7 claim that all three Appendix A resume rows stay live.
The hoisted dirty-tree guard fired on the Step 8 callback
implementation-plan writes the spec and HTML before calling back, so the guard saw them as uncommitted work and prompted at exactly the post-interview moment the hoist prevents, or stopped the chain headless. Plan artifacts are now excluded; verified by running a build against an uncommitted spec plus HTML, which proceeded silently to completed.
The proposal path assumed an artifact
a Tier 0 idea is answered directly with no document written, leaving the chain calling implementation-plan with a path that does not exist. It now falls through to the direct path with the original objective. The following remain verified by construction rather than execution, needing an interactive session no harness here can provide, and are checked off under Step 3.4 on the user's explicit instruction:
Criteria 2 and 10, the no-specs chain entry and the objective prompt on the None of these path
both sit behind the discovery offer, which is now confirmed to stop rather than self-resolve in a headless run.
Criteria 8 and 9, the two non-implementing Step 8 terminations (Exit and Run as workflow)
asserted statically by check 13. The option's full label carries an em-dash, which the report renderer reads as its own item separator, so it is named in prose here rather than quoted.

Next steps

Follow-up ideas that came up along the way — none of them are required to finish this plan.

Reconstruct a spec from a legacy HTML-only plan

Closes the third no-plan branch that step 2 deliberately leaves stopping.

Paste this prompt into Claude to execute this follow-up:

In the agentics repo, add a spec-reconstruction path to
kit/plugins/plan-agent/skills/build/SKILL.md: when the user passes a .html
plan that has no sibling .md spec, offer to derive a spec from the rendered
HTML using scripts/extract-plan-spec.mjs, write it beside the HTML, re-render
with scripts/build-plan-html.mjs, and continue implementing. Keep the current
stop as the decline path. Add a check to tests/plugins/test-build-skill.sh,
bump the plan-agent minor version in .claude-plugin/marketplace.json, and add
a CHANGELOG entry under kit/plugins/plan-agent/. Verify with
`bash tests/plugins/test-build-skill.sh` exiting 0 before reporting done.
Cut the gate count of a chained build run

Wish list — this is the option B design the proposal deliberately deferred.

Paste this prompt into Claude to execute this follow-up:

In the agentics repo, reduce the interactive gate count when
kit/plugins/plan-agent/skills/build/SKILL.md drives a chained run. Add a
suppress-menu flag to kit/plugins/plan-agent/skills/implementation-plan/SKILL.md
so its Step 8 next-step question is skipped when the caller has already
committed to implementing, and have build pass it. Leave the tracking-issue
question intact. Bump the plan-agent minor version in
.claude-plugin/marketplace.json, add a CHANGELOG entry, and add a check to
tests/plugins/test-build-skill.sh asserting the flag is both documented and
passed. Verify with `bash tests/plugins/test-build-skill.sh` exiting 0 before
reporting done.