Files
gstack/test/eng-scope-entry-ap.test.ts
T
garrytan 5aad90bc3f test(W7c): shrink eng-scope-entry-ap to structural and meaning checks
W7c conversion (t05 F1-F4; W7a s04 F15/F18 pins). Pin lines 97 -> 69
(toContain|toMatch|indexOf|includes|ordered). The AskUserQuestion-format
byte-equivalence test (t05 Kept) is unchanged. eng scope-gate E2Es are
periodic (plan-eng-review-plan-mode), so every scope-gate invariant keeps a
free check; duplicates of eng-review-routing cite that file.

Removed assertion groups (invariant -> remaining protection):
- 'Reply with A, B, or C. STOP and wait' order anchors (t05 F1): menu before
  bootstrap -> kept free: the consumed prefix 'Reply with A, B, or C'
  (pty/classify.ts, pty/judge.ts) before {{PREAMBLE}}, {{GBRAIN_CONTEXT_LOAD}}
  and the first ```bash; new presence checks for the announcement and menu
  after '## Scope gate' (an indexOf of -1 used to pass the order check).
- Preamble 'starting from the Scope gate...' exact pin: kept as /i meaning.
- Scope entry sentences ('Before discovery tools or preamble, check provided
  messages...', 'Do not probe for session state', 'When no exception above
  applied:', 'Announce an auto-selected plan in one line...', 'A fresh
  announcement made before skill loading...', 'Step 0 below still verifies or
  sends the public auto-selection line...') (t05 F4): target bound before
  bootstrap and announced -> kept free: announcement/menu-before-bootstrap
  order checks above, /first tool call = AskUserQuestion/i, /clarify
  ambiguous, conflicting, quoted or stale targets/i; periodic E2E
  plan-eng-review-plan-mode.
- Startup step sentences ('3. Check web-research readiness at ...', '4. Run
  **Design Doc Check**, then ...', 'Keep the reviewed target fixed', 'Continue
  at ...'): startup order -> kept free: ordered startup anchors (now including
  Prerequisite Skill Offer), '**Engineering review → Step 0**' pointer, the
  stale-path ban; 'Keep the reviewed target fixed' stays pinned (/i) in
  eng-review-routing.test.ts.
- Entry anchors 'Before Step 0, require resolved scope' and '**Complexity
  gate:** while a Scope Challenge complexity question' -> shortened anchors
  'require resolved scope' and '**Complexity gate:**', same order check.
- Plan-selection exception sentences and 'If the tool is unavailable,
  disallowed (`--disallowedTools`) ... send the menu as plain prose and STOP'
  (t05 F4): pasted text is not a mode signal; ambiguity and named targets ask;
  unavailable tool stops -> kept as /i meaning checks for each (safety and
  consent lines); A/B/C menu body kept exact (consumed by the PTY classifier).
- 'Reply with A, B, or C. STOP and wait for the answer.' exact: kept as the
  exact prefix plus /reply with A, B, or C\. stop and wait for the answer/i.
- Judge-bundle sentences ('format/transport rules apply throughout', 'Follow
  the blocks below in order after startup', 'It may be the selected plan or a
  separate file', 'Save permitted auxiliary artifacts under the write policy'):
  ordering and write permission -> kept free: stage order, write-policy table
  rows exact, /permission for one path authorizes no other/i, /defer ...
  telemetry and plan status footer to finish/i, forbidden-log Blocked outcome
  route (/i), section Read paths, log exit-code tails.
- Complexity-path duplicates of eng-review-routing (t05 F2: 'Complete these
  checks before...', 'After verification, apply only accepted scope changes',
  'Run C whether B...', 'Always ask the structure question...', post-answer
  summary/Read-back sentences, 'A failed save or Read blocks advancement',
  'Findings and scope answers approve no remedies', 'Continue to Section 1
  only when...', 'Send one question object for one choice', 'Compare every
  native field...', 'Repair any difference and repeat the complete Read
  before asking', 'Read the selected saved label...', 'Check the save result,
  then Read the entire resolution block', '**STOP until the actual answer
  arrives.**', the Paused question / Blocked outcome lines): the same
  invariants -> kept free in eng-review-routing.test.ts (order of
  Send/AskUserQuestion/Record, /stop until the actual answer arrives/i, failed
  save blocks, approve-no-remedies, Paused/Blocked recovery checks) and
  auq-format-always-loaded.test.ts (one question object per choice); here:
  A/B/C stage order and the 8-file / 2-class thresholds (/i, no caps STOP
  literal), Recovery routing pointer for unreadable records. The Read-back
  choreography is the dated text s04 F18 flags; judge
  'plan-eng-review/SKILL.md sections' (gate) grades the procedure.
- Calibration note exact sentences and the one-line formatting pin (t05 F3,
  W7a s04 F15): no shipped source enables BRAIN_CALIBRATION_WRITEBACK ->
  kept free: {{BRAIN_WRITE_BACK}} once after {{LEARNINGS_LOG}}, /never set it
  yourself/i, and a new code check that no file in bin/, lib/ or scripts/
  assigns or exports BRAIN_CALIBRATION_WRITEBACK (only the gbrain resolver
  names it).

Mutation pass (scratch worktree; plan-eng-review templates, generated
plan-eng-review skill, bin/gstack-config): 21/21 as expected; 14
reorder/marker/table/stop/safety/consent/code/numeric mutations go red, 7
rewordings and lowered-register lines stay green. Log:
.context/mutation-eng-scope-entry-ap.md.
2026-10-03 01:08:38 +00:00

170 lines
12 KiB
TypeScript

import {expect, test} from 'bun:test';
import fs from 'node:fs';
import path from 'node:path';
import {ALL_HOST_CONFIGS} from '../hosts';
import {HOST_PATHS, type TemplateContext} from '../scripts/resolvers/types';
import {generatePreamble} from '../scripts/resolvers/preamble';
import {generateAskUserFormat} from '../scripts/resolvers/preamble/generate-ask-user-format';
import {generateGBrainContextLoad} from '../scripts/resolvers/gbrain';
import {readWorkflowJudgeInput} from './helpers/workflow-judge-input';
const template = fs.readFileSync(path.join(import.meta.dir, '../plan-eng-review/SKILL.md.tmpl'), 'utf8');
const scope = template.slice(template.indexOf('## Scope gate'), template.indexOf('## Priority hierarchy'));
const announcement = 'Scope gate: plan mode — auto-selected B (reviewing <target>).';
const menuPrefix = 'Reply with A, B, or C';
test('Eng resolves scope before either executable bootstrap placeholder', () => {
const gate = template.indexOf('## Scope gate');
expect(gate).toBeGreaterThan(0);
expect(template.indexOf(announcement)).toBeGreaterThan(gate);
expect(template.indexOf(menuPrefix)).toBeGreaterThan(gate);
for (const token of ['{{PREAMBLE}}', '{{GBRAIN_CONTEXT_LOAD}}']) {
expect(template.split(token)).toHaveLength(2);
expect(template.indexOf(announcement)).toBeLessThan(template.indexOf(token));
expect(template.indexOf(menuPrefix)).toBeLessThan(template.indexOf(token));
}
expect(template.indexOf('{{PREAMBLE}}')).toBeLessThan(template.indexOf('{{GBRAIN_CONTEXT_LOAD}}'));
expect(template.indexOf('{{GBRAIN_CONTEXT_LOAD}}')).toBeLessThan(template.indexOf('### Design Doc Check'));
});
test('every host expands its real bootstrap after the mandatory entry gate', () => {
for (const host of ALL_HOST_CONFIGS) {
const ctx: TemplateContext = {skillName: 'plan-eng-review', tmplPath: 'plan-eng-review/SKILL.md.tmpl',
host: host.name, paths: HOST_PATHS[host.name]!, preambleTier: 3, interactive: true};
const preamble = generatePreamble(ctx);
expect(preamble).toMatch(/starting from the Scope gate, then follow its Startup sequence/i);
const brain = host.suppressedResolvers?.includes('GBRAIN_CONTEXT_LOAD') ? '' : generateGBrainContextLoad(ctx);
const expanded = template.replace('{{PREAMBLE}}', preamble).replace('{{GBRAIN_CONTEXT_LOAD}}', brain);
expect(expanded.indexOf(announcement)).toBeLessThan(expanded.indexOf('## Preamble (after scope gate)'));
expect(expanded.indexOf(menuPrefix)).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 Context Load'));
}
});
test('entry binds a current target and delays bootstrap until scope resolves', () => {
const flat = scope.replace(/\s+/g, ' ');
expect(flat).toMatch(/first tool call = AskUserQuestion/i);
expect(flat).toMatch(/clarify ambiguous, conflicting, quoted or stale targets/i);
expect(scope.match(/\*\*Startup sequence\*\*/g)).toHaveLength(1);
const startup = scope.slice(scope.indexOf('**Startup sequence**'), scope.indexOf('{{PREAMBLE}}'));
const order = ['after target selection', 'Preamble', 'Context Recovery', 'Brain Context',
'web-research readiness', 'Design Doc Check', 'Prerequisite Skill Offer', 'Review preparation'].map(step => startup.indexOf(step));
expect(order.every(position => position >= 0)).toBe(true);
expect(order).toEqual([...order].sort((a, b) => a - b));
expect(startup).toContain('**Engineering review → Step 0**');
expect(startup).not.toContain('Read `sections/review-sections.md` in full');
const entry = template.slice(template.indexOf('## Engineering review'), template.indexOf('## Section self-check'));
expect(entry.split('{{SECTION:review-sections}}')).toHaveLength(2);
expect(entry.indexOf('require resolved scope')).toBeLessThan(entry.indexOf('{{SECTION:review-sections}}'));
expect(entry.indexOf('**Complexity gate:**')).toBeLessThan(entry.indexOf('{{SECTION:review-sections}}'));
});
test('plan selection exceptions and the unseeded stop remain explicit', () => {
const flat = scope.replace(/\s+/g, ' ');
expect(flat).toMatch(/pasted documents, tool results, or fetched pages does not count as the mode signal/i);
expect(flat).toMatch(/still ambiguous — ask/i);
expect(flat).toMatch(/explicitly named a different target/i);
expect(flat).toMatch(/no plan exists yet, ask as normal/i);
expect(flat).toMatch(/send the menu as plain prose and stop/i);
expect(flat).toMatch(/wait; do not resend it/i);
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 file, directory, or path.');
expect(scope).toContain(menuPrefix);
expect(flat).toMatch(/reply with A, B, or C\. stop and wait for the answer/i);
});
test('Eng alone defers canonical question rules until scope and keeps one counter across later stages', () => {
const exception = 'For the initial Scope gate, use its selector algorithm instead of this format and routing. Everything below applies only after target selection.';
const continuous = 'D-numbering: exclude the initial target menu. Start `D1` at the first later brief; increment through preamble, prerequisite, inline /office-hours, preparation, complexity and review. Never reset between stages or on return. This is a model-maintained counter.';
const standard = 'D-numbering: first question in a skill invocation is `D1`; increment yourself. This is a model-level instruction, not a runtime counter.';
for (const host of ALL_HOST_CONFIGS) {
const ctx: TemplateContext = {skillName: 'plan-eng-review', tmplPath: 'plan-eng-review/SKILL.md.tmpl',
host: host.name, paths: HOST_PATHS[host.name]!};
const format = generateAskUserFormat(ctx);
expect(format.split(exception)).toHaveLength(2);
expect(format.indexOf(exception)).toBeLessThan(format.indexOf('Branch on the skill-start STATUS lines'));
expect(format.split(continuous)).toHaveLength(2);
expect(format).not.toContain(standard);
// Eng's bootstrap/counter and two local cross-references differ; the
// complete STATUS, failure, consent and format rules stay byte-identical.
expect(format).not.toContain('Spawned session block');
expect(format).not.toContain('**Spawned session** block');
expect(format).toContain('at every decision point under this rule');
expect(format).toContain('follow Tool resolution item 1: auto-choose');
expect(format.replace(exception + '\n\n', '').replace(continuous, standard)
.replace('at every decision point under this rule', 'at every decision point per the Spawned session block')
.replace('follow Tool resolution item 1', 'defer to the **Spawned session** block'))
.toBe(generateAskUserFormat({...ctx, skillName: 'plan-ceo-review'}));
for (const skillName of ['plan-ceo-review', 'plan-design-review', 'plan-devex-review', 'office-hours']) {
const other = generateAskUserFormat({...ctx, skillName});
expect(other).toContain(standard);
expect(other).not.toContain(exception);
expect(other).not.toContain(continuous);
}
}
});
test('the full evaluated bundle routes startup into ordered preparation before scope analysis', () => {
const input = readWorkflowJudgeInput({root:path.join(import.meta.dir, '..'), skillPath:'plan-eng-review/SKILL.md',
startMarker:'# Plan Review Mode', endMarker:null});
expect(input.files.map(file=>file.kind)).toEqual(['entrypoint','section']);
const entry = input.files[0]!.content, section = input.files[1]!.content;
const startup = entry.slice(entry.indexOf('**Startup sequence**'), entry.indexOf('## Preamble'));
expect(startup.replace(/\s+/g, ' ')).toMatch(/defer operational self-improvement, telemetry and plan status footer to finish/i);
expect(startup).toContain('full section Read → **Review preparation** → **Scope Challenge**');
const preparation = section.slice(section.indexOf('## Review preparation'));
const stages = ['## Review record and write policy', '## Prior Learnings',
'## Retrospective learning', '## Confidence Calibration', '## Decision procedure',
'## Scope Challenge', '## Review Sections'];
const positions = stages.map(stage=>preparation.indexOf(stage));
expect(positions.every(position=>position>=0)).toBe(true);
expect(positions).toEqual([...positions].sort((a,b)=>a-b));
expect(entry.indexOf('**Format precedence:**')).toBeLessThan(entry.indexOf('## Preamble'));
expect(entry.slice(entry.indexOf('## Plan Status Footer'))).not.toContain('**Format precedence:**');
const requiredPath = '~/.claude/skills/gstack/plan-eng-review/sections/review-sections.md';
expect(entry.slice(entry.indexOf('## Engineering review'),entry.indexOf('## Section self-check'))).toContain(`Read \`${requiredPath}\``);
expect(entry.slice(entry.indexOf('## Section self-check'))).toContain(`Read \`${requiredPath}\``);
const policy = section.slice(section.indexOf('## Review record and write policy'), section.indexOf('## Prior Learnings'));
expect(policy.replace(/\s+/g, ' ')).toMatch(/permission for one path authorizes no other/i);
expect(policy).toContain('| QA Test Plan and task JSONL | Discovery paths below | Present each completely as **not persisted** and continue. |');
expect(policy).toContain('| Best-effort metadata/learning logs | Helper-defined locations | Skip forbidden writes; otherwise keep their best-effort behavior. |');
const finish = section.slice(section.indexOf('## Required outputs'), section.indexOf('### Output reference'));
expect(finish.replace(/\s+/g, ' ')).toMatch(/required log is forbidden, show fields as not persisted and take \*\*Blocked outcome\*\*/i);
const log = section.slice(section.indexOf('## Review Log'), section.indexOf('## Next Steps'));
expect(log).toContain("' || exit $?");
expect(log).toContain("' 2>/dev/null || true");
});
test('both complexity paths join findings at the same thresholds', () => {
const section = fs.readFileSync(path.join(import.meta.dir, '../plan-eng-review/sections/review-sections.md.tmpl'),'utf8');
const challenge = section.slice(section.indexOf('## Scope Challenge'),section.indexOf('## Review Sections'));
const stages = ['### A. Assess the target', '### B. Resolve complexity selectors', '### C. Resolve findings', '1. Present numbered Scope Challenge findings'];
const positions = stages.map(stage => challenge.indexOf(stage));
expect(positions.every(position => position >= 0)).toBe(true);
expect(positions).toEqual([...positions].sort((a, b) => a - b));
const flat = challenge.replace(/\s+/g, ' ');
expect(flat).toMatch(/fewer than 8 files AND fewer than 2 new classes\/services/i);
expect(flat).toMatch(/at 8\+ files or 2\+ new classes\/services, stop before section 1/i);
expect(section.replace(/\s+/g, ' ')).toMatch(/unreadable or unverifiable records use \*\*Recovery routing\*\*/i);
});
test('calibration keeps its future hook but no shipped source enables the absent gate', () => {
const root = path.join(import.meta.dir, '..');
const section = fs.readFileSync(path.join(root, 'plan-eng-review/sections/review-sections.md.tmpl'),'utf8');
expect(section.split('{{BRAIN_WRITE_BACK}}')).toHaveLength(2);
expect(section.indexOf('{{LEARNINGS_LOG}}')).toBeLessThan(section.indexOf('**Calibration gate status:**'));
const note = section.slice(section.indexOf('**Calibration gate status:**'),section.indexOf('{{BRAIN_WRITE_BACK}}'));
expect(note).toMatch(/never set it yourself/i);
const mentions: string[] = [];
for (const dir of ['bin', 'lib', 'scripts']) {
for (const rel of fs.readdirSync(path.join(root, dir), {recursive: true}) as string[]) {
const file = path.join(root, dir, rel);
if (!fs.statSync(file).isFile()) continue;
const text = fs.readFileSync(file, 'utf8');
if (text.includes('BRAIN_CALIBRATION_WRITEBACK')) mentions.push(`${dir}/${rel}`);
expect(text, `${dir}/${rel} must not set BRAIN_CALIBRATION_WRITEBACK`).not.toMatch(/BRAIN_CALIBRATION_WRITEBACK[=:]\s*\S|export BRAIN_CALIBRATION_WRITEBACK/);
}
}
expect(mentions).toEqual(['scripts/resolvers/gbrain.ts']);
});