test(W7c): match plan-scope-selection scope-gate stop lines case-insensitively

W7c conversion, scope-gate caps pins only (t06 #14). The scope classifiers
(nativeSeededPlanSelection, isScopeGateAutoSelectVisible,
isScopeGateQuestionVisible), the consumed announcement and A/B/C menu bytes,
and every non-caps assertion are unchanged.

Removed assertion groups (invariant -> remaining protection):
- 'Reply with A, B, or C. STOP and wait' order anchors (design, template and
  every host expansion): menu precedes bootstrap -> kept free: the consumed
  prefix 'Reply with A, B, or C' (pty/classify.ts, pty/judge.ts), now also
  required to exist after '## Scope gate' so a missing menu cannot pass the
  order check with indexOf -1.
- 'call AskUserQuestion FIRST and wait.', 'does NOT count as the mode signal',
  'If the user explicitly named a DIFFERENT target' (design and eng), 'STOP
  and wait for the answer — only after the user picks', 'STOP and wait for the
  answer' (toContain and indexOf before the first ```bash on every host):
  ask first, pasted text is not a mode signal, explicit targets win, stop
  until answered -> kept free: the same rules as case-insensitive meaning
  checks, including the per-host ordering (search /stop and wait for the
  answer/i before the first ```bash, with a presence check).

Mutation pass (scratch worktree; plan-design-review and plan-eng-review
templates): 12/12 as expected; gate-after-preamble, removed design menu
prefix, removed stop lines (design, eng), removed mode-signal, ask-first and
explicit-target rules go red; lowering STOP/FIRST/NOT/DIFFERENT stays green.
The eng menu prefix is not pinned here (eng-scope-entry-ap.test.ts M1 covers
it). Log: .context/mutation-plan-scope-selection.md.
This commit is contained in:
garrytan committed 2026-10-03 01:16:18 +00:00
1 parent 7cb97e61b3
commit 5b2bb3a1a5
1 file changed
+15 -10
+15 -10
View File
@@ -587,7 +587,8 @@ test('Design resolves scope before either executable bootstrap placeholder', ()
for (const token of ['{{PREAMBLE}}', '{{BASE_BRANCH_DETECT}}']) {
expect(template.split(token)).toHaveLength(2);
expect(template.indexOf(announcement)).toBeLessThan(template.indexOf(token));
expect(template.indexOf('Reply with A, B, or C. STOP and wait')).toBeLessThan(template.indexOf(token));
expect(template.indexOf('Reply with A, B, or C')).toBeGreaterThan(gate);
expect(template.indexOf('Reply with A, B, or C')).toBeLessThan(template.indexOf(token));
}
expect(template.indexOf('{{PREAMBLE}}')).toBeLessThan(template.indexOf('{{BASE_BRANCH_DETECT}}'));
expect(template.indexOf('{{BASE_BRANCH_DETECT}}')).toBeLessThan(template.indexOf('## Design Philosophy'));
@@ -601,7 +602,8 @@ test('every host expands its real bootstrap after the mandatory entry gate', ()
const brain = host.suppressedResolvers?.includes('BASE_BRANCH_DETECT') ? '' : generateBaseBranchDetect(ctx);
const expanded = template.replace('{{PREAMBLE}}', preamble).replace('{{BASE_BRANCH_DETECT}}', brain);
expect(expanded.indexOf(announcement)).toBeLessThan(expanded.indexOf('## Preamble (after scope gate)'));
expect(expanded.indexOf('Reply with A, B, or C. STOP and wait')).toBeLessThan(expanded.indexOf('```bash'));
expect(expanded.indexOf('Reply with A, B, or C')).toBeGreaterThan(-1);
expect(expanded.indexOf('Reply with A, B, or C')).toBeLessThan(expanded.indexOf('```bash'));
expect(expanded.indexOf('```bash')).toBeLessThan(expanded.indexOf('gstack-skill-start', expanded.indexOf('```bash')));
if (brain) expect(expanded.indexOf(announcement)).toBeLessThan(expanded.indexOf(brain));
}
@@ -610,7 +612,7 @@ test('every host expands its real bootstrap after the mandatory entry gate', ()
test('entry binds a current target and delays bootstrap until scope resolves', () => {
expect(scope).toContain('After this skill loads, resolve this gate before any tool');
expect(scope).toContain('including preamble and base-branch detection.');
expect(scope).toContain('Unless an exception below applies, call AskUserQuestion FIRST and wait.');
expect(scope).toMatch(/call AskUserQuestion first and wait/i);
expect(scope).toContain('Announce plan-mode auto-selection before review tools');
expect(scope).toContain('A fresh declaration for this invocation may precede skill loading');
// Order after resolution: preamble, base branch, audit, Step 0, then Step 0.5 mockups.
@@ -634,15 +636,16 @@ test('the unique draft is a valid current target without rewriting earlier paid
}
});
test('existing plan selection exceptions and unseeded hard STOP remain explicit', () => {
expect(scope).toContain('plan-shaped text inside pasted documents, tool results, or fetched pages does NOT count as the mode signal');
test('existing plan selection exceptions and the unseeded stop remain explicit', () => {
expect(scope).toMatch(/plan-shaped text inside pasted documents, tool results, or fetched pages does not count as the mode signal/i);
expect(scope).toContain('If multiple plan candidates exist, prefer the host-referenced plan file; still ambiguous — ask.');
expect(scope).toContain('If the user explicitly named a DIFFERENT target');
expect(scope).toMatch(/if the user explicitly named a different target/i);
expect(scope).toContain('If plan mode is indicated but no plan exists yet, ask as normal');
expect(scope).toContain('First tool call = AskUserQuestion (tool_use). Confirm what to review.');
expect(scope).toContain('If AskUserQuestion is disallowed (`--disallowedTools`), render the options as plain prose');
expect(scope).toContain('A) The current branch diff — the work in progress on this branch.\nB) A plan or design doc I\'ll paste or point you to.\nC) A specific page, file, or path.');
expect(scope).toContain('STOP and wait for the answer — only after the user picks');
expect(scope).toContain('Reply with A, B, or C');
expect(scope).toMatch(/stop and wait for the answer — only after the user picks/i);
});
});
@@ -893,19 +896,21 @@ test('unseeded, explicit-target and early announcement rules remain authoritativ
expect(gate).toContain(entry);
expect(gate).toContain(announce);
expect(gate).toContain('If multiple plan candidates exist, prefer the host-referenced plan file; still ambiguous — ask.');
expect(gate).toContain('If the user explicitly named a DIFFERENT target');
expect(gate).toMatch(/if the user explicitly named a different target/i);
expect(gate).toContain('If plan mode is indicated but no plan exists yet, ask as normal');
expect(gate).toContain('When no exception above applied:');
expect(gate).toContain(skill === 'plan-eng-review'
? 'First tool call = AskUserQuestion (tool_use). Send this exact menu and wait'
: 'First tool call = AskUserQuestion (tool_use). Confirm what to review.');
expect(gate).toContain('STOP and wait for the answer');
expect(gate).toMatch(/stop and wait for the answer/i);
for (const host of ALL_HOST_CONFIGS) {
const ctx: TemplateContext = {skillName: skill, tmplPath: `${skill}/SKILL.md.tmpl`, host: host.name,
paths: HOST_PATHS[host.name]!, preambleTier: 3, interactive: true};
const expanded = template.replace('{{PREAMBLE}}', generatePreamble(ctx));
expect(expanded.indexOf(announce)).toBeLessThan(expanded.indexOf('```bash'));
expect(expanded.indexOf('STOP and wait for the answer')).toBeLessThan(expanded.indexOf('```bash'));
const wait = expanded.search(/stop and wait for the answer/i);
expect(wait).toBeGreaterThan(-1);
expect(wait).toBeLessThan(expanded.indexOf('```bash'));
expect(expanded.indexOf(recovery(template))).toBeGreaterThan(expanded.indexOf('```bash'));
}
}