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 <sim@local>
This commit is contained in:
Tom Boucher
2026-08-14 10:33:09 -04:00
committed by GitHub
parent da7d4dac52
commit 5452f1a700
5 changed files with 105 additions and 12 deletions

View File

@@ -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.

View File

@@ -733,12 +733,13 @@ increases monotonically across waves. `{status}` is `complete` (success),
</parallel_execution>
<execution_context>
@~/.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`'}
</execution_context>
<files_to_read>

View File

@@ -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."
}
}

View File

@@ -0,0 +1,6 @@
{
"version": 1,
"paths": {
"execute-phase.md": "#3324: the <execution_context> 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 <worktree_branch_check> 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."
}
}

View File

@@ -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 <worktree_branch_check> ' +
'and <execution_context>) or inline the content. See #3324.'
);
});
test('execute-phase.md <execution_context> 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(/<execution_context>([\s\S]{0,4000}?)<\/execution_context>/);
assert.ok(
block,
'execute-phase.md must keep an <execution_context> block in the executor dispatch prompt'
);
assert.ok(
/ORCHESTRATOR build-time embed/.test(block[1]),
'<execution_context> must carry the ORCHESTRATOR build-time embed instruction (#3324)'
);
assert.ok(
/`~\/\.claude\/gsd-core\/workflows\/execute-plan\.md`/.test(block[1]),
'<execution_context> 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)`
);
}
});
});