fix: preserve complete review inputs and planning decisions

This commit is contained in:
Garry Tan committed 2026-09-09 05:25:42 +00:00
1 parent 33882c1b6e
commit e804d374d4
37 files changed
+1966 -113

No files matched your search

+2 -2
View File
@@ -16,8 +16,8 @@
- Failed, refused, empty, or malformed outside reviews can no longer count as a clean pass. Claude runner failures include authentication, timeout, and output overflow diagnoses, and stale skills stop before invoking their own harness.
- Spec review stops when redaction fails, before sending the spec to a reviewer or saving it downstream.
- Planning reviews request each unresolved decision before editing and carry approved remedies across sections without asking again. Choosing a scope or approach does not approve every finding.
- Autoplan reads the review skills installed for the current harness at each phase, waits for its reviewers, and reports completion after saving the required outputs. Disabling extra plan or documentation review also skips replacement reviewers.
- Planning reviews request each unresolved decision before editing and carry approved remedies across sections without asking again. Choosing a scope or approach does not approve every finding. Reviews preserve stated requirements unless you authorize changing them.
- Autoplan preserves the original plan and gives each reviewer the complete current phase input. It reads the review skills installed for the current harness, waits for its reviewers, and reports completion after saving the required outputs. Disabling extra plan or documentation review also skips replacement reviewers.
## [1.81.0.0] - 2026-09-06
+18 -27
View File
@@ -623,12 +623,12 @@ Phases MUST execute in strict order: CEO → Design (if UI scope) → DX (if
developer-facing scope) → Eng. Eng runs LAST, always, reviewing all prior amendments.
Keep ONE phase active, completing these gates in order:
1. Load its phase instructions and full skill/sections, recording complete Read ranges.
2. Create the fresh snapshot and dispatch its generated nativePrompt unchanged.
2. Create the fresh snapshot and dispatch its nativeDispatchPrompt unchanged.
3. Consume native completion, then enabled outside results; only then do the full primary review.
4. Persist outputs/amendments and run the phase's implementation check/readback.
5. Emit actual completion; only then load the next required phase.
A missing gate means the current phase remains open, even if a reviewer finished.
INPUT correlates reported input; it is not independent proof of uptake or review quality.
Read requests/self-reports and INPUT hashes do not prove uptake or review quality.
Never draft future-phase reviews or outputs. Headings/promises are not completion.
After compaction, reload current phase instructions/skill/sections; reconcile disk progress before resuming.
@@ -689,10 +689,14 @@ Prefix every Codex prompt:
### Step 1: Capture restore point
Pin absolute paths: SOURCE_PLAN is the input; ACTIVE_PLAN is the harness-assigned
plan, else SOURCE_PLAN. All amendments/outputs go to ACTIVE_PLAN. Save
SOURCE_PLAN's full current state externally:
Absolute paths: SOURCE_PLAN (input), ACTIVE_PLAN (harness-assigned plan, else SOURCE_PLAN).
Write all amendments/outputs to ACTIVE_PLAN. Resolve SNAPSHOT_TOOL once:
```bash
bun -e 'console.log(require("fs").realpathSync(process.argv[1]))' "$HOME/.claude/skills/gstack/bin/gstack-autoplan-snapshot.ts"
```
Fresh external RESTORE_PATH:
```bash
eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)" && mkdir -p ~/.gstack/projects/$SLUG
BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null | tr '/' '-')
@@ -700,28 +704,15 @@ DATETIME=$(date +%Y%m%d-%H%M%S)
echo "RESTORE_PATH=$HOME/.gstack/projects/$SLUG/${BRANCH}-autoplan-restore-${DATETIME}.md"
```
Write SOURCE_PLAN's full contents to the restore path with this header:
```
# /autoplan Restore Point
Captured: [timestamp] | Branch: [branch] | Commit: [short hash]
## Re-run Instructions
1. Copy "Original Plan State" below back to your plan file
2. Invoke /autoplan
## Original Plan State
[verbatim plan file contents]
```
Copy SOURCE_PLAN into ACTIVE_PLAN's `## Implementation plan` without dropping
requirements. Keep analyses/audit in `## Review record`. Prepend:
`<!-- /autoplan restore point: [RESTORE_PATH] -->`
Save this absolute path as SNAPSHOT_TOOL for all shells. Stop on helper errors:
Before scope/review:
```bash
bun -e 'console.log(require("fs").realpathSync(process.argv[1]))' "$HOME/.claude/skills/gstack/bin/gstack-autoplan-snapshot.ts"
bun "<SNAPSHOT_TOOL>" init "<SOURCE_PLAN>" "<ACTIVE_PLAN>" "<RESTORE_PATH>"
```
Use returned paths/`scope`; never hand-wrap. init backs up SOURCE_PLAN exactly,
then initializes ACTIVE_PLAN atomically without losing requirements.
Reviewers get only `## Implementation plan`; analysis stays in `## Review record`,
including structured inputs. On helper errors, stop; no stderr hiding/grep fallback.
Re-run: copy RESTORE_PATH's bytes to SOURCE_PLAN, then /autoplan.
### Step 2: Read context
@@ -730,11 +721,11 @@ bun -e 'console.log(require("fs").realpathSync(process.argv[1]))' "$HOME/.claude
- Detect UI scope: grep the plan for view/rendering terms (component, screen, form,
button, modal, layout, dashboard, sidebar, nav, dialog). Require 2+ matches. Exclude
false positives ("page" alone, "UI" in acronyms).
- Detect DX scope from the full implementation input:
- Use init's full-input `scope`. For changed input or semantic enabling flags, rerun:
```bash
bun "<SNAPSHOT_TOOL>" scope "<ACTIVE_PLAN>"
```
Use returned `dxRequired` and record its input hash/matched terms. The existing
Use returned `dxRequired` (initially `scope.dxRequired`) and record its input hash/matched terms. The existing
threshold is 2+ term matches (occurrences, not distinct terms). Also enable DX when the product is a developer tool
(developers install, integrate or build on it) or an AI agent is the primary user:
add `--developer-tool` or `--agent-primary` to this command. These flags only enable
+16 -25
View File
@@ -108,12 +108,12 @@ Phases MUST execute in strict order: CEO → Design (if UI scope) → DX (if
developer-facing scope) → Eng. Eng runs LAST, always, reviewing all prior amendments.
Keep ONE phase active, completing these gates in order:
1. Load its phase instructions and full skill/sections, recording complete Read ranges.
2. Create the fresh snapshot and dispatch its generated nativePrompt unchanged.
2. Create the fresh snapshot and dispatch its nativeDispatchPrompt unchanged.
3. Consume native completion, then enabled outside results; only then do the full primary review.
4. Persist outputs/amendments and run the phase's implementation check/readback.
5. Emit actual completion; only then load the next required phase.
A missing gate means the current phase remains open, even if a reviewer finished.
INPUT correlates reported input; it is not independent proof of uptake or review quality.
Read requests/self-reports and INPUT hashes do not prove uptake or review quality.
Never draft future-phase reviews or outputs. Headings/promises are not completion.
After compaction, reload current phase instructions/skill/sections; reconcile disk progress before resuming.
@@ -174,10 +174,11 @@ Prefix every {{OUTSIDE_LABEL}} prompt:
### Step 1: Capture restore point
Pin absolute paths: SOURCE_PLAN is the input; ACTIVE_PLAN is the harness-assigned
plan, else SOURCE_PLAN. All amendments/outputs go to ACTIVE_PLAN. Save
SOURCE_PLAN's full current state externally:
Absolute paths: SOURCE_PLAN (input), ACTIVE_PLAN (harness-assigned plan, else SOURCE_PLAN).
Write all amendments/outputs to ACTIVE_PLAN. Resolve SNAPSHOT_TOOL once:
{{AUTOPLAN_SNAPSHOT_TOOL}}
Fresh external RESTORE_PATH:
```bash
{{SLUG_SETUP}}
BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null | tr '/' '-')
@@ -185,25 +186,15 @@ DATETIME=$(date +%Y%m%d-%H%M%S)
echo "RESTORE_PATH=$HOME/.gstack/projects/$SLUG/${BRANCH}-autoplan-restore-${DATETIME}.md"
```
Write SOURCE_PLAN's full contents to the restore path with this header:
Before scope/review:
```bash
bun "<SNAPSHOT_TOOL>" init "<SOURCE_PLAN>" "<ACTIVE_PLAN>" "<RESTORE_PATH>"
```
# /autoplan Restore Point
Captured: [timestamp] | Branch: [branch] | Commit: [short hash]
## Re-run Instructions
1. Copy "Original Plan State" below back to your plan file
2. Invoke /autoplan
## Original Plan State
[verbatim plan file contents]
```
Copy SOURCE_PLAN into ACTIVE_PLAN's `## Implementation plan` without dropping
requirements. Keep analyses/audit in `## Review record`. Prepend:
`<!-- /autoplan restore point: [RESTORE_PATH] -->`
Save this absolute path as SNAPSHOT_TOOL for all shells. Stop on helper errors:
{{AUTOPLAN_SNAPSHOT_TOOL}}
Use returned paths/`scope`; never hand-wrap. init backs up SOURCE_PLAN exactly,
then initializes ACTIVE_PLAN atomically without losing requirements.
Reviewers get only `## Implementation plan`; analysis stays in `## Review record`,
including structured inputs. On helper errors, stop; no stderr hiding/grep fallback.
Re-run: copy RESTORE_PATH's bytes to SOURCE_PLAN, then /autoplan.
### Step 2: Read context
@@ -212,11 +203,11 @@ Save this absolute path as SNAPSHOT_TOOL for all shells. Stop on helper errors:
- Detect UI scope: grep the plan for view/rendering terms (component, screen, form,
button, modal, layout, dashboard, sidebar, nav, dialog). Require 2+ matches. Exclude
false positives ("page" alone, "UI" in acronyms).
- Detect DX scope from the full implementation input:
- Use init's full-input `scope`. For changed input or semantic enabling flags, rerun:
```bash
bun "<SNAPSHOT_TOOL>" scope "<ACTIVE_PLAN>"
```
Use returned `dxRequired` and record its input hash/matched terms. The existing
Use returned `dxRequired` (initially `scope.dxRequired`) and record its input hash/matched terms. The existing
threshold is 2+ term matches (occurrences, not distinct terms). Also enable DX when the product is a developer tool
(developers install, integrate or build on it) or an AI agent is the primary user:
add `--developer-tool` or `--agent-primary` to this command. These flags only enable
+4 -3
View File
@@ -28,15 +28,16 @@ bun "<SNAPSHOT_TOOL>" create ceo "<ACTIVE_PLAN>" "<RESTORE_PATH>"
Claude Code: set Agent `run_in_background: false` if its schema exposes it.
Other hosts: use foreground dispatch and await completion when supported.
Send `nativePrompt` verbatim. If truncated, Read `nativePromptPath` to EOF.
It contains all criteria and snapshot bytes; no summaries or prior reviews.
Send `nativeDispatchPrompt` verbatim: ONLY/FINAL tool call this response.
Keep native Reads enabled. Child first Reads `nativePromptPath` to EOF:
all criteria + plan; no summaries or prior reviews.
**Native completion barrier:** If `isAsync: true` / `status: "async_launched"`,
Claude Code: immediately end this response with "Waiting for <agent ID>."
Do no more tool calls or review work until that ID's terminal notification is
delivered. Other hosts: await that ID. Then outside → this phase's review ONLY.
For completed native reviews, match INPUT phase/hash to this snapshot. Missing
or mismatched INPUT: retry the full payload once, then use failure policy if
or mismatched INPUT: retry this dispatch once, then use failure policy if
still invalid.
No inline substitute; apply failure policy.
+4 -3
View File
@@ -26,15 +26,16 @@ bun "<SNAPSHOT_TOOL>" create ceo "<ACTIVE_PLAN>" "<RESTORE_PATH>"
Claude Code: set Agent `run_in_background: false` if its schema exposes it.
Other hosts: use foreground dispatch and await completion when supported.
Send `nativePrompt` verbatim. If truncated, Read `nativePromptPath` to EOF.
It contains all criteria and snapshot bytes; no summaries or prior reviews.
Send `nativeDispatchPrompt` verbatim: ONLY/FINAL tool call this response.
Keep native Reads enabled. Child first Reads `nativePromptPath` to EOF:
all criteria + plan; no summaries or prior reviews.
**Native completion barrier:** If `isAsync: true` / `status: "async_launched"`,
Claude Code: immediately end this response with "Waiting for <agent ID>."
Do no more tool calls or review work until that ID's terminal notification is
delivered. Other hosts: await that ID. Then outside → this phase's review ONLY.
For completed native reviews, match INPUT phase/hash to this snapshot. Missing
or mismatched INPUT: retry the full payload once, then use failure policy if
or mismatched INPUT: retry this dispatch once, then use failure policy if
still invalid.
No inline substitute; apply failure policy.
+4 -3
View File
@@ -19,15 +19,16 @@ bun "<SNAPSHOT_TOOL>" create design "<ACTIVE_PLAN>" "<RESTORE_PATH>"
Claude Code: set Agent `run_in_background: false` if its schema exposes it.
Other hosts: use foreground dispatch and await completion when supported.
Send `nativePrompt` verbatim. If truncated, Read `nativePromptPath` to EOF.
It contains all criteria and snapshot bytes; no summaries or prior reviews.
Send `nativeDispatchPrompt` verbatim: ONLY/FINAL tool call this response.
Keep native Reads enabled. Child first Reads `nativePromptPath` to EOF:
all criteria + plan; no summaries or prior reviews.
**Native completion barrier:** If `isAsync: true` / `status: "async_launched"`,
Claude Code: immediately end this response with "Waiting for <agent ID>."
Do no more tool calls or review work until that ID's terminal notification is
delivered. Other hosts: await that ID. Then outside → this phase's review ONLY.
For completed native reviews, match INPUT phase/hash to this snapshot. Missing
or mismatched INPUT: retry the full payload once, then use failure policy if
or mismatched INPUT: retry this dispatch once, then use failure policy if
still invalid.
No inline substitute; apply failure policy.
+4 -3
View File
@@ -17,15 +17,16 @@ bun "<SNAPSHOT_TOOL>" create design "<ACTIVE_PLAN>" "<RESTORE_PATH>"
Claude Code: set Agent `run_in_background: false` if its schema exposes it.
Other hosts: use foreground dispatch and await completion when supported.
Send `nativePrompt` verbatim. If truncated, Read `nativePromptPath` to EOF.
It contains all criteria and snapshot bytes; no summaries or prior reviews.
Send `nativeDispatchPrompt` verbatim: ONLY/FINAL tool call this response.
Keep native Reads enabled. Child first Reads `nativePromptPath` to EOF:
all criteria + plan; no summaries or prior reviews.
**Native completion barrier:** If `isAsync: true` / `status: "async_launched"`,
Claude Code: immediately end this response with "Waiting for <agent ID>."
Do no more tool calls or review work until that ID's terminal notification is
delivered. Other hosts: await that ID. Then outside → this phase's review ONLY.
For completed native reviews, match INPUT phase/hash to this snapshot. Missing
or mismatched INPUT: retry the full payload once, then use failure policy if
or mismatched INPUT: retry this dispatch once, then use failure policy if
still invalid.
No inline substitute; apply failure policy.
+4 -3
View File
@@ -23,15 +23,16 @@ bun "<SNAPSHOT_TOOL>" create dx "<ACTIVE_PLAN>" "<RESTORE_PATH>"
Claude Code: set Agent `run_in_background: false` if its schema exposes it.
Other hosts: use foreground dispatch and await completion when supported.
Send `nativePrompt` verbatim. If truncated, Read `nativePromptPath` to EOF.
It contains all criteria and snapshot bytes; no summaries or prior reviews.
Send `nativeDispatchPrompt` verbatim: ONLY/FINAL tool call this response.
Keep native Reads enabled. Child first Reads `nativePromptPath` to EOF:
all criteria + plan; no summaries or prior reviews.
**Native completion barrier:** If `isAsync: true` / `status: "async_launched"`,
Claude Code: immediately end this response with "Waiting for <agent ID>."
Do no more tool calls or review work until that ID's terminal notification is
delivered. Other hosts: await that ID. Then outside → this phase's review ONLY.
For completed native reviews, match INPUT phase/hash to this snapshot. Missing
or mismatched INPUT: retry the full payload once, then use failure policy if
or mismatched INPUT: retry this dispatch once, then use failure policy if
still invalid.
No inline substitute; apply failure policy.
+4 -3
View File
@@ -21,15 +21,16 @@ bun "<SNAPSHOT_TOOL>" create dx "<ACTIVE_PLAN>" "<RESTORE_PATH>"
Claude Code: set Agent `run_in_background: false` if its schema exposes it.
Other hosts: use foreground dispatch and await completion when supported.
Send `nativePrompt` verbatim. If truncated, Read `nativePromptPath` to EOF.
It contains all criteria and snapshot bytes; no summaries or prior reviews.
Send `nativeDispatchPrompt` verbatim: ONLY/FINAL tool call this response.
Keep native Reads enabled. Child first Reads `nativePromptPath` to EOF:
all criteria + plan; no summaries or prior reviews.
**Native completion barrier:** If `isAsync: true` / `status: "async_launched"`,
Claude Code: immediately end this response with "Waiting for <agent ID>."
Do no more tool calls or review work until that ID's terminal notification is
delivered. Other hosts: await that ID. Then outside → this phase's review ONLY.
For completed native reviews, match INPUT phase/hash to this snapshot. Missing
or mismatched INPUT: retry the full payload once, then use failure policy if
or mismatched INPUT: retry this dispatch once, then use failure policy if
still invalid.
No inline substitute; apply failure policy.
+4 -3
View File
@@ -16,15 +16,16 @@ bun "<SNAPSHOT_TOOL>" create eng "<ACTIVE_PLAN>" "<RESTORE_PATH>"
Claude Code: set Agent `run_in_background: false` if its schema exposes it.
Other hosts: use foreground dispatch and await completion when supported.
Send `nativePrompt` verbatim. If truncated, Read `nativePromptPath` to EOF.
It contains all criteria and snapshot bytes; no summaries or prior reviews.
Send `nativeDispatchPrompt` verbatim: ONLY/FINAL tool call this response.
Keep native Reads enabled. Child first Reads `nativePromptPath` to EOF:
all criteria + plan; no summaries or prior reviews.
**Native completion barrier:** If `isAsync: true` / `status: "async_launched"`,
Claude Code: immediately end this response with "Waiting for <agent ID>."
Do no more tool calls or review work until that ID's terminal notification is
delivered. Other hosts: await that ID. Then outside → this phase's review ONLY.
For completed native reviews, match INPUT phase/hash to this snapshot. Missing
or mismatched INPUT: retry the full payload once, then use failure policy if
or mismatched INPUT: retry this dispatch once, then use failure policy if
still invalid.
No inline substitute; apply failure policy.
+4 -3
View File
@@ -14,15 +14,16 @@ bun "<SNAPSHOT_TOOL>" create eng "<ACTIVE_PLAN>" "<RESTORE_PATH>"
Claude Code: set Agent `run_in_background: false` if its schema exposes it.
Other hosts: use foreground dispatch and await completion when supported.
Send `nativePrompt` verbatim. If truncated, Read `nativePromptPath` to EOF.
It contains all criteria and snapshot bytes; no summaries or prior reviews.
Send `nativeDispatchPrompt` verbatim: ONLY/FINAL tool call this response.
Keep native Reads enabled. Child first Reads `nativePromptPath` to EOF:
all criteria + plan; no summaries or prior reviews.
**Native completion barrier:** If `isAsync: true` / `status: "async_launched"`,
Claude Code: immediately end this response with "Waiting for <agent ID>."
Do no more tool calls or review work until that ID's terminal notification is
delivered. Other hosts: await that ID. Then outside → this phase's review ONLY.
For completed native reviews, match INPUT phase/hash to this snapshot. Missing
or mismatched INPUT: retry the full payload once, then use failure policy if
or mismatched INPUT: retry this dispatch once, then use failure policy if
still invalid.
No inline substitute; apply failure policy.
+136 -4
View File
@@ -1,8 +1,8 @@
#!/usr/bin/env bun
/** Autoplan's blind reviewer inputs contain only the current implementation plan. */
import { createHash } from 'node:crypto';
import { mkdtempSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync } from 'node:fs';
import { basename, dirname, join } from 'node:path';
import { linkSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, renameSync, rmdirSync, rmSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
import { basename, dirname, isAbsolute, join } from 'node:path';
const PHASES = ['ceo', 'design', 'dx', 'eng'];
const sha256 = (text: string) => createHash('sha256').update(text).digest('hex');
@@ -94,6 +94,123 @@ export function extractImplementationPlan(plan: string): string {
return body;
}
/** Initialize the existing strict section contract before any scope or review call. */
export function initializePlan(sourcePlan: string, activePlan: string, restorePath: string) {
if (![sourcePlan, activePlan, restorePath].every(isAbsolute)) throw new Error('Initialization requires three absolute paths');
const source = realpathSync(sourcePlan);
const destination = (file: string) => {
let parent = dirname(file);
const missing: string[] = [];
while (!lstatSync(parent, { throwIfNoEntry: false })) {
missing.unshift(basename(parent)); parent = dirname(parent);
}
const canonical = join(realpathSync(parent), ...missing, basename(file));
const state = lstatSync(canonical, { throwIfNoEntry: false });
if (state && !state.isFile()) throw new Error('Initialization destinations must be regular files, not links or directories');
return { file: canonical, state, bytes: state ? readFileSync(canonical) : undefined };
};
const active = destination(activePlan);
const restore = destination(restorePath);
const sourceState = statSync(source);
if (!sourceState.isFile()) throw new Error('Initialization source must be a regular file');
const sourceBytes = readFileSync(source);
const sameFile = (a: typeof sourceState, b: typeof sourceState) => a.dev === b.dev && a.ino === b.ino;
if (restore.file === source || restore.file === active.file ||
(restore.state && (sameFile(restore.state, sourceState) || (active.state && sameFile(restore.state, active.state)))) ||
(active.state && active.file !== source && sameFile(active.state, sourceState))) {
throw new Error('Initialization source, active and restore paths have an ambiguous alias');
}
const normalized = (original: Buffer) => {
const text = original.toString('utf8');
if (!text.trim() || !Buffer.from(text).equals(original)) throw new Error('Initialization source must be nonempty UTF-8 text');
let plan = text;
try { extractImplementationPlan(plan); }
catch {
plan = `## Implementation plan\n${text}${text.endsWith('\n') ? '' : '\n'}## Review record\n`;
// Partial/duplicate boundaries and unclosed fences remain errors, not raw-plan fallbacks.
extractImplementationPlan(plan);
}
const reference = JSON.stringify(restore.file).replace(/--/g, '\\u002d\\u002d');
return Buffer.from(`<!-- /autoplan restore point: ${reference} -->\n${plan}`);
};
const result = (original: Buffer, reused: boolean) => ({
sourcePlan: source, activePlan: active.file, restorePath: restore.file,
originalSha256: sha256(original.toString('utf8')), originalBytes: original.length,
reused, scope: detectDxScope(active.file),
});
if (restore.bytes) {
if (!active.bytes?.equals(normalized(restore.bytes)) || (source !== active.file && !sourceBytes.equals(restore.bytes))) {
throw new Error('Existing restore does not match this initialization; preserve it and use a new restore path');
}
return result(restore.bytes, true);
}
if (active.bytes && active.bytes.length && !active.bytes.equals(sourceBytes)) {
throw new Error('Active plan already has different content; refusing to overwrite it');
}
const next = normalized(sourceBytes);
const expectedScopeHash = sha256(extractImplementationPlan(next.toString('utf8')));
const unchanged = () => {
const now = statSync(source);
const current = lstatSync(active.file, { throwIfNoEntry: false });
if (!sameFile(now, sourceState) || !readFileSync(source).equals(sourceBytes) ||
(active.state ? !current?.isFile() || !sameFile(current, active.state) || !readFileSync(active.file).equals(active.bytes!) : current !== undefined)) {
throw new Error('Initialization input or destination changed; refusing to overwrite it');
}
};
let activeStage: string | undefined;
let restoreStage: string | undefined;
let backupPublished = false;
let activePublished = false;
const createdParents: string[] = [];
const ensureParent = (dir: string) => {
const existing = lstatSync(dir, { throwIfNoEntry: false });
if (existing) {
if (!existing.isDirectory() || realpathSync(dir) !== dir) throw new Error('Initialization parent changed or is not a directory');
return;
}
ensureParent(dirname(dir));
mkdirSync(dir, { mode: 0o700 });
createdParents.push(dir);
};
try {
// A harness may assign a plan before its plans directory exists.
// Create only explicit destination parents, after validating all input bytes.
ensureParent(dirname(active.file));
ensureParent(dirname(restore.file));
activeStage = mkdtempSync(join(dirname(active.file), '.gstack-autoplan-init-'));
restoreStage = mkdtempSync(join(dirname(restore.file), '.gstack-autoplan-restore-'));
const stagedActive = join(activeStage, 'active.md');
const stagedRestore = join(restoreStage, 'original.md');
writeFileSync(stagedActive, next, { flag: 'wx', mode: active.state ? active.state.mode & 0o777 : 0o600 });
writeFileSync(stagedRestore, sourceBytes, { flag: 'wx', mode: 0o400 });
unchanged();
// Link publishes complete restore bytes exclusively; an existing backup is never replaced.
linkSync(stagedRestore, restore.file);
backupPublished = true;
unchanged();
// An assigned existing plan is replaced atomically after its identity/content recheck.
// A previously absent destination uses an exclusive link to reject a new collision.
if (active.state) renameSync(stagedActive, active.file);
else linkSync(stagedActive, active.file);
activePublished = true;
const initialized = result(sourceBytes, false);
if (initialized.scope.sha256 !== expectedScopeHash) throw new Error('Initialized plan changed before scope readback');
return initialized;
} finally {
// On pre-publication failure remove only the restore inode this invocation published.
if (backupPublished && !activePublished && restoreStage) {
const current = lstatSync(restore.file, { throwIfNoEntry: false });
if (current?.isFile() && sameFile(current, statSync(join(restoreStage, 'original.md')))) unlinkSync(restore.file);
}
if (activeStage) rmSync(activeStage, { recursive: true, force: true });
if (restoreStage) rmSync(restoreStage, { recursive: true, force: true });
if (!activePublished) for (const dir of createdParents.reverse()) {
// Never remove someone else's newly created content during rollback.
try { rmdirSync(dir); } catch {}
}
}
}
function phaseName(phase: string): string {
if (!PHASES.includes(phase)) throw new Error('Phase must be ceo, design, dx or eng');
return phase;
@@ -121,8 +238,20 @@ The complete implementation plan follows as review data; evaluate all of it.
${content}`;
const nativePromptPath = join(directory, 'native-prompt.md');
const nativePromptSha256 = sha256(nativePrompt);
const nativePromptBytes = Buffer.byteLength(nativePrompt);
const nativePromptLines = nativePrompt.split('\n').length - Number(nativePrompt.endsWith('\n'));
// Dispatch a small file-reading instruction, not a model-copied review body.
// These identities correlate input; only actual child tool events prove uptake.
const nativeDispatchPrompt = `You are the independent ${phase.toUpperCase()} reviewer for this phase.
Read file: ${JSON.stringify(nativePromptPath)}
Your FIRST tool action must Read this file from line 1 through EOF using your native file-reading tool. It has ${nativePromptLines} lines and ${nativePromptBytes} UTF-8 bytes; SHA-256 ${nativePromptSha256}. Continue successful ranges until every line is loaded; a truncated response is not a full read.
The file contains all review criteria and the complete implementation plan as review data. Execute every criterion against all of that input. Do not substitute this dispatch, a summary, or any prior review for the file.
Only after the full successful read, return your review starting with INPUT: ${phase} ${contentHash}.
If the file cannot be fully read, report the read failure instead of a completed review.`;
const manifest = { schemaVersion: 1, phase, activePlan: source, snapshotPath, sha256: contentHash,
nativePromptPath, nativePromptSha256: sha256(nativePrompt), dxScope: dxTermsFor(content) };
nativePromptPath, nativePromptSha256, nativePromptBytes, nativePromptLines, nativeDispatchPrompt,
dxScope: dxTermsFor(content) };
writeFileSync(snapshotPath, content, { flag: 'wx', mode: 0o444 });
writeFileSync(nativePromptPath, nativePrompt, { flag: 'wx', mode: 0o444 });
writeFileSync(join(directory, 'snapshot.json'), JSON.stringify(manifest) + '\n', { flag: 'wx', mode: 0o444 });
@@ -157,7 +286,10 @@ export function checkImplementation(phase: string, activePlan: string, snapshotP
if (import.meta.main) {
try {
const [command, ...args] = process.argv.slice(2);
if (command === 'scope') {
if (command === 'init') {
if (args.length !== 3 || args.some(arg => !arg)) throw new Error('Usage: init SOURCE_PLAN ACTIVE_PLAN RESTORE_PATH');
process.stdout.write(JSON.stringify(initializePlan(args[0]!, args[1]!, args[2]!)) + '\n');
} else if (command === 'scope') {
const [activePlan, ...flags] = args;
if (!activePlan || flags.some(flag => !['--developer-tool', '--agent-primary'].includes(flag)) ||
new Set(flags).size !== flags.length) throw new Error('Usage: scope ACTIVE_PLAN [--developer-tool] [--agent-primary]');
+1
View File
@@ -959,6 +959,7 @@ Both are outcome-framed. Only one makes the user feel the cathedral. Lead with t
**For HOLD SCOPE** — run this:
1. Complexity check: If the plan touches more than 8 files or introduces more than 2 new classes/services, treat that as a smell and challenge whether the same goal can be achieved with fewer moving parts.
2. What is the minimum set of changes that achieves the stated goal? Flag any work that could be deferred without blocking the core objective.
3. Keep stated invariants and acceptance criteria; repairs needed to meet them are in scope.
**For SCOPE REDUCTION** — run this:
1. Ruthless cut: What is the absolute minimum that ships value to a user? Everything else is deferred. No exceptions.
+1
View File
@@ -317,6 +317,7 @@ Both are outcome-framed. Only one makes the user feel the cathedral. Lead with t
**For HOLD SCOPE** — run this:
1. Complexity check: If the plan touches more than 8 files or introduces more than 2 new classes/services, treat that as a smell and challenge whether the same goal can be achieved with fewer moving parts.
2. What is the minimum set of changes that achieves the stated goal? Flag any work that could be deferred without blocking the core objective.
3. Keep stated invariants and acceptance criteria; repairs needed to meet them are in scope.
**For SCOPE REDUCTION** — run this:
1. Ruthless cut: What is the absolute minimum that ships value to a user? Everything else is deferred. No exceptions.
@@ -23,6 +23,21 @@ still needs its own decision; explain what the earlier remedy does not cover.
This does not approve an unraised finding or a new TODO: continue to present each
new finding and each potential TODO individually under the rules below.
**Preserve accepted requirements.** Compare the implementation with the stated
invariants and acceptance criteria. If they conflict, report an implementation
gap and propose a remedy that meets the requirement. In HOLD SCOPE, that work is
in scope even when the sketch omits the necessary mechanism. A sketch describes
what is proposed; it does not authorize weakening the required behavior.
Do not resolve the gap by rewriting the guarantee, calling the violation
acceptable, or changing a test to expect the prohibited result. Low frequency,
bounded impact, and documentation do not satisfy a stricter requirement.
Changing a requirement needs an explicit decision under the existing approval
rules; until approved, keep that proposal pending and the original gap unresolved.
Earlier explicitly approved requirement changes and explicit authority to change
that scope remain valid. Routine auto-decide permission alone cannot override an
explicit user constraint or non-goal. Preserve the distinction in findings, tasks,
and the completion report.
### Section 1: Architecture Review
Evaluate and diagram:
* Overall system design and component boundaries. Draw the dependency graph.
@@ -21,6 +21,21 @@ still needs its own decision; explain what the earlier remedy does not cover.
This does not approve an unraised finding or a new TODO: continue to present each
new finding and each potential TODO individually under the rules below.
**Preserve accepted requirements.** Compare the implementation with the stated
invariants and acceptance criteria. If they conflict, report an implementation
gap and propose a remedy that meets the requirement. In HOLD SCOPE, that work is
in scope even when the sketch omits the necessary mechanism. A sketch describes
what is proposed; it does not authorize weakening the required behavior.
Do not resolve the gap by rewriting the guarantee, calling the violation
acceptable, or changing a test to expect the prohibited result. Low frequency,
bounded impact, and documentation do not satisfy a stricter requirement.
Changing a requirement needs an explicit decision under the existing approval
rules; until approved, keep that proposal pending and the original gap unresolved.
Earlier explicitly approved requirement changes and explicit authority to change
that scope remain valid. Routine auto-decide permission alone cannot override an
explicit user constraint or non-goal. Preserve the distinction in findings, tasks,
and the completion report.
### Section 1: Architecture Review
Evaluate and diagram:
* Overall system design and component boundaries. Draw the dependency graph.
+8
View File
@@ -1179,6 +1179,14 @@ descriptions of what 10/10 looks like.
Confirm you Read the review section the Section index named, and executed all 7 design passes, the required outputs, and the review report in full. If you produced findings or the review report from memory without Reading `sections/review-sections.md`, stop and Read it now.
Before the completion summary, review log, next-step menu, or ExitPlanMode,
reconcile every proposed fix with the exact user decision for that issue. A new
accepted task, raised score, or "decision made" needs its individual approval;
an explicit deferral stays unresolved. Existing accepted requirements need no repeat approval.
Keep the preamble's authorized auto-decisions distinct and record their per-issue basis.
If an issue has no such decision, ask about it now and wait; a next-step answer
cannot approve it. If a draft applied it, return that change to pending before continuing.
## EXIT PLAN MODE GATE (BLOCKING)
Before calling ExitPlanMode, run this self-check. If any item fails, do the
+8
View File
@@ -314,4 +314,12 @@ descriptions of what 10/10 looks like.
Confirm you Read the review section the Section index named, and executed all 7 design passes, the required outputs, and the review report in full. If you produced findings or the review report from memory without Reading `sections/review-sections.md`, stop and Read it now.
Before the completion summary, review log, next-step menu, or ExitPlanMode,
reconcile every proposed fix with the exact user decision for that issue. A new
accepted task, raised score, or "decision made" needs its individual approval;
an explicit deferral stays unresolved. Existing accepted requirements need no repeat approval.
Keep the preamble's authorized auto-decisions distinct and record their per-issue basis.
If an issue has no such decision, ask about it now and wait; a next-step answer
cannot approve it. If a draft applied it, return that change to pending before continuing.
{{EXIT_PLAN_MODE_GATE}}
+15 -2
View File
@@ -4,7 +4,15 @@
**Anti-skip rule:** Never condense, abbreviate, or skip any review pass (1-7) regardless of plan type (strategy, spec, code, infra). Every pass in this skill exists for a reason. "This is a strategy doc so design passes don't apply" is always wrong — design gaps are where implementation breaks down. If a pass genuinely has zero findings, say "No issues found" and move on — but you must evaluate it.
**Anti-shortcut clause:** The plan file is the OUTPUT of the interactive review, not a substitute for it. Writing every finding into one plan write and calling ExitPlanMode without firing AskUserQuestion is the precise failure mode of the May 2026 transcript bug — the model explored, found issues, and dumped them into a deliverable rather than walking the user through them. If you have ANY non-trivial finding in any review section, the path from finding to ExitPlanMode goes THROUGH AskUserQuestion. Zero findings in every section is the only path to ExitPlanMode that bypasses AskUserQuestion. If you find yourself wanting to write a plan with findings before asking, stop and call AskUserQuestion now — that's the bug, recognize it.
**Anti-shortcut clause:** Complete one decision cycle per unresolved finding:
explain the gap, recommend options, obtain its individual decision, then apply
the selected fix. Scope, focus, setup, and next-step choices approve no remedies.
Writing a report, mapping a token, creating a mockup, or listing a task does not
approve a remedy. Inherited exact decisions and preamble-authorized per-issue
auto-decisions remain valid; do not re-ask them.
If findings exist but only navigation was answered, the review is still waiting
for its first issue decision. Never use the final next-step AskUserQuestion to satisfy the issue-approval loop.
With no unresolved findings, no issue question is required.
## Prior Learnings
@@ -242,6 +250,11 @@ For design debt: missing a11y, unresolved responsive behavior, deferred empty st
Then present options: **A)** Add to TODOS.md **B)** Skip — not valuable enough **C)** Build it now in this PR instead of deferring.
Before synthesizing tasks or the completion summary, perform the approval
reconciliation from the Section self-check. Export only agreed implementation work; retain unapproved remedies as pending findings.
Count only individually approved new decisions in "Decisions made" and the
review log; a proposed remedy or next-step answer contributes zero.
## Implementation Tasks
Before closing this review, synthesize the findings above into a flat list of
@@ -343,7 +356,7 @@ If all passes 8+: "Plan is design-complete. Run /design-review after implementat
If any below 8: note what's unresolved and why (user chose to defer).
### Unresolved Decisions
If any AskUserQuestion goes unanswered, note it here. Never silently default to an option.
List every unresolved finding here, including a finding not yet asked or an unanswered AskUserQuestion. Never silently default to an option.
### Approved Mockups
@@ -2,7 +2,15 @@
**Anti-skip rule:** Never condense, abbreviate, or skip any review pass (1-7) regardless of plan type (strategy, spec, code, infra). Every pass in this skill exists for a reason. "This is a strategy doc so design passes don't apply" is always wrong — design gaps are where implementation breaks down. If a pass genuinely has zero findings, say "No issues found" and move on — but you must evaluate it.
{{ANTI_SHORTCUT_CLAUSE}}
**Anti-shortcut clause:** Complete one decision cycle per unresolved finding:
explain the gap, recommend options, obtain its individual decision, then apply
the selected fix. Scope, focus, setup, and next-step choices approve no remedies.
Writing a report, mapping a token, creating a mockup, or listing a task does not
approve a remedy. Inherited exact decisions and preamble-authorized per-issue
auto-decisions remain valid; do not re-ask them.
If findings exist but only navigation was answered, the review is still waiting
for its first issue decision. Never use the final next-step AskUserQuestion to satisfy the issue-approval loop.
With no unresolved findings, no issue question is required.
{{LEARNINGS_SEARCH}}
@@ -132,6 +140,11 @@ For design debt: missing a11y, unresolved responsive behavior, deferred empty st
Then present options: **A)** Add to TODOS.md **B)** Skip — not valuable enough **C)** Build it now in this PR instead of deferring.
Before synthesizing tasks or the completion summary, perform the approval
reconciliation from the Section self-check. Export only agreed implementation work; retain unapproved remedies as pending findings.
Count only individually approved new decisions in "Decisions made" and the
review log; a proposed remedy or next-step answer contributes zero.
{{TASKS_SECTION_EMIT:design-review}}
### Completion Summary
@@ -163,7 +176,7 @@ If all passes 8+: "Plan is design-complete. Run /design-review after implementat
If any below 8: note what's unresolved and why (user chose to defer).
### Unresolved Decisions
If any AskUserQuestion goes unanswered, note it here. Never silently default to an option.
List every unresolved finding here, including a finding not yet asked or an unanswered AskUserQuestion. Never silently default to an option.
### Approved Mockups
+196
View File
@@ -0,0 +1,196 @@
import { afterEach, expect, test } from 'bun:test';
import { createHash } from 'node:crypto';
import { existsSync, linkSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync, symlinkSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join, resolve } from 'node:path';
import { spawnSync } from 'node:child_process';
const ROOT = resolve(import.meta.dir, '..');
const TOOL = join(ROOT, 'bin/gstack-autoplan-snapshot.ts');
const original = readFileSync(join(ROOT, 'test/fixtures/plans/autoplan-dashboard.md'));
const hash = (value: Buffer | string) => createHash('sha256').update(value).digest('hex');
const owned: string[] = [];
function fixture() {
const dir = mkdtempSync(join(tmpdir(), 'gstack-autoplan-init-')); owned.push(dir);
const source = join(dir, 'source plan.md');
const active = join(dir, 'assigned plan.md');
const restore = join(dir, 'restore point.md');
writeFileSync(source, original);
return { dir, source, active, restore };
}
function cli(...args: string[]) {
return spawnSync(process.execPath, [TOOL, ...args], {
cwd: ROOT, encoding: 'utf8', timeout: 10_000, maxBuffer: 1024 * 1024,
});
}
function invoke(...args: string[]) {
const result = cli(...args);
expect(result.error).toBeUndefined();
expect(result.status, result.stderr).toBe(0);
return JSON.parse(result.stdout);
}
afterEach(() => { for (const dir of owned.splice(0)) rmSync(dir, { recursive: true, force: true }); });
test('actual raw R input initializes before scope and reaches a complete CEO dispatch payload', () => {
const f = fixture();
expect(original.length).toBe(4607);
expect(hash(original)).toBe('2fdf0ece590925869fe25ae941301894f8f4505da6674302df25d5c4546fddbc');
const missing = cli('scope', f.source);
expect(missing.status).toBe(1);
expect(missing.stderr).toContain('Expected one Implementation plan section');
const initialized = invoke('init', f.source, f.source, f.restore);
expect(initialized.activePlan).toBe(f.source);
expect(initialized.originalBytes).toBe(4607);
expect(initialized.originalSha256).toBe(hash(original));
expect(readFileSync(f.restore)).toEqual(original);
expect(initialized.scope.sha256).toBe(hash(original));
expect(initialized.scope.matchCount).toBe(21);
expect(initialized.scope.dxRequired).toBe(true);
expect(invoke('scope', f.source)).toEqual(initialized.scope);
const ceo = invoke('create', 'ceo', f.source, f.restore);
expect(readFileSync(ceo.snapshotPath)).toEqual(original);
expect(ceo.nativePrompt.endsWith(original.toString())).toBe(true);
expect(ceo.nativePrompt).toContain('Mutations already require CSRF tokens');
expect(ceo.sha256).toBe(initialized.scope.sha256);
expect(invoke('check', 'ceo', f.source, ceo.snapshotPath, 'unchanged').changed).toBe(false);
});
test('assigned active path preserves the separate original source and exact restore', () => {
for (const emptyAssigned of [false, true]) {
const f = fixture();
if (emptyAssigned) writeFileSync(f.active, '');
const sourceMtime = statSync(f.source).mtimeMs;
const initialized = invoke('init', f.source, f.active, f.restore);
expect(initialized.activePlan).toBe(f.active);
expect(initialized.restorePath).toBe(f.restore);
expect(readFileSync(f.source)).toEqual(original);
expect(statSync(f.source).mtimeMs).toBe(sourceMtime);
expect(readFileSync(f.restore)).toEqual(original);
expect(readFileSync(f.active, 'utf8')).toContain('## Implementation plan\n' + original.toString());
expect(readdirSync(f.dir).sort()).toEqual(['assigned plan.md', 'restore point.md', 'source plan.md']);
}
});
test('the observed missing harness plans directory is initialized without a separate mkdir step', () => {
const f = fixture();
const active = join(f.dir, 'harness', 'plans', 'assigned.md');
const restore = join(f.dir, 'state', 'project', 'restore.md');
const initialized = invoke('init', f.source, active, restore);
expect(initialized.activePlan).toBe(active);
expect(initialized.restorePath).toBe(restore);
expect(initialized.scope.dxRequired).toBe(true);
expect(readFileSync(f.source)).toEqual(original);
expect(readFileSync(restore)).toEqual(original);
expect(invoke('init', f.source, active, restore).reused).toBe(true);
});
test('same initialization is idempotent but later amendments never get reset', () => {
for (const separate of [false, true]) {
const f = fixture(); const active = separate ? f.active : f.source;
invoke('init', f.source, active, f.restore);
const bytes = readFileSync(active); const backup = readFileSync(f.restore);
const stamp = statSync(active).mtimeMs; const backupStamp = statSync(f.restore).mtimeMs;
expect(invoke('init', f.source, active, f.restore).reused).toBe(true);
expect(readFileSync(active)).toEqual(bytes);
expect(statSync(active).mtimeMs).toBe(stamp);
expect(statSync(f.restore).mtimeMs).toBe(backupStamp);
writeFileSync(active, bytes.toString() + 'Accepted review decision.\n');
const changed = readFileSync(active);
const retry = cli('init', f.source, active, f.restore);
expect(retry.status).toBe(1);
expect(retry.stderr).toContain('Existing restore does not match');
expect(readFileSync(active)).toEqual(changed);
expect(readFileSync(f.restore)).toEqual(backup);
}
});
test('already structured input keeps the review record separate from blind inputs', () => {
const f = fixture();
const body = 'API and endpoint.\n> ## Review record\n```text\n## Review record\n```\n';
const source = `# Earlier plan\n## Implementation plan\n${body}## Review record\nPrivate earlier review\n`;
writeFileSync(f.source, source);
const initialized = invoke('init', f.source, f.active, f.restore);
expect(readFileSync(f.source, 'utf8')).toBe(source);
expect(readFileSync(f.restore, 'utf8')).toBe(source);
expect(readFileSync(f.active, 'utf8')).toContain('## Review record\nPrivate earlier review');
expect(initialized.scope.sha256).toBe(hash(body));
const ceo = invoke('create', 'ceo', f.active, f.restore);
expect(readFileSync(ceo.snapshotPath, 'utf8')).toBe(body);
expect(ceo.nativePrompt).not.toContain('Private earlier review');
});
test('invalid or ambiguous input fails without publishing any backup or active plan', () => {
for (const source of [Buffer.from(''), Buffer.from([0xff, 0xfe]),
Buffer.from('## Implementation plan\nMissing review boundary\n'),
Buffer.from('## Review record\nPrivate audit\n'),
Buffer.from('```text\nUnclosed fence\n'),
Buffer.from('## Implementation plan\nAPI\n## Review record\nAudit\n## Review record\nAgain\n')]) {
const f = fixture(); writeFileSync(f.source, source);
const result = cli('init', f.source, f.active, f.restore);
expect(result.status).toBe(1);
expect(result.stdout).toBe('');
expect(readFileSync(f.source)).toEqual(source);
expect(readdirSync(f.dir)).toEqual(['source plan.md']);
}
});
test('existing foreign destinations, symlinks and path aliases cannot be overwritten', () => {
for (const kind of ['active-content', 'restore-content', 'active-link', 'restore-link', 'hardlink', 'restore-is-source', 'same-destinations', 'relative']) {
const f = fixture(); let active = f.active; let restore = f.restore;
if (kind === 'active-content') writeFileSync(active, 'Other assigned work');
if (kind === 'restore-content') writeFileSync(restore, 'Existing history');
if (kind === 'active-link') symlinkSync('missing', active);
if (kind === 'restore-link') symlinkSync('missing', restore);
if (kind === 'hardlink') linkSync(f.source, active);
if (kind === 'restore-is-source') restore = f.source;
if (kind === 'same-destinations') restore = active;
if (kind === 'relative') active = 'relative.md';
const entries = readdirSync(f.dir).sort();
const result = cli('init', f.source, active, restore);
expect(result.status, kind).toBe(1);
expect(result.stdout).toBe('');
expect(readdirSync(f.dir).sort()).toEqual(entries);
expect(readFileSync(f.source)).toEqual(original);
if (kind === 'active-content') expect(readFileSync(active, 'utf8')).toBe('Other assigned work');
if (kind === 'restore-content') expect(readFileSync(restore, 'utf8')).toBe('Existing history');
}
});
test('line endings and Unicode survive normalization; only a missing final separator LF is added', () => {
for (const text of ['最後の API 要件 🧪\r\nREST must remain.\r\n', 'API and endpoint without final newline']) {
const f = fixture(); const restore = join(f.dir, 'restore -- quoted "名前".md');
writeFileSync(f.source, text);
invoke('init', f.source, f.active, restore);
expect(readFileSync(restore, 'utf8')).toBe(text);
const ceo = invoke('create', 'ceo', f.active, restore);
expect(readFileSync(ceo.snapshotPath, 'utf8')).toBe(text + (text.endsWith('\n') ? '' : '\n'));
expect(invoke('init', f.source, f.active, restore).reused).toBe(true);
}
});
test('staging failure cleans owned temporary files without changing source or active bytes', () => {
const f = fixture();
const active = join(f.dir, 'harness', 'plans', 'assigned.md');
const restore = join(f.dir, 'state', 'project', 'restore.md');
const worker = join(f.dir, 'fail-stage.ts');
writeFileSync(worker, `import { mock } from 'bun:test';
const real = { ...await import('node:fs') };
mock.module('node:fs', () => ({ ...real, mkdtempSync(prefix, options) {
if (String(prefix).includes('.gstack-autoplan-restore-')) throw new Error('Injected restore staging failure');
return real.mkdtempSync(prefix, options);
} }));
const { initializePlan } = await import(${JSON.stringify(TOOL)});
try { initializePlan(...process.argv.slice(2)); process.exitCode = 5; }
catch (error) { console.error(error.message); process.exitCode = 1; }
`);
const result = spawnSync(process.execPath, [worker, f.source, active, restore], {
encoding: 'utf8', timeout: 10_000,
});
expect(result.status).toBe(1);
expect(result.stdout).toBe('');
expect(result.stderr).toContain('Injected restore staging failure');
expect(readFileSync(f.source)).toEqual(original);
expect(readdirSync(f.dir).sort()).toEqual(['fail-stage.ts', 'source plan.md']);
expect(existsSync(active)).toBe(false);
expect(existsSync(restore)).toBe(false);
});
+17 -11
View File
@@ -96,12 +96,16 @@ describe('autoplan phase execution checkpoints', () => {
expect(native).toBeLessThan(outside);
const dispatch = section.slice(native, outside);
expect(dispatch).toContain('run_in_background: false');
expect(dispatch).toContain("ONLY/FINAL tool call");
expect(dispatch).toContain('Keep native Reads enabled');
expect(dispatch).toContain('Child first Reads `nativePromptPath` to EOF');
expect(dispatch).toContain('all criteria + plan');
expect(dispatch).toContain('if its schema exposes it');
expect(dispatch).toContain('isAsync: true');
expect(dispatch).toContain('Claude Code: immediately end this response with');
expect(dispatch).toContain('Do no more tool calls or review work until that ID');
expect(dispatch).toContain('For completed native reviews, match INPUT phase/hash');
expect(dispatch).toContain('retry the full payload once, then use failure policy');
expect(dispatch).toContain('retry this dispatch once, then use failure policy');
expect(dispatch).toContain('Other hosts: await that ID');
expect(dispatch).toContain("Then outside → this phase's review ONLY");
expect(dispatch).toContain('No inline substitute; apply failure policy');
@@ -118,12 +122,12 @@ describe('autoplan phase execution checkpoints', () => {
expect(contract).toContain('Never draft future-phase reviews or outputs');
expect(contract).toContain('After compaction, reload current phase instructions/skill/sections; reconcile disk progress before resuming');
expect(contract).toContain('Load its phase instructions and full skill/sections');
expect(contract).toContain('Create the fresh snapshot and dispatch its generated nativePrompt unchanged');
expect(contract).toContain('Create the fresh snapshot and dispatch its nativeDispatchPrompt unchanged');
expect(contract).toContain('Consume native completion, then enabled outside results; only then do the full primary review');
expect(contract).toContain("Persist outputs/amendments and run the phase's implementation check/readback");
expect(contract).toContain('Emit actual completion; only then load the next required phase');
expect(contract).toContain('A missing gate means the current phase remains open');
expect(contract).toContain('INPUT correlates reported input; it is not independent proof of uptake or review quality');
expect(contract).toContain('Read requests/self-reports and INPUT hashes do not prove uptake or review quality');
expect(contract).toContain('Pending is not unavailable');
expect(contract).toContain('Time/context pressure or your own review never permits\nskipping native passes or required sections');
expect(contract).toContain('Never read raw agent transcripts');
@@ -166,12 +170,14 @@ describe('autoplan phase execution checkpoints', () => {
describe('autoplan current implementation-plan identity', () => {
test('pins the assigned active plan and keeps accepted amendments separate from review analyses', () => {
const intake = read('autoplan/SKILL.md.tmpl').split('## Phase 0: Intake')[1]?.split('### Step 2:')[0] ?? '';
expect(intake).toContain('ACTIVE_PLAN is the harness-assigned');
expect(intake).toContain('All amendments/outputs go to ACTIVE_PLAN');
expect(intake).toContain("SOURCE_PLAN's full current state externally");
expect(intake).toContain('without dropping\nrequirements');
expect(intake).toContain("Copy SOURCE_PLAN into ACTIVE_PLAN's `## Implementation plan`");
expect(intake).toContain('Keep analyses/audit in `## Review record`');
expect(intake).toContain('ACTIVE_PLAN (harness-assigned plan, else SOURCE_PLAN)');
expect(intake).toContain('Write all amendments/outputs to ACTIVE_PLAN');
expect(intake).toContain("init backs up SOURCE_PLAN exactly");
expect(intake).toContain('without losing requirements');
expect(intake).toContain('init "<SOURCE_PLAN>" "<ACTIVE_PLAN>" "<RESTORE_PATH>"');
expect(intake).toContain('Use returned paths/`scope`');
expect(intake).toContain('On helper errors, stop');
expect(intake).toContain('analysis stays in `## Review record`');
// Binding belongs to the lazy execution site, not a stale intake variable.
expect(intake).not.toContain('Bind `<review_plan_path>`');
});
@@ -199,8 +205,8 @@ describe('autoplan current implementation-plan identity', () => {
expect(preparation).toContain(`create ${phase} "<ACTIVE_PLAN>" "<RESTORE_PATH>"`);
expect(preparation).toContain('`snapshotPath` as `<' + phase.toUpperCase() + '_INPUT>` for both voices');
expect(preparation).toContain('excludes `Review record`');
expect(section).toContain('Send `nativePrompt` verbatim');
expect(section).toContain('Read `nativePromptPath` to EOF');
expect(section).toContain('Send `nativeDispatchPrompt` verbatim');
expect(section).toContain('Reads `nativePromptPath` to EOF');
expect(section).toContain(`Outside prompt: inline the full contents of <${phase.toUpperCase()}_INPUT>`);
expect(section).toContain(`check ${phase} "<ACTIVE_PLAN>" "<${phase.toUpperCase()}_INPUT>" changed`);
expect(section).toContain('Use `unchanged` only if no implementation changes were accepted');
+63
View File
@@ -28,6 +28,69 @@ function cli(...args: string[]) {
afterEach(() => { for (const dir of owned.splice(0)) rmSync(dir, { recursive: true, force: true }); });
describe('Autoplan phase snapshot continuity', () => {
test('short native dispatch binds the complete immutable file for each phase', () => {
const f = fixture();
for (const phase of ['ceo', 'design', 'dx', 'eng']) {
const result = cli('create', phase, f.active, f.restore);
expect(result.status, result.stderr).toBe(0);
const generated = JSON.parse(result.stdout);
expect(generated.nativeDispatchPrompt).toBeString();
expect(generated.nativeDispatchPrompt).toContain(`Read file: ${JSON.stringify(generated.nativePromptPath)}`);
expect(generated.nativeDispatchPrompt).toContain('FIRST tool action');
expect(generated.nativeDispatchPrompt).toContain('line 1 through EOF');
expect(generated.nativeDispatchPrompt).toContain('Continue successful ranges until every line is loaded');
expect(generated.nativeDispatchPrompt).toContain('Execute every criterion');
expect(generated.nativeDispatchPrompt).toContain(`INPUT: ${phase} ${generated.sha256}`);
expect(generated.nativeDispatchPrompt).toContain('report the read failure instead of a completed review');
expect(generated.nativeDispatchPrompt).toContain(generated.nativePromptSha256);
expect(generated.nativeDispatchPrompt).toContain(`${generated.nativePromptBytes} UTF-8 bytes`);
expect(generated.nativePromptBytes).toBe(Buffer.byteLength(generated.nativePrompt));
// Blank physical lines count; only the empty split after a final LF is not a line.
expect(generated.nativePromptLines).toBe([...generated.nativePrompt.matchAll(/[^\n]*\n|[^\n]+$/g)].length);
expect(generated.nativeDispatchPrompt).not.toContain('Mutations already require CSRF tokens');
expect(Buffer.byteLength(generated.nativeDispatchPrompt)).toBeLessThan(1600);
const manifest = JSON.parse(readFileSync(join(generated.nativePromptPath, '..', 'snapshot.json'), 'utf8'));
expect(manifest.nativeDispatchPrompt).toBe(generated.nativeDispatchPrompt);
expect(manifest.nativePromptLines).toBe(generated.nativePromptLines);
expect(manifest.nativePromptBytes).toBe(generated.nativePromptBytes);
}
});
test('a standalone file reader can recover all criteria and late plan bytes from only the dispatch', () => {
// This is transport evidence with a deterministic child, not evidence that
// a model followed the instruction. Paid validation must inspect its own child.
const f = fixture();
const location = join(f.dir, process.platform === 'win32' ? '資料 with spaces' : '資料 "quoted" with spaces');
mkdirSync(location);
const restore = join(location, 'original.md'); writeFileSync(restore, f.body);
const body = f.body + '\n' + Array.from({ length: 2200 }, (_, i) => `Contract ${i}: preserve the entire input.\r\n`).join('') + 'LAST REQUIREMENT: tenant isolation + CSRF. 🧪\n';
writeFileSync(f.active, `## Implementation plan\n${body}## Review record\nPRIVATE PRIOR REVIEW\n`);
const created = cli('create', 'ceo', f.active, restore);
expect(created.status, created.stderr).toBe(0);
const generated = JSON.parse(created.stdout);
expect(generated.nativeDispatchPrompt).toBeString();
const child = spawnSync(process.execPath, ['-e', `
const dispatch = await Bun.stdin.text();
const matched = /^Read file: (.+)$/m.exec(dispatch);
if (!matched) throw new Error('Dispatch has no complete file path');
const content = require('node:fs').readFileSync(JSON.parse(matched[1]), 'utf8');
process.stdout.write(JSON.stringify({ content, bytes: Buffer.byteLength(content),
sha256: require('node:crypto').createHash('sha256').update(content).digest('hex') }));
`], { input: generated.nativeDispatchPrompt, encoding: 'utf8', timeout: 10_000 });
expect(child.status, child.stderr).toBe(0);
const read = JSON.parse(child.stdout);
expect(read.content).toBe(generated.nativePrompt);
expect(read.bytes).toBe(generated.nativePromptBytes);
expect(read.sha256).toBe(generated.nativePromptSha256);
expect(read.content.endsWith(body)).toBe(true);
expect(read.content).toContain('What alternatives were dismissed without sufficient analysis?');
expect(read.content).toContain('LAST REQUIREMENT: tenant isolation + CSRF. 🧪');
expect(read.content).not.toContain('PRIVATE PRIOR REVIEW');
expect(generated.nativePromptLines).toBeGreaterThan(2200);
expect(generated.nativeDispatchPrompt).toContain(`${generated.nativePromptLines} lines`);
expect(Buffer.byteLength(generated.nativeDispatchPrompt)).toBeLessThan(1600);
});
test('generated native dispatch carries every snapshot byte instead of the observed abbreviated input', () => {
const f = fixture();
for (const phase of ['ceo', 'design', 'dx', 'eng']) {
+76
View File
@@ -8,6 +8,7 @@ import type { NativePlanQuestionCall } from './helpers/plan-count-transcript';
import captures from './fixtures/ceo-completion-handoff-calls.json';
import currentHandoffs from './fixtures/ceo-completion-handoff-j-calls.json';
import kHandoffs from './fixtures/ceo-completion-handoff-k-calls.json';
import rCalls from './fixtures/ceo-completion-handoff-r-calls.json';
type CapturedCall = typeof captures.cases[number]['calls'][number];
function nativeCall(record: CapturedCall, sessionId = 'native-capture'): NativePlanQuestionCall {
@@ -21,6 +22,81 @@ function nativeCall(record: CapturedCall, sessionId = 'native-capture'): NativeP
const handoff = () => nativeCall(captures.cases[0]!.calls.at(-1)!);
const fingerprint = (call: NativePlanQuestionCall) => nativePlanCallFingerprint(call, 0, false);
describe('native direct Eng/manual handoff with described CEO closure', () => {
const captured = () => structuredClone(rCalls.at(-1)!) as NativePlanQuestionCall;
const pending = (call: NativePlanQuestionCall) => {
const copy = structuredClone(call); copy.answered = false; delete copy.answers;
delete copy.unansweredQuestionIndices;
return fingerprint(copy);
};
test('actual R calls retain zero findings and choose offered manual instead of starting Eng', () => {
const calls = structuredClone(rCalls) as NativePlanQuestionCall[];
expect(replay(calls, false, ceoFirstReviewAUQ)).toMatchObject({ step0Count: 3, reviewCount: 0, administrativeCount: 1, reviewStarted: true });
expect(replay(calls, false, ceoFirstReviewAUQ).reviewCount).toBeLessThan(2); // Existing paired floor still fails.
expect(isCeoCompletionHandoff(fingerprint(captured()))).toBe(true);
expect(pickCeoCompletionHandoff(pending(captured())) ?? 1).toBe(2);
expect(calls).toEqual(rCalls);
});
test('manual choice follows real option order and still requires pending native identity', () => {
const call = captured(); call.questions[0]!.options.reverse();
expect(pickCeoCompletionHandoff(pending(call))).toBe(1);
expect(pickCeoCompletionHandoff(fingerprint(call))).toBeNull();
expect(pickCeoCompletionHandoff({ ...pending(call), signature: 'foreign-call' })).toBeNull();
expect(pickCeoCompletionHandoff({ ...pending(call), nativeCall: undefined })).toBeNull();
call.failed = true;
expect(pickCeoCompletionHandoff(pending(call))).toBeNull();
});
test('same native menu retains every incomplete, conditional, quoted or substantive obligation', () => {
const changes: Array<(c: NativePlanQuestionCall) => void> = [
c => { c.questions[0]!.question = 'Should we fix the missing authorization test before the next review?'; },
c => { c.questions[0]!.question += ' First repair the missing assertion.'; },
c => { c.questions[0]!.question = 'The review did not finish. ' + c.questions[0]!.question; },
c => { c.questions[0]!.header = 'Authorization gap'; },
c => { c.questions[0]!.question += ' <gstack-qid:ceo-security-finding>'; },
c => { c.questions[0]!.options[1]!.label = 'Skip'; },
c => { c.questions[0]!.options[1]!.label = 'Repair authorization before Eng'; },
c => { c.questions[0]!.options.push({ ...c.questions[0]!.options[1]! }); },
c => { c.questions[0]!.options.push({ label: 'Run /plan-design-review' }); },
c => { c.questions[0]!.options[1]!.description = 'The CEO review is not clear.'; },
c => { c.questions[0]!.options[1]!.description = 'The CEO review remains incomplete.'; },
c => { c.questions[0]!.options[1]!.description = 'The CEO review is clear once tests pass.'; },
c => { c.questions[0]!.options[1]!.description = 'Once tests pass, the CEO review will be clear.'; },
c => { c.questions[0]!.options[1]!.description += ' All findings become resolved after tests pass.'; },
c => { c.questions[0]!.options[1]!.description += ' The contrast gap remains unresolved.'; },
c => { c.questions[0]!.options[1]!.description += ' Not all decisions are resolved.'; },
c => { c.questions[0]!.options[1]!.description += ' Repair the missing authorization test.'; },
c => { c.questions[0]!.options[1]!.description += ' Recommendation: repair the missing assertion.'; },
c => { c.questions[0]!.options[1]!.description += ' We may repair the missing assertion.'; },
c => { c.questions[0]!.options[1]!.description += ' We must add the authorization test.'; },
c => { c.questions[0]!.options[1]!.description += ' Delete the failing regression test before Eng.'; },
c => { c.questions[0]!.options[1]!.description += ' Remove the owner check before Eng.'; },
c => { c.questions[0]!.options[1]!.description += ' Change the guarantee to permit old results.'; },
c => { c.questions[0]!.options[1]!.description += ' Rewrite the acceptance criteria before shipping.'; },
c => { c.questions[0]!.options[1]!.description += ' Do you want me to fix the missing test?'; },
c => { c.questions[0]!.options[1]!.description = 'Example: The CEO review is clear.'; },
c => { c.questions[0]!.options[1]!.description = '> The CEO review is clear.'; },
c => { c.questions[0]!.options[1]!.description = '```text\nThe CEO review is clear.'; },
];
for (const change of changes) {
const call = captured(); change(call);
call.answers = { [call.questions[0]!.question]: call.questions[0]!.options[0]!.label };
expect(isCeoCompletionHandoff(fingerprint(call))).toBe(false);
expect(pickCeoCompletionHandoff(pending(call))).toBeNull();
}
});
test('an unconditional completed recap permits next Eng sequencing but no failed or free-form answer', () => {
const call = captured();
call.questions[0]!.options[1]!.description = 'The CEO review is complete. Run /plan-eng-review after implementation and before shipping.';
expect(isCeoCompletionHandoff(fingerprint(call))).toBe(true);
expect(pickCeoCompletionHandoff(pending(call))).toBe(2);
call.answers = { [call.questions[0]!.question]: 'First fix the missing receipt assertion' };
expect(isCeoCompletionHandoff(fingerprint(call))).toBe(false);
call.answers = { [call.questions[0]!.question]: call.questions[0]!.options[0]!.label };
call.unansweredQuestionIndices = [0];
expect(isCeoCompletionHandoff(fingerprint(call))).toBe(false);
});
});
function replay(calls: NativePlanQuestionCall[], reviewStarted = true, firstReview = (_fp: ReturnType<typeof fingerprint>) => true) {
const counts = { step0Count: 0, reviewCount: 0, administrativeCount: 0 };
const classifications = [];
+13
View File
@@ -194,3 +194,16 @@ describe('accepted consistency model is not a stale-fill finding', () => {
expect(hasStaleFillRaceFinding(text)).toBe(true);
});
});
describe('CEO R report requirement weakening remains rejected', () => {
test('a race trace followed by acceptance and a weaker guarantee is not an unresolved defect', async () => {
// Exact delivered R retry report; the existing oracle already rejects it.
// This pins the policy failure without changing the paid fixture or oracle.
const report = await Bun.file(new URL('./fixtures/ceo-section-r-rejected-report.md', import.meta.url)).text();
expect(report).toContain('Every read begun after that write completes must');
expect(report).toContain('this is an accepted design choice, not a quality gap');
expect(report).toContain('Verify: new reads see stale value until TTL');
expect(hasStaleFillRaceFinding(report)).toBe(false);
});
});
+29
View File
@@ -0,0 +1,29 @@
{
"sessionId": "2aa1d111-e41c-4305-b511-560f75c591f6",
"toolUseId": "toolu_01HBKnGS2bULyawnQhA8RZY4",
"questions": [
{
"question": "Which implementation approach should this plan follow? <gstack-qid:plan-ceo-review-approach>",
"header": "Approach",
"options": [
{
"label": "Minimal — 2 tests as written",
"description": "Exactly 2 tests matching the spec: happy path + 502. Assertions are left as the plan describes them ('assert correct receipt is generated', 'assert retry-with-backoff fires once, then fails clean'). Fast, small diff. Completeness: 7/10 — tests exist but assertion specifics are left to the implementer.",
"preview": "test 'happy path: successful Stripe charge' do\n receipt = process_payment(factory.valid_card)\n assert receipt.generated? # vague\nend\n\ntest '502 error: retry once, then fail' do\n result = process_payment(factory.bad_card)\n assert result.failed? # vague\nend"
},
{
"label": "Assertions-complete — 2 tests with explicit assertions (recommended)",
"description": "Same 2 tests, but the plan specifies: (1) which receipt fields to assert for the happy path, (2) the exact failure type for the 502 path, (3) Stripe call count == 2, and (4) backoff was recorded. Same file count, ~20 extra lines of assertion clarity. Completeness: 9/10 — tests are complete enough to catch real regressions.",
"preview": "test 'happy path: successful Stripe charge' do\n receipt = process_payment(factory.valid_card)\n assert_equal stripe_mock.charge_id, receipt.stripe_charge_id\n assert_equal 100_00, receipt.amount_cents\n assert_equal 'paid', receipt.status\nend\n\ntest '502 error: retry once, then fail' do\n assert_raises(PaymentError) { process_payment(factory.bad_card) }\n assert_equal 2, stripe_mock.call_count # original + 1 retry\n assert virtual_sleeper.backoff_recorded?\nend"
}
]
}
],
"answered": true,
"failed": false,
"answers": {
"Which implementation approach should this plan follow? <gstack-qid:plan-ceo-review-approach>": "Minimal — 2 tests as written"
},
"unansweredQuestionIndices": [],
"answeredAt": "2026-09-09T04:42:01.283Z"
}
+32
View File
@@ -0,0 +1,32 @@
{
"sessionId": "6416932f-4f06-403d-ba98-00668d5e1fe3",
"toolUseId": "toolu_01JAFX6db7H44tLpFeRjLakm",
"questions": [
{
"question": "D1 \u2014 Which implementation approach should this plan use? <gstack-qid:plan-ceo-approach-selection>\n\nELI10: The plan introduces a new Stripe webhook handler that reads a user ID from Stripe event metadata into a raw SQL query \u2014 and the plan itself explicitly says this is unsafe SQL. There are also no tests, and email errors propagate silently after the payment is committed. We have three paths: ship it as-is (fast, insecure), fix just the SQL (quick win), or fix all three known issues (secure, tested, reliable).\n\nStakes if we pick wrong: Approach A ships a handler with a named SQL injection vulnerability the plan's own text acknowledges. Approach B leaves a reliability gap where email errors cause misleading HTTP 500s after committed payments.\n\nRecommendation: C because the SQL injection is P0, the email fix is one rescue clause, and the test suite is ~20 min with CC \u2014 deferring all three accepts known risk for no real benefit.\n\nCompleteness: A=3/10, B=6/10, C=9/10\n\nPros/cons:\nA) As-specified \u2014 raw SQL, no tests, no email rescue\n \u2705 Smallest possible diff; matches the plan document exactly as written\n \u274c Ships a SQL injection the plan itself calls out as unsafe; no test coverage means no regression baseline\n\nB) Minimal safe \u2014 parameterize the SQL query only\n \u2705 Eliminates the P0 SQL injection with minimal code change; everything else stays the same\n \u274c Email exceptions still propagate after DB commits (misleading HTTP 500, no auto-retry for the notification); still no tests\n\nC) Full fix \u2014 parameterized queries + email rescue + unit tests (recommended)\n \u2705 Eliminates SQL injection; email failures no longer misrepresent committed payments; tests create a regression baseline for future changes\n \u274c More code to write; requires existing test harness to support the new handler class\n\nNet: trading ~15 extra CC minutes for a handler that doesn't ship with a named security vulnerability and has a test baseline.",
"header": "Approach",
"multiSelect": false,
"options": [
{
"label": "A) As-specified (raw SQL)",
"description": "Implement exactly as written \u2014 raw SQL fragment, no email rescue, no tests. Fast but ships the acknowledged SQL injection."
},
{
"label": "B) Minimal safe (parameterize only)",
"description": "Fix the SQL injection with parameterized queries; keep email inline with no rescue and no tests."
},
{
"label": "C) Full fix \u2014 all three gaps (Recommended)",
"description": "Parameterized queries + rescue email exceptions + unit tests for the critical paths."
}
]
}
],
"answered": true,
"failed": false,
"answers": {
"D1 \u2014 Which implementation approach should this plan use? <gstack-qid:plan-ceo-approach-selection>\n\nELI10: The plan introduces a new Stripe webhook handler that reads a user ID from Stripe event metadata into a raw SQL query \u2014 and the plan itself explicitly says this is unsafe SQL. There are also no tests, and email errors propagate silently after the payment is committed. We have three paths: ship it as-is (fast, insecure), fix just the SQL (quick win), or fix all three known issues (secure, tested, reliable).\n\nStakes if we pick wrong: Approach A ships a handler with a named SQL injection vulnerability the plan's own text acknowledges. Approach B leaves a reliability gap where email errors cause misleading HTTP 500s after committed payments.\n\nRecommendation: C because the SQL injection is P0, the email fix is one rescue clause, and the test suite is ~20 min with CC \u2014 deferring all three accepts known risk for no real benefit.\n\nCompleteness: A=3/10, B=6/10, C=9/10\n\nPros/cons:\nA) As-specified \u2014 raw SQL, no tests, no email rescue\n \u2705 Smallest possible diff; matches the plan document exactly as written\n \u274c Ships a SQL injection the plan itself calls out as unsafe; no test coverage means no regression baseline\n\nB) Minimal safe \u2014 parameterize the SQL query only\n \u2705 Eliminates the P0 SQL injection with minimal code change; everything else stays the same\n \u274c Email exceptions still propagate after DB commits (misleading HTTP 500, no auto-retry for the notification); still no tests\n\nC) Full fix \u2014 parameterized queries + email rescue + unit tests (recommended)\n \u2705 Eliminates SQL injection; email failures no longer misrepresent committed payments; tests create a regression baseline for future changes\n \u274c More code to write; requires existing test harness to support the new handler class\n\nNet: trading ~15 extra CC minutes for a handler that doesn't ship with a named security vulnerability and has a test baseline.": "A) As-specified (raw SQL)"
},
"unansweredQuestionIndices": [],
"answeredAt": "2026-09-09T04:43:57.146Z"
}
+119
View File
@@ -0,0 +1,119 @@
[
{
"sessionId": "2aa1d111-e41c-4305-b511-560f75c591f6",
"toolUseId": "toolu_01TFNyYCkuEbD4ka29tNm3cq",
"questions": [
{
"question": "No design doc found for this branch. `/office-hours` produces a structured problem statement, premise challenge, and explored alternatives — it gives this review much sharper input to work with. Takes about 10 minutes. The design doc is per-feature, not per-product — it captures the thinking behind this specific change. Run it first, or skip straight to the standard review?",
"header": "Prerequisites",
"options": [
{
"label": "Skip — proceed with standard review (recommended)",
"description": "The plan is small and the problem is well-defined: add 2 unit tests for processPayment(). /office-hours is for exploring what to build; this plan already knows exactly what to build."
},
{
"label": "Run /office-hours now",
"description": "Produces a deeper problem statement and premise challenge before the CEO review. Best for large or ambiguous feature plans."
}
],
"multiSelect": false
}
],
"answered": true,
"failed": false,
"answers": {
"No design doc found for this branch. `/office-hours` produces a structured problem statement, premise challenge, and explored alternatives — it gives this review much sharper input to work with. Takes about 10 minutes. The design doc is per-feature, not per-product — it captures the thinking behind this specific change. Run it first, or skip straight to the standard review?": "Skip — proceed with standard review (recommended)"
},
"unansweredQuestionIndices": [],
"answeredAt": "2026-09-09T04:41:29.182Z"
},
{
"sessionId": "2aa1d111-e41c-4305-b511-560f75c591f6",
"toolUseId": "toolu_01HBKnGS2bULyawnQhA8RZY4",
"questions": [
{
"question": "Which implementation approach should this plan follow? <gstack-qid:plan-ceo-review-approach>",
"header": "Approach",
"options": [
{
"label": "Minimal — 2 tests as written",
"description": "Exactly 2 tests matching the spec: happy path + 502. Assertions are left as the plan describes them ('assert correct receipt is generated', 'assert retry-with-backoff fires once, then fails clean'). Fast, small diff. Completeness: 7/10 — tests exist but assertion specifics are left to the implementer.",
"preview": "test 'happy path: successful Stripe charge' do\n receipt = process_payment(factory.valid_card)\n assert receipt.generated? # vague\nend\n\ntest '502 error: retry once, then fail' do\n result = process_payment(factory.bad_card)\n assert result.failed? # vague\nend"
},
{
"label": "Assertions-complete — 2 tests with explicit assertions (recommended)",
"description": "Same 2 tests, but the plan specifies: (1) which receipt fields to assert for the happy path, (2) the exact failure type for the 502 path, (3) Stripe call count == 2, and (4) backoff was recorded. Same file count, ~20 extra lines of assertion clarity. Completeness: 9/10 — tests are complete enough to catch real regressions.",
"preview": "test 'happy path: successful Stripe charge' do\n receipt = process_payment(factory.valid_card)\n assert_equal stripe_mock.charge_id, receipt.stripe_charge_id\n assert_equal 100_00, receipt.amount_cents\n assert_equal 'paid', receipt.status\nend\n\ntest '502 error: retry once, then fail' do\n assert_raises(PaymentError) { process_payment(factory.bad_card) }\n assert_equal 2, stripe_mock.call_count # original + 1 retry\n assert virtual_sleeper.backoff_recorded?\nend"
}
]
}
],
"answered": true,
"failed": false,
"answers": {
"Which implementation approach should this plan follow? <gstack-qid:plan-ceo-review-approach>": "Minimal — 2 tests as written"
},
"unansweredQuestionIndices": [],
"answeredAt": "2026-09-09T04:42:01.283Z"
},
{
"sessionId": "2aa1d111-e41c-4305-b511-560f75c591f6",
"toolUseId": "toolu_01DjcTXd1dvAsF63ZXP6kzvx",
"questions": [
{
"question": "Which review mode? <gstack-qid:plan-ceo-review-mode>",
"header": "Review mode",
"options": [
{
"label": "HOLD SCOPE — make the 2 tests bulletproof (recommended)",
"description": "Default for test coverage work. The plan's scope (2 unit tests) is locked. Review focuses on rigor: correct assertions, failure observability, test isolation, and naming. No scope additions. Note: options differ in kind, not coverage — no completeness score."
},
{
"label": "SELECTIVE EXPANSION — hold scope but surface adjacent opportunities",
"description": "Same HOLD SCOPE rigor, but also surfaces adjacent test opportunities (402, 429, idempotency) as individual cherry-picks you can accept or skip. Good if you want to see what’s nearby without committing to it. Note: options differ in kind, not coverage — no completeness score."
},
{
"label": "SCOPE REDUCTION — strip to the absolute minimum",
"description": "Find the smallest test that proves payment works. Could reduce to a single smoke test. Use if timeline is the constraint. Note: options differ in kind, not coverage — no completeness score."
}
],
"multiSelect": false
}
],
"answered": true,
"failed": false,
"answers": {
"Which review mode? <gstack-qid:plan-ceo-review-mode>": "HOLD SCOPE — make the 2 tests bulletproof (recommended)"
},
"unansweredQuestionIndices": [],
"answeredAt": "2026-09-09T04:42:13.320Z"
},
{
"sessionId": "2aa1d111-e41c-4305-b511-560f75c591f6",
"toolUseId": "toolu_01UHbAH2Ls8okH8qSocge4J3",
"questions": [
{
"question": "Run /plan-eng-review next (the required shipping gate), or handle reviews manually?",
"header": "Next review",
"options": [
{
"label": "Run /plan-eng-review next (recommended)",
"description": "Required gate before shipping. Validates test isolation, factory patterns, and confirms no hidden dependencies. Fast run expected given zero CEO findings."
},
{
"label": "Skip — I’ll handle reviews manually",
"description": "You’ll need to run /plan-eng-review separately before merging. The CEO review is clear but eng review is the shipping gate."
}
],
"multiSelect": false
}
],
"answered": true,
"failed": false,
"answers": {
"Run /plan-eng-review next (the required shipping gate), or handle reviews manually?": "Run /plan-eng-review next (recommended)"
},
"unansweredQuestionIndices": [],
"answeredAt": "2026-09-09T04:48:18.389Z"
}
]
+872
View File
@@ -0,0 +1,872 @@
# Plan: cache profile summaries in one process
## Measured problem and accepted scope
The existing profile-summary service has one active process. A one-week trace
shows repeated reads of about 900 hot keys: DB CPU is 70%, with read p95 120 ms.
Add a process-local LRU wrapper to the existing repository. Acceptance targets
are at least 60% cache hits, DB CPU below 50%, and read p95 below 60 ms, with the
existing error-rate and correctness SLOs unchanged. This is an internal backend
change with no UI, API, schema, pricing, or developer onboarding change.
## Existing contracts retained
- All reads and writes use this repository in the same process; there are no
external DB writers. Multi-process operation remains unsupported and startup
rejects that configuration while caching is enabled.
- Authentication and authorization run before repository access. Keys encode
the authenticated tenant ID and validated profile ID without ambiguity.
Values are immutable profile-summary DTOs; secrets and cache keys are never
logged. Cached results cannot bypass authorization.
- The existing LRU adapter supports 1000 entries, a 16 MiB byte cap, and a
30-second TTL. Recorded hot data fits those limits. Absent records use a
distinct sentinel with a 10-second TTL; undefined means a cache miss.
- Cache operations are synchronous and atomic in the single JS event loop.
On any cache failure the existing adapter bypasses the cache until an empty
cache is reinitialized; repository errors keep the current typed API error
mapping. The existing per-key
single-flight wrapper coalesces simultaneous misses and releases on failure.
- A read already in progress when a write commits may return its earlier DB
snapshot to that caller. Every read begun after that write completes must
observe the committed version. TTL expiry is not a substitute for this rule.
## Proposed wrapper integration
Keep the current read-through repository interface and shared adapters. These
are the complete new read/write ordering rules; no additional version checks or
coordination between a cache fill and a write are proposed:
```javascript
async function readProfile(key) {
const cached = cache.get(key);
if (cached !== undefined) return cached;
const value = await repository.read(key);
cache.set(key, value);
return value;
}
async function writeProfile(key, update) {
const saved = await repository.write(key, update);
cache.delete(key);
return saved;
}
```
## Verification and rollout
Existing repository contract tests cover tenant isolation, key validation,
absence, DB failures, and authorization. New wrapper tests cover hit/miss,
eviction and byte limits, TTL, adapter-failure fallback, successful-write
invalidation, failed-write preservation, and concurrent-miss coalescing.
The rollout uses the existing runtime feature flag: enable for 10% of keys,
then 50%, then all keys after one healthy hour at each stage. Monitor hit/miss,
eviction, cache bytes, fallback errors, DB CPU, and read p95 without raw IDs.
On error-rate or latency regression, disable the flag immediately; both reads
and writes bypass the cache while disabled, and enabling creates an empty cache.
Cold starts remain within the existing DB capacity. The service owner monitors
the rollout and records the results against the acceptance targets.
## Out of scope
Distributed caching, cross-process coherence, prewarming, changing consistency
semantics, or adding new product surfaces. The repository interface preserves a
future replacement path without introducing a general cache framework now.
---
## CEO Review — HOLD SCOPE Mode
*Conducted 2026-09-09 — automated run, AskUserQuestion unavailable; skill-recommended options auto-selected.*
### Step 0: Scope Challenge + Mode
**Mode: HOLD SCOPE** (requested by caller; appropriate — this is a backend performance fix with well-measured targets, not a greenfield feature).
**0A. Premise Challenge**
1. Is this the right problem? Yes. 70% DB CPU and 120 ms p95 on ~900 hot keys is measured, not hypothetical. A process-local cache is the canonical first response.
2. Is the plan the most direct path? Yes. The read/write pseudocode is minimal, and the rollout uses an existing flag.
3. What happens if we do nothing? DB CPU stays at 70%, approaching saturation; tail latency and error risk grow.
**0B. Existing Code Leverage**
| Sub-problem | Existing code | Reused? |
|---|---|---|
| LRU eviction + byte cap | Existing LRU adapter | Yes |
| Concurrent miss coalescing | Existing per-key single-flight wrapper | Yes |
| Bypass on adapter failure | Existing adapter behavior | Yes |
| Feature flag rollout | Existing runtime flag | Yes |
| Tenant/auth scoping | Existing key encoding + auth layer | Yes |
Nothing is being rebuilt. This is a thin orchestration wrapper.
**0C. Dream State Mapping**
```
CURRENT STATE THIS PLAN 12-MONTH IDEAL
─────────────────────────────────────────────────────────────────────────────
DB CPU 70%, p95 120ms → In-process LRU, 60%+ hit rate → DB CPU <50%,
900 hot keys repeated DB CPU <50%, p95 <60ms p95 <60ms,
No cache layer Staged rollout w/ flag possible Redis
if multi-process
needed later
```
This plan moves directly toward the 12-month ideal. The interface preserves the Redis upgrade path.
**0C-bis. Implementation Alternatives**
```
APPROACH A: Process-local LRU wrapper (CHOSEN by plan)
Summary: Wrap existing repository with cache.get/set/delete at call sites.
Effort: S
Risk: Low
Pros: No new dependencies; reuses existing LRU adapter and single-flight
wrapper; rollback via existing flag; synchronous/atomic in JS event loop.
Cons: Cache lost on restart; does not help if multi-process is ever needed.
Reuses: Existing LRU adapter, single-flight wrapper, feature flag.
APPROACH B: Redis-backed distributed cache
Summary: Replace in-process LRU with Redis read-through.
Effort: L
Risk: High
Pros: Survives restarts; supports multi-process; higher cache capacity.
Cons: New infrastructure dependency; network RTT adds latency; multi-process
coherence becomes a real problem; overkill for single-process service.
Reuses: None of the existing LRU infrastructure.
APPROACH C: DB-level read replica / materialized view
Summary: Move hot reads to a read replica with DB-level caching.
Effort: XL
Risk: High
Pros: No application-layer cache; DB manages consistency.
Cons: Infrastructure changes outside the service; 6-month lead time;
out of the service team's control.
Reuses: None.
```
**RECOMMENDATION:** Approach A. The service is single-process by contract; Approach A is the minimum correct solution. Approaches B and C are valid future paths if the architecture changes — the plan's interface intentionally preserves those paths.
**0D. HOLD SCOPE Analysis**
Complexity check: The change touches one repository wrapper (2 functions). No new services, no new classes. Well within complexity budget.
Minimum set: The plan is already minimal — the pseudocode is the complete implementation surface. No work can be deferred without blocking the core objective.
**0E. Temporal Interrogation**
```
HOUR 1 (foundations): Does the existing LRU adapter expose the exact
get/set/delete/bypass interface the wrapper assumes?
Confirm the adapter's undefined-on-miss contract.
HOUR 2-3 (core logic): What is the concrete type of the sentinel? A specific
symbol, null, or an object? undefined must be reserved
for cache miss.
HOUR 4-5 (integration):How does the single-flight wrapper interact with
cache.set — does it call readProfile or repository.read
directly? The cache.set must be inside the single-flight
scope to avoid double population.
HOUR 6+ (polish/tests):Controlled pause/release tests for concurrent read+write
interleaving (read starts, write completes + deletes,
read resumes and calls cache.set with stale value).
This is the hardest behavior to reason about.
```
---
### Section 1: Architecture Review
**System architecture:**
```
CALLER
│
▼
ProfileRepository (interface unchanged)
│
├─ cache.get(key) ──────────────────▶ LRU Adapter (in-process)
│ │ │
│ [hit: value] [synchronous,
│ │ atomic in JS
│ return cached ◀───────────────── event loop]
│ │
│ [miss: undefined]
│ │
▼ ▼
repository.read(key) ◀──── DB
│
├─[success]──▶ cache.set(key, value) ──▶ LRU Adapter
│ │
│ return value
│
└─[error]──▶ propagate (no cache.set)
WRITE PATH:
CALLER
│
▼
repository.write(key, update) ──▶ DB
│
├─[success]──▶ cache.delete(key) ──▶ LRU Adapter
│ │
│ return saved
│
└─[error]──▶ propagate (cache.delete NOT called — intentional)
FAILURE PATH:
Any cache.get / cache.set / cache.delete throws
│
▼
LRU Adapter enters BYPASS mode (existing behavior)
│
▼
All reads/writes go directly to DB until adapter reinitialized (empty cache)
```
**Cache state machine:**
```
[flag disabled] ──────────────────────────────────▶ BYPASS (all ops → DB)
│
[flag enabled]
│
▼
EMPTY ──(first miss)──▶ WARMING ──(adapter error)──▶ BYPASS
▲ │ │
│ (write) (reinit/empty)
│ │ │
│ key deleted │
│ │ ▼
└────────────────── (TTL expiry) ◀──────────────── EMPTY ◀──┘
```
**Coupling concerns:** The wrapper introduces coupling between ProfileRepository and the LRU Adapter. This coupling is intentional and bounded: the adapter is an existing shared component, and the bypass behavior decouples the failure modes.
**Before/after dependency graph:**
```
BEFORE: AFTER:
Caller ──▶ Repository ──▶ DB Caller ──▶ Repository ──▶ LRU Adapter
└──▶ DB (on miss)
```
**Single points of failure:**
- The LRU Adapter: mitigated by bypass mode.
- The single process: by design; multi-process is explicitly unsupported and rejected at startup.
**Scaling:**
- 10x load: Hot 900 keys still cached; miss rate is unchanged; DB load stays lower. Holds.
- 100x load: Single-process limit is reached before the cache becomes the bottleneck. Out of scope by contract.
**Security architecture:** Auth runs before repository access; keys encode tenant+profile IDs; no new API surface; no new secrets; cached values cannot bypass authorization. OK.
**Production failure scenarios:**
- LRU Adapter throws on get: Bypass mode. DB reads continue. OK.
- LRU Adapter throws on set: Bypass mode. Worst case: one read returns stale (see Section 4). OK.
- LRU Adapter throws on delete: The plan states "any cache failure" triggers bypass. This must include delete — see Section 2 gap.
- DB connection fails during read: Error propagates as before. Adapter not written. OK.
- OOM: 16 MiB byte cap + 1000-entry cap prevents unbounded growth. OK.
**Rollback posture:** Disable feature flag → reads and writes bypass cache → DB returns to baseline behavior. Enabling creates an empty cache. Rollback is fast, clean, and reversible. 5/5.
**Assessment:** Architecture is sound. No structural issues. Required diagram produced above.
---
### Section 2: Error & Rescue Map
```
METHOD/CODEPATH | WHAT CAN GO WRONG | EXCEPTION CLASS
------------------------|------------------------------|--------------------
cache.get(key) | Adapter internal error | AdapterError
repository.read(key) | DB connection failure | DBConnectionError
| Query timeout | TimeoutError
| Record not found | (sentinel, not error)
cache.set(key, value) | Adapter internal error | AdapterError
| Byte cap full | (handled by eviction)
repository.write(k,u) | DB write failure | DBWriteError
| Validation error | ValidationError
| Conflict / constraint | ConflictError
cache.delete(key) | Adapter internal error | AdapterError
```
```
EXCEPTION CLASS | RESCUED? | RESCUE ACTION | USER SEES
------------------------|-----------|--------------------------------|----------------
AdapterError (get) | Y | Bypass mode until reinit | Transparent
AdapterError (set) | Y | Bypass mode until reinit | Transparent
AdapterError (delete) | Y* | Bypass mode until reinit | Transparent*
DBConnectionError | Y | Existing typed API mapping | Existing error
TimeoutError | Y | Existing typed API mapping | Existing error
DBWriteError | Y | Propagate; cache NOT deleted | Existing error
ValidationError | Y | Propagate; cache NOT deleted | Existing error
ConflictError | Y | Propagate; cache NOT deleted | Existing error
```
*AdapterError (delete): The plan says "on any cache failure the existing adapter bypasses the cache." This SHOULD cover delete failures, but the adapter-failure test suite must explicitly verify this case (see Section 6 gap).
**GAP identified:** If cache.delete throws after a successful repository.write, bypass mode should activate. This preserves the write's committed value (all reads go to DB). The plan's wording covers this but the test suite does not explicitly verify it.
**Assessment:** No critical gaps. One test coverage gap on delete-path adapter failure. Error handling architecture is correct.
---
### Section 3: Security & Threat Model
| Threat | Likelihood | Impact | Mitigated? |
|---|---|---|---|
| Cross-tenant cache read | Low | High | Yes — keys encode tenant ID + profile ID |
| Auth bypass via cache | Low | High | Yes — auth runs before repository access |
| PII in logs/metrics | Low | High | Yes — raw IDs excluded from all monitoring |
| Stale cache serving wrong user | Low | Med | Yes — tenant ID in key prevents cross-user access |
| Cache poisoning via write | Low | Med | Yes — write path deletes key; cache only stores DB-confirmed values |
| DoS via cache bypass (flag off) | Low | Low | Yes — existing DB capacity handles cold starts |
| New dependency exploit surface | None | — | N/A — no new dependencies added |
**Assessment:** Security posture is solid. No new attack surfaces. The key-encoding contract and auth-before-cache design are correctly stated. No findings.
---
### Section 4: Data Flow & Interaction Edge Cases
**Read flow (all paths):**
```
KEY ──▶ cache.get(key)
│
[hit: value] ──────────────────────────────▶ return value
│
[miss: undefined]
│
▼
repository.read(key)
│
[nil/absent] ──▶ cache.set(key, SENTINEL, TTL=10s) ──▶ return sentinel
│
[value] ──▶ cache.set(key, value, TTL=30s) ──▶ return value
│
[error] ──▶ propagate (no cache.set)
│
[adapter error on set] ──▶ bypass mode; return value anyway
```
**Write flow (all paths):**
```
KEY, UPDATE ──▶ repository.write(key, update)
│
[success: saved]
│
cache.delete(key) ──[adapter error]──▶ bypass mode
│
return saved
│
[write error] ──▶ propagate
(cache.delete NOT called — correct)
```
**Async ordering — the in-flight read race:**
```
T0: Read R1 starts → cache.get(key) = undefined (miss)
T1: Write W1 commits → repository.write returns → cache.delete(key) called
T2: R1 resumes → repository.read returns old snapshot
T3: R1 calls cache.set(key, OLD_VALUE) ← stale write to cache
T4: New Read R2 starts → cache.get(key) = OLD_VALUE (stale hit)
```
**Finding (WARNING):** The plan's consistency invariant states: "Every read begun after that write completes must observe the committed version." But R2 in the timeline above begins after W1 completes and may see the stale value R1 wrote at T3. The stale window is bounded by the 30s TTL, and the plan explicitly excludes "additional version checks or coordination." However, the current invariant wording overstates the guarantee — the implementation provides *eventual* consistency with a bounded stale window (max 30s), not strict post-write read-your-writes for concurrent reads.
The plan should clarify: the consistency guarantee applies to reads that begin AFTER both (a) the write commits AND (b) any in-flight concurrent read's cache.set has completed. For the narrow window between W1's cache.delete and R1's cache.set, a subsequently-started reader may see a stale cache entry for up to 30s. This is acceptable given the "immutable profile-summary DTO" semantics (updates are infrequent), but it should be stated rather than implied by "TTL expiry is not a substitute."
**No interaction edge cases apply** — there is no user-visible interaction (backend-only change).
**Assessment:** One WARNING on consistency invariant wording. No critical data flow gaps.
---
### Section 5: Code Quality Review
```javascript
async function readProfile(key) {
const cached = cache.get(key);
if (cached !== undefined) return cached;
const value = await repository.read(key);
cache.set(key, value);
return value;
}
async function writeProfile(key, update) {
const saved = await repository.write(key, update);
cache.delete(key);
return saved;
}
```
- **Organization:** Correct. Two functions, each with a single responsibility.
- **DRY:** No violations. The cache adapter is the single source of eviction/TTL logic.
- **Naming:** Clear. `readProfile`/`writeProfile` are semantic and consistent with the repository interface.
- **Error handling:** Delegated correctly. Cache errors flow to the adapter's bypass mechanism; DB errors propagate through existing typed mapping.
- **Over-engineering:** None. The plan correctly resists building a general cache framework.
- **Under-engineering:** The async race (Section 4) is an inherent property of read-through caching without version checks. Given the plan explicitly excludes version checks, this is an accepted design choice, not a quality gap — but must be documented.
- **Cyclomatic complexity:** Both functions have ≤1 branch. No concern.
- **Sentinel handling:** `undefined` as the cache-miss signal is correct as long as the repository never returns `undefined` for a valid absent record. The plan uses a sentinel object for absent records — this distinction must be verified in implementation (absent ≠ undefined).
**Assessment:** Code quality is high. One documentation gap (consistency model for in-flight race). No structural defects.
---
### Section 6: Test Review
**New items introduced:**
```
NEW UX FLOWS:
None — backend-only change.
NEW DATA FLOWS:
1. readProfile: cache hit path
2. readProfile: cache miss → DB read → cache.set
3. readProfile: absent key → sentinel
4. writeProfile: success → cache.delete
5. writeProfile: failure → no cache.delete
NEW CODEPATHS:
1. cache.get returns hit
2. cache.get returns miss (undefined)
3. cache.set called on successful read
4. cache.set skipped on read error
5. cache.delete called on successful write
6. cache.delete skipped on failed write
7. Adapter failure → bypass mode (get path)
8. Adapter failure → bypass mode (set path)
9. Adapter failure → bypass mode (delete path) ← gap
10. Single-flight coalescing (existing wrapper, new context)
NEW BACKGROUND JOBS / ASYNC WORK:
None.
NEW INTEGRATIONS / EXTERNAL CALLS:
None (existing DB, existing adapter).
NEW ERROR/RESCUE PATHS:
1. Adapter failure on get → bypass
2. Adapter failure on set → bypass
3. Adapter failure on delete → bypass (needs explicit test)
```
**Coverage per plan vs. required:**
| Test | In Plan? | Gap? |
|---|---|---|
| Cache hit | Yes | No |
| Cache miss | Yes | No |
| Eviction + byte limits | Yes | No |
| TTL expiry | Yes | No |
| Adapter-failure fallback (get) | Yes (implied) | No |
| Adapter-failure fallback (set) | Yes (implied) | No |
| Adapter-failure fallback (delete) | Unclear | **GAP** |
| Successful write invalidation | Yes | No |
| Failed write preservation | Yes | No |
| Concurrent-miss coalescing | Yes | No |
| Concurrent read+write interleave | Not listed | **GAP** |
| Absent key sentinel (10s TTL) | Implied by TTL test | Partial |
| Sentinel vs. undefined distinction | Not listed | **GAP** (minor) |
**Missing test specs:**
```javascript
// GAP: cache.delete adapter failure triggers bypass
test('adapter failure on cache.delete enters bypass mode', async () => {
cache.simulateDeleteFailure(true);
const saved = await writeProfile(key, update);
expect(bypassMode).toBe(true);
expect(saved).toEqual(expectedSaved); // write still succeeds
});
// GAP: concurrent read+write interleave
test('read started before write completes may set stale cache entry', async () => {
// Pause R1 after repository.read, before cache.set
// Let W1 complete (write + cache.delete)
// Resume R1: cache.set with old value
// Verify: new reads see stale value until TTL
// Verify: stale window is bounded by TTL (≤30s)
});
// GAP (minor): sentinel vs. undefined contract
test('absent key uses sentinel, not undefined, so undefined remains cache-miss signal', async () => {
const result = await readProfile(absentKey);
expect(result).not.toBeUndefined(); // sentinel, not miss signal
expect(cache.get(absentKey)).not.toBeUndefined(); // sentinel is cached
});
```
**Test pyramid:** Unit tests (cache hit/miss/TTL/eviction) + integration tests (write invalidation, single-flight) + contract tests (existing). No E2E needed for this backend change. Pyramid is well-formed.
**Flakiness risk:** The concurrent interleave test requires controlled pause/resume points (e.g., `--expose-gc` or manual promise sequencing). Must not rely on timing.
**2am Friday test:** "Does a write always invalidate the cache, even when the adapter throws on delete?" — currently not tested.
**Hostile QA test:** "What if a read and a write race, and the read wins the cache?" — currently not tested.
**Chaos test:** "What if the adapter throws intermittently — does bypass mode activate, and do reads still return correct DB data?" — partially covered by adapter-failure test if it's thorough.
**Assessment:** 3 test gaps (delete-path bypass, read/write interleave, sentinel contract). No blocking gaps, but the interleave test is important for validating the consistency claim.
---
### Section 7: Performance Review
**Acceptance target analysis:**
The target (p95 < 60 ms) with 60% cache hit rate requires the DB tail latency to drop, not just the volume. The key insight: DB CPU at 70% is likely in the queuing-saturated zone where latency grows super-linearly. Going from 70% → <50% CPU utilization can reduce the 95th-percentile queuing delay by 3-5x (M/M/1 queuing model). DB p95 could drop from 120 ms to ~50-60 ms after load reduction, making the overall p95 target achievable even though p95 falls in the DB-served 40%.
The plan's acceptance targets are achievable but depend on this queuing dynamic — not just cache hit rate alone. The rollout monitoring will empirically confirm this.
**Memory:** 16 MiB / 1000 entries = ~16 KB average entry size. Profile summaries at this size are plausible but should be verified in staging (large profiles with many fields could skew this).
**Cache miss penalty:** On a cold start or after bypass reinit, all 900 hot keys miss. With 120 ms DB p95 and 70% CPU (warm period), a burst of 900 sequential misses takes ~108 s at 120 ms each, or less in parallel (event loop concurrency). The single-flight wrapper prevents stampede. Cold starts remain within existing DB capacity — this is stated and correct.
**Connection pool pressure:** No new DB connections. Cache operations are in-process. No change to connection pool.
**Slow paths:**
1. Cold cache (post-enable or post-bypass): All reads hit DB at 120 ms p95 until warm. Bounded by hot key TTL (30s from first fill).
2. Bypass mode (sustained adapter failure): All reads hit DB indefinitely. Monitor fallback errors.
3. Eviction pressure (>1000 entries or >16 MiB): Evicted keys promote misses. Monitor eviction rate; if high, the 900 hot keys may not all fit — investigate DTO sizes.
**Assessment:** No blocking performance issues. One observation to add to the plan: p95 target depends on DB queuing-load reduction, not cache hit rate alone. Verify average DTO size in staging to confirm 16 MiB cap is sufficient.
---
### Section 8: Observability & Debuggability Review
**What the plan specifies:**
- Metrics: hit/miss, eviction, cache bytes, fallback errors, DB CPU, read p95
**Gaps:**
| Observability item | Plan covers? | Gap |
|---|---|---|
| Structured log at adapter failure | No | Missing |
| Log when bypass mode activates | No | Missing |
| Alert: fallback errors > threshold | No | Missing |
| Alert: DB CPU > 60% (early warning) | No | Missing |
| Alert: read p95 > 80 ms (regression warning) | No | Missing |
| Alert: hit rate < 50% (cache degradation) | No | Missing |
| Dashboard: hit rate trend over rollout | No | Missing |
| Dashboard: eviction rate (cap pressure indicator) | No | Missing |
| Runbook: adapter failure → bypass → reinit procedure | No | Missing |
| Runbook: rollback decision criteria | No | Missing |
| Debuggability: can a 3-week-old incident be reconstructed from logs? | Partial | Only if fallback errors are logged with timestamp + bypass entry event |
**Required additions to plan:**
1. **Structured log on bypass activation:** `{event: "cache_bypass_activated", reason: "adapter_error", error_class: "<class>", ts: <ts>}` — no key or value content.
2. **Structured log on bypass exit (reinit):** `{event: "cache_reinitialized", ts: <ts>}`.
3. **Alert thresholds:**
- `fallback_errors > 0 for 5 minutes` → page (adapter likely broken)
- `cache_hit_rate < 50%` for 10 minutes → warn (eviction pressure or warm-up issue)
- `db_cpu > 60%` → warn (cache not holding)
- `read_p95 > 80ms` → warn (regression early warning; flag off if sustained)
4. **Day 1 dashboard panels:** hit rate %, eviction rate/s, cache bytes used, fallback error rate, DB CPU %, read p95 (5m rolling).
5. **Runbook (adapter failure):** (1) Observe fallback_errors alert. (2) Check adapter health (memory pressure? process restart?). (3) Restart process to reinitialize cache. (4) Monitor that bypass exits and hit rate recovers. (5) If repeated, open incident — do not re-enable flag until root cause clear.
6. **Runbook (rollback decision):** If error-rate or p95 exceeds baseline by >10% for >5 min, disable flag immediately. Both reads and writes go to DB. Investigate before re-enabling.
**Assessment:** Significant observability gaps. Alerts, dashboard spec, and runbooks are needed for safe operation. These should be added to the plan before implementation.
---
### Section 9: Deployment & Rollout Review
**What the plan specifies:**
- Feature flag: existing runtime flag
- Stages: 10% → 50% → 100%
- Advance criterion: "one healthy hour at each stage"
- Rollback trigger: "error-rate or latency regression"
**Gaps:**
**Finding (P1):** "One healthy hour" is undefined. Without explicit success criteria, the service owner cannot make a consistent, objective advance decision. Define:
```
HEALTHY = all of the following for 60 consecutive minutes:
- Cache hit rate ≥ 40% (warming, approaching target)
- Fallback error rate = 0
- DB CPU < previous-stage DB CPU (trending down)
- read p95 ≤ previous-stage read p95 (trending down or flat)
- Application error rate within existing SLO
```
**Finding:** "Error-rate or latency regression" as the rollback trigger needs quantification:
```
ROLLBACK if any of:
- Application error rate increases > 10% above baseline
- read p95 increases > 20% above the stage-entry baseline
- Fallback errors > 0 for > 5 consecutive minutes
```
**Other deployment items:**
- **No DB migrations:** In-memory cache only. Deploy is a code deploy. Zero downtime.
- **Old/new code interop:** Feature flag gates the wrapper. Old code path (bypass) is always valid. No interop risk.
- **Cold start risk:** Bounded. Single-flight wrapper prevents stampede. OK.
- **Staging test:** Not mentioned. Should validate hit rate and bypass behavior before production rollout.
- **Post-deploy verification (first 5 min):** Check fallback errors = 0, hit rate rising, DB CPU trending down.
- **Post-deploy verification (first hour):** Confirm hit rate ≥ 40% at 10% stage, no error regressions.
- **Smoke test:** Run the existing contract test suite post-deploy to confirm correctness is preserved.
**Assessment:** One P1 finding (healthy hour definition) and one gap (rollback quantification). Both should be added to the plan.
---
### Section 10: Long-Term Trajectory Review
**Technical debt:**
- Code debt: Minimal. The wrapper is 2 functions over existing infrastructure.
- Operational debt: Cache state adds a new invisible variable (what's cached, when was it warmed?). Mitigated by bypass mode and short TTL (30s max staleness).
- Testing debt: 3 gaps identified in Section 6. Small.
- Documentation debt: The in-flight read race (Section 4 finding) must be documented in a code comment, or future maintainers will strengthen the invariant incorrectly.
**Path dependency:** The interface is not locked. The plan explicitly states the repository interface "preserves a future replacement path." Redis or another adapter can be substituted without changing the caller interface. Two-way door.
**Knowledge concentration:** The sentinel pattern (absent ≠ undefined ≠ null) and the bypass model are non-obvious. A code comment or inline doc on the consistency model prevents future bugs.
**Reversibility:** 4/5. Flag off = instant rollback. Process restart = empty cache. Only risk: a bypass mode bug could persist until restart.
**Ecosystem fit:** Standard JS async/await pattern with process-local LRU. Aligns with the ecosystem.
**1-year question:** The wrapper is simple enough to be understood immediately. The race condition in the consistency model is the one non-obvious piece — document it.
**Assessment:** Long-term trajectory is clean. One documentation debt item (consistency model comment).
---
### Section 11: Design & UX Review
**SKIPPED** — no UI scope. The plan explicitly states "no UI, API, schema, pricing, or developer onboarding change." This is a backend-only performance change.
---
### Outside Voice
**Codex review skipped** (`codex_reviews: disabled` per isolated config). Re-enable: `gstack-config set codex_reviews enabled`.
Outside coverage: **disabled** (intentional opt-out, not a provider failure).
---
## Required Outputs
### NOT in scope
| Item | Rationale |
|---|---|
| Distributed (Redis) caching | Single-process contract; multi-process is rejected at startup |
| Cross-process cache coherence | Same as above |
| Cache prewarming | Not needed; hot keys warm naturally within one TTL cycle |
| Changing consistency semantics | Out of scope by plan |
| Cache compression / serialization | Not needed for 16 MiB cap with current DTO sizes |
| New product surfaces or API changes | Explicitly excluded |
| Cache statistics admin UI | Operational tooling beyond current scope |
### What already exists
| Component | Existing code | Used by plan |
|---|---|---|
| LRU adapter | 1000 entries, 16 MiB cap, 30s TTL, sentinel support | Yes — cache.get/set/delete |
| Per-key single-flight wrapper | Coalesces concurrent misses | Yes — wraps repository.read |
| Runtime feature flag | Existing flag mechanism | Yes — rollout stages |
| Tenant/profile key encoding | Auth layer + key schema | Yes — no changes needed |
| Repository contract tests | Tenant isolation, key validation, absence, DB failures, authorization | Yes — extended, not replaced |
| Adapter bypass behavior | On any adapter failure, bypass until reinit | Yes — error handling relies on this |
### Dream state delta
```
THIS PLAN LEAVES US AT:
DB CPU < 50% (from 70%) → target met
read p95 < 60 ms (from 120 ms) → target met (after queuing load drops)
Cache hit rate 60%+ → target met for hot 900 keys
No UI/API/schema change → clean internal change
Future Redis path preserved → interface is adapter-agnostic
REMAINING DELTA TO 12-MONTH IDEAL:
If multi-process operation is needed later, the LRU wrapper becomes invalid
and a distributed cache (Redis) is required. The interface is ready; the
infrastructure and coherence design are not yet built.
Runbooks, alerts, and dashboard spec are deferred — they should be built
before or during rollout.
```
### Error & Rescue Registry
| Method/Codepath | What can go wrong | Exception class | Rescued? | Rescue action | User sees |
|---|---|---|---|---|---|
| cache.get(key) | Adapter internal error | AdapterError | Y | Bypass mode | Transparent |
| repository.read(key) | DB connection failure | DBConnectionError | Y | Existing typed API | Existing error |
| repository.read(key) | Query timeout | TimeoutError | Y | Existing typed API | Existing error |
| repository.read(key) | Record not found | (sentinel, not error) | Y | Sentinel cached | Absent result |
| cache.set(key, v) | Adapter internal error | AdapterError | Y | Bypass mode | Transparent |
| cache.set(key, v) | Byte cap | (handled by eviction) | Y | Eviction | Transparent |
| repository.write(k,u) | DB write failure | DBWriteError | Y | Propagate; no delete | Existing error |
| repository.write(k,u) | Validation error | ValidationError | Y | Propagate; no delete | Existing error |
| repository.write(k,u) | Conflict | ConflictError | Y | Propagate; no delete | Existing error |
| cache.delete(key) | Adapter internal error | AdapterError | Y* | Bypass mode* | Transparent* |
*Bypass on delete failure: covered by "any cache failure" language; needs explicit test.
### Failure Modes Registry
| Codepath | Failure mode | Rescued? | Test? | User sees | Logged? |
|---|---|---|---|---|---|
| cache.get | Adapter throws | Y | Yes (implied) | Transparent | Should be (gap) |
| cache.get | Returns stale (in-flight race) | N/A | No (gap) | Stale data ≤30s | No |
| cache.set | Adapter throws | Y | Yes (implied) | Transparent | Should be (gap) |
| cache.delete | Adapter throws | Y* | No (gap) | Transparent | Should be (gap) |
| repository.read | DB failure | Y | Yes (existing) | Existing error | Yes (existing) |
| repository.write | DB failure | Y | Yes (existing) | Existing error | Yes (existing) |
| Bypass mode | Sustained adapter failure | Y | Partial | Transparent | Gap (no log) |
| Cold start | All keys miss | Y | Implicit | Temporary latency | No |
Rows with `Test? = No (gap)` are test gaps; none are CRITICAL GAPSSince bypass mode prevents silent failures. The in-flight race row is a documentation gap, not a rescue gap (the bounded stale window is accepted behavior).
### TODOS.md updates
No TODOs proposed for HOLD SCOPE mode beyond the implementation tasks below. The observability and test additions are within-scope for the current PR, not deferred work. The future Redis path is already noted in the plan's "out of scope" section.
### Diagrams produced
1. System architecture — Section 1 ✓
2. Cache state machine — Section 1 ✓
3. Before/after dependency graph — Section 1 ✓
4. Read flow (all paths) — Section 4 ✓
5. Write flow (all paths) — Section 4 ✓
6. Async race timeline — Section 4 ✓
Rollback flowchart: feature flag disable → bypass mode → empty cache on re-enable. Single step; no diagram needed beyond the state machine above.
### Stale Diagram Audit
No existing ASCII diagrams found in the plan file prior to this review. No stale diagrams to audit.
---
## Implementation Tasks
Synthesized from this review's findings. Each task derives from a specific finding above. Run with Claude Code or Codex; checkbox as you ship.
- [ ] **T1 (P1, human: ~30min / CC: ~5min)** — Plan document — Define rollout "healthy hour" success criteria
- Surfaced by: Section 9 — "one healthy hour" is undefined; operators cannot make objective advance decisions
- Files: PLAN.md (or inline rollout runbook)
- Verify: Criteria are quantitative and unambiguous (hit rate %, fallback errors, DB CPU, p95 bounds)
- [ ] **T2 (P2, human: ~1h / CC: ~10min)** — Wrapper tests — Add explicit test: cache.delete adapter failure triggers bypass mode
- Surfaced by: Section 2 / Section 6 — "adapter-failure fallback" test set does not explicitly cover the delete code path
- Files: `<wrapper test file>`
- Verify: Test simulates adapter throw on delete; asserts bypass mode activates; write still returns saved value
- [ ] **T3 (P2, human: ~2h / CC: ~15min)** — Wrapper tests — Add concurrent read+write interleave test
- Surfaced by: Section 4 / Section 6 — Async race where read's cache.set follows write's cache.delete is untested and invalidates the stated consistency invariant
- Files: `<wrapper test file>`
- Verify: Controlled pause/release test confirms bounded stale window (≤30s TTL); no panic or error
- [ ] **T4 (P2, human: ~1h / CC: ~10min)** — Plan document — Clarify consistency invariant to reflect the in-flight read race
- Surfaced by: Section 4 — Current invariant ("every read begun after write completes must observe committed version") is overstated; the implementation allows a bounded stale window for concurrent in-flight reads
- Files: PLAN.md (consistency contract section), `<wrapper implementation file>` (comment)
- Verify: Updated invariant accurately describes actual guarantees; code comment explains the race
- [ ] **T5 (P2, human: ~2h / CC: ~20min)** — Observability — Add structured logs, alert thresholds, and runbooks
- Surfaced by: Section 8 — Bypass activation/exit have no log events; no alert thresholds defined; no operational runbook
- Files: `<wrapper implementation file>` (logs), monitoring config, runbook doc
- Verify: Bypass activation logs `{event: "cache_bypass_activated", reason, error_class}`; alerts fire in staging test; runbook covers adapter failure and rollback decision
- [ ] **T6 (P2, human: ~30min / CC: ~5min)** — Wrapper tests — Add sentinel vs. undefined contract test
- Surfaced by: Section 6 — The absent-key sentinel must not be undefined (which is the cache-miss signal); this contract is untested
- Files: `<wrapper test file>`
- Verify: Absent key returns sentinel; `cache.get` of an absent key returns sentinel (not undefined)
_No new tasks from Section 1 (Architecture) — no structural issues._
_No new tasks from Section 3 (Security) — no attack surfaces introduced._
_No new tasks from Section 5 (Code Quality) — code is minimal and correct._
_No new tasks from Section 7 (Performance) — targets are achievable; p95 dependency on queuing dynamics is an observation, not a gap._
_No new tasks from Section 10 (Long-term) — documentation need is captured in T4._
_No new tasks from Section 11 (Design) — skipped, no UI scope._
---
## Completion Summary
```
+====================================================================+
| MEGA PLAN REVIEW — COMPLETION SUMMARY |
+====================================================================+
| Mode selected | HOLD SCOPE |
| System Audit | Skipped (automated run, per instructions) |
| Step 0 | HOLD SCOPE confirmed; Approach A accepted; |
| | In-flight read race identified as key concern|
| Section 1 (Arch) | 0 issues found; architecture is sound |
| Section 2 (Errors) | 9 error paths mapped, 1 gap (delete bypass) |
| Section 3 (Security)| 0 issues found; 0 High severity |
| Section 4 (Data/UX) | 1 WARNING: consistency invariant overstated;|
| | in-flight read race bounded by 30s TTL |
| Section 5 (Quality) | 0 issues found |
| Section 6 (Tests) | Diagram produced; 3 gaps (delete bypass, |
| | read/write interleave, sentinel contract) |
| Section 7 (Perf) | 0 issues; p95 target achievable via |
| | queuing-load reduction |
| Section 8 (Observ) | 6 gaps: logs, alerts, dashboard, runbooks |
| Section 9 (Deploy) | 1 P1: "healthy hour" undefined; 1 gap: |
| | rollback trigger not quantified |
| Section 10 (Future) | Reversibility: 4/5; 1 doc debt item |
| Section 11 (Design) | SKIPPED (no UI scope) |
+--------------------------------------------------------------------+
| NOT in scope | written (7 items) |
| What already exists | written (6 components mapped) |
| Dream state delta | written |
| Error/rescue registry| 10 paths, 0 CRITICAL GAPS |
| Failure modes | 8 total, 0 CRITICAL GAPS |
| TODOS.md updates | 0 items proposed (gaps are in-PR tasks) |
| Scope proposals | 0 proposed, 0 accepted (HOLD SCOPE) |
| CEO plan | skipped (HOLD SCOPE) |
| Outside voice | disabled (codex_reviews=disabled in config) |
| Lake Score | 6/6 findings chose complete option |
| Diagrams produced | 6 (arch, state machine, dep graph, |
| | read flow, write flow, race timeline) |
| Stale diagrams found | 0 |
| Unresolved decisions | 0 (automated run; all auto-selected) |
+====================================================================+
```
### Unresolved Decisions
None. All decision points were auto-selected per spawned-session rules (recommended option). No outstanding AskUserQuestion items.
---
## Review Readiness Dashboard
```
+====================================================================+
| REVIEW READINESS DASHBOARD |
+====================================================================+
| Review | Runs | Last Run | Status | Required |
|-----------------|------|---------------------|-----------|----------|
| CEO Review | 1 | 2026-09-09 | HOLD SCOPE| no |
| Eng Review | 0 | — | — | YES |
| Design Review | 0 | — | — | no |
| Adversarial | 0 | — | — | no |
| Outside Voice | 0 | — | disabled | no |
+--------------------------------------------------------------------+
| VERDICT: NOT CLEARED — Eng Review required before shipping |
+====================================================================+
```
**Outside coverage:** `codex_reviews: disabled` — intentional opt-out. No outside review ran and no fallback was dispatched (disabled is a terminal branch, not a failure state).
**Next step:** Run `/plan-eng-review` to complete the required shipping gate. This CEO review found no blocking issues — eng review should be straightforward.
---
## GSTACK REVIEW REPORT
| Review | Trigger | Why | Runs | Status | Findings |
|--------|---------|-----|------|--------|----------|
| CEO Review | `/plan-ceo-review` | Scope & strategy | 1 | HOLD SCOPE / issues_open | mode: HOLD_SCOPE, 0 critical gaps; 6 tasks (1 P1, 5 P2) |
| Outside Review | disabled | Independent 2nd opinion | 0 | disabled | codex_reviews=disabled; no fallback dispatched |
| Eng Review | `/plan-eng-review` | Architecture & tests (required) | 0 | — | not yet run |
| Design Review | `/plan-design-review` | UI/UX gaps | 0 | — | not applicable (no UI scope) |
| DX Review | `/plan-devex-review` | Developer experience gaps | 0 | — | not yet run |
**OUTSIDE COVERAGE:** Disabled via `codex_reviews: disabled` in isolated config. No provider invoked; no native fallback dispatched. This is an intentional opt-out, not a missing-coverage failure.
**VERDICT:** CEO review complete (HOLD SCOPE). Eng Review required before shipping — not yet run.
NO UNRESOLVED DECISIONS
+28
View File
@@ -0,0 +1,28 @@
{
"sessionId": "342a4ade-0ac4-414d-893e-9c105a1189b6",
"toolUseId": "toolu_01UejNHDeBfix3rsNtczmySN",
"questions": [
{
"question": "D3 — No design doc found for this branch. Run /office-hours first, or proceed with standard review? <gstack-qid:plan-devex-prereq-office-hours>\n\nELI10: /office-hours produces a structured problem statement, premise challenge, and explored alternatives — it gives this DX review sharper input to work with. Takes about 10 minutes. The design doc is per-feature, not per-product — it captures the thinking behind this specific change.\n\nFor EvalKit's beta polish, the plan is already explicit about scope (DX POLISH, five specific contracts to review), so the review has strong grounding without a separate design doc.\n\nRecommendation: B — skip. The plan scope is already precise and the contracts are documented. /office-hours would help most when scope is still fuzzy.\n\nNote: options differ in kind, not coverage — no completeness score.",
"header": "Design doc",
"multiSelect": false,
"options": [
{
"label": "Run /office-hours first",
"description": "Produces a design doc with problem statement and alternatives. ~10 min. Pick up DX review after."
},
{
"label": "Skip — proceed (Recommended)",
"description": "Plan scope is already precise. Proceed with standard DX POLISH review."
}
]
}
],
"answered": true,
"failed": false,
"answers": {
"D3 — No design doc found for this branch. Run /office-hours first, or proceed with standard review? <gstack-qid:plan-devex-prereq-office-hours>\n\nELI10: /office-hours produces a structured problem statement, premise challenge, and explored alternatives — it gives this DX review sharper input to work with. Takes about 10 minutes. The design doc is per-feature, not per-product — it captures the thinking behind this specific change.\n\nFor EvalKit's beta polish, the plan is already explicit about scope (DX POLISH, five specific contracts to review), so the review has strong grounding without a separate design doc.\n\nRecommendation: B — skip. The plan scope is already precise and the contracts are documented. /office-hours would help most when scope is still fuzzy.\n\nNote: options differ in kind, not coverage — no completeness score.": "Run /office-hours first"
},
"unansweredQuestionIndices": [],
"answeredAt": "2026-09-09T04:42:06.631Z"
}
+53
View File
@@ -1814,6 +1814,39 @@ describe('DESIGN_HARD_RULES resolver', () => {
});
});
describe('Design approval reconciliation', () => {
test('loaded review section distinguishes individual issue decisions from navigation', () => {
const section = fs.readFileSync(path.join(ROOT, 'plan-design-review/sections/review-sections.md'), 'utf8');
expect(section).toContain('Complete one decision cycle per unresolved finding');
expect(section).toContain('Scope, focus, setup, and next-step choices approve no remedies.');
expect(section).toContain('Never use the final next-step AskUserQuestion to satisfy the issue-approval loop.');
expect(section).toContain('With no unresolved findings, no issue question is required.');
});
test('finish-time check requires exact issue decisions and repairs prematurely accepted drafts', () => {
const main = fs.readFileSync(path.join(ROOT, 'plan-design-review/SKILL.md'), 'utf8');
const check = extractMarkdownSection(main, '## Section self-check');
expect(check).toContain('reconcile every proposed fix with the exact user decision for that issue');
expect(check).toContain('an explicit deferral stays unresolved');
expect(check).toContain('Existing accepted requirements need no repeat approval');
expect(check).toContain("preamble's authorized auto-decisions");
expect(check).toContain('return that change to pending before continuing');
const exitGate = main.indexOf('\n## EXIT PLAN MODE GATE (BLOCKING)');
expect(exitGate).toBeGreaterThan(0);
expect(main.indexOf('reconcile every proposed fix')).toBeLessThan(exitGate);
});
test('task and decision reporting require approval while preserving unanswered findings', () => {
const section = fs.readFileSync(path.join(ROOT, 'plan-design-review/sections/review-sections.md'), 'utf8');
const reconcile = section.indexOf('Before synthesizing tasks or the completion summary');
expect(reconcile).toBeGreaterThan(0);
expect(reconcile).toBeLessThan(section.indexOf('## Implementation Tasks'));
expect(section).toContain('Export only agreed implementation work; retain unapproved remedies as pending findings.');
expect(section).toContain('Count only individually approved new decisions');
expect(section).toContain('including a finding not yet asked');
});
});
// --- Extended DESIGN_SKETCH resolver tests ---
describe('DESIGN_SKETCH extended with outside voices', () => {
@@ -3440,6 +3473,26 @@ describe('voice-triggers processing', () => {
});
});
describe('CEO accepted requirement preservation', () => {
test('HOLD SCOPE includes implementation work needed to meet accepted requirements', () => {
const main = fs.readFileSync(path.join(ROOT, 'plan-ceo-review/SKILL.md'), 'utf8');
const hold = main.slice(main.indexOf('**For HOLD SCOPE**'), main.indexOf('**For SCOPE REDUCTION**'));
expect(hold).toContain('Keep stated invariants and acceptance criteria; repairs needed to meet them are in scope.');
});
test('loaded sections keep requirement conflicts unresolved until an authorized decision', () => {
const section = fs.readFileSync(path.join(ROOT, 'plan-ceo-review/sections/review-sections.md'), 'utf8');
const policy = section.slice(section.indexOf('**Preserve accepted requirements.**'), section.indexOf('### Section 1:'));
expect(policy).toContain('report an implementation\ngap and propose a remedy that meets the requirement');
expect(policy).toContain('it does not authorize weakening the required behavior');
expect(policy).toContain('changing a test to expect the prohibited result');
expect(policy).toContain('keep that proposal pending and the original gap unresolved');
expect(policy).toContain('Earlier explicitly approved requirement changes and explicit authority to change\nthat scope remain valid');
expect(policy).toContain('Routine auto-decide permission alone cannot override an\nexplicit user constraint or non-goal');
expect(policy).toContain('Preserve the distinction in findings, tasks,\nand the completion report');
});
});
describe('plan-mode-info resolver (handshake-replacement)', () => {
const REVIEW_SKILLS = [
'plan-ceo-review',
+4 -2
View File
@@ -13,8 +13,10 @@ export function pickCeoRecommendedApproach(fp: AskUserQuestionFingerprint): numb
if (ids.length !== 1 || (q.question.match(/<gstack-qid/gi)?.length ?? 0) !== 1) return null;
const qid = ids[0]![1]!.toLowerCase();
const question = q.question.trim().replace(/^D\s*\d+\s*[—–:-]\s*/i, '');
const planApproach = qid === 'plan-ceo-approach' &&
/^Which implementation approach should this plan use\?(?:\s|$)/i.test(question);
// Native approach menus vary their routing id and use/follow wording. Match
// the selector's structure, retaining the explicit recommendation below.
const planApproach = /^plan-ceo(?:-review)?-approach(?:-selection)?$/.test(qid) &&
/^Which implementation approach should this plan (?:use|follow)\?(?:\s|$)/i.test(question);
const testApproach = qid === 'plan-ceo-review-impl-approach' &&
/^Which implementation approach for the [a-z_$][\w$]*(?:\.[a-z_$][\w$]*)*\(\) tests\?(?:\s|$)/i.test(question);
if (!planApproach && !testApproach) return null;
+27 -1
View File
@@ -35,6 +35,30 @@ function metadataClosedReviewNavigation(declaration: string, context: string): b
closedNavigationContext(context);
}
/** A direct Eng/manual choice can put its unconditional CEO recap in a native description. */
function describedEngNavigation(question: string, descriptions: string[], context: string): boolean {
if (!/^Run\s+\/plan-eng-review\s+(?:next|now)\s*\((?:the\s+)?required(?:\s+shipping)?\s+gate\),?\s+or\s+handle\s+reviews\s+manually\?$/i.test(question)) return false;
const recap = /^(?:The\s+)?CEO\s+review\s+is\s+(?:clear|complete|cleared|clean|done)(?:\s+but\s+eng\s+review\s+is\s+the\s+(?:required\s+)?shipping\s+gate)?$/i;
const topics = String.raw`(?:test isolation|factory patterns|test coverage|architecture|dependencies)`;
const reviewExplanation = new RegExp(String.raw`^(?:Validates|Checks|Reviews)\s+${topics}(?:,\s+${topics})*(?:,?\s+and\s+(?:${topics}|confirms no hidden dependencies))?$`, 'i');
const sentences = descriptions.flatMap(description => description.trim().split(/[.!](?:\s+|$)/).map(sentence => sentence.trim()).filter(Boolean));
const closedRecap = sentences.some(sentence => recap.test(sentence));
// Every sentence must explain this closed handoff. Arbitrary prose after
// a valid recap could add work (including verbs no blacklist anticipates).
if (!sentences.every(sentence => recap.test(sentence) || reviewExplanation.test(sentence) ||
/^Required(?:\s+shipping)?\s+gate\s+before\s+(?:shipping|merging|implementation)$/i.test(sentence) ||
/^(?:You['’]ll|You will)\s+need\s+to\s+run\s+\/plan-eng-review\s+(?:separately\s+)?before\s+(?:merging|shipping)$/i.test(sentence) ||
/^Run\s+\/plan-eng-review\s+(?:next|now|before\s+(?:merging|shipping)|after\s+implementation\s+and\s+before\s+shipping)$/i.test(sentence) ||
/^(?:Fast|Quick|Short)\s+(?:run|review)\s+expected\s+given\s+(?:zero|no|0)\s+CEO\s+findings$/i.test(sentence))) return false;
if (!closedRecap || /`{3}|~{3}|(?:^|\n)[ \t]*>|\b(?:example|quoted source)\s*:/im.test(context) ||
/\b(?:incomplete|unfinished|not|never)\b|n['’]t\b/i.test(context)) return false;
// "Clear" is also a closure claim here; a future condition cannot supply it.
const clearClosure = String.raw`(?:(?:the\s+)?CEO|the)\s+review\s+(?:(?:is|was|becomes?|became|(?:will|would|can|could|may|might)\s+(?:be|become))\s+)?clear`;
if (new RegExp(String.raw`\b(?:once|when|after)\b[^.!?]{0,180}\b${clearClosure}\b|\b${clearClosure}\b[^.!?]{0,100}\b(?:once|when|after)\b`, 'i').test(context)) return false;
return !/(?:^|[.!?;:]\s+|\b(?:proceed to|continue to|should|must|will|need to|can|could|would|may|might)\s+)(?:(?:please|first|then|also)\s+)*(?:add|fix|repair|implement|resolve|decide)\b/im.test(context) &&
closedNavigationContext(context);
}
/** Shared closed-review guards; next-review sequencing is still navigation. */
function closedNavigationContext(context: string): boolean {
const unfinished = context.replace(/\b(?:no|0)\s+unresolved\s+(?:decisions|gaps|issues|findings)\b/gi, '');
@@ -77,7 +101,9 @@ function manualHandoffIndex(fp: AskUserQuestionFingerprint): number | null {
!/(?:^|[.!?;]\s+|\b(?:please|must|need\s+to)\s+)(?:(?:please|first|then|also)\s+)*(?:add|fix|implement|resolve|decide)\b/im.test(gateContext);
const metadataCompletion = Boolean(id) && metadataClosedReviewNavigation(declaration, gateContext);
if (isMetadataNavigationQuestion(questionText) && /\n[ \t]*ELI10:/i.test(questionText) && !metadataCompletion) return null;
const completion = explicitCompletion || describedCompletion || metadataCompletion;
const describedEngCompletion = q.options.length === 2 &&
describedEngNavigation(questionText, q.options.map(option => option.description ?? ''), gateContext);
const completion = explicitCompletion || describedCompletion || metadataCompletion || describedEngCompletion;
const requiredEng = /(?:\bEng(?:ineering)?\s+review|\/plan-eng-review)\b[^.!?]{0,180}\brequired(?:\s+shipping)?\s+gate\b/i.test(gateContext) ||
/\brequired(?:\s+shipping)?\s+gate\s+is\s+(?:an?\s+)?(?:Eng(?:ineering)?\s+review|\/plan-eng-review)\b/i.test(gateContext);
// These native next-review identities share a closed navigation contract;
+24 -3
View File
@@ -1450,7 +1450,7 @@ export function createPlanCountPermissionGuard(): (visible: string, completionHi
}
/** Keep a seeded count plan intact by declining its optional prerequisite. */
export function planCountPrerequisitePick(fp: AskUserQuestionFingerprint): number | null {
export function planCountPrerequisitePick(fp: AskUserQuestionFingerprint, activeCapture: AskUserQuestionFingerprint = fp): number | null {
// Require the recognized prerequisite body AND both opposed labels. A
// generic Skip, an outside-review offer, or a review finding keeps its
// existing answer policy. Collapsed whitespace occurs in captured PTYs.
@@ -1460,6 +1460,24 @@ export function planCountPrerequisitePick(fp: AskUserQuestionFingerprint): numbe
const skip = fp.options.filter(({ label }) =>
/^Skip\s*[—–-]\s*(?:proceed\s*with\s*)?standard\s*review(?:\s*\(recommended\))?$/i.test(label));
if (run.length === 1 && skip.length === 1) return skip[0].index;
// Short labels such as "Skip — proceed" need their full native meaning.
// A visible generic Skip alone cannot authorize leaving a review question.
const call = activeCapture.nativeCall;
if (call && call.answered === false && call.failed === false && activeCapture.preReview && call.questions.length === 1 &&
activeCapture.signature === `${call.sessionId}:${call.toolUseId}` &&
fp.signature === activeCapture.signature &&
(activeCapture.nativeQuestionIndex === undefined || activeCapture.nativeQuestionIndex === 0)) {
const q = call.questions[0]!;
const optionsMatch = (capture: AskUserQuestionFingerprint) => capture.options.length === q.options.length &&
capture.options.every((option, i) => option.index === i + 1 && option.label === q.options[i]!.label);
if (!q.multiSelect && q.options.length === 2 && optionsMatch(fp) && optionsMatch(activeCapture)) {
const nativeRun = q.options.findIndex(option => /^Run\s*\/office-hours\s*(?:now|first)(?:\s*\(recommended\))?$/i.test(option.label));
const nativeSkip = q.options.findIndex(option =>
/^Skip\s*[—–-]\s*proceed(?:\s*\(recommended\))?$/i.test(option.label) &&
/^(?:(?:The\s+)?plan\s+(?:scope\s+)?is\s+(?:already\s+)?(?:precise|clear|well-defined|explicit)\.\s*)?Proceed\s+with\s+standard(?:\s+DX(?:\s+(?:POLISH|EXPANSION|TRIAGE))?)?\s+review\.?$/i.test((option.description ?? '').trim()));
if (nativeRun >= 0 && nativeSkip >= 0 && nativeRun !== nativeSkip) return nativeSkip + 1;
}
}
// The same prerequisite also offers "Skip — review now" or a direct
// "Proceed with standard review". Require exactly
// the two opposed actions so this wording cannot skip a mixed finding.
@@ -2848,8 +2866,11 @@ export async function runPlanSkillCounting(opts: {
async function waitForWork(ms: number): Promise<boolean> {
const remaining = remainingWork();
if (remaining <= 0) return false;
const clipped = ms >= remaining;
await Bun.sleep(Math.min(ms, remaining));
return remainingWork() > 0;
// A clipped wait cannot finish the requested interval. Timers may wake
// just before the fractional deadline; that is no license to advance.
return !clipped && remainingWork() > 0;
}
const fixture = createPlanCountFixture(opts.followUpPrompt, { nativeReviewOnly: true, files: opts.fixtureFiles });
@@ -3105,7 +3126,7 @@ export async function runPlanSkillCounting(opts: {
// Press to advance — first AUQ may use the override pick.
const routing = pending?.questions.length === 1
? nativePlanCallFingerprint(pending, fp.observedAtMs, fp.preReview) : fp;
const prerequisitePick = planCountPrerequisitePick(routing);
const prerequisitePick = planCountPrerequisitePick(routing, fp);
// Native tool records may flush only after the answer. Let a guarded
// caller recognize that visible menu. A known packet needs a positively
// matched active tab before a caller can change that tab's choice.
+10 -10
View File
@@ -148,22 +148,22 @@ export const E2E_TOUCHFILES: Record<string, string[]> = {
'tpa-absent-darwin': ['scripts/resolvers/third-party-actions.ts', 'ship/SKILL.md.tmpl', 'ship/sections/apple-release.md.tmpl', 'scripts/gen-skill-docs.ts', 'test/helpers/session-runner.ts', 'test/skill-e2e-third-party-actions.test.ts'],
'tpa-apple-ban': ['scripts/resolvers/third-party-actions.ts', 'ship/SKILL.md.tmpl', 'ship/sections/apple-release.md.tmpl', 'scripts/gen-skill-docs.ts', 'test/helpers/session-runner.ts', 'test/skill-e2e-third-party-actions.test.ts'],
'ship-section-loading': ['ship/**', 'scripts/resolvers/sections.ts', 'scripts/gen-skill-docs.ts', 'test/helpers/auq-sdk-capture.ts', 'test/helpers/session-runner.ts', 'test/session-runner-tools.test.ts', 'test/skill-e2e-ship-section-loading.test.ts'],
'plan-ceo-section-loading': ['plan-ceo-review/**', 'test/skill-ceo-section-ordering.test.ts', 'scripts/resolvers/sections.ts', 'scripts/gen-skill-docs.ts', 'test/helpers/auq-sdk-capture.ts', 'test/helpers/session-runner.ts', 'test/session-runner-tools.test.ts', 'test/helpers/ceo-section-loading-fixture.ts', 'test/ceo-section-loading-fixture.test.ts', 'test/skill-e2e-plan-ceo-review-section-loading.test.ts', 'test/fixtures/ceo-section-loading-l-report.md', 'test/fixtures/ceo-section-loading-q-report.md'],
'plan-ceo-section-loading': ['plan-ceo-review/**', 'test/skill-ceo-section-ordering.test.ts', 'scripts/resolvers/sections.ts', 'scripts/gen-skill-docs.ts', 'test/helpers/auq-sdk-capture.ts', 'test/helpers/session-runner.ts', 'test/session-runner-tools.test.ts', 'test/helpers/ceo-section-loading-fixture.ts', 'test/ceo-section-loading-fixture.test.ts', 'test/skill-e2e-plan-ceo-review-section-loading.test.ts', 'test/fixtures/ceo-section-loading-l-report.md', 'test/fixtures/ceo-section-loading-q-report.md', 'test/fixtures/ceo-section-r-rejected-report.md'],
// Data-driven behavioral guard for the 'plan'/'prompt' carves (eng, design,
// devex, office-hours + future PR2 carves). One file iterating CARVE_GUARDS;
// the selector sets GSTACK_CARVE_SKILL=<name> to scope cost to the changed
// skill (D-CODEX A). Touching the registry/helper or sections.ts runs all.
'carve-section-loading': ['scripts/resolvers/composition.ts', 'test/autoplan-review-discovery.test.ts', 'test/autoplan-phase-order.test.ts', 'design-html/**', 'design-shotgun/**', 'qa/**', 'browse/**', 'retro/**', 'autoplan/**', 'spec/**', 'setup-gbrain/**', 'review/**', 'codex/**', 'land-and-deploy/**', 'plan-eng-review/**', 'plan-design-review/**', 'plan-devex-review/**', 'office-hours/**', 'document-release/**', 'design-consultation/**', 'cso/**', 'test/helpers/carve-guards.ts', 'scripts/resolvers/sections.ts', 'scripts/resolvers/redact-doc.ts', 'scripts/gen-skill-docs.ts', 'test/helpers/auq-sdk-capture.ts', 'test/helpers/session-runner.ts', 'test/session-runner-tools.test.ts', 'test/carve-section-loading.test.ts', 'bin/gstack-autoplan-snapshot.ts', 'test/autoplan-snapshot.test.ts'],
'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/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'],
'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'],
'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/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'],
// Per-finding AskUserQuestion count + review-report-at-bottom assertion.
// Each test drives its skill end-to-end; touchfiles include preamble +
// completion-status resolvers because they affect question cadence and
// terminal output (the regression surface this test catches).
'plan-ceo-finding-count': ['bin/gstack-config', 'bin/gstack-skill-start', 'bin/gstack-skill-end', 'plan-ceo-review/**', 'test/skill-ceo-section-ordering.test.ts', 'scripts/resolvers/preamble.ts', 'scripts/resolvers/preamble/generate-ask-user-format.ts', 'scripts/resolvers/preamble/generate-completion-status.ts', 'test/helpers/claude-pty-runner.ts', 'test/plan-count-native-input.test.ts', 'test/helpers/pty-screen.ts', 'test/pty-screen.test.ts', 'test/pty-screen-session.test.ts', 'test/fixtures/pty-screen/**', 'test/helpers/hermetic-skill-runtime.ts', 'test/hermetic-skill-runtime.test.ts', 'test/helpers/claude-pty-runner.unit.test.ts', 'test/plan-count-completion.test.ts', 'test/plan-count-dx-handoff.test.ts', 'test/fixtures/devex-handoff-n-call.json', 'test/helpers/ceo-completion-handoff.ts', 'test/ceo-completion-handoff.test.ts', 'test/fixtures/ceo-completion-handoff-calls.json', 'test/fixtures/ceo-completion-handoff-j-calls.json', 'test/fixtures/ceo-completion-handoff-k-calls.json', 'test/helpers/pty-trust-dialog.ts', 'test/pty-trust-dialog.test.ts', 'test/helpers/plan-count-transcript.ts', 'test/helpers/plan-count-pending-exit.ts', 'test/plan-count-pending-exit.test.ts', 'test/plan-count-transcript.test.ts', 'test/helpers/plan-count-artifacts.ts', 'test/plan-count-artifacts.test.ts', 'test/helpers/eval-store.ts', 'test/helpers/plan-count-fixture.ts', 'test/plan-count-fixture.test.ts', 'test/plan-count-timeout.test.ts', 'test/plan-count-prerequisite-n.test.ts', 'test/fixtures/ceo-prerequisite-n-call.json', 'test/skill-e2e-plan-ceo-finding-count.test.ts', 'test/ceo-completion-handoff-l.test.ts', 'test/fixtures/ceo-completion-handoff-l-calls.json', 'test/ceo-completion-handoff-m.test.ts', 'test/fixtures/ceo-completion-handoff-m-call.json', 'test/fixtures/devex-review-l-calls.json', 'test/plan-count-checkbox.test.ts', 'test/fixtures/ceo-checkbox-l.screen.txt', 'test/fixtures/ceo-handoff-n-calls.json', 'test/ceo-completion-handoff-o.test.ts', 'test/fixtures/ceo-completion-handoff-o-call.json', 'test/plan-count-empty-review.test.ts', 'test/fixtures/plan-count-design-questionless-report.md', 'test/plan-count-dx-handoff-o.test.ts', 'test/fixtures/devex-handoff-o-call.json', 'test/helpers/ceo-approach-pick.ts', 'test/ceo-approach-pick.test.ts', 'test/fixtures/ceo-approach-q-call.json', 'test/fixtures/ceo-approach-q-paired-call.json', 'test/fixtures/ceo-completion-handoff-q-call.json'],
'plan-eng-finding-count': ['bin/gstack-config', 'bin/gstack-skill-start', 'bin/gstack-skill-end', 'plan-eng-review/**', 'scripts/resolvers/preamble.ts', 'scripts/resolvers/preamble/generate-ask-user-format.ts', 'scripts/resolvers/preamble/generate-completion-status.ts', 'test/helpers/claude-pty-runner.ts', 'test/plan-count-native-input.test.ts', 'test/helpers/pty-screen.ts', 'test/pty-screen.test.ts', 'test/pty-screen-session.test.ts', 'test/fixtures/pty-screen/**', 'test/helpers/hermetic-skill-runtime.ts', 'test/hermetic-skill-runtime.test.ts', 'test/helpers/claude-pty-runner.unit.test.ts', 'test/plan-count-completion.test.ts', 'test/plan-count-dx-handoff.test.ts', 'test/fixtures/devex-handoff-n-call.json', 'test/helpers/pty-trust-dialog.ts', 'test/pty-trust-dialog.test.ts', 'test/helpers/plan-count-transcript.ts', 'test/helpers/plan-count-pending-exit.ts', 'test/plan-count-pending-exit.test.ts', 'test/plan-count-transcript.test.ts', 'test/helpers/plan-count-artifacts.ts', 'test/plan-count-artifacts.test.ts', 'test/helpers/eval-store.ts', 'test/helpers/plan-count-fixture.ts', 'test/plan-count-fixture.test.ts', 'test/plan-count-timeout.test.ts', 'test/plan-count-prerequisite-n.test.ts', 'test/fixtures/ceo-prerequisite-n-call.json', 'test/skill-e2e-plan-eng-finding-count.test.ts', 'test/fixtures/devex-review-l-calls.json', 'test/plan-count-checkbox.test.ts', 'test/fixtures/ceo-checkbox-l.screen.txt', 'test/plan-count-empty-review.test.ts', 'test/fixtures/plan-count-design-questionless-report.md', 'test/plan-count-dx-handoff-o.test.ts', 'test/fixtures/devex-handoff-o-call.json'],
'plan-design-finding-count': ['bin/gstack-config', 'bin/gstack-skill-start', 'bin/gstack-skill-end', 'plan-design-review/**', 'scripts/resolvers/preamble.ts', 'scripts/resolvers/preamble/generate-ask-user-format.ts', 'scripts/resolvers/preamble/generate-completion-status.ts', 'test/helpers/claude-pty-runner.ts', 'test/plan-count-native-input.test.ts', 'test/helpers/pty-screen.ts', 'test/pty-screen.test.ts', 'test/pty-screen-session.test.ts', 'test/fixtures/pty-screen/**', 'test/helpers/hermetic-skill-runtime.ts', 'test/hermetic-skill-runtime.test.ts', 'test/helpers/claude-pty-runner.unit.test.ts', 'test/plan-count-completion.test.ts', 'test/plan-count-dx-handoff.test.ts', 'test/fixtures/devex-handoff-n-call.json', 'test/helpers/pty-trust-dialog.ts', 'test/pty-trust-dialog.test.ts', 'test/helpers/plan-count-transcript.ts', 'test/helpers/plan-count-pending-exit.ts', 'test/plan-count-pending-exit.test.ts', 'test/plan-count-transcript.test.ts', 'test/helpers/plan-count-artifacts.ts', 'test/plan-count-artifacts.test.ts', 'test/helpers/eval-store.ts', 'test/helpers/plan-count-fixture.ts', 'test/plan-count-fixture.test.ts', 'test/plan-count-timeout.test.ts', 'test/plan-count-prerequisite-n.test.ts', 'test/fixtures/ceo-prerequisite-n-call.json', 'test/helpers/design-count-review.ts', 'test/design-count-review.test.ts', 'test/fixtures/design-review-j-calls.json', 'test/helpers/design-count-outside.ts', 'test/design-count-outside.test.ts', 'test/skill-e2e-plan-design-finding-count.test.ts', 'test/fixtures/design-review-l-calls.json', 'test/design-completion-handoff.test.ts', 'test/fixtures/design-handoff-l-calls.json', 'test/fixtures/devex-review-l-calls.json', 'test/plan-count-checkbox.test.ts', 'test/fixtures/ceo-checkbox-l.screen.txt', 'test/fixtures/design-review-n-calls.json', 'test/design-completion-handoff-scored.test.ts', 'test/fixtures/design-handoff-n-calls.json', 'test/fixtures/design-handoff-q-calls.json', 'test/plan-count-empty-review.test.ts', 'test/fixtures/plan-count-design-questionless-report.md', 'test/plan-count-dx-handoff-o.test.ts', 'test/fixtures/devex-handoff-o-call.json'],
'plan-devex-finding-count': ['bin/gstack-config', 'bin/gstack-skill-start', 'bin/gstack-skill-end', 'plan-devex-review/**', 'scripts/resolvers/preamble.ts', 'scripts/resolvers/preamble/generate-ask-user-format.ts', 'scripts/resolvers/preamble/generate-completion-status.ts', 'test/helpers/claude-pty-runner.ts', 'test/plan-count-native-input.test.ts', 'test/helpers/pty-screen.ts', 'test/pty-screen.test.ts', 'test/pty-screen-session.test.ts', 'test/fixtures/pty-screen/**', 'test/helpers/hermetic-skill-runtime.ts', 'test/hermetic-skill-runtime.test.ts', 'test/helpers/claude-pty-runner.unit.test.ts', 'test/plan-count-completion.test.ts', 'test/plan-count-dx-handoff.test.ts', 'test/fixtures/devex-handoff-n-call.json', 'test/helpers/pty-trust-dialog.ts', 'test/pty-trust-dialog.test.ts', 'test/helpers/plan-count-transcript.ts', 'test/helpers/plan-count-pending-exit.ts', 'test/plan-count-pending-exit.test.ts', 'test/plan-count-transcript.test.ts', 'test/helpers/plan-count-artifacts.ts', 'test/plan-count-artifacts.test.ts', 'test/helpers/eval-store.ts', 'test/helpers/plan-count-fixture.ts', 'test/plan-count-fixture.test.ts', 'test/plan-count-timeout.test.ts', 'test/plan-count-prerequisite-n.test.ts', 'test/fixtures/ceo-prerequisite-n-call.json', 'test/helpers/devex-count-fixture.ts', 'test/devex-count-fixture.test.ts', 'test/skill-e2e-plan-devex-finding-count.test.ts', 'test/fixtures/devex-review-l-calls.json', 'test/plan-count-checkbox.test.ts', 'test/fixtures/ceo-checkbox-l.screen.txt', 'test/fixtures/devex-review-n-calls.json', 'test/plan-count-empty-review.test.ts', 'test/fixtures/plan-count-design-questionless-report.md', 'test/devex-output-o.test.ts', 'test/fixtures/devex-review-o-calls.json', 'test/fixtures/devex-output-o-retry-call.json', 'test/devex-setup-remedy-o.test.ts', 'test/fixtures/devex-review-o-retry-calls.json', 'test/plan-count-dx-handoff-o.test.ts', 'test/fixtures/devex-handoff-o-call.json'],
'plan-ceo-finding-count': ['bin/gstack-config', 'bin/gstack-skill-start', 'bin/gstack-skill-end', 'plan-ceo-review/**', 'test/skill-ceo-section-ordering.test.ts', 'scripts/resolvers/preamble.ts', 'scripts/resolvers/preamble/generate-ask-user-format.ts', 'scripts/resolvers/preamble/generate-completion-status.ts', 'test/helpers/claude-pty-runner.ts', 'test/plan-count-native-input.test.ts', 'test/helpers/pty-screen.ts', 'test/pty-screen.test.ts', 'test/pty-screen-session.test.ts', 'test/fixtures/pty-screen/**', 'test/helpers/hermetic-skill-runtime.ts', 'test/hermetic-skill-runtime.test.ts', 'test/helpers/claude-pty-runner.unit.test.ts', 'test/plan-count-completion.test.ts', 'test/plan-count-dx-handoff.test.ts', 'test/fixtures/devex-handoff-n-call.json', 'test/helpers/ceo-completion-handoff.ts', 'test/ceo-completion-handoff.test.ts', 'test/fixtures/ceo-completion-handoff-calls.json', 'test/fixtures/ceo-completion-handoff-j-calls.json', 'test/fixtures/ceo-completion-handoff-k-calls.json', 'test/helpers/pty-trust-dialog.ts', 'test/pty-trust-dialog.test.ts', 'test/helpers/plan-count-transcript.ts', 'test/helpers/plan-count-pending-exit.ts', 'test/plan-count-pending-exit.test.ts', 'test/plan-count-transcript.test.ts', 'test/helpers/plan-count-artifacts.ts', 'test/plan-count-artifacts.test.ts', 'test/helpers/eval-store.ts', 'test/helpers/plan-count-fixture.ts', 'test/plan-count-fixture.test.ts', 'test/plan-count-timeout.test.ts', 'test/plan-count-navigation-r.test.ts', 'test/plan-count-prerequisite-n.test.ts', 'test/fixtures/ceo-prerequisite-n-call.json', 'test/skill-e2e-plan-ceo-finding-count.test.ts', 'test/ceo-completion-handoff-l.test.ts', 'test/fixtures/ceo-completion-handoff-l-calls.json', 'test/ceo-completion-handoff-m.test.ts', 'test/fixtures/ceo-completion-handoff-m-call.json', 'test/fixtures/devex-review-l-calls.json', 'test/plan-count-checkbox.test.ts', 'test/fixtures/ceo-checkbox-l.screen.txt', 'test/fixtures/ceo-handoff-n-calls.json', 'test/ceo-completion-handoff-o.test.ts', 'test/fixtures/ceo-completion-handoff-o-call.json', 'test/plan-count-empty-review.test.ts', 'test/fixtures/plan-count-design-questionless-report.md', 'test/plan-count-dx-handoff-o.test.ts', 'test/fixtures/devex-handoff-o-call.json', 'test/helpers/ceo-approach-pick.ts', 'test/ceo-approach-pick.test.ts', 'test/fixtures/ceo-approach-q-call.json', 'test/fixtures/ceo-approach-r-call.json', 'test/fixtures/ceo-approach-r-distinct-call.json', 'test/fixtures/ceo-approach-q-paired-call.json', 'test/fixtures/ceo-completion-handoff-q-call.json', 'test/fixtures/ceo-completion-handoff-r-calls.json'],
'plan-eng-finding-count': ['bin/gstack-config', 'bin/gstack-skill-start', 'bin/gstack-skill-end', 'plan-eng-review/**', 'scripts/resolvers/preamble.ts', 'scripts/resolvers/preamble/generate-ask-user-format.ts', 'scripts/resolvers/preamble/generate-completion-status.ts', 'test/helpers/claude-pty-runner.ts', 'test/plan-count-native-input.test.ts', 'test/helpers/pty-screen.ts', 'test/pty-screen.test.ts', 'test/pty-screen-session.test.ts', 'test/fixtures/pty-screen/**', 'test/helpers/hermetic-skill-runtime.ts', 'test/hermetic-skill-runtime.test.ts', 'test/helpers/claude-pty-runner.unit.test.ts', 'test/plan-count-completion.test.ts', 'test/plan-count-dx-handoff.test.ts', 'test/fixtures/devex-handoff-n-call.json', 'test/helpers/pty-trust-dialog.ts', 'test/pty-trust-dialog.test.ts', 'test/helpers/plan-count-transcript.ts', 'test/helpers/plan-count-pending-exit.ts', 'test/plan-count-pending-exit.test.ts', 'test/plan-count-transcript.test.ts', 'test/helpers/plan-count-artifacts.ts', 'test/plan-count-artifacts.test.ts', 'test/helpers/eval-store.ts', 'test/helpers/plan-count-fixture.ts', 'test/plan-count-fixture.test.ts', 'test/plan-count-timeout.test.ts', 'test/plan-count-navigation-r.test.ts', 'test/plan-count-prerequisite-n.test.ts', 'test/fixtures/ceo-prerequisite-n-call.json', 'test/skill-e2e-plan-eng-finding-count.test.ts', 'test/fixtures/devex-review-l-calls.json', 'test/plan-count-checkbox.test.ts', 'test/fixtures/ceo-checkbox-l.screen.txt', 'test/plan-count-empty-review.test.ts', 'test/fixtures/plan-count-design-questionless-report.md', 'test/plan-count-dx-handoff-o.test.ts', 'test/fixtures/devex-handoff-o-call.json'],
'plan-design-finding-count': ['bin/gstack-config', 'bin/gstack-skill-start', 'bin/gstack-skill-end', 'plan-design-review/**', 'scripts/resolvers/preamble.ts', 'scripts/resolvers/preamble/generate-ask-user-format.ts', 'scripts/resolvers/preamble/generate-completion-status.ts', 'test/helpers/claude-pty-runner.ts', 'test/plan-count-native-input.test.ts', 'test/helpers/pty-screen.ts', 'test/pty-screen.test.ts', 'test/pty-screen-session.test.ts', 'test/fixtures/pty-screen/**', 'test/helpers/hermetic-skill-runtime.ts', 'test/hermetic-skill-runtime.test.ts', 'test/helpers/claude-pty-runner.unit.test.ts', 'test/plan-count-completion.test.ts', 'test/plan-count-dx-handoff.test.ts', 'test/fixtures/devex-handoff-n-call.json', 'test/helpers/pty-trust-dialog.ts', 'test/pty-trust-dialog.test.ts', 'test/helpers/plan-count-transcript.ts', 'test/helpers/plan-count-pending-exit.ts', 'test/plan-count-pending-exit.test.ts', 'test/plan-count-transcript.test.ts', 'test/helpers/plan-count-artifacts.ts', 'test/plan-count-artifacts.test.ts', 'test/helpers/eval-store.ts', 'test/helpers/plan-count-fixture.ts', 'test/plan-count-fixture.test.ts', 'test/plan-count-timeout.test.ts', 'test/plan-count-navigation-r.test.ts', 'test/plan-count-prerequisite-n.test.ts', 'test/fixtures/ceo-prerequisite-n-call.json', 'test/helpers/design-count-review.ts', 'test/design-count-review.test.ts', 'test/fixtures/design-review-j-calls.json', 'test/helpers/design-count-outside.ts', 'test/design-count-outside.test.ts', 'test/skill-e2e-plan-design-finding-count.test.ts', 'test/fixtures/design-review-l-calls.json', 'test/design-completion-handoff.test.ts', 'test/fixtures/design-handoff-l-calls.json', 'test/fixtures/devex-review-l-calls.json', 'test/plan-count-checkbox.test.ts', 'test/fixtures/ceo-checkbox-l.screen.txt', 'test/fixtures/design-review-n-calls.json', 'test/design-completion-handoff-scored.test.ts', 'test/fixtures/design-handoff-n-calls.json', 'test/fixtures/design-handoff-q-calls.json', 'test/plan-count-empty-review.test.ts', 'test/fixtures/plan-count-design-questionless-report.md', 'test/plan-count-dx-handoff-o.test.ts', 'test/fixtures/devex-handoff-o-call.json'],
'plan-devex-finding-count': ['bin/gstack-config', 'bin/gstack-skill-start', 'bin/gstack-skill-end', 'plan-devex-review/**', 'scripts/resolvers/preamble.ts', 'scripts/resolvers/preamble/generate-ask-user-format.ts', 'scripts/resolvers/preamble/generate-completion-status.ts', 'test/helpers/claude-pty-runner.ts', 'test/plan-count-native-input.test.ts', 'test/helpers/pty-screen.ts', 'test/pty-screen.test.ts', 'test/pty-screen-session.test.ts', 'test/fixtures/pty-screen/**', 'test/helpers/hermetic-skill-runtime.ts', 'test/hermetic-skill-runtime.test.ts', 'test/helpers/claude-pty-runner.unit.test.ts', 'test/plan-count-completion.test.ts', 'test/plan-count-dx-handoff.test.ts', 'test/fixtures/devex-handoff-n-call.json', 'test/helpers/pty-trust-dialog.ts', 'test/pty-trust-dialog.test.ts', 'test/helpers/plan-count-transcript.ts', 'test/helpers/plan-count-pending-exit.ts', 'test/plan-count-pending-exit.test.ts', 'test/plan-count-transcript.test.ts', 'test/helpers/plan-count-artifacts.ts', 'test/plan-count-artifacts.test.ts', 'test/helpers/eval-store.ts', 'test/helpers/plan-count-fixture.ts', 'test/plan-count-fixture.test.ts', 'test/plan-count-timeout.test.ts', 'test/plan-count-navigation-r.test.ts', 'test/plan-count-prerequisite-n.test.ts', 'test/fixtures/ceo-prerequisite-n-call.json', 'test/helpers/devex-count-fixture.ts', 'test/devex-count-fixture.test.ts', 'test/skill-e2e-plan-devex-finding-count.test.ts', 'test/fixtures/dx-prerequisite-r-call.json', 'test/fixtures/devex-review-l-calls.json', 'test/plan-count-checkbox.test.ts', 'test/fixtures/ceo-checkbox-l.screen.txt', 'test/fixtures/devex-review-n-calls.json', 'test/plan-count-empty-review.test.ts', 'test/fixtures/plan-count-design-questionless-report.md', 'test/devex-output-o.test.ts', 'test/fixtures/devex-review-o-calls.json', 'test/fixtures/devex-output-o-retry-call.json', 'test/devex-setup-remedy-o.test.ts', 'test/fixtures/devex-review-o-retry-calls.json', 'test/plan-count-dx-handoff-o.test.ts', 'test/fixtures/devex-handoff-o-call.json'],
// Gate-tier reviewCount-floor counterparts. Catch the May 2026 transcript
// bug (model wrote a plan-mode plan and ExitPlanMode'd without firing any
@@ -180,8 +180,8 @@ export const E2E_TOUCHFILES: Record<string, string[]> = {
// a model fires one AUQ then batches the rest into a "## Decisions to
// confirm" plan write. runPlanSkillFloorCheck cannot detect that shape
// (it exits on first AUQ); runPlanSkillCounting can.
'plan-eng-multi-finding-batching': ['bin/gstack-config', 'bin/gstack-skill-start', 'bin/gstack-skill-end', 'plan-eng-review/**', 'scripts/resolvers/preamble.ts', 'scripts/resolvers/preamble/generate-ask-user-format.ts', 'scripts/resolvers/preamble/generate-completion-status.ts', 'scripts/resolvers/review.ts', 'test/helpers/claude-pty-runner.ts', 'test/plan-count-native-input.test.ts', 'test/helpers/pty-screen.ts', 'test/pty-screen.test.ts', 'test/pty-screen-session.test.ts', 'test/fixtures/pty-screen/**', 'test/helpers/hermetic-skill-runtime.ts', 'test/hermetic-skill-runtime.test.ts', 'test/helpers/claude-pty-runner.unit.test.ts', 'test/helpers/pty-trust-dialog.ts', 'test/pty-trust-dialog.test.ts', 'test/helpers/plan-count-transcript.ts', 'test/helpers/plan-count-pending-exit.ts', 'test/plan-count-pending-exit.test.ts', 'test/plan-count-transcript.test.ts', 'test/helpers/plan-count-artifacts.ts', 'test/plan-count-artifacts.test.ts', 'test/helpers/eval-store.ts', 'test/helpers/plan-count-fixture.ts', 'test/plan-count-fixture.test.ts', 'test/plan-count-timeout.test.ts', 'test/plan-count-prerequisite-n.test.ts', 'test/fixtures/ceo-prerequisite-n-call.json', 'test/fixtures/forcing-finding-seeds.ts', 'test/skill-e2e-plan-eng-multi-finding-batching.test.ts', 'test/plan-count-checkbox.test.ts', 'test/fixtures/ceo-checkbox-l.screen.txt'],
'plan-ceo-split-overflow': ['bin/gstack-config', 'plan-ceo-review/**', 'scripts/resolvers/preamble.ts', 'scripts/resolvers/preamble/generate-ask-user-format.ts', 'bin/gstack-question-preference', 'test/helpers/claude-pty-runner.ts', 'test/plan-count-native-input.test.ts', 'test/helpers/pty-screen.ts', 'test/pty-screen.test.ts', 'test/pty-screen-session.test.ts', 'test/fixtures/pty-screen/**', 'test/helpers/hermetic-skill-runtime.ts', 'test/hermetic-skill-runtime.test.ts', 'test/helpers/claude-pty-runner.unit.test.ts', 'test/helpers/pty-trust-dialog.ts', 'test/pty-trust-dialog.test.ts', 'test/helpers/plan-count-transcript.ts', 'test/helpers/plan-count-pending-exit.ts', 'test/plan-count-pending-exit.test.ts', 'test/plan-count-transcript.test.ts', 'test/helpers/plan-count-artifacts.ts', 'test/plan-count-artifacts.test.ts', 'test/helpers/eval-store.ts', 'test/helpers/plan-count-fixture.ts', 'test/plan-count-fixture.test.ts', 'test/plan-count-timeout.test.ts', 'test/plan-count-prerequisite-n.test.ts', 'test/fixtures/ceo-prerequisite-n-call.json', 'test/fixtures/forcing-finding-seeds.ts', 'test/skill-e2e-plan-ceo-split-overflow.test.ts', 'test/plan-count-checkbox.test.ts', 'test/fixtures/ceo-checkbox-l.screen.txt'],
'plan-eng-multi-finding-batching': ['bin/gstack-config', 'bin/gstack-skill-start', 'bin/gstack-skill-end', 'plan-eng-review/**', 'scripts/resolvers/preamble.ts', 'scripts/resolvers/preamble/generate-ask-user-format.ts', 'scripts/resolvers/preamble/generate-completion-status.ts', 'scripts/resolvers/review.ts', 'test/helpers/claude-pty-runner.ts', 'test/plan-count-native-input.test.ts', 'test/helpers/pty-screen.ts', 'test/pty-screen.test.ts', 'test/pty-screen-session.test.ts', 'test/fixtures/pty-screen/**', 'test/helpers/hermetic-skill-runtime.ts', 'test/hermetic-skill-runtime.test.ts', 'test/helpers/claude-pty-runner.unit.test.ts', 'test/helpers/pty-trust-dialog.ts', 'test/pty-trust-dialog.test.ts', 'test/helpers/plan-count-transcript.ts', 'test/helpers/plan-count-pending-exit.ts', 'test/plan-count-pending-exit.test.ts', 'test/plan-count-transcript.test.ts', 'test/helpers/plan-count-artifacts.ts', 'test/plan-count-artifacts.test.ts', 'test/helpers/eval-store.ts', 'test/helpers/plan-count-fixture.ts', 'test/plan-count-fixture.test.ts', 'test/plan-count-timeout.test.ts', 'test/plan-count-navigation-r.test.ts', 'test/plan-count-prerequisite-n.test.ts', 'test/fixtures/ceo-prerequisite-n-call.json', 'test/fixtures/forcing-finding-seeds.ts', 'test/skill-e2e-plan-eng-multi-finding-batching.test.ts', 'test/plan-count-checkbox.test.ts', 'test/fixtures/ceo-checkbox-l.screen.txt'],
'plan-ceo-split-overflow': ['bin/gstack-config', 'plan-ceo-review/**', 'scripts/resolvers/preamble.ts', 'scripts/resolvers/preamble/generate-ask-user-format.ts', 'bin/gstack-question-preference', 'test/helpers/claude-pty-runner.ts', 'test/plan-count-native-input.test.ts', 'test/helpers/pty-screen.ts', 'test/pty-screen.test.ts', 'test/pty-screen-session.test.ts', 'test/fixtures/pty-screen/**', 'test/helpers/hermetic-skill-runtime.ts', 'test/hermetic-skill-runtime.test.ts', 'test/helpers/claude-pty-runner.unit.test.ts', 'test/helpers/pty-trust-dialog.ts', 'test/pty-trust-dialog.test.ts', 'test/helpers/plan-count-transcript.ts', 'test/helpers/plan-count-pending-exit.ts', 'test/plan-count-pending-exit.test.ts', 'test/plan-count-transcript.test.ts', 'test/helpers/plan-count-artifacts.ts', 'test/plan-count-artifacts.test.ts', 'test/helpers/eval-store.ts', 'test/helpers/plan-count-fixture.ts', 'test/plan-count-fixture.test.ts', 'test/plan-count-timeout.test.ts', 'test/plan-count-navigation-r.test.ts', 'test/plan-count-prerequisite-n.test.ts', 'test/fixtures/ceo-prerequisite-n-call.json', 'test/fixtures/forcing-finding-seeds.ts', 'test/skill-e2e-plan-ceo-split-overflow.test.ts', 'test/plan-count-checkbox.test.ts', 'test/fixtures/ceo-checkbox-l.screen.txt'],
'brain-privacy-gate': ['bin/gstack-skill-start', 'bin/gstack-skill-end', 'scripts/resolvers/preamble/generate-brain-sync-block.ts', 'scripts/resolvers/preamble.ts', 'bin/gstack-brain-sync', 'bin/gstack-artifacts-init', 'bin/gstack-config', 'test/helpers/agent-sdk-runner.ts', 'test/skill-e2e-brain-privacy-gate.test.ts'],
// /setup-gbrain Path 4 (Remote MCP) — happy + bad-token end-to-end via
@@ -361,7 +361,7 @@ export const E2E_TOUCHFILES: Record<string, string[]> = {
// Autoplan
'autoplan-dual-voice': ['scripts/resolvers/composition.ts', 'test/autoplan-review-discovery.test.ts', 'test/autoplan-phase-order.test.ts', 'autoplan/**', 'codex/**', 'bin/gstack-codex-probe', 'scripts/resolvers/review.ts', 'scripts/resolvers/design.ts', 'test/skill-e2e-autoplan-dual-voice.test.ts', 'bin/gstack-autoplan-snapshot.ts', 'test/autoplan-snapshot.test.ts'],
'autoplan-dual-voice': ['scripts/resolvers/composition.ts', 'test/autoplan-review-discovery.test.ts', 'test/autoplan-phase-order.test.ts', 'autoplan/**', 'codex/**', 'bin/gstack-codex-probe', 'scripts/resolvers/review.ts', 'scripts/resolvers/design.ts', 'test/skill-e2e-autoplan-dual-voice.test.ts', 'bin/gstack-autoplan-snapshot.ts', 'test/autoplan-snapshot.test.ts', 'test/autoplan-init.test.ts'],
// Multi-provider benchmark adapters — live API smoke against real claude/codex/gemini CLIs
'benchmark-providers-live': ['bin/gstack-model-benchmark', 'test/helpers/providers/**', 'test/helpers/benchmark-runner.ts', 'test/helpers/pricing.ts', 'test/skill-e2e-benchmark-providers.test.ts'],
+110
View File
@@ -0,0 +1,110 @@
import { describe, expect, test } from 'bun:test';
import { capturePlanCountQuestion, nativePlanCallFingerprint, planCountPrerequisitePick, planCountQuestionInput } from './helpers/claude-pty-runner';
import { pickCeoCountQuestion } from './helpers/ceo-approach-pick';
import type { NativePlanQuestionCall } from './helpers/plan-count-transcript';
import paired from './fixtures/ceo-approach-r-call.json';
import distinct from './fixtures/ceo-approach-r-distinct-call.json';
import prerequisite from './fixtures/dx-prerequisite-r-call.json';
function pending(source: NativePlanQuestionCall): NativePlanQuestionCall {
const call = structuredClone(source);
call.answered = false;
delete call.answers;
delete call.unansweredQuestionIndices;
delete call.answeredAt;
return call;
}
function frame(call: NativePlanQuestionCall) {
const q = call.questions[0]!;
const visible = `☐ ${q.header}\n${q.question}\n${q.options.map((o, i) => `${i ? ' ' : '❯'} ${i + 1}. ${o.label}`).join('\n')}\nEnter to select · ↑/↓ to navigate · Esc to cancel`;
const active = capturePlanCountQuestion(visible, new Set(), 0, true, call)!;
return { visible, active, routing: nativePlanCallFingerprint(call, 0, true) };
}
describe('captured R planning navigation', () => {
test('distinct CEO selects its third recommended approach without seed-word matching', () => {
const call = pending(distinct as NativePlanQuestionCall);
const { visible, active, routing } = frame(call);
expect(distinct.answers[distinct.questions[0]!.question]).toBe(distinct.questions[0]!.options[0]!.label);
const pick = pickCeoCountQuestion(routing, active) ?? 1;
expect(pick).toBe(3);
expect(planCountQuestionInput(visible, active, pick)).toBe('3');
});
test('paired CEO follows the offered recommendation instead of accepting vague assertions', () => {
const call = pending(paired as NativePlanQuestionCall);
const { visible, active, routing } = frame(call);
expect(active.nativeCall).toBe(call);
expect(paired.answers[paired.questions[0]!.question]).toBe(paired.questions[0]!.options[0]!.label);
const pick = pickCeoCountQuestion(routing, active) ?? 1;
expect(pick).toBe(2);
expect(planCountQuestionInput(visible, active, pick)).toBe('2');
});
test('DX declines its optional office-hours detour using the full native option meaning', () => {
const call = pending(prerequisite as NativePlanQuestionCall);
const { visible, active, routing } = frame(call);
expect(active.nativeCall).toBe(call);
expect(prerequisite.answers[prerequisite.questions[0]!.question]).toBe(prerequisite.questions[0]!.options[0]!.label);
const pick = planCountPrerequisitePick(routing, active) ?? 1;
expect(pick).toBe(2);
expect(planCountQuestionInput(visible, active, pick)).toBe('2');
});
test('CEO routing id and selector wording vary independently of option content and order', () => {
for (const id of ['plan-ceo-approach', 'plan-ceo-review-approach', 'plan-ceo-approach-selection', 'plan-ceo-review-approach-selection']) {
for (const verb of ['use', 'follow']) {
const call = pending(paired as NativePlanQuestionCall);
call.questions[0]!.question = `Which implementation approach should this plan ${verb}? <gstack-qid:${id}>`;
call.questions[0]!.options = [{ label: 'Existing renderer (Recommended)' }, { label: 'Custom renderer' }];
const { active, routing } = frame(call);
expect(pickCeoCountQuestion(routing, active)).toBe(1);
}
}
});
test('short prerequisite labels require the current single native question and affirmative review action', () => {
for (const change of [
(c: NativePlanQuestionCall) => { c.questions[0]!.options[1]!.description = ''; },
(c: NativePlanQuestionCall) => { c.questions[0]!.options[1]!.description = 'Do not proceed with standard DX POLISH review.'; },
(c: NativePlanQuestionCall) => { c.questions[0]!.options[1]!.description = 'Accept the finding and proceed with standard DX POLISH review.'; },
(c: NativePlanQuestionCall) => { c.questions[0]!.options[1]!.description = 'Accept this security finding. Proceed with standard DX POLISH review.'; },
(c: NativePlanQuestionCall) => { c.questions[0]!.options[1]!.description = 'Proceed with standard DX POLISH review after running /office-hours first.'; },
(c: NativePlanQuestionCall) => { c.questions[0]!.options[1]!.description = 'Proceed with standard DX POLISH review? No, run /office-hours first.'; },
(c: NativePlanQuestionCall) => { c.questions[0]!.options[1]!.description = 'Proceed with standard DX POLISH review. Accept the security risk.'; },
(c: NativePlanQuestionCall) => { c.questions[0]!.options[1]!.description = 'Plan scope is already precise. Proceed with standard DX POLISH review if the tests pass.'; },
(c: NativePlanQuestionCall) => { c.questions[0]!.options[1]!.label = 'Skip'; },
(c: NativePlanQuestionCall) => { c.questions[0]!.options.push({ label: 'Accept this security finding' }); },
(c: NativePlanQuestionCall) => { c.questions[0]!.multiSelect = true; },
(c: NativePlanQuestionCall) => { c.questions.push(structuredClone(c.questions[0]!)); },
(c: NativePlanQuestionCall) => { c.answered = true; },
(c: NativePlanQuestionCall) => { c.failed = true; },
]) {
const call = pending(prerequisite as NativePlanQuestionCall);
change(call);
const fp = nativePlanCallFingerprint(call, 0, true);
expect(planCountPrerequisitePick(fp)).toBeNull();
}
const call = pending(prerequisite as NativePlanQuestionCall);
const { active, routing, visible } = frame(call);
expect(planCountPrerequisitePick(routing, { ...active, preReview: false })).toBeNull();
expect(planCountPrerequisitePick(routing, { ...active, signature: 'stale:question' })).toBeNull();
expect(planCountPrerequisitePick(routing, { ...active, nativeQuestionIndex: 1 })).toBeNull();
expect(planCountPrerequisitePick(routing, { ...active, options: active.options.slice().reverse() })).toBeNull();
const visibleOnly = capturePlanCountQuestion(visible, new Set(), 0, true)!;
expect(visibleOnly.nativeCall).toBeUndefined();
expect(planCountPrerequisitePick(routing, visibleOnly)).toBeNull();
call.questions[0]!.options.reverse();
const reordered = frame(call);
expect(planCountPrerequisitePick(reordered.routing, reordered.active)).toBe(1);
});
test('short labels allow only a benign plan-scope premise plus the unconditional review action', () => {
for (const description of ['Proceed with standard review.', 'Proceed with standard DX review', 'The plan is clear. Proceed with standard DX POLISH review.', 'Plan scope is already precise. Proceed with standard DX POLISH review.']) {
const call = pending(prerequisite as NativePlanQuestionCall);
call.questions[0]!.options[1]!.description = description;
const { active, routing } = frame(call);
expect(planCountPrerequisitePick(routing, active)).toBe(2);
}
});
});
+11
View File
@@ -145,6 +145,16 @@ setInterval(()=>{},1000);
const mode=process.env.BOUNDARY_MODE;
let start=0, reads=0;
const log=(event,extra={})=>fs.appendFileSync(process.env.BOUNDARY_EVENTS,JSON.stringify({event,at:Date.now(),invocation:1,...extra})+'\n');
if(mode==='boot') {
const sleep=Bun.sleep.bind(Bun);
Bun.sleep=async ms=>{
if(typeof ms==='number' && ms>1000 && ms<8000) {
log('early-clipped-wake',{requested:ms});
return sleep(Math.max(0,ms-250));
}
return sleep(ms);
};
}
if(mode==='screen') mock.module(screenModule,()=>({createPtyScreen:async(...args)=>{
const screen=await originalScreen(...args);
return {...screen,read:async()=>{reads++; await Bun.sleep(Math.max(0,start+13200-Date.now()));return screen.read();}};
@@ -176,6 +186,7 @@ log('returned',{outcome:observation.outcome,elapsed:Date.now()-start,reads,fixtu
expect(returned.elapsed).toBeLessThan(mode==='boot'?12000:18000);
const input=events.filter(e=>e.event==='input').map(e=>e.data).join('');
expect(input).toBe(mode==='boot'?'':mode==='screen'?'/plan-design-review\r':'/plan-design-review\r2');
if(mode==='boot') expect(events.some(e=>e.event==='early-clipped-wake')).toBe(true);
if(mode==='screen') { expect(returned.reads).toBe(1);expect(events.some(e=>e.event==='picker')).toBe(false); }
expect(ownedFake(events.find(e=>e.event==='ready'),fake)).toBe(false);
}));