* fix(#4728): stop presenting the retired Gemini CLI as a supported runtime
#1928 removed the Gemini CLI runtime after Google sunset it on 2026-06-18, and
updated the ENGLISH docs. The locale mirrors and the runtime-loaded workflow
prose were not updated in the same change, and no gate asserts the ABSENCE of a
retired runtime, so both drifted quietly for a year.
The finding that shaped this change: English is already correct. docs/
ARCHITECTURE.md, CONFIGURATION.md, USER-GUIDE.md, how-to/install-on-your-runtime.md
and CLI-TOOLS.md carry zero runtime-axis Gemini references; the only English hits
anywhere are a Gemini 2.5 Pro MODEL line, the GEMINI_API_KEY row, and prose that
correctly documents the retirement. So the docs half of this is translation lag,
not a content decision, and every locale edit here is parity with an existing
English line rather than new wording:
- install-on-your-runtime.md English has NO `### Gemini CLI` section -> deleted
- USER-GUIDE.md :843 "…, Antigravity CLI, Kilo)" -> substituted
- ARCHITECTURE.md English has NO Gemini CLI table row -> row deleted
- ARCHITECTURE.md :24 English holds `Kimi CLI` in that slot -> Kimi CLI
- context-monitor.md :3 "`AfterTool` for Antigravity CLI" -> substituted
- spike-and-sketch.md :93 "(Codex, Antigravity CLI, etc.)" -> substituted
- configure-model-profiles "Codex, OpenCode, Antigravity CLI, or Kilo" -> substituted
- COMMANDS.md English keeps only hyphen + Codex bullets -> colon bullet deleted
- FEATURES.md source docs/features/multi-runtime-support.md:10
lists no Gemini CLI -> name removed
ARCHITECTURE.md:24 is the clearest case for reading English rather than
substituting blind: Antigravity ALREADY appears later in that list, so replacing
Gemini CLI with Antigravity would have named it twice. English holds Kimi CLI
there, so that is what the locales get.
The largest single class was hand-duplicated boilerplate. A "Text mode" paragraph
repeated across 34 runtime-loaded workflow files ends "…required for non-Claude
runtimes (OpenAI Codex, Gemini CLI, etc.)". No lint enforces that sentence and no
script syncs it, so every copy was edited. These files are read by the agent at
runtime, so they steer behavior rather than only informing a reader — which is why
this class matters more than its word count suggests.
The slash-command-form section is restructured in all four languages to match
English, which had already dropped its colon-form bullet. That bullet claimed the
colon form is "Gemini CLI only", which was false on its own terms independent of
the retirement: `/gsd:…` is GSD's canonical AUTHORING token, rewritten per runtime
at install time, and NO runtime registers it — VALID_COMMAND_STYLES is
{slash-hyphen, shell-var} and 18 of 19 runtimes declare slash-hyphen. Substituting
the runtime name would have left the claim false with Antigravity's name in it, so
the claim is gone, matching English.
Two anchor regressions were caught and fixed while doing that. zh-CN lost its
explicit {#slash-command-forms-hyphen-vs-colon} anchor while its TOC still linked
it; the anchor is restored. ko-KR and pt-BR never had an explicit anchor and rely
on the slug generated from the heading text, so shortening the heading broke their
own TOC links; those links now point at the new slugs. English's heading lost its
anchor while its TOC still links the old one — that latent English bug is
deliberately NOT copied.
Preserved, because `gemini` is not one thing here and a blanket sweep breaks the
product: ~/.gemini/antigravity{,-ide,-cli} and ~/.gemini as their parent;
~/.gemini/config (#3738); GEMINI.md; hookEvents "gemini"; GEMINI_API_KEY in all
four locales; every gemini-* model id and the Gemini 2.5 Pro references in
ko-KR/pt-BR/zh-CN (ja-JP genuinely lacks that line — the locales have diverged, so
a uniform patch would be wrong); the hook-event dialect notes, which are
RE-ATTRIBUTED rather than deleted because Antigravity inherits that dialect;
reapply-patches.md:93's legacy-install note; host-integration-capability-matrix.md
:27 and :342, which correctly record the sunset and Antigravity's contract;
whats-new-1.7.0.md and FEATURES.md:3506, which document the retirement itself; and
the generated launcher preamble, which belongs to epic #4632 — zero
_GSD_SHIM_NAME lines appear in this diff.
Coverage: a #4728 block in tests/gemini-runtime-removed.test.cjs asserts the
retired name is gone from STRUCTURAL POSITIONS (a level-3 heading, a table row's
first cell, a runtime-example parenthetical) rather than asserting the string is
absent, which would be wrong. It pairs those with positive PRESERVE assertions
over the same files — Antigravity's heading, ~/.gemini/antigravity, GEMINI_API_KEY,
AfterTool — so a patch that deletes too much fails as loudly as one that deletes
too little. The model-axis test pins both the presence in three locales and the
absence in ja-JP, so a later uniform patch that "helpfully" adds it back fails.
The new docs/ reads tripped lint-docs-guard-registration for the first time in
this file, so the test is registered in scripts/docs-guard-registry.cjs.
Not covered here, by design: nothing above would catch a Gemini-as-runtime
reference appearing in a NEW file tomorrow. That is the repo-wide drift guard,
#4729, which must land last — written now it would red on the very references this
change removes.
Fixes #4728
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(#4728): fix four review blockers, including a vacuous test and my own duplicate
A full matrix run on 31f12d7943 FAILED with 3 real failures, and an isolated
adversarial review returned BLOCK on four blockers. All of it was correct.
1. I committed the exact error I claimed to have avoided. The commit message
boasted that ARCHITECTURE.md:24 proved the value of reading English rather
than substituting blind, because Antigravity already appeared later in that
list. Five hundred lines further down the SAME four files, my
`Gemini:` -> `Antigravity:` substitution produced TWO consecutive
`- Antigravity:` bullets, because an Antigravity bullet was already there.
English (ARCHITECTURE.md:827) merges them into one. Now merged in all four
locales, reusing each locale's existing words.
2. `--gemini` survived in the runtime-detection CLI flag list in all four
locale ARCHITECTURE.md files. English:817 holds `--kimi` in that slot and
already lists `--antigravity` later, so this is another place where
substituting Antigravity would have duplicated it. Now `--kimi`.
3. Two runtime-loaded workflow files still enumerated Gemini one line ABOVE the
line I had already corrected -- the "Adaptive (Recommended)" option in
settings.md:192 and new-project/steps/auto-mode-config.md:95.
4. THE NEW TEST WAS VACUOUS for two of its five files. It matched only
`non-Claude runtimes (` and `(e.g. `, and neither regex could reach the two
lines the change actually fixed: health.md:52 reads `non-Claude (Codex, ...)`
without the word "runtimes", and execute-phase.md:1028 has no parenthetical
at all. The reviewer proved it by re-introducing Gemini at both lines and
watching the assertion stay GREEN. That same blind spot is what hid finding 3.
Replaced with a case-sensitive `/\bGemini\b/` walk over every
`gsd-core/workflows/**/*.md`, which works because every LEGITIMATE gemini
reference in that tree is spelled differently and cannot match: Antigravity's
paths are lowercase with a slash (`~/.gemini/antigravity`), Google's model ids
are lowercase and hyphenated (`gemini-3.1-pro-preview`), and the env vars are
uppercase (`GEMINI_CONFIG_DIR`, `GEMINI_SESSION_ID`). A bare capitalised
`Gemini` there means the retired RUNTIME is being named. The walk asserts it
found at least 50 files so an empty walk cannot pass vacuously, and it now
covers the nested `new-project/steps/` directory where finding 3 lived.
Two allowlist entries, both by line CONTENT and both justified:
reapply-patches.md's `Legacy: ... pre-#1928` note, and settings-advanced.md's
`Known provider` menu. The second was escalated by the agent rather than
decided: Section 8 of that file says model policy is defined "independently"
of the runtime, so `(Claude / OpenAI / Gemini / Qwen)` is the PROVIDER axis --
the same axis as the lowercase model ids -- and must keep working.
Proven to fail, not just asserted: the predicate reports 0 offenders on the
real tree and exactly 2 on a /tmp copy with Gemini re-injected at
health.md:52 and execute-phase.md:1028.
Also from the review: a `| Gemini |` COLUMN survived in the locale FEATURES.md
comparison tables (English has none) -- removed from all three, with header,
separator and every body row kept aligned; two ENGLISH runtime-axis sites were
missed by my own parity standard (how-to/execute-a-phase.md:88 and
how-to/verify-and-ship.md:89, the latter doubly stale since #4716 retired the
Gemini reviewer lane); docs/USER-GUIDE.md:12 linked a dead anchor, which I had
found and deliberately left -- record-and-proceed on a known defect is exactly
what the rules forbid, so it is fixed; docs/COMMANDS.md:12 and all four mirrors
still claimed "the hyphen and colon forms are runtime-specific spellings" with
no colon form documented anywhere, so that false sentence is deleted; and ko-KR
had the installer rather than the user doing the targeting.
The other two matrix failures were the compact-content benchmark baseline, which
drifted because this PR changes byte counts, refreshed via the script's own
`--write` path rather than by hand; and this commit's emitted-drift-ack trailers.
Method note on the acks: the failing run measured growth against
origin/next@1110c3b4ee, which is the STALE LOCAL `next` ref -- gsd-test merges
into the local base branch, and this machine's `next` is seven commits behind
origin/next, which is checked out in the main worktree and so cannot be
fast-forwarded from here. The 32 trailers below are computed against the REAL
base (origin/next @ ca8d9d4459) by comparing each tracked file's blob size, which
is one more file than that run reported -- the extra is settings.md, grown again
by fix 3. docs-update.md and map-codebase.md are deliberately NOT acked: they
SHRANK, since there the fix deleted ", Gemini CLI" rather than substituting, and
acking a file no delta consumed is itself an error.
Refs #4728
Emitted-Drift-Ack-Growth: add-tests.md — retiring the Gemini CLI runtime name; Antigravity is one byte longer
Emitted-Drift-Ack-Growth: add-todo.md — retiring the Gemini CLI runtime name; Antigravity is one byte longer
Emitted-Drift-Ack-Growth: ai-integration-phase.md — retiring the Gemini CLI runtime name; Antigravity is one byte longer
Emitted-Drift-Ack-Growth: check-todos.md — retiring the Gemini CLI runtime name; Antigravity is one byte longer
Emitted-Drift-Ack-Growth: cleanup.md — retiring the Gemini CLI runtime name; Antigravity is one byte longer
Emitted-Drift-Ack-Growth: complete-milestone.md — retiring the Gemini CLI runtime name; Antigravity is one byte longer
Emitted-Drift-Ack-Growth: do.md — retiring the Gemini CLI runtime name; Antigravity is one byte longer
Emitted-Drift-Ack-Growth: eval-review.md — retiring the Gemini CLI runtime name; Antigravity is one byte longer
Emitted-Drift-Ack-Growth: execute-phase.md — retiring the Gemini CLI runtime name; Antigravity is one byte longer
Emitted-Drift-Ack-Growth: execute-plan.md — retiring the Gemini CLI runtime name; Antigravity is one byte longer
Emitted-Drift-Ack-Growth: health.md — retiring the Gemini CLI runtime name; Antigravity is one byte longer
Emitted-Drift-Ack-Growth: import.md — retiring the Gemini CLI runtime name; Antigravity is one byte longer
Emitted-Drift-Ack-Growth: inbox.md — retiring the Gemini CLI runtime name; Antigravity is one byte longer
Emitted-Drift-Ack-Growth: manager.md — retiring the Gemini CLI runtime name; Antigravity is one byte longer
Emitted-Drift-Ack-Growth: new-milestone.md — retiring the Gemini CLI runtime name; Antigravity is one byte longer
Emitted-Drift-Ack-Growth: new-workspace.md — retiring the Gemini CLI runtime name; Antigravity is one byte longer
Emitted-Drift-Ack-Growth: note.md — retiring the Gemini CLI runtime name; Antigravity is one byte longer
Emitted-Drift-Ack-Growth: onboard.md — retiring the Gemini CLI runtime name; Antigravity is one byte longer
Emitted-Drift-Ack-Growth: plant-seed.md — retiring the Gemini CLI runtime name; Antigravity is one byte longer
Emitted-Drift-Ack-Growth: profile-user.md — retiring the Gemini CLI runtime name; Antigravity is one byte longer
Emitted-Drift-Ack-Growth: quick.md — retiring the Gemini CLI runtime name; Antigravity is one byte longer
Emitted-Drift-Ack-Growth: remove-workspace.md — retiring the Gemini CLI runtime name; Antigravity is one byte longer
Emitted-Drift-Ack-Growth: secure-phase.md — retiring the Gemini CLI runtime name; Antigravity is one byte longer
Emitted-Drift-Ack-Growth: settings.md — retiring the Gemini CLI runtime name; Antigravity is one byte longer
Emitted-Drift-Ack-Growth: ship.md — retiring the Gemini CLI runtime name; Antigravity is one byte longer
Emitted-Drift-Ack-Growth: smart-entry.md — retiring the Gemini CLI runtime name; Antigravity is one byte longer
Emitted-Drift-Ack-Growth: ui-phase.md — retiring the Gemini CLI runtime name; Antigravity is one byte longer
Emitted-Drift-Ack-Growth: ui-review.md — retiring the Gemini CLI runtime name; Antigravity is one byte longer
Emitted-Drift-Ack-Growth: undo.md — retiring the Gemini CLI runtime name; Antigravity is one byte longer
Emitted-Drift-Ack-Growth: update.md — retiring the Gemini CLI runtime name; Antigravity is one byte longer
Emitted-Drift-Ack-Growth: validate-phase.md — retiring the Gemini CLI runtime name; Antigravity is one byte longer
Emitted-Drift-Ack-Growth: verify-work.md — retiring the Gemini CLI runtime name; Antigravity is one byte longer
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* chore(#4728): add the changeset fragment
The PR body claimed one was present and it was not — caught by
scripts/changeset/lint.cjs reporting fail_missing_fragment, not by the
checklist, which is exactly why the lint exists.
Type Fixed: the diff is prose, and a docs-only fix uses Fixed since there is
no Documentation type.
Refs #4728
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: sim <sim@local>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
38 KiB
Start a new milestone cycle for an existing project. Loads project context, gathers milestone goals (from MILESTONE-CONTEXT.md or conversation), updates PROJECT.md and STATE.md, optionally runs parallel research, defines scoped requirements with REQ-IDs, spawns the roadmapper to create phased execution plan, and commits all artifacts. Brownfield equivalent of new-project.
<required_reading>
Read all files referenced by the invoking prompt's execution_context before starting.
</required_reading>
<available_agent_types> Valid GSD subagent types (use exact names — do not fall back to 'general-purpose'):
- gsd-project-researcher — Researches project-level technical decisions
- gsd-research-synthesizer — Synthesizes findings from parallel research agents
- gsd-roadmapper — Creates phased execution roadmaps </available_agent_types>
1. Load Context
Parse $ARGUMENTS before doing anything else:
--reset-phase-numbersflag → opt into restarting roadmap phase numbering at1. If absent, keep the current behavior of continuing phase numbering from the previous milestone.--ws <name>flag → active workstream scope, parsed intoGSD_WS- remaining text, with
--ws <name>stripped → use as milestone name if present, captured intoMILESTONE_ARG
Parse GSD_WS and MILESTONE_ARG using the established idiom (see verify-work.md):
_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}"; _gsd_at() { for _p; do if [ -f "$_p" ]; then GSD_TOOLS="$_p"; return 0; fi; done; return 1; }; if _gsd_at "${_GSD_RUNTIME_ROOT}/gsd-core/bin/${_GSD_SHIM_NAME}" "${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" "${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}"; then gsd_run() { node "$GSD_TOOLS" "$@"; }; elif unset -f gsd_run; _G="$(command -v gsd_run)"; then GSD_TOOLS="$_G"; gsd_run() { "$GSD_TOOLS" "$@"; }; elif _gsd_at "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/gsd-core/bin/${_GSD_SHIM_NAME}" "${HERMES_HOME:-$HOME/.hermes}/gsd-core/bin/${_GSD_SHIM_NAME}" "${CURSOR_CONFIG_DIR:-$HOME/.cursor}/gsd-core/bin/${_GSD_SHIM_NAME}" "${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}" "${GEMINI_CONFIG_DIR:-$HOME/.gemini}/gsd-core/bin/${_GSD_SHIM_NAME}" "${COPILOT_CONFIG_DIR:-$HOME/.copilot}/gsd-core/bin/${_GSD_SHIM_NAME}" "${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/gsd-core/bin/${_GSD_SHIM_NAME}" "${AUGMENT_CONFIG_DIR:-$HOME/.augment}/gsd-core/bin/${_GSD_SHIM_NAME}" "${TRAE_CONFIG_DIR:-$HOME/.trae}/gsd-core/bin/${_GSD_SHIM_NAME}" "${QWEN_CONFIG_DIR:-$HOME/.qwen}/gsd-core/bin/${_GSD_SHIM_NAME}" "${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/gsd-core/bin/${_GSD_SHIM_NAME}" "${CLINE_CONFIG_DIR:-$HOME/.cline}/gsd-core/bin/${_GSD_SHIM_NAME}" "${GROK_AGENTS_HOME:-$HOME/.agents}/gsd-core/bin/${_GSD_SHIM_NAME}" "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/gsd-core/bin/${_GSD_SHIM_NAME}" "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/gsd-core/bin/${_GSD_SHIM_NAME}" "${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/gsd-core/bin/${_GSD_SHIM_NAME}"; then gsd_run() { node "$GSD_TOOLS" "$@"; }; else echo "ERROR: gsd-tools.cjs not found at $GSD_TOOLS and gsd_run is not on PATH. Run: npx -y @opengsd/gsd-core@latest --claude --local" >&2; exit 1; fi; GSD_IDENTITY_STATUS=unverified; case "$(gsd_run runtime-identity --raw 2>/dev/null || true)" in '{"packageName":"@opengsd/gsd-core"'*'}') GSD_IDENTITY_STATUS=ok;; esac; export GSD_IDENTITY_STATUS; [ "$GSD_IDENTITY_STATUS" = ok ] || echo "WARNING: \"$GSD_TOOLS\" did not prove it is @opengsd/gsd-core - it is either a different package or an @opengsd/gsd-core older than the runtime-identity verb. See docs/how-to/diagnose-a-foreign-gsd-tools.md" >&2; 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_WS=""
echo "$ARGUMENTS" | grep -qE -- '--ws[[:space:]]+[A-Za-z0-9._-]+' && GSD_WS=$(echo "$ARGUMENTS" | grep -oE -- '--ws[[:space:]]+[A-Za-z0-9._-]+')
MILESTONE_ARG=$(echo "$ARGUMENTS" | sed -E 's/--ws[[:space:]]+[A-Za-z0-9._-]+//g' | xargs)
# #4456: persist GSD_WS to a file so later steps' bash fences (each a
# separate shell) can forward it — the same cross-fence problem Step 5/6
# already solve for OUTGOING_MILESTONE via .gsd-outgoing-milestone.
printf '%s' "$GSD_WS" > .planning/.gsd-ws-arg 2>/dev/null || true
RESPONSE_LANGUAGE=$(gsd_run query config-get response_language --raw --default "" 2>/dev/null || echo "")
# #2994: EARLY, section-manifest-only init.new-milestone call — needed here
# (before Step 4) to gate the project-md-milestone-write section. This is
# DELIBERATELY separate from Step 7's full init.new-milestone call below,
# which must stay AFTER Step 6's phase archival/phases.clear so its
# phase_dir_count / roadmap_exists / latest_completed_milestone fields
# reflect POST-archival state — moving that call here would compute those
# fields too early and corrupt the roadmapper's phase-numbering context.
# init.new-milestone is a pure read (no mutation), so calling it twice is
# safe; only `section_manifest` is consumed from this early call.
# #4456: $GSD_WS forwarded (same fence as the parse above, no round-trip
# needed here) so the section manifest — and the shared PROJECT.md write
# guard it gates — reflects the EXPLICITLY requested workstream, not
# whatever ambient GSD_WORKSTREAM/session pointer happens to be active.
INIT_EARLY=$(gsd_run query init.new-milestone $GSD_WS)
if [[ "$INIT_EARLY" == @file:* ]]; then INIT_EARLY=$(cat "${INIT_EARLY#@file:}"); fi
GSD_WS must chain to every downstream routing suggestion in this workflow (Step 4's shared-file guard, and the /gsd:discuss-phase//gsd:plan-phase routing hints below) per the routing-propagation contract in gsd-core/references/workstream-flag.md — never let it silently drop.
If response_language is set: All user-facing output of this workflow — narration between tool calls, status updates, progress notes, findings, questions, prompts, and explanations (including the "What do you want to build next?" prompt and seed-selection questions below) — MUST be presented in {response_language}. Technical terms, code, file paths, and subagent prompts stay in English — only user-facing output is translated.
- Read PROJECT.md (existing project, validated requirements, decisions)
- Read MILESTONES.md (what shipped previously)
- Read STATE.md (pending todos, blockers)
- Check for MILESTONE-CONTEXT.md (from /gsd-discuss-milestone)
2. Gather Milestone Goals
If MILESTONE-CONTEXT.md exists:
- Use features and scope from discuss-milestone
- Present summary for confirmation
If no context file:
- Present what shipped in last milestone
Text mode (workflow.text_mode: true in config or --text flag): Set TEXT_MODE=true if --text is present in $ARGUMENTS OR text_mode from init JSON is true. When TEXT_MODE is active, replace every AskUserQuestion call with a plain-text numbered list and ask the user to type their choice number. This is required for non-Claude runtimes (OpenAI Codex, Antigravity, etc.) where AskUserQuestion is not available.
- Ask inline (freeform, NOT AskUserQuestion): "What do you want to build next?"
- Wait for their response, then use AskUserQuestion to probe specifics
- If user selects "Other" at any point to provide freeform input, ask follow-up as plain text — not another AskUserQuestion
2.5. Scan Planted Seeds
Check .planning/seeds/ for seed files that match the milestone goals gathered in step 2.
ls .planning/seeds/SEED-*.md 2>/dev/null
If no seed files exist: Skip this step silently — do not print any message or prompt.
If seed files exist: Read each SEED-*.md file and extract from its frontmatter and body:
- Idea — the seed title (heading after frontmatter, e.g.
# SEED-001: <idea>) - Trigger conditions — the
trigger_whenfrontmatter field and the "When to Surface" section's bullet list - Planted during — the
planted_duringfrontmatter field (for context)
Compare each seed's trigger conditions against the milestone goals from step 2. A seed matches when its trigger conditions are relevant to any of the milestone's target features or goals.
If no seeds match: Skip silently — do not prompt the user.
If matching seeds found:
--auto mode: Auto-select ALL matching seeds. Log: [auto] Selected N matching seed(s): [list seed names]
Text mode (TEXT_MODE=true): Present matching seeds as a plain-text numbered list:
Seeds that match your milestone goals:
1. SEED-001: <idea> (trigger: <trigger_when>)
2. SEED-003: <idea> (trigger: <trigger_when>)
Enter numbers to include (comma-separated), or "none" to skip:
Normal mode: Present via AskUserQuestion:
AskUserQuestion(
header: "Seeds",
question: "These planted seeds match your milestone goals. Include any in this milestone's scope?",
multiSelect: true,
options: [
{ label: "SEED-001: <idea>", description: "Trigger: <trigger_when> | Planted during: <planted_during>" },
...
]
)
After selection:
- Selected seeds become additional context for requirement definition in step 9. Store them in an accumulator (e.g.
$SELECTED_SEEDS) so step 9 can reference the ideas and their "Why This Matters" sections when defining requirements. - Unselected seeds remain untouched in
.planning/seeds/— never delete or modify seed files during this workflow.
3. Determine Milestone Version
- Parse last version from MILESTONES.md
- Suggest next version (v1.0 → v1.1, or v2.0 for major)
- Confirm with user
3.5. Verify Milestone Understanding
Before writing any files, present a summary of what was gathered and ask for confirmation.
### GSD ► MILESTONE SUMMARY
**Milestone v[X.Y]: [Name]**
**Goal:** [One sentence]
**Target features:**
- [Feature 1]
- [Feature 2]
- [Feature 3]
**Key context:** [Any important constraints, decisions, or notes from questioning]
AskUserQuestion:
- header: "Confirm?"
- question: "Does this capture what you want to build in this milestone?"
- options:
- "Looks good" — Proceed to write PROJECT.md
- "Adjust" — Let me correct or add details
If "Adjust": Ask what needs changing (plain text, NOT AskUserQuestion). Incorporate changes, re-present the summary. Loop until "Looks good" is selected.
If "Looks good": Proceed to Step 4.
4. Update PROJECT.md
PROJECT.md is shared across workstreams (gsd-core/references/workstream-flag.md marks it # Shared in the directory diagram). This step has two independently-scoped parts — only Part A is workstream-guarded.
If section_manifest (from INIT_EARLY) is null or "project-md-milestone-write" is in its included list: read and execute gsd-core/workflows/new-milestone/steps/project-md-milestone-write.md. Otherwise (a workstream is active) skip — do not read the file; Part B below still runs regardless of GSD_WS.
Part B — Evolution structural repair (always runs, regardless of GSD_WS). ## Evolution is a shared, idempotent structural section, not workstream state — a pre-Evolution project must be backfilled whether or not a workstream is active, so this part is NOT covered by Part A's skip. Ensure the ## Evolution section exists in PROJECT.md. If missing (projects created before this feature), add it before the footer:
## Evolution
This document evolves at phase transitions and milestone boundaries.
**After each phase transition** (via `/gsd-transition`):
1. Requirements invalidated? → Move to Out of Scope with reason
2. Requirements validated? → Move to Validated with phase reference
3. New requirements emerged? → Add to Active
4. Decisions to log? → Add to Key Decisions
5. "What This Is" still accurate? → Update if drifted
**After each milestone** (via `/gsd:complete-milestone`):
1. Full review of all sections
2. Core Value check — still the right priority?
3. Audit Out of Scope — reasons still valid?
4. Update Context with current state
5. Update STATE.md
Reset STATE.md frontmatter AND body atomically via the SDK. This writes the new
milestone version/name into the YAML frontmatter, resets status to
planning, zeroes progress.* counters, and rewrites the ## Current Position
section to the new-milestone template. Accumulated Context (decisions,
blockers, todos) is preserved across the switch — symmetric with
milestone.complete.
GSD_WS_ARG=$(cat .planning/.gsd-ws-arg 2>/dev/null || true)
OUTGOING_MILESTONE=$(gsd_run query state.get milestone --raw $GSD_WS_ARG 2>/dev/null || true)
printf '%s' "$OUTGOING_MILESTONE" > .planning/.gsd-outgoing-milestone 2>/dev/null || true
echo "Outgoing milestone (phase history archives under THIS version in step 6): ${OUTGOING_MILESTONE:-<unknown>}"
gsd_run query state.milestone-switch --milestone "v[X.Y]" --name "[Name]" $GSD_WS_ARG
Capture the outgoing version now. The lines above read the current (previous) milestone
version BEFORE the switch flips STATE.md's milestone: field to the new one, and persist it to
.planning/.gsd-outgoing-milestone so Step 6 can consume it via a shell variable — do NOT
transcribe the echoed value into a later command by hand. Step 6 reads that file back into
--archive-version so the previous milestone's phase directories archive under
<outgoing-version>-phases/, not the new one (#2288). Once state.milestone-switch runs,
current-milestone state no longer holds the outgoing version, which is why it is captured here.
The resulting Current Position section looks like:
## Current Position
Phase: Not started (defining requirements)
Plan: —
Status: Defining requirements
Last activity: [today] — Milestone v[X.Y] started
Bug #2630: a prior version of this workflow rewrote the Current Position body
manually but left the frontmatter pointing at the previous milestone, so every
downstream reader (state.json, getMilestoneInfo, progress bars) reported the
stale milestone until the first phase advance forced a resync. Always use the
SDK handler above — do not hand-edit STATE.md here.
6. Cleanup and Commit
Delete MILESTONE-CONTEXT.md if exists (consumed).
Clear leftover phase directories from the previous milestone. Read the outgoing version
persisted in Step 5 back into a shell variable and pass it as --archive-version so the
archive lands under the previous milestone's label — the switch in Step 5 has already
advanced current-milestone state, so without this override the archive would be mislabeled
with the new version (#2288). Use the shell variable directly (quoted) — never hand-retype
the captured value into the command, so untrusted STATE.md content cannot be re-parsed by the
shell:
GSD_WS_ARG=$(cat .planning/.gsd-ws-arg 2>/dev/null || true)
OUTGOING_MILESTONE=$(cat .planning/.gsd-outgoing-milestone 2>/dev/null || true)
if [ -n "$OUTGOING_MILESTONE" ]; then
gsd_run query phases.clear --confirm --archive-version "$OUTGOING_MILESTONE" $GSD_WS_ARG
else
gsd_run query phases.clear --confirm $GSD_WS_ARG
fi
rm -f .planning/.gsd-outgoing-milestone 2>/dev/null || true
If the captured file is empty or absent (a fresh project with no prior milestone), the
fallback branch runs phases.clear --confirm with no override — it then uses current-milestone
state, and a dated archive label only if no version label is resolvable at all. phases.clear
rejects any --archive-version value that is not a plain version token (no path separators or
..), so a malformed capture fails loudly rather than writing outside the archive directory.
Stage the phase archive move + source removal so they land in the same commit as the milestone start (atomic — no orphaned uncommitted deletions, no un-archived dirs carried forward). phases.clear archives each non-999 dir to milestones/<version>-phases/; staging both dirs captures the new archive and the removals together (#1871).
COMMIT_DOCS=$(gsd_run query config-get commit_docs --raw 2>/dev/null || echo "true")
GSD_WS_ARG=$(cat .planning/.gsd-ws-arg 2>/dev/null || true)
INIT_STAGE=$(gsd_run query init.new-milestone $GSD_WS_ARG)
if [[ "$INIT_STAGE" == @file:* ]]; then INIT_STAGE=$(cat "${INIT_STAGE#@file:}"); fi
_gsd_field() { node -e "const o=JSON.parse(process.argv[1]); const v=o[process.argv[2]]; process.stdout.write(v==null?'':String(v))" "$1" "$2"; }
ARCHIVE_DIR=$(_gsd_field "$INIT_STAGE" archive_dir)
PHASES_DIR=$(_gsd_field "$INIT_STAGE" phases_dir)
if [ "$COMMIT_DOCS" != "false" ]; then
git add "$ARCHIVE_DIR/" "$PHASES_DIR/" 2>/dev/null || true
fi
When commit_docs is false, the archive move and phase removals are deliberately left unstaged here — not a bug — since Step 6's commit is skipped too.
Stage PROJECT.md in both modes. Step 4's Part A guard — not this commit — is what protects the shared ## Current Milestone heading (#2308): when a workstream is active Part A never writes it, so the only change PROJECT.md can carry here is Part B's idempotent ## Evolution backfill, which must be committed rather than stranded as a dangling edit. Do NOT reintroduce a [ -n "$GSD_WS" ] branch around this commit: GSD_WS is set in Step 1's shell and each step's bash block runs in its own shell (the same reason Step 5 round-trips OUTGOING_MILESTONE through a file), so such a guard reads an unset variable, always takes the flat-mode branch, and only appears to work. STATE.md, unlike PROJECT.md, IS workstream-scoped (Step 5's switch just wrote the workstream's own copy) — resolved below via init.new-milestone rather than a literal .planning/STATE.md, which would commit the wrong (or a stale, unrelated) file under an active workstream.
GSD_WS_ARG=$(cat .planning/.gsd-ws-arg 2>/dev/null || true)
INIT_COMMIT=$(gsd_run query init.new-milestone $GSD_WS_ARG)
if [[ "$INIT_COMMIT" == @file:* ]]; then INIT_COMMIT=$(cat "${INIT_COMMIT#@file:}"); fi
_gsd_field() { node -e "const o=JSON.parse(process.argv[1]); const v=o[process.argv[2]]; process.stdout.write(v==null?'':String(v))" "$1" "$2"; }
STATE_PATH=$(_gsd_field "$INIT_COMMIT" state_path)
PROJECT_PATH=$(_gsd_field "$INIT_COMMIT" project_path)
gsd_run query commit "docs: start milestone v[X.Y] [Name]" --files "$PROJECT_PATH" "$STATE_PATH"
7. Load Context and Resolve Models
RESET_PHASE_NUMBERS_PARAM=""; if [[ "$ARGUMENTS" =~ (^|[[:space:]])--reset-phase-numbers([[:space:]]|$) ]]; then RESET_PHASE_NUMBERS_PARAM="--reset-phase-numbers"; fi
GSD_WS_ARG=$(cat .planning/.gsd-ws-arg 2>/dev/null || true)
INIT=$(gsd_run query init.new-milestone $RESET_PHASE_NUMBERS_PARAM $GSD_WS_ARG)
if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
AGENT_SKILLS_RESEARCHER=$(gsd_run query agent-skills gsd-project-researcher)
AGENT_SKILLS_SYNTHESIZER=$(gsd_run query agent-skills gsd-research-synthesizer)
AGENT_SKILLS_ROADMAPPER=$(gsd_run query agent-skills gsd-roadmapper)
Extract from init JSON: researcher_model, synthesizer_model, roadmapper_model, commit_docs, research_enabled, current_milestone, project_exists, roadmap_exists, latest_completed_milestone, phase_dir_count, phase_archive_path, agents_installed, missing_agents, project_path, roadmap_path, requirements_path, config_path, research_dir, milestones_path, phases_dir, archive_dir.
If agents_installed is false: Display a warning before proceeding:
⚠ GSD agents not installed. The following agents are missing from your agents directory:
{missing_agents joined with newline}
Subagent spawns (gsd-project-researcher, gsd-research-synthesizer, gsd-roadmapper) will fail
with "agent type not found". Run the installer with --global to make agents available:
npx @opengsd/gsd-core@latest --global
Proceeding without research subagents — roadmap will be generated inline.
Skip the parallel research spawn step and generate the roadmap inline.
If section_manifest is null or "reset-phase-safety" is in its included list: read and execute gsd-core/workflows/new-milestone/steps/reset-phase-safety.md. Otherwise skip — do not read the file.
8. Research Decision
Check research_enabled from init JSON (loaded from config).
If research_enabled is true:
AskUserQuestion: "Research the domain ecosystem for new features before defining requirements?"
- "Research first (Recommended)" — Discover patterns, features, architecture for NEW capabilities
- "Skip research for this milestone" — Go straight to requirements (does not change your default)
If research_enabled is false:
AskUserQuestion: "Research the domain ecosystem for new features before defining requirements?"
- "Skip research (current default)" — Go straight to requirements
- "Research first" — Discover patterns, features, architecture for NEW capabilities
IMPORTANT: Do NOT persist this choice to config.json. The workflow.research setting is a persistent user preference that controls plan-phase behavior across the project. Changing it here would silently alter future /gsd:plan-phase behavior. To change the default, use /gsd:settings.
If user chose "Research first":
### GSD ► RESEARCHING
◆ Spawning 4 researchers in parallel... (each runs in a subagent — no output until they return, ~1–5 min; expected, not a freeze)
→ Stack, Features, Architecture, Pitfalls
mkdir -p .planning/research
Spawn 4 parallel gsd-project-researcher agents. Each uses this template with dimension-specific fields:
Common structure for all 4 researchers:
Model omission (#2517). Omit the
modelparameter entirely when the value it would carry (researcher_model,synthesizer_model,roadmapper_model) is"inherit"or empty. An empty value 404s on runtimes without native tier aliases — the default on non-Claude runtimes. Omitting it inherits the orchestrator's model. See @gsd-core/references/model-profile-resolution.md.
Agent(prompt="
<research_type>Project Research — {DIMENSION} for [new features].</research_type>
<milestone_context>
SUBSEQUENT MILESTONE — Adding [target features] to existing app.
{EXISTING_CONTEXT}
Focus ONLY on what's needed for the NEW features.
</milestone_context>
<question>{QUESTION}</question>
<required_reading>
- {project_path} (Project context)
</required_reading>
${AGENT_SKILLS_RESEARCHER}
<downstream_consumer>{CONSUMER}</downstream_consumer>
<quality_gate>{GATES}</quality_gate>
<!-- #2508 runtime-aware-dispatch -->
> **Runtime-aware dispatch (#2508 Phase 4).** GSD workflows dispatch specialized subagents by role. Before dispatching on a built-in-only runtime (kimi-code — three built-ins only), resolve the role to a built-in via `gsd_run query resolve-dispatch-type --requested <role> --raw`. On named-dispatch runtimes (Claude/OpenCode/…) the role is returned unchanged; on kimi-code it maps to `coder`/`explore`/`plan` by role-suffix. The persona rides `${AGENT_SKILLS_<ROLE>}` (Phase 3) regardless. See @gsd-core/references/runtime-aware-dispatch.md.
<output>
Write to: {research_dir}/{FILE}
Use template: ~/.claude/gsd-core/templates/research-project/{FILE}
</output>
", subagent_type="gsd-project-researcher", model="{researcher_model}", description="{DIMENSION} research")
Dimension-specific fields:
| Field | Stack | Features | Architecture | Pitfalls |
|---|---|---|---|---|
| EXISTING_CONTEXT | Existing validated capabilities (DO NOT re-research): [from PROJECT.md] | Existing features (already built): [from PROJECT.md] | Existing architecture: [from PROJECT.md or codebase map] | Focus on common mistakes when ADDING these features to existing system |
| QUESTION | What stack additions/changes are needed for [new features]? | How do [target features] typically work? Expected behavior? | How do [target features] integrate with existing architecture? | Common mistakes when adding [target features] to [domain]? |
| CONSUMER | Specific libraries with versions for NEW capabilities, integration points, what NOT to add | Table stakes vs differentiators vs anti-features, complexity noted, dependencies on existing | Integration points, new components, data flow changes, suggested build order | Warning signs, prevention strategy, which phase should address it |
| GATES | Versions current (verify with Context7), rationale explains WHY, integration considered | Categories clear, complexity noted, dependencies identified | Integration points identified, new vs modified explicit, build order considers deps | Pitfalls specific to adding these features, integration pitfalls covered, prevention actionable |
| FILE | STACK.md | FEATURES.md | ARCHITECTURE.md | PITFALLS.md |
ORCHESTRATOR RULE — CODEX RUNTIME: After calling all 4 researcher Agent() calls above, do NOT read research files or synthesize content independently while the subagents are active. Wait for all 4 researchers to complete before spawning the synthesizer. This prevents duplicate work and wasted context.
After all 4 complete, spawn synthesizer:
Agent(prompt="
Synthesize research outputs into SUMMARY.md.
<required_reading>
- {research_dir}/STACK.md
- {research_dir}/FEATURES.md
- {research_dir}/ARCHITECTURE.md
- {research_dir}/PITFALLS.md
</required_reading>
${AGENT_SKILLS_SYNTHESIZER}
Write to: {research_dir}/SUMMARY.md
Use template: ~/.claude/gsd-core/templates/research-project/SUMMARY.md
Commit after writing.
", subagent_type="gsd-research-synthesizer", model="{synthesizer_model}", description="Synthesize research")
ORCHESTRATOR RULE — CODEX RUNTIME: 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.
Synthesizer output self-heal (#222) — verify SUMMARY.md materialized: The synthesizer's canonical output is .planning/research/SUMMARY.md on disk; its brief structured return (## SYNTHESIS COMPLETE plus a few ### confirmation lines) is NOT the file content. A known LLM false-refusal (issue #222) sometimes makes the agent return the full SUMMARY.md document inline — fabricating a write restriction (e.g. "the runtime is blocking file writes") — instead of writing the file. Prompt hardening alone does not fully eliminate it, so the orchestrator MUST absorb the failure deterministically before spawning gsd-roadmapper:
- Verify
.planning/research/SUMMARY.mdexists AND is substantive — non-empty, and free of any leftover<!-- gsd:write-continue -->continuation sentinel (which marks a truncated/incomplete write). You may validate withgsd_run verify-summary .planning/research/SUMMARY.md— it exits 0 regardless, so check its JSONpassedfield ("passed": falsemeans missing or invalid), not the process exit code. If it passes, continue normally. - If it is MISSING or invalid AND the synthesizer's return message contains the FULL SUMMARY.md document — recognizable by the template's top-level markers
# Project Research Summary,## Key Findings,## Implications for Roadmap, and## Sources, not merely the brief## SYNTHESIS COMPLETEconfirmation — the false-refusal fired: write that returned document to.planning/research/SUMMARY.mdwith the Write tool, then commit ALL research artifacts the synthesizer owns (it commits on behalf of the four researchers) withgsd_run query commit "docs: complete project research" --files .planning/research/unless they are already committed. Log⚠ #222 self-heal: synthesizer returned SUMMARY.md inline without writing it; orchestrator persisted the file. - If it is MISSING or invalid AND the return is only a brief confirmation (no full SUMMARY document to recover), the synthesizer genuinely failed — surface the error and stop; do NOT spawn
gsd-roadmapperagainst a missing or incomplete SUMMARY.md.
This guarantees gsd-roadmapper (which lists SUMMARY.md as required reading) never runs against a missing or truncated SUMMARY.md.
Display key findings from SUMMARY.md:
### GSD ► RESEARCH COMPLETE ✓
**Stack additions:** [from SUMMARY.md]
**Feature table stakes:** [from SUMMARY.md]
**Watch Out For:** [from SUMMARY.md]
If "Skip research": Continue to Step 9.
9. Define Requirements
### GSD ► DEFINING REQUIREMENTS
Read PROJECT.md: core value, current milestone goals, validated requirements (what exists).
If $SELECTED_SEEDS is non-empty (from step 2.5): Include selected seed ideas and their "Why This Matters" sections as additional input when defining requirements. Seeds provide user-validated feature ideas that should be incorporated into the requirement categories alongside research findings or conversation-gathered features.
If research exists: Read FEATURES.md, extract feature categories.
Present features by category:
## [Category 1]
**Table stakes:** Feature A, Feature B
**Differentiators:** Feature C, Feature D
**Research notes:** [any relevant notes]
If no research: Gather requirements through conversation. Ask: "What are the main things users need to do with [new features]?" Clarify, probe for related capabilities, group into categories.
Scope each category via AskUserQuestion (multiSelect: true, header max 12 chars):
- "[Feature 1]" — [brief description]
- "[Feature 2]" — [brief description]
- "None for this milestone" — Defer entire category
Track: Selected → this milestone. Unselected table stakes → future. Unselected differentiators → out of scope.
Identify gaps via AskUserQuestion:
- "No, research covered it" — Proceed
- "Yes, let me add some" — Capture additions
Generate REQUIREMENTS.md:
- v1 Requirements grouped by category (checkboxes, REQ-IDs)
- Future Requirements (deferred)
- Out of Scope (explicit exclusions with reasoning)
- Traceability section (empty, filled by roadmap)
REQ-ID format: [CATEGORY]-[NUMBER] (AUTH-01, NOTIF-02). Continue numbering from existing.
Requirement quality criteria:
Good requirements are:
- Specific and testable: "User can reset password via email link" (not "Handle password reset")
- User-centric: "User can X" (not "System does Y")
- Atomic: One capability per requirement (not "User can login and manage profile")
- Independent: Minimal dependencies on other requirements
Present FULL requirements list for confirmation:
## Milestone v[X.Y] Requirements
### [Category 1]
- [ ] **CAT1-01**: User can do X
- [ ] **CAT1-02**: User can do Y
### [Category 2]
- [ ] **CAT2-01**: User can do Z
Does this capture what you're building? (yes / adjust)
If "adjust": Return to scoping.
Commit requirements:
GSD_WS_ARG=$(cat .planning/.gsd-ws-arg 2>/dev/null || true)
INIT_REQ=$(gsd_run query init.new-milestone $GSD_WS_ARG)
if [[ "$INIT_REQ" == @file:* ]]; then INIT_REQ=$(cat "${INIT_REQ#@file:}"); fi
_gsd_field() { node -e "const o=JSON.parse(process.argv[1]); const v=o[process.argv[2]]; process.stdout.write(v==null?'':String(v))" "$1" "$2"; }
REQUIREMENTS_PATH=$(_gsd_field "$INIT_REQ" requirements_path)
gsd_run query commit "docs: define milestone v[X.Y] requirements" --files "$REQUIREMENTS_PATH"
10. Create Roadmap
### GSD ► CREATING ROADMAP
◆ Spawning roadmapper... (runs in a subagent — no output until it returns, ~1–5 min; expected, not a freeze)
Starting phase number:
- If
--reset-phase-numbersis active, start at Phase 1 - Otherwise, continue from the previous milestone's last phase number (v1.0 ended at phase 5 → v1.1 starts at phase 6)
Agent(prompt="
<planning_context>
<required_reading>
- {project_path}
- {requirements_path}
- {research_dir}/SUMMARY.md (if exists)
- {config_path}
- {milestones_path}
</required_reading>
${AGENT_SKILLS_ROADMAPPER}
</planning_context>
<instructions>
Create roadmap for milestone v[X.Y]:
1. Respect the selected numbering mode:
- `--reset-phase-numbers` → start at Phase 1
- default behavior → continue from the previous milestone's last phase number
2. Derive phases from THIS MILESTONE's requirements only
3. Map every requirement to exactly one phase
4. Derive 2-5 success criteria per phase (observable user behaviors)
5. Validate 100% coverage
6. Write files immediately (ROADMAP.md, STATE.md, update REQUIREMENTS.md traceability)
7. Return ROADMAP CREATED with summary
Write files first, then return.
</instructions>
", subagent_type="gsd-roadmapper", model="{roadmapper_model}", description="Create roadmap")
ORCHESTRATOR RULE — CODEX RUNTIME: 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:
If ## ROADMAP BLOCKED: Present blocker, work with user, re-spawn.
If ## ROADMAP CREATED: Read ROADMAP.md, present inline:
## Proposed Roadmap
**[N] phases** | **[X] requirements mapped** | All covered ✓
| # | Phase | Goal | Requirements | Success Criteria |
|---|-------|------|--------------|------------------|
| [N] | [Name] | [Goal] | [REQ-IDs] | [count] |
### Phase Details
**Phase [N]: [Name]**
Goal: [goal]
Requirements: [REQ-IDs]
Success criteria:
1. [criterion]
2. [criterion]
Ask for approval via AskUserQuestion:
- "Approve" — Commit and continue
- "Adjust phases" — Tell me what to change
- "Review full file" — Show raw ROADMAP.md
If "Adjust": Get notes, re-spawn roadmapper with revision context, loop until approved. If "Review": Display raw ROADMAP.md, re-ask.
Commit roadmap (after approval):
GSD_WS_ARG=$(cat .planning/.gsd-ws-arg 2>/dev/null || true)
INIT_ROADMAP=$(gsd_run query init.new-milestone $GSD_WS_ARG)
if [[ "$INIT_ROADMAP" == @file:* ]]; then INIT_ROADMAP=$(cat "${INIT_ROADMAP#@file:}"); fi
_gsd_field() { node -e "const o=JSON.parse(process.argv[1]); const v=o[process.argv[2]]; process.stdout.write(v==null?'':String(v))" "$1" "$2"; }
ROADMAP_PATH=$(_gsd_field "$INIT_ROADMAP" roadmap_path)
STATE_PATH=$(_gsd_field "$INIT_ROADMAP" state_path)
REQUIREMENTS_PATH=$(_gsd_field "$INIT_ROADMAP" requirements_path)
gsd_run query commit "docs: create milestone v[X.Y] roadmap ([N] phases)" --files "$ROADMAP_PATH" "$STATE_PATH" "$REQUIREMENTS_PATH"
# #4456: true last consumer of the persisted --ws in this workflow — the
# round-trip file is no longer needed after this commit.
rm -f .planning/.gsd-ws-arg 2>/dev/null || true
10.5. Link Pending Todos to Roadmap Phases
After roadmap approval, scan pending todos against the newly approved phases. For each todo whose scope matches a phase, tag it with resolves_phase: N in its YAML frontmatter.
Check for pending todos:
PENDING_TODOS=$(ls .planning/todos/pending/*.md 2>/dev/null | head -50)
If no pending todos exist: Skip this step silently.
If pending todos exist:
Read the approved ROADMAP.md and extract the phase list: phase number, phase name, goal, and requirement IDs.
For each pending todo, compare:
- The todo's
titleandareafrontmatter fields - The todo body (Problem and Solution sections)
Against each phase's:
- Phase goal
- Requirement IDs and descriptions
Match criteria (best-effort — do not over-match): A todo is considered resolved by a phase if the phase's goal or requirements directly describe implementing the same feature, area, or capability as the todo. Narrow, specific todos with concrete scopes are the best candidates. Vague or cross-cutting todos should be left unlinked.
For each matched todo, add resolves_phase: [N] to the YAML frontmatter block (after the existing fields):
---
created: [existing]
title: [existing]
area: [existing]
resolves_phase: [N]
files: [existing]
---
Only modify todos that have a clear, confident match. Leave unmatched todos unmodified.
If any todos were linked:
gsd_run query commit "docs: tag [count] pending todos with resolves_phase after milestone v[X.Y] roadmap" --files .planning/todos/pending/*.md
Print a summary:
◆ Linked [N] pending todos to roadmap phases:
→ [todo title] → Phase [N]: [Phase Name]
(Leave [M] unmatched todos in pending/)
11. Done
### GSD ► MILESTONE INITIALIZED ✓
**Milestone v[X.Y]: [Name]**
| Artifact | Location |
|----------------|-----------------------------|
| Project | `.planning/PROJECT.md` |
| Research | `.planning/research/` |
| Requirements | `.planning/REQUIREMENTS.md` |
| Roadmap | `.planning/ROADMAP.md` |
**[N] phases** | **[X] requirements** | Ready to build ✓
## ▶ Next Up — [${PROJECT_CODE}] ${PROJECT_TITLE}
**Phase [N]: [Phase Name]** — [Goal]
`/clear` then:
`/gsd:discuss-phase [N] ${GSD_WS}` — gather context and clarify approach
Also: `/gsd:plan-phase [N] ${GSD_WS}` — skip discussion, plan directly
<success_criteria>
- PROJECT.md updated with Current Milestone section (skipped when a workstream is active — shared file, see Step 4)
- STATE.md reset for new milestone
- MILESTONE-CONTEXT.md consumed and deleted (if existed)
- Research completed (if selected) — 4 parallel agents, milestone-aware
- Requirements gathered and scoped per category
- REQUIREMENTS.md created with REQ-IDs
- gsd-roadmapper spawned with phase numbering context
- Roadmap files written immediately (not draft)
- User feedback incorporated (if any)
- Phase numbering mode respected (continued or reset)
- All commits made (if planning docs committed)
- Pending todos scanned for phase matches; matched todos tagged with
resolves_phase: N - User knows next step:
/gsd:discuss-phase [N] ${GSD_WS}
Atomic commits: Each phase commits its artifacts immediately. </success_criteria>