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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* chore(#214): add changeset for OpenCode write-truncation fix

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 21:42:08 -04:00
committed by GitHub
parent a11ba2dfcb
commit 82fb847754
4 changed files with 93 additions and 0 deletions

View File

@@ -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.

View File

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

View File

@@ -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 `<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.
**If CONTEXT.md exists, FIRST content section MUST be `<user_constraints>`:**
```markdown

View File

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