mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-06 17:04:27 +01:00
fix: honor review preferences and recognize native eval evidence
This commit is contained in:
1 parent
c73102357c
commit
fd7698b23d
21 files changed
+1306
-28
No files matched your search
@@ -934,7 +934,7 @@ Present these approach options via AskUserQuestion using the preamble's AskUserQ
|
||||
After 0C-bis, before 0D; keep labels stable.
|
||||
Every mode requires your explicit approval for added scope.
|
||||
|
||||
Present four options:
|
||||
The four modes are:
|
||||
1. **SCOPE EXPANSION:** The plan is good but could be great. Dream big — propose the ambitious version. Every expansion is presented individually for your approval. You opt in to each one.
|
||||
2. **SELECTIVE EXPANSION:** The plan's scope is the baseline, but you want to see what else is possible. Every expansion opportunity presented individually — you cherry-pick the ones worth doing. Neutral recommendations.
|
||||
3. **HOLD SCOPE:** The plan's scope is right. Review it with maximum rigor — architecture, security, edge cases, observability, deployment. Make it bulletproof. No expansions surfaced.
|
||||
@@ -949,13 +949,15 @@ Context-dependent defaults:
|
||||
* User says "go big" / "ambitious" / "cathedral" → EXPANSION, no question
|
||||
* User says "hold scope but tempt me" / "show me options" / "cherry-pick" → SELECTIVE EXPANSION, no question
|
||||
|
||||
Keep the approved 0C-bis approach. Reopen it only if this mode requires a concrete change; explain that change and obtain approval for it.
|
||||
For this mode, use `question_id=plan-ceo-review-mode` for the preamble's Question Tuning check, marker and log (`auto_decided: true` when automatic).
|
||||
|
||||
Once selected, commit fully. Do not silently drift.
|
||||
Keep the approved 0C-bis approach; explain and obtain approval for any mode-required change.
|
||||
|
||||
When asking, offer all four modes for any approach. Use one AskUserQuestion per preamble format; defaults set RECOMMENDATION. These options differ in kind (review posture), not coverage — do NOT emit `Completeness: N/10` per option. Include the one-line note from step 4 of the preamble format rule instead: `Note: options differ in kind, not coverage — no completeness score.`
|
||||
Keep the selected mode.
|
||||
|
||||
**STOP.** Unless the user already explicitly selected a mode, ask via AskUserQuestion and wait for their choice. Then continue to 0D-prelude, 0D, 0D-POST, and 0E as applicable.
|
||||
When asking, offer all four modes in one AskUserQuestion; use preamble format and context defaults for RECOMMENDATION. Do NOT emit `Completeness: N/10` per option; include `Note: options differ in kind, not coverage — no completeness score.`
|
||||
|
||||
**STOP.** Ask and wait unless the user explicitly selected a mode or tuning is enabled and the actual mode check exits 0 with `AUTO_DECIDE`. This settles only the mode, not approach or scope approval. Then continue to 0D-prelude, 0D, 0D-POST, and 0E as applicable.
|
||||
**Reminder: Do NOT make any code changes. Review only.**
|
||||
|
||||
### 0D-prelude. Expansion Framing (shared by EXPANSION and SELECTIVE EXPANSION)
|
||||
|
||||
@@ -292,7 +292,7 @@ Present these approach options via AskUserQuestion using the preamble's AskUserQ
|
||||
After 0C-bis, before 0D; keep labels stable.
|
||||
Every mode requires your explicit approval for added scope.
|
||||
|
||||
Present four options:
|
||||
The four modes are:
|
||||
1. **SCOPE EXPANSION:** The plan is good but could be great. Dream big — propose the ambitious version. Every expansion is presented individually for your approval. You opt in to each one.
|
||||
2. **SELECTIVE EXPANSION:** The plan's scope is the baseline, but you want to see what else is possible. Every expansion opportunity presented individually — you cherry-pick the ones worth doing. Neutral recommendations.
|
||||
3. **HOLD SCOPE:** The plan's scope is right. Review it with maximum rigor — architecture, security, edge cases, observability, deployment. Make it bulletproof. No expansions surfaced.
|
||||
@@ -307,13 +307,15 @@ Context-dependent defaults:
|
||||
* User says "go big" / "ambitious" / "cathedral" → EXPANSION, no question
|
||||
* User says "hold scope but tempt me" / "show me options" / "cherry-pick" → SELECTIVE EXPANSION, no question
|
||||
|
||||
Keep the approved 0C-bis approach. Reopen it only if this mode requires a concrete change; explain that change and obtain approval for it.
|
||||
For this mode, use `question_id=plan-ceo-review-mode` for the preamble's Question Tuning check, marker and log (`auto_decided: true` when automatic).
|
||||
|
||||
Once selected, commit fully. Do not silently drift.
|
||||
Keep the approved 0C-bis approach; explain and obtain approval for any mode-required change.
|
||||
|
||||
When asking, offer all four modes for any approach. Use one AskUserQuestion per preamble format; defaults set RECOMMENDATION. These options differ in kind (review posture), not coverage — do NOT emit `Completeness: N/10` per option. Include the one-line note from step 4 of the preamble format rule instead: `Note: options differ in kind, not coverage — no completeness score.`
|
||||
Keep the selected mode.
|
||||
|
||||
**STOP.** Unless the user already explicitly selected a mode, ask via AskUserQuestion and wait for their choice. Then continue to 0D-prelude, 0D, 0D-POST, and 0E as applicable.
|
||||
When asking, offer all four modes in one AskUserQuestion; use preamble format and context defaults for RECOMMENDATION. Do NOT emit `Completeness: N/10` per option; include `Note: options differ in kind, not coverage — no completeness score.`
|
||||
|
||||
**STOP.** Ask and wait unless the user explicitly selected a mode or tuning is enabled and the actual mode check exits 0 with `AUTO_DECIDE`. This settles only the mode, not approach or scope approval. Then continue to 0D-prelude, 0D, 0D-POST, and 0E as applicable.
|
||||
**Reminder: Do NOT make any code changes. Review only.**
|
||||
|
||||
### 0D-prelude. Expansion Framing (shared by EXPANSION and SELECTIVE EXPANSION)
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
import {test,expect,afterEach} from 'bun:test';
|
||||
import fs from 'node:fs';import os from 'node:os';import path from 'node:path';import {spawnSync} from 'node:child_process';
|
||||
import fixture from './fixtures/autoplan-edit-digests-al.json';
|
||||
import {createAutoplanArtifactRecorder,recordAutoplanArtifact,readPendingAutoplanArtifact,autoplanArtifactRecorderStatus} from './helpers/autoplan-artifact-recorder';
|
||||
import {pendingAutoplanArtifactPermissionInput,autoplanArtifactMenuKey} from './helpers/autoplan-artifact-permission';
|
||||
import {createAutoplanEditDigest,validAutoplanEditDigest,autoplanEditLineHash} from './helpers/autoplan-artifact-digest';
|
||||
import type {NativePublicToolEvent} from './helpers/plan-count-transcript';
|
||||
import {E2E_TOUCHFILES,selectTests} from './helpers/touchfiles';
|
||||
const cleanups:Array<()=>void>=[];afterEach(()=>{for(const cleanup of cleanups.splice(0))cleanup()});
|
||||
function replay(record=true) {
|
||||
const root=fs.mkdtempSync(path.join(os.tmpdir(),'ap-digest-')),cwd=path.join(root,path.basename(fixture.cwd)),ownedStateRoot=path.join(root,'home','.gstack'),config=path.join(root,'config');
|
||||
const file=fixture.pending.file.replace(fixture.ownedStateRoot,ownedStateRoot),transcript=path.join(config,'projects','owned',fixture.pending.sessionId+'.jsonl');
|
||||
fs.mkdirSync(cwd,{recursive:true});fs.mkdirSync(path.dirname(file),{recursive:true});fs.mkdirSync(path.dirname(transcript),{recursive:true});fs.writeFileSync(file,fixture.before);fs.writeFileSync(transcript,'');
|
||||
const old=new Date(Date.parse(fixture.pending.timestamp)-1000);fs.utimesSync(file,old,old);
|
||||
const recorder=createAutoplanArtifactRecorder(cwd,config,ownedStateRoot);cleanups.push(()=>{recorder.dispose();fs.rmSync(root,{recursive:true,force:true})});
|
||||
const event={hook_event_name:'PreToolUse',tool_name:'Edit',session_id:fixture.pending.sessionId,tool_use_id:fixture.pending.toolUseId,cwd,transcript_path:transcript,tool_input:{file_path:file,...fixture.reconstructedRequest}};
|
||||
const history=structuredClone(fixture.events) as NativePublicToolEvent[];for(const e of history)if(e.input?.file_path===fixture.pending.file)e.input.file_path=file;
|
||||
const startedAt=Date.parse(history[0]!.timestamp)-1;
|
||||
if(record)recordAutoplanArtifact(JSON.stringify(event),recorder.file,cwd,config,ownedStateRoot);
|
||||
const pending=readPendingAutoplanArtifact(recorder.file,cwd,config,ownedStateRoot,startedAt,history);
|
||||
const context={cwd,ownedStateRoot,commandStartedAt:startedAt,transcriptStatus:'ready',publicTools:history,pending,viewportCapturedAt:Date.now(),now:Date.now()+1000};
|
||||
return {root,file,config,recorder,event,context,viewport:fixture.viewport};
|
||||
}
|
||||
const pick=(r:ReturnType<typeof replay>,seen=new Set<string>())=>pendingAutoplanArtifactPermissionInput(r.viewport,r.context,seen);
|
||||
test('actual added-only three-digit pane rejects without request digests, accepts a separately recorded reconstructed insertion',()=>{
|
||||
const r=replay();expect(r.context.pending?.editDigest).toBeDefined();expect(pick(r)?.input).toBe('1\r');
|
||||
delete r.context.pending!.editDigest;expect(pick(r)).toBeNull();
|
||||
expect(fixture.provenance.reconstruction).toContain('not the original');
|
||||
});
|
||||
test('hook persists bounded digests from its input, never request or result text',()=>{
|
||||
const r=replay(false),hook=r.recorder.hooks.PreToolUse[0]!.hooks[0]!;
|
||||
const child=spawnSync('bash',['-c',hook.command],{input:JSON.stringify({...r.event,tool_response:'PRIVATE_RESULT_SENTINEL'}),encoding:'utf8',timeout:6000});
|
||||
expect(child.status).toBe(0);expect(child.stdout).toBe('');expect(child.stderr).toBe('');
|
||||
const raw=fs.readFileSync(r.recorder.file,'utf8'),state=JSON.parse(raw);expect(validAutoplanEditDigest(state.pending.editDigest)).toBe(true);
|
||||
for(const secret of ['old_string','new_string','PRIVATE_RESULT_SENTINEL','Owner: the user.','Toast stacking ahead'])expect(raw).not.toContain(secret);
|
||||
expect(state.pending.editDigest).toEqual(createAutoplanEditDigest(r.file,r.event.tool_input.old_string,r.event.tool_input.new_string));
|
||||
expect(fs.statSync(r.recorder.file).size).toBeLessThan(64*1024);expect(fs.statSync(r.recorder.file).mode&0o777).toBe(0o600);
|
||||
});
|
||||
test('digests of a different request cannot authorize the displayed additions',()=>{
|
||||
const r=replay();r.context.pending!.editDigest=createAutoplanEditDigest(r.file,'Owner: the user.\n','Owner: the user.\nDifferent requested insertion.\n')!;expect(pick(r)).toBeNull();
|
||||
r.context.pending!.editDigest=createAutoplanEditDigest(r.file,r.event.tool_input.old_string,r.event.tool_input.new_string)!;
|
||||
r.context.pending!.editDigest.newLineHashes=r.context.pending!.editDigest.oldLineHashes;expect(pick(r)).toBeNull();
|
||||
});
|
||||
test('current file hash, native identity, predecessor and single use stay required',()=>{
|
||||
const mutations:Array<(r:ReturnType<typeof replay>)=>void>=[
|
||||
r=>{r.context.pending!.sessionId='foreign';},r=>{r.context.pending!.file=path.join(r.root,'foreign.md');},
|
||||
r=>{r.context.pending!.editDigest!.beforeSHA256='0'.repeat(64);},
|
||||
r=>{fs.writeFileSync(r.file,fixture.before+'Changed concurrently.');const old=new Date(0);fs.utimesSync(r.file,old,old);},
|
||||
r=>{r.context.pending!.timestamp=new Date(r.context.now+1000).toISOString();},r=>{r.context.viewportCapturedAt=Date.parse(r.context.pending!.timestamp)-1;},
|
||||
r=>{r.context.commandStartedAt=r.context.now+1;},r=>{r.context.publicTools[1]!.isError=true;},r=>{r.context.publicTools=[];},
|
||||
r=>{r.context.publicTools.push({kind:'result',sessionId:r.context.pending!.sessionId,toolUseId:r.context.pending!.toolUseId,timestamp:new Date(r.context.now).toISOString(),isError:false});},
|
||||
r=>{r.context.publicTools.push({kind:'use',name:'Write',sessionId:r.context.pending!.sessionId,toolUseId:'successor',timestamp:new Date(r.context.now).toISOString(),input:{file_path:r.file}});},
|
||||
];for(const change of mutations){const r=replay();change(r);expect(pick(r)).toBeNull()}
|
||||
const r=replay();expect(pick(r,new Set([r.context.pending!.sessionId+':'+r.context.pending!.toolUseId]))).toBeNull();expect(pick(r,new Set([autoplanArtifactMenuKey(r.viewport)]))).toBeNull();
|
||||
});
|
||||
test('exact three-digit marker column rejects wrong gutters, arbitrary source rows and malformed numbering',()=>{
|
||||
for(const change of [
|
||||
(s:string)=>s.replace(/^ \+/m,' +'),(s:string)=>s.replace(/^ \+/m,' +'),
|
||||
(s:string)=>s.replace(/^ \+/m,' Source: '),(s:string)=>'> quoted example\n'+s,
|
||||
(s:string)=>s.replace(/^ 140 /m,' 0 '),(s:string)=>s.replace(/^ 141 /m,' 139 '),
|
||||
(s:string)=>s.replace(/^ 140 /m,' 999999999999999999999 '),(s:string)=>s.replace(/^ 140 \+/m,' 140 -'),
|
||||
(s:string)=>s.replace('3. No','3. Maybe'),(s:string)=>s.replace('❯ 1. Yes','❯ 2. Yes'),
|
||||
(s:string)=>s.replace('2026-09-10-user-dashboard.md?','foreign.md?'),(s:string)=>s+'\nUnrelated prompt',
|
||||
]){const r=replay();r.viewport=change(r.viewport);expect(pick(r)).toBeNull()}
|
||||
});
|
||||
test('four-space continuation is accepted only with the matching two-digit numbered gutter',()=>{
|
||||
const r=replay();r.viewport=r.viewport.replace(/^ (1[4][0-9]) /gm,(_,n)=>' '+(Number(n)-130)+' ').replace(/^ ([+ -])/gm,' $1');expect(pick(r)?.input).toBe('1\r');
|
||||
});
|
||||
test('original or context rows cannot supply insertion authority',()=>{
|
||||
const r=replay(),menu=r.viewport.slice(r.viewport.indexOf('╌'));
|
||||
r.context.pending!.editDigest=createAutoplanEditDigest(r.file,'Owner: the user.\n','Owner: the user.\nNew actual request.\n')!;
|
||||
r.viewport=' 140 +Owner: the user.\n 141 +Owner: the user.\n'+menu;expect(pick(r)).toBeNull();
|
||||
r.viewport=' 140 Owner: the user.\n 141 Owner: the user.\n'+menu;expect(pick(r)).toBeNull();
|
||||
});
|
||||
test('malformed, sparse and high-volume persisted digest records fail closed',()=>{
|
||||
for(const change of [(d:any)=>{d.version=2},(d:any)=>{d.extra='text'},(d:any)=>{d.beforeSHA256='bad'},(d:any)=>{d.newLineHashes=[]},(d:any)=>{d.newLineHashes=Array(513).fill('a'.repeat(64))},(d:any)=>{d.oldLineHashes[0]=null}]){
|
||||
const r=replay(),s=JSON.parse(fs.readFileSync(r.recorder.file,'utf8'));change(s.pending.editDigest);fs.writeFileSync(r.recorder.file,JSON.stringify(s));expect(autoplanArtifactRecorderStatus(r.recorder.file,r.context.cwd,r.config,r.context.ownedStateRoot).status).toBe('invalid');
|
||||
r.context.pending!.editDigest=s.pending.editDigest;expect(pick(r)).toBeNull();
|
||||
}
|
||||
const r=replay(),sparse={...r.context.pending!.editDigest!,newLineHashes:Array(2)};expect(validAutoplanEditDigest(sparse)).toBe(false);
|
||||
});
|
||||
test('unavailable or oversized before/request data yields no new digest authority',()=>{
|
||||
const r=replay();expect(createAutoplanEditDigest(r.file,'missing original','new')).toBeUndefined();expect(createAutoplanEditDigest(r.file,'Owner: the user.\n','x\n'.repeat(513))).toBeUndefined();
|
||||
const link=path.join(r.root,'linked');fs.symlinkSync(r.file,link);expect(createAutoplanEditDigest(link,r.event.tool_input.old_string,r.event.tool_input.new_string)).toBeUndefined();
|
||||
fs.writeFileSync(r.file,'x'.repeat(1024*1024+1));expect(createAutoplanEditDigest(r.file,'x','new')).toBeUndefined();fs.unlinkSync(r.file);expect(createAutoplanEditDigest(r.file,'old','new')).toBeUndefined();
|
||||
});
|
||||
test('normalization joins display wrapping but keeps changed nonwhitespace bytes distinct',()=>{
|
||||
expect(autoplanEditLineHash('same body\t')).toBe(autoplanEditLineHash('samebody'));expect(autoplanEditLineHash('same body')).not.toBe(autoplanEditLineHash('different body'));
|
||||
const r=replay();r.viewport=r.viewport.replace('Toast stacking','Toast stacKING');expect(pick(r)).toBeNull();
|
||||
});
|
||||
test('only Autoplan owns the new digest helper and regression evidence',()=>{
|
||||
const owner=E2E_TOUCHFILES['autoplan-chain-pty']!;for(let i=0;i<owner.length;i++){expect(Object.hasOwn(owner,i)).toBe(true);expect(typeof owner[i]).toBe('string');}
|
||||
for(const file of ['test/helpers/autoplan-artifact-digest.ts','test/autoplan-edit-digests-al.test.ts','test/fixtures/autoplan-edit-digests-al.json'])expect(selectTests([file],E2E_TOUCHFILES,[]).selected).toEqual(['autoplan-chain-pty']);
|
||||
});
|
||||
test('identical pending hook replay cannot refresh digest or timestamp',()=>{
|
||||
const r=replay(),before=fs.readFileSync(r.recorder.file,'utf8');recordAutoplanArtifact(JSON.stringify(r.event),r.recorder.file,r.context.cwd,r.config,r.context.ownedStateRoot);expect(fs.readFileSync(r.recorder.file,'utf8')).toBe(before);
|
||||
});
|
||||
test.each(['changed-new','changed-old','whitespace-only','missing-input','over-limit','replace-all','before-file'])('same pending identity with %s invalidates prior digest authority',kind=>{
|
||||
const r=replay(),e=structuredClone(r.event) as any;
|
||||
if(kind==='changed-new')e.tool_input.new_string+='A different final action.\n';
|
||||
if(kind==='changed-old')e.tool_input.old_string='Owner: the user.';
|
||||
if(kind==='whitespace-only')e.tool_input.new_string=e.tool_input.new_string.replace('Owner: the user.','Owner: the user.');
|
||||
if(kind==='missing-input')delete e.tool_input.new_string;
|
||||
if(kind==='over-limit')e.tool_input.new_string='new\n'.repeat(513);
|
||||
if(kind==='replace-all')e.tool_input.replace_all=true;
|
||||
if(kind==='before-file')fs.writeFileSync(r.file,fixture.before+'Unobserved change.');
|
||||
recordAutoplanArtifact(JSON.stringify(e),r.recorder.file,r.context.cwd,r.config,r.context.ownedStateRoot);
|
||||
expect(autoplanArtifactRecorderStatus(r.recorder.file,r.context.cwd,r.config,r.context.ownedStateRoot)).toEqual({status:'invalid',reason:'conflicting_replay'});
|
||||
expect(readPendingAutoplanArtifact(r.recorder.file,r.context.cwd,r.config,r.context.ownedStateRoot,r.context.commandStartedAt,r.context.publicTools,r.context.now)).toBeUndefined();
|
||||
});
|
||||
|
||||
// Synthetic legacy crops use the actual generated PreToolUse subprocess. They
|
||||
// preserve the frozen deletion/context policy, not new insertion-only authority.
|
||||
test.each([
|
||||
{name:'leading partial deletion',rows:[' -full line',' 11 -Old second',' 12 +New replacement'],removed:'First original full line\nOld second',added:'New replacement'},
|
||||
{name:'leading partial context',rows:[' full line',' 11 -Old second',' 12 +New replacement'],removed:'Old second',added:'New replacement'},
|
||||
{name:'deletion-only rows',rows:[' 10 -First original full line',' 11 -Old second',' 12 Context'],removed:'First original full line\nOld second\n',added:''},
|
||||
{name:'old/new line numbering reset',rows:[' 10 -First original full line',' 11 -Old second',' 10 +New first',' 11 +New second',' 12 Context'],removed:'First original full line\nOld second',added:'New first\nNew second'},
|
||||
])('recording a digest preserves an owned legacy $name crop',c=>{
|
||||
const r=replay(false),before='First original full line\nOld second\nContext\n';
|
||||
fs.writeFileSync(r.file,before);const old=new Date(Date.parse(fixture.pending.timestamp)-1000);fs.utimesSync(r.file,old,old);
|
||||
for(const e of r.context.publicTools)if(e.name==='Write'&&e.input?.file_path===r.file)e.input.content=before;
|
||||
r.event.tool_input.old_string=c.removed;r.event.tool_input.new_string=c.added;
|
||||
const child=spawnSync('bash',['-c',r.recorder.hooks.PreToolUse[0]!.hooks[0]!.command],{input:JSON.stringify(r.event),encoding:'utf8',timeout:6000});
|
||||
expect(child.status).toBe(0);expect(child.stdout).toBe('');expect(child.stderr).toBe('');
|
||||
r.context.pending=readPendingAutoplanArtifact(r.recorder.file,r.context.cwd,r.config,r.context.ownedStateRoot,r.context.commandStartedAt,r.context.publicTools);
|
||||
r.context.viewportCapturedAt=Date.now();r.context.now=Date.now()+1000;
|
||||
const menu=r.viewport.slice(r.viewport.indexOf('Do you want to make this edit'));
|
||||
r.viewport=c.rows.join('\n')+'\n'+'╌'.repeat(20)+'\n'+menu;
|
||||
expect(validAutoplanEditDigest(r.context.pending?.editDigest)).toBe(true);
|
||||
const digest=structuredClone(r.context.pending!.editDigest!);
|
||||
expect(pick(r)?.input).toBe('1\r');
|
||||
delete r.context.pending!.editDigest;expect(pick(r)?.input).toBe('1\r');
|
||||
r.context.pending!.editDigest={...digest,beforeSHA256:'0'.repeat(64)};expect(pick(r)).toBeNull();
|
||||
r.context.pending!.editDigest={...digest,beforeSHA256:'malformed'};expect(pick(r)).toBeNull();
|
||||
r.context.pending!.editDigest=digest;
|
||||
const viewport=r.viewport;r.viewport=r.viewport.replace(/^((?: {0,3}\d+ | {4})-).*$/gm,'$1Foreign unowned deletion');expect(pick(r)).toBeNull();r.viewport=viewport;
|
||||
fs.unlinkSync(r.file);expect(pick(r)).toBeNull();
|
||||
});
|
||||
@@ -0,0 +1,63 @@
|
||||
import {expect, test} from 'bun:test';
|
||||
import {ceoFirstReviewAUQ, nativePlanCallFingerprint} from './helpers/claude-pty-runner';
|
||||
import fixture from './fixtures/ceo-decision-prefix-al.json';
|
||||
import {E2E_TOUCHFILES} from './helpers/touchfiles-data';
|
||||
const call=(n=0):any=>structuredClone(fixture.calls[n]);
|
||||
const accepts=(c:any)=>ceoFirstReviewAUQ(nativePlanCallFingerprint(c,0,true));
|
||||
function text(c:any,fn:(s:string)=>string){const q=c.questions[0],a=c.answers[q.question];q.question=fn(q.question);c.answers={[q.question]:a};}
|
||||
function menu(c:any,fn:(o:any,i:number)=>void){const q=c.questions[0],i=q.options.findIndex((o:any)=>o.label===c.answers[q.question]);q.options.forEach(fn);c.answers={[q.question]:q.options[i].label};}
|
||||
test('the actual completed email finding uses decision-prefixed options and a bare recommendation',()=>expect(accepts(call())).toBe(true));
|
||||
test('the actual completed SQL finding includes a raw SQL qualifier',()=>expect(accepts(call(1))).toBe(true));
|
||||
test('decision and finding identifiers remain independent when consistently renamed',()=>{
|
||||
for(const n of [0,1]){
|
||||
for(const dotted of [false,true]){const c=call(n);text(c,s=>s.replace(/^D\d+/,'D27').replace(/\(Finding \d+\)/,`(Finding ${dotted?'8.3':'8'})`));menu(c,o=>{o.label=o.label.replace(/^\d+/,'27')});expect(accepts(c)).toBe(true);}
|
||||
const c=call(n);menu(c,o=>{o.label=o.label.replace(/^\d+/,'')});text(c,s=>s.replace("'no error handling on the email leg'",'no error handling on the email leg').replace('a raw SQL fragment','a SQL fragment'));expect(accepts(c)).toBe(true);
|
||||
const q=call(n);text(q,s=>s+'\nOld note: "This finding is withdrawn."');expect(accepts(q)).toBe(true);
|
||||
const lower=call(n);text(lower,s=>s.replace(/^D/,'d'));expect(accepts(lower)).toBe(true);
|
||||
}
|
||||
});
|
||||
test('native ownership, offered answers and unambiguous decision identities are mandatory',()=>{
|
||||
for(const mutate of [
|
||||
(c:any)=>{c.answered=false},(c:any)=>{c.failed=true},(c:any)=>{c.unansweredQuestionIndices=[0]},(c:any)=>{c.sessionId=''},
|
||||
(c:any)=>{c.answers={}},(c:any)=>{c.answers[c.questions[0].question]='A'},(c:any)=>{c.questions[0].multiSelect=true},
|
||||
(c:any)=>{c.questions[0].header='Finding 9'},(c:any)=>{c.questions[0].header='Approach'},
|
||||
(c:any)=>text(c,s=>s.replace(/^D4/,'D9')),
|
||||
(c:any)=>menu(c,o=>{o.label=o.label.replace(/^4/,'9')}),
|
||||
(c:any)=>menu(c,(o,i)=>{if(i===1)o.label=o.label.replace(/^4/,'9')}),
|
||||
(c:any)=>menu(c,(o,i)=>{if(i===1)o.label=o.label.replace(/^4/,'')}),
|
||||
(c:any)=>text(c,s=>s.replace(/^Recommendation: A/m,'Recommendation: 9A')),
|
||||
(c:any)=>text(c,s=>s.replace(/^Recommendation: A/m,'Recommendation: Z')),
|
||||
(c:any)=>menu(c,(o,i)=>{if(i===1)o.label=o.label.replace(/^4B/,'4A')}),
|
||||
(c:any)=>{c.questions[0].options[1].description=''},
|
||||
]){const c=call();mutate(c);expect(accepts(c)).toBe(false);}
|
||||
const f=nativePlanCallFingerprint(call(),0,true);expect(ceoFirstReviewAUQ({...f,signature:'foreign:tool'})).toBe(false);
|
||||
});
|
||||
test('embedded quoted contract terms cannot supply a hypothetical, historical or withdrawn assessment',()=>{
|
||||
for(const n of [0,1])for(const fn of [
|
||||
(s:string)=>'Source excerpt: '+s,(s:string)=>'> '+s,(s:string)=>'```\n'+s+'\n```',
|
||||
(s:string)=>s.replace(/^ELI10: (.+)$/m,'ELI10: "$1"'),
|
||||
(s:string)=>s.replace(/^ELI10: /m,'ELI10: If approved, '),
|
||||
(s:string)=>s.replace(/^ELI10: /m,'ELI10: Source excerpt. '),
|
||||
(s:string)=>s.replace(/^ELI10: /m,'ELI10: The following is a historical source excerpt. '),
|
||||
(s:string)=>s.replace(/^ELI10: /m,'ELI10: Previously, '),
|
||||
(s:string)=>s+'\nThis finding is withdrawn.',
|
||||
(s:string)=>s+'\nNo current defect remains.',
|
||||
(s:string)=>s.replace(/^ELI10: .+$/m,'ELI10: The plan sends the email inline with \'no error handling\' only in a historical example.'),
|
||||
(s:string)=>s.replace(/^ELI10: .+$/m,'ELI10: The plan does not send the email inline with \'no error handling\'.'),
|
||||
(s:string)=>s.replace(/^ELI10: .+$/m,'ELI10: The plan used to paste the user ID straight into a raw SQL fragment. The current query is parameterized.'),
|
||||
(s:string)=>s.replace(/^ELI10: .+$/m,'ELI10: A proposed example pastes the user ID string straight into a raw SQL fragment.'),
|
||||
(s:string)=>s.replace(/^ELI10: .+$/m,'ELI10: The historical example pastes the user ID string straight into a raw SQL fragment.'),
|
||||
(s:string)=>s.replace(/^ELI10: .+$/m,'ELI10: A template pastes the user ID string straight into a raw SQL fragment.'),
|
||||
(s:string)=>s.replace(/^ELI10: .+$/m,'ELI10: An unrelated example pastes the user ID string straight into a raw SQL fragment.'),
|
||||
(s:string)=>s.replace(/^ELI10: .+$/m,'ELI10: The plan pastes the user ID string straight into a raw SQL fragment only in a hypothetical example.'),
|
||||
]){const c=call(n);text(c,fn);expect(accepts(c)).toBe(false);}
|
||||
});
|
||||
test('a substantive current assessment still needs an offered technical amendment',()=>{
|
||||
for(const description of ['Archive this report.','If approved: ✅ Rescue named mail exceptions.','Source excerpt: ✅ Rescue named mail exceptions.','❌ Rescue named mail exceptions.','✅ "Rescue named mail exceptions."']){
|
||||
const c=call();menu(c,(o,i)=>{o.label=`4${String.fromCharCode(65+i)}: Consider candidate ${i}`;o.description=description});expect(accepts(c)).toBe(false);
|
||||
}
|
||||
});
|
||||
test('only the existing CEO count owner selects the captured regression',()=>{
|
||||
for(const dependency of E2E_TOUCHFILES['plan-ceo-finding-count']) expect(typeof dependency).toBe('string');
|
||||
for(const d of ['test/ceo-decision-prefix-al.test.ts','test/fixtures/ceo-decision-prefix-al.json'])expect(Object.entries(E2E_TOUCHFILES).filter(([,v])=>v.includes(d)).map(([k])=>k)).toEqual(['plan-ceo-finding-count']);
|
||||
});
|
||||
@@ -0,0 +1,105 @@
|
||||
import {afterAll, beforeAll, expect, test} from 'bun:test';
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import {spawnSync} from 'node:child_process';
|
||||
import {getQuestion} from '../scripts/question-registry';
|
||||
import {E2E_TOUCHFILES} from './helpers/touchfiles-data';
|
||||
import {CARVE_GUARDS} from './helpers/carve-guards';
|
||||
const root=path.resolve(import.meta.dir,'..');
|
||||
const temp=fs.mkdtempSync(path.join(os.tmpdir(),'gstack-ceo-mode-preference-'));
|
||||
const section=(s:string)=>s.split('### 0F. Mode Selection\n')[1]!.split('\n### 0D-prelude.')[0]!;
|
||||
const rendered=new Map<string,string>();
|
||||
const env=(state:string)=>({...process.env,GSTACK_HOME:state,GSTACK_STATE_ROOT:state});
|
||||
beforeAll(()=>{
|
||||
for(const host of ['claude','codex']){
|
||||
const out=path.join(temp,host),state=path.join(temp,'render-state');
|
||||
const result=spawnSync(process.execPath,['run','scripts/gen-skill-docs.ts','--host',host,'--out-dir',out],{cwd:root,env:env(state),encoding:'utf8',timeout:120_000});
|
||||
if(result.status!==0)throw new Error(result.stderr||result.stdout);
|
||||
rendered.set(host,fs.readFileSync(path.join(out,host==='claude'?'plan-ceo-review':'.agents/skills/gstack-plan-ceo-review','SKILL.md'),'utf8'));
|
||||
}
|
||||
},120_000);
|
||||
afterAll(()=>fs.rmSync(temp,{recursive:true,force:true}));
|
||||
function modeId(document:string){
|
||||
const ids=[...section(document).matchAll(/`question_id=([^`]+)`/g)].map(m=>m[1]!);
|
||||
expect(ids).toHaveLength(1);return ids[0]!;
|
||||
}
|
||||
function tuning(document:string){
|
||||
return document.split('## Question Tuning (skip entirely if')[1]!.split('\n## ')[0]!;
|
||||
}
|
||||
function renderedCheck(host:string,id:string){
|
||||
const match=tuning(rendered.get(host)!).match(/`(printf '%s' "<question summary>" \| ([^`]+)\/gstack-question-preference --check "<id>" --summary-stdin)`/)!;
|
||||
expect(match).not.toBeNull();
|
||||
expect(match[2]).toBe(host==='claude'?'~/.claude/skills/gstack/bin':'$GSTACK_BIN');
|
||||
const quote=(value:string)=>"'"+value.replace(/'/g,"'\\''")+"'";
|
||||
// Run the rendered command, substituting its documented fields and mapping
|
||||
// the host's installed executable location to this isolated checkout.
|
||||
return match[1]!.replace('<question summary>','Select the CEO review mode for the current plan.')
|
||||
.replace('"<id>"',quote(id))
|
||||
.replace(match[2]!+'/gstack-question-preference',quote(path.join(root,'bin/gstack-question-preference')));
|
||||
}
|
||||
function checkWithPreference(host:string,preference?:string,writeId?:string){
|
||||
const id=modeId(rendered.get(host)!),state=fs.mkdtempSync(path.join(temp,'state-'));
|
||||
const run=(args:string[],input?:string)=>spawnSync(path.join(root,'bin/gstack-question-preference'),args,{cwd:root,env:env(state),input,encoding:'utf8',timeout:30_000});
|
||||
if(preference){const written=run(['--write',JSON.stringify({question_id:writeId??id,preference,source:'plan-tune'})]);expect(written.status).toBe(0);}
|
||||
const result=spawnSync('bash',['-c',renderedCheck(host,id)],{cwd:root,env:env(state),encoding:'utf8',timeout:30_000});
|
||||
expect(result.status).toBe(0);return {id,result,run};
|
||||
}
|
||||
test('source and both isolated host renders bind the shared check, marker and log to the registered mode identity',()=>{
|
||||
const source=fs.readFileSync(path.join(root,'plan-ceo-review/SKILL.md.tmpl'),'utf8');
|
||||
for(const document of [source,...rendered.values()]){
|
||||
const id=modeId(document),s=section(document);
|
||||
expect(getQuestion(id)).toMatchObject({id:'plan-ceo-review-mode',skill:'plan-ceo-review',category:'routing',door_type:'two-way'});
|
||||
expect(s).toContain("preamble's Question Tuning check, marker and log");
|
||||
expect(s).toContain('`auto_decided: true` when automatic');
|
||||
expect(s).not.toContain('plan-ceo-review-mode-selection');
|
||||
}
|
||||
for(const document of rendered.values()){
|
||||
expect(tuning(document)).toContain('<gstack-qid:{question_id}>');
|
||||
expect(tuning(document)).toContain('"question_id":"<id>"');
|
||||
}
|
||||
});
|
||||
test('both rendered canonical checks actually honor a stored never-ask preference',()=>{
|
||||
for(const host of rendered.keys()){
|
||||
const {result,run}=checkWithPreference(host,'never-ask');
|
||||
expect(result.stdout.trim()).toBe('AUTO_DECIDE');
|
||||
const wrong=run(['--check','plan-ceo-review-mode-selection','--summary-stdin'],'Select the CEO review mode.');
|
||||
expect(wrong.status).toBe(0);expect(wrong.stdout.trim()).toBe('ASK_NORMALLY');
|
||||
}
|
||||
});
|
||||
test('absent, always-ask and foreign preferences do not authorize either host to select a mode',()=>{
|
||||
for(const host of rendered.keys()){
|
||||
for(const [preference,id] of [[undefined,undefined],['always-ask',undefined],['never-ask','plan-design-review-mode']] as const){
|
||||
expect(checkWithPreference(host,preference,id).result.stdout.trim()).toBe('ASK_NORMALLY');
|
||||
}
|
||||
}
|
||||
});
|
||||
test('only an explicit user selection or enabled successful mode check bypasses asking',()=>{
|
||||
for(const document of rendered.values()){
|
||||
const s=section(document),q=tuning(document);
|
||||
expect(s).toContain('Ask and wait unless the user explicitly selected a mode or tuning is enabled and the actual mode check exits 0 with `AUTO_DECIDE`');
|
||||
expect(document).toContain('Question Tuning (skip entirely if `QUESTION_TUNING: false`)');
|
||||
expect(q).toContain('`AUTO_DECIDE` means choose the recommended option');
|
||||
expect(q).toContain('Auto-decided [summary] → [option] (your preference). Change with /plan-tune.');
|
||||
expect(q).toContain('`ASK_NORMALLY` means ask.');
|
||||
expect(s).toContain('This settles only the mode, not approach or scope approval.');
|
||||
expect(document).toContain('Do NOT proceed to mode selection (0F) without user approval of the chosen approach.');
|
||||
expect(s).toContain('Every mode requires your explicit approval for added scope.');
|
||||
expect(s).toContain('Keep the approved 0C-bis approach; explain and obtain approval for any mode-required change.');
|
||||
expect(s).toContain('offer all four modes in one AskUserQuestion');
|
||||
expect(s).toContain('context defaults for RECOMMENDATION');
|
||||
expect(s).toContain('Do NOT emit `Completeness: N/10` per option');
|
||||
expect(s).toContain('Note: options differ in kind, not coverage — no completeness score.');
|
||||
}
|
||||
});
|
||||
test('the new render/runtime regression belongs to the existing auto-decide owner',()=>{
|
||||
expect(Object.entries(E2E_TOUCHFILES).filter(([,v])=>v.includes('test/ceo-mode-preference-al.test.ts')).map(([k])=>k)).toEqual(['auto-decide-preserved']);
|
||||
});
|
||||
test('rendered mode contract stays within the existing canonical skeleton cap',()=>{
|
||||
// --out-dir changes section-link roots only. Undo that output-location
|
||||
// substitution before measuring the same canonical bytes as parity-suite.
|
||||
const out=path.join(temp,'claude').replace(/[.*+?^${}()|[\]\\]/g,'\\$&');
|
||||
const canonical=rendered.get('claude')!.replace(new RegExp(out+'/([^\\s)`"\'*]+/sections/)','g'),(_m,p1)=>'~/.claude/skills/gstack/'+p1);
|
||||
const cap=Object.values(CARVE_GUARDS).find(g=>g.skill==='plan-ceo-review')!.maxSkeletonBytes;
|
||||
expect(Buffer.byteLength(canonical)).toBeLessThanOrEqual(cap);
|
||||
});
|
||||
@@ -0,0 +1,62 @@
|
||||
import {describe, expect, test} from 'bun:test';
|
||||
import fixture from './fixtures/design-primary-decision-al.json';
|
||||
import {isDesignCountFirstReview} from './helpers/design-count-review';
|
||||
import type {AskUserQuestionFingerprint} from './helpers/claude-pty-runner';
|
||||
type FP=AskUserQuestionFingerprint;
|
||||
type Q=NonNullable<FP['nativeCall']>['questions'][number];
|
||||
const original=()=>structuredClone(fixture.fingerprint) as unknown as FP;
|
||||
function edit(change:(q:Q,fp:FP)=>void):FP {
|
||||
const fp=original(),c=fp.nativeCall!,q=c.questions[0]!,chosen=q.options.findIndex(o=>o.label===c.answers![q.question]);
|
||||
change(q,fp);c.answers={[q.question]:q.options[chosen]!.label};
|
||||
fp.options=q.options.map((o,i)=>({index:i+1,label:o.label}));return fp;
|
||||
}
|
||||
describe('answered primary-action decision with compact style choices',()=>{
|
||||
test('recognizes the exact current native issue independently of its interrogative title',()=>{
|
||||
expect(isDesignCountFirstReview(original())).toBe(true);
|
||||
});
|
||||
const positive:Array<[string,(q:Q,fp:FP)=>void]>=[
|
||||
['different named primary',q=>{q.question=q.question.replaceAll('Save','Submit');q.options=q.options.map(o=>({...o,label:o.label.replaceAll('Save','Submit'),description:o.description?.replaceAll('Save','Submit')}));}],
|
||||
['explicit fill role and foreground',q=>{q.options[0]!.description=q.options[0]!.description!.replace('filled #1d4ed8/white','filled primary #234abc with black text').replace('neutral ghost.','neutral ghost buttons.');}],
|
||||
['actions without header qualification',q=>{q.question=q.question.replace('the header actions','actions');}],
|
||||
['imperative title with the compact style',q=>{q.question=q.question.replace('How should the header actions establish that Save is the primary action','Make Save the visible primary action');}],
|
||||
['interrogative title with expanded style',q=>{q.options[0]!.description='Apply DESIGN.md tokens: Save #1d4ed8 filled with white text; Reset, Cancel, Export neutral ghost.';}],
|
||||
['existing explicit open-gap deferral',q=>{q.options[2]!.description='Decline the fix; gap stays documented and lowers the score.';}],
|
||||
['numeric control count in deferral',q=>{q.options[2]!.description=q.options[2]!.description!.replace('four','4');}],
|
||||
['quoted historical note does not withdraw current amendment',q=>{q.options[0]!.description+=' Prior note: "This amendment is withdrawn."';}],
|
||||
];
|
||||
test.each(positive)('%s retains the same owned decision',(_,change)=>expect(isDesignCountFirstReview(edit(change))).toBe(true));
|
||||
const negative:Array<[string,(q:Q,fp:FP)=>void]>=[
|
||||
['equality qualified as archived only',q=>{q.question=q.question.replace('look identical.','look identical only in the archived screenshot. Today they are distinct.');}],
|
||||
['amendment relabelled as historical',q=>{q.options[0]!.description+=' This is a historical example, not the current amendment.';}],
|
||||
['amendment explicitly withdrawn',q=>{q.options[0]!.description+=' This amendment is withdrawn.';}],
|
||||
['deferral relabelled as historical',q=>{q.options[2]!.description+=' This is a historical example, not the current deferral.';}],
|
||||
['failed native call',(_,fp)=>{fp.nativeCall!.failed=true;}],
|
||||
['unanswered native call',(_,fp)=>{fp.nativeCall!.answered=false;}],
|
||||
['unbound signature',(_,fp)=>{fp.signature='other:call';}],
|
||||
['missing completion time',(_,fp)=>{delete fp.nativeCall!.answeredAt;}],
|
||||
['competing issue number',q=>{q.header='Issue 2';}],
|
||||
['competing option identity',q=>{q.options[0]!.label='2A Primary + ghost';}],
|
||||
['source-framed question',q=>{q.question='Historical example:\n'+q.question;}],
|
||||
['historical premise',q=>{q.question=q.question.replace('ELI10: Right now','ELI10: Previously');}],
|
||||
['quoted premise',q=>{q.question=q.question.replace('ELI10: Right now','> ELI10: Right now');}],
|
||||
['conditional premise',q=>{q.question=q.question.replace('ELI10: Right now','ELI10: If right now');}],
|
||||
['negated equality',q=>{q.question=q.question.replace('look identical','do not look identical');}],
|
||||
['competing premise',q=>{q.question+='\nELI10: No current hierarchy gap exists.';}],
|
||||
['resolved finding',q=>{q.question+='\nCorrection: this gap is already resolved.';}],
|
||||
['other primary in remedy',q=>{q.options[0]!.description=q.options[0]!.description!.replace('Save filled','Reset filled');}],
|
||||
['no prescribed fill',q=>{q.options[0]!.description=q.options[0]!.description!.replace('filled','outlined');}],
|
||||
['no prescribed foreground',q=>{q.options[0]!.description=q.options[0]!.description!.replace('/white','/unknown');}],
|
||||
['primary also a ghost',q=>{q.options[0]!.description=q.options[0]!.description!.replace('; Reset','; Save, Reset');}],
|
||||
['quoted amendment',q=>{q.options[0]!.description='> '+q.options[0]!.description;}],
|
||||
['conditional amendment',q=>{q.options[0]!.description='If approved later, '+q.options[0]!.description;}],
|
||||
['negated amendment',q=>{q.options[0]!.description='Do not apply: '+q.options[0]!.description;}],
|
||||
['withdrawn amendment',q=>{q.options[0]!.description+=' Correction: do not apply these tokens.';}],
|
||||
['wrong design authority',q=>{q.options[0]!.description=q.options[0]!.description!.replace('Exact DESIGN.md.','Archived example.');}],
|
||||
['no opposed choice',q=>{q.options[2]!.label='1C Export preferences';}],
|
||||
['defer does not retain equality',q=>{q.options[2]!.description=q.options[2]!.description!.replace('identical','distinct');}],
|
||||
['historical deferral',q=>{q.options[2]!.description='Historical source excerpt: '+q.options[2]!.description;}],
|
||||
['conditional deferral',q=>{q.options[2]!.description='If accepted later: '+q.options[2]!.description;}],
|
||||
['deferral closes gap',q=>{q.options[2]!.description+=' Correction: the violation is now closed.';}],
|
||||
];
|
||||
test.each(negative)('%s is not completed current-review evidence',(_,change)=>expect(isDesignCountFirstReview(edit(change))).toBe(false));
|
||||
});
|
||||
@@ -0,0 +1,115 @@
|
||||
import { expect, test } from 'bun:test';
|
||||
import fixture from './fixtures/eng-golden-master-al.json';
|
||||
import { evaluateEngSeedCoverage } from './helpers/eng-seeded-coverage';
|
||||
import type { PlanCountTranscript } from './helpers/plan-count-transcript';
|
||||
import { E2E_TOUCHFILES, selectTests } from './helpers/touchfiles';
|
||||
|
||||
const plan = [fixture.required, '## Implementation Tasks\n\n' + fixture.task, fixture.verification, fixture.reviewReport].join('\n\n');
|
||||
const { start, end } = fixture.provenance.window;
|
||||
const native = () => structuredClone(fixture.transcript) as PlanCountTranscript;
|
||||
const evaluate = (p = plan, t = native()) => evaluateEngSeedCoverage(t, p, start, end);
|
||||
|
||||
test('the captured golden-master requirement binds a numbered task to an untouched baseline', () => {
|
||||
expect(evaluate().regression).toBe('plan');
|
||||
expect(evaluate().ok).toBe(true);
|
||||
});
|
||||
|
||||
test('task identities and presentation may vary without changing the required oracle', () => {
|
||||
for (const p of [
|
||||
plan.replaceAll('T1', 'T23'),
|
||||
plan.replace('— legacy —', '— auth/legacy —'),
|
||||
plan.replaceAll('golden-master', 'golden master'),
|
||||
plan.replace('Capture current outputs', 'Record current outputs'),
|
||||
plan.replace('identical behaviour', 'identical behavior'),
|
||||
plan.replace('success / expired / revoked / wrong-tenant /\nlogout', 'success / invalid audience / expired'),
|
||||
plan + '\n## Assessment of T8\nT8 is cancelled.',
|
||||
plan + '\n## Payment regression suite\nThe regression suite is no longer required.',
|
||||
plan + '\n## Payment golden-master fixtures\nThe golden-master fixtures are no longer required.',
|
||||
plan + '\n## Historical note\n"The legacy regression suite is no longer required."',
|
||||
plan.replace(fixture.task, '- [ ] T0 — renderer — Test literal output\n - Verify: renders "This is a hypothetical example."\n\n' + fixture.task),
|
||||
]) { expect(p).not.toBe(plan); expect(evaluate(p).regression).toBe('plan'); }
|
||||
});
|
||||
|
||||
test('the mandatory declaration, numbered task and linked verification are all necessary', () => {
|
||||
for (const p of [
|
||||
plan.replace(fixture.required, ''), plan.replace(fixture.task, ''), plan.replace(fixture.verification, ''),
|
||||
plan.replace('regression rule, mandatory', 'optional future idea'),
|
||||
plan.replace('Capture current outputs', 'Describe proposed outputs'),
|
||||
plan.replace('BEFORE any change', 'AFTER the rewrite'),
|
||||
plan.replace('assert identical behaviour', 'accept different behaviour'),
|
||||
plan.replace('`legacyAuthFlow` golden-master', '`newAuthFlow` golden-master'),
|
||||
plan.replace('fixtures for legacyAuthFlow', 'fixtures for newAuthFlow'),
|
||||
plan.replace('fixtures for legacyAuthFlow before any change', 'fixtures for legacyAuthFlow after the rewrite'),
|
||||
plan.replace('fixtures pass against untouched legacy', 'fixtures pass against modified legacy'),
|
||||
plan.replace('rerun after every later task', 'rerun optionally after launch'),
|
||||
plan.replace('1. Run T1 fixtures', '1. Run T9 fixtures'),
|
||||
plan.replace('2. After each task, rerun the full suite plus T1 fixtures.', '2. After each task, rerun the full suite plus T9 fixtures.'),
|
||||
plan.replace('before touching anything; they must pass', 'after rewriting legacy; they may pass'),
|
||||
plan.replace('1. Run T1 fixtures', '3. Run T1 fixtures'),
|
||||
]) { expect(p).not.toBe(plan); expect(evaluate(p).regression).toBeUndefined(); }
|
||||
});
|
||||
|
||||
test('source, conditional and optional owners cannot provide current mandatory evidence', () => {
|
||||
for (const p of [
|
||||
'# Source\n\n' + plan,
|
||||
'# Hypothetical example\n\n' + plan,
|
||||
'The following is source text only.\n\n' + plan,
|
||||
plan.replace(fixture.required, '```md\n' + fixture.required + '\n```'),
|
||||
plan.replace(fixture.task, fixture.task.split('\n').map(s => '> ' + s).join('\n')),
|
||||
plan.replace(fixture.verification, '```md\n' + fixture.verification + '\n```'),
|
||||
plan.replace('**CRITICAL', 'If approved:\n**CRITICAL'),
|
||||
plan.replace('**CRITICAL', 'The following is a quoted source excerpt.\n**CRITICAL'),
|
||||
plan.replace('**CRITICAL', 'Source excerpt:\n\n**CRITICAL'),
|
||||
plan.replace(/Capture current outputs[\s\S]*?no existing coverage\./, claim => '`' + claim + '`'),
|
||||
plan.replace(fixture.task, 'If approved:\n' + fixture.task),
|
||||
plan.replace(fixture.task, 'The following is a quoted source excerpt.\n' + fixture.task),
|
||||
plan.replace('## Implementation Tasks', '## Optional Implementation Tasks'),
|
||||
plan.replace('## Verification', '## Quoted Verification'),
|
||||
plan.replace('1. Run T1', 'If approved:\n1. Run T1'),
|
||||
plan.replace('1. Run T1', 'The following is a quoted source excerpt.\n1. Run T1'),
|
||||
plan.replace('1. Run T1', 'Source excerpt:\n\n1. Run T1'),
|
||||
plan.replace(' - Verify:', ' If approved:\n - Verify:'),
|
||||
]) { expect(p).not.toBe(plan); expect(evaluate(p).regression).toBeUndefined(); }
|
||||
});
|
||||
|
||||
test('a previous unrelated task cannot hide a source or conditional prefix', () => {
|
||||
for (const prefix of ['If approved:', 'The following is a quoted source excerpt.']) {
|
||||
const p = plan.replace(fixture.task, '- [ ] T0 — setup — Prepare fixtures\n - Verify: setup passes.\n\n' + prefix + '\n' + fixture.task);
|
||||
expect(evaluate(p).regression).toBeUndefined();
|
||||
}
|
||||
});
|
||||
|
||||
test('the required suite, numbered task, and unchanged verification remain withdrawable', () => {
|
||||
for (const p of [
|
||||
plan.replace(fixture.required, fixture.required + '\nThis suite is withdrawn.'),
|
||||
plan.replace(fixture.task, fixture.task + '\nT1 is cancelled.'),
|
||||
plan + '\n## Assessment of T1\nT1 is rejected.',
|
||||
plan + '\n## Final regression suite assessment\nThe regression suite is no longer required.',
|
||||
plan + '\n## Payment regression suite\nThe legacy regression suite is no longer required.',
|
||||
plan.replace(fixture.verification, fixture.verification + '\nThis baseline is no longer required.'),
|
||||
plan.replace(fixture.task, fixture.task + '\nCorrection: this unchanged-code verification is withdrawn.'),
|
||||
plan.replace(fixture.verification, fixture.verification + '\nCorrection: the T1 rerun is withdrawn.'),
|
||||
plan + '\n## Final regression assessment\nThe golden-master fixtures are no longer required.',
|
||||
plan + '\n## Payment regression suite\nThe legacy golden-master fixtures are withdrawn.',
|
||||
plan.replace('T1 (P1,', 'T1 (optional,'),
|
||||
]) { expect(p).not.toBe(plan); expect(evaluate(p).regression).toBeUndefined(); }
|
||||
});
|
||||
|
||||
test('the four completed owned decisions and final review report remain required', () => {
|
||||
expect(evaluate().missing).toEqual([]);
|
||||
expect(new Set(Object.values(evaluate().decisions)).size).toBe(4);
|
||||
expect(evaluate(plan.replace(fixture.reviewReport, '')).ok).toBe(false);
|
||||
for (const mutate of [
|
||||
(t: PlanCountTranscript) => { t.calls[0]!.answered = false; },
|
||||
(t: PlanCountTranscript) => { t.calls[0]!.sessionId = 'foreign'; },
|
||||
(t: PlanCountTranscript) => { t.calls[0]!.answeredAt = new Date(start - 1).toISOString(); },
|
||||
(t: PlanCountTranscript) => { t.calls[0]!.answeredAt = new Date(end + 1).toISOString(); },
|
||||
(t: PlanCountTranscript) => { t.calls.push(structuredClone(t.calls[0]!)); },
|
||||
]) { const t = native(); mutate(t); expect(evaluate(plan, t).ok).toBe(false); }
|
||||
});
|
||||
|
||||
test('only the existing Eng finding-count owner selects these public evidence regressions', () => {
|
||||
for (const file of ['test/eng-golden-master-al.test.ts', 'test/fixtures/eng-golden-master-al.json']) {
|
||||
expect(selectTests([file], E2E_TOUCHFILES, []).selected).toEqual(['plan-eng-finding-count']);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,111 @@
|
||||
import { expect, test } from 'bun:test';
|
||||
import { nativeSeededPlanSelection } from './helpers/plan-scope-selection';
|
||||
import type { NativePublicToolEvent, PlanCountTranscript } from './helpers/plan-count-transcript';
|
||||
import { E2E_TOUCHFILES, selectTests } from './helpers/touchfiles';
|
||||
import fixture from './fixtures/eng-option-b-scope-al.json';
|
||||
|
||||
const input = (attempt = 1) => structuredClone(fixture.attempts[attempt]!.projection);
|
||||
type Input = ReturnType<typeof input>;
|
||||
const verdict = (p = input()) => nativeSeededPlanSelection(p.transcript as PlanCountTranscript, p.tools as NativePublicToolEvent[], p.opts);
|
||||
const declaration = (p: Input) => p.transcript.assistantMessages.find(m => m.text.startsWith("I've selected option B,"))!;
|
||||
|
||||
test('the captured retry selects its named pasted plan while the first retained poll does not', () => {
|
||||
expect(fixture.attempts.map(a => a.rawScopeGateAutoSelectObserved)).toEqual([false, false]);
|
||||
expect(verdict(input(0))).toBe(false);
|
||||
expect(verdict(input(1))).toBe(true);
|
||||
});
|
||||
|
||||
test('the named target is necessary; an unnamed option-B notice supplies no selection', () => {
|
||||
const p = input(); p.transcript.assistantMessages = p.transcript.assistantMessages.filter(m => m !== declaration(p));
|
||||
expect(verdict(p)).toBe(false);
|
||||
for (const replacement of ['option A,', 'option C,', 'option B if approved,', 'option B, possibly']) {
|
||||
const p = input(); declaration(p).text = declaration(p).text.replace('option B,', replacement); expect(verdict(p)).toBe(false);
|
||||
}
|
||||
for (const target of ['Unrelated draft', 'branch diff']) {
|
||||
const p = input(); declaration(p).text = declaration(p).text.replace('Parallelize unit tests', target); expect(verdict(p)).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
test('equivalent current wording and consistently renamed title preserve selection', () => {
|
||||
for (const change of [
|
||||
(s: string) => s.replace("I've", 'I have'),
|
||||
(s: string) => s.replace('Next I', 'Next, I'),
|
||||
(s: string) => s.replace('reviewing the pasted', 'to review the pasted'),
|
||||
(s: string) => s.replace(/\. Next.*$/, '.'),
|
||||
(s: string) => s.replace('Design Doc Check, brain context, and context recovery, along with the Aside probe', 'audit for DESIGN.md'),
|
||||
]) { const p = input(); declaration(p).text = change(declaration(p).text); expect(verdict(p)).toBe(true); }
|
||||
const p = input(); p.opts.seed = p.opts.seed.replace('Parallelize unit tests', 'Build cache invalidation');
|
||||
declaration(p).text = declaration(p).text.replace('Parallelize unit tests', 'Build cache invalidation'); expect(verdict(p)).toBe(true);
|
||||
});
|
||||
|
||||
test('quoted, source, hypothetical, historical and conditional first lines cannot select', () => {
|
||||
for (const prefix of ['> ', ' ', '\t', '```\n', 'Source excerpt:\n', 'Historical example only.\n', 'The following is a hypothetical example. ', 'If approved, ', '"']) {
|
||||
const p = input(); declaration(p).text = prefix + declaration(p).text; expect(verdict(p)).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
test('the same successful post-command Skill completion and current native session are required', () => {
|
||||
for (const change of [
|
||||
(p: Input) => { p.opts.sessionId = 'foreign'; },
|
||||
(p: Input) => { p.transcript.status = 'unavailable'; },
|
||||
(p: Input) => { p.tools = []; },
|
||||
(p: Input) => { p.tools[0]!.input!.skill = 'plan-design-review'; },
|
||||
(p: Input) => { p.tools[1]!.isError = true; },
|
||||
(p: Input) => { p.tools[1]!.sessionId = 'foreign'; },
|
||||
(p: Input) => { p.tools[1]!.toolUseId = 'unrelated'; },
|
||||
(p: Input) => { p.opts.commandStartedAt = Date.parse(p.tools[0]!.timestamp) + 1; },
|
||||
(p: Input) => { declaration(p).timestamp = new Date(Date.parse(p.tools[1]!.timestamp) - 1).toISOString(); },
|
||||
(p: Input) => { declaration(p).sessionId = 'foreign'; },
|
||||
(p: Input) => { p.tools.push(structuredClone(p.tools[1]!)); },
|
||||
]) { const p = input(); change(p); expect(verdict(p)).toBe(false); }
|
||||
});
|
||||
|
||||
test('conditional, questioning and replacement continuations cannot borrow a completed selection', () => {
|
||||
for (const change of [
|
||||
(s: string) => s.replace('draft plan.', 'draft plan if approved.'),
|
||||
(s: string) => s.replace('Next I', 'If approved, I'),
|
||||
(s: string) => s.replace('Aside probe.', 'Aside probe?'),
|
||||
(s: string) => s.replace('Design Doc Check', 'branch diff review instead'),
|
||||
(s: string) => s.replace('Design Doc Check', 'unrelated work'),
|
||||
]) { const p = input(); declaration(p).text = change(declaration(p).text); expect(verdict(p)).toBe(false); }
|
||||
});
|
||||
|
||||
test('same-message or later owned withdrawals and target changes defeat the declaration', () => {
|
||||
for (const correction of ['Correction: this selection is withdrawn.', 'This declaration has been retracted.', 'The selected target is now the branch diff.']) {
|
||||
for (const placement of ['same-line', 'same-message', 'later']) {
|
||||
const p = input(), m = declaration(p);
|
||||
if (placement === 'later') p.transcript.assistantMessages.push({ ...m, timestamp: new Date(Date.parse(m.timestamp) + 1000).toISOString(), text: correction });
|
||||
else m.text += (placement === 'same-line' ? ' ' : '\n') + correction;
|
||||
expect(verdict(p)).toBe(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('foreign, historical and literal corrections do not retract a current named selection', () => {
|
||||
for (const text of ['> This selection is withdrawn.', 'Source excerpt:\nThis selection is withdrawn.', 'A prior assistant said "This selection is withdrawn."', 'The verification suite is withdrawn.', 'Is this selection withdrawn?']) {
|
||||
const p = input(), m = declaration(p); p.transcript.assistantMessages.push({ ...m, timestamp: new Date(Date.parse(m.timestamp) + 1000).toISOString(), text }); expect(verdict(p)).toBe(true);
|
||||
}
|
||||
const p = input(), m = declaration(p); p.transcript.assistantMessages.push({ ...m, sessionId: 'foreign', text: 'This selection is withdrawn.' }); expect(verdict(p)).toBe(true);
|
||||
});
|
||||
|
||||
test('a later explicit reselection follows the existing currentness rule', () => {
|
||||
const p = input(), m = declaration(p); p.transcript.assistantMessages.push({ ...m, timestamp: new Date(Date.parse(m.timestamp) + 1000).toISOString(), text: 'This selection is withdrawn.' }); expect(verdict(p)).toBe(false);
|
||||
p.transcript.assistantMessages.push({ ...m, timestamp: new Date(Date.parse(m.timestamp) + 2000).toISOString() }); expect(verdict(p)).toBe(true);
|
||||
});
|
||||
|
||||
test('both new dependencies select exactly the existing five scope observers', () => {
|
||||
const expected = selectTests(['test/helpers/plan-scope-selection.ts'], E2E_TOUCHFILES, []).selected;
|
||||
expect(expected).toHaveLength(5);
|
||||
for (const path of ['test/eng-option-b-scope-al.test.ts', 'test/fixtures/eng-option-b-scope-al.json']) expect(selectTests([path], E2E_TOUCHFILES, []).selected).toEqual(expected);
|
||||
});
|
||||
|
||||
for (const owner of [
|
||||
'plan-ceo-review-plan-mode', 'plan-eng-review-plan-mode',
|
||||
'plan-design-review-plan-mode', 'plan-devex-review-plan-mode', 'plan-mode-no-op',
|
||||
]) test(`scope dependency registration is dense for ${owner}`, () => {
|
||||
const paths = E2E_TOUCHFILES[owner]!;
|
||||
for (let index = 0; index < paths.length; index++) {
|
||||
expect(Object.hasOwn(paths, index)).toBe(true);
|
||||
expect(typeof paths[index]).toBe('string');
|
||||
}
|
||||
});
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"provenance": {
|
||||
"sourceHead": "c73102357cbc3466d6a3c8d3ad0ac7e3177ce62c",
|
||||
"proofSHA256": "fdb3a655c14b5754dae440ba0255f4da9e1247a3557b68f85d5f7afe43bc750c",
|
||||
"nativeSHA256": "ecca735afd983490f0ea44e93541b1a5694479205522910d5ebb4a2cea2b16f9",
|
||||
"scope": "Exact current pane, before file, pending identity, and latest successful same-file public Write+ack. Original pending Edit input is unpublished and unavailable.",
|
||||
"reconstruction": "Synthetic insertion request uses the exact current visible ten-line tail only; it is not the original unretained Edit body or paid approval evidence."
|
||||
},
|
||||
"cwd": "/tmp/gstack-paid-shard-tVuvKy/tmp/gstack-autoplan-chain-tcPo1b",
|
||||
"ownedStateRoot": "/tmp/gstack-paid-shard-tVuvKy/tmp/gstack-hermetic-1626312-olYxKb/skill-home-deJ6wd/.gstack",
|
||||
"pending": {
|
||||
"source": "pre_tool_use",
|
||||
"sessionId": "a828fa7a-3d8e-48cd-ae1f-e3a9cf0611b2",
|
||||
"toolUseId": "toolu_01NUtCP9y2fRrv6wtKyje5EY",
|
||||
"tool": "Edit",
|
||||
"file": "/tmp/gstack-paid-shard-tVuvKy/tmp/gstack-hermetic-1626312-olYxKb/skill-home-deJ6wd/.gstack/projects/gstack-autoplan-chain-tcPo1b/ceo-plans/2026-09-10-user-dashboard.md",
|
||||
"timestamp": "2026-09-10T08:15:50.752Z"
|
||||
},
|
||||
"before": "---\nstatus: ACTIVE\n---\n# CEO Plan: User Dashboard Page\nGenerated by /plan-ceo-review (via /autoplan) on 2026-09-10\nBranch: main | Mode: SELECTIVE EXPANSION\nRepo: gstack-autoplan-chain-tcPo1b (local, no remote)\n\n## Step 0 \u2014 Premise P0: verify the \"existing\" contracts before building\n\nThe repository reviewed contains no application code (only README.md and the plan). Every \"existing\" item below is taken from the plan author's contract section and is unverified. Before any implementation task starts, confirm each exists and behaves as stated:\n\n- [ ] Cookie session + workspace membership middleware; request context exposes `memberId` and `workspaceId`\n- [ ] Activity and notification list repository methods: latest 20 + cursor, indexed on workspace/member + created_at; note whether they accept a `limit` argument and an `AbortSignal`\n- [ ] Existing item types `ActivityItem`, `NotificationItem` (must carry a read flag such as `readAt`), `QuickAction` (id, label, routeTarget) as used by the current activity/notification pages and the action registry\n- [ ] A notifications unread-count read method, or an indexed `(member_id) WHERE read_at IS NULL` path that a one-line COUNT can use\n- [ ] Action registry: 3 actions with stable IDs, labels, route targets, server-side eligibility predicates\n- [ ] **Existing** member-scoped bulk-read mutation (working name `POST /api/notifications/read-all`): idempotent, marks notifications with `created_at \u2264 snapshotAt`, requires CSRF token\n- [ ] Typed HTTP client errors: unauthenticated, forbidden, validation, retryable-service, network\n- [ ] Dialog primitive with focus trap, Escape dismissal, focus return; Tailwind tokens; responsive page shell; motion tokens or a `prefers-reduced-motion` utility\n- [ ] Vitest + React Testing Library + Playwright in CI; fixtures for member, other-workspace, empty lists, service failures\n- [ ] Staging feature flags with member-cohort targeting (two independent boolean flags can be created); request/error metrics\n- [ ] Existing analytics events: login, action start, action completion, permission error; confirm they accept a context/source attribute (needed for `source: 'dashboard'`)\n- [ ] Existing full pages for activity and notifications (targets of the \"View all\" links); existing post-login redirect code and its `returnTo` handling\n\nIf any box fails, the Eng phase reopens the affected decision before implementation. Known fallbacks: if repos do not accept an `AbortSignal`, a thin signal-aware wrapper (`runSubQuery`) enters blast radius and the timeout is a `Promise.race` that abandons the still-running query (accepted: bounded user wait, not bounded DB work). If no unread-count method exists, a single indexed COUNT query is added inside `DashboardService` (a read, no schema change).\n\n## Success criteria\n\nMetric: median time from login to first completed task. Baseline: 75s (team walkthrough, small sample). Target: 45s. Guardrails: completed-task rate must not regress overall or for the high-frequency member segment; permission-error rate for actions started from the dashboard must not exceed the permission-error rate for the same actions started from the current three-page flow (control cohort). Existing action start / completion / permission-error events gain a `source: 'dashboard' | 'legacy'` attribute so both comparisons are possible. Baseline instrumentation of the current three-page flow runs in parallel with the build and must be live before the first redirect step.\n\n## Vision\n\n### 10x Check\nThe login surface that knows what you should do next: assigned work ranked by urgency, alerts that block that work, and a digest of what changed since your last visit, each one a single keypress away. This plan builds the home surface and the two shared primitives (toast, confirm dialog) that version needs; the ranking and digest services are explicitly deferred to the personalization plan.\n\n## Approach \u2014 PROVISIONAL pending T2 (aggregate endpoint) at the Final Gate\n\nOne aggregate `GET /api/dashboard`, no request parameters, scoped from request context only. Three sub-queries run in parallel through `runSubQuery(name, fn, signal)` and `Promise.allSettled`.\n\n```ts\ntype Envelope<T> = { ok: true; data: T } | { ok: false; error: { code: 'timeout' | 'unavailable' } };\ntype DashboardResponse = {\n activity: Envelope<ActivityItem[]>; // 5 items\n notifications: Envelope<NotificationItem[]> & { snapshotAt: string; unreadCount: number }; // 5 items\n quickActions: Envelope<QuickAction[]>; // 0-3 items\n meta: { serverTime: string; requestId: string };\n};\n```\n\nRow limits: each list panel shows 5 rows. The endpoint passes `limit: 5` if the repository method accepts it; otherwise it slices the returned 20 server-side. `unreadCount` is the member's total unread count, not the count within the 5 rows. `snapshotAt` is the datLine truncated
|
||||
"viewport": " 140 +4. **DB-clock `snapshotAt` \"same round trip\" touches the repo** \u2014 ADOPTED. `SELECT now()` runs in its own round tr\n +ip immediately before the notifications list query. A notification inserted between the two stays unread (the safe\n + direction). The ordering test inserts between the clock read and the list query as well as between GET and POST. \n 141 +5. **Notifications type requires `snapshotAt`/`unreadCount` on `ok:false`** \u2014 ADOPTED. `notifications: Envelope<{ \n +items: NotificationItem[]; snapshotAt: string; unreadCount: number }>`. `unreadCount` is computed inside the notif\n +ications sub-query and shares its envelope. \n 142 +6. **`dashboard_view` undefined** \u2014 ADOPTED. Emitted once per page mount on the first 200 response from the endpoi\n +nt (regardless of envelope statuses). New events/metrics are explicit work items: `dashboard_view`, `panel_rendere\n +d{panel,state}`, `quick_action_click{actionId}`, `mark_all_read{result}`, `dashboard_request_total{status}`, `dash\n +board_request_ms`, `dashboard_panel_status{panel,status}`, `dashboard_predicate_error_total{actionId}`. \n 143 +7. **`redirect=on, route=off`** \u2014 ADOPTED. The post-login redirect applies only when both flags are on for the mem\n +ber; the route flag alone never redirects. \n 144 +8. **\"Stale `snapshotAt` \u2192 400\" not in the contract** \u2014 ADOPTED. Step 0 gains \"bulk-read rejects malformed or futu\n +re `snapshotAt` with a validation error\"; if it does not, the client guard (refetch then retry once) is the only d\n +efense and the copy stays \"Couldn't mark as read. Reload and try again.\" \n 145 +9. **\"Guardrails flat\" has no threshold** \u2014 ADOPTED. Per step: completed-task rate for the redirect cohort within \n +2 percentage points of the control cohort (overall and for the high-frequency segment); permission-error rate for \n +dashboard-sourced actions no more than 1 percentage point above the legacy-sourced rate for the same actions. \n 146 +10. **Row anatomy, md breakpoint, Escape collision, TimeoutError mapping, unreadCount placement** \u2014 ADOPTED. Notif\n +ication row: unread dot with `aria-label=\"Unread\"`, title (two-line clamp), relative time; panel heading shows \"No\n +tifications (N)\" from `unreadCount`. Activity row: actor, summary (two-line clamp), relative time. Layout is stack\n +ed below `lg`, two-column at `lg` and above. Toast Escape dismisses only when focus is inside the toast region, so\n + it never competes with the dialog's Escape. `runSubQuery` maps the `TimeoutError` DOMException (or the race timeo\n +ut) to `RepoTimeoutError`. \n 147 +11. **Unknown sub-query error \u2192 whole-page 500** \u2014 DECLINED, intentional. An unknown exception class is a bug, not\n + an outage; hiding it as `unavailable` would render a green-looking page over a defect. The 500 carries `requestId\n +`, the page shows a page-level error with Retry, and the alert fires. Trade-off recorded. \n 148 +12. **500-event threshold may be unreachable at 5%** \u2014 ADOPTED. Hold rule: at least 24h and at least 500 `dashboar\n +d_view` events, capped at 7 days; after 7 days proceed if guardrails are within threshold on whatever volume exist\n +s. \n 149 +13. **Toast stacking ahead of need** \u2014 DECLINED. The cap and eviction are a few lines; the primitive is app-root b\n +y design and the plan names a toast *system*. \n\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\u254c\n Do you want to make this edit to 2026-09-10-user-dashboard.md?\n \u276f 1. Yes\n 2. Yes, and switch to accept edits (auto-approve file edits and common file commands) for this session (shift+tab)\n 3. No\n\n Esc to cancel \u00b7 Tab to amend\n",
|
||||
"events": [
|
||||
{
|
||||
"sessionId": "a828fa7a-3d8e-48cd-ae1f-e3a9cf0611b2",
|
||||
"timestamp": "2026-09-10T08:12:51.142Z",
|
||||
"toolUseId": "toolu_01UB3V8rTihFzkB6i7Hhbe8B",
|
||||
"kind": "use",
|
||||
"name": "Write",
|
||||
"input": {
|
||||
"file_path": "/tmp/gstack-paid-shard-tVuvKy/tmp/gstack-hermetic-1626312-olYxKb/skill-home-deJ6wd/.gstack/projects/gstack-autoplan-chain-tcPo1b/ceo-plans/2026-09-10-user-dashboard.md",
|
||||
"content": "---\nstatus: ACTIVE\n---\n# CEO Plan: User Dashboard Page\nGenerated by /plan-ceo-review (via /autoplan) on 2026-09-10\nBranch: main | Mode: SELECTIVE EXPANSION\nRepo: gstack-autoplan-chain-tcPo1b (local, no remote)\n\n## Step 0 \u2014 Premise P0: verify the \"existing\" contracts before building\n\nThe repository reviewed contains no application code (only README.md and the plan). Every \"existing\" item below is taken from the plan author's contract section and is unverified. Before any implementation task starts, confirm each exists and behaves as stated:\n\n- [ ] Cookie session + workspace membership middleware; request context exposes `memberId` and `workspaceId`\n- [ ] Activity and notification list repository methods: latest 20 + cursor, indexed on workspace/member + created_at; note whether they accept a `limit` argument and an `AbortSignal`\n- [ ] Existing item types `ActivityItem`, `NotificationItem` (must carry a read flag such as `readAt`), `QuickAction` (id, label, routeTarget) as used by the current activity/notification pages and the action registry\n- [ ] A notifications unread-count read method, or an indexed `(member_id) WHERE read_at IS NULL` path that a one-line COUNT can use\n- [ ] Action registry: 3 actions with stable IDs, labels, route targets, server-side eligibility predicates\n- [ ] **Existing** member-scoped bulk-read mutation (working name `POST /api/notifications/read-all`): idempotent, marks notifications with `created_at \u2264 snapshotAt`, requires CSRF token\n- [ ] Typed HTTP client errors: unauthenticated, forbidden, validation, retryable-service, network\n- [ ] Dialog primitive with focus trap, Escape dismissal, focus return; Tailwind tokens; responsive page shell; motion tokens or a `prefers-reduced-motion` utility\n- [ ] Vitest + React Testing Library + Playwright in CI; fixtures for member, other-workspace, empty lists, service failures\n- [ ] Staging feature flags with member-cohort targeting (two independent boolean flags can be created); request/error metrics\n- [ ] Existing analytics events: login, action start, action completion, permission error; confirm they accept a context/source attribute (needed for `source: 'dashboard'`)\n- [ ] Existing full pages for activity and notifications (targets of the \"View all\" links); existing post-login redirect code and its `returnTo` handling\n\nIf any box fails, the Eng phase reopens the affected decision before implementation. Known fallbacks: if repos do not accept an `AbortSignal`, a thin signal-aware wrapper (`runSubQuery`) enters blast radius and the timeout is a `Promise.race` that abandons the still-running query (accepted: bounded user wait, not bounded DB work). If no unread-count method exists, a single indexed COUNT query is added inside `DashboardService` (a read, no schema change).\n\n## Success criteria\n\nMetric: median time from login to first completed task. Baseline: 75s (team walkthrough, small sample). Target: 45s. Guardrails: completed-task rate must not regress overall or for the high-frequency member segment; permission-error rate for actions started from the dashboard must not exceed the permission-error rate for the same actions started from the current three-page flow (control cohort). Existing action start / completion / permission-error events gain a `source: 'dashboard' | 'legacy'` attribute so both comparisons are possible. Baseline instrumentation of the current three-page flow runs in parallel with the build and must be live before the first redirect step.\n\n## Vision\n\n### 10x Check\nThe login surface that knows what you should do next: assigned work ranked by urgency, alerts that block that work, and a digest of what changed since your last visit, each one a single keypress away. This plan builds the home surface and the two shared primitives (toast, confirm dialog) that version needs; the ranking and digest services are explicitly deferred to the personalization plan.\n\n## Approach \u2014 PROVISIONAL pending T2 (aggregate endpoint) at the Final Gate\n\nOne aggregate `GET /api/dashboard`, no request parameters, scoped from request context only. Three sub-queries run in parallel through `runSubQuery(name, fn, signal)` and `Promise.allSettled`.\n\n```ts\ntype Envelope<T> = { ok: true; data: T } | { ok: false; error: { code: 'timeout' | 'unavailable' } };\ntype DashboardResponse = {\n activity: Envelope<ActivityItem[]>; // 5 items\n notifications: Envelope<NotificationItem[]> & { snapshotAt: string; unreadCount: number }; // 5 items\n quickActions: Envelope<QuickAction[]>; // 0-3 items\n meta: { serverTime: string; requestId: string };\n};\n```\n\nRow limits: each list panel shows 5 rows. The endpoint passes `limit: 5` if the repository method accepts it; otherwise it slices the returned 20 server-side. `unreadCount` is the member's total unread count, not the count within the 5 rows. `snapshotAt` is Line truncated
|
||||
}
|
||||
},
|
||||
{
|
||||
"sessionId": "a828fa7a-3d8e-48cd-ae1f-e3a9cf0611b2",
|
||||
"timestamp": "2026-09-10T08:12:54.651Z",
|
||||
"toolUseId": "toolu_01UB3V8rTihFzkB6i7Hhbe8B",
|
||||
"kind": "result",
|
||||
"content": "The file /tmp/gstack-paid-shard-tVuvKy/tmp/gstack-hermetic-1626312-olYxKb/skill-home-deJ6wd/.gstack/projects/gstack-autoplan-chain-tcPo1b/ceo-plans/2026-09-10-user-dashboard.md has been updated successfully. (file state is current in your context \u2014 no need to Read it back)",
|
||||
"isError": false
|
||||
}
|
||||
],
|
||||
"reconstructedRequest": {
|
||||
"old_string": "Owner: the user.\n",
|
||||
"new_string": "Owner: the user.\n\n4. **DB-clock `snapshotAt` \"same round trip\" touches the repo** \u2014 ADOPTED. `SELECT now()` runs in its own round trip immediately before the notifications list query. A notification inserted between the two stays unread (the safe direction). The ordering test inserts between the clock read and the list query as well as between GET and POST.\n5. **Notifications type requires `snapshotAt`/`unreadCount` on `ok:false`** \u2014 ADOPTED. `notifications: Envelope<{ items: NotificationItem[]; snapshotAt: string; unreadCount: number }>`. `unreadCount` is computed inside the notifications sub-query and shares its envelope.\n6. **`dashboard_view` undefined** \u2014 ADOPTED. Emitted once per page mount on the first 200 response from the endpoint (regardless of envelope statuses). New events/metrics are explicit work items: `dashboard_view`, `panel_rendered{panel,state}`, `quick_action_click{actionId}`, `mark_all_read{result}`, `dashboard_request_total{status}`, `dashboard_request_ms`, `dashboard_panel_status{panel,status}`, `dashboard_predicate_error_total{actionId}`.\n7. **`redirect=on, route=off`** \u2014 ADOPTED. The post-login redirect applies only when both flags are on for the member; the route flag alone never redirects.\n8. **\"Stale `snapshotAt` \u2192 400\" not in the contract** \u2014 ADOPTED. Step 0 gains \"bulk-read rejects malformed or future `snapshotAt` with a validation error\"; if it does not, the client guard (refetch then retry once) is the only defense and the copy stays \"Couldn't mark as read. Reload and try again.\"\n9. **\"Guardrails flat\" has no threshold** \u2014 ADOPTED. Per step: completed-task rate for the redirect cohort within 2 percentage points of the control cohort (overall and for the high-frequency segment); permission-error rate for dashboard-sourced actions no more than 1 percentage point above the legacy-sourced rate for the same actions.\n10. **Row anatomy, md breakpoint, Escape collision, TimeoutError mapping, unreadCount placement** \u2014 ADOPTED. Notification row: unread dot with `aria-label=\"Unread\"`, title (two-line clamp), relative time; panel heading shows \"Notifications (N)\" from `unreadCount`. Activity row: actor, summary (two-line clamp), relative time. Layout is stacked below `lg`, two-column at `lg` and above. Toast Escape dismisses only when focus is inside the toast region, so it never competes with the dialog's Escape. `runSubQuery` maps the `TimeoutError` DOMException (or the race timeout) to `RepoTimeoutError`.\n11. **Unknown sub-query error \u2192 whole-page 500** \u2014 DECLINED, intentional. An unknown exception class is a bug, not an outage; hiding it as `unavailable` would render a green-looking page over a defect. The 500 carries `requestId`, the page shows a page-level error with Retry, and the alert fires. Trade-off recorded.\n12. **500-event threshold may be unreachable at 5%** \u2014 ADOPTED. Hold rule: at least 24h and at least 500 `dashboard_view` events, capped at 7 days; after 7 days proceed if guardrails are within threshold on whatever volume exists.\n13. **Toast stacking ahead of need** \u2014 DECLINED. The cap and eviction are a few lines; the primitive is app-root by design and the plan names a toast *system*.\n"
|
||||
}
|
||||
}
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
{
|
||||
"sourceHead": "c73102357cbc3466d6a3c8d3ad0ac7e3177ce62c",
|
||||
"sourceObservationSHA256": "2a1a437e68cee9fbef60bb5cb619deb0eb51faabfdd2fa822822b212d576a554",
|
||||
"calls": [
|
||||
{
|
||||
"sessionId": "49908e50-9673-4962-82b2-7beb6ab6550e",
|
||||
"toolUseId": "toolu_01C2v9YKfPda2mqfNeMDGfq8",
|
||||
"questions": [
|
||||
{
|
||||
"question": "D4 (Finding 2) — What happens when the confirmation email fails after the payment update has committed?\nProject/branch/task: gstack-plan-count-wGAjut on main, Section 2 of the CEO review of PLAN.md.\nELI10: The plan sends the email inline with 'no error handling on the email leg'. The shared mail client rethrows, so a mail outage becomes an HTTP 500 after the user is already marked paid. Stripe then retries the whole payment event for up to three days, the webhook-failure alert pages on-call for a mail problem, and if the email was actually sent but the client timed out, the retry sends it again. Your own runbook says: retry only the failed notification, never replay the payment. As written, the code forces the replay the runbook forbids. Stripe also gives you 10 seconds per delivery, so a slow mail call inside the request can fail the webhook even when everything worked.\nStakes if we pick wrong: a mail provider incident turns into a payment-webhook incident, with duplicate emails and a failing endpoint in the Stripe dashboard.\nRecommendation: A because it keeps the runbook's contract (payment committed, notification retried on its own), names each mail exception instead of a catch-all, and stays visible through the mail client's existing failure metric and on-call alert.\nCompleteness: A=9/10, B=10/10 only if a job queue already exists, C=2/10\nNet: rescue the mail leg narrowly and return 200 (A), move the send off the request path entirely (B, needs queue infra), or leave the 500 and let Stripe replay the payment (C).",
|
||||
"header": "Email leg",
|
||||
"multiSelect": false,
|
||||
"options": [
|
||||
{
|
||||
"label": "4A: Commit, then send with a narrow rescue (recommended)",
|
||||
"description": "Order: lookup, update, COMMIT, then notify. Rescue only the mail client's named classes (DeliveryError, Timeout, RateLimited, InvalidRecipient; real names per the client). On rescue: log at warn with event ID, user ID, exception class; the shared client's tracing and failure-rate metric plus the existing on-call alert make it visible; handler returns success so Stripe gets 200 and the runbook's notification-retry path owns the redelivery. Bound the mail call timeout so total handler time stays under Stripe's 10 s deadline. Recipient is the looked-up user's stored email, never a value from the Stripe payload. Tests: email raises each class → 200, payment committed, warn line present. Effort S (human ~2h / CC ~10 min).\n✅ Mail outage no longer produces payment-webhook 500s or duplicate payment processing\n✅ Matches the runbook: committed payment, failed notification retried alone\n❌ Timeout-after-send can still yield one duplicate email when the runbook retries; bounded and visible"
|
||||
},
|
||||
{
|
||||
"label": "4B: Commit, then enqueue the email to a background job",
|
||||
"description": "Same ordering, but the handler enqueues a notification job after commit and never calls the mail provider inline. The job retries with backoff and its failures surface via the existing mail metrics. Only viable if the repo already has a job queue; adding one is out of HOLD SCOPE. Effort M (human ~1 day / CC ~30 min).\n✅ Request path never waits on the mail provider; Stripe deadline is never at risk\n✅ Retries with backoff are automatic instead of runbook-driven\n❌ Requires queue infrastructure the plan does not mention; if absent this becomes an expansion"
|
||||
},
|
||||
{
|
||||
"label": "4C: Keep as written (inline, unrescued)",
|
||||
"description": "Email exception propagates to the ingress wrapper, HTTP 500, Stripe retries the event. Effort none.\n✅ Zero new code on the email leg\n✅ Stripe's retry eventually delivers the email if the provider recovers within three days\n❌ Contradicts the runbook, pages on-call for mail incidents as webhook failures, and duplicates emails on timeout-after-send"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"answered": true,
|
||||
"failed": false,
|
||||
"answers": {
|
||||
"D4 (Finding 2) — What happens when the confirmation email fails after the payment update has committed?\nProject/branch/task: gstack-plan-count-wGAjut on main, Section 2 of the CEO review of PLAN.md.\nELI10: The plan sends the email inline with 'no error handling on the email leg'. The shared mail client rethrows, so a mail outage becomes an HTTP 500 after the user is already marked paid. Stripe then retries the whole payment event for up to three days, the webhook-failure alert pages on-call for a mail problem, and if the email was actually sent but the client timed out, the retry sends it again. Your own runbook says: retry only the failed notification, never replay the payment. As written, the code forces the replay the runbook forbids. Stripe also gives you 10 seconds per delivery, so a slow mail call inside the request can fail the webhook even when everything worked.\nStakes if we pick wrong: a mail provider incident turns into a payment-webhook incident, with duplicate emails and a failing endpoint in the Stripe dashboard.\nRecommendation: A because it keeps the runbook's contract (payment committed, notification retried on its own), names each mail exception instead of a catch-all, and stays visible through the mail client's existing failure metric and on-call alert.\nCompleteness: A=9/10, B=10/10 only if a job queue already exists, C=2/10\nNet: rescue the mail leg narrowly and return 200 (A), move the send off the request path entirely (B, needs queue infra), or leave the 500 and let Stripe replay the payment (C).": "4A: Commit, then send with a narrow rescue (recommended)"
|
||||
},
|
||||
"unansweredQuestionIndices": [],
|
||||
"answeredAt": "2026-09-10T08:01:09.223Z"
|
||||
},
|
||||
{
|
||||
"sessionId": "49908e50-9673-4962-82b2-7beb6ab6550e",
|
||||
"toolUseId": "toolu_013ZWhw6pGLUNzGpSrdLhi19",
|
||||
"questions": [
|
||||
{
|
||||
"question": "D5 (Finding 3) — How does the lookup query receive `request.params.userId`?\nProject/branch/task: gstack-plan-count-wGAjut on main, Section 3 of the CEO review of PLAN.md.\nELI10: The plan pastes the user ID string straight into a raw SQL fragment. Your contracts say the adapter forwards that string unchanged, the signature and ownership checks do not make it SQL-safe, and user IDs are opaque text that can contain quotes and Unicode. So a legitimate ID like O'Brien breaks the query and returns HTTP 500 on every Stripe retry for three days, and a hostile ID that matches its stored binding can read or change other rows. The fix is the standard one: pass the ID as a bound parameter through the existing DB client, so the database never parses it as SQL.\nStakes if we pick wrong: a poison event that can never succeed, or a database read/write outside the intended user row.\nRecommendation: A because a bound parameter is the only mechanism that is correct for every nonempty string the contracts allow, and it uses the DB client you already have.\nCompleteness: A=10/10, B=5/10, C=1/10\nNet: bind (A), hand-escape (B, fragile and still not a defense against all encodings), or leave the fragment (C).",
|
||||
"header": "SQL lookup",
|
||||
"multiSelect": false,
|
||||
"options": [
|
||||
{
|
||||
"label": "5A: Bound query parameter via the existing DB client (recommended)",
|
||||
"description": "Lookup is `WHERE id = ?` (or the client's named-bind form) with userId passed as a parameter; no interpolation anywhere in the handler or service. Same for the payment_intent_id write. Tests: IDs `O'Brien`, `'; DROP TABLE users; --`, `用户-☃`, and a 1000-char string each return the correct row or nil with no statement error; a static grep in CI or review for string-built SQL in `Payments::`. Failure visibility: any DbClient error still propagates to the ingress 500 path per contract. Effort S (human ~1h / CC ~5 min).\n✅ Correct for every opaque TEXT ID the contracts allow, including punctuation and Unicode\n✅ Reuses the DB client's existing binding; zero new code paths or dependencies\n❌ None beyond writing the tests; this is the default form of the query"
|
||||
},
|
||||
{
|
||||
"label": "5B: Escape or quote the string before interpolating",
|
||||
"description": "Keep the raw fragment but run userId through the DB client's quote/escape helper first. Effort S (human ~1h / CC ~5 min).\n✅ Small textual change to the plan's current sketch\n✅ Stops the simple quote-in-name breakage\n❌ Depends on the helper matching the connection's charset and driver; one wrong encoding path and injection is back. Fragile where a bind is free"
|
||||
},
|
||||
{
|
||||
"label": "5C: Keep the raw SQL fragment as written",
|
||||
"description": "No change. Effort none.\n✅ Nothing to implement\n✅ Works for IDs that happen to contain no special characters\n❌ Any quote in a legitimate ID yields a permanent 500; a hostile ID with a valid binding can reach other users' rows"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"answered": true,
|
||||
"failed": false,
|
||||
"answers": {
|
||||
"D5 (Finding 3) — How does the lookup query receive `request.params.userId`?\nProject/branch/task: gstack-plan-count-wGAjut on main, Section 3 of the CEO review of PLAN.md.\nELI10: The plan pastes the user ID string straight into a raw SQL fragment. Your contracts say the adapter forwards that string unchanged, the signature and ownership checks do not make it SQL-safe, and user IDs are opaque text that can contain quotes and Unicode. So a legitimate ID like O'Brien breaks the query and returns HTTP 500 on every Stripe retry for three days, and a hostile ID that matches its stored binding can read or change other rows. The fix is the standard one: pass the ID as a bound parameter through the existing DB client, so the database never parses it as SQL.\nStakes if we pick wrong: a poison event that can never succeed, or a database read/write outside the intended user row.\nRecommendation: A because a bound parameter is the only mechanism that is correct for every nonempty string the contracts allow, and it uses the DB client you already have.\nCompleteness: A=10/10, B=5/10, C=1/10\nNet: bind (A), hand-escape (B, fragile and still not a defense against all encodings), or leave the fragment (C).": "5A: Bound query parameter via the existing DB client (recommended)"
|
||||
},
|
||||
"unansweredQuestionIndices": [],
|
||||
"answeredAt": "2026-09-10T08:01:49.453Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"provenance": {
|
||||
"sourceHead": "c73102357cbc3466d6a3c8d3ad0ac7e3177ce62c",
|
||||
"run": "ship-source-al-delta-paid-20260910-v1",
|
||||
"job": 3,
|
||||
"call": "toolu_01K7VggKijiLKU8q4npuhHQG",
|
||||
"session": "cf23eff4-0c82-4e37-a38e-5a2475744a4c",
|
||||
"scope": "Exact completed public question; replay does not revise the live outcome."
|
||||
},
|
||||
"fingerprint": {
|
||||
"signature": "cf23eff4-0c82-4e37-a38e-5a2475744a4c:toolu_01K7VggKijiLKU8q4npuhHQG",
|
||||
"promptSnippet": "Issue 1 D3 \u2014 Issue 1: How should the header actions establish that Save is the primary action? Project/branch/task: branch main, Settings Page UI redesign, Pass 1 Information Architecture. ELI10: Right now Save, Reset, Cancel, and Export lo",
|
||||
"options": [
|
||||
{
|
||||
"index": 1,
|
||||
"label": "1A Primary + ghost (recommended)"
|
||||
},
|
||||
{
|
||||
"index": 2,
|
||||
"label": "1B Save primary only"
|
||||
},
|
||||
{
|
||||
"index": 3,
|
||||
"label": "1C Defer"
|
||||
}
|
||||
],
|
||||
"observedAtMs": 242723,
|
||||
"preReview": true,
|
||||
"nativeCall": {
|
||||
"sessionId": "cf23eff4-0c82-4e37-a38e-5a2475744a4c",
|
||||
"toolUseId": "toolu_01K7VggKijiLKU8q4npuhHQG",
|
||||
"questions": [
|
||||
{
|
||||
"question": "D3 \u2014 Issue 1: How should the header actions establish that Save is the primary action?\nProject/branch/task: branch main, Settings Page UI redesign, Pass 1 Information Architecture.\nELI10: Right now Save, Reset, Cancel, and Export look identical. A user scanning the header cannot tell which button does the thing they came for, and at 640px and below Save already sits alone on its own row while looking like a secondary control. DESIGN.md already decides this: Save is the only filled primary button, the other three are neutral ghost buttons. Principle: hierarchy as service; if everything shouts, nothing wins.\nStakes if we pick wrong: users hesitate or hit Reset/Cancel by mistake, and the confirmation dialog becomes a safety net for a hierarchy problem.\nRecommendation: 1A because it applies the existing Button variants with zero new tokens and matches the approved design system exactly.\nCompleteness: 1A=10/10, 1B=6/10, 1C=3/10\nPros / cons:\n1A) Save filled primary (#1d4ed8, white text); Reset, Cancel, Export as neutral ghost buttons (recommended) (human: ~1h / CC: ~5min)\n \u2705 Exact DESIGN.md tokens via the existing Button component variants, nothing new to invent\n \u2705 Primary reads first at every viewport, including the full-width Save row at 640px and below\n \u274c Touches the button variant props on all four actions, so a small visual regression check is needed\n1B) Save filled primary; leave Reset, Cancel, Export exactly as they render today (human: ~30min / CC: ~3min)\n \u2705 Smallest possible change, only the Save button's variant flips\n \u2705 Still gives the header one obvious primary control for the 3-second scan\n \u274c Three secondary buttons stay in whatever inconsistent style they have now, so the group still does not match DESIGN.md\n1C) Defer: keep all four identical for this update (human: 0 / CC: 0)\n \u2705 No implementation work in this PR\n \u2705 Avoids any styling change to the action group\n \u274c Ships a documented DESIGN.md violation and leaves the page with no visible primary action\nNet: full alignment with the approved Button variants versus a partial or deferred fix that leaves the header ambiguous.",
|
||||
"header": "Issue 1",
|
||||
"multiSelect": false,
|
||||
"options": [
|
||||
{
|
||||
"label": "1A Primary + ghost (recommended)",
|
||||
"description": "Save filled #1d4ed8/white; Reset, Cancel, Export neutral ghost. Exact DESIGN.md."
|
||||
},
|
||||
{
|
||||
"label": "1B Save primary only",
|
||||
"description": "Flip only Save to the primary variant; leave the other three untouched."
|
||||
},
|
||||
{
|
||||
"label": "1C Defer",
|
||||
"description": "Keep all four identical; record as an open DESIGN.md violation."
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"answered": true,
|
||||
"failed": false,
|
||||
"answers": {
|
||||
"D3 \u2014 Issue 1: How should the header actions establish that Save is the primary action?\nProject/branch/task: branch main, Settings Page UI redesign, Pass 1 Information Architecture.\nELI10: Right now Save, Reset, Cancel, and Export look identical. A user scanning the header cannot tell which button does the thing they came for, and at 640px and below Save already sits alone on its own row while looking like a secondary control. DESIGN.md already decides this: Save is the only filled primary button, the other three are neutral ghost buttons. Principle: hierarchy as service; if everything shouts, nothing wins.\nStakes if we pick wrong: users hesitate or hit Reset/Cancel by mistake, and the confirmation dialog becomes a safety net for a hierarchy problem.\nRecommendation: 1A because it applies the existing Button variants with zero new tokens and matches the approved design system exactly.\nCompleteness: 1A=10/10, 1B=6/10, 1C=3/10\nPros / cons:\n1A) Save filled primary (#1d4ed8, white text); Reset, Cancel, Export as neutral ghost buttons (recommended) (human: ~1h / CC: ~5min)\n \u2705 Exact DESIGN.md tokens via the existing Button component variants, nothing new to invent\n \u2705 Primary reads first at every viewport, including the full-width Save row at 640px and below\n \u274c Touches the button variant props on all four actions, so a small visual regression check is needed\n1B) Save filled primary; leave Reset, Cancel, Export exactly as they render today (human: ~30min / CC: ~3min)\n \u2705 Smallest possible change, only the Save button's variant flips\n \u2705 Still gives the header one obvious primary control for the 3-second scan\n \u274c Three secondary buttons stay in whatever inconsistent style they have now, so the group still does not match DESIGN.md\n1C) Defer: keep all four identical for this update (human: 0 / CC: 0)\n \u2705 No implementation work in this PR\n \u2705 Avoids any styling change to the action group\n \u274c Ships a documented DESIGN.md violation and leaves the page with no visible primary action\nNet: full alignment with the approved Button variants versus a partial or deferred fix that leaves the header ambiguous.": "1A Primary + ghost (recommended)"
|
||||
},
|
||||
"unansweredQuestionIndices": [],
|
||||
"answeredAt": "2026-09-10T07:58:51.241Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
+170
@@ -0,0 +1,170 @@
|
||||
{
|
||||
"sourceHead": "c73102357cbc3466d6a3c8d3ad0ac7e3177ce62c",
|
||||
"provenance": {
|
||||
"proof": "/home/vercel-sandbox/gstack/.context/ship-source-al-delta-paid-20260910-v1/eng-current-public-evidence-ledger-v1/proof.json",
|
||||
"proofSha256": "94c4c1e8cc924a1d90ad419f115b987d6f608a8d02b3a39ee57f5e0c3f5f34ee",
|
||||
"reportSha256": "7227cea004a0db3d55fc674d9dd0a4022b54d75d73cbf869ffba059be96c5427",
|
||||
"window": {
|
||||
"start": 1789027086774,
|
||||
"end": 1789027715816,
|
||||
"startSource": "Actual parent job startedAt; conservative bound before owned native question answers",
|
||||
"endSource": "Actual observation capture.at"
|
||||
},
|
||||
"projection": "Four exact completed seed calls. Assistant narration omitted in compact controls to prevent unrelated valid prose from masking missing plan evidence. Report blocks are exact unchanged public strings."
|
||||
},
|
||||
"required": "## Tests (D9-6A: all gaps written alongside the code)\nNo test framework is detectable in this repo snapshot. Match the project's\nexisting convention when implementing; requirements below are framework-\nneutral.\n\n**CRITICAL (regression rule, mandatory):** `legacyAuthFlow` golden-master.\nCapture current outputs for success / expired / revoked / wrong-tenant /\nlogout inputs BEFORE any change, assert identical behaviour after the\nrewrite and after the `validateToken` extraction. What broke otherwise:\nthe legacy path is modified in place with no existing coverage.",
|
||||
"task": "- [ ] **T1 (P1, human: ~2h / CC: ~10min)** — legacy — Capture golden-master regression fixtures for legacyAuthFlow before any change\n - Surfaced by: Tests — regression rule, PLAN.md:27-28\n - Files: auth/legacy/ tests\n - Verify: fixtures pass against untouched legacy; rerun after every later task",
|
||||
"verification": "## Verification\n1. Run T1 fixtures before touching anything; they must pass.\n2. After each task, rerun the full suite plus T1 fixtures.\n3. Flip the flag on for one internal tenant in staging; walk the four E2E\n journeys; check logs show tenant-tagged typed errors only where induced.\n4. Confirm login latency on a cold cache is one IDP round trip, not five.\n5. Confirm a suspended tenant is rejected on the very next request with\n the flag on and with it off.",
|
||||
"reviewReport": "## GSTACK REVIEW REPORT\n\n| Review | Trigger | Why | Runs | Status | Findings |\n|--------|---------|-----|------|--------|----------|\n| CEO Review | `/plan-ceo-review` | Scope & strategy | 0 | — | — |\n| Outside Review | codex via `/plan-eng-review` (host: claude, phase: plan-review) | Independent 2nd opinion | 1 | disabled | skipped, 0 findings |\n| Eng Review | `/plan-eng-review` | Architecture & tests (required) | 1 | CLEAR (PLAN) | 39 issues, 0 critical gaps, mode SCOPE_REDUCED |\n| Design Review | `/plan-design-review` | UI/UX gaps | 0 | — | — |\n| DX Review | `/plan-devex-review` | Developer experience gaps | 0 | — | — |\n\n- **OUTSIDE COVERAGE:** provider codex, phase plan-review, outside_status disabled (codex_reviews=disabled), source none, 0 findings. No native fallback dispatched; disabled is an intentional opt-out, not missing coverage to backfill.\n- **VERDICT:** ENG CLEARED — ready to implement (commit 760555a, 2026-09-10).\n\nNO UNRESOLVED DECISIONS\n",
|
||||
"transcript": {
|
||||
"status": "ready",
|
||||
"calls": [
|
||||
{
|
||||
"sessionId": "50406da4-c6d6-4944-ba42-85b9d92a7f6e",
|
||||
"toolUseId": "toolu_01WjzdCkJkzZpVJnj7hWDV9c",
|
||||
"questions": [
|
||||
{
|
||||
"question": "D2 — Enable cross-project learnings? Project/branch/task: main, Multi-tenant Auth Refactor plan. ELI10: gstack can search learnings saved from your other projects on this machine to spot patterns that apply here. Nothing leaves the machine. Stakes if we pick wrong: enabling on a machine with multiple client codebases could mix contexts; disabling loses reusable pitfalls. Recommendation: A because this is a local, solo-style environment with no client separation signals. Note: options differ in kind, not coverage — no completeness score.",
|
||||
"header": "Learnings",
|
||||
"multiSelect": false,
|
||||
"options": [
|
||||
{
|
||||
"label": "Enable cross-project learnings (recommended)",
|
||||
"description": "✅ Past pitfalls from other repos on this machine inform this review. ✅ Stays local, no network. ❌ Could surface irrelevant learnings from unrelated codebases."
|
||||
},
|
||||
{
|
||||
"label": "Keep learnings project-scoped only",
|
||||
"description": "✅ No cross-contamination between client codebases. ✅ Smaller, more targeted learning set. ❌ Loses reusable auth/caching pitfalls found elsewhere."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"question": "D3 — Scope: 12 files and 4 new classes trips the complexity gate. Reduce or proceed? Project/branch/task: main, Multi-tenant Auth Refactor (PLAN.md:34-36). ELI10: the plan adds TokenStore, SessionMint, AuthCache and RequestPolicy. AuthCache is a facade over the existing cache adapter, and TokenStore has no stated job the adapter does not already do. RequestPolicy reads like data plus a check, not a class. More moving parts means more places for tenant-isolation bugs to hide. Stakes if we pick wrong: over-reduce and you cram two responsibilities into one class; proceed as-is and you ship a parallel token store next to the cache you promised to keep. Recommendation: A because it keeps the two real services and the facade while removing the duplicate store, matching the plan's own 'one backing cache' contract. Note: options differ in kind, not coverage — no completeness score.",
|
||||
"header": "Scope",
|
||||
"multiSelect": false,
|
||||
"options": [
|
||||
{
|
||||
"label": "A) Reduce: drop TokenStore, RequestPolicy as module fn (recommended)",
|
||||
"description": "✅ One backing cache, as PLAN.md:11-12 already promises; no parallel token store to drift. ✅ Fewer files (est. 8-9) and 2 new classes plus a facade, under the smell threshold. ❌ If TokenStore had a real distinct job (e.g. refresh-token persistence), it must be re-justified in the plan. (human: ~1 day less / CC: ~10 min less)"
|
||||
},
|
||||
{
|
||||
"label": "B) Proceed as-is: 4 classes, 12 files",
|
||||
"description": "✅ No re-planning; every named component ships as designed. ✅ Keeps whatever unstated reason TokenStore exists for. ❌ Two stores for token state invites stale reads and inconsistent invalidation across tenants. (human: ~3 days / CC: ~45 min)"
|
||||
},
|
||||
{
|
||||
"label": "C) Reduce harder: single AuthService, no new classes beyond facade",
|
||||
"description": "✅ Smallest possible diff and one place to audit tenant isolation. ✅ Easiest to reason about at 3am. ❌ Broker and mint have different lifecycles and callers; merging them recreates the god-object the refactor is trying to escape. (human: ~1.5 days / CC: ~20 min)"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"answered": true,
|
||||
"failed": false,
|
||||
"answers": {
|
||||
"D2 — Enable cross-project learnings? Project/branch/task: main, Multi-tenant Auth Refactor plan. ELI10: gstack can search learnings saved from your other projects on this machine to spot patterns that apply here. Nothing leaves the machine. Stakes if we pick wrong: enabling on a machine with multiple client codebases could mix contexts; disabling loses reusable pitfalls. Recommendation: A because this is a local, solo-style environment with no client separation signals. Note: options differ in kind, not coverage — no completeness score.": "Enable cross-project learnings (recommended)",
|
||||
"D3 — Scope: 12 files and 4 new classes trips the complexity gate. Reduce or proceed? Project/branch/task: main, Multi-tenant Auth Refactor (PLAN.md:34-36). ELI10: the plan adds TokenStore, SessionMint, AuthCache and RequestPolicy. AuthCache is a facade over the existing cache adapter, and TokenStore has no stated job the adapter does not already do. RequestPolicy reads like data plus a check, not a class. More moving parts means more places for tenant-isolation bugs to hide. Stakes if we pick wrong: over-reduce and you cram two responsibilities into one class; proceed as-is and you ship a parallel token store next to the cache you promised to keep. Recommendation: A because it keeps the two real services and the facade while removing the duplicate store, matching the plan's own 'one backing cache' contract. Note: options differ in kind, not coverage — no completeness score.": "A) Reduce: drop TokenStore, RequestPolicy as module fn (recommended)"
|
||||
},
|
||||
"unansweredQuestionIndices": [],
|
||||
"answeredAt": "2026-09-10T08:00:33.281Z"
|
||||
},
|
||||
{
|
||||
"sessionId": "50406da4-c6d6-4944-ba42-85b9d92a7f6e",
|
||||
"toolUseId": "toolu_01S7zMsPkmYQEdNBVLThq6dw",
|
||||
"questions": [
|
||||
{
|
||||
"question": "D4 — Issue 1 (Architecture): AuthBroker and SessionMint both mutate a module-level global AuthCache with no serialized writes. How do we fix the shared-state hazard? Project/branch/task: main, Multi-tenant Auth Refactor (PLAN.md:19-20, :10). ELI10: two services write to the same cache object that any importer can grab. The plan says the cache's rules 'do not serialize mutations', so a broker write and a mint write for the same tenant key can interleave. Result: one tenant's fresh token overwritten by a stale one, or an invalidation lost, and tests cannot isolate the cache between cases. Stakes if we pick wrong: a lost invalidation on tenant suspension means a suspended tenant keeps authenticating until TTL expiry. Recommendation: A because it hits the root cause once in the facade, keeps the diff small, and makes the cache injectable for tests. [P1] (confidence: 8/10). Completeness: A=9/10, B=6/10, C=3/10.",
|
||||
"header": "Arch #1",
|
||||
"multiSelect": false,
|
||||
"options": [
|
||||
{
|
||||
"label": "1A) Inject AuthCache; facade owns per-key write serialization (recommended)",
|
||||
"description": "✅ Constructor injection: each service receives its AuthCache; no module-level mutable export, tests get a fresh instance. ✅ Facade serializes mutations per tenant key (async mutex / compare-and-set) so broker and mint cannot interleave; invalidation always wins over a stale set. ❌ Adds a small mutex utility and a composition root that wires both services. (human: ~1 day / CC: ~20 min)"
|
||||
},
|
||||
{
|
||||
"label": "1B) Keep global export, add per-key mutex inside AuthCache only",
|
||||
"description": "✅ Fixes the interleaving without touching service constructors. ✅ Smallest change to call sites. ❌ Global remains: any module can import and mutate it, and tests share state across cases unless they reset the singleton. (human: ~half day / CC: ~10 min)"
|
||||
},
|
||||
{
|
||||
"label": "1C) Do nothing; document that callers must not write concurrently",
|
||||
"description": "✅ Zero code change now. ✅ Fine if traffic is strictly single-writer, which the plan does not establish. ❌ A comment does not stop a 3am race; the lost-invalidation failure is silent and tenant-scoped. (human: ~0 / CC: ~0)"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"answered": true,
|
||||
"failed": false,
|
||||
"answers": {
|
||||
"D4 — Issue 1 (Architecture): AuthBroker and SessionMint both mutate a module-level global AuthCache with no serialized writes. How do we fix the shared-state hazard? Project/branch/task: main, Multi-tenant Auth Refactor (PLAN.md:19-20, :10). ELI10: two services write to the same cache object that any importer can grab. The plan says the cache's rules 'do not serialize mutations', so a broker write and a mint write for the same tenant key can interleave. Result: one tenant's fresh token overwritten by a stale one, or an invalidation lost, and tests cannot isolate the cache between cases. Stakes if we pick wrong: a lost invalidation on tenant suspension means a suspended tenant keeps authenticating until TTL expiry. Recommendation: A because it hits the root cause once in the facade, keeps the diff small, and makes the cache injectable for tests. [P1] (confidence: 8/10). Completeness: A=9/10, B=6/10, C=3/10.": "1A) Inject AuthCache; facade owns per-key write serialization (recommended)"
|
||||
},
|
||||
"unansweredQuestionIndices": [],
|
||||
"answeredAt": "2026-09-10T08:01:06.883Z"
|
||||
},
|
||||
{
|
||||
"sessionId": "50406da4-c6d6-4944-ba42-85b9d92a7f6e",
|
||||
"toolUseId": "toolu_01TRybLhHgvxF9LMQcKQN6yh",
|
||||
"questions": [
|
||||
{
|
||||
"question": "D7 — Issue 4 (Code Quality): validateAndDispatch() is 60 lines with three nested try/catch blocks, each swallowing a different error class. Restructure? Project/branch/task: main, Multi-tenant Auth Refactor (PLAN.md:23-24). ELI10: an auth function that swallows errors turns 'the IDP timed out' and 'this token is forged' into the same silent no-op. Nested catches also make it impossible to test one failure without setting up the two outer ones. Flatten it into named steps, one error boundary, and an explicit error-to-outcome map, and every failure becomes a typed, logged, testable result. Stakes if we pick wrong: a forged-token rejection that is swallowed looks identical to a network blip, and nobody pages on it. Recommendation: A because it is the explicit-over-clever version, removes the triple-nested duplication, and each step becomes a unit-testable pure function. [P1] (confidence: 8/10). Completeness: A=9/10, B=6/10, C=2/10.",
|
||||
"header": "Quality #4",
|
||||
"multiSelect": false,
|
||||
"options": [
|
||||
{
|
||||
"label": "4A) Flatten: named steps + one boundary + typed error map, never swallow (recommended)",
|
||||
"description": "✅ Split into parseRequest / validateToken / evaluatePolicy / dispatch; one try/catch at the top maps known error classes to a discriminated AuthOutcome and rethrows unknowns. ✅ Every error class is logged with tenant ID and gets its own unit test; no catch is empty. ❌ The rewrite touches every caller that relied on the old silent behaviour; they must handle the returned outcome. (human: ~1 day / CC: ~20 min)"
|
||||
},
|
||||
{
|
||||
"label": "4B) Keep structure, add logging inside each catch",
|
||||
"description": "✅ Minimal diff; errors stop being invisible. ✅ No caller changes. ❌ Still 60 lines of nesting, still three copies of catch logic, still untestable in isolation. (human: ~1h / CC: ~5 min)"
|
||||
},
|
||||
{
|
||||
"label": "4C) Do nothing",
|
||||
"description": "✅ Zero effort. ✅ The function is already in production shape. ❌ Swallowed auth errors are the textbook silent failure this review exists to catch. (human: ~0 / CC: ~0)"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"answered": true,
|
||||
"failed": false,
|
||||
"answers": {
|
||||
"D7 — Issue 4 (Code Quality): validateAndDispatch() is 60 lines with three nested try/catch blocks, each swallowing a different error class. Restructure? Project/branch/task: main, Multi-tenant Auth Refactor (PLAN.md:23-24). ELI10: an auth function that swallows errors turns 'the IDP timed out' and 'this token is forged' into the same silent no-op. Nested catches also make it impossible to test one failure without setting up the two outer ones. Flatten it into named steps, one error boundary, and an explicit error-to-outcome map, and every failure becomes a typed, logged, testable result. Stakes if we pick wrong: a forged-token rejection that is swallowed looks identical to a network blip, and nobody pages on it. Recommendation: A because it is the explicit-over-clever version, removes the triple-nested duplication, and each step becomes a unit-testable pure function. [P1] (confidence: 8/10). Completeness: A=9/10, B=6/10, C=2/10.": "4A) Flatten: named steps + one boundary + typed error map, never swallow (recommended)"
|
||||
},
|
||||
"unansweredQuestionIndices": [],
|
||||
"answeredAt": "2026-09-10T08:02:33.163Z"
|
||||
},
|
||||
{
|
||||
"sessionId": "50406da4-c6d6-4944-ba42-85b9d92a7f6e",
|
||||
"toolUseId": "toolu_018cGG1hJKWcWTjc3Ba994fX",
|
||||
"questions": [
|
||||
{
|
||||
"question": "D10 — Issue 7 (Performance): token validation makes 5 sequential IDP round trips; the plan notes Promise.all would work but does not commit to it or define failure semantics. How should validateToken() fan out? Project/branch/task: main, Multi-tenant Auth Refactor (PLAN.md:31-32). ELI10: five calls in a row means login latency is five IDP round trips stacked. Running them at once cuts that to the slowest single call. But parallel calls need rules: if one fails, do the others keep burning IDP quota? How long do we wait? Stakes if we pick wrong: either login stays 5x slower than it needs to be, or a flaky IDP produces hung requests and quota burn. Recommendation: A because every one of the five results is required (a token is valid only if all checks pass), so fail-fast is correct, and cancelling the rest plus a bounded timeout is a few lines. [Layer 1] [P2] (confidence: 8/10). Completeness: A=9/10, B=6/10, C=4/10.",
|
||||
"header": "Perf #7",
|
||||
"multiSelect": false,
|
||||
"options": [
|
||||
{
|
||||
"label": "7A) Promise.all + shared AbortController + per-call timeout; cache hit skips IDP (recommended)",
|
||||
"description": "✅ Latency drops from 5 sequential to 1 slowest call; first failure aborts the remaining four and returns a typed error. ✅ Broker checks the tenant cache first so hot paths never touch the IDP. ❌ Needs the IDP client to honour abort signals; verify before relying on cancellation. (human: ~half day / CC: ~10 min)"
|
||||
},
|
||||
{
|
||||
"label": "7B) Bare Promise.all, no abort, no timeout",
|
||||
"description": "✅ Trivial change, exactly what the plan hints at. ✅ Same latency win on the happy path. ❌ On one failure the other four keep running; on a hung IDP the request hangs with them. (human: ~15 min / CC: ~2 min)"
|
||||
},
|
||||
{
|
||||
"label": "7C) Keep sequential, rely on cache hits to hide latency",
|
||||
"description": "✅ No concurrency to reason about; simplest failure story. ✅ Fine if cache hit rate is very high. ❌ Every cold login and every post-invalidation request pays the full 5x. (human: ~0 / CC: ~0)"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"answered": true,
|
||||
"failed": false,
|
||||
"answers": {
|
||||
"D10 — Issue 7 (Performance): token validation makes 5 sequential IDP round trips; the plan notes Promise.all would work but does not commit to it or define failure semantics. How should validateToken() fan out? Project/branch/task: main, Multi-tenant Auth Refactor (PLAN.md:31-32). ELI10: five calls in a row means login latency is five IDP round trips stacked. Running them at once cuts that to the slowest single call. But parallel calls need rules: if one fails, do the others keep burning IDP quota? How long do we wait? Stakes if we pick wrong: either login stays 5x slower than it needs to be, or a flaky IDP produces hung requests and quota burn. Recommendation: A because every one of the five results is required (a token is valid only if all checks pass), so fail-fast is correct, and cancelling the rest plus a bounded timeout is a few lines. [Layer 1] [P2] (confidence: 8/10). Completeness: A=9/10, B=6/10, C=4/10.": "7A) Promise.all + shared AbortController + per-call timeout; cache hit skips IDP (recommended)"
|
||||
},
|
||||
"unansweredQuestionIndices": [],
|
||||
"answeredAt": "2026-09-10T08:04:27.601Z"
|
||||
}
|
||||
],
|
||||
"assistantMessages": [],
|
||||
"planReadyRequests": []
|
||||
}
|
||||
}
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
{
|
||||
"sourceHead": "c73102357cbc3466d6a3c8d3ad0ac7e3177ce62c",
|
||||
"provenance": {
|
||||
"replay": ".context/ship-source-al-delta-paid-20260910-v1/eng23-scope-replay-ledger-v1.json",
|
||||
"projection": "Exact owned public assistant messages and the successful Skill use/result only. First is the retained native poll, not an exhaustive terminal journal; no post-load selected-plan announcement is present. Both original callback failures remain failures."
|
||||
},
|
||||
"attempts": [
|
||||
{
|
||||
"attempt": 1,
|
||||
"sourceObservation": {
|
||||
"sha256": "6a50c83263b4b18f8bcd49c3f5116144f43369b3046f6f43775543d534551c3c",
|
||||
"path": ".context/ship-source-al-delta-paid-20260910-v1/eng23-first-evidence-ledger-v1/seeded-observation.json"
|
||||
},
|
||||
"rawScopeGateAutoSelectObserved": false,
|
||||
"projection": {
|
||||
"transcript": {
|
||||
"status": "ready",
|
||||
"calls": [],
|
||||
"assistantMessages": [
|
||||
{
|
||||
"sessionId": "3b7d1fc0-f1c8-40b2-9aa5-d08b84138d07",
|
||||
"text": "I'll invoke the /plan-eng-review skill to review this draft plan.",
|
||||
"timestamp": "2026-09-10T08:18:19.536Z"
|
||||
},
|
||||
{
|
||||
"sessionId": "3b7d1fc0-f1c8-40b2-9aa5-d08b84138d07",
|
||||
"text": "Interactive session, no Conductor. Running context recovery, brain context, and the design doc check in parallel.",
|
||||
"timestamp": "2026-09-10T08:18:39.042Z"
|
||||
},
|
||||
{
|
||||
"sessionId": "3b7d1fc0-f1c8-40b2-9aa5-d08b84138d07",
|
||||
"text": "Found `scripts/test-free-shards.ts` already exists and a related P2 item in TODOS.md about bun-native `--shard`. I'll read those along with the design doc's review sections next.",
|
||||
"timestamp": "2026-09-10T08:19:06.553Z"
|
||||
},
|
||||
{
|
||||
"sessionId": "3b7d1fc0-f1c8-40b2-9aa5-d08b84138d07",
|
||||
"text": "The plan calls for building a custom runner, but that already exists in `scripts/test-free-shards.ts` (1631 lines, duration-packed, strict-classified, backed by 17 tests). I'll keep gathering the remaining context before starting Step 0.\n\n",
|
||||
"timestamp": "2026-09-10T08:19:30.967Z"
|
||||
},
|
||||
{
|
||||
"sessionId": "3b7d1fc0-f1c8-40b2-9aa5-d08b84138d07",
|
||||
"text": "AskUserQuestion and Aside aren't available, so I'll fall back to prose for decision briefs and use WebSearch for the Step 0 search check. Loading that now and reading the packer's successor note.\n\n",
|
||||
"timestamp": "2026-09-10T08:19:55.872Z"
|
||||
},
|
||||
{
|
||||
"sessionId": "3b7d1fc0-f1c8-40b2-9aa5-d08b84138d07",
|
||||
"text": "Bun is at 1.4.0 here and in every CI workflow, so the packer's own \"replace me when unpinned past 1.3.13\" note has already fired. Running the Step 0 search check.",
|
||||
"timestamp": "2026-09-10T08:20:04.932Z"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tools": [
|
||||
{
|
||||
"sessionId": "3b7d1fc0-f1c8-40b2-9aa5-d08b84138d07",
|
||||
"timestamp": "2026-09-10T08:18:19.579Z",
|
||||
"toolUseId": "toolu_01St7VYUhEJNWgXSKthMKTJ1",
|
||||
"kind": "use",
|
||||
"name": "Skill",
|
||||
"input": {
|
||||
"skill": "plan-eng-review"
|
||||
}
|
||||
},
|
||||
{
|
||||
"sessionId": "3b7d1fc0-f1c8-40b2-9aa5-d08b84138d07",
|
||||
"timestamp": "2026-09-10T08:18:22.516Z",
|
||||
"toolUseId": "toolu_01St7VYUhEJNWgXSKthMKTJ1",
|
||||
"kind": "result",
|
||||
"content": "Launching skill: plan-eng-review",
|
||||
"isError": false
|
||||
}
|
||||
],
|
||||
"opts": {
|
||||
"seed": "\n# Parallelize unit tests\n\n## Plan\nBuild a custom test runner: scripts/test-parallel.ts, scripts/test-shard-impl.ts,\nscripts/test-merge-results.ts, scripts/test-progress.ts, scripts/test-watch.ts,\nscripts/test-coverage.ts, scripts/test-cli.ts, scripts/test-config.ts.\n\nAdd new TestRunner class, new ShardManager class, new ResultMerger class.\n\nIgnore Bun's native --shard flag because we want full control.\n\n## Files\n- scripts/test-parallel.ts (new)\n- scripts/test-shard-impl.ts (new)\n- scripts/test-merge-results.ts (new)\n- scripts/test-progress.ts (new)\n- scripts/test-watch.ts (new)\n- scripts/test-coverage.ts (new)\n- scripts/test-cli.ts (new)\n- scripts/test-config.ts (new)\n- package.json (add scripts)\n\n## Tests\nNone planned \u2014 will add later.\n",
|
||||
"skillName": "plan-eng-review",
|
||||
"sessionId": "3b7d1fc0-f1c8-40b2-9aa5-d08b84138d07",
|
||||
"commandStartedAt": 1789028293455
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"attempt": 2,
|
||||
"sourceObservation": {
|
||||
"sha256": "0e9174bb66916b6d86b20688d9e1ff28c251104237bf0816d4cc05cc4946b1dc",
|
||||
"path": ".context/ship-source-al-delta-paid-20260910-v1/eng23-retry-evidence-ledger-v1/observation.json"
|
||||
},
|
||||
"rawScopeGateAutoSelectObserved": false,
|
||||
"projection": {
|
||||
"transcript": {
|
||||
"status": "ready",
|
||||
"calls": [],
|
||||
"assistantMessages": [
|
||||
{
|
||||
"sessionId": "58b7a2a4-20c6-4779-bca1-0e748059d0a4",
|
||||
"text": "I'll run the eng review skill on this draft plan.",
|
||||
"timestamp": "2026-09-10T08:21:23.762Z"
|
||||
},
|
||||
{
|
||||
"sessionId": "58b7a2a4-20c6-4779-bca1-0e748059d0a4",
|
||||
"text": "Plan mode is active, so the scope gate auto-selects B. Running the preamble first.",
|
||||
"timestamp": "2026-09-10T08:21:33.170Z"
|
||||
},
|
||||
{
|
||||
"sessionId": "58b7a2a4-20c6-4779-bca1-0e748059d0a4",
|
||||
"text": "I've selected option B, reviewing the pasted \"Parallelize unit tests\" draft plan. Next I'll run the Design Doc Check, brain context, and context recovery, along with the Aside probe.",
|
||||
"timestamp": "2026-09-10T08:21:43.626Z"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tools": [
|
||||
{
|
||||
"sessionId": "58b7a2a4-20c6-4779-bca1-0e748059d0a4",
|
||||
"timestamp": "2026-09-10T08:21:23.804Z",
|
||||
"toolUseId": "toolu_01TCceDrqU6vMJTMtNFR8bjF",
|
||||
"kind": "use",
|
||||
"name": "Skill",
|
||||
"input": {
|
||||
"skill": "plan-eng-review"
|
||||
}
|
||||
},
|
||||
{
|
||||
"sessionId": "58b7a2a4-20c6-4779-bca1-0e748059d0a4",
|
||||
"timestamp": "2026-09-10T08:21:27.842Z",
|
||||
"toolUseId": "toolu_01TCceDrqU6vMJTMtNFR8bjF",
|
||||
"kind": "result",
|
||||
"content": "Launching skill: plan-eng-review",
|
||||
"isError": false
|
||||
}
|
||||
],
|
||||
"opts": {
|
||||
"seed": "\n# Parallelize unit tests\n\n## Plan\nBuild a custom test runner: scripts/test-parallel.ts, scripts/test-shard-impl.ts,\nscripts/test-merge-results.ts, scripts/test-progress.ts, scripts/test-watch.ts,\nscripts/test-coverage.ts, scripts/test-cli.ts, scripts/test-config.ts.\n\nAdd new TestRunner class, new ShardManager class, new ResultMerger class.\n\nIgnore Bun's native --shard flag because we want full control.\n\n## Files\n- scripts/test-parallel.ts (new)\n- scripts/test-shard-impl.ts (new)\n- scripts/test-merge-results.ts (new)\n- scripts/test-progress.ts (new)\n- scripts/test-watch.ts (new)\n- scripts/test-coverage.ts (new)\n- scripts/test-cli.ts (new)\n- scripts/test-config.ts (new)\n- package.json (add scripts)\n\n## Tests\nNone planned \u2014 will add later.\n",
|
||||
"skillName": "plan-eng-review",
|
||||
"sessionId": "58b7a2a4-20c6-4779-bca1-0e748059d0a4",
|
||||
"commandStartedAt": 1789028478459
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
/** Bounded, body-free evidence from one owned PreToolUse Edit request. */
|
||||
import * as fs from 'node:fs';
|
||||
import { createHash } from 'node:crypto';
|
||||
const MAX_BYTES = 1024 * 1024, MAX_LINES = 512;
|
||||
const sha = (bytes: string | Buffer) => createHash('sha256').update(bytes).digest('hex');
|
||||
export const autoplanEditLineHash = (line: string) => sha(line.replace(/\s/g, ''));
|
||||
export interface AutoplanEditDigest {
|
||||
version: 1;
|
||||
beforeSHA256: string;
|
||||
requestSHA256: string;
|
||||
oldLineHashes: string[];
|
||||
newLineHashes: string[];
|
||||
}
|
||||
export function validAutoplanEditDigest(value: unknown): value is AutoplanEditDigest {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
|
||||
const v = value as Record<string, unknown>;
|
||||
const hashes = (a: unknown): a is string[] => Array.isArray(a) && a.length > 0 && a.length <= MAX_LINES &&
|
||||
Array.from(a).every(h => typeof h === 'string' && /^[a-f0-9]{64}$/.test(h));
|
||||
return Object.keys(v).length === 5 && v.version === 1 && typeof v.beforeSHA256 === 'string' &&
|
||||
/^[a-f0-9]{64}$/.test(v.beforeSHA256) && typeof v.requestSHA256 === 'string' &&
|
||||
/^[a-f0-9]{64}$/.test(v.requestSHA256) && hashes(v.oldLineHashes) && hashes(v.newLineHashes) &&
|
||||
v.oldLineHashes.length + v.newLineHashes.length <= MAX_LINES;
|
||||
}
|
||||
/** The caller must validate the owned path before this capped, no-follow read. */
|
||||
export function readAutoplanDigestFile(file: string): Buffer | undefined {
|
||||
let fd: number | undefined;
|
||||
try {
|
||||
fd = fs.openSync(file, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0));
|
||||
const initial = fs.fstatSync(fd);
|
||||
if (!initial.isFile() || initial.size > MAX_BYTES) return undefined;
|
||||
const buffer = Buffer.alloc(MAX_BYTES + 1); let length = 0;
|
||||
while (length < buffer.length) {
|
||||
const n = fs.readSync(fd, buffer, length, buffer.length - length, null);
|
||||
if (!n) break; length += n;
|
||||
}
|
||||
const final = fs.fstatSync(fd);
|
||||
if (length > MAX_BYTES || length !== initial.size || final.size !== initial.size ||
|
||||
final.mtimeMs !== initial.mtimeMs || final.ctimeMs !== initial.ctimeMs) return undefined;
|
||||
return buffer.subarray(0, length);
|
||||
} catch { return undefined; }
|
||||
finally { if (fd !== undefined) fs.closeSync(fd); }
|
||||
}
|
||||
export function createAutoplanEditDigest(file: string, removed: string, added: string): AutoplanEditDigest | undefined {
|
||||
const oldLines = removed.split('\n'), newLines = added.split('\n');
|
||||
if (!removed || removed === added || oldLines.length + newLines.length > MAX_LINES) return undefined;
|
||||
const bytes = readAutoplanDigestFile(file); if (!bytes) return undefined;
|
||||
const before = bytes.toString('utf8'), at = before.indexOf(removed);
|
||||
// Require one exact old substring. Only complete request lines can match a
|
||||
// displayed row; partial edge lines cannot establish added-line authority.
|
||||
if (!Buffer.from(before).equals(bytes) || at < 0 || at !== before.lastIndexOf(removed)) return undefined;
|
||||
return { version: 1, beforeSHA256: sha(bytes), requestSHA256: sha(JSON.stringify([removed, added])),
|
||||
oldLineHashes: oldLines.map(autoplanEditLineHash), newLineHashes: newLines.map(autoplanEditLineHash) };
|
||||
}
|
||||
|
||||
/** Require complete numbered rows and the marker column of this exact diff. */
|
||||
export function matchesAutoplanDigestRows(rows: string[], before: Buffer, digest: AutoplanEditDigest): boolean {
|
||||
if (!validAutoplanEditDigest(digest) || sha(before) !== digest.beforeSHA256) return false;
|
||||
const chunks: Array<{kind: string; text: string}> = [];
|
||||
let column: number | undefined, previousLine = 0;
|
||||
for (const row of rows) {
|
||||
const full = /^( {0,3})([1-9]\d*) ([+ -])(.*)$/.exec(row);
|
||||
if (full) {
|
||||
const line = Number(full[2]), markerColumn = full[1]!.length + full[2]!.length + 1;
|
||||
if (!Number.isSafeInteger(line) || line < previousLine ||
|
||||
(column !== undefined && column !== markerColumn)) return false;
|
||||
column = markerColumn; previousLine = line;
|
||||
chunks.push({kind: full[3]!, text: full[4]!});
|
||||
} else {
|
||||
if (column === undefined || !row.startsWith(' '.repeat(column))) return false;
|
||||
const last = chunks.at(-1), kind = row[column];
|
||||
if (!last || kind !== last.kind) return false;
|
||||
last.text += row.slice(column + 1);
|
||||
}
|
||||
}
|
||||
const originals = new Set(before.toString('utf8').split('\n').map(autoplanEditLineHash));
|
||||
return chunks.length >= 2 && chunks.some(c => c.kind === '+' && /\S/.test(c.text) &&
|
||||
!originals.has(autoplanEditLineHash(c.text)) && !digest.oldLineHashes.includes(autoplanEditLineHash(c.text))) &&
|
||||
chunks.every(c => c.kind === '+' ? digest.newLineHashes.includes(autoplanEditLineHash(c.text)) :
|
||||
originals.has(autoplanEditLineHash(c.text)) && (c.kind !== '-' || digest.oldLineHashes.includes(autoplanEditLineHash(c.text))));
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { validAutoplanEditDigest, readAutoplanDigestFile, matchesAutoplanDigestRows } from './autoplan-artifact-digest';
|
||||
import type { PendingAutoplanArtifact } from './autoplan-artifact-recorder';
|
||||
import type { NativePublicToolEvent } from './plan-count-transcript';
|
||||
|
||||
@@ -204,6 +205,7 @@ export function pendingAutoplanArtifactPermissionInput(viewport: string,
|
||||
seen: ReadonlySet<string>,
|
||||
): { input: '1\r'; signature: string; file: string } | null {
|
||||
const p = context.pending, now = context.now ?? Date.now();
|
||||
if (p?.editDigest !== undefined && !validAutoplanEditDigest(p.editDigest)) return null;
|
||||
if (!p || !Number.isFinite(now) || context.transcriptStatus !== 'ready' || !Number.isFinite(context.commandStartedAt) ||
|
||||
!Number.isFinite(context.viewportCapturedAt) || context.viewportCapturedAt > now ||
|
||||
context.commandStartedAt > context.viewportCapturedAt || viewport.length > MAX_BYTES ||
|
||||
@@ -241,6 +243,13 @@ export function pendingAutoplanArtifactPermissionInput(viewport: string,
|
||||
const diffRows = ownedEditDiffRows(rows,p.file,context.ownedStateRoot);
|
||||
if (!diffRows) return null;
|
||||
if (Math.floor(fs.statSync(p.file).mtimeMs) > pendingTime) return null;
|
||||
if (p.editDigest) {
|
||||
const before = readAutoplanDigestFile(p.file);
|
||||
if (!before || createHash('sha256').update(before).digest('hex') !== p.editDigest.beforeSHA256) return null;
|
||||
if (matchesAutoplanDigestRows(diffRows,before,p.editDigest)) return {input:'1\r', signature, file:p.file};
|
||||
// Digest authority adds insertion-only crops; existing anchored deletion
|
||||
// authority remains available after the current-file binding succeeds.
|
||||
}
|
||||
const originals = fs.readFileSync(p.file, 'utf8').split('\n').map(compact);
|
||||
const chunks: Array<{kind:string; text:string; partial?:boolean}> = [];
|
||||
let numbered = 0;
|
||||
|
||||
@@ -3,6 +3,7 @@ import * as fs from 'node:fs';
|
||||
import * as os from 'node:os';
|
||||
import * as path from 'node:path';
|
||||
import { ownedAutoplanArtifact } from './autoplan-artifact-permission';
|
||||
import { createAutoplanEditDigest, validAutoplanEditDigest, type AutoplanEditDigest } from './autoplan-artifact-digest';
|
||||
import type { NativePublicToolEvent } from './plan-count-transcript';
|
||||
|
||||
const MAX_BYTES = 64 * 1024, MAX_INPUT = 4 * 1024 * 1024, MAX_IDS = 128;
|
||||
@@ -14,7 +15,7 @@ const id = (v: unknown): v is string => typeof v === 'string' && /^[A-Za-z0-9_-]
|
||||
const keys = (v: Record<string, any>, allowed: string[]) => Object.keys(v).every(k => allowed.includes(k));
|
||||
const quote = (v: string) => `'${(process.platform === 'win32' ? v.replaceAll('\\','/') : v).replaceAll("'", "'\\''")}'`;
|
||||
export interface PendingAutoplanArtifact {
|
||||
source:'pre_tool_use'; sessionId:string; toolUseId:string; tool:'Edit'; file:string; timestamp:string;
|
||||
source:'pre_tool_use'; sessionId:string; toolUseId:string; tool:'Edit'; file:string; timestamp:string; editDigest?:AutoplanEditDigest;
|
||||
}
|
||||
interface Pending extends PendingAutoplanArtifact { transcriptPath:string }
|
||||
interface State { version:1; cwd:string; config:string; stateRoot:string; sessionId?:string; seenIds:string[]; pending:Pending|null }
|
||||
@@ -36,8 +37,9 @@ function readState(file:string, cwd:string, config:string, stateRoot:string): St
|
||||
!Array.isArray(s.seenIds) || s.seenIds.length > MAX_IDS || !s.seenIds.every(id) || new Set(s.seenIds).size !== s.seenIds.length ||
|
||||
(s.sessionId === undefined && (s.seenIds.length || s.pending !== null))) throw Error('record');
|
||||
const p = s.pending;
|
||||
if (p !== null && (!object(p) || !keys(p,['source','sessionId','toolUseId','tool','file','timestamp','transcriptPath']) ||
|
||||
if (p !== null && (!object(p) || !keys(p,['source','sessionId','toolUseId','tool','file','timestamp','transcriptPath','editDigest']) ||
|
||||
p.source !== 'pre_tool_use' || p.tool !== 'Edit' || p.sessionId !== s.sessionId || !id(p.toolUseId) || !s.seenIds.includes(p.toolUseId) ||
|
||||
(p.editDigest!==undefined && !validAutoplanEditDigest(p.editDigest)) ||
|
||||
!scopedTranscript(p.transcriptPath,config,p.sessionId) || !Number.isFinite(Date.parse(p.timestamp)) ||
|
||||
!ownedAutoplanArtifact(p.file,{cwd,ownedStateRoot:stateRoot}))) throw Error('record');
|
||||
return s as State;
|
||||
@@ -57,7 +59,7 @@ export function createAutoplanArtifactRecorder(cwd:string, config:string, stateR
|
||||
dispose:()=>fs.rmSync(dir,{recursive:true,force:true})};
|
||||
}
|
||||
|
||||
/** Never persists edit text, tool results, permissions, stdout, or model context. */
|
||||
/** Persists metadata and bounded request digests, never edit text or tool results. */
|
||||
export function recordAutoplanArtifact(input:string, file:string, cwd:string, config:string, stateRoot:string) {
|
||||
let lock:number|undefined, reason:Reason='invalid_event';
|
||||
const temporary = `${file}.${process.pid}.tmp`;
|
||||
@@ -90,7 +92,16 @@ export function recordAutoplanArtifact(input:string, file:string, cwd:string, co
|
||||
}
|
||||
const state:State={...old,sessionId:e.session_id};
|
||||
if (e.hook_event_name==='PreToolUse') {
|
||||
if (old.seenIds.includes(e.tool_use_id)) return;
|
||||
if (old.seenIds.includes(e.tool_use_id)) {
|
||||
if (previous?.toolUseId===e.tool_use_id && previous.editDigest) {
|
||||
reason='conflicting_replay';
|
||||
const digest=typeof e.tool_input.old_string==='string' && typeof e.tool_input.new_string==='string' &&
|
||||
(e.tool_input.replace_all===undefined || e.tool_input.replace_all===false)
|
||||
? createAutoplanEditDigest(e.tool_input.file_path,e.tool_input.old_string,e.tool_input.new_string) : undefined;
|
||||
if (!digest || JSON.stringify(digest)!==JSON.stringify(previous.editDigest)) throw Error('request changed');
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (previous) { reason='concurrent_pending'; throw Error('concurrent'); }
|
||||
// Match the published-path edit scope; inspect but never retain content.
|
||||
if (typeof e.tool_input.old_string!=='string' || !e.tool_input.old_string || typeof e.tool_input.new_string!=='string' ||
|
||||
@@ -98,6 +109,8 @@ export function recordAutoplanArtifact(input:string, file:string, cwd:string, co
|
||||
(e.tool_input.replace_all!==undefined && e.tool_input.replace_all!==false)) throw Error('edit scope');
|
||||
state.pending={source:'pre_tool_use',sessionId:e.session_id,toolUseId:e.tool_use_id,tool:'Edit',file:e.tool_input.file_path,
|
||||
timestamp:new Date().toISOString(),transcriptPath:e.transcript_path};
|
||||
const digest=createAutoplanEditDigest(e.tool_input.file_path,e.tool_input.old_string,e.tool_input.new_string);
|
||||
if (digest) state.pending.editDigest=digest;
|
||||
} else if (previous?.toolUseId===e.tool_use_id) state.pending=null;
|
||||
// Success/failure hooks clear pending and tombstone the ID, but never
|
||||
// supply successful history or phase coverage. Those still require JSONL.
|
||||
|
||||
@@ -2213,7 +2213,7 @@ function ceoNumberedBriefDecision(q: NativePlanQuestionCall['questions'][number]
|
||||
const recommendation = recommendations[0];
|
||||
const label = (text: string) => /^([1-9]\d*)?([A-Z])[):.]\s*/i.exec(text.trim());
|
||||
if (explanations.length !== 1 || recommendations.length !== 1 || !/\w/.test(explanation) ||
|
||||
/^(?:if|unless|whether|suppose|imagine|example|template|hypothetical|historical|quoted)\b|^["'‘“`]/i.test(explanation.trim()) ||
|
||||
/^(?:if|unless|whether|suppose|imagine|example|template|hypothetical|historical|quoted|source|previously|formerly)\b|^["'‘“`]/i.test(explanation.trim()) ||
|
||||
/\b(?:is|was|presents?|represents?)\s+(?:(?:only|just)\s+)?(?:an?\s+)?(?:quoted|hypothetical|historical|example|template)\b/i.test(openingAssessment) ||
|
||||
/\b(?:hypothetical|example|template)\b/i.test(publicText(subject)) ||
|
||||
!recommendation || q.options.length < 2 || q.options.some(o => !o.description?.trim()) ||
|
||||
@@ -2246,13 +2246,21 @@ function ceoNumberedBriefDecision(q: NativePlanQuestionCall['questions'][number]
|
||||
const escapingMailFailure = /^(?:(?:today|currently|now)[,:]?\s+)?(?:(?:the|this|current)\s+)?(?:plan|handler|implementation)\s+(?:lets?|allows?)\s+(?:(?:any|a|an|the)\s+)?(?:mail|email|notification)\s+failures?(?:\s*\([^()\n]*\))?\s+(?:to\s+)?escape\b/i.test(statement.trim()) &&
|
||||
!/^(?:source|previously|formerly)\b/i.test(openingAssessment) &&
|
||||
!/\b(?:if|unless|whether|hypothetical|historical|quoted|example|template|previously|formerly)\b|\bsource\s+(?:excerpt|material|text)\b/i.test(statement);
|
||||
// A quoted contract term can describe the current plan's own behavior.
|
||||
// Keep the affirmative owner outside the quotation; source examples and
|
||||
// negated or past behavior cannot lend that term current status.
|
||||
const embeddedMissingContract = /^(?:the|this|current)\s+(?:plan|handler|implementation)\s+(?:sends?|delivers?|calls?|performs?|executes?|runs?)\b/i.test(statement.trim()) &&
|
||||
!/\b(?:if|unless|whether|not|never|historical|hypothetical|quoted|example|template|previously|formerly|source)\b|\b(?:no longer|used to)\b/i.test(statement) &&
|
||||
/\bwith\s+['‘]no\s+(?:(?:automated|explicit|defined)\s+)?(?:error handling|tests?|checks?|validation|coordination|cap|bound|timeout)(?:\s+(?:on|for|in)\s+[^'’\n.!?]+)?['’]/i.test(statement);
|
||||
const rawSqlGap = /^(?:the|this|current)\s+(?:plan|handler|implementation|(?:lookup\s+)?query)\s+pastes?\b[^.!?]*\bstraight into (?:a )?raw SQL\b/i.test(statement.trim()) &&
|
||||
!/\b(?:if|unless|whether|not|never|historical|hypothetical|quoted|example|template|previously|formerly|source)\b|\b(?:no longer|used to)\b/i.test(statement);
|
||||
return !/^(?:if|unless|whether|example|template|hypothetical|quoted)\b|\b(?:already resolved|no (?:current )?(?:defect|gap|issue|problem)s?\b|not true)\b/i.test(statement.trim()) &&
|
||||
!/\b(?:not|never|no longer|isn't)\s+(?:missing|unspecified|unvalidated|unhandled)\b/i.test(statement) &&
|
||||
!/\b(?:not|never|no longer|doesn't|does not)\s+(?:asserts?|checks?)\s+only\b/i.test(statement) &&
|
||||
!/\b(?:was|were)\s+(?:missing|unspecified|unvalidated|unhandled)\b/i.test(statement) &&
|
||||
!/\b(?:not|never|no longer|doesn't|does not)\s+(?:paste|send|deliver|receive)\b/i.test(statement) &&
|
||||
!/\b(?:not|never|no longer|doesn't|does not|used to|previously|formerly)\s+(?:pastes?|sends?|delivers?|receives?)\b/i.test(statement) &&
|
||||
!/\b(?:not|never|no longer|doesn't|does not|used to|previously|formerly)\s+(?:interpolates?|reads?|fetch(?:es)?|loads?|quer(?:y|ies))\b/i.test(statement) &&
|
||||
(assertionGap || /\b(?:missing|unspecified|unvalidated|unhandled)\b|\b(?:(?:has|with|leaves)\s+no|without)\s+(?:(?:automated|explicit|defined)\s+)?(?:error handling|tests?|checks?|validation|coordination|cap|bound|timeout)\b|\b(?:asserts?|checks?)\s+only\b|\b(?:does not|doesn't|never)\s+(?:say|says|state|define|specify|cover|handle)\b|\bpastes?\b[^.!?]*\bstraight into (?:a )?SQL\b|\b(?:gets?|sends?|delivers?|receives?)\b[^.!?]*\btwice\b|\b(?:proves?|checks?|tests?|covers?)\s+(?:the\s+)?happy path\s+and\s+nothing else\b|\binterpolates?\b[^!?]*\b(?:raw\s+)?SQL\s+(?:fragment|string)\b|\bno\s+(?:automated\s+)?tests?\s+(?:are\s+)?planned\b|\b(?:fetch(?:es)?|reads?|loads?|queries)\b[^!?]*\bN\+1\b/i.test(statement) || escapingMailFailure);
|
||||
(assertionGap || /\b(?:missing|unspecified|unvalidated|unhandled)\b|\b(?:(?:has|with|leaves)\s+no|without)\s+(?:(?:automated|explicit|defined)\s+)?(?:error handling|tests?|checks?|validation|coordination|cap|bound|timeout)\b|\b(?:asserts?|checks?)\s+only\b|\b(?:does not|doesn't|never)\s+(?:say|says|state|define|specify|cover|handle)\b|\bpastes?\b[^.!?]*\bstraight into (?:a )?SQL\b|\b(?:gets?|sends?|delivers?|receives?)\b[^.!?]*\btwice\b|\b(?:proves?|checks?|tests?|covers?)\s+(?:the\s+)?happy path\s+and\s+nothing else\b|\binterpolates?\b[^!?]*\b(?:raw\s+)?SQL\s+(?:fragment|string)\b|\bno\s+(?:automated\s+)?tests?\s+(?:are\s+)?planned\b|\b(?:fetch(?:es)?|reads?|loads?|queries)\b[^!?]*\bN\+1\b/i.test(statement) || escapingMailFailure || embeddedMissingContract || rawSqlGap);
|
||||
});
|
||||
const amendment = q.options.some(option => {
|
||||
const token = label(option.label);
|
||||
@@ -2298,6 +2306,14 @@ function ceoParenthesizedIssueBrief(q: NativePlanQuestionCall['questions'][numbe
|
||||
const explanations = [...prose.matchAll(/^ELI10:\s*(.+)$/gm)];
|
||||
const recommendations = [...prose.matchAll(/^Recommendation:\s*([1-9]\d*)?([A-Z])\b/gim)];
|
||||
const section = number.split('.')[0]!;
|
||||
// A bare recommendation letter can select an offered decision-numbered
|
||||
// choice (D4 / 4A) independently of the Finding number. Normalize only a
|
||||
// uniform prefix matching this exact decision; mixed or foreign IDs fail.
|
||||
const decision = /^D([1-9]\d*)\b/i.exec(title)![1]!;
|
||||
if (finding && recommendations.length === 1 && !recommendations[0]![1] &&
|
||||
q.options.every(option => new RegExp('^' + decision + '[A-Z][):.]\\s*\\S', 'i').test(option.label))) {
|
||||
q = { ...q, options: q.options.map(option => ({ ...option, label: option.label.replace(/^[1-9]\d*(?=[A-Z][):.])/i, '') })) };
|
||||
}
|
||||
const optionPrefix = recommendations[0]?.[1] ?? '';
|
||||
if (explanations.length !== 1 || recommendations.length !== 1 ||
|
||||
(optionPrefix ? optionPrefix !== section : !finding) || !/\w/.test(explanations[0]![1]!) ||
|
||||
|
||||
@@ -65,7 +65,7 @@ function ordinaryDesignIssue(fp: AskUserQuestionFingerprint): boolean {
|
||||
const title = q.question.split('\n')[0]!.trim();
|
||||
const issue = /^(?:D[1-9]\d*\s*[—–:-]\s*)?Issue ([1-9]\d*)(?: \((?:G[1-9]\d*, )?(?:Visual Hierarchy|Spacing|Color|Typography|Motion)\))?: ([^?]+)\?$/i.exec(title);
|
||||
if (!issue || !new RegExp(`^Issue ${issue[1]}(?:: [A-Za-z][A-Za-z0-9 _-]{0,39})?$`, 'i').test(q.header.trim()) ||
|
||||
!/\b(?:fix|resolve|address|make)\b/i.test(title) || /<gstack-qid:/i.test(q.question) || q.multiSelect ||
|
||||
/<gstack-qid:/i.test(q.question) || q.multiSelect ||
|
||||
q.options.length < 2 || new Set(q.options.map(o => o.label)).size !== q.options.length ||
|
||||
fp.options.length !== q.options.length || !fp.options.every((o, i) => o.index === i + 1 && o.label === q.options[i]!.label) ||
|
||||
!q.options.some(o => o.label === call.answers?.[q.question])) return false;
|
||||
@@ -78,7 +78,8 @@ function ordinaryDesignIssue(fp: AskUserQuestionFingerprint): boolean {
|
||||
// A source citation alone can describe a report or the next reviewer.
|
||||
// Bind the alternate wording to a named control's concrete style amendment
|
||||
// and the opposed choice that leaves the documented violation unresolved.
|
||||
const primary = /^Make ([A-Za-z][A-Za-z0-9 _-]{0,39}) the (?:visible|only filled) primary action$/i.exec(issue[2]!);
|
||||
const primary = /^Make ([A-Za-z][A-Za-z0-9 _-]{0,39}) the (?:visible|only filled) primary action$/i.exec(issue[2]!) ??
|
||||
/^How should (?:the )?(?:header )?actions establish that ([A-Za-z][A-Za-z0-9 _-]{0,39}) is the primary action$/i.exec(issue[2]!);
|
||||
const amendments = primary && [
|
||||
new RegExp(`^(?:✅\\s*)?Matches DESIGN\\.md exactly: ${primary[1]} filled #[0-9a-f]{6} with (?:white|black) text; ` +
|
||||
'[A-Za-z][A-Za-z0-9 ,_-]{0,99} as neutral ghost buttons\\.', 'i'),
|
||||
@@ -88,6 +89,8 @@ function ordinaryDesignIssue(fp: AskUserQuestionFingerprint): boolean {
|
||||
'[A-Za-z][A-Za-z0-9 /,_-]{0,99} become neutral ghosts, exactly per DESIGN\\.md\\b', 'i'),
|
||||
new RegExp(`^(?:✅\\s*)?Apply DESIGN\\.md tokens: ${primary[1]} #[0-9a-f]{6} filled with (?:white|black) text; ` +
|
||||
'[A-Za-z][A-Za-z0-9 ,/_-]{0,99} neutral ghost(?: buttons)?\\.', 'i'),
|
||||
new RegExp(`^(?:✅\\s*)?${primary[1]} filled (?:primary )?#[0-9a-f]{6}(?:/| with )(?:white|black)(?: text)?; ` +
|
||||
'[A-Za-z][A-Za-z0-9 ,/_-]{0,99} neutral ghost(?: buttons)?\\. Exact DESIGN\\.md\\.', 'i'),
|
||||
];
|
||||
const primaryHeader = !q.header.includes(':') || q.header.split(':')[1]!.trim().toLowerCase() === primary?.[1]?.toLowerCase();
|
||||
// The style wordings share one owned decision: a current equal-weight gap,
|
||||
@@ -101,13 +104,16 @@ function ordinaryDesignIssue(fp: AskUserQuestionFingerprint): boolean {
|
||||
const assessments = [...questionText.matchAll(/^ELI10: (.+)$/gm)];
|
||||
const prefix = questionText.slice(0, assessments[0]?.index ?? 0)
|
||||
.split('\n').filter(line => line.trim()).slice(1);
|
||||
const sourceAssessment = /\b(?:historical|hypothetical|quoted|source)\s+(?:example|excerpt|assessment|material|text)\b|\bnot\s+(?:the\s+)?current\s+(?:UI|assessment|finding)\b/i;
|
||||
const sourceAssessment = /\b(?:historical|hypothetical|quoted|source)\s+(?:example|excerpt|assessment|material|text)\b|\bnot\s+(?:the\s+)?current\s+(?:UI|assessment|finding|amendment|deferral|remedy|choice|option)\b/i;
|
||||
const assessment = assessments.length === 1 &&
|
||||
prefix.every(line => /^(?:Project\/branch\/task:|\[P[0-3]\])/.test(line)) &&
|
||||
!sourceAssessment.test(prefix.join(' ')) && !sourceAssessment.test(assessments[0]![1]!)
|
||||
? assessments[0]![1]! : '';
|
||||
const currentPrimary = primary && new RegExp(`^(?:Right now|Today) ${primary[1]}(?:, [A-Za-z][A-Za-z0-9 _-]{0,39})+(?:,? and [A-Za-z][A-Za-z0-9 _-]{0,39})? (?:(?:all )?look (?:the same|identical)|are (?:all )?(?:(?:two|three|four|five|six|seven|eight|nine|ten|[1-9]\\d*) )?identical buttons)\\b`, 'i').test(assessment);
|
||||
const withdrawn = new RegExp(`(?:^|[.!?]\\s+|\\n)(?:Correction:\\s*)?(?:(?:This (?:issue|finding|question)|Issue ${issue[1]}) (?:is|was|has been) (?:withdrawn|resolved|closed|hypothetical)|We have (?:resolved|closed|withdrawn) this (?:issue|finding)|No current (?:issue|finding|gap|violation) (?:remains|exists))\\b`, 'i');
|
||||
const primaryAssessment = primary && new RegExp(`^(?:Right now|Today) ${primary[1]}(?:, [A-Za-z][A-Za-z0-9 _-]{0,39})+(?:,? and [A-Za-z][A-Za-z0-9 _-]{0,39})? (?:(?:all )?look (?:the same|identical)|are (?:all )?(?:(?:two|three|four|five|six|seven|eight|nine|ten|[1-9]\\d*) )?identical buttons)\\b`, 'i').exec(assessment)?.[0];
|
||||
const premiseSentence = assessment.split(/[.!?](?:\s|$)/)[0] ?? '';
|
||||
const currentPrimary = !!primaryAssessment && !/\b(?:not|never|no longer)\b/i.test(primaryAssessment) &&
|
||||
!/\b(?:archived|historical|hypothetical|quoted|example|previous|earlier)\b/i.test(premiseSentence);
|
||||
const withdrawn = new RegExp(`(?:^|[.!?]\\s+|\\n)(?:Correction:\\s*)?(?:(?:This (?:issue|finding|question|amendment|deferral|style|fix|remedy|choice|option)|Issue ${issue[1]}) (?:is|was|has been) (?:withdrawn|resolved|closed|hypothetical)|We have (?:resolved|closed|withdrawn) this (?:issue|finding)|No current (?:issue|finding|gap|violation) (?:remains|exists))\\b`, 'i');
|
||||
const closedGap = /(?:^|[.!?;]\s+|\n)(?:Correction:\s*)?(?:this|the|that) (?:gap|violation) (?:is|was|has been) (?:already\s+|now\s+)?(?:resolved|fixed|closed)\b/i;
|
||||
const cancelledStyle = /(?:^|[.!?;]\s+|\n)(?:Correction:\s*)?(?:do not|don't|never|skip|cancel|withdraw)\s+(?:apply|use|add|keep)\s+(?:(?:these|the|this)\s+)?(?:tokens?|styles?|primary treatment)\b/i;
|
||||
const choiceIds = q.options.map(o => /^([1-9]\d*)[A-Z](?:[).:]?\s+)/.exec(o.label));
|
||||
@@ -120,7 +126,7 @@ function ordinaryDesignIssue(fp: AskUserQuestionFingerprint): boolean {
|
||||
const style = amendments.map(pattern => pattern.exec(body)).find(Boolean)?.[0];
|
||||
// A named primary cannot simultaneously occur in the ghost-control list.
|
||||
if (!style || new RegExp(`\\b${primary![1]}\\b`, 'i').test(style.slice(style.indexOf(';') + 1)) ||
|
||||
withdrawn.test(body) || closedGap.test(body) || cancelledStyle.test(body)) return false;
|
||||
sourceAssessment.test(body) || withdrawn.test(body) || closedGap.test(body) || cancelledStyle.test(body)) return false;
|
||||
return opposed.some(defer => {
|
||||
const declined = currentText(defer.description ?? '');
|
||||
// Native menus can list current benefits before the gap retained by
|
||||
@@ -128,8 +134,8 @@ function ordinaryDesignIssue(fp: AskUserQuestionFingerprint): boolean {
|
||||
// framing a source example or a future condition cannot expose an icon.
|
||||
const pros = /^(?:✅(?!\s*(?:If|When|Unless|Historical|Hypothetical|Quoted|Source|Example)\b)\s*[^✅❌]+)+❌\s*/i.exec(declined);
|
||||
const remaining = pros && !sourceAssessment.test(pros[0]) ? declined.slice(pros[0].length) : declined;
|
||||
return defer !== amendment && !withdrawn.test(declined) && !closedGap.test(declined) &&
|
||||
/^(?:❌\s*)?(?:Leaves a documented DESIGN\.md violation in place|Ships the documented violation;[^.\n]*\bthe gap remains open|Primary-action ambiguity ships; documented DESIGN\.md violation remains|Decline the fix; gap stays documented and lowers the score)\b/i.test(remaining);
|
||||
return defer !== amendment && !sourceAssessment.test(declined) && !withdrawn.test(declined) && !closedGap.test(declined) &&
|
||||
/^(?:❌\s*)?(?:Leaves a documented DESIGN\.md violation in place|Ships the documented violation;[^.\n]*\bthe gap remains open|Primary-action ambiguity ships; documented DESIGN\.md violation remains|Decline the fix; gap stays documented and lowers the score|Keep all (?:two|three|four|five|six|seven|eight|nine|ten|[1-9]\d*) identical; record as an open DESIGN\.md violation)\b/i.test(remaining);
|
||||
});
|
||||
});
|
||||
return opposed.length > 0 && !!(repair || primaryRepair);
|
||||
|
||||
@@ -224,6 +224,54 @@ function declaredLegacyCharacterization(text: string): boolean {
|
||||
}
|
||||
}
|
||||
}
|
||||
// A golden-master requirement names the existing output oracle, then ties
|
||||
// its capture task to an untouched baseline and reruns after later tasks.
|
||||
const goldenSourceOwner = (body: string) => sourceOwner(body) || /(?:^|\n)\s*(?:Source|Quoted source) excerpt:\s*(?:\n|$)/i.test(body);
|
||||
const goldenWithdrawn = current.some(s => {
|
||||
const namedSuite = /^(.*?)\b(?:regression|characterization|golden[ -]master)\s+(?:suite|fixtures?|tests?)\b/i.exec(s.title);
|
||||
const foreignSuite = Boolean(namedSuite?.[1]?.trim() && !/^(?:legacy(?:AuthFlow(?:\(\))?)?|final|current|updated)[\s:—–-]*$/i.test(namedSuite[1]));
|
||||
return unquoted(s.body.join('\n')).split(/\n|[.!?]\s+/).some(statement => {
|
||||
const subject = /^(?:Correction:\s*)?(?:the|this|that)\s+(legacy(?:AuthFlow(?:\(\))?)?\s+)?golden[ -]master\s+(?:suite|fixtures?|tests?)\b/i.exec(statement.trim());
|
||||
return Boolean(subject && (!foreignSuite || subject[1]) && withdrawn(statement
|
||||
.replace(/golden[ -]master\s+(?:suite|fixtures?|tests?)/i, 'regression suite').replace(/\b(?:are|were|have been)\b/i, 'is')));
|
||||
});
|
||||
});
|
||||
for (const section of current.filter(s => /^Tests(?: \([^\n]*\))?$/i.test(s.title))) {
|
||||
const body = unquoted(section.body.join('\n')).split(/\n\s*\n/)
|
||||
.map(paragraph => paragraph.replace(/\s+/g, ' ').trim()).join('\n\n');
|
||||
const claim = /^CRITICAL \(regression rule, mandatory\): legacyAuthFlow(?:\(\))? golden[ -]master\.\s+(?:Capture|Pin|Record) current outputs for [^.!?]{1,300} BEFORE any change, assert identical behavio[u]?r after the rewrite(?: and after [^.!?]{1,120})?\./im.exec(body);
|
||||
if (!claim || suiteWithdrawn || goldenWithdrawn || withdrawn(body) ||
|
||||
!snapshotSource.includes(claim[0].replace(/\s+/g, ' ')) ||
|
||||
goldenSourceOwner(body.slice(0, claim.index)) || conditionalOwner(body.slice(0, claim.index))) continue;
|
||||
for (const tasks of current.filter(s => s.title === 'Implementation Tasks')) {
|
||||
const taskBody = tasks.body.join('\n').trim();
|
||||
const taskPrefix = taskBody.split(/\n(?=-\s)/)[0]?.trim() ?? '';
|
||||
if (!taskBody.startsWith('- ') && (!/^Synthesized from (?:this|the) review's findings\./.test(taskPrefix) ||
|
||||
/\b(?:if|unless|optional|hypothetical|example|source|quoted|unproven)\b/i.test(unquoted(taskPrefix)))) continue;
|
||||
for (const task of taskBody.split(/\n(?=-\s)/)) {
|
||||
const match = /^- (?:\[[ xX]\] )?(T[1-9]\d*)(?: \([^\n)]*\))? [—–-] [A-Za-z][\w-]*(?:\/[A-Za-z][\w-]*)* [—–-] Capture golden[ -]master regression fixtures for legacyAuthFlow(?:\(\))? before any change[\t ]*(?:\n|$)/i.exec(task);
|
||||
const verify = /^\s+- Verify: fixtures pass against untouched legacy; rerun after every later task[\t ]*$/m.exec(task);
|
||||
const taskIntro = unquoted(taskBody.slice(0, taskBody.indexOf(task))).trim().split('\n').at(-1) ?? '';
|
||||
if (!match || !verify || !snapshotSource.includes(task.replace(/\s+/g, ' ').trim()) ||
|
||||
/\b(?:if|unless|optional|hypothetical|source|quoted|unproven)\b/i.test(match[0]) ||
|
||||
conditionalOwner(taskIntro) || goldenSourceOwner(taskIntro) ||
|
||||
conditionalOwner(task.slice(0, verify.index)) || goldenSourceOwner(unquoted(task.slice(0, verify.index))) ||
|
||||
withdrawn(unquoted(task).replace(/\b(?:this|that|the)\s+(?:(?:unchanged-code|baseline)\s+)?verification\b/gi, 'this requirement'), match[1])) continue;
|
||||
const taskWithdrawal = new RegExp(`\\b${match[1]}(?:\\s+rerun)?\\s+(?:is|was|has been)\\s+(?:cancelled|canceled|withdrawn|rejected|deferred|optional|not required|no longer required)\\b`, 'i');
|
||||
if (current.some(s => taskWithdrawal.test(unquoted(s.body.join('\n'))))) continue;
|
||||
for (const verification of current.filter(s => /^Verification(?: \([^\n]*\))?$/i.test(s.title))) {
|
||||
const body = unquoted(verification.body.join('\n')).trim();
|
||||
const baseline = new RegExp(`^([1-9]\\d*)\\. Run ${match[1]} fixtures before touching anything; they must pass\\.[\\t ]*$`, 'm').exec(body);
|
||||
const rerun = new RegExp(`^([1-9]\\d*)\\. After each task, rerun the full suite plus ${match[1]} fixtures\\.[\\t ]*$`, 'm').exec(body);
|
||||
if (baseline && rerun && Number(baseline[1]) < Number(rerun[1]) && baseline.index < rerun.index &&
|
||||
!conditionalOwner(body.slice(0, baseline.index)) && !conditionalOwner(body.slice(0, rerun.index)) &&
|
||||
!goldenSourceOwner(body.slice(0, baseline.index)) && !goldenSourceOwner(body.slice(0, rerun.index)) &&
|
||||
!withdrawn(body.replace(/\b(?:this|that|the)\s+(?:baseline|verification)\b/gi, 'this requirement'), match[1]) &&
|
||||
snapshotSource.includes(baseline[0]) && snapshotSource.includes(rerun[0])) return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ export function nativeSeededPlanSelection(
|
||||
const line = message.text.split(/\r?\n/).find(value => value.trim());
|
||||
if (!line || /^(?: {4}|\t)/.test(line)) continue;
|
||||
const text = line.trim();
|
||||
const selectedNow = /^(?:I've|I have) selected (?:reviewing|to review)\s+(?:the\s+)?pasted\s+(?:"([^"\n]+)"|“([^”\n]+)”|`([^`\n]+)`)\s+(?:draft(?:\s+plan)?|plan)(.*)$/i.exec(text);
|
||||
const selectedNow = /^(?:I've|I have) selected (?:option B, )?(?:reviewing|to review)\s+(?:the\s+)?pasted\s+(?:"([^"\n]+)"|“([^”\n]+)”|`([^`\n]+)`)\s+(?:draft(?:\s+plan)?|plan)(.*)$/i.exec(text);
|
||||
const selected = selectedNow ?? /^(?:Scope gate confirms plan mode, so )?(?:I'll review|I will review|I'll go with reviewing|I will go with reviewing|I'll proceed with reviewing|I will proceed with reviewing|I'm proceeding with reviewing|I am proceeding with reviewing)\s+(?:the\s+)?(?:pasted\s+)?(?:"([^"\n]+)"|“([^”\n]+)”|`([^`\n]+)`)\s+(?:draft(?:\s+plan)?|plan)(?:\s+(?:you pasted|pasted here))?(.*)$/i.exec(text);
|
||||
if (!selected || (selected[1] ?? selected[2] ?? selected[3])!.trim().toLowerCase() !== title.toLowerCase()) continue;
|
||||
const tail = selected[4]!;
|
||||
@@ -78,7 +78,7 @@ export function nativeSeededPlanSelection(
|
||||
// A completed selection may name the pasted target before its plan-mode
|
||||
// reason. Keep the first assertion bound; later work is not a new target.
|
||||
// Internal token dots (DESIGN.md) do not open another sentence.
|
||||
if (/^(?:\s+(?:since|because)\s+(?:we're|we are|I'm|I am)\s+in plan mode)?\.(?:\s+(?:Now|Next,|Then)\s+(?:I'll|I will)\s+(?:run|start|begin)\s+(?:the\s+)?(?:pre-review\s+)?audit\b(?:[^.!?]|\.(?=\S))*\.)?$/i.test(tail) && remainsSelected(message.timestamp)) return true;
|
||||
if (/^(?:\s+(?:since|because)\s+(?:we're|we are|I'm|I am)\s+in plan mode)?\.(?:\s+(?:Now|Next,?|Then)\s+(?:I'll|I will)\s+(?:run|start|begin)\s+(?:the\s+)?(?:pre-review\s+)?(?:audit|Design Doc Check)\b(?:[^.!?]|\.(?=\S))*\.)?$/i.test(tail) && remainsSelected(message.timestamp)) return true;
|
||||
continue;
|
||||
}
|
||||
if (/^(?:\.(?:\s+(?:Next,|Then\b).*)?|,\s*(?:starting|beginning)\s+(?:with|by)\b.*|,\s*and\s+now\s+(?:I'm|I am)\s+(?:running|starting|beginning)\s+(?:the\s+)?(?:pre-review\s+)?audit\b[^?]*\.|\. Running (?:the )?(?:pre-review )?audit\b(?:[^.!?]|\.(?=\S))*\.|)$/.test(tail) && remainsSelected(message.timestamp)) return true;
|
||||
|
||||
@@ -104,21 +104,29 @@ export const E2E_TOUCHFILES: Record<string, string[]> = {
|
||||
'test/auto-decide-saved-ai.test.ts', 'test/fixtures/auto-decide-saved-ai.json', 'test/fixtures/auto-decide-retry-ai.json','bin/gstack-skill-start', 'bin/gstack-skill-end', 'plan-ceo-review/**', 'scripts/resolvers/preamble/generate-completion-status.ts', 'scripts/resolvers/question-tuning.ts', 'scripts/resolvers/preamble/generate-ask-user-format.ts', 'scripts/resolvers/preamble.ts', 'scripts/resolvers/review.ts', 'test/helpers/claude-pty-runner.ts', 'test/helpers/hermetic-skill-runtime.ts', 'test/hermetic-skill-runtime.test.ts', 'test/helpers/pty-trust-dialog.ts', 'test/pty-trust-dialog.test.ts', 'test/skill-e2e-plan-ceo-plan-mode.test.ts', 'test/plan-count-truncated-question.test.ts', 'test/fixtures/ceo-approach-z-call.json', 'test/fixtures/ceo-approach-z-screen.txt', 'test/helpers/plan-scope-selection.ts', 'test/plan-scope-selection.test.ts', 'test/helpers/native-auto-decide.ts', 'test/native-auto-decide.test.ts', 'test/native-auto-decide-pty.test.ts', 'test/fixtures/native-auto-decide-ag.json', 'test/eng-seeded-completion-ai.test.ts', 'test/fixtures/eng-seeded-completion-ai.json', 'test/helpers/plan-count-pending-exit.ts', 'test/plan-count-pending-exit.test.ts', 'test/helpers/pty-screen.ts', 'test/pty-screen.test.ts', 'test/pty-screen-session.test.ts', 'test/fixtures/pty-screen/**',
|
||||
'test/design-scope-declaration-ak.test.ts',
|
||||
'test/fixtures/design-scope-declaration-ak.json',
|
||||
"test/eng-option-b-scope-al.test.ts",
|
||||
"test/fixtures/eng-option-b-scope-al.json",
|
||||
],
|
||||
'plan-eng-review-plan-mode': [
|
||||
'test/auto-decide-saved-ai.test.ts', 'test/fixtures/auto-decide-saved-ai.json', 'test/fixtures/auto-decide-retry-ai.json','bin/gstack-skill-start', 'bin/gstack-skill-end', 'plan-eng-review/**', 'scripts/resolvers/preamble/generate-completion-status.ts', 'scripts/resolvers/question-tuning.ts', 'scripts/resolvers/preamble/generate-ask-user-format.ts', 'scripts/resolvers/preamble.ts', 'scripts/resolvers/review.ts', 'test/helpers/claude-pty-runner.ts', 'test/helpers/hermetic-skill-runtime.ts', 'test/hermetic-skill-runtime.test.ts', 'test/helpers/pty-trust-dialog.ts', 'test/pty-trust-dialog.test.ts', 'test/skill-e2e-plan-eng-plan-mode.test.ts', 'test/plan-count-truncated-question.test.ts', 'test/fixtures/ceo-approach-z-call.json', 'test/fixtures/ceo-approach-z-screen.txt', 'test/helpers/plan-scope-selection.ts', 'test/plan-scope-selection.test.ts', 'test/fixtures/design-plan-scope-ag.json', 'test/design-scope-selection-aj.test.ts', 'test/fixtures/design-scope-selection-aj.json', 'test/helpers/native-auto-decide.ts', 'test/native-auto-decide.test.ts', 'test/native-auto-decide-pty.test.ts', 'test/fixtures/native-auto-decide-ag.json', 'test/eng-seeded-completion-ai.test.ts', 'test/fixtures/eng-seeded-completion-ai.json', 'test/helpers/plan-count-pending-exit.ts', 'test/plan-count-pending-exit.test.ts', 'test/helpers/pty-screen.ts', 'test/pty-screen.test.ts', 'test/pty-screen-session.test.ts', 'test/fixtures/pty-screen/**',
|
||||
'test/design-scope-declaration-ak.test.ts',
|
||||
'test/fixtures/design-scope-declaration-ak.json',
|
||||
"test/eng-option-b-scope-al.test.ts",
|
||||
"test/fixtures/eng-option-b-scope-al.json",
|
||||
],
|
||||
'plan-design-review-plan-mode': [
|
||||
'test/auto-decide-saved-ai.test.ts', 'test/fixtures/auto-decide-saved-ai.json', 'test/fixtures/auto-decide-retry-ai.json','bin/gstack-skill-start', 'bin/gstack-skill-end', 'plan-design-review/**', 'scripts/resolvers/preamble/generate-completion-status.ts', 'scripts/resolvers/question-tuning.ts', 'scripts/resolvers/preamble/generate-ask-user-format.ts', 'scripts/resolvers/preamble.ts', 'scripts/resolvers/review.ts', 'test/helpers/claude-pty-runner.ts', 'test/helpers/hermetic-skill-runtime.ts', 'test/hermetic-skill-runtime.test.ts', 'test/helpers/pty-trust-dialog.ts', 'test/pty-trust-dialog.test.ts', 'test/skill-e2e-plan-design-plan-mode.test.ts', 'test/skill-e2e-design.test.ts', 'test/plan-count-truncated-question.test.ts', 'test/fixtures/ceo-approach-z-call.json', 'test/fixtures/ceo-approach-z-screen.txt', 'test/helpers/plan-scope-selection.ts', 'test/plan-scope-selection.test.ts', 'test/fixtures/design-plan-scope-ag.json', 'test/design-scope-selection-aj.test.ts', 'test/fixtures/design-scope-selection-aj.json', 'test/helpers/native-auto-decide.ts', 'test/native-auto-decide.test.ts', 'test/native-auto-decide-pty.test.ts', 'test/fixtures/native-auto-decide-ag.json', 'test/eng-seeded-completion-ai.test.ts', 'test/fixtures/eng-seeded-completion-ai.json', 'test/helpers/plan-count-pending-exit.ts', 'test/plan-count-pending-exit.test.ts', 'test/helpers/pty-screen.ts', 'test/pty-screen.test.ts', 'test/pty-screen-session.test.ts', 'test/fixtures/pty-screen/**',
|
||||
'test/design-scope-declaration-ak.test.ts',
|
||||
'test/fixtures/design-scope-declaration-ak.json',
|
||||
"test/eng-option-b-scope-al.test.ts",
|
||||
"test/fixtures/eng-option-b-scope-al.json",
|
||||
],
|
||||
'plan-devex-review-plan-mode': [
|
||||
'test/auto-decide-saved-ai.test.ts', 'test/fixtures/auto-decide-saved-ai.json', 'test/fixtures/auto-decide-retry-ai.json','bin/gstack-skill-start', 'bin/gstack-skill-end', 'plan-devex-review/**', 'scripts/resolvers/preamble/generate-completion-status.ts', 'scripts/resolvers/question-tuning.ts', 'scripts/resolvers/preamble/generate-ask-user-format.ts', 'scripts/resolvers/preamble.ts', 'scripts/resolvers/review.ts', 'test/helpers/claude-pty-runner.ts', 'test/helpers/hermetic-skill-runtime.ts', 'test/hermetic-skill-runtime.test.ts', 'test/helpers/pty-trust-dialog.ts', 'test/pty-trust-dialog.test.ts', 'test/skill-e2e-plan-devex-plan-mode.test.ts', 'test/plan-count-truncated-question.test.ts', 'test/fixtures/ceo-approach-z-call.json', 'test/fixtures/ceo-approach-z-screen.txt', 'test/helpers/plan-scope-selection.ts', 'test/plan-scope-selection.test.ts', 'test/helpers/native-auto-decide.ts', 'test/native-auto-decide.test.ts', 'test/native-auto-decide-pty.test.ts', 'test/fixtures/native-auto-decide-ag.json', 'test/eng-seeded-completion-ai.test.ts', 'test/fixtures/eng-seeded-completion-ai.json', 'test/helpers/plan-count-pending-exit.ts', 'test/plan-count-pending-exit.test.ts', 'test/helpers/pty-screen.ts', 'test/pty-screen.test.ts', 'test/pty-screen-session.test.ts', 'test/fixtures/pty-screen/**',
|
||||
'test/design-scope-declaration-ak.test.ts',
|
||||
'test/fixtures/design-scope-declaration-ak.json',
|
||||
"test/eng-option-b-scope-al.test.ts",
|
||||
"test/fixtures/eng-option-b-scope-al.json",
|
||||
],
|
||||
// Covers ceo (preamble misfire) + eng/design (scope-gate bypass must not
|
||||
// fire outside plan mode) + the named-target exception case. 4 PTY runs;
|
||||
@@ -130,6 +138,8 @@ export const E2E_TOUCHFILES: Record<string, string[]> = {
|
||||
'test/auto-decide-saved-ai.test.ts', 'test/fixtures/auto-decide-saved-ai.json', 'test/fixtures/auto-decide-retry-ai.json','bin/gstack-skill-start', 'bin/gstack-skill-end', 'plan-ceo-review/**', 'plan-eng-review/**', 'plan-design-review/**', 'scripts/resolvers/preamble/generate-completion-status.ts', 'scripts/resolvers/preamble.ts', 'test/helpers/claude-pty-runner.ts', 'test/helpers/hermetic-skill-runtime.ts', 'test/hermetic-skill-runtime.test.ts', 'test/helpers/pty-trust-dialog.ts', 'test/pty-trust-dialog.test.ts', 'test/skill-e2e-plan-mode-no-op.test.ts', 'test/plan-count-truncated-question.test.ts', 'test/fixtures/ceo-approach-z-call.json', 'test/fixtures/ceo-approach-z-screen.txt', 'test/helpers/plan-scope-selection.ts', 'test/plan-scope-selection.test.ts', 'test/helpers/native-auto-decide.ts', 'test/native-auto-decide.test.ts', 'test/native-auto-decide-pty.test.ts', 'test/fixtures/native-auto-decide-ag.json', 'test/eng-seeded-completion-ai.test.ts', 'test/fixtures/eng-seeded-completion-ai.json', 'test/helpers/plan-count-pending-exit.ts', 'test/plan-count-pending-exit.test.ts', 'test/helpers/pty-screen.ts', 'test/pty-screen.test.ts', 'test/pty-screen-session.test.ts', 'test/fixtures/pty-screen/**',
|
||||
'test/design-scope-declaration-ak.test.ts',
|
||||
'test/fixtures/design-scope-declaration-ak.json',
|
||||
"test/eng-option-b-scope-al.test.ts",
|
||||
"test/fixtures/eng-option-b-scope-al.json",
|
||||
],
|
||||
|
||||
// v1.21+ AskUserQuestion-blocked regression tests — Conductor launches
|
||||
@@ -149,7 +159,9 @@ export const E2E_TOUCHFILES: Record<string, string[]> = {
|
||||
// surfacing the question. Touches the question-tuning + preference
|
||||
// infrastructure plus the resolvers that own the AUTO_DECIDE preamble.
|
||||
'auto-decide-preserved': [
|
||||
'test/auto-decide-saved-ai.test.ts', 'test/fixtures/auto-decide-saved-ai.json', 'test/fixtures/auto-decide-retry-ai.json','bin/gstack-skill-start', 'bin/gstack-skill-end', 'bin/gstack-session-kind', 'scripts/resolvers/question-tuning.ts', 'scripts/resolvers/preamble/generate-ask-user-format.ts', 'scripts/resolvers/preamble/generate-preamble-bash.ts', 'scripts/resolvers/preamble/generate-completion-status.ts', 'plan-ceo-review/**', 'bin/gstack-question-preference', 'bin/gstack-config', 'bin/gstack-slug', 'hosts/claude/hooks/question-preference-hook.ts', 'hosts/claude/hooks/spawned-directive.ts', 'lib/is-conductor.ts', 'test/helpers/claude-pty-runner.ts', 'test/helpers/hermetic-skill-runtime.ts', 'test/hermetic-skill-runtime.test.ts', 'test/helpers/pty-trust-dialog.ts', 'test/pty-trust-dialog.test.ts', 'test/skill-e2e-auto-decide-preserved.test.ts', 'test/plan-count-truncated-question.test.ts', 'test/fixtures/ceo-approach-z-call.json', 'test/fixtures/ceo-approach-z-screen.txt', 'test/helpers/native-auto-decide.ts', 'test/native-auto-decide.test.ts', 'test/native-auto-decide-pty.test.ts', 'test/fixtures/native-auto-decide-ag.json', 'test/eng-seeded-completion-ai.test.ts', 'test/fixtures/eng-seeded-completion-ai.json', 'test/helpers/plan-count-pending-exit.ts', 'test/plan-count-pending-exit.test.ts', 'test/helpers/pty-screen.ts', 'test/pty-screen.test.ts', 'test/pty-screen-session.test.ts', 'test/fixtures/pty-screen/**'],
|
||||
'test/auto-decide-saved-ai.test.ts', 'test/fixtures/auto-decide-saved-ai.json', 'test/fixtures/auto-decide-retry-ai.json','bin/gstack-skill-start', 'bin/gstack-skill-end', 'bin/gstack-session-kind', 'scripts/resolvers/question-tuning.ts', 'scripts/resolvers/preamble/generate-ask-user-format.ts', 'scripts/resolvers/preamble/generate-preamble-bash.ts', 'scripts/resolvers/preamble/generate-completion-status.ts', 'plan-ceo-review/**', 'bin/gstack-question-preference', 'bin/gstack-config', 'bin/gstack-slug', 'hosts/claude/hooks/question-preference-hook.ts', 'hosts/claude/hooks/spawned-directive.ts', 'lib/is-conductor.ts', 'test/helpers/claude-pty-runner.ts', 'test/helpers/hermetic-skill-runtime.ts', 'test/hermetic-skill-runtime.test.ts', 'test/helpers/pty-trust-dialog.ts', 'test/pty-trust-dialog.test.ts', 'test/skill-e2e-auto-decide-preserved.test.ts', 'test/plan-count-truncated-question.test.ts', 'test/fixtures/ceo-approach-z-call.json', 'test/fixtures/ceo-approach-z-screen.txt', 'test/helpers/native-auto-decide.ts', 'test/native-auto-decide.test.ts', 'test/native-auto-decide-pty.test.ts', 'test/fixtures/native-auto-decide-ag.json', 'test/eng-seeded-completion-ai.test.ts', 'test/fixtures/eng-seeded-completion-ai.json', 'test/helpers/plan-count-pending-exit.ts', 'test/plan-count-pending-exit.test.ts', 'test/helpers/pty-screen.ts', 'test/pty-screen.test.ts', 'test/pty-screen-session.test.ts', 'test/fixtures/pty-screen/**',
|
||||
"test/ceo-mode-preference-al.test.ts",
|
||||
],
|
||||
|
||||
// Conductor → prose decision brief (Conductor signal makes prose the default;
|
||||
// the PreToolUse hook denies the flaky tool). Touches the resolver that owns
|
||||
@@ -179,6 +191,9 @@ export const E2E_TOUCHFILES: Record<string, string[]> = {
|
||||
'autoplan-chain-pty': ['scripts/resolvers/composition.ts', 'test/autoplan-review-discovery.test.ts', 'test/autoplan-phase-order.test.ts', 'autoplan/**', 'plan-ceo-review/**', 'plan-design-review/**', 'plan-eng-review/**', 'plan-devex-review/**', 'test/fixtures/plans/autoplan-dashboard.md', 'test/autoplan-chain-fixture.test.ts', 'test/helpers/plan-count-fixture.ts', 'test/plan-count-fixture.test.ts', 'test/plan-count-prerequisite-n.test.ts', 'test/fixtures/ceo-prerequisite-n-call.json', 'bin/gstack-config', 'test/helpers/claude-pty-runner.ts', 'test/plan-count-native-input.test.ts', 'test/helpers/pty-screen.ts', 'test/pty-screen.test.ts', 'test/pty-screen-session.test.ts', 'test/fixtures/pty-screen/**', 'test/helpers/hermetic-skill-runtime.ts', 'test/hermetic-skill-runtime.test.ts', 'test/helpers/pty-trust-dialog.ts', 'test/pty-trust-dialog.test.ts', 'test/helpers/autoplan-setup-question.ts', 'test/autoplan-setup-question.test.ts', 'test/helpers/autoplan-phase-observer.ts', 'test/autoplan-phase-observer.test.ts', 'test/helpers/plan-count-transcript.ts', 'test/autoplan-public-narration.test.ts', 'test/fixtures/autoplan-public-narration-ad.json', 'test/plan-count-transcript.test.ts', 'test/helpers/plan-count-artifacts.ts', 'test/plan-count-artifacts.test.ts', 'test/skill-e2e-autoplan-chain.test.ts', 'bin/gstack-autoplan-snapshot.ts', 'test/autoplan-snapshot.test.ts', 'test/autoplan-init.test.ts', 'test/autoplan-obligations.test.ts', 'test/fixtures/autoplan/t-ceo-omitted-obligations.json', 'test/plan-count-checkbox.test.ts', 'test/fixtures/ceo-checkbox-l.screen.txt', 'test/helpers/eval-budgets.ts', 'test/autoplan-eval-budget.test.ts', 'test/eval-budgets-policy.test.ts', 'test/eval-detach-timeout-floor.test.ts', 'scripts/test-paid-shards.ts', '.github/workflows/evals-periodic.yml', 'test/fixtures/autoplan-routing-n-screen.txt', 'test/autoplan-routing-o.test.ts', 'test/fixtures/autoplan-routing-o-screen.txt', 'test/autoplan-setup-packet-o.test.ts', 'test/fixtures/autoplan-setup-packet-o-screen.txt', 'test/fixtures/autoplan-setup-packet-o-call.json', 'test/fixtures/autoplan/u-ceo-original-loss.json', 'test/fixtures/autoplan/v-ceo-dangling-references.json', 'test/fixtures/autoplan-setup-z-packet.json', 'test/plan-count-truncated-question.test.ts', 'test/fixtures/ceo-approach-z-call.json', 'test/fixtures/ceo-approach-z-screen.txt', 'test/helpers/autoplan-method-read-audit.ts', 'test/autoplan-method-read-audit.test.ts', 'test/fixtures/autoplan-method-read-aa-events.json', 'test/autoplan-routing-manual-skills.test.ts', 'test/fixtures/autoplan-routing-manual-skills-ac.json', 'test/helpers/plan-count-pending-question.ts', 'test/autoplan-pending-question.test.ts', 'test/plan-pending-question-pty.test.ts', 'test/pending-question-completion.test.ts', 'test/fixtures/pending-question-completion-ad.json', 'test/fixtures/autoplan-setup-ad-v2-packet.json', 'test/helpers/autoplan-artifact-permission.ts', 'test/autoplan-artifact-permission.test.ts', 'test/fixtures/autoplan-artifact-permission-ad-v3.json', 'scripts/resolvers/testing.ts', 'test/helpers/autoplan-artifact-recorder.ts', 'test/autoplan-artifact-recorder.test.ts', 'test/autoplan-pending-artifact.test.ts', 'test/fixtures/autoplan-pending-artifact-ae.json', 'test/autoplan-edit-header-ag.test.ts', 'test/fixtures/autoplan-edit-header-ag.json', 'test/autoplan-edit-prefix-ai.test.ts', 'test/fixtures/autoplan-edit-prefix-ai.json', 'test/autoplan-edit-panel-aj.test.ts', 'test/fixtures/autoplan-edit-panel-aj.json',
|
||||
'test/autoplan-repeated-header-ak.test.ts',
|
||||
'test/fixtures/autoplan-repeated-header-ak.json',
|
||||
"test/helpers/autoplan-artifact-digest.ts",
|
||||
"test/autoplan-edit-digests-al.test.ts",
|
||||
"test/fixtures/autoplan-edit-digests-al.json",
|
||||
],
|
||||
|
||||
// Per-finding AskUserQuestion count + review-report-at-bottom assertion.
|
||||
@@ -194,6 +209,8 @@ export const E2E_TOUCHFILES: Record<string, string[]> = {
|
||||
'test/fixtures/ceo-finding-brief-ak.json',
|
||||
'test/plan-count-quoted-frame-ak.test.ts',
|
||||
'test/fixtures/plan-count-quoted-frame-ak.json',
|
||||
"test/ceo-decision-prefix-al.test.ts",
|
||||
"test/fixtures/ceo-decision-prefix-al.json",
|
||||
],
|
||||
'plan-eng-finding-count': ['bin/gstack-config', 'bin/gstack-skill-start', 'bin/gstack-skill-end', 'plan-eng-review/**', 'scripts/resolvers/preamble.ts', 'scripts/resolvers/preamble/generate-ask-user-format.ts', 'scripts/resolvers/preamble/generate-completion-status.ts', 'test/helpers/claude-pty-runner.ts', 'test/plan-count-native-input.test.ts', 'test/helpers/pty-screen.ts', 'test/pty-screen.test.ts', 'test/pty-screen-session.test.ts', 'test/fixtures/pty-screen/**', 'test/helpers/hermetic-skill-runtime.ts', 'test/hermetic-skill-runtime.test.ts', 'test/helpers/claude-pty-runner.unit.test.ts', 'test/plan-count-completion.test.ts', 'test/plan-count-dx-handoff.test.ts', 'test/fixtures/devex-handoff-n-call.json', 'test/helpers/pty-trust-dialog.ts', 'test/pty-trust-dialog.test.ts', 'test/helpers/plan-count-transcript.ts', 'test/autoplan-public-narration.test.ts', 'test/fixtures/autoplan-public-narration-ad.json', 'test/helpers/plan-count-pending-exit.ts', 'test/plan-count-pending-exit.test.ts', 'test/plan-count-transcript.test.ts', 'test/helpers/plan-count-artifacts.ts', 'test/plan-count-artifacts.test.ts', 'test/helpers/eval-store.ts', 'test/helpers/plan-count-fixture.ts', 'test/plan-count-fixture.test.ts', 'test/plan-count-timeout.test.ts', 'test/plan-count-navigation-r.test.ts', 'test/plan-count-prerequisite-n.test.ts', 'test/fixtures/ceo-prerequisite-n-call.json', 'test/skill-e2e-plan-eng-finding-count.test.ts', 'test/fixtures/devex-review-l-calls.json', 'test/plan-count-checkbox.test.ts', 'test/fixtures/ceo-checkbox-l.screen.txt', 'test/plan-count-empty-review.test.ts', 'test/fixtures/ceo-count-s-distinct.json', 'test/fixtures/plan-count-design-questionless-report.md', 'test/plan-count-dx-handoff-o.test.ts', 'test/fixtures/devex-handoff-o-call.json', 'test/eng-devex-s-count.test.ts', 'test/fixtures/eng-devex-s-first-calls.json', 'test/fixtures/eng-devex-s-retry-calls.json', 'test/helpers/plan-count-file-permission.ts', 'test/plan-count-file-permission.test.ts', 'test/fixtures/plan-count-edit-permission-t.json', 'test/eng-first-review-t.test.ts', 'test/fixtures/eng-batching-t-calls.json', 'test/plan-count-preview-footer.test.ts', 'test/fixtures/ceo-preview-u-call.json', 'test/fixtures/ceo-preview-u-screen.txt', 'test/fixtures/design-preview-v-screen.txt', 'test/plan-count-owned-permission.test.ts', 'test/fixtures/plan-count-owned-permission-v.json', 'test/fixtures/ceo-questionless-w-native.json', 'test/eng-scope-y.test.ts', 'test/fixtures/eng-scope-y-calls.json', 'test/plan-count-truncated-question.test.ts', 'test/fixtures/ceo-approach-z-call.json', 'test/fixtures/ceo-approach-z-screen.txt', 'test/eng-binding-z.test.ts', 'test/fixtures/eng-binding-z-calls.json', 'test/eng-binding-retry-z.test.ts', 'test/fixtures/eng-binding-retry-z-calls.json', 'test/plan-count-permission-ac.test.ts', 'test/fixtures/plan-count-permission-ac.json', 'test/fixtures/plan-count-permission-ad.json', 'test/fixtures/plan-count-permission-ae.json', 'test/fixtures/plan-count-permission-target-ad-v2.json', 'test/helpers/eng-completion-handoff.ts', 'test/eng-count-ad-v2.test.ts', 'test/fixtures/eng-count-ad-v2.json', 'test/helpers/eng-seeded-coverage.ts', 'test/eng-seeded-coverage.test.ts', 'test/eng-seeded-packet-ae.test.ts', 'test/fixtures/eng-seeded-packet-ae.json', 'test/eng-first-category-af.test.ts', 'test/fixtures/eng-first-category-af.json', 'test/eng-regression-pinning-ag.test.ts', 'test/fixtures/eng-regression-pinning-ag.json', 'test/fixtures/plan-count-permission-ah.json', 'test/eng-next-handoff-ah.test.ts', 'test/fixtures/eng-next-handoff-ah.json', 'test/eng-declared-regression-ai.test.ts', 'test/fixtures/eng-declared-regression-ai.json', 'test/eng-snapshot-adapter-aj.test.ts', 'test/fixtures/eng-snapshot-adapter-aj.json',
|
||||
'test/eng-declared-suite-ak.test.ts',
|
||||
@@ -202,6 +219,8 @@ export const E2E_TOUCHFILES: Record<string, string[]> = {
|
||||
'test/fixtures/plan-count-crop-ak.json',
|
||||
'test/plan-count-quoted-frame-ak.test.ts',
|
||||
'test/fixtures/plan-count-quoted-frame-ak.json',
|
||||
"test/eng-golden-master-al.test.ts",
|
||||
"test/fixtures/eng-golden-master-al.json",
|
||||
],
|
||||
'plan-design-finding-count': ['bin/gstack-config', 'bin/gstack-skill-start', 'bin/gstack-skill-end', 'plan-design-review/**', 'scripts/resolvers/preamble.ts', 'scripts/resolvers/preamble/generate-ask-user-format.ts', 'scripts/resolvers/preamble/generate-completion-status.ts', 'test/helpers/claude-pty-runner.ts', 'test/plan-count-native-input.test.ts', 'test/helpers/pty-screen.ts', 'test/pty-screen.test.ts', 'test/pty-screen-session.test.ts', 'test/fixtures/pty-screen/**', 'test/helpers/hermetic-skill-runtime.ts', 'test/hermetic-skill-runtime.test.ts', 'test/helpers/claude-pty-runner.unit.test.ts', 'test/plan-count-completion.test.ts', 'test/plan-count-dx-handoff.test.ts', 'test/fixtures/devex-handoff-n-call.json', 'test/helpers/pty-trust-dialog.ts', 'test/pty-trust-dialog.test.ts', 'test/helpers/plan-count-transcript.ts', 'test/autoplan-public-narration.test.ts', 'test/fixtures/autoplan-public-narration-ad.json', 'test/helpers/plan-count-pending-exit.ts', 'test/plan-count-pending-exit.test.ts', 'test/plan-count-transcript.test.ts', 'test/helpers/plan-count-artifacts.ts', 'test/plan-count-artifacts.test.ts', 'test/helpers/eval-store.ts', 'test/helpers/plan-count-fixture.ts', 'test/plan-count-fixture.test.ts', 'test/plan-count-timeout.test.ts', 'test/plan-count-navigation-r.test.ts', 'test/plan-count-prerequisite-n.test.ts', 'test/fixtures/ceo-prerequisite-n-call.json', 'test/helpers/design-count-review.ts', 'test/design-count-review.test.ts', 'test/fixtures/design-review-j-calls.json', 'test/helpers/design-count-outside.ts', 'test/design-count-outside.test.ts', 'test/skill-e2e-plan-design-finding-count.test.ts', 'test/fixtures/design-review-l-calls.json', 'test/design-completion-handoff.test.ts', 'test/fixtures/design-handoff-l-calls.json', 'test/fixtures/devex-review-l-calls.json', 'test/plan-count-checkbox.test.ts', 'test/fixtures/ceo-checkbox-l.screen.txt', 'test/fixtures/design-review-n-calls.json', 'test/design-completion-handoff-scored.test.ts', 'test/fixtures/design-handoff-n-calls.json', 'test/fixtures/design-handoff-q-calls.json', 'test/plan-count-empty-review.test.ts', 'test/fixtures/ceo-count-s-distinct.json', 'test/fixtures/plan-count-design-questionless-report.md', 'test/plan-count-dx-handoff-o.test.ts', 'test/fixtures/devex-handoff-o-call.json', 'test/helpers/plan-count-file-permission.ts', 'test/plan-count-file-permission.test.ts', 'test/fixtures/plan-count-edit-permission-t.json', 'test/plan-count-preview-footer.test.ts', 'test/fixtures/ceo-preview-u-call.json', 'test/fixtures/ceo-preview-u-screen.txt', 'test/design-completion-handoff-u.test.ts', 'test/fixtures/design-handoff-u-calls.json', 'test/fixtures/design-preview-v-screen.txt', 'test/plan-count-owned-permission.test.ts', 'test/fixtures/plan-count-owned-permission-v.json', 'test/fixtures/ceo-questionless-w-native.json', 'test/helpers/design-artifact-question.ts', 'test/design-artifact-question.test.ts', 'test/fixtures/design-artifacts-w-calls.json', 'test/fixtures/design-outside-y-calls.json', 'test/fixtures/design-boundaries-y-calls.json', 'test/plan-count-truncated-question.test.ts', 'test/fixtures/ceo-approach-z-call.json', 'test/fixtures/ceo-approach-z-screen.txt', 'test/fixtures/design-gap-z-calls.json', 'test/plan-count-permission-ac.test.ts', 'test/fixtures/plan-count-permission-ac.json', 'test/fixtures/plan-count-permission-ad.json', 'test/fixtures/plan-count-permission-ae.json', 'test/fixtures/plan-count-permission-target-ad-v2.json', 'test/design-count-ad-v2.test.ts', 'test/fixtures/design-count-ad-v2.json', 'test/design-first-decision-af.test.ts', 'test/fixtures/design-first-decision-af.json', 'test/fixtures/design-first-decision-af-retry.json', 'test/fixtures/plan-count-permission-ah.json', 'test/design-first-issue-ai.test.ts', 'test/fixtures/design-first-issue-ai.json', 'test/design-primary-action-aj.test.ts', 'test/fixtures/design-primary-action-aj.json', 'test/fixtures/design-future-todo-aj.json',
|
||||
'test/design-primary-contract-ak.test.ts',
|
||||
@@ -210,6 +229,8 @@ export const E2E_TOUCHFILES: Record<string, string[]> = {
|
||||
'test/fixtures/plan-count-crop-ak.json',
|
||||
'test/plan-count-quoted-frame-ak.test.ts',
|
||||
'test/fixtures/plan-count-quoted-frame-ak.json',
|
||||
"test/design-primary-decision-al.test.ts",
|
||||
"test/fixtures/design-primary-decision-al.json",
|
||||
],
|
||||
'plan-devex-finding-count': ['bin/gstack-config', 'bin/gstack-skill-start', 'bin/gstack-skill-end', 'plan-devex-review/**', 'scripts/resolvers/preamble.ts', 'scripts/resolvers/preamble/generate-ask-user-format.ts', 'scripts/resolvers/preamble/generate-completion-status.ts', 'test/helpers/claude-pty-runner.ts', 'test/plan-count-native-input.test.ts', 'test/helpers/pty-screen.ts', 'test/pty-screen.test.ts', 'test/pty-screen-session.test.ts', 'test/fixtures/pty-screen/**', 'test/helpers/hermetic-skill-runtime.ts', 'test/hermetic-skill-runtime.test.ts', 'test/helpers/claude-pty-runner.unit.test.ts', 'test/plan-count-completion.test.ts', 'test/plan-count-dx-handoff.test.ts', 'test/fixtures/devex-handoff-n-call.json', 'test/helpers/pty-trust-dialog.ts', 'test/pty-trust-dialog.test.ts', 'test/helpers/plan-count-transcript.ts', 'test/autoplan-public-narration.test.ts', 'test/fixtures/autoplan-public-narration-ad.json', 'test/helpers/plan-count-pending-exit.ts', 'test/plan-count-pending-exit.test.ts', 'test/plan-count-transcript.test.ts', 'test/helpers/plan-count-artifacts.ts', 'test/plan-count-artifacts.test.ts', 'test/helpers/eval-store.ts', 'test/helpers/plan-count-fixture.ts', 'test/plan-count-fixture.test.ts', 'test/plan-count-timeout.test.ts', 'test/plan-count-navigation-r.test.ts', 'test/plan-count-prerequisite-n.test.ts', 'test/fixtures/ceo-prerequisite-n-call.json', 'test/helpers/devex-count-fixture.ts', 'test/devex-count-fixture.test.ts', 'test/skill-e2e-plan-devex-finding-count.test.ts', 'test/fixtures/dx-prerequisite-r-call.json', 'test/fixtures/devex-review-l-calls.json', 'test/plan-count-checkbox.test.ts', 'test/fixtures/ceo-checkbox-l.screen.txt', 'test/fixtures/devex-review-n-calls.json', 'test/plan-count-empty-review.test.ts', 'test/fixtures/ceo-count-s-distinct.json', 'test/fixtures/plan-count-design-questionless-report.md', 'test/devex-output-o.test.ts', 'test/fixtures/devex-review-o-calls.json', 'test/fixtures/devex-output-o-retry-call.json', 'test/devex-setup-remedy-o.test.ts', 'test/fixtures/devex-review-o-retry-calls.json', 'test/plan-count-dx-handoff-o.test.ts', 'test/fixtures/devex-handoff-o-call.json', 'test/eng-devex-s-count.test.ts', 'test/fixtures/eng-devex-s-first-calls.json', 'test/fixtures/eng-devex-s-retry-calls.json', 'test/fixtures/devex-review-t-calls.json', 'test/helpers/plan-count-file-permission.ts', 'test/plan-count-file-permission.test.ts', 'test/fixtures/plan-count-edit-permission-t.json', 'test/plan-count-preview-footer.test.ts', 'test/fixtures/ceo-preview-u-call.json', 'test/fixtures/ceo-preview-u-screen.txt', 'test/fixtures/devex-count-u-calls.json', 'test/fixtures/devex-count-u-retry-calls.json', 'test/fixtures/devex-empathy-v-calls.json', 'test/fixtures/devex-handoff-v-call.json', 'test/fixtures/design-preview-v-screen.txt', 'test/plan-count-owned-permission.test.ts', 'test/fixtures/plan-count-owned-permission-v.json', 'test/fixtures/ceo-questionless-w-native.json', 'test/fixtures/devex-count-y-calls.json', 'test/plan-count-truncated-question.test.ts', 'test/fixtures/ceo-approach-z-call.json', 'test/fixtures/ceo-approach-z-screen.txt', 'test/fixtures/devex-count-z-calls.json', 'test/fixtures/devex-handoff-z-call.json', 'test/review-handoffs-aa.test.ts', 'test/fixtures/review-handoff-aa-ceo.json', 'test/fixtures/review-handoff-aa-dx.json', 'test/devex-empathy-ab.test.ts', 'test/fixtures/devex-empathy-ab-calls.json', 'test/devex-ac-accounting.test.ts', 'test/fixtures/devex-ac-first-attempt-calls.json', 'test/plan-count-permission-ac.test.ts', 'test/fixtures/plan-count-permission-ac.json', 'test/fixtures/plan-count-permission-ad.json', 'test/fixtures/plan-count-permission-ae.json', 'test/fixtures/plan-count-permission-target-ad-v2.json', 'test/devex-reconfirmation-ad-v2.test.ts', 'test/fixtures/devex-reconfirmation-ad-v2.json', 'test/plan-count-history.test.ts', 'test/helpers/devex-seed-coverage.ts', 'test/devex-seed-coverage.test.ts', 'test/fixtures/devex-seed-coverage-ad-v3.json', 'test/fixtures/plan-count-permission-ah.json',
|
||||
'test/dx-signature-identity-ak.test.ts',
|
||||
|
||||
Reference in new issue
Block a user