* 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:
5
.changeset/merry-birds-gather.md
Normal file
5
.changeset/merry-birds-gather.md
Normal 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.
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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:**
|
||||
```
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user