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)`
+ );
+ }
+ });
+});