diff --git a/.changeset/sturdy-writers-survive.md b/.changeset/sturdy-writers-survive.md new file mode 100644 index 000000000..5a4f4ec26 --- /dev/null +++ b/.changeset/sturdy-writers-survive.md @@ -0,0 +1,5 @@ +--- +type: Fixed +pr: 599 +--- +**Large-file GSD writer agents no longer fail to write their output on OpenCode** — `gsd-research-synthesizer`, `gsd-planner`, `gsd-executor`, `gsd-domain-researcher`, `gsd-project-researcher`, and `gsd-ui-researcher` now carry the same truncation-resilient write contract added for `gsd-phase-researcher` in #214. Each keeps its single-write default and falls back to an incremental, sentinel-based Write→Read→Edit sequence only when a runtime truncates an oversized tool call (upstream opencode#18108), instead of doom-looping on `JSON parsing failed: Expected '}'`. diff --git a/agents/gsd-domain-researcher.md b/agents/gsd-domain-researcher.md index 1528bbdec..9db13a768 100644 --- a/agents/gsd-domain-researcher.md +++ b/agents/gsd-domain-researcher.md @@ -98,6 +98,19 @@ If internal tooling with no regulated domain, "domain expert" = product owner or **ALWAYS use the Write tool to create files** — never use `Bash(cat << 'EOF')` or heredoc commands for file creation. +**Write contract (hard rules — must follow):** + +Section 1b of AI-SPEC.md is the output of this step. The orchestrator reads `AI-SPEC.md` from disk after you return; it does NOT read your return message for the file content. + +1. **Default: write the section in a single `Write` call.** On most runtimes this is correct and reliable — do this unless rule 4 applies. +2. **Do NOT return the AI-SPEC.md content in your response.** Your return message is a brief confirmation; the content lives on disk. +3. **Do NOT use `Bash(cat << 'EOF')` or heredoc** for file creation. Use the `Write` tool. +4. **Large-file / truncation fallback.** Some runtimes (e.g. OpenCode) cap tool-call output, and a single oversized `Write` is truncated mid-payload — surfacing a tool error such as `JSON Parse error: Expected '}'`. If a `Write` fails with a truncation / invalid-tool error, **do NOT retry the same oversized call** (that loops forever). Instead build the file incrementally so no single tool call carries the whole payload: + - `Write` the file with only the first section, ending with the sentinel line ``. + - `Read` the file, then `Edit` it, replacing `` with the next section followed by the sentinel again. Repeat, one section per `Edit`. + - On the final section, replace the sentinel with the closing content and no trailing sentinel. +5. **If writing still fails, surface the actual error in your return message.** **Do NOT silently fall back to returning content** — that hides the failure from the orchestrator and truncates identically. + Update AI-SPEC.md at `ai_spec_path`. Add/update Section 1b: ```markdown diff --git a/agents/gsd-executor.md b/agents/gsd-executor.md index c9d5d0f5b..eb93620ea 100644 --- a/agents/gsd-executor.md +++ b/agents/gsd-executor.md @@ -589,6 +589,19 @@ After all tasks complete, create `{phase}-{plan}-SUMMARY.md` at `.planning/phase Use the Write tool to create files — never use `Bash(cat << 'EOF')` or heredoc commands for file creation. +**Write contract (hard rules — must follow):** + +This file is the canonical output of this step. The orchestrator reads `.planning/phases/XX-name/{phase}-{plan}-SUMMARY.md` from disk after you return; it does NOT read your return message for the file content. + +1. **Default: write the whole file in a single `Write` call.** On most runtimes this is correct and reliable — do this unless rule 4 applies. +2. **Do NOT return the SUMMARY.md content in your response.** Your return message is a brief confirmation; the content lives on disk. +3. **Do NOT use `Bash(cat << 'EOF')` or heredoc** for file creation. Use the `Write` tool. +4. **Large-file / truncation fallback.** Some runtimes (e.g. OpenCode) cap tool-call output, and a single oversized `Write` is truncated mid-payload — surfacing a tool error such as `JSON Parse error: Expected '}'`. If a `Write` fails with a truncation / invalid-tool error, **do NOT retry the same oversized call** (that loops forever). Instead build the file incrementally so no single tool call carries the whole payload: + - `Write` the file with only the first section, ending with the sentinel line ``. + - `Read` the file, then `Edit` it, replacing `` with the next section followed by the sentinel again. Repeat, one section per `Edit`. + - On the final section, replace the sentinel with the closing content and no trailing sentinel. +5. **If writing still fails, surface the actual error in your return message.** **Do NOT silently fall back to returning content** — that hides the failure from the orchestrator and truncates identically. + **Use template:** @~/.claude/get-shit-done/templates/summary.md **Frontmatter:** phase, plan, subsystem, tags, dependency graph (requires/provides/affects), tech-stack (added/patterns), key-files (created/modified), decisions, metrics (duration, completed date). diff --git a/agents/gsd-planner.md b/agents/gsd-planner.md index 4edfa33f1..aef1ce6f3 100644 --- a/agents/gsd-planner.md +++ b/agents/gsd-planner.md @@ -1021,6 +1021,19 @@ Use template structure for each PLAN.md. **ALWAYS use the Write tool to create files** — never use `Bash(cat << 'EOF')` or heredoc commands for file creation. +**Write contract (hard rules — must follow):** + +These PLAN.md files are the canonical output of this agent. The orchestrator reads each `.planning/phases/{padded_phase}-{slug}/{padded_phase}-{NN}-PLAN.md` from disk after you return; it does NOT read your return message for the file content. + +1. **Default: write each PLAN.md in a single `Write` call.** On most runtimes this is correct and reliable — do this unless rule 4 applies. +2. **Do NOT return the PLAN.md content in your response.** Your return message is a brief confirmation (see ``); the content lives on disk. +3. **Do NOT use `Bash(cat << 'EOF')` or heredoc** for file creation. Use the `Write` tool. +4. **Large-file / truncation fallback.** Some runtimes (e.g. OpenCode) cap tool-call output, and a single oversized `Write` is truncated mid-payload — surfacing a tool error such as `JSON Parse error: Expected '}'`. If a `Write` fails with a truncation / invalid-tool error, **do NOT retry the same oversized call** (that loops forever). Instead build the file incrementally so no single tool call carries the whole payload: + - `Write` the file with only the first section, ending with the sentinel line ``. + - `Read` the file, then `Edit` it, replacing `` with the next section followed by the sentinel again. Repeat, one section per `Edit`. + - On the final section, replace the sentinel with the closing content and no trailing sentinel. +5. **If writing still fails, surface the actual error in your return message.** **Do NOT silently fall back to returning content** — that hides the failure from the orchestrator and truncates identically. + **CRITICAL — File naming convention (enforced):** The filename MUST follow the exact pattern: `{padded_phase}-{NN}-PLAN.md` diff --git a/agents/gsd-project-researcher.md b/agents/gsd-project-researcher.md index 22b951c9a..75135ff99 100644 --- a/agents/gsd-project-researcher.md +++ b/agents/gsd-project-researcher.md @@ -574,6 +574,19 @@ Run pre-submission checklist (see verification_protocol). **ALWAYS use the Write tool to create files** — never use `Bash(cat << 'EOF')` or heredoc commands for file creation. +**Write contract (hard rules — must follow):** + +These files are the canonical output of this agent. The orchestrator reads them from `.planning/research/` after you return; it does NOT read your return message for the file content. + +1. **Default: write each file in a single `Write` call.** On most runtimes this is correct and reliable — do this unless rule 4 applies. +2. **Do NOT return the file contents in your response.** Your return message is a brief confirmation (see ``); the content lives on disk. +3. **Do NOT use `Bash(cat << 'EOF')` or heredoc** for file creation. Use the `Write` tool. +4. **Large-file / truncation fallback.** Some runtimes (e.g. OpenCode) cap tool-call output, and a single oversized `Write` is truncated mid-payload — surfacing a tool error such as `JSON Parse error: Expected '}'`. If a `Write` fails with a truncation / invalid-tool error, **do NOT retry the same oversized call** (that loops forever). Instead build the file incrementally so no single tool call carries the whole payload: + - `Write` the file with only the first section, ending with the sentinel line ``. + - `Read` the file, then `Edit` it, replacing `` with the next section followed by the sentinel again. Repeat, one section per `Edit`. + - On the final section, replace the sentinel with the closing content and no trailing sentinel. +5. **If writing still fails, surface the actual error in your return message.** **Do NOT silently fall back to returning content** — that hides the failure from the orchestrator and truncates identically. + In `.planning/research/`: 1. **SUMMARY.md** — Always 2. **STACK.md** — Always diff --git a/agents/gsd-research-synthesizer.md b/agents/gsd-research-synthesizer.md index 5e79f629f..f9e6c5d07 100644 --- a/agents/gsd-research-synthesizer.md +++ b/agents/gsd-research-synthesizer.md @@ -137,6 +137,10 @@ Identify gaps that couldn't be resolved and need attention during planning. 3. **Do NOT ask permission to write.** Writing `.planning/research/SUMMARY.md` is the explicit purpose of this agent. Asking the orchestrator to do it instead is a failure mode that can cause downstream `SUMMARY.md not found` failures. 4. **Do NOT use `Bash(cat << 'EOF')` or heredoc** for file creation. Use the `Write` tool. In short: **never use `Bash(cat << 'EOF')` or heredoc**. 5. **If the Write tool errors,** surface the actual error in your return message. Do not silently fall back to returning content; that hides the failure from the orchestrator. +6. **Large-file / truncation fallback.** Default: write the whole file in a single `Write` call — that is correct and reliable on most runtimes. But some runtimes (e.g. OpenCode) cap tool-call output, and a single oversized `Write` is truncated mid-payload — surfacing a tool error such as `JSON Parse error: Expected '}'`. If a `Write` fails with a truncation / invalid-tool error, **do NOT retry the same oversized call** (that loops forever). Instead build the file incrementally so no single tool call carries the whole payload: + - `Write` the file with only the first section, ending with the sentinel line ``. + - `Read` the file, then `Edit` it, replacing `` with the next section followed by the sentinel again. Repeat, one section per `Edit`. + - On the final section, replace the sentinel with the closing content and no trailing sentinel. Use template: ~/.claude/get-shit-done/templates/research-project/SUMMARY.md diff --git a/agents/gsd-ui-researcher.md b/agents/gsd-ui-researcher.md index e6ae09383..5e2205075 100644 --- a/agents/gsd-ui-researcher.md +++ b/agents/gsd-ui-researcher.md @@ -289,6 +289,19 @@ Read template: `~/.claude/get-shit-done/templates/UI-SPEC.md` Fill all sections. Write to `$PHASE_DIR/$PADDED_PHASE-UI-SPEC.md`. +**Write contract (hard rules — must follow):** + +This file is the canonical output of this agent. The orchestrator reads `$PHASE_DIR/$PADDED_PHASE-UI-SPEC.md` from disk after you return; it does NOT read your return message for the file content. + +1. **Default: write the whole file in a single `Write` call.** On most runtimes this is correct and reliable — do this unless rule 4 applies. +2. **Do NOT return the UI-SPEC.md content in your response.** Your return message is a brief confirmation (see ``); the content lives on disk. +3. **Do NOT use `Bash(cat << 'EOF')` or heredoc** for file creation. Use the `Write` tool. +4. **Large-file / truncation fallback.** Some runtimes (e.g. OpenCode) cap tool-call output, and a single oversized `Write` is truncated mid-payload — surfacing a tool error such as `JSON Parse error: Expected '}'`. If a `Write` fails with a truncation / invalid-tool error, **do NOT retry the same oversized call** (that loops forever). Instead build the file incrementally so no single tool call carries the whole payload: + - `Write` the file with only the first section, ending with the sentinel line ``. + - `Read` the file, then `Edit` it, replacing `` with the next section followed by the sentinel again. Repeat, one section per `Edit`. + - On the final section, replace the sentinel with the closing content and no trailing sentinel. +5. **If writing still fails, surface the actual error in your return message.** **Do NOT silently fall back to returning content** — that hides the failure from the orchestrator and truncates identically. + ## Step 6: Commit (optional) ```bash diff --git a/tests/bug-214-writer-agents-write-truncation-contract.test.cjs b/tests/bug-214-writer-agents-write-truncation-contract.test.cjs new file mode 100644 index 000000000..59df4f6f1 --- /dev/null +++ b/tests/bug-214-writer-agents-write-truncation-contract.test.cjs @@ -0,0 +1,96 @@ +// allow-test-rule: source-text-is-the-product +'use strict'; + +const { test, describe } = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); + +const REPO_ROOT = path.resolve(__dirname, '..'); + +// Every agent that writes a large file in a single Write call must carry the +// same truncation-resilient write contract added for bug #214. OpenCode shares +// OUTPUT_TOKEN_MAX=32000 with the thinking budget (upstream opencode#18108), so +// an oversized single `write` tool call is truncated mid-payload, yielding +// `JSON Parse error: Expected '}'`, and OpenCode then doom-loops. gsd-phase-researcher +// is locked by its own bug-214 test; this locks the other large-file writers. +const WRITER_AGENTS = [ + 'gsd-research-synthesizer', + 'gsd-planner', + 'gsd-executor', + 'gsd-domain-researcher', + 'gsd-project-researcher', + 'gsd-ui-researcher', +]; + +function readAgent(name) { + return fs.readFileSync(path.join(REPO_ROOT, 'agents', `${name}.md`), 'utf8'); +} + +describe('bug #214: large-file writer agents must survive write-tool truncation', () => { + for (const name of WRITER_AGENTS) { + describe(name, () => { + const prompt = readAgent(name); + + test('keeps single-Write as the default path', () => { + assert.match( + prompt, + /in a single `Write` call/i, + `${name}: must keep single-Write as the default (no regression for non-truncating runtimes).` + ); + }); + + test('names the truncation failure mode', () => { + assert.match(prompt, /truncat/i, `${name}: must name the truncation failure mode.`); + }); + + test('instructs incremental construction on large files', () => { + assert.match( + prompt, + /incrementa/i, + `${name}: must instruct incremental construction on large files.` + ); + }); + + test('defines the continuation sentinel', () => { + assert.match( + prompt, + //, + `${name}: must define the continuation sentinel for incremental writes.` + ); + }); + + test('forbids identical retry of the oversized write (doom-loop guard)', () => { + assert.match( + prompt, + /do NOT retry the same oversized call/i, + `${name}: must forbid identical retry of the oversized write.` + ); + }); + + test('requires Read before Edit', () => { + assert.match( + prompt, + /`Read` the file, then `Edit`/i, + `${name}: must require Read before Edit (OpenCode edit requires a prior Read).` + ); + }); + + test('instructs removing the sentinel on the final section', () => { + assert.match( + prompt, + /no trailing sentinel/i, + `${name}: must instruct removing the sentinel on the final section.` + ); + }); + + test('forbids silent fallback to returning content', () => { + assert.match( + prompt, + /do NOT silently fall back to returning content/i, + `${name}: must forbid silent fallback to returning content.` + ); + }); + }); + } +});