From ffd353f080a27e9f3309a2590a6ee980c01a4d8f Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Mon, 13 Jul 2026 11:58:02 -0400 Subject: [PATCH 1/8] docs(#2240): clarify PLAN.md is install-relative, record #2238 wontfix (#2241) The reference presented @~/.claude/gsd-core/... as canonical and described the paths as files "the executor reads before starting". Both are misleading: the prefix is install- and runtime-relative (Claude global vs Cursor .cursor/gsd-core vs an absolute --local path), so a committed plan is not clone-portable, and /gsd-execute-phase loads the workflow from its own installed copy rather than gating on the committed block. - docs/reference/plan-md.md: describe the install-relative, non-clone-portable nature of the block and contrast it with repository-relative . - .out-of-scope/plan-md-execution-context-portability.md: record the #2238 wontfix decision (PLAN.md is a machine artifact; #2158 precedent) with a revisit-if condition. Refs #2238. Closes #2240. Co-authored-by: Claude Opus 4.8 (1M context) --- .../plan-md-execution-context-portability.md | 42 +++++++++++++++++++ docs/reference/plan-md.md | 4 +- 2 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 .out-of-scope/plan-md-execution-context-portability.md diff --git a/.out-of-scope/plan-md-execution-context-portability.md b/.out-of-scope/plan-md-execution-context-portability.md new file mode 100644 index 000000000..e5989acd6 --- /dev/null +++ b/.out-of-scope/plan-md-execution-context-portability.md @@ -0,0 +1,42 @@ +# Clone-Portable `` in Committed PLAN.md + +GSD does not make the `` block in committed `PLAN.md` files +clone-portable — it does not rewrite the planner's install-relative +`@…/gsd-core/…` references into repository-relative or install-neutral paths so +that a committed plan reads identically across developers, machines, or runtimes. + +## Why this is out of scope + +PLAN.md is a **machine artifact**, not a human- or clone-facing document — the +same principle that governs [plan-md-human-rendering.md](./plan-md-human-rendering.md) +(from #2158). A committed PLAN.md is a per-run agent instruction set, produced by +`gsd-planner` and consumed in place by `gsd-executor`, `gsd-plan-checker`, and +`gsd-verifier`. There is no documented step in which a plan is read on another +machine after `git clone` without a local GSD install. + +Under that model, ``'s `@` references point at the reader's +own local GSD install (Claude `~/.claude/gsd-core/…`, Cursor `.cursor/gsd-core/…`, +or an absolute path for a `--local` install). They are install-relative by design, +and the executor loads those workflows from its own installed copy — it never +consumes the paths a *different* machine wrote into a committed plan. +`/gsd-execute-phase` builds its own `` inline from the +orchestrator's installed workflow (`workflows/execute-phase.md`), so the block a +planner writes into a committed plan does not gate execution anywhere. + +Making committed plans clone-portable would treat PLAN.md as a shared +cross-developer document — the boundary #2158 declined to cross — for a block that +no consumer reads across machines. + +**Revisit if** GSD introduces a documented cross-developer / cross-machine contract +for committed PLAN.md — a human- or teammate-facing use where plans are read after +`git clone` without a local install — at which point `` +portability becomes in scope. + +## Prior requests + +- #2238 — "Planner embeds machine-specific gsd-core paths in committed PLAN.md execution_context" + +## Related + +- `.out-of-scope/plan-md-human-rendering.md` — #2158, the governing "PLAN.md is a machine artifact" precedent. +- `docs/reference/plan-md.md` — the `` reference, which describes the install-relative behaviour. diff --git a/docs/reference/plan-md.md b/docs/reference/plan-md.md index 38982e2d8..a76197027 100644 --- a/docs/reference/plan-md.md +++ b/docs/reference/plan-md.md @@ -118,7 +118,7 @@ Output: PostFeed and PostCard components wired to /api/feed. ### `` -Lists workflow files the executor reads before starting. Always includes the execute-plan workflow; adds the checkpoints reference when the plan contains checkpoint tasks: +Lists the workflow files associated with executing the plan. Always includes the execute-plan workflow; adds the checkpoints reference when the plan contains checkpoint tasks: ```xml @@ -127,6 +127,8 @@ Lists workflow files the executor reads before starting. Always includes the exe ``` +These `@` paths point at the local GSD install, not at repository files. The prefix shown here (`~/.claude/gsd-core/…`) is the Claude global-install location; other runtimes and local installs resolve to their own install directory — for example `.cursor/gsd-core/…`, or an absolute project path for a `--local` install. Because the prefix is install-relative, this block is not clone-portable: a committed plan carries whichever prefix the authoring install had. Execution does not depend on it — `/gsd-execute-phase` loads the execute-plan workflow from its own installed copy — so the block records the execution context rather than resolvable repository references. Contrast `` (below), whose repository-relative `@` paths resolve after a `git clone`. + ### `` References source files the executor needs to read. Includes project-level planning docs and any source files whose patterns or types the plan must replicate. Prior plan `SUMMARY.md` files are included only when there is a genuine dependency (imported types, shared decision) — not reflexively: From 7ccf57200d51913e6144886260a5cd68c35bb194 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Mon, 13 Jul 2026 12:40:52 -0400 Subject: [PATCH 2/8] fix(#2202): preserve unknown frontmatter keys in syncStateFrontmatter (#2233) * fix(#2202): preserve unknown frontmatter keys in syncStateFrontmatter syncStateFrontmatter rebuilds frontmatter from a fixed schema, dropping any custom/unknown key on every mutating verb. Before reconstruction, merge any existing frontmatter key the schema does not own. Schema keys still win. Closes #2202 * docs(#2202): add changeset fragment * docs(#2202): backfill PR number * fix(#2202): add regression test + remove redundant type assertion - tests/state.test.cjs: behavioral regression test asserting custom/unknown STATE.md frontmatter keys survive a mutating verb (they were silently dropped before the syncStateFrontmatter carry-forward). - src/state.cts: drop the unnecessary `as Record` assertion that tripped @typescript-eslint/no-unnecessary-type-assertion (the lint-tests gate failure). Refs #2202 Co-Authored-By: Claude Opus 4.8 (1M context) --------- Co-authored-by: Claude Opus 4.8 (1M context) --- .changeset/agile-pandas-dance.md | 5 +++++ src/state.cts | 10 ++++++++++ tests/state.test.cjs | 31 +++++++++++++++++++++++++++++++ 3 files changed, 46 insertions(+) create mode 100644 .changeset/agile-pandas-dance.md diff --git a/.changeset/agile-pandas-dance.md b/.changeset/agile-pandas-dance.md new file mode 100644 index 000000000..5041d24ea --- /dev/null +++ b/.changeset/agile-pandas-dance.md @@ -0,0 +1,5 @@ +--- +type: Fixed +pr: 2233 +--- +**Custom STATE.md frontmatter keys are no longer dropped on every mutating verb** — syncStateFrontmatter rebuilt the frontmatter from a fixed schema, silently dropping any custom key. It now carries forward existing keys the schema does not own. (#2202) diff --git a/src/state.cts b/src/state.cts index 5e59b7370..f7f3811ac 100644 --- a/src/state.cts +++ b/src/state.cts @@ -1650,6 +1650,16 @@ function syncStateFrontmatter(content: string, cwd: string | undefined): string derivedFm['progress'] = normalizeProgressNumbers(existingFm['progress']); } + // #2202: carry forward any existing frontmatter key that the schema does not + // own, so custom/unknown keys are not silently dropped on every mutating verb. + // Schema-owned keys (already in derivedFm from buildStateFrontmatter + the + // preserve guards above) still win. + for (const key of Object.keys(existingFm)) { + if (!(key in derivedFm) && existingFm[key] !== undefined) { + derivedFm[key] = existingFm[key]; + } + } + const yamlStr = reconstructFrontmatter(derivedFm as unknown as Frontmatter); return `---\n${yamlStr}\n---\n\n${body}`; } diff --git a/tests/state.test.cjs b/tests/state.test.cjs index eaf61a34a..5257bb23d 100644 --- a/tests/state.test.cjs +++ b/tests/state.test.cjs @@ -611,6 +611,37 @@ milestone: v1.0 assert.ok(!content.includes('status: unknown'), 'should not contain unknown status'); }); + test('#2202: preserves unknown frontmatter keys the schema does not own', () => { + // Regression: a mutating verb rewrites STATE.md via syncStateFrontmatter, + // which rebuilds frontmatter from the body + schema. Before #2202 it dropped + // any frontmatter key the schema does not own; custom/tooling keys must + // survive every write. + fs.writeFileSync( + path.join(tmpDir, '.planning', 'STATE.md'), + `--- +status: executing +milestone: v1.0 +custom_tracking_id: ABC-123 +team: platform +--- + +# Project State + +**Current Phase:** 03 +**Current Plan:** 03-02 +` + ); + + // Any writeStateMd triggers syncStateFrontmatter. + runGsdTools('state update "Current Plan" "03-03"', tmpDir); + + const content = fs.readFileSync(path.join(tmpDir, '.planning', 'STATE.md'), 'utf-8'); + assert.match(content, /custom_tracking_id: ABC-123/, 'unknown key custom_tracking_id must be preserved'); + assert.match(content, /team: platform/, 'unknown key team must be preserved'); + // Schema-owned keys still win / survive alongside the carried-forward keys. + assert.ok(content.includes('status: executing'), 'schema-owned status still preserved'); + }); + test('round-trip: write then read via state json', () => { fs.writeFileSync( path.join(tmpDir, '.planning', 'STATE.md'), From 8d636671210959db475f39de06d6f4575dc816e9 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Mon, 13 Jul 2026 12:52:35 -0400 Subject: [PATCH 3/8] fix(#2203): traceability parser matches REQ-IDs in any column (#2234) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(#2203): traceability parser matches REQ-IDs in any column, not just the first The traceability table-row parser required the REQ-ID in the first column (`^| REQ-ID |`). A table that leads with a status column (e.g. `| ☐ | REQ-01 |`) matched zero rows, so phase complete warned every body REQ-ID was missing. Match REQ-IDs in any pipe-delimited cell (drop the ^ anchor). Closes #2203 * docs(#2203): add changeset fragment * docs(#2203): backfill PR number --- .changeset/noble-wasps-greet.md | 5 +++++ src/phase.cts | 6 +++++- 2 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 .changeset/noble-wasps-greet.md diff --git a/.changeset/noble-wasps-greet.md b/.changeset/noble-wasps-greet.md new file mode 100644 index 000000000..c89e3b5ca --- /dev/null +++ b/.changeset/noble-wasps-greet.md @@ -0,0 +1,5 @@ +--- +type: Fixed +pr: 2234 +--- +**`phase complete` no longer false-reports REQ-IDs as missing when the traceability table leads with a status column** — the parser required the REQ-ID in the first column, so a table shaped `| ☐ | REQ-01 | …` matched zero rows and every body REQ-ID was reported missing. It now matches REQ-IDs in any column. (#2203) diff --git a/src/phase.cts b/src/phase.cts index 57f23c655..97dec6580 100644 --- a/src/phase.cts +++ b/src/phase.cts @@ -1672,7 +1672,11 @@ function cmdPhaseComplete(cwd: string, phaseNum: string, raw: boolean): void { ? reqContent.slice(traceabilityHeadingMatch.index) : ''; const tableReqIds = new Set(); - const tableRowPat = /^\|\s*([A-Z][A-Z0-9]*-\d+)\s*\|/gm; + // #2203: match REQ-IDs in any pipe-delimited cell (not just the first + // column) so a traceability table that leads with a status column (e.g. + // | ☐ | REQ-01 | …) is parsed correctly instead of reporting every row + // as missing. + const tableRowPat = /\|\s*([A-Z][A-Z0-9]*-\d+)\s*\|/g; let tableMatch: RegExpExecArray | null; while ((tableMatch = tableRowPat.exec(traceabilitySection)) !== null) { tableReqIds.add(tableMatch[1]); From 880fbd963a616148df6aa169c82563763b958e60 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Mon, 13 Jul 2026 13:04:16 -0400 Subject: [PATCH 4/8] fix(#2206): strip trailing slashes in isGitIgnored to avoid CRLF check-ignore quirk (#2235) * fix(#2206): strip trailing slashes in isGitIgnored to avoid CRLF check-ignore quirk isGitIgnored was called with a trailing slash (`.planning/`) in config-loader. git check-ignore has a longstanding quirk: a CRLF .gitignore with blank lines falsely reports any path WITH a trailing slash as ignored. This silently set commit_docs=false on Windows repos (where CRLF .gitignore is the norm), skipping all planning-doc commits. Normalize trailing slashes inside isGitIgnored so every call site is protected. Closes #2206 * docs(#2206): add changeset fragment * docs(#2206): backfill PR number --- .changeset/tidy-voles-glide.md | 5 +++++ src/config-loader.cts | 8 ++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 .changeset/tidy-voles-glide.md diff --git a/.changeset/tidy-voles-glide.md b/.changeset/tidy-voles-glide.md new file mode 100644 index 000000000..92bd662ce --- /dev/null +++ b/.changeset/tidy-voles-glide.md @@ -0,0 +1,5 @@ +--- +type: Fixed +pr: 2235 +--- +**`commit_docs` no longer silently disables on CRLF `.gitignore` repos** — git check-ignore falsely reports a trailing-slash path (e.g. `.planning/`) as ignored when the .gitignore has CRLF line endings with blank lines. isGitIgnored now strips trailing slashes before querying, so the false positive cannot occur. (#2206) diff --git a/src/config-loader.cts b/src/config-loader.cts index 2384f60a2..c5216dfb5 100644 --- a/src/config-loader.cts +++ b/src/config-loader.cts @@ -175,10 +175,14 @@ interface ParsedConfig { const _gitIgnoredCache = new Map(); function isGitIgnored(cwd: string, targetPath: string): boolean { - const key = cwd + '::' + targetPath; + // #2206: strip trailing slashes — `git check-ignore` has a quirk where a + // CRLF .gitignore with blank lines falsely reports a trailing-slash path + // (e.g. `.planning/`) as ignored. Normalizing here protects every call site. + const normalized = targetPath.replace(/\/+$/, ''); + const key = cwd + '::' + normalized; if (_gitIgnoredCache.has(key)) return _gitIgnoredCache.get(key)!; // --no-index checks .gitignore rules regardless of whether the file is tracked. - const result = execGit(['check-ignore', '-q', '--no-index', '--', targetPath], { cwd }); + const result = execGit(['check-ignore', '-q', '--no-index', '--', normalized], { cwd }); const ignored = result.exitCode === 0; _gitIgnoredCache.set(key, ignored); return ignored; From ad7111e50be025eda5b5e4930c2629159a3ddc51 Mon Sep 17 00:00:00 2001 From: Cody Anderson <70287898+arakasi1@users.noreply.github.com> Date: Mon, 13 Jul 2026 11:25:47 -0600 Subject: [PATCH 5/8] feat(#2161): opt-in absolute token count on the statusline context meter (#2174) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(#2161): opt-in absolute token count on the statusline context meter New statusline.show_context_tokens config (default false). When enabled, the context meter shows the absolute token total after the percentage, e.g. "████░░░░░░ 46% (156k)" — summing input, cache-creation, cache-read, and output tokens from context_window.current_usage (matching /context). Default output is byte-for-byte unchanged when the flag is absent or false. The .planning config is now read once per render and shared with the last-command/position block instead of being re-read. Claude-Session: https://claude.ai/code/session_01Hme55Pvq6BhpgwBcyC5HAg * docs(#2161): changeset fragment for PR #2174 * fix(#2161): review fixes — k-to-M threshold, boundary tests, changeset format - formatTokens promotes to the M branch when k-rounding reaches 1000 (999,500-999,999 rendered "1000k" instead of "1.0M") - boundary tests at 999499/999500/999999/1000000/1000001 - Number() guards on the four usage fields (silent string-concat gap) - changeset body ends with the (#2161) citation per house convention Claude-Session: https://claude.ai/code/session_01Hme55Pvq6BhpgwBcyC5HAg * fix(#2161): round-2 review fixes — config-set coverage, precision claim, exports style - config-set accept/reject tests for statusline.show_context_tokens (mirrors the post-planning-gaps precedent the issue scope names) - changeset + docs no longer claim parity with /context: the suffix sums four fields while the meter %% derives from used_percentage (three), so the figures can diverge slightly - module.exports one entry per line Claude-Session: https://claude.ai/code/session_01Hme55Pvq6BhpgwBcyC5HAg * test: regenerate golden-install-parity fixtures for the statusline hook change Claude-Session: https://claude.ai/code/session_01Hme55Pvq6BhpgwBcyC5HAg --------- Co-authored-by: Tom Boucher --- .changeset/jolly-jays-march.md | 5 + docs/CONFIGURATION.md | 1 + .../bin/shared/config-schema.manifest.json | 1 + hooks/gsd-statusline.js | 52 +++++- src/config.cts | 7 + .../golden-install-parity/antigravity.json | 4 +- .../golden-install-parity/augment.json | 4 +- .../golden-install-parity/claude-local.json | 4 +- .../golden-install-parity/claude.json | 4 +- .../fixtures/golden-install-parity/cline.json | 2 +- .../golden-install-parity/codebuddy.json | 4 +- .../fixtures/golden-install-parity/codex.json | 2 +- .../golden-install-parity/copilot.json | 2 +- .../golden-install-parity/cursor.json | 2 +- .../golden-install-parity/hermes.json | 4 +- .../fixtures/golden-install-parity/kilo.json | 2 +- .../fixtures/golden-install-parity/kimi.json | 4 +- .../golden-install-parity/opencode.json | 4 +- tests/fixtures/golden-install-parity/pi.json | 4 +- .../fixtures/golden-install-parity/qwen.json | 4 +- .../fixtures/golden-install-parity/trae.json | 2 +- .../golden-install-parity/windsurf.json | 2 +- .../fixtures/golden-install-parity/zcode.json | 2 +- tests/gsd-statusline.test.cjs | 171 ++++++++++++++++++ 24 files changed, 260 insertions(+), 33 deletions(-) create mode 100644 .changeset/jolly-jays-march.md diff --git a/.changeset/jolly-jays-march.md b/.changeset/jolly-jays-march.md new file mode 100644 index 000000000..430f1cd44 --- /dev/null +++ b/.changeset/jolly-jays-march.md @@ -0,0 +1,5 @@ +--- +type: Added +pr: 2174 +--- +**Opt-in absolute token count on the statusline context meter** — new `statusline.show_context_tokens` config (default `false`). When enabled, the meter shows the absolute context total after the percentage, e.g. "████░░░░░░ 46% (156k)", summing input, cache-creation, cache-read, and output tokens from the hook payload (a broader basis than the meter's percentage, which is derived from `used_percentage` and excludes output tokens — the two figures can diverge slightly). Default meter output is unchanged. (#2161) diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 1812c9470..4b15aab2b 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -452,6 +452,7 @@ If `.planning/` is in `.gitignore`, `commit_docs` is automatically `false` regar | `hooks.workflow_guard` | boolean | `false` | Warn when file edits happen outside GSD workflow context (advises using `/gsd-quick` or `/gsd-fast`) | | `statusline.show_last_command` | boolean | `false` | Append `last: /` suffix to the statusline showing the most recently invoked slash command. Opt-in; reads the active session transcript to extract the latest `` tag (closes #2538) | | `statusline.context_position` | string | `"end"` | Position of the context-window meter. `"end"` (default) renders at line tail; `"front"` renders immediately after the model name so the meter stays visible in narrow terminals. Closes #2937 | +| `statusline.show_context_tokens` | boolean | `false` | Append the absolute token count (e.g. `(156k)`) after the context meter's percentage. Sums input, cache-creation, cache-read, and output tokens from the hook payload — a broader basis than the meter's percentage (which excludes output tokens), so the two figures can diverge slightly. Opt-in; the meter is unchanged when the flag is absent | The prompt injection guard hook (`gsd-prompt-guard.js`) is always active and cannot be disabled — it's a security feature, not a workflow toggle. diff --git a/gsd-core/bin/shared/config-schema.manifest.json b/gsd-core/bin/shared/config-schema.manifest.json index eeadb2d2a..721e23b1d 100644 --- a/gsd-core/bin/shared/config-schema.manifest.json +++ b/gsd-core/bin/shared/config-schema.manifest.json @@ -71,6 +71,7 @@ "workflow.context_coverage_gate", "statusline.show_last_command", "statusline.context_position", + "statusline.show_context_tokens", "workflow.max_discuss_passes", "features.thinking_partner", "context", diff --git a/hooks/gsd-statusline.js b/hooks/gsd-statusline.js index 847530a2e..a0b611b03 100755 --- a/hooks/gsd-statusline.js +++ b/hooks/gsd-statusline.js @@ -286,6 +286,36 @@ function formatGsdState(s) { return parts.join(' · '); } +// --- Context token count (opt-in) --------------------------------------------- + +/** + * Format a token count compactly: 156342 → '156k', 1234567 → '1.2M'. + */ +function formatTokens(tokens) { + // Promote to the M branch when k-rounding would reach 1000 (999,500-999,999 + // must render "1.0M", never "1000k"). + if (tokens >= 1000000 || Math.round(tokens / 1000) >= 1000) { + return (tokens / 1000000).toFixed(1) + 'M'; + } + if (tokens >= 1000) return Math.round(tokens / 1000) + 'k'; + return String(tokens); +} + +/** + * Pure function: build the token-count suffix for the context meter from the + * hook input's context_window.current_usage block. Sums input, cache-creation, + * cache-read, and output tokens (the same total Claude Code's /context shows). + * Returns ' (156k)' or '' when usage is absent/empty. + */ +function contextTokenSuffix(currentUsage) { + if (!currentUsage || typeof currentUsage !== 'object') return ''; + const total = (Number(currentUsage.input_tokens) || 0) + + (Number(currentUsage.cache_creation_input_tokens) || 0) + + (Number(currentUsage.cache_read_input_tokens) || 0) + + (Number(currentUsage.output_tokens) || 0); + return total > 0 ? ` (${formatTokens(total)})` : ''; +} + // --- stdin ------------------------------------------------------------------ function runStatusline() { @@ -304,6 +334,11 @@ function runStatusline() { const session = data.session_id || ''; const remaining = data.context_window?.remaining_percentage; + // Read .planning config once — used by the context meter (token suffix) + // and the last-command/position block below. Fail-soft to {}. + let cfg = {}; + try { cfg = readGsdConfig(dir); } catch (e) {} + // Context window display (shows USED percentage scaled to usable context) // Claude Code reserves a buffer for autocompact. By default this is ~16.5% // of the total window, but users can override it via CLAUDE_CODE_AUTO_COMPACT_WINDOW @@ -349,15 +384,21 @@ function runStatusline() { const filled = Math.floor(used / 10); const bar = '█'.repeat(filled) + '░'.repeat(10 - filled); + // Opt-in absolute token count after the percentage (statusline.show_context_tokens) + let tokenSuffix = ''; + if (getConfigValue(cfg, 'statusline.show_context_tokens') === true) { + tokenSuffix = contextTokenSuffix(data.context_window?.current_usage); + } + // Color based on usable context thresholds if (used < 50) { - ctx = ` \x1b[32m${bar} ${used}%\x1b[0m`; + ctx = ` \x1b[32m${bar} ${used}%${tokenSuffix}\x1b[0m`; } else if (used < 65) { - ctx = ` \x1b[33m${bar} ${used}%\x1b[0m`; + ctx = ` \x1b[33m${bar} ${used}%${tokenSuffix}\x1b[0m`; } else if (used < 80) { - ctx = ` \x1b[38;5;208m${bar} ${used}%\x1b[0m`; + ctx = ` \x1b[38;5;208m${bar} ${used}%${tokenSuffix}\x1b[0m`; } else { - ctx = ` \x1b[5;31m💀 ${bar} ${used}%\x1b[0m`; + ctx = ` \x1b[5;31m💀 ${bar} ${used}%${tokenSuffix}\x1b[0m`; } } @@ -422,7 +463,6 @@ function runStatusline() { let lastCmdSuffix = ''; let position = 'end'; try { - const cfg = readGsdConfig(dir); if (getConfigValue(cfg, 'statusline.show_last_command') === true) { const transcriptPath = data.transcript_path; const lastCmd = readLastSlashCommand(transcriptPath); @@ -531,6 +571,8 @@ module.exports = { composeStatusline, isInstalledAheadOfLatest, evaluateUpdateCache, + formatTokens, + contextTokenSuffix, }; /** diff --git a/src/config.cts b/src/config.cts index a0989f78c..45afc6892 100644 --- a/src/config.cts +++ b/src/config.cts @@ -763,6 +763,13 @@ function cmdConfigSet(cwd: string, keyPath: string | undefined, value: string | const VALID_CONTEXT_POSITIONS = ['front', 'end']; if (kp === 'statusline.context_position') assertEnumValue(parsedValue, val, VALID_CONTEXT_POSITIONS, 'statusline.context_position'); + // statusline.show_context_tokens — boolean only + if (kp === 'statusline.show_context_tokens') { + if (typeof parsedValue !== 'boolean') { + error(`Invalid statusline.show_context_tokens '${val}'. Must be a boolean (true or false).`); + } + } + // Fallow scope + profile enum validation (#3424) const VALID_FALLOW_SCOPES = ['phase', 'repo']; if (kp === 'code_quality.fallow.scope') assertEnumValue(parsedValue, val, VALID_FALLOW_SCOPES, 'code_quality.fallow.scope'); diff --git a/tests/fixtures/golden-install-parity/antigravity.json b/tests/fixtures/golden-install-parity/antigravity.json index ee69a5669..fc15df65d 100644 --- a/tests/fixtures/golden-install-parity/antigravity.json +++ b/tests/fixtures/golden-install-parity/antigravity.json @@ -41,7 +41,7 @@ "gsd-core/bin/gsd-tools.cjs": "600e9ed0cb6ed7bb", "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", - "gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8", + "gsd-core/bin/shared/config-schema.manifest.json": "b67705431d738ae7", "gsd-core/bin/shared/model-catalog.json": "b55176ca044728d3", "gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a", "gsd-core/bin/verify-reapply-patches.cjs": "8bc541aabc2e143c", @@ -328,7 +328,7 @@ "hooks/gsd-read-guard.js": "9e423cd03e2d1b16", "hooks/gsd-read-injection-scanner.js": "eefea61f9b0e464c", "hooks/gsd-session-state.sh": "e54379ba86bf1b6d", - "hooks/gsd-statusline.js": "8ae31be7a006204b", + "hooks/gsd-statusline.js": "9e64af477e774de6", "hooks/gsd-update-banner.js": "55143a25f978f301", "hooks/gsd-validate-commit.sh": "bf5dd61d33cb3a38", "hooks/gsd-windsurf-pre-command.js": "948be1c6d14c79cd", diff --git a/tests/fixtures/golden-install-parity/augment.json b/tests/fixtures/golden-install-parity/augment.json index 61d2c8ad0..120386618 100644 --- a/tests/fixtures/golden-install-parity/augment.json +++ b/tests/fixtures/golden-install-parity/augment.json @@ -112,7 +112,7 @@ "gsd-core/bin/gsd-tools.cjs": "37fb6081fcb18038", "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", - "gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8", + "gsd-core/bin/shared/config-schema.manifest.json": "b67705431d738ae7", "gsd-core/bin/shared/model-catalog.json": "b55176ca044728d3", "gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a", "gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904", @@ -399,7 +399,7 @@ "hooks/gsd-read-guard.js": "9e423cd03e2d1b16", "hooks/gsd-read-injection-scanner.js": "c8800819f7443a15", "hooks/gsd-session-state.sh": "e54379ba86bf1b6d", - "hooks/gsd-statusline.js": "3be32d2012c77fc1", + "hooks/gsd-statusline.js": "3e633cd082663b99", "hooks/gsd-update-banner.js": "55143a25f978f301", "hooks/gsd-validate-commit.sh": "bf5dd61d33cb3a38", "hooks/gsd-windsurf-pre-command.js": "948be1c6d14c79cd", diff --git a/tests/fixtures/golden-install-parity/claude-local.json b/tests/fixtures/golden-install-parity/claude-local.json index f660e3b97..de6f1260e 100644 --- a/tests/fixtures/golden-install-parity/claude-local.json +++ b/tests/fixtures/golden-install-parity/claude-local.json @@ -111,7 +111,7 @@ "gsd-core/bin/gsd-tools.cjs": "37fb6081fcb18038", "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", - "gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8", + "gsd-core/bin/shared/config-schema.manifest.json": "b67705431d738ae7", "gsd-core/bin/shared/model-catalog.json": "b55176ca044728d3", "gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a", "gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904", @@ -398,7 +398,7 @@ "hooks/gsd-read-guard.js": "9e423cd03e2d1b16", "hooks/gsd-read-injection-scanner.js": "00d2449afefd2e5f", "hooks/gsd-session-state.sh": "e54379ba86bf1b6d", - "hooks/gsd-statusline.js": "7c315416ffc99a9a", + "hooks/gsd-statusline.js": "920558a55eb86587", "hooks/gsd-update-banner.js": "b457746cb76c1957", "hooks/gsd-validate-commit.sh": "bf5dd61d33cb3a38", "hooks/gsd-windsurf-pre-command.js": "948be1c6d14c79cd", diff --git a/tests/fixtures/golden-install-parity/claude.json b/tests/fixtures/golden-install-parity/claude.json index 7e0d512eb..278315fb6 100644 --- a/tests/fixtures/golden-install-parity/claude.json +++ b/tests/fixtures/golden-install-parity/claude.json @@ -40,7 +40,7 @@ "gsd-core/bin/gsd-tools.cjs": "37fb6081fcb18038", "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", - "gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8", + "gsd-core/bin/shared/config-schema.manifest.json": "b67705431d738ae7", "gsd-core/bin/shared/model-catalog.json": "b55176ca044728d3", "gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a", "gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904", @@ -327,7 +327,7 @@ "hooks/gsd-read-guard.js": "9e423cd03e2d1b16", "hooks/gsd-read-injection-scanner.js": "00d2449afefd2e5f", "hooks/gsd-session-state.sh": "e54379ba86bf1b6d", - "hooks/gsd-statusline.js": "7c315416ffc99a9a", + "hooks/gsd-statusline.js": "920558a55eb86587", "hooks/gsd-update-banner.js": "b457746cb76c1957", "hooks/gsd-validate-commit.sh": "bf5dd61d33cb3a38", "hooks/gsd-windsurf-pre-command.js": "948be1c6d14c79cd", diff --git a/tests/fixtures/golden-install-parity/cline.json b/tests/fixtures/golden-install-parity/cline.json index 598d068aa..d1151047b 100644 --- a/tests/fixtures/golden-install-parity/cline.json +++ b/tests/fixtures/golden-install-parity/cline.json @@ -44,7 +44,7 @@ "gsd-core/bin/gsd-tools.cjs": "a84914f7cab74332", "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", - "gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8", + "gsd-core/bin/shared/config-schema.manifest.json": "b67705431d738ae7", "gsd-core/bin/shared/model-catalog.json": "b55176ca044728d3", "gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a", "gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904", diff --git a/tests/fixtures/golden-install-parity/codebuddy.json b/tests/fixtures/golden-install-parity/codebuddy.json index aa369e9c0..7468c5bbf 100644 --- a/tests/fixtures/golden-install-parity/codebuddy.json +++ b/tests/fixtures/golden-install-parity/codebuddy.json @@ -112,7 +112,7 @@ "gsd-core/bin/gsd-tools.cjs": "37fb6081fcb18038", "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", - "gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8", + "gsd-core/bin/shared/config-schema.manifest.json": "b67705431d738ae7", "gsd-core/bin/shared/model-catalog.json": "b55176ca044728d3", "gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a", "gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904", @@ -399,7 +399,7 @@ "hooks/gsd-read-guard.js": "9e423cd03e2d1b16", "hooks/gsd-read-injection-scanner.js": "7f7a7615b303369a", "hooks/gsd-session-state.sh": "e54379ba86bf1b6d", - "hooks/gsd-statusline.js": "ef8dcb6d64fd4493", + "hooks/gsd-statusline.js": "6ab2905537f885ba", "hooks/gsd-update-banner.js": "55143a25f978f301", "hooks/gsd-validate-commit.sh": "bf5dd61d33cb3a38", "hooks/gsd-windsurf-pre-command.js": "948be1c6d14c79cd", diff --git a/tests/fixtures/golden-install-parity/codex.json b/tests/fixtures/golden-install-parity/codex.json index 36c21e16f..1ddaf96c1 100644 --- a/tests/fixtures/golden-install-parity/codex.json +++ b/tests/fixtures/golden-install-parity/codex.json @@ -147,7 +147,7 @@ "gsd-core/bin/gsd-tools.cjs": "37fb6081fcb18038", "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", - "gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8", + "gsd-core/bin/shared/config-schema.manifest.json": "b67705431d738ae7", "gsd-core/bin/shared/model-catalog.json": "b55176ca044728d3", "gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a", "gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904", diff --git a/tests/fixtures/golden-install-parity/copilot.json b/tests/fixtures/golden-install-parity/copilot.json index 27c773c07..2aab090d5 100644 --- a/tests/fixtures/golden-install-parity/copilot.json +++ b/tests/fixtures/golden-install-parity/copilot.json @@ -42,7 +42,7 @@ "gsd-core/bin/gsd-tools.cjs": "600e9ed0cb6ed7bb", "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", - "gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8", + "gsd-core/bin/shared/config-schema.manifest.json": "b67705431d738ae7", "gsd-core/bin/shared/model-catalog.json": "b55176ca044728d3", "gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a", "gsd-core/bin/verify-reapply-patches.cjs": "10226e9512dd44bf", diff --git a/tests/fixtures/golden-install-parity/cursor.json b/tests/fixtures/golden-install-parity/cursor.json index 870503ea7..41ddd34a1 100644 --- a/tests/fixtures/golden-install-parity/cursor.json +++ b/tests/fixtures/golden-install-parity/cursor.json @@ -112,7 +112,7 @@ "gsd-core/bin/gsd-tools.cjs": "db16ded31dd6eecf", "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", - "gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8", + "gsd-core/bin/shared/config-schema.manifest.json": "b67705431d738ae7", "gsd-core/bin/shared/model-catalog.json": "b55176ca044728d3", "gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a", "gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904", diff --git a/tests/fixtures/golden-install-parity/hermes.json b/tests/fixtures/golden-install-parity/hermes.json index 5eb2530ec..fe439f057 100644 --- a/tests/fixtures/golden-install-parity/hermes.json +++ b/tests/fixtures/golden-install-parity/hermes.json @@ -41,7 +41,7 @@ "gsd-core/bin/gsd-tools.cjs": "b3f927ee4b4c4711", "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", - "gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8", + "gsd-core/bin/shared/config-schema.manifest.json": "b67705431d738ae7", "gsd-core/bin/shared/model-catalog.json": "b55176ca044728d3", "gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a", "gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904", @@ -328,7 +328,7 @@ "hooks/gsd-read-guard.js": "1f58b020a91f032b", "hooks/gsd-read-injection-scanner.js": "f358eca3fa1eab24", "hooks/gsd-session-state.sh": "e54379ba86bf1b6d", - "hooks/gsd-statusline.js": "861808560e60b233", + "hooks/gsd-statusline.js": "34d2ae303558b917", "hooks/gsd-update-banner.js": "b457746cb76c1957", "hooks/gsd-validate-commit.sh": "bf5dd61d33cb3a38", "hooks/gsd-windsurf-pre-command.js": "948be1c6d14c79cd", diff --git a/tests/fixtures/golden-install-parity/kilo.json b/tests/fixtures/golden-install-parity/kilo.json index c274dadf3..8f387eb90 100644 --- a/tests/fixtures/golden-install-parity/kilo.json +++ b/tests/fixtures/golden-install-parity/kilo.json @@ -112,7 +112,7 @@ "gsd-core/bin/gsd-tools.cjs": "37fb6081fcb18038", "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", - "gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8", + "gsd-core/bin/shared/config-schema.manifest.json": "b67705431d738ae7", "gsd-core/bin/shared/model-catalog.json": "b55176ca044728d3", "gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a", "gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904", diff --git a/tests/fixtures/golden-install-parity/kimi.json b/tests/fixtures/golden-install-parity/kimi.json index cd7ac3baa..5da5f0f58 100644 --- a/tests/fixtures/golden-install-parity/kimi.json +++ b/tests/fixtures/golden-install-parity/kimi.json @@ -18,7 +18,7 @@ ".kimi/hooks/gsd-read-guard.js": "9e423cd03e2d1b16", ".kimi/hooks/gsd-read-injection-scanner.js": "c519598b9257aafa", ".kimi/hooks/gsd-session-state.sh": "e54379ba86bf1b6d", - ".kimi/hooks/gsd-statusline.js": "2736b0885aa97bbf", + ".kimi/hooks/gsd-statusline.js": "ae0c0e7a934c5deb", ".kimi/hooks/gsd-update-banner.js": "55143a25f978f301", ".kimi/hooks/gsd-validate-commit.sh": "bf5dd61d33cb3a38", ".kimi/hooks/gsd-windsurf-pre-command.js": "948be1c6d14c79cd", @@ -105,7 +105,7 @@ "gsd-core/bin/gsd-tools.cjs": "37fb6081fcb18038", "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", - "gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8", + "gsd-core/bin/shared/config-schema.manifest.json": "b67705431d738ae7", "gsd-core/bin/shared/model-catalog.json": "b55176ca044728d3", "gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a", "gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904", diff --git a/tests/fixtures/golden-install-parity/opencode.json b/tests/fixtures/golden-install-parity/opencode.json index 0e3348de8..aa6842369 100644 --- a/tests/fixtures/golden-install-parity/opencode.json +++ b/tests/fixtures/golden-install-parity/opencode.json @@ -112,7 +112,7 @@ "gsd-core/bin/gsd-tools.cjs": "37fb6081fcb18038", "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", - "gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8", + "gsd-core/bin/shared/config-schema.manifest.json": "b67705431d738ae7", "gsd-core/bin/shared/model-catalog.json": "b55176ca044728d3", "gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a", "gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904", @@ -399,7 +399,7 @@ "hooks/gsd-read-guard.js": "9e423cd03e2d1b16", "hooks/gsd-read-injection-scanner.js": "f72060dfe035f706", "hooks/gsd-session-state.sh": "e54379ba86bf1b6d", - "hooks/gsd-statusline.js": "9c132b5985800462", + "hooks/gsd-statusline.js": "3dd40389fa786dd6", "hooks/gsd-update-banner.js": "55143a25f978f301", "hooks/gsd-validate-commit.sh": "bf5dd61d33cb3a38", "hooks/gsd-windsurf-pre-command.js": "948be1c6d14c79cd", diff --git a/tests/fixtures/golden-install-parity/pi.json b/tests/fixtures/golden-install-parity/pi.json index 15e4a4e56..2cd30cb92 100644 --- a/tests/fixtures/golden-install-parity/pi.json +++ b/tests/fixtures/golden-install-parity/pi.json @@ -8,7 +8,7 @@ "gsd-core/bin/gsd-tools.cjs": "37fb6081fcb18038", "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", - "gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8", + "gsd-core/bin/shared/config-schema.manifest.json": "b67705431d738ae7", "gsd-core/bin/shared/model-catalog.json": "b55176ca044728d3", "gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a", "gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904", @@ -295,7 +295,7 @@ "hooks/gsd-read-guard.js": "9e423cd03e2d1b16", "hooks/gsd-read-injection-scanner.js": "f454242c010804cf", "hooks/gsd-session-state.sh": "e54379ba86bf1b6d", - "hooks/gsd-statusline.js": "5539e1ae859b987e", + "hooks/gsd-statusline.js": "41b0080cf0f3e7e9", "hooks/gsd-update-banner.js": "55143a25f978f301", "hooks/gsd-validate-commit.sh": "bf5dd61d33cb3a38", "hooks/gsd-windsurf-pre-command.js": "948be1c6d14c79cd", diff --git a/tests/fixtures/golden-install-parity/qwen.json b/tests/fixtures/golden-install-parity/qwen.json index 256b73ba0..ccd12a98c 100644 --- a/tests/fixtures/golden-install-parity/qwen.json +++ b/tests/fixtures/golden-install-parity/qwen.json @@ -41,7 +41,7 @@ "gsd-core/bin/gsd-tools.cjs": "6454021dec4d9563", "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", - "gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8", + "gsd-core/bin/shared/config-schema.manifest.json": "b67705431d738ae7", "gsd-core/bin/shared/model-catalog.json": "b55176ca044728d3", "gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a", "gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904", @@ -328,7 +328,7 @@ "hooks/gsd-read-guard.js": "2c8d417d12b51040", "hooks/gsd-read-injection-scanner.js": "396574bd25e99ff9", "hooks/gsd-session-state.sh": "e54379ba86bf1b6d", - "hooks/gsd-statusline.js": "739140996a3c0d49", + "hooks/gsd-statusline.js": "5686cf31c721b2b1", "hooks/gsd-update-banner.js": "b457746cb76c1957", "hooks/gsd-validate-commit.sh": "bf5dd61d33cb3a38", "hooks/gsd-windsurf-pre-command.js": "948be1c6d14c79cd", diff --git a/tests/fixtures/golden-install-parity/trae.json b/tests/fixtures/golden-install-parity/trae.json index 4a5148a3f..25b84997c 100644 --- a/tests/fixtures/golden-install-parity/trae.json +++ b/tests/fixtures/golden-install-parity/trae.json @@ -41,7 +41,7 @@ "gsd-core/bin/gsd-tools.cjs": "c28c9076058c5e73", "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", - "gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8", + "gsd-core/bin/shared/config-schema.manifest.json": "b67705431d738ae7", "gsd-core/bin/shared/model-catalog.json": "b55176ca044728d3", "gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a", "gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904", diff --git a/tests/fixtures/golden-install-parity/windsurf.json b/tests/fixtures/golden-install-parity/windsurf.json index 625774ee5..48e41a345 100644 --- a/tests/fixtures/golden-install-parity/windsurf.json +++ b/tests/fixtures/golden-install-parity/windsurf.json @@ -41,7 +41,7 @@ "gsd-core/bin/gsd-tools.cjs": "568e7c6bdd415c1a", "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", - "gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8", + "gsd-core/bin/shared/config-schema.manifest.json": "b67705431d738ae7", "gsd-core/bin/shared/model-catalog.json": "b55176ca044728d3", "gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a", "gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904", diff --git a/tests/fixtures/golden-install-parity/zcode.json b/tests/fixtures/golden-install-parity/zcode.json index fe60606c9..790d628ad 100644 --- a/tests/fixtures/golden-install-parity/zcode.json +++ b/tests/fixtures/golden-install-parity/zcode.json @@ -112,7 +112,7 @@ "gsd-core/bin/gsd-tools.cjs": "37fb6081fcb18038", "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", - "gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8", + "gsd-core/bin/shared/config-schema.manifest.json": "b67705431d738ae7", "gsd-core/bin/shared/model-catalog.json": "b55176ca044728d3", "gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a", "gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904", diff --git a/tests/gsd-statusline.test.cjs b/tests/gsd-statusline.test.cjs index 0ac03eb01..f2fb70b07 100644 --- a/tests/gsd-statusline.test.cjs +++ b/tests/gsd-statusline.test.cjs @@ -1190,5 +1190,176 @@ test('config-set rejects invalid statusline.context_position', () => { cleanup(tmpDir); } }); + +// Same write-path enforcement for the boolean statusline.show_context_tokens +// key (#2161) — mirrors the workflow.post_planning_gaps precedent the issue's +// scope names (tests/post-planning-gaps-2493.test.cjs). +test('config-set statusline.show_context_tokens true → persisted as boolean', () => { + const tmpDir = createTempProject(); + try { + const r = runGsdTools(['config-set', 'statusline.show_context_tokens', 'true'], tmpDir); + assert.ok(r.success, r.error); + const config = JSON.parse( + fs.readFileSync(path.join(tmpDir, '.planning', 'config.json'), 'utf-8')); + assert.strictEqual(config.statusline.show_context_tokens, true); + } finally { + cleanup(tmpDir); + } +}); + +test('config-set statusline.show_context_tokens yes → rejected', () => { + const tmpDir = createTempProject(); + try { + const r = runGsdTools(['config-set', 'statusline.show_context_tokens', 'yes'], tmpDir); + assert.equal(r.success, false, 'non-boolean value must be rejected'); + assert.match(r.error || r.output, /boolean|true|false/i); + } finally { + cleanup(tmpDir); + } +}); + }); +} + + +// ──────────────────────────────────────────────────────────────────────── +// Context meter token count (statusline.show_context_tokens) +// ──────────────────────────────────────────────────────────────────────── +{ + const { test, describe } = require('node:test'); + const assert = require('node:assert/strict'); + const fs = require('node:fs'); + const os = require('node:os'); + const path = require('node:path'); + const { execFileSync } = require('node:child_process'); + const { cleanup } = require('./helpers.cjs'); + const { formatTokens, contextTokenSuffix } = require('../hooks/gsd-statusline.js'); + const { VALID_CONFIG_KEYS } = require('../gsd-core/bin/lib/config-schema.cjs'); + + const hookPath = path.join(__dirname, '..', 'hooks', 'gsd-statusline.js'); + + describe('config schema: statusline.show_context_tokens', () => { + test('registers statusline.show_context_tokens', () => { + assert.ok( + VALID_CONFIG_KEYS.has('statusline.show_context_tokens'), + 'statusline.show_context_tokens must be in VALID_CONFIG_KEYS', + ); + }); + }); + + describe('formatTokens', () => { + test('passes small counts through', () => { + assert.equal(formatTokens(0), '0'); + assert.equal(formatTokens(999), '999'); + }); + test('rounds thousands to k', () => { + assert.equal(formatTokens(1000), '1k'); + assert.equal(formatTokens(156342), '156k'); + assert.equal(formatTokens(156700), '157k'); + }); + test('formats millions with one decimal', () => { + assert.equal(formatTokens(1000000), '1.0M'); + assert.equal(formatTokens(1234567), '1.2M'); + }); + test('k-to-M threshold boundary: limit-1 / limit / limit+1', () => { + // 999,999 k-rounds to 1000 — must promote to the M branch, never "1000k" + assert.equal(formatTokens(999999), '1.0M'); + assert.equal(formatTokens(1000000), '1.0M'); + assert.equal(formatTokens(1000001), '1.0M'); + // 999,499 is the last value that still k-rounds below 1000 + assert.equal(formatTokens(999499), '999k'); + assert.equal(formatTokens(999500), '1.0M'); + }); + }); + + describe('contextTokenSuffix', () => { + test('returns empty string for absent/malformed usage', () => { + assert.equal(contextTokenSuffix(null), ''); + assert.equal(contextTokenSuffix(undefined), ''); + assert.equal(contextTokenSuffix('nope'), ''); + assert.equal(contextTokenSuffix({}), ''); + }); + test('sums all four token dimensions', () => { + const suffix = contextTokenSuffix({ + input_tokens: 1000, + cache_creation_input_tokens: 2000, + cache_read_input_tokens: 150000, + output_tokens: 3000, + }); + assert.equal(suffix, ' (156k)'); + }); + test('tolerates missing dimensions', () => { + assert.equal(contextTokenSuffix({ input_tokens: 500 }), ' (500)'); + }); + }); + + describe('statusline output token suffix (e2e)', () => { + function makeProject(flag) { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ctx-tokens-')); + fs.mkdirSync(path.join(dir, '.planning'), { recursive: true }); + if (flag !== undefined) { + fs.writeFileSync( + path.join(dir, '.planning', 'config.json'), + JSON.stringify({ statusline: { show_context_tokens: flag } }), + ); + } + return dir; + } + + function runHook(dir) { + const payload = JSON.stringify({ + model: { display_name: 'Claude' }, + workspace: { current_dir: dir }, + session_id: `test-tokens-${Date.now()}-${Math.random().toString(36).slice(2)}`, + context_window: { + remaining_percentage: 70, + total_tokens: 200000, + current_usage: { + input_tokens: 1000, + cache_read_input_tokens: 150000, + output_tokens: 5000, + }, + }, + }); + let stdout = ''; + try { + stdout = execFileSync(process.execPath, [hookPath], { + input: payload, encoding: 'utf8', timeout: 4000, + }); + } catch (e) { + stdout = e.stdout || ''; + } + // eslint-disable-next-line no-control-regex -- stripping ANSI SGR sequences from captured CLI output + return stdout.replace(/\x1b\[[0-9;]*m/g, ''); + } + + test('flag=true appends the token count after the percentage', () => { + const dir = makeProject(true); + try { + const out = runHook(dir); + assert.match(out, /% \(156k\)/, `expected "(156k)" after the meter %; got: ${out}`); + } finally { + cleanup(dir); + } + }); + + test('default (flag absent) meter is unchanged — no token count', () => { + const dir = makeProject(undefined); + try { + const out = runHook(dir); + assert.doesNotMatch(out, /\(\d+(?:\.\d+)?[kM]?\)/, `expected no token suffix; got: ${out}`); + } finally { + cleanup(dir); + } + }); + + test('flag=false meter is unchanged — no token count', () => { + const dir = makeProject(false); + try { + const out = runHook(dir); + assert.doesNotMatch(out, /\(\d+(?:\.\d+)?[kM]?\)/, `expected no token suffix; got: ${out}`); + } finally { + cleanup(dir); + } + }); }); } From 3592697bed7ca1b5f48e39166734211b6514540f Mon Sep 17 00:00:00 2001 From: Adnan Date: Mon, 13 Jul 2026 18:43:29 +0100 Subject: [PATCH 6/8] fix(#2107): orchestrator honors gate="blocking-human" checkpoints in auto-mode (#2113) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(execute-phase): honor gate="blocking-human" in auto-mode checkpoint handling The package-legitimacy gate (#2827) spans two layers. gsd-executor refuses to auto-approve a gate="blocking-human" checkpoint and escalates it so a human can vet the package. execute-phase's checkpoint_handling step then dispatched purely on checkpoint *type* and never read gate -- so under --auto/--chain it auto-approved the checkpoint the executor had just refused to auto-approve. Net effect: the slopsquatting defence was inert in exactly the unattended mode where it matters. An [ASSUMED]/[SUS] package reached install with no human ever seeing the prompt. - gsd-core/workflows/execute-phase.md: carve out gate="blocking-human" (and the package-legitimacy what-built markers) ahead of every auto-mode branch. - gsd-core/references/checkpoints.md: document the gate attribute and its two values. blocking-human previously appeared nowhere outside gsd-executor.md, so no planner had a documented way to author a non-auto-approvable checkpoint. - tests/package-legitimacy-gate.test.cjs: the existing regression test asserted the executor half only, which is why it stayed green while the gate was open. Now asserts the orchestrator half too. * chore(changeset): link to issue #2107 * chore(changeset): backfill PR number 2113 Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01JNR8m2pv5U7ubn4iiXVrMa * test(#2107): refresh golden-install-parity hashes for edited gsd-core files The golden fixtures pin content hashes for gsd-core/references/checkpoints.md and gsd-core/workflows/execute-phase.md, both edited by this fix. Regenerated via UPDATE_GOLDEN=1; only those two keys change across all 17 runtime fixtures. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01JNR8m2pv5U7ubn4iiXVrMa * fix(#2107): keep the carve-out inside the ADR-857 host-loop budget The ADR-857 phase-6 ratchet pins execute-phase.md below 93600 LF bytes so optional-feature logic keeps migrating out of the host loop. The carve-out first landed 623 bytes over that ceiling. Move the two-layer rationale (why gsd-executor escalates these checkpoints) into references/checkpoints.md, where the gate is now documented, and reduce the workflow to the operative rule. execute-phase.md is 93589 bytes, under the ceiling; the gate token and both marker strings are kept because the orchestrator matches on them. Refresh the two baselines the edit invalidates: golden-install-parity fixtures (only the checkpoints.md and execute-phase.md hashes move) and workflow-size-baseline.json (one line). The ADR-857 ceiling itself is untouched. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01JNR8m2pv5U7ubn4iiXVrMa * fix(#2107): executor honors blocking-human on the decision branch + gate transport Review found the fix incomplete one layer down. Two executor-layer gaps: 1. Blocker — agents/gsd-executor.md auto-mode dispatch gated checkpoint:human-verify on gate="blocking-human" but the checkpoint:decision branch below auto-selected the first option with no gate check. The executor resolves a decision itself (auto-selects and continues) without returning it, so the orchestrator carve-out never runs for it. A planner following the new checkpoints.md rule 6 ("gate a decision whose default would be wrong to assume") would have it silently auto-selected under --auto/--chain — the exact #2107 harm, one checkpoint type over. The decision branch now STOPs and returns for an explicit human decision when gate="blocking-human". 2. Major (transport) — checkpoint_return_format carried no field conveying the gate to the freshly-spawned orchestrator, so recognition of the proactive pre-install checkpoint rested on freeform prose. Added a **Gate:** field to the return format and re-pointed the execute-phase carve-out at it ("If the returned Gate: is blocking-human"). Net byte-negative: execute-phase.md drops 93589 -> 93583, widening ADR-857 headroom from 11 to 17 bytes. Co-Authored-By: Claude Opus 4.8 * test(#2107): cover decision carve-out + gate transport, de-vacuum conditional tests - New: 'auto mode does not auto-select a blocking-human decision checkpoint' asserts the executor decision branch STOPs on blocking-human. Verified red on the pre-fix executor (2 fail), green with the fix (27 pass). - New: 'checkpoint_return_format transports the gate ...' asserts the **Gate:** field carries blocking-human across the executor->orchestrator boundary. - New: 'auto-select rule for decision is conditional' — orchestrator-side mirror of the human-verify conditional test, for the execute-phase decision branch. - Fix vacuous test: both conditional tests now assert the anchor matched (length > 0) before iterating, so anchor drift can no longer pass with zero assertions. Co-Authored-By: Claude Opus 4.8 * test(#2107): refresh golden + size baselines for executor + execute-phase edits Regenerated via UPDATE_GOLDEN=1 and update-size-baseline.cjs. Only the gsd-executor.md and gsd-core/workflows/execute-phase.md hashes move across the runtime fixtures (35 ins / 35 del, no keys added or removed); checkpoints.md is unchanged this round. Size baselines: gsd-executor.md 43607 -> 43973, execute-phase.md 93589 -> 93583 (still under the ADR-857 ceiling). Co-Authored-By: Claude Opus 4.8 --------- Co-authored-by: Claude Opus 4.8 Co-authored-by: Tom Boucher --- ...orchestrator-honors-blocking-human-gate.md | 5 + agents/gsd-executor.md | 3 +- gsd-core/references/checkpoints.md | 12 ++ gsd-core/workflows/execute-phase.md | 8 +- tests/agent-size-baseline.json | 2 +- .../golden-install-parity/antigravity.json | 6 +- .../golden-install-parity/augment.json | 6 +- .../golden-install-parity/claude-local.json | 6 +- .../golden-install-parity/claude.json | 6 +- .../fixtures/golden-install-parity/cline.json | 6 +- .../golden-install-parity/codebuddy.json | 6 +- .../fixtures/golden-install-parity/codex.json | 8 +- .../golden-install-parity/copilot.json | 6 +- .../golden-install-parity/cursor.json | 6 +- .../golden-install-parity/hermes.json | 6 +- .../fixtures/golden-install-parity/kilo.json | 6 +- .../fixtures/golden-install-parity/kimi.json | 6 +- .../golden-install-parity/opencode.json | 6 +- tests/fixtures/golden-install-parity/pi.json | 4 +- .../fixtures/golden-install-parity/qwen.json | 6 +- .../fixtures/golden-install-parity/trae.json | 6 +- .../golden-install-parity/windsurf.json | 6 +- .../fixtures/golden-install-parity/zcode.json | 6 +- tests/package-legitimacy-gate.test.cjs | 139 ++++++++++++++++++ tests/workflow-size-baseline.json | 2 +- 25 files changed, 219 insertions(+), 60 deletions(-) create mode 100644 .changeset/2107-orchestrator-honors-blocking-human-gate.md diff --git a/.changeset/2107-orchestrator-honors-blocking-human-gate.md b/.changeset/2107-orchestrator-honors-blocking-human-gate.md new file mode 100644 index 000000000..a4d745e9b --- /dev/null +++ b/.changeset/2107-orchestrator-honors-blocking-human-gate.md @@ -0,0 +1,5 @@ +--- +type: Security +pr: 2113 +--- +**`gate="blocking-human"` checkpoints are no longer auto-approved by the execute-phase orchestrator** — the package-legitimacy gate (#2827) spans two layers: `gsd-executor` refuses to auto-approve a `gate="blocking-human"` checkpoint and escalates it via `checkpoint_return_format` so a human can vet the package, and `execute-phase`'s `checkpoint_handling` step decides what happens next. That step dispatched purely on checkpoint *type* and never read `gate`, so under `--auto` / `--chain` it immediately auto-approved the very checkpoint the executor had just refused to auto-approve (`human-verify → {user_response} = "approved"`). The slopsquatting defence was therefore inert in exactly the unattended mode where nobody is watching: an `[ASSUMED]`/`[SUS]` package reached install with no human ever seeing the verification prompt. `checkpoint_handling` now carves out `gate="blocking-human"` (and the package-legitimacy `what-built` markers) ahead of every auto-mode branch, routing those checkpoints to the standard present-to-user flow regardless of type. `references/checkpoints.md` documents the `gate` attribute and its two values for the first time — previously `blocking-human` appeared nowhere outside `agents/gsd-executor.md`, so no planner had a documented way to author a checkpoint that auto-mode could not bypass. The existing regression test asserted the executor half only; it now asserts the orchestrator half too, which is why it stayed green while the gate was open. (#2107) diff --git a/agents/gsd-executor.md b/agents/gsd-executor.md index 6f4ecc5fd..f3f57169e 100644 --- a/agents/gsd-executor.md +++ b/agents/gsd-executor.md @@ -315,7 +315,7 @@ For full automation-first patterns, server lifecycle, CLI handling: **Auto-mode checkpoint behavior** (when `AUTO_CFG` is `"true"`): - **checkpoint:human-verify** → Auto-approve **except package-legitimacy checkpoints**. If checkpoint has `gate="blocking-human"` OR its purpose indicates package legitimacy verification (`what-built` mentions `Package verification required before install` or `Package install failed — human verification required`), do **not** auto-approve. STOP and return checkpoint_return_format for explicit human confirmation. -- **checkpoint:decision** → Auto-select first option (planners front-load the recommended choice). Log `⚡ Auto-selected: [option name]`. Continue to next task. +- **checkpoint:decision** → If checkpoint has `gate="blocking-human"`, do **not** auto-select — STOP and return checkpoint_return_format for an explicit human decision (a `blocking-human` decision exists because its default answer would be wrong to assume). Otherwise auto-select first option (planners front-load the recommended choice), log `⚡ Auto-selected: [option name]`, continue to next task. - **checkpoint:human-action** → STOP normally. Auth gates cannot be automated — return structured checkpoint message using checkpoint_return_format. **Standard checkpoint behavior** (when `AUTO_CFG` is not `"true"`): @@ -340,6 +340,7 @@ When hitting checkpoint or auth gate, return this structure: ## CHECKPOINT REACHED **Type:** [human-verify | decision | human-action] +**Gate:** [blocking | blocking-human] — copy the task's `gate` attribute verbatim so the orchestrator's carve-out sees it **Plan:** {phase}-{plan} **Progress:** {completed}/{total} tasks complete diff --git a/gsd-core/references/checkpoints.md b/gsd-core/references/checkpoints.md index d63f06707..2fd6bbb69 100644 --- a/gsd-core/references/checkpoints.md +++ b/gsd-core/references/checkpoints.md @@ -9,6 +9,18 @@ Plans execute autonomously. Checkpoints formalize interaction points where human 3. **User only does what requires human judgment** - Visual checks, UX evaluation, "does this feel right?" 4. **Secrets come from user, automation comes from Claude** - Ask for API keys, then Claude uses them via CLI 5. **Auto-mode bypasses verification/decision checkpoints** — When `workflow._auto_chain_active` or `workflow.auto_advance` is true in config: human-verify auto-approves, decision auto-selects first option, human-action still stops (auth gates cannot be automated) +6. **`gate="blocking-human"` is never auto-approved** — a checkpoint carrying this gate stops for a human in *every* mode, including auto-mode, regardless of its type. Rule 5 does not apply to it. + +**The `gate` attribute:** + +| Value | Auto-mode behavior | Use for | +|-------|--------------------|---------| +| `gate="blocking"` | Bypassed per rule 5 (human-verify auto-approves, decision auto-selects) | The default. Post-hoc verification and implementation choices that are safe to take the recommended path on when unattended. | +| `gate="blocking-human"` | **Never bypassed.** Stops for a human in auto-mode too. | Irreversible or trust-establishing steps a human must actually see: package-legitimacy verification before install, and any decision whose default answer would be wrong to assume. | + +Reach for `gate="blocking-human"` whenever auto-approving the checkpoint would defeat its purpose. If the checkpoint exists because a human must *decide* something, `blocking` is the wrong gate — auto-mode will decide it for them. + +The gate spans two layers, and both must honor it. `gsd-executor` refuses to auto-approve a `gate="blocking-human"` checkpoint and escalates it via `checkpoint_return_format` precisely so a human sees it; `execute-phase`'s `checkpoint_handling` step then decides what the user is actually shown. An orchestrator that dispatches on checkpoint *type* alone would auto-approve the very checkpoint the executor just refused to auto-approve, nullifying that refusal one layer up and letting an unattended `--auto` / `--chain` run install a package no human ever vetted. diff --git a/gsd-core/workflows/execute-phase.md b/gsd-core/workflows/execute-phase.md index 212b5c293..667bbccd5 100644 --- a/gsd-core/workflows/execute-phase.md +++ b/gsd-core/workflows/execute-phase.md @@ -1056,11 +1056,13 @@ AUTO_MODE=$(gsd_run query check auto-mode --pick active 2>/dev/null || echo "fal ``` When executor returns a checkpoint AND `AUTO_MODE` is `true`: -- **human-verify** → Auto-spawn continuation agent with `{user_response}` = `"approved"`. Log `⚡ Auto-approved checkpoint`. -- **decision** → Auto-spawn continuation agent with `{user_response}` = first option from checkpoint details. Log `⚡ Auto-selected: [option]`. +- **human-verify** → Auto-spawn continuation agent with `{user_response}` = `"approved"`. Log `⚡ Auto-approved checkpoint`. **Except `blocking-human`.** +- **decision** → Auto-spawn continuation agent with `{user_response}` = first option from checkpoint details. Log `⚡ Auto-selected: [option]`. **Except `blocking-human`.** - **human-action** → Present to user (existing behavior below). Auth gates cannot be automated. -**Standard flow (not auto-mode, or human-action type):** +**Carve-out — overrides all branches above.** If the returned `Gate:` is `blocking-human`, or its `` mentions `Package verification required before install` or `Package install failed — human verification required`, never auto-approve or auto-select, regardless of type. Present to user (standard flow below). Log `⛔ blocking-human gate — auto-mode suspended`. + +**Standard flow (not auto-mode, human-action, or blocking-human):** 1. Spawn agent for checkpoint plan 2. Agent runs until checkpoint task or auth gate → returns structured state diff --git a/tests/agent-size-baseline.json b/tests/agent-size-baseline.json index 5044c3856..84ec93622 100644 --- a/tests/agent-size-baseline.json +++ b/tests/agent-size-baseline.json @@ -14,7 +14,7 @@ "gsd-domain-researcher.md": 7032, "gsd-eval-auditor.md": 12496, "gsd-eval-planner.md": 7008, - "gsd-executor.md": 43607, + "gsd-executor.md": 43973, "gsd-framework-selector.md": 6778, "gsd-integration-checker.md": 15238, "gsd-intel-updater.md": 18166, diff --git a/tests/fixtures/golden-install-parity/antigravity.json b/tests/fixtures/golden-install-parity/antigravity.json index fc15df65d..5b607fd97 100644 --- a/tests/fixtures/golden-install-parity/antigravity.json +++ b/tests/fixtures/golden-install-parity/antigravity.json @@ -16,7 +16,7 @@ "agents/gsd-domain-researcher.md": "1db46cac3f4d9889", "agents/gsd-eval-auditor.md": "1b8391f1aafb067f", "agents/gsd-eval-planner.md": "3d10fd11147f6857", - "agents/gsd-executor.md": "152ca51a10b99ff8", + "agents/gsd-executor.md": "6b7b461429de7d35", "agents/gsd-framework-selector.md": "daa62c79619c76bf", "agents/gsd-integration-checker.md": "0643cd2d779b131c", "agents/gsd-intel-updater.md": "26c1f1e028c6346a", @@ -55,7 +55,7 @@ "gsd-core/references/api-coverage.md": "205a43c5fa7c221c", "gsd-core/references/artifact-types.md": "e176817364a7cbf4", "gsd-core/references/autonomous-smart-discuss.md": "efd80aca449032ad", - "gsd-core/references/checkpoints.md": "2de680837faa9752", + "gsd-core/references/checkpoints.md": "509700508de43306", "gsd-core/references/common-bug-patterns.md": "780145be56352626", "gsd-core/references/context-budget.md": "177520ead2ae3a23", "gsd-core/references/continuation-format.md": "580287399ad3ba68", @@ -233,7 +233,7 @@ "gsd-core/workflows/docs-update.md": "8e986e26d0e6e1a0", "gsd-core/workflows/edit-phase.md": "fc932e82ba1f585a", "gsd-core/workflows/eval-review.md": "eb4040eaa5b8497f", - "gsd-core/workflows/execute-phase.md": "82beafd82b24210c", + "gsd-core/workflows/execute-phase.md": "894d0e9efed72258", "gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "55d0706e80a2554a", "gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028", "gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "9b7107b31b60a3b9", diff --git a/tests/fixtures/golden-install-parity/augment.json b/tests/fixtures/golden-install-parity/augment.json index 120386618..4db1c9c1f 100644 --- a/tests/fixtures/golden-install-parity/augment.json +++ b/tests/fixtures/golden-install-parity/augment.json @@ -16,7 +16,7 @@ "agents/gsd-domain-researcher.md": "671c9ea949889c4a", "agents/gsd-eval-auditor.md": "fcaec7b00f94c435", "agents/gsd-eval-planner.md": "a4a5b4b3f7828ba3", - "agents/gsd-executor.md": "46f911cd211034b0", + "agents/gsd-executor.md": "ed2c39fa8c03d056", "agents/gsd-framework-selector.md": "4b77eebbe9288d80", "agents/gsd-integration-checker.md": "fa53e2d78be1de74", "agents/gsd-intel-updater.md": "fa40e685d7441ace", @@ -126,7 +126,7 @@ "gsd-core/references/api-coverage.md": "66264d41dfd9154a", "gsd-core/references/artifact-types.md": "a6d2e1f9453ffbf5", "gsd-core/references/autonomous-smart-discuss.md": "2fc710cde0ec7785", - "gsd-core/references/checkpoints.md": "6aa620c6ca38bdf0", + "gsd-core/references/checkpoints.md": "b1530628d693fc85", "gsd-core/references/common-bug-patterns.md": "780145be56352626", "gsd-core/references/context-budget.md": "f1ce57bf418824af", "gsd-core/references/continuation-format.md": "ce73e57d053ab8a7", @@ -304,7 +304,7 @@ "gsd-core/workflows/docs-update.md": "f35922d15b7061c9", "gsd-core/workflows/edit-phase.md": "966a3eadd1bebc04", "gsd-core/workflows/eval-review.md": "f898936e2cfe4130", - "gsd-core/workflows/execute-phase.md": "1b2d0b868f574e80", + "gsd-core/workflows/execute-phase.md": "7d42e81188b3613a", "gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "4e265392b3f2ba0e", "gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028", "gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "811b6d8489571581", diff --git a/tests/fixtures/golden-install-parity/claude-local.json b/tests/fixtures/golden-install-parity/claude-local.json index de6f1260e..6cc6844b6 100644 --- a/tests/fixtures/golden-install-parity/claude-local.json +++ b/tests/fixtures/golden-install-parity/claude-local.json @@ -15,7 +15,7 @@ "agents/gsd-domain-researcher.md": "f1e03df842ddfb95", "agents/gsd-eval-auditor.md": "d0f45fff7370bb0b", "agents/gsd-eval-planner.md": "9cc049b82897daa4", - "agents/gsd-executor.md": "bf1de739df0c9245", + "agents/gsd-executor.md": "62f37fae936e0d2f", "agents/gsd-framework-selector.md": "85005d716f9d98f7", "agents/gsd-integration-checker.md": "17a8ee731986564d", "agents/gsd-intel-updater.md": "4953a465db9dadc1", @@ -125,7 +125,7 @@ "gsd-core/references/api-coverage.md": "205a43c5fa7c221c", "gsd-core/references/artifact-types.md": "251040866a3a1818", "gsd-core/references/autonomous-smart-discuss.md": "2fc710cde0ec7785", - "gsd-core/references/checkpoints.md": "6aa620c6ca38bdf0", + "gsd-core/references/checkpoints.md": "b1530628d693fc85", "gsd-core/references/common-bug-patterns.md": "780145be56352626", "gsd-core/references/context-budget.md": "6bfac08025ea3106", "gsd-core/references/continuation-format.md": "580287399ad3ba68", @@ -303,7 +303,7 @@ "gsd-core/workflows/docs-update.md": "cd753783ab95da00", "gsd-core/workflows/edit-phase.md": "dbbb6191f5a8b65e", "gsd-core/workflows/eval-review.md": "086a1f2b3c11462c", - "gsd-core/workflows/execute-phase.md": "d7d8ac751aa2915e", + "gsd-core/workflows/execute-phase.md": "e5bc721629beaa01", "gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "6d38bfd540030da4", "gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028", "gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "611b2be3bd133eb1", diff --git a/tests/fixtures/golden-install-parity/claude.json b/tests/fixtures/golden-install-parity/claude.json index 278315fb6..d79453924 100644 --- a/tests/fixtures/golden-install-parity/claude.json +++ b/tests/fixtures/golden-install-parity/claude.json @@ -15,7 +15,7 @@ "agents/gsd-domain-researcher.md": "5f7d366251b957fe", "agents/gsd-eval-auditor.md": "fea2759beff0a642", "agents/gsd-eval-planner.md": "112f6730f23854e3", - "agents/gsd-executor.md": "22fc2f17098a4d14", + "agents/gsd-executor.md": "d1750d26580daa7f", "agents/gsd-framework-selector.md": "c350ee693cb1aa4e", "agents/gsd-integration-checker.md": "c8b4e65dee89c8ea", "agents/gsd-intel-updater.md": "5b41e05f90ce89d9", @@ -54,7 +54,7 @@ "gsd-core/references/api-coverage.md": "205a43c5fa7c221c", "gsd-core/references/artifact-types.md": "8bd01fd75a2ba70e", "gsd-core/references/autonomous-smart-discuss.md": "2fc710cde0ec7785", - "gsd-core/references/checkpoints.md": "6aa620c6ca38bdf0", + "gsd-core/references/checkpoints.md": "b1530628d693fc85", "gsd-core/references/common-bug-patterns.md": "780145be56352626", "gsd-core/references/context-budget.md": "533eae480bfc4bb8", "gsd-core/references/continuation-format.md": "580287399ad3ba68", @@ -232,7 +232,7 @@ "gsd-core/workflows/docs-update.md": "63082608d3ae92be", "gsd-core/workflows/edit-phase.md": "8323bfe10faa0c0a", "gsd-core/workflows/eval-review.md": "f59e8329dae1e528", - "gsd-core/workflows/execute-phase.md": "4aa35e8cb9d68cde", + "gsd-core/workflows/execute-phase.md": "5231186012da87a2", "gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "facb0e816d87a0c7", "gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028", "gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "811b6d8489571581", diff --git a/tests/fixtures/golden-install-parity/cline.json b/tests/fixtures/golden-install-parity/cline.json index d1151047b..960882cc5 100644 --- a/tests/fixtures/golden-install-parity/cline.json +++ b/tests/fixtures/golden-install-parity/cline.json @@ -19,7 +19,7 @@ "agents/gsd-domain-researcher.md": "0fecdaea86466a56", "agents/gsd-eval-auditor.md": "36c44303085df2f8", "agents/gsd-eval-planner.md": "3ddea88a69b4da3f", - "agents/gsd-executor.md": "441e624e9685e505", + "agents/gsd-executor.md": "2ca77ad7d9909f82", "agents/gsd-framework-selector.md": "564669d479433f15", "agents/gsd-integration-checker.md": "1bbbdd3d420b994e", "agents/gsd-intel-updater.md": "42c40fffbc720d0b", @@ -58,7 +58,7 @@ "gsd-core/references/api-coverage.md": "66264d41dfd9154a", "gsd-core/references/artifact-types.md": "a6d2e1f9453ffbf5", "gsd-core/references/autonomous-smart-discuss.md": "2fc710cde0ec7785", - "gsd-core/references/checkpoints.md": "9a7ba3a17ece1698", + "gsd-core/references/checkpoints.md": "4919e8aa1130fb04", "gsd-core/references/common-bug-patterns.md": "780145be56352626", "gsd-core/references/context-budget.md": "c154ba00dbbf4477", "gsd-core/references/continuation-format.md": "ce73e57d053ab8a7", @@ -236,7 +236,7 @@ "gsd-core/workflows/docs-update.md": "39f288623a8f6f32", "gsd-core/workflows/edit-phase.md": "9c9fadc047c61d74", "gsd-core/workflows/eval-review.md": "3e1d7829ed2ed494", - "gsd-core/workflows/execute-phase.md": "ae12e044fb2f3b57", + "gsd-core/workflows/execute-phase.md": "0d9ff2faecb72225", "gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "67ebc93f51968cb6", "gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028", "gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "82e6cfe1e1b1ec0e", diff --git a/tests/fixtures/golden-install-parity/codebuddy.json b/tests/fixtures/golden-install-parity/codebuddy.json index 7468c5bbf..4c2dfb970 100644 --- a/tests/fixtures/golden-install-parity/codebuddy.json +++ b/tests/fixtures/golden-install-parity/codebuddy.json @@ -16,7 +16,7 @@ "agents/gsd-domain-researcher.md": "1c1a800108a2b225", "agents/gsd-eval-auditor.md": "99012004b14ea602", "agents/gsd-eval-planner.md": "4ebdd7fe9cbb0cfe", - "agents/gsd-executor.md": "d0bfc88c01a63181", + "agents/gsd-executor.md": "d61e084540bb6ee2", "agents/gsd-framework-selector.md": "7726fccc86bfeb50", "agents/gsd-integration-checker.md": "2d8339790bbb2dc3", "agents/gsd-intel-updater.md": "c51339956197cbd3", @@ -126,7 +126,7 @@ "gsd-core/references/api-coverage.md": "66264d41dfd9154a", "gsd-core/references/artifact-types.md": "a6d2e1f9453ffbf5", "gsd-core/references/autonomous-smart-discuss.md": "2fc710cde0ec7785", - "gsd-core/references/checkpoints.md": "6aa620c6ca38bdf0", + "gsd-core/references/checkpoints.md": "b1530628d693fc85", "gsd-core/references/common-bug-patterns.md": "780145be56352626", "gsd-core/references/context-budget.md": "f1ce57bf418824af", "gsd-core/references/continuation-format.md": "ce73e57d053ab8a7", @@ -304,7 +304,7 @@ "gsd-core/workflows/docs-update.md": "f35922d15b7061c9", "gsd-core/workflows/edit-phase.md": "966a3eadd1bebc04", "gsd-core/workflows/eval-review.md": "f898936e2cfe4130", - "gsd-core/workflows/execute-phase.md": "cf72b6db051f9189", + "gsd-core/workflows/execute-phase.md": "f17719f6b780cd2b", "gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "4e265392b3f2ba0e", "gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028", "gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "811b6d8489571581", diff --git a/tests/fixtures/golden-install-parity/codex.json b/tests/fixtures/golden-install-parity/codex.json index 1ddaf96c1..cc6bbab50 100644 --- a/tests/fixtures/golden-install-parity/codex.json +++ b/tests/fixtures/golden-install-parity/codex.json @@ -102,8 +102,8 @@ "agents/gsd-eval-auditor.toml": "9b81d61b3c5f722d", "agents/gsd-eval-planner.md": "73f2ad2ff2797a51", "agents/gsd-eval-planner.toml": "09468ad1a34ac468", - "agents/gsd-executor.md": "93a05c1436bc4458", - "agents/gsd-executor.toml": "f5a55d4eacd8613c", + "agents/gsd-executor.md": "0393e5f72e932127", + "agents/gsd-executor.toml": "cdb7bdc04d3b6651", "agents/gsd-framework-selector.md": "ebae32430887d2e0", "agents/gsd-framework-selector.toml": "637e4e021b7ec380", "agents/gsd-integration-checker.md": "9cc875676cf7d741", @@ -161,7 +161,7 @@ "gsd-core/references/api-coverage.md": "524382216a8e713f", "gsd-core/references/artifact-types.md": "3218cafb0c92dc32", "gsd-core/references/autonomous-smart-discuss.md": "4156025334411073", - "gsd-core/references/checkpoints.md": "9feb961f644afa96", + "gsd-core/references/checkpoints.md": "afc6933d99f73358", "gsd-core/references/common-bug-patterns.md": "780145be56352626", "gsd-core/references/context-budget.md": "0b67ea1d5db4bc08", "gsd-core/references/continuation-format.md": "e64c0da2b3d0f2f0", @@ -339,7 +339,7 @@ "gsd-core/workflows/docs-update.md": "e255317df939e302", "gsd-core/workflows/edit-phase.md": "e592a4d85ce5380f", "gsd-core/workflows/eval-review.md": "63d0d0670b54c244", - "gsd-core/workflows/execute-phase.md": "e62b664cf4db4d55", + "gsd-core/workflows/execute-phase.md": "b15bbe2f8a8d8d02", "gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "f4cacd27d37bac65", "gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028", "gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "811b6d8489571581", diff --git a/tests/fixtures/golden-install-parity/copilot.json b/tests/fixtures/golden-install-parity/copilot.json index 2aab090d5..a39d2d9ae 100644 --- a/tests/fixtures/golden-install-parity/copilot.json +++ b/tests/fixtures/golden-install-parity/copilot.json @@ -16,7 +16,7 @@ "agents/gsd-domain-researcher.agent.md": "d603239b3e9fe428", "agents/gsd-eval-auditor.agent.md": "3c03009564de55c8", "agents/gsd-eval-planner.agent.md": "14751876fc2b5f16", - "agents/gsd-executor.agent.md": "0058082fef5a4985", + "agents/gsd-executor.agent.md": "2ee1020062d8d800", "agents/gsd-framework-selector.agent.md": "cafeec0b3489be45", "agents/gsd-integration-checker.agent.md": "30439b804927acc7", "agents/gsd-intel-updater.agent.md": "238c1a886f35a25c", @@ -56,7 +56,7 @@ "gsd-core/references/api-coverage.md": "205a43c5fa7c221c", "gsd-core/references/artifact-types.md": "f992de8b2b1a4420", "gsd-core/references/autonomous-smart-discuss.md": "efd80aca449032ad", - "gsd-core/references/checkpoints.md": "c70b323dcb1583d5", + "gsd-core/references/checkpoints.md": "53ca1c205dda8b65", "gsd-core/references/common-bug-patterns.md": "780145be56352626", "gsd-core/references/context-budget.md": "435474d5e10be65a", "gsd-core/references/continuation-format.md": "580287399ad3ba68", @@ -234,7 +234,7 @@ "gsd-core/workflows/docs-update.md": "9292cfa3c52c434e", "gsd-core/workflows/edit-phase.md": "8667c28b22b1599f", "gsd-core/workflows/eval-review.md": "81c8e72ba3862856", - "gsd-core/workflows/execute-phase.md": "6f7aa92705d3527e", + "gsd-core/workflows/execute-phase.md": "80a1e0722f72dbb1", "gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "63b712920f21a40f", "gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028", "gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "1b73ab2fc47c9dbf", diff --git a/tests/fixtures/golden-install-parity/cursor.json b/tests/fixtures/golden-install-parity/cursor.json index 41ddd34a1..a6afd63ea 100644 --- a/tests/fixtures/golden-install-parity/cursor.json +++ b/tests/fixtures/golden-install-parity/cursor.json @@ -16,7 +16,7 @@ "agents/gsd-domain-researcher.md": "56395dbdabf076f6", "agents/gsd-eval-auditor.md": "ad2840fd5cd76172", "agents/gsd-eval-planner.md": "2049dac060d00eda", - "agents/gsd-executor.md": "1fb2c778178cfa1c", + "agents/gsd-executor.md": "e416f9c48fb44c75", "agents/gsd-framework-selector.md": "4b77eebbe9288d80", "agents/gsd-integration-checker.md": "5da30584d06b878c", "agents/gsd-intel-updater.md": "b8971c5d96e63b38", @@ -126,7 +126,7 @@ "gsd-core/references/api-coverage.md": "205a43c5fa7c221c", "gsd-core/references/artifact-types.md": "8bd01fd75a2ba70e", "gsd-core/references/autonomous-smart-discuss.md": "273b371c5751f35a", - "gsd-core/references/checkpoints.md": "3001eeccb319781b", + "gsd-core/references/checkpoints.md": "f852c96c5fe25015", "gsd-core/references/common-bug-patterns.md": "780145be56352626", "gsd-core/references/context-budget.md": "304dcdab82a27623", "gsd-core/references/continuation-format.md": "580287399ad3ba68", @@ -304,7 +304,7 @@ "gsd-core/workflows/docs-update.md": "e86d7d7e2e3dac6d", "gsd-core/workflows/edit-phase.md": "8323bfe10faa0c0a", "gsd-core/workflows/eval-review.md": "a86279dd98dd5c03", - "gsd-core/workflows/execute-phase.md": "cf1b15f505b519d8", + "gsd-core/workflows/execute-phase.md": "b3267f23af6444b2", "gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "facb0e816d87a0c7", "gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028", "gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "811b6d8489571581", diff --git a/tests/fixtures/golden-install-parity/hermes.json b/tests/fixtures/golden-install-parity/hermes.json index fe439f057..a3b083743 100644 --- a/tests/fixtures/golden-install-parity/hermes.json +++ b/tests/fixtures/golden-install-parity/hermes.json @@ -16,7 +16,7 @@ "agents/gsd-domain-researcher.md": "412cdbb05ba252ea", "agents/gsd-eval-auditor.md": "4ffb265063c318e5", "agents/gsd-eval-planner.md": "03448fc9c5774b56", - "agents/gsd-executor.md": "18365948be7c2dff", + "agents/gsd-executor.md": "9c00261ee92596bb", "agents/gsd-framework-selector.md": "ea9981d65d6b3429", "agents/gsd-integration-checker.md": "35b4f2969d279871", "agents/gsd-intel-updater.md": "5fe5edfae2719cb8", @@ -55,7 +55,7 @@ "gsd-core/references/api-coverage.md": "205a43c5fa7c221c", "gsd-core/references/artifact-types.md": "8bd01fd75a2ba70e", "gsd-core/references/autonomous-smart-discuss.md": "2fc710cde0ec7785", - "gsd-core/references/checkpoints.md": "db8a7425ed808e24", + "gsd-core/references/checkpoints.md": "36b2de4768b228af", "gsd-core/references/common-bug-patterns.md": "780145be56352626", "gsd-core/references/context-budget.md": "b93e9f47aa1b1753", "gsd-core/references/continuation-format.md": "580287399ad3ba68", @@ -233,7 +233,7 @@ "gsd-core/workflows/docs-update.md": "4d6c06e611d83b6d", "gsd-core/workflows/edit-phase.md": "7f27003f20e88fb8", "gsd-core/workflows/eval-review.md": "f510e5762212dc6f", - "gsd-core/workflows/execute-phase.md": "f36c7b810285838c", + "gsd-core/workflows/execute-phase.md": "371ab0a4dae7a049", "gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "ceb8758c22660c1a", "gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028", "gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "c9ad17d6cc6dfe45", diff --git a/tests/fixtures/golden-install-parity/kilo.json b/tests/fixtures/golden-install-parity/kilo.json index 8f387eb90..017d2705c 100644 --- a/tests/fixtures/golden-install-parity/kilo.json +++ b/tests/fixtures/golden-install-parity/kilo.json @@ -16,7 +16,7 @@ "agents/gsd-domain-researcher.md": "a3874d80bcbc7380", "agents/gsd-eval-auditor.md": "630d4cd3bd6ea195", "agents/gsd-eval-planner.md": "3db12cde12aeb2c1", - "agents/gsd-executor.md": "c9aca9129bd4cab3", + "agents/gsd-executor.md": "24a1c8c8e829d2d1", "agents/gsd-framework-selector.md": "ad5f2c6b9bec6270", "agents/gsd-integration-checker.md": "c503e2f4a3d8ec05", "agents/gsd-intel-updater.md": "231393da62a45b2e", @@ -126,7 +126,7 @@ "gsd-core/references/api-coverage.md": "205a43c5fa7c221c", "gsd-core/references/artifact-types.md": "8bd01fd75a2ba70e", "gsd-core/references/autonomous-smart-discuss.md": "3986d58011bf9006", - "gsd-core/references/checkpoints.md": "9feb961f644afa96", + "gsd-core/references/checkpoints.md": "afc6933d99f73358", "gsd-core/references/common-bug-patterns.md": "780145be56352626", "gsd-core/references/context-budget.md": "533eae480bfc4bb8", "gsd-core/references/continuation-format.md": "580287399ad3ba68", @@ -304,7 +304,7 @@ "gsd-core/workflows/docs-update.md": "79afaaf19fd527cc", "gsd-core/workflows/edit-phase.md": "8323bfe10faa0c0a", "gsd-core/workflows/eval-review.md": "926eda8bbee28b23", - "gsd-core/workflows/execute-phase.md": "ec448a1e25854abf", + "gsd-core/workflows/execute-phase.md": "43c8c218f8933cc8", "gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "facb0e816d87a0c7", "gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028", "gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "811b6d8489571581", diff --git a/tests/fixtures/golden-install-parity/kimi.json b/tests/fixtures/golden-install-parity/kimi.json index 5da5f0f58..0a0c46cfc 100644 --- a/tests/fixtures/golden-install-parity/kimi.json +++ b/tests/fixtures/golden-install-parity/kimi.json @@ -61,7 +61,7 @@ "agents/subagents/gsd-eval-auditor.yaml": "e3d868bd5fefe938", "agents/subagents/gsd-eval-planner.md": "70f8c5727bfb9876", "agents/subagents/gsd-eval-planner.yaml": "df8499f7af297ec2", - "agents/subagents/gsd-executor.md": "278a569a7306244c", + "agents/subagents/gsd-executor.md": "d3b2806c227c6a27", "agents/subagents/gsd-executor.yaml": "e29422986636fd64", "agents/subagents/gsd-framework-selector.md": "a15b7aa1e0576e16", "agents/subagents/gsd-framework-selector.yaml": "fb52c31cde27b0e3", @@ -119,7 +119,7 @@ "gsd-core/references/api-coverage.md": "66264d41dfd9154a", "gsd-core/references/artifact-types.md": "a6d2e1f9453ffbf5", "gsd-core/references/autonomous-smart-discuss.md": "2fc710cde0ec7785", - "gsd-core/references/checkpoints.md": "6aa620c6ca38bdf0", + "gsd-core/references/checkpoints.md": "b1530628d693fc85", "gsd-core/references/common-bug-patterns.md": "780145be56352626", "gsd-core/references/context-budget.md": "f1ce57bf418824af", "gsd-core/references/continuation-format.md": "ce73e57d053ab8a7", @@ -297,7 +297,7 @@ "gsd-core/workflows/docs-update.md": "f35922d15b7061c9", "gsd-core/workflows/edit-phase.md": "966a3eadd1bebc04", "gsd-core/workflows/eval-review.md": "f898936e2cfe4130", - "gsd-core/workflows/execute-phase.md": "91fe17be2d178f24", + "gsd-core/workflows/execute-phase.md": "39def0423fb4eb45", "gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "4e265392b3f2ba0e", "gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028", "gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "811b6d8489571581", diff --git a/tests/fixtures/golden-install-parity/opencode.json b/tests/fixtures/golden-install-parity/opencode.json index aa6842369..5bae80a00 100644 --- a/tests/fixtures/golden-install-parity/opencode.json +++ b/tests/fixtures/golden-install-parity/opencode.json @@ -16,7 +16,7 @@ "agents/gsd-domain-researcher.md": "71250e759ca9e723", "agents/gsd-eval-auditor.md": "c88890105f32ace6", "agents/gsd-eval-planner.md": "60bddb70a937f796", - "agents/gsd-executor.md": "b091b0126895ae03", + "agents/gsd-executor.md": "cad2c28464d535df", "agents/gsd-framework-selector.md": "1c0a10355e787675", "agents/gsd-integration-checker.md": "a9de5928e5a5c649", "agents/gsd-intel-updater.md": "493e07482fa6198a", @@ -126,7 +126,7 @@ "gsd-core/references/api-coverage.md": "205a43c5fa7c221c", "gsd-core/references/artifact-types.md": "218c55caf8aff6df", "gsd-core/references/autonomous-smart-discuss.md": "3986d58011bf9006", - "gsd-core/references/checkpoints.md": "9feb961f644afa96", + "gsd-core/references/checkpoints.md": "afc6933d99f73358", "gsd-core/references/common-bug-patterns.md": "780145be56352626", "gsd-core/references/context-budget.md": "355826e667f9ccd1", "gsd-core/references/continuation-format.md": "580287399ad3ba68", @@ -304,7 +304,7 @@ "gsd-core/workflows/docs-update.md": "850366c2ef8fb780", "gsd-core/workflows/edit-phase.md": "1876c855fb0a0a39", "gsd-core/workflows/eval-review.md": "5394694d29ad7543", - "gsd-core/workflows/execute-phase.md": "9026c312d564381f", + "gsd-core/workflows/execute-phase.md": "2a1c33e0bda26211", "gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "1804215577f1ad35", "gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028", "gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "baa2c401af10a80a", diff --git a/tests/fixtures/golden-install-parity/pi.json b/tests/fixtures/golden-install-parity/pi.json index 2cd30cb92..753308586 100644 --- a/tests/fixtures/golden-install-parity/pi.json +++ b/tests/fixtures/golden-install-parity/pi.json @@ -22,7 +22,7 @@ "gsd-core/references/api-coverage.md": "66264d41dfd9154a", "gsd-core/references/artifact-types.md": "a6d2e1f9453ffbf5", "gsd-core/references/autonomous-smart-discuss.md": "2fc710cde0ec7785", - "gsd-core/references/checkpoints.md": "6aa620c6ca38bdf0", + "gsd-core/references/checkpoints.md": "b1530628d693fc85", "gsd-core/references/common-bug-patterns.md": "780145be56352626", "gsd-core/references/context-budget.md": "f1ce57bf418824af", "gsd-core/references/continuation-format.md": "ce73e57d053ab8a7", @@ -200,7 +200,7 @@ "gsd-core/workflows/docs-update.md": "f35922d15b7061c9", "gsd-core/workflows/edit-phase.md": "966a3eadd1bebc04", "gsd-core/workflows/eval-review.md": "f898936e2cfe4130", - "gsd-core/workflows/execute-phase.md": "8f6c2443655aceb1", + "gsd-core/workflows/execute-phase.md": "8f6dc95cc8030259", "gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "4e265392b3f2ba0e", "gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028", "gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "811b6d8489571581", diff --git a/tests/fixtures/golden-install-parity/qwen.json b/tests/fixtures/golden-install-parity/qwen.json index ccd12a98c..b26a125e8 100644 --- a/tests/fixtures/golden-install-parity/qwen.json +++ b/tests/fixtures/golden-install-parity/qwen.json @@ -16,7 +16,7 @@ "agents/gsd-domain-researcher.md": "bd054bb27beed2a7", "agents/gsd-eval-auditor.md": "57cc7458ab5de6b7", "agents/gsd-eval-planner.md": "01b665728dde4ccf", - "agents/gsd-executor.md": "b50d53e45df6431c", + "agents/gsd-executor.md": "72c02583b569a25f", "agents/gsd-framework-selector.md": "82ba6abea84226b7", "agents/gsd-integration-checker.md": "90835dbc7dfa1691", "agents/gsd-intel-updater.md": "3cc4f6ddd04676ec", @@ -55,7 +55,7 @@ "gsd-core/references/api-coverage.md": "205a43c5fa7c221c", "gsd-core/references/artifact-types.md": "8bd01fd75a2ba70e", "gsd-core/references/autonomous-smart-discuss.md": "2fc710cde0ec7785", - "gsd-core/references/checkpoints.md": "046171320f816346", + "gsd-core/references/checkpoints.md": "83c7ac100419e9e0", "gsd-core/references/common-bug-patterns.md": "780145be56352626", "gsd-core/references/context-budget.md": "a1351dd40ef8691f", "gsd-core/references/continuation-format.md": "580287399ad3ba68", @@ -233,7 +233,7 @@ "gsd-core/workflows/docs-update.md": "45d2f0d173c84e07", "gsd-core/workflows/edit-phase.md": "0fb5e0123cfc6f36", "gsd-core/workflows/eval-review.md": "6dee8a1e40ececd4", - "gsd-core/workflows/execute-phase.md": "60deeb0a791c3cd5", + "gsd-core/workflows/execute-phase.md": "b6d1f7dbc3c81a4f", "gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "36af8d91e4ae8b9c", "gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028", "gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "98db1ba4c39cd784", diff --git a/tests/fixtures/golden-install-parity/trae.json b/tests/fixtures/golden-install-parity/trae.json index 25b84997c..ec33c7022 100644 --- a/tests/fixtures/golden-install-parity/trae.json +++ b/tests/fixtures/golden-install-parity/trae.json @@ -16,7 +16,7 @@ "agents/gsd-domain-researcher.md": "b80f76874c04e515", "agents/gsd-eval-auditor.md": "470bf16303ec4d2e", "agents/gsd-eval-planner.md": "22334fde85723c9d", - "agents/gsd-executor.md": "45eeb443a6e6dbe8", + "agents/gsd-executor.md": "e5b96a52bd3c96c8", "agents/gsd-framework-selector.md": "7726fccc86bfeb50", "agents/gsd-integration-checker.md": "7cd2072984411c7f", "agents/gsd-intel-updater.md": "83de6ba9172891c3", @@ -55,7 +55,7 @@ "gsd-core/references/api-coverage.md": "205a43c5fa7c221c", "gsd-core/references/artifact-types.md": "8bd01fd75a2ba70e", "gsd-core/references/autonomous-smart-discuss.md": "2fc710cde0ec7785", - "gsd-core/references/checkpoints.md": "4b8c645ffa695067", + "gsd-core/references/checkpoints.md": "610690dba4d600c1", "gsd-core/references/common-bug-patterns.md": "780145be56352626", "gsd-core/references/context-budget.md": "b7e9640063775c98", "gsd-core/references/continuation-format.md": "580287399ad3ba68", @@ -233,7 +233,7 @@ "gsd-core/workflows/docs-update.md": "f13571f08e083356", "gsd-core/workflows/edit-phase.md": "7facd0faa33c8cad", "gsd-core/workflows/eval-review.md": "37d545d4f0db4927", - "gsd-core/workflows/execute-phase.md": "aa5abb8c77a00153", + "gsd-core/workflows/execute-phase.md": "80ffe75fda2a90ba", "gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "c985a30317a1aa6b", "gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028", "gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "0a9e915170c7121c", diff --git a/tests/fixtures/golden-install-parity/windsurf.json b/tests/fixtures/golden-install-parity/windsurf.json index 48e41a345..92066bebf 100644 --- a/tests/fixtures/golden-install-parity/windsurf.json +++ b/tests/fixtures/golden-install-parity/windsurf.json @@ -16,7 +16,7 @@ "agents/gsd-domain-researcher.md": "56395dbdabf076f6", "agents/gsd-eval-auditor.md": "fb64fc5acf359747", "agents/gsd-eval-planner.md": "2049dac060d00eda", - "agents/gsd-executor.md": "406fdcc0597bea77", + "agents/gsd-executor.md": "1eb9f58c5e9da94d", "agents/gsd-framework-selector.md": "4b77eebbe9288d80", "agents/gsd-integration-checker.md": "4ffb37fb230c2b90", "agents/gsd-intel-updater.md": "a81d77c143c02108", @@ -55,7 +55,7 @@ "gsd-core/references/api-coverage.md": "205a43c5fa7c221c", "gsd-core/references/artifact-types.md": "8bd01fd75a2ba70e", "gsd-core/references/autonomous-smart-discuss.md": "273b371c5751f35a", - "gsd-core/references/checkpoints.md": "808e4fcaa2fda15c", + "gsd-core/references/checkpoints.md": "de9f3b7bd86a44ac", "gsd-core/references/common-bug-patterns.md": "780145be56352626", "gsd-core/references/context-budget.md": "ff843cd6139c8564", "gsd-core/references/continuation-format.md": "580287399ad3ba68", @@ -233,7 +233,7 @@ "gsd-core/workflows/docs-update.md": "70f73cc8c27e0ca0", "gsd-core/workflows/edit-phase.md": "c0ae7d0063f3e789", "gsd-core/workflows/eval-review.md": "b28be79ef29f16fd", - "gsd-core/workflows/execute-phase.md": "1dac1bdda0696dc0", + "gsd-core/workflows/execute-phase.md": "d6bf589fb0370bc7", "gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "47ae5482f8e64100", "gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028", "gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "15bca39a75c664be", diff --git a/tests/fixtures/golden-install-parity/zcode.json b/tests/fixtures/golden-install-parity/zcode.json index 790d628ad..935b04b5f 100644 --- a/tests/fixtures/golden-install-parity/zcode.json +++ b/tests/fixtures/golden-install-parity/zcode.json @@ -16,7 +16,7 @@ "agents/gsd-domain-researcher.md": "049f588663814fa2", "agents/gsd-eval-auditor.md": "54870d3b07433525", "agents/gsd-eval-planner.md": "552e9fa164c51ce8", - "agents/gsd-executor.md": "7bc50a045cb23811", + "agents/gsd-executor.md": "fdd9635cee849c82", "agents/gsd-framework-selector.md": "8a795f230436ad2e", "agents/gsd-integration-checker.md": "c1760a0bbd4f7bf5", "agents/gsd-intel-updater.md": "944f1d903e2e9e09", @@ -126,7 +126,7 @@ "gsd-core/references/api-coverage.md": "66264d41dfd9154a", "gsd-core/references/artifact-types.md": "a6d2e1f9453ffbf5", "gsd-core/references/autonomous-smart-discuss.md": "2fc710cde0ec7785", - "gsd-core/references/checkpoints.md": "6aa620c6ca38bdf0", + "gsd-core/references/checkpoints.md": "b1530628d693fc85", "gsd-core/references/common-bug-patterns.md": "780145be56352626", "gsd-core/references/context-budget.md": "f1ce57bf418824af", "gsd-core/references/continuation-format.md": "ce73e57d053ab8a7", @@ -304,7 +304,7 @@ "gsd-core/workflows/docs-update.md": "f35922d15b7061c9", "gsd-core/workflows/edit-phase.md": "966a3eadd1bebc04", "gsd-core/workflows/eval-review.md": "f898936e2cfe4130", - "gsd-core/workflows/execute-phase.md": "bb4f1120804c5a90", + "gsd-core/workflows/execute-phase.md": "55c0e6f659e27ff2", "gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "4e265392b3f2ba0e", "gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028", "gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "811b6d8489571581", diff --git a/tests/package-legitimacy-gate.test.cjs b/tests/package-legitimacy-gate.test.cjs index 5d0a3e051..858363d4b 100644 --- a/tests/package-legitimacy-gate.test.cjs +++ b/tests/package-legitimacy-gate.test.cjs @@ -5,6 +5,12 @@ * * Verifies that the three agents (researcher, planner, executor) contain the * interlocking instruction text that forms the slopsquatting defence gate. + * + * The gate spans TWO layers. The executor stops at a `gate="blocking-human"` + * checkpoint and hands it up; the execute-phase orchestrator then decides + * whether the human ever sees it. Asserting only the executor half leaves the + * orchestrator free to auto-approve the checkpoint the executor just refused + * to auto-approve. */ const { describe, test, before } = require('node:test'); @@ -17,6 +23,9 @@ const RESEARCHER = path.join(AGENTS, 'gsd-phase-researcher.md'); const PLANNER = path.join(AGENTS, 'gsd-planner.md'); const EXECUTOR = path.join(AGENTS, 'gsd-executor.md'); +const WORKFLOWS = path.join(__dirname, '..', 'gsd-core', 'workflows'); +const EXECUTE_PHASE = path.join(WORKFLOWS, 'execute-phase.md'); + function parseSections(md) { const lines = md.split(/\r?\n/); const sections = []; @@ -475,4 +484,134 @@ describe('gsd-executor.md — package installs excluded from RULE 3 auto-fix', ( 'executor auto mode must explicitly block auto-approval for package-legitimacy checkpoints' ); }); + + // #2107 harm, one checkpoint type over: the executor auto-resolves a decision + // checkpoint itself (auto-selects the first option and continues) without ever + // returning it, so a blocking-human decision must be carved out HERE — the + // orchestrator's carve-out never runs for a checkpoint the executor swallowed. + test('auto mode does not auto-select a blocking-human decision checkpoint', () => { + const autoModeLine = lineIndexes(model.lines, (line) => hasAllTokens(line, ['auto-mode', 'checkpoint', 'behavior']))[0]; + assert.notEqual(autoModeLine, undefined, 'executor must define auto-mode checkpoint behavior'); + + const window = model.lines.slice(autoModeLine, autoModeLine + 25); + + const decisionLines = window.filter((line) => hasAllTokens(line, ['checkpoint:decision'])); + assert.ok(decisionLines.length > 0, 'executor auto-mode must document the checkpoint:decision branch'); + + const gatesDecision = decisionLines.some( + (line) => + hasAllTokens(line, ['blocking-human']) && + (hasAllTokens(line, ['stop']) || hasAllTokens(line, ['not', 'auto-select'])) + ); + + assert.ok( + gatesDecision, + 'checkpoint:decision must carve out gate="blocking-human" (STOP + return) instead of auto-selecting the first option' + ); + }); + + test('checkpoint_return_format transports the gate across the executor→orchestrator boundary', () => { + const fmt = extractXmlElement(model.text, 'checkpoint_return_format'); + assert.ok(fmt.length > 0, 'executor must define checkpoint_return_format'); + + const fmtLines = fmt.split(/\r?\n/); + const hasGateField = fmtLines.some((line) => hasAllTokens(line, ['gate:', 'blocking-human'])); + + assert.ok( + hasGateField, + 'checkpoint_return_format must carry a **Gate:** field so blocking-human reaches the orchestrator carve-out' + ); + }); +}); + +describe('execute-phase.md — orchestrator honors the blocking-human gate', () => { + let model; + + before(() => { + model = readModel(EXECUTE_PHASE); + }); + + // The executor refuses to auto-approve a gate="blocking-human" checkpoint and + // returns it via checkpoint_return_format. The orchestrator's auto-mode branch + // is what runs next. If that branch dispatches purely on checkpoint *type*, it + // auto-approves the checkpoint the executor just escalated — nullifying the + // slopsquatting gate in exactly the unattended mode where it matters. + test('auto-mode checkpoint handling excludes blocking-human checkpoints', () => { + // NB: normalizeTokens keeps ':' as a word character, so the heading + // "**Auto-mode checkpoint handling:**" yields the token `handling:`, not + // `handling`. Anchor on the two tokens that survive intact. + const autoModeLine = lineIndexes(model.lines, (line) => + hasAllTokens(line, ['auto-mode', 'checkpoint']) + )[0]; + + assert.notEqual( + autoModeLine, + undefined, + 'execute-phase.md must define auto-mode checkpoint handling' + ); + + const window = model.lines.slice(autoModeLine, autoModeLine + 20); + + const honorsGate = + anyLineHasAll(window, ['blocking-human']) || + anyLineHasAll(window, ['except', 'package-legitimacy']); + + assert.ok( + honorsGate, + 'execute-phase auto-mode must not auto-approve gate="blocking-human" checkpoints — ' + + 'the executor escalates them precisely so a human sees them' + ); + }); + + test('auto-approve rule for human-verify is conditional, not unconditional', () => { + const autoApproveLines = lineIndexes(model.lines, (line) => + hasAllTokens(line, ['human-verify', 'auto-spawn', 'approved']) + ); + + assert.ok( + autoApproveLines.length > 0, + 'anchor drift: no human-verify auto-approve line matched — the conditional carve-out would pass vacuously' + ); + + for (const idx of autoApproveLines) { + const line = model.lines[idx]; + const isConditional = + hasAllTokens(line, ['unless']) || + hasAllTokens(line, ['except']) || + hasAllTokens(line, ['blocking-human']) || + hasAllTokens(line, ['if', 'not']); + + assert.ok( + isConditional, + `execute-phase.md:${idx + 1} auto-approves human-verify unconditionally; ` + + 'it must carve out gate="blocking-human"' + ); + } + }); + + test('auto-select rule for decision is conditional, not unconditional', () => { + const autoSelectLines = lineIndexes(model.lines, (line) => + hasAllTokens(line, ['decision', 'auto-spawn', 'first', 'option']) + ); + + assert.ok( + autoSelectLines.length > 0, + 'anchor drift: no decision auto-select line matched — the conditional carve-out would pass vacuously' + ); + + for (const idx of autoSelectLines) { + const line = model.lines[idx]; + const isConditional = + hasAllTokens(line, ['unless']) || + hasAllTokens(line, ['except']) || + hasAllTokens(line, ['blocking-human']) || + hasAllTokens(line, ['if', 'not']); + + assert.ok( + isConditional, + `execute-phase.md:${idx + 1} auto-selects a decision unconditionally; ` + + 'it must carve out gate="blocking-human"' + ); + } + }); }); diff --git a/tests/workflow-size-baseline.json b/tests/workflow-size-baseline.json index b91272241..4446e82b6 100644 --- a/tests/workflow-size-baseline.json +++ b/tests/workflow-size-baseline.json @@ -24,7 +24,7 @@ "docs-update.md": 55706, "edit-phase.md": 12927, "eval-review.md": 9967, - "execute-phase.md": 93132, + "execute-phase.md": 93583, "execute-plan.md": 32655, "explore.md": 10541, "extract-learnings.md": 12893, From e0f969af6a76a2b7b1c71a7458192b6906ea57e6 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Mon, 13 Jul 2026 15:36:17 -0400 Subject: [PATCH 7/8] refactor(#2246): centralize cross-platform path-separator handling (toPosixPath / toNativePath / posixNormalize) (#2247) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace every open-coded separator translation across the installer/hooks source with named, tested seams in shell-command-projection.cts (the platform seam), removing all hardcoded `/`+`\` from path handling: - toPosixPath(p) — this machine's native path → POSIX (running-OS relative; for local filesystem paths). - toNativePath(p) — POSIX → native (collapses the win32 `/\//g,'\\'` ternary). - posixNormalize(p)— unconditional `\`→`/`, OS-independent; for emitting paths to a POSIX/bash TARGET (which may differ from the running OS) and for parsing mixed-separator input. core-utils.toPosixPath now delegates to the seam, so its 20+ existing consumers resolve to one implementation; no duplicate helper. - ~47 sites across runtime-hooks-surface, runtime-artifact-conversion, runtime-artifact-install-plan, drift, init, worktree-safety, installer-migrations, installer-migration-authoring, install-engine, surface, verify, runtime-artifact-layout, schema-detect, check-command-router. - Closes the latent POSIX-literal-backslash corruption class (the regex form corrupts a POSIX path containing a literal backslash; split(path.sep) does not). - New unit + fast-check property tests for all three helpers. Closes #2246 Co-authored-by: Claude Opus 4.8 (1M context) --- .../bin/lib/runtime-artifact-install-plan.cjs | 5 +- src/check-command-router.cts | 4 +- src/core-utils.cts | 10 +- src/drift.cts | 10 +- src/init.cts | 12 +- src/install-engine.cts | 5 +- src/installer-migration-authoring.cts | 3 +- src/installer-migrations.cts | 4 +- src/runtime-artifact-conversion.cts | 13 +- src/runtime-artifact-install-plan.cts | 5 +- src/runtime-artifact-layout.cts | 3 +- src/runtime-hooks-surface.cts | 24 +-- src/schema-detect.cts | 4 +- src/shell-command-projection.cts | 55 +++++-- src/surface.cts | 6 +- src/verify.cts | 4 +- src/worktree-safety.cts | 4 +- ...shell-command-projection-path-sep.test.cjs | 148 ++++++++++++++++++ 18 files changed, 259 insertions(+), 60 deletions(-) create mode 100644 tests/shell-command-projection-path-sep.test.cjs diff --git a/gsd-core/bin/lib/runtime-artifact-install-plan.cjs b/gsd-core/bin/lib/runtime-artifact-install-plan.cjs index 1f203c95f..c0d9c09a4 100644 --- a/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +++ b/gsd-core/bin/lib/runtime-artifact-install-plan.cjs @@ -67,9 +67,10 @@ function createRuntimeArtifactInstallPlan(args) { // to kind.stage() for agents kind entries with a converter (convertedAgentsKind). // NO _stampNonClaudeRuntimeDefaults — agents are NOT stamped in the inline loop. const os = _require('node:os'); + const { posixNormalize } = _require('./shell-command-projection.cjs'); const homedirFn = homedir ?? (() => os.homedir()); - const resolvedTarget = path.resolve(layout.configDir).replace(/\\/g, '/'); - const homeDir = homedirFn().replace(/\\/g, '/'); + const resolvedTarget = posixNormalize(path.resolve(layout.configDir)); + const homeDir = posixNormalize(homedirFn()); const isGlobal = scope === 'global'; const isOpencode = layout.runtime === 'opencode'; const isWindowsHost = (platform ?? process.platform) === 'win32'; diff --git a/src/check-command-router.cts b/src/check-command-router.cts index 30f5dbf05..a180eb0cc 100644 --- a/src/check-command-router.cts +++ b/src/check-command-router.cts @@ -38,7 +38,7 @@ const { evaluatePredicate } = gatePredicateEval; // eslint-disable-next-line @typescript-eslint/no-require-imports import apiCoverageMod = require('./api-coverage.cjs'); const { detectApiIntegration, validateCoverageMatrix } = apiCoverageMod; -import { execTool } from './shell-command-projection.cjs'; +import { execTool, posixNormalize } from './shell-command-projection.cjs'; // ─── Helpers ────────────────────────────────────────────────────────────────── @@ -1035,7 +1035,7 @@ function cmdApiCoverageVerifyPre(projectDir: string, args: string[], raw: boolea // .planning/phases/ (or a milestone archive). The raw arg is never used as a // path, so `..`, absolute paths, and arbitrary directories cannot reach a // file read. Mirrors cmdVerifySchemaDrift's token-match approach. - let token = phaseArg.replace(/\\/g, '/').split('/').filter(Boolean).pop() || ''; + let token = posixNormalize(phaseArg).split('/').filter(Boolean).pop() || ''; // A token like ".." or "." carries no phase identity → unresolvable. if (token === '.' || token === '..') token = ''; diff --git a/src/core-utils.cts b/src/core-utils.cts index ec6f18f5c..ea96ed380 100644 --- a/src/core-utils.cts +++ b/src/core-utils.cts @@ -24,12 +24,18 @@ const { comparePhaseNum } = phaseIdModule; // eslint-disable-next-line @typescript-eslint/no-require-imports import planningWorkspace = require('./planning-workspace.cjs'); const { findContextMdIn } = planningWorkspace; +// eslint-disable-next-line @typescript-eslint/no-require-imports +import shellCommandProjection = require('./shell-command-projection.cjs'); // ─── Path helpers ──────────────────────────────────────────────────────────── -/** Normalize a relative path to always use forward slashes (cross-platform). */ +/** + * Normalize a relative path to always use forward slashes (cross-platform). + * Delegates to the single separator seam in shell-command-projection so there is + * exactly one implementation of native→POSIX conversion across the codebase. + */ function toPosixPath(p: string): string { - return p.split(path.sep).join('/'); + return shellCommandProjection.toPosixPath(p); } /** diff --git a/src/drift.cts b/src/drift.cts index 73e24d19d..e1a56837c 100644 --- a/src/drift.cts +++ b/src/drift.cts @@ -35,7 +35,7 @@ 'use strict'; import fs from 'node:fs'; -import { platformWriteSync } from './shell-command-projection.cjs'; +import { platformWriteSync, posixNormalize } from './shell-command-projection.cjs'; import { formatGsdSlash } from './runtime-slash.cjs'; // ─── Constants ─────────────────────────────────────────────────────────────── @@ -79,7 +79,7 @@ type DriftCategory = 'barrel' | 'migration' | 'route' | 'new_dir'; */ function classifyFile(file: unknown): DriftCategory | null { if (typeof file !== 'string' || !file) return null; - const norm = file.replace(/\\/g, '/'); + const norm = posixNormalize(file); if (MIGRATION_RES.some((r) => r.test(norm))) return 'migration'; if (ROUTE_RES.some((r) => r.test(norm))) return 'route'; if (BARREL_RE.test(norm)) return 'barrel'; @@ -95,7 +95,7 @@ function classifyFile(file: unknown): DriftCategory | null { * check `structureMd.includes('src/lib')` holds. */ function isPathMapped(file: string, structureMd: string): boolean { - const norm = file.replace(/\\/g, '/'); + const norm = posixNormalize(file); const parts = norm.split('/'); // Check prefixes from longest to shortest; any hit means "mapped". for (let i = parts.length - 1; i >= 1; i--) { @@ -191,7 +191,7 @@ function detectDrift(input: unknown): DetectDriftResult | SkippedResult { const seen = new Map(); for (const rawFile of added) { - const file = rawFile.replace(/\\/g, '/'); + const file = posixNormalize(rawFile); const specific = classifyFile(file); let category: string | null = specific; if (!category) { @@ -318,7 +318,7 @@ function chooseAffectedPaths(paths: string[]): string[] { const out = new Set(); for (const raw of paths || []) { if (typeof raw !== 'string' || !raw) continue; - const file = raw.replace(/\\/g, '/'); + const file = posixNormalize(raw); const parts = file.split('/'); if (parts.length === 0) continue; const top = parts[0]; diff --git a/src/init.cts b/src/init.cts index 0ec0da3f4..772d2fa71 100644 --- a/src/init.cts +++ b/src/init.cts @@ -9,7 +9,7 @@ import fs from 'node:fs'; import path from 'node:path'; import os from 'node:os'; -import { execGit, platformWriteSync, platformReadSync } from './shell-command-projection.cjs'; +import { execGit, platformWriteSync, platformReadSync, toNativePath, posixNormalize } from './shell-command-projection.cjs'; import { realClock } from './clock.cjs'; // eslint-disable-next-line @typescript-eslint/no-require-imports -- io.cjs is an export= CommonJS module import io = require('./io.cjs'); @@ -282,7 +282,7 @@ function getInitGitState(cwd: string): GitState { } resolved = path.resolve(resolved); if (process.platform === 'win32') { - return resolved.replace(/\//g, '\\').toLowerCase(); + return toNativePath(resolved).toLowerCase(); } return resolved; }; @@ -293,7 +293,7 @@ function getInitGitState(cwd: string): GitState { try { const prefixResult = execGit(['rev-parse', '--show-prefix'], { cwd, timeout: 5000 }) as unknown as Record; if (prefixResult['exitCode'] === 0) { - const prefix = (typeof prefixResult['stdout'] === 'string' ? prefixResult['stdout'] : '').trim().replace(/\\/g, '/'); + const prefix = posixNormalize((typeof prefixResult['stdout'] === 'string' ? prefixResult['stdout'] : '').trim()); inNestedSubdir = prefix.length > 0 && prefix !== '.' && prefix !== './'; resolvedByGitPrefix = true; } @@ -309,7 +309,7 @@ function getInitGitState(cwd: string): GitState { inNestedSubdir = false; } else { const rel = path.relative(rootNorm, cwdNorm); - const relNorm = process.platform === 'win32' ? rel.replace(/\//g, '\\') : rel; + const relNorm = toNativePath(rel); inNestedSubdir = relNorm !== '' && relNorm !== '.' && @@ -323,7 +323,7 @@ function getInitGitState(cwd: string): GitState { } if (inNestedSubdir && typeof worktreeRoot === 'string') { - const toComparableRaw = (p: string) => p.replace(/\\/g, '/').replace(/\/+$/g, '').toLowerCase(); + const toComparableRaw = (p: string) => posixNormalize(p).replace(/\/+$/g, '').toLowerCase(); if (toComparableRaw(worktreeRoot) === toComparableRaw(String(cwd))) { inNestedSubdir = false; } @@ -2226,7 +2226,7 @@ function buildAgentSkillsBlock( if (entry.kind === 'directive') { return `- Load the \`${entry.name}\` skill via the Skill tool before proceeding (plugin-provided).`; } - return `- @${String(entry.ref).replace(/\\/g, '/')}`; + return `- @${posixNormalize(String(entry.ref))}`; }).join('\n'); return `\nRead these user-configured skills:\n${lines}\n`; } diff --git a/src/install-engine.cts b/src/install-engine.cts index 31a6f4357..4910a6938 100644 --- a/src/install-engine.cts +++ b/src/install-engine.cts @@ -27,6 +27,7 @@ import runtimeArtifactLayout = require('./runtime-artifact-layout.cjs'); import runtimeArtifactInstallPlan = require('./runtime-artifact-install-plan.cjs'); import runtimeNamePolicy = require('./runtime-name-policy.cjs'); import installProfiles = require('./install-profiles.cjs'); +import { posixNormalize } from './shell-command-projection.cjs'; const { processAttribution } = runtimeArtifactConversion; // resolveRuntimeArtifactLayout: accessed via module ref (not destructured) so @@ -970,8 +971,8 @@ function installOpencodeFamilyArtifacts( isGlobal, isOpencode: behaviors.skipHomePrefixSubstitution === true, isWindowsHost: process.platform === 'win32', - resolvedTarget: path.resolve(configDir).replace(/\\/g, '/'), - homeDir: os.homedir().replace(/\\/g, '/'), + resolvedTarget: posixNormalize(path.resolve(configDir)), + homeDir: posixNormalize(os.homedir()), }); const commandDir = runtimeArtifactInstallPlan.assertDestWithinConfigHome(configDir, 'command'); diff --git a/src/installer-migration-authoring.cts b/src/installer-migration-authoring.cts index a859363d7..ca47135a9 100644 --- a/src/installer-migration-authoring.cts +++ b/src/installer-migration-authoring.cts @@ -8,6 +8,7 @@ */ import path from 'node:path'; +import { posixNormalize } from './shell-command-projection.cjs'; /** An unvalidated migration record supplied by the caller. */ export type MigrationRecord = Record; @@ -64,7 +65,7 @@ function requireActionEvidence(action: MigrationAction, field: string, migration function validateSafeRelPath(relPath: string, migration: MigrationRecord, actionType: string): void { const source = actionSource(migration, { relPath }); - const normalized = relPath.replace(/\\/g, '/'); + const normalized = posixNormalize(relPath); if (path.isAbsolute(normalized) || path.win32.isAbsolute(normalized)) { throw new Error(`migration action ${actionType} relPath must stay inside configDir: ${source}`); } diff --git a/src/installer-migrations.cts b/src/installer-migrations.cts index bbc82f355..2232401f2 100644 --- a/src/installer-migrations.cts +++ b/src/installer-migrations.cts @@ -16,7 +16,7 @@ import { type MigrationRecord, type MigrationAction, } from './installer-migration-authoring.cjs'; -import { platformWriteSync, retryRenameSync } from './shell-command-projection.cjs'; +import { platformWriteSync, retryRenameSync, posixNormalize } from './shell-command-projection.cjs'; import { realClock, type Clock } from './clock.cjs'; const MANIFEST_NAME = 'gsd-file-manifest.json'; @@ -139,7 +139,7 @@ function normalizeRelPath(relPath: string): string { if (typeof relPath !== 'string' || relPath.trim() === '') { throw new Error('migration action relPath must be a non-empty string'); } - const normalized = relPath.replace(/\\/g, '/'); + const normalized = posixNormalize(relPath); if (path.isAbsolute(normalized) || path.win32.isAbsolute(normalized)) { throw new Error(`migration action relPath must stay inside configDir: ${relPath}`); } diff --git a/src/runtime-artifact-conversion.cts b/src/runtime-artifact-conversion.cts index 79454dba8..76fadeabc 100644 --- a/src/runtime-artifact-conversion.cts +++ b/src/runtime-artifact-conversion.cts @@ -24,6 +24,7 @@ const { readGsdCommandNames, transformContentToHyphen } = commandRoster; import runtimeNamePolicy = require('./runtime-name-policy.cjs'); const { getDirName } = runtimeNamePolicy; import capabilityRegistry = require('./capability-registry.cjs'); +import { posixNormalize } from './shell-command-projection.cjs'; // #1383: resolve GSD's version WITHOUT a top-level // `require('../../../package.json')`. That require ran at module load on every @@ -2349,8 +2350,8 @@ function computePathPrefix({ isGlobal, isOpencode, isWindowsHost: _isWindowsHost // Without this, path.join on Windows produces a backslash prefix that // leaks into markdown content and breaks cross-platform substring checks. // See DEFECT.WINDOWS-PATH-LEAK-IN-MARKDOWN-CONTENT in CONTEXT.md. - const posixTarget = String(resolvedTarget).replace(/\\/g, '/'); - const posixHome = homeDir ? String(homeDir).replace(/\\/g, '/') : homeDir; + const posixTarget = posixNormalize(String(resolvedTarget)); + const posixHome = homeDir ? posixNormalize(String(homeDir)) : homeDir; if (isGlobal && posixTarget.startsWith(posixHome) && !isOpencode) { return '$HOME' + posixTarget.slice(posixHome.length) + '/'; } @@ -2696,8 +2697,8 @@ function rewriteStagedSkillBodies(stagedDir, opts) { } = opts; if (!fs.existsSync(stagedDir)) return; - const resolvedTarget = path.resolve(configDir).replace(/\\/g, '/'); - const homeDir = homedir().replace(/\\/g, '/'); + const resolvedTarget = posixNormalize(path.resolve(configDir)); + const homeDir = posixNormalize(homedir()); const isGlobal = scope === 'global'; const isOpencode = false; // #2087: opencode installs via the combined-family engine path, never through the generic rewrite const isWindowsHost = platform === 'win32'; @@ -2734,8 +2735,8 @@ function rewriteStagedCommandBodies(stagedDir, opts) { } = opts; if (!fs.existsSync(stagedDir)) return stagedDir; - const resolvedTarget = path.resolve(configDir).replace(/\\/g, '/'); - const homeDir = homedir().replace(/\\/g, '/'); + const resolvedTarget = posixNormalize(path.resolve(configDir)); + const homeDir = posixNormalize(homedir()); const isGlobal = scope === 'global'; const isOpencode = false; // #2087: opencode installs via the combined-family engine path, never through the generic rewrite const isWindowsHost = platform === 'win32'; diff --git a/src/runtime-artifact-install-plan.cts b/src/runtime-artifact-install-plan.cts index e2daf8d3e..e25a2c25f 100644 --- a/src/runtime-artifact-install-plan.cts +++ b/src/runtime-artifact-install-plan.cts @@ -177,9 +177,10 @@ function createRuntimeArtifactInstallPlan(args: CreateRuntimeArtifactInstallPlan // to kind.stage() for agents kind entries with a converter (convertedAgentsKind). // NO _stampNonClaudeRuntimeDefaults — agents are NOT stamped in the inline loop. const os = _require('node:os') as typeof import('node:os'); + const { posixNormalize } = _require('./shell-command-projection.cjs') as { posixNormalize: (p: string) => string }; const homedirFn: () => string = homedir ?? (() => os.homedir()); - const resolvedTarget = path.resolve(layout.configDir).replace(/\\/g, '/'); - const homeDir = homedirFn().replace(/\\/g, '/'); + const resolvedTarget = posixNormalize(path.resolve(layout.configDir)); + const homeDir = posixNormalize(homedirFn()); const isGlobal = scope === 'global'; const isOpencode = layout.runtime === 'opencode'; const isWindowsHost = (platform ?? process.platform) === 'win32'; diff --git a/src/runtime-artifact-layout.cts b/src/runtime-artifact-layout.cts index b964f9018..28339f19c 100644 --- a/src/runtime-artifact-layout.cts +++ b/src/runtime-artifact-layout.cts @@ -30,6 +30,7 @@ import runtimeArtifactConversion = require('./runtime-artifact-conversion.cjs'); const conversionExports = runtimeArtifactConversion as Record & { readGsdCommandNames?: () => string[]; }; +import { posixNormalize } from './shell-command-projection.cjs'; // In .cts (CommonJS output) files, `require` is available as a global. const _require: NodeRequire = require; @@ -279,7 +280,7 @@ function kimiAgentsKind(destSubpath: string, prefix: string, configDir: string): if (!entry.isFile() || !entry.name.endsWith('.md')) continue; const agentPath = path.join(stagedAgents, entry.name); subagents.push({ - path: path.join('agents', entry.name).replace(/\\/g, '/'), + path: posixNormalize(path.join('agents', entry.name)), content: fs.readFileSync(agentPath, 'utf8'), }); } diff --git a/src/runtime-hooks-surface.cts b/src/runtime-hooks-surface.cts index a6955df00..a5bee923c 100644 --- a/src/runtime-hooks-surface.cts +++ b/src/runtime-hooks-surface.cts @@ -310,7 +310,7 @@ function normalizeNodePath(execPath: string, opts?: NodeNormOpts): string { const env = (opts && opts.env) || process.env; const existsSync = (opts && opts.existsSync) || fs.existsSync; - const normalizedForMatch = execPath.replace(/\\/g, '/'); + const normalizedForMatch = shellCmdProjection.posixNormalize(execPath); if (/\/fnm_multishells\/[0-9]+_[0-9]+\/node(\.exe)?$/i.test(normalizedForMatch)) { const candidates: string[] = []; if (env.FNM_DIR) { @@ -363,7 +363,7 @@ function resolveNodeRunner(opts?: NodeNormOpts): string | null { const execPath = typeof process.execPath === 'string' ? process.execPath : ''; if (!execPath) return null; const stablePath = normalizeNodePath(execPath, opts); - return JSON.stringify(stablePath.replace(/\\/g, '/')); + return JSON.stringify(shellCmdProjection.posixNormalize(stablePath)); } interface BashRunnerOpts { @@ -389,7 +389,7 @@ function resolveBashRunner(opts?: BashRunnerOpts): string | null { for (const candidate of candidates) { if (candidate && exists(candidate)) { - return JSON.stringify(candidate.replace(/\\/g, '/')); + return JSON.stringify(shellCmdProjection.posixNormalize(candidate)); } } return null; @@ -449,7 +449,7 @@ function rewriteLegacyManagedNodeHookCommands(settings: Settings, absoluteRunner scriptPath = m[2] || m[3] || m[4] || ''; } else { _runnerToken = m[1]; - const runnerPath = (m[2] || m[3] || m[4] || '').replace(/\\/g, '/'); + const runnerPath = shellCmdProjection.posixNormalize(m[2] || m[3] || m[4] || ''); const stableRunner = normalizeNodePath(runnerPath); if (stableRunner === runnerPath && platform !== 'win32') continue; scriptToken = m[5]; @@ -694,10 +694,10 @@ function buildCodexHookWindowsShimIR(scriptAbsPath: string, absoluteRunnerToken: } catch { interpreter = absoluteRunnerToken; } - const targetAbs = scriptAbsPath.replace(/\\/g, '/'); + const targetAbs = shellCmdProjection.posixNormalize(scriptAbsPath); const scriptQuoted = JSON.stringify(targetAbs); const cmdPath = scriptAbsPath.replace(/\.js$/, '.cmd'); - const hookCommand = JSON.stringify(cmdPath.replace(/\\/g, '/')); + const hookCommand = JSON.stringify(shellCmdProjection.posixNormalize(cmdPath)); const runnerQuoted = JSON.stringify(interpreter); return { invocation: { interpreter, target: scriptAbsPath }, @@ -726,7 +726,7 @@ function ensureCodexHooksJsonSessionStart(targetDir: string, opts: EnsureCodexSe const hooksJsonPath = path.join(targetDir, 'hooks.json'); if (!absoluteRunner) return { changed: false, wrote: false, path: hooksJsonPath }; - const scriptPath = path.resolve(targetDir, 'hooks', 'gsd-check-update.js').replace(/\\/g, '/'); + const scriptPath = shellCmdProjection.posixNormalize(path.resolve(targetDir, 'hooks', 'gsd-check-update.js')); const cmdShimPath = scriptPath.replace(/\.js$/, '.cmd'); let managedCommand: string | undefined; @@ -757,7 +757,7 @@ function ensureCodexHooksJsonSessionStart(targetDir: string, opts: EnsureCodexSe if (!managedCommand) return { changed: false, wrote: false, path: hooksJsonPath }; const commandWindows = platform === 'win32' - ? JSON.stringify(cmdShimPath.replace(/\\/g, '/')) + ? JSON.stringify(shellCmdProjection.posixNormalize(cmdShimPath)) : undefined; return reconcileCodexHooksJsonSessionStart(targetDir, { managedCommand, commandWindows }); @@ -778,7 +778,7 @@ function ensureCodexHooksJsonEvent(targetDir: string, eventName: string, opts: E const hooksJsonPath = path.join(targetDir, 'hooks.json'); if (!absoluteRunner) return { changed: false, wrote: false, path: hooksJsonPath }; - const scriptPath = path.resolve(targetDir, 'hooks', 'gsd-context-monitor.js').replace(/\\/g, '/'); + const scriptPath = shellCmdProjection.posixNormalize(path.resolve(targetDir, 'hooks', 'gsd-context-monitor.js')); let managedCommand: string | undefined; if (platform === 'win32') { @@ -846,7 +846,7 @@ function buildHookCommand(configDir: string, hookName: string, opts?: BuildHookC }); return JSON.stringify(`${portableBaseDir}/hooks/${hookName}`); } - return JSON.stringify(configDir.replace(/\\/g, '/') + '/hooks/' + hookName); + return JSON.stringify(shellCmdProjection.posixNormalize(configDir) + '/hooks/' + hookName); } const nodeRunner = resolveNodeRunner(); @@ -866,7 +866,7 @@ function buildHookCommand(configDir: string, hookName: string, opts?: BuildHookC }); } - const hooksPath = configDir.replace(/\\/g, '/') + '/hooks/' + hookName; + const hooksPath = shellCmdProjection.posixNormalize(configDir) + '/hooks/' + hookName; return projectManagedHookCommand({ absoluteRunner: runner, scriptPath: hooksPath, @@ -1027,7 +1027,7 @@ function writeClineArtifacts(targetDir: string, isGlobalInstall: boolean): strin function buildCursorHookEntry(scriptPath: string): Record { return { type: 'command', - command: scriptPath.replace(/\\/g, '/'), + command: shellCmdProjection.posixNormalize(scriptPath), [GSD_CURSOR_HOOK_MARKER]: true, }; } diff --git a/src/schema-detect.cts b/src/schema-detect.cts index 12e770e77..ca590fa61 100644 --- a/src/schema-detect.cts +++ b/src/schema-detect.cts @@ -8,6 +8,8 @@ * This module does not read the filesystem directly. */ +import { posixNormalize } from './shell-command-projection.cjs'; + // ─── ORM Patterns ─────────────────────────────────────────────────────────── export interface SchemaPattern { @@ -82,7 +84,7 @@ export function detectSchemaFiles(files: string[]): DetectSchemaFilesResult { const matches: string[] = []; const orms = new Set(); for (const rawFile of files) { - const file = rawFile.replace(/\\/g, '/'); + const file = posixNormalize(rawFile); for (const { pattern, orm } of SCHEMA_PATTERNS) { if (pattern.test(file)) { matches.push(rawFile); diff --git a/src/shell-command-projection.cts b/src/shell-command-projection.cts index 14d5dbfc8..2f0d00cc6 100644 --- a/src/shell-command-projection.cts +++ b/src/shell-command-projection.cts @@ -18,6 +18,43 @@ import fs from 'node:fs'; // at load time and become un-mockable. import childProcess from 'node:child_process'; +/** + * Convert a filesystem path to POSIX form (forward slashes) by translating the + * platform-native separator. Single seam for native→POSIX conversion. + * + * Prefer this over `p.replace(/\\/g, '/')`: the regex form hardcodes both + * separators and corrupts POSIX paths containing a literal backslash (a legal + * filename character). Splitting on `path.sep` only ever touches real + * separators — a no-op on POSIX, `\`→`/` on Windows. + */ +export function toPosixPath(p: string): string { + return p.split(path.sep).join(path.posix.sep); +} + +/** + * Convert a filesystem path to the platform-native separator form. No-op on + * POSIX; `/`→`\` on Windows. Prefer this over a + * `process.platform === 'win32' ? p.replace(/\//g, '\\') : p` ternary. + */ +export function toNativePath(p: string): string { + return p.split(path.posix.sep).join(path.sep); +} + +/** + * Normalize ALL backslashes to forward slashes, unconditionally and independent + * of the running OS. Use this when emitting a path into a POSIX/bash target + * (which may differ from the running platform — e.g. generating a Windows config + * on a Linux runner) or when parsing input whose separators are unpredictable. + * + * Contrast `toPosixPath`, which is running-OS-relative (splits on `path.sep`) and + * is for *this machine's* filesystem paths. Do NOT use `toPosixPath` for + * target-platform projection — on a Linux runner it would not convert a + * Windows-target path's backslashes. + */ +export function posixNormalize(p: string): string { + return p.replace(/\\/g, '/'); +} + /** * Return true when a managed hook command must be prefixed with PowerShell's * call operator so a quoted executable token is invokable by the target @@ -95,7 +132,7 @@ export function buildLocalShellHookCommand({ localPrefix, hookFile, bashRunner, export function formatManagedHookScriptToken(scriptPath: string, opts: { platform?: string } = {}): string | null { const platform = opts.platform || process.platform; if (platform !== 'win32') return null; - return JSON.stringify(scriptPath.replace(/\\/g, '/')); + return JSON.stringify(posixNormalize(scriptPath)); } export function projectLocalHookPrefix({ runtime: _runtime = 'claude', dirName, hookPathStyle }: { runtime?: string; dirName?: string | null; hookPathStyle?: string | null }): string | undefined | null { @@ -114,8 +151,8 @@ export function projectLocalHookPrefix({ runtime: _runtime = 'claude', dirName, } export function projectPortableHookBaseDir({ configDir, homeDir }: { configDir?: string | null; homeDir?: string | null }): string { - const normalizedConfigDir = String(configDir || '').replace(/\\/g, '/'); - const normalizedHome = String(homeDir || '').replace(/\\/g, '/'); + const normalizedConfigDir = posixNormalize(String(configDir || '')); + const normalizedHome = posixNormalize(String(homeDir || '')); if (!normalizedConfigDir || !normalizedHome) return normalizedConfigDir; return normalizedConfigDir.startsWith(normalizedHome) ? '$HOME' + normalizedConfigDir.slice(normalizedHome.length) @@ -145,7 +182,7 @@ export function projectManagedHookCommand({ absoluteRunner, scriptPath, runtime platform?: string; }): string | null { if (!absoluteRunner || !scriptPath) return null; - const normalizedScriptPath = platform === 'win32' ? scriptPath.replace(/\\/g, '/') : scriptPath; + const normalizedScriptPath = platform === 'win32' ? posixNormalize(scriptPath) : scriptPath; return projectShellCommandText({ runnerToken: absoluteRunner, argTokens: [JSON.stringify(normalizedScriptPath)], @@ -245,15 +282,15 @@ export function isManagedHookCommand(commandText: unknown, opts: { surface?: str if (Array.isArray(opts.args) && opts.args.length > 0) { for (const arg of opts.args) { if (typeof arg !== 'string') continue; - const argBasename = arg.replace(/\\/g, '/').split('/').pop() || ''; + const argBasename = posixNormalize(arg).split('/').pop() || ''; if (isManagedHookBasename(argBasename, { surface })) return true; } } - const normalizedCommand = commandText.replace(/\\/g, '/'); + const normalizedCommand = posixNormalize(commandText); if (typeof opts.configDir === 'string' && opts.configDir.length > 0) { - const normalizedHooksDir = `${path.join(opts.configDir, 'hooks').replace(/\\/g, '/')}/`; + const normalizedHooksDir = `${posixNormalize(path.join(opts.configDir, 'hooks'))}/`; if (!normalizedCommand.includes(normalizedHooksDir)) return false; } @@ -295,7 +332,7 @@ export function projectLegacySettingsHookCommand({ platform?: string; }): string | null { if (!absoluteRunner || !scriptPath) return null; - const normalizedScriptPath = platform === 'win32' ? scriptPath.replace(/\\/g, '/') : scriptPath; + const normalizedScriptPath = platform === 'win32' ? posixNormalize(scriptPath) : scriptPath; // #1693: a script path already carrying a `"$CLAUDE_PROJECT_DIR"`-anchored // quoted prefix (local installs) is already a valid shell token — only the // variable is quoted, the rest is bare. JSON.stringify-ing it on Windows @@ -378,7 +415,7 @@ export function projectPathActionProjection({ let shellActions: ShellAction[]; if (isWin32) { const psTargetDir = escapePowerShellSingleQuoted(targetDir); - const bashTargetDir = escapeSingleQuotedShellLiteral(String(targetDir).replace(/\\/g, '/')); + const bashTargetDir = escapeSingleQuotedShellLiteral(posixNormalize(String(targetDir))); shellActions = [ { label: 'PowerShell', diff --git a/src/surface.cts b/src/surface.cts index 63f536b52..436c123ec 100644 --- a/src/surface.cts +++ b/src/surface.cts @@ -31,7 +31,7 @@ import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; -import { platformWriteSync } from './shell-command-projection.cjs'; +import { platformWriteSync, posixNormalize } from './shell-command-projection.cjs'; // eslint-disable-next-line @typescript-eslint/no-require-imports import installProfiles = require('./install-profiles.cjs'); const { @@ -354,8 +354,8 @@ function applySurface(runtimeConfigDir: string, layout: Layout, manifest: Map string = opts?.homedir ?? (() => os.homedir()); - const _resolvedTarget = path.resolve(layout.configDir).replace(/\\/g, '/'); - const _homeDir = _homedirFn().replace(/\\/g, '/'); + const _resolvedTarget = posixNormalize(path.resolve(layout.configDir)); + const _homeDir = posixNormalize(_homedirFn()); const _isGlobal = (layout.scope ?? 'global') === 'global'; const _isOpencode = layout.runtime === 'opencode'; const _isWindowsHost = (opts?.platform ?? process.platform) === 'win32'; diff --git a/src/verify.cts b/src/verify.cts index a72621bd9..d3337151c 100644 --- a/src/verify.cts +++ b/src/verify.cts @@ -22,7 +22,7 @@ import stateMod = require('./state.cjs'); import modelProfilesMod = require('./model-profiles.cjs'); // eslint-disable-next-line @typescript-eslint/no-require-imports -- plan-scan.cjs is an export= CommonJS module import planScanMod = require('./plan-scan.cjs'); -import { execGit, platformReadSync as safeReadFile, platformWriteSync } from './shell-command-projection.cjs'; +import { execGit, platformReadSync as safeReadFile, platformWriteSync, posixNormalize } from './shell-command-projection.cjs'; import { PACKAGE_NAME } from './package-identity.cjs'; import { formatGsdSlash, resolveRuntime } from './runtime-slash.cjs'; import { detectSchemaFiles, checkSchemaDrift } from './schema-detect.cjs'; @@ -1157,7 +1157,7 @@ function cmdValidateConsistency(cwd: string, raw: boolean): void { for (const dir of dirs) { const phasePath = path.join(phaseRoot, dir); - const phaseLabel = path.relative(planBase, phasePath).replace(/\\/g, '/'); + const phaseLabel = posixNormalize(path.relative(planBase, phasePath)); const phaseFiles = fs.readdirSync(phasePath); const plans = phaseFiles.filter((f) => f.endsWith('-PLAN.md')).sort(); diff --git a/src/worktree-safety.cts b/src/worktree-safety.cts index 5212d8ceb..47a37aafd 100644 --- a/src/worktree-safety.cts +++ b/src/worktree-safety.cts @@ -10,7 +10,7 @@ import fs from 'node:fs'; import path from 'node:path'; -import { execGit as execGitSeam } from './shell-command-projection.cjs'; +import { execGit as execGitSeam, posixNormalize } from './shell-command-projection.cjs'; // Default timeout for worktree-related git subprocess calls. // 10 s is generous enough for normal git operations on large repos while still @@ -588,7 +588,7 @@ function rescueSummaryArtifacts( // relPath is the path relative to the worktree root (e.g. ".planning/q1-SUMMARY.md") // Normalize to forward slashes so the Set comparison against `git status --porcelain` // output works on Windows too (git always emits forward slashes in porcelain output). - const relPath = absPath.slice(worktreePath.length).replace(/^[/\\]/, '').replace(/\\/g, '/'); + const relPath = posixNormalize(absPath.slice(worktreePath.length).replace(/^[/\\]/, '')); // #706: skip rescue when the SUMMARY is already committed on the branch. // Use `git cat-file -e HEAD:` (not `ls-files --error-unmatch`) so diff --git a/tests/shell-command-projection-path-sep.test.cjs b/tests/shell-command-projection-path-sep.test.cjs new file mode 100644 index 000000000..265fd7cf9 --- /dev/null +++ b/tests/shell-command-projection-path-sep.test.cjs @@ -0,0 +1,148 @@ +'use strict'; + +// Focused unit tests for the path-separator helpers on shell-command-projection +// (toPosixPath / toNativePath / posixNormalize). These are drop-in replacements +// for open-coded `.replace(/\\/g, '/')` and +// `process.platform === 'win32' ? x.replace(/\//g, '\\') : x` +// call sites — see shell-command-projection.cts for the platform-relative contract. + +const { describe, test } = require('node:test'); +const assert = require('node:assert/strict'); +const path = require('node:path'); +const fc = require('fast-check'); + +const { toPosixPath, toNativePath, posixNormalize } = require('../gsd-core/bin/lib/shell-command-projection.cjs'); + +describe('toPosixPath', () => { + test('plain relative path: already-POSIX segments pass through unchanged', () => { + assert.equal(toPosixPath('a/b/c'), 'a/b/c'); + }); + + test('plain relative path built with the native separator normalizes to POSIX segments', () => { + const native = path.join('alpha', 'beta', 'gamma'); + assert.equal(toPosixPath(native), ['alpha', 'beta', 'gamma'].join('/')); + }); + + test('absolute path: segment list matches the native path split on path.sep', () => { + const abs = path.resolve('alpha', 'beta'); + assert.equal(toPosixPath(abs), abs.split(path.sep).join('/')); + }); + + test('idempotency: applying twice equals applying once', () => { + const native = path.join('alpha', 'beta', 'gamma'); + const once = toPosixPath(native); + const twice = toPosixPath(once); + assert.equal(once, twice); + }); + + test('empty string in, empty string out', () => { + assert.equal(toPosixPath(''), ''); + }); + + test('does not corrupt a POSIX-style input containing a literal backslash-free path', () => { + // A string that already uses only '/' as its separator and contains no + // occurrence of path.sep-as-backslash must survive unchanged regardless of + // the host platform's path.sep (no-op on POSIX; nothing to split on Windows + // either, since there is no backslash present). + const alreadyPosix = 'already/posix/style/path'; + assert.equal(toPosixPath(alreadyPosix), alreadyPosix); + }); + + test('property: toPosixPath output never contains path.sep when path.sep differs from "/"', () => { + fc.assert( + fc.property(fc.array(fc.stringMatching(/^[a-zA-Z0-9_-]+$/), { minLength: 1, maxLength: 5 }), (segments) => { + const native = segments.join(path.sep); + const posix = toPosixPath(native); + if (path.sep !== '/') { + assert.ok(!posix.includes(path.sep)); + } + assert.equal(posix, segments.join('/')); + }), + ); + }); +}); + +describe('toNativePath', () => { + test('plain relative path: already-native segments pass through unchanged', () => { + const native = path.join('alpha', 'beta', 'gamma'); + assert.equal(toNativePath(native), native); + }); + + test('POSIX-style relative path converts to native segments', () => { + const posix = 'alpha/beta/gamma'; + assert.equal(toNativePath(posix), ['alpha', 'beta', 'gamma'].join(path.sep)); + }); + + test('absolute path: round-trips back to the original POSIX form via split/join', () => { + const posixAbs = path.posix.resolve('/', 'alpha', 'beta'); + const native = toNativePath(posixAbs); + assert.equal(native.split(path.sep).join('/'), posixAbs); + }); + + test('idempotency: applying twice equals applying once', () => { + const posix = 'alpha/beta/gamma'; + const once = toNativePath(posix); + const twice = toNativePath(once); + assert.equal(once, twice); + }); + + test('empty string in, empty string out', () => { + assert.equal(toNativePath(''), ''); + }); + + test('does not corrupt an already-native-style input containing no POSIX separator', () => { + // A string that already uses only path.sep as its separator (and contains + // no '/' occurrence) must survive unchanged: no-op on POSIX (path.sep is + // '/', so it IS the separator being normalized to); on Windows there is no + // '/' present to split on, so the single-element join reproduces the input. + const alreadyNative = ['already', 'native', 'style', 'path'].join(path.sep); + assert.equal(toNativePath(alreadyNative), alreadyNative); + }); + + test('property: toPosixPath and toNativePath round-trip a POSIX-style path through both conversions', () => { + fc.assert( + fc.property(fc.array(fc.stringMatching(/^[a-zA-Z0-9_-]+$/), { minLength: 1, maxLength: 5 }), (segments) => { + const posix = segments.join('/'); + assert.equal(toPosixPath(toNativePath(posix)), posix); + }), + ); + }); +}); + +describe('posixNormalize', () => { + test('unconditional conversion: literal backslashes always become forward slashes, regardless of host platform', () => { + // Unlike toPosixPath (which splits on path.sep — a no-op on POSIX hosts), + // posixNormalize must convert backslashes even when running on a POSIX + // host, because the input represents a TARGET platform's path, not this + // machine's filesystem path. + assert.equal(posixNormalize('alpha\\beta\\gamma'), 'alpha/beta/gamma'); + }); + + test('mixed separators: only backslashes are converted, forward slashes are left alone', () => { + assert.equal(posixNormalize('alpha\\beta/gamma\\delta'), 'alpha/beta/gamma/delta'); + }); + + test('already-POSIX input with no backslash passes through unchanged', () => { + const alreadyPosix = 'already/posix/style/path'; + assert.equal(posixNormalize(alreadyPosix), alreadyPosix); + }); + + test('idempotency: applying twice equals applying once', () => { + const once = posixNormalize('alpha\\beta\\gamma'); + const twice = posixNormalize(once); + assert.equal(once, twice); + }); + + test('empty string in, empty string out', () => { + assert.equal(posixNormalize(''), ''); + }); + + test('property: posixNormalize output never contains a backslash', () => { + fc.assert( + fc.property(fc.string(), (input) => { + const normalized = posixNormalize(input); + assert.ok(!normalized.includes('\\')); + }), + ); + }); +}); From 98e4233ce9f20f827d01ac4e7e53a2f4c5ec3fff Mon Sep 17 00:00:00 2001 From: Cody Anderson <70287898+arakasi1@users.noreply.github.com> Date: Mon, 13 Jul 2026 13:54:19 -0600 Subject: [PATCH 8/8] fix(#2176): ground the Antigravity reviewer in the repo under review (#2184) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(#2176): ground the Antigravity reviewer in the repo under review - capability-probe --add-dir (mirrors the Codex bypass-flag probe) and pass the repo root on both invocation arms - anchor _AGY_PROMPT to the absolute repo root; mandate a REVIEWED-WITHOUT-REPO-ACCESS self-report when the repo is unreadable - stamp a [reviewed-without-repo-access] marker on self-reported or scratch-anchored output; Consensus Summary down-weights marked reviews - apply the same absolute-root anchor to the cursor-agent prompt (AC5) Claude-Session: https://claude.ai/code/session_01Hme55Pvq6BhpgwBcyC5HAg * docs(#2176): changeset fragment for PR #2184 Claude-Session: https://claude.ai/code/session_01Hme55Pvq6BhpgwBcyC5HAg * fix(#2176): review fixes — size baseline, cursor root anchor, anchored blind tells - regenerate tests/workflow-size-baseline.json for review.md's growth - cursor anchor uses git rev-parse --show-toplevel (bare pwd resolved the wrong root from a repo subdirectory) - blind-review tells anchored: self-report to the first lines of output, scratch tell to a workspace-declaration phrasing — a grounded review quoting either string is no longer mis-stamped Claude-Session: https://claude.ai/code/session_01Hme55Pvq6BhpgwBcyC5HAg * fix(#2176): round-2 review fixes — scratch-tell bridge, behavioral test, changeset Claude-Session: https://claude.ai/code/session_01Hme55Pvq6BhpgwBcyC5HAg * test: regenerate golden-install-parity fixtures for the review.md change Claude-Session: https://claude.ai/code/session_01Hme55Pvq6BhpgwBcyC5HAg * test(#2176): pass the transcript path to bash with forward slashes The behavioral detection test substitutes a mkdtemp path into the bash compound; on Windows runners that path contains backslashes, which bash strips, so the transcript is never found and the first assertion fails (windows-latest/24 lane). Git Bash accepts D:/-style paths. Claude-Session: https://claude.ai/code/session_01Hme55Pvq6BhpgwBcyC5HAg * fix(#2176): use /gsd:review namespace syntax in workflow comment The slash-command namespace invariant (#3443) bans retired /gsd- references in Claude-facing sources; a cursor-anchor comment used /gsd-review. Size baseline + golden fixtures regenerated for the byte change. Claude-Session: https://claude.ai/code/session_01Hme55Pvq6BhpgwBcyC5HAg * test(#2176): derive the POSIX path via path.sep, not a hardcoded separator Review finding: out.replaceAll('\\', '/') hardcodes both separators; use the separator-safe out.split(path.sep).join(path.posix.sep) idiom. Claude-Session: https://claude.ai/code/session_01Hme55Pvq6BhpgwBcyC5HAg * test(#2176): use the merged toPosixPath seam for the bash path Per maintainer note: #2247's shell-command-projection now centralizes running-OS → POSIX path conversion; import it instead of the inline split/join idiom. Claude-Session: https://claude.ai/code/session_01Hme55Pvq6BhpgwBcyC5HAg --- .changeset/fierce-pumas-gather.md | 5 + gsd-core/workflows/review.md | 43 +++- tests/antigravity-repo-grounding.test.cjs | 192 ++++++++++++++++++ .../golden-install-parity/antigravity.json | 2 +- .../golden-install-parity/augment.json | 2 +- .../golden-install-parity/claude-local.json | 2 +- .../golden-install-parity/claude.json | 2 +- .../fixtures/golden-install-parity/cline.json | 2 +- .../golden-install-parity/codebuddy.json | 2 +- .../fixtures/golden-install-parity/codex.json | 2 +- .../golden-install-parity/copilot.json | 2 +- .../golden-install-parity/cursor.json | 2 +- .../golden-install-parity/hermes.json | 2 +- .../fixtures/golden-install-parity/kilo.json | 2 +- .../fixtures/golden-install-parity/kimi.json | 2 +- .../golden-install-parity/opencode.json | 2 +- tests/fixtures/golden-install-parity/pi.json | 2 +- .../fixtures/golden-install-parity/qwen.json | 2 +- .../fixtures/golden-install-parity/trae.json | 2 +- .../golden-install-parity/windsurf.json | 2 +- .../fixtures/golden-install-parity/zcode.json | 2 +- tests/workflow-size-baseline.json | 2 +- 22 files changed, 256 insertions(+), 22 deletions(-) create mode 100644 .changeset/fierce-pumas-gather.md create mode 100644 tests/antigravity-repo-grounding.test.cjs diff --git a/.changeset/fierce-pumas-gather.md b/.changeset/fierce-pumas-gather.md new file mode 100644 index 000000000..352163e1e --- /dev/null +++ b/.changeset/fierce-pumas-gather.md @@ -0,0 +1,5 @@ +--- +type: Fixed +pr: 2184 +--- +**The Antigravity reviewer in `/gsd-review` no longer reviews blind** — `agy -p` never granted the agent the repo under review, so it frequently anchored on its own scratch directory and returned plan-text-only verdicts counted at full consensus weight. The reviewer is now granted the repo (capability-probed `--add-dir`) and anchored to the absolute repo root; a review that still runs without repo access is stamped `[reviewed-without-repo-access]` and down-weighted in the Consensus Summary. The cursor-agent prompt gains the same absolute-root anchor. (#2176) diff --git a/gsd-core/workflows/review.md b/gsd-core/workflows/review.md index a34ddefeb..fe6db0b82 100644 --- a/gsd-core/workflows/review.md +++ b/gsd-core/workflows/review.md @@ -366,7 +366,12 @@ fi # prompt as an ARGUMENT, not stdin. A full review prompt can exceed the OS argument limit, so # reference the prompt file by path rather than inlining it. Capture stderr so a failure is # diagnosable instead of a silent empty result. -CURSOR_PROMPT_ARG="Read the file at /tmp/gsd-review-prompt-{phase}.md in full and carry out the review request it contains. Output only the resulting markdown review. Do not edit any files." +# #2176: same absolute-root anchor as the Antigravity block — cursor-agent runs +# in the repo cwd, but repo-relative references in the assembled prompt still +# need an explicit root to resolve against. rev-parse (not bare pwd) so the +# anchor is correct even when /gsd:review is invoked from a repo subdirectory. +_CURSOR_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)" +CURSOR_PROMPT_ARG="Read the file at /tmp/gsd-review-prompt-{phase}.md in full and carry out the review request it contains. The repository under review is at $_CURSOR_ROOT — resolve every relative file path in the review request against that absolute root. Output only the resulting markdown review. Do not edit any files." cursor-agent -p --mode ask --trust --output-format text "$CURSOR_PROMPT_ARG" 2>/tmp/gsd-review-cursor-{phase}.err > /tmp/gsd-review-cursor-{phase}.md if [ ! -s /tmp/gsd-review-cursor-{phase}.md ]; then echo "Cursor review failed or returned empty output. stderr:" > /tmp/gsd-review-cursor-{phase}.md @@ -458,7 +463,19 @@ if [ -n "$AGY_MODEL" ] && [ "$AGY_MODEL" != "null" ]; then else set -- fi -_AGY_PROMPT="Read the file at /tmp/gsd-review-prompt-{phase}.md in full and carry out the review request it contains. Output only the resulting markdown review. Do not edit any files." +# #2176: grant the reviewer the repo under review. Without --add-dir, agy's +# permission context never receives the cwd repo — the agent anchors on its own +# ~/.gemini/antigravity-cli/scratch dir and reviews the plan text in isolation +# (the exact failure the Review Instructions forbid). Capability-probed like the +# Codex bypass flag so an older agy without --add-dir still runs; the prompt +# anchor below keeps absolute-path reads possible on that fallback. +if agy --help 2>/dev/null | grep -q -- '--add-dir'; then + set -- "$@" --add-dir "$_AGY_WS" +fi +# #2176: anchor the prompt to the absolute repo root so repo-relative references +# in the assembled review prompt resolve even on the no---add-dir fallback, and +# require an explicit self-report if the reviewer still cannot read the repo. +_AGY_PROMPT="Read the file at /tmp/gsd-review-prompt-{phase}.md in full and carry out the review request it contains. The repository under review is at $_AGY_WS — resolve every relative file path in the review request against that absolute root and verify claims against those files. If you cannot read files under $_AGY_WS, begin your output with the exact line REVIEWED-WITHOUT-REPO-ACCESS before the review. Output only the resulting markdown review. Do not edit any files." # Capability-probe an external wall-clock killer (GNU coreutils `timeout` or the # macOS Homebrew `gtimeout`). Stock macOS ships NEITHER — a bare `timeout …` would # fail with rc 127 ("command not found") and silently lose the reviewer, so fall @@ -525,6 +542,26 @@ if [ ! -s /tmp/gsd-review-antigravity-{phase}.md ]; then echo "If no agy run started, that is the pre-session-stall case: check whether a new ~/.gemini/antigravity-cli/brain// dir appeared within ~30s of launch." } > /tmp/gsd-review-antigravity-{phase}.md fi + +# #2176: blind-review marker. Two tells that the reviewer ran without repo +# access: the prompt's mandated REVIEWED-WITHOUT-REPO-ACCESS self-report in the +# first lines of output, or the agent DECLARING the scratch dir as its +# workspace. Both patterns are anchored — the self-report to the head of the +# file, the scratch tell to a workspace-declaration phrasing — so a grounded +# review that merely QUOTES these strings (e.g. reviewing this very file) is +# never mis-stamped. Stamp a machine-readable marker so the Consensus Summary +# down-weights the review instead of counting an ungrounded verdict at full +# weight. (Temp file + mv, no in-place sed — BSD/GNU safe.) +if [ -s /tmp/gsd-review-antigravity-{phase}.md ] && \ + { head -5 /tmp/gsd-review-antigravity-{phase}.md | grep -q 'REVIEWED-WITHOUT-REPO-ACCESS' || \ + grep -qiE '(workspace|working) (directory|dir).{0,40}antigravity-cli/scratch' /tmp/gsd-review-antigravity-{phase}.md; }; then + { + echo "> [reviewed-without-repo-access] This reviewer ran without visibility into the repo under review — down-weight its verdict in the Consensus Summary." + echo "" + cat /tmp/gsd-review-antigravity-{phase}.md + } > /tmp/gsd-review-antigravity-{phase}.md.tmp && \ + mv /tmp/gsd-review-antigravity-{phase}.md.tmp /tmp/gsd-review-antigravity-{phase}.md +fi ``` **Ollama (local, OpenAI-compatible):** @@ -837,7 +874,7 @@ trimmed_reviewers: # only present if at least one reviewer was trimmed ## Consensus Summary -{synthesize common concerns across all reviewers. CodeRabbit is a diff-only reviewer (it never received the source-grounding prompt), so do not weight its verdict as a grounded plan review — fold in its diff findings, but base plan-level consensus on the prompt-fed reviewers.} +{synthesize common concerns across all reviewers. CodeRabbit is a diff-only reviewer (it never received the source-grounding prompt), so do not weight its verdict as a grounded plan review — fold in its diff findings, but base plan-level consensus on the prompt-fed reviewers. A reviewer output carrying the `[reviewed-without-repo-access]` marker (or beginning with `REVIEWED-WITHOUT-REPO-ACCESS`) ran without repo access (#2176) — treat it the same way: note its concerns, but do not count its verdict at full consensus weight.} ### Agreed Strengths {strengths mentioned by 2+ reviewers} diff --git a/tests/antigravity-repo-grounding.test.cjs b/tests/antigravity-repo-grounding.test.cjs new file mode 100644 index 000000000..dac385443 --- /dev/null +++ b/tests/antigravity-repo-grounding.test.cjs @@ -0,0 +1,192 @@ +// allow-test-rule: source-text-is-the-product (see #2073) +// gsd-core/workflows/review.md is a workflow document whose bash blocks ARE +// what /gsd-review loads and executes at runtime. Asserting the invocation +// shape asserts the deployed contract — this is behavioral coverage of the +// workflow, not a source-grep over application code. + +/** + * Antigravity reviewer repo-grounding tests (#2176) + * + * The agy block invoked the CLI without granting it the repo under review: + * no --add-dir on either invocation arm, and no absolute repo-root anchor in + * _AGY_PROMPT. The agent frequently anchored on its own + * ~/.gemini/antigravity-cli/scratch dir, reviewed the plan text in isolation + * (the exact failure the block's Review Instructions forbid), and its + * ungrounded verdict flowed into the Consensus Summary at full weight, + * undetected. + * + * These tests pin the fix: capability-probed --add-dir (mirrors the Codex + * bypass-flag probe), absolute-root prompt anchor (agy AND the cursor-agent + * block, which shared the anchor gap), a mandated self-report line, a stamped + * blind-review marker, and consensus down-weighting of marked reviews. + * Each assertion fails against the pre-fix block. + */ + +const { test, describe } = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('fs'); +const path = require('path'); + +const ROOT = path.join(__dirname, '..'); +const REVIEW_PATH = path.join(ROOT, 'gsd-core', 'workflows', 'review.md'); + +function reviewContent() { + return fs.readFileSync(REVIEW_PATH, 'utf-8'); +} + +function agyBashBlock() { + const fences = reviewContent().match(/```bash[\s\S]*?```/g) || []; + const agy = fences.find((f) => /\bagy\b/.test(f) && /gsd-review-antigravity/.test(f)); + assert.ok(agy, 'review.md should contain the agy invocation bash block'); + return agy; +} + +function cursorBashBlock() { + const fences = reviewContent().match(/```bash[\s\S]*?```/g) || []; + const cursor = fences.find((f) => /cursor-agent -p/.test(f)); + assert.ok(cursor, 'review.md should contain the cursor-agent invocation bash block'); + return cursor; +} + +describe('Antigravity reviewer repo grounding in /gsd-review (#2176)', () => { + test('probes agy for --add-dir support (capability-probe idiom, mirrors the Codex block)', () => { + const block = agyBashBlock(); + assert.ok( + /agy --help 2>\/dev\/null \| grep -q -- '--add-dir'/.test(block), + 'agy block must capability-probe --add-dir via `agy --help | grep -q` so older CLIs still run', + ); + }); + + test('passes --add-dir with the repo root when supported', () => { + const block = agyBashBlock(); + assert.ok( + /set -- "\$@" --add-dir "\$_AGY_WS"/.test(block), + 'the probed arm must append --add-dir "$_AGY_WS" so both invocation arms (which expand "$@") receive it', + ); + }); + + test('_AGY_PROMPT is anchored to the absolute repo root', () => { + const block = agyBashBlock(); + const promptLine = block.split('\n').find((l) => l.startsWith('_AGY_PROMPT=')); + assert.ok(promptLine, 'agy block must define _AGY_PROMPT'); + assert.ok( + /\$_AGY_WS/.test(promptLine), + '_AGY_PROMPT must embed the absolute repo root ($_AGY_WS) so repo-relative references resolve on the no---add-dir fallback', + ); + }); + + test('_AGY_PROMPT mandates a REVIEWED-WITHOUT-REPO-ACCESS self-report', () => { + const block = agyBashBlock(); + const promptLine = block.split('\n').find((l) => l.startsWith('_AGY_PROMPT=')); + assert.ok( + /REVIEWED-WITHOUT-REPO-ACCESS/.test(promptLine), + '_AGY_PROMPT must require the exact self-report line when the reviewer cannot read the repo', + ); + }); + + test('stamps a blind-review marker on self-reported or scratch-anchored output', () => { + const block = agyBashBlock(); + assert.ok( + /head -5 [^|]*\| grep -q 'REVIEWED-WITHOUT-REPO-ACCESS'/.test(block), + 'the self-report tell must be anchored to the head of the output, not a whole-body substring', + ); + assert.ok( + /grep -qiE '\(workspace\|working\) \(directory\|dir\)\.\{0,40\}antigravity-cli\/scratch'/.test(block), + 'the scratch tell must be anchored to a workspace-declaration phrasing whose bridge (.{0,40}) can span the dotted ~/.gemini/ path prefix', + ); + assert.ok( + /\[reviewed-without-repo-access\]/.test(block), + 'detected blind reviews must be stamped with the [reviewed-without-repo-access] marker', + ); + }); + + test('marker stamping avoids sed -i (BSD/GNU divergence) — uses temp file + mv', () => { + const block = agyBashBlock(); + assert.ok(!/sed -i/.test(block), 'agy block must not use sed -i (BSD vs GNU incompatibility)'); + assert.ok( + /\.tmp && \\?\s*\n?\s*mv /.test(block), + 'marker stamping should rewrite via temp file + mv', + ); + }); + + test('Consensus Summary down-weights marked blind reviews', () => { + const content = reviewContent(); + const consensusIdx = content.indexOf('## Consensus Summary'); + assert.ok(consensusIdx >= 0, 'review.md should contain the Consensus Summary section'); + const consensus = content.slice(consensusIdx, consensusIdx + 2000); + assert.ok( + /\[reviewed-without-repo-access\]/.test(consensus), + 'consensus instructions must reference the blind-review marker', + ); + assert.ok( + /REVIEWED-WITHOUT-REPO-ACCESS/.test(consensus), + 'consensus instructions must also honor the raw self-report line', + ); + assert.ok( + /not count its verdict at full consensus weight/.test(consensus), + 'marked reviews must be down-weighted, not counted at full weight', + ); + }); + + test('blind-review detection behaves correctly on synthetic transcripts', () => { + // Behavioral, not string-on-string: extract the actual detection compound + // from the fence, point it at a temp file, and run it through bash for + // ungrounded and grounded transcript shapes. + const os = require('os'); + const { execFileSync } = require('node:child_process'); + const { toPosixPath } = require('../gsd-core/bin/lib/shell-command-projection.cjs'); + const block = agyBashBlock(); + const m = block.match(/\{ head -5[\s\S]*?\}; then/); + assert.ok(m, 'detection compound not found in the agy block'); + const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'agy-detect-')); + const out = path.join(tmp, 'review-out.md'); + const detect = m[0] + .replace(/\}; then$/, '}') + // Convert the native path to POSIX form so it survives bash on Windows + // runners (Git Bash accepts D:/... but eats backslashes). + .replaceAll('/tmp/gsd-review-antigravity-{phase}.md', toPosixPath(out)); + const runDetect = (content) => { + fs.writeFileSync(out, content); + try { + execFileSync('bash', ['-c', detect], { stdio: 'ignore' }); + return true; // exit 0 → blind review detected + } catch { + return false; + } + }; + try { + // Ungrounded tells — must be stamped + assert.equal(runDetect('REVIEWED-WITHOUT-REPO-ACCESS\n\n## Review\nPlan-only review.\n'), true, + 'self-report line in the head must be detected'); + assert.equal(runDetect('## Review\nMy working directory is ~/.gemini/antigravity-cli/scratch.\nPlan-only review.\n'), true, + 'full dotted scratch path in a workspace declaration must be detected (#2184 re-review Major)'); + assert.equal(runDetect('Workspace directory: /home/me/.gemini/antigravity-cli/scratch\n'), true, + 'colon-style workspace declaration must be detected'); + // Grounded shapes — must NOT be stamped + assert.equal(runDetect('## Review\nVerified hooks/gsd-statusline.js against the plan. Solid.\n'), false, + 'ordinary grounded review must not be stamped'); + assert.equal(runDetect('## Review\nThe workflow mentions antigravity-cli/scratch as the agy scratch dir; the guard there is correct.\n'), false, + 'grounded review merely quoting the scratch path must not be stamped'); + assert.equal( + runDetect('## Review\n\nGrounded findings below.\n\n### Details\nLine 20 of the workflow mentions REVIEWED-WITHOUT-REPO-ACCESS as the self-report marker; fine.\n'), + false, + 'self-report string quoted beyond the first 5 lines must not be stamped'); + } finally { + require('./helpers.cjs').cleanup(tmp); + } + }); + + test('cursor-agent prompt carries the same absolute-root anchor (identical gap, #2176 AC5)', () => { + const block = cursorBashBlock(); + assert.ok( + /_CURSOR_ROOT="\$\(git rev-parse --show-toplevel 2>\/dev\/null \|\| pwd\)"/.test(block), + 'cursor anchor must resolve the repo TOP-LEVEL (rev-parse, not bare pwd) so subdirectory invocations anchor correctly', + ); + const promptLine = block.split('\n').find((l) => l.startsWith('CURSOR_PROMPT_ARG=')); + assert.ok(promptLine, 'cursor block must define CURSOR_PROMPT_ARG'); + assert.ok( + /repository under review is at \$_CURSOR_ROOT/.test(promptLine), + 'CURSOR_PROMPT_ARG must anchor repo-relative references to the absolute repo root', + ); + }); +}); diff --git a/tests/fixtures/golden-install-parity/antigravity.json b/tests/fixtures/golden-install-parity/antigravity.json index 5b607fd97..9f99d3402 100644 --- a/tests/fixtures/golden-install-parity/antigravity.json +++ b/tests/fixtures/golden-install-parity/antigravity.json @@ -285,7 +285,7 @@ "gsd-core/workflows/remove-phase.md": "23b9eb0858a2535e", "gsd-core/workflows/remove-workspace.md": "d0bd7e0601138798", "gsd-core/workflows/resume-project.md": "98e2cf8908e73a52", - "gsd-core/workflows/review.md": "43c052bba1cbd4ac", + "gsd-core/workflows/review.md": "c4622380e61615c8", "gsd-core/workflows/scan.md": "a7fecd67e5cd655f", "gsd-core/workflows/secure-phase.md": "52ddc46233e8fa66", "gsd-core/workflows/session-report.md": "2e5b1205324ddefa", diff --git a/tests/fixtures/golden-install-parity/augment.json b/tests/fixtures/golden-install-parity/augment.json index 4db1c9c1f..2931d7735 100644 --- a/tests/fixtures/golden-install-parity/augment.json +++ b/tests/fixtures/golden-install-parity/augment.json @@ -356,7 +356,7 @@ "gsd-core/workflows/remove-phase.md": "df9a45f0b1880999", "gsd-core/workflows/remove-workspace.md": "a7ca66db6b7c132c", "gsd-core/workflows/resume-project.md": "f28da1200e4545f4", - "gsd-core/workflows/review.md": "ad7c0f372ed986ae", + "gsd-core/workflows/review.md": "3ce57633978035ad", "gsd-core/workflows/scan.md": "003883d71c37da7d", "gsd-core/workflows/secure-phase.md": "22ab3bb5da494ea7", "gsd-core/workflows/session-report.md": "2e5b1205324ddefa", diff --git a/tests/fixtures/golden-install-parity/claude-local.json b/tests/fixtures/golden-install-parity/claude-local.json index 6cc6844b6..84d3b28af 100644 --- a/tests/fixtures/golden-install-parity/claude-local.json +++ b/tests/fixtures/golden-install-parity/claude-local.json @@ -355,7 +355,7 @@ "gsd-core/workflows/remove-phase.md": "8effc8742d58a11a", "gsd-core/workflows/remove-workspace.md": "10882656198d9075", "gsd-core/workflows/resume-project.md": "af9761bcec0f6fe9", - "gsd-core/workflows/review.md": "eec3a15bebb7fcf0", + "gsd-core/workflows/review.md": "15b8defb2d791092", "gsd-core/workflows/scan.md": "75c670d08cee8680", "gsd-core/workflows/secure-phase.md": "8030d2b2a5bfdf07", "gsd-core/workflows/session-report.md": "2e5b1205324ddefa", diff --git a/tests/fixtures/golden-install-parity/claude.json b/tests/fixtures/golden-install-parity/claude.json index d79453924..15d5be2dd 100644 --- a/tests/fixtures/golden-install-parity/claude.json +++ b/tests/fixtures/golden-install-parity/claude.json @@ -284,7 +284,7 @@ "gsd-core/workflows/remove-phase.md": "ada8a0546c686483", "gsd-core/workflows/remove-workspace.md": "f3ab3a88a7e9e1ed", "gsd-core/workflows/resume-project.md": "7f8dc986f0f35d96", - "gsd-core/workflows/review.md": "b0baf1dafebe3821", + "gsd-core/workflows/review.md": "f1dbb3769d461ec2", "gsd-core/workflows/scan.md": "47371c2073d6c0be", "gsd-core/workflows/secure-phase.md": "d6ac1f4db6a5da75", "gsd-core/workflows/session-report.md": "2e5b1205324ddefa", diff --git a/tests/fixtures/golden-install-parity/cline.json b/tests/fixtures/golden-install-parity/cline.json index 960882cc5..2ce000795 100644 --- a/tests/fixtures/golden-install-parity/cline.json +++ b/tests/fixtures/golden-install-parity/cline.json @@ -288,7 +288,7 @@ "gsd-core/workflows/remove-phase.md": "e336350f8113a328", "gsd-core/workflows/remove-workspace.md": "e685dfbd736dfd90", "gsd-core/workflows/resume-project.md": "e23981178fa37b3d", - "gsd-core/workflows/review.md": "6c689f4ff8146d28", + "gsd-core/workflows/review.md": "8c996dc10e923685", "gsd-core/workflows/scan.md": "dfd92717caea0ce7", "gsd-core/workflows/secure-phase.md": "00de56d6d993bb2c", "gsd-core/workflows/session-report.md": "2e5b1205324ddefa", diff --git a/tests/fixtures/golden-install-parity/codebuddy.json b/tests/fixtures/golden-install-parity/codebuddy.json index 4c2dfb970..0611c54a4 100644 --- a/tests/fixtures/golden-install-parity/codebuddy.json +++ b/tests/fixtures/golden-install-parity/codebuddy.json @@ -356,7 +356,7 @@ "gsd-core/workflows/remove-phase.md": "df9a45f0b1880999", "gsd-core/workflows/remove-workspace.md": "a7ca66db6b7c132c", "gsd-core/workflows/resume-project.md": "f28da1200e4545f4", - "gsd-core/workflows/review.md": "ad7c0f372ed986ae", + "gsd-core/workflows/review.md": "3ce57633978035ad", "gsd-core/workflows/scan.md": "003883d71c37da7d", "gsd-core/workflows/secure-phase.md": "22ab3bb5da494ea7", "gsd-core/workflows/session-report.md": "2e5b1205324ddefa", diff --git a/tests/fixtures/golden-install-parity/codex.json b/tests/fixtures/golden-install-parity/codex.json index cc6bbab50..6f99e05ae 100644 --- a/tests/fixtures/golden-install-parity/codex.json +++ b/tests/fixtures/golden-install-parity/codex.json @@ -391,7 +391,7 @@ "gsd-core/workflows/remove-phase.md": "9ee0fddd11a0d9d4", "gsd-core/workflows/remove-workspace.md": "19d7465aaa50cb62", "gsd-core/workflows/resume-project.md": "9965f87eb278f7f8", - "gsd-core/workflows/review.md": "5faef3f4feb45c99", + "gsd-core/workflows/review.md": "a980007626185b70", "gsd-core/workflows/scan.md": "1a3caa5d724d39e9", "gsd-core/workflows/secure-phase.md": "ab387a4bca381c18", "gsd-core/workflows/session-report.md": "dd8fa011c9394075", diff --git a/tests/fixtures/golden-install-parity/copilot.json b/tests/fixtures/golden-install-parity/copilot.json index a39d2d9ae..d488feccb 100644 --- a/tests/fixtures/golden-install-parity/copilot.json +++ b/tests/fixtures/golden-install-parity/copilot.json @@ -286,7 +286,7 @@ "gsd-core/workflows/remove-phase.md": "e262654e319d1bc4", "gsd-core/workflows/remove-workspace.md": "ceddfeef5f2d6754", "gsd-core/workflows/resume-project.md": "40db7f350f5866d8", - "gsd-core/workflows/review.md": "4b649f31865a1785", + "gsd-core/workflows/review.md": "1f44a33c50b1cd36", "gsd-core/workflows/scan.md": "dcc2f76d0850e2fb", "gsd-core/workflows/secure-phase.md": "9bec6635ee1cbaed", "gsd-core/workflows/session-report.md": "2e5b1205324ddefa", diff --git a/tests/fixtures/golden-install-parity/cursor.json b/tests/fixtures/golden-install-parity/cursor.json index a6afd63ea..b6af16b0a 100644 --- a/tests/fixtures/golden-install-parity/cursor.json +++ b/tests/fixtures/golden-install-parity/cursor.json @@ -356,7 +356,7 @@ "gsd-core/workflows/remove-phase.md": "ada8a0546c686483", "gsd-core/workflows/remove-workspace.md": "433affcd1a200826", "gsd-core/workflows/resume-project.md": "7f8dc986f0f35d96", - "gsd-core/workflows/review.md": "3ba8bb85c8ede5b8", + "gsd-core/workflows/review.md": "ad115fadce422eae", "gsd-core/workflows/scan.md": "47371c2073d6c0be", "gsd-core/workflows/secure-phase.md": "3063b0b6f7b56d46", "gsd-core/workflows/session-report.md": "2e5b1205324ddefa", diff --git a/tests/fixtures/golden-install-parity/hermes.json b/tests/fixtures/golden-install-parity/hermes.json index a3b083743..1a52e39cf 100644 --- a/tests/fixtures/golden-install-parity/hermes.json +++ b/tests/fixtures/golden-install-parity/hermes.json @@ -285,7 +285,7 @@ "gsd-core/workflows/remove-phase.md": "fce799aae3ab2715", "gsd-core/workflows/remove-workspace.md": "8facde381657dd71", "gsd-core/workflows/resume-project.md": "a0443839f1f83c2d", - "gsd-core/workflows/review.md": "761dd1ae5be40613", + "gsd-core/workflows/review.md": "d8c53e495b067afe", "gsd-core/workflows/scan.md": "b28f65d88c522767", "gsd-core/workflows/secure-phase.md": "a503dc469fd7a252", "gsd-core/workflows/session-report.md": "2e5b1205324ddefa", diff --git a/tests/fixtures/golden-install-parity/kilo.json b/tests/fixtures/golden-install-parity/kilo.json index 017d2705c..77d27e242 100644 --- a/tests/fixtures/golden-install-parity/kilo.json +++ b/tests/fixtures/golden-install-parity/kilo.json @@ -356,7 +356,7 @@ "gsd-core/workflows/remove-phase.md": "ada8a0546c686483", "gsd-core/workflows/remove-workspace.md": "fc83f362a2d0a1b7", "gsd-core/workflows/resume-project.md": "7f8dc986f0f35d96", - "gsd-core/workflows/review.md": "f8109b9ec1f56962", + "gsd-core/workflows/review.md": "3d62fa3096f46ecb", "gsd-core/workflows/scan.md": "47371c2073d6c0be", "gsd-core/workflows/secure-phase.md": "4977cf9e0462745b", "gsd-core/workflows/session-report.md": "2e5b1205324ddefa", diff --git a/tests/fixtures/golden-install-parity/kimi.json b/tests/fixtures/golden-install-parity/kimi.json index 0a0c46cfc..bc7778630 100644 --- a/tests/fixtures/golden-install-parity/kimi.json +++ b/tests/fixtures/golden-install-parity/kimi.json @@ -349,7 +349,7 @@ "gsd-core/workflows/remove-phase.md": "df9a45f0b1880999", "gsd-core/workflows/remove-workspace.md": "a7ca66db6b7c132c", "gsd-core/workflows/resume-project.md": "f28da1200e4545f4", - "gsd-core/workflows/review.md": "ad7c0f372ed986ae", + "gsd-core/workflows/review.md": "3ce57633978035ad", "gsd-core/workflows/scan.md": "003883d71c37da7d", "gsd-core/workflows/secure-phase.md": "22ab3bb5da494ea7", "gsd-core/workflows/session-report.md": "2e5b1205324ddefa", diff --git a/tests/fixtures/golden-install-parity/opencode.json b/tests/fixtures/golden-install-parity/opencode.json index 5bae80a00..d811cce36 100644 --- a/tests/fixtures/golden-install-parity/opencode.json +++ b/tests/fixtures/golden-install-parity/opencode.json @@ -356,7 +356,7 @@ "gsd-core/workflows/remove-phase.md": "dea4661e8f89596f", "gsd-core/workflows/remove-workspace.md": "446847e71aa52504", "gsd-core/workflows/resume-project.md": "ad9f06a10bab8cc0", - "gsd-core/workflows/review.md": "2d28a6683ff587de", + "gsd-core/workflows/review.md": "98e441bde98a9d92", "gsd-core/workflows/scan.md": "ad8ebcad4626d4a8", "gsd-core/workflows/secure-phase.md": "71e6e689e80288ec", "gsd-core/workflows/session-report.md": "2e5b1205324ddefa", diff --git a/tests/fixtures/golden-install-parity/pi.json b/tests/fixtures/golden-install-parity/pi.json index 753308586..43c3f37eb 100644 --- a/tests/fixtures/golden-install-parity/pi.json +++ b/tests/fixtures/golden-install-parity/pi.json @@ -252,7 +252,7 @@ "gsd-core/workflows/remove-phase.md": "df9a45f0b1880999", "gsd-core/workflows/remove-workspace.md": "a7ca66db6b7c132c", "gsd-core/workflows/resume-project.md": "f28da1200e4545f4", - "gsd-core/workflows/review.md": "ad7c0f372ed986ae", + "gsd-core/workflows/review.md": "3ce57633978035ad", "gsd-core/workflows/scan.md": "003883d71c37da7d", "gsd-core/workflows/secure-phase.md": "22ab3bb5da494ea7", "gsd-core/workflows/session-report.md": "2e5b1205324ddefa", diff --git a/tests/fixtures/golden-install-parity/qwen.json b/tests/fixtures/golden-install-parity/qwen.json index b26a125e8..7b882d4c8 100644 --- a/tests/fixtures/golden-install-parity/qwen.json +++ b/tests/fixtures/golden-install-parity/qwen.json @@ -285,7 +285,7 @@ "gsd-core/workflows/remove-phase.md": "e8ae4fbbfac700f0", "gsd-core/workflows/remove-workspace.md": "4ac64de862dc650e", "gsd-core/workflows/resume-project.md": "7f20769f302e5427", - "gsd-core/workflows/review.md": "bcbc20cb8df021cc", + "gsd-core/workflows/review.md": "7e1d8901d796338f", "gsd-core/workflows/scan.md": "949692db4834dd27", "gsd-core/workflows/secure-phase.md": "ef7b5ad194b687bf", "gsd-core/workflows/session-report.md": "2e5b1205324ddefa", diff --git a/tests/fixtures/golden-install-parity/trae.json b/tests/fixtures/golden-install-parity/trae.json index ec33c7022..84f717a92 100644 --- a/tests/fixtures/golden-install-parity/trae.json +++ b/tests/fixtures/golden-install-parity/trae.json @@ -285,7 +285,7 @@ "gsd-core/workflows/remove-phase.md": "a46c2fe853bf4e86", "gsd-core/workflows/remove-workspace.md": "ae0e1c6d4438d663", "gsd-core/workflows/resume-project.md": "f242e4c8aba18ea2", - "gsd-core/workflows/review.md": "835cf8c9594f17c1", + "gsd-core/workflows/review.md": "5fd9c6a9d7777eee", "gsd-core/workflows/scan.md": "63631467651d9ca8", "gsd-core/workflows/secure-phase.md": "4a647aec1e4d2dfe", "gsd-core/workflows/session-report.md": "2e5b1205324ddefa", diff --git a/tests/fixtures/golden-install-parity/windsurf.json b/tests/fixtures/golden-install-parity/windsurf.json index 92066bebf..6d506daa1 100644 --- a/tests/fixtures/golden-install-parity/windsurf.json +++ b/tests/fixtures/golden-install-parity/windsurf.json @@ -285,7 +285,7 @@ "gsd-core/workflows/remove-phase.md": "e7a6af429b36e77b", "gsd-core/workflows/remove-workspace.md": "b5e60fbb33b3e33a", "gsd-core/workflows/resume-project.md": "82cfe1b8cb17c085", - "gsd-core/workflows/review.md": "3e72508a5dccd45a", + "gsd-core/workflows/review.md": "a63e0b5bb3b73730", "gsd-core/workflows/scan.md": "12c11b2edc165df9", "gsd-core/workflows/secure-phase.md": "185a15d389951e6e", "gsd-core/workflows/session-report.md": "2e5b1205324ddefa", diff --git a/tests/fixtures/golden-install-parity/zcode.json b/tests/fixtures/golden-install-parity/zcode.json index 935b04b5f..d9193766f 100644 --- a/tests/fixtures/golden-install-parity/zcode.json +++ b/tests/fixtures/golden-install-parity/zcode.json @@ -356,7 +356,7 @@ "gsd-core/workflows/remove-phase.md": "df9a45f0b1880999", "gsd-core/workflows/remove-workspace.md": "a7ca66db6b7c132c", "gsd-core/workflows/resume-project.md": "f28da1200e4545f4", - "gsd-core/workflows/review.md": "ad7c0f372ed986ae", + "gsd-core/workflows/review.md": "3ce57633978035ad", "gsd-core/workflows/scan.md": "003883d71c37da7d", "gsd-core/workflows/secure-phase.md": "22ab3bb5da494ea7", "gsd-core/workflows/session-report.md": "2e5b1205324ddefa", diff --git a/tests/workflow-size-baseline.json b/tests/workflow-size-baseline.json index 4446e82b6..a74bfd4d7 100644 --- a/tests/workflow-size-baseline.json +++ b/tests/workflow-size-baseline.json @@ -64,7 +64,7 @@ "remove-phase.md": 8513, "remove-workspace.md": 7551, "resume-project.md": 17270, - "review.md": 47168, + "review.md": 50362, "scan.md": 7732, "secure-phase.md": 13622, "session-report.md": 4044,