From 82fb8477549616af2f1e0f9da5d1a04472cef543 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Mon, 1 Jun 2026 21:42:08 -0400 Subject: [PATCH] fix(#214): make gsd-phase-researcher survive OpenCode write-tool truncation (#598) * chore: wire docs/agents config into AGENTS.md Agent skills section Add the `## Agent skills` discovery block pointing the engineering skills at the existing docs/agents/{issue-tracker,triage-labels,domain}.md files (issue tracker, triage label mapping, single-context domain docs). Co-Authored-By: Claude Opus 4.8 * fix(#214): make gsd-phase-researcher survive OpenCode write-tool truncation OpenCode caps model output at OUTPUT_TOKEN_MAX=32000 and the thinking budget shares that pool (upstream opencode#18108). A single oversized `write` tool call for RESEARCH.md is truncated mid-payload, yielding `JSON Parse error: Expected '}'`, which OpenCode misclassifies and then doom-loops retrying identically. Short content writes fine; long content fails 100% (reproducible, OpenCode 1.15.10). Add a Step 6 write contract to agents/gsd-phase-researcher.md: keep the single-Write default (no behavior change for Claude Code and other runtimes that don't truncate), but on a truncation/invalid-tool failure build the file incrementally via a sentinel-based Write -> Read -> Edit sequence so no single tool-call payload is large enough to truncate; never silently fall back to returning content (which truncates identically). This is the upstream-recommended mitigation (write in smaller chunks; use edit for follow-on writes). Locked with a prompt-contract regression test mirroring the bug-222 write-contract pattern. Co-Authored-By: Claude Opus 4.8 * chore(#214): add changeset for OpenCode write-truncation fix Co-Authored-By: Claude Opus 4.8 --------- Co-authored-by: Claude Opus 4.8 --- .changeset/graceful-quails-hop.md | 5 ++ AGENTS.md | 14 +++++ agents/gsd-phase-researcher.md | 13 ++++ ...earcher-write-truncation-contract.test.cjs | 61 +++++++++++++++++++ 4 files changed, 93 insertions(+) create mode 100644 .changeset/graceful-quails-hop.md create mode 100644 tests/bug-214-phase-researcher-write-truncation-contract.test.cjs diff --git a/.changeset/graceful-quails-hop.md b/.changeset/graceful-quails-hop.md new file mode 100644 index 000000000..19d8ebae0 --- /dev/null +++ b/.changeset/graceful-quails-hop.md @@ -0,0 +1,5 @@ +--- +type: Fixed +pr: 598 +--- +**`gsd-phase-researcher` no longer fails to write `RESEARCH.md` on OpenCode** — large research files that previously hit `JSON parsing failed: Expected '}'` and a retry doom-loop now write reliably. The agent keeps its single-write default and falls back to incremental section-by-section writes only when a runtime truncates an oversized tool call. diff --git a/AGENTS.md b/AGENTS.md index f3cc2ed7d..7b1827085 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -39,3 +39,17 @@ Every PR must link an approved or confirmed issue with `Closes #123`, `Fixes #12 ## Security & Configuration Tips Do not commit secrets, local config, or generated worktree artifacts. Before release-facing changes, run the relevant scan scripts in `scripts/`, especially `secret-scan.sh`, `base64-scan.sh`, and `prompt-injection-scan.sh`. + +## Agent skills + +### Issue tracker + +Issues live in GitHub Issues at `open-gsd/gsd-core` (via the `gh` CLI, always with `--repo open-gsd/gsd-core`). See `docs/agents/issue-tracker.md`. + +### Triage labels + +Five canonical triage roles mapped to this repo's labels — `needs-info`→`needs-reproduction`, `ready-for-agent`→`confirmed`, `ready-for-human`→`approved-enhancement`/`approved-feature`, others default. See `docs/agents/triage-labels.md`. + +### Domain docs + +Single-context — `CONTEXT.md` (domain glossary + recurring PR rules) and `docs/adr/` at the repo root. See `docs/agents/domain.md`. diff --git a/agents/gsd-phase-researcher.md b/agents/gsd-phase-researcher.md index 102a61521..c7ffa759d 100644 --- a/agents/gsd-phase-researcher.md +++ b/agents/gsd-phase-researcher.md @@ -804,6 +804,19 @@ List missing test files, framework config, or shared fixtures needed before impl Use the Write tool to create files — never use `Bash(cat << 'EOF')` or heredoc commands for file creation. This rule applies regardless of `commit_docs` setting. +**Write contract (hard rules — must follow):** + +This file is the canonical output of this agent. The orchestrator reads `$PHASE_DIR/$PADDED_PHASE-RESEARCH.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 RESEARCH.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. + **If CONTEXT.md exists, FIRST content section MUST be ``:** ```markdown diff --git a/tests/bug-214-phase-researcher-write-truncation-contract.test.cjs b/tests/bug-214-phase-researcher-write-truncation-contract.test.cjs new file mode 100644 index 000000000..4d3f26279 --- /dev/null +++ b/tests/bug-214-phase-researcher-write-truncation-contract.test.cjs @@ -0,0 +1,61 @@ +// 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, '..'); +const RESEARCHER_PATH = path.join(REPO_ROOT, 'agents', 'gsd-phase-researcher.md'); + +function readResearcherPrompt() { + return fs.readFileSync(RESEARCHER_PATH, 'utf8'); +} + +describe('bug #214: phase researcher must survive OpenCode write-tool truncation', () => { + test('Step 6 documents the large-file / truncation fallback write contract', () => { + const prompt = readResearcherPrompt(); + + assert.match( + prompt, + /truncat/i, + 'Step 6 must name the truncation failure mode.' + ); + assert.match( + prompt, + /incrementa/i, + 'Step 6 must instruct incremental construction on large files.' + ); + assert.match( + prompt, + //, + 'Step 6 must define the continuation sentinel for incremental writes.' + ); + assert.match( + prompt, + /do NOT retry the same oversized call/i, + 'Step 6 must forbid identical retry of the oversized write (doom-loop guard).' + ); + assert.match( + prompt, + /do NOT silently fall back to returning content/i, + 'Step 6 must forbid silent fallback to returning content.' + ); + assert.match( + prompt, + /`Read` the file, then `Edit`/i, + 'Step 6 must require Read before Edit (OpenCode edit requires a prior Read).' + ); + assert.match( + prompt, + /no trailing sentinel/i, + 'Step 6 must instruct removing the sentinel on the final section.' + ); + assert.match( + prompt, + /write the whole file in a single `Write` call/i, + 'Step 6 must keep single-Write as the default path (no regression for non-truncating runtimes).' + ); + }); +});