fix(#214): apply OpenCode write-truncation contract to all large-file writer agents (#599)

* fix(#214): apply OpenCode write-truncation contract to all large-file writer agents

Issue #214 / PR #598 fixed gsd-phase-researcher's OpenCode write-tool
truncation by adding a single-Write-default + sentinel-based
Write->Read->Edit incremental fallback contract to its Step 6. The root
cause is upstream opencode#18108: OUTPUT_TOKEN_MAX=32000 is shared with
the thinking budget, so a single oversized `write` tool call's JSON is
truncated mid-payload (`JSON Parse error: Expected '}'`) and OpenCode
doom-loops.

The same failure affects every GSD subagent that writes a large file in
one Write call. Mirror the phase-researcher write contract (adapted per
output filename) into the other large-file writers:

- gsd-research-synthesizer (SUMMARY.md) — extends the existing bug-222
  hard-rules block with the truncation fallback as rule 6, preserving
  every original rule
- gsd-planner (PLAN.md)
- gsd-executor (SUMMARY.md)
- gsd-domain-researcher (AI-SPEC.md Section 1b)
- gsd-project-researcher (.planning/research/*.md)
- gsd-ui-researcher (UI-SPEC.md)

Each keeps the single-Write default (no behavior change for Claude Code
and other non-truncating runtimes) and falls back to incremental,
sentinel-based section-by-section writes only on a truncation/invalid-tool
failure; never silently falls back to returning content.

Locked with a parametrized prompt-contract regression test mirroring the
bug-214 / bug-222 pattern across all six agents.

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

* chore(#214): set changeset pr to 599

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-01 22:31:37 -04:00
committed by GitHub
parent 82fb847754
commit a594f5175c
8 changed files with 170 additions and 0 deletions

View File

@@ -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 '}'`.

View File

@@ -98,6 +98,19 @@ If internal tooling with no regulated domain, "domain expert" = product owner or
<step name="write_section_1b">
**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 `<!-- gsd:write-continue -->`.
- `Read` the file, then `Edit` it, replacing `<!-- gsd:write-continue -->` 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

View File

@@ -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 `<!-- gsd:write-continue -->`.
- `Read` the file, then `Edit` it, replacing `<!-- gsd:write-continue -->` 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).

View File

@@ -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 `<structured_returns>`); 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 `<!-- gsd:write-continue -->`.
- `Read` the file, then `Edit` it, replacing `<!-- gsd:write-continue -->` 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`

View File

@@ -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 `<structured_returns>`); 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 `<!-- gsd:write-continue -->`.
- `Read` the file, then `Edit` it, replacing `<!-- gsd:write-continue -->` 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

View File

@@ -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 `<!-- gsd:write-continue -->`.
- `Read` the file, then `Edit` it, replacing `<!-- gsd:write-continue -->` 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

View File

@@ -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 `<structured_returns>`); 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 `<!-- gsd:write-continue -->`.
- `Read` the file, then `Edit` it, replacing `<!-- gsd:write-continue -->` 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

View File

@@ -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,
/<!-- gsd:write-continue -->/,
`${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.`
);
});
});
}
});