From d4f42b69a1dd0eb48d89500e03fb1c4407229284 Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Wed, 9 Sep 2026 18:10:58 +0000 Subject: [PATCH] fix: handle native review question completions --- test/ceo-mode-posture-ad.test.ts | 116 +++++ test/fixtures/ceo-mode-posture-ad.json | 455 ++++++++++++++++++ .../pending-question-completion-ad.json | 232 +++++++++ test/helpers/ceo-mode-option.ts | 60 ++- test/helpers/plan-count-pending-question.ts | 26 +- test/helpers/touchfiles-data.ts | 4 +- test/pending-question-completion.test.ts | 195 ++++++++ 7 files changed, 1081 insertions(+), 7 deletions(-) create mode 100644 test/ceo-mode-posture-ad.test.ts create mode 100644 test/fixtures/ceo-mode-posture-ad.json create mode 100644 test/fixtures/pending-question-completion-ad.json create mode 100644 test/pending-question-completion.test.ts diff --git a/test/ceo-mode-posture-ad.test.ts b/test/ceo-mode-posture-ad.test.ts new file mode 100644 index 000000000..188f17e40 --- /dev/null +++ b/test/ceo-mode-posture-ad.test.ts @@ -0,0 +1,116 @@ +import { describe, expect, test } from 'bun:test'; +import * as fs from 'node:fs'; +import * as os from 'node:os'; +import * as path from 'node:path'; +import { hasNativePostAnswerCeoPosture, nativeCeoModeAnswer } from './helpers/ceo-mode-option'; +import { readPlanCountTranscript, type NativePublicToolEvent } from './helpers/plan-count-transcript'; +import { E2E_TOUCHFILES, selectTests } from './helpers/touchfiles'; +import captured from './fixtures/ceo-mode-posture-ad.json'; + +const patterns = { + 'HOLD SCOPE': /\b(rigor|bulletproof|hold\s*scope|maximum\s+rigor)\b/i, + 'SCOPE EXPANSION': /\b(expansion|10x|delight|dream|cathedral|opt[\s-]?in)\b/i, +}; +function replay(index: number, change?: (rows: any[]) => void) { + const item = captured.cases[index]!; + const rows = structuredClone(item.records); + change?.(rows); + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ceo-posture-ad-')); + const project = path.join(dir, 'projects', 'owned'); + fs.mkdirSync(project, {recursive:true}); + fs.writeFileSync(path.join(project, item.process.sessionId+'.jsonl'), rows.map(row=>JSON.stringify(row)).join('\n')+'\n'); + const events: NativePublicToolEvent[]=[]; + try { return {item, transcript:readPlanCountTranscript(dir,item.process.cwd,event=>events.push(event)),events}; } + finally { fs.rmSync(dir,{recursive:true,force:true}); } +} +function matches(e: ReturnType) { + const mode=e.item.mode as keyof typeof patterns; + return hasNativePostAnswerCeoPosture(e.transcript,mode,patterns[mode],e.item.selectedAt,e.events); +} +function rebind(e: ReturnType) { + const decision=e.transcript.calls[1]!; + e.events[2]!.input={questions:decision.questions}; + decision.answers={[decision.questions[0]!.question]:decision.questions[0]!.options[0]!.label}; +} + +for (const index of [0,1]) describe(`${captured.cases[index]!.mode} actual completed mode application`,()=>{ + test('the exact mode answer and concrete scope decision supply posture without finalized prose',()=>{ + const e=replay(index); + expect(e.item.actualFailure.state).toBe('failed'); + expect(e.transcript.calls.map(call=>call.toolUseId)).toEqual([e.item.modeToolUseId,e.item.decisionToolUseId]); + expect(nativeCeoModeAnswer(e.transcript,e.item.mode as keyof typeof patterns,e.item.selectedAt)?.toolUseId).toBe(e.item.modeToolUseId); + expect(e.transcript.assistantMessages.every(message=>Date.parse(message.timestamp){ + const e=replay(index);const [mode,decision]=e.transcript.calls;const q=decision!.questions[0]!; + switch(failure){ + case 'wrong selected mode':mode!.answers![mode!.questions[0]!.question]=index===0?'SCOPE EXPANSION':'HOLD SCOPE';break; + case 'pending mode':mode!.answered=false;break; + case 'failed mode':mode!.failed=true;break; + case 'answer before selection':mode!.answeredAt=new Date(e.item.selectedAt-1).toISOString();break; + case 'pending decision':decision!.answered=false;break; + case 'failed decision':decision!.failed=true;break; + case 'foreign session':decision!.sessionId=e.events[2]!.sessionId=e.events[3]!.sessionId='foreign';break; + case 'pre-mode request':e.events[2]!.timestamp=e.events[0]!.timestamp;break; + case 'reply before request':decision!.answeredAt=e.events[3]!.timestamp=new Date(Date.parse(e.events[2]!.timestamp)-1).toISOString();break; + case 'reply before selection':decision!.answeredAt=e.events[3]!.timestamp=new Date(e.item.selectedAt-1).toISOString();break; + case 'reply timestamp mismatch':e.events[3]!.timestamp=new Date(Date.parse(decision!.answeredAt!)+1).toISOString();break; + case 'wrong tool':e.events[2]!.name='Read';break; + case 'missing request':e.events.splice(2,1);break; + case 'missing reply':e.events.splice(3,1);break; + case 'failed public reply':e.events[3]!.isError=true;break; + case 'duplicate request':e.events.push({...e.events[2]!});break; + case 'duplicate reply':e.events.push({...e.events[3]!});break; + case 'request mismatch':e.events[2]!.input={questions:[]};break; + case 'unknown answer':decision!.answers![q.question]='Unrecognized';break; + case 'extra question':decision!.questions.push({...structuredClone(q),header:'Also',question:'Also remove the CI gate?'});rebind(e);break; + case 'extra option':q.options.push({label:'Remove the CI gate',description:'A separate obligation.'});rebind(e);break; + case 'multiselect':q.multiSelect=true;rebind(e);break; + case 'quoted decision':q.question=q.question.split('\n').map(line=>'> '+line).join('\n');rebind(e);break; + case 'fenced decision':q.question='```text\n'+q.question+'\n```';rebind(e);break; + case 'mere mode mention':q.question=`D6 — Continue the review?\nSelected ${e.item.mode}.`;rebind(e);break; + case 'extra obligation':q.question+=' Also, should we remove the CI gate?';rebind(e);break; + case 'extra imperative':q.question+=' Also remove the CI gate.';rebind(e);break; + case 'option imperative':q.options[0]!.description+=' Please remove the CI gate.';rebind(e);break; + } + expect(matches(e),failure).toBe(false); + }); + test.each(['Delete the CI gate.', 'Ship the new endpoint now.', 'After that, disable authentication.'])('an instruction appended after the final comparison is not part of the scope brief: %s', extra=>{ + const e=replay(index);const q=e.transcript.calls[1]!.questions[0]!; + q.question+=' '+extra;rebind(e);expect(matches(e)).toBe(false); + }); + test('foreign, sidechain, missing and failed native records do not become completed evidence',()=>{ + for(const change of [(rows:any[])=>{rows[3].cwd='/foreign';},(rows:any[])=>{rows[3].isSidechain=true;}, + (rows:any[])=>{rows.pop();},(rows:any[])=>{rows[4].message.content[0].is_error=true;}]) expect(matches(replay(index,change))).toBe(false); + }); +}); + +test('HOLD requires the explicit out-of-scope deferral and its selected defer answer',()=>{ + for(const change of [(q:any)=>{q.question=q.question.replace('Under HOLD SCOPE, keep or defer','Under HOLD SCOPE, automatically add');}, + (q:any)=>{q.question=q.question.replace('not in the plan text','required by the plan text');}, + (q:any)=>{q.question=q.question.replace('pure additions, not repairs to meet a stated invariant','repairs needed to meet a stated invariant');}, + (q:any)=>{q.options[0].label='Keep all three (recommended)';}, + (q:any)=>{q.options[1].label='Remove CI gate';}]){ + const e=replay(0);change(e.transcript.calls[1]!.questions[0]);rebind(e);expect(matches(e)).toBe(false); + } + const e=replay(0);const q=e.transcript.calls[1]!.questions[0]!; + e.transcript.calls[1]!.answers={[q.question]:q.options[1]!.label};expect(matches(e)).toBe(false); +}); + +test('E-number shorthand requires matching header, add decision and explicit expansion context',()=>{ + for(const change of [(q:any)=>{q.header='E2 shared';},(q:any)=>{q.question=q.question.replace('E1: Add','E1: Mention');}, + (q:any)=>{q.question=q.question.replace('SCOPE EXPANSION mode','HOLD SCOPE mode');}, + (q:any)=>{q.options[1].label='Enable telemetry';}]){ + const e=replay(1);change(e.transcript.calls[1]!.questions[0]);rebind(e);expect(matches(e)).toBe(false); + } +}); + +test('the new replay controls and fixture select the actual periodic mode-routing caller',()=>{ + for(const file of ['test/ceo-mode-posture-ad.test.ts','test/fixtures/ceo-mode-posture-ad.json']) + expect(selectTests([file],E2E_TOUCHFILES,[]).selected).toEqual(['plan-ceo-mode-routing']); +}); diff --git a/test/fixtures/ceo-mode-posture-ad.json b/test/fixtures/ceo-mode-posture-ad.json new file mode 100644 index 000000000..e574b9288 --- /dev/null +++ b/test/fixtures/ceo-mode-posture-ad.json @@ -0,0 +1,455 @@ +{ + "provenance": { + "run": "source-ad-focused-diagnostic-20260909-v1", + "sourceHead": "c943a0c30e901a97957846b0c5e53c949d76ceb4", + "retention": "Exact public native AUQ request/reply blocks and pre-selection public text only; no hidden reasoning or stdout substitutes. Both actual first attempts failed; no historical paid pass is claimed." + }, + "cases": [ + { + "mode": "HOLD SCOPE", + "attempt": 1, + "process": { + "pid": 925998, + "startTicks": "596177", + "cwd": "/tmp/gstack-paid-shard-7L31S7/tmp/gstack-plan-count-bbDT0N", + "config": "/tmp/gstack-paid-shard-7L31S7/tmp/gstack-hermetic-925944-iQAwYg/with-skills/.claude", + "sessionId": "14add4a4-0e7b-4791-814b-54a396bad1b7" + }, + "native": { + "path": "/home/vercel-sandbox/gstack/.context/source-ad-focused-diagnostic-20260909-v1/focused-evidence/blobs/a9c5dff6992f426a51b0a933644f4baadebfd8b94c69cb6eab7dd0968fdb63db/current.jsonl", + "sha256": "042f22992b22471876f59048c748107a008883cc56a16d222a063a40019a9d23", + "bytes": 546523 + }, + "actualFailure": { + "state": "failed", + "at": "2026-09-09T17:44:07.645Z" + }, + "selectedAt": 1788975606604, + "modeToolUseId": "toolu_01WoCBKG1toNXrQ1yDnw4RLB", + "decisionToolUseId": "toolu_018XtfbUM4WFW8kvaEHBMGnL", + "records": [ + { + "type": "assistant", + "sessionId": "14add4a4-0e7b-4791-814b-54a396bad1b7", + "cwd": "/tmp/gstack-paid-shard-7L31S7/tmp/gstack-plan-count-bbDT0N", + "isSidechain": false, + "timestamp": "2026-09-09T17:39:51.863Z", + "message": { + "role": "assistant", + "content": [ + { + "type": "text", + "text": "Approach B is locked. Next is mode selection." + } + ] + } + }, + { + "type": "assistant", + "sessionId": "14add4a4-0e7b-4791-814b-54a396bad1b7", + "cwd": "/tmp/gstack-paid-shard-7L31S7/tmp/gstack-plan-count-bbDT0N", + "timestamp": "2026-09-09T17:40:04.532Z", + "isSidechain": false, + "message": { + "role": "assistant", + "content": [ + { + "type": "tool_use", + "id": "toolu_01WoCBKG1toNXrQ1yDnw4RLB", + "name": "AskUserQuestion", + "input": { + "questions": [ + { + "header": "Review mode", + "question": "D5 \u2014 Which review posture should I take for the rest of this review? Project/branch/task: PLAN.md 'Add saved project views' on main, approach B locked. ELI10: this decides how ambitious I am allowed to be. Expansion means I pitch the cathedral and you opt in to each piece. Selective means the plan is the baseline and I show you extras one at a time to cherry-pick. Hold means no extras, just maximum rigor on what is written. Reduction means I cut it to the bone. Stakes if wrong: too ambitious delays a two-week pilot; too narrow and we re-review in a month. Recommendation: SELECTIVE EXPANSION because this is an enhancement to an existing task list, and the landscape check turned up 5-6 small adjacent wins (default view, last-filter memory, deep links) that deserve a yes/no each rather than a blanket decision. Note: options differ in kind, not coverage \u2014 no completeness score. Net: how many extra decisions you want to make vs how much you trust the 15-line plan as the full scope.", + "options": [ + { + "label": "SELECTIVE EXPANSION (recommended)", + "description": "\u2705 Plan scope is the baseline and gets the full rigor pass; every extra is an individual yes/defer/skip. \u2705 Neutral pitches with effort and risk on each, so you cherry-pick without pressure. \u274c Adds 5-6 short decision prompts before the deep review starts." + }, + { + "label": "SCOPE EXPANSION", + "description": "\u2705 I describe the 10x version and the platonic ideal first, then break it into opt-in proposals. \u2705 Best if you suspect the plan is under-ambitious for what the team needs. \u274c Enthusiastic recommendations bias toward more scope, which risks the two-week pilot date." + }, + { + "label": "HOLD SCOPE", + "description": "\u2705 Zero scope drift; the four approach bullets are exactly what ships. \u2705 Fastest path to a bulletproof version of the written plan. \u274c Cheap adjacent wins like a default view go unmentioned until someone asks post-pilot." + }, + { + "label": "SCOPE REDUCTION", + "description": "\u2705 Ships the absolute minimum that stops filter rebuilding, possibly just last-filter memory. \u2705 Fastest to pilot. \u274c Conflicts with the approach B schema you just chose; would mean reopening D4." + } + ], + "multiSelect": false + } + ] + }, + "caller": { + "type": "direct" + } + } + ] + } + }, + { + "type": "user", + "sessionId": "14add4a4-0e7b-4791-814b-54a396bad1b7", + "cwd": "/tmp/gstack-paid-shard-7L31S7/tmp/gstack-plan-count-bbDT0N", + "timestamp": "2026-09-09T17:40:06.685Z", + "isSidechain": false, + "message": { + "role": "user", + "content": [ + { + "type": "tool_result", + "content": "Your questions have been answered: \"D5 \u2014 Which review posture should I take for the rest of this review? Project/branch/task: PLAN.md 'Add saved project views' on main, approach B locked. ELI10: this decides how ambitious I am allowed to be. Expansion means I pitch the cathedral and you opt in to each piece. Selective means the plan is the baseline and I show you extras one at a time to cherry-pick. Hold means no extras, just maximum rigor on what is written. Reduction means I cut it to the bone. Stakes if wrong: too ambitious delays a two-week pilot; too narrow and we re-review in a month. Recommendation: SELECTIVE EXPANSION because this is an enhancement to an existing task list, and the landscape check turned up 5-6 small adjacent wins (default view, last-filter memory, deep links) that deserve a yes/no each rather than a blanket decision. Note: options differ in kind, not coverage \u2014 no completeness score. Net: how many extra decisions you want to make vs how much you trust the 15-line plan as the full scope.\"=\"HOLD SCOPE\". You can now continue with these answers in mind.", + "tool_use_id": "toolu_01WoCBKG1toNXrQ1yDnw4RLB" + } + ] + }, + "toolUseResult": { + "questions": [ + { + "question": "D5 \u2014 Which review posture should I take for the rest of this review? Project/branch/task: PLAN.md 'Add saved project views' on main, approach B locked. ELI10: this decides how ambitious I am allowed to be. Expansion means I pitch the cathedral and you opt in to each piece. Selective means the plan is the baseline and I show you extras one at a time to cherry-pick. Hold means no extras, just maximum rigor on what is written. Reduction means I cut it to the bone. Stakes if wrong: too ambitious delays a two-week pilot; too narrow and we re-review in a month. Recommendation: SELECTIVE EXPANSION because this is an enhancement to an existing task list, and the landscape check turned up 5-6 small adjacent wins (default view, last-filter memory, deep links) that deserve a yes/no each rather than a blanket decision. Note: options differ in kind, not coverage \u2014 no completeness score. Net: how many extra decisions you want to make vs how much you trust the 15-line plan as the full scope.", + "header": "Review mode", + "options": [ + { + "label": "SELECTIVE EXPANSION (recommended)", + "description": "\u2705 Plan scope is the baseline and gets the full rigor pass; every extra is an individual yes/defer/skip. \u2705 Neutral pitches with effort and risk on each, so you cherry-pick without pressure. \u274c Adds 5-6 short decision prompts before the deep review starts." + }, + { + "label": "SCOPE EXPANSION", + "description": "\u2705 I describe the 10x version and the platonic ideal first, then break it into opt-in proposals. \u2705 Best if you suspect the plan is under-ambitious for what the team needs. \u274c Enthusiastic recommendations bias toward more scope, which risks the two-week pilot date." + }, + { + "label": "HOLD SCOPE", + "description": "\u2705 Zero scope drift; the four approach bullets are exactly what ships. \u2705 Fastest path to a bulletproof version of the written plan. \u274c Cheap adjacent wins like a default view go unmentioned until someone asks post-pilot." + }, + { + "label": "SCOPE REDUCTION", + "description": "\u2705 Ships the absolute minimum that stops filter rebuilding, possibly just last-filter memory. \u2705 Fastest to pilot. \u274c Conflicts with the approach B schema you just chose; would mean reopening D4." + } + ], + "multiSelect": false + } + ], + "answers": { + "D5 \u2014 Which review posture should I take for the rest of this review? Project/branch/task: PLAN.md 'Add saved project views' on main, approach B locked. ELI10: this decides how ambitious I am allowed to be. Expansion means I pitch the cathedral and you opt in to each piece. Selective means the plan is the baseline and I show you extras one at a time to cherry-pick. Hold means no extras, just maximum rigor on what is written. Reduction means I cut it to the bone. Stakes if wrong: too ambitious delays a two-week pilot; too narrow and we re-review in a month. Recommendation: SELECTIVE EXPANSION because this is an enhancement to an existing task list, and the landscape check turned up 5-6 small adjacent wins (default view, last-filter memory, deep links) that deserve a yes/no each rather than a blanket decision. Note: options differ in kind, not coverage \u2014 no completeness score. Net: how many extra decisions you want to make vs how much you trust the 15-line plan as the full scope.": "HOLD SCOPE" + }, + "annotations": {} + } + }, + { + "type": "assistant", + "sessionId": "14add4a4-0e7b-4791-814b-54a396bad1b7", + "cwd": "/tmp/gstack-paid-shard-7L31S7/tmp/gstack-plan-count-bbDT0N", + "timestamp": "2026-09-09T17:40:49.613Z", + "isSidechain": false, + "message": { + "role": "assistant", + "content": [ + { + "type": "tool_use", + "id": "toolu_018XtfbUM4WFW8kvaEHBMGnL", + "name": "AskUserQuestion", + "input": { + "questions": [ + { + "header": "B extras", + "question": "D6 \u2014 Under HOLD SCOPE, keep or defer the three approach-B conveniences that are not in the plan text (per-member default view, last-used filter memory, ?view= deep links)? Project/branch/task: PLAN.md 'Add saved project views' on main. ELI10: you picked approach B for its future-proof schema. B also bundled three small UX niceties that the 15-line plan never asked for. HOLD SCOPE says the written scope is the baseline, so I have to surface these as deferrable rather than silently keep them. The schema stays either way. Stakes if wrong: keeping them adds ~3 files and touches member prefs, a table this plan otherwise never touches; deferring them means a saved view is only reachable via the picker and every session opens unfiltered. Recommendation: B (defer all three) because HOLD SCOPE means the plan text is the contract and these are pure additions, not repairs to meet a stated invariant. Completeness: A=10/10 for the approved approach, B=10/10 for the written plan, C=8/10. Net: honoring the plan as written vs honoring the approach as approved.", + "options": [ + { + "label": "Defer all three to TODOS (recommended)", + "description": "\u2705 Scope matches the plan's four bullets exactly; member prefs table is untouched by this change. \u2705 All three are additive later with zero schema change thanks to the B table shape. \u274c Reopening the project starts unfiltered every time, so the 'recreate filters' pain is only half-solved until the default view lands." + }, + { + "label": "Keep all three", + "description": "\u2705 Ships the version of B you approved in D4 with no trimming. \u2705 Default view directly attacks the stated pain: members land on their filters without a click. \u274c Adds member-prefs writes and URL-param handling, ~3 extra files, and is scope beyond the written plan under a HOLD SCOPE review." + }, + { + "label": "Keep deep links only", + "description": "\u2705 ?view= is a URL param read on the existing page; smallest of the three, no new table writes. \u2705 Lets members bookmark or paste a view into chat without a sharing feature. \u274c Default view and last-used are deferred, so the open-project-see-my-filters moment still needs one click." + } + ], + "multiSelect": false + } + ] + }, + "caller": { + "type": "direct" + } + } + ] + } + }, + { + "type": "user", + "sessionId": "14add4a4-0e7b-4791-814b-54a396bad1b7", + "cwd": "/tmp/gstack-paid-shard-7L31S7/tmp/gstack-plan-count-bbDT0N", + "timestamp": "2026-09-09T17:40:51.783Z", + "isSidechain": false, + "message": { + "role": "user", + "content": [ + { + "type": "tool_result", + "content": "Your questions have been answered: \"D6 \u2014 Under HOLD SCOPE, keep or defer the three approach-B conveniences that are not in the plan text (per-member default view, last-used filter memory, ?view= deep links)? Project/branch/task: PLAN.md 'Add saved project views' on main. ELI10: you picked approach B for its future-proof schema. B also bundled three small UX niceties that the 15-line plan never asked for. HOLD SCOPE says the written scope is the baseline, so I have to surface these as deferrable rather than silently keep them. The schema stays either way. Stakes if wrong: keeping them adds ~3 files and touches member prefs, a table this plan otherwise never touches; deferring them means a saved view is only reachable via the picker and every session opens unfiltered. Recommendation: B (defer all three) because HOLD SCOPE means the plan text is the contract and these are pure additions, not repairs to meet a stated invariant. Completeness: A=10/10 for the approved approach, B=10/10 for the written plan, C=8/10. Net: honoring the plan as written vs honoring the approach as approved.\"=\"Defer all three to TODOS (recommended)\". You can now continue with these answers in mind.", + "tool_use_id": "toolu_018XtfbUM4WFW8kvaEHBMGnL" + } + ] + }, + "toolUseResult": { + "questions": [ + { + "question": "D6 \u2014 Under HOLD SCOPE, keep or defer the three approach-B conveniences that are not in the plan text (per-member default view, last-used filter memory, ?view= deep links)? Project/branch/task: PLAN.md 'Add saved project views' on main. ELI10: you picked approach B for its future-proof schema. B also bundled three small UX niceties that the 15-line plan never asked for. HOLD SCOPE says the written scope is the baseline, so I have to surface these as deferrable rather than silently keep them. The schema stays either way. Stakes if wrong: keeping them adds ~3 files and touches member prefs, a table this plan otherwise never touches; deferring them means a saved view is only reachable via the picker and every session opens unfiltered. Recommendation: B (defer all three) because HOLD SCOPE means the plan text is the contract and these are pure additions, not repairs to meet a stated invariant. Completeness: A=10/10 for the approved approach, B=10/10 for the written plan, C=8/10. Net: honoring the plan as written vs honoring the approach as approved.", + "header": "B extras", + "options": [ + { + "label": "Defer all three to TODOS (recommended)", + "description": "\u2705 Scope matches the plan's four bullets exactly; member prefs table is untouched by this change. \u2705 All three are additive later with zero schema change thanks to the B table shape. \u274c Reopening the project starts unfiltered every time, so the 'recreate filters' pain is only half-solved until the default view lands." + }, + { + "label": "Keep all three", + "description": "\u2705 Ships the version of B you approved in D4 with no trimming. \u2705 Default view directly attacks the stated pain: members land on their filters without a click. \u274c Adds member-prefs writes and URL-param handling, ~3 extra files, and is scope beyond the written plan under a HOLD SCOPE review." + }, + { + "label": "Keep deep links only", + "description": "\u2705 ?view= is a URL param read on the existing page; smallest of the three, no new table writes. \u2705 Lets members bookmark or paste a view into chat without a sharing feature. \u274c Default view and last-used are deferred, so the open-project-see-my-filters moment still needs one click." + } + ], + "multiSelect": false + } + ], + "answers": { + "D6 \u2014 Under HOLD SCOPE, keep or defer the three approach-B conveniences that are not in the plan text (per-member default view, last-used filter memory, ?view= deep links)? Project/branch/task: PLAN.md 'Add saved project views' on main. ELI10: you picked approach B for its future-proof schema. B also bundled three small UX niceties that the 15-line plan never asked for. HOLD SCOPE says the written scope is the baseline, so I have to surface these as deferrable rather than silently keep them. The schema stays either way. Stakes if wrong: keeping them adds ~3 files and touches member prefs, a table this plan otherwise never touches; deferring them means a saved view is only reachable via the picker and every session opens unfiltered. Recommendation: B (defer all three) because HOLD SCOPE means the plan text is the contract and these are pure additions, not repairs to meet a stated invariant. Completeness: A=10/10 for the approved approach, B=10/10 for the written plan, C=8/10. Net: honoring the plan as written vs honoring the approach as approved.": "Defer all three to TODOS (recommended)" + }, + "annotations": {} + } + } + ] + }, + { + "mode": "SCOPE EXPANSION", + "attempt": 1, + "process": { + "pid": 925999, + "startTicks": "596177", + "cwd": "/tmp/gstack-paid-shard-7L31S7/tmp/gstack-plan-count-rY0De7", + "config": "/tmp/gstack-paid-shard-7L31S7/tmp/gstack-hermetic-925944-iQAwYg/with-skills/.claude", + "sessionId": "e59fb50d-6564-4361-a79f-05851e530a68" + }, + "native": { + "path": "/home/vercel-sandbox/gstack/.context/source-ad-focused-diagnostic-20260909-v1/focused-evidence/blobs/d313936f3927d34dd5a2a84e79fc95f27c6ca6c5731d065aed19a241a162c11c/current.jsonl", + "sha256": "032941aa6b614b73adecaea56f50850d6004a7029fd9e3443761ab5839feabc3", + "bytes": 427819 + }, + "actualFailure": { + "state": "failed", + "at": "2026-09-09T17:44:50.822Z" + }, + "selectedAt": 1788975649757, + "modeToolUseId": "toolu_01LxBLkEdbM1HeqA1savUoC8", + "decisionToolUseId": "toolu_01QwSxVav6eeKorjQvU5CxvZ", + "records": [ + { + "type": "assistant", + "sessionId": "e59fb50d-6564-4361-a79f-05851e530a68", + "cwd": "/tmp/gstack-paid-shard-7L31S7/tmp/gstack-plan-count-rY0De7", + "isSidechain": false, + "timestamp": "2026-09-09T17:40:21.073Z", + "message": { + "role": "assistant", + "content": [ + { + "type": "text", + "text": "Approach B is approved. Logging it as a durable architecture decision, then mode selection." + } + ] + } + }, + { + "type": "assistant", + "sessionId": "e59fb50d-6564-4361-a79f-05851e530a68", + "cwd": "/tmp/gstack-paid-shard-7L31S7/tmp/gstack-plan-count-rY0De7", + "timestamp": "2026-09-09T17:40:48.497Z", + "isSidechain": false, + "message": { + "role": "assistant", + "content": [ + { + "type": "tool_use", + "id": "toolu_01LxBLkEdbM1HeqA1savUoC8", + "name": "AskUserQuestion", + "input": { + "questions": [ + { + "question": "D5 \u2014 Which review posture should I take for the rest of this review?\nProject/branch/task: main branch, saved project views; approach B (validated, versioned view) is locked.\nELI10: The approach is decided. Now I need to know how ambitious to be about scope. I can hold exactly what PLAN.md asks for and make it bulletproof, or also surface adjacent ideas (shared views, default view, view-as-link) one at a time for you to accept or reject, or dream big and push every expansion, or cut the plan down to the bare minimum. Whatever you choose, no scope is added or removed without you saying yes to each item.\nStakes if we pick wrong: hold too tight and you ship a feature that needs a second migration in a month; expand too far and a two-week pilot turns into a quarter.\nRecommendation: SELECTIVE EXPANSION because this is an enhancement to an existing task list, the baseline scope is sound, and approach B already leaves cheap one-flag expansions on the table that you should see before deciding.\nNote: options differ in kind, not coverage \u2014 no completeness score.\nNet: how many adjacent opportunities you want to see before the deep review, from none to all.", + "header": "Mode", + "multiSelect": false, + "options": [ + { + "label": "SELECTIVE EXPANSION (recommended)", + "description": "\u2705 Baseline scope held and reviewed at full rigor; every expansion shown individually with effort and risk, neutral recommendation. \u2705 You cherry-pick; rejected items go to NOT in scope. \u274c Five or six extra decisions before the deep review begins." + }, + { + "label": "HOLD SCOPE", + "description": "\u2705 Fastest path to a bulletproof version of exactly what PLAN.md says. \u2705 No expansion questions at all; maximum rigor on failure modes, tests, observability. \u274c Cheap adjacent wins like default view or shared views are not surfaced." + }, + { + "label": "SCOPE EXPANSION", + "description": "\u2705 Ten-x vision and platonic ideal described first, then each proposal offered with an enthusiastic recommendation. \u2705 Best when this feature is meant to become a platform for dashboards and notifications. \u274c Pilot scope grows; more decisions and more implementation before first user feedback." + }, + { + "label": "SCOPE REDUCTION", + "description": "\u2705 Strips to the minimum that ships value: save, list, apply, delete. \u2705 Fastest to a pilot. \u274c Conflicts with approach B you just chose; drift handling and usage tracking would be cut and the pilot becomes unmeasurable." + } + ] + } + ] + }, + "caller": { + "type": "direct" + } + } + ] + } + }, + { + "type": "user", + "sessionId": "e59fb50d-6564-4361-a79f-05851e530a68", + "cwd": "/tmp/gstack-paid-shard-7L31S7/tmp/gstack-plan-count-rY0De7", + "timestamp": "2026-09-09T17:40:49.795Z", + "isSidechain": false, + "message": { + "role": "user", + "content": [ + { + "type": "tool_result", + "content": "Your questions have been answered: \"D5 \u2014 Which review posture should I take for the rest of this review?\nProject/branch/task: main branch, saved project views; approach B (validated, versioned view) is locked.\nELI10: The approach is decided. Now I need to know how ambitious to be about scope. I can hold exactly what PLAN.md asks for and make it bulletproof, or also surface adjacent ideas (shared views, default view, view-as-link) one at a time for you to accept or reject, or dream big and push every expansion, or cut the plan down to the bare minimum. Whatever you choose, no scope is added or removed without you saying yes to each item.\nStakes if we pick wrong: hold too tight and you ship a feature that needs a second migration in a month; expand too far and a two-week pilot turns into a quarter.\nRecommendation: SELECTIVE EXPANSION because this is an enhancement to an existing task list, the baseline scope is sound, and approach B already leaves cheap one-flag expansions on the table that you should see before deciding.\nNote: options differ in kind, not coverage \u2014 no completeness score.\nNet: how many adjacent opportunities you want to see before the deep review, from none to all.\"=\"SCOPE EXPANSION\". You can now continue with these answers in mind.", + "tool_use_id": "toolu_01LxBLkEdbM1HeqA1savUoC8" + } + ] + }, + "toolUseResult": { + "questions": [ + { + "question": "D5 \u2014 Which review posture should I take for the rest of this review?\nProject/branch/task: main branch, saved project views; approach B (validated, versioned view) is locked.\nELI10: The approach is decided. Now I need to know how ambitious to be about scope. I can hold exactly what PLAN.md asks for and make it bulletproof, or also surface adjacent ideas (shared views, default view, view-as-link) one at a time for you to accept or reject, or dream big and push every expansion, or cut the plan down to the bare minimum. Whatever you choose, no scope is added or removed without you saying yes to each item.\nStakes if we pick wrong: hold too tight and you ship a feature that needs a second migration in a month; expand too far and a two-week pilot turns into a quarter.\nRecommendation: SELECTIVE EXPANSION because this is an enhancement to an existing task list, the baseline scope is sound, and approach B already leaves cheap one-flag expansions on the table that you should see before deciding.\nNote: options differ in kind, not coverage \u2014 no completeness score.\nNet: how many adjacent opportunities you want to see before the deep review, from none to all.", + "header": "Mode", + "options": [ + { + "label": "SELECTIVE EXPANSION (recommended)", + "description": "\u2705 Baseline scope held and reviewed at full rigor; every expansion shown individually with effort and risk, neutral recommendation. \u2705 You cherry-pick; rejected items go to NOT in scope. \u274c Five or six extra decisions before the deep review begins." + }, + { + "label": "HOLD SCOPE", + "description": "\u2705 Fastest path to a bulletproof version of exactly what PLAN.md says. \u2705 No expansion questions at all; maximum rigor on failure modes, tests, observability. \u274c Cheap adjacent wins like default view or shared views are not surfaced." + }, + { + "label": "SCOPE EXPANSION", + "description": "\u2705 Ten-x vision and platonic ideal described first, then each proposal offered with an enthusiastic recommendation. \u2705 Best when this feature is meant to become a platform for dashboards and notifications. \u274c Pilot scope grows; more decisions and more implementation before first user feedback." + }, + { + "label": "SCOPE REDUCTION", + "description": "\u2705 Strips to the minimum that ships value: save, list, apply, delete. \u2705 Fastest to a pilot. \u274c Conflicts with approach B you just chose; drift handling and usage tracking would be cut and the pilot becomes unmeasurable." + } + ], + "multiSelect": false + } + ], + "answers": { + "D5 \u2014 Which review posture should I take for the rest of this review?\nProject/branch/task: main branch, saved project views; approach B (validated, versioned view) is locked.\nELI10: The approach is decided. Now I need to know how ambitious to be about scope. I can hold exactly what PLAN.md asks for and make it bulletproof, or also surface adjacent ideas (shared views, default view, view-as-link) one at a time for you to accept or reject, or dream big and push every expansion, or cut the plan down to the bare minimum. Whatever you choose, no scope is added or removed without you saying yes to each item.\nStakes if we pick wrong: hold too tight and you ship a feature that needs a second migration in a month; expand too far and a two-week pilot turns into a quarter.\nRecommendation: SELECTIVE EXPANSION because this is an enhancement to an existing task list, the baseline scope is sound, and approach B already leaves cheap one-flag expansions on the table that you should see before deciding.\nNote: options differ in kind, not coverage \u2014 no completeness score.\nNet: how many adjacent opportunities you want to see before the deep review, from none to all.": "SCOPE EXPANSION" + }, + "annotations": {} + } + }, + { + "type": "assistant", + "sessionId": "e59fb50d-6564-4361-a79f-05851e530a68", + "cwd": "/tmp/gstack-paid-shard-7L31S7/tmp/gstack-plan-count-rY0De7", + "timestamp": "2026-09-09T17:41:44.450Z", + "isSidechain": false, + "message": { + "role": "assistant", + "content": [ + { + "type": "tool_use", + "id": "toolu_01QwSxVav6eeKorjQvU5CxvZ", + "name": "AskUserQuestion", + "input": { + "questions": [ + { + "question": "D6 \u2014 E1: Add project-shared views to this plan?\nProject/branch/task: main branch, saved views under approach B, SCOPE EXPANSION mode.\nELI10: Right now every saved view is private to the member who made it. Shared views let a member flip a view to \"visible to this project\" so teammates see and apply it from the same picker. Only the owner (or a project admin) can edit or delete it; everyone else gets a read-only copy they can \"Save as mine\". Approach B already has the visibility column, so this is the endpoint authorization rules, the picker section, and the ownership tests.\nStakes if we pick wrong: skip it and the team keeps recreating the lead's filters by hand, which is the exact pain the plan set out to remove, just moved one person over. Add it and the authorization surface doubles, which is the riskiest part of the feature.\nRecommendation: A because this is the single expansion that turns a personal shortcut into a team tool, and the column already exists; the real cost is tests, which CC makes cheap.\nCompleteness: A=10/10, B=7/10, C=7/10\nNet: doubled authorization surface now vs the feature staying single-player and needing a second launch later.", + "header": "E1 shared", + "multiSelect": false, + "options": [ + { + "label": "A) Add to scope (recommended)", + "description": "human ~2 days / CC ~30 min. \u2705 Leads save a view once and the whole project sees it; the core pain disappears for everyone, not one person. \u2705 Visibility column and soft delete already exist under approach B, so no migration later. \u274c Doubles the authorization matrix: owner vs member vs non-member on read, update, delete, and apply, each needing tests." + }, + { + "label": "B) Defer to TODOS.md", + "description": "\u2705 Pilot stays single-player and simpler to measure. \u2705 Visibility column ships now so the follow-up is endpoints and UI only. \u274c The most common team request lands in the backlog on day one." + }, + { + "label": "C) Skip", + "description": "\u2705 Smallest authorization surface possible. \u2705 Nothing to explain about who can see what. \u274c Contradicts the 10x vision you just chose; sharing would need a fresh design later." + } + ] + } + ] + }, + "caller": { + "type": "direct" + } + } + ] + } + }, + { + "type": "user", + "sessionId": "e59fb50d-6564-4361-a79f-05851e530a68", + "cwd": "/tmp/gstack-paid-shard-7L31S7/tmp/gstack-plan-count-rY0De7", + "timestamp": "2026-09-09T17:41:45.015Z", + "isSidechain": false, + "message": { + "role": "user", + "content": [ + { + "type": "tool_result", + "content": "Your questions have been answered: \"D6 \u2014 E1: Add project-shared views to this plan?\nProject/branch/task: main branch, saved views under approach B, SCOPE EXPANSION mode.\nELI10: Right now every saved view is private to the member who made it. Shared views let a member flip a view to \"visible to this project\" so teammates see and apply it from the same picker. Only the owner (or a project admin) can edit or delete it; everyone else gets a read-only copy they can \"Save as mine\". Approach B already has the visibility column, so this is the endpoint authorization rules, the picker section, and the ownership tests.\nStakes if we pick wrong: skip it and the team keeps recreating the lead's filters by hand, which is the exact pain the plan set out to remove, just moved one person over. Add it and the authorization surface doubles, which is the riskiest part of the feature.\nRecommendation: A because this is the single expansion that turns a personal shortcut into a team tool, and the column already exists; the real cost is tests, which CC makes cheap.\nCompleteness: A=10/10, B=7/10, C=7/10\nNet: doubled authorization surface now vs the feature staying single-player and needing a second launch later.\"=\"A) Add to scope (recommended)\". You can now continue with these answers in mind.", + "tool_use_id": "toolu_01QwSxVav6eeKorjQvU5CxvZ" + } + ] + }, + "toolUseResult": { + "questions": [ + { + "question": "D6 \u2014 E1: Add project-shared views to this plan?\nProject/branch/task: main branch, saved views under approach B, SCOPE EXPANSION mode.\nELI10: Right now every saved view is private to the member who made it. Shared views let a member flip a view to \"visible to this project\" so teammates see and apply it from the same picker. Only the owner (or a project admin) can edit or delete it; everyone else gets a read-only copy they can \"Save as mine\". Approach B already has the visibility column, so this is the endpoint authorization rules, the picker section, and the ownership tests.\nStakes if we pick wrong: skip it and the team keeps recreating the lead's filters by hand, which is the exact pain the plan set out to remove, just moved one person over. Add it and the authorization surface doubles, which is the riskiest part of the feature.\nRecommendation: A because this is the single expansion that turns a personal shortcut into a team tool, and the column already exists; the real cost is tests, which CC makes cheap.\nCompleteness: A=10/10, B=7/10, C=7/10\nNet: doubled authorization surface now vs the feature staying single-player and needing a second launch later.", + "header": "E1 shared", + "options": [ + { + "label": "A) Add to scope (recommended)", + "description": "human ~2 days / CC ~30 min. \u2705 Leads save a view once and the whole project sees it; the core pain disappears for everyone, not one person. \u2705 Visibility column and soft delete already exist under approach B, so no migration later. \u274c Doubles the authorization matrix: owner vs member vs non-member on read, update, delete, and apply, each needing tests." + }, + { + "label": "B) Defer to TODOS.md", + "description": "\u2705 Pilot stays single-player and simpler to measure. \u2705 Visibility column ships now so the follow-up is endpoints and UI only. \u274c The most common team request lands in the backlog on day one." + }, + { + "label": "C) Skip", + "description": "\u2705 Smallest authorization surface possible. \u2705 Nothing to explain about who can see what. \u274c Contradicts the 10x vision you just chose; sharing would need a fresh design later." + } + ], + "multiSelect": false + } + ], + "answers": { + "D6 \u2014 E1: Add project-shared views to this plan?\nProject/branch/task: main branch, saved views under approach B, SCOPE EXPANSION mode.\nELI10: Right now every saved view is private to the member who made it. Shared views let a member flip a view to \"visible to this project\" so teammates see and apply it from the same picker. Only the owner (or a project admin) can edit or delete it; everyone else gets a read-only copy they can \"Save as mine\". Approach B already has the visibility column, so this is the endpoint authorization rules, the picker section, and the ownership tests.\nStakes if we pick wrong: skip it and the team keeps recreating the lead's filters by hand, which is the exact pain the plan set out to remove, just moved one person over. Add it and the authorization surface doubles, which is the riskiest part of the feature.\nRecommendation: A because this is the single expansion that turns a personal shortcut into a team tool, and the column already exists; the real cost is tests, which CC makes cheap.\nCompleteness: A=10/10, B=7/10, C=7/10\nNet: doubled authorization surface now vs the feature staying single-player and needing a second launch later.": "A) Add to scope (recommended)" + }, + "annotations": {} + } + } + ] + } + ] +} diff --git a/test/fixtures/pending-question-completion-ad.json b/test/fixtures/pending-question-completion-ad.json new file mode 100644 index 000000000..469537818 --- /dev/null +++ b/test/fixtures/pending-question-completion-ad.json @@ -0,0 +1,232 @@ +{ + "version": 1, + "provenance": { + "run": "source-ad-focused-diagnostic-20260909-v1", + "kind": "actual public request/result projections plus code-derived completion input", + "rawHookEnvelopeCaptured": false, + "completionInputDerivation": "Frozen AskUserQuestion.call returns questions, answers and annotations from the same input passed to PostToolUse; the retained successful native result exposes those fields. Synthetic hook envelopes and owned temp paths are required in tests.", + "proofSHA256": "a92c6f502db270fdd292bf0ed327f4d5cfa49d17774f890b5bc0b0f66af5e323" + }, + "cases": [ + { + "mode": "HOLD SCOPE", + "sessionId": "14add4a4-0e7b-4791-814b-54a396bad1b7", + "toolUseId": "toolu_01TysjiG4mwuYsrxJBXfXtG3", + "requestedAt": "2026-09-09T17:38:37.218Z", + "completedAt": "2026-09-09T17:38:50.892Z", + "questions": [ + { + "header": "Routing", + "question": "D1 \u2014 Add gstack skill routing rules to this project's CLAUDE.md? Project/branch/task: gstack-plan-count-bbDT0N on main, reviewing PLAN.md. ELI10: gstack works best when CLAUDE.md tells the agent which slash skill to reach for (bugs \u2192 /investigate, ship \u2192 /ship, etc). Without it you invoke skills by hand each time. Stakes if wrong: none permanent, it's a text block you can delete. Recommendation: A because it is a two-way door and saves manual routing. Note: options differ in kind, not coverage \u2014 no completeness score. Net: convenience vs keeping a fixture CLAUDE.md untouched. (In plan mode, the edit and commit happen after plan approval.)", + "options": [ + { + "label": "Add routing rules (recommended)", + "description": "\u2705 Future requests auto-route to the right gstack skill without typing slash names. \u2705 Two-way door: a plain text section you can delete any time. \u274c Adds a section to a CLAUDE.md that is currently a review fixture and commits it." + }, + { + "label": "No thanks, manual invocation", + "description": "\u2705 CLAUDE.md stays exactly as the fixture author wrote it, no extra commit. \u2705 You keep full control over when a skill fires. \u274c Every skill has to be invoked by name each time; no proactive suggestions." + } + ], + "multiSelect": false + }, + { + "header": "Learnings", + "question": "D2 \u2014 Enable cross-project learnings search? Project/branch/task: same repo, plan-ceo-review preamble. ELI10: gstack can search learnings recorded from your other projects on this machine to spot patterns that apply here. Everything stays local. Good for solo devs; skip if you juggle multiple client codebases where cross-contamination matters. Stakes if wrong: a wrong choice just means noisier or sparser prior-learning hints; flip it any time with gstack-config. Recommendation: A because a single-developer machine gets more signal from pooled learnings. Note: options differ in kind, not coverage \u2014 no completeness score. Net: more context vs strict per-project isolation.", + "options": [ + { + "label": "Enable cross-project (recommended)", + "description": "\u2705 Reviews surface 'prior learning applied' hints from every project on this machine. \u2705 Data never leaves the machine; only a local config flag changes. \u274c Learnings from unrelated client codebases could show up where they don't apply." + }, + { + "label": "Project-scoped only", + "description": "\u2705 Zero risk of one client's patterns leaking into another client's review. \u2705 Learnings stay tightly relevant to this repo. \u274c This repo has 0 learnings today, so prior-learning hints will be empty for a while." + } + ], + "multiSelect": false + }, + { + "header": "Design doc", + "question": "D3 \u2014 No design doc found for this branch. Run /office-hours first? Project/branch/task: PLAN.md 'Add saved project views' on main. ELI10: /office-hours produces a structured problem statement, premise challenge, and explored alternatives (about 10 minutes). It gives this CEO review sharper input. The plan here is short and clear enough that the review can also run directly. Stakes if wrong: skipping means I challenge premises myself from a 15-line plan; running it costs ~10 minutes. Recommendation: B because the goal is crisp (stop recreating filters) and this is a well-known feature shape, so the premise challenge inside Step 0A is enough. Note: options differ in kind, not coverage \u2014 no completeness score. Net: ten minutes of structured discovery vs going straight into rigor.", + "options": [ + { + "label": "Skip, standard review (recommended)", + "description": "\u2705 Straight into premise challenge, alternatives, and the 11-section review now. \u2705 The plan's goal and approach are already concrete enough to challenge directly. \u274c No written design doc artifact for teammates to read later." + }, + { + "label": "Run /office-hours now", + "description": "\u2705 Produces a durable design doc with problem statement and alternatives on disk. \u2705 Forces the 'why saved views and not X' conversation before any review. \u274c Adds ~10 minutes before the review begins on a plan that is already fairly clear." + } + ], + "multiSelect": false + } + ], + "completionInput": { + "questions": [ + { + "question": "D1 \u2014 Add gstack skill routing rules to this project's CLAUDE.md? Project/branch/task: gstack-plan-count-bbDT0N on main, reviewing PLAN.md. ELI10: gstack works best when CLAUDE.md tells the agent which slash skill to reach for (bugs \u2192 /investigate, ship \u2192 /ship, etc). Without it you invoke skills by hand each time. Stakes if wrong: none permanent, it's a text block you can delete. Recommendation: A because it is a two-way door and saves manual routing. Note: options differ in kind, not coverage \u2014 no completeness score. Net: convenience vs keeping a fixture CLAUDE.md untouched. (In plan mode, the edit and commit happen after plan approval.)", + "header": "Routing", + "options": [ + { + "label": "Add routing rules (recommended)", + "description": "\u2705 Future requests auto-route to the right gstack skill without typing slash names. \u2705 Two-way door: a plain text section you can delete any time. \u274c Adds a section to a CLAUDE.md that is currently a review fixture and commits it." + }, + { + "label": "No thanks, manual invocation", + "description": "\u2705 CLAUDE.md stays exactly as the fixture author wrote it, no extra commit. \u2705 You keep full control over when a skill fires. \u274c Every skill has to be invoked by name each time; no proactive suggestions." + } + ], + "multiSelect": false + }, + { + "question": "D2 \u2014 Enable cross-project learnings search? Project/branch/task: same repo, plan-ceo-review preamble. ELI10: gstack can search learnings recorded from your other projects on this machine to spot patterns that apply here. Everything stays local. Good for solo devs; skip if you juggle multiple client codebases where cross-contamination matters. Stakes if wrong: a wrong choice just means noisier or sparser prior-learning hints; flip it any time with gstack-config. Recommendation: A because a single-developer machine gets more signal from pooled learnings. Note: options differ in kind, not coverage \u2014 no completeness score. Net: more context vs strict per-project isolation.", + "header": "Learnings", + "options": [ + { + "label": "Enable cross-project (recommended)", + "description": "\u2705 Reviews surface 'prior learning applied' hints from every project on this machine. \u2705 Data never leaves the machine; only a local config flag changes. \u274c Learnings from unrelated client codebases could show up where they don't apply." + }, + { + "label": "Project-scoped only", + "description": "\u2705 Zero risk of one client's patterns leaking into another client's review. \u2705 Learnings stay tightly relevant to this repo. \u274c This repo has 0 learnings today, so prior-learning hints will be empty for a while." + } + ], + "multiSelect": false + }, + { + "question": "D3 \u2014 No design doc found for this branch. Run /office-hours first? Project/branch/task: PLAN.md 'Add saved project views' on main. ELI10: /office-hours produces a structured problem statement, premise challenge, and explored alternatives (about 10 minutes). It gives this CEO review sharper input. The plan here is short and clear enough that the review can also run directly. Stakes if wrong: skipping means I challenge premises myself from a 15-line plan; running it costs ~10 minutes. Recommendation: B because the goal is crisp (stop recreating filters) and this is a well-known feature shape, so the premise challenge inside Step 0A is enough. Note: options differ in kind, not coverage \u2014 no completeness score. Net: ten minutes of structured discovery vs going straight into rigor.", + "header": "Design doc", + "options": [ + { + "label": "Skip, standard review (recommended)", + "description": "\u2705 Straight into premise challenge, alternatives, and the 11-section review now. \u2705 The plan's goal and approach are already concrete enough to challenge directly. \u274c No written design doc artifact for teammates to read later." + }, + { + "label": "Run /office-hours now", + "description": "\u2705 Produces a durable design doc with problem statement and alternatives on disk. \u2705 Forces the 'why saved views and not X' conversation before any review. \u274c Adds ~10 minutes before the review begins on a plan that is already fairly clear." + } + ], + "multiSelect": false + } + ], + "answers": { + "D1 \u2014 Add gstack skill routing rules to this project's CLAUDE.md? Project/branch/task: gstack-plan-count-bbDT0N on main, reviewing PLAN.md. ELI10: gstack works best when CLAUDE.md tells the agent which slash skill to reach for (bugs \u2192 /investigate, ship \u2192 /ship, etc). Without it you invoke skills by hand each time. Stakes if wrong: none permanent, it's a text block you can delete. Recommendation: A because it is a two-way door and saves manual routing. Note: options differ in kind, not coverage \u2014 no completeness score. Net: convenience vs keeping a fixture CLAUDE.md untouched. (In plan mode, the edit and commit happen after plan approval.)": "Add routing rules (recommended)", + "D2 \u2014 Enable cross-project learnings search? Project/branch/task: same repo, plan-ceo-review preamble. ELI10: gstack can search learnings recorded from your other projects on this machine to spot patterns that apply here. Everything stays local. Good for solo devs; skip if you juggle multiple client codebases where cross-contamination matters. Stakes if wrong: a wrong choice just means noisier or sparser prior-learning hints; flip it any time with gstack-config. Recommendation: A because a single-developer machine gets more signal from pooled learnings. Note: options differ in kind, not coverage \u2014 no completeness score. Net: more context vs strict per-project isolation.": "Enable cross-project (recommended)", + "D3 \u2014 No design doc found for this branch. Run /office-hours first? Project/branch/task: PLAN.md 'Add saved project views' on main. ELI10: /office-hours produces a structured problem statement, premise challenge, and explored alternatives (about 10 minutes). It gives this CEO review sharper input. The plan here is short and clear enough that the review can also run directly. Stakes if wrong: skipping means I challenge premises myself from a 15-line plan; running it costs ~10 minutes. Recommendation: B because the goal is crisp (stop recreating filters) and this is a well-known feature shape, so the premise challenge inside Step 0A is enough. Note: options differ in kind, not coverage \u2014 no completeness score. Net: ten minutes of structured discovery vs going straight into rigor.": "Skip, standard review (recommended)" + }, + "annotations": {} + }, + "evidenceSHA256": "4a1663f121fe618b459812f0d5dfccb32172e32a92573d1d26d047061d19f9a2" + }, + { + "mode": "SCOPE EXPANSION", + "sessionId": "e59fb50d-6564-4361-a79f-05851e530a68", + "toolUseId": "toolu_0153fUMNJ4f5E56N2sCrZt3W", + "requestedAt": "2026-09-09T17:38:41.273Z", + "completedAt": "2026-09-09T17:38:54.927Z", + "questions": [ + { + "question": "D1 \u2014 Add gstack skill routing rules to CLAUDE.md?\nProject/branch/task: main branch of the saved-views plan fixture; CLAUDE.md is 935 bytes and has no routing section.\nELI10: gstack works best when your project's CLAUDE.md tells Claude which skill to reach for on each kind of request. Without it, you invoke skills by hand every time. Note: we are in plan mode, so if you say yes the edit and commit happen after this review exits plan mode, not now.\nStakes if we pick wrong: minor. Either extra manual skill invocations, or a routing section you did not want in a checked-in file.\nRecommendation: A because it is a small append-only edit and it makes /investigate, /ship, and friends fire automatically.\nNote: options differ in kind, not coverage \u2014 no completeness score.\nNet: convenience of auto-routing vs keeping CLAUDE.md untouched.", + "header": "Routing", + "multiSelect": false, + "options": [ + { + "label": "Add routing rules (recommended)", + "description": "\u2705 Skills fire automatically on matching requests (bugs \u2192 /investigate, ship \u2192 /ship). \u2705 Append-only edit, easy to revert. \u274c Adds a section to a checked-in file; deferred until plan mode exits." + }, + { + "label": "No thanks, manual invocation", + "description": "\u2705 CLAUDE.md stays exactly as the fixture defines it. \u2705 Nothing written to the repo by this review. \u274c You type /skill-name by hand every time; the offer will not repeat for this project." + } + ] + }, + { + "question": "D2 \u2014 Search learnings across your other projects on this machine?\nProject/branch/task: same fixture; this project has zero learnings so far.\nELI10: gstack keeps notes on pitfalls and patterns it learns per project. Cross-project mode lets it also read notes from your other repos on this machine when reviewing this one. Everything stays local. Good for a solo developer; skip if you work on separate client codebases where cross-contamination matters.\nStakes if we pick wrong: low. Either you miss a relevant prior lesson, or a lesson from an unrelated codebase gets applied where it does not fit.\nRecommendation: A because this project has no learnings of its own yet, so cross-project is the only way any prior lesson can help.\nNote: options differ in kind, not coverage \u2014 no completeness score.\nNet: broader recall vs strict per-project isolation.", + "header": "Learnings", + "multiSelect": false, + "options": [ + { + "label": "Enable cross-project (recommended)", + "description": "\u2705 Prior lessons from other repos on this machine can surface in this review. \u2705 Local only; nothing leaves the machine. \u274c A lesson from an unrelated codebase might be surfaced where it does not apply." + }, + { + "label": "Project-scoped only", + "description": "\u2705 Strict isolation between codebases, safest for client work. \u2705 No cross-talk between unrelated projects. \u274c This project has zero learnings, so recall stays empty for now." + } + ] + }, + { + "question": "D3 \u2014 Run /office-hours first, or go straight to the standard review?\nProject/branch/task: no design doc exists for this branch; PLAN.md is 15 lines.\nELI10: /office-hours produces a structured problem statement, challenges the premise, and explores alternatives before a plan is reviewed. It gives this review sharper input. It takes about 10 minutes and is per-feature, not per-product. The current plan is short, so there is real room for it to add signal, but the goal statement is already clear enough to review directly.\nStakes if we pick wrong: either 10 extra minutes you did not need, or a review that inherits an unexamined premise.\nRecommendation: B because the plan's problem statement is concrete and the premise challenge in Step 0A covers the same ground at lower cost; you can always run /office-hours later.\nNote: options differ in kind, not coverage \u2014 no completeness score.\nNet: deeper problem framing up front vs getting to the review now.", + "header": "Office hours", + "multiSelect": false, + "options": [ + { + "label": "Skip, standard review (recommended)", + "description": "\u2705 Review starts now; Step 0A still challenges the premise explicitly. \u2705 The goal in PLAN.md is already concrete enough to review. \u274c No written design doc for later reviews to reference." + }, + { + "label": "Run /office-hours now", + "description": "\u2705 Produces a design doc with premise challenge and explored alternatives. \u2705 Review picks up right after with sharper input. \u274c About 10 more minutes (human) before the review itself begins." + } + ] + } + ], + "completionInput": { + "questions": [ + { + "question": "D1 \u2014 Add gstack skill routing rules to CLAUDE.md?\nProject/branch/task: main branch of the saved-views plan fixture; CLAUDE.md is 935 bytes and has no routing section.\nELI10: gstack works best when your project's CLAUDE.md tells Claude which skill to reach for on each kind of request. Without it, you invoke skills by hand every time. Note: we are in plan mode, so if you say yes the edit and commit happen after this review exits plan mode, not now.\nStakes if we pick wrong: minor. Either extra manual skill invocations, or a routing section you did not want in a checked-in file.\nRecommendation: A because it is a small append-only edit and it makes /investigate, /ship, and friends fire automatically.\nNote: options differ in kind, not coverage \u2014 no completeness score.\nNet: convenience of auto-routing vs keeping CLAUDE.md untouched.", + "header": "Routing", + "options": [ + { + "label": "Add routing rules (recommended)", + "description": "\u2705 Skills fire automatically on matching requests (bugs \u2192 /investigate, ship \u2192 /ship). \u2705 Append-only edit, easy to revert. \u274c Adds a section to a checked-in file; deferred until plan mode exits." + }, + { + "label": "No thanks, manual invocation", + "description": "\u2705 CLAUDE.md stays exactly as the fixture defines it. \u2705 Nothing written to the repo by this review. \u274c You type /skill-name by hand every time; the offer will not repeat for this project." + } + ], + "multiSelect": false + }, + { + "question": "D2 \u2014 Search learnings across your other projects on this machine?\nProject/branch/task: same fixture; this project has zero learnings so far.\nELI10: gstack keeps notes on pitfalls and patterns it learns per project. Cross-project mode lets it also read notes from your other repos on this machine when reviewing this one. Everything stays local. Good for a solo developer; skip if you work on separate client codebases where cross-contamination matters.\nStakes if we pick wrong: low. Either you miss a relevant prior lesson, or a lesson from an unrelated codebase gets applied where it does not fit.\nRecommendation: A because this project has no learnings of its own yet, so cross-project is the only way any prior lesson can help.\nNote: options differ in kind, not coverage \u2014 no completeness score.\nNet: broader recall vs strict per-project isolation.", + "header": "Learnings", + "options": [ + { + "label": "Enable cross-project (recommended)", + "description": "\u2705 Prior lessons from other repos on this machine can surface in this review. \u2705 Local only; nothing leaves the machine. \u274c A lesson from an unrelated codebase might be surfaced where it does not apply." + }, + { + "label": "Project-scoped only", + "description": "\u2705 Strict isolation between codebases, safest for client work. \u2705 No cross-talk between unrelated projects. \u274c This project has zero learnings, so recall stays empty for now." + } + ], + "multiSelect": false + }, + { + "question": "D3 \u2014 Run /office-hours first, or go straight to the standard review?\nProject/branch/task: no design doc exists for this branch; PLAN.md is 15 lines.\nELI10: /office-hours produces a structured problem statement, challenges the premise, and explores alternatives before a plan is reviewed. It gives this review sharper input. It takes about 10 minutes and is per-feature, not per-product. The current plan is short, so there is real room for it to add signal, but the goal statement is already clear enough to review directly.\nStakes if we pick wrong: either 10 extra minutes you did not need, or a review that inherits an unexamined premise.\nRecommendation: B because the plan's problem statement is concrete and the premise challenge in Step 0A covers the same ground at lower cost; you can always run /office-hours later.\nNote: options differ in kind, not coverage \u2014 no completeness score.\nNet: deeper problem framing up front vs getting to the review now.", + "header": "Office hours", + "options": [ + { + "label": "Skip, standard review (recommended)", + "description": "\u2705 Review starts now; Step 0A still challenges the premise explicitly. \u2705 The goal in PLAN.md is already concrete enough to review. \u274c No written design doc for later reviews to reference." + }, + { + "label": "Run /office-hours now", + "description": "\u2705 Produces a design doc with premise challenge and explored alternatives. \u2705 Review picks up right after with sharper input. \u274c About 10 more minutes (human) before the review itself begins." + } + ], + "multiSelect": false + } + ], + "answers": { + "D1 \u2014 Add gstack skill routing rules to CLAUDE.md?\nProject/branch/task: main branch of the saved-views plan fixture; CLAUDE.md is 935 bytes and has no routing section.\nELI10: gstack works best when your project's CLAUDE.md tells Claude which skill to reach for on each kind of request. Without it, you invoke skills by hand every time. Note: we are in plan mode, so if you say yes the edit and commit happen after this review exits plan mode, not now.\nStakes if we pick wrong: minor. Either extra manual skill invocations, or a routing section you did not want in a checked-in file.\nRecommendation: A because it is a small append-only edit and it makes /investigate, /ship, and friends fire automatically.\nNote: options differ in kind, not coverage \u2014 no completeness score.\nNet: convenience of auto-routing vs keeping CLAUDE.md untouched.": "Add routing rules (recommended)", + "D2 \u2014 Search learnings across your other projects on this machine?\nProject/branch/task: same fixture; this project has zero learnings so far.\nELI10: gstack keeps notes on pitfalls and patterns it learns per project. Cross-project mode lets it also read notes from your other repos on this machine when reviewing this one. Everything stays local. Good for a solo developer; skip if you work on separate client codebases where cross-contamination matters.\nStakes if we pick wrong: low. Either you miss a relevant prior lesson, or a lesson from an unrelated codebase gets applied where it does not fit.\nRecommendation: A because this project has no learnings of its own yet, so cross-project is the only way any prior lesson can help.\nNote: options differ in kind, not coverage \u2014 no completeness score.\nNet: broader recall vs strict per-project isolation.": "Enable cross-project (recommended)", + "D3 \u2014 Run /office-hours first, or go straight to the standard review?\nProject/branch/task: no design doc exists for this branch; PLAN.md is 15 lines.\nELI10: /office-hours produces a structured problem statement, challenges the premise, and explores alternatives before a plan is reviewed. It gives this review sharper input. It takes about 10 minutes and is per-feature, not per-product. The current plan is short, so there is real room for it to add signal, but the goal statement is already clear enough to review directly.\nStakes if we pick wrong: either 10 extra minutes you did not need, or a review that inherits an unexamined premise.\nRecommendation: B because the plan's problem statement is concrete and the premise challenge in Step 0A covers the same ground at lower cost; you can always run /office-hours later.\nNote: options differ in kind, not coverage \u2014 no completeness score.\nNet: deeper problem framing up front vs getting to the review now.": "Skip, standard review (recommended)" + }, + "annotations": {} + }, + "evidenceSHA256": "fe6de28c6d1f09784b8ca39bb1064f55819f01f494d42f0085d0b602af69ebc2" + } + ] +} diff --git a/test/helpers/ceo-mode-option.ts b/test/helpers/ceo-mode-option.ts index 4a40d8aea..9172d0d0a 100644 --- a/test/helpers/ceo-mode-option.ts +++ b/test/helpers/ceo-mode-option.ts @@ -175,6 +175,53 @@ function completedQuestionTimes(call: NativePlanQuestionCall, events: ReadonlyAr return { requestedAt, answeredAt }; } +/** New shorthand forms must be one complete decision, not a mode mention or extra question. */ +function singleScopeBrief(text: string, descriptions: readonly string[]): boolean { + if ([text, ...descriptions].some(value => /(?:^|[.!?]\s+|\n)\s*(?:Also|Separately|Additionally)\b|\b(?:Please|We must|You must|The plan must)\b/i.test(value))) return false; + // Query parameter names such as ?view= are not another decision prompt. + const questions = text.replace(/\?[A-Za-z_][\w-]*=/g, '=').match(/\?/g); + if (questions?.length !== 1 || /```|~~~|^\s*>/m.test(text)) return false; + const markers = [/Project\/branch\/task:/gi, /ELI10:/gi, /Stakes if (?:we pick )?wrong:/gi, + /Recommendation:/gi, /Completeness:/gi, /Net:/gi]; + let previous = -1; + const complete = markers.every(marker => { + const matches = [...text.matchAll(marker)]; + if (matches.length !== 1 || matches[0]!.index! <= previous) return false; + previous = matches[0]!.index!; + return true; + }); + // Net closes this decision brief. A following instruction is not part of its + // comparison; this is not a general classifier of instructions inside prose. + return complete && /^[^.!?;\n]+ (?:vs|versus) [^.!?;\n]+\.$/.test(text.slice(previous + 'Net:'.length).trim()); +} + +/** HOLD can apply its boundary in a completed defer decision, before standalone prose. */ +function hasAnsweredHoldPosture(transcript: PlanCountTranscript, selected: NativePlanQuestionCall, + posture: RegExp, events: ReadonlyArray): boolean { + const modeTimes = completedQuestionTimes(selected, events); + if (!modeTimes) return false; + return transcript.calls.some(call => { + if (call === selected || call.sessionId !== selected.sessionId || call.questions.length !== 1) return false; + const times = completedQuestionTimes(call, events); + if (!times || times.requestedAt <= modeTimes.answeredAt) return false; + const q = call.questions[0]!; + if (q.multiSelect || q.options.length !== 3 || !singleScopeBrief(q.question, q.options.map(o => o.description ?? ''))) return false; + const title = /^D\d+\s*[—–-]\s*Under HOLD SCOPE, keep or defer the (\w+)\b([\s\S]+?)\?\s+Project\/branch\/task:/i.exec(q.question); + if (!title || !/\bnot in the plan text\b/i.test(title[2]!) || + !/\bwritten scope is the baseline\b/i.test(q.question) || + !/\bpure additions,\s*not repairs to meet a stated invariant\b/i.test(q.question)) return false; + const labels = q.options.map(o => o.label.trim().replace(/^[A-C][):.]\s*/i, '') + .replace(/\s*\(recommended\)\s*$/i, '').toLowerCase()); + const count = title[1]!.toLowerCase(); + const defer = labels.findIndex(label => label === `defer all ${count} to todos` || label === `defer all ${count} to todos.md`); + const subset = labels.find(label => /^keep .+ only$/.test(label)); + if (new Set(labels).size !== 3 || defer < 0 || !labels.includes(`keep all ${count}`) || + !subset || !title[2]!.toLowerCase().includes(subset.slice(5, -5)) || + call.answers?.[q.question] !== q.options[defer]!.label) return false; + return hasPostAnswerCeoPosture(`● ${q.question}`, posture); + }); +} + /** A concrete, opted-in expansion brief is itself assistant posture evidence. */ function hasAnsweredExpansionPosture( transcript: PlanCountTranscript, selected: NativePlanQuestionCall, @@ -189,8 +236,12 @@ function hasAnsweredExpansionPosture( const question = call.questions[0]!; const title = question.question.split('\n')[0]! .replace(/\s*\s*$/i, ''); - if (question.multiSelect || question.options.length !== 3 || - !/^D\d+\s*[—–-]\s*Expansion\s+\d+\s+of\s+\d+:\s+\S.+\?\s*$/i.test(title)) return false; + const numbered = /^D\d+\s*[—–-]\s*Expansion\s+\d+\s+of\s+\d+:\s+\S.+\?\s*$/i.test(title); + const short = /^D\d+\s*[—–-]\s*E(\d+):\s+Add\s+[^?\n]+\?\s*$/i.exec(title); + const explicitShort = short && new RegExp(`^E${short[1]}\\s+\\S`, 'i').test(question.header) && + /\nProject\/branch\/task:[^\n]+\bSCOPE EXPANSION mode\.\s*\nELI10:/i.test(question.question) && + singleScopeBrief(question.question, question.options.map(o => o.description ?? '')); + if (question.multiSelect || question.options.length !== 3 || (!numbered && !explicitShort)) return false; const labels = question.options.map(option => option.label.trim() .replace(/^[A-C][):.]\s*/i, '').replace(/\s*\(recommended\)\s*$/i, '').toLowerCase()); if (new Set(labels).size !== 3 || !['add to scope', 'defer to todos.md', 'skip'].every(label => labels.includes(label)) || @@ -202,7 +253,7 @@ function hasAnsweredExpansionPosture( }); } -/** Match finalized assistant prose or an answered expansion after the actual mode answer. */ +/** Match finalized prose or a completed concrete scope decision after the actual mode answer. */ export function hasNativePostAnswerCeoPosture( transcript: PlanCountTranscript, targetMode: CeoMode, @@ -223,7 +274,8 @@ export function hasNativePostAnswerCeoPosture( return !/^(?:(?:You\s+)?selected(?:\s+(?:option|mode))?\s*[::]?\s*)?(?:HOLD SCOPE|SCOPE EXPANSION|SELECTIVE EXPANSION|SCOPE REDUCTION)(?:\s+mode)?(?:\s+confirmed)?(?:\s*\(recommended\))?[.!]?$/i.test(plain); }).join('\n'); return hasPostAnswerCeoPosture(`● ${prose}`, posture); - }) || (targetMode === 'SCOPE EXPANSION' && hasAnsweredExpansionPosture(transcript, selected, posture, publicTools)); + }) || (targetMode === 'SCOPE EXPANSION' && hasAnsweredExpansionPosture(transcript, selected, posture, publicTools)) || + (targetMode === 'HOLD SCOPE' && hasAnsweredHoldPosture(transcript, selected, posture, publicTools)); } type PosturePacket = { headers: string[]; screens: string[]; next: number; nativeId?: string; submitted: boolean }; diff --git a/test/helpers/plan-count-pending-question.ts b/test/helpers/plan-count-pending-question.ts index a40fb7a4c..efaed2eb9 100644 --- a/test/helpers/plan-count-pending-question.ts +++ b/test/helpers/plan-count-pending-question.ts @@ -26,6 +26,25 @@ function questions(value: unknown): value is NativePlanQuestion[] { new Set(value.map(q => q.header)).size === value.length && new Set(value.map(q => q.question)).size === value.length; } +/** Completion-only fields from Claude's permission UI; never retained as answers. */ +function completionInput(input: Record): boolean { + if (!keysOnly(input, ['questions', 'answers', 'annotations'])) return false; + const texts = new Set(input.questions.map((q: NativePlanQuestion) => q.question)); + if (input.answers !== undefined && (!object(input.answers) || Object.entries(input.answers).some(([key, value]) => + !texts.has(key) || typeof value !== 'string'))) return false; + if (input.annotations !== undefined && (!object(input.annotations) || Object.entries(input.annotations).some(([key, value]) => + !texts.has(key) || !object(value) || !keysOnly(value, ['preview', 'notes']) || + Object.values(value).some(field => typeof field !== 'string')))) return false; + return true; +} + +function questionIdentity(value: NativePlanQuestion[]): string { + // Claude normalizes object-key order after permission collection; question + // order, option order and every supported field must still match. + return JSON.stringify(value.map(q => ({header:q.header, question:q.question, multiSelect:q.multiSelect, + options:q.options.map(o => ({label:o.label, description:o.description}))}))); +} + /** One real parent JSONL, inside the owned config; never a subagent or symlink. */ function scopedTranscript(file: unknown, configDir: string, session: string): file is string { if (typeof file !== 'string' || !path.isAbsolute(file)) return false; @@ -113,12 +132,17 @@ export function recordPendingQuestion(input: string, file: string, cwd: string, if (!object(e) || e.cwd !== cwd || !['PreToolUse', 'PostToolUse', 'PostToolUseFailure'].includes(e.hook_event_name) || e.tool_name !== 'AskUserQuestion' || !identifier(e.session_id) || !identifier(e.tool_use_id) || !scopedTranscript(e.transcript_path, configDir, e.session_id) || - !object(e.tool_input) || !keysOnly(e.tool_input, ['questions']) || !questions(e.tool_input.questions)) { + !object(e.tool_input) || !questions(e.tool_input.questions) || + !(e.hook_event_name === 'PreToolUse' ? keysOnly(e.tool_input, ['questions']) : completionInput(e.tool_input))) { throw Error('invalid owned hook event'); } if (old.sessionId !== undefined && old.sessionId !== e.session_id) return; const state: State = {...old, sessionId:e.session_id}; const pending = old.pending; + if (e.hook_event_name !== 'PreToolUse' && pending?.toolUseId === e.tool_use_id && + (pending.transcriptPath !== e.transcript_path || questionIdentity(pending.questions) !== questionIdentity(e.tool_input.questions))) { + throw Error('completion does not match pending request'); + } if (e.hook_event_name === 'PreToolUse') { if (old.seenIds.includes(e.tool_use_id)) { if (pending?.toolUseId === e.tool_use_id && JSON.stringify(pending.questions) !== JSON.stringify(e.tool_input.questions)) { diff --git a/test/helpers/touchfiles-data.ts b/test/helpers/touchfiles-data.ts index 60d349127..bfaa8e8ba 100644 --- a/test/helpers/touchfiles-data.ts +++ b/test/helpers/touchfiles-data.ts @@ -139,7 +139,7 @@ export const E2E_TOUCHFILES: Record = { // Each one tests behavior the SDK harness can't observe (rendered TTY, // numbered-option lists, multi-phase ordering, idempotency state echo). 'auq-format-gate': ['plan-ceo-review/**', 'scripts/resolvers/preamble/generate-ask-user-format.ts', 'scripts/resolvers/preamble/generate-completeness-section.ts', 'scripts/resolvers/preamble.ts', 'test/helpers/auq-sdk-capture.ts', 'test/helpers/session-runner.ts', 'test/helpers/llm-judge.ts', 'test/skill-e2e-ask-user-question-format-compliance.test.ts'], - 'plan-ceo-mode-routing': ['plan-ceo-review/**', 'scripts/resolvers/preamble/generate-ask-user-format.ts', 'scripts/resolvers/preamble.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/pty-trust-dialog.ts', 'test/pty-trust-dialog.test.ts', 'test/helpers/ceo-mode-option.ts', 'test/ceo-mode-option.test.ts', 'test/pty-option-selection.test.ts', 'test/helpers/plan-count-transcript.ts', 'test/plan-count-transcript.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', 'test/skill-e2e-plan-ceo-mode-routing.test.ts', 'test/ceo-mode-posture-native.test.ts', 'test/fixtures/ceo-hold-posture-l.json', 'test/ceo-mode-labels-native.test.ts', 'test/fixtures/ceo-mode-labels-l.json', 'test/plan-count-checkbox.test.ts', 'test/fixtures/ceo-checkbox-l.screen.txt', 'test/ceo-mode-prerequisite.test.ts', 'test/fixtures/ceo-mode-prerequisite-o-calls.json', 'test/fixtures/ceo-mode-prerequisite-q-call.json', 'test/plan-count-preview-footer.test.ts', 'test/fixtures/ceo-preview-u-call.json', 'test/fixtures/ceo-preview-u-screen.txt', 'test/ceo-posture-packet.test.ts', 'test/fixtures/design-preview-v-screen.txt', 'test/plan-count-truncated-question.test.ts', 'test/fixtures/ceo-approach-z-call.json', 'test/fixtures/ceo-approach-z-screen.txt', 'test/fixtures/ceo-mode-preview-aa-screen.txt', 'test/fixtures/ceo-count-mode-preview-aa-screen.txt', 'test/ceo-expansion-auq.test.ts', 'test/fixtures/ceo-expansion-auq-ac.json', 'test/helpers/plan-count-pending-question.ts', 'test/autoplan-pending-question.test.ts', 'test/plan-pending-question-pty.test.ts', 'test/ceo-barless-submit.test.ts', 'test/fixtures/ceo-barless-submit-ac.json'], + 'plan-ceo-mode-routing': ['plan-ceo-review/**', 'scripts/resolvers/preamble/generate-ask-user-format.ts', 'scripts/resolvers/preamble.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/pty-trust-dialog.ts', 'test/pty-trust-dialog.test.ts', 'test/helpers/ceo-mode-option.ts', 'test/ceo-mode-option.test.ts', 'test/pty-option-selection.test.ts', 'test/helpers/plan-count-transcript.ts', 'test/plan-count-transcript.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', 'test/skill-e2e-plan-ceo-mode-routing.test.ts', 'test/ceo-mode-posture-native.test.ts', 'test/fixtures/ceo-hold-posture-l.json', 'test/ceo-mode-labels-native.test.ts', 'test/fixtures/ceo-mode-labels-l.json', 'test/plan-count-checkbox.test.ts', 'test/fixtures/ceo-checkbox-l.screen.txt', 'test/ceo-mode-prerequisite.test.ts', 'test/fixtures/ceo-mode-prerequisite-o-calls.json', 'test/fixtures/ceo-mode-prerequisite-q-call.json', 'test/plan-count-preview-footer.test.ts', 'test/fixtures/ceo-preview-u-call.json', 'test/fixtures/ceo-preview-u-screen.txt', 'test/ceo-posture-packet.test.ts', 'test/fixtures/design-preview-v-screen.txt', 'test/plan-count-truncated-question.test.ts', 'test/fixtures/ceo-approach-z-call.json', 'test/fixtures/ceo-approach-z-screen.txt', 'test/fixtures/ceo-mode-preview-aa-screen.txt', 'test/fixtures/ceo-count-mode-preview-aa-screen.txt', 'test/ceo-expansion-auq.test.ts', 'test/fixtures/ceo-expansion-auq-ac.json', 'test/helpers/plan-count-pending-question.ts', 'test/autoplan-pending-question.test.ts', 'test/plan-pending-question-pty.test.ts', 'test/ceo-barless-submit.test.ts', 'test/fixtures/ceo-barless-submit-ac.json', 'test/pending-question-completion.test.ts', 'test/fixtures/pending-question-completion-ad.json', 'test/ceo-mode-posture-ad.test.ts', 'test/fixtures/ceo-mode-posture-ad.json'], 'plan-design-with-ui-scope': ['plan-design-review/**', 'test/fixtures/plans/ui-heavy-feature.md', '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-with-ui.test.ts', 'test/plan-count-truncated-question.test.ts', 'test/fixtures/ceo-approach-z-call.json', 'test/fixtures/ceo-approach-z-screen.txt'], 'ship-idempotency-pty': ['ship/**', 'bin/gstack-next-version', 'bin/gstack-version-bump', 'scripts/resolvers/sections.ts', 'lib/worktree.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-ship-idempotency.test.ts', 'test/plan-count-truncated-question.test.ts', 'test/fixtures/ceo-approach-z-call.json', 'test/fixtures/ceo-approach-z-screen.txt'], 'tpa-present': ['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'], @@ -154,7 +154,7 @@ export const E2E_TOUCHFILES: Record = { // the selector sets GSTACK_CARVE_SKILL= 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/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'], + '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/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'], // Per-finding AskUserQuestion count + review-report-at-bottom assertion. // Each test drives its skill end-to-end; touchfiles include preamble + diff --git a/test/pending-question-completion.test.ts b/test/pending-question-completion.test.ts new file mode 100644 index 000000000..603e621aa --- /dev/null +++ b/test/pending-question-completion.test.ts @@ -0,0 +1,195 @@ +import { describe, expect, test } from 'bun:test'; +import * as fs from 'node:fs'; +import * as os from 'node:os'; +import * as path from 'node:path'; +import { spawnSync } from 'node:child_process'; +import { createPendingQuestionRecorder, recordPendingQuestion, readPendingQuestion, + pendingQuestionRecorderStatus } from './helpers/plan-count-pending-question'; +import { readPlanCountTranscript } from './helpers/plan-count-transcript'; +import { E2E_TOUCHFILES, selectTests } from './helpers/touchfiles'; +import captured from './fixtures/pending-question-completion-ad.json'; + +// Public question/result contents are retained; hook envelopes, paths and live +// timestamps below are synthetic. The raw historical hook stdin is unknown. +function fixture(index = 0) { + const actual = captured.cases[index]!; + const root = fs.mkdtempSync(path.join(os.tmpdir(), "pending-completion-' spaces-")); + const cwd = path.join(root, 'repo'), config = path.join(root, 'config'); + const project = path.join(config, 'projects', 'owned'); + fs.mkdirSync(cwd, {recursive:true}); fs.mkdirSync(project, {recursive:true}); + const session = actual.sessionId, transcriptPath = path.join(project, `${session}.jsonl`); + const startedAt = Date.now() - 1; + fs.writeFileSync(transcriptPath, JSON.stringify({cwd, sessionId:session, isSidechain:false, + timestamp:new Date().toISOString(), message:{role:'assistant', content:[{type:'text',text:'Synthetic recorder readiness.'}]}})+'\n'); + const recorder = createPendingQuestionRecorder(cwd, config); + const event = (kind = 'PreToolUse', id = actual.toolUseId): any => ({hook_event_name:kind, + tool_name:'AskUserQuestion', session_id:session, tool_use_id:id, cwd, transcript_path:transcriptPath, + tool_input:structuredClone(kind === 'PreToolUse' ? {questions:actual.questions} : actual.completionInput)}); + const write = (value: unknown) => recordPendingQuestion(JSON.stringify(value),recorder.file,cwd,config); + const read = () => readPendingQuestion(recorder.file,cwd,config,startedAt,readPlanCountTranscript(config,cwd)); + const status = () => pendingQuestionRecorderStatus(recorder.file,cwd,config); + const dispose = () => {recorder.dispose();fs.rmSync(root,{recursive:true,force:true});}; + return {actual,root,cwd,config,project,session,transcriptPath,recorder,event,write,read,status,dispose}; +} + +describe('scoped pending question completion payloads', () => { + for (let index=0;index { + const f=fixture(index); + try { + expect(captured.provenance.rawHookEnvelopeCaptured).toBe(false); + expect(f.actual.completionInput.questions).toEqual(f.actual.questions); + expect(JSON.stringify(f.actual.completionInput.questions)).not.toBe(JSON.stringify(f.actual.questions)); + f.write(f.event()); expect(f.read()?.toolUseId).toBe(f.actual.toolUseId); + f.write(f.event('PostToolUse')); + expect(f.status()).toEqual({status:'idle'}); + expect(f.read()).toBeUndefined(); + f.write(f.event('PreToolUse','toolu_next_owned')); + expect(f.read()).toMatchObject({toolUseId:'toolu_next_owned',answered:false,failed:false,source:'pre_tool_use'}); + expect(f.read()?.answers).toBeUndefined(); + expect(f.read()?.answeredAt).toBeUndefined(); + } finally {f.dispose();} + }); + } + + test.each(['PostToolUse','PostToolUseFailure'])('%s accepts optional completion maps and records no answers', kind => { + for (const fields of ['questions only','empty maps','notes and preview']) { + const f=fixture(); + try { + f.write(f.event()); + const completion=f.event(kind), question=f.actual.questions[0]!.question; + if (fields==='questions only') completion.tool_input={questions:completion.tool_input.questions}; + if (fields==='empty maps') {completion.tool_input.answers={};completion.tool_input.annotations={};} + if (fields==='notes and preview') { + completion.tool_input.answers={[question]:'Synthetic custom answer content'}; + completion.tool_input.annotations={[question]:{preview:'Synthetic preview content',notes:'Synthetic note content'}}; + } + f.write(completion); + expect(f.status()).toEqual({status:'idle'}); + expect(f.read()).toBeUndefined(); + const state=JSON.parse(fs.readFileSync(f.recorder.file,'utf8')); + expect(state.pending).toBeNull(); + expect(state.answers).toBeUndefined();expect(state.annotations).toBeUndefined(); + f.write(f.event('PreToolUse','toolu_after_completion')); + expect(f.read()?.toolUseId).toBe('toolu_after_completion'); + } finally {f.dispose();} + } + }); + + test.each(['answers','annotations'])('PreToolUse remains questions-only and rejects %s', field => { + const f=fixture(); + try { + const request=f.event();request.tool_input[field]={};f.write(request); + expect(f.status()).toMatchObject({status:'invalid'}); + f.write(f.event());expect(f.read()).toBeUndefined(); + } finally {f.dispose();} + }); + + test.each(['answers array','answers null','answer number','unknown answer question','annotations array', + 'annotations null','annotation string','unknown annotation question','preview number','notes boolean', + 'extra annotation property','extra input property','changed question','reordered questions','changed option', + 'reordered options','changed multiSelect'])('malformed owned completion %s poisons closed', change => { + const f=fixture(); + try { + f.write(f.event());const completion=f.event('PostToolUse'),input=completion.tool_input; + const question=f.actual.questions[0]!.question; + switch(change) { + case 'answers array':input.answers=[];break; + case 'answers null':input.answers=null;break; + case 'answer number':input.answers={[question]:1};break; + case 'unknown answer question':input.answers={'Foreign question':'Yes'};break; + case 'annotations array':input.annotations=[];break; + case 'annotations null':input.annotations=null;break; + case 'annotation string':input.annotations={[question]:'notes'};break; + case 'unknown annotation question':input.annotations={'Foreign question':{notes:'Text'}};break; + case 'preview number':input.annotations={[question]:{preview:1}};break; + case 'notes boolean':input.annotations={[question]:{notes:true}};break; + case 'extra annotation property':input.annotations={[question]:{notes:'Text',answer:'Yes'}};break; + case 'extra input property':input.approved=true;break; + case 'changed question':input.questions[0].question+=' Changed.';break; + case 'reordered questions':input.questions.reverse();break; + case 'changed option':input.questions[0].options[0].label+=' Changed.';break; + case 'reordered options':input.questions[0].options.reverse();break; + case 'changed multiSelect':input.questions[0].multiSelect=true;break; + } + f.write(completion);expect(f.status()).toMatchObject({status:'invalid'}); + expect(f.read()).toBeUndefined(); + f.write(f.event('PostToolUse'));f.write(f.event('PreToolUse','toolu_after_invalid')); + expect(f.read()).toBeUndefined(); + } finally {f.dispose();} + }); + + test.each(['cwd','session','subagent'])('foreign completion %s preserves the exact owned pending state', field => { + const f=fixture(); + try { + f.write(f.event());const before=fs.readFileSync(f.recorder.file,'utf8'); + for(const kind of ['PostToolUse','PostToolUseFailure']) { + const completion=f.event(kind); + if(field==='cwd')completion.cwd=path.join(f.root,'foreign'); + if(field==='subagent')completion.agent_id='foreign-subagent'; + if(field==='session') { + completion.session_id='foreign-session'; + completion.transcript_path=path.join(f.project,'foreign-session.jsonl'); + fs.writeFileSync(completion.transcript_path,''); + } + f.write(completion);expect(fs.readFileSync(f.recorder.file,'utf8')).toBe(before); + expect(f.read()?.toolUseId).toBe(f.actual.toolUseId); + } + } finally {f.dispose();} + }); + + test('a same-session completion from another native transcript cannot clear the pending request', () => { + const f=fixture(); + try { + f.write(f.event());const completion=f.event('PostToolUse'); + const other=path.join(f.config,'projects','other');fs.mkdirSync(other); + completion.transcript_path=path.join(other,`${f.session}.jsonl`); + fs.writeFileSync(completion.transcript_path,''); + f.write(completion);expect(f.status()).toMatchObject({status:'invalid'}); + f.write(f.event('PostToolUse'));f.write(f.event('PreToolUse','toolu_after_foreign_path')); + expect(f.read()).toBeUndefined(); + } finally {f.dispose();} + }); + + test('a late other-ID completion records a tombstone without clearing or replacing the current question', () => { + const f=fixture(); + try { + f.write(f.event());const before=f.read(); + f.write(f.event('PostToolUse','toolu_prior_question')); + expect(f.read()).toEqual(before); + f.write(f.event('PreToolUse','toolu_prior_question'));expect(f.read()).toEqual(before); + f.write(f.event('PostToolUse'));expect(f.status()).toEqual({status:'idle'}); + f.write(f.event('PreToolUse','toolu_prior_question'));expect(f.read()).toBeUndefined(); + f.write(f.event('PreToolUse','toolu_after_both'));expect(f.read()?.toolUseId).toBe('toolu_after_both'); + f.write(f.event('PostToolUse'));expect(f.read()?.toolUseId).toBe('toolu_after_both'); + } finally {f.dispose();} + }); + + test('completion before PreToolUse cannot be reopened by a replay', () => { + const f=fixture(); + try { + f.write(f.event('PostToolUse'));expect(f.status()).toEqual({status:'idle'}); + f.write(f.event());expect(f.read()).toBeUndefined(); + f.write(f.event('PreToolUse','toolu_newer'));expect(f.read()?.toolUseId).toBe('toolu_newer'); + } finally {f.dispose();} + }); + + test('the actual generated completion hook is silent and grants no answer or tool decision', () => { + const f=fixture(); + try { + f.write(f.event()); + const command=f.recorder.hooks.PostToolUse[0]!.hooks[0]!.command; + const result=spawnSync('bash',['-c',command],{cwd:f.cwd,input:JSON.stringify(f.event('PostToolUse')), + encoding:'utf8',timeout:6000}); + expect(result.error).toBeUndefined();expect(result.status).toBe(0); + expect(result.stdout).toBe('');expect(result.stderr).toBe(''); + expect(f.status()).toEqual({status:'idle'});expect(f.read()).toBeUndefined(); + } finally {f.dispose();} + }); + + test('the completion regression and fixture select exactly the two opted-in paid workflows', () => { + for(const file of ['test/pending-question-completion.test.ts','test/fixtures/pending-question-completion-ad.json']) { + expect(selectTests([file],E2E_TOUCHFILES,[]).selected.sort()).toEqual(['autoplan-chain-pty','plan-ceo-mode-routing']); + } + }); +});