From 79781e68eb9d2ecc6e739b2900c7217526181f63 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Wed, 19 Aug 2026 15:21:15 -0400 Subject: [PATCH] enhance(#2401): ground verify-command paths and inherit prior-phase commands (#3678) * feat(#2401): ground verify-command paths and inherit prior-phase commands Adds a deterministic resolvability probe over each PLAN.md verify command and surfaces the nearest prior phase's proven commands to the planner at every context window. - src/verify-command-grounding.cts: recognizer (not a shell interpreter) that grounds a leading cd chain and npm --prefix , and reports unresolvable rather than guessing. Never executes command text. - gsd-tools check verify-command-paths : per-phase probe, wired into plan-phase.md before the plan-check pass. - init.plan-phase gains prior_verify_commands, ungated by context_window. - gsd-plan-checker: new Verify Command Path Resolvability dimension that reports the failing target and never prescribes a replacement. Also fixes first-match-wins prefix bucketing in scripts/lint-test-file-count.cjs (readdir order is not stable across platforms, so a module whose name extends another's with a hyphen bucketed differently on Linux than on macOS). Closes #2401 Co-Authored-By: Claude Opus 5 * fix(#2401): ground the canonical --prefix form, quoted paths, and absolute cd resets Independent review found three defects in the recognizer: - npm --prefix DIR run SCRIPT never reached the script-existence check, because the pattern required npm and run to be adjacent. That is the form the docs tell planners to prefer, so script_missing never fired for it. The prefix flag and its value are now stripped before matching. - --prefix captured with \S+, so a quoted path containing a space was truncated to a stray opening quote and reported as a missing directory - a false blocker, worse than the bug this feature fixes. The capture is now quote-aware. - A chained cd whose later segment was absolute concatenated instead of resetting, producing a nonsense path and another false blocker. The fold now resets on an absolute segment. Also replaces the bespoke phase-directory regex with the canonical phase-id helpers. Real phase directories are NN-slug, not phase-N-slug, so the prior-command harvest matched nothing outside its own fixtures and the planner-inheritance half of this feature was dead code. Co-Authored-By: Claude Opus 5 * refactor(#2401): source task blocks from the canonical sectionizer The module carried its own copy of the -block grammar - a fourth hand-rolled mirror of the one markdown-sectionizer owns. verify.cts keeps its copy only because it needs the type= attribute the canonical helper discards; this module never reads that attribute, so it can share the owner outright instead of adding a test around a copy. extractAutomatedCommands now takes task bodies from extractTaggedBlocks and the out-of-task remainder from stripTaggedBlocks. A task-grammar parity test pins the attributed task-name set against the canonical helper across six awkward task shapes. Co-Authored-By: Claude Opus 5 * fix(#2401): extract agent-file overflow to references and repair the property arbitrary The remote matrix run came back red with 19 failures, four root causes: - agents/gsd-plan-checker.md and agents/gsd-planner.md both blew the 49152 agent cap. Their bodies move to gsd-core/references/, leaving @-reference stubs, per the documented overflow pattern. - The new checker dimension invoked gsd_run before the canonical preamble that defines it. The call is deleted outright: plan-phase.md already runs the probe and hands the result in as {VERIFY_PATHS}, so the dimension consumes that rather than re-running anything. - fc.fullUnicodeString does not exist in fast-check 4.8.0. Replaced with fc.string({ unit: 'binary' }), which covers the same 0000-10FFFF range. - Three runtime-loaded files grew; acknowledged in the existing ack fragments that already own those bare filenames, since two ack sources may never name the same path. Co-Authored-By: Claude Opus 5 * test(#2401): regenerate golden install-tree fixtures for the new references Adding two files under gsd-core/references/ changes what the installer emits into every runtime's tree, so all 19 golden install-parity fixtures went stale. Regenerated with npm run gen:install-tree; the delta is exactly the two new reference paths per runtime, no removals. Co-Authored-By: Claude Opus 5 * chore(#2401): backfill changeset pr number to 3678 * fix(#2401): treat ~ as a home expansion only at the start of a path Windows CI caught this on both shards; the Linux-only remote matrix cannot see it. The dynamic-path refusal rejected ~ anywhere, and a GitHub Windows runner's tmpdir is an 8.3 short name - C:\Users\RUNNER~1\AppData\Local\Temp - so a valid absolute Windows path came back unresolvable/dynamic_path. This was a production bug, not a test artifact: any Windows user whose project path carries an 8.3 short name, or any literal ~, silently lost the probe entirely - every command degrading to unresolvable with no explanation. ~ is a home expansion only at the start of a path; elsewhere it is an ordinary literal. The check is now split: $, backtick, *, ? and newline stay refused anywhere (substitution and globs, and the glob characters are illegal in Windows path components regardless), while ~ is refused only leading, tolerating one leading quote since the check runs before quote stripping. The prior tests only caught this on Windows because only Windows puts a ~ in tmpdir. Four new tests pin it on every platform via a fixture directory literally named RUNNER~1. Co-Authored-By: Claude Opus 5 --------- Co-authored-by: sim Co-authored-by: Claude Opus 5 --- .changeset/bold-pandas-snooze.md | 5 + .gitignore | 1 + CONTEXT.md | 3 + agents/gsd-plan-checker.md | 5 + agents/gsd-planner.md | 2 + docs/COMMANDS.md | 58 ++ docs/FEATURES.md | 26 + docs/INVENTORY-MANIFEST.json | 3 + docs/INVENTORY.md | 3 + docs/README.md | 1 + .../resolve-verify-command-path-findings.md | 105 ++ eslint.config.mjs | 2 + .../planner-verify-command-grounding.md | 17 + .../verify-command-path-resolvability.md | 42 + gsd-core/workflows/plan-phase.md | 37 +- scripts/lint-test-file-count.cjs | 17 +- src/check-command-router.cts | 78 +- src/init.cts | 26 + src/verify-command-grounding.cts | 754 ++++++++++++++ ...1954-plan-checker-undeclared-coupling.json | 2 +- .../2775-planner-package-legitimacy-gate.json | 2 +- .../3409-unreachable-guard-arms.json | 2 +- tests/fixtures/install-tree/antigravity.json | 2 + tests/fixtures/install-tree/augment.json | 2 + tests/fixtures/install-tree/claude-local.json | 2 + tests/fixtures/install-tree/claude.json | 2 + tests/fixtures/install-tree/cline.json | 2 + tests/fixtures/install-tree/codebuddy.json | 2 + tests/fixtures/install-tree/codex.json | 2 + tests/fixtures/install-tree/copilot.json | 2 + tests/fixtures/install-tree/cursor.json | 2 + tests/fixtures/install-tree/hermes.json | 2 + tests/fixtures/install-tree/kilo.json | 2 + tests/fixtures/install-tree/kimi-code.json | 2 + tests/fixtures/install-tree/kimi.json | 2 + tests/fixtures/install-tree/opencode.json | 2 + tests/fixtures/install-tree/pi.json | 2 + tests/fixtures/install-tree/qwen.json | 2 + tests/fixtures/install-tree/trae.json | 2 + tests/fixtures/install-tree/windsurf.json | 2 + tests/fixtures/install-tree/zcode.json | 2 + tests/lint-test-file-count.test.cjs | 37 + tests/verify-command-grounding.test.cjs | 972 ++++++++++++++++++ 43 files changed, 2231 insertions(+), 7 deletions(-) create mode 100644 .changeset/bold-pandas-snooze.md create mode 100644 docs/how-to/resolve-verify-command-path-findings.md create mode 100644 gsd-core/references/planner-verify-command-grounding.md create mode 100644 gsd-core/references/verify-command-path-resolvability.md create mode 100644 src/verify-command-grounding.cts create mode 100644 tests/verify-command-grounding.test.cjs diff --git a/.changeset/bold-pandas-snooze.md b/.changeset/bold-pandas-snooze.md new file mode 100644 index 000000000..890375e32 --- /dev/null +++ b/.changeset/bold-pandas-snooze.md @@ -0,0 +1,5 @@ +--- +type: Added +pr: 3678 +--- +**Verify-command path grounding for phase planning** — a plan's `` verify command whose target directory does not exist (or holds no `package.json`) is now caught deterministically before execution instead of being hand-reasoned by the plan checker, which previously prescribed wrong replacement paths. The planner also inherits the nearest prior phase's proven verify commands at every context window, not only above 500k. (#2401) diff --git a/.gitignore b/.gitignore index 2adb1412b..d7b9c17a1 100644 --- a/.gitignore +++ b/.gitignore @@ -251,6 +251,7 @@ build/ /gsd-core/bin/lib/gate-predicate-evaluator.cjs /gsd-core/bin/lib/docs.cjs /gsd-core/bin/lib/check-command-router.cjs +/gsd-core/bin/lib/verify-command-grounding.cjs /gsd-core/bin/lib/frontmatter.cjs /gsd-core/bin/lib/learnings.cjs /gsd-core/bin/lib/gsd2-import.cjs diff --git a/CONTEXT.md b/CONTEXT.md index 2f096ef92..ffe447ccd 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -26,6 +26,9 @@ Module owning phase-effort estimation and its calibration against measured reali ### Verification Module Module owning the canonical phase-verification status projection shared by phase transition, progress, manager, autonomous, and closeout readiness paths. `readVerificationStatus(phaseDir, opts?)` reads the first `*-VERIFICATION.md` frontmatter `status`, maps it through `VERIFICATION_ROUTING_TABLE`, and fail-closes — only `{passed}` satisfies the canonical gate; `missing`/`unknown`/`gaps_found`/`human_needed`/`stale` all route away from "complete" (#1522). `findStaleVerificationSummary` flags a SUMMARY newer than the VERIFICATION file (status `stale`). Both honor a no-throw, degrade-to-safe contract (any FS error → `missing` / not-stale) and an injectable `opts.fs` seam. `isPhaseComplete(phaseDir, deps?)` is the single canonical owner of "is phase P complete?" (ADR-3180 §7.4, issue #3186, disk-strict per #2957): it wraps `readVerificationStatus`, calling it UNCONDITIONALLY — plan count is never a precondition, so a zero-plan phase with a passing `*-VERIFICATION.md` is complete (#3168) — and returns `{ value: { complete, verification }, scope }`; `complete` is exactly `verification.status === 'passed'`. A ROADMAP checkbox carries no machine authority and is never consulted. `cmdPhaseComplete`, `buildPhaseCompletionProjection`, and `buildStateFrontmatter` all route through it. Source of truth: `gsd-core/bin/lib/verification.cjs` (generated from `src/verification.cts`). +### Verify Command Grounding Module +Module owning the deterministic resolvability probe over PLAN.md `` verify commands (#2401), plus the prior-phase command harvest that feeds the planner. `extractAutomatedCommands(planText)` pulls every `…` body with its owning ``, in document order, via a ReDoS-safe stop-at-next-open task pattern (shape mirrors `PLAN_TASK_BLOCK_RE` in `verify.cjs`) and a monotonic span pointer; non-string input yields `[]`. `resolveVerifyCommandTarget(command, {projectRoot, declaredPaths})` is a **RECOGNIZER, not a shell interpreter** (deliberate, per Greenspun): it grounds exactly two forms — a folded leading `cd ` chain and `npm --prefix ` — and any path carrying `$`, a backtick, `*`, `?`, `~`, or a newline returns `unresolvable`/`dynamic_path` at WARNING severity, never BLOCKER. Status is a closed 5-atom enum (`ok`/`broken`/`unresolvable`/`not_applicable`/`pending_creation`) and severity a closed 3-atom enum (`blocker`/`warning`/`none`); `broken` is only ever `missing_dir` or `no_manifest`, while `script_missing`/`manifest_unreadable`/`outside_root` stay advisory on an `ok` status. A target an earlier task in the same phase declares (`` or the `## Artifacts this phase produces` section) is `pending_creation`, never a blocker — without that, every greenfield phase would red. A bare ancestor climb (`cd ../..`, every segment `..`) short-circuits to `outside_root` without touching the filesystem, because the checker's root and a parallel executor worktree's root differ; a climb naming a concrete sibling (`cd ../../frontend` — the exact #2401 shape) still names something checkable and is probed normally. **The module never executes command text** (`fs.statSync`/`existsSync`/`readFileSync`/`readdirSync` only — PLAN.md is model-authored untrusted input) and deliberately exposes **no `suggestion` field**: prescribing a replacement path is the failure being fixed, not the fix. `probePhaseVerifyCommands({phaseDir, projectRoot})` backs `gsd-tools check verify-command-paths ` (routed in `check-command-router.cjs`), degrading to a populated `readError` rather than throwing — an empty `commands` with a non-empty `readError` means *could not look*, not *nothing to report*. `harvestPriorVerifyCommands({planningDir, beforePhase, limit=20, lookback=3})` walks descending phase dirs for the nearest prior phase with any command, deduped first-seen and capped, and is emitted as `init.plan-phase`'s `prior_verify_commands` **ungated by `context_window`** — the `>= 500000` enrichment gate is exactly what starved the planner at 200k. Source of truth: `gsd-core/bin/lib/verify-command-grounding.cjs` (generated from `src/verify-command-grounding.cts`). + ### Phase Locator Module Module owning phase-directory search and location: active-phase discovery against the `.planning/phases/` tree (`searchPhaseInDir`, `findPhaseInternal`) and archived-phase-dir enumeration (`getArchivedPhaseDirs`), matching phase ids/tokens against the filesystem. Depends only on leaf modules (`phase-id` for token/name matching, `core-utils` for fs-scan/path helpers, `planning-workspace` for `planningDir`) — no `loadConfig`, no other core dependency. Extracted from the Core module per ADR-857 rollout phase 2d (#881); the `core.cjs` re-export spine was retired in epic #1267, so callers import this leaf directly. Source of truth: `gsd-core/bin/lib/phase-locator.cjs` (generated from `src/phase-locator.cts`). Since #2830, `searchPhaseInDir` also parses each plan's `depends_on` and each completed plan's SUMMARY `status` and calls Plan Dependency Graph Module's `computeHaltPropagation` to populate `halted_plans`/`blocked_by`/`runnable_plans` — additive fields; `incomplete_plans` keeps its pre-#2830 meaning unchanged. Since #3185 (ADR-3180 Decision 1, Phase 3), the module also owns `listMilestonePhaseDirs(phasesDir, { cwd, ws, versionOverride, phaseIdConvention })`, the single canonical owner of milestone-scoped phase-directory enumeration: it applies the current milestone's `ROADMAP.md` window (via `getMilestonePhaseFilter`) and then the canonical `isSentinelPhaseId` sentinel filter, in that order, over the raw `phasesDir` directory listing. It returns `{ value: string[], scope }`, where `scope` is the `SCOPE` enum from `src/planning-scope.cts` (`complete`/`truncated`/`unscoped`/`unreadable`), so a caller can distinguish a genuinely empty milestone from an enumeration that could not be scoped. Consumed by `query progress`, `stats`, and the bare `phases list`, all of which need "which phases belong to this milestone." `phases list --phase` and `--include-archived` (lookup/archive questions) read the unscoped physical directory set and do not call this owner. `phases clear` and `milestone complete`'s phase-archival move call `isSentinelPhaseId` directly instead — they must sweep every non-sentinel phase directory regardless of milestone window, so they take the sentinel filter without this owner's window scoping. diff --git a/agents/gsd-plan-checker.md b/agents/gsd-plan-checker.md index 1f18ea591..7a4e25387 100644 --- a/agents/gsd-plan-checker.md +++ b/agents/gsd-plan-checker.md @@ -715,6 +715,11 @@ issue: 2. For each `` block containing `2>/dev/null || echo` where the result feeds a `[ "$VAR" = ... ]` comparison: BLOCKER. 3. For each `` block asserting a specific numeric count not cited as measured in this plan: WARNING. +## Dimension: Verify Command Path Resolvability (#2401) + +**Question:** Does each `` command's target resolve? Consume the supplied +`{VERIFY_PATHS}` probe, never re-run/hand-reason it: @gsd-core/references/verify-command-path-resolvability.md + ## Dimension: Numeric/Factual Claim Authority (#1480) **Rule:** RESEARCH.md is produced at research time and may be stale. Numeric claims (test counts, file counts, version numbers) and factual state claims ("feature X is implemented") in RESEARCH.md may not reflect the current codebase. The plan may be more current. RESEARCH.md is authoritative for architectural decisions and constraints — not for measurements. diff --git a/agents/gsd-planner.md b/agents/gsd-planner.md index 3125b2730..84f630b15 100644 --- a/agents/gsd-planner.md +++ b/agents/gsd-planner.md @@ -190,6 +190,8 @@ Every task has four required fields: **Nyquist Rule:** Every `` includes ``. If no test exists, set `MISSING — Wave 0 must create {test_file} first` and create that scaffold. +**Inherit the command that already worked (#2401):** reuse `prior_verify_commands` verbatim, prefer `npm --prefix run