feat(question-pref): runtime AUTO_DECIDE carve-out for *-split-* ids

Split chains (per-option AskUserQuestion calls emitted by the new
"Handling 5+ options" rule) must never be silently auto-approved
via /plan-tune preferences. The user's option set is sacred.

Layer 1 (mechanism): unique <skill>-split-<option-slug> ids prevent
cross-option preference leakage. Layer 2 (this commit): the runtime
checker `gstack-question-preference --check` detects any id matching
*-split-* and forces ASK_NORMALLY even when never-ask or
ask-only-for-one-way preferences exist for that exact id. An
explanatory note tells the user their preference was bypassed and why.

7 tests pin the carve-out: no-pref baseline, never-ask override,
explanatory note text, ask-only-for-one-way override, always-ask
(no note), non-split id containing "split" word (negative case for
regex specificity), multi-skill split id formats.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-05-26 22:27:51 -07:00
parent f2e2ef15d9
commit 975312ef3f
2 changed files with 74 additions and 0 deletions
+59
View File
@@ -103,6 +103,65 @@ describe('--check with preferences set', () => {
});
});
// Split-chain carve-out: question_ids matching <skill>-split-<option-slug>
// must always ASK_NORMALLY regardless of stored preferences.
// See scripts/resolvers/preamble/generate-ask-user-format.ts
// "Handling 5+ options — split, never drop" for the surrounding mechanism.
describe('--check split-chain carve-out (*-split-* always ASK_NORMALLY)', () => {
function setPref(id: string, pref: string) {
return run('--write', JSON.stringify({ question_id: id, preference: pref, source: 'plan-tune' }));
}
test('split-id without preference → ASK_NORMALLY', () => {
const r = run('--check', 'plan-ceo-review-split-e4-detect-mappings');
expect(r.stdout.trim()).toContain('ASK_NORMALLY');
});
test('split-id + never-ask → ASK_NORMALLY (carve-out overrides preference)', () => {
setPref('plan-ceo-review-split-e4-detect-mappings', 'never-ask');
const r = run('--check', 'plan-ceo-review-split-e4-detect-mappings');
expect(r.stdout).toContain('ASK_NORMALLY');
expect(r.stdout).not.toContain('AUTO_DECIDE');
});
test('split-id + never-ask → emits explanatory note', () => {
setPref('plan-ceo-review-split-e4-detect-mappings', 'never-ask');
const r = run('--check', 'plan-ceo-review-split-e4-detect-mappings');
expect(r.stdout).toContain('split-chain per-option calls always ASK_NORMALLY');
expect(r.stdout).toContain('never-ask');
});
test('split-id + ask-only-for-one-way → ASK_NORMALLY (carve-out overrides preference)', () => {
setPref('ship-split-version-bump', 'ask-only-for-one-way');
const r = run('--check', 'ship-split-version-bump');
expect(r.stdout).toContain('ASK_NORMALLY');
expect(r.stdout).not.toContain('AUTO_DECIDE');
});
test('split-id + always-ask → ASK_NORMALLY (no note since preference agrees)', () => {
setPref('plan-eng-review-split-add-test', 'always-ask');
const r = run('--check', 'plan-eng-review-split-add-test');
expect(r.stdout.trim()).toContain('ASK_NORMALLY');
expect(r.stdout).not.toContain('does not apply');
});
test('non-split id that just happens to contain "split" word is NOT carved out', () => {
// The carve-out matches `-split-` (kebab-cased), not the substring "split".
// A question id like `qa-splitscreen-test` (hypothetical) would not match.
// Verify by using a never-ask pref that should fire AUTO_DECIDE.
setPref('qa-splitscreen-test', 'never-ask');
const r = run('--check', 'qa-splitscreen-test');
expect(r.stdout.trim()).toContain('AUTO_DECIDE');
});
test('multiple split-id formats: skill-split-anything matches', () => {
setPref('autoplan-split-ceo-finding-7', 'never-ask');
const r = run('--check', 'autoplan-split-ceo-finding-7');
expect(r.stdout).toContain('ASK_NORMALLY');
expect(r.stdout).not.toContain('AUTO_DECIDE');
});
});
// -----------------------------------------------------------------------
// --write
// -----------------------------------------------------------------------