* 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:
5
.changeset/sturdy-writers-survive.md
Normal file
5
.changeset/sturdy-writers-survive.md
Normal 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 '}'`.
|
||||
@@ -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
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.`
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user