chore(#1671): fragmentize plan-phase.md and repair flag forwarding to the init bundle — Phase 6.2 (#3019)

* chore(#2993): fragmentize plan-phase.md onto the fragment model

Epic #1671 Phase 6.2. plan-phase.md is the largest workflow in the repo and
carried zero markers; it was deferred out of the Phase 3 pilot for two
reasons, both now dead. The 36-byte PRE_PHASE6 headroom was never the
blocker it looked like — fragmentizing is net-negative on host source, so
the trim is what creates the room. The --mvp interleaving was resolved by
measurement in #2992 and no sub-line mechanism is built.

- widen WHEN_VOCABULARY 14 -> 19 via a second coordinated ADR-1671
  amendment: flag:--ingest, flag:--prd, flag:--research-phase,
  flag:--reviews, state:chunked-mode
- state:chunked-mode is `--chunked` OR config workflow.plan_chunked, and
  that disjunction is resolved in the FACT, never in the grammar, so a
  compound condition never becomes an operator
- parse the new flags on the plan-phase route; extract six gated bodies to
  gsd-core/workflows/plan-phase/steps/ behind manifest-gated stubs
- prd-express-path.md was already extracted but read unconditionally; its
  wrapper is now gated, so the existing extraction finally pays off

plan-phase.md 94,483 -> 87,575 bytes (cap 94,519): headroom goes from 36
bytes to 6,944.

Also closes a surfaced docs gap: five real plan-phase flags (--chunked,
--skip-ui, --bounce, --skip-bounce, --granularity) were documented in
neither the argument-hint nor help. Making --chunked load-bearing without
fixing its siblings would leave the defect class half-open.

Refs #2993

* fix(#2993): forward flags to the init bundle so section gating actually fires

Blocker found by the correctness review, confirmed directly, and missed by
both the isolated reviewer and every test in this branch.

Neither workflow forwarded its flags to the init CLI:

  plan-phase.md:71    INIT=$(gsd_run query init.plan-phase "$PHASE" $GRAN_PARAM)
  execute-phase.md:84 INIT=$(gsd_run query init.execute-phase "${PHASE_ARG}")

So every flag: atom was permanently false in production and its section
permanently excluded. For plan-phase that made the PRD express path
UNREACHABLE — a regression, since it was an unconditional read before.
For execute-phase this is PRE-EXISTING: #2932 shipped `flag:--wave` gating
that has never once been true, so `--wave` silently dropped its own
wave-filtering guidance. Fixed here under the no-defer rule.

Why every test missed it: they drive the init CLI directly with flags,
which works. Production goes through the workflow's bash line, which did
not pass them — the exact "assert against the shape production uses" trap
this branch's own test matrix warns about.

- parse and forward --prd/--ingest/--research-phase/--reviews/--chunked
  (plan-phase) and --wave (execute-phase), using the anchored regex idiom
  the neighbouring GRAN_PARAM line already uses
- add a regression guard DERIVED FROM THE MANIFEST: for every flag:--X
  section, the owning workflow's init line must forward --X. It fails
  against the pre-fix files and covers any future atom, rather than
  spot-checking today's six.

Verified through the workflow shape, not the CLI shape: `3 --prd spec.md`
now yields ["prd-express-gate"] (was []), `2 --wave 2` yields
["partial-wave"] (was []).

Refs #2993

* test(#2993): acknowledge the execute-phase ripple and regenerate install-tree fixtures

Remote matrix was red with 46 unique failures, identical on both lanes.
Both causes are mechanical consequences of changing shipped workflow
content, and neither is visible to any local gate.

- emitted-attribution: execute-phase.md grew 163 bytes from the WAVE_PARAM
  forwarding fix and was unacknowledged, while the ack fragment named
  plan-phase.md, which SHRANK and therefore needed no ack at all — a stale
  entry is itself a failure. The reason now names the real ripple.
  The entry had to merge into the existing 2930 fragment: the ack linter
  does unconditional cross-fragment duplicate-key detection with no
  spent/live exception, so a second fragment declaring execute-phase.md
  collides even when the first is already merged and inert. Resolved per
  the linter's own guidance and that file's precedent of appending
  successive ripple reasons to one entry.
- golden-install-tree: tests/fixtures/install-tree/*.json are committed and
  deliberately excluded from the ADR-2719 attribution cutover, so they must
  be regenerated when shipped tree content changes. Regenerated after
  build:lib per the ordering landmine. 19 runtimes each gained exactly the
  six new plan-phase step files; zero paths removed, which is the absolute
  failure shape those fixtures exist to catch.

Refs #2993

* fix(#2993): restore the launcher preamble in an extracted step and follow moved content in its drift guards

Second red run: 26 unique failures, identical on both lanes, in two classes.

RUNTIME BUG (runtime-launcher-parity, 7 failures) — chunked-planning-mode.md
calls gsd_run but carried no canonical launcher preamble, which is what
DEFINES gsd_run(). On any non-Claude runtime that step would fail outright.
The preamble is now copied verbatim from the canonical source of truth,
gsd-core/workflows/_runtime-launcher.snippet.sh, and the fence dedented to
column 0 to match the prd-express-path.md sibling (a list-continuation
indent breaks the byte-equal preamble match). prd-express-path.md already
had a correct one. This is the same defect #2932 hit when it extracted
steps; the parity test caught a real bug, not a stale assertion.

DRIFT GUARDS (plan-phase-drift-guard, issue-2762-plan-reviews-chunked,
skill-frontmatter-contract) — these assert plan-phase.md contains content
this branch moved into step files. Retargeted at where the content now
lives, with the asserted property unchanged; the ALL-RUNTIMES label COUNT
test now reads host + every step file so the count is preserved across the
split rather than reduced. Each retargeted guard was verified to still fail
when its step file is stripped, so none was weakened into vacuity.

No emitted-drift ack was needed: currentSizes() enumerates
gsd-core/workflows/*.md non-recursively, so files under
plan-phase/steps/ are never in the size ratchet's scope.

Refs #2993

* chore(#2993): backfill changeset pr number to 3019

---------

Co-authored-by: sim <sim@local>
This commit is contained in:
Tom Boucher
2026-08-03 09:38:58 -04:00
committed by GitHub
parent 9640968f8e
commit ad3b9ec486
48 changed files with 1285 additions and 254 deletions

View File

@@ -0,0 +1,5 @@
---
type: Changed
pr: 3019
---
**Flag-gated workflow guidance is now actually loaded on demand** — `/gsd-plan-phase` reads its PRD-express, ADR-ingest, reviews-prerequisite, research-only and chunked-planning guidance only when the matching flag or config is active, instead of always inlining all six branches. This also repairs `/gsd-execute-phase --wave`, whose section gating never took effect because the workflow never forwarded the flag to the init bundle, so wave-filtering guidance was silently skipped on every run. (#2993)

View File

@@ -1,7 +1,7 @@
---
name: gsd:plan-phase
description: Create detailed phase plan (PLAN.md) with verification loop
argument-hint: "[phase] [--auto] [--research] [--skip-research] [--research-phase <N>] [--view] [--gaps] [--skip-verify] [--prd <file>] [--ingest <path-or-glob>] [--ingest-format <auto|nygard|madr|narrative>] [--reviews] [--text] [--tdd] [--mvp] [--no-tracer] [--no-reversibility-gates]"
argument-hint: "[phase] [--auto] [--research] [--skip-research] [--research-phase <N>] [--view] [--gaps] [--skip-verify] [--skip-ui] [--prd <file>] [--ingest <path-or-glob>] [--ingest-format <auto|nygard|madr|narrative>] [--reviews] [--text] [--bounce] [--skip-bounce] [--chunked] [--tdd] [--mvp] [--granularity <coarse|standard|fine>] [--no-tracer] [--no-reversibility-gates]"
effort: max
allowed-tools:
- Read

View File

@@ -115,6 +115,33 @@ Pure Agent Skills (A alone) and pure MCP (D alone) were rejected as the foundati
Playwright-MCP session), live git repository state, Capability-Registry/hook-resolved conditions,
interactive answers given mid-run, and UAT/verification runtime results. None is knowable from
parsed CLI arguments or `.planning/` state at init time.
**Amended by #2993 (Phase 6.2) — the vocabulary widens 14 → 19, second
coordinated amendment.** Rolling the fragment model onto `plan-phase.md` —
the largest workflow in the repo — surfaced 5 more atoms, gated by the same
two admission tests #2992 established: a named consuming section of at
least 400 bytes, and a fact the init seam demonstrably computes. **Shipped
(5).** `flag:--ingest`, `flag:--prd`, `flag:--research-phase`,
`flag:--reviews` (each a direct `parseNamedArgs` addition to the
`plan-phase` router handler; the generic flags-Set builder in `init.cts`
picks them up automatically), and `state:chunked-mode`.
`state:chunked-mode` is the one atom in this batch that is not a bare flag
check: `plan-phase.md`'s `CHUNKED_MODE` is true when EITHER `--chunked` is
passed OR `.planning/config.json`'s `workflow.plan_chunked` is set — a
disjunction of a flag and a config read. That disjunction is resolved to a
single boolean **in the fact**, computed once by the init seam
(`buildSectionManifestField` in `src/init.cts`) before `selectSections` is
ever called; `WHEN_PREDICATES['state:chunked-mode']` reads only
`facts.chunkedMode` and contains no `||`. The `when=` grammar therefore
still sees exactly one atom with no operator — the same invariant #2992
restated is unchanged by this amendment. This generalizes to a rule for
every future atom: **any condition that cannot be reduced to a single
boolean fact is not an atom** — it is either resolved upstream in fact
computation (as here) or it is not eligible for the grammar at all, per
the "Rejected" cases (`--auto`/`--chain`/persisted-config interleaving;
negated `--skip-bounce` OR `--gaps` OR NOT(...)) recorded in
`.gsd/phase/chore-2993-fragmentize-plan-phase/40-design.md`.
- **Budget unit:** bytes for emission caps (matches `lfByteCount`, deterministic, offline-safe); a token estimate for run-time selection.
**Corrected by #2931 (Phase 4) — the Windsurf cap was never load-bearing.** The Context

View File

@@ -46,8 +46,9 @@ gap fragment and composes back byte-identical to its source.
## The frozen `when=` vocabulary
`when=` takes exactly one of 14 atoms (widened from 4 via the ADR-1671
amendment for #2992, epic #1671 Phase 6.1):
`when=` takes exactly one of 19 atoms (widened from 4 to 14 via the ADR-1671
amendment for #2992, epic #1671 Phase 6.1, then from 14 to 19 via the
ADR-1671 amendment for #2993, epic #1671 Phase 6.2):
| Value | Meaning |
|---|---|
@@ -59,9 +60,14 @@ amendment for #2992, epic #1671 Phase 6.1):
| `flag:--discuss` | Applicable when the workflow runs with `--discuss`. |
| `flag:--forensic` | Applicable when the workflow runs with `--forensic`. |
| `flag:--full` | Applicable when the workflow runs with `--full`. |
| `flag:--ingest` | Applicable when the workflow runs with `--ingest <path-or-glob>`. |
| `flag:--prd` | Applicable when the workflow runs with `--prd <file>`. |
| `flag:--research` | Applicable when the workflow runs with `--research`. |
| `flag:--research-phase` | Applicable when the workflow runs with `--research-phase <N>`. A distinct atom from `flag:--research` above — neither aliases the other. |
| `flag:--reset-phase-numbers` | Applicable when the workflow runs with `--reset-phase-numbers`. |
| `flag:--reviews` | Applicable when the workflow runs with `--reviews`. |
| `flag:--validate` | Applicable when the workflow runs with `--validate`. |
| `state:chunked-mode` | Applicable when chunked planning mode is active — see [Compound conditions are resolved in the fact, never the grammar](#compound-conditions-are-resolved-in-the-fact-never-the-grammar) below. |
| `state:needs-codebase-map` | Applicable when a codebase map is needed (init-computed). |
| `state:phase-mvp-mode` | Applicable when the current phase's `ROADMAP.md` entry declares `**Mode:** mvp`. |
| `state:worktrees-enabled` | Applicable when `.planning/config.json`'s `workflow.use_worktrees` is enabled. |
@@ -99,6 +105,31 @@ points invoked by 20+ other workflows, so a dedicated `cmdInit*` seam does
not yet exist to compute their facts. They are withheld pending that seam,
not rejected.
### Compound conditions are resolved in the fact, never the grammar
`state:chunked-mode` looks, at the section-body level, like it should be a
compound condition: plan-phase's chunked planning mode activates on
`--chunked` **OR** `.planning/config.json`'s `workflow.plan_chunked` being
`true`. The vocabulary stays operator-free anyway, because the disjunction is
resolved **before** it ever reaches `when=` — the init seam
(`buildSectionManifestField` in `src/init.cts`) computes ONE boolean,
`InvocationFacts.chunkedMode = flags.has('--chunked') ||
readConfigJsonBoolean(cwd, ['workflow', 'plan_chunked'])`, and
`WHEN_PREDICATES['state:chunked-mode']` reads only that single field. The
marker grammar never sees `--chunked`, never sees the config key, and never
sees an `OR` — it sees exactly one atom with no operator, same as every other
entry in the frozen list.
This is the general rule for any future atom whose real-world trigger is
itself a compound expression: **compounding belongs in fact computation
(`src/init.cts`), never in the `when=` grammar (`src/workflow-fragments.cts` /
`src/section-manifest.cts`).** A condition that cannot be reduced to one
boolean fact computed ahead of evaluation is not eligible to become an atom —
widening the grammar itself to express `OR`/`AND`/negation is exactly the
Greenspun's Tenth Rule drift [The frozen `when=`
vocabulary](#the-frozen-when-vocabulary) above exists to prevent, regardless
of how reasonable a single compound condition looks in isolation.
## Fails closed
An authoring mistake throws at parse time, naming the source file and 1-based
@@ -192,7 +223,7 @@ At init time, a separate pure evaluator, `src/section-manifest.cts`
(`selectSections`), partitions a workflow's manifest sections into
`included`/`excluded` id lists against one invocation's
`InvocationFacts` — `{flags, phaseNumber, hasPriorPhases, needsCodebaseMap?,
phaseMvpMode?, worktreesEnabled?}`. Only a workflow with a **dedicated
phaseMvpMode?, worktreesEnabled?, chunkedMode?}`. Only a workflow with a **dedicated
`cmdInit*` entry point** in `src/init.cts` can have this evaluation run for
it, because only that entry point can assemble `InvocationFacts` from its own
parsed CLI options and `.planning/` state reads — this is admission gate 2
@@ -214,42 +245,51 @@ own `false` into `undefined` (`namedArgs['wave'] || undefined`) before
handing options to the facts builder, so `flags` only ever contains tokens
that were actually seen.
## Piloted on one workflow so far
## Piloted on execute-phase.md, then rolled out to plan-phase.md
Only `gsd-core/workflows/execute-phase.md` carries markers today. The marker
grammar and composer seam are general-purpose across any workflow file, but
rollout to other LARGE/XL workflows is intentionally sequenced as later work,
not part of this phase.
Two workflows carry markers today: `gsd-core/workflows/execute-phase.md` (the
#2930/Phase-3 pilot) and `gsd-core/workflows/plan-phase.md` (#2993, epic
#1671 Phase 6.2). The marker grammar and composer seam are general-purpose
across any workflow file; rollout to further LARGE/XL workflows remains
sequenced as later work.
The pilot marks three `<step>` blocks: `partial-wave` (`flag:--wave`),
`gap-closure-artifacts` (`state:gap-closure-phase`), and `regression-gate`
(`state:has-prior-phases`).
`execute-phase.md` marks three `<step>` blocks: `partial-wave`
(`flag:--wave`), `gap-closure-artifacts` (`state:gap-closure-phase`), and
`regression-gate` (`state:has-prior-phases`).
**The pilot was retargeted from `plan-phase.md` mid-phase.** Issue #2930's
own motivating mutually-exclusive branches (`--prd`, `--ingest`, `--mvp`,
`--reviews`) all live in `plan-phase.md`, not `execute-phase.md`. But
`plan-phase.md` sits only 36 B under an independent, pre-existing size gate
(`tests/phase6-capstone-conformance.test.cjs`'s `PRE_PHASE6`, an ADR-857
Phase-6 completion property) and cannot absorb any marker overhead at all —
so it could not be fragmentized under this phase's grammar regardless of
branch shape. This is direct evidence for the epic's premise that
fragmentization pays off, and it also means Phase 4 (moving size caps from
source bytes to emitted bytes) may need to land before `plan-phase.md`
itself can be fragmentized. Separately, and independent of the size-gate
finding, `--mvp` would remain unmarkable by this grammar even if the size
gate allowed it: its content in `plan-phase.md` is INTERLEAVED with other
flags rather than living in its own contiguous section (`MVP_MODE`
`plan-phase.md` marks six sections: `reviews-prerequisite` (`flag:--reviews`),
`prd-express-gate` (`flag:--prd`), `adr-ingest-express-path` (`flag:--ingest`),
`research-only-modifiers` and `research-only-early-exit` (both
`flag:--research-phase` — two consumers sharing one atom, gated by the same
`RESEARCH_ONLY` condition, so they include/exclude together), and
`chunked-planning-mode` (`state:chunked-mode`).
**`plan-phase.md` was originally retargeted away from the #2930 pilot,
then fragmentized here once the blocker cleared.** Issue #2930's own
motivating mutually-exclusive branches (`--prd`, `--ingest`, `--mvp`,
`--reviews`) all live in `plan-phase.md`, not `execute-phase.md`, but at the
time `plan-phase.md` sat only 36 B under an independent, pre-existing size
gate (`tests/phase6-capstone-conformance.test.cjs`'s `PRE_PHASE6`, an
ADR-857 Phase-6 completion property) and could not absorb any marker
overhead at all. #2993 resolves this **because fragmentizing is net-negative
on host source, not net-positive**: each gated body moves from always-inline
prose to a `gsd-core/workflows/plan-phase/steps/<id>.md` step file, leaving
only a ~200 B conditional-read stub behind — the six extractions trim
`plan-phase.md` from 94,483 B to 87,575 B, moving the file from 36 B of
`PRE_PHASE6` headroom to roughly 7,000 B, well clear of the cap.
`--mvp` remains unmarkable by this grammar, unchanged by #2993 and by
deliberate ADR-1671 decision: its content in `plan-phase.md` is INTERLEAVED
with other flags rather than living in its own contiguous section (`MVP_MODE`
resolution shares a single bash block with `--tdd`, `--no-tracer`, and
`--no-reversibility-gates` handling at `plan-phase.md:125-158`, and
elsewhere it is inline `${MVP_MODE === 'true' ? ... }` template
interpolation embedded inside the planner prompt at `plan-phase.md:794-803`)
— the marker grammar is closed, non-nesting, and whole-line (see
[Marker syntax](#marker-syntax) above), with no way to wrap part of a line
or split a shared conditional block without either corrupting the
`--no-reversibility-gates` handling, and elsewhere it is inline
`${MVP_MODE === 'true' ? ... }` template interpolation embedded inside the
planner prompt) — the marker grammar is closed, non-nesting, and whole-line
(see [Marker syntax](#marker-syntax) above), with no way to wrap part of a
line or split a shared conditional block without either corrupting the
conditional or bundling unrelated flags into one section. See
[ADR-1671](../adr/1671-dynamic-context-management-platform.md) open
question 1's resolution for the full record, and Phase 6 (LARGE/XL rollout)
for how both limits get addressed.
question 1's resolution for the full record.
## Related

View File

@@ -81,7 +81,8 @@ Load all context in one call:
```bash
_GSD_SHIM_NAME="gsd-tools.cjs"; _GSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; GSD_TOOLS="${_GSD_RUNTIME_ROOT}/gsd-core/bin/${_GSD_SHIM_NAME}"; if [ -f "$GSD_TOOLS" ]; then gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif command -v gsd-tools >/dev/null 2>&1; then GSD_TOOLS="$(command -v gsd-tools)"; gsd_run() { "$GSD_TOOLS" "$@"; }; elif [ -f "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${HERMES_HOME:-$HOME/.hermes}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${HERMES_HOME:-$HOME/.hermes}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CURSOR_CONFIG_DIR:-$HOME/.cursor}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CURSOR_CONFIG_DIR:-$HOME/.cursor}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${GEMINI_CONFIG_DIR:-$HOME/.gemini}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${GEMINI_CONFIG_DIR:-$HOME/.gemini}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${COPILOT_CONFIG_DIR:-$HOME/.copilot}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${COPILOT_CONFIG_DIR:-$HOME/.copilot}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${AUGMENT_CONFIG_DIR:-$HOME/.augment}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${AUGMENT_CONFIG_DIR:-$HOME/.augment}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${TRAE_CONFIG_DIR:-$HOME/.trae}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${TRAE_CONFIG_DIR:-$HOME/.trae}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${QWEN_CONFIG_DIR:-$HOME/.qwen}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${QWEN_CONFIG_DIR:-$HOME/.qwen}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CLINE_CONFIG_DIR:-$HOME/.cline}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CLINE_CONFIG_DIR:-$HOME/.cline}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${GROK_AGENTS_HOME:-$HOME/.agents}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${GROK_AGENTS_HOME:-$HOME/.agents}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; else echo "ERROR: gsd-tools.cjs not found at $GSD_TOOLS and gsd-tools is not on PATH. Run: npx -y @opengsd/gsd-core@latest --claude --local" >&2; exit 1; fi; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${GSD_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${GSD_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi
INIT=$(gsd_run query init.execute-phase "${PHASE_ARG}")
WAVE_PARAM=""; if [[ "$ARGUMENTS" =~ (^|[[:space:]])--wave[[:space:]]+([^[:space:]-][^[:space:]]*) ]]; then WAVE_PARAM="--wave ${BASH_REMATCH[2]}"; fi
INIT=$(gsd_run query init.execute-phase "${PHASE_ARG}" $WAVE_PARAM)
if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
AGENT_SKILLS=$(gsd_run query agent-skills gsd-executor)
```

View File

@@ -105,7 +105,7 @@ Usage: `/gsd:discuss-phase 2`
Usage: `/gsd:discuss-phase 2 --batch`
Usage: `/gsd:discuss-phase 2 --batch=3`
**`/gsd:plan-phase <number> [--research] [--skip-research] [--research-phase <N>] [--view] [--gaps] [--skip-verify] [--prd <file>] [--ingest <path-or-glob>] [--ingest-format <auto|nygard|madr|narrative>] [--reviews] [--text] [--tdd] [--mvp] [--no-tracer] [--no-reversibility-gates]`**
**`/gsd:plan-phase <number> [--research] [--skip-research] [--research-phase <N>] [--view] [--gaps] [--skip-verify] [--skip-ui] [--prd <file>] [--ingest <path-or-glob>] [--ingest-format <auto|nygard|madr|narrative>] [--reviews] [--text] [--bounce] [--skip-bounce] [--chunked] [--tdd] [--mvp] [--granularity <coarse|standard|fine>] [--no-tracer] [--no-reversibility-gates]`**
Create detailed execution plan for a specific phase.
- `--skip-research` — bypass the research subagent
@@ -113,10 +113,15 @@ Create detailed execution plan for a specific phase.
- Modifiers: `--research` forces refresh (re-spawn researcher). `--view` prints existing `RESEARCH.md` to stdout without spawning. With neither, auto-uses an existing `RESEARCH.md` (one-line notice, then clean exit).
- `--gaps` — focus only on closing gaps from a prior plan-check
- `--skip-verify` — skip the post-plan verifier loop
- `--skip-ui` — skip the UI-SPEC gate for a detected frontend phase (not recommended for frontend phases)
- `--ingest <path-or-glob>` — pre-ingest external ADRs/PRDs/SPECs before planning (see *PRD Express Path* below)
- `--ingest-format <auto|nygard|madr|narrative>` — hint the ADR ingester's parser when `--ingest` is set; defaults to `auto`
- `--bounce` — run the optional external plan-refinement pass (or set `workflow.plan_bounce: true` to activate by default); requires `workflow.plan_bounce_script`
- `--skip-bounce` — disable the plan-refinement pass even when `workflow.plan_bounce` config enables it
- `--chunked` — split the planner run into a short outline pass plus one short per-plan pass each (~3–5 min), committing each plan individually for crash resilience; re-running `--chunked` resumes from the last committed plan (or set `workflow.plan_chunked: true` to activate by default)
- `--tdd` — plan in test-driven order (tests before code)
- `--mvp` — MVP enrichment (user story + Walking Skeleton) on top of the default tracer-first ordering (see also `/gsd:mvp-phase`)
- `--granularity <coarse|standard|fine>` — override the resolved plan granularity for this run (wins over per-phase/top-level config and project defaults)
- `--no-tracer` — opt out of the default tracer-first slice and plan horizontal layers (legacy default)
- `--no-reversibility-gates` — suppress the `checkpoint:decision` a `one-way`-door decision normally earns, for intentionally-unattended runs (ratings are still recorded)

View File

@@ -68,7 +68,12 @@ Load all context in one call (paths only to minimize orchestrator context):
```bash
_GSD_SHIM_NAME="gsd-tools.cjs"; _GSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; GSD_TOOLS="${_GSD_RUNTIME_ROOT}/gsd-core/bin/${_GSD_SHIM_NAME}"; if [ -f "$GSD_TOOLS" ]; then gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif command -v gsd-tools >/dev/null 2>&1; then GSD_TOOLS="$(command -v gsd-tools)"; gsd_run() { "$GSD_TOOLS" "$@"; }; elif [ -f "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${HERMES_HOME:-$HOME/.hermes}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${HERMES_HOME:-$HOME/.hermes}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CURSOR_CONFIG_DIR:-$HOME/.cursor}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CURSOR_CONFIG_DIR:-$HOME/.cursor}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${GEMINI_CONFIG_DIR:-$HOME/.gemini}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${GEMINI_CONFIG_DIR:-$HOME/.gemini}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${COPILOT_CONFIG_DIR:-$HOME/.copilot}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${COPILOT_CONFIG_DIR:-$HOME/.copilot}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${AUGMENT_CONFIG_DIR:-$HOME/.augment}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${AUGMENT_CONFIG_DIR:-$HOME/.augment}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${TRAE_CONFIG_DIR:-$HOME/.trae}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${TRAE_CONFIG_DIR:-$HOME/.trae}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${QWEN_CONFIG_DIR:-$HOME/.qwen}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${QWEN_CONFIG_DIR:-$HOME/.qwen}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CLINE_CONFIG_DIR:-$HOME/.cline}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CLINE_CONFIG_DIR:-$HOME/.cline}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${GROK_AGENTS_HOME:-$HOME/.agents}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${GROK_AGENTS_HOME:-$HOME/.agents}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; else echo "ERROR: gsd-tools.cjs not found at $GSD_TOOLS and gsd-tools is not on PATH. Run: npx -y @opengsd/gsd-core@latest --claude --local" >&2; exit 1; fi; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${GSD_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${GSD_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi
GRAN_PARAM=""; if [[ "$ARGUMENTS" =~ (^|[[:space:]])--granularity[[:space:]]+([^[:space:]-][^[:space:]]*) ]]; then GRAN_PARAM="--granularity ${BASH_REMATCH[2]}"; fi
INIT=$(gsd_run query init.plan-phase "$PHASE" $GRAN_PARAM)
PRD_PARAM=""; if [[ "$ARGUMENTS" =~ (^|[[:space:]])--prd[[:space:]]+([^[:space:]-][^[:space:]]*) ]]; then PRD_PARAM="--prd ${BASH_REMATCH[2]}"; fi
INGEST_PARAM=""; if [[ "$ARGUMENTS" =~ (^|[[:space:]])--ingest[[:space:]]+([^[:space:]-][^[:space:]]*) ]]; then INGEST_PARAM="--ingest ${BASH_REMATCH[2]}"; fi
RESEARCH_PHASE_PARAM=""; if [[ "$ARGUMENTS" =~ (^|[[:space:]])--research-phase[[:space:]]+([^[:space:]-][^[:space:]]*) ]]; then RESEARCH_PHASE_PARAM="--research-phase ${BASH_REMATCH[2]}"; fi
REVIEWS_PARAM=""; if [[ "$ARGUMENTS" =~ (^|[[:space:]])--reviews([[:space:]]|$) ]]; then REVIEWS_PARAM="--reviews"; fi
CHUNKED_PARAM=""; if [[ "$ARGUMENTS" =~ (^|[[:space:]])--chunked([[:space:]]|$) ]]; then CHUNKED_PARAM="--chunked"; fi
INIT=$(gsd_run query init.plan-phase "$PHASE" $GRAN_PARAM $PRD_PARAM $INGEST_PARAM $RESEARCH_PHASE_PARAM $REVIEWS_PARAM $CHUNKED_PARAM)
if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
AGENT_SKILLS_RESEARCHER=$(gsd_run query agent-skills gsd-phase-researcher)
AGENT_SKILLS_PLANNER=$(gsd_run query agent-skills gsd-planner)
@@ -182,23 +187,9 @@ if [[ "$ARGUMENTS" =~ --chunked ]] || [[ "$CHUNKED_CFG" == "true" ]]; then
fi
```
## 2.5. Validate `--reviews` Prerequisite
**Skip if:** No `--reviews` flag.
**If `--reviews` AND `--gaps`:** Error — cannot combine `--reviews` with `--gaps`. These are conflicting modes.
**If `--reviews` AND `has_reviews` is false (no REVIEWS.md in phase dir):**
Error:
```
No REVIEWS.md found for Phase {N}. Run reviews first:
/gsd:review --phase {N}
Then re-run /gsd:plan-phase {N} --reviews
```
Exit workflow.
<!-- gsd:section id="reviews-prerequisite" when="flag:--reviews" -->
If `section_manifest` is `null` or `"reviews-prerequisite"` is in its `included` list: read and execute `gsd-core/workflows/plan-phase/steps/reviews-prerequisite.md`. Otherwise skip — do not read the file.
<!-- /gsd:section -->
## 3. Validate Phase
@@ -213,28 +204,13 @@ Now that `PHASE` is finalized, resolve MVP mode:
MVP_MODE=$(gsd_run query phase.mvp-mode "${PHASE}" $MVP_FLAG_ARG --pick active)
```
## 3.5. Handle PRD Express Path
<!-- gsd:section id="prd-express-gate" when="flag:--prd" -->
If `section_manifest` is `null` or `"prd-express-gate"` is in its `included` list: read and execute `gsd-core/workflows/plan-phase/steps/prd-express-gate.md`. Otherwise skip — do not read the file.
<!-- /gsd:section -->
**Skip if:** No `--prd` flag in arguments.
**If `--prd <filepath>` provided:**
Read and execute `gsd-core/workflows/plan-phase/steps/prd-express-path.md` — it reads the PRD (`$PRD_FILE`), generates `CONTEXT.md` (every PRD requirement/story/criterion → locked decision, uncovered areas → "Claude's Discretion", canonical refs extracted from ROADMAP.md + PRD-referenced specs), commits it, sets `context_content`, and bypasses step 4 (Load CONTEXT.md). The rest of the workflow proceeds normally with the PRD-derived context.
## 3.6. Handle ADR Ingest Express Path
**Skip if:** No `--ingest` flag in arguments.
**If `--ingest <path-or-glob>` provided:**
1. Display banner: `GSD ► ADR Ingest Express Path` with `{INGEST_PATH}` and `{INGEST_FORMAT}`.
2. Parse each resolved ADR through `gsd-core/bin/lib/adr-parser.cjs` (`--input`, `--format`) and collect normalized records.
3. Status gate: reject `superseded`/`rejected`/`deprecated`; warn on `proposed`; missing status defaults to `accepted`.
4. Empty-decisions fallback: if all parsed ADRs have zero `decisions[]`, emit `ADR ingest produced no locked decisions; fall back to discuss-phase for this phase.` and exit with `/gsd:discuss-phase {N}` guidance.
5. Generate CONTEXT.md using `<domain>`, `<decisions>`, `<canonical_refs>`, `<specifics>`, `<deferred>`, `<scope_fence>`, map `consequences_positive[]` to Success Criteria and `consequences_negative[]` to Risk Summary, and include `**Source:** ADR Ingest Express Path ({INGEST_PATH})`.
6. Commit with `gsd-tools.cjs query commit "docs(${padded_phase}): generate context from ADR ingest" --files "${phase_dir}/${padded_phase}-CONTEXT.md"` and set `context_content`; continue to step 5.
**Effect:** This bypasses step 4 (Load CONTEXT.md) since CONTEXT.md was synthesized from ADR input.
<!-- gsd:section id="adr-ingest-express-path" when="flag:--ingest" -->
If `section_manifest` is `null` or `"adr-ingest-express-path"` is in its `included` list: read and execute `gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md`. Otherwise skip — do not read the file.
<!-- /gsd:section -->
## 4. Load CONTEXT.md
@@ -305,22 +281,9 @@ If `AI_SPEC_FILE` is non-empty, pass `AI_SPEC_PATH` and `FRAMEWORK_LINE` to the
**Skip if:** `--gaps` flag or `--skip-research` flag or `--reviews` flag.
### 5.0. Research-Only Modifiers (`--view`, `--research`)
**Skip if:** `RESEARCH_ONLY` is `false`.
Three branches in research-only mode (`--research-phase <N>`):
1. **`--view`**: print `RESEARCH.md` to stdout, no spawn, exit. If `RESEARCH.md` is missing, error with: `--view requires an existing RESEARCH.md; drop --view to spawn the researcher.`
2. **`--research`** (force-refresh): re-spawn researcher unconditionally — fall through to "Spawn gsd-phase-researcher" below.
3. **Neither flag AND `has_research=true`:** auto-use the existing research and exit cleanly — do not prompt, do not re-spawn. Emit `RESEARCH.md already exists for Phase ${PHASE}, using it. To force-refresh, re-invoke with --research; to print, re-invoke with --view. Path: ${research_path}` then exit. The explicit-flag escape hatches cover any deviation; this matches §5.1's promptless auto-use of existing research, removing the §5.0/§5.1 inconsistency (#159).
```bash
if [[ "$VIEW_ONLY" == "true" ]]; then
[[ -f "$research_path" ]] || { echo "Error: --view requires an existing RESEARCH.md (Phase ${PHASE}). Drop --view to spawn the researcher."; exit 1; }
cat "$research_path"; exit 0
fi
```
<!-- gsd:section id="research-only-modifiers" when="flag:--research-phase" -->
If `section_manifest` is `null` or `"research-only-modifiers"` is in its `included` list: read and execute `gsd-core/workflows/plan-phase/steps/research-only-modifiers.md`. Otherwise skip — do not read the file.
<!-- /gsd:section -->
### 5.1. Standard Research Decision
@@ -408,23 +371,9 @@ Agent(
- **`## RESEARCH COMPLETE`:** Display confirmation, continue to step 6
- **`## RESEARCH BLOCKED`:** Display blocker, offer: 1) Provide context, 2) Skip research, 3) Abort
### Research-Only Early Exit (`--research-phase`)
**Skip if:** `RESEARCH_ONLY` is `false` (the default).
**If `RESEARCH_ONLY=true`:** the user invoked `/gsd:plan-phase --research-phase <N>` for research-only mode. Do **not** continue to Section 5.5+ (validation strategy, planner, plan-checker, verification, gaps, bounce, post-planning-gaps). Print the research-complete summary and exit cleanly:
```text
✓ Research-only mode complete (#3042)
Phase: ${PHASE}
RESEARCH.md: ${research_path}
Re-run /gsd:plan-phase ${PHASE} to plan the phase using this research,
or /gsd:plan-phase ${PHASE} --research to refresh research and plan.
```
This exits the workflow. The planner / plan-checker / verifier blocks below are skipped.
<!-- gsd:section id="research-only-early-exit" when="flag:--research-phase" -->
If `section_manifest` is `null` or `"research-only-early-exit"` is in its `included` list: read and execute `gsd-core/workflows/plan-phase/steps/research-only-early-exit.md`. Otherwise skip — do not read the file.
<!-- /gsd:section -->
## 5.5. Create Validation Strategy
@@ -889,112 +838,9 @@ Agent(
**If `CHUNKED_MODE` is `true`:** Skip the Agent() call above — proceed to step 8.5 instead.
## 8.5. Chunked Planning Mode
**Skip if `CHUNKED_MODE` is `false`.**
Chunked mode splits the single planner run into a short outline run + N short per-plan
runs (~3–5 min each), committing each plan individually for crash resilience. Rerunning
`/gsd:plan-phase {N} --chunked` resumes from the last committed plan.
For recovering plans from a prior *non-chunked* run, use step 6's "Add more plans" or
proceed to `/gsd:execute-phase` — don't start a fresh chunked run over them.
### 8.5.1 Outline Phase (outline-only mode, ~2 min)
**Resume detection:** If `${PHASE_DIR}/${PADDED_PHASE}-PLAN-OUTLINE.md` exists and contains
the `## OUTLINE COMPLETE` marker (written by the outline agent — #2762), skip to 8.5.2.
```bash
OUTLINE_FILE="${PHASE_DIR}/${PADDED_PHASE}-PLAN-OUTLINE.md"
if [[ -f "$OUTLINE_FILE" ]] && grep -q "^## OUTLINE COMPLETE" "$OUTLINE_FILE"; then
# reuse existing outline — skip to 8.5.2
fi
```
Display:
```text
◆ Chunked mode: spawning outline planner... (runs in a subagent — no output until it returns, ~1–5 min; expected, not a freeze)
```
Spawn the planner in **outline-only** mode — it must write only the outline manifest, not any
PLAN.md files:
```javascript
Agent(
prompt="{same planning_context as step 8, plus:}
**Chunked mode: outline-only.**
Do NOT write any PLAN.md files in this Task.
Write only: {PHASE_DIR}/{PADDED_PHASE}-PLAN-OUTLINE.md
The outline must be a markdown table with columns:
Plan ID | Objective | Wave | Depends On | Requirements
End the file with a final line `## OUTLINE COMPLETE` — §8.5.1's resume-check greps
the file for it, so it MUST be written here, not just returned.
Return: ## OUTLINE COMPLETE with plan count.",
subagent_type="gsd-planner",
model="{planner_model}",
description="Outline Phase {phase} (chunked)"
)
```
> **ORCHESTRATOR RULE — ALL RUNTIMES**: After calling Agent() above, stop working on this task immediately. Do not read more files, edit code, or run tests related to this task while the subagent is active. Wait for the subagent to return its result. This prevents duplicate work, conflicting edits, and wasted context. Only resume when the subagent result is available.
Handle return:
- **`## OUTLINE COMPLETE`:** Read `PLAN-OUTLINE.md`, extract plan list. Continue to 8.5.2.
- **Any other return or empty:** Display error. Offer: 1) Retry outline, 2) Stop.
### 8.5.2 Per-Plan Tasks (single-plan mode, ~3-5 min each)
For each plan entry extracted from `PLAN-OUTLINE.md`:
1. **Resume check:** Skip if `${PHASE_DIR}/{plan_id}-PLAN.md` exists with valid frontmatter
(resume safety) — UNLESS `--reviews` is set, whose purpose is to REPLAN with review
feedback (§6), so existing plans are overwritten, not skipped (#2762).
```bash
PLAN_FILE="${PHASE_DIR}/${plan_id}-PLAN.md"
if [[ -f "$PLAN_FILE" ]] && head -1 "$PLAN_FILE" | grep -q '^---' && [[ "$ARGUMENTS" != *"--reviews"* ]]; then
continue # resume safety — NOT under --reviews (replan)
fi
```
2. Display:
```text
◆ Chunked mode: planning {plan_id} ({k}/{N})... (runs in a subagent — no output until it returns, ~1–5 min; expected, not a freeze)
```
3. Spawn the planner in **single-plan** mode — it must write exactly one PLAN.md file:
```javascript
Agent(
prompt="{same planning_context as step 8, plus:}
**Chunked mode: single-plan.**
Write exactly ONE plan file: {PHASE_DIR}/{plan_id}-PLAN.md
Plan to write: {plan_id} — {objective}
Wave: {wave} | Depends on: {depends_on}
Phase requirement IDs to cover in this plan: {plan_requirements}
Return: ## PLAN COMPLETE with the plan ID.",
subagent_type="gsd-planner",
model="{planner_model}",
description="Plan {plan_id} (chunked {k}/{N})"
)
```
> **ORCHESTRATOR RULE — ALL RUNTIMES**: After calling Agent() above, stop working on this task immediately. Do not read more files, edit code, or run tests related to this task while the subagent is active. Wait for the subagent to return its result. This prevents duplicate work, conflicting edits, and wasted context. Only resume when the subagent result is available.
4. **Verify disk:** Check `${PHASE_DIR}/{plan_id}-PLAN.md` exists. If missing: offer 1) Retry, 2) Stop.
5. **Commit per-plan:**
```bash
gsd_run query commit "docs(${PADDED_PHASE}): plan ${plan_id} (chunked)" --files "${PHASE_DIR}/${plan_id}-PLAN.md"
```
After all N plans are written and committed, treat this as `## PLANNING COMPLETE` and continue
to step 9.
<!-- gsd:section id="chunked-planning-mode" when="state:chunked-mode" -->
If `section_manifest` is `null` or `"chunked-planning-mode"` is in its `included` list: read and execute `gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md`. Otherwise skip — do not read the file.
<!-- /gsd:section -->
## 9. Handle Planner Return

View File

@@ -0,0 +1,15 @@
## 3.6. Handle ADR Ingest Express Path
**Skip if:** No `--ingest` flag in arguments.
**If `--ingest <path-or-glob>` provided:**
1. Display banner: `GSD ► ADR Ingest Express Path` with `{INGEST_PATH}` and `{INGEST_FORMAT}`.
2. Parse each resolved ADR through `gsd-core/bin/lib/adr-parser.cjs` (`--input`, `--format`) and collect normalized records.
3. Status gate: reject `superseded`/`rejected`/`deprecated`; warn on `proposed`; missing status defaults to `accepted`.
4. Empty-decisions fallback: if all parsed ADRs have zero `decisions[]`, emit `ADR ingest produced no locked decisions; fall back to discuss-phase for this phase.` and exit with `/gsd:discuss-phase {N}` guidance.
5. Generate CONTEXT.md using `<domain>`, `<decisions>`, `<canonical_refs>`, `<specifics>`, `<deferred>`, `<scope_fence>`, map `consequences_positive[]` to Success Criteria and `consequences_negative[]` to Risk Summary, and include `**Source:** ADR Ingest Express Path ({INGEST_PATH})`.
6. Commit with `gsd-tools.cjs query commit "docs(${padded_phase}): generate context from ADR ingest" --files "${phase_dir}/${padded_phase}-CONTEXT.md"` and set `context_content`; continue to step 5.
**Effect:** This bypasses step 4 (Load CONTEXT.md) since CONTEXT.md was synthesized from ADR input.

View File

@@ -0,0 +1,108 @@
## 8.5. Chunked Planning Mode
**Skip if `CHUNKED_MODE` is `false`.**
Chunked mode splits the single planner run into a short outline run + N short per-plan
runs (~3–5 min each), committing each plan individually for crash resilience. Rerunning
`/gsd:plan-phase {N} --chunked` resumes from the last committed plan.
For recovering plans from a prior *non-chunked* run, use step 6's "Add more plans" or
proceed to `/gsd:execute-phase` — don't start a fresh chunked run over them.
### 8.5.1 Outline Phase (outline-only mode, ~2 min)
**Resume detection:** If `${PHASE_DIR}/${PADDED_PHASE}-PLAN-OUTLINE.md` exists and contains
the `## OUTLINE COMPLETE` marker (written by the outline agent — #2762), skip to 8.5.2.
```bash
OUTLINE_FILE="${PHASE_DIR}/${PADDED_PHASE}-PLAN-OUTLINE.md"
if [[ -f "$OUTLINE_FILE" ]] && grep -q "^## OUTLINE COMPLETE" "$OUTLINE_FILE"; then
# reuse existing outline — skip to 8.5.2
fi
```
Display:
```text
◆ Chunked mode: spawning outline planner... (runs in a subagent — no output until it returns, ~1–5 min; expected, not a freeze)
```
Spawn the planner in **outline-only** mode — it must write only the outline manifest, not any
PLAN.md files:
```javascript
Agent(
prompt="{same planning_context as step 8, plus:}
**Chunked mode: outline-only.**
Do NOT write any PLAN.md files in this Task.
Write only: {PHASE_DIR}/{PADDED_PHASE}-PLAN-OUTLINE.md
The outline must be a markdown table with columns:
Plan ID | Objective | Wave | Depends On | Requirements
End the file with a final line `## OUTLINE COMPLETE` — §8.5.1's resume-check greps
the file for it, so it MUST be written here, not just returned.
Return: ## OUTLINE COMPLETE with plan count.",
subagent_type="gsd-planner",
model="{planner_model}",
description="Outline Phase {phase} (chunked)"
)
```
> **ORCHESTRATOR RULE — ALL RUNTIMES**: After calling Agent() above, stop working on this task immediately. Do not read more files, edit code, or run tests related to this task while the subagent is active. Wait for the subagent to return its result. This prevents duplicate work, conflicting edits, and wasted context. Only resume when the subagent result is available.
Handle return:
- **`## OUTLINE COMPLETE`:** Read `PLAN-OUTLINE.md`, extract plan list. Continue to 8.5.2.
- **Any other return or empty:** Display error. Offer: 1) Retry outline, 2) Stop.
### 8.5.2 Per-Plan Tasks (single-plan mode, ~3-5 min each)
For each plan entry extracted from `PLAN-OUTLINE.md`:
1. **Resume check:** Skip if `${PHASE_DIR}/{plan_id}-PLAN.md` exists with valid frontmatter
(resume safety) — UNLESS `--reviews` is set, whose purpose is to REPLAN with review
feedback (§6), so existing plans are overwritten, not skipped (#2762).
```bash
PLAN_FILE="${PHASE_DIR}/${plan_id}-PLAN.md"
if [[ -f "$PLAN_FILE" ]] && head -1 "$PLAN_FILE" | grep -q '^---' && [[ "$ARGUMENTS" != *"--reviews"* ]]; then
continue # resume safety — NOT under --reviews (replan)
fi
```
2. Display:
```text
◆ Chunked mode: planning {plan_id} ({k}/{N})... (runs in a subagent — no output until it returns, ~1–5 min; expected, not a freeze)
```
3. Spawn the planner in **single-plan** mode — it must write exactly one PLAN.md file:
```javascript
Agent(
prompt="{same planning_context as step 8, plus:}
**Chunked mode: single-plan.**
Write exactly ONE plan file: {PHASE_DIR}/{plan_id}-PLAN.md
Plan to write: {plan_id} — {objective}
Wave: {wave} | Depends on: {depends_on}
Phase requirement IDs to cover in this plan: {plan_requirements}
Return: ## PLAN COMPLETE with the plan ID.",
subagent_type="gsd-planner",
model="{planner_model}",
description="Plan {plan_id} (chunked {k}/{N})"
)
```
> **ORCHESTRATOR RULE — ALL RUNTIMES**: After calling Agent() above, stop working on this task immediately. Do not read more files, edit code, or run tests related to this task while the subagent is active. Wait for the subagent to return its result. This prevents duplicate work, conflicting edits, and wasted context. Only resume when the subagent result is available.
4. **Verify disk:** Check `${PHASE_DIR}/{plan_id}-PLAN.md` exists. If missing: offer 1) Retry, 2) Stop.
5. **Commit per-plan:**
```bash
_GSD_SHIM_NAME="gsd-tools.cjs"; _GSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; GSD_TOOLS="${_GSD_RUNTIME_ROOT}/gsd-core/bin/${_GSD_SHIM_NAME}"; if [ -f "$GSD_TOOLS" ]; then gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif command -v gsd-tools >/dev/null 2>&1; then GSD_TOOLS="$(command -v gsd-tools)"; gsd_run() { "$GSD_TOOLS" "$@"; }; elif [ -f "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${HERMES_HOME:-$HOME/.hermes}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${HERMES_HOME:-$HOME/.hermes}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CURSOR_CONFIG_DIR:-$HOME/.cursor}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CURSOR_CONFIG_DIR:-$HOME/.cursor}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${GEMINI_CONFIG_DIR:-$HOME/.gemini}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${GEMINI_CONFIG_DIR:-$HOME/.gemini}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${COPILOT_CONFIG_DIR:-$HOME/.copilot}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${COPILOT_CONFIG_DIR:-$HOME/.copilot}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${AUGMENT_CONFIG_DIR:-$HOME/.augment}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${AUGMENT_CONFIG_DIR:-$HOME/.augment}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${TRAE_CONFIG_DIR:-$HOME/.trae}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${TRAE_CONFIG_DIR:-$HOME/.trae}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${QWEN_CONFIG_DIR:-$HOME/.qwen}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${QWEN_CONFIG_DIR:-$HOME/.qwen}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CLINE_CONFIG_DIR:-$HOME/.cline}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CLINE_CONFIG_DIR:-$HOME/.cline}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${GROK_AGENTS_HOME:-$HOME/.agents}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${GROK_AGENTS_HOME:-$HOME/.agents}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; else echo "ERROR: gsd-tools.cjs not found at $GSD_TOOLS and gsd-tools is not on PATH. Run: npx -y @opengsd/gsd-core@latest --claude --local" >&2; exit 1; fi; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${GSD_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${GSD_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi
gsd_run query commit "docs(${PADDED_PHASE}): plan ${plan_id} (chunked)" --files "${PHASE_DIR}/${plan_id}-PLAN.md"
```
After all N plans are written and committed, treat this as `## PLANNING COMPLETE` and continue
to step 9.

View File

@@ -0,0 +1,8 @@
## 3.5. Handle PRD Express Path
**Skip if:** No `--prd` flag in arguments.
**If `--prd <filepath>` provided:**
Read and execute `gsd-core/workflows/plan-phase/steps/prd-express-path.md` — it reads the PRD (`$PRD_FILE`), generates `CONTEXT.md` (every PRD requirement/story/criterion → locked decision, uncovered areas → "Claude's Discretion", canonical refs extracted from ROADMAP.md + PRD-referenced specs), commits it, sets `context_content`, and bypasses step 4 (Load CONTEXT.md). The rest of the workflow proceeds normally with the PRD-derived context.

View File

@@ -0,0 +1,17 @@
### Research-Only Early Exit (`--research-phase`)
**Skip if:** `RESEARCH_ONLY` is `false` (the default).
**If `RESEARCH_ONLY=true`:** the user invoked `/gsd:plan-phase --research-phase <N>` for research-only mode. Do **not** continue to Section 5.5+ (validation strategy, planner, plan-checker, verification, gaps, bounce, post-planning-gaps). Print the research-complete summary and exit cleanly:
```text
✓ Research-only mode complete (#3042)
Phase: ${PHASE}
RESEARCH.md: ${research_path}
Re-run /gsd:plan-phase ${PHASE} to plan the phase using this research,
or /gsd:plan-phase ${PHASE} --research to refresh research and plan.
```
This exits the workflow. The planner / plan-checker / verifier blocks below are skipped.

View File

@@ -0,0 +1,16 @@
### 5.0. Research-Only Modifiers (`--view`, `--research`)
**Skip if:** `RESEARCH_ONLY` is `false`.
Three branches in research-only mode (`--research-phase <N>`):
1. **`--view`**: print `RESEARCH.md` to stdout, no spawn, exit. If `RESEARCH.md` is missing, error with: `--view requires an existing RESEARCH.md; drop --view to spawn the researcher.`
2. **`--research`** (force-refresh): re-spawn researcher unconditionally — fall through to "Spawn gsd-phase-researcher" below.
3. **Neither flag AND `has_research=true`:** auto-use the existing research and exit cleanly — do not prompt, do not re-spawn. Emit `RESEARCH.md already exists for Phase ${PHASE}, using it. To force-refresh, re-invoke with --research; to print, re-invoke with --view. Path: ${research_path}` then exit. The explicit-flag escape hatches cover any deviation; this matches §5.1's promptless auto-use of existing research, removing the §5.0/§5.1 inconsistency (#159).
```bash
if [[ "$VIEW_ONLY" == "true" ]]; then
[[ -f "$research_path" ]] || { echo "Error: --view requires an existing RESEARCH.md (Phase ${PHASE}). Drop --view to spawn the researcher."; exit 1; }
cat "$research_path"; exit 0
fi
```

View File

@@ -0,0 +1,17 @@
## 2.5. Validate `--reviews` Prerequisite
**Skip if:** No `--reviews` flag.
**If `--reviews` AND `--gaps`:** Error — cannot combine `--reviews` with `--gaps`. These are conflicting modes.
**If `--reviews` AND `has_reviews` is false (no REVIEWS.md in phase dir):**
Error:
```
No REVIEWS.md found for Phase {N}. Run reviews first:
/gsd:review --phase {N}
Then re-run /gsd:plan-phase {N} --reviews
```
Exit workflow.

View File

@@ -16,6 +16,38 @@
"when": "state:has-prior-phases",
"read": "gsd-core/workflows/execute-phase/steps/regression-gate.md"
}
],
"plan-phase": [
{
"id": "reviews-prerequisite",
"when": "flag:--reviews",
"read": "gsd-core/workflows/plan-phase/steps/reviews-prerequisite.md"
},
{
"id": "prd-express-gate",
"when": "flag:--prd",
"read": "gsd-core/workflows/plan-phase/steps/prd-express-gate.md"
},
{
"id": "adr-ingest-express-path",
"when": "flag:--ingest",
"read": "gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md"
},
{
"id": "research-only-modifiers",
"when": "flag:--research-phase",
"read": "gsd-core/workflows/plan-phase/steps/research-only-modifiers.md"
},
{
"id": "research-only-early-exit",
"when": "flag:--research-phase",
"read": "gsd-core/workflows/plan-phase/steps/research-only-early-exit.md"
},
{
"id": "chunked-planning-mode",
"when": "state:chunked-mode",
"read": "gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md"
}
]
}
}

View File

@@ -1,7 +1,7 @@
---
name: gsd-plan-phase
description: "Create detailed phase plan (PLAN.md) with verification loop"
argument-hint: "[phase] [--auto] [--research] [--skip-research] [--research-phase <N>] [--view] [--gaps] [--skip-verify] [--prd <file>] [--ingest <path-or-glob>] [--ingest-format <auto|nygard|madr|narrative>] [--reviews] [--text] [--tdd] [--mvp] [--no-tracer] [--no-reversibility-gates]"
argument-hint: "[phase] [--auto] [--research] [--skip-research] [--research-phase <N>] [--view] [--gaps] [--skip-verify] [--skip-ui] [--prd <file>] [--ingest <path-or-glob>] [--ingest-format <auto|nygard|madr|narrative>] [--reviews] [--text] [--bounce] [--skip-bounce] [--chunked] [--tdd] [--mvp] [--granularity <coarse|standard|fine>] [--no-tracer] [--no-reversibility-gates]"
effort: max
allowed-tools:
- Read

View File

@@ -76,11 +76,20 @@ function routeInitCommand({ init, args, cwd, raw, error }: RouteInitCommandOptio
});
},
'plan-phase': () => {
const namedArgs = parseNamedArgs(args, ['granularity'], ['validate', 'tdd']);
const namedArgs = parseNamedArgs(
args,
['granularity', 'prd', 'ingest', 'research-phase'],
['validate', 'tdd', 'reviews', 'chunked'],
);
init.cmdInitPlanPhase(cwd, args[2], raw, {
validate: namedArgs['validate'],
tdd: namedArgs['tdd'],
granularity: namedArgs['granularity'],
prd: namedArgs['prd'],
ingest: namedArgs['ingest'],
'research-phase': namedArgs['research-phase'],
reviews: namedArgs['reviews'],
chunked: namedArgs['chunked'],
});
},
'new-project': () => {

View File

@@ -557,6 +557,14 @@ function buildSectionManifestField(
flags.add(`--${key}`);
}
// `state:chunked-mode` (#2993) is a disjunction — `--chunked` flag OR
// `.planning/config.json` `workflow.plan_chunked` — resolved to ONE
// boolean HERE, in fact computation, never in the `when=` grammar itself
// (WHEN_PREDICATES['state:chunked-mode'] reads only `facts.chunkedMode`).
// That separation is what keeps ADR-1671:69's Greenspun guard intact: the
// grammar still sees exactly one atom with no operator.
const chunkedMode = flags.has('--chunked') || readConfigJsonBoolean(cwd, ['workflow', 'plan_chunked']);
const facts: sectionManifest.InvocationFacts = {
flags,
phaseNumber,
@@ -564,6 +572,7 @@ function buildSectionManifestField(
worktreesEnabled: readConfigJsonBoolean(cwd, ['workflow', 'use_worktrees']),
phaseMvpMode: detectPhaseMvpMode(cwd, phaseNumber),
needsCodebaseMap: overrides.needsCodebaseMap,
chunkedMode,
};
try {

View File

@@ -18,6 +18,8 @@
* binding on this implementation: `when=` is a closed vocabulary, widened
* from 4 to 14 entries via the ADR-1671 amendment for #2992 (epic #1671
* Phase 6.1; see `.gsd/phase/chore-2992-widen-when-vocabulary/
* 40-design.md`), then from 14 to 19 via the ADR-1671 amendment for #2993
* (epic #1671 Phase 6.2; see `.gsd/phase/chore-2993-fragmentize-plan-phase/
* 40-design.md`). {@link WHEN_PREDICATES} is a total map from each frozen
* vocabulary entry to exactly one predicate over {@link InvocationFacts}.
* It MUST NOT tokenize, split on operators, or interpret structure in the
@@ -80,6 +82,16 @@ export interface InvocationFacts {
readonly phaseMvpMode?: boolean;
/** Whether worktrees are enabled (`.planning/config.json` `workflow.use_worktrees`). Absent/undefined is falsy, never throws. */
readonly worktreesEnabled?: boolean;
/**
* Whether plan-phase chunked mode is active: `--chunked` flag OR
* `.planning/config.json` `workflow.plan_chunked` (#2993). The disjunction
* is resolved by the caller (the init seam) into this single boolean
* BEFORE it reaches this module — `state:chunked-mode`'s predicate below
* reads only this field, never `--chunked` and the config value
* separately, which is what keeps the `when=` grammar operator-free.
* Absent/undefined is falsy, never throws.
*/
readonly chunkedMode?: boolean;
}
/** A single input to {@link selectSections}: structurally compatible with {@link workflowFragments.WorkflowSection}. */
@@ -167,9 +179,14 @@ export const WHEN_PREDICATES: Readonly<Record<string, (facts: InvocationFacts) =
'flag:--discuss': (facts: InvocationFacts) => hasFlag(facts, '--discuss'),
'flag:--forensic': (facts: InvocationFacts) => hasFlag(facts, '--forensic'),
'flag:--full': (facts: InvocationFacts) => hasFlag(facts, '--full'),
'flag:--ingest': (facts: InvocationFacts) => hasFlag(facts, '--ingest'),
'flag:--prd': (facts: InvocationFacts) => hasFlag(facts, '--prd'),
'flag:--research': (facts: InvocationFacts) => hasFlag(facts, '--research'),
'flag:--research-phase': (facts: InvocationFacts) => hasFlag(facts, '--research-phase'),
'flag:--reset-phase-numbers': (facts: InvocationFacts) => hasFlag(facts, '--reset-phase-numbers'),
'flag:--reviews': (facts: InvocationFacts) => hasFlag(facts, '--reviews'),
'flag:--validate': (facts: InvocationFacts) => hasFlag(facts, '--validate'),
'state:chunked-mode': (facts: InvocationFacts) => facts.chunkedMode === true,
'state:needs-codebase-map': (facts: InvocationFacts) => facts.needsCodebaseMap === true,
'state:phase-mvp-mode': (facts: InvocationFacts) => facts.phaseMvpMode === true,
'state:worktrees-enabled': (facts: InvocationFacts) => facts.worktreesEnabled === true,

View File

@@ -93,8 +93,10 @@ import contextComposer = require('./context-composer.cjs');
*
* Widened from 4 to 14 entries via the ADR-1671 amendment for #2992 (epic
* #1671 Phase 6.1; see `.gsd/phase/chore-2992-widen-when-vocabulary/
* 40-design.md`), then from 14 to 19 via the ADR-1671 amendment for #2993
* (epic #1671 Phase 6.2; see `.gsd/phase/chore-2993-fragmentize-plan-phase/
* 40-design.md`). The vocabulary remains CLOSED: no operators, no negation,
* no nesting. Cardinality is not expressiveness — a 14-entry flat list with
* no nesting. Cardinality is not expressiveness — a 19-entry flat list with
* no composition is still not a language.
*
* Held at 14, not wider: an atom whose fact is never computed always
@@ -106,6 +108,13 @@ import contextComposer = require('./context-composer.cjs');
* (docs-update, autonomous, code-review, complete-milestone) have no
* dedicated `cmdInit*` entry point yet to compute the backing fact, so they
* are withheld until that entry point exists (#2992 / ADR-1671 Phase 6.1).
*
* The #2993 widening adds 5 entries fragmentizing `plan-phase.md`:
* `flag:--ingest`, `flag:--prd`, `flag:--research-phase`, `flag:--reviews`,
* `state:chunked-mode`. `state:chunked-mode` is a disjunction (`--chunked`
* flag OR `.planning/config.json` `workflow.plan_chunked`) resolved to a
* single boolean FACT by the init seam (`src/init.cts`) — the grammar still
* sees exactly one atom with no operator, preserving the same guard.
*/
export const WHEN_VOCABULARY: readonly string[] = Object.freeze([
'always',
@@ -116,9 +125,14 @@ export const WHEN_VOCABULARY: readonly string[] = Object.freeze([
'flag:--discuss',
'flag:--forensic',
'flag:--full',
'flag:--ingest',
'flag:--prd',
'flag:--research',
'flag:--research-phase',
'flag:--reset-phase-numbers',
'flag:--reviews',
'flag:--validate',
'state:chunked-mode',
'state:needs-codebase-map',
'state:phase-mvp-mode',
'state:worktrees-enabled',

View File

@@ -1,6 +1,6 @@
{
"version": 1,
"paths": {
"execute-phase.md": "#2930 (epic #1671 Phase 3): pilots the in-file `<!-- gsd:section -->` marker grammar by wrapping the --wave/gap-closure/regression-gate branch sections (partial-wave, gap-closure-artifacts, regression-gate) in marker pairs, proving the composeWorkflow seam runs at install time before per-runtime rewrites. Retargeted from plan-phase.md (chore/2930 review): plan-phase.md sits only 36 B under the ADR-857 Phase-6 PRE_PHASE6 gate (tests/phase6-capstone-conformance.test.cjs) and cannot absorb marker overhead, so the maintainer retargeted the pilot to execute-phase.md, which has 728 B of headroom under its own PRE_PHASE6 cap. SOURCE grows by exactly 275 marker bytes (6 marker lines); the EMITTED artifact composeWorkflow produces at install is byte-identical to the pre-#2930 file (markers are stripped, never shipped). See .gsd/phase/chore-2930-fragmentize-xl-workflow/40-design.md 'Known limits' item 5. #2639: handle_branching now warns when local is ahead of origin (+376 B condensed one-line WARNING + rev-list --count check)."
"execute-phase.md": "#2930 (epic #1671 Phase 3): pilots the in-file `<!-- gsd:section -->` marker grammar by wrapping the --wave/gap-closure/regression-gate branch sections (partial-wave, gap-closure-artifacts, regression-gate) in marker pairs, proving the composeWorkflow seam runs at install time before per-runtime rewrites. Retargeted from plan-phase.md (chore/2930 review): plan-phase.md sits only 36 B under the ADR-857 Phase-6 PRE_PHASE6 gate (tests/phase6-capstone-conformance.test.cjs) and cannot absorb marker overhead, so the maintainer retargeted the pilot to execute-phase.md, which has 728 B of headroom under its own PRE_PHASE6 cap. SOURCE grows by exactly 275 marker bytes (6 marker lines); the EMITTED artifact composeWorkflow produces at install is byte-identical to the pre-#2930 file (markers are stripped, never shipped). See .gsd/phase/chore-2930-fragmentize-xl-workflow/40-design.md 'Known limits' item 5. #2639: handle_branching now warns when local is ahead of origin (+376 B condensed one-line WARNING + rev-list --count check). #2993 (epic #1671 Phase 6.2): fixes the sibling gap #2932 shipped — `flag:--wave` section gating was added to the WHEN_VOCABULARY and to init.execute-phase's section manifest, but execute-phase.md never actually parsed `--wave` out of `$ARGUMENTS` or forwarded it on the `gsd_run query init.execute-phase` line, so the flag could never fire. This diff adds a WAVE_PARAM extraction (`--wave <value>` via BASH_REMATCH) and appends it to the init call, growing the file 163 bytes (89,507 -> 89,670)."
}
}

View File

@@ -291,8 +291,14 @@
"gsd-core/workflows/pause-work.md",
"gsd-core/workflows/plan-milestone-gaps.md",
"gsd-core/workflows/plan-phase.md",
"gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md",
"gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md",
"gsd-core/workflows/plan-phase/steps/closed-phase-gate.md",
"gsd-core/workflows/plan-phase/steps/prd-express-gate.md",
"gsd-core/workflows/plan-phase/steps/prd-express-path.md",
"gsd-core/workflows/plan-phase/steps/research-only-early-exit.md",
"gsd-core/workflows/plan-phase/steps/research-only-modifiers.md",
"gsd-core/workflows/plan-phase/steps/reviews-prerequisite.md",
"gsd-core/workflows/plan-phase/steps/windows-troubleshooting.md",
"gsd-core/workflows/plan-review-convergence.md",
"gsd-core/workflows/plant-seed.md",

View File

@@ -362,8 +362,14 @@
"gsd-core/workflows/pause-work.md",
"gsd-core/workflows/plan-milestone-gaps.md",
"gsd-core/workflows/plan-phase.md",
"gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md",
"gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md",
"gsd-core/workflows/plan-phase/steps/closed-phase-gate.md",
"gsd-core/workflows/plan-phase/steps/prd-express-gate.md",
"gsd-core/workflows/plan-phase/steps/prd-express-path.md",
"gsd-core/workflows/plan-phase/steps/research-only-early-exit.md",
"gsd-core/workflows/plan-phase/steps/research-only-modifiers.md",
"gsd-core/workflows/plan-phase/steps/reviews-prerequisite.md",
"gsd-core/workflows/plan-phase/steps/windows-troubleshooting.md",
"gsd-core/workflows/plan-review-convergence.md",
"gsd-core/workflows/plant-seed.md",

View File

@@ -361,8 +361,14 @@
"gsd-core/workflows/pause-work.md",
"gsd-core/workflows/plan-milestone-gaps.md",
"gsd-core/workflows/plan-phase.md",
"gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md",
"gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md",
"gsd-core/workflows/plan-phase/steps/closed-phase-gate.md",
"gsd-core/workflows/plan-phase/steps/prd-express-gate.md",
"gsd-core/workflows/plan-phase/steps/prd-express-path.md",
"gsd-core/workflows/plan-phase/steps/research-only-early-exit.md",
"gsd-core/workflows/plan-phase/steps/research-only-modifiers.md",
"gsd-core/workflows/plan-phase/steps/reviews-prerequisite.md",
"gsd-core/workflows/plan-phase/steps/windows-troubleshooting.md",
"gsd-core/workflows/plan-review-convergence.md",
"gsd-core/workflows/plant-seed.md",

View File

@@ -290,8 +290,14 @@
"gsd-core/workflows/pause-work.md",
"gsd-core/workflows/plan-milestone-gaps.md",
"gsd-core/workflows/plan-phase.md",
"gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md",
"gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md",
"gsd-core/workflows/plan-phase/steps/closed-phase-gate.md",
"gsd-core/workflows/plan-phase/steps/prd-express-gate.md",
"gsd-core/workflows/plan-phase/steps/prd-express-path.md",
"gsd-core/workflows/plan-phase/steps/research-only-early-exit.md",
"gsd-core/workflows/plan-phase/steps/research-only-modifiers.md",
"gsd-core/workflows/plan-phase/steps/reviews-prerequisite.md",
"gsd-core/workflows/plan-phase/steps/windows-troubleshooting.md",
"gsd-core/workflows/plan-review-convergence.md",
"gsd-core/workflows/plant-seed.md",

View File

@@ -294,8 +294,14 @@
"gsd-core/workflows/pause-work.md",
"gsd-core/workflows/plan-milestone-gaps.md",
"gsd-core/workflows/plan-phase.md",
"gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md",
"gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md",
"gsd-core/workflows/plan-phase/steps/closed-phase-gate.md",
"gsd-core/workflows/plan-phase/steps/prd-express-gate.md",
"gsd-core/workflows/plan-phase/steps/prd-express-path.md",
"gsd-core/workflows/plan-phase/steps/research-only-early-exit.md",
"gsd-core/workflows/plan-phase/steps/research-only-modifiers.md",
"gsd-core/workflows/plan-phase/steps/reviews-prerequisite.md",
"gsd-core/workflows/plan-phase/steps/windows-troubleshooting.md",
"gsd-core/workflows/plan-review-convergence.md",
"gsd-core/workflows/plant-seed.md",

View File

@@ -362,8 +362,14 @@
"gsd-core/workflows/pause-work.md",
"gsd-core/workflows/plan-milestone-gaps.md",
"gsd-core/workflows/plan-phase.md",
"gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md",
"gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md",
"gsd-core/workflows/plan-phase/steps/closed-phase-gate.md",
"gsd-core/workflows/plan-phase/steps/prd-express-gate.md",
"gsd-core/workflows/plan-phase/steps/prd-express-path.md",
"gsd-core/workflows/plan-phase/steps/research-only-early-exit.md",
"gsd-core/workflows/plan-phase/steps/research-only-modifiers.md",
"gsd-core/workflows/plan-phase/steps/reviews-prerequisite.md",
"gsd-core/workflows/plan-phase/steps/windows-troubleshooting.md",
"gsd-core/workflows/plan-review-convergence.md",
"gsd-core/workflows/plant-seed.md",

View File

@@ -397,8 +397,14 @@
"gsd-core/workflows/pause-work.md",
"gsd-core/workflows/plan-milestone-gaps.md",
"gsd-core/workflows/plan-phase.md",
"gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md",
"gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md",
"gsd-core/workflows/plan-phase/steps/closed-phase-gate.md",
"gsd-core/workflows/plan-phase/steps/prd-express-gate.md",
"gsd-core/workflows/plan-phase/steps/prd-express-path.md",
"gsd-core/workflows/plan-phase/steps/research-only-early-exit.md",
"gsd-core/workflows/plan-phase/steps/research-only-modifiers.md",
"gsd-core/workflows/plan-phase/steps/reviews-prerequisite.md",
"gsd-core/workflows/plan-phase/steps/windows-troubleshooting.md",
"gsd-core/workflows/plan-review-convergence.md",
"gsd-core/workflows/plant-seed.md",

View File

@@ -292,8 +292,14 @@
"gsd-core/workflows/pause-work.md",
"gsd-core/workflows/plan-milestone-gaps.md",
"gsd-core/workflows/plan-phase.md",
"gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md",
"gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md",
"gsd-core/workflows/plan-phase/steps/closed-phase-gate.md",
"gsd-core/workflows/plan-phase/steps/prd-express-gate.md",
"gsd-core/workflows/plan-phase/steps/prd-express-path.md",
"gsd-core/workflows/plan-phase/steps/research-only-early-exit.md",
"gsd-core/workflows/plan-phase/steps/research-only-modifiers.md",
"gsd-core/workflows/plan-phase/steps/reviews-prerequisite.md",
"gsd-core/workflows/plan-phase/steps/windows-troubleshooting.md",
"gsd-core/workflows/plan-review-convergence.md",
"gsd-core/workflows/plant-seed.md",

View File

@@ -362,8 +362,14 @@
"gsd-core/workflows/pause-work.md",
"gsd-core/workflows/plan-milestone-gaps.md",
"gsd-core/workflows/plan-phase.md",
"gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md",
"gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md",
"gsd-core/workflows/plan-phase/steps/closed-phase-gate.md",
"gsd-core/workflows/plan-phase/steps/prd-express-gate.md",
"gsd-core/workflows/plan-phase/steps/prd-express-path.md",
"gsd-core/workflows/plan-phase/steps/research-only-early-exit.md",
"gsd-core/workflows/plan-phase/steps/research-only-modifiers.md",
"gsd-core/workflows/plan-phase/steps/reviews-prerequisite.md",
"gsd-core/workflows/plan-phase/steps/windows-troubleshooting.md",
"gsd-core/workflows/plan-review-convergence.md",
"gsd-core/workflows/plant-seed.md",

View File

@@ -291,8 +291,14 @@
"gsd-core/workflows/pause-work.md",
"gsd-core/workflows/plan-milestone-gaps.md",
"gsd-core/workflows/plan-phase.md",
"gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md",
"gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md",
"gsd-core/workflows/plan-phase/steps/closed-phase-gate.md",
"gsd-core/workflows/plan-phase/steps/prd-express-gate.md",
"gsd-core/workflows/plan-phase/steps/prd-express-path.md",
"gsd-core/workflows/plan-phase/steps/research-only-early-exit.md",
"gsd-core/workflows/plan-phase/steps/research-only-modifiers.md",
"gsd-core/workflows/plan-phase/steps/reviews-prerequisite.md",
"gsd-core/workflows/plan-phase/steps/windows-troubleshooting.md",
"gsd-core/workflows/plan-review-convergence.md",
"gsd-core/workflows/plant-seed.md",

View File

@@ -362,8 +362,14 @@
"gsd-core/workflows/pause-work.md",
"gsd-core/workflows/plan-milestone-gaps.md",
"gsd-core/workflows/plan-phase.md",
"gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md",
"gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md",
"gsd-core/workflows/plan-phase/steps/closed-phase-gate.md",
"gsd-core/workflows/plan-phase/steps/prd-express-gate.md",
"gsd-core/workflows/plan-phase/steps/prd-express-path.md",
"gsd-core/workflows/plan-phase/steps/research-only-early-exit.md",
"gsd-core/workflows/plan-phase/steps/research-only-modifiers.md",
"gsd-core/workflows/plan-phase/steps/reviews-prerequisite.md",
"gsd-core/workflows/plan-phase/steps/windows-troubleshooting.md",
"gsd-core/workflows/plan-review-convergence.md",
"gsd-core/workflows/plant-seed.md",

View File

@@ -321,8 +321,14 @@
"gsd-core/workflows/pause-work.md",
"gsd-core/workflows/plan-milestone-gaps.md",
"gsd-core/workflows/plan-phase.md",
"gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md",
"gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md",
"gsd-core/workflows/plan-phase/steps/closed-phase-gate.md",
"gsd-core/workflows/plan-phase/steps/prd-express-gate.md",
"gsd-core/workflows/plan-phase/steps/prd-express-path.md",
"gsd-core/workflows/plan-phase/steps/research-only-early-exit.md",
"gsd-core/workflows/plan-phase/steps/research-only-modifiers.md",
"gsd-core/workflows/plan-phase/steps/reviews-prerequisite.md",
"gsd-core/workflows/plan-phase/steps/windows-troubleshooting.md",
"gsd-core/workflows/plan-review-convergence.md",
"gsd-core/workflows/plant-seed.md",

View File

@@ -357,8 +357,14 @@
"gsd-core/workflows/pause-work.md",
"gsd-core/workflows/plan-milestone-gaps.md",
"gsd-core/workflows/plan-phase.md",
"gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md",
"gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md",
"gsd-core/workflows/plan-phase/steps/closed-phase-gate.md",
"gsd-core/workflows/plan-phase/steps/prd-express-gate.md",
"gsd-core/workflows/plan-phase/steps/prd-express-path.md",
"gsd-core/workflows/plan-phase/steps/research-only-early-exit.md",
"gsd-core/workflows/plan-phase/steps/research-only-modifiers.md",
"gsd-core/workflows/plan-phase/steps/reviews-prerequisite.md",
"gsd-core/workflows/plan-phase/steps/windows-troubleshooting.md",
"gsd-core/workflows/plan-review-convergence.md",
"gsd-core/workflows/plant-seed.md",

View File

@@ -362,8 +362,14 @@
"gsd-core/workflows/pause-work.md",
"gsd-core/workflows/plan-milestone-gaps.md",
"gsd-core/workflows/plan-phase.md",
"gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md",
"gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md",
"gsd-core/workflows/plan-phase/steps/closed-phase-gate.md",
"gsd-core/workflows/plan-phase/steps/prd-express-gate.md",
"gsd-core/workflows/plan-phase/steps/prd-express-path.md",
"gsd-core/workflows/plan-phase/steps/research-only-early-exit.md",
"gsd-core/workflows/plan-phase/steps/research-only-modifiers.md",
"gsd-core/workflows/plan-phase/steps/reviews-prerequisite.md",
"gsd-core/workflows/plan-phase/steps/windows-troubleshooting.md",
"gsd-core/workflows/plan-review-convergence.md",
"gsd-core/workflows/plant-seed.md",

View File

@@ -259,8 +259,14 @@
"gsd-core/workflows/pause-work.md",
"gsd-core/workflows/plan-milestone-gaps.md",
"gsd-core/workflows/plan-phase.md",
"gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md",
"gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md",
"gsd-core/workflows/plan-phase/steps/closed-phase-gate.md",
"gsd-core/workflows/plan-phase/steps/prd-express-gate.md",
"gsd-core/workflows/plan-phase/steps/prd-express-path.md",
"gsd-core/workflows/plan-phase/steps/research-only-early-exit.md",
"gsd-core/workflows/plan-phase/steps/research-only-modifiers.md",
"gsd-core/workflows/plan-phase/steps/reviews-prerequisite.md",
"gsd-core/workflows/plan-phase/steps/windows-troubleshooting.md",
"gsd-core/workflows/plan-review-convergence.md",
"gsd-core/workflows/plant-seed.md",

View File

@@ -291,8 +291,14 @@
"gsd-core/workflows/pause-work.md",
"gsd-core/workflows/plan-milestone-gaps.md",
"gsd-core/workflows/plan-phase.md",
"gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md",
"gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md",
"gsd-core/workflows/plan-phase/steps/closed-phase-gate.md",
"gsd-core/workflows/plan-phase/steps/prd-express-gate.md",
"gsd-core/workflows/plan-phase/steps/prd-express-path.md",
"gsd-core/workflows/plan-phase/steps/research-only-early-exit.md",
"gsd-core/workflows/plan-phase/steps/research-only-modifiers.md",
"gsd-core/workflows/plan-phase/steps/reviews-prerequisite.md",
"gsd-core/workflows/plan-phase/steps/windows-troubleshooting.md",
"gsd-core/workflows/plan-review-convergence.md",
"gsd-core/workflows/plant-seed.md",

View File

@@ -291,8 +291,14 @@
"gsd-core/workflows/pause-work.md",
"gsd-core/workflows/plan-milestone-gaps.md",
"gsd-core/workflows/plan-phase.md",
"gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md",
"gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md",
"gsd-core/workflows/plan-phase/steps/closed-phase-gate.md",
"gsd-core/workflows/plan-phase/steps/prd-express-gate.md",
"gsd-core/workflows/plan-phase/steps/prd-express-path.md",
"gsd-core/workflows/plan-phase/steps/research-only-early-exit.md",
"gsd-core/workflows/plan-phase/steps/research-only-modifiers.md",
"gsd-core/workflows/plan-phase/steps/reviews-prerequisite.md",
"gsd-core/workflows/plan-phase/steps/windows-troubleshooting.md",
"gsd-core/workflows/plan-review-convergence.md",
"gsd-core/workflows/plant-seed.md",

View File

@@ -291,8 +291,14 @@
"gsd-core/workflows/pause-work.md",
"gsd-core/workflows/plan-milestone-gaps.md",
"gsd-core/workflows/plan-phase.md",
"gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md",
"gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md",
"gsd-core/workflows/plan-phase/steps/closed-phase-gate.md",
"gsd-core/workflows/plan-phase/steps/prd-express-gate.md",
"gsd-core/workflows/plan-phase/steps/prd-express-path.md",
"gsd-core/workflows/plan-phase/steps/research-only-early-exit.md",
"gsd-core/workflows/plan-phase/steps/research-only-modifiers.md",
"gsd-core/workflows/plan-phase/steps/reviews-prerequisite.md",
"gsd-core/workflows/plan-phase/steps/windows-troubleshooting.md",
"gsd-core/workflows/plan-review-convergence.md",
"gsd-core/workflows/plant-seed.md",

View File

@@ -362,8 +362,14 @@
"gsd-core/workflows/pause-work.md",
"gsd-core/workflows/plan-milestone-gaps.md",
"gsd-core/workflows/plan-phase.md",
"gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md",
"gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md",
"gsd-core/workflows/plan-phase/steps/closed-phase-gate.md",
"gsd-core/workflows/plan-phase/steps/prd-express-gate.md",
"gsd-core/workflows/plan-phase/steps/prd-express-path.md",
"gsd-core/workflows/plan-phase/steps/research-only-early-exit.md",
"gsd-core/workflows/plan-phase/steps/research-only-modifiers.md",
"gsd-core/workflows/plan-phase/steps/reviews-prerequisite.md",
"gsd-core/workflows/plan-phase/steps/windows-troubleshooting.md",
"gsd-core/workflows/plan-review-convergence.md",
"gsd-core/workflows/plant-seed.md",

View File

@@ -576,6 +576,174 @@ describe('gen-section-manifest.cjs --check / --write (matrix D)', () => {
});
});
// ─── #2993 (epic #1671 Phase 6.2) rows C1/C2: the shipped, committed
// gsd-core/workflows/section-manifest.json artifact itself — never a
// regenerated fixture — gains a `plan-phase` key with all 6 sections in
// document order (C1), while `execute-phase`'s own entry stays
// BYTE-IDENTICAL (C2, a Hyrum gate: #2932/#2992's 3 pre-existing sections
// must not shift shape just because a sibling workflow key was added).
describe('shipped gsd-core/workflows/section-manifest.json (#2993 rows C1/C2)', () => {
const SHIPPED_MANIFEST_PATH = path.join(ROOT, 'gsd-core', 'workflows', 'section-manifest.json');
function readShippedManifest() {
return JSON.parse(fs.readFileSync(SHIPPED_MANIFEST_PATH, 'utf8'));
}
test('planPhaseKeyHasAllSixSectionsInDocumentOrder (row C1)', () => {
const manifest = readShippedManifest();
assert.deepEqual(manifest.workflows['plan-phase'], [
{
id: 'reviews-prerequisite',
when: 'flag:--reviews',
read: 'gsd-core/workflows/plan-phase/steps/reviews-prerequisite.md',
},
{
id: 'prd-express-gate',
when: 'flag:--prd',
read: 'gsd-core/workflows/plan-phase/steps/prd-express-gate.md',
},
{
id: 'adr-ingest-express-path',
when: 'flag:--ingest',
read: 'gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md',
},
{
id: 'research-only-modifiers',
when: 'flag:--research-phase',
read: 'gsd-core/workflows/plan-phase/steps/research-only-modifiers.md',
},
{
id: 'research-only-early-exit',
when: 'flag:--research-phase',
read: 'gsd-core/workflows/plan-phase/steps/research-only-early-exit.md',
},
{
id: 'chunked-planning-mode',
when: 'state:chunked-mode',
read: 'gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md',
},
]);
});
test('executePhaseKeyIsByteIdenticalToBeforeThisChange (row C2 — Hyrum gate)', () => {
const manifest = readShippedManifest();
assert.deepEqual(manifest.workflows['execute-phase'], [
{
id: 'partial-wave',
when: 'flag:--wave',
read: 'gsd-core/workflows/execute-phase/steps/partial-wave.md',
},
{
id: 'gap-closure-artifacts',
when: 'state:gap-closure-phase',
read: 'gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md',
},
{
id: 'regression-gate',
when: 'state:has-prior-phases',
read: 'gsd-core/workflows/execute-phase/steps/regression-gate.md',
},
]);
});
test('shippedManifestPassesTheRealCheckAgainstTheRealRepo (idempotency, mirrors row 29)', () => {
const r = runGenSectionManifest([
'--check', '--workflows-dir', path.join(ROOT, 'gsd-core', 'workflows'), '--manifest-path', SHIPPED_MANIFEST_PATH, '--repo-root', ROOT,
]);
assert.equal(r.code, 0, `the shipped manifest must already be up to date; stderr: ${r.stderr}`);
});
});
// ─── Flag-forwarding regression guard (BLOCKER fix, epic #1671 Phase 6.2) ──
//
// The workflows never forwarded their flags to the init CLI, so every
// `flag:--X` atom was permanently FALSE in production and its gated section
// permanently EXCLUDED (`prd-express-gate` unreachable under `--prd`;
// `partial-wave` unreachable under `--wave`, pre-existing since #2932 Phase
// 5). This guard is DERIVED from the shipped manifest — never a hardcoded
// {workflow, flag} list — so it also catches the NEXT `flag:--X` atom added
// to any workflow without its own dedicated test.
describe('workflow gsd_run query init.<workflow> invocations forward every flag:--X atom (regression guard)', () => {
const SHIPPED_MANIFEST_PATH = path.join(ROOT, 'gsd-core', 'workflows', 'section-manifest.json');
function readShippedManifest() {
return JSON.parse(fs.readFileSync(SHIPPED_MANIFEST_PATH, 'utf8'));
}
/**
* All shell variable names assigned (anywhere in `scope`, via a
* double-quoted `VAR="..."` assignment) a value whose whitespace-split
* tokens include the exact literal `atomFlag` token (e.g. `--prd`).
* Whole-token comparison (not substring) so `--research-phase` never
* satisfies a check for `--research`, and vice versa.
*/
function varsCarryingFlagToken(scope, atomFlag) {
const carriers = new Set();
const assignRe = /([A-Za-z_][A-Za-z0-9_]*)="([^"]*)"/g;
let m;
while ((m = assignRe.exec(scope)) !== null) {
const [, varName, rhs] = m;
if (rhs.split(/\s+/).includes(atomFlag)) carriers.add(varName);
}
return carriers;
}
/** `$VAR` / `${VAR}` variable references appearing anywhere in `line`. */
function varsReferencedIn(line) {
const referenced = new Set();
const refRe = /\$\{?([A-Za-z_][A-Za-z0-9_]*)\}?/g;
let m;
while ((m = refRe.exec(line)) !== null) referenced.add(m[1]);
return referenced;
}
const manifest = readShippedManifest();
for (const [workflow, sections] of Object.entries(manifest.workflows)) {
const flagAtoms = [
...new Set(
sections
.map((s) => s.when)
.filter((when) => when.startsWith('flag:--'))
.map((when) => when.slice('flag:'.length)),
),
];
if (flagAtoms.length === 0) continue;
test(`${workflow}.md forwards [${flagAtoms.join(', ')}] to its gsd_run query init.${workflow} invocation`, () => {
const workflowMdPath = path.join(ROOT, 'gsd-core', 'workflows', `${workflow}.md`);
const content = fs.readFileSync(workflowMdPath, 'utf8');
const initLineRe = new RegExp(`^.*gsd_run query init\\.${workflow}\\b.*$`, 'm');
const initLineMatch = content.match(initLineRe);
assert.ok(initLineMatch, `${workflow}.md must contain a "gsd_run query init.${workflow}" invocation line`);
const initLine = initLineMatch[0];
// Everything up to and including the invocation line: real workflows
// parse $ARGUMENTS into a param variable earlier in the same bash
// block, then reference that variable on the INIT= line itself.
const scope = content.slice(0, content.indexOf(initLine) + initLine.length);
const referencedVars = varsReferencedIn(initLine);
for (const atomFlag of flagAtoms) {
const directlyPresent = initLine
.split(/\s+/)
.includes(atomFlag);
const carriers = varsCarryingFlagToken(scope, atomFlag);
const forwardedViaVar = [...carriers].some((v) => referencedVars.has(v));
assert.ok(
directlyPresent || forwardedViaVar,
`${workflow}.md's "gsd_run query init.${workflow}" invocation line must forward a parameter for ` +
`${atomFlag} (its gated section is otherwise permanently excluded — see BLOCKER, epic #1671 Phase 6.2). ` +
`Invocation line: ${initLine}`,
);
}
});
}
});
// ─── REASON enum shape lock (mirrors gen-context-index.cjs precedent) ──────
describe('gen-section-manifest.cjs REASON enum', () => {

View File

@@ -3613,22 +3613,35 @@ describe('init section manifest', () => {
// execute-phase-only to per-workflow (`buildSectionManifestField` now wires
// into `cmdInitPlanPhase`/`cmdInitNewProject`/`cmdInitNewMilestone`/
// `cmdInitQuick`/`cmdInitProgress` too — src/init.cts:740/787/839/885/1810).
// The OLD assertion here ("section_manifest must be execute-phase-only")
// is stale: plan-phase's field is now PRESENT, but `null` — the shipped
// gsd-core/workflows/section-manifest.json still keys only `execute-phase`
// (design row C3: a workflow absent from the artifact's `workflows` map
// degrades to `null`, never an absent field). `resume` has no dedicated
// `cmdInit*` manifest wiring at all (design's withheld-atom survey), so its
// field truly remains absent — that half of the guard is unchanged.
// #2993 (epic #1671 Phase 6.2) makes the OLD assertion here doubly stale:
// plan-phase's field is no longer `null` at all — the shipped
// gsd-core/workflows/section-manifest.json now keys `plan-phase` too, with
// 6 real sections (row C1). A no-flag invocation excludes every one of
// them (none of the 5 governing flags/state were supplied). `resume` has
// no dedicated `cmdInit*` manifest wiring at all (design's withheld-atom
// survey), so its field truly remains absent — that half of the guard is
// unchanged.
describe('init dispatch: other subcommands unaffected (#2932/#2992 row 59, CRITICAL radius guard)', () => {
test('planPhaseEmitsANullManifestNotAnAbsentField (row C3/C16 — absence is not empty)', (t) => {
describe('init dispatch: other subcommands unaffected (#2932/#2992/#2993 row 59, CRITICAL radius guard)', () => {
test('planPhaseEmitsARealComputedManifestNotNull (#2993 — the shipped artifact now has a plan-phase key)', (t) => {
const dir = seedSinglePhaseProject(t, 'gsd-e59-');
const planPhase = parseOkJson(runSectionManifestCli(['init.plan-phase', '1'], dir), 'plan-phase');
assert.equal(planPhase.phase_found, true);
assert.ok('section_manifest' in planPhase, 'section_manifest field must be present (computed, degraded to null) for plan-phase');
assert.equal(planPhase.section_manifest, null, 'plan-phase has no key in the shipped artifact, so it must degrade to null, never {included:[]}');
assert.ok('section_manifest' in planPhase, 'section_manifest field must be present for plan-phase');
assert.deepStrictEqual(planPhase.section_manifest, {
workflow: 'plan-phase',
included: [],
excluded: [
'reviews-prerequisite',
'prd-express-gate',
'adr-ingest-express-path',
'research-only-modifiers',
'research-only-early-exit',
'chunked-planning-mode',
],
read: [],
});
});
test('resumeNeverEmitsASectionManifestField', (t) => {
@@ -3638,6 +3651,221 @@ describe('init section manifest', () => {
});
});
// ── C6 (#2993): plan-phase's section_manifest degrades to null under a
// missing/malformed manifest artifact, exactly like execute-phase's own
// E57/E58 rows — the "safe superset" contract (all 6 sections read) is
// documented per-section in plan-phase.md's own stub prose ("If
// `section_manifest` is `null` or `"<id>"` is in its `included` list:
// read ... Otherwise skip"), verified for each of the 6 ids below; this
// test proves the JS-side half — that plan-phase really does receive
// `null`, never a stale/partial selection, under a degraded artifact.
describe('init plan-phase: section_manifest degrades to null (#2993 row C6)', () => {
test('degradesToNullManifestWhenArtifactMissing', (t) => {
const dir = seedSinglePhaseProject(t, 'gsd-c6-missing-');
const missingPath = path.join(dir, 'does-not-exist-section-manifest.json');
const result = runSectionManifestCli(['init.plan-phase', '1'], dir, { GSD_SECTION_MANIFEST: missingPath });
assert.equal(result.status, 0, `expected exit 0 despite missing manifest artifact, got ${result.status} (stderr: ${result.stderr})`);
assertNoStackTrace(result.stderr, 'plan-phase-manifest-missing');
const body = JSON.parse(result.stdout);
assert.equal(body.section_manifest, null);
});
test('degradesToNullManifestWhenArtifactMalformed', (t) => {
const dir = seedSinglePhaseProject(t, 'gsd-c6-malformed-');
const badPath = path.join(dir, 'bad-section-manifest.json');
fs.writeFileSync(badPath, '{ this is not valid json');
const result = runSectionManifestCli(['init.plan-phase', '1'], dir, { GSD_SECTION_MANIFEST: badPath });
assert.equal(result.status, 0, `expected exit 0 despite malformed manifest artifact, got ${result.status} (stderr: ${result.stderr})`);
assertNoStackTrace(result.stderr, 'plan-phase-manifest-malformed');
const body = JSON.parse(result.stdout);
assert.equal(body.section_manifest, null);
});
test('everyPlanPhaseSectionsHostStubDocumentsTheNullSafeSuperset (doc-level half of row C6)', () => {
const planPhasePath = path.join(GSD_ROOT, 'workflows', 'plan-phase.md');
const content = fs.readFileSync(planPhasePath, 'utf-8');
const ids = [
'reviews-prerequisite',
'prd-express-gate',
'adr-ingest-express-path',
'research-only-modifiers',
'research-only-early-exit',
'chunked-planning-mode',
];
for (const id of ids) {
const expectedGate = `If \`section_manifest\` is \`null\` or \`"${id}"\` is in its \`included\` list:`;
assert.ok(
content.includes(expectedGate),
`plan-phase.md must document the null-safe-superset gate for "${id}": expected to find "${expectedGate}"`,
);
}
});
});
// ── #2993 (epic #1671 Phase 6.2) rows B1-B11: plan-phase facts assembly,
// driven through the REAL CLI (prod-shape — matrix section B header) ─────
describe('init plan-phase: new flag facts assembly (#2993 rows B1-B6)', () => {
function runPlanPhase(phaseArgs, cwd, env = {}) {
return runSectionManifestCli(['init.plan-phase', ...phaseArgs], cwd, env);
}
test('reviewsFlagPresentIncludesOnlyReviewsPrerequisite (row B1)', (t) => {
const dir = seedSinglePhaseProject(t, 'gsd-b1-present-');
const body = parseOkJson(runPlanPhase(['1', '--reviews'], dir), 'b1-present');
assert.deepStrictEqual(body.section_manifest.included, ['reviews-prerequisite']);
});
test('reviewsFlagAbsentExcludesReviewsPrerequisite (row B1)', (t) => {
const dir = seedSinglePhaseProject(t, 'gsd-b1-absent-');
const body = parseOkJson(runPlanPhase(['1'], dir), 'b1-absent');
assert.ok(!body.section_manifest.included.includes('reviews-prerequisite'));
assert.ok(body.section_manifest.excluded.includes('reviews-prerequisite'));
});
test('prdFlagWithValuePresentIncludesOnlyPrdExpressGate (row B2)', (t) => {
const dir = seedSinglePhaseProject(t, 'gsd-b2-present-');
const body = parseOkJson(runPlanPhase(['1', '--prd', 'some-prd.md'], dir), 'b2-present');
assert.deepStrictEqual(body.section_manifest.included, ['prd-express-gate']);
});
test('prdFlagAbsentExcludesPrdExpressGate (row B2 — value flag absence is null)', (t) => {
const dir = seedSinglePhaseProject(t, 'gsd-b2-absent-');
const body = parseOkJson(runPlanPhase(['1'], dir), 'b2-absent');
assert.ok(!body.section_manifest.included.includes('prd-express-gate'));
assert.ok(body.section_manifest.excluded.includes('prd-express-gate'));
});
test('ingestFlagWithValuePresentIncludesOnlyAdrIngestExpressPath (row B3)', (t) => {
const dir = seedSinglePhaseProject(t, 'gsd-b3-present-');
const body = parseOkJson(runPlanPhase(['1', '--ingest', 'some-adr.md'], dir), 'b3-present');
assert.deepStrictEqual(body.section_manifest.included, ['adr-ingest-express-path']);
});
test('ingestFlagAbsentExcludesAdrIngestExpressPath (row B3)', (t) => {
const dir = seedSinglePhaseProject(t, 'gsd-b3-absent-');
const body = parseOkJson(runPlanPhase(['1'], dir), 'b3-absent');
assert.ok(!body.section_manifest.included.includes('adr-ingest-express-path'));
assert.ok(body.section_manifest.excluded.includes('adr-ingest-express-path'));
});
test('researchPhaseFlagWithValuePresentIncludesBothResearchOnlySections (row B4)', (t) => {
const dir = seedSinglePhaseProject(t, 'gsd-b4-present-');
const body = parseOkJson(runPlanPhase(['1', '--research-phase', '3'], dir), 'b4-present');
assert.deepStrictEqual(body.section_manifest.included, ['research-only-modifiers', 'research-only-early-exit']);
});
test('researchPhaseFlagAbsentExcludesBothResearchOnlySections (row B4)', (t) => {
const dir = seedSinglePhaseProject(t, 'gsd-b4-absent-');
const body = parseOkJson(runPlanPhase(['1'], dir), 'b4-absent');
assert.ok(!body.section_manifest.included.includes('research-only-modifiers'));
assert.ok(!body.section_manifest.included.includes('research-only-early-exit'));
});
test('emptyPrdValueIsFalsyAndTreatedAsAbsent (row B5 — no spurious inclusion)', (t) => {
// `--prd` immediately followed by another flag token (no value token
// present at all) resolves to `null` in parseNamedArgs — the "empty
// value" shape for a value flag. Combined with a second, independently
// gated flag to prove ONLY the second flag's section activates.
const dir = seedSinglePhaseProject(t, 'gsd-b5-');
const body = parseOkJson(runPlanPhase(['1', '--prd', '--reviews'], dir), 'b5');
assert.ok(!body.section_manifest.included.includes('prd-express-gate'), '--prd with no value must never spuriously include prd-express-gate');
assert.deepStrictEqual(body.section_manifest.included, ['reviews-prerequisite']);
});
test('chunkedFlagPresentIncludesChunkedPlanningMode (row B6)', (t) => {
const dir = seedSinglePhaseProject(t, 'gsd-b6-');
const body = parseOkJson(runPlanPhase(['1', '--chunked'], dir), 'b6');
assert.deepStrictEqual(body.section_manifest.included, ['chunked-planning-mode']);
});
});
describe('init plan-phase: state:chunked-mode disjunction — flag OR config (#2993 rows B7-B11)', () => {
function runPlanPhase(phaseArgs, cwd, env = {}) {
return runSectionManifestCli(['init.plan-phase', ...phaseArgs], cwd, env);
}
function writeConfig(dir, workflowConfig) {
fs.writeFileSync(path.join(dir, '.planning', 'config.json'), JSON.stringify({ workflow: workflowConfig }));
}
test('chunkedFlagAbsentConfigTrueIncludesChunkedPlanningMode (row B7 — config arm of the disjunction)', (t) => {
const dir = seedSinglePhaseProject(t, 'gsd-b7-');
writeConfig(dir, { plan_chunked: true });
const body = parseOkJson(runPlanPhase(['1'], dir), 'b7');
assert.deepStrictEqual(body.section_manifest.included, ['chunked-planning-mode']);
});
test('chunkedFlagAbsentConfigFalseExcludesChunkedPlanningMode (row B8)', (t) => {
const dir = seedSinglePhaseProject(t, 'gsd-b8-false-');
writeConfig(dir, { plan_chunked: false });
const body = parseOkJson(runPlanPhase(['1'], dir), 'b8-false');
assert.ok(!body.section_manifest.included.includes('chunked-planning-mode'));
});
test('chunkedFlagAbsentConfigAbsentExcludesChunkedPlanningMode (row B8)', (t) => {
const dir = seedSinglePhaseProject(t, 'gsd-b8-absent-');
writeConfig(dir, {});
const body = parseOkJson(runPlanPhase(['1'], dir), 'b8-absent');
assert.ok(!body.section_manifest.included.includes('chunked-planning-mode'));
});
test('chunkedFlagAbsentConfigFileMissingExcludesChunkedPlanningMode (row B8)', (t) => {
const dir = seedSinglePhaseProject(t, 'gsd-b8-nofile-');
assert.equal(fs.existsSync(path.join(dir, '.planning', 'config.json')), false, 'sanity: no config.json');
const body = parseOkJson(runPlanPhase(['1'], dir), 'b8-nofile');
assert.ok(!body.section_manifest.included.includes('chunked-planning-mode'));
});
test('configStringTrueDegradesToFalse (row B9 — strict === true, never coerced)', (t) => {
const dir = seedSinglePhaseProject(t, 'gsd-b9-');
writeConfig(dir, { plan_chunked: 'true' });
const body = parseOkJson(runPlanPhase(['1'], dir), 'b9');
assert.ok(
!body.section_manifest.included.includes('chunked-planning-mode'),
'a string "true" config value must never coerce to boolean true',
);
});
test('configReadThrowsDegradesToFalseBoundedNeverPropagates (row B10)', (t) => {
// `readConfigJsonBoolean` (src/init.cts) is private and reads
// `.planning/config.json` via `fs.readFileSync`, so its `catch` clause
// cannot be exercised via an in-process fs monkeypatch across the
// spawned-CLI process boundary this suite otherwise drives (the whole
// matrix section is prod-shape: "drive the real CLI"). A directory at
// the config path forces a REAL, deterministic, cross-platform fs
// fault (EISDIR-class on every OS `fs.readFileSync` targets) through
// the real CLI — never a chmod/permission trick — landing on the same
// bounded, non-throwing degrade path a monkeypatched throw would.
const dir = seedSinglePhaseProject(t, 'gsd-b10-');
// sanity: seedSinglePhaseProject never writes a config.json, so this is
// the fixture's own natural state, not a removal.
assert.equal(fs.existsSync(path.join(dir, '.planning', 'config.json')), false);
fs.mkdirSync(path.join(dir, '.planning', 'config.json'));
const result = runPlanPhase(['1'], dir);
assert.equal(result.status, 0, `expected exit 0 despite an unreadable config.json, got ${result.status} (stderr: ${result.stderr})`);
assertNoStackTrace(result.stderr, 'config-read-throws');
const body = JSON.parse(result.stdout);
assert.ok(!body.section_manifest.included.includes('chunked-planning-mode'), 'a config read fault must degrade chunkedMode to false, never throw or propagate');
});
test('nonObjectConfigJsonDegradesToFalse (row B11)', (t) => {
const dir = seedSinglePhaseProject(t, 'gsd-b11-');
for (const hostileJson of ['0', '"s"', '[]', 'null', 'true']) {
fs.writeFileSync(path.join(dir, '.planning', 'config.json'), hostileJson);
const result = runPlanPhase(['1'], dir);
assert.equal(result.status, 0, `hostile config JSON ${hostileJson}: expected exit 0 (stderr: ${result.stderr})`);
assertNoStackTrace(result.stderr, `hostile-config-json:${hostileJson}`);
const body = JSON.parse(result.stdout);
assert.ok(
!body.section_manifest.included.includes('chunked-planning-mode'),
`hostile config JSON ${hostileJson} must degrade chunkedMode to false`,
);
}
});
});
// ── Row 62: stub <execution_context> @-refs still resolve (ADR-0002) ────
describe('commands/gsd/execute-phase.md: <execution_context> @-refs resolve (#2932 row 62)', () => {

View File

@@ -14,7 +14,11 @@ const assert = require('node:assert/strict');
const fs = require('fs');
const path = require('path');
const MD = path.join(__dirname, '..', 'gsd-core', 'workflows', 'plan-phase.md');
// #2993 fragmentization moved §8.5 (chunked planning mode, including §8.5.1 /
// §8.5.2) out of plan-phase.md into gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md.
// Read that step file directly — it is the sole remaining source of the §8.5.1/§8.5.2
// content these regression guards assert on.
const MD = path.join(__dirname, '..', 'gsd-core', 'workflows', 'plan-phase', 'steps', 'chunked-planning-mode.md');
const read = () => fs.readFileSync(MD, 'utf8');
test('§8.5.1 outline agent writes the resume marker into the file (#2762 defect A)', () => {

View File

@@ -32,6 +32,27 @@ const WORKFLOW_PATH = path.join(
'plan-phase.md'
);
const PLAN_PHASE_STEPS_DIR = path.join(__dirname, '..', 'gsd-core', 'workflows', 'plan-phase', 'steps');
/**
* plan-phase.md was fragmented (#2993) into gsd-core/workflows/plan-phase/steps/*.md.
* Content this drift guard asserts on may now live in the host file, an extracted
* step file, or be split across both (e.g. a rule-label count). Guards that need to
* see the full picture read this combined blob instead of `workflow` alone so they
* don't go blind the next time a section moves out of the host.
*/
function readPlanPhaseCombined() {
let combined = fs.readFileSync(WORKFLOW_PATH, 'utf8');
if (fs.existsSync(PLAN_PHASE_STEPS_DIR)) {
for (const entry of fs.readdirSync(PLAN_PHASE_STEPS_DIR).sort()) {
if (entry.endsWith('.md')) {
combined += '\n' + fs.readFileSync(path.join(PLAN_PHASE_STEPS_DIR, entry), 'utf8');
}
}
}
return combined;
}
// ─── Fixture ──────────────────────────────────────────────────────────────────
const workflow = fs.readFileSync(WORKFLOW_PATH, 'utf8');
@@ -185,11 +206,15 @@ describe('plan-phase workflow: top-level spawn guard (#913)', () => {
});
test('workflow contains ALL RUNTIMES orchestrator rule labels (count preserved)', () => {
// Must have all 7 agent-spawn wait rules still present (none dropped during rename)
const allRuntimesCount = (workflow.match(/ORCHESTRATOR RULE — ALL RUNTIMES/g) || []).length;
// Must have all 7 agent-spawn wait rules still present (none dropped during rename).
// #2993 fragmentization moved some spawn sites (e.g. chunked planning) into
// gsd-core/workflows/plan-phase/steps/*.md, so the count is taken across the
// host file AND every extracted step file — not the host alone.
const combined = readPlanPhaseCombined();
const allRuntimesCount = (combined.match(/ORCHESTRATOR RULE — ALL RUNTIMES/g) || []).length;
assert.ok(
allRuntimesCount >= 7,
`plan-phase must have at least 7 "ORCHESTRATOR RULE — ALL RUNTIMES" labels (one per agent spawn site); found ${allRuntimesCount} (#913)`
`plan-phase (host + plan-phase/steps/*.md) must have at least 7 "ORCHESTRATOR RULE — ALL RUNTIMES" labels (one per agent spawn site); found ${allRuntimesCount} (#913)`
);
});
});
@@ -780,12 +805,17 @@ describe('plan-phase.md — chunked mode flag and config (#2310)', () => {
});
describe('plan-phase.md — chunked mode implementation (#2310)', () => {
const content = fs.readFileSync(PLAN_PHASE, 'utf-8');
// #2993 fragmentization moved §8.5 (chunked planning mode) out of the host file
// into gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md. These
// assertions now read the combined host + step-files blob (readPlanPhaseCombined,
// defined at module scope) so they keep guarding the same property regardless of
// which file the content physically lives in.
const content = readPlanPhaseCombined();
test('step 8.5 chunked planning section exists', () => {
assert.ok(
content.includes('## 8.5.'),
'plan-phase.md must have a step 8.5 section for chunked planning mode'
'plan-phase.md (or its extracted plan-phase/steps/*.md) must have a step 8.5 section for chunked planning mode'
);
});
@@ -942,16 +972,22 @@ describe('enh #3209: plan-phase ADR ingest express path', () => {
);
});
// #2993 fragmentization moved the ADR ingest express-path step (former §3.6) out
// of the host file into gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md.
// These three tests read the combined host + step-files blob (readPlanPhaseCombined,
// defined at module scope) so they keep guarding the same property regardless of
// which file the content physically lives in.
test('workflow defines an ADR ingest express-path step', () => {
const workflow = read(WORKFLOW_PATH);
const workflow = readPlanPhaseCombined();
assert.ok(/##\s*(?:\d+(?:\.\d+)*)?\.?\s*Handle ADR Ingest Express Path/i.test(workflow),
'plan-phase workflow must include a dedicated ADR ingest express-path step');
'plan-phase workflow (host + plan-phase/steps/*.md) must include a dedicated ADR ingest express-path step');
assert.ok(workflow.includes('ADR Ingest Express Path'),
'workflow must display ADR ingest express-path banner text');
});
test('ADR ingest context template includes scope fence and ADR source attribution', () => {
const workflow = read(WORKFLOW_PATH);
const workflow = readPlanPhaseCombined();
assert.ok(workflow.includes('<scope_fence>'),
'ADR ingest context template must include <scope_fence> for hard out-of-scope exclusions');
assert.ok(workflow.includes('Source:** ADR Ingest Express Path'),
@@ -959,7 +995,7 @@ describe('enh #3209: plan-phase ADR ingest express path', () => {
});
test('workflow documents status gate and no-decisions fallback', () => {
const workflow = read(WORKFLOW_PATH);
const workflow = readPlanPhaseCombined();
assert.ok(
workflow.includes('Reject `superseded`/`rejected`/`deprecated`') ||
workflow.includes('reject `superseded`/`rejected`/`deprecated`') ||

View File

@@ -82,6 +82,10 @@ const factsArb = fc.record({
needsCodebaseMap: fc.boolean(),
phaseMvpMode: fc.boolean(),
worktreesEnabled: fc.boolean(),
// #2993 (epic #1671 Phase 6.2): chunkedMode is a shape addition to
// InvocationFacts — must also participate in the partition/totality
// properties below, not just the six pre-existing fact keys.
chunkedMode: fc.boolean(),
});
// ─── Row 25: exact partition ────────────────────────────────────────────────
@@ -132,8 +136,8 @@ describe('property: never throws for vocabulary-valid when and arbitrary facts',
test('neverThrowsWhenFactsAreMissingKeysEntirely', () => {
// Totality also over PARTIAL facts objects (row 19's property-level
// twin): dropping zero or more of the six fact keys must never throw.
const factKeys = ['flags', 'phaseNumber', 'hasPriorPhases', 'needsCodebaseMap', 'phaseMvpMode', 'worktreesEnabled'];
// twin): dropping zero or more of the seven fact keys must never throw.
const factKeys = ['flags', 'phaseNumber', 'hasPriorPhases', 'needsCodebaseMap', 'phaseMvpMode', 'worktreesEnabled', 'chunkedMode'];
fc.assert(
fc.property(sectionsArb, factsArb, fc.subarray(factKeys), (sections, facts, keysToKeep) => {
const partialFacts = {};

View File

@@ -31,7 +31,10 @@ const BRANCH_SECTIONS = Object.freeze([
]);
function facts(overrides) {
return { flags: new Set(), phaseNumber: null, hasPriorPhases: false, ...overrides };
// #2993 (epic #1671 Phase 6.2): chunkedMode is a shape addition to
// InvocationFacts — defaulted false here so every pre-existing call site
// above keeps its prior behavior unchanged.
return { flags: new Set(), phaseNumber: null, hasPriorPhases: false, chunkedMode: false, ...overrides };
}
// ─── Rows 1-8: happy path + combinations over W/D/P ─────────────────────────
@@ -361,6 +364,85 @@ describe('state:needs-codebase-map / state:phase-mvp-mode / state:worktrees-enab
});
});
// ─── #2993 (epic #1671 Phase 6.2) row A6: flag:--research-phase and
// flag:--research are DISTINCT atoms — neither aliases the other, each
// gates only its own sections ─────────────────────────────────────────────
describe('flag:--research-phase and flag:--research do not alias each other (row A6)', () => {
const sections = Object.freeze([
{ id: 'research-section', when: 'flag:--research' },
{ id: 'research-phase-section', when: 'flag:--research-phase' },
]);
test('onlyResearchFlagIncludesOnlyTheResearchSection', () => {
const result = selectSections(sections, facts({ flags: new Set(['--research']) }));
assert.deepEqual(result, { included: ['research-section'], excluded: ['research-phase-section'] });
});
test('onlyResearchPhaseFlagIncludesOnlyTheResearchPhaseSection', () => {
const result = selectSections(sections, facts({ flags: new Set(['--research-phase']) }));
assert.deepEqual(result, { included: ['research-phase-section'], excluded: ['research-section'] });
});
test('bothFlagsIncludeBothSections', () => {
const result = selectSections(sections, facts({ flags: new Set(['--research', '--research-phase']) }));
assert.deepEqual(result, { included: ['research-section', 'research-phase-section'], excluded: [] });
});
test('neitherFlagExcludesBothSections', () => {
const result = selectSections(sections, facts({ flags: new Set() }));
assert.deepEqual(result, { included: [], excluded: ['research-section', 'research-phase-section'] });
});
test('predicatesAreIndependentFunctionsNotSharedByToken', () => {
// A stronger structural guard than the behavioral ones above: the two
// predicates must not literally be the same function reference (which
// would make aliasing possible by construction, even if it happened to
// pass every input/output check above by coincidence).
assert.notEqual(WHEN_PREDICATES['flag:--research'], WHEN_PREDICATES['flag:--research-phase']);
});
});
// ─── #2993 (epic #1671 Phase 6.2) row A8: state:chunked-mode fact true /
// false / absent ────────────────────────────────────────────────────────
describe('state:chunked-mode predicate (row A8)', () => {
test('chunkedModeTrueWhenFactIsTrue', () => {
assert.equal(WHEN_PREDICATES['state:chunked-mode'](facts({ chunkedMode: true })), true);
});
test('chunkedModeFalseWhenFactIsFalse', () => {
assert.equal(WHEN_PREDICATES['state:chunked-mode'](facts({ chunkedMode: false })), false);
});
test('chunkedModeFalseWhenFactIsAbsent', () => {
assert.doesNotThrow(() => WHEN_PREDICATES['state:chunked-mode'](facts({})));
const { chunkedMode: _omit, ...withoutChunkedMode } = facts({});
assert.doesNotThrow(() => WHEN_PREDICATES['state:chunked-mode'](withoutChunkedMode));
assert.equal(WHEN_PREDICATES['state:chunked-mode'](withoutChunkedMode), false);
});
test('chunkedModeFalseWhenFactIsUndefined', () => {
assert.doesNotThrow(() => WHEN_PREDICATES['state:chunked-mode'](facts({ chunkedMode: undefined })));
assert.equal(WHEN_PREDICATES['state:chunked-mode'](facts({ chunkedMode: undefined })), false);
});
test('chunkedModeFalseForTheStringLiteralTrue (strict === true, never coerced)', () => {
// Mirrors readConfigJsonBoolean's own strict discipline (src/init.cts):
// a truthy-but-non-boolean value must never pass. This is the FACT's own
// strictness contract; the config-string-"true" case itself is exercised
// at the init seam in tests/init.test.cjs (prod-shape, row B9).
assert.equal(WHEN_PREDICATES['state:chunked-mode'](facts({ chunkedMode: 'true' })), false);
});
test('selectSectionsIncludesChunkedPlanningModeSectionOnlyWhenFactIsTrue', () => {
const sections = [{ id: 'chunked-section', when: 'state:chunked-mode' }];
assert.deepEqual(selectSections(sections, facts({ chunkedMode: true })), { included: ['chunked-section'], excluded: [] });
assert.deepEqual(selectSections(sections, facts({ chunkedMode: false })), { included: [], excluded: ['chunked-section'] });
assert.deepEqual(selectSections(sections, facts({})), { included: [], excluded: ['chunked-section'] });
});
});
// ─── Rows 25-33: Object.prototype-shaped when= values fail closed ──────────
// Added during review — prototype-chain fail-open found by isolated
// adversarial pass. A bracket lookup on a plain frozen object resolves

View File

@@ -174,7 +174,10 @@ describe('skill frontmatter: /gsd-plan-phase --research-phase flag absorbs the s
});
test('research-only mode auto-uses existing RESEARCH.md (no update/view/skip prompt)', () => {
const content = read('gsd-core/workflows/plan-phase.md');
// #2993 fragmentization moved §5.0 (research-only modifiers, including the
// existing-RESEARCH.md auto-use notice) out of plan-phase.md into
// gsd-core/workflows/plan-phase/steps/research-only-modifiers.md.
const content = read('gsd-core/workflows/plan-phase/steps/research-only-modifiers.md');
// #159: the §5.0 existing-RESEARCH.md path no longer prompts
// update/view/skip. When RESEARCH.md exists and neither --research nor
// --view is set, the workflow emits a brief "using it" notice naming

View File

@@ -63,9 +63,13 @@ const PILOT_REL = path.join('gsd-core', 'workflows', 'execute-phase.md');
const PILOT_PATH = path.join(REPO_ROOT, PILOT_REL);
// plan-phase.md was the original #2930 pilot but was reverted to unmarked
// (chore/2930 retarget: it sits 36 B under the ADR-857 Phase-6 PRE_PHASE6
// gate and cannot absorb marker overhead) — it is now a genuinely unmarked
// file again, so row 33 uses it instead of plan-phase.md.
const UNMARKED_REL = path.join('gsd-core', 'workflows', 'plan-phase.md');
// gate and cannot absorb marker overhead) — it was a genuinely unmarked
// file again, so row 33 used it instead of plan-phase.md. #2993 (epic #1671
// Phase 6.2) now marks plan-phase.md itself (6 sections, the fragmentization
// this change ships), so it is no longer a valid "genuinely unmarked" fixture
// either — retargeted a second time to discuss-phase.md, which carries no
// gsd:section markers as of this change.
const UNMARKED_REL = path.join('gsd-core', 'workflows', 'discuss-phase.md');
const RUNTIMES = Object.keys(RUNTIME_META);
@@ -292,8 +296,8 @@ test('unmarkedWorkflowEmitsByteIdenticalForEveryRuntime', () => {
);
const realPath = path.join(real.configDir, UNMARKED_REL);
const stubPath = path.join(stub.configDir, UNMARKED_REL);
assert.ok(fs.existsSync(realPath), `${runtime}: real install is missing plan-phase.md`);
assert.ok(fs.existsSync(stubPath), `${runtime}: stub install is missing plan-phase.md`);
assert.ok(fs.existsSync(realPath), `${runtime}: real install is missing discuss-phase.md`);
assert.ok(fs.existsSync(stubPath), `${runtime}: stub install is missing discuss-phase.md`);
// Normalize each side's own randomly-generated temp root out of the
// content before hashing: some runtimes (opencode) embed the
@@ -306,14 +310,14 @@ test('unmarkedWorkflowEmitsByteIdenticalForEveryRuntime', () => {
assert.equal(
Buffer.byteLength(realText, 'utf8'),
Buffer.byteLength(stubText, 'utf8'),
`${runtime}: plan-phase.md byte size drifted between real compose and identity-stub compose`,
`${runtime}: discuss-phase.md byte size drifted between real compose and identity-stub compose`,
);
const realHash = crypto.createHash('sha256').update(realText).digest('hex');
const stubHash = crypto.createHash('sha256').update(stubText).digest('hex');
assert.equal(
realHash,
stubHash,
`${runtime}: plan-phase.md content drifted between real compose and identity-stub compose`,
`${runtime}: discuss-phase.md content drifted between real compose and identity-stub compose`,
);
} finally {
cleanup(real.root);
@@ -461,26 +465,34 @@ test('nonWorkflowMarkdownWithMarkerShapedLineIsNotComposed', () => {
test('leavesUnmarkedWorkflowEmissionByteIdentical', () => {
const workflowsDir = path.join(REPO_ROOT, 'gsd-core', 'workflows');
// execute-phase.md (#2932 Phase 5) and plan-phase.md (#2993 Phase 6.2) are
// the two files this repo has fragmentized with gsd:section markers — both
// excluded here since composeWorkflow is deliberately NOT a no-op for them.
const MARKED_WORKFLOWS = new Set(['execute-phase.md', 'plan-phase.md']);
const workflowFiles = fs
.readdirSync(workflowsDir, { withFileTypes: true })
.filter((d) => d.isFile() && d.name.endsWith('.md'))
.map((d) => d.name);
assert.ok(workflowFiles.length > 1, 'sanity: there must be more than the pilot workflow on disk');
assert.ok(workflowFiles.length > MARKED_WORKFLOWS.size, 'sanity: there must be more than the marked workflows on disk');
let checkedCount = 0;
for (const fileName of workflowFiles) {
if (fileName === 'execute-phase.md') continue; // the one file #2932 Phase 5 migrates
if (MARKED_WORKFLOWS.has(fileName)) continue;
const filePath = path.join(workflowsDir, fileName);
const source = fs.readFileSync(filePath, 'utf8');
const composed = composeWorkflow(source, { sourcePath: filePath });
assert.equal(
composed,
source,
`${fileName}: emission drifted — Phase 5's execute-phase.md extraction must not touch any other workflow`,
`${fileName}: emission drifted — a marked workflow's extraction must not touch any other workflow`,
);
checkedCount += 1;
}
assert.equal(checkedCount, workflowFiles.length - 1, 'every workflow file except execute-phase.md must have been checked');
assert.equal(
checkedCount,
workflowFiles.length - MARKED_WORKFLOWS.size,
'every workflow file except the marked ones must have been checked',
);
});
test('malformedMarkersFailInstallWithoutPartialEmit', () => {

View File

@@ -29,12 +29,18 @@ const {
REASON,
} = require('../gsd-core/bin/lib/workflow-fragments.cjs');
const { composeWithinBudget } = require('../gsd-core/bin/lib/context-composer.cjs');
const { selectSections } = require('../gsd-core/bin/lib/section-manifest.cjs');
const measureBytes = (text) => Buffer.byteLength(text, 'utf8');
/** Compose a document string from an array of lines, joined with '\n'. */
const doc = (...lines) => lines.join('\n');
/** Minimal InvocationFacts factory for the real-plan-phase.md D4 test below. */
function facts(overrides) {
return { flags: new Set(), phaseNumber: null, hasPriorPhases: false, ...overrides };
}
// ─── Row 1: unmarked document (the 88/89 production shape) ─────────────────
describe('unmarked document round trip', () => {
@@ -182,6 +188,126 @@ describe('real execute-phase.md', () => {
});
});
// ─── #2993 (epic #1671 Phase 6.2): real plan-phase.md — C1/D1/D4/D5 ───────
describe('real plan-phase.md (#2993)', () => {
const PLAN_PHASE_PATH = path.join(__dirname, '..', 'gsd-core', 'workflows', 'plan-phase.md');
const STEPS_DIR = path.join(__dirname, '..', 'gsd-core', 'workflows', 'plan-phase', 'steps');
// The 6 sections the #2993 design survey names, in document order.
const EXPECTED_SECTIONS = Object.freeze([
{ id: 'reviews-prerequisite', when: 'flag:--reviews' },
{ id: 'prd-express-gate', when: 'flag:--prd' },
{ id: 'adr-ingest-express-path', when: 'flag:--ingest' },
{ id: 'research-only-modifiers', when: 'flag:--research-phase' },
{ id: 'research-only-early-exit', when: 'flag:--research-phase' },
{ id: 'chunked-planning-mode', when: 'state:chunked-mode' },
]);
test('parsesExactlySixSectionsInDocumentOrder (row C1)', () => {
const source = fs.readFileSync(PLAN_PHASE_PATH, 'utf8');
const sections = parseWorkflowSections(source, PLAN_PHASE_PATH);
const explicitSections = sections.filter((s) => s.explicit);
assert.deepEqual(
explicitSections.map((s) => ({ id: s.id, when: s.when })),
[...EXPECTED_SECTIONS],
);
});
test('parsesAndRendersWithoutThrowingAndStripsEveryMarker', () => {
const source = fs.readFileSync(PLAN_PHASE_PATH, 'utf8');
const composed = composeWorkflow(source, { sourcePath: PLAN_PHASE_PATH });
assert.equal(composed.includes('gsd:section'), false);
assert.ok(Buffer.byteLength(composed, 'utf8') < Buffer.byteLength(source, 'utf8'));
});
test('everyExtractedStepFileExistsIsNonEmptyAndTheHostNoLongerCarriesItsBody (row D1)', () => {
// Behavioral, never a source-grep substring match: for each of the 6
// sections, the step file on disk (1) exists, (2) is non-empty, and (3)
// is EXACTLY the section's own parsed body (the bytes the marker pair
// wraps in the host) is a stub/reference line, not a re-paste of the
// step file's content — the host's marker body and the step file's
// content are two DIFFERENT, disjoint pieces of text after the move.
const source = fs.readFileSync(PLAN_PHASE_PATH, 'utf8');
const sections = parseWorkflowSections(source, PLAN_PHASE_PATH);
const byId = new Map(sections.filter((s) => s.explicit).map((s) => [s.id, s]));
for (const { id } of EXPECTED_SECTIONS) {
const stepPath = path.join(STEPS_DIR, `${id}.md`);
assert.ok(fs.existsSync(stepPath), `expected step file to exist: ${stepPath}`);
const stepContent = fs.readFileSync(stepPath, 'utf8');
assert.ok(stepContent.length > 0, `expected non-empty step file: ${stepPath}`);
const hostSection = byId.get(id);
assert.ok(hostSection, `expected an explicit host section for id="${id}"`);
// The host's marker body is a short stub (the conditional read-and-execute
// instruction), never the step file's own moved content — proves the
// body actually left the host rather than being duplicated in place.
assert.ok(
hostSection.body.length < stepContent.length,
`expected host stub body for "${id}" to be shorter than the extracted step file`,
);
assert.equal(
hostSection.body.includes(stepContent.trim()),
false,
`host stub body for "${id}" must not still contain the moved step file's content verbatim`,
);
}
});
test('prdExpressPathIsReadOnlyWhenPrdSectionIsIncluded (row D4)', () => {
// prd-express-path.md was previously read unconditionally; the #2993
// wrapper (prd-express-gate, when="flag:--prd") is what now actually
// gates it. Prove the gate via the real evaluator: absent --prd excludes
// prd-express-gate; present --prd includes it.
const source = fs.readFileSync(PLAN_PHASE_PATH, 'utf8');
const sections = parseWorkflowSections(source, PLAN_PHASE_PATH).filter((s) => s.explicit);
const withoutPrd = selectSections(sections, facts({}));
assert.ok(withoutPrd.excluded.includes('prd-express-gate'), 'prd-express-gate must be excluded when --prd is absent');
assert.ok(!withoutPrd.included.includes('prd-express-gate'));
const withPrd = selectSections(sections, facts({ flags: new Set(['--prd']) }));
assert.ok(withPrd.included.includes('prd-express-gate'), 'prd-express-gate must be included when --prd is present');
assert.ok(!withPrd.excluded.includes('prd-express-gate'));
// The host's marker body is a stub that reads plan-phase/steps/prd-express-gate.md
// ONLY when the section is included; THAT step file (nested, one hop
// further — the reachability shape gen-section-manifest.cjs's own
// "nested step reference" precedent covers) is what references
// prd-express-path.md — proving the express-path read is reachable only
// through the now-conditional wrapper, never as a second, independent
// unconditional read site elsewhere in the host.
const gateSection = sections.find((s) => s.id === 'prd-express-gate');
assert.ok(gateSection.body.includes('prd-express-gate.md'), 'prd-express-gate\'s host stub must read its own step file');
const gateStepContent = fs.readFileSync(path.join(STEPS_DIR, 'prd-express-gate.md'), 'utf8');
assert.ok(
gateStepContent.includes('prd-express-path.md'),
'prd-express-gate.md must be the (nested) step that references prd-express-path.md',
);
});
test('skipIfProseAppearsExactlyOnceAcrossHostAndStepFile (row D5)', () => {
// For each section, the "Skip if:" gating prose must live in exactly ONE
// place — either the host stub or the step file — never duplicated in
// both after the move.
const source = fs.readFileSync(PLAN_PHASE_PATH, 'utf8');
const sections = parseWorkflowSections(source, PLAN_PHASE_PATH);
const byId = new Map(sections.filter((s) => s.explicit).map((s) => [s.id, s]));
for (const { id } of EXPECTED_SECTIONS) {
const stepContent = fs.readFileSync(path.join(STEPS_DIR, `${id}.md`), 'utf8');
const hostBody = byId.get(id).body;
const hostHasSkipIf = /Skip if:/.test(hostBody);
const stepHasSkipIf = /Skip if:/.test(stepContent);
assert.notEqual(
hostHasSkipIf && stepHasSkipIf,
true,
`"Skip if:" prose for "${id}" must not appear in BOTH the host stub and the step file`,
);
}
});
});
// ─── Row 5/6: fence negative space ──────────────────────────────────────────
describe('marker lookalikes inside fences', () => {
@@ -483,6 +609,12 @@ describe('frozen when= vocabulary', () => {
// for an array only reflects index positions '0','1',... and would not
// catch a value being silently renamed). See the dispatch report for
// this deliberate deviation from the test matrix's literal wording.
//
// #2993 (epic #1671 Phase 6.2, matrix row A3) widens this lock 14 -> 19:
// flag:--ingest, flag:--prd, flag:--research-phase, flag:--reviews,
// state:chunked-mode. This is the row the lock exists to force — a
// deliberate, coordinated update, never a silent drift (Greenspun's
// Tenth Rule / ADR-1671:69).
assert.equal(Object.isFrozen(WHEN_VOCABULARY), true);
assert.deepEqual(
[...WHEN_VOCABULARY].sort(),
@@ -492,10 +624,15 @@ describe('frozen when= vocabulary', () => {
'flag:--discuss',
'flag:--forensic',
'flag:--full',
'flag:--ingest',
'flag:--prd',
'flag:--research',
'flag:--research-phase',
'flag:--reset-phase-numbers',
'flag:--reviews',
'flag:--validate',
'flag:--wave',
'state:chunked-mode',
'state:gap-closure-phase',
'state:has-prior-phases',
'state:needs-codebase-map',
@@ -506,6 +643,63 @@ describe('frozen when= vocabulary', () => {
});
});
// ─── #2993 (epic #1671 Phase 6.2): widened when= vocabulary (19 atoms) ─────
// 50-test-matrix.md rows A1/A6.
describe('widened when= vocabulary (#2993)', () => {
// The 5 net-new atoms shipped by #2993, fragmentizing plan-phase.md.
const NET_NEW_ATOMS_2993 = Object.freeze([
'flag:--ingest',
'flag:--prd',
'flag:--research-phase',
'flag:--reviews',
'state:chunked-mode',
]);
test('everyNetNewAtomIsInWhenVocabulary', () => {
for (const atom of NET_NEW_ATOMS_2993) {
assert.ok(WHEN_VOCABULARY.includes(atom), `expected "${atom}" in WHEN_VOCABULARY`);
}
});
test('acceptsEveryWidenedAtom (row A1)', () => {
for (const when of NET_NEW_ATOMS_2993) {
const source = `<!-- gsd:section id="x" when="${when}" -->\nbody\n<!-- /gsd:section -->`;
const sections = parseWorkflowSections(source);
const explicitSections = sections.filter((s) => s.explicit);
assert.equal(explicitSections.length, 1, `expected acceptance for when="${when}"`);
assert.equal(explicitSections[0].when, when);
assert.equal(composeWorkflow(source), 'body\n');
}
});
test('researchPhaseAndResearchAreDistinctAtomsAtTheParserLevel (row A6)', () => {
// flag:--research-phase (net-new, #2993) and flag:--research (pre-existing,
// #2992) are DISTINCT vocabulary entries — the parser must accept both,
// as different `when` values, on sections that sit side by side, and must
// never conflate one for the other (e.g. via prefix-matching or a shared
// token derivation). See section-manifest.test.cjs for the predicate-level
// half of this guard (no aliasing at evaluation time).
const source = doc(
'<!-- gsd:section id="research-section" when="flag:--research" -->',
'researchBody',
'<!-- /gsd:section -->',
'<!-- gsd:section id="research-phase-section" when="flag:--research-phase" -->',
'researchPhaseBody',
'<!-- /gsd:section -->',
);
const sections = parseWorkflowSections(source);
const explicitSections = sections.filter((s) => s.explicit);
assert.deepEqual(
explicitSections.map((s) => ({ id: s.id, when: s.when })),
[
{ id: 'research-section', when: 'flag:--research' },
{ id: 'research-phase-section', when: 'flag:--research-phase' },
],
);
});
});
// ─── #2992 (epic #1671 Phase 6.1): widened when= vocabulary (14 atoms) ─────
// 50-test-matrix.md rows A2/A5/A6/A14/A19.