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).' + ); + }); +});