From 5452f1a7006c7f465149b50302dd6639e6e72136 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Fri, 14 Aug 2026 10:33:09 -0400 Subject: [PATCH] fix(#3324): build-time embed execution context instead of literal @-includes (#3462) * fix(#3324): build-time embed execution context instead of literal @-includes * chore(#3324): add changeset * chore(#3324): set changeset pr reference * fix(#3324): trim embed note to stay under the 93400 margin ceiling --------- Co-authored-by: sim --- .changeset/serene-ibex-hum.md | 5 ++ gsd-core/workflows/execute-phase.md | 13 +-- .../1689-agent-hint-executor-routing.json | 6 -- .../3324-at-includes-literal-text.json | 6 ++ tests/workflow-size-budget.test.cjs | 87 +++++++++++++++++++ 5 files changed, 105 insertions(+), 12 deletions(-) create mode 100644 .changeset/serene-ibex-hum.md delete mode 100644 tests/emitted-drift-acks/1689-agent-hint-executor-routing.json create mode 100644 tests/emitted-drift-acks/3324-at-includes-literal-text.json diff --git a/.changeset/serene-ibex-hum.md b/.changeset/serene-ibex-hum.md new file mode 100644 index 000000000..1d76afd41 --- /dev/null +++ b/.changeset/serene-ibex-hum.md @@ -0,0 +1,5 @@ +--- +type: Fixed +pr: 3462 +--- +Executor dispatch prompts no longer list companion files as raw @-include lines that Claude Code never expands inside an Agent() prompt string. The orchestrator now build-time embeds execute-plan.md and its companion references (summary template, checkpoints, tdd, worktree-path-safety, executor-examples) into the dispatched gsd-executor prompt, so execute-plan-only steps (segment_execution, previous_phase_check, verification_failure_gate, update_codebase_map) actually reach executors instead of silently never running. diff --git a/gsd-core/workflows/execute-phase.md b/gsd-core/workflows/execute-phase.md index fd8591acc..d82a5864c 100644 --- a/gsd-core/workflows/execute-phase.md +++ b/gsd-core/workflows/execute-phase.md @@ -733,12 +733,13 @@ increases monotonically across waves. `{status}` is `complete` (success), - @~/.claude/gsd-core/workflows/execute-plan.md - @~/.claude/gsd-core/templates/summary.md - @~/.claude/gsd-core/references/checkpoints.md - @~/.claude/gsd-core/references/tdd.md - @~/.claude/gsd-core/references/worktree-path-safety.md - ${CONTEXT_WINDOW < 200000 ? '' : '@~/.claude/gsd-core/references/executor-examples.md'} + ORCHESTRATOR build-time embed (NOT a sub-agent runtime step): before this dispatch, read each file listed below and replace this note with those files' contents, inlined verbatim in this block in the listed order. Never leave `@`-include lines in the dispatched prompt — `@path` never expands inside an Agent() `prompt="..."` string (#3324), so an include arrives as literal text the executor never sees. + - `~/.claude/gsd-core/workflows/execute-plan.md` + - `~/.claude/gsd-core/templates/summary.md` + - `~/.claude/gsd-core/references/checkpoints.md` + - `~/.claude/gsd-core/references/tdd.md` + - `~/.claude/gsd-core/references/worktree-path-safety.md` + ${CONTEXT_WINDOW < 200000 ? '' : '- `~/.claude/gsd-core/references/executor-examples.md`'} diff --git a/tests/emitted-drift-acks/1689-agent-hint-executor-routing.json b/tests/emitted-drift-acks/1689-agent-hint-executor-routing.json deleted file mode 100644 index 90a12e953..000000000 --- a/tests/emitted-drift-acks/1689-agent-hint-executor-routing.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "version": 1, - "paths": { - "execute-phase.md": "#1689: per-plan agent_hint routing adds a one-line per-plan fragment reference and swaps the gsd-executor literal for a {EXECUTOR_TYPE} placeholder. Net +6 bytes, still under the ADR-857 Phase 6 byte ceiling (93600); all resolution detail lives in the execute-phase/steps/per-plan-executor-routing.md fragment to keep the host lean." - } -} diff --git a/tests/emitted-drift-acks/3324-at-includes-literal-text.json b/tests/emitted-drift-acks/3324-at-includes-literal-text.json new file mode 100644 index 000000000..2f4a626d4 --- /dev/null +++ b/tests/emitted-drift-acks/3324-at-includes-literal-text.json @@ -0,0 +1,6 @@ +{ + "version": 1, + "paths": { + "execute-phase.md": "#3324: the block of the gsd-executor Agent() dispatch prompt listed companion files as raw @~/ include lines. Claude Code expands @path only in natively-loaded markdown bodies, never inside a dynamically constructed prompt=\"...\" string, so every execute-plan.md-only step (segment_execution, previous_phase_check, verification_failure_gate, update_codebase_map) silently never reached dispatched executors. The block now carries the ORCHESTRATOR build-time embed instruction already established by the adjacent block (b88d6d6ef / #589) — the orchestrator reads each listed file and inlines its contents verbatim before calling Agent() — and the paths are backticked list entries with no @ sigil so no literal include line can leak into the prompt. Both dispatch paths share this block (sequential reuses the worktree-mode structure), and a repo-wide prompt-region guard in tests/workflow-size-budget.test.cjs prevents reintroduction anywhere under gsd-core/workflows/**. The 432-byte growth (92954 -> 93386, still under the XL 96 KiB ceiling) is the embed instruction line itself plus the backticked path list replacing the five bare include lines; every byte of it exists to prevent the silent step-loss failure mode from returning." + } +} diff --git a/tests/workflow-size-budget.test.cjs b/tests/workflow-size-budget.test.cjs index fa85509c5..7c42d1611 100644 --- a/tests/workflow-size-budget.test.cjs +++ b/tests/workflow-size-budget.test.cjs @@ -602,3 +602,90 @@ describe('SIZE: byteCount is line-ending independent (#683 regression)', () => { } }); }); + +// --------------------------------------------------------------------------- +// #3324 — @-include lines inside Agent() prompt strings never expand +// --------------------------------------------------------------------------- +// Claude Code expands @path only in natively-loaded markdown bodies (CLAUDE.md, +// slash-command/skill bodies, agent definitions) — never inside the prompt +// parameter of a dynamically constructed Agent() call, which is delivered as +// literal turn text. A bare @-include line in a prompt string means the +// subagent never sees the referenced file (#3324). +describe('#3324: no @-include lines inside Agent() prompt strings', () => { + const BARE_INCLUDE_LINE = /^\s*@(\$HOME|~)\//; + + function listWorkflowFilesRecursive(dir, out = []) { + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + const p = path.join(dir, entry.name); + if (entry.isDirectory()) listWorkflowFilesRecursive(p, out); + else if (entry.name.endsWith('.md')) out.push(p); + } + return out; + } + + // A prompt region opens at a line ending in `prompt="` and closes at the + // first subsequent line that is only whitespace + a double quote. + function bareIncludeLinesInPromptRegions(content) { + const hits = []; + let inPrompt = false; + content.split('\n').forEach((line, i) => { + if (!inPrompt && /prompt="\s*$/.test(line)) { inPrompt = true; return; } + if (inPrompt && /^\s*"\s*$/.test(line)) { inPrompt = false; return; } + if (inPrompt && BARE_INCLUDE_LINE.test(line)) { + hits.push({ line: i + 1, text: line.trim() }); + } + }); + return hits; + } + + test('no workflow prompt string contains a bare @-include line (repo-wide guard)', () => { + const offenders = []; + for (const file of listWorkflowFilesRecursive(WORKFLOWS_DIR)) { + const rel = path.relative(path.join(__dirname, '..'), file); + for (const hit of bareIncludeLinesInPromptRegions(fs.readFileSync(file, 'utf-8'))) { + offenders.push(`${rel}:${hit.line} ${hit.text}`); + } + } + assert.deepEqual( + offenders, + [], + 'Claude Code never expands @path inside a dynamically built Agent() ' + + 'prompt="..." string — the include arrives as literal text and the ' + + 'subagent never sees the referenced file. Use the ORCHESTRATOR ' + + 'build-time embed pattern (see execute-phase.md ' + + 'and ) or inline the content. See #3324.' + ); + }); + + test('execute-phase.md build-time embeds execute-plan.md instead of @-including it', () => { + const content = fs.readFileSync(path.join(WORKFLOWS_DIR, 'execute-phase.md'), 'utf-8'); + const block = content.match(/([\s\S]{0,4000}?)<\/execution_context>/); + assert.ok( + block, + 'execute-phase.md must keep an block in the executor dispatch prompt' + ); + assert.ok( + /ORCHESTRATOR build-time embed/.test(block[1]), + ' must carry the ORCHESTRATOR build-time embed instruction (#3324)' + ); + assert.ok( + /`~\/\.claude\/gsd-core\/workflows\/execute-plan\.md`/.test(block[1]), + ' must list execute-plan.md (backticked, no @ sigil) for build-time embed (#3324)' + ); + }); + + test('execute-plan.md still defines the steps only it carries into the dispatch', () => { + const content = fs.readFileSync(path.join(WORKFLOWS_DIR, 'execute-plan.md'), 'utf-8'); + for (const marker of [ + 'segment_execution', + 'previous_phase_check', + 'verification_failure_gate', + 'update_codebase_map', + ]) { + assert.ok( + content.includes(marker), + `execute-plan.md must still define ${marker} — it reaches executors only via the build-time embed (#3324)` + ); + } + }); +});