From c3e667df34a5ae18a644ccf800b7bc219825f7f6 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Fri, 28 Aug 2026 12:31:02 -0400 Subject: [PATCH] fix(#3786): authorize mutable scope from live observation only in the quick planner (#4005) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * test(#3786): the quick planner constraints must carry a mutable-scope authority rule * fix(#3786): authorize mutable scope from live observation only in the quick planner The quick planner could commit HISTORICAL scope as authorized edit or verification scope before any live observation of the mutable state: a minimized probe used cached PR-diff paths (65 of them, "pending replacement") or broadened verification to the PR integration surface in 2 of 3 trials (#3786). One explicit authority requirement reduced that to 0 of 3. The new planner constraint authorizes mutable-state scope ONLY from a live observation made at planning time (for conflict resolution, the fresh merge index via git diff --name-only --diff-filter=U) or keeps files/verify CONDITIONAL on it; historical STATE.md entries, recovery notes, and cached PR/base diffs may guide investigation only. A structural guard pins the rule in the shipped constraints block. Emitted-Drift-Ack-Growth: quick.md — #3786: +620 bytes — one MUTABLE-SCOPE AUTHORITY constraint bullet added to the planner block * chore(#3786): changeset fragment (pr number backfilled after PR creation) * chore(#3786): backfill changeset PR number (4005) --------- Co-authored-by: sim --- .changeset/rapid-wasps-sing.md | 5 +++ gsd-core/workflows/quick.md | 1 + tests/quick-planner-scope-guard.test.cjs | 56 ++++++++++++++++++++++++ 3 files changed, 62 insertions(+) create mode 100644 .changeset/rapid-wasps-sing.md create mode 100644 tests/quick-planner-scope-guard.test.cjs 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', + ); +});