* feat(#3841): assert gsd-tools identity before any state-mutating verb only this package publishes. The path-based branches — a project-local install, a runtime config directory — had no such guarantee; they trusted their configured location. This closes them. Mechanism: once resolution finishes, and before any verb runs, the preamble probes the tool it picked with `runtime-identity --raw` and matches the answer with a shell `case` pattern ANCHORED to the start of the compact payload (`{"packageName":"@opengsd/gsd-core"`). An unanchored substring match accepts the decoy `{"packageName":"get-shit-done-cc","note":"@opengsd/gsd-core"}`, which any colliding package could publish. The outcome is exported as the two-valued `GSD_IDENTITY_STATUS` (`ok`/`unverified`), so the gate is asserted on a VALUE rather than on warning prose. Rollout is warn-then-fail per the #3146 ruling: `unverified` prints one line naming BOTH causes and continues, because `no_identity_verb` cannot tell a foreign package from an `@opengsd/gsd-core` older than the verb, and at rollout the old-version case is the common one. The blocker was byte budget, not design. The preamble is inlined into 112 shipped files and several sat within single-digit bytes of frozen ceilings (`gsd-verifier.md` 16 bytes, `gsd-executor.md` 33, `execute-phase.md` 234); a first attempt broke five of them. What made room was collapsing the resolver's twenty near-identical `elif [ -f … ]` arms into one candidate-list helper (`_gsd_at`), which buys far more than the assertion costs. The preamble is now 2,624 bytes against 4,500 — a net 1,876 bytes SMALLER per inlined file, so every capped file moved away from its ceiling rather than toward it. No cap raised, no size-budget exception added, no override token emitted. Resolution order, every runtime-home probe, the `unset -f gsd_run` re-source fix, the fail-closed `exit 1`, and the `CLAUDE_ENV_FILE` persistence are all preserved byte-for-byte in substring terms; the snippet still begins with `_GSD_SHIM_NAME=` and still ends with `fi`, which the parity extractors anchor on. `gsd-core/references/gsd-run-resolver.md` is re-synced byte-equal. Also fixes two stale claims found in passing: CONTEXT.md and FEATURES.md both described an `[ -x ]` guard as the load-bearing re-source defense. That guard was tried and REMOVED in #3831 — it rejected the bare function name, fell through every branch, and hit `exit 1`, which kills a sourced caller's shell. `unset -f gsd_run` is the actual mechanism. Refs #3841 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(#3841): pair the anchor's brace by requiring a closed identity payload The matrix went red on `tests/new-project-mvp-prompt.test.cjs` — "new-project.md has unbalanced braces: net depth 2" — plus a knock-on report from its parent `bug #1516` describe, which is the same failure counted once at the child and once at the block. Root cause: that guard (:182-189, mirroring #3784 bd53925f) walks characters and increments on `{`, decrements on `}`, with no awareness of shell quoting. It scans `new-project.md` PLUS every `new-project/steps/*.md`, and both `new-project.md` and `steps/auto-mode-config.md` carry one inlined preamble copy — hence net 2 from a snippet that was off by exactly one. The unpaired brace was the `{` inside the single-quoted `case` pattern of the identity anchor, which is correct shell and invisible to a text scanner. Fix in the snippet, not the guard. The pattern now anchors at BOTH ends: `'{"packageName":"@opengsd/gsd-core"'*'}'`. That balances 51/51 with a brace that does real work rather than a cosmetic pair — a truncated payload whose prefix matches now fails too, where before it verified. Safe for any future additive field: a JSON object's own closing brace is always the last character, whatever type the last value has, which is pinned by two negative-space tests (a nested object and an array-valued last key must both still verify). Cost: +3 bytes, against the 1,873 the resolver fold already gave back. The alternative considered and rejected was dropping the literal `{` for a `?` glob. It balances too, but weakens the anchor from "must be an opening brace" to "must be any one character", and the anchor is the entire point. Two guards added so this cannot recur silently: - runtime-launcher-parity (F0) pins brace balance at the SNIPPET, so the next edit to that pattern fails on the file it broke instead of surfacing three files downstream in a test whose name mentions neither the launcher nor this issue. It also asserts depth never goes negative, since a `}` preceding its `{` nets to zero while being unbalanced at every prefix. - runtime-identity gains behavioral truncated-payload and trailing-garbage fixtures, so the added `}` is proven load-bearing rather than merely present. Verified: snippet 51/51 braces; new-project combined net depth 0; the seven other preamble-bearing files with nonzero depth are unchanged from merged next (their own prose, not the preamble, and not in any guard's scan set); all 112 inlined copies and the resolver reference re-synced byte-equal; sync:launcher idempotent on the second run. Refs #3841 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * chore(#3841): backfill changeset PR number Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: sim <sim@local> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
18 KiB
<required_reading> Read all files referenced by the invoking prompt's execution_context before starting. </required_reading>
```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}"; _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 RESPONSE_LANGUAGE=$(gsd_run query config-get response_language --default "" 2>/dev/null || echo "") ```If response_language is set: All user-facing questions, prompts, and explanations in this workflow MUST be presented in {response_language}. Technical terms, code, file paths, and subagent prompts stay in English — only user-facing output is translated.
Check if --repair, --backfill, or --context flags are present in the command arguments.
REPAIR_FLAG=""
BACKFILL_FLAG=""
CONTEXT_MODE=""
if arguments contain "--repair"; then
REPAIR_FLAG="--repair"
fi
if arguments contain "--backfill"; then
BACKFILL_FLAG="--backfill"
fi
if arguments contain "--context"; then
CONTEXT_MODE="true"
fi
If CONTEXT_MODE is set, jump to the context_check step and skip the
integrity validation steps. The two modes are orthogonal — context utilization
has nothing to do with .planning/ directory health.
The model running this workflow self-reports the current session's
approximate tokensUsed and the active model's contextWindow. Use the values
visible in your runtime (Claude Code's /context slash command output, or the
model's own session telemetry). If the runtime exposes neither, prompt the user
once via AskUserQuestion for both numbers.
TEXT_MODE fallback: when text_mode is true (config or --text flag) the
runtime is non-Claude (Codex, Gemini, etc.) and AskUserQuestion is not
available — replace the prompt with a plain-text two-question sequence
("Approximate tokens used? Context window size?") and read the answers as
plain text from the user's response.
gsd_run query validate.context \
--tokens-used "$TOKENS_USED" \
--context-window "$CONTEXT_WINDOW"
The query prints a one-line status (Context utilization: NN% (state)) plus
a recommendation line for the warning and critical states. Print the SDK
output verbatim and end the workflow — do not mix in .planning/
health output, the two modes are independent diagnostics.
gsd_run query validate.health $REPAIR_FLAG $BACKFILL_FLAG
Parse JSON output:
status: "healthy" | "degraded" | "broken"errors[]: Critical issues (code, message, fix, repairable)warnings[]: Non-critical issuesinfo[]: Informational notesrepairable_count: Number of auto-fixable issuesrepairs_performed[]: Actions taken if --repair was used
Isolation/worktrees compatibility check (#2486): the SDK is runtime-neutral, so this check runs here, using the same negotiation the execution-workflow guards use. It catches a config that carries an explicit workflow.use_worktrees: true on a runtime whose declared dispatch.isolation is none (e.g. inherited from a worktree-capable install sharing the repo) — a value /gsd:execute-phase and /gsd:quick fail closed on. The gate is the declared capability, not the runtime name (#2584), and the resolver fail-closes unknown/undeclared/undocumented values to none. Use inspect-dispatch-isolation, the sentinel-free inspection verb — never dispatch-isolation, whose #3045 contract records the resolved decision to the executor-dispatch sentinel as an unconditional side effect; a read-only diagnostic must not be able to stamp a sentinel the isolation guards then enforce against real dispatches. "Sentinel-free" is the precise claim and the only one this check depends on: like every gsd-tools invocation, inspection still runs the shared CLI bootstrap, which may self-heal a stale .planning/active-workstream pointer or rebuild compiled modules. Those are pre-existing, verb-independent, and cannot block a dispatch; writing the sentinel can:
The worktrees read deliberately carries no --default/fallback, exactly as settings.md does
it: W025's claim is that the config sets the key to a non-false value, so an absent key
(empty output) must stay distinguishable from an explicit true. A || echo "true" fallback
collapses that distinction and makes the check fire on a config that never set the key —
correct only on an emit where _stampNonClaudeRuntimeDefaults happened to rewrite the line to
--default false, and wrong on the un-stamped source/Claude emit. The [ -n … ] guard below
removes that dependency on stamping entirely (#2486 review, Major 1).
A failed query is not a capability verdict. || echo "none" would collapse "this runtime
declares no primitive" and "the query did not answer" into the same value, and W025's text asserts
the former — telling a Claude Code user their runtime declares no executor-isolation primitive,
which is false. Track the two apart and report which one happened.
Know the limit of that signal (#2486 review): the verb itself fail-closes an unknown, undeclared or
out-of-vocabulary runtime — and any internal resolution error — to none and exits 0, so INSPECTED_RESOLVED=true means "the query
answered", not "the runtime published a declaration". W025's resolved-case wording below is
therefore written to be true of every path that reaches it — a declared none and an
internally-fail-closed none alike. Distinguishing those two would need the verb to report
provenance, which is out of scope here.
_INSPECTED_RAW=$(gsd_run query inspect-dispatch-isolation --raw 2>/dev/null)
_ISOLATION_RC=$?
if [ $_ISOLATION_RC -ne 0 ] || [ -z "$_INSPECTED_RAW" ]; then
INSPECTED_ISOLATION=none
INSPECTED_RESOLVED=false # no verdict learned — not the same as "declares none"
else
INSPECTED_ISOLATION="$_INSPECTED_RAW"
INSPECTED_RESOLVED=true
fi
case "$INSPECTED_ISOLATION" in
harness-worktree|orchestrator-worktree|none) ;;
*) INSPECTED_ISOLATION=none; INSPECTED_RESOLVED=false ;; # out of vocabulary is not a verdict either
esac
USE_WORKTREES=$(gsd_run query config-get workflow.use_worktrees --raw 2>/dev/null)
if [ "$INSPECTED_ISOLATION" = "none" ] && [ -n "$USE_WORKTREES" ] && [ "$USE_WORKTREES" != "false" ]; then
if [ "$INSPECTED_RESOLVED" = "true" ]; then
echo "W025: the project config sets workflow.use_worktrees to a non-false value, but this runtime has no usable executor-isolation primitive — dispatch.isolation resolves to none, declared as none, or fail-closed because the capability could not be determined — so /gsd:execute-phase and /gsd:quick will fail closed. Fix: run /gsd:settings and answer No to Worktrees, or set workflow.use_worktrees: false in the project config (the active workstream's config.json when one is active). Set it explicitly rather than deleting the key — an absent key resolves to false only on an emit whose default was stamped to false, and to true otherwise."
else
echo "W025: the project config sets workflow.use_worktrees to a non-false value, and GSD could not resolve this runtime's executor-isolation capability ('gsd_run query inspect-dispatch-isolation' failed or returned nothing) — so it cannot tell whether /gsd:execute-phase and /gsd:quick will fail closed on that value. This is a report of an unverifiable config, NOT a finding that the runtime declares no primitive. Fix: re-run once the gsd-tools shim resolves; if the warning persists, run /gsd:settings and answer No to Worktrees."
fi
fi
If the check prints, append it to the Warnings section of the report as [W025] with the printed fix, include it in the displayed warning count, and report Status: DEGRADED if validate.health returned healthy (a config the execution workflows fail closed on is not a healthy planning state). It is not auto-repairable: an explicit true may be intentional for a worktree-capable install sharing the same .planning/config.json, so the remedy is the user's call (#2486).
### GSD Health Check
Status: HEALTHY | DEGRADED | BROKEN
Errors: N | Warnings: N | Info: N
If repairs were performed:
## Repairs Performed
- ✓ config.json: Created with defaults
- ✓ STATE.md: Regenerated from roadmap
If errors exist:
## Errors
- [E001] config.json: JSON parse error at line 5
Fix: Run /gsd:health --repair to reset to defaults
- [E002] PROJECT.md not found
Fix: Run /gsd:new-project to create
If warnings exist:
## Warnings
- [W002] STATE.md references phase 5, but only phases 1-3 exist
Fix: Review STATE.md manually before changing it; repair will not overwrite an existing STATE.md
- [W005] Phase directory "1-setup" doesn't follow NN-name format
Fix: Rename to match pattern (e.g., 01-setup)
If info exists:
## Info
- [I001] 02-implementation/02-01-PLAN.md has no SUMMARY.md
Note: May be in progress
Footer (if repairable issues exist and --repair was NOT used):
---
N issues can be auto-repaired. Run: /gsd:health --repair
Ask user if they want to run repairs:
Would you like to run /gsd:health --repair to fix N issues automatically?
If yes, re-run with --repair flag and display results.
**If repairs were performed:**Re-run health check without --repair to confirm issues are resolved:
gsd_run query validate.health
Report final status.
<error_codes>
| Code | Severity | Description | Repairable |
|---|---|---|---|
| E001 | error | .planning/ directory not found | No |
| E002 | error | PROJECT.md not found | No |
| E003 | error | ROADMAP.md not found | No |
| E004 | error | STATE.md not found | No |
| E005 | error | config.json parse error | No |
| E010 | error | CWD resolves to the user's home directory — health check would target the wrong .planning/ | No |
| W001 | warning | PROJECT.md missing required section | No |
| W002 | warning | STATE.md references invalid phase | No |
| W003 | warning | config.json not found | Yes |
| W004 | warning | config.json invalid field value | No |
| W005 | warning | Phase directory naming mismatch | No |
| W006 | warning | Phase in ROADMAP but no directory | No |
| W007 | warning | Phase on disk but not in ROADMAP | No |
| W008 | warning | config.json: workflow.nyquist_validation absent (defaults to enabled but agents may skip) | Yes |
| W009 | warning | Phase has Validation Architecture in RESEARCH.md but no VALIDATION.md | No |
| W010 | warning | GSD agent installation missing or incomplete | No |
| W011 | warning | STATE.md current-phase status disagrees with ROADMAP.md checkbox | No |
| W012 | warning | config.json invalid branching_strategy value | No |
| W013 | warning | config.json context_window not a positive integer | No |
| W014 | warning | config.json phase_branch_template missing {phase} placeholder | No |
| W015 | warning | config.json milestone_branch_template missing {milestone} placeholder | No |
| W016 | warning | config.json: workflow.ai_integration_phase absent (defaults to enabled but agents may skip AI-integration-phase planning) | Yes |
| W017 | warning | Orphan git worktree (path no longer exists on disk) | No |
| W018 | warning | MILESTONES.md missing entry for archived milestone snapshot | Yes (--backfill) |
| W019 | warning | Unrecognized .planning/ root file — not a canonical GSD artifact | No |
| W020 | warning | Worktree health scan degraded — git worktree list timed out, failed, or a finding could not be verified | No |
| W021 | warning | Phase's integer prefix implies a different milestone than its ROADMAP section (phase_id_convention: milestone-prefixed) | No |
| W022 | warning | config.json models entry malformed (unknown phase type, invalid tier, or non-object value) | No |
| W023 | warning | Phase directories collide on normalized key | No |
| W024 | warning | STATE.md was written many commits ago — treat its contents as approximate | No |
| W026 | warning | STATE says milestone complete but ROADMAP lists an unstarted phase for that milestone | No |
| W027 | warning | Stale git worktree (not modified in a long time) | No |
| W028 | warning | A GSD-owned install scope shadows another on this machine | No |
| W029 | warning | .planning/ matches a gitignore rule but is still tracked by git (gitignore has no effect on already-tracked files) | No |
| I001 | info | Plan without SUMMARY (may be in progress) | No |
| I010 | info | Resolved CWD reported alongside the E010 home-directory guard | No |
Note: this table is generated — do not hand-edit it. It is produced by node scripts/gen-health-docs.cjs --write from src/health-diagnostic.cts's RULES table (31 rules as of #3309, each carrying a static description/repairable on its Rule entry — see src/health-diagnostic-types.cts) plus the 3 pre-checks that stay outside the rule table by design (E001, E010, I010 — safety rails in cmdValidateHealth, src/verify.cts, never .planning/ findings). scripts/lint-health-diagnostic-rule-table.cjs already enforces the 1:1 code invariant this table depends on (no duplicate codes; severity is always a Rule property, never set per emit call) — before assigning a new code, add a Rule entry under src/health-diagnostic-rules/ (its code is simply the next free number the lint guard has not yet seen) and run node scripts/gen-health-docs.cjs --write to regenerate this table; npm run lint:generated-sync fails if it drifts. W025 (the workflow.use_worktrees/dispatch.isolation check, #2486) is a workflow-layer diagnostic emitted directly by this file's own run_health_check step, not by cmdValidateHealth — it stays documented in that step, not in this generated table.
</error_codes>
<repair_actions>
| Action | Effect | Risk |
|---|---|---|
| createConfig | Create config.json with defaults | None |
| resetConfig | Delete + recreate config.json | Loses custom settings |
| regenerateState | Create STATE.md from ROADMAP structure when it is missing | Loses session history |
| addNyquistKey | Add workflow.nyquist_validation: true to config.json | None — matches existing default |
| addAiIntegrationPhaseKey | Add workflow.ai_integration_phase: true to config.json | None — matches existing default |
| backfillMilestones | Synthesize missing MILESTONES.md entries from .planning/milestones/vX.Y-ROADMAP.md snapshots |
None — additive only; triggered by --backfill flag |
Not repairable (too risky):
- PROJECT.md, ROADMAP.md content
- Phase directory renaming
- Orphaned plan cleanup
</repair_actions>
<stale_task_cleanup> Windows-specific: Check for stale Claude Code task directories that accumulate on crash/freeze. These are left behind when subagents are force-killed and consume disk space.
When --repair is active, detect and clean up:
# Check for stale task directories (older than 24 hours)
TASKS_DIR="$HOME/.claude/tasks"
if [ -d "$TASKS_DIR" ]; then
STALE_COUNT=$( (find "$TASKS_DIR" -maxdepth 1 -type d -mtime +1 2>/dev/null || true) | wc -l )
if [ "$STALE_COUNT" -gt 0 ]; then
echo "⚠️ Found $STALE_COUNT stale task directories in ~/.claude/tasks/"
echo " These are leftover from crashed subagent sessions."
echo " Run: rm -rf ~/.claude/tasks/* (safe — only affects dead sessions)"
fi
fi
Report as info diagnostic: I002 | info | Stale subagent task directories found | Yes (--repair removes them)
</stale_task_cleanup>