fix(#1359): migrate workflows off deprecated TaskOutput to Read(outputFile) (#1362)

* fix(#1359): migrate workflows off deprecated TaskOutput to Read(outputFile)

The map-codebase and docs-update workflows collected background sub-agent
results with the deprecated Claude Code `TaskOutput` tool using `block: true`,
which has a confirmed main-session hang after the agent completes
(anthropics/claude-code#20236).

Migrate the collection steps to the upstream-recommended pattern: keep
`run_in_background=true` on the Agent spawn, then `Read` each agent's
`outputFile` (from the `async_launched` result) once it reports completion.
Completion-marker contracts and on-disk verification are unchanged, and the
non-Claude runtime fallbacks (sequential_mapping / sequential_generation) are
preserved byte-for-byte. docs-update's timeout note no longer references the
unwired `workflow.subagent_timeout` key (it kept a literal before).

Regression coverage folded into tests/subagent-timeout.test.cjs (the owning
module for background-subagent collection). Workflow size baseline regenerated
for the justified prose growth.

Refs #1355 (same latent hang surface).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore(#1359): add changeset for TaskOutput migration

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-06-16 21:55:45 -04:00
committed by GitHub
parent 1698e83336
commit 1a678eb0e9
5 changed files with 136 additions and 43 deletions

View File

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

View File

@@ -457,27 +457,23 @@ Continue to collect_wave_1.
<step name="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.
<step name="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.

View File

@@ -255,21 +255,19 @@ Continue to collect_confirmations.
</step>
<step name="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:**
```

View File

@@ -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(`<step name="${stepName}"`);
assert.ok(start !== -1, `step "${stepName}" must exist`);
const end = content.indexOf('</step>', 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('<step name="sequential_mapping"'), 'sequential_mapping fallback must be preserved');
assert.ok(content.includes('<step name="detect_runtime_capabilities"'), 'runtime capability detection must be preserved');
assert.ok(!stepBlock(content, 'sequential_mapping').includes('TaskOutput'), 'sequential_mapping fallback must not reference TaskOutput');
});
test('keeps the mapper completion marker contract', () => {
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('<step name="sequential_generation"'), 'sequential_generation fallback must be preserved');
assert.ok(!stepBlock(content, 'sequential_generation').includes('TaskOutput'), 'sequential_generation fallback must not reference TaskOutput');
});
test('keeps the doc-writer completion marker contract', () => {
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');

View File

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