Imported from dhigby/virtual-ltct (
.claude/skills/speckit-companion-implement/SKILL.md). Install upstream withnpx skills add dhigby/virtual-ltct --skill speckit-companion-implement. Copyright stays with the author.
User Input
$ARGUMENTS
Record this step's start: before anything else runs
A step's recorded window has to contain the work it claims. Stamping the start partway down the body leaves the extension hooks, and any node above the stamp, outside the window the step later reports. So this is the first instruction in the command, ahead of the hooks.
Let <step> be this command's phase and <status> its in-progress status: specify/specifying, plan/planning, tasks/tasking, implement/implementing.
Which feature directory this step stamps against decides when it stamps.
- A step that mints its own feature directory, meaning any fresh-spec entry point such as
specifyorauto, has nothing to stamp against yet..specify/feature.jsonis this step's output: it still points at the previous spec, so stamping now would write this run's status onto finished work. Resolve the directory first, then stamp the instant it exists, before any other work in the step. - Every other step reads the feature directory it was given, from the invocation or from
.specify/feature.json, which by then points at this spec. Stamp immediately, before the extension hooks and before any node.
In both cases the call is the same:
python3 .specify/extensions/companion/scripts/write-context.py --feature-dir <feature_directory> --step <step> --status <status> --kind start --by extension
Add --at "<dispatch time>" when the dispatcher printed one; otherwise the script stamps now. Two things keep this honest:
- Run it, never hand-write it. The script stamps the real clock and writes atomically. A hand-authored entry in
.spec-context.jsonis what corrupts the file. - A second start is refused, not reconciled. History is append-only, so if the extension already seeded this step's start, this call appends nothing and the earlier timestamp stands. Running it is always safe; skipping it loses the window.
Name every command the way this project registers it
Commands are named in dot form throughout this body, speckit.companion.plan, because that is their canonical id, and without a leading slash, because the spelling a host actually registers is not always this one. Claude Code installs /speckit-companion-plan. Look at how the commands are installed in this project, under the agent's own commands or skills directory, and use that spelling every time you name one to the developer or dispatch one yourself. A dotted name typed into a host that registered dashes resolves to nothing at all.
Pre-Execution Checks: stock spec-kit extension hooks
Companion runs on top of stock spec-kit, so a project's installed spec-kit extensions (git, and any others in .specify/extensions.yml) must fire on a Companion run exactly as on a stock /speckit.* run. That is separate from Companion's own node-hooks in .specify/companion.yml; both fire. Like the rest of the pipeline this must never fail the host command: anything missing or malformed is skipped silently.
Let <step> be this command's phase: specify, plan, tasks, or implement. Run the pass twice: hooks.before_<step> now, before any of the work below, and hooks.after_<step> once this command's work is fully reported, before handing off.
-
Read
.specify/extensions.yml. Absent, unparseable, or carrying no entries for that anchor: skip silently. Skip a hook that isenabled: false(noenabledfield means enabled), and any hook whoseextensioniscompanion: those record a stock run's lifecycle, which this command records in its own body, so dispatching them would rewrite what this step just wrote. A hook with a non-emptyconditionis left to the HookExecutor and never evaluated by you; no condition, or a null or empty one, is executable. -
Emit one block per executable hook. An optional hook (
optional: true):## Extension Hooks **Optional Pre-Hook**: {extension} Command: `/{command}` Description: {description} Prompt: {prompt} To execute: `/{command}`A mandatory hook (
optional: false) instead:## Extension Hooks **Automatic Pre-Hook**: {extension} Executing: `/{command}` EXECUTE_COMMAND: {command} Wait for the result of the hook command before proceeding to the Outline.Those labels are the before pass's. In the after pass drop
Pre-from the label and drop the closing wait line.
For specify, branch creation is normally one of these before_specify hooks (the git extension); the spec directory and its files are always created by the command body itself.
The smallest thing that works
Before building anything, stop at the first rung that holds: does it need to exist at all; does this codebase already have it; does the standard library, the platform, or an installed dependency do it; can it be one line; only then, the minimum code that works. Fix the cause where every caller passes through, not the symptom one caller reported. Delete rather than add, boring rather than clever: no interface with one implementation, no factory for one product, no scaffolding for later. Lean governs what you build and write, not how the work is split: sending work to a worker where a step says to is not extra.
Write it the way you would say it. One idea per sentence. No em-dashes: a full stop, a comma or a colon says it. Say what happens, not what the system "shall be capable of". Never a section that exists to say "N/A": remove it instead.
The same test governs what you write, and again once it is written. A section nobody acts on is removed, not filled in. No requirement for what a type or a test already enforces. A third scenario has to cover a failure the first two miss. Then reread and cut: the sentence restating the one before it, the example longer than its rule, the clause defending a choice nobody challenged, the prose repeating a table. Each reads as thoroughness and is what makes a body too long to follow.
Never simplify away validation at a trust boundary, error handling that prevents data loss, security, accessibility, or anything the spec asks for. A corner cut on purpose carries // simplified: <ceiling>, <what to do when it binds> in the code and one concerns entry in this step's capture.
Outline
Execute tasks.md phase by phase in dependency order. Each phase is laid out as ordered waves split by ⟶ Wait … join lines: a dependency map where tasks within a wave are independent and a ⟶ Wait marks where the next tasks depend on what came before. Setup and polish are built inline, wave by wave, stopping at each ⟶ Wait line until the wave above is done; Foundational waves of four or more tasks go to workers through dispatch-briefs.py --waves, and smaller ones stay inline; each user-story phase goes to its own worker wherever a subagent tool exists, and inline is the fallback for a host that cannot dispatch. Each task's finish is logged as it completes; then mark the spec complete.
-
Read
.specify/feature.jsonfor the feature directory. Load<feature_directory>/tasks.md,plan.md, and the feature spec (<short-name>.spec.md, orspec.mdin a project written before this), plusdata-model.mdandcontracts/if present. The step's start is already stamped, above. -
Work
tasks.mdphase by phase, in dependency order: Setup, then Foundational (which blocks every story), then each user-story phase in priority order (P1 first), then Polish.tasks.mdlays each phase out as ordered waves separated by**⟶ Wait …**join lines. The waves are a dependency map: tasks inside one wave are independent of each other, so any order is safe, and a⟶ Waitline marks where the next tasks depend on everything above it. Execute wave by wave, in order, and stop at each⟶ Waitline until the wave above is done. Halt on a failed task and report the cause. -
Dispatch one worker per user-story phase that owns five or more files. If you have a subagent tool, Claude Code's
Agent/Tasktool or your host's equivalent, you use it there. Setup and Polish stay with you: Setup is trivial, Polish is cross-cutting. In Foundational, before each wave, runpython3 .specify/extensions/companion/scripts/dispatch-briefs.py --feature-dir <feature_directory> --wavesand do exactly what it prints: that wave's worker briefs, dispatched together and unedited, or the tasks to build yourself. Start nothing after that wave until its workers return and pass step 5's check. Never split a story phase per task: the startup costs more than the task.Count the phase's own files line, and that alone decides it. It holds in an auto run too: specify, plan and tasks having run in this same session is not a reason to build inline. Below five files a worker's startup is the whole cost, so build a phase under the threshold inline, in phase order, and say which phases you dispatched and which you kept.
Read each story phase's files line,
Files:orFiles owned by this phase:. That is its ownership. The tasks step gave every file one owner, so the story phases are disjoint by construction and you dispatch them all together. Two phases naming the same file is a task-list defect: say so, and run those two one after another. Give each worker its phase's task lines, that user story fromspec.md, the plan's Structure Decision, and its own living-spec slice,resolve-spec-paths.py --changed <that phase's files> --requirements-for --follow-aligns --json, so it carries the requirements about the files it touches and none of yours. The flag adds rules from other capabilities that constrain the code the worker writes. Then ask it to read what it needs, write the code and that story's tests, run only the test files its phase owns (the full suite runs once, at the end), and return a distilled result only: what it built, the files it touched, and any test still failing. A worker must never return file contents.# the worker, per task it finishes: append only, never fold python3 .specify/extensions/companion/scripts/write-context.py --feature-dir <feature_directory> --task <TaskID> --kind complete --by ai --did "<one line>" --files "<files>" --append # you, the moment each worker's result returns python3 .specify/extensions/companion/scripts/write-context.py --feature-dir <feature_directory> --materializeFolding is a read-modify-write on the shared file, so two folders at once race. Workers only ever append, and you do every fold, in the foreground, one at a time.
-
What you build yourself (Setup, Polish, small phases and waves, or everything without a subagent tool), close each task as you finish it, never batched at the end of a wave:
python3 .specify/extensions/companion/scripts/write-context.py --feature-dir <feature_directory> --close-task <TaskID> --by ai --did "<one line>" --files "<files>"--close-taskappends the finish and folds it in one call: the panel updates and the task'stasks.mdbox is checked. Never hand-edit the checkbox. -
At each join line, check the workers' claims before crossing it. A worker's report names files it touched and tests it ran; confirm the files exist and the test files are on disk before its result becomes the next phase's input. A claim that does not check out gets one re-run with the specific correction, and a second failure stops the step rather than building on it.
Then reconcile. Hand the type-check and the lint to one worker and take back only the verdict and the failing names. Fix any seam drift between workers. Then run
--materializeonce more as a backstop: it is idempotent, and it catches any finish whose fold was missed.tasks.mdis owned only through--materialize. -
Run the project's own checks before you call this done. Validating against the spec's Functional Requirements and Success Criteria by reading is not validation. Run the suite and the type-check or build the project actually uses, read from its
package.jsonscripts,Makefile, or the repo's own instructions, and do not invent a command: a test you wrote and never executed is a guess about your own code.- A test you authored that fails is your task, not a follow-up. Fix it now.
- A pre-existing test your change invalidated is also yours. Renaming what a component shows breaks the test asserting the old text. Updating it is part of the change, not scope creep.
- A test file that does not compile counts as failing. Check the suite actually ran, not merely that the command exited.
- If you genuinely cannot run them, because no test script exists or the environment forbids it, say so explicitly in the summary and record it as a concern below. Do not describe a read-through as though it were a run.
Then read your own diff and delete what it does not need: a helper with one caller, a branch no input reaches, a wrapper that only forwards. Then report a short summary of what was built and anything left undone.
Output: working changes per tasks.md, with completed tasks checked off.
-
Capture what was verified and decided the moment validation ends (best-effort; JSON when you can, bare text when not; skip silently if
python3is unavailable):python3 .specify/extensions/companion/scripts/write-context.py --feature-dir <feature_directory> --step implement --batch '{ "verified": [{"what": "<check>", "command": "<cmd>", "result": "<outcome>", "warnings": ["<seen-and-dismissed>"]}], "decisions": [{"decision": "<implementation choice>", "why": "<why>", "rejected": "<alternative>"}], "concerns": [{"note": "<friction, residual risk, or a `// simplified:` ceiling you left in the code>", "step": "implement"}], "coverage": [{"req": "FR-001", "tests": "<path.test.ts::case,other.test.ts>"}], "step_summary": {"summary": "<what shipped in one line>"}, "last_action": "<final breadcrumb, e.g. all tasks done, 18/18 tests pass>" }'One call, not one per item.
--batchtakes the whole volley as a single JSON object and applies each writer additively, so the shared context file is read and rewritten once instead of once per entry. Emit one--batch. Include only the keys you actually have: an empty list is not the same as an absent one, and on a clean runconcernsis genuinely absent.Record a check that can be run with
--verify-run "<what>::<command>": it runs the command and keeps the exit code, rather than taking your word for it. Every suite, build, lint and script goes that way.--verifiedstays for what genuinely cannot be run — a manual pass, a judgement — and reads in the viewer as your account rather than as evidence, which is what it is. Never write a--verifieddescribing a command you ran: that is the case--verify-runexists for, and a typed result is indistinguishable from an imagined one. If a check could not be run at all, record that as a--concernnaming what was skipped and why, and do not record a--verifiedfor it.One
--verify-runper runnable check and one--verifiedper judgement (a manual pass, a warning you saw and judged benign), one--coverage-req … --tests …per requirement a test covers, one--decisionper genuine implementation choice. Record--concernonly for real friction; on a clean run record none.
-
Mark the spec complete. Once every task in
tasks.mdis checked off and the work validates, finish the lifecycle so the spec lands atcompletedinstead of stopping atimplemented."Validates" means the project's own checks ran and passed. A spec MUST NOT be marked complete over a failing suite the run introduced: fix it, or leave the spec at
implementedand say why. Where the checks genuinely could not be run, record that as a concern before completing, so the state reads "finished, unverified" instead of implying "finished, verified". Run from the repository root; the feature directory resolves on its own:python3 .specify/extensions/companion/scripts/write-context.py --mark-complete --by ai --set workflow=companionThe
--setpins which workflow finished the spec in the same write, so a mid-run join keeps Companion dispatch. This is the only sanctioned writer ofcompleted: it closes the implement step and promotes animplementedspec, or animplementingone whose tasks are all checked, straight tocompleted, keepingcurrentStepatimplement. Best-effort and idempotent: ifpython3is unavailable, warn and skip without failing the host command, and a spec alreadycompletedis left untouched. Running it when the spec-kit workflow engine already called the same path is harmless.-
Account for every loaded capability first: a delta or an explicit skip, never silence. Before folding, read
livingSpecs.loadedin this feature's.spec-context.json. An absent key or an empty list means nothing was loaded and there is nothing to account for; skip to the fold. Go through every name in that list, and give each exactly one of two outcomes. For a loaded capability whose behavior this feature actually changed, append a delta block to this feature'sspec.mdcapturing the real new or changed requirement, marked with that capability's name so the fold routes it to the right spec:## ADDED Requirements <!-- capability: <name> --> ### <the new capability requirement, as a testable statement> #### Scenario: <name> - **WHEN** <trigger> - **THEN** <observable outcome>A file no loaded capability claims belongs to a capability that does not exist yet. Before writing any block, run the resolver on the files you changed:
python3 .specify/extensions/companion/scripts/resolve-spec-paths.py --changed <files> --json. A file with no match is not yet a reason to write anything. Ask one question: would someone planning a change here need to know this? A file that only supports behaviour a spec already states needs no block: say so in your summary and move on. New code is never squeezed into a requirement to give it a home. When the file does add something a person can now do, that is new behaviour with no home. Do not route it to the nearest capability you happened to load. Give it a block of its own with a new name,<!-- capability: <name> -->, where the name is a thing a person can now do, said out loud, never a directory. Register it before the fold, with the directories you touched as its match:python3 .specify/extensions/companion/scripts/register-capability.py --name <name> --match '<dir>/**'. The fold then creates the spec from your block. Say in your summary that a capability appeared and why.Pick the verb by whether the requirement heading already exists in the capability's living spec (
capabilities/<name>/<name>.spec.md). A requirement that is not already there goes under## ADDED Requirements, even if it revises the same behavior area. Reserve## MODIFIED Requirementsfor changing the body of a requirement whose heading is already in the living spec; the heading must match an existing one for the edit to replace it in place. Read the existing headings before choosing: a new heading that says what an existing one says in other words is that requirement, changed, and belongs under MODIFIED with the existing heading. A// simplified:ceiling you left in the code is not a delta entry: inside a delta block every###is a requirement heading, so a "Known limits" heading there folds into the capability as a requirement. Record ceilings asconcernsin this step's capture instead, and letliving-syncplace them. Use## REMOVED Requirementswhen you deleted one, and## RENAMED Requirements(### Old heading -> New heading) for a rename. Write one block per changed capability, each with its own<!-- capability: <name> -->marker: several marked blocks fan out, each capability spec receiving only its own requirements. Never invent requirements to pad the list, and add a third scenario to an existing requirement only when it covers a failure the first two miss, saying which in the scenario name.For a loaded capability whose behavior this feature did not change, one you merely read for context, record an explicit skip. One call per untouched capability:
python3 .specify/extensions/companion/scripts/write-context.py --living-spec-skip "<name>: <one-line reason it wasn't changed>"By the end, every name in
livingSpecs.loadedis accounted for by a delta block or a recorded skip. A capability that is neither is a hole the fold flags. -
Have the deltas reviewed before they fold. A living spec is context every later run loads, so what folds into it is read by someone other than its author. Run
python3 .specify/extensions/companion/scripts/dispatch-briefs.py --feature-dir <feature_directory> --livingand do exactly what it prints. -
Fold living-spec deltas (opt-in, best-effort). After the completion write, fold the deltas you just authored into the durable living spec, OpenSpec's "archive" step:
python3 .specify/extensions/companion/scripts/write-context.py --fold-living-spec --by aiIt parses the feature spec for
## ADDED / MODIFIED / REMOVED / RENAMED Requirementsblocks and applies each to the resolvedcapabilities/<name>/<name>.spec.md: the changed-files-matched capability for unmarked blocks, and every<!-- capability: <name> -->-marked capability for the rest. Opt-in (it only acts whenlivingSpecs.enabled: true), a clean no-op when there is no delta block, idempotent on re-run, and it records the synced names ontolivingSpecs.synced. Never fails the host command.
-
Timing: keep .spec-context.json honest
Record every boundary by running the writer script. Never edit .spec-context.json yourself; a hand-authored edit is what corrupts the file. The model is finish-only: one finish per task and per substep, its duration the gap back to the previous finish. Never a start+complete pair for either, which stamps a 0s tick and measures nothing.
-
Close your own step, as the last thing you do, after emitting any mandatory after-hook block:
python3 .specify/extensions/companion/scripts/write-context.py --feature-dir <feature_dir> --step <this step> --advance --by ai--advanceappends the step's complete and flipsstatusin one atomic write. It is idempotent and first-writer-wins, so it changes nothing when the after-hook already closed the step, and it is the only thing that closes the step when that hook was printed rather than dispatched. Run it every time, with two exceptions: clarify and analyze use--finish, which records a boundary without owning a status; implement runs neither, because its own final node writescompletedand closes the step in the same write. -
One finish per substep, the moment it ends. Plan records
researchanddesign, tasks recordsgenerate, implement recordsliving-reviewandliving-foldwhen living specs are on. Never two in one batch, never a separate start: a finish measures the gap back to the previous boundary, so several stamped together at the close of a step read as0seach and record nothing.python3 .specify/extensions/companion/scripts/write-context.py --feature-dir <feature_dir> --step <step> --substep <name> --finish --by ai -
Closing a task is that task's last action, not a bookkeeping pass you batch later. Feature dir from
.specify/feature.json:python3 .specify/extensions/companion/scripts/write-context.py --feature-dir <feature_dir> --close-task <TaskID> --by ai --did "<one line>" --files "<files>"One call appends the finish with its own real clock, folds it into the panel, and flips that task's box in
tasks.md. Never hand-edit that box or hand-author per-task JSON, and never write a per-task start. Re-closing is safe. Batching is a defect the doctor catches: it names any cluster of finishes stamped seconds apart, because those timestamps record when the batch was written, and history is append-only so it cannot be repaired afterwards. Trust the per-task summaries and their order; the timestamps are best-effort.A fanned-out worker appends only, because folding is a read-modify-write and two folders contend:
python3 .specify/extensions/companion/scripts/write-context.py --feature-dir <feature_dir> --task <TaskID> --kind complete --by ai --did "<one line>" --files "<files>" --appendThe MAIN agent folds each returned result with
--materialize, one at a time, and once more at a wave join as a backstop. -
Never write the next step's start. The next command owns it. Writing it here renders a phantom "Generating …".
Self-advance: hand off to the next step
This is one step in the Companion pipeline. How the run continues depends on the environment you are running in; do not invoke a separate headless/deterministic run command for the everyday flow.
- On an agentic CLI that keeps acting after a step finishes: once this step's work is complete, dispatch the next step's
speckit.companion.*command and keep going. The order is fixed and this step's own handoff names its successor, so there is no file to open to find out. - Pause at every review gate, and name the command that continues. Where the workflow marks a
gate(e.g. review-spec, review-plan), stop and wait for approval rather than running past it. When you stop, name the next command literally: "approve and runspeckit.companion.plan <feature_dir>", not "approve to move to plan". Only continue once the gate is approved. - Nothing follows implement. Implement's own final node writes
completedthroughwrite-context.py --mark-complete, so the spec is already finished when this step ends. Do not dispatchspeckit.companion.mark-completeafterwards: it is the manual recovery command and the workflow engine's terminal step, not a step a run adds for itself. There is exactly one writer ofcompleted; never introduce a second. - Degrade gracefully on a one-shot environment. If your environment runs one step and then stops, the handoff simply does not fire: finish this step, record its progress, and stop. The run stays valid and resumable, and the next step is triggered manually, by the developer or the companion panel. Completion likewise stays a manual action there.
Node hooks: run the project's before/after inserts
This command is assembled from ordered nodes. A project attaches its own work around any node in .specify/companion.yml, and you are the runtime. Like the rest of the pipeline it must never fail the host command: degrade and continue.
Find the hooks. Look up commands.<this-command>.hooks, whose before and after anchors are keyed by a node id from this command's order. Run a node's before hooks immediately before its work and its after hooks immediately after, several at one anchor in declared order. An absent, empty, malformed or unparseable file means no hooks: run the shipped command unchanged, silently when it is absent and with one short warning when it is broken.
Hook types:
{ type: command, run: "<shell>" }: run it with your terminal tool, then continue. Without a terminal tool, report the command you would have run rather than pretending.{ type: prompt, text: "<instruction>" }: act on the text before moving on.{ type: node, ref: <id> }: carry out.specify/companion/nodes/<id>.mdas part of this command. A missingreffile is a real misconfiguration: report it and stop rather than silently skipping.
Background hooks. Any hook may add background: true: start it and continue without waiting, detaching a command (&, nohup … &) and not blocking on a node or prompt. Report its result whenever it lands. Never background anything that writes .spec-context.json, meaning the timing and capture calls: they read-modify-write a shared file, so two at once lose an update. Background is for slow side-effects like a test run or a build, not for bookkeeping.
A hook anchored to a node this run does not include, because a recipe dropped it, warns once and is skipped. A hook whose own work fails is reported and the pipeline continues, unless the failure clearly makes the rest unsafe.
