mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-08 01:38:10 +01:00
test: recognize current review evidence across native forms
This commit is contained in:
1 parent
a6eaafacb8
commit
04c62ac678
21 files changed
+2053
-49
No files matched your search
@@ -0,0 +1,100 @@
|
||||
import { afterEach, expect, test } from 'bun:test';
|
||||
import * as fs from 'node:fs';
|
||||
import * as os from 'node:os';
|
||||
import * as path from 'node:path';
|
||||
import captured from './fixtures/autoplan-edit-panel-aj.json';
|
||||
import published from './fixtures/autoplan-edit-prefix-ai.json';
|
||||
import { autoplanArtifactPermissionInput, pendingAutoplanArtifactPermissionInput, autoplanArtifactMenuKey } from './helpers/autoplan-artifact-permission';
|
||||
import { E2E_TOUCHFILES, selectTests } from './helpers/touchfiles';
|
||||
import type { NativePublicToolEvent } from './helpers/plan-count-transcript';
|
||||
const roots: string[] = [];
|
||||
afterEach(() => { for (const root of roots.splice(0)) fs.rmSync(root, { recursive: true, force: true }); });
|
||||
function replay() {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'autoplan-edit-panel-')); roots.push(root);
|
||||
const cwd = path.join(root, path.basename(captured.cwd)), ownedStateRoot = path.join(root, 'home', '.gstack');
|
||||
const file = captured.pending.file.replace(captured.ownedStateRoot, ownedStateRoot);
|
||||
fs.mkdirSync(cwd, { recursive: true }); fs.mkdirSync(path.dirname(file), { recursive: true }); fs.writeFileSync(file, captured.before);
|
||||
const time = new Date(Date.parse(captured.pending.timestamp) - 1000); fs.utimesSync(file, time, time);
|
||||
const events = structuredClone(captured.events) as NativePublicToolEvent[];
|
||||
for (const event of events) if (event.input?.file_path === captured.pending.file) event.input.file_path = file;
|
||||
const context = { cwd, ownedStateRoot, commandStartedAt: Date.parse(events[0]!.timestamp) - 1,
|
||||
now: captured.viewportCapturedAt, viewportCapturedAt: captured.viewportCapturedAt,
|
||||
pending: { ...captured.pending, source: 'pre_tool_use' as const, tool: 'Edit' as const, file }, transcriptStatus: 'ready', publicTools: events };
|
||||
const viewport = captured.viewport.replace(/^ …[^\n]+$/m, ' …' + path.relative(ownedStateRoot, file));
|
||||
return { root, file, context, viewport };
|
||||
}
|
||||
const pick = (r: ReturnType<typeof replay>, seen = new Set<string>()) => pendingAutoplanArtifactPermissionInput(r.viewport, r.context, seen);
|
||||
|
||||
test('the exact standalone native Edit panel binds the owned current unpublished request', () => {
|
||||
const r = replay();
|
||||
expect(pick(r)).toEqual({ input: '1\r', signature: r.context.pending.sessionId + ':' + r.context.pending.toolUseId, file: r.file });
|
||||
expect(autoplanArtifactPermissionInput(r.viewport, r.context, new Set())).toBeNull();
|
||||
});
|
||||
|
||||
test('complete absolute, home alias and full relative suffix paths retain ownership', () => {
|
||||
for (const displayed of ['absolute', 'alias', 'suffix'] as const) {
|
||||
const r = replay(), relative = path.relative(r.context.ownedStateRoot, r.file);
|
||||
const value = displayed === 'absolute' ? r.file : displayed === 'alias' ? '~/.gstack/' + relative : '…' + relative;
|
||||
r.viewport = r.viewport.replace(/^ …[^\n]+$/m, ' ' + value); expect(pick(r)?.input).toBe('1\r');
|
||||
}
|
||||
const crop = replay(); crop.viewport = crop.viewport.split('\n').slice(4).join('\n'); expect(pick(crop)?.input).toBe('1\r');
|
||||
});
|
||||
|
||||
test('missing, foreign, quoted and ambiguous headers do not authorize the current file', () => {
|
||||
for (const change of [
|
||||
(s: string) => s.replace(/^ …[^\n]+$/m, ' /tmp/foreign.md'),
|
||||
(s: string) => s.replace(/^ …[^\n]+$/m, ' …' + path.basename(captured.pending.file)),
|
||||
(s: string) => s.replace(/^ …[^\n]+$/m, ' …projects/sibling/ceo-plans/' + path.basename(captured.pending.file)),
|
||||
(s: string) => s.replace(' Edit file\n', ''),
|
||||
(s: string) => s.replace(' Edit file', ' Read file'),
|
||||
(s: string) => s.split('\n').slice(1).join('\n'),
|
||||
(s: string) => s.replace(/^─+\n/, '--------\n'),
|
||||
(s: string) => '> quoted panel\n' + s,
|
||||
(s: string) => '```text\n' + s + '\n```',
|
||||
(s: string) => s.split('\n').slice(0, 4).join('\n') + '\n' + s,
|
||||
(s: string) => '● Update(/tmp/foreign.md)\n\n' + s,
|
||||
(s: string) => s + '\n' + s,
|
||||
]) { const r = replay(); r.viewport = change(r.viewport); expect(pick(r)).toBeNull(); }
|
||||
});
|
||||
|
||||
test('current hook, observed time, same-file history and one-time menu remain required', () => {
|
||||
const once = replay(), granted = pick(once)!;
|
||||
expect(pick(once, new Set([granted.signature]))).toBeNull();
|
||||
expect(pick(once, new Set([autoplanArtifactMenuKey(once.viewport)]))).toBeNull();
|
||||
for (const change of [
|
||||
(r: ReturnType<typeof replay>) => { r.context.pending.sessionId = 'foreign'; },
|
||||
(r: ReturnType<typeof replay>) => { r.context.pending.file = r.file + '.foreign'; },
|
||||
(r: ReturnType<typeof replay>) => { r.context.viewportCapturedAt = Date.parse(r.context.pending.timestamp) - 1; },
|
||||
(r: ReturnType<typeof replay>) => { r.context.publicTools[1]!.isError = true; },
|
||||
(r: ReturnType<typeof replay>) => { 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: ReturnType<typeof replay>) => { r.context.publicTools.push({ kind: 'use', sessionId: r.context.pending.sessionId, toolUseId: 'newer', timestamp: new Date(r.context.now).toISOString(), name: 'Write', input: { file_path: r.file } }); },
|
||||
(r: ReturnType<typeof replay>) => { fs.writeFileSync(r.file, 'Foreign content'); },
|
||||
(r: ReturnType<typeof replay>) => { fs.renameSync(r.file, r.file + '.target'); fs.symlinkSync(r.file + '.target', r.file); },
|
||||
(r: ReturnType<typeof replay>) => { r.viewport = r.viewport.replace('❯ 1. Yes', '❯ 2. Yes'); },
|
||||
(r: ReturnType<typeof replay>) => { r.viewport = r.viewport.replace('3. No', '3. Maybe'); },
|
||||
(r: ReturnType<typeof replay>) => { r.viewport = r.viewport.replace(' 10 ', ' 0 '); },
|
||||
]) { const r = replay(); change(r); expect(pick(r)).toBeNull(); }
|
||||
});
|
||||
|
||||
test('published edits retain exact old/new content guards with the standalone presentation', () => {
|
||||
const r = replay(), events = structuredClone(published.events) as NativePublicToolEvent[];
|
||||
const edit = events.find(e => e.kind === 'use' && e.toolUseId === published.pending.toolUseId)!;
|
||||
const oldFile = edit.input!.file_path;
|
||||
const file = (oldFile as string).replace(published.ownedStateRoot, r.context.ownedStateRoot);
|
||||
const cwd = path.join(r.root, path.basename(published.cwd)); fs.mkdirSync(cwd, { recursive: true });
|
||||
fs.mkdirSync(path.dirname(file), { recursive: true }); fs.writeFileSync(file, published.before);
|
||||
for (const event of events) if (event.input?.file_path === oldFile) event.input.file_path = file;
|
||||
const header = published.viewport.lastIndexOf('\n● Update(') + 1;
|
||||
const viewport = published.viewport.slice(header).split('\n').slice(2).join('\n').replace(/^ …[^\n]+$/m, ' …' + path.relative(r.context.ownedStateRoot, file));
|
||||
const context = { cwd, ownedStateRoot: r.context.ownedStateRoot, commandStartedAt: Date.parse(events[0]!.timestamp) - 1, now: Date.parse(published.viewportCapturedAt), transcriptStatus: 'ready', publicTools: events };
|
||||
expect(autoplanArtifactPermissionInput(viewport, context, new Set())?.input).toBe('1\r');
|
||||
const original = edit.input!.new_string; edit.input!.new_string = 'Unrelated replacement';
|
||||
expect(autoplanArtifactPermissionInput(viewport, context, new Set())).toBeNull();
|
||||
edit.input!.new_string = original; edit.input!.old_string = 'Unrelated original';
|
||||
expect(autoplanArtifactPermissionInput(viewport, context, new Set())).toBeNull();
|
||||
});
|
||||
|
||||
test('only Autoplan owns the standalone panel regression inputs', () => {
|
||||
for (const file of ['test/autoplan-edit-panel-aj.test.ts', 'test/fixtures/autoplan-edit-panel-aj.json'])
|
||||
expect(selectTests([file], E2E_TOUCHFILES, []).selected).toEqual(['autoplan-chain-pty']);
|
||||
});
|
||||
@@ -0,0 +1,218 @@
|
||||
import { expect, test } from 'bun:test';
|
||||
import { ceoFirstReviewAUQ, ceoStep0Boundary, nativePlanCallFingerprint, planCountQuestionPhase } from './helpers/claude-pty-runner';
|
||||
import captured from './fixtures/ceo-annotation-aj.json';
|
||||
import { E2E_TOUCHFILES } from './helpers/touchfiles-data';
|
||||
|
||||
const call = (index = 3): any => structuredClone(captured.cases.paired.calls[index]);
|
||||
const fp = (c: any) => nativePlanCallFingerprint(c, 0, true);
|
||||
function edit(c: any, change: (s: string) => string) {
|
||||
const q = c.questions[0], answer = c.answers[q.question];
|
||||
q.question = change(q.question); c.answers = { [q.question]: answer };
|
||||
}
|
||||
|
||||
test('completed native receipt and retry findings retain identity through section annotations', () => {
|
||||
for (const index of [3, 4]) expect(ceoFirstReviewAUQ(fp(call(index)))).toBe(true);
|
||||
});
|
||||
|
||||
test('actual setup remains excluded before the two completed assertion findings', () => {
|
||||
let started = false;
|
||||
const phases = captured.cases.paired.calls.map(c => {
|
||||
const phase = planCountQuestionPhase(fp(c), started, ceoStep0Boundary, ceoFirstReviewAUQ);
|
||||
started = phase.reviewStarted;
|
||||
return phase.preReview;
|
||||
});
|
||||
expect(phases).toEqual([true, true, true, false, false]);
|
||||
const approach = structuredClone(captured.cases.distinct.calls[2]);
|
||||
expect(ceoFirstReviewAUQ(fp(approach))).toBe(false);
|
||||
});
|
||||
|
||||
test('the new captured inputs belong only to the existing CEO count owner', () => {
|
||||
for (const dependency of ['test/ceo-annotation-aj.test.ts', 'test/fixtures/ceo-annotation-aj.json']) {
|
||||
expect(Object.entries(E2E_TOUCHFILES).filter(([, paths]) => paths.includes(dependency)).map(([name]) => name))
|
||||
.toEqual(['plan-ceo-finding-count']);
|
||||
}
|
||||
});
|
||||
|
||||
test('section references do not replace finding or native option identity', () => {
|
||||
for (const index of [3, 4]) {
|
||||
const c = call(index);
|
||||
edit(c, s => s.replace(/\(Sections? [^)]+\)/, '(Sections 3, 5 and 8, Error Handling)'));
|
||||
expect(ceoFirstReviewAUQ(fp(c))).toBe(true);
|
||||
c.answers[c.questions[0].question] = c.questions[0].options[1].label;
|
||||
expect(ceoFirstReviewAUQ(fp(c))).toBe(true);
|
||||
const renamed = call(index), q = renamed.questions[0], old = String(index - 2);
|
||||
edit(renamed, s => s.replace(new RegExp('Finding F' + old), 'Finding F9')
|
||||
.replace(new RegExp('^Recommendation: ' + old, 'm'), 'Recommendation: 9')
|
||||
.replace(new RegExp('^' + old + '([A-Z][)])', 'gm'), '9$1'));
|
||||
q.header = q.header.replace(/^F\d+/, 'F9');
|
||||
q.options.forEach((o: any) => { o.label = o.label.replace(/^\d+/, '9'); });
|
||||
renamed.answers = { [q.question]: q.options[0].label };
|
||||
expect(ceoFirstReviewAUQ(fp(renamed))).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
test('source frames and conditional or missing assessments cannot supply a current finding', () => {
|
||||
for (const index of [3, 4]) for (const change of [
|
||||
(s: string) => 'Example: ' + s,
|
||||
(s: string) => '> ' + s,
|
||||
(s: string) => '```\n' + s + '\n```',
|
||||
(s: string) => s.replace(/^ELI10: (.+)$/m, 'ELI10: `$1`'),
|
||||
(s: string) => s.replace(/^ELI10: (.+)$/m, 'ELI10: "$1"'),
|
||||
(s: string) => s.replace(/^ELI10: /m, 'ELI10: If '),
|
||||
(s: string) => s.replace(/^ELI10: /m, 'ELI10: Suppose '),
|
||||
(s: string) => s.replace(/^ELI10: .+$/m, ''),
|
||||
(s: string) => s + '\nELI10: A second contradictory assessment.',
|
||||
(s: string) => s.replace(/: the (success|repeated)/, ': the hypothetical $1'),
|
||||
(s: string) => s.replace(/\(Sections? [^)]+\)/, '(Section 6, Quoted Source)'),
|
||||
(s: string) => s.replace(/\(Sections? [^)]+\)/, '(Section 6, Historical Example)'),
|
||||
]) { const c = call(index); edit(c, change); expect(ceoFirstReviewAUQ(fp(c))).toBe(false); }
|
||||
});
|
||||
|
||||
test('same-brief withdrawals defeat a finding while attributed historical quotes do not', () => {
|
||||
for (const index of [3, 4]) {
|
||||
for (const tail of ['This issue is withdrawn.', 'We have withdrawn this finding.',
|
||||
'There is no current defect or unresolved issue.', `F${index - 2} is rejected.`]) {
|
||||
const c = call(index); edit(c, s => s + '\n' + tail); expect(ceoFirstReviewAUQ(fp(c))).toBe(false);
|
||||
}
|
||||
const quoted = call(index); edit(quoted, s => s + '\nOld note: "This issue is withdrawn."');
|
||||
expect(ceoFirstReviewAUQ(fp(quoted))).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
test('administrative options and stale action rows do not amend the current contract', () => {
|
||||
for (const index of [3, 4]) {
|
||||
for (const wording of ['Start review', 'Pause', 'Write the completed report']) {
|
||||
const stale = call(index), native = stale.questions[0];
|
||||
native.options.forEach((o: any, i: number) => {
|
||||
o.label = `${index - 2}${String.fromCharCode(65 + i)}: ${wording}`;
|
||||
o.description = wording;
|
||||
});
|
||||
stale.answers = { [native.question]: native.options[0].label };
|
||||
expect(ceoFirstReviewAUQ(fp(stale))).toBe(false);
|
||||
}
|
||||
const c = call(index), q = c.questions[0];
|
||||
q.options.forEach((o: any, i: number) => {
|
||||
o.label = `${index - 2}${String.fromCharCode(65 + i)}: Archive the completed report ${i}`;
|
||||
o.description = 'Save the completed review for reference.';
|
||||
});
|
||||
c.answers = { [q.question]: q.options[0].label };
|
||||
expect(ceoFirstReviewAUQ(fp(c))).toBe(false);
|
||||
const noGap = call(index);
|
||||
edit(noGap, s => s.replace(/^D\d+[^\n]+/, `D4 — Finding F${index - 2} (Sections 2 and 6): where should the completed report be stored?`)
|
||||
.replace(/^ELI10: .+$/m, 'ELI10: The review is complete and all assertions already enforce the full contract.'));
|
||||
expect(ceoFirstReviewAUQ(fp(noGap))).toBe(false);
|
||||
const negated = call(index);
|
||||
edit(negated, s => s.replace(/asserts only/, 'does not assert only')
|
||||
.replace(/^ELI10: .+$/m, 'ELI10: The assertions enforce the complete receipt and retry contracts.'));
|
||||
expect(ceoFirstReviewAUQ(fp(negated))).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
test('completion, recommendation, identity and actual offered options remain required', () => {
|
||||
for (const index of [3, 4]) 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] = 'Foreign answer'; },
|
||||
(c: any) => { c.questions[0].multiSelect = true; },
|
||||
(c: any) => { c.questions.push(structuredClone(c.questions[0])); },
|
||||
(c: any) => { c.questions[0].header = 'Approach'; },
|
||||
(c: any) => { c.questions[0].header = 'Finding 99'; },
|
||||
(c: any) => { c.questions[0].options[1].description = ''; },
|
||||
(c: any) => { c.questions[0].options[1].label = '99B: Different finding'; },
|
||||
(c: any) => edit(c, s => s.replace(/^Recommendation: .+$/m, 'Recommendation: 99Z')),
|
||||
(c: any) => edit(c, s => s.replace(/^Recommendation: .+$/m, '')),
|
||||
(c: any) => edit(c, s => s + '\n<gstack-qid:plan-eng-review-finding>'),
|
||||
]) { const c = call(index); mutate(c); expect(ceoFirstReviewAUQ(fp(c))).toBe(false); }
|
||||
for (const index of [3, 4]) {
|
||||
const original = fp(call(index));
|
||||
expect(ceoFirstReviewAUQ({ ...original, signature: 'foreign:call' })).toBe(false);
|
||||
expect(ceoFirstReviewAUQ({ ...original, nativeCall: undefined })).toBe(false);
|
||||
expect(ceoFirstReviewAUQ({ ...original, options: original.options.slice(1) })).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
test('completed dotted issue briefs retain their full identity and section option binding', () => {
|
||||
for (const source of captured.cases.distinct.calls.slice(4)) {
|
||||
expect(ceoFirstReviewAUQ(fp(source))).toBe(true);
|
||||
}
|
||||
let started = false;
|
||||
const phases = captured.cases.distinct.calls.map(c => {
|
||||
const phase = planCountQuestionPhase(fp(c), started, ceoStep0Boundary, ceoFirstReviewAUQ);
|
||||
started = phase.reviewStarted;
|
||||
return phase.preReview;
|
||||
});
|
||||
expect(phases).toEqual([true, true, true, true, false, false, false, false, false]);
|
||||
});
|
||||
|
||||
test('dotted issue syntax never supplies missing current defect or remedy evidence', () => {
|
||||
for (const source of captured.cases.distinct.calls.slice(4)) {
|
||||
const identity = /\(Issue ([\d.]+)\)/.exec(source.questions[0]!.question)![1]!;
|
||||
for (const change of [
|
||||
(s: string) => 'Example: ' + s,
|
||||
(s: string) => s.replace(/^ELI10: /m, 'ELI10: If '),
|
||||
(s: string) => s.replace(/^ELI10: /m, 'ELI10: Historical example: '),
|
||||
(s: string) => s.replace(/^ELI10: (.+)$/m, 'ELI10: `$1`'),
|
||||
(s: string) => s.replace(/^ELI10: .+$/m, ''),
|
||||
(s: string) => s.replace(/^ELI10: .+$/m, 'ELI10: The completed review has no current defect or unresolved issue.'),
|
||||
(s: string) => s.replace(/^ELI10: .+$/m, 'ELI10: The existing behavior satisfies every contract and needs no change.'),
|
||||
(s: string) => s + '\nThis issue has been resolved.',
|
||||
(s: string) => s + `\nIssue ${identity} is rejected.`,
|
||||
(s: string) => s.replace(/^Recommendation: \d+[A-Z]/m, 'Recommendation: 99Z'),
|
||||
(s: string) => s + '\n<gstack-qid:plan-eng-review-finding>',
|
||||
]) { const c = structuredClone(source); edit(c, change); expect(ceoFirstReviewAUQ(fp(c))).toBe(false); }
|
||||
const sourceOnly = structuredClone(source), q = sourceOnly.questions[0]!;
|
||||
q.options.forEach((o, i) => { o.label = `${identity.split('.')[0]}${String.fromCharCode(65 + i)}: Archive the completed report ${i}`; o.description = 'Save the completed review.'; });
|
||||
sourceOnly.answers = { [q.question]: q.options[0]!.label };
|
||||
expect(ceoFirstReviewAUQ(fp(sourceOnly))).toBe(false);
|
||||
const quoted = structuredClone(source); edit(quoted, s => s + `\nOld note: "Issue ${identity} is rejected."`);
|
||||
expect(ceoFirstReviewAUQ(fp(quoted))).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
test('dotted identities remain complete while the option prefix names the containing section', () => {
|
||||
for (const source of captured.cases.distinct.calls.slice(4)) {
|
||||
const identity = /\(Issue ([\d.]+)\)/.exec(source.questions[0]!.question)![1]!;
|
||||
for (const mutate of [
|
||||
(c: any) => { c.answered = false; },
|
||||
(c: any) => { c.failed = true; },
|
||||
(c: any) => { c.unansweredQuestionIndices = [0]; },
|
||||
(c: any) => { c.answers = {}; },
|
||||
(c: any) => { c.answers[c.questions[0].question] = 'Unrelated answer'; },
|
||||
(c: any) => { c.questions[0].header = 'Approach'; },
|
||||
(c: any) => { c.questions[0].header = `Finding ${identity.split('.')[0]}`; },
|
||||
(c: any) => { c.questions[0].header = 'Issue 99.1'; },
|
||||
(c: any) => { c.questions[0].options[1].label = '99B: Borrowed option'; },
|
||||
(c: any) => { c.questions[0].options[1].description = ''; },
|
||||
(c: any) => { c.questions[0].multiSelect = true; },
|
||||
(c: any) => { c.questions.push(structuredClone(c.questions[0])); },
|
||||
(c: any) => edit(c, s => s.replace(`Issue ${identity}`, 'Issue 1.0')),
|
||||
]) { const c = structuredClone(source); mutate(c); expect(ceoFirstReviewAUQ(fp(c))).toBe(false); }
|
||||
const header = structuredClone(source); header.questions[0]!.header = `Finding ${identity}`;
|
||||
expect(ceoFirstReviewAUQ(fp(header))).toBe(true);
|
||||
const localQid = structuredClone(source); edit(localQid, s => s + '\n<gstack-qid:plan-ceo-review-handler-correctness>');
|
||||
expect(ceoFirstReviewAUQ(fp(localQid))).toBe(true);
|
||||
expect(ceoFirstReviewAUQ({ ...fp(source), signature: 'foreign:call' })).toBe(false);
|
||||
expect(ceoFirstReviewAUQ({ ...fp(source), nativeCall: undefined })).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
test('an owning assessment declaration cannot relabel source or hypothetical prose as a current finding', () => {
|
||||
for (const source of [...captured.cases.paired.calls.slice(3), ...captured.cases.distinct.calls.slice(4)]) {
|
||||
for (const frame of [
|
||||
'The following is a quoted source excerpt.',
|
||||
'The following is a hypothetical example.',
|
||||
'This assessment is only a historical example.',
|
||||
]) {
|
||||
const c = structuredClone(source);
|
||||
edit(c, s => s.replace(/^ELI10: /m, 'ELI10: ' + frame + ' '));
|
||||
expect(ceoFirstReviewAUQ(fp(c))).toBe(false);
|
||||
}
|
||||
const quoted = structuredClone(source);
|
||||
edit(quoted, s => s.replace(/^(ELI10: .+)$/m, '$1 Old note: "The following is a hypothetical example."'));
|
||||
expect(ceoFirstReviewAUQ(fp(quoted))).toBe(true);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,231 @@
|
||||
import { expect, test } from 'bun:test';
|
||||
import captured from './fixtures/design-primary-action-aj.json';
|
||||
import { isDesignCountFirstReview, isDesignCountSetup } from './helpers/design-count-review';
|
||||
import { designStep0Boundary, planCountQuestionPhase } from './helpers/claude-pty-runner';
|
||||
|
||||
test('the exact completed current primary-action choice starts Design review', () => {
|
||||
expect(isDesignCountFirstReview(captured)).toBe(true);
|
||||
expect(planCountQuestionPhase(captured, false, designStep0Boundary, isDesignCountFirstReview, isDesignCountSetup))
|
||||
.toMatchObject({ preReview: false, reviewStarted: true });
|
||||
});
|
||||
|
||||
function fresh(): any { return structuredClone(captured); }
|
||||
function changeText(fp: any, change: (text: string) => string) {
|
||||
const q = fp.nativeCall.questions[0], answer = fp.nativeCall.answers[q.question];
|
||||
q.question = change(q.question); fp.nativeCall.answers = { [q.question]: answer };
|
||||
}
|
||||
function changeMenu(fp: any, change: (q: any) => void) {
|
||||
const q = fp.nativeCall.questions[0]; change(q);
|
||||
fp.options = q.options.map((o: any, i: number) => ({ index: i + 1, label: o.label }));
|
||||
fp.nativeCall.answers = { [q.question]: q.options[0].label };
|
||||
}
|
||||
|
||||
test('an offered deferral, reordered menu and consistently renamed control remain review decisions', () => {
|
||||
for (const option of captured.nativeCall.questions[0]!.options) {
|
||||
const fp = fresh(); fp.nativeCall.answers = { [fp.nativeCall.questions[0].question]: option.label };
|
||||
expect(isDesignCountFirstReview(fp)).toBe(true);
|
||||
}
|
||||
const reordered = fresh(); changeMenu(reordered, q => q.options.reverse());
|
||||
expect(isDesignCountFirstReview(reordered)).toBe(true);
|
||||
const renamed = fresh(); changeText(renamed, s => s.replaceAll('Save', 'Submit'));
|
||||
changeMenu(renamed, q => q.options.forEach((o: any) => {
|
||||
o.label = o.label.replaceAll('Save', 'Submit'); o.description = o.description.replaceAll('Save', 'Submit');
|
||||
}));
|
||||
expect(isDesignCountFirstReview(renamed)).toBe(true);
|
||||
const numbered = fresh(); changeText(numbered, s => s.replaceAll('Issue 1', 'Issue 6').replaceAll('1A', '6A').replaceAll('1B', '6B').replaceAll('1C', '6C'));
|
||||
changeMenu(numbered, q => { q.header = 'Issue 6'; q.options.forEach((o: any) => { o.label = o.label.replace(/^1/, '6'); }); });
|
||||
expect(isDesignCountFirstReview(numbered)).toBe(true);
|
||||
});
|
||||
|
||||
test('native completion, owned identity, offered answers and aligned numbering are necessary', () => {
|
||||
for (const change of [
|
||||
(f: any) => { delete f.nativeCall; },
|
||||
(f: any) => { f.nativeCall.answered = false; },
|
||||
(f: any) => { f.nativeCall.failed = true; },
|
||||
(f: any) => { f.nativeCall.sessionId = 'foreign-session'; },
|
||||
(f: any) => { f.nativeCall.toolUseId = 'foreign-request'; },
|
||||
(f: any) => { f.nativeCall.answeredAt = 'invalid'; },
|
||||
(f: any) => { delete f.nativeCall.answeredAt; },
|
||||
(f: any) => { f.nativeCall.unansweredQuestionIndices = [0]; },
|
||||
(f: any) => { f.nativeQuestionIndex = 1; },
|
||||
(f: any) => { f.nativeCall.answers = {}; },
|
||||
(f: any) => { f.nativeCall.answers[f.nativeCall.questions[0].question] = 'unoffered'; },
|
||||
(f: any) => { f.nativeCall.questions[0].question += ' altered'; },
|
||||
(f: any) => { f.nativeCall.questions[0].header = 'Issue 2'; },
|
||||
(f: any) => { f.nativeCall.questions[0].multiSelect = true; },
|
||||
(f: any) => { f.options.reverse(); },
|
||||
(f: any) => { changeMenu(f, q => { q.options[0].label = q.options[0].label.replace('1A', '2A'); }); },
|
||||
]) { const fp = fresh(); change(fp); expect(isDesignCountFirstReview(fp)).toBe(false); }
|
||||
});
|
||||
|
||||
test('quoted, hypothetical, future and explicitly withdrawn assessments cannot borrow style choices', () => {
|
||||
for (const change of [
|
||||
(s: string) => 'Example: ' + s,
|
||||
(s: string) => '```text\n' + s + '\n```',
|
||||
(s: string) => s.replace('ELI10: Right now', 'ELI10: Suppose right now'),
|
||||
(s: string) => s.replace('ELI10: Right now', 'ELI10: If approved, right now'),
|
||||
(s: string) => s.replace(/^ELI10: (.+)$/m, 'ELI10: "$1"'),
|
||||
(s: string) => s.replace(/^ELI10: (.+)$/m, "ELI10: '$1'"),
|
||||
(s: string) => s.replace(/^ELI10: (.+)$/m, 'ELI10: $1 This issue is withdrawn.'),
|
||||
(s: string) => s.replace(/^ELI10: (.+)$/m, 'ELI10: $1 We have resolved this finding.'),
|
||||
(s: string) => s.replace(/^ELI10: (.+)$/m, 'ELI10: $1 No current gap remains.'),
|
||||
(s: string) => s + '\nCorrection: this issue is withdrawn.',
|
||||
(s: string) => s.replace('make Save the only filled primary action?', 'make Save the only filled primary action in a future redesign?'),
|
||||
(s: string) => s.replace('make Save the only filled primary action?', 'make the primary reviewer the next step?'),
|
||||
(s: string) => s.replace('ELI10: Right now Save,', 'ELI10: Right now Publish,'),
|
||||
]) { const fp = fresh(); changeText(fp, change); expect(isDesignCountFirstReview(fp)).toBe(false); }
|
||||
});
|
||||
|
||||
test('the named amendment and unresolved violation belong to distinct current offered choices', () => {
|
||||
for (const change of [
|
||||
(q: any) => { q.options[0].description = q.options[0].description.replace('✅ Save is', '✅ Publish is'); },
|
||||
(q: any) => { q.options[0].description = '✅ Example only: ' + q.options[0].description; },
|
||||
(q: any) => { q.options[0].description = '✅ Save is not the single filled primary action.'; },
|
||||
(q: any) => { q.options[0].description += ' This issue is withdrawn.'; },
|
||||
(q: any) => { q.options[2].description = 'All buttons already comply. No current issue remains.'; },
|
||||
(q: any) => { q.options[2].description = '❌ Hypothetical: Primary-action ambiguity ships; documented DESIGN.md violation remains.'; },
|
||||
(q: any) => { q.options[2].description += ' Correction: this issue is resolved.'; },
|
||||
(q: any) => { q.options[0].description += ' ' + q.options[2].description; q.options[2].description = 'Another compliant option.'; },
|
||||
]) { const fp = fresh(); changeMenu(fp, change); expect(isDesignCountFirstReview(fp)).toBe(false); }
|
||||
});
|
||||
|
||||
test('conditional stakes and an unrelated quoted historical claim retain the current choice', () => {
|
||||
const fp = fresh(); changeText(fp, s => s.replace('Stakes if we pick wrong:', 'Stakes if we pick wrong: If unchanged,'));
|
||||
expect(isDesignCountFirstReview(fp)).toBe(true);
|
||||
const history = fresh(); changeText(history, s => s.replace('\nStakes if we pick wrong:', ' The old report claimed "This issue is resolved.", but that claim was wrong.\nStakes if we pick wrong:'));
|
||||
expect(isDesignCountFirstReview(history)).toBe(true);
|
||||
});
|
||||
|
||||
import { E2E_TOUCHFILES, selectTests } from './helpers/touchfiles';
|
||||
test('the exact public fixture and controls select only the affected Design count workflow', () => {
|
||||
for (const file of ['test/design-primary-action-aj.test.ts', 'test/fixtures/design-primary-action-aj.json'])
|
||||
expect(selectTests([file], E2E_TOUCHFILES, []).selected).toEqual(['plan-design-finding-count']);
|
||||
});
|
||||
|
||||
import deferredTodo from './fixtures/design-future-todo-aj.json';
|
||||
import { isDesignArtifactGeneration } from './helpers/design-artifact-question';
|
||||
test('the actual completed future-only TODO recording is administrative and retains freshness', () => {
|
||||
expect(isDesignArtifactGeneration(deferredTodo)).toBe(true);
|
||||
expect(planCountQuestionPhase(deferredTodo, true, designStep0Boundary, isDesignCountFirstReview,
|
||||
isDesignCountSetup, undefined, isDesignArtifactGeneration)).toEqual({ preReview: false, reviewStarted: true, administrative: 'artifact-generation' });
|
||||
});
|
||||
|
||||
test('skipping a future TODO is administrative, while building it now remains a review decision', () => {
|
||||
for (const index of [0, 1, 2]) {
|
||||
const fp: any = structuredClone(deferredTodo), q = fp.nativeCall.questions[0];
|
||||
fp.nativeCall.answers = { [q.question]: q.options[index].label };
|
||||
expect(isDesignArtifactGeneration(fp)).toBe(index !== 2);
|
||||
const phase = planCountQuestionPhase(fp, true, designStep0Boundary, isDesignCountFirstReview,
|
||||
isDesignCountSetup, undefined, isDesignArtifactGeneration);
|
||||
expect(phase.preReview).toBe(false);
|
||||
expect(phase.administrative).toBe(index !== 2 ? 'artifact-generation' : undefined);
|
||||
}
|
||||
const fp: any = structuredClone(deferredTodo);
|
||||
expect(planCountQuestionPhase(fp, false, designStep0Boundary, isDesignCountFirstReview,
|
||||
isDesignCountSetup, undefined, isDesignArtifactGeneration).reviewStarted).toBe(false);
|
||||
});
|
||||
|
||||
test('a deferred artifact requires completed native identity, the exact answer and full aligned menu', () => {
|
||||
for (const change of [
|
||||
(f: any) => { delete f.nativeCall; },
|
||||
(f: any) => { f.nativeCall.answered = false; },
|
||||
(f: any) => { f.nativeCall.failed = true; },
|
||||
(f: any) => { f.nativeCall.sessionId = 'foreign'; },
|
||||
(f: any) => { f.nativeCall.answeredAt = 'invalid'; },
|
||||
(f: any) => { f.nativeCall.unansweredQuestionIndices = [0]; },
|
||||
(f: any) => { f.nativeQuestionIndex = 1; },
|
||||
(f: any) => { f.nativeCall.answers = {}; },
|
||||
(f: any) => { f.nativeCall.answers[f.nativeCall.questions[0].question] = 'unoffered'; },
|
||||
(f: any) => { f.options.reverse(); },
|
||||
(f: any) => { f.nativeCall.questions[0].multiSelect = true; },
|
||||
(f: any) => { f.nativeCall.questions.push(structuredClone(f.nativeCall.questions[0])); },
|
||||
(f: any) => { f.nativeCall.questions[0].options.pop(); },
|
||||
]) { const fp: any = structuredClone(deferredTodo); change(fp); expect(isDesignArtifactGeneration(fp)).toBe(false); }
|
||||
});
|
||||
|
||||
test('a deferred TODO cannot conceal current implementation, changed scope or source-only declarations', () => {
|
||||
for (const change of [
|
||||
(s: string) => 'Example: ' + s,
|
||||
(s: string) => '```text\n' + s + '\n```',
|
||||
(s: string) => s.replace('ELI10: DESIGN.md', 'ELI10: Suppose DESIGN.md'),
|
||||
(s: string) => s.replace(/^ELI10: (.+)$/m, 'ELI10: "$1"'),
|
||||
(s: string) => s.replace('so nothing changes now.', 'but replace the font now.'),
|
||||
(s: string) => s.replace('in a later design pass?', 'in this update?'),
|
||||
(s: string) => s + '\nCorrection: font replacement is now in scope; implement it now.',
|
||||
]) { const fp: any = structuredClone(deferredTodo); changeText(fp, change); expect(isDesignArtifactGeneration(fp)).toBe(false); }
|
||||
for (const index of [0, 1, 2]) {
|
||||
const fp: any = structuredClone(deferredTodo);
|
||||
fp.nativeCall.questions[0].options[index].description += ' Also fix the current form typography in this PR.';
|
||||
expect(isDesignArtifactGeneration(fp)).toBe(false);
|
||||
}
|
||||
const current = fresh();
|
||||
expect(isDesignArtifactGeneration(current)).toBe(false);
|
||||
expect(isDesignCountFirstReview(current)).toBe(true);
|
||||
});
|
||||
|
||||
test('deferred artifact classification follows offered identities and the current approved font', () => {
|
||||
const reordered: any = structuredClone(deferredTodo);
|
||||
changeMenu(reordered, q => q.options.reverse());
|
||||
reordered.nativeCall.answers = { [reordered.nativeCall.questions[0].question]: 'A Add to TODOS.md (recommended)' };
|
||||
expect(isDesignArtifactGeneration(reordered)).toBe(true);
|
||||
const renamed: any = structuredClone(deferredTodo);
|
||||
changeText(renamed, s => s.replaceAll('system-ui', 'ApprovedSans'));
|
||||
changeMenu(renamed, q => q.options.forEach((o: any) => { o.description = o.description.replaceAll('system-ui', 'ApprovedSans'); }));
|
||||
expect(isDesignArtifactGeneration(renamed)).toBe(true);
|
||||
});
|
||||
|
||||
test('the deferred TODO fixture selects the same affected Design count workflow', () => {
|
||||
expect(selectTests(['test/fixtures/design-future-todo-aj.json'], E2E_TOUCHFILES, []).selected).toEqual(['plan-design-finding-count']);
|
||||
});
|
||||
|
||||
test('deferred TODO scope survives benign explanations, estimates and a concise equivalent proposal', () => {
|
||||
for (const change of [
|
||||
(s: string) => s.replace('Why: a chosen typeface is the cheapest tell that the app was designed rather than assembled. Pros: brand voice across the whole app.', 'Why: a deliberate typeface could make the application recognizable. Pros: a consistent future brand voice.'),
|
||||
(s: string) => s.replace('(for example DM Sans, Instrument Sans, IBM Plex Sans)', '(for example Atkinson Hyperlegible)'),
|
||||
(s: string) => s.replace('Stakes if we pick wrong: either the debt is forgotten, or a note lands in TODOS.md that you consider noise.', 'Stakes if we pick wrong: the future debt may be forgotten, or the backlog may become noisy.').replace('Recommendation: A because the debt is real but explicitly out of scope, and a written TODO costs nothing.', 'Recommendation: A to retain the explicitly out-of-scope debt for later.').replace('Net: keep the typography debt visible vs. drop it.', 'Net: record the deferred typography debt or omit the note.'),
|
||||
(s: string) => s.replace('DESIGN.md and this plan keep system-ui as the app font, and you excluded visual exploration from this update, so nothing changes now.', 'DESIGN.md and this plan retain system-ui as the app font. Visual exploration remains out of scope for this update, so nothing changes now.'),
|
||||
]) { const fp: any = structuredClone(deferredTodo); changeText(fp, change); expect(isDesignArtifactGeneration(fp)).toBe(true); }
|
||||
const estimate: any = structuredClone(deferredTodo);
|
||||
estimate.nativeCall.questions[0].options[0].description = estimate.nativeCall.questions[0].options[0].description.replace('human: ~5min / CC: ~1min to record', 'human: ~10min / CC: ~2min to record');
|
||||
expect(isDesignArtifactGeneration(estimate)).toBe(true);
|
||||
const concise: any = structuredClone(deferredTodo);
|
||||
changeText(concise, s => s.replace('record a deferred TODOS.md item to evaluate a real body typeface', 'add a deferred TODOS.md note to consider an alternate body typeface').replace('in a later design pass?', 'during a future design pass?').replace(/^ELI10: .+$/m,
|
||||
'ELI10: DESIGN.md and the current plan preserve system-ui as the app font. Visual exploration is out of scope for this update, so the current design remains unchanged. This question only records a deferred TODOS.md note for a future /design-consultation; it does not change the current design.'));
|
||||
changeMenu(concise, q => {
|
||||
q.options[0].description = '✅ Records only a TODOS.md note for a future /design-consultation. No design changes in this update; DESIGN.md and system-ui remain unchanged.';
|
||||
q.options[1].description = '✅ No TODO is recorded. No follow-up work.';
|
||||
q.options[2].description = '✅ Replace the font now in this PR.';
|
||||
});
|
||||
expect(isDesignArtifactGeneration(concise)).toBe(true);
|
||||
});
|
||||
|
||||
test('paraphrased facts still require affirmative preservation and reject present work', () => {
|
||||
for (const change of [
|
||||
(s: string) => s.replace('so nothing changes now.', 'so it is false that nothing changes now.'),
|
||||
(s: string) => s.replace('ELI10: DESIGN.md and this plan keep system-ui as the app font', 'ELI10: DESIGN.md and this plan keep Roboto as the app font'),
|
||||
(s: string) => s.replace('Net: keep the typography debt visible vs. drop it.', 'Net: replace the font now.'),
|
||||
(s: string) => s.replace('Net: keep the typography debt visible vs. drop it.', 'Net: this scope is withdrawn.'),
|
||||
]) { const fp: any = structuredClone(deferredTodo); changeText(fp, change); expect(isDesignArtifactGeneration(fp)).toBe(false); }
|
||||
const conditional: any = structuredClone(deferredTodo);
|
||||
conditional.nativeCall.questions[0].options[0].description = conditional.nativeCall.questions[0].options[0].description.replace('Nothing changes in this update;', 'If approved: Nothing changes in this update;');
|
||||
expect(isDesignArtifactGeneration(conditional)).toBe(false);
|
||||
const additional: any = structuredClone(deferredTodo);
|
||||
additional.nativeCall.questions[0].options[0].description += ' Add a 48px button target to this plan.';
|
||||
expect(isDesignArtifactGeneration(additional)).toBe(false);
|
||||
for (const suffix of ['Add a TODOS.md note and make the Save button 48px.', 'Add a TODOS.md note for the future font review and make the Save button 48px.']) {
|
||||
const mixed: any = structuredClone(deferredTodo);
|
||||
mixed.nativeCall.questions[0].options[0].description += ' ' + suffix;
|
||||
expect(isDesignArtifactGeneration(mixed)).toBe(false);
|
||||
}
|
||||
const recordingOnly: any = structuredClone(deferredTodo);
|
||||
recordingOnly.nativeCall.questions[0].options[0].description += ' Add a TODOS.md note for the future font review.';
|
||||
expect(isDesignArtifactGeneration(recordingOnly)).toBe(true);
|
||||
for (const suffix of ['Visual exploration is no longer out of scope.', 'This plan no longer keeps system-ui.']) {
|
||||
const fp: any = structuredClone(deferredTodo);
|
||||
changeText(fp, s => s.replace(/^ELI10: (.+)$/m, 'ELI10: $1 ' + suffix));
|
||||
expect(isDesignArtifactGeneration(fp)).toBe(false);
|
||||
}
|
||||
const archival: any = structuredClone(deferredTodo);
|
||||
changeText(archival, s => s.replace(/^ELI10: (.+)$/m, 'ELI10: $1 Historical note: "Visual exploration is no longer out of scope."'));
|
||||
expect(isDesignArtifactGeneration(archival)).toBe(true);
|
||||
});
|
||||
@@ -0,0 +1,110 @@
|
||||
import { expect, test } from 'bun:test';
|
||||
import capture from './fixtures/design-scope-selection-aj.json';
|
||||
import { nativeSeededPlanSelection } from './helpers/plan-scope-selection';
|
||||
import type { PlanCountTranscript, NativePublicToolEvent } from './helpers/plan-count-transcript';
|
||||
import { selectTests, E2E_TOUCHFILES } from './helpers/touchfiles';
|
||||
|
||||
const originals = capture.observations;
|
||||
const check = (observation = structuredClone(originals[0]!)) => nativeSeededPlanSelection(
|
||||
observation.transcript as PlanCountTranscript,
|
||||
observation.tools as NativePublicToolEvent[],
|
||||
observation.opts,
|
||||
);
|
||||
const selectedMessage = (o: typeof originals[number]) => o.transcript.assistantMessages.find(m => m.text.includes('"Marketing landing page"'))!;
|
||||
|
||||
test('both actual explicit draft selections bind the named seed after this session loaded the skill', () => {
|
||||
for (const o of originals) expect(check(o)).toBe(true);
|
||||
for (const verb of ["I'll review", 'I will review', "I'll go with reviewing", 'I will go with reviewing']) {
|
||||
const o = structuredClone(originals[1]!);
|
||||
selectedMessage(o).text = `${verb} the "Marketing landing page" draft, starting by checking the design system.`;
|
||||
expect(check(o)).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
test('a named target still requires the successful current skill and invocation', () => {
|
||||
for (const original of originals) {
|
||||
for (const mutate of [
|
||||
(o: typeof original) => { o.opts.seed = '# Plan: Other page'; },
|
||||
(o: typeof original) => { o.opts.seed += '\n# Plan: Another'; },
|
||||
(o: typeof original) => { o.opts.sessionId = 'foreign'; },
|
||||
(o: typeof original) => { o.opts.commandStartedAt = Date.parse(selectedMessage(o).timestamp) + 1; },
|
||||
(o: typeof original) => { o.tools[0]!.input!.skill = 'plan-ceo-review'; },
|
||||
(o: typeof original) => { o.tools[1]!.isError = true; },
|
||||
(o: typeof original) => { o.tools[1]!.toolUseId = 'foreign'; },
|
||||
(o: typeof original) => { o.tools.pop(); },
|
||||
]) {
|
||||
const o = structuredClone(original); mutate(o); expect(check(o)).toBe(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('quoted, hypothetical, conditional and withdrawn selections do not select the seed', () => {
|
||||
for (const original of originals) {
|
||||
const text = selectedMessage(original).text.trim();
|
||||
for (const invalid of [
|
||||
'> ' + text, ' ' + text, '"' + text + '"', 'Example:\n' + text,
|
||||
'The following is a source excerpt.\n' + text, 'An unproven hypothesis.\n' + text,
|
||||
text.replace("I'll", 'I might'), text.replace("I'll", "I won't"),
|
||||
text.replace('Marketing landing page', 'Other page'),
|
||||
text.replace('draft', 'branch diff'), text.replace(/,$/, '?'),
|
||||
text.replace(', ', ', if approved, '),
|
||||
text + ' I retract that selection.', text + ' This selection is withdrawn.',
|
||||
text + ' Treat that declaration as a hypothetical example.',
|
||||
].filter(value => value !== text)) {
|
||||
const o = structuredClone(original); selectedMessage(o).text = invalid;
|
||||
expect(check(o), invalid).toBe(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('scope selection remains mapped to the existing design and engineering mode workflows', () => {
|
||||
for (const file of ['test/design-scope-selection-aj.test.ts', 'test/fixtures/design-scope-selection-aj.json']) {
|
||||
expect(selectTests([file], E2E_TOUCHFILES, []).selected.sort()).toEqual(['plan-design-review-plan-mode', 'plan-eng-review-plan-mode']);
|
||||
}
|
||||
});
|
||||
|
||||
test('a complete owned observation cannot use a withdrawn selection or a replacement target', () => {
|
||||
for (const original of originals) {
|
||||
for (const correction of ['The selection has been withdrawn.', 'The selected target is now the branch diff.', 'I have withdrawn this selection.', 'Correction: The selected target is now the branch diff.']) {
|
||||
for (const separator of [' ', '\n\n']) {
|
||||
const o = structuredClone(original);
|
||||
selectedMessage(o).text = selectedMessage(o).text.trim() + separator + correction;
|
||||
expect(check(o)).toBe(false);
|
||||
}
|
||||
const o = structuredClone(original);
|
||||
o.transcript.assistantMessages.push({ sessionId: o.opts.sessionId, timestamp: new Date(Date.parse(selectedMessage(o).timestamp) + 1000).toISOString(), text: correction });
|
||||
expect(check(o)).toBe(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('old, unrelated, foreign and quoted assessments do not withdraw the current target', () => {
|
||||
for (const original of originals) {
|
||||
for (const text of [
|
||||
'Old note: "The selection has been withdrawn."',
|
||||
'> The selection has been withdrawn.',
|
||||
'```text\nThe selected target is now the branch diff.\n```',
|
||||
'Source excerpt:\nThe selection has been withdrawn.',
|
||||
'The following is a hypothetical example.\nThe selected target is now the branch diff.',
|
||||
'An unrelated payment selection has been withdrawn.',
|
||||
'The selected target is now the "Marketing landing page" draft.',
|
||||
'If approved, the selection has been withdrawn.',
|
||||
'The selected target is now the branch diff?',
|
||||
'The selected target is now the branch diff? This is a question.',
|
||||
'I have withdrawn this selection?',
|
||||
]) {
|
||||
const o = structuredClone(original);
|
||||
o.transcript.assistantMessages.push({ sessionId: o.opts.sessionId, timestamp: new Date(Date.parse(selectedMessage(o).timestamp) + 1000).toISOString(), text });
|
||||
expect(check(o), text).toBe(true);
|
||||
}
|
||||
for (const foreign of [false, true]) {
|
||||
const o = structuredClone(original);
|
||||
o.transcript.assistantMessages.push({ sessionId: foreign ? 'foreign' : o.opts.sessionId, timestamp: new Date(Date.parse(selectedMessage(o).timestamp) + (foreign ? 1000 : -1000)).toISOString(), text: 'The selection has been withdrawn.' });
|
||||
expect(check(o)).toBe(true);
|
||||
}
|
||||
const o = structuredClone(original), selected = structuredClone(selectedMessage(o));
|
||||
o.transcript.assistantMessages.push({ sessionId: o.opts.sessionId, timestamp: new Date(Date.parse(selected.timestamp) + 1000).toISOString(), text: 'The selection has been withdrawn.' });
|
||||
o.transcript.assistantMessages.push({ ...selected, timestamp: new Date(Date.parse(selected.timestamp) + 2000).toISOString() });
|
||||
expect(check(o)).toBe(true);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,130 @@
|
||||
import { expect, test } from 'bun:test';
|
||||
import fixture from './fixtures/eng-snapshot-adapter-aj.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 = ['## Tests\n\n### Required tests (write alongside the code, not after)\n\n' + fixture.required,
|
||||
'## Implementation Tasks\n\n' + fixture.taskIntro + '\n\n' + fixture.task, fixture.reviewReport].join('\n\n');
|
||||
const native = () => structuredClone(fixture.transcript) as PlanCountTranscript;
|
||||
const { start, end } = fixture.provenance.window;
|
||||
const evaluate = (p = plan, t = native()) => evaluateEngSeedCoverage(t, p, start, end);
|
||||
const cacheCall = (t: PlanCountTranscript) => t.calls.find(c => c.toolUseId === 'toolu_01RP1Yzt5jbat8STPd4k3bER')!;
|
||||
function edit(t: PlanCountTranscript, change: (s: string) => string) {
|
||||
const c = cacheCall(t), q = c.questions[0]!, answer = c.answers[q.question]!;
|
||||
q.question = change(q.question); c.answers = { [q.question]: answer };
|
||||
}
|
||||
|
||||
test('the exact completed adapter race identifies shared-cache ownership in its own explanation', () => {
|
||||
expect(evaluate().missing).toEqual([]);
|
||||
expect(new Set(Object.values(evaluate().decisions)).size).toBe(4);
|
||||
});
|
||||
|
||||
test('the exact mandatory snapshot, parity and unchanged baseline task establish regression coverage', () => {
|
||||
expect(evaluate().regression).toBe('plan');
|
||||
expect(evaluate().ok).toBe(true);
|
||||
expect(fixture.provenance.historicalPaidFailurePreserved).toBe(true);
|
||||
});
|
||||
|
||||
test('shared-adapter title cannot borrow a cache from source text or unrelated options', () => {
|
||||
for (const change of [
|
||||
(s: string) => s.replace('on the shared adapter', 'on the logging adapter'),
|
||||
(s: string) => s.replace('SessionMint and AuthBroker', 'QueueWorker and AuthBroker'),
|
||||
(s: string) => s.replace('write-after-invalidate race', 'completed design documentation'),
|
||||
(s: string) => s.replace(/^ELI10: .+$/m, ''),
|
||||
(s: string) => s.replace('both services write into the same cache', 'both services read unrelated caches'),
|
||||
(s: string) => s.replace('both services write into the same cache', 'both services do not write into the same cache'),
|
||||
(s: string) => s.replace(/^ELI10: /m, 'ELI10: If approved, '),
|
||||
(s: string) => s.replace(/^ELI10: /m, 'ELI10: The following is a hypothetical example. '),
|
||||
(s: string) => s.replace(/^ELI10: (.+)$/m, 'ELI10: "$1"'),
|
||||
(s: string) => s.replace(/^ELI10: (.+)$/m, 'ELI10: `$1`'),
|
||||
(s: string) => s.replace(/^ELI10: (.+)$/m, '```text\nELI10: $1\n```'),
|
||||
(s: string) => s.replace(/^ELI10:/m, 'Source excerpt, not a current finding:\nELI10:'),
|
||||
(s: string) => s.replace(/^ELI10:/m, 'If approved:\nELI10:'),
|
||||
(s: string) => s.replace(/^Project\/branch\/task: .+$/m, 'Project/branch/task: copied source example; following ELI10 is not a current finding'),
|
||||
(s: string) => s + '\nThis issue is withdrawn.',
|
||||
(s: string) => s + '\nIssue 1 is rejected.',
|
||||
(s: string) => s + '\nThere is no current shared-cache race.',
|
||||
]) { const t = native(); edit(t, change); expect(evaluate(plan, t).missing).toContain('shared-cache'); }
|
||||
const t = native(), q = cacheCall(t).questions[0]!;
|
||||
q.options = [{ label: 'Archive review' }, { label: 'Pause review' }];
|
||||
cacheCall(t).answers = { [q.question]: q.options[0]!.label };
|
||||
expect(evaluate(plan, t).missing).toContain('shared-cache');
|
||||
});
|
||||
|
||||
test('the shared-adapter decision retains owned completed native identity and offered answer gates', () => {
|
||||
for (const mutate of [
|
||||
(t: PlanCountTranscript) => { cacheCall(t).answered = false; },
|
||||
(t: PlanCountTranscript) => { cacheCall(t).failed = true; },
|
||||
(t: PlanCountTranscript) => { cacheCall(t).sessionId = 'foreign'; },
|
||||
(t: PlanCountTranscript) => { cacheCall(t).answeredAt = new Date(start - 1).toISOString(); },
|
||||
(t: PlanCountTranscript) => { cacheCall(t).answeredAt = new Date(end + 1).toISOString(); },
|
||||
(t: PlanCountTranscript) => { cacheCall(t).answers = {}; },
|
||||
(t: PlanCountTranscript) => { const c=cacheCall(t); c.answers = { [c.questions[0]!.question]: 'unoffered' }; },
|
||||
(t: PlanCountTranscript) => { cacheCall(t).unansweredQuestionIndices = [0]; },
|
||||
(t: PlanCountTranscript) => { t.calls.push(structuredClone(cacheCall(t))); },
|
||||
]) { const t=native(); mutate(t); expect(evaluate(plan,t).ok).toBe(false); }
|
||||
for (const choice of cacheCall(native()).questions[0]!.options) {
|
||||
const t=native(),c=cacheCall(t);c.answers={[c.questions[0]!.question]:choice.label};
|
||||
expect(evaluate(plan,t).missing).toEqual([]);
|
||||
}
|
||||
});
|
||||
|
||||
test('snapshot declaration and task must independently bind current legacy behavior and the unchanged baseline', () => {
|
||||
for (const p of [
|
||||
plan.replace(fixture.required, ''), plan.replace(fixture.task, ''),
|
||||
plan.replace('capture current outputs', 'describe future outputs'),
|
||||
plan.replace('BEFORE any change', 'AFTER the rewrite'),
|
||||
plan.replace('produce identical', 'produce similar'),
|
||||
plan.replace('both legacy (flag OFF) and new (flag ON)', 'only the new (flag ON)'),
|
||||
plan.replace('Mandatory under', 'Optional under'),
|
||||
plan.replace('`legacyAuthFlow` snapshot', '`newAuthFlow` snapshot'),
|
||||
plan.replace('Snapshot legacyAuthFlow() behavior', 'Snapshot newAuthFlow() behavior'),
|
||||
plan.replace('as regression tests before any change', 'as future examples after deployment'),
|
||||
plan.replace('against unmodified legacy code', 'against modified new code'),
|
||||
plan.replace('then against flag-OFF route', 'then against flag-ON route'),
|
||||
plan.replace(' - Verify: tests pass against', ' - Verify: tests might pass against'),
|
||||
]) { expect(p).not.toBe(plan); expect(evaluate(p).regression,p).toBeUndefined(); }
|
||||
});
|
||||
|
||||
test('source, conditional and withdrawn snapshot instructions do not become required tests', () => {
|
||||
for (const p of [
|
||||
'# Quoted source\n\n'+plan, '# Hypothetical example\n\n'+plan,
|
||||
'The following is a hypothetical example.\n\n'+plan,
|
||||
plan.replace(fixture.required, 'If approved:\n'+fixture.required),
|
||||
plan.replace(fixture.required, '"'+fixture.required+'"'),
|
||||
plan.replace(fixture.required, '`'+fixture.required.replaceAll('`','')+'`'),
|
||||
plan.replace(fixture.required, '```\n'+fixture.required+'\n```'),
|
||||
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## Regression correction\nThe regression suite is no longer required.',
|
||||
plan+'\n## Payment regression suite\nThe legacy regression suite is no longer required.',
|
||||
plan+'\n## Final regression suite assessment\nThe regression suite is no longer required.',
|
||||
plan.replace(fixture.task, fixture.task+'\n - Correction: this unchanged-code verification is withdrawn.'),
|
||||
plan.replace(fixture.task, 'If approved:\n'+fixture.task),
|
||||
plan.replace(fixture.task, '`'+fixture.task.replaceAll('`','')+'`'),
|
||||
plan.replace(fixture.task, fixture.task.split('\n').map(s=>'> '+s).join('\n')),
|
||||
]) expect(evaluate(p).regression,p).toBeUndefined();
|
||||
});
|
||||
|
||||
test('task identities and ordinary presentation vary while unrelated rejection and quoted notes remain harmless', () => {
|
||||
for (const p of [
|
||||
plan.replaceAll('T1','T12'),
|
||||
'```\nPrior completed output\n```\n\n'+plan,
|
||||
plan.replace('tests/auth —','core/auth —'),
|
||||
plan.replace('valid, expired, wrong-audience, wrong-tenant','valid, expired, wrong-issuer, wrong-tenant'),
|
||||
plan+'\n## Assessment of T9\nT9 is rejected.',
|
||||
plan.replace(fixture.task,fixture.task+'\nOld note: "T1 is cancelled."'),
|
||||
plan.replace(fixture.task,fixture.task+'\nOld note: "This unchanged-code verification is withdrawn."'),
|
||||
plan+'\n## Historical note\nOld note: "The regression suite is no longer required."',
|
||||
plan+'\n## Payment regression suite\nThe regression suite is no longer required.',
|
||||
]) expect(evaluate(p).regression).toBe('plan');
|
||||
const t=native();edit(t,s=>s+'\nOld note: "This issue is withdrawn."');
|
||||
expect(evaluate(plan,t).missing).toEqual([]);
|
||||
});
|
||||
|
||||
test('the new exact public evidence belongs only to the existing Eng count owner', () => {
|
||||
for (const file of ['test/eng-snapshot-adapter-aj.test.ts','test/fixtures/eng-snapshot-adapter-aj.json'])
|
||||
expect(selectTests([file],E2E_TOUCHFILES,[]).selected).toEqual(['plan-eng-finding-count']);
|
||||
});
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"cwd": "/tmp/gstack-paid-shard-s9u5nX/tmp/gstack-autoplan-chain-2fm0gB",
|
||||
"ownedStateRoot": "/tmp/gstack-paid-shard-s9u5nX/tmp/gstack-hermetic-1053224-0wnKD0/skill-home-Apv3Kh/.gstack",
|
||||
"viewportCapturedAt": 1789019855180,
|
||||
"pending": {
|
||||
"source": "pre_tool_use",
|
||||
"sessionId": "a6866038-b8f3-4d4b-96ef-2b7bedb87909",
|
||||
"toolUseId": "toolu_01MPtrmcKUsSV5jQTxotGRH4",
|
||||
"tool": "Edit",
|
||||
"file": "/tmp/gstack-paid-shard-s9u5nX/tmp/gstack-hermetic-1053224-0wnKD0/skill-home-Apv3Kh/.gstack/projects/gstack-autoplan-chain-2fm0gB/ceo-plans/2026-09-10-user-dashboard.md",
|
||||
"timestamp": "2026-09-10T05:55:22.812Z"
|
||||
},
|
||||
"events": [
|
||||
{
|
||||
"sessionId": "a6866038-b8f3-4d4b-96ef-2b7bedb87909",
|
||||
"timestamp": "2026-09-10T05:53:57.405Z",
|
||||
"toolUseId": "toolu_016zuUSxgruR3zxXqQCK18wG",
|
||||
"kind": "use",
|
||||
"name": "Write",
|
||||
"input": {
|
||||
"file_path": "/tmp/gstack-paid-shard-s9u5nX/tmp/gstack-hermetic-1053224-0wnKD0/skill-home-Apv3Kh/.gstack/projects/gstack-autoplan-chain-2fm0gB/ceo-plans/2026-09-10-user-dashboard.md"
|
||||
}
|
||||
},
|
||||
{
|
||||
"sessionId": "a6866038-b8f3-4d4b-96ef-2b7bedb87909",
|
||||
"timestamp": "2026-09-10T05:54:02.558Z",
|
||||
"toolUseId": "toolu_016zuUSxgruR3zxXqQCK18wG",
|
||||
"kind": "result",
|
||||
"isError": false
|
||||
}
|
||||
],
|
||||
"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-2fm0gB (no remote configured)\nBase plan: `.claude/plans/ui-heavy-feature.md` (its \"Existing product and application\ncontracts\" section defines the terms used below; this document adds decisions, not contracts).\n\n## Glossary (from the base plan)\n- **Action registry**: existing server-side list of three actions (create an item, resume assigned work, invite a member), each with a stable ID, label, route target, and an **eligibility predicate** evaluated on the server against the request context. The registry supplies labels and routes only; it does not supply item titles.\n- **Hero**: the first quick action in this fixed priority order among the eligible ones: resume assigned work, create an item, invite a member. It is rendered larger than the other quick actions and uses the registry label (so \"Resume assigned work\", never an item title). It is not a new slot and adds no new data.\n- **Bulk-read API**: existing member-scoped mutation that marks notifications at or before a supplied snapshot time as read. It already accepts the snapshot; no new mutation is added.\n- **fetchedAt**: ISO-8601 UTC timestamp set by the server when it composes the dashboard response.\n- **Exposure events**: `dashboard_viewed{panelStates}` once per page mount after first render; `dashboard_panel_rendered{panel,state}` on each transition to a terminal state.\n- **Interaction events**: `quick_action_clicked{actionId}`, `dashboard_view_all_clicked{panel}`, `dashboard_panel_retry_clicked{panel}`, `mark_all_read_confirmed{count}`, `mark_all_read_cancelled`.\n- **Current landing page**: the page members reach today after login (flag off). Its p95 is measured server-side from the existing request metrics over the same window as the dashboard measurement.\n\n## Vision\n\n### 10x Check\nThe 10x version is not a prettier dashboard. It is a landing page that already\nknows the member's next task. The moment the page paints, the hero quick action\nsits at the top: \"Resume assigned work\" when that action is eligible, otherwise\n\"Create an item\". Notifications sit second with an unread count and one-click\n\"Mark all as read\". Activity is third, a calm answer to \"what changed while I was\naway\". The page is measured, not assumed: exposure and interaction events are\nemitted as defined above, so login-to-first-completed-task is read from production\nanalytics for the flag-on cohort against a concurrent flag-off control. Effort for\nthe hero-first hierarchy over a flat three-card grid: human ~0.5 day / CC ~10 min.\nThe redirect-when-resumable variant is a separate experiment (deferred, see below)\nbecause it changes the stated direction that the dashboard is the landing page.\n\n## Layout\nTwelve-column grid from the existing page shell.\n- **sm**: single column, order QuickActions, Notifications, Activity.\n- **md**: QuickActions spans 12; beneath it Notifications 6 and Activity 6.\n- **lg**: three columns: QuickActions 5, Notifications 4, Activity 3.\n\n## Scope Decisions\n\n| # | Proposal | Effort | Decision | Reasoning |\n|---|----------|--------|----------|-----------|\n| 1 | Per-panel result envelope in `GET /api/dashboard`: each key is `{status:'ok',data}` or `{status:'error',error:{code}}`, inside an HTTP 200; HTTP 503 only when all three fail. Repository calls run in parallel with a 2 s per-panel deadline passed to each call as an abort signal; on deadline the panel yields `{status:'error',error:{code:'timeout'}}` and the call is aborted. If a repository method cannot take a signal, the leaked work is bounded by the existing database statement timeout and that bound is noted in the handler | S | ACCEPTED | In blast radius; makes partial failure explicit (P5); the deadline is what lets the latency gate hold without caching |\n| 2 | Server `fetchedAt` used as the snapshot passed to the existing bulk-read API | S | ACCEPTED | The API already takes a snapshot; using the server time preserves \"later arrivals remain unread\" against client clock skew. No new mutation |\n| 3 | `quickActions` filtered server-side by the registry eligibility predicates | S | ACCEPTED | Without it the permission-error guardrail trips by construction |\n| 4 | p95 latency of `GET /api/dashboard` at or below the current landing page's server-side p95, both measured over 1000 requests in staging; rollout gate | S | ACCEPTED | Plan already says perf checks must be specified; this specifies them |\n| 5 | Production baseline (p50 and p90 login-to-first-completion from existing login and action-completion events) pulled before rollout; required members per arm computed from the baseline variance for detecting a 20% p50 change at 80% power before the cohort starts; flag `dashboard_landing` assigned randomly per member; success = p50 improves by at least 20% Line truncated
|
||||
"viewport": "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n Edit file\n \u20261053224-0wnKD0/skill-home-Apv3Kh/.gstack/projects/gstack-autoplan-chain-2fm0gB/ceo-plans/2026-09-10-user-dashboard.md\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 10 \n 11 ## Glossary (from the base plan)\n 12 - **Action registry**: existing server-side list of three actions (create an item, resume assigned work, invite a m\n ember), each with a stable ID, label, route target, and an **eligibility predicate** evaluated on the server agains\n t the request context. The registry supplies labels and routes only; it does not supply item titles.\n 13 -- **Hero**: the first quick action in this fixed priority order among the eligible ones: resume assigned work, crea\n -te an item, invite a member. It is rendered larger than the other quick actions and uses the registry label (so \"Re\n -sume assigned work\", never an item title). It is not a new slot and adds no new data. \n 14 -- **Bulk-read API**: existing member-scoped mutation that marks notifications at or before a supplied snapshot time\n - as read. It already accepts the snapshot; no new mutation is added. \n 13 +- **Hero**: the first quick action in this fixed priority order among the eligible ones: resume assigned work, crea\n +te an item, invite a member. It is rendered larger than the other quick actions and uses the registry label (so \"Re\n +sume assigned work\", never an item title), with one client-side copy substitution: in the new-member case (below) t\n +he create action's label is shown as \"Create your first item\". It is not a new slot and adds no new data. \n 14 +- **New-member case**: resume is not eligible (so create is the hero) and both Notifications and Activity are empty\n +. \n 15 +- **Bulk-read API**: existing member-scoped mutation that marks notifications at or before a supplied snapshot time\n + as read. It already accepts the snapshot; no new mutation is added. The base plan does not state a return value, s\n +o the \"Marked N as read\" count is the client's unread count from the last dashboard response, not a server-reported\n + number. \n 16 +- **Refetch**: a refetch (after mark-all-read or on `visibilitychange`) keeps the current data on screen, does not \n +re-enter the loading state, and re-emits `dashboard_panel_rendered` only if the panel's state value changes (for ex\n +ample success to empty). \n 17 - **fetchedAt**: ISO-8601 UTC timestamp set by the server when it composes the dashboard response.\n 18 - **Exposure events**: `dashboard_viewed{panelStates}` once per page mount after first render; `dashboard_panel_ren\n dered{panel,state}` on each transition to a terminal state.\n 19 - **Interaction events**: `quick_action_clicked{actionId}`, `dashboard_view_all_clicked{panel}`, `dashboard_panel_r\n etry_clicked{panel}`, `mark_all_read_confirmed{count}`, `mark_all_read_cancelled`.\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\u2Line truncated
|
||||
}
|
||||
Vendored
+457
@@ -0,0 +1,457 @@
|
||||
{
|
||||
"sourceHead": "a6eaafacb846bdb018a1ead13985be9c05e2c2df",
|
||||
"evidence": ".context/ship-source-aj-delta-paid-20260910-v1/ceo-first-terminal-ledger-v1/proof.json",
|
||||
"evidenceSha256": "3615811b654f38f21bff2fe907c9a18504188bdea50fc4171f2fefe736c1f36e",
|
||||
"cases": {
|
||||
"paired": {
|
||||
"capture": {
|
||||
"skill": "plan-ceo-review",
|
||||
"runId": "ship-source-aj-delta-paid-20260910-v1-4",
|
||||
"cwd": "/tmp/gstack-paid-shard-rGMMii/tmp/gstack-plan-count-knpyoQ",
|
||||
"claudeConfigDir": "/tmp/gstack-paid-shard-rGMMii/tmp/gstack-hermetic-1054183-PR2vOl/with-skills/.claude",
|
||||
"at": "2026-09-10T05:47:58.520Z"
|
||||
},
|
||||
"calls": [
|
||||
{
|
||||
"sessionId": "a9f1e8d0-71c2-4c5d-8eb8-e2ae6883b58d",
|
||||
"toolUseId": "toolu_0157dCDX8MR1dwx2WLrM5w8p",
|
||||
"questions": [
|
||||
{
|
||||
"question": "D1 — Add gstack skill routing rules to this project's CLAUDE.md?\nProject/branch/task: gstack-plan-count-knpyoQ on main, CEO review of PLAN.md (payment test coverage).\nELI10: gstack wants a short block in CLAUDE.md that tells Claude which skill to reach for (bugs → /investigate, scope → /plan-ceo-review, etc.). It is a one-time setup prompt, not part of the plan review. Note: plan mode blocks editing CLAUDE.md and committing right now, so option A would be recorded and applied only after you leave plan mode; this repo is also a review fixture, so the block may not be wanted here.\nStakes if we pick wrong: pick A on a fixture and you get an unwanted CLAUDE.md commit later; pick B and future sessions in this repo won't auto-route to gstack skills (re-enable anytime with gstack-config).\nRecommendation: A because that is gstack's default onboarding path, but B is equally reasonable for a throwaway fixture.\nNote: options differ in kind, not coverage — no completeness score.\nA) Add routing rules to CLAUDE.md (recommended)\n ✅ Future sessions in this repo auto-route requests to the right gstack skill\n ✅ Standard gstack onboarding; matches every other gstack-enabled project\n ❌ Cannot be applied in plan mode; lands as a deferred CLAUDE.md edit + commit after this review\nB) No thanks, I'll invoke skills manually\n ✅ No edits or commits to a review-fixture repo; nothing deferred past this session\n ✅ Records the decline in ~/.gstack config so this prompt never fires again here\n ❌ Skill routing stays manual for this project until you flip the config back\nNet: convenience for future sessions in this repo versus keeping a fixture repo untouched.",
|
||||
"header": "Routing",
|
||||
"multiSelect": false,
|
||||
"options": [
|
||||
{
|
||||
"label": "Add routing rules (recommended)",
|
||||
"description": "Append the gstack skill-routing section to CLAUDE.md and commit it once plan mode ends."
|
||||
},
|
||||
{
|
||||
"label": "No thanks, manual skills",
|
||||
"description": "Set routing_declined=true in gstack config; leave CLAUDE.md untouched."
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"answered": true,
|
||||
"failed": false,
|
||||
"answers": {
|
||||
"D1 — Add gstack skill routing rules to this project's CLAUDE.md?\nProject/branch/task: gstack-plan-count-knpyoQ on main, CEO review of PLAN.md (payment test coverage).\nELI10: gstack wants a short block in CLAUDE.md that tells Claude which skill to reach for (bugs → /investigate, scope → /plan-ceo-review, etc.). It is a one-time setup prompt, not part of the plan review. Note: plan mode blocks editing CLAUDE.md and committing right now, so option A would be recorded and applied only after you leave plan mode; this repo is also a review fixture, so the block may not be wanted here.\nStakes if we pick wrong: pick A on a fixture and you get an unwanted CLAUDE.md commit later; pick B and future sessions in this repo won't auto-route to gstack skills (re-enable anytime with gstack-config).\nRecommendation: A because that is gstack's default onboarding path, but B is equally reasonable for a throwaway fixture.\nNote: options differ in kind, not coverage — no completeness score.\nA) Add routing rules to CLAUDE.md (recommended)\n ✅ Future sessions in this repo auto-route requests to the right gstack skill\n ✅ Standard gstack onboarding; matches every other gstack-enabled project\n ❌ Cannot be applied in plan mode; lands as a deferred CLAUDE.md edit + commit after this review\nB) No thanks, I'll invoke skills manually\n ✅ No edits or commits to a review-fixture repo; nothing deferred past this session\n ✅ Records the decline in ~/.gstack config so this prompt never fires again here\n ❌ Skill routing stays manual for this project until you flip the config back\nNet: convenience for future sessions in this repo versus keeping a fixture repo untouched.": "Add routing rules (recommended)"
|
||||
},
|
||||
"unansweredQuestionIndices": [],
|
||||
"answeredAt": "2026-09-10T05:43:09.369Z"
|
||||
},
|
||||
{
|
||||
"sessionId": "a9f1e8d0-71c2-4c5d-8eb8-e2ae6883b58d",
|
||||
"toolUseId": "toolu_01PjbbZEfu1qtNuKovC9byvG",
|
||||
"questions": [
|
||||
{
|
||||
"question": "D2 — Let gstack search learnings from your other projects on this machine?\nProject/branch/task: gstack-plan-count-knpyoQ on main, CEO review of PLAN.md (payment test coverage).\nELI10: gstack keeps a local file of lessons it learns per project (pitfalls, command fixes, patterns). It can also read the lessons from your OTHER projects on this machine and apply the ones that match. Nothing leaves the machine. This is a one-time config choice; this project currently has zero learnings either way.\nStakes if we pick wrong: enable it on a machine with several client codebases and one client's project quirks could color advice in another; keep it scoped and you lose cross-pollination of generic lessons (test patterns, CLI fixes).\nRecommendation: A because gstack recommends it for solo developers and the data stays local; choose B if you work across multiple client codebases.\nNote: options differ in kind, not coverage — no completeness score.\nA) Enable cross-project learnings (recommended)\n ✅ Generic lessons (test-factory pitfalls, tooling fixes) carry over between your projects\n ✅ Stays on this machine; sets one local config key you can flip back anytime\n ❌ Lessons from unrelated client codebases can surface where they don't apply\nB) Keep learnings project-scoped only\n ✅ Hard wall between codebases; nothing from another client's project appears here\n ✅ Simplest mental model: each repo's learnings file is the whole story\n ❌ Every new project starts cold and relearns the same tooling pitfalls\nNet: faster compounding across projects versus a strict per-repo boundary.",
|
||||
"header": "Learnings",
|
||||
"multiSelect": false,
|
||||
"options": [
|
||||
{
|
||||
"label": "Enable cross-project (recommended)",
|
||||
"description": "Set cross_project_learnings=true; searches learnings across all your local projects."
|
||||
},
|
||||
{
|
||||
"label": "Project-scoped only",
|
||||
"description": "Set cross_project_learnings=false; only this repo's learnings are searched."
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"answered": true,
|
||||
"failed": false,
|
||||
"answers": {
|
||||
"D2 — Let gstack search learnings from your other projects on this machine?\nProject/branch/task: gstack-plan-count-knpyoQ on main, CEO review of PLAN.md (payment test coverage).\nELI10: gstack keeps a local file of lessons it learns per project (pitfalls, command fixes, patterns). It can also read the lessons from your OTHER projects on this machine and apply the ones that match. Nothing leaves the machine. This is a one-time config choice; this project currently has zero learnings either way.\nStakes if we pick wrong: enable it on a machine with several client codebases and one client's project quirks could color advice in another; keep it scoped and you lose cross-pollination of generic lessons (test patterns, CLI fixes).\nRecommendation: A because gstack recommends it for solo developers and the data stays local; choose B if you work across multiple client codebases.\nNote: options differ in kind, not coverage — no completeness score.\nA) Enable cross-project learnings (recommended)\n ✅ Generic lessons (test-factory pitfalls, tooling fixes) carry over between your projects\n ✅ Stays on this machine; sets one local config key you can flip back anytime\n ❌ Lessons from unrelated client codebases can surface where they don't apply\nB) Keep learnings project-scoped only\n ✅ Hard wall between codebases; nothing from another client's project appears here\n ✅ Simplest mental model: each repo's learnings file is the whole story\n ❌ Every new project starts cold and relearns the same tooling pitfalls\nNet: faster compounding across projects versus a strict per-repo boundary.": "Enable cross-project (recommended)"
|
||||
},
|
||||
"unansweredQuestionIndices": [],
|
||||
"answeredAt": "2026-09-10T05:43:27.436Z"
|
||||
},
|
||||
{
|
||||
"sessionId": "a9f1e8d0-71c2-4c5d-8eb8-e2ae6883b58d",
|
||||
"toolUseId": "toolu_01CYrgmg182hJ1Hbxm3XAhgS",
|
||||
"questions": [
|
||||
{
|
||||
"question": "D3 — Where and how should the two processPayment tests be structured?\nProject/branch/task: gstack-plan-count-knpyoQ on main, CEO review of PLAN.md (payment test coverage).\nELI10: The plan adds two tests: one for a successful charge, one for two 502s exhausting the single retry. The structural choice is where they live and whether to add shared assertion helpers. This question is about shape only; what each test asserts is decided separately in the findings that follow.\nStakes if we pick wrong: put the tests in the adapter suite and they never exercise processPayment's own receipt-shaping and retry-exhaustion path; add helpers for two call sites and you get an abstraction nobody else uses yet.\nRecommendation: A because two focused tests in the caller's own suite is the smallest diff that fully proves both contracts, and helpers for two call sites is premature abstraction.\nCompleteness: A=10/10, B=10/10, C=4/10\nA) Two focused tests in the existing processPayment suite, using the current factory, Stripe mock and virtual sleeper as planned (recommended) (human: ~1h / CC: ~5 min)\n ✅ Exercises processPayment's real caller path: receipt shaping and retry exhaustion\n ✅ Zero new files, zero new helpers; reuses every fixture the plan already names\n ❌ Assertions are spelled out inline per test rather than through a shared helper\nB) Same two tests plus small shared helpers (expectReceipt, expectRetryTrace) in the payment test factory (human: ~2h / CC: ~10 min)\n ✅ Future processPayment tests get exact receipt and retry assertions for free\n ✅ One place to update if the sleeper record or receipt shape ever changes\n ❌ Two call sites do not justify a helper; it is premature abstraction and widens the diff into the factory\nC) Add the assertions to the existing Stripe adapter suite instead of the processPayment suite (human: ~1h / CC: ~5 min)\n ✅ Keeps all Stripe-related tests in one suite\n ✅ Adapter suite already has 502-recovery scaffolding to copy from\n ❌ Adapter coverage does not prove processPayment's own receipt or PaymentUnavailable path; the stated contracts are the caller's\nNet: A and B cover the same contracts; A wins on diff size and avoids abstraction for two uses. C misses the caller path entirely.",
|
||||
"header": "Approach",
|
||||
"multiSelect": false,
|
||||
"options": [
|
||||
{
|
||||
"label": "A: Two tests in processPayment suite (recommended)",
|
||||
"description": "As planned: reuse factory, Stripe mock, virtual sleeper; inline assertions. Completeness 10/10."
|
||||
},
|
||||
{
|
||||
"label": "B: Two tests + factory helpers",
|
||||
"description": "Same tests plus expectReceipt/expectRetryTrace helpers in the factory. Completeness 10/10, wider diff."
|
||||
},
|
||||
{
|
||||
"label": "C: Put them in the adapter suite",
|
||||
"description": "Cover via Stripe adapter suite instead. Completeness 4/10; misses caller path."
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"answered": true,
|
||||
"failed": false,
|
||||
"answers": {
|
||||
"D3 — Where and how should the two processPayment tests be structured?\nProject/branch/task: gstack-plan-count-knpyoQ on main, CEO review of PLAN.md (payment test coverage).\nELI10: The plan adds two tests: one for a successful charge, one for two 502s exhausting the single retry. The structural choice is where they live and whether to add shared assertion helpers. This question is about shape only; what each test asserts is decided separately in the findings that follow.\nStakes if we pick wrong: put the tests in the adapter suite and they never exercise processPayment's own receipt-shaping and retry-exhaustion path; add helpers for two call sites and you get an abstraction nobody else uses yet.\nRecommendation: A because two focused tests in the caller's own suite is the smallest diff that fully proves both contracts, and helpers for two call sites is premature abstraction.\nCompleteness: A=10/10, B=10/10, C=4/10\nA) Two focused tests in the existing processPayment suite, using the current factory, Stripe mock and virtual sleeper as planned (recommended) (human: ~1h / CC: ~5 min)\n ✅ Exercises processPayment's real caller path: receipt shaping and retry exhaustion\n ✅ Zero new files, zero new helpers; reuses every fixture the plan already names\n ❌ Assertions are spelled out inline per test rather than through a shared helper\nB) Same two tests plus small shared helpers (expectReceipt, expectRetryTrace) in the payment test factory (human: ~2h / CC: ~10 min)\n ✅ Future processPayment tests get exact receipt and retry assertions for free\n ✅ One place to update if the sleeper record or receipt shape ever changes\n ❌ Two call sites do not justify a helper; it is premature abstraction and widens the diff into the factory\nC) Add the assertions to the existing Stripe adapter suite instead of the processPayment suite (human: ~1h / CC: ~5 min)\n ✅ Keeps all Stripe-related tests in one suite\n ✅ Adapter suite already has 502-recovery scaffolding to copy from\n ❌ Adapter coverage does not prove processPayment's own receipt or PaymentUnavailable path; the stated contracts are the caller's\nNet: A and B cover the same contracts; A wins on diff size and avoids abstraction for two uses. C misses the caller path entirely.": "A: Two tests in processPayment suite (recommended)"
|
||||
},
|
||||
"unansweredQuestionIndices": [],
|
||||
"answeredAt": "2026-09-10T05:43:53.548Z"
|
||||
},
|
||||
{
|
||||
"sessionId": "a9f1e8d0-71c2-4c5d-8eb8-e2ae6883b58d",
|
||||
"toolUseId": "toolu_01NVdw9AQyubkBhpzuZTvCPg",
|
||||
"questions": [
|
||||
{
|
||||
"question": "D4 — Finding F1 (Section 6, Test Review): the success test asserts only that the receipt is truthy. What should it assert?\nProject/branch/task: gstack-plan-count-knpyoQ on main, CEO review of PLAN.md (payment test coverage).\nELI10: The plan's own contract says a 1000-cent USD charge with Stripe id ch_paid must return exactly { chargeId: \"ch_paid\", amountCents: 1000, currency: \"USD\" }. Test 1 as written only checks that the receipt is not null/undefined/false. An empty object {} is truthy. A receipt with amountCents 100000 (cents/dollars mix-up), currency \"usd\", or chargeId undefined would all pass. The test cannot fail for any of the bugs it exists to catch.\nStakes if we pick wrong: a refactor of receipt building ships with a wrong amount or missing chargeId and the suite stays green; customers see wrong receipts and refunds cannot be matched to Stripe charges.\nRecommendation: 1A because the plan already states the exact receipt; the assertion is a direct translation of the retained contract, and deep equality also rejects extra or missing fields.\nCompleteness: 1A=10/10, 1B=7/10, 1C=2/10\n1A) Assert the receipt deep-equals { chargeId: \"ch_paid\", amountCents: 1000, currency: \"USD\" } (recommended) (human: ~15 min / CC: ~1 min)\n ✅ Rejects wrong amount, wrong currency case, missing chargeId, and any extra or missing field in one assertion\n ✅ Failure output is a field-level diff, so a 3-week-later regression is diagnosable from the test log alone\n ❌ If the receipt legitimately gains a new field later, this test must be updated (that is the point)\n1B) Assert only receipt.chargeId === \"ch_paid\" (human: ~10 min / CC: ~1 min)\n ✅ Proves the Stripe id flows through to the receipt\n ✅ Tolerates receipt shape changes without edits\n ❌ Leaves amountCents and currency unverified; the cents/dollars bug the contract exists to prevent slips through\n1C) Keep the truthy-only assertion as planned (human: ~5 min / CC: ~1 min)\n ✅ Matches the plan text verbatim\n ✅ Never needs updating\n ❌ Passes for {} and for every wrong-value receipt; the test protects nothing the contract states\nNet: 1A is the stated contract written as code; anything less is a test that cannot fail for the bugs it targets.",
|
||||
"header": "F1 receipt",
|
||||
"multiSelect": false,
|
||||
"options": [
|
||||
{
|
||||
"label": "1A: Deep-equal full receipt (recommended)",
|
||||
"description": "toEqual({ chargeId: 'ch_paid', amountCents: 1000, currency: 'USD' }). Completeness 10/10."
|
||||
},
|
||||
{
|
||||
"label": "1B: Assert chargeId only",
|
||||
"description": "Check chargeId === 'ch_paid'; amount and currency unverified. Completeness 7/10."
|
||||
},
|
||||
{
|
||||
"label": "1C: Keep truthy-only",
|
||||
"description": "As planned; passes for {} and any wrong values. Completeness 2/10."
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"answered": true,
|
||||
"failed": false,
|
||||
"answers": {
|
||||
"D4 — Finding F1 (Section 6, Test Review): the success test asserts only that the receipt is truthy. What should it assert?\nProject/branch/task: gstack-plan-count-knpyoQ on main, CEO review of PLAN.md (payment test coverage).\nELI10: The plan's own contract says a 1000-cent USD charge with Stripe id ch_paid must return exactly { chargeId: \"ch_paid\", amountCents: 1000, currency: \"USD\" }. Test 1 as written only checks that the receipt is not null/undefined/false. An empty object {} is truthy. A receipt with amountCents 100000 (cents/dollars mix-up), currency \"usd\", or chargeId undefined would all pass. The test cannot fail for any of the bugs it exists to catch.\nStakes if we pick wrong: a refactor of receipt building ships with a wrong amount or missing chargeId and the suite stays green; customers see wrong receipts and refunds cannot be matched to Stripe charges.\nRecommendation: 1A because the plan already states the exact receipt; the assertion is a direct translation of the retained contract, and deep equality also rejects extra or missing fields.\nCompleteness: 1A=10/10, 1B=7/10, 1C=2/10\n1A) Assert the receipt deep-equals { chargeId: \"ch_paid\", amountCents: 1000, currency: \"USD\" } (recommended) (human: ~15 min / CC: ~1 min)\n ✅ Rejects wrong amount, wrong currency case, missing chargeId, and any extra or missing field in one assertion\n ✅ Failure output is a field-level diff, so a 3-week-later regression is diagnosable from the test log alone\n ❌ If the receipt legitimately gains a new field later, this test must be updated (that is the point)\n1B) Assert only receipt.chargeId === \"ch_paid\" (human: ~10 min / CC: ~1 min)\n ✅ Proves the Stripe id flows through to the receipt\n ✅ Tolerates receipt shape changes without edits\n ❌ Leaves amountCents and currency unverified; the cents/dollars bug the contract exists to prevent slips through\n1C) Keep the truthy-only assertion as planned (human: ~5 min / CC: ~1 min)\n ✅ Matches the plan text verbatim\n ✅ Never needs updating\n ❌ Passes for {} and for every wrong-value receipt; the test protects nothing the contract states\nNet: 1A is the stated contract written as code; anything less is a test that cannot fail for the bugs it targets.": "1A: Deep-equal full receipt (recommended)"
|
||||
},
|
||||
"unansweredQuestionIndices": [],
|
||||
"answeredAt": "2026-09-10T05:44:41.784Z"
|
||||
},
|
||||
{
|
||||
"sessionId": "a9f1e8d0-71c2-4c5d-8eb8-e2ae6883b58d",
|
||||
"toolUseId": "toolu_01PFhyS7ji9nKUsiHERMy8Jx",
|
||||
"questions": [
|
||||
{
|
||||
"question": "D5 — Finding F2 (Sections 2 and 6): the repeated-502 test asserts only that processPayment rejects with PaymentUnavailable. Should it also prove the retry contract?\nProject/branch/task: gstack-plan-count-knpyoQ on main, CEO review of PLAN.md (payment test coverage).\nELI10: The plan's contract says two 502s with max_retries=1 mean exactly two charge attempts, one recorded 100 ms backoff, then PaymentUnavailable. The test as written checks only the final rejection. An implementation that never retries (one attempt, no backoff) passes. One that retries five times with no backoff also passes, as long as it eventually throws PaymentUnavailable. The factory already exposes the Stripe mock call history and the sleeper's recorded backoffs, so the evidence is free; the plan just says it will not look at it.\nStakes if we pick wrong: a retry-loop regression (zero retries, or an unbounded loop that hammers Stripe during an outage) ships green; users get instant failures on transient blips, or Stripe rate-limits the account during an incident.\nRecommendation: 2A because the plan states exact counts (two attempts, one 100 ms backoff) and Section 6 rules forbid weakening an exact count to a lower bound; the mock history and sleeper record are already exposed for exactly this purpose.\nCompleteness: 2A=10/10, 2B=7/10, 2C=3/10\n2A) Assert rejection is an instance of PaymentUnavailable AND Stripe mock call history has exactly 2 charge calls AND the sleeper record deep-equals one 100 ms backoff; build a fresh factory inside the test so both records start empty (recommended) (human: ~30 min / CC: ~2 min)\n ✅ Rejects zero-retry, over-retry, and wrong-backoff regressions; the whole stated contract becomes executable\n ✅ Fresh factory per test prevents call-history bleed from earlier tests, so the exact count cannot flake; failure prints the recorded arrays\n ❌ Three assertions instead of one; must match the sleeper's actual record shape (number vs object) when implementing\n2B) Assert PaymentUnavailable AND exactly 2 charge calls, but skip the backoff assertion (human: ~20 min / CC: ~1 min)\n ✅ Catches zero-retry and over-retry regressions\n ✅ Does not depend on the sleeper record's shape\n ❌ A retry loop that drops the backoff and hammers Stripe immediately still passes; the 100 ms contract goes unverified\n2C) Keep rejection-only as planned (human: ~10 min / CC: ~1 min)\n ✅ Matches the plan text verbatim\n ✅ Smallest possible test body\n ❌ Passes with zero retries, five retries, or no backoff; the retry contract the plan states is not tested at all\nNet: 2A turns the plan's own sentence about two attempts and one 100 ms backoff into assertions; 2B and 2C leave part or all of it as prose.",
|
||||
"header": "F2 retries",
|
||||
"multiSelect": false,
|
||||
"options": [
|
||||
{
|
||||
"label": "2A: Rejection + 2 calls + [100ms] backoff (recommended)",
|
||||
"description": "Full contract: instanceof PaymentUnavailable, mock calls === 2, sleeper record equals one 100 ms entry, fresh factory per test. Completeness 10/10."
|
||||
},
|
||||
{
|
||||
"label": "2B: Rejection + 2 calls only",
|
||||
"description": "Skip the backoff assertion. Completeness 7/10."
|
||||
},
|
||||
{
|
||||
"label": "2C: Keep rejection-only",
|
||||
"description": "As planned; retry count and backoff unverified. Completeness 3/10."
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"answered": true,
|
||||
"failed": false,
|
||||
"answers": {
|
||||
"D5 — Finding F2 (Sections 2 and 6): the repeated-502 test asserts only that processPayment rejects with PaymentUnavailable. Should it also prove the retry contract?\nProject/branch/task: gstack-plan-count-knpyoQ on main, CEO review of PLAN.md (payment test coverage).\nELI10: The plan's contract says two 502s with max_retries=1 mean exactly two charge attempts, one recorded 100 ms backoff, then PaymentUnavailable. The test as written checks only the final rejection. An implementation that never retries (one attempt, no backoff) passes. One that retries five times with no backoff also passes, as long as it eventually throws PaymentUnavailable. The factory already exposes the Stripe mock call history and the sleeper's recorded backoffs, so the evidence is free; the plan just says it will not look at it.\nStakes if we pick wrong: a retry-loop regression (zero retries, or an unbounded loop that hammers Stripe during an outage) ships green; users get instant failures on transient blips, or Stripe rate-limits the account during an incident.\nRecommendation: 2A because the plan states exact counts (two attempts, one 100 ms backoff) and Section 6 rules forbid weakening an exact count to a lower bound; the mock history and sleeper record are already exposed for exactly this purpose.\nCompleteness: 2A=10/10, 2B=7/10, 2C=3/10\n2A) Assert rejection is an instance of PaymentUnavailable AND Stripe mock call history has exactly 2 charge calls AND the sleeper record deep-equals one 100 ms backoff; build a fresh factory inside the test so both records start empty (recommended) (human: ~30 min / CC: ~2 min)\n ✅ Rejects zero-retry, over-retry, and wrong-backoff regressions; the whole stated contract becomes executable\n ✅ Fresh factory per test prevents call-history bleed from earlier tests, so the exact count cannot flake; failure prints the recorded arrays\n ❌ Three assertions instead of one; must match the sleeper's actual record shape (number vs object) when implementing\n2B) Assert PaymentUnavailable AND exactly 2 charge calls, but skip the backoff assertion (human: ~20 min / CC: ~1 min)\n ✅ Catches zero-retry and over-retry regressions\n ✅ Does not depend on the sleeper record's shape\n ❌ A retry loop that drops the backoff and hammers Stripe immediately still passes; the 100 ms contract goes unverified\n2C) Keep rejection-only as planned (human: ~10 min / CC: ~1 min)\n ✅ Matches the plan text verbatim\n ✅ Smallest possible test body\n ❌ Passes with zero retries, five retries, or no backoff; the retry contract the plan states is not tested at all\nNet: 2A turns the plan's own sentence about two attempts and one 100 ms backoff into assertions; 2B and 2C leave part or all of it as prose.": "2A: Rejection + 2 calls + [100ms] backoff (recommended)"
|
||||
},
|
||||
"unansweredQuestionIndices": [],
|
||||
"answeredAt": "2026-09-10T05:45:03.909Z"
|
||||
}
|
||||
]
|
||||
},
|
||||
"distinct": {
|
||||
"capture": {
|
||||
"skill": "plan-ceo-review",
|
||||
"runId": "ship-source-aj-delta-paid-20260910-v1-4",
|
||||
"cwd": "/tmp/gstack-paid-shard-rGMMii/tmp/gstack-plan-count-XMrFHU",
|
||||
"claudeConfigDir": "/tmp/gstack-paid-shard-rGMMii/tmp/gstack-hermetic-1054183-PR2vOl/with-skills/.claude",
|
||||
"at": "2026-09-10T05:54:50.782Z"
|
||||
},
|
||||
"calls": [
|
||||
{
|
||||
"sessionId": "c28b8f0e-b2ad-4c33-9441-a93b68c440f2",
|
||||
"toolUseId": "toolu_019mAdWcT7psVdpKNQ3kGAfZ",
|
||||
"questions": [
|
||||
{
|
||||
"question": "D1 — Add gstack skill routing rules to this project's CLAUDE.md?\nProject/branch/task: gstack-plan-count-XMrFHU on main, reviewing PLAN.md (Payment Processing Integration).\nELI10: gstack works best when your project's CLAUDE.md includes skill routing rules. These tell the agent which slash skill to invoke when you say things like 'review this' or 'ship it', so you get the right workflow without naming it. This is a one-time setup prompt per project.\nStakes if we pick wrong: Nothing breaks either way. Without routing you invoke skills by name manually.\nRecommendation: A because routing makes skill invocation automatic and costs one small CLAUDE.md append. Note: plan mode is active, so the append and commit happen after this review exits plan mode.\nNote: options differ in kind, not coverage — no completeness score.\nNet: convenience now versus keeping CLAUDE.md untouched.",
|
||||
"header": "Routing",
|
||||
"multiSelect": false,
|
||||
"options": [
|
||||
{
|
||||
"label": "A) Add routing rules (recommended)",
|
||||
"description": "✅ Future requests like 'review this plan' auto-route to the right gstack skill without naming it. ✅ One-time small append to CLAUDE.md, committed as a chore. ❌ Adds a section to CLAUDE.md that this test fixture repo may not want; edit deferred until plan mode exits."
|
||||
},
|
||||
{
|
||||
"label": "B) No thanks, invoke manually",
|
||||
"description": "✅ CLAUDE.md stays exactly as seeded, no extra commit in the fixture repo. ✅ Config records the decline so this prompt never reappears here. ❌ You must type skill names explicitly every time."
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"answered": true,
|
||||
"failed": false,
|
||||
"answers": {
|
||||
"D1 — Add gstack skill routing rules to this project's CLAUDE.md?\nProject/branch/task: gstack-plan-count-XMrFHU on main, reviewing PLAN.md (Payment Processing Integration).\nELI10: gstack works best when your project's CLAUDE.md includes skill routing rules. These tell the agent which slash skill to invoke when you say things like 'review this' or 'ship it', so you get the right workflow without naming it. This is a one-time setup prompt per project.\nStakes if we pick wrong: Nothing breaks either way. Without routing you invoke skills by name manually.\nRecommendation: A because routing makes skill invocation automatic and costs one small CLAUDE.md append. Note: plan mode is active, so the append and commit happen after this review exits plan mode.\nNote: options differ in kind, not coverage — no completeness score.\nNet: convenience now versus keeping CLAUDE.md untouched.": "A) Add routing rules (recommended)"
|
||||
},
|
||||
"unansweredQuestionIndices": [],
|
||||
"answeredAt": "2026-09-10T05:41:43.499Z"
|
||||
},
|
||||
{
|
||||
"sessionId": "c28b8f0e-b2ad-4c33-9441-a93b68c440f2",
|
||||
"toolUseId": "toolu_013oTguWa66iMTSRDUFKxGFq",
|
||||
"questions": [
|
||||
{
|
||||
"question": "D2 — Enable cross-project learnings search?\nProject/branch/task: gstack-plan-count-XMrFHU on main, reviewing PLAN.md (Payment Processing Integration).\nELI10: gstack can search learnings from your other projects on this machine to find patterns that might apply here. This stays local; no data leaves your machine. It is recommended for solo developers. Skip if you work on multiple client codebases where cross-contamination would be a concern.\nStakes if we pick wrong: Too broad and a client's project quirks could leak into another client's review; too narrow and you lose reusable pitfalls you already paid to learn.\nRecommendation: A because a single-machine solo setup gains reusable pitfalls with no privacy cost, and the setting is a two-way door you can flip later with gstack-config.\nNote: options differ in kind, not coverage — no completeness score.\nNet: reuse of past learnings versus strict per-project isolation.",
|
||||
"header": "Learnings",
|
||||
"multiSelect": false,
|
||||
"options": [
|
||||
{
|
||||
"label": "A) Enable cross-project (recommended)",
|
||||
"description": "✅ Pitfalls and patterns you logged in other repos surface here, so the review gets sharper over time. ✅ Everything stays on this machine; nothing is uploaded anywhere. ❌ If you juggle multiple client codebases, one client's quirks can bleed into another's review."
|
||||
},
|
||||
{
|
||||
"label": "B) Keep project-scoped only",
|
||||
"description": "✅ Hard isolation between projects, safe for multi-client consulting setups. ✅ Still logs and searches learnings for this repo alone. ❌ Loses reusable insights you already earned elsewhere, so each project relearns the same pitfalls."
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"answered": true,
|
||||
"failed": false,
|
||||
"answers": {
|
||||
"D2 — Enable cross-project learnings search?\nProject/branch/task: gstack-plan-count-XMrFHU on main, reviewing PLAN.md (Payment Processing Integration).\nELI10: gstack can search learnings from your other projects on this machine to find patterns that might apply here. This stays local; no data leaves your machine. It is recommended for solo developers. Skip if you work on multiple client codebases where cross-contamination would be a concern.\nStakes if we pick wrong: Too broad and a client's project quirks could leak into another client's review; too narrow and you lose reusable pitfalls you already paid to learn.\nRecommendation: A because a single-machine solo setup gains reusable pitfalls with no privacy cost, and the setting is a two-way door you can flip later with gstack-config.\nNote: options differ in kind, not coverage — no completeness score.\nNet: reuse of past learnings versus strict per-project isolation.": "A) Enable cross-project (recommended)"
|
||||
},
|
||||
"unansweredQuestionIndices": [],
|
||||
"answeredAt": "2026-09-10T05:42:01.568Z"
|
||||
},
|
||||
{
|
||||
"sessionId": "c28b8f0e-b2ad-4c33-9441-a93b68c440f2",
|
||||
"toolUseId": "toolu_012MEvW2WrMb8DqbdxTT9xbx",
|
||||
"questions": [
|
||||
{
|
||||
"question": "D3 — Which implementation structure should the handler use?\nProject/branch/task: gstack-plan-count-XMrFHU on main, reviewing PLAN.md (Stripe payment webhook handler).\nELI10: The plan wants a brand-new handler class that skips the existing WebhookDispatcher for 'clean namespace separation'. But the plan also promises the handler runs inside the existing guards and uses the existing feature flag and rollback. Those two promises are easiest to keep if the handler is registered through the dispatcher like the prior handler was. A third path moves the email into a background job, which is the industry default but changes the plan's stated 'both happen inline' contract.\nStakes if we pick wrong: A bypassing handler can silently drift out from under the dedup lock or the feature flag, so a bad deploy has no tested rollback and duplicates slip through. Going async changes the deletion-ordering guarantee the plan relies on.\nRecommendation: B because it makes 'runs inside unchanged guards' and 'uses the existing rollout path' true by construction and keeps one webhook routing path (DRY, explicit over clever).\nCompleteness: A=5/10, B=9/10, C=8/10\nNet: namespace purity (A) versus guard and rollback reuse by construction (B) versus resilience that rewrites a stated contract (C).",
|
||||
"header": "Approach",
|
||||
"multiSelect": false,
|
||||
"options": [
|
||||
{
|
||||
"label": "B) Register via WebhookDispatcher (recommended)",
|
||||
"description": "Completeness 9/10. human ~1 day / CC ~25 min. ✅ Existing feature flag, tested rollback, guard order and logging apply to the new class unchanged. ✅ The existing integration suite exercises the new code once the flag is on, instead of testing only the prior handler. ❌ Must conform to the dispatcher's handler interface; namespace separation comes from file layout, not a parallel entry point."
|
||||
},
|
||||
{
|
||||
"label": "A) Standalone class, bypass dispatcher",
|
||||
"description": "Completeness 5/10. human ~1 day / CC ~20 min. ✅ Smallest possible diff and exactly what the plan sketch says. ✅ Dispatcher module stays untouched, zero risk of regressing other event types. ❌ Second routing path; the flag/rollback and guard wiring must be re-proven for the bypass, and the existing suite never touches the new class."
|
||||
},
|
||||
{
|
||||
"label": "C) Dispatcher + background email job",
|
||||
"description": "Completeness 8/10. human ~2-3 days / CC ~45 min. ✅ Email latency leaves the Stripe 10-second window and the per-user lock. ✅ Email retries become independent of payment retries, matching the 'receive fast, process safe' convention. ❌ Changes the stated inline contract and the deletion-ordering guarantee; adds a queue failure surface. In HOLD SCOPE this is a behavior change, not a repair."
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"answered": true,
|
||||
"failed": false,
|
||||
"answers": {
|
||||
"D3 — Which implementation structure should the handler use?\nProject/branch/task: gstack-plan-count-XMrFHU on main, reviewing PLAN.md (Stripe payment webhook handler).\nELI10: The plan wants a brand-new handler class that skips the existing WebhookDispatcher for 'clean namespace separation'. But the plan also promises the handler runs inside the existing guards and uses the existing feature flag and rollback. Those two promises are easiest to keep if the handler is registered through the dispatcher like the prior handler was. A third path moves the email into a background job, which is the industry default but changes the plan's stated 'both happen inline' contract.\nStakes if we pick wrong: A bypassing handler can silently drift out from under the dedup lock or the feature flag, so a bad deploy has no tested rollback and duplicates slip through. Going async changes the deletion-ordering guarantee the plan relies on.\nRecommendation: B because it makes 'runs inside unchanged guards' and 'uses the existing rollout path' true by construction and keeps one webhook routing path (DRY, explicit over clever).\nCompleteness: A=5/10, B=9/10, C=8/10\nNet: namespace purity (A) versus guard and rollback reuse by construction (B) versus resilience that rewrites a stated contract (C).": "B) Register via WebhookDispatcher (recommended)"
|
||||
},
|
||||
"unansweredQuestionIndices": [],
|
||||
"answeredAt": "2026-09-10T05:44:45.846Z"
|
||||
},
|
||||
{
|
||||
"sessionId": "c28b8f0e-b2ad-4c33-9441-a93b68c440f2",
|
||||
"toolUseId": "toolu_01M7iPAc7vziAUL7HRXKV9o6",
|
||||
"questions": [
|
||||
{
|
||||
"question": "D4 — What is the per-order fetch loop for, and how should the handler load orders?\nProject/branch/task: gstack-plan-count-XMrFHU on main, reviewing PLAN.md (Stripe payment webhook handler).\nELI10: The plan says each webhook looks up the user and then 'fetches each order in a loop', but the described behavior only marks the user paid and sends one email. Nothing in the plan consumes the orders. If the email needs order details, the fetch belongs in one batched query. If nothing needs them, the loop is dead work running inside the per-user lock and inside Stripe's 10-second response window. The implementer will hit this ambiguity in hour 4 and guess; better to decide now.\nStakes if we pick wrong: A user with 200 orders turns one webhook into 201 queries under a lock, Stripe times out at 10s, retries, and the retries queue on the same lock. Dropping the fetch when the email needs it ships a receipt with no line items.\nRecommendation: A because it preserves the plan's stated behavior (orders are fetched) while removing the N+1, and it maps to the 'handle more edge cases, engineered enough' preference.\nCompleteness: A=9/10, B=7/10, C=3/10\nNet: keep orders but load them once (A) versus remove an unexplained fetch (B) versus ship the loop as sketched (C).",
|
||||
"header": "Orders loop",
|
||||
"multiSelect": false,
|
||||
"options": [
|
||||
{
|
||||
"label": "A) Batch-load orders in one query (recommended)",
|
||||
"description": "Completeness 9/10. human ~1h / CC ~5 min. ✅ One query with the user's ID (or an IN list) replaces N round trips; lock hold time and Stripe latency stay flat as order count grows. ✅ Whatever consumes the orders (email body, audit) still gets them; a test asserts query count is constant. ❌ Requires stating in the plan what the orders feed, so the batch shape is right."
|
||||
},
|
||||
{
|
||||
"label": "B) Remove the orders fetch entirely",
|
||||
"description": "Completeness 7/10. human ~30 min / CC ~3 min. ✅ Smallest handler: lookup, update, email, nothing else under the lock. ✅ Removes a whole failure surface and its tests. ❌ If the email or a downstream consumer actually needs order data, this silently ships an incomplete notification."
|
||||
},
|
||||
{
|
||||
"label": "C) Keep the per-order loop as written",
|
||||
"description": "Completeness 3/10. human 0 / CC 0. ✅ No change from the sketch, zero extra implementation thought. ✅ Works fine for users with a handful of orders. ❌ N+1 under a per-user lock inside a 10s webhook timeout; heavy users trigger timeout-retry storms that the dedup guard then serializes."
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"answered": true,
|
||||
"failed": false,
|
||||
"answers": {
|
||||
"D4 — What is the per-order fetch loop for, and how should the handler load orders?\nProject/branch/task: gstack-plan-count-XMrFHU on main, reviewing PLAN.md (Stripe payment webhook handler).\nELI10: The plan says each webhook looks up the user and then 'fetches each order in a loop', but the described behavior only marks the user paid and sends one email. Nothing in the plan consumes the orders. If the email needs order details, the fetch belongs in one batched query. If nothing needs them, the loop is dead work running inside the per-user lock and inside Stripe's 10-second response window. The implementer will hit this ambiguity in hour 4 and guess; better to decide now.\nStakes if we pick wrong: A user with 200 orders turns one webhook into 201 queries under a lock, Stripe times out at 10s, retries, and the retries queue on the same lock. Dropping the fetch when the email needs it ships a receipt with no line items.\nRecommendation: A because it preserves the plan's stated behavior (orders are fetched) while removing the N+1, and it maps to the 'handle more edge cases, engineered enough' preference.\nCompleteness: A=9/10, B=7/10, C=3/10\nNet: keep orders but load them once (A) versus remove an unexplained fetch (B) versus ship the loop as sketched (C).": "A) Batch-load orders in one query (recommended)"
|
||||
},
|
||||
"unansweredQuestionIndices": [],
|
||||
"answeredAt": "2026-09-10T05:45:51.708Z"
|
||||
},
|
||||
{
|
||||
"sessionId": "c28b8f0e-b2ad-4c33-9441-a93b68c440f2",
|
||||
"toolUseId": "toolu_01KepjnLm3wYHtNTqMPzLjym",
|
||||
"questions": [
|
||||
{
|
||||
"question": "D5 (Issue 1.1) — Should the inline email call get an explicit timeout budget inside the webhook?\nProject/branch/task: gstack-plan-count-XMrFHU on main, reviewing PLAN.md (Stripe payment webhook handler), Section 1 Architecture.\nELI10: Stripe waits 10 seconds for your webhook to answer, then treats it as failed and retries. The plan sends the email inline while holding the per-user lock, with no cap on how long the mail provider may take. A slow mail provider makes every webhook time out at Stripe, the retries pile up behind the lock, and the 'failed webhook processing' alert fires for events that actually succeeded. A fixed per-call mail timeout keeps the handler under 10s and turns slowness into a named, traced exception.\nStakes if we pick wrong: Without a budget, a mail-provider slowdown becomes a webhook outage plus alert noise; with too aggressive a budget, healthy sends get cut off and retried.\nRecommendation: 1A because explicit over clever: name the failure (mail timeout), bound it, test it. It preserves the plan's inline contract and lets the existing 500-and-retry path do the rest.\nCompleteness: A=9/10, B=6/10, C=2/10\nNet: bound the mail call and test the bound (A) versus rely on whatever default the mail client has (B) versus accept unbounded latency (C).",
|
||||
"header": "Mail budget",
|
||||
"multiSelect": false,
|
||||
"options": [
|
||||
{
|
||||
"label": "1A) Explicit mail timeout, ~4s, tested (recommended)",
|
||||
"description": "Completeness 9/10. human ~1h / CC ~5 min. ✅ Handler worst case stays under Stripe's 10s window; slowness surfaces as the mail client's named timeout exception, traced with event and user IDs. ✅ Test stubs a hanging mail client and asserts the timeout exception is raised inside the budget. ❌ A send that would have finished at 5s now fails and is retried by Stripe; the notification marker from Section 2 keeps that from duplicating."
|
||||
},
|
||||
{
|
||||
"label": "1B) Rely on the shared mail client's default timeout",
|
||||
"description": "Completeness 6/10. human 0 / CC 0. ✅ No handler-specific configuration to maintain. ✅ Works if the shared default already happens to be short. ❌ The plan does not state that default; if it is 30s or unset, the webhook silently exceeds Stripe's window and nobody notices until the retry storm."
|
||||
},
|
||||
{
|
||||
"label": "1C) No timeout budget, accept the risk",
|
||||
"description": "Completeness 2/10. human 0 / CC 0. ✅ Zero implementation work. ✅ Fine while the mail provider is fast. ❌ Mail-provider p99 becomes webhook p99 with no bound; a provider slowdown turns into Stripe retry storms serialized on the per-user lock and repeated false 'failed processing' alerts."
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"answered": true,
|
||||
"failed": false,
|
||||
"answers": {
|
||||
"D5 (Issue 1.1) — Should the inline email call get an explicit timeout budget inside the webhook?\nProject/branch/task: gstack-plan-count-XMrFHU on main, reviewing PLAN.md (Stripe payment webhook handler), Section 1 Architecture.\nELI10: Stripe waits 10 seconds for your webhook to answer, then treats it as failed and retries. The plan sends the email inline while holding the per-user lock, with no cap on how long the mail provider may take. A slow mail provider makes every webhook time out at Stripe, the retries pile up behind the lock, and the 'failed webhook processing' alert fires for events that actually succeeded. A fixed per-call mail timeout keeps the handler under 10s and turns slowness into a named, traced exception.\nStakes if we pick wrong: Without a budget, a mail-provider slowdown becomes a webhook outage plus alert noise; with too aggressive a budget, healthy sends get cut off and retried.\nRecommendation: 1A because explicit over clever: name the failure (mail timeout), bound it, test it. It preserves the plan's inline contract and lets the existing 500-and-retry path do the rest.\nCompleteness: A=9/10, B=6/10, C=2/10\nNet: bound the mail call and test the bound (A) versus rely on whatever default the mail client has (B) versus accept unbounded latency (C).": "1A) Explicit mail timeout, ~4s, tested (recommended)"
|
||||
},
|
||||
"unansweredQuestionIndices": [],
|
||||
"answeredAt": "2026-09-10T05:47:17.691Z"
|
||||
},
|
||||
{
|
||||
"sessionId": "c28b8f0e-b2ad-4c33-9441-a93b68c440f2",
|
||||
"toolUseId": "toolu_01NRSM7BJG9DzYZrHSAA5uPB",
|
||||
"questions": [
|
||||
{
|
||||
"question": "D6 (Issue 2.1) — How should the email leg behave when Stripe retries after the user update already committed?\nProject/branch/task: gstack-plan-count-XMrFHU on main, reviewing PLAN.md (Stripe payment webhook handler), Section 2 Error map.\nELI10: The handler commits 'paid' to the database, then sends the email. If the email fails, the whole request returns 500 and Stripe retries the event for up to three days. The dedup guard only records 'done' when the handler finishes cleanly, so every retry reruns the email. A mail timeout where the provider actually delivered means the customer gets the email twice. A permanently bad address means three days of alerts for a payment that is already fine. Recording 'email sent' per Stripe event, under the same lock, lets retries skip the email once it has gone out.\nStakes if we pick wrong: Duplicate payment emails on every retry storm erode customer trust, and permanent mail rejections page on-call for three days about a committed payment.\nRecommendation: 2A because zero silent failures and every error has a name: the marker makes the email idempotent per event, keeps the inline contract, and keeps permanent rejections loud through the existing alert.\nCompleteness: A=9/10, B=5/10, C=2/10\nNet: idempotent email via a per-event marker (A) versus rescue-and-log the email so the webhook succeeds (B) versus ship 'no error handling' as written (C).",
|
||||
"header": "Email retry",
|
||||
"multiSelect": false,
|
||||
"options": [
|
||||
{
|
||||
"label": "2A) Per-event notification marker under the lock (recommended)",
|
||||
"description": "Completeness 9/10. human ~3h / CC ~15 min. ✅ Rerun after a transient mail failure sends exactly once more then records completion; rerun after 'sent' sends zero emails; Stripe event ID doubles as the mail idempotency key where the provider supports it. ✅ Permanent rejections still propagate to the existing alert and runbook; the residual ambiguous-timeout case is logged with notification_retry=true. ❌ Adds one small table or column plus three tests."
|
||||
},
|
||||
{
|
||||
"label": "2B) Rescue MailDeliveryError, log, return 200",
|
||||
"description": "Completeness 5/10. human ~1h / CC ~5 min. ✅ Webhook succeeds once the payment commits; no Stripe retry storm from mail failures. ✅ Mail failure still visible on the shared mail dashboard and on-call alert. ❌ The email is never retried automatically; it relies on on-call running the manual notification retry procedure, and the plan's own contract says tracing must not rescue email errors."
|
||||
},
|
||||
{
|
||||
"label": "2C) Keep 'no error handling on the email leg' as written",
|
||||
"description": "Completeness 2/10. human 0 / CC 0. ✅ Zero implementation work; matches the sketch exactly. ✅ Transient mail failures do get retried by Stripe. ❌ Every retry re-sends the email; timeouts after delivery duplicate it; a bad address alerts for three days about a committed payment; the runbook's 'never replay the payment blindly' is violated by design."
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"answered": true,
|
||||
"failed": false,
|
||||
"answers": {
|
||||
"D6 (Issue 2.1) — How should the email leg behave when Stripe retries after the user update already committed?\nProject/branch/task: gstack-plan-count-XMrFHU on main, reviewing PLAN.md (Stripe payment webhook handler), Section 2 Error map.\nELI10: The handler commits 'paid' to the database, then sends the email. If the email fails, the whole request returns 500 and Stripe retries the event for up to three days. The dedup guard only records 'done' when the handler finishes cleanly, so every retry reruns the email. A mail timeout where the provider actually delivered means the customer gets the email twice. A permanently bad address means three days of alerts for a payment that is already fine. Recording 'email sent' per Stripe event, under the same lock, lets retries skip the email once it has gone out.\nStakes if we pick wrong: Duplicate payment emails on every retry storm erode customer trust, and permanent mail rejections page on-call for three days about a committed payment.\nRecommendation: 2A because zero silent failures and every error has a name: the marker makes the email idempotent per event, keeps the inline contract, and keeps permanent rejections loud through the existing alert.\nCompleteness: A=9/10, B=5/10, C=2/10\nNet: idempotent email via a per-event marker (A) versus rescue-and-log the email so the webhook succeeds (B) versus ship 'no error handling' as written (C).": "2A) Per-event notification marker under the lock (recommended)"
|
||||
},
|
||||
"unansweredQuestionIndices": [],
|
||||
"answeredAt": "2026-09-10T05:48:13.537Z"
|
||||
},
|
||||
{
|
||||
"sessionId": "c28b8f0e-b2ad-4c33-9441-a93b68c440f2",
|
||||
"toolUseId": "toolu_01JLWZnbpnVmmtT58UjwbFQY",
|
||||
"questions": [
|
||||
{
|
||||
"question": "D7 (Issue 3.1) — How should the handler build the user lookup query?\nProject/branch/task: gstack-plan-count-XMrFHU on main, reviewing PLAN.md (Stripe payment webhook handler), Section 3 Security.\nELI10: The plan pastes the user ID string straight into SQL. The plan's own contracts say user IDs are opaque text that can contain punctuation and Unicode, and that a valid Stripe signature does not make the string safe for SQL. So a perfectly legitimate ID with an apostrophe breaks the query, the webhook returns 500 forever, and that customer is never marked paid. If users can influence their own ID, they can also make their own payment run arbitrary SQL under the app's database role. A bound parameter (the existing user finder) makes both problems disappear.\nStakes if we pick wrong: Silent non-payment for any user with an odd ID, plus a self-service SQL injection path that signature checks and ownership checks both wave through.\nRecommendation: 3A because security is not optional and the fix is smaller than the bug: reuse the existing finder with a bound parameter and prove it with an adversarial-ID test.\nCompleteness: A=10/10, B=6/10, C=1/10\nNet: parameterize and test (A) versus escape or whitelist the string (B) versus raw fragment as written (C).",
|
||||
"header": "SQL lookup",
|
||||
"multiSelect": false,
|
||||
"options": [
|
||||
{
|
||||
"label": "3A) Existing finder, bound parameter, adversarial-ID tests (recommended)",
|
||||
"description": "Completeness 10/10. human ~1h / CC ~5 min. ✅ Any opaque TEXT ID, including quotes, semicolons, Unicode and 512 chars, resolves correctly or hits the unknown-user path with no exception. ✅ Same treatment for the D4 orders batch query; a lint or review rule forbids string interpolation into SQL in this module. ❌ Requires the implementer to locate and reuse the existing lookup rather than writing a one-liner."
|
||||
},
|
||||
{
|
||||
"label": "3B) Escape or format-validate the ID before interpolating",
|
||||
"description": "Completeness 6/10. human ~2h / CC ~10 min. ✅ Keeps the raw-fragment structure the plan sketched. ✅ Blocks the most obvious injection strings. ❌ Contradicts the contract that every nonempty string is a valid ID (a whitelist rejects legitimate users); escaping is driver-specific and historically the source of bypasses."
|
||||
},
|
||||
{
|
||||
"label": "3C) Keep the raw SQL fragment as written",
|
||||
"description": "Completeness 1/10. human 0 / CC 0. ✅ No change from the sketch. ✅ Works for IDs that happen to be plain alphanumerics. ❌ Legitimate punctuated IDs break the query and are never marked paid; self-service SQL injection under the app's DB role; three days of 500 retries per affected event."
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"answered": true,
|
||||
"failed": false,
|
||||
"answers": {
|
||||
"D7 (Issue 3.1) — How should the handler build the user lookup query?\nProject/branch/task: gstack-plan-count-XMrFHU on main, reviewing PLAN.md (Stripe payment webhook handler), Section 3 Security.\nELI10: The plan pastes the user ID string straight into SQL. The plan's own contracts say user IDs are opaque text that can contain punctuation and Unicode, and that a valid Stripe signature does not make the string safe for SQL. So a perfectly legitimate ID with an apostrophe breaks the query, the webhook returns 500 forever, and that customer is never marked paid. If users can influence their own ID, they can also make their own payment run arbitrary SQL under the app's database role. A bound parameter (the existing user finder) makes both problems disappear.\nStakes if we pick wrong: Silent non-payment for any user with an odd ID, plus a self-service SQL injection path that signature checks and ownership checks both wave through.\nRecommendation: 3A because security is not optional and the fix is smaller than the bug: reuse the existing finder with a bound parameter and prove it with an adversarial-ID test.\nCompleteness: A=10/10, B=6/10, C=1/10\nNet: parameterize and test (A) versus escape or whitelist the string (B) versus raw fragment as written (C).": "3A) Existing finder, bound parameter, adversarial-ID tests (recommended)"
|
||||
},
|
||||
"unansweredQuestionIndices": [],
|
||||
"answeredAt": "2026-09-10T05:48:59.359Z"
|
||||
},
|
||||
{
|
||||
"sessionId": "c28b8f0e-b2ad-4c33-9441-a93b68c440f2",
|
||||
"toolUseId": "toolu_01XFSnvQXHHcLXz7eZngavzq",
|
||||
"questions": [
|
||||
{
|
||||
"question": "D8 (Issue 6.1) — Should the PR include automated tests for the handler, and at what depth?\nProject/branch/task: gstack-plan-count-XMrFHU on main, reviewing PLAN.md (Stripe payment webhook handler), Section 6 Tests.\nELI10: The plan ships a handler that changes who is marked as paid with no automated tests, relying on the existing integration suite and a manual staging replay. The existing suite was written for the prior handler and cannot check anything decided in this review: parameterized IDs, the mail timeout, the once-per-event email, the batched orders query. Every one of those decisions named a test as its proof. Without the tests, they are promises in a document, not behavior in the code.\nStakes if we pick wrong: A regression in payment marking or a duplicate-email bug reaches production and is caught by customers, not CI; the tested rollback flag limits blast radius but cannot detect the bug.\nRecommendation: 6A because well-tested code is non-negotiable and AI-assisted test writing compresses a day of human work into minutes. Boil the lake.\nCompleteness: A=10/10, B=6/10, C=1/10\nNet: full unit plus integration coverage of every decided invariant (A) versus unit tests only (B) versus none as written (C).",
|
||||
"header": "Tests",
|
||||
"multiSelect": false,
|
||||
"options": [
|
||||
{
|
||||
"label": "6A) Full table: unit + integration + 2 ordering tests (recommended)",
|
||||
"description": "Completeness 10/10. human ~1 day / CC ~20 min. ✅ Every D4-D7 invariant has an observable assertion: exact mail send counts across reruns, query count == 1, adversarial IDs resolve, MailTimeout under budget, completion marker only after clean return. ✅ Two controlled-pause tests prove Schedules 1 and 2 instead of asserting them. ❌ Ordering tests need explicit pause/release hooks in the handler or guard test seam."
|
||||
},
|
||||
{
|
||||
"label": "6B) Unit tests only, skip integration and ordering",
|
||||
"description": "Completeness 6/10. human ~3h / CC ~10 min. ✅ Covers lookup, update, orders count, timeout and the marker branch in isolation. ✅ Fast, no DB or lock fixtures. ❌ The two bugs most likely to ship (duplicate email on rerun, dispatcher flag routing) live at the integration boundary and stay untested."
|
||||
},
|
||||
{
|
||||
"label": "6C) No automated tests, as written",
|
||||
"description": "Completeness 1/10. human 0 / CC 0. ✅ No test code to write or maintain. ✅ Manual staging replay still exercises one happy path before broad enablement. ❌ Zero automated proof for a payment-mutating codepath; D5/D6/D7 become unverified claims; regressions surface via customers or on-call."
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"answered": true,
|
||||
"failed": false,
|
||||
"answers": {
|
||||
"D8 (Issue 6.1) — Should the PR include automated tests for the handler, and at what depth?\nProject/branch/task: gstack-plan-count-XMrFHU on main, reviewing PLAN.md (Stripe payment webhook handler), Section 6 Tests.\nELI10: The plan ships a handler that changes who is marked as paid with no automated tests, relying on the existing integration suite and a manual staging replay. The existing suite was written for the prior handler and cannot check anything decided in this review: parameterized IDs, the mail timeout, the once-per-event email, the batched orders query. Every one of those decisions named a test as its proof. Without the tests, they are promises in a document, not behavior in the code.\nStakes if we pick wrong: A regression in payment marking or a duplicate-email bug reaches production and is caught by customers, not CI; the tested rollback flag limits blast radius but cannot detect the bug.\nRecommendation: 6A because well-tested code is non-negotiable and AI-assisted test writing compresses a day of human work into minutes. Boil the lake.\nCompleteness: A=10/10, B=6/10, C=1/10\nNet: full unit plus integration coverage of every decided invariant (A) versus unit tests only (B) versus none as written (C).": "6A) Full table: unit + integration + 2 ordering tests (recommended)"
|
||||
},
|
||||
"unansweredQuestionIndices": [],
|
||||
"answeredAt": "2026-09-10T05:50:25.599Z"
|
||||
},
|
||||
{
|
||||
"sessionId": "c28b8f0e-b2ad-4c33-9441-a93b68c440f2",
|
||||
"toolUseId": "toolu_01JCY2zE5AcfrXDDX3dCk1ku",
|
||||
"questions": [
|
||||
{
|
||||
"question": "D9 (Issue 9.1) — Should the manual staging replay checklist gain failure-path replays before broad enablement?\nProject/branch/task: gstack-plan-count-XMrFHU on main, reviewing PLAN.md (Stripe payment webhook handler), Section 9 Deployment.\nELI10: The existing rollout checklist replays one successful payment in staging and checks the update, the email and the trace. That proves the happy path and nothing else. The failure modes this review fixed (odd user IDs breaking SQL, duplicate deliveries re-sending email, a failing mail provider) would pass that checklist untouched. Adding three short replays to the same checklist makes the manual gate check what the automated tests check, against real staging infrastructure.\nStakes if we pick wrong: A staging pass gives false confidence and the first punctuated user ID or mail-provider blip in production is the real test.\nRecommendation: 9A because deployments are not atomic and the checklist is already the documented gate; three extra replays cost minutes per release and catch the exact classes of bugs found here.\nCompleteness: A=9/10, B=4/10\nNet: extend the existing gate with the failure paths (A) versus keep the happy-path-only replay (B).",
|
||||
"header": "Staging gate",
|
||||
"multiSelect": false,
|
||||
"options": [
|
||||
{
|
||||
"label": "9A) Add three failure-path replays to the checklist (recommended)",
|
||||
"description": "Completeness 9/10. human ~1h to write, ~15 min per release / CC ~5 min to write. ✅ Punctuated/Unicode ID replay proves the parameterized lookup against the real staging DB; double delivery proves exactly one email and a sent marker. ✅ Failing-sink replay proves 500, alert, no marker, then one email on redelivery, exercising the runbook path end to end. ❌ Adds ~15 minutes to every release's manual gate."
|
||||
},
|
||||
{
|
||||
"label": "9B) Keep the happy-path replay only",
|
||||
"description": "Completeness 4/10. human 0 / CC 0. ✅ No change to the documented, tested checklist. ✅ CI integration tests from D8 already cover the failure paths against test doubles. ❌ Staging never exercises the real DB driver, real mail provider, or real dedup guard on the failure paths; a mismatch between test doubles and staging goes unnoticed until production."
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"answered": true,
|
||||
"failed": false,
|
||||
"answers": {
|
||||
"D9 (Issue 9.1) — Should the manual staging replay checklist gain failure-path replays before broad enablement?\nProject/branch/task: gstack-plan-count-XMrFHU on main, reviewing PLAN.md (Stripe payment webhook handler), Section 9 Deployment.\nELI10: The existing rollout checklist replays one successful payment in staging and checks the update, the email and the trace. That proves the happy path and nothing else. The failure modes this review fixed (odd user IDs breaking SQL, duplicate deliveries re-sending email, a failing mail provider) would pass that checklist untouched. Adding three short replays to the same checklist makes the manual gate check what the automated tests check, against real staging infrastructure.\nStakes if we pick wrong: A staging pass gives false confidence and the first punctuated user ID or mail-provider blip in production is the real test.\nRecommendation: 9A because deployments are not atomic and the checklist is already the documented gate; three extra replays cost minutes per release and catch the exact classes of bugs found here.\nCompleteness: A=9/10, B=4/10\nNet: extend the existing gate with the failure paths (A) versus keep the happy-path-only replay (B).": "9A) Add three failure-path replays to the checklist (recommended)"
|
||||
},
|
||||
"unansweredQuestionIndices": [],
|
||||
"answeredAt": "2026-09-10T05:51:33.727Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"signature": "9aedb365-c1e6-4f04-ac07-e4fc5dd32b9c:toolu_01JzHqoCyg4cy5S94wCE5p3q",
|
||||
"promptSnippet": "TODO 1 D10 \u2014 TODO proposal: record a deferred TODOS.md item to evaluate a real body typeface for the settings shell (replacing system-ui) in a later design pass? Project/branch/task: main, /plan-design-review of PLAN.md, post-pass TODOS.md ",
|
||||
"options": [
|
||||
{
|
||||
"index": 1,
|
||||
"label": "A Add to TODOS.md (recommended)"
|
||||
},
|
||||
{
|
||||
"index": 2,
|
||||
"label": "B Skip, not valuable enough"
|
||||
},
|
||||
{
|
||||
"index": 3,
|
||||
"label": "C Build it now in this PR"
|
||||
}
|
||||
],
|
||||
"observedAtMs": 445568,
|
||||
"preReview": false,
|
||||
"nativeCall": {
|
||||
"sessionId": "9aedb365-c1e6-4f04-ac07-e4fc5dd32b9c",
|
||||
"toolUseId": "toolu_01JzHqoCyg4cy5S94wCE5p3q",
|
||||
"questions": [
|
||||
{
|
||||
"question": "D10 \u2014 TODO proposal: record a deferred TODOS.md item to evaluate a real body typeface for the settings shell (replacing system-ui) in a later design pass?\nProject/branch/task: main, /plan-design-review of PLAN.md, post-pass TODOS.md updates.\nELI10: DESIGN.md and this plan keep system-ui as the app font, and you excluded visual exploration from this update, so nothing changes now. The only design-debt item left is that system-ui is the one AI-slop signal the review flagged: it is the default stack, not a chosen voice. This question is only about whether to write that down in TODOS.md so a future /design-consultation picks it up, not about changing anything here. What: evaluate one role-scoped sans (for example DM Sans, Instrument Sans, IBM Plex Sans) for the settings shell and controls. Why: a chosen typeface is the cheapest tell that the app was designed rather than assembled. Pros: brand voice across the whole app. Cons: touches every screen, needs a font-loading strategy and a DESIGN.md revision; not a settings-page task. Depends on: a DESIGN.md update via /design-consultation. Principle: subtraction default says do not add it now; the TODO just keeps the debt visible.\nStakes if we pick wrong: either the debt is forgotten, or a note lands in TODOS.md that you consider noise.\nRecommendation: A because the debt is real but explicitly out of scope, and a written TODO costs nothing.\nNote: options differ in kind, not coverage \u2014 no completeness score.\nNet: keep the typography debt visible vs. drop it.",
|
||||
"header": "TODO 1",
|
||||
"multiSelect": false,
|
||||
"options": [
|
||||
{
|
||||
"label": "A Add to TODOS.md (recommended)",
|
||||
"description": "\u2705 The one remaining design-debt item stays visible for the next /design-consultation run (human: ~5min / CC: ~1min to record). \u2705 Nothing changes in this update; DESIGN.md and system-ui stay as approved. \u274c Adds a TODOS.md file to a repo that does not have one yet (written after plan mode ends)."
|
||||
},
|
||||
{
|
||||
"label": "B Skip, not valuable enough",
|
||||
"description": "\u2705 No TODOS.md noise; system-ui is a deliberate design-system choice. \u2705 Zero follow-up work. \u274c The typography debt is not recorded anywhere and may be re-discovered later."
|
||||
},
|
||||
{
|
||||
"label": "C Build it now in this PR",
|
||||
"description": "\u2705 Removes the slop signal immediately across the settings page. \u2705 Fonts load with the existing shell. \u274c Directly contradicts the plan's \"no visual exploration\" and DESIGN.md's system-ui rule; app-wide scope in a settings-page PR."
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"answered": true,
|
||||
"failed": false,
|
||||
"answers": {
|
||||
"D10 \u2014 TODO proposal: record a deferred TODOS.md item to evaluate a real body typeface for the settings shell (replacing system-ui) in a later design pass?\nProject/branch/task: main, /plan-design-review of PLAN.md, post-pass TODOS.md updates.\nELI10: DESIGN.md and this plan keep system-ui as the app font, and you excluded visual exploration from this update, so nothing changes now. The only design-debt item left is that system-ui is the one AI-slop signal the review flagged: it is the default stack, not a chosen voice. This question is only about whether to write that down in TODOS.md so a future /design-consultation picks it up, not about changing anything here. What: evaluate one role-scoped sans (for example DM Sans, Instrument Sans, IBM Plex Sans) for the settings shell and controls. Why: a chosen typeface is the cheapest tell that the app was designed rather than assembled. Pros: brand voice across the whole app. Cons: touches every screen, needs a font-loading strategy and a DESIGN.md revision; not a settings-page task. Depends on: a DESIGN.md update via /design-consultation. Principle: subtraction default says do not add it now; the TODO just keeps the debt visible.\nStakes if we pick wrong: either the debt is forgotten, or a note lands in TODOS.md that you consider noise.\nRecommendation: A because the debt is real but explicitly out of scope, and a written TODO costs nothing.\nNote: options differ in kind, not coverage \u2014 no completeness score.\nNet: keep the typography debt visible vs. drop it.": "A Add to TODOS.md (recommended)"
|
||||
},
|
||||
"unansweredQuestionIndices": [],
|
||||
"answeredAt": "2026-09-10T05:47:13.383Z"
|
||||
}
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"signature": "9aedb365-c1e6-4f04-ac07-e4fc5dd32b9c:toolu_01QSUqwbg4YnBNKRvCUJKxHZ",
|
||||
"promptSnippet": "Issue 1 D3 \u2014 Issue 1 (Visual Hierarchy): make Save the only filled primary action? Project/branch/task: main, /plan-design-review of PLAN.md, Pass 1 Information Architecture. ELI10: Right now Save, Reset, Cancel, and Export look identical i",
|
||||
"options": [
|
||||
{
|
||||
"index": 1,
|
||||
"label": "1A Filled Save, ghost others (recommended)"
|
||||
},
|
||||
{
|
||||
"index": 2,
|
||||
"label": "1B Filled Save only, keep others as-is"
|
||||
},
|
||||
{
|
||||
"index": 3,
|
||||
"label": "1C Defer, keep four identical buttons"
|
||||
}
|
||||
],
|
||||
"observedAtMs": 176039,
|
||||
"preReview": true,
|
||||
"nativeCall": {
|
||||
"sessionId": "9aedb365-c1e6-4f04-ac07-e4fc5dd32b9c",
|
||||
"toolUseId": "toolu_01QSUqwbg4YnBNKRvCUJKxHZ",
|
||||
"questions": [
|
||||
{
|
||||
"question": "D3 \u2014 Issue 1 (Visual Hierarchy): make Save the only filled primary action?\nProject/branch/task: main, /plan-design-review of PLAN.md, Pass 1 Information Architecture.\nELI10: Right now Save, Reset, Cancel, and Export look identical in the page header. A user scanning the page has no signal which button finishes their task, and the two destructive ones (Reset, Cancel) look just as inviting as Save. DESIGN.md already says Save is the only filled primary (#1d4ed8, white text) and the other three are neutral ghost buttons. Principle: hierarchy as service; users satisfice and pick the most visible option.\nStakes if we pick wrong: users mis-click Reset or Cancel and hit a discard dialog they did not want, or hesitate before every save.\nRecommendation: 1A because it is the exact DESIGN.md token, reuses the existing Button variants, and fixes the primary-action ambiguity at zero new-component cost.\nCompleteness: 1A=10/10, 1B=6/10, 1C=3/10\nNet: one filled button vs. leaving the header ambiguous.",
|
||||
"header": "Issue 1",
|
||||
"multiSelect": false,
|
||||
"options": [
|
||||
{
|
||||
"label": "1A Filled Save, ghost others (recommended)",
|
||||
"description": "\u2705 Save is the single filled #1d4ed8 button; Reset/Cancel/Export become neutral ghosts, exactly per DESIGN.md (human: ~1h / CC: ~5min). \u2705 Reuses existing Button variants; no new component. \u274c Ghost buttons rely on border and text for affordance, so their contrast must be verified."
|
||||
},
|
||||
{
|
||||
"label": "1B Filled Save only, keep others as-is",
|
||||
"description": "\u2705 Save stands out immediately with minimal change. \u2705 Smallest diff to the existing header. \u274c Reset/Cancel/Export stay heavy and visually compete; drifts from DESIGN.md's ghost spec."
|
||||
},
|
||||
{
|
||||
"label": "1C Defer, keep four identical buttons",
|
||||
"description": "\u2705 No header changes in this update. \u2705 Zero implementation effort now. \u274c Primary-action ambiguity ships; documented DESIGN.md violation remains and Pass 1 stays at 7."
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"answered": true,
|
||||
"failed": false,
|
||||
"answers": {
|
||||
"D3 \u2014 Issue 1 (Visual Hierarchy): make Save the only filled primary action?\nProject/branch/task: main, /plan-design-review of PLAN.md, Pass 1 Information Architecture.\nELI10: Right now Save, Reset, Cancel, and Export look identical in the page header. A user scanning the page has no signal which button finishes their task, and the two destructive ones (Reset, Cancel) look just as inviting as Save. DESIGN.md already says Save is the only filled primary (#1d4ed8, white text) and the other three are neutral ghost buttons. Principle: hierarchy as service; users satisfice and pick the most visible option.\nStakes if we pick wrong: users mis-click Reset or Cancel and hit a discard dialog they did not want, or hesitate before every save.\nRecommendation: 1A because it is the exact DESIGN.md token, reuses the existing Button variants, and fixes the primary-action ambiguity at zero new-component cost.\nCompleteness: 1A=10/10, 1B=6/10, 1C=3/10\nNet: one filled button vs. leaving the header ambiguous.": "1A Filled Save, ghost others (recommended)"
|
||||
},
|
||||
"unansweredQuestionIndices": [],
|
||||
"answeredAt": "2026-09-10T05:42:43.858Z"
|
||||
}
|
||||
}
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
{
|
||||
"observations": [
|
||||
{
|
||||
"observationSha256": "ba974f73975a2b66bb57ba033810cd85365d1fb016bd44e95d70c3c752ad75a5",
|
||||
"observationPath": ".context/ship-source-aj-delta-paid-20260910-v1/evals/job-19/shards/skill-e2e-plan-design-plan-mode/pty-count/ship-source-aj-delta-paid-20260910-v1-19/plan-design-review-1789020195325-SyATGW/observation.json",
|
||||
"opts": {
|
||||
"skillName": "plan-design-review",
|
||||
"sessionId": "808696cc-cbb7-4309-b3ae-ba2df3c25e55",
|
||||
"commandStartedAt": 1789020092112,
|
||||
"seed": "\n# Plan: Marketing landing page\n\n## Layout\nAll headings, taglines, and body copy will be center-aligned for a\n\"clean modern look.\" The hero h1 sits 8px above the subhead; the CTA\nbutton has the same visual weight as the \"Learn more\" link beside it.\n\n## Pages\n- / (hero, 3-column features grid, testimonials carousel, footer)\n- /pricing (3 tier cards)\n\n## States\nOnly the happy path is designed. No empty states, no error states,\nno loading states. Mobile: \"stacks on mobile.\"\n"
|
||||
},
|
||||
"transcript": {
|
||||
"status": "ready",
|
||||
"calls": [],
|
||||
"assistantMessages": [
|
||||
{
|
||||
"sessionId": "808696cc-cbb7-4309-b3ae-ba2df3c25e55",
|
||||
"text": "I'll run the design plan review skill on this draft.",
|
||||
"timestamp": "2026-09-10T06:01:37.058Z"
|
||||
},
|
||||
{
|
||||
"sessionId": "808696cc-cbb7-4309-b3ae-ba2df3c25e55",
|
||||
"text": "Running the skill preamble first.",
|
||||
"timestamp": "2026-09-10T06:01:44.578Z"
|
||||
},
|
||||
{
|
||||
"sessionId": "808696cc-cbb7-4309-b3ae-ba2df3c25e55",
|
||||
"text": "I'll go with reviewing the \"Marketing landing page\" draft plan you pasted, and now I'm running the pre-review audit, design-binary setup check, brain context, and context recovery checks in parallel.\n\n",
|
||||
"timestamp": "2026-09-10T06:01:57.951Z"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tools": [
|
||||
{
|
||||
"sessionId": "808696cc-cbb7-4309-b3ae-ba2df3c25e55",
|
||||
"timestamp": "2026-09-10T06:01:38.217Z",
|
||||
"toolUseId": "toolu_01TZiuAG9dfxkLeVa8fqpAiC",
|
||||
"kind": "use",
|
||||
"name": "Skill",
|
||||
"input": {
|
||||
"skill": "plan-design-review"
|
||||
}
|
||||
},
|
||||
{
|
||||
"sessionId": "808696cc-cbb7-4309-b3ae-ba2df3c25e55",
|
||||
"timestamp": "2026-09-10T06:01:39.549Z",
|
||||
"toolUseId": "toolu_01TZiuAG9dfxkLeVa8fqpAiC",
|
||||
"kind": "result",
|
||||
"content": "Launching skill: plan-design-review",
|
||||
"isError": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"observationSha256": "641c71f3e241aebf6f4a25a521f5268e068f1e1b9d2ed5be89633c89a7d9adde",
|
||||
"observationPath": ".context/ship-source-aj-delta-paid-20260910-v1/evals/job-19/shards/skill-e2e-plan-design-plan-mode/pty-count/ship-source-aj-delta-paid-20260910-v1-19/plan-design-review-1789020323069-dJQJlj/observation.json",
|
||||
"opts": {
|
||||
"skillName": "plan-design-review",
|
||||
"sessionId": "7714cb87-097d-43a6-996b-481517779210",
|
||||
"commandStartedAt": 1789020206706,
|
||||
"seed": "\n# Plan: Marketing landing page\n\n## Layout\nAll headings, taglines, and body copy will be center-aligned for a\n\"clean modern look.\" The hero h1 sits 8px above the subhead; the CTA\nbutton has the same visual weight as the \"Learn more\" link beside it.\n\n## Pages\n- / (hero, 3-column features grid, testimonials carousel, footer)\n- /pricing (3 tier cards)\n\n## States\nOnly the happy path is designed. No empty states, no error states,\nno loading states. Mobile: \"stacks on mobile.\"\n"
|
||||
},
|
||||
"transcript": {
|
||||
"status": "ready",
|
||||
"calls": [],
|
||||
"assistantMessages": [
|
||||
{
|
||||
"sessionId": "7714cb87-097d-43a6-996b-481517779210",
|
||||
"text": "I'll run the plan-design-review skill on your draft plan.",
|
||||
"timestamp": "2026-09-10T06:03:32.514Z"
|
||||
},
|
||||
{
|
||||
"sessionId": "7714cb87-097d-43a6-996b-481517779210",
|
||||
"text": "I'll review the \"Marketing landing page\" draft, starting by checking git status, design tool availability, brain context, base branch, and any DESIGN.md/TODOS files.\n\n",
|
||||
"timestamp": "2026-09-10T06:03:51.333Z"
|
||||
},
|
||||
{
|
||||
"sessionId": "7714cb87-097d-43a6-996b-481517779210",
|
||||
"text": "Base branch is `main`. DESIGN.md exists, and the design binary is ready. Reading DESIGN.md, the review sections, and design-related TODOs.",
|
||||
"timestamp": "2026-09-10T06:04:09.509Z"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tools": [
|
||||
{
|
||||
"sessionId": "7714cb87-097d-43a6-996b-481517779210",
|
||||
"timestamp": "2026-09-10T06:03:32.556Z",
|
||||
"toolUseId": "toolu_01GDBKDDZnYTY3qMsMdL6s1r",
|
||||
"kind": "use",
|
||||
"name": "Skill",
|
||||
"input": {
|
||||
"skill": "plan-design-review"
|
||||
}
|
||||
},
|
||||
{
|
||||
"sessionId": "7714cb87-097d-43a6-996b-481517779210",
|
||||
"timestamp": "2026-09-10T06:03:33.859Z",
|
||||
"toolUseId": "toolu_01GDBKDDZnYTY3qMsMdL6s1r",
|
||||
"kind": "result",
|
||||
"content": "Launching skill: plan-design-review",
|
||||
"isError": false
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+168
@@ -0,0 +1,168 @@
|
||||
{
|
||||
"provenance": {
|
||||
"sourceHead": "a6eaafacb846bdb018a1ead13985be9c05e2c2df",
|
||||
"proof": ".context/ship-source-aj-delta-paid-20260910-v1/eng-first-evidence-ledger-v1/proof.json",
|
||||
"proofSha256": "ac5fa6b2784246b90b4828d9f12cedd698e55fc7a6c7fa711a8f858355086bf6",
|
||||
"reportSha256": "374e8874eb1b0a6e6d5723b2a1b8efae4abe4ebe7d3a3ca55a6f2947974de67a",
|
||||
"window": {
|
||||
"start": 1789019430381.0,
|
||||
"end": 1789019938117.0
|
||||
},
|
||||
"historicalPaidFailurePreserved": true
|
||||
},
|
||||
"transcript": {
|
||||
"status": "ready",
|
||||
"calls": [
|
||||
{
|
||||
"sessionId": "0f5f0040-2ba7-4029-b33d-43002ac0ce18",
|
||||
"toolUseId": "toolu_01RZuA5L1oHbKJRRaptFtWX9",
|
||||
"questions": [
|
||||
{
|
||||
"question": "D3 — Step 0 scope: reduce 4 new classes / 12 files to the minimal version, or proceed as-is?\nProject/branch/task: gstack-plan-count-NOHUUz on main, PLAN.md Multi-tenant Auth Refactor.\nELI10: The plan adds four new classes, but the cache adapter you already have does the storage and invalidation job (PLAN.md:7-13). TokenStore has no stated job the adapter does not already do. AuthCache is a thin wrapper over that same adapter and is shared as a global that two services write to (PLAN.md:19-20). RequestPolicy has no named caller. Fewer moving parts means fewer places a tenant-isolation bug can hide.\nStakes if we pick wrong: overbuilt means three cache-shaped layers to keep consistent and a global-state bug class in auth code; underbuilt means we later discover a real reason for TokenStore or RequestPolicy and bolt it on.\nRecommendation: A because the two services that carry the goal (AuthBroker, SessionMint) can take the existing adapter by constructor injection, which also kills the shared-global smell in one move. Explicit over clever, smallest diff that expresses the change.\nNote: options differ in kind, not coverage — no completeness score.\nNet: 2 injected classes vs 3 vs 4; every option ships multi-tenant auth, they differ in how many layers sit between a service and the cache.",
|
||||
"header": "Scope",
|
||||
"multiSelect": false,
|
||||
"options": [
|
||||
{
|
||||
"label": "A) Reduce to 2 classes, inject adapter (recommended)",
|
||||
"description": "✅ AuthBroker + SessionMint receive the existing cache adapter via constructor, no module-level global, ~7-8 files (human: ~3 days / CC: ~1h)\n✅ Drops TokenStore (adapter already keys tokens per tenant) and folds RequestPolicy into AuthBroker until a second consumer exists\n❌ If AuthCache was meant to add tenant-scoped behavior the adapter lacks, that logic now lives inside AuthBroker instead of a named facade"
|
||||
},
|
||||
{
|
||||
"label": "B) Keep AuthCache as injected facade, drop TokenStore",
|
||||
"description": "✅ Keeps a named seam for future cache behavior while still removing the module-level global (human: ~3.5 days / CC: ~1.2h)\n✅ Still drops TokenStore and folds RequestPolicy, so 3 classes and ~9 files\n❌ A facade over one adapter with no added rules is an extra layer to test and keep in sync for now"
|
||||
},
|
||||
{
|
||||
"label": "C) Proceed as-is: 4 classes, 12 files",
|
||||
"description": "✅ No re-planning; the shape you already drafted goes straight to the review sections (human: ~5 days / CC: ~2h)\n✅ Each concern gets its own named class if you know TokenStore and RequestPolicy have roles the plan just did not write down\n❌ Three storage-shaped layers plus a shared mutable global in auth code; the review will still push back on the global in Section 1"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"question": "D4 — Enable cross-project learnings search for gstack on this machine?\nProject/branch/task: gstack-plan-count-NOHUUz on main, first-time setup prompt during the Prior Learnings step.\nELI10: gstack records small lessons per project. With cross-project on, this review can also read lessons captured in your other local projects. Nothing leaves the machine. Good for a solo developer; skip if you work across client codebases that must not cross-contaminate.\nStakes if we pick wrong: mild; either missed reusable lessons or a lesson from an unrelated codebase showing up here.\nRecommendation: A because it is local-only and the benefit compounds over time.\nNote: options differ in kind, not coverage — no completeness score.\nNet: broader recall vs strict per-project isolation.",
|
||||
"header": "Learnings",
|
||||
"multiSelect": false,
|
||||
"options": [
|
||||
{
|
||||
"label": "A) Enable cross-project (recommended)",
|
||||
"description": "✅ Reviews get smarter using lessons from all your local projects, not just this one\n✅ Stays on this machine; no network involved in learnings search\n❌ A lesson from an unrelated codebase can surface where it does not apply"
|
||||
},
|
||||
{
|
||||
"label": "B) Project-scoped only",
|
||||
"description": "✅ Strict isolation between codebases, right for multi-client work\n✅ Every surfaced lesson is guaranteed to come from this repo\n❌ Slower compounding; each project learns from scratch"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"answered": true,
|
||||
"failed": false,
|
||||
"answers": {
|
||||
"D3 — Step 0 scope: reduce 4 new classes / 12 files to the minimal version, or proceed as-is?\nProject/branch/task: gstack-plan-count-NOHUUz on main, PLAN.md Multi-tenant Auth Refactor.\nELI10: The plan adds four new classes, but the cache adapter you already have does the storage and invalidation job (PLAN.md:7-13). TokenStore has no stated job the adapter does not already do. AuthCache is a thin wrapper over that same adapter and is shared as a global that two services write to (PLAN.md:19-20). RequestPolicy has no named caller. Fewer moving parts means fewer places a tenant-isolation bug can hide.\nStakes if we pick wrong: overbuilt means three cache-shaped layers to keep consistent and a global-state bug class in auth code; underbuilt means we later discover a real reason for TokenStore or RequestPolicy and bolt it on.\nRecommendation: A because the two services that carry the goal (AuthBroker, SessionMint) can take the existing adapter by constructor injection, which also kills the shared-global smell in one move. Explicit over clever, smallest diff that expresses the change.\nNote: options differ in kind, not coverage — no completeness score.\nNet: 2 injected classes vs 3 vs 4; every option ships multi-tenant auth, they differ in how many layers sit between a service and the cache.": "A) Reduce to 2 classes, inject adapter (recommended)",
|
||||
"D4 — Enable cross-project learnings search for gstack on this machine?\nProject/branch/task: gstack-plan-count-NOHUUz on main, first-time setup prompt during the Prior Learnings step.\nELI10: gstack records small lessons per project. With cross-project on, this review can also read lessons captured in your other local projects. Nothing leaves the machine. Good for a solo developer; skip if you work across client codebases that must not cross-contaminate.\nStakes if we pick wrong: mild; either missed reusable lessons or a lesson from an unrelated codebase showing up here.\nRecommendation: A because it is local-only and the benefit compounds over time.\nNote: options differ in kind, not coverage — no completeness score.\nNet: broader recall vs strict per-project isolation.": "A) Enable cross-project (recommended)"
|
||||
},
|
||||
"unansweredQuestionIndices": [],
|
||||
"answeredAt": "2026-09-10T05:50:30.382Z"
|
||||
},
|
||||
{
|
||||
"sessionId": "0f5f0040-2ba7-4029-b33d-43002ac0ce18",
|
||||
"toolUseId": "toolu_01RP1Yzt5jbat8STPd4k3bER",
|
||||
"questions": [
|
||||
{
|
||||
"question": "D5 — Issue 1 (Architecture): write-after-invalidate race between SessionMint and AuthBroker on the shared adapter\nProject/branch/task: gstack-plan-count-NOHUUz on main, PLAN.md Multi-tenant Auth Refactor, post-scope-cut (2 injected services, one adapter).\n[P1] (confidence: 8/10) PLAN.md:10 \"they do not serialize mutations\" + PLAN.md:20 \"Both services mutate it\" + PLAN.md:8-9 invalidation on logout/revocation/suspension.\nELI10: Even after injection, both services write into the same cache. Picture SessionMint minting a session for tenant A while, at the same instant, the tenant gets suspended and AuthBroker wipes tenant A's entries. If the mint write lands a few milliseconds after the wipe, a suspended tenant now holds a live cached session until it expires. The plan states outright that nothing orders these writes.\nStakes if we pick wrong: a suspended or logged-out tenant keeps working against a cache entry that should be gone. Silent, and only visible in an incident.\nRecommendation: A because a version-guarded write is the smallest change that makes the race impossible rather than unlikely, and it uses the policy-version key the adapter already has. Explicit over clever; handle the edge case, not the demo path.\nCompleteness: A=10/10, B=8/10, C=3/10\nNet: guaranteed ordering on every write vs a single-writer topology vs documenting a known hole.",
|
||||
"header": "Issue 1",
|
||||
"multiSelect": false,
|
||||
"options": [
|
||||
{
|
||||
"label": "1A) Guarded writes: re-check tenant status + policy version on put (recommended)",
|
||||
"description": "✅ Every cache write carries the tenant epoch/policy version it was computed under; the adapter rejects writes whose epoch is stale, so a post-wipe mint can never land (human: ~1 day / CC: ~20 min)\n✅ Uses the existing policy-version key, so no new storage shape; adds a unit test that interleaves suspend and mint and asserts the entry is absent\n❌ Requires an epoch or version bump on suspension/revocation if one does not exist today; one extra read per write"
|
||||
},
|
||||
{
|
||||
"label": "1B) Single writer: only AuthBroker writes, SessionMint returns results to it",
|
||||
"description": "✅ Removes the two-writer problem by topology; SessionMint becomes a pure minting function with no cache dependency, which is simpler to test (human: ~1 day / CC: ~20 min)\n✅ Invalidation and writes now flow through one code path, so ordering is a local concern\n❌ A late mint can still land after a wipe inside AuthBroker unless it also checks tenant status before put; it narrows the race, it does not close it"
|
||||
},
|
||||
{
|
||||
"label": "1C) Accept the race, document it, rely on TTL expiry",
|
||||
"description": "✅ Zero implementation cost now (human: 0 / CC: 0)\n✅ Matches the current plan text, which already acknowledges unserialized mutations\n❌ A suspended tenant can hold a valid cached session for the full TTL; a security-adjacent hole the review would flag as a critical gap"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"answered": true,
|
||||
"failed": false,
|
||||
"answers": {
|
||||
"D5 — Issue 1 (Architecture): write-after-invalidate race between SessionMint and AuthBroker on the shared adapter\nProject/branch/task: gstack-plan-count-NOHUUz on main, PLAN.md Multi-tenant Auth Refactor, post-scope-cut (2 injected services, one adapter).\n[P1] (confidence: 8/10) PLAN.md:10 \"they do not serialize mutations\" + PLAN.md:20 \"Both services mutate it\" + PLAN.md:8-9 invalidation on logout/revocation/suspension.\nELI10: Even after injection, both services write into the same cache. Picture SessionMint minting a session for tenant A while, at the same instant, the tenant gets suspended and AuthBroker wipes tenant A's entries. If the mint write lands a few milliseconds after the wipe, a suspended tenant now holds a live cached session until it expires. The plan states outright that nothing orders these writes.\nStakes if we pick wrong: a suspended or logged-out tenant keeps working against a cache entry that should be gone. Silent, and only visible in an incident.\nRecommendation: A because a version-guarded write is the smallest change that makes the race impossible rather than unlikely, and it uses the policy-version key the adapter already has. Explicit over clever; handle the edge case, not the demo path.\nCompleteness: A=10/10, B=8/10, C=3/10\nNet: guaranteed ordering on every write vs a single-writer topology vs documenting a known hole.": "1A) Guarded writes: re-check tenant status + policy version on put (recommended)"
|
||||
},
|
||||
"unansweredQuestionIndices": [],
|
||||
"answeredAt": "2026-09-10T05:51:16.032Z"
|
||||
},
|
||||
{
|
||||
"sessionId": "0f5f0040-2ba7-4029-b33d-43002ac0ce18",
|
||||
"toolUseId": "toolu_01P5QTtaYgRQRBHubfVbLkS8",
|
||||
"questions": [
|
||||
{
|
||||
"question": "D8 — Issue 4 (Code Quality): validateAndDispatch() swallows three error classes in nested try/catch\nProject/branch/task: gstack-plan-count-NOHUUz on main, PLAN.md Multi-tenant Auth Refactor.\n[P1] (confidence: 8/10) PLAN.md:23-24 \"60 lines with three nested try/catch blocks; each catch swallows a different error class.\"\nELI10: This function decides whether a login is valid and where to send it. Three separate places catch an error and quietly continue. In auth code, quietly continuing after a validation error means a request that should have been denied keeps going. It also means production failures leave no trace, so the first sign of a broken IDP integration is users complaining, not a log line.\nStakes if we pick wrong: fail-open on a validation error, and blind debugging when the IDP misbehaves.\nRecommendation: A because splitting into validate() then dispatch() with one explicit typed error outcome per class removes the nesting and makes every failure path visible and testable; explicit over clever, and this is where DRY and testability meet.\nCompleteness: A=10/10, B=6/10, C=2/10\nNet: restructure so every error is an explicit, tested outcome vs keep the shape but stop hiding errors vs leave it.",
|
||||
"header": "Issue 4",
|
||||
"multiSelect": false,
|
||||
"options": [
|
||||
{
|
||||
"label": "4A) Split into validate() + dispatch(), typed AuthError outcomes, fail-closed (recommended)",
|
||||
"description": "✅ Each of the three error classes maps to a named outcome (deny-with-reason, retryable, fail-closed) returned as a typed result, logged with tenant + reason, never swallowed (human: ~1 day / CC: ~20 min)\n✅ Two flat functions under 25 lines each; one test per error class asserts the exact outcome and that the request does not proceed\n❌ Changes the function's return shape, so its callers are touched in the same PR"
|
||||
},
|
||||
{
|
||||
"label": "4B) Keep structure, log and rethrow in each catch",
|
||||
"description": "✅ Smallest diff; errors become visible in logs and stop the request instead of continuing (human: ~2h / CC: ~5 min)\n✅ No caller changes\n❌ Still 60 lines with three nested blocks; the branches remain hard to test individually and the nesting hides which operation failed"
|
||||
},
|
||||
{
|
||||
"label": "4C) Leave as-is",
|
||||
"description": "✅ No work now (human: 0 / CC: 0)\n✅ Behavior unchanged for callers\n❌ Fail-open on swallowed validation errors in the auth path; the review would mark this a critical gap"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"answered": true,
|
||||
"failed": false,
|
||||
"answers": {
|
||||
"D8 — Issue 4 (Code Quality): validateAndDispatch() swallows three error classes in nested try/catch\nProject/branch/task: gstack-plan-count-NOHUUz on main, PLAN.md Multi-tenant Auth Refactor.\n[P1] (confidence: 8/10) PLAN.md:23-24 \"60 lines with three nested try/catch blocks; each catch swallows a different error class.\"\nELI10: This function decides whether a login is valid and where to send it. Three separate places catch an error and quietly continue. In auth code, quietly continuing after a validation error means a request that should have been denied keeps going. It also means production failures leave no trace, so the first sign of a broken IDP integration is users complaining, not a log line.\nStakes if we pick wrong: fail-open on a validation error, and blind debugging when the IDP misbehaves.\nRecommendation: A because splitting into validate() then dispatch() with one explicit typed error outcome per class removes the nesting and makes every failure path visible and testable; explicit over clever, and this is where DRY and testability meet.\nCompleteness: A=10/10, B=6/10, C=2/10\nNet: restructure so every error is an explicit, tested outcome vs keep the shape but stop hiding errors vs leave it.": "4A) Split into validate() + dispatch(), typed AuthError outcomes, fail-closed (recommended)"
|
||||
},
|
||||
"unansweredQuestionIndices": [],
|
||||
"answeredAt": "2026-09-10T05:52:32.294Z"
|
||||
},
|
||||
{
|
||||
"sessionId": "0f5f0040-2ba7-4029-b33d-43002ac0ce18",
|
||||
"toolUseId": "toolu_01EZvvBgo24EYYYMRadbFhna",
|
||||
"questions": [
|
||||
{
|
||||
"question": "D10 — Issue 6 (Performance): 5 sequential IDP calls; parallelize, but define timeout and partial-failure semantics\nProject/branch/task: gstack-plan-count-NOHUUz on main, PLAN.md Multi-tenant Auth Refactor.\n[P2] (confidence: 8/10) PLAN.md:31-32 \"Token validation issues 5 sequential API calls to the IDP; they could be parallelized via Promise.all trivially (calls are independent).\"\nELI10: Five round trips to the identity provider happen one after another, so login latency is five IDP latencies added up. Running them at once cuts that to the slowest single call. But Promise.all rejects the moment any one fails, and with no timeout a single hung IDP call hangs the login forever. The plan calls this trivial; the fan-out is, the failure semantics are not.\nStakes if we pick wrong: either logins wait 5x longer than needed, or a hung IDP endpoint pins requests open and a partial IDP outage produces confusing half-validated states.\nRecommendation: A because validation must be all-or-nothing (fail-closed), so fail-fast Promise.all is the right primitive, but only wrapped in a per-call timeout and a single explicit IDPUnavailable outcome that Issue 4's typed errors already give us a home for.\nCompleteness: A=10/10, B=5/10, C=6/10\nNet: parallel + bounded + fail-closed vs parallel with no bounds vs keep sequential and safe but slow.",
|
||||
"header": "Issue 6",
|
||||
"multiSelect": false,
|
||||
"options": [
|
||||
{
|
||||
"label": "6A) Promise.all + per-call timeout + fail-closed IDPUnavailable outcome, latency test (recommended)",
|
||||
"description": "✅ Each IDP call wrapped with AbortSignal.timeout; any rejection or timeout yields one typed IDPUnavailable outcome, request denied, logged once with which call failed (human: ~4h / CC: ~15 min)\n✅ Tests: all-succeed, one-rejects, one-hangs-past-timeout, and a timing test asserting total wall time is near max not sum\n❌ Retry policy is explicitly out of scope here; a flapping IDP still denies logins until it recovers"
|
||||
},
|
||||
{
|
||||
"label": "6B) Bare Promise.all as the plan states",
|
||||
"description": "✅ Simplest change, immediate 5x latency win on the happy path (human: ~30 min / CC: ~3 min)\n✅ Fail-fast is the correct semantics for validation\n❌ No timeout: one hung call hangs the login; rejection surfaces as a raw error with no typed outcome or log context"
|
||||
},
|
||||
{
|
||||
"label": "6C) Keep sequential, add timeouts only",
|
||||
"description": "✅ No concurrency change, so behavior stays easiest to reason about (human: ~2h / CC: ~10 min)\n✅ Timeouts still stop the hang\n❌ Login latency remains the sum of five IDP round trips when the calls are independent"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"answered": true,
|
||||
"failed": false,
|
||||
"answers": {
|
||||
"D10 — Issue 6 (Performance): 5 sequential IDP calls; parallelize, but define timeout and partial-failure semantics\nProject/branch/task: gstack-plan-count-NOHUUz on main, PLAN.md Multi-tenant Auth Refactor.\n[P2] (confidence: 8/10) PLAN.md:31-32 \"Token validation issues 5 sequential API calls to the IDP; they could be parallelized via Promise.all trivially (calls are independent).\"\nELI10: Five round trips to the identity provider happen one after another, so login latency is five IDP latencies added up. Running them at once cuts that to the slowest single call. But Promise.all rejects the moment any one fails, and with no timeout a single hung IDP call hangs the login forever. The plan calls this trivial; the fan-out is, the failure semantics are not.\nStakes if we pick wrong: either logins wait 5x longer than needed, or a hung IDP endpoint pins requests open and a partial IDP outage produces confusing half-validated states.\nRecommendation: A because validation must be all-or-nothing (fail-closed), so fail-fast Promise.all is the right primitive, but only wrapped in a per-call timeout and a single explicit IDPUnavailable outcome that Issue 4's typed errors already give us a home for.\nCompleteness: A=10/10, B=5/10, C=6/10\nNet: parallel + bounded + fail-closed vs parallel with no bounds vs keep sequential and safe but slow.": "6A) Promise.all + per-call timeout + fail-closed IDPUnavailable outcome, latency test (recommended)"
|
||||
},
|
||||
"unansweredQuestionIndices": [],
|
||||
"answeredAt": "2026-09-10T05:53:38.545Z"
|
||||
}
|
||||
],
|
||||
"assistantMessages": [],
|
||||
"planReadyRequests": []
|
||||
},
|
||||
"required": "- **CRITICAL regression** `legacyAuthFlow` snapshot: capture current outputs\n for valid, expired, wrong-audience, wrong-tenant inputs BEFORE any change;\n assert both legacy (flag OFF) and new (flag ON) paths produce identical\n observable results. Mandatory under the coverage-audit regression rule.",
|
||||
"task": "- [ ] **T1 (P1, human: ~2h / CC: ~10min)** — tests/auth — Snapshot legacyAuthFlow() behavior as regression tests before any change\n - Surfaced by: Test review — REGRESSION RULE, PLAN.md:27-28\n - Files: tests/auth/legacy-auth-flow.regression.test.*\n - Verify: tests pass against unmodified legacy code, then against flag-OFF route",
|
||||
"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` (recorded provider: codex, host: claude) | Independent 2nd opinion | 1 | DISABLED (skipped, 2026-09-10 05:53) | outside coverage missing: codex_reviews disabled, no substitute reviewer |\n| Eng Review | `/plan-eng-review` | Architecture & tests (required) | 1 | CLEAN (PLAN, 2026-09-10 05:57, commit f6418d1, SCOPE_REDUCED) | 7 issues, 0 critical gaps |\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 (skipped by config `codex_reviews=disabled`), source none, host claude. No findings; no native fallback was dispatched because disabled is an intentional opt-out. Re-enable with `gstack-config set codex_reviews enabled`.\n\n**VERDICT:** ENG CLEARED — ready to implement (scope reduced per D3; all 7 findings folded into the plan). CEO, Design, DX reviews not run; none required for a backend auth refactor with no UI scope.\n\nNO UNRESOLVED DECISIONS\n",
|
||||
"taskIntro": "Synthesized from this review's findings. Each task derives from a specific\nfinding above. Run with Claude Code or Codex; checkbox as you ship."
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"sourceReportSha256": "d94858e2b0cfd620c320524ac7b721fe1f83e71ce40416699809ff6c817fe898",
|
||||
"nativeToolUseId": "toolu_011jnHNnavxztPsNtrLL78PR",
|
||||
"finding": "### Findings registry\nEach finding is recorded once. Sections cross-reference by F#.\n\n| F# | Section | Finding and evidence | Selected remedy (auto-decided, recommended) | Residual risk | Verification |\n|---|---|---|---|---|---|\n| F1 | 1 (schedule in 4) | Original plan lines 31\u201334 disclaim any fill/write coordination. Schedule Diagram 2b: fill starts, write commits, write deletes (no-op), fill sets pre-commit v1, later read hits v1. Violates contract \"every read begun after that write completes must observe the committed version.\" Single-flight does not help: a later reader joins the stale fill. **CRITICAL GAP.** | D2 \u2192 B: `invalidated` flag on the single-flight record; write marks it on commit; invalidated fill skips `cache.set` and is not joinable; metric `cache.fill_skipped_stale`. (A: accept as-is rejected, weakens a stated requirement. C: row version rejected, schema.) | A fresh fill started after DB commit but before the write continuation is discarded needlessly; one extra miss, counted. | Ordering tests A, B, non-join test. |\n"
|
||||
}
|
||||
@@ -44,6 +44,20 @@ function ownedEditDiffRows(rows: string[], file: string, ownedStateRoot?: string
|
||||
if (!numbered) return null;
|
||||
rows = rows.slice(header);
|
||||
}
|
||||
// A viewport can start at the native Edit panel after its tool title has
|
||||
// scrolled away. The remaining displayed path must still bind the complete
|
||||
// owned project/artifact path; the menu and current request are checked below.
|
||||
if (/^[─╌]{8,}$/.test(rows[0] ?? '') && rows[1]?.trim() === 'Edit file') {
|
||||
if (!ownedStateRoot || !/^[─╌]{8,}$/.test(rows[3] ?? '')) return null;
|
||||
const relative = path.relative(ownedStateRoot, file).split(path.sep).join('/');
|
||||
const alias = path.basename(ownedStateRoot) === '.gstack' ? `~/.gstack/${relative}` : undefined;
|
||||
const displayed = rows[2]?.trim() ?? '';
|
||||
if (displayed !== file && displayed !== alias) {
|
||||
const suffix = displayed.startsWith('…') ? displayed.slice(1) : '';
|
||||
if ((suffix !== relative && !suffix.endsWith('/' + relative)) || !file.endsWith(suffix)) return null;
|
||||
}
|
||||
return rows.slice(4);
|
||||
}
|
||||
const update = /^[●⏺] Update\(([^\n]+)\)$/.exec(rows[0] ?? '');
|
||||
if (!update) return rows; // Existing cropped-only row guards still apply.
|
||||
if (!ownedStateRoot || rows[1]?.trim() !== '' || !/^[─╌]{8,}$/.test(rows[2] ?? '') ||
|
||||
|
||||
@@ -347,7 +347,7 @@ function assertedStructuredOwner(prose: string[], index: number): boolean {
|
||||
}
|
||||
|
||||
/** A same-ID assessment survives intervening diagrams and named assessment headings. */
|
||||
function structuredFindingAssessment(prose: string[], finding: number, traceEnd: number, ids: string[]): string[] {
|
||||
function structuredFindingAssessment(prose: string[], finding: number, traceEnd: number, ids: string[], assertedOwner = assertedStructuredOwner): string[] {
|
||||
const assessment: string[] = [];
|
||||
const sameId = new RegExp(`^(?:${ids.join('|')})\\b`);
|
||||
const ownsHeading = new RegExp(`\\b(?:${ids.join('|')})\\b`);
|
||||
@@ -362,7 +362,7 @@ function structuredFindingAssessment(prose: string[], finding: number, traceEnd:
|
||||
followingTrace = false;
|
||||
namedAssessment = false;
|
||||
}
|
||||
if (assertedStructuredOwner(prose, i) && (sameId.test(line) || namedAssessment || followingTrace)) assessment.push(line);
|
||||
if (assertedOwner(prose, i) && (sameId.test(line) || namedAssessment || followingTrace)) assessment.push(line);
|
||||
}
|
||||
return assessment;
|
||||
}
|
||||
@@ -633,11 +633,41 @@ function hasOrderedStaleFillOperations(text: string): boolean {
|
||||
// Table cells cannot lend operation order to each other. Bare "reader"
|
||||
// refers back to the missed read; explicit foreign cache/key references,
|
||||
// quoted examples and conditional/negated executions cannot establish it.
|
||||
return text.split(/\s*\|\s*/).some(cell =>
|
||||
!/["“”?]|\b(?:if|unless|whether|might|may|could|not|never|no\s+longer|example|template|quoted)\b/i.test(cell)
|
||||
return text.split(/\s*\|\s*/).some(cell => {
|
||||
// A finding may describe a fill's lifetime instead of naming the first
|
||||
// cache miss. Its original-plan sequence must still place the same old
|
||||
// version in the cache after commit/delete and deliver it to a later read.
|
||||
// Quoted requirements can accompany that assertion, but quoted operations
|
||||
// cannot supply it; never join fragments across a quoted span or table cell.
|
||||
const unquoted = cell.replace(/"(?:[^"\\]|\\.)*"|“[^”]*”/g, '[quoted]');
|
||||
const compact = /^Original (?:plan|sketch|wrapper)\b[^.!?]*\.\s+(?:Schedule(?: Diagram)? [A-Za-z0-9][\w.-]*:\s*)?fill starts,\s*write commits,\s*write deletes(?:\s*\(no-op\))?,\s*fill sets pre-commit ([A-Za-z0-9][\w.-]*),\s*later read hits ([A-Za-z0-9][\w.-]*)\.(?=\s|$)/i.exec(unquoted);
|
||||
if (compact && compact[1] === compact[2]
|
||||
&& !/\b(?:if|unless|whether|might|may|could|never|no\s+longer|example|template|hypothetical|historical)\b/i.test(unquoted)
|
||||
&& !/\b(?:another|different|separate|unrelated|other)\s+(?:cache|key|entry|reader|read|request)\b/i.test(unquoted)
|
||||
&& !/\b(?:trace|scenario|execution|sequence)\s+(?:is|was|remains)\s+impossible\b/i.test(unquoted)) return true;
|
||||
return !/["“”?]|\b(?:if|unless|whether|might|may|could|not|never|no\s+longer|example|template|quoted)\b/i.test(cell)
|
||||
&& !/\b(?:another|different|separate|unrelated|other)\s+(?:cache|key|entry|reader|read|request)\b/i.test(cell)
|
||||
&& !/\b(?:(?:this|that|the)\s+(?:trace|scenario|execution|sequence)|this|that|it)\s+(?:is|was|remains)\s+impossible\b/i.test(cell)
|
||||
&& sequence.test(cell));
|
||||
&& sequence.test(cell);
|
||||
});
|
||||
}
|
||||
|
||||
/** Explicit copied/example framing owns its section and descendant headings. */
|
||||
function assertedProseOwner(prose: string[], index: number): boolean {
|
||||
const owners = [{ level: 0, source: false }];
|
||||
for (const line of prose.slice(0, index + 1)) {
|
||||
const heading = /^(#{1,6})\s+(.+)$/.exec(line);
|
||||
if (heading) {
|
||||
while (owners.length > 1 && owners.at(-1)!.level >= heading[1]!.length) owners.pop();
|
||||
// An explicit fresh review ends an unheaded introductory source block.
|
||||
if (/^(?:Current|Actual)\s+(?:review|findings|assessment)\b/i.test(heading[2]!)) owners[0]!.source = false;
|
||||
owners.push({ level: heading[1]!.length, source: /\b(?:hypothetical|examples?|quoted|copied|historical|template|source)\b/i.test(heading[2]!) });
|
||||
} else if (/\b(?:unproven\s+hypothesis|(?:hypothetical|historical)\s+example|quoted\s+source)\b/i.test(line)
|
||||
|| /^(?:The\s+following\b|Below\b|This\s+(?:section|material|example)\b)[^.!?]*\b(?:copied|quoted|source|examples?|hypothetical|historical|template)\b/i.test(line.trim())) {
|
||||
owners.at(-1)!.source = true;
|
||||
}
|
||||
}
|
||||
return !owners.some(owner => owner.source);
|
||||
}
|
||||
|
||||
function hasProseStaleFillFinding(report: string): boolean {
|
||||
@@ -655,9 +685,12 @@ function hasProseStaleFillFinding(report: string): boolean {
|
||||
}).join('\n');
|
||||
// Independent list items and table rows cannot borrow each other's words.
|
||||
const blocks = prose.split(/\n\s*\n|\n(?=\s*(?:#{1,6}\s|\||\d+\.\s|[-*]\s))/).map(block => block.trim());
|
||||
const lines = blocks.flatMap(block => block.split('\n'));
|
||||
const normalize = (text: string) => text.replace(/[*_`]/g, '').replace(/\s+/g, ' ').trim();
|
||||
return blocks.some((block, index) => {
|
||||
const text = normalize(block);
|
||||
const owners = blocks.slice(0, index + 1).flatMap(part => part.split('\n'));
|
||||
if (!assertedProseOwner(owners, owners.length - 1)) return false;
|
||||
const stale = /\b(?:stale|outdated)\b|\b(?:old(?:er)?|pre[- ]write)\s+(?:value|data|result|version|snapshot)\b/i.test(text);
|
||||
const inFlight = /\b(?:race|racing|concurrent|concurrency|in[- ]flight|pending)\b/i.test(text)
|
||||
|| hasOrderedStaleFillOperations(text);
|
||||
@@ -681,7 +714,13 @@ function hasProseStaleFillFinding(report: string): boolean {
|
||||
// following dismissal cannot turn a traced race into positive coverage.
|
||||
const next = blocks[index + 1] ?? '';
|
||||
const independent = /^(?:#{1,6}(?:\s|\d)|\d+\.\s|[-*]\s|\||(?:[*_]+)?(?:Finding\b|Section\s|P[0-3]\b))/i.test(next);
|
||||
const context = text + (independent ? '' : ' ' + normalize(next));
|
||||
const explicitId = /^\|\s*(F[1-9]\d*)\s*\|/.exec(text)?.[1];
|
||||
const assessment = explicitId
|
||||
? structuredFindingAssessment(lines, owners.length - 1, owners.length - 1, [explicitId], assertedProseOwner).join(' ')
|
||||
: '';
|
||||
const context = text + (independent ? '' : ' ' + normalize(next)) + ' ' + normalize(assessment);
|
||||
const findingId = explicitId ?? 'F[1-9]\\d*';
|
||||
if (new RegExp(`\\b(?:(?:this|that|the)\\s+(?:finding|issue|gap|race)|${findingId})\\s+(?:is|was|remains)\\s+(?:withdrawn|rejected|dismissed)\\b`, 'i').test(context)) return false;
|
||||
|
||||
const finding = /\b(?:P[0-3]|missing|gap|bug|defect|violat\w*|unsafe|incorrect)\b|\bno\s+mention\s+of\s+(?:this|the)\s+race\b/i.test(context);
|
||||
const subsequentRead = /\b(?:next|later|subsequent|new|fresh|future)\s+(?:read\w*|request\w*|caller\w*)\b/i.test(context);
|
||||
@@ -691,7 +730,8 @@ function hasProseStaleFillFinding(report: string): boolean {
|
||||
/\bper[- ]key\s+(?:epoch|generation|version)\b/i.test(sentence));
|
||||
// Table cells and semicolon-separated statements have separate owners;
|
||||
// retain an explicit "that return" continuation with the return it names.
|
||||
const claims = context.split(/(?:[.!?]\s+|;\s+(?!that\s+return\b)|\s+\|\s+|\b(?:but|however|nevertheless|yet)\s*[:,]?\s+)/i);
|
||||
const claims = context.split(/\s*\|\s*/).flatMap(cell =>
|
||||
cell.split(/(?:[.!?]\s+|;\s+(?!that\s+return\b)|\b(?:but|however|nevertheless|yet)\s*[:,]?\s+)/i));
|
||||
const violation = claims.some(claim => /\b(?:violat\w*|break\w*)\b[^.!?]*\b(?:contract|guarantee|consistency|rule)\b/i.test(claim)
|
||||
&& !/\b(?:not|no|never)\b/i.test(claim));
|
||||
for (const [claimIndex, claim] of claims.entries()) {
|
||||
|
||||
@@ -2193,41 +2193,77 @@ function ceoAssertionMismatchBrief(q: NativePlanQuestionCall['questions'][number
|
||||
}
|
||||
|
||||
/** Native finding evidence when CEO mode selection is omitted or left unanswered. */
|
||||
function ceoNumberedBriefDecision(q: NativePlanQuestionCall['questions'][number], subject: string): boolean {
|
||||
function ceoNumberedBriefDecision(q: NativePlanQuestionCall['questions'][number], subject: string, inspectFullAssessment = false): boolean {
|
||||
// A numbered title may be declarative. Its current problem and proposed
|
||||
// decision still have to be present in the complete native question.
|
||||
const explanation = /^ELI10:\s*(.+)$/m.exec(q.question)?.[1] ?? '';
|
||||
const recommendation = /^Recommendation:\s*([1-9]\d*)?([A-Z])\b/im.exec(q.question);
|
||||
const label = (text: string) => /^([1-9]\d*)?([A-Z])[):.]\s*/i.exec(text.trim());
|
||||
if (!explanation || !recommendation || q.options.length < 2 || q.options.some(o => !o.description?.trim()) ||
|
||||
!q.options.some(o => { const token = label(o.label); return token && `${token[1] ?? ''}${token[2]}`.toLowerCase() === `${recommendation[1] ?? ''}${recommendation[2]}`.toLowerCase(); })) return false;
|
||||
const publicText = (text: string) => text.replace(/"(?:[^"\\]|\\.)*"|“[^”]*”|`[^`]*`/g, '""');
|
||||
const prose = publicText(q.question
|
||||
.replace(/```[\s\S]*?(?:```|$)|~~~[\s\S]*?(?:~~~|$)/g, '')
|
||||
.replace(/^(?:\s*>| {4}|\t).*$/gm, ''));
|
||||
const explanations = [...prose.matchAll(/^ELI10:\s*(.+)$/gm)];
|
||||
const recommendations = [...prose.matchAll(/^Recommendation:\s*([1-9]\d*)?([A-Z])\b/gim)];
|
||||
const explanation = explanations[0]?.[1] ?? '';
|
||||
// The opening declaration owns the assessment that follows. A source or
|
||||
// hypothetical frame cannot lend its later defect wording current status.
|
||||
const openingAssessment = explanation.trim().split(/[.!?]\s+/, 1)[0] ?? '';
|
||||
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()) ||
|
||||
/\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()) ||
|
||||
!q.options.some(o => { const token = label(o.label); return token && `${token[1] ?? ''}${token[2]}`.toLowerCase() === `${recommendation[1] ?? ''}${recommendation[2]}`.toLowerCase(); })) return false;
|
||||
if (/\b(?:this|that|the) (?:issue|finding|gap|problem|defect)\s+(?:is|was|has been)\s+(?:(?:already|now)\s+)?(?:resolved|fixed|closed|withdrawn|retracted|rejected)\b|\b(?:I|we)\s+(?:(?:have|has)\s+)?(?:withdraw|withdrawn|retract|retracted|resolve|resolved)\s+(?:this|that|the)\s+(?:finding|issue|question)\b/i.test(prose)) return false;
|
||||
if (prose.split(/[.!?;]\s+|\n/).some(clause =>
|
||||
/^(?:there\s+(?:is|are)\s+no\s+(?:current\s+)?|no\s+current\s+)(?:defect|gap|issue|problem)s?\b/i.test(clause.trim()))) return false;
|
||||
const findingIdentity = /\b(?:Finding|Issue)\s+F?([1-9]\d*(?:\.[1-9]\d*)*)\b/i.exec(prose.split('\n')[0]!);
|
||||
if (findingIdentity && new RegExp('\\b(?:(?:Finding|Issue)\\s+F?|F)' + findingIdentity[1]!.replace(/\./g, '\\.') + '\\s+(?:is|was|has been)\\s+(?:withdrawn|rejected|retracted|resolved)\\b', 'i').test(prose)) return false;
|
||||
if ([subject, explanation].some(text => /\b(?:gap|defect|issue|problem)\b[^.!?]{0,80}\b(?:already|now)\s+(?:resolved|fixed|closed)\b/i.test(publicText(text)))) return false;
|
||||
// A complete assessment can withdraw a historical problem in a later
|
||||
// sentence. Quoted old assessments do not make that current assertion.
|
||||
if ([subject, explanation].some(text => publicText(text).split(/[.!?;]\s+/).some(clause =>
|
||||
/^(?:there\s+(?:is|are)\s+no\s+(?:current\s+)?|no\s+current\s+)(?:defect|gap|issue|problem)s?\b/i.test(clause.trim())))) return false;
|
||||
const currentProblem = [subject, explanation].some(text => {
|
||||
const statement = publicText(text).split(/[.!?]\s+/)[0]!;
|
||||
const currentProblem = [subject, explanation].flatMap(text => {
|
||||
const statements = publicText(text).split(/[.!?]\s+/);
|
||||
return inspectFullAssessment ? statements : statements.slice(0, 1);
|
||||
}).some(statement => {
|
||||
// A numbered test may state its assertion gap through the regression it
|
||||
// cannot reject, without using the word "missing" or a question mark.
|
||||
const assertionGap = /^test\s+[1-9]\d*(?:\s+\([^()\n]*\))?\s+(?:cannot\s+(?:detect|catch|reject)\b[^.!?\n]*\bregressions|accepts\s+any\s+truthy\s+value)\b/i.test(statement.trim()) && ceoAssertionMismatchBrief(q);
|
||||
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) &&
|
||||
(assertionGap || /\b(?:missing|unspecified|unvalidated|unhandled)\b|\b(?:(?:has|with|leaves)\s+no|without)\s+(?:error handling|tests?|checks?|validation|coordination)\b|\b(?:asserts?|checks?)\s+only\b|\b(?:does not|doesn't|never)\s+(?:say|says|state|define|specify|cover|handle)\b/i.test(statement));
|
||||
!/\b(?:not|never|no longer|doesn't|does not)\s+(?:paste|send|deliver|receive)\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/i.test(statement));
|
||||
});
|
||||
const amendment = q.options.some(option => [option.label.replace(/^([1-9]\d*)?[A-Z][):.]\s*/i, ''), option.description ?? ''].some(text =>
|
||||
const amendment = q.options.some(option => {
|
||||
const token = label(option.label);
|
||||
const optionLabel = option.label.replace(/^([1-9]\d*)?[A-Z][):.]\s*/i, '');
|
||||
if (/^(?:keep|leave|preserve|save|archive|record|document|render|format|start|pause|resume|continue|finish|end|defer|proceed)\b/i.test(optionLabel) ||
|
||||
/^[a-z-]+\s+(?:(?:the|a|this|prior|previous|completed|reviewed|current|saved|stored|exact|expected|full|complete|whole)\s+)*(?:review\s+)?(?:plan|report|summary|note|record|document|log)\b/i.test(optionLabel)) return false;
|
||||
// The full brief may spell out an assertion while the native menu uses
|
||||
// an abbreviated label. Only that offered option's own numbered row can
|
||||
// supply the action; source quotations and neighboring choices cannot.
|
||||
const optionRows = token?.[1] ? [...prose.matchAll(new RegExp('^' + token[1] + token[2] + '[):.]\\s*(.+)$', 'gmi'))] : [];
|
||||
if (optionRows.length > 1) return false;
|
||||
const boundedConfiguration = /\b(?:no|without)\s+(?:cap|bound|timeout)\b/i.test(explanation) &&
|
||||
/^explicit\b[^.!?]*\b(?:timeout|budget|cap|bound)\b/i.test(optionLabel);
|
||||
const completeTestSuite = /\b(?:no|without)\s+(?:automated\s+)?tests?\b/i.test(explanation) &&
|
||||
/^full\s+(?:test\s+)?(?:matrix|table|suite)\b[^.!?]*\b(?:unit|integration|ordering|tests?)\b/i.test(optionLabel);
|
||||
return boundedConfiguration || completeTestSuite || [optionLabel, option.description ?? '', optionRows[0]?.[1] ?? ''].some(text =>
|
||||
publicText(text).split(/[.;]\s+/).some(clause =>
|
||||
/^(?:add|remove|replace|send|rescue|handle|validate|check|assert|pin|require|define|specify|guard|serialize|parameterize|use|implement|write)\b/i.test(clause.trim()) &&
|
||||
!/^[a-z]+\s+(?:(?:the|a|this|prior|previous|completed|reviewed|current|saved|stored)\s+)*(?:review\s+)?(?:plan|report|summary|note|record|document|log)\b/i.test(clause.trim()))));
|
||||
/^(?:add|remove|replace|send|rescue|handle|validate|check|assert|pin|deep-equal|require|define|specify|guard|serialize|parameterize|escape|use|implement|write)\b/i.test(clause.trim()) &&
|
||||
!/^[a-z-]+\s+(?:(?:the|a|this|prior|previous|completed|reviewed|current|saved|stored|exact|expected|full|complete|whole)\s+)*(?:review\s+)?(?:plan|report|summary|note|record|document|log)\b/i.test(clause.trim())));
|
||||
});
|
||||
return currentProblem && amendment;
|
||||
}
|
||||
|
||||
/** A descriptive menu header can accompany a fully numbered issue brief. */
|
||||
function ceoParenthesizedIssueBrief(q: NativePlanQuestionCall['questions'][number], number: string): boolean {
|
||||
const title = q.question.split('\n')[0]!;
|
||||
if (!/^D[1-9]\d*\s+\(Issue [1-9]\d*\)\s*[—–-]\s*(?:What|How|Which|Should)\b[^\n?]+\?$/i.test(title) ||
|
||||
if (!/^D[1-9]\d*\s+\(Issue [1-9]\d*(?:\.[1-9]\d*)*\)\s*[—–-]\s*(?:What|How|Which|Should)\b[^\n?]+\?$/i.test(title) ||
|
||||
/\b(?:hypothetical|example|template)\b/i.test(title)) return false;
|
||||
const prose = q.question
|
||||
.replace(/```[\s\S]*?(?:```|$)|~~~[\s\S]*?(?:~~~|$)/g, '')
|
||||
@@ -2235,8 +2271,9 @@ function ceoParenthesizedIssueBrief(q: NativePlanQuestionCall['questions'][numbe
|
||||
.replace(/"(?:[^"\\]|\\.)*"|“[^”]*”|`[^`]*`/g, '""');
|
||||
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]!;
|
||||
if (explanations.length !== 1 || recommendations.length !== 1 ||
|
||||
recommendations[0]![1] !== number || !/\w/.test(explanations[0]![1]!) ||
|
||||
recommendations[0]![1] !== section || !/\w/.test(explanations[0]![1]!) ||
|
||||
/^(?:if|unless|whether|example|template|hypothetical|historical|quoted)\b/i.test(explanations[0]![1]!.trim())) return false;
|
||||
// Current prose may explicitly withdraw an earlier issue. Literal examples
|
||||
// and attributed quotations cannot supply either the brief or its withdrawal.
|
||||
@@ -2250,13 +2287,13 @@ function ceoParenthesizedIssueBrief(q: NativePlanQuestionCall['questions'][numbe
|
||||
.replace(/^Completeness\s+\d+\/10\.\s*/i, '').split(/[.;]\s+/).some(clause =>
|
||||
/^(?:add|remove|replace|send|rescue|handle|validate|check|assert|pin|require|define|specify|guard|serialize|parameterize|use|implement|write)\b/i.test(clause.trim()) &&
|
||||
!/^[a-z]+\s+(?:(?:the|a|this|prior|previous|completed|reviewed|current|saved|stored)\s+)*(?:review\s+)?(?:plan|report|summary|note|record|document|log)\b/i.test(clause.trim()))));
|
||||
if (!missingContract || !amendment) return false;
|
||||
const headerNumber = /^(?:(?:Finding|Issue)\s+F?|F)([1-9]\d*)(?:\s+[a-z][a-z -]*)?$/i.exec(q.header.trim());
|
||||
if (number.includes('.') ? !ceoNumberedBriefDecision(q, title.replace(/^D[1-9]\d*\s+\(Issue [^)]+\)\s*[—–-]\s*/i, ''), true) : !missingContract || !amendment) return false;
|
||||
const headerNumber = /^(?:(?:Finding|Issue)\s+F?|F)([1-9]\d*(?:\.[1-9]\d*)*)(?:\s+[a-z][a-z -]*)?$/i.exec(q.header.trim());
|
||||
if (/^(?:finding|issue)\b|^f\d/i.test(q.header.trim()) && !headerNumber) return false;
|
||||
if (headerNumber && headerNumber[1] !== number) return false;
|
||||
const labels = q.options.map(option => /^([1-9]\d*)([A-Z])[):.]\s*\S/i.exec(option.label));
|
||||
return q.options.length >= 2 && q.options.every((option, i) =>
|
||||
Boolean(option.description?.trim()) && labels[i]?.[1] === number) &&
|
||||
Boolean(option.description?.trim()) && labels[i]?.[1] === section) &&
|
||||
new Set(labels.map(label => label![2]!.toUpperCase())).size === labels.length &&
|
||||
labels.some(label => label![2]!.toUpperCase() === recommendations[0]![2]!.toUpperCase());
|
||||
}
|
||||
@@ -2334,10 +2371,21 @@ function nativeExplicitCeoFinding(fp: AskUserQuestionFingerprint, allowQuestionI
|
||||
/\b(?:exact|exactly|full|complete)\b/i.test(directAssertion[1]!) &&
|
||||
!/\b(?:if|unless|hypothetical|no|not|already)\b/i.test(normalized) &&
|
||||
ceoAssertionMismatchBrief(q) && ceoNumberedBriefDecision(q, normalized)) return true;
|
||||
const identity = /^(Finding|Issue)\s+F?([1-9]\d*(?:\.[1-9]\d*)*)(?:\s+\(Section\s+[1-9]\d*\))?\s*:\s*([^\n]+)$/i.exec(normalized);
|
||||
const identity = /^(Finding|Issue)\s+F?([1-9]\d*(?:\.[1-9]\d*)*)(?:\s+\(Sections?\s+[1-9]\d*(?:\s+(?:and|&)\s+[1-9]\d*|,\s*[1-9]\d*)*(?:,\s*[a-z][a-z -]*)?\))?\s*:\s*([^\n]+)$/i.exec(normalized);
|
||||
const annotation = /\((Sections?\s+[^)]+)\)/i.exec(normalized)?.[1];
|
||||
if (identity && annotation && !/^Section\s+[1-9]\d*$/i.test(annotation)) {
|
||||
if (/\b(?:hypothetical|example|template|historical|quoted)\b/i.test(annotation) ||
|
||||
!ceoNumberedBriefDecision(q, identity[3]!)) return false;
|
||||
const recommended = /^Recommendation:\s*([1-9]\d*)?([A-Z])\b/im.exec(q.question);
|
||||
const labels = q.options.map(option => /^([1-9]\d*)?([A-Z])[):.]\s*\S/i.exec(option.label));
|
||||
if (!recommended || labels.some(token => !token || (token[1] ?? '') !== (recommended[1] ?? '')) ||
|
||||
(recommended[1] && recommended[1] !== identity[2]) ||
|
||||
new Set(labels.map(token => token![2]!.toUpperCase())).size !== labels.length) return false;
|
||||
}
|
||||
const numberedSubject = /^([1-9]\d*(?:\.[1-9]\d*)+)\s+([^:\n]+):\s*([^\n]+)$/.exec(normalized);
|
||||
const parenthesized = /^D[1-9]\d*\s+\(issue\s+([1-9]\d*(?:\.[1-9]\d*)*)\)\s*[—–-]\s*[^\n?]+\?$/i.exec(title);
|
||||
if (parenthesized && allowQuestionId) return ceoParenthesizedIssueBrief(q, parenthesized[1]!);
|
||||
if (parenthesized && (allowQuestionId || (parenthesized[1]!.includes('.') &&
|
||||
!/^(?:(?:Finding|Issue)\s+F?|F)[1-9]\d*/i.test(q.header.trim())))) return ceoParenthesizedIssueBrief(q, parenthesized[1]!);
|
||||
if (identity && !/^[^\n?]+\?$/.test(identity[3]!) && !ceoNumberedBriefDecision(q, identity[3]!)) return false;
|
||||
if (numberedSubject && (numberedSubject[2]!.trim().toLowerCase() !== q.header.trim().toLowerCase() ||
|
||||
!ceoNumberedBriefDecision(q, numberedSubject[3]!))) return false;
|
||||
@@ -2365,7 +2413,7 @@ export const ceoFirstReviewAUQ: Step0BoundaryPredicate = (fp) =>
|
||||
const id = /<gstack-qid:\s*(?:plan-)?ceo-(?:review-)?([a-z0-9-]+)/i.exec(q.question)?.[1];
|
||||
if (!id || /(?:^|-)(?:scope|mode|approach|routing|office-hours|prerequisites?|setup|next-steps|completion)(?:-|$)/i.test(id)) return false;
|
||||
const title = q.question.split('\n')[0];
|
||||
if (/^D[1-9]\d*\s+\(Issue\s+[1-9]\d*\)\s*[—–-]/i.test(title)) return nativeExplicitCeoFinding(fp, true);
|
||||
if (/^D[1-9]\d*\s+\(Issue\s+[1-9]\d*(?:\.[1-9]\d*)*\)\s*[—–-]/i.test(title)) return nativeExplicitCeoFinding(fp, true);
|
||||
if (!/^(?:D\s*\d+\s*[—–-]|(?:Finding|Section)\s*\d+)/i.test(title)) return false;
|
||||
if (/^(?:D\s*\d+\s*[—–-]\s*)?(?:Finding|Issue)\s+F?\d/i.test(title)) return nativeExplicitCeoFinding(fp, true);
|
||||
if (/\bfinding\b|\bmissing\b|\bambiguous\b|\bundefined\b|doesn['’]t\s+(?:define|specify|cover|mention)/i.test(title)) return true;
|
||||
|
||||
@@ -1,5 +1,55 @@
|
||||
import type { AskUserQuestionFingerprint } from './claude-pty-runner';
|
||||
|
||||
/** Recording or skipping an explicitly deferred typography TODO preserves the
|
||||
* current design. Selecting its build-now alternative remains a review choice.
|
||||
*/
|
||||
function deferredTypographyTodo(q: NonNullable<AskUserQuestionFingerprint['nativeCall']>['questions'][number], selected: string): boolean {
|
||||
const clean = (text: string) => text.trim().replace(/\s+/g, ' ');
|
||||
const lines = q.question.trim().split('\n').map(clean);
|
||||
const title = /^D[1-9]\d* [—–-] TODO proposal: (?:record|add) a deferred TODOS\.md (?:item|note) to (?:evaluate|explore|consider) [^?\n]+ \(replacing ([A-Za-z][A-Za-z0-9-]{0,39})\) (?:in a later|during a future) design pass\?$/.exec(lines[0] ?? '');
|
||||
if (!title || !/^TODO [1-9]\d*$/.test(q.header) || lines.length !== 7 ||
|
||||
!/^Project\/branch\/task: [A-Za-z0-9_./-]+, \/plan-design-review of PLAN\.md, post-pass TODOS\.md updates\.$/.test(lines[1]!)) return false;
|
||||
const font = title[1]!;
|
||||
const assessment = lines[2]!;
|
||||
// Bind the current scope before reading the optional explanation of future
|
||||
// value. Font examples, effort estimates and brand prose are not evidence.
|
||||
const scope = new RegExp(`^ELI10: DESIGN\\.md and (?:this|the current) plan (?:keep|retain|preserve) ${font} as the app font(?:, and you excluded visual exploration from this update|\\. Visual exploration (?:remains|is) out of scope for this update), so (?:nothing changes now|the current design remains unchanged)\\.`);
|
||||
const recordOnly = /\bThis question is only about whether to (?:write|record) [^.]+ in TODOS\.md [^.]*\bfuture \/design-consultation\b[^.]*, not about changing anything here\./.test(assessment)
|
||||
|| /\bThis question only (?:records|skips) a deferred TODOS\.md (?:item|note) for a future \/design-consultation; it does not change the current design\./.test(assessment);
|
||||
if (!scope.test(assessment) || !recordOnly ||
|
||||
!/^Stakes if we pick wrong: .+\.$/.test(lines[3]!) ||
|
||||
!/^Recommendation: A\b/.test(lines[4]!) || !/\bout[- ]of[- ]scope\b/.test(lines[4]!) ||
|
||||
!/^Note: options differ in kind, not coverage\b/.test(lines[5]!) ||
|
||||
!/^Net: .+\.$/.test(lines[6]!)) return false;
|
||||
const choices = ['A Add to TODOS.md', 'B Skip, not valuable enough', 'C Build it now in this PR'];
|
||||
const labels = q.options.map(o => clean(o.label).replace(/ \(recommended\)$/i, ''));
|
||||
if (labels.length !== 3 || choices.some(label => labels.filter(l => l === label).length !== 1)) return false;
|
||||
const description = (label: string) => clean(q.options[labels.indexOf(label)]!.description!);
|
||||
const [add, skip, build] = choices.map(description) as [string, string, string];
|
||||
const selectedLabel = labels[q.options.findIndex(o => o.label === selected)];
|
||||
if (selectedLabel !== choices[0] && selectedLabel !== choices[1]) return false;
|
||||
// A and B are record/skip-only choices; C is explicitly an implementation
|
||||
// alternative. Additional present-work instructions invalidate the boundary.
|
||||
const all = [...lines, add, skip, build].join('\n');
|
||||
if (/\b(?:Correction|Hypothetical|Source only|Example only)\s*:|\b(?:this (?:scope|proposal|deferment)|the (?:scope|proposal|deferment)) (?:is|was) (?:withdrawn|cancelled|rejected)\b/i.test(all) ||
|
||||
/\b(?:Also|Additionally|Instead|Now|Then)\s+(?:we\s+)?(?:must\s+|will\s+)?(?:fix|replace|change|implement|build|add|load|remove)\b/i.test(all) ||
|
||||
/\b(?:font replacement|visual exploration|typography work)\s+(?:is|becomes|remains)\s+(?:now\s+)?in scope\b/i.test(all)) return false;
|
||||
const unquoted = (text: string) => text.replace(/"[^"\n]*"|“[^”\n]*”/g, '[quoted]');
|
||||
const retained = unquoted([...lines, add, skip].join('\n'));
|
||||
if (/\bVisual exploration (?:is|remains) (?:no longer|not) out of scope\b/i.test(retained) ||
|
||||
new RegExp(`\\b(?:This|The current) plan (?:no longer|does not) (?:keeps?|retains?|preserves?) ${font}\\b`, 'i').test(retained)) return false;
|
||||
const recording = unquoted(add + '\n' + skip).split(/[.!?]\s+|[✅❌]|\n/).map(clean);
|
||||
if (recording.some(sentence => /^(?:Add|Create|Fix|Replace|Change|Implement|Build|Load|Remove|Set|Make)\b/i.test(sentence) &&
|
||||
(!/^(?:Add|Create) (?:a |the |one )?TODOS\.md (?:file|item|note)(?: for (?:the )?(?:future|deferred|later) [A-Za-z0-9 /_-]+)?\.?$/i.test(sentence) || /\b(?:and|then|plus)\s+(?:add|create|fix|replace|change|implement|build|load|remove|set|make)\b/i.test(sentence)))) return false;
|
||||
if (/\b(?:it is false that|not true that|if approved|no longer preserves?)\b/i.test(retained) ||
|
||||
/\b(?:replace|change|implement|build|load|remove)\b[^.!?\n]*\b(?:now|in this PR|in this update)\b/i.test(retained.replace(/not about changing anything here\./g, '').replace(/do not add it now/g, 'deferred'))) return false;
|
||||
const preservesFont = new RegExp(`(?:Nothing changes|No design changes) in this update; DESIGN\\.md and ${font} (?:stay as approved|remain unchanged)\\.`);
|
||||
return /\b(?:next|future) \/design-consultation\b/.test(add) && preservesFont.test(add) &&
|
||||
/\b(?:Adds a TODOS\.md file|Records only a TODOS\.md note)\b/.test(add) &&
|
||||
/\b(?:No TODOS\.md noise|No TODO is recorded)\b/.test(skip) && /\b(?:Zero follow-up work|No follow-up work)\./.test(skip) &&
|
||||
/\b(?:immediately|now|this PR)\b/i.test(build) && /\b(?:Fonts? load|Replace the font|Change the font)\b/i.test(build);
|
||||
}
|
||||
|
||||
/** Accepted rendering of existing decisions adds an artifact, not a finding.
|
||||
* It still changes the deliverable and therefore remains a freshness boundary.
|
||||
*/
|
||||
@@ -10,13 +60,15 @@ export function isDesignArtifactGeneration(fp: AskUserQuestionFingerprint): bool
|
||||
call.unansweredQuestionIndices.length || !Number.isFinite(Date.parse(call.answeredAt ?? '')) ||
|
||||
fp.signature !== `${call.sessionId}:${call.toolUseId}`) return false;
|
||||
const q = call.questions[0]!;
|
||||
if (q.multiSelect || q.options.length !== 2 || Object.keys(call.answers ?? {}).length !== 1 ||
|
||||
if (q.multiSelect || q.options.length < 2 || q.options.length > 3 || Object.keys(call.answers ?? {}).length !== 1 ||
|
||||
q.options.some(o => typeof o.description !== 'string' || ('preview' in o && Boolean(o.preview))) ||
|
||||
fp.options.length !== q.options.length || fp.options.some((o, i) => o.index !== i + 1 || o.label !== q.options[i]!.label)) return false;
|
||||
const clean = (text: string) => text.trim().replace(/\s+/g, ' ');
|
||||
const label = (text: string) => clean(text).replace(/^[AB]\) /, '').replace(/ \(Recommended\)$/, '');
|
||||
const positive = q.options.find(o => call.answers?.[q.question] === o.label);
|
||||
if (!positive) return false;
|
||||
if (q.options.length === 3) return (fp.nativeQuestionIndex === undefined || fp.nativeQuestionIndex === 0) &&
|
||||
deferredTypographyTodo(q, positive.label);
|
||||
const other = q.options.find(o => o !== positive)!;
|
||||
const question = clean(q.question);
|
||||
const description = clean(positive.description!);
|
||||
|
||||
@@ -78,17 +78,30 @@ 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 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]!);
|
||||
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'),
|
||||
new RegExp(`^(?:✅\\s*)?${primary[1]} becomes the single filled primary \\(#[0-9a-f]{6}, (?:white|black) text\\); ` +
|
||||
'[A-Za-z][A-Za-z0-9 /,_-]{0,99} become neutral ghost buttons exactly as DESIGN\\.md specifies\\b', 'i'),
|
||||
new RegExp(`^(?:✅\\s*)?${primary[1]} is the single filled #[0-9a-f]{6} button; ` +
|
||||
'[A-Za-z][A-Za-z0-9 /,_-]{0,99} become neutral ghosts, exactly per DESIGN\\.md\\b', 'i'),
|
||||
];
|
||||
const primaryHeader = !q.header.includes(':') || q.header.split(':')[1]!.trim().toLowerCase() === primary?.[1]?.toLowerCase();
|
||||
const primaryRepair = primaryHeader && amendments &&
|
||||
q.options.some(o => amendments.some(pattern => pattern.test(o.description ?? ''))) &&
|
||||
opposed.some(o => /\b(?:Leaves a documented DESIGN\.md violation in place|Ships the documented violation;[^.\n]*\bthe gap remains open)\b/i.test(o.description ?? ''));
|
||||
// The style wordings share one owned decision: a current equal-weight gap,
|
||||
// a named control's DESIGN.md amendment, and a different choice retaining it.
|
||||
const assessment = /^ELI10: (.+)$/m.exec(q.question)?.[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)\\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 choiceIds = q.options.map(o => /^([1-9]\d*)[A-Z](?:[).:]?\s+)/.exec(o.label));
|
||||
const primaryRepair = primaryHeader && amendments && currentPrimary &&
|
||||
!!call.answeredAt && Number.isFinite(Date.parse(call.answeredAt)) &&
|
||||
choiceIds.every(id => id?.[1] === issue[1]) &&
|
||||
!withdrawn.test(q.question) &&
|
||||
q.options.some(amendment => amendments.some(pattern => pattern.test(amendment.description ?? '')) &&
|
||||
!withdrawn.test(amendment.description ?? '') && opposed.some(defer => defer !== amendment &&
|
||||
!withdrawn.test(defer.description ?? '') &&
|
||||
/(?:^|❌\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)\b/i.test(defer.description ?? '')));
|
||||
return opposed.length > 0 && !!(repair || primaryRepair);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,9 +5,9 @@ export const ENG_DECISION_SEEDS = ['complexity', 'shared-cache', 'swallowed-erro
|
||||
type Seed = typeof ENG_DECISION_SEEDS[number];
|
||||
|
||||
// Ignore displayed examples/code, while retaining inline code identifiers.
|
||||
function prose(text: string): string {
|
||||
function prose(text: string, omitLiteralProse = false): string {
|
||||
let fence: string | undefined;
|
||||
return text.split('\n').filter(line => {
|
||||
const lines = text.split('\n').filter(line => {
|
||||
const mark = line.match(/^ {0,3}(`{3,}|~{3,})/);
|
||||
if (mark) {
|
||||
if (!fence) fence = mark[1];
|
||||
@@ -15,7 +15,8 @@ function prose(text: string): string {
|
||||
return false;
|
||||
}
|
||||
return !fence && !/^(?: {0,3}>| {4}|\t)/.test(line);
|
||||
}).join('\n').replace(/[`*]/g, '');
|
||||
}).join('\n');
|
||||
return (omitLiteralProse ? lines.replace(/`([^`]+)`/g, (span, body: string) => /\s/.test(body) ? '' : span) : lines).replace(/[`*]/g, '');
|
||||
}
|
||||
|
||||
function seedSubjects(q: NativePlanQuestionCall['questions'][number]): Seed[] {
|
||||
@@ -27,12 +28,26 @@ function seedSubjects(q: NativePlanQuestionCall['questions'][number]): Seed[] {
|
||||
const offered = q.options.map(o => `${o.label} ${o.description ?? ''}`).join('\n');
|
||||
const directAction = title.match(/\b(?:should|shall|can|do|would)\s+(?:we|I)\s+([^?]+)\?\s*$/i)?.[1];
|
||||
const action = (re: RegExp) => re.test(offered) || Boolean(directAction && new RegExp(`^(?:${re.source})`, re.flags).test(directAction));
|
||||
// A shared adapter title may name its cache in the issue's own asserted
|
||||
// explanation. Options alone or a neighboring source excerpt cannot do so.
|
||||
const adapterPublic = prose(q.question, true);
|
||||
const adapterExplanations = [...adapterPublic.matchAll(/^ELI10:\s*(.+)$/gm)];
|
||||
const adapterPrefix = adapterPublic.slice(0, adapterExplanations[0]?.index ?? 0).split('\n').filter(line => line.trim()).slice(1);
|
||||
const adapterMetadata = adapterPrefix.join(' ').replace(/"[^"\n]*"|“[^”\n]*”/g, '');
|
||||
const adapterExplanation = adapterExplanations.length === 1 && adapterPrefix.every(line => /^(?:Project\/branch\/task:|\[P[0-3]\])/.test(line))
|
||||
&& !/\b(?:copied|quoted|source|hypothetical|historical)\s+(?:(?:source|quoted)\s+)?(?:example|excerpt|text|material)\b|\b(?:ELI10|assessment|finding)\s+is\s+not\s+(?:a\s+)?current\b/i.test(adapterMetadata)
|
||||
? adapterExplanations[0]![1]! : '';
|
||||
const adapterAssessment = adapterPublic.replace(/"[^"\n]*"|“[^”\n]*”/g, '');
|
||||
const sharedAdapter = /\bwrite-after-invalidate race\b/i.test(title)
|
||||
&& /\bSessionMint\b/.test(title) && /\bAuthBroker\b/.test(title) && /\bshared adapter\b/i.test(title)
|
||||
&& /^(?:Even after injection,\s*)?(?:both|the two) services (?:write into|mutate) the same cache\./i.test(adapterExplanation)
|
||||
&& !/\b(?:(?:this|that|the) (?:issue|finding|race)|Issue\s+[1-9]\d*)\s+(?:is|was|has been)\s+(?:withdrawn|retracted|rejected|resolved|fixed)\b|\bno (?:current )?shared-cache race\b/i.test(adapterAssessment);
|
||||
const ids: Seed[] = [];
|
||||
if (/\b(?:scope|complexity|classes|types|abstractions)\b/i.test(title) &&
|
||||
/\b(?:files|classes|types|abstractions)\b/i.test(title) &&
|
||||
action(/\b(?:reduce|cut|simplify|remove|collapse|merge|pure function)\b/i)) ids.push('complexity');
|
||||
if (/\b(?:AuthCache|cache)\b/i.test(title) &&
|
||||
/\b(?:global|module[ -]level|mutab\w*|both|shar\w*|ownership|writers|same\s+(?:AuthCache|cache))\b/i.test(title) &&
|
||||
if ((sharedAdapter || /\b(?:AuthCache|cache)\b/i.test(title) &&
|
||||
/\b(?:global|module[ -]level|mutab\w*|both|shar\w*|ownership|writers|same\s+(?:AuthCache|cache))\b/i.test(title)) &&
|
||||
action(/\b(?:inject\w*|DI|serializ\w*|single[ -]writer|ownership|composition root)\b/i)) ids.push('shared-cache');
|
||||
if (/\b(?:validateAndDispatch|catch\w*)\b/i.test(title) &&
|
||||
/\b(?:swallow\w*|nested|silent\w*|hidden|suppres\w*)\b/i.test(title) &&
|
||||
@@ -133,6 +148,44 @@ function declaredLegacyCharacterization(text: string): boolean {
|
||||
}
|
||||
}
|
||||
}
|
||||
// A mandatory snapshot can state the legacy oracle as a required test
|
||||
// list item, then bind it to the numbered task's unchanged-code verification.
|
||||
const snapshotSource = prose(text, true).replace(/\s+/g, ' ');
|
||||
const unquoted = (body: string) => body.replace(/"[^"\n]*"|“[^”\n]*”/g, '');
|
||||
const suiteWithdrawn = current.some(s => {
|
||||
// A named foreign suite owns its generic withdrawal; it cannot cancel
|
||||
// the legacy obligation in another section of the same report.
|
||||
const namedSuite = /^(.*?)\b(?:regression|characterization)\s+(?:suite|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\s+)?(?:regression|characterization)\s+(?:suite|tests?)\b/i.exec(statement.trim());
|
||||
return Boolean(subject && (!foreignSuite || subject[1]) && withdrawn(statement));
|
||||
});
|
||||
});
|
||||
for (const section of current.filter(s => /^Required tests(?: \([^\n]*\))?$/i.test(s.title))) {
|
||||
const body = section.body.join('\n').trim();
|
||||
if (!body.startsWith('- ')) continue;
|
||||
for (const block of body.split(/\n(?=-\s)/)) {
|
||||
const claim = block.replace(/\s+/g, ' ').trim();
|
||||
if (suiteWithdrawn || !/^- CRITICAL regression legacyAuthFlow(?:\(\))? snapshot: capture current outputs for [^;.!?]{1,240} BEFORE any change; assert both legacy \(flag OFF\) and new \(flag ON\) paths produce identical observable results\. Mandatory under the coverage-audit regression rule\./.test(claim)
|
||||
|| !snapshotSource.includes(claim) || withdrawn(unquoted(claim))
|
||||
|| /["“”]|\b(?:not|never|maybe|might|could|if|unless|optional|hypothetical|unproven)\b/i.test(claim)) continue;
|
||||
for (const tasks of current.filter(s => s.title === 'Implementation Tasks')) {
|
||||
const body = tasks.body.join('\n').trim();
|
||||
const taskPrefix = body.split(/\n(?=-\s)/)[0]?.trim() ?? '';
|
||||
if (!body.startsWith('- ') && (!/^Synthesized from (?:this|the) review's findings\./.test(taskPrefix)
|
||||
|| /\b(?:if|unless|optional|hypothetical|example|source|quoted|unproven)\b/i.test(taskPrefix))) continue;
|
||||
for (const task of body.split(/\n(?=-\s)/)) {
|
||||
const match = /^- (?:\[[ xX]\] )?(T[1-9]\d*)(?: \([^\n)]*\))? [—–] [A-Za-z][\w-]*(?:\/[A-Za-z][\w-]*)+ [—–] Snapshot legacyAuthFlow\(\) behavior as regression tests before any change[\t ]*(?:\n|$)/.exec(task);
|
||||
if (!match || !snapshotSource.includes(task.replace(/\s+/g, ' ').trim())
|
||||
|| !/^\s+- Verify: tests pass against unmodified legacy code, then against flag-OFF route[\t ]*$/m.test(task)
|
||||
|| 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+(?: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'))))) return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,39 @@
|
||||
import type { NativePublicToolEvent, PlanCountTranscript } from './plan-count-transcript';
|
||||
|
||||
/** An asserted correction can retract a declaration; quoted source cannot. */
|
||||
function withdrawsPlanSelection(message: string, title: string): boolean {
|
||||
let fence: { char: string; length: number } | undefined;
|
||||
let source = false;
|
||||
const assertions: string[] = [];
|
||||
for (const line of message.split(/\r?\n/)) {
|
||||
const mark = /^ {0,3}(`{3,}|~{3,})(.*)$/.exec(line);
|
||||
if (mark) {
|
||||
if (!fence) fence = { char: mark[1]![0]!, length: mark[1]!.length };
|
||||
else if (mark[1]![0] === fence.char && mark[1]!.length >= fence.length && !mark[2]!.trim()) fence = undefined;
|
||||
continue;
|
||||
}
|
||||
if (fence || /^(?:\s*>| {4}|\t)/.test(line)) continue;
|
||||
const text = line.trim();
|
||||
if (/^(?:#{1,6}\s+)?(?:Current|Actual)\s+(?:assessment|scope|selection)\b/i.test(text)) source = false;
|
||||
else if (/^(?:#{1,6}\s+)?(?:Source|Example|Historical|Quoted|Original message|Expected output)\b/i.test(text)
|
||||
|| /^(?:The following|This is)\b[^.!?]*\b(?:source|example|hypothetical|quoted)\b/i.test(text)) source = true;
|
||||
if (!source) assertions.push(text);
|
||||
}
|
||||
for (const statement of assertions.join('\n').split(/(?<=[.!?])\s+|\n+/).map(line => line.trim())) {
|
||||
if (statement.endsWith('?')) continue;
|
||||
const claim = statement.replace(/^Correction:\s*/i, '');
|
||||
const plain = claim.replace(/"[^"\n]*"|“[^”\n]*”|`[^`\n]*`/g, '[quoted]');
|
||||
if (/^(?:The|This|That|My)\s+(?:(?:scope|target)\s+)?(?:selection|declaration)\s+(?:is|was|has been|remains)\s+(?:now\s+)?(?:withdrawn|retracted|cancelled|canceled|hypothetical)\b/i.test(plain)
|
||||
|| /^(?:(?:I|We)\s+(?:have\s+)?)?(?:withdrawn?|withdrew|retract(?:ed)?|cancel(?:led|ed)?|disregard(?:ed)?|ignore(?:d)?)\s+(?:this|that|the|my)\s+(?:selection|declaration)\b/i.test(plain)) return true;
|
||||
const changedTarget = /^(?:The|This|My)\s+(?:selected|review)\s+target\s+is\s+(?:now\s+)?(.+?)[.!?]?$/i.exec(claim);
|
||||
if (changedTarget) {
|
||||
const target = changedTarget[1]!.replace(/^(?:the\s+)/i, '').replace(/["“”`]/g, '').replace(/\s+(?:draft(?:\s+plan)?|plan)[.!?]?$/i, '').trim();
|
||||
if (target.toLowerCase() !== title.toLowerCase()) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** A selected pasted plan may be announced by name instead of the menu letter. */
|
||||
export function nativeSeededPlanSelection(
|
||||
transcript: PlanCountTranscript,
|
||||
@@ -21,6 +55,9 @@ export function nativeSeededPlanSelection(
|
||||
event.sessionId === opts.sessionId && event.toolUseId === call.toolUseId && event.isError === false &&
|
||||
Number.isFinite(at(event.timestamp)) && at(event.timestamp) >= at(call.timestamp)));
|
||||
if (loaded.length !== 1) return false;
|
||||
const remainsSelected = (timestamp: string) => !transcript.assistantMessages.some(later =>
|
||||
later.sessionId === opts.sessionId && Number.isFinite(at(later.timestamp)) &&
|
||||
at(later.timestamp) >= at(timestamp) && withdrawsPlanSelection(later.text, title));
|
||||
for (const message of transcript.assistantMessages) {
|
||||
if (message.sessionId !== opts.sessionId || !Number.isFinite(at(message.timestamp)) ||
|
||||
at(message.timestamp) < at(loaded[0]!.timestamp)) continue;
|
||||
@@ -30,20 +67,21 @@ export function nativeSeededPlanSelection(
|
||||
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 selected = selectedNow ?? /^(?:I'll review|I will review|I'm proceeding with reviewing|I am proceeding with reviewing)\s+(?:the\s+)?(?:"([^"\n]+)"|“([^”\n]+)”|`([^`\n]+)`)\s+(?:draft(?:\s+plan)?|plan)\s+(?:you pasted|pasted here)(.*)$/i.exec(text);
|
||||
const selected = selectedNow ?? /^(?:I'll review|I will review|I'll go with reviewing|I will go with reviewing|I'm proceeding with reviewing|I am proceeding with reviewing)\s+(?:the\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]!;
|
||||
if (/\b(?:if|unless|assuming|pending|only after|instead|not|won't|cannot)\b/i.test(tail)) continue;
|
||||
if (/\b(?:retract|withdraw|cancel|disregard|ignore)\s+(?:that|this|the|my)\s+(?:selection|declaration)\b/i.test(tail)) continue;
|
||||
if (/\b(?:treat|consider|regard)\s+(?:that|this|the|my)\s+(?:selection|declaration)\s+as\s+(?:a\s+)?(?:hypothetical|example|proposal)\b/i.test(tail)) continue;
|
||||
if (/\b(?:that|this|the|my)\s+(?:selection|declaration)\s+(?:is|was)\s+(?:withdrawn|cancelled|canceled|hypothetical|retracted)\b/i.test(tail)) continue;
|
||||
if (selectedNow) {
|
||||
// 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.
|
||||
if (/\b(?:retract|withdraw|cancel|disregard|ignore)\s+(?:that|this|the|my)\s+(?:selection|declaration)\b/i.test(tail)) continue;
|
||||
if (/\b(?:treat|consider|regard)\s+(?:that|this|the|my)\s+(?:selection|declaration)\s+as\s+(?:a\s+)?(?:hypothetical|example|proposal)\b/i.test(tail)) continue;
|
||||
// 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)) 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\b(?:[^.!?]|\.(?=\S))*\.)?$/i.test(tail) && remainsSelected(message.timestamp)) return true;
|
||||
continue;
|
||||
}
|
||||
if (/^(?:\.(?:\s+(?:Next,|Then\b).*)?|,\s*(?:starting|beginning)\s+with\b.*|)$/.test(tail)) return true;
|
||||
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[^?]*\.|)$/.test(tail) && remainsSelected(message.timestamp)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -103,9 +103,9 @@ export const E2E_TOUCHFILES: Record<string, string[]> = {
|
||||
'plan-ceo-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-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/**'],
|
||||
'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/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', '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/**'],
|
||||
'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/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', '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/**'],
|
||||
'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/**'],
|
||||
// Covers ceo (preamble misfire) + eng/design (scope-gate bypass must not
|
||||
@@ -155,21 +155,21 @@ export const E2E_TOUCHFILES: Record<string, string[]> = {
|
||||
'tpa-absent-darwin': ['scripts/resolvers/third-party-actions.ts', 'ship/SKILL.md.tmpl', 'ship/sections/apple-release.md.tmpl', 'scripts/gen-skill-docs.ts', 'test/helpers/session-runner.ts', 'test/skill-e2e-third-party-actions.test.ts', 'test/helpers/third-party-actions.ts'],
|
||||
'tpa-apple-ban': ['scripts/resolvers/third-party-actions.ts', 'ship/SKILL.md.tmpl', 'ship/sections/apple-release.md.tmpl', 'scripts/gen-skill-docs.ts', 'test/helpers/session-runner.ts', 'test/skill-e2e-third-party-actions.test.ts', 'test/helpers/third-party-actions.ts'],
|
||||
'ship-section-loading': ['ship/**', 'scripts/resolvers/sections.ts', 'scripts/gen-skill-docs.ts', 'test/helpers/auq-sdk-capture.ts', 'test/helpers/session-runner.ts', 'test/session-runner-tools.test.ts', 'test/skill-e2e-ship-section-loading.test.ts'],
|
||||
'plan-ceo-section-loading': ['plan-ceo-review/**', 'test/skill-ceo-section-ordering.test.ts', 'scripts/resolvers/sections.ts', 'scripts/gen-skill-docs.ts', 'test/helpers/auq-sdk-capture.ts', 'test/helpers/session-runner.ts', 'test/session-runner-tools.test.ts', 'test/helpers/ceo-section-loading-fixture.ts', 'test/ceo-section-loading-fixture.test.ts', 'test/skill-e2e-plan-ceo-review-section-loading.test.ts', 'test/fixtures/ceo-section-loading-l-report.md', 'test/fixtures/ceo-section-loading-q-report.md', 'test/fixtures/ceo-section-r-rejected-report.md', 'test/fixtures/ceo-section-s-trace-report.md', 'test/fixtures/ceo-section-u-report.md', 'test/fixtures/ceo-section-y-report.md', 'test/fixtures/ceo-section-aa-report.md', 'test/sdk-stale-table-ad-v3.test.ts', 'test/fixtures/sdk-stale-table-ad-v3.json', 'test/sdk-ordering-ae.test.ts', 'test/fixtures/sdk-ordering-ae.json', 'test/sdk-columnar-af.test.ts', 'test/fixtures/sdk-columnar-af.json', 'test/sdk-order-b-ag.test.ts', 'test/fixtures/sdk-order-b-ag.json', 'test/sdk-schedule-continuation-ah.test.ts', 'test/fixtures/sdk-schedule-continuation-ah.json', 'test/sdk-original-order-ai.test.ts', 'test/fixtures/sdk-original-order-ai.json'],
|
||||
'plan-ceo-section-loading': ['plan-ceo-review/**', 'test/skill-ceo-section-ordering.test.ts', 'scripts/resolvers/sections.ts', 'scripts/gen-skill-docs.ts', 'test/helpers/auq-sdk-capture.ts', 'test/helpers/session-runner.ts', 'test/session-runner-tools.test.ts', 'test/helpers/ceo-section-loading-fixture.ts', 'test/ceo-section-loading-fixture.test.ts', 'test/skill-e2e-plan-ceo-review-section-loading.test.ts', 'test/fixtures/ceo-section-loading-l-report.md', 'test/fixtures/ceo-section-loading-q-report.md', 'test/fixtures/ceo-section-r-rejected-report.md', 'test/fixtures/ceo-section-s-trace-report.md', 'test/fixtures/ceo-section-u-report.md', 'test/fixtures/ceo-section-y-report.md', 'test/fixtures/ceo-section-aa-report.md', 'test/sdk-stale-table-ad-v3.test.ts', 'test/fixtures/sdk-stale-table-ad-v3.json', 'test/sdk-ordering-ae.test.ts', 'test/fixtures/sdk-ordering-ae.json', 'test/sdk-columnar-af.test.ts', 'test/fixtures/sdk-columnar-af.json', 'test/sdk-order-b-ag.test.ts', 'test/fixtures/sdk-order-b-ag.json', 'test/sdk-schedule-continuation-ah.test.ts', 'test/fixtures/sdk-schedule-continuation-ah.json', 'test/sdk-original-order-ai.test.ts', 'test/fixtures/sdk-original-order-ai.json', 'test/sdk-compact-sequence-aj.test.ts', 'test/fixtures/sdk-compact-sequence-aj.json'],
|
||||
// Data-driven behavioral guard for the 'plan'/'prompt' carves (eng, design,
|
||||
// devex, office-hours + future PR2 carves). One file iterating CARVE_GUARDS;
|
||||
// the selector sets GSTACK_CARVE_SKILL=<name> to scope cost to the changed
|
||||
// skill (D-CODEX A). Touching the registry/helper or sections.ts runs all.
|
||||
'carve-section-loading': ['scripts/resolvers/composition.ts', 'test/autoplan-review-discovery.test.ts', 'test/autoplan-phase-order.test.ts', 'design-html/**', 'design-shotgun/**', 'qa/**', 'browse/**', 'retro/**', 'autoplan/**', 'spec/**', 'setup-gbrain/**', 'review/**', 'codex/**', 'land-and-deploy/**', 'plan-eng-review/**', 'plan-design-review/**', 'plan-devex-review/**', 'office-hours/**', 'document-release/**', 'design-consultation/**', 'cso/**', 'test/helpers/carve-guards.ts', 'scripts/resolvers/sections.ts', 'scripts/resolvers/redact-doc.ts', 'scripts/gen-skill-docs.ts', 'test/helpers/auq-sdk-capture.ts', 'test/helpers/session-runner.ts', 'test/session-runner-tools.test.ts', 'test/carve-section-loading.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/fixtures/autoplan/u-ceo-original-loss.json', 'test/fixtures/autoplan/v-ceo-dangling-references.json'],
|
||||
'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'],
|
||||
'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'],
|
||||
|
||||
// Per-finding AskUserQuestion count + review-report-at-bottom assertion.
|
||||
// Each test drives its skill end-to-end; touchfiles include preamble +
|
||||
// completion-status resolvers because they affect question cadence and
|
||||
// terminal output (the regression surface this test catches).
|
||||
'plan-ceo-finding-count': ['bin/gstack-config', 'bin/gstack-skill-start', 'bin/gstack-skill-end', 'plan-ceo-review/**', 'test/skill-ceo-section-ordering.test.ts', '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/ceo-completion-handoff.ts', 'test/ceo-completion-handoff.test.ts', 'test/ceo-count-s-terminals.test.ts', 'test/fixtures/ceo-count-s-paired.json', 'test/fixtures/ceo-completion-handoff-calls.json', 'test/fixtures/ceo-completion-handoff-j-calls.json', 'test/fixtures/ceo-completion-handoff-k-calls.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-ceo-finding-count.test.ts', 'test/ceo-completion-handoff-l.test.ts', 'test/fixtures/ceo-completion-handoff-l-calls.json', 'test/ceo-completion-handoff-m.test.ts', 'test/fixtures/ceo-completion-handoff-m-call.json', 'test/fixtures/devex-review-l-calls.json', 'test/plan-count-checkbox.test.ts', 'test/fixtures/ceo-checkbox-l.screen.txt', 'test/fixtures/ceo-handoff-n-calls.json', 'test/ceo-completion-handoff-o.test.ts', 'test/fixtures/ceo-completion-handoff-o-call.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/ceo-approach-pick.ts', 'test/ceo-approach-pick.test.ts', 'test/fixtures/ceo-approach-q-call.json', 'test/fixtures/ceo-approach-r-call.json', 'test/fixtures/ceo-approach-r-distinct-call.json', 'test/fixtures/ceo-approach-q-paired-call.json', 'test/fixtures/ceo-completion-handoff-q-call.json', 'test/fixtures/ceo-completion-handoff-r-calls.json', 'test/fixtures/ceo-completion-handoff-t-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/fixtures/ceo-completion-handoff-u-call.json', 'test/fixtures/ceo-completion-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/plan-count-ceo-body-finding.test.ts', 'test/fixtures/ceo-count-w-paired.json', 'test/fixtures/ceo-completion-handoff-w-call.json', 'test/fixtures/ceo-approach-y-call.json', 'test/fixtures/ceo-approach-y-screen.txt', 'test/ceo-handoff-y.test.ts', 'test/fixtures/ceo-handoff-y-call.json', 'test/plan-count-truncated-question.test.ts', 'test/fixtures/ceo-approach-z-call.json', 'test/fixtures/ceo-approach-z-screen.txt', 'test/fixtures/ceo-handoff-z-call.json', 'test/fixtures/ceo-approach-aa-call.json', 'test/review-handoffs-aa.test.ts', 'test/fixtures/review-handoff-aa-ceo.json', 'test/fixtures/review-handoff-aa-dx.json', 'test/helpers/ceo-mode-option.ts', 'test/ceo-count-mode.test.ts', 'test/fixtures/ceo-count-mode-ab-call.json', 'test/ceo-count-ac.test.ts', 'test/fixtures/ceo-count-ac-calls.json', 'test/fixtures/ceo-count-ac-later-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/ceo-count-ad-v2.test.ts', 'test/fixtures/ceo-count-ad-v2.json', 'test/fixtures/plan-count-permission-target-ad-v2.json', 'test/fixtures/ceo-finding-alias-af.json', 'test/fixtures/ceo-numbered-brief-af.json', 'test/ceo-contract-assertions-ag.test.ts', 'test/fixtures/ceo-contract-assertions-ag.json', 'test/fixtures/ceo-contract-assertions-ag-retry.json', 'test/fixtures/plan-count-permission-ah.json', 'test/ceo-parenthesized-issue-ah.test.ts', 'test/fixtures/ceo-parenthesized-issue-ah.json', 'test/ceo-section-chLine truncated
|
||||
'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'],
|
||||
'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'],
|
||||
'plan-ceo-finding-count': ['bin/gstack-config', 'bin/gstack-skill-start', 'bin/gstack-skill-end', 'plan-ceo-review/**', 'test/skill-ceo-section-ordering.test.ts', '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/ceo-completion-handoff.ts', 'test/ceo-completion-handoff.test.ts', 'test/ceo-count-s-terminals.test.ts', 'test/fixtures/ceo-count-s-paired.json', 'test/fixtures/ceo-completion-handoff-calls.json', 'test/fixtures/ceo-completion-handoff-j-calls.json', 'test/fixtures/ceo-completion-handoff-k-calls.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-ceo-finding-count.test.ts', 'test/ceo-completion-handoff-l.test.ts', 'test/fixtures/ceo-completion-handoff-l-calls.json', 'test/ceo-completion-handoff-m.test.ts', 'test/fixtures/ceo-completion-handoff-m-call.json', 'test/fixtures/devex-review-l-calls.json', 'test/plan-count-checkbox.test.ts', 'test/fixtures/ceo-checkbox-l.screen.txt', 'test/fixtures/ceo-handoff-n-calls.json', 'test/ceo-completion-handoff-o.test.ts', 'test/fixtures/ceo-completion-handoff-o-call.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/ceo-approach-pick.ts', 'test/ceo-approach-pick.test.ts', 'test/fixtures/ceo-approach-q-call.json', 'test/fixtures/ceo-approach-r-call.json', 'test/fixtures/ceo-approach-r-distinct-call.json', 'test/fixtures/ceo-approach-q-paired-call.json', 'test/fixtures/ceo-completion-handoff-q-call.json', 'test/fixtures/ceo-completion-handoff-r-calls.json', 'test/fixtures/ceo-completion-handoff-t-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/fixtures/ceo-completion-handoff-u-call.json', 'test/fixtures/ceo-completion-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/plan-count-ceo-body-finding.test.ts', 'test/fixtures/ceo-count-w-paired.json', 'test/fixtures/ceo-completion-handoff-w-call.json', 'test/fixtures/ceo-approach-y-call.json', 'test/fixtures/ceo-approach-y-screen.txt', 'test/ceo-handoff-y.test.ts', 'test/fixtures/ceo-handoff-y-call.json', 'test/plan-count-truncated-question.test.ts', 'test/fixtures/ceo-approach-z-call.json', 'test/fixtures/ceo-approach-z-screen.txt', 'test/fixtures/ceo-handoff-z-call.json', 'test/fixtures/ceo-approach-aa-call.json', 'test/review-handoffs-aa.test.ts', 'test/fixtures/review-handoff-aa-ceo.json', 'test/fixtures/review-handoff-aa-dx.json', 'test/helpers/ceo-mode-option.ts', 'test/ceo-count-mode.test.ts', 'test/fixtures/ceo-count-mode-ab-call.json', 'test/ceo-count-ac.test.ts', 'test/fixtures/ceo-count-ac-calls.json', 'test/fixtures/ceo-count-ac-later-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/ceo-count-ad-v2.test.ts', 'test/fixtures/ceo-count-ad-v2.json', 'test/fixtures/plan-count-permission-target-ad-v2.json', 'test/fixtures/ceo-finding-alias-af.json', 'test/fixtures/ceo-numbered-brief-af.json', 'test/ceo-contract-assertions-ag.test.ts', 'test/fixtures/ceo-contract-assertions-ag.json', 'test/fixtures/ceo-contract-assertions-ag-retry.json', 'test/fixtures/plan-count-permission-ah.json', 'test/ceo-parenthesized-issue-ah.test.ts', 'test/fixtures/ceo-parenthesized-issue-ah.json', 'test/ceo-section-chLine truncated
|
||||
'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'],
|
||||
'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'],
|
||||
'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'],
|
||||
|
||||
// Gate-tier reviewCount-floor counterparts. Catch the May 2026 transcript
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import { test, expect } from 'bun:test';
|
||||
import { hasStaleFillRaceFinding } from './helpers/ceo-section-loading-fixture';
|
||||
import captured from './fixtures/sdk-compact-sequence-aj.json';
|
||||
|
||||
const sequence = 'fill starts, write commits, write deletes (no-op), fill sets pre-commit v1, later read hits v1.';
|
||||
const report = captured.finding;
|
||||
|
||||
test('recognizes the captured current original-plan sequence without borrowing the amended diagram', () => {
|
||||
expect(hasStaleFillRaceFinding(report)).toBe(true);
|
||||
expect(hasStaleFillRaceFinding(report.replaceAll('v1', 'snapshot_A'))).toBe(true);
|
||||
expect(hasStaleFillRaceFinding(report.replace('Schedule Diagram 2b: ', ''))).toBe(true);
|
||||
});
|
||||
|
||||
test('requires the ordered original fill, commit, invalidation, old cache value and same later value', () => {
|
||||
for (const changed of [
|
||||
sequence.replace('fill starts, ', ''),
|
||||
sequence.replace('write commits, ', ''),
|
||||
sequence.replace('write deletes (no-op), ', ''),
|
||||
sequence.replace('fill sets pre-commit v1, ', ''),
|
||||
sequence.replace(', later read hits v1', ''),
|
||||
sequence.replace('later read hits v1', 'later read hits v2'),
|
||||
sequence.replace('pre-commit v1', 'post-commit v1'),
|
||||
sequence.replace('fill starts, write commits', 'write commits, fill starts'),
|
||||
sequence.replace('write deletes (no-op), fill sets pre-commit v1', 'fill sets pre-commit v1, write deletes (no-op)'),
|
||||
sequence.replace('later read hits', 'another key later read hits'),
|
||||
]) expect(hasStaleFillRaceFinding(report.replace(sequence, changed))).toBe(false);
|
||||
expect(hasStaleFillRaceFinding(report.replace('Original plan', 'Amended plan'))).toBe(false);
|
||||
expect(hasStaleFillRaceFinding(report.replace(sequence, '"' + sequence + '"'))).toBe(false);
|
||||
});
|
||||
|
||||
test('preserves accepted-staleness and explicit dismissal boundaries', () => {
|
||||
for (const suffix of [
|
||||
'This is not a gap; no guard is needed.',
|
||||
'This staleness is the accepted consistency model.',
|
||||
'This finding is withdrawn.',
|
||||
'F1 is rejected.',
|
||||
]) expect(hasStaleFillRaceFinding(report.trimEnd() + '\n\n' + suffix)).toBe(false);
|
||||
expect(hasStaleFillRaceFinding(report.replace('fill starts', 'fill never starts'))).toBe(false);
|
||||
});
|
||||
|
||||
test('source, quotes and hypothetical framing cannot supply current coverage', () => {
|
||||
for (const text of [
|
||||
'```text\n' + report + '```',
|
||||
report.split('\n').map(line => '> ' + line).join('\n'),
|
||||
report.split('\n').map(line => ' ' + line).join('\n'),
|
||||
'## Historical example\n\n' + report,
|
||||
'## Quoted source\n\n' + report,
|
||||
'An unproven hypothesis.\n\n' + report,
|
||||
'The following is a hypothetical example.\n\n' + report,
|
||||
]) expect(hasStaleFillRaceFinding(text)).toBe(false);
|
||||
expect(hasStaleFillRaceFinding('## Historical example\nOld material.\n\n## Current review\n' + report)).toBe(true);
|
||||
expect(hasStaleFillRaceFinding(report + '\n## Unrelated issue\nF2 is rejected.')).toBe(true);
|
||||
});
|
||||
|
||||
test('source framing remains attached to descendant registry headings', () => {
|
||||
for (const prefix of [
|
||||
'## Copied material\nThe following subsections reproduce source examples, not current findings.\n\n',
|
||||
'## Input material\nThe following sections quote historical examples.\n\n',
|
||||
'The following subsections reproduce source examples, not current findings.\n\n',
|
||||
]) expect(hasStaleFillRaceFinding(prefix + report)).toBe(false);
|
||||
expect(hasStaleFillRaceFinding('## Source notes\nThe following material quotes historical examples.\n\n## Current findings\n' + report)).toBe(true);
|
||||
});
|
||||
|
||||
test('same finding assessments retain identity across sections and unrelated findings', () => {
|
||||
for (const suffix of [
|
||||
'## F1 assessment\nThis finding is withdrawn.',
|
||||
'## Final assessment\nF1 is rejected.',
|
||||
'## F2\nUnrelated issue accepted.\n\n## Final assessment\nF1 is dismissed.',
|
||||
]) expect(hasStaleFillRaceFinding(report + '\n\n' + suffix)).toBe(false);
|
||||
for (const suffix of [
|
||||
'## F2 assessment\nThis finding is withdrawn.',
|
||||
'## Final assessment\nF2 is rejected.',
|
||||
'## Quoted source\nF1 is rejected.',
|
||||
'## Source notes\nThe following subsections quote historical examples.\n\n### F1 assessment\nThis finding is withdrawn.',
|
||||
]) expect(hasStaleFillRaceFinding(report + '\n\n' + suffix)).toBe(true);
|
||||
});
|
||||
|
||||
import { E2E_TOUCHFILES, selectTests } from './helpers/touchfiles';
|
||||
test('compact evidence selects exactly the existing section-loading workflow', () => {
|
||||
for (const file of ['test/sdk-compact-sequence-aj.test.ts', 'test/fixtures/sdk-compact-sequence-aj.json']) {
|
||||
expect(selectTests([file], E2E_TOUCHFILES, []).selected).toEqual(['plan-ceo-section-loading']);
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user