diff --git a/.changeset/rapid-wasps-sing.md b/.changeset/rapid-wasps-sing.md new file mode 100644 index 000000000..9e40f938b --- /dev/null +++ b/.changeset/rapid-wasps-sing.md @@ -0,0 +1,5 @@ +--- +type: Fixed +pr: 4005 +--- +**`/gsd-quick` no longer authorizes edit/verification scope from historical state** — when scope depends on mutable external state (a fresh merge index, PR diffs, the working tree), the planner must observe it live or keep the plan's scope conditional; cached PR-diff paths and stale recovery notes are investigation guidance only, so a merge-conflict task can no longer provisionally "authorize" 65 historical paths. (#3786) diff --git a/gsd-core/workflows/quick.md b/gsd-core/workflows/quick.md index 4a4c1ae58..8514092f1 100644 --- a/gsd-core/workflows/quick.md +++ b/gsd-core/workflows/quick.md @@ -315,6 +315,7 @@ ${AGENT_SKILLS_PLANNER} - Create a SINGLE plan with 1-3 focused tasks - Quick tasks should be atomic and self-contained +- MUTABLE-SCOPE AUTHORITY (#3786): when concrete edit or verification scope depends on mutable external state (a merge index, PR/base diffs, the working tree), authorize scope ONLY from a live observation made at planning time — for conflict resolution that is the fresh merge index via `git diff --name-only --diff-filter=U` — or keep `files`/`verify` CONDITIONAL on that observation. Historical STATE.md entries, recovery notes, and cached PR/base diff paths may guide investigation only; they are never edit or verification authority, and a plan must not enumerate them as authorized files "pending replacement". ${RESEARCH_MODE ? '- Research findings are available — use them to inform library/pattern choices' : '- No research phase'} ${VALIDATE_MODE ? '- Target ~40% context usage (structured for verification)' : '- Target ~30% context usage (simple, focused)'} ${VALIDATE_MODE ? '- MUST generate `must_haves` in plan frontmatter (truths, artifacts, key_links)' : ''} diff --git a/tests/quick-planner-scope-guard.test.cjs b/tests/quick-planner-scope-guard.test.cjs new file mode 100644 index 000000000..6f321f3bd --- /dev/null +++ b/tests/quick-planner-scope-guard.test.cjs @@ -0,0 +1,56 @@ +'use strict'; + +// ───────────────────────────────────────────────────────────────────────────── +// #3786 — the /gsd-quick planner constraints must carry a mutable-scope +// AUTHORITY rule. +// +// A minimized planner probe committed HISTORICAL scope as authorized edit / +// verification scope in 2 of 3 trials: for a merge-conflict task whose fresh +// merge had not run, one trial provisionally authorized 65 cached PR-diff +// paths, another broadened verification to the whole PR integration surface. +// Adding one explicit requirement — authorized scope comes only from the +// fresh merge index — reduced failures to 0 of 3. The rule lives in the +// shipped planner prompt (quick.md's block), so a structural +// guard over that text pins it. +// ───────────────────────────────────────────────────────────────────────────── + +const { test } = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); + +const QUICK_MD = path.join(__dirname, '..', 'gsd-core', 'workflows', 'quick.md'); + +// quick.md is shipped workflow text — the bytes ARE what the runtime loads, +// so a structural scan over it tests the deployed contract (same shape as +// tests/config-get-raw-guard.test.cjs; no allow-test-rule marker needed for +// .md reads). +function plannerConstraints() { + const md = fs.readFileSync(QUICK_MD, 'utf-8'); + // quick.md carries TWO blocks (planner at ~315, executor at + // ~479). Anchor to the PLANNER's: the last block opening before its + // subagent_type declaration — step reordering can never silently redirect + // the guard to another agent's block. + const plannerDispatch = md.indexOf('subagent_type="gsd-planner"'); + assert.ok(plannerDispatch > 0, 'quick.md must dispatch the gsd-planner agent'); + const start = md.lastIndexOf('', plannerDispatch); + const end = md.indexOf('', start); + assert.ok(start > 0 && end > start, 'quick.md must contain the planner block'); + return md.slice(start, end); +} + +test('#3786: the quick planner constraints carry a mutable-scope authority rule', () => { + const constraints = plannerConstraints(); + assert.ok( + /live observation/i.test(constraints) && /conditional/i.test(constraints), + '#3786: scope derived from mutable external state must be live-observed or kept conditional', + ); + assert.ok( + /may guide investigation only/i.test(constraints), + '#3786: historical STATE.md/recovery/cached-diff paths must be labeled investigation-only, never edit/verification authority', + ); + assert.ok( + constraints.includes('git diff --name-only --diff-filter=U'), + '#3786: the conflict-resolution case must name the fresh-merge-index command the probe validated', + ); +});