* 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>
97 lines
3.1 KiB
JavaScript
97 lines
3.1 KiB
JavaScript
// 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.`
|
|
);
|
|
});
|
|
});
|
|
}
|
|
});
|