#4909·gsd-core

Epic: shipped workflow bodies are a compiled artifact, contract-checked against the CLI and the installed tree

Author: trek-eCreated Sep 21, 2026Updated Sep 21, 2026
Labelsarea: workflowtype: choreepic

An approved epic does not approve its children — each child is its own issue before code.

Epic: shipped workflow bodies are a compiled artifact, contract-checked against the CLI and the installed tree

The deliverable is the seam, not the individual fixes. The issues absorbed below are evidence that one piece of behavior has no single owner. They are the symptom list, not the work list. Repairing them where they were reported — N fixes for N issues — leaves the missing owner still missing, and the next occurrence is already being written somewhere else in the tree. An implementation that does that has not closed this epic, even with every symptom gone and CI green.

Closing this epic means three things land together: a generated CLI surface manifest (verbs, flags, arity) and a post-install tree manifest (every path an artifact may address after each runtime's rewrite), both produced by the same generators that build them; the hand-maintained per-surface scans are deleted rather than kept in sync, because two copies that agree today are the same defect as two that disagree; and enforcement is by construction — one extractor over workflows/, agents/, commands/ and skills/ that resolves every gsd_run invocation, every ${VAR}, every @-ref and every referenced file against those manifests, with --check. The first two without the third un-consolidate quietly as soon as someone who has not read this issue touches the area.

A defect that cannot be expressed through the seam is a finding about the seam's design and belongs in this issue as a comment — one special case is a reference that is correct for one runtime and unresolvable for another.

This is the shape that worked in #1372 (markdown sectionizer), #2143 (table, bounded mutation, fail-loud) and #2121 (phase identifier): each closed its bug class with a seam, a migration and an anti-divergence guard, none of them by fixing the reported symptoms. Splitting delivery across several PRs is expected and welcome; splitting it so that the seam never lands is not.

Thirteen open confirmed-bug issues are one defect: the shipped workflow bodies are executable shell and addressable references, and nothing validates them against the surfaces they call. They are the largest executable surface in the product and the only one with no compiler.

What is already checked, and the exact shape of the hole

lint:ci runs 58 scripts, 15 of them named *-drift*. lint:generated-sync --checks 20 generated artifacts. This is not a repo that neglects validation — it is a repo whose validation is per-surface and hand-written, and the open bugs sit precisely in the gaps between the scans.

scripts/lint-command-contract.cjs (ADR-0002) is the closest existing check and states its own scope in its header: commands/gsd/*.md — frontmatter, allowed-tools against CANONICAL_TOOLS, execution_context @-refs resolving on disk, and workflow reachability. It does not read agents/. It does not read the bash inside gsd-core/workflows/*.md at all. lint-workflow-shellcheck.cjs lints that shell for shell correctness — a script that references an unassigned variable is valid shell. check-contract-drift.cjs follows @~/.claude/gsd-core/references/… and, by #4841's measurement, its regex cannot see the other spelling.

Every absorbed issue below is a thing one of these scans was the natural owner of and does not cover.

Four arms, one remedy

(a) A variable is referenced and never assigned — the code path is dead and reports success.

#4777secure-phase.md:21 and validate-phase.md:21 call gsd_run query init.phase-op "${PHASE_ARG}". Neither file ever assigns PHASE_ARG, from $ARGUMENTS or anything else. verify-work.md derives it with sed; discuss-phase.md derives PHASE_NUMBER via jq on $INIT; these two have neither. Traced into code: guardedFindPhase(cwd, '') returns null, guardedGetRoadmapPhase(cwd, '') returns null, so cmdInitPhaseOp resolves phase_found: false on every invocation from either workflow. Introduced 2026-08-19 in ea594300d and unnoticed since. Shellcheck passes it; nothing else looks.

(b) A flag the caller already holds is not passed, systematically.

Issue Omission Measured
#4772 gsd_run query agent-skills <agent> with no --ws 38 calls in 20 workflow files, none passing it, none exporting GSD_WORKSTREAM. A phase started with --ws <name> silently gets the active-workstream pointer's skills — empty or foreign. The same workflows call init.* without --ws too, and --ws on init.* sets no pointer
#4483 --phase-dir / --phase-number / --phase-req-ids on check predicate each workflow passes a different subset, and at least two omissions drop a value the same workflow file already holds as a live bash variable at that exact point. A capability-declared command-exit-zero gate referencing the omitted placeholder silently receives ''

#4483 is the clearest statement of why this is a contract problem and not 20 editing mistakes: nothing anywhere declares that check predicate has three placeholder inputs, so nothing can notice that a call site provides one of them.

(c) A reference addresses something that does not exist after install.

Issue Reference Reality
#4841 @gsd-core/references/<x>.md11 occurrences across 4 agents matches no installer rewrite on any profile, in either prefix mode. After install they address <consuming-project>/gsd-core/references/<x>.md, a directory no consuming project has, while the files sit at ~/.claude/gsd-core/references/. 44,655 bytes of planner / verifier / plan-checker / debugger guidance. The other spelling — 137 occurrences across 48 agents — is rewritten correctly and is followed by the drift scan. gsd-planner.md carries both at once, so this is not a per-agent design choice
#4783 execute-phase.md:1068"Spawn continuation agent … using continuation-prompt.md template" no such file in 1.14.0, verified against a clean npx install. references/continuation-format.md is the "Next Up" handoff block, not a spawn prompt. Every lane that hits a checkpoint improvises the surrounding prompt — exactly the part where consistency matters, since the point of step 6 is that a fresh agent gets explicit state
#4805 resume-project.md discovers .continue-here*.md with bare find -maxdepth allowlisted agent shells block it. lint-portable-grep.cjs exists for the sibling case; find has no equivalent

#4841 is the defect class of #3719 in the one spelling that fix could not see: those 11 use neither ~/.claude nor $HOME/.claude, so nothing keyed on either reaches them.

(d) The install surface produces collisions and unregistered forms the source cannot see.

  • #4860.claude-plugin/plugin.json declares "commands": "./commands/gsd/" and "skills": "./skills/", both holding the same 72 workflows. Claude Code treats each as a separate component surface: 72 workflows appear as 144 entries in the always-on description inventory.
  • #4855 — the Claude Code install writes agents/*.compact.md under the same name as the canonical agents, so Claude Code loads only one of each pair, by directory order.
  • #4859 — compiled runtime code emits the unregistered /gsd:<cmd> colon form in user-facing guidance: 97 occurrences in bin/lib, including the smart-entry action menu. The raw colon tokens under agents/, commands/ and workflows/ are the intended authoring form and are rewritten at install (#2903); bin/lib is downstream of that rewrite and was never covered. A session told the user /gsd:plan-phase 1; Claude Code answered "Unknown command."
  • #4838 — Windows hook commands pin a versioned node.exe path the next WinGet upgrade breaks.
  • #4790 — the built-in code reviewer is unreachable by capability contributions at execute:post.
  • #4776ui-phase.md step 4 has no non-interactive branch, so --auto stops at the "Existing UI-SPEC" question.
  • #4780$ARGUMENTS is spliced into template prose with no labeled arguments field. On a real /gsd-update --reapply, the expanded <process> read "Parse the first token of --reapply:" while <objective> and <flags> stayed generic; the model read it as ordinary prose, concluded no flag was passed, and began the wrong workflow. Only the user interrupting stopped it.

The seam this epic delivers

  1. A generated CLI surface manifest. gsd-tools already knows its verbs and flags; emit them as a checked artifact, the way gen-exit-code-registry / gen-capability-registry already do. A declared verb names its required inputs, which is what makes #4483 a detectable defect rather than an invisible one.
  2. A generated post-install tree manifest, per runtime and profile. Every path an artifact may address after each rewrite. #4841's two spellings become a manifest lookup, not a regex that one of them evades.
  3. One extractor over every authored surfaceworkflows/, agents/, commands/, skills/ — not one scan per directory. It resolves: every gsd_run <verb> [flags] against the CLI manifest; every ${VAR} to an assignment upstream in the same file; every @-ref and every named template/reference file against the post-install manifest; every shell builtin against the agent-shell allowlist. --check in lint:ci.
  4. The install rewrite is a one-way generator with --check. Source authors one token; the generator emits per-runtime forms and asserts no unrewritten form survives into a shipped artifact — including compiled bin/lib, which is where #4859's 97 occurrences live. Name collisions (#4855) and duplicate component registration (#4860) are manifest conflicts, detected at generation.

The ratchet

A workflow body is not shipped until it resolves. The extractor runs in lint:ci and in lint:generated-sync --check, and the per-surface scans it subsumes are deleted rather than left beside it. Positive controls per rule, the same doctrine the drift guards already carry: a fixture workflow with an unassigned variable, one with an unknown flag, one with a dangling @-ref, each proven to drive the check red. #4777's dead code path survived thirteen months; the acceptance test is that the same file fails the check today.

Absorbed issues — evidence of the missing seam, NOT a work list

Issue Defect Arm
#4777 ${PHASE_ARG} never assigned in secure-phase.md / validate-phase.md; init.phase-op returns phase_found: false on every invocation unassigned var
#4772 38 query agent-skills calls across 20 workflows omit --ws; per-workstream skills never injected omitted flag
#4483 check predicate dispatch omits PHASE_* flags the calling workflow already holds in scope omitted flag
#4841 11 bare @gsd-core/references/… pointers match no installer rewrite; 44 KB of guidance addressed at a nonexistent path dangling ref
#4783 execute-phase.md checkpoint handling references continuation-prompt.md, which does not exist dangling ref
#4805 resume-project.md uses bare find -maxdepth, which allowlisted agent shells block shell allowlist
#4859 compiled bin/lib emits the unregistered /gsd:<cmd> colon form — 97 occurrences, including the smart-entry action menu install rewrite
#4860 72 workflows register as 144 skill entries through commands/ and skills/ install surface
#4855 agents/*.compact.md installed under the canonical agent name; only one of each pair loads install surface
#4838 Windows hook commands pin a versioned node.exe path the next WinGet upgrade breaks install surface
#4790 built-in code reviewer unreachable by capability contributions at execute:post contract
#4776 ui-phase.md step 4 has no non-interactive branch; --auto stalls on a question contract
#4780 $ARGUMENTS spliced into prose with no labeled arguments field; the model read a passed flag as literal text contract

Closed prior art: #3719 (27/34 agents shipping unresolvable @-includes) is #4841's sibling in the spelling that fix could see. #2903 established the install-time colon rewrite that #4859 shows is incomplete downstream. ADR-0002 / lint-command-contract.cjs is the check this epic generalizes past commands/gsd/.

Done when

  • A generated CLI surface manifest exists and is --checked
  • A generated post-install tree manifest exists per runtime and profile, and is --checked
  • One extractor validates gsd_run verbs and flags, ${VAR} assignment, @-refs, referenced files and shell builtins across workflows/, agents/, commands/ and skills/
  • The per-surface scans it subsumes are deleted, not kept beside it
  • No unrewritten command form survives into any shipped artifact, compiled bin/lib included
  • Two artifacts cannot install to the same path; duplicate component registration is a generation-time conflict
  • Every flag-taking verb declares its required inputs; a call site omitting one fails the check
  • Every rule has a positive control fixture proven to drive it red
  • secure-phase.md and validate-phase.md fail the check as authored today
  • Failing-first regression per absorbed issue

Non-goals

  • Repairing the absorbed issues individually at their existing call sites without building the seam. Thirteen edits leave the fourteenth already written.
  • Changing the authoring convention. The colon form stays the canonical authoring token per the documented surface-dependent rule; this epic asserts the rewrite is complete, not that the token changes.
  • Rewriting the workflows in a different language or moving logic out of markdown. The constraint is that these bodies are runtime-loaded prose-and-shell; the requirement is that they are checked, not relocated.
  • Re-litigating lint-workflow-shellcheck.cjs. Shell correctness and contract correctness are different checks; this adds the second.
  • Changing capability dispatch policy (#4790's route decision) — the finding here is that the contract is unvalidated; what the correct route is belongs in that issue.

Generated with Claude Code