diff --git a/.changeset/merry-birds-gather.md b/.changeset/merry-birds-gather.md new file mode 100644 index 000000000..78fd848e4 --- /dev/null +++ b/.changeset/merry-birds-gather.md @@ -0,0 +1,5 @@ +--- +type: Fixed +pr: 1362 +--- +**The `map-codebase` and `docs-update` workflows no longer collect background sub-agent results with the deprecated Claude Code `TaskOutput` tool** — they keep `run_in_background=true` on the spawn and `Read` each agent's `outputFile` (from the `async_launched` result) once it reports completion, removing the `TaskOutput(block=true)` main-session hang surface (anthropics/claude-code#20236). Completion-marker contracts and on-disk verification are unchanged, and the non-Claude runtime fallbacks are preserved. diff --git a/gsd-core/workflows/docs-update.md b/gsd-core/workflows/docs-update.md index 6911d07b3..b5a18310b 100644 --- a/gsd-core/workflows/docs-update.md +++ b/gsd-core/workflows/docs-update.md @@ -457,27 +457,23 @@ Continue to collect_wave_1. **Read the work manifest first:** `Read .planning/tmp/docs-work-manifest.json` — update `status` to `"completed"` or `"failed"` for each Wave 1 item after collection. Write the updated manifest back to disk. -Wait for all 3 Wave 1 agents to complete using the TaskOutput tool. +Wait for all 3 Wave 1 background agents to finish, then read each agent's output file to collect confirmations. -Call TaskOutput for all 3 agents in parallel (single message with 3 TaskOutput calls): +Each `Agent(...)` call above with `run_in_background=true` returns an `async_launched` result that carries an `outputFile` path (and `canReadOutputFile: true`). Each agent's completion arrives as a message in this conversation when it finishes — do NOT issue a separate blocking call to wait. Once all 3 agents have reported completion, read their output files in parallel (single message with 3 Read calls): ``` -TaskOutput tool: - task_id: "{task_id from README agent result}" - block: true - timeout: 300000 +Read tool: + file_path: "{outputFile from README agent result}" -TaskOutput tool: - task_id: "{task_id from ARCHITECTURE agent result}" - block: true - timeout: 300000 +Read tool: + file_path: "{outputFile from ARCHITECTURE agent result}" -TaskOutput tool: - task_id: "{task_id from CONFIGURATION agent result}" - block: true - timeout: 300000 +Read tool: + file_path: "{outputFile from CONFIGURATION agent result}" ``` +> Allow up to 5 minutes (300000 ms) for the slowest agent to finish before treating it as failed. + **Expected confirmation format from each agent:** ``` ## Doc Generation Complete @@ -676,29 +672,25 @@ Continue to collect_wave_2. **Read the work manifest first:** `Read .planning/tmp/docs-work-manifest.json` — update `status` to `"completed"` or `"failed"` for each Wave 2 item after collection. Write the updated manifest back to disk. -Wait for all Wave 2 agents to complete using the TaskOutput tool. +Wait for all Wave 2 background agents to finish, then read each agent's output file to collect confirmations. -Call TaskOutput for all Wave 2 agents in parallel (single message with N TaskOutput calls — one per spawned Wave 2 agent): +Each `Agent(...)` call above with `run_in_background=true` returns an `async_launched` result that carries an `outputFile` path (and `canReadOutputFile: true`). Each agent's completion arrives as a message in this conversation when it finishes — do NOT issue a separate blocking call to wait. Once all Wave 2 agents have reported completion, read their output files in parallel (single message with N Read calls — one per spawned Wave 2 agent): ``` -TaskOutput tool: - task_id: "{task_id from GETTING-STARTED agent result}" - block: true - timeout: 300000 +Read tool: + file_path: "{outputFile from GETTING-STARTED agent result}" -TaskOutput tool: - task_id: "{task_id from DEVELOPMENT agent result}" - block: true - timeout: 300000 +Read tool: + file_path: "{outputFile from DEVELOPMENT agent result}" -TaskOutput tool: - task_id: "{task_id from TESTING agent result}" - block: true - timeout: 300000 +Read tool: + file_path: "{outputFile from TESTING agent result}" -# Add one TaskOutput call per conditional agent spawned (API, DEPLOYMENT, CONTRIBUTING) +# Add one Read call per conditional agent spawned (API, DEPLOYMENT, CONTRIBUTING) ``` +> Allow up to 5 minutes (300000 ms) for the slowest agent to finish before treating it as failed. + **After collection, verify all Wave 2 files exist on disk** using the `resolved_path` from each manifest entry: ```bash ls -la {resolved_path for each wave 2 item} 2>/dev/null @@ -752,9 +744,9 @@ Write {package_dir}/README.md directly. Return confirmation only — do not retu ) ``` -> **ORCHESTRATOR RULE — CODEX RUNTIME**: After calling all per-package Agent() calls above with `run_in_background=true`, do NOT generate any package READMEs independently while the subagents are active. Wait for all agents to complete via TaskOutput before proceeding. This prevents duplicate work and wasted context. +> **ORCHESTRATOR RULE — CODEX RUNTIME**: After calling all per-package Agent() calls above with `run_in_background=true`, do NOT generate any package READMEs independently while the subagents are active. Wait for all agents to complete before proceeding. This prevents duplicate work and wasted context. -Collect confirmations via TaskOutput for all package agents. Note failures in the final report. +Collect confirmations by reading each package agent's `outputFile` once it reports completion — each `run_in_background=true` Agent call returns an `async_launched` result carrying an `outputFile` path (with `canReadOutputFile: true`). Note failures in the final report. **Fallback when Task tool is unavailable:** Generate per-package READMEs sequentially inline after the `sequential_generation` step. For each package directory with a `package.json`, construct the equivalent `doc_assignment` block and generate the README following gsd-doc-writer instructions. diff --git a/gsd-core/workflows/map-codebase.md b/gsd-core/workflows/map-codebase.md index 12d13efd0..70a842696 100644 --- a/gsd-core/workflows/map-codebase.md +++ b/gsd-core/workflows/map-codebase.md @@ -255,21 +255,19 @@ Continue to collect_confirmations. -Wait for all 4 agents to complete using TaskOutput tool. +Wait for all 4 background agents to finish, then read each agent's output file to collect confirmations. -**For each agent task_id returned by the Agent tool calls above:** +Each `Agent(...)` call above with `run_in_background=true` returns an `async_launched` result that carries an `outputFile` path (and `canReadOutputFile: true`). The 4 agents run concurrently and each one's completion arrives as a message in this conversation when it finishes — do NOT issue a separate blocking call to wait for them. + +**Once all 4 agents have reported completion, read each agent's output file (single message with 4 Read calls):** ``` -TaskOutput tool: - task_id: "{task_id from Agent result}" - block: true - timeout: {subagent_timeout from init context, default 300000} +Read tool: + file_path: "{outputFile from that agent's async_launched result}" ``` -> The timeout is configurable via `workflow.subagent_timeout` in `.planning/config.json` (milliseconds). Default: 300000 (5 minutes). Increase for large codebases or slower models. +> Allow up to `workflow.subagent_timeout` for the slowest agent to finish before treating it as failed. The timeout is configurable via `workflow.subagent_timeout` in `.planning/config.json` (milliseconds). Default: 300000 (5 minutes). Increase for large codebases or slower models. -Call TaskOutput for all 4 agents in parallel (single message with 4 TaskOutput calls). - -Once all TaskOutput calls return, read each agent's output file to collect confirmations. +Each output file contains that agent's completion confirmation. Parse the confirmation marker (see below) from the file contents. **Expected confirmation format from each agent:** ``` diff --git a/tests/subagent-timeout.test.cjs b/tests/subagent-timeout.test.cjs index fa456d243..9db81a8f3 100644 --- a/tests/subagent-timeout.test.cjs +++ b/tests/subagent-timeout.test.cjs @@ -113,6 +113,104 @@ describe('map-codebase workflow references configurable timeout (#1472)', () => }); }); +// ─── #1359: background-subagent collection migrated off deprecated TaskOutput ── +// Anthropic deprecated the Claude Code `TaskOutput` tool (prefer `Read` on the +// task's output file) and `TaskOutput(block=true)` has a confirmed main-session +// hang (anthropics/claude-code#20236). The collect steps must spawn with +// `run_in_background=true` then `Read` each agent's `outputFile` (from the +// `async_launched` result). The non-Claude runtime fallbacks must be preserved +// and must not reference TaskOutput either. + +describe('#1359: workflows collect background subagents via Read(outputFile), not TaskOutput', () => { + const WORKFLOWS_DIR = path.join(__dirname, '..', 'gsd-core', 'workflows'); + + function readWorkflow(name) { + return fs.readFileSync(path.join(WORKFLOWS_DIR, name), 'utf8'); + } + + function stepBlock(content, stepName) { + const start = content.indexOf(`', start); + assert.ok(end !== -1, `step "${stepName}" must be closed`); + return content.slice(start, end); + } + + describe('map-codebase.md', () => { + const content = readWorkflow('map-codebase.md'); + + test('contains no deprecated TaskOutput tool references', () => { + assert.ok( + !content.includes('TaskOutput'), + 'map-codebase.md must not reference the deprecated TaskOutput tool (claude-code#20236 hang)' + ); + }); + + test('collect_confirmations reads each agent outputFile', () => { + const block = stepBlock(content, 'collect_confirmations'); + assert.ok(block.includes('outputFile'), 'collect_confirmations must read each agent outputFile'); + assert.ok(/Read tool:/.test(block), 'collect_confirmations must instruct a Read tool call'); + assert.ok(block.includes('async_launched'), 'collect_confirmations must reference the async_launched result'); + assert.ok(!/block:\s*true/.test(block), 'collect_confirmations must not use a blocking collect call'); + }); + + test('still spawns mappers with run_in_background=true', () => { + assert.ok(content.includes('run_in_background=true'), 'background spawn must be preserved'); + }); + + test('preserves the non-Agent runtime fallback (sequential_mapping)', () => { + assert.ok(content.includes(' { + assert.ok(content.includes('## Mapping Complete'), 'mapper completion marker must remain documented'); + }); + }); + + describe('docs-update.md', () => { + const content = readWorkflow('docs-update.md'); + + test('contains no deprecated TaskOutput tool references', () => { + assert.ok( + !content.includes('TaskOutput'), + 'docs-update.md must not reference the deprecated TaskOutput tool (claude-code#20236 hang)' + ); + }); + + for (const step of ['collect_wave_1', 'collect_wave_2']) { + test(`${step} reads each agent outputFile`, () => { + const block = stepBlock(content, step); + assert.ok(block.includes('outputFile'), `${step} must read each agent outputFile`); + assert.ok(block.includes('async_launched'), `${step} must reference the async_launched result`); + assert.ok(/Read tool:/.test(block), `${step} must instruct a Read tool call`); + assert.ok(!/block:\s*true/.test(block), `${step} must not use a blocking collect call`); + }); + } + + test('dispatch_monorepo_packages collects per-package READMEs via outputFile', () => { + const block = stepBlock(content, 'dispatch_monorepo_packages'); + assert.ok(block.includes('outputFile'), 'per-package collection must read each agent outputFile'); + assert.ok(block.includes('async_launched'), 'per-package collection must reference the async_launched result'); + assert.ok(!/block:\s*true/.test(block), 'per-package collection must not use a blocking collect call'); + }); + + test('still spawns doc-writers with run_in_background=true', () => { + assert.ok(content.includes('run_in_background=true'), 'background spawn must be preserved'); + }); + + test('preserves the non-Task runtime fallback (sequential_generation)', () => { + assert.ok(content.includes(' { + assert.ok(content.includes('## Doc Generation Complete'), 'doc-writer completion marker must remain documented'); + }); + }); +}); + describe('planning-config.md documents subagent_timeout (#1472)', () => { test('reference doc includes subagent_timeout entry', () => { const refPath = path.join(__dirname, '..', 'gsd-core', 'references', 'planning-config.md'); diff --git a/tests/workflow-size-baseline.json b/tests/workflow-size-baseline.json index 1bf91a196..f5ddd2b06 100644 --- a/tests/workflow-size-baseline.json +++ b/tests/workflow-size-baseline.json @@ -21,7 +21,7 @@ "discuss-phase-power.md": 11273, "discuss-phase.md": 31965, "do.md": 10068, - "docs-update.md": 54770, + "docs-update.md": 55662, "edit-phase.md": 12883, "eval-review.md": 9923, "execute-phase.md": 93157, @@ -40,7 +40,7 @@ "list-phase-assumptions.md": 4305, "list-workspaces.md": 5655, "manager.md": 25937, - "map-codebase.md": 20360, + "map-codebase.md": 20789, "milestone-summary.md": 11774, "mvp-phase.md": 13582, "new-milestone.md": 32422,