mirror of
https://github.com/garrytan/gstack.git
synced 2026-10-31 22:14:25 +01:00
fix(plan-ceo-review): Step 0E mode handoff runs gstack-ceo-mode-handoff so the selected mode is printed, not only composed
This commit is contained in:
1 parent
b33caad493
commit
e3546d9ac4
11 files changed
+222
-8
No files matched your search
Executable
+42
@@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env bash
|
||||
# gstack-ceo-mode-handoff — print /plan-ceo-review's Step 0E mode handoff line.
|
||||
#
|
||||
# Usage: gstack-ceo-mode-handoff <MODE> [--auto] [--decisions "<rows>"]
|
||||
# MODE: HOLD SCOPE | SCOPE EXPANSION | SELECTIVE EXPANSION | SCOPE REDUCTION
|
||||
# (underscores accepted for spaces)
|
||||
# --auto the mode came from a saved AUTO_DECIDE preference
|
||||
# --decisions governing approved rows; defaults to "none"
|
||||
#
|
||||
# The handoff used to be chat-only, and the model often composed it in its
|
||||
# reasoning without ever sending it. Running this prints the line as a tool
|
||||
# result, so the selected mode is observable before the review continues.
|
||||
set -euo pipefail
|
||||
|
||||
mode="${1:-}"
|
||||
[ "$#" -gt 0 ] && shift
|
||||
auto=0
|
||||
decisions="none"
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case "$1" in
|
||||
--auto) auto=1; shift ;;
|
||||
--decisions)
|
||||
[ "$#" -ge 2 ] || { echo "gstack-ceo-mode-handoff: --decisions needs a value" >&2; exit 2; }
|
||||
decisions="$2"; shift 2 ;;
|
||||
*) echo "gstack-ceo-mode-handoff: unknown argument: $1" >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
mode="${mode//_/ }"
|
||||
case "$mode" in
|
||||
"HOLD SCOPE"|"SCOPE EXPANSION"|"SELECTIVE EXPANSION"|"SCOPE REDUCTION") ;;
|
||||
*) echo "gstack-ceo-mode-handoff: unknown mode: ${mode:-<empty>}" >&2; exit 2 ;;
|
||||
esac
|
||||
decisions="${decisions//$'\n'/ }"
|
||||
decisions="${decisions%.}"
|
||||
[ -n "${decisions// /}" ] || decisions="none"
|
||||
|
||||
if [ "$auto" = 1 ]; then
|
||||
echo "Auto-decided review mode → $mode (your preference). Change with /plan-tune. Approved decisions: $decisions."
|
||||
else
|
||||
echo "Mode: $mode; approved decisions: $decisions."
|
||||
fi
|
||||
@@ -975,7 +975,7 @@ Follow the preamble's session rules; `CONDUCTOR_SESSION: true` changes transport
|
||||
wins. When `QUESTION_TUNING: true`, include `<gstack-qid:plan-ceo-review-mode>`.
|
||||
These modes differ in kind, not coverage; do NOT score completeness.
|
||||
|
||||
4. **Mode handoff:** After selection, send brief chat before tools or further questions: the mode's application and rationale; every governing approved row's ID, answer reference and accepted scope. Keep rows separate. Begin with the exact matching line below:
|
||||
4. **Mode handoff:** After selection, before other tools or further questions, run `~/.claude/skills/gstack/bin/gstack-ceo-mode-handoff "<selected mode>" --decisions "<rows or none>"` (add `--auto` for `plan-ceo-review-mode: AUTO_DECIDE`). It prints the matching line below. Then send brief chat beginning with that line: the mode's application and rationale; every governing approved row's ID, answer reference and accepted scope. Keep rows separate.
|
||||
- `plan-ceo-review-mode: AUTO_DECIDE`: `Auto-decided review mode → <selected mode> (your preference). Change with /plan-tune. Approved decisions: <rows or none>. <Application and rationale>.`
|
||||
- Other selections: `Mode: <selected mode>; approved decisions: <rows or none>. <Application and rationale>.`
|
||||
|
||||
|
||||
@@ -407,7 +407,7 @@ Follow the preamble's session rules; `CONDUCTOR_SESSION: true` changes transport
|
||||
wins. When `QUESTION_TUNING: true`, include `<gstack-qid:plan-ceo-review-mode>`.
|
||||
These modes differ in kind, not coverage; do NOT score completeness.
|
||||
|
||||
4. **Mode handoff:** After selection, send brief chat before tools or further questions: the mode's application and rationale; every governing approved row's ID, answer reference and accepted scope. Keep rows separate. Begin with the exact matching line below:
|
||||
4. **Mode handoff:** After selection, before other tools or further questions, run `~/.claude/skills/gstack/bin/gstack-ceo-mode-handoff "<selected mode>" --decisions "<rows or none>"` (add `--auto` for `plan-ceo-review-mode: AUTO_DECIDE`). It prints the matching line below. Then send brief chat beginning with that line: the mode's application and rationale; every governing approved row's ID, answer reference and accepted scope. Keep rows separate.
|
||||
- `plan-ceo-review-mode: AUTO_DECIDE`: `Auto-decided review mode → <selected mode> (your preference). Change with /plan-tune. Approved decisions: <rows or none>. <Application and rationale>.`
|
||||
- Other selections: `Mode: <selected mode>; approved decisions: <rows or none>. <Application and rationale>.`
|
||||
|
||||
|
||||
@@ -480,7 +480,6 @@
|
||||
"test/skill-e2e-plan-decision-classification.test.ts\tTS2769\tNo overload matches this call. The last overload gave the following error. Argument of type 'number | undefined' is not assignable to parameter of type 'number'. Type 'undefined' is not assignable to type 'number'.": 1,
|
||||
"test/skill-e2e-shared-libs-paths.test.ts\tTS2769\tNo overload matches this call. The last overload gave the following error. Argument of type 'unknown' is not assignable to parameter of type 'string'.": 1,
|
||||
"test/skill-e2e-ship-docsync.test.ts\tTS2345\tArgument of type 'EvalCollector | null' is not assignable to parameter of type 'EvalCollector'. Type 'null' is not assignable to type 'EvalCollector'.": 9,
|
||||
"test/skill-e2e-third-party-actions.test.ts\tTS2345\tArgument of type '{ maxTurns: 8; allowedTools: readonly ['Read', 'Bash']; timeout: 240000; runId: string; env: { PATH: string; }; prompt: string; workingDirectory: string; testName: string; }' is not assignable to parameter of type '{ prompt: string; workingDirectory: string; maxTurns?: number | undefined; appendSystemPrompt?: string | undefined; completionReserveMs?: number | undefined; allowedTools?: string[] | undefined; ... 9 more ...; nativeLifecycle?: { ...; } | undefined; }'. Types of property 'allowedTools' are incompatible. The type 'readonly [\"Read\", \"Bash\"]' is 'readonly' and cannot be assigned to the mutable type 'string[]'.": 5,
|
||||
"test/skill-routing-e2e.test.ts\tTS2345\tArgument of type '\"e2e-routing\"' is not assignable to parameter of type '\"e2e\" | \"llm-judge\"'.": 1,
|
||||
"test/strict-output.test.ts\tTS2739\tType '{ failedTests: number; unhandledBetweenTests: number; terminalFileCounts: number[]; }' is missing the following properties from type 'BunTestOutputSummary': terminalTestCounts, skippedTests, passedTests": 5,
|
||||
"test/test-free-shards.test.ts\tTS2345\tArgument of type 'number | ReadableStream<Uint8Array<ArrayBuffer>> | undefined' is not assignable to parameter of type 'BodyInit | null | undefined'. Type 'number' is not assignable to type 'BodyInit | null | undefined'.": 2,
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
import { describe, expect, test } from 'bun:test';
|
||||
import * as fs from 'node:fs';
|
||||
import * as os from 'node:os';
|
||||
import * as path from 'node:path';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { ceoModeHandoffs, findNativeAutoDecision } from './helpers/native-auto-decide';
|
||||
import { hasNativePostAnswerCeoPosture } from './helpers/ceo-mode-option';
|
||||
import { readPlanCountTranscript, type NativePublicToolEvent } from './helpers/plan-count-transcript';
|
||||
import expansionCapture from './fixtures/ceo-expansion-auq-ac.json';
|
||||
|
||||
const ROOT = path.resolve(import.meta.dir, '..');
|
||||
const BIN = path.join(ROOT, 'bin', 'gstack-ceo-mode-handoff');
|
||||
const CMD = '"$HOME/.claude/skills/gstack/bin/gstack-ceo-mode-handoff"';
|
||||
const AUTO_LINE = 'Auto-decided review mode → HOLD SCOPE (your preference). Change with /plan-tune. Approved decisions: none.';
|
||||
|
||||
function run(...args: string[]) {
|
||||
return spawnSync(BIN, args, { encoding: 'utf8', timeout: 10_000 });
|
||||
}
|
||||
|
||||
function handoff(sessionId: string, at: number, command: string, content: string, id = 'toolu_handoff'): NativePublicToolEvent[] {
|
||||
return [
|
||||
{ sessionId, toolUseId: id, kind: 'use', name: 'Bash', timestamp: new Date(at).toISOString(), input: { command } },
|
||||
{ sessionId, toolUseId: id, kind: 'result', timestamp: new Date(at + 500).toISOString(), content, isError: false },
|
||||
];
|
||||
}
|
||||
|
||||
describe('gstack-ceo-mode-handoff', () => {
|
||||
test('prints the exact Step 0E handoff line for asked and auto-decided modes', () => {
|
||||
expect(run('HOLD_SCOPE', '--auto').stdout).toBe(`${AUTO_LINE}\n`);
|
||||
expect(run('SCOPE EXPANSION', '--decisions', 'D1 (A): keep the CLI.').stdout)
|
||||
.toBe('Mode: SCOPE EXPANSION; approved decisions: D1 (A): keep the CLI.\n');
|
||||
expect(run('SCOPE REDUCTION', '--decisions', ' ').stdout).toBe('Mode: SCOPE REDUCTION; approved decisions: none.\n');
|
||||
});
|
||||
|
||||
test('rejects unknown modes and arguments without printing a handoff', () => {
|
||||
for (const args of [['BIG'], [], ['HOLD SCOPE', '--decisions'], ['HOLD SCOPE', '--force']]) {
|
||||
const result = run(...args);
|
||||
expect(result.status).toBe(2);
|
||||
expect(result.stdout).toBe('');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('native handoff evidence', () => {
|
||||
const at = Date.parse('2026-10-04T06:00:00.000Z');
|
||||
|
||||
test('accepts only a successful literal invocation whose output matches its arguments', () => {
|
||||
expect(ceoModeHandoffs(handoff('s', at, `${CMD} "HOLD SCOPE" --auto`, AUTO_LINE), 's'))
|
||||
.toMatchObject([{ option: 'HOLD SCOPE', auto: true, line: AUTO_LINE }]);
|
||||
const rejected: Array<[string, string, (events: NativePublicToolEvent[]) => void]> = [
|
||||
['wrong output', `${CMD} "HOLD SCOPE" --auto`, events => { events[1]!.content = AUTO_LINE.replace('HOLD SCOPE', 'SCOPE EXPANSION'); }],
|
||||
['missing --auto', `${CMD} "HOLD SCOPE"`, () => {}],
|
||||
['failed result', `${CMD} "HOLD SCOPE" --auto`, events => { events[1]!.isError = true; }],
|
||||
['echoed line', `echo "${AUTO_LINE}"`, () => {}],
|
||||
['chained command', `${CMD} "HOLD SCOPE" --auto; true`, () => {}],
|
||||
['relative path', 'bin/gstack-ceo-mode-handoff "HOLD SCOPE" --auto', () => {}],
|
||||
['foreign session', `${CMD} "HOLD SCOPE" --auto`, events => { events[1]!.sessionId = 'other'; }],
|
||||
['result before use', `${CMD} "HOLD SCOPE" --auto`, events => { events[1]!.timestamp = new Date(at - 1).toISOString(); }],
|
||||
];
|
||||
for (const [name, command, mutate] of rejected) {
|
||||
const events = handoff('s', at, command, AUTO_LINE);
|
||||
mutate(events);
|
||||
expect(ceoModeHandoffs(events, 's'), name).toEqual([]);
|
||||
}
|
||||
});
|
||||
|
||||
test('an auto-decided handoff stands in for the unsent annotation, unless later withdrawn', () => {
|
||||
const captured = JSON.parse(fs.readFileSync(path.join(import.meta.dir, 'fixtures/native-auto-decide-ag.json'), 'utf8'));
|
||||
const attempt = () => {
|
||||
const f = structuredClone(captured.attempts[0]);
|
||||
// Census 37179171083 shape: the decision was logged but the line never reached chat.
|
||||
const message = f.transcript.assistantMessages.find((m: any) => m.text.includes('Auto-decided'));
|
||||
message.text = 'Continuing with the HOLD SCOPE review.';
|
||||
return f;
|
||||
};
|
||||
const verdict = (f: any) => findNativeAutoDecision(f.transcript, f.tools, f.options);
|
||||
const unsent = attempt();
|
||||
expect(verdict(unsent)).toBeNull();
|
||||
const sessionId = unsent.options.sessionId;
|
||||
const after = Date.parse('2026-09-10T02:19:00.000Z');
|
||||
const sent = attempt();
|
||||
sent.tools.push(...handoff(sessionId, after, `${CMD} "HOLD SCOPE" --auto`, AUTO_LINE));
|
||||
expect(verdict(sent)).toMatchObject({ option: 'HOLD SCOPE', annotation: AUTO_LINE });
|
||||
const asked = attempt();
|
||||
asked.tools.push(...handoff(sessionId, after, `${CMD} "HOLD SCOPE"`, 'Mode: HOLD SCOPE; approved decisions: none.'));
|
||||
expect(verdict(asked)).toBeNull();
|
||||
const withdrawn = attempt();
|
||||
withdrawn.tools.push(...handoff(sessionId, after, `${CMD} "HOLD SCOPE" --auto`, AUTO_LINE));
|
||||
withdrawn.transcript.assistantMessages.push({ sessionId, timestamp: '2026-09-10T02:19:05.000Z', text: 'Correction: I withdraw this auto-decision.' });
|
||||
expect(verdict(withdrawn)).toBeNull();
|
||||
});
|
||||
|
||||
test('an answered mode handoff establishes that mode posture, and only that mode', () => {
|
||||
const records = structuredClone(expansionCapture.records);
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ceo-mode-handoff-'));
|
||||
const project = path.join(root, 'projects', 'fixture');
|
||||
fs.mkdirSync(project, { recursive: true });
|
||||
fs.writeFileSync(path.join(project, `${records[0]!.sessionId}.jsonl`), records.map(r => JSON.stringify(r)).join('\n') + '\n');
|
||||
let transcript;
|
||||
try { transcript = readPlanCountTranscript(root, records[0]!.cwd, () => {}); }
|
||||
finally { fs.rmSync(root, { recursive: true, force: true }); }
|
||||
const posture = /\b(expansion|10x|delight|dream|cathedral|opt[\s-]?in)\b/i;
|
||||
const selectedAt = Date.parse(expansionCapture.provenance.testSelectionLowerBound.at);
|
||||
const mode = transcript.calls.find(call => call.toolUseId === expansionCapture.modeToolUseId)!;
|
||||
const answeredAt = Date.parse(mode.answeredAt!);
|
||||
const check = (events: NativePublicToolEvent[]) =>
|
||||
hasNativePostAnswerCeoPosture(transcript!, 'SCOPE EXPANSION', posture, selectedAt, events);
|
||||
expect(check([])).toBe(false);
|
||||
const line = 'Mode: SCOPE EXPANSION; approved decisions: none.';
|
||||
expect(check(handoff(mode.sessionId, answeredAt + 1000, `${CMD} "SCOPE EXPANSION"`, line))).toBe(true);
|
||||
expect(check(handoff(mode.sessionId, answeredAt - 5000, `${CMD} "SCOPE EXPANSION"`, line))).toBe(false);
|
||||
expect(check(handoff(mode.sessionId, answeredAt + 1000, `${CMD} "HOLD SCOPE"`, 'Mode: HOLD SCOPE; approved decisions: none.'))).toBe(false);
|
||||
expect(check(handoff(mode.sessionId, answeredAt + 1000, `${CMD} "SCOPE EXPANSION" --auto`,
|
||||
AUTO_LINE.replace('HOLD SCOPE', 'SCOPE EXPANSION')))).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -73,7 +73,8 @@ test('source and both isolated host renders bind the shared check, marker and lo
|
||||
expect(s.indexOf('3. Resolve that recommendation')).toBeGreaterThanOrEqual(0);
|
||||
expect(s.indexOf('4. **Mode handoff:**')).toBeGreaterThan(s.indexOf('3. Resolve that recommendation'));
|
||||
expect(handoff).toContain('After selection');
|
||||
expect(handoff).toContain('send brief chat before tools or further questions');
|
||||
expect(handoff).toMatch(/before other tools or further questions, run `[^`]*\/bin\/gstack-ceo-mode-handoff /);
|
||||
expect(handoff).toContain('Then send brief chat beginning with that line');
|
||||
expect(s.slice(0,s.indexOf('4. **Mode handoff:**'))).not.toMatch(/\blog (?:with|that ID)\b/);
|
||||
expect(handoff.indexOf('Record mode provenance after the handoff')).toBeGreaterThan(handoff.indexOf('- Other selections:'));
|
||||
expect(handoff.indexOf("Follow the selected mode's route:")).toBeGreaterThan(handoff.indexOf('Record mode provenance after the handoff'));
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
type AskUserQuestionFingerprint,
|
||||
} from './claude-pty-runner';
|
||||
import type { NativePlanQuestionCall, NativePublicToolEvent, PlanCountTranscript } from './plan-count-transcript';
|
||||
import { ceoModeHandoffs } from './native-auto-decide';
|
||||
|
||||
type CeoMode = 'HOLD SCOPE' | 'SCOPE EXPANSION' | 'SELECTIVE EXPANSION' | 'SCOPE REDUCTION';
|
||||
|
||||
@@ -655,6 +656,10 @@ export function hasNativePostAnswerCeoPosture(
|
||||
const selected = nativeCeoModeAnswer(transcript, targetMode, selectionStartedAt);
|
||||
if (!selected) return false;
|
||||
const answeredAt = Date.parse(selected.answeredAt!);
|
||||
// The handoff helper prints the answered mode as a tool result; it applies
|
||||
// the selected posture exactly as the same line sent as chat would.
|
||||
if (ceoModeHandoffs(publicTools, selected.sessionId).some(h => !h.auto && h.option === targetMode &&
|
||||
Date.parse(h.timestamp) > answeredAt && Date.parse(h.timestamp) <= Date.now() && posture.test(h.line))) return true;
|
||||
return transcript.assistantMessages.some(message => {
|
||||
if (message.sessionId !== selected.sessionId || Date.parse(message.timestamp) <= answeredAt) return false;
|
||||
return hasNativePostureProse(message.text, posture) ||
|
||||
|
||||
@@ -229,6 +229,34 @@ const modeNames = ['HOLD SCOPE', 'SCOPE EXPANSION', 'SELECTIVE EXPANSION', 'SCOP
|
||||
const modeValue = (value: unknown) => typeof value === 'string' && modeNames.includes(value.replaceAll('_', ' '))
|
||||
? value.replaceAll('_', ' ') : null;
|
||||
|
||||
export interface CeoModeHandoff { sessionId: string; toolUseId: string; timestamp: string; option: string; auto: boolean; line: string }
|
||||
|
||||
/** Successful `gstack-ceo-mode-handoff` runs whose single output line is exactly what that invocation prints. */
|
||||
export function ceoModeHandoffs(tools: ReadonlyArray<NativePublicToolEvent>, sessionId: string): CeoModeHandoff[] {
|
||||
return tools.flatMap(use => {
|
||||
if (use.kind !== 'use' || use.name !== 'Bash' || use.sessionId !== sessionId || !use.toolUseId ||
|
||||
typeof use.input?.command !== 'string') return [];
|
||||
const args = cliArgs((use.input.command as string).trim(), 'gstack-ceo-mode-handoff');
|
||||
const option = modeValue(args?.[0]);
|
||||
if (!args || !option) return [];
|
||||
let auto = false, decisions = 'none';
|
||||
for (let i = 1; i < args.length; i++) {
|
||||
if (args[i] === '--auto' && !auto) auto = true;
|
||||
else if (args[i] === '--decisions' && i + 1 < args.length) decisions = args[++i]!.replace(/\n/g, ' ').replace(/\.$/, '') || 'none';
|
||||
else return [];
|
||||
}
|
||||
if (!decisions.trim()) decisions = 'none';
|
||||
const line = auto
|
||||
? `Auto-decided review mode → ${option} (your preference). Change with /plan-tune. Approved decisions: ${decisions}.`
|
||||
: `Mode: ${option}; approved decisions: ${decisions}.`;
|
||||
const results = tools.filter(e => e.kind === 'result' && e.sessionId === sessionId && e.toolUseId === use.toolUseId);
|
||||
const result = results.length === 1 ? results[0]! : null;
|
||||
if (!result || result.isError !== false || typeof result.content !== 'string' || result.content.trim() !== line ||
|
||||
!(Date.parse(result.timestamp) >= Date.parse(use.timestamp))) return [];
|
||||
return [{ sessionId, toolUseId: use.toolUseId, timestamp: result.timestamp, option, auto, line }];
|
||||
});
|
||||
}
|
||||
|
||||
function currentModeStatement(text: string, questionSummary?: string): { option: string; statement: string } | null {
|
||||
const prose = publicProse(text);
|
||||
const lines = prose.split('\n');
|
||||
@@ -286,6 +314,17 @@ function structuredModeDecision(transcript: PlanCountTranscript, tools: NativePu
|
||||
});
|
||||
if (starts.length !== 1) return null;
|
||||
const start = starts[0]!, questionId = `${opts.skillName}-mode`;
|
||||
// The handoff helper's printed AUTO_DECIDE line is the current declaration
|
||||
// when it follows the decision record and nothing later withdraws or changes it.
|
||||
const autoHandoff = (after: number, option: string, summary: string) => {
|
||||
const handoff = ceoModeHandoffs(owned, opts.sessionId).find(h => h.auto && h.option === option &&
|
||||
timely(h.timestamp) && time(h.timestamp) >= after);
|
||||
if (!handoff) return null;
|
||||
const later = current.filter(m => time(m.timestamp) >= time(handoff.timestamp));
|
||||
if (withdrawn(later.map(m => m.text).join('\n\n'), option, summary) ||
|
||||
later.some(m => { const other = currentModeStatement(m.text, summary)?.option; return other !== undefined && other !== option; })) return null;
|
||||
return { sessionId: opts.sessionId, timestamp: handoff.timestamp, option, annotation: handoff.line };
|
||||
};
|
||||
// Opt-in fixture evidence bypasses no failed shell ACK: the owned file is
|
||||
// the completed write. The preamble, record and current public declaration
|
||||
// must all agree in this native session, after invocation and before now.
|
||||
@@ -298,6 +337,8 @@ function structuredModeDecision(transcript: PlanCountTranscript, tools: NativePu
|
||||
row.auto_decided === true && modeValue(row.user_choice) && modeValue(row.user_choice) === modeValue(row.recommended) &&
|
||||
typeof row.question_summary === 'string' && row.question_summary.trim() &&
|
||||
loggedAt >= time(start.result.timestamp) && loggedAt <= opts.now) {
|
||||
const handoff = autoHandoff(loggedAt, modeValue(row.user_choice)!, row.question_summary);
|
||||
if (handoff) return { ...handoff, summary: row.question_summary, preambleToolUseId: start.use.toolUseId, stateRecord: row };
|
||||
for (const message of current) {
|
||||
const declared = currentModeStatement(message.text, row.question_summary);
|
||||
if (time(message.timestamp) < loggedAt || !declared || declared.option !== modeValue(row.user_choice)) continue;
|
||||
@@ -346,6 +387,9 @@ function structuredModeDecision(transcript: PlanCountTranscript, tools: NativePu
|
||||
});
|
||||
if (logs.length !== 1 || !logs[0]!.result) return null;
|
||||
const logged = logs[0]!;
|
||||
const handoff = autoHandoff(time(logged.result!.timestamp), modeValue(logged.log.user_choice)!, logged.log.question_summary);
|
||||
if (handoff) return { ...handoff, summary: logged.log.question_summary, preambleToolUseId: start.use.toolUseId,
|
||||
preferenceToolUseId: check.use.toolUseId, questionLogToolUseId: logged.use.toolUseId };
|
||||
for (const message of current) {
|
||||
if (time(message.timestamp) < time(logged.result!.timestamp)) continue;
|
||||
const declared = currentModeStatement(message.text, logged.log.question_summary);
|
||||
@@ -383,6 +427,13 @@ export function findNativeAutoDecision(
|
||||
const messages = transcript.assistantMessages.filter(m => m.sessionId === opts.sessionId);
|
||||
if (messages.some(m => !Number.isFinite(at(m.timestamp)) || at(m.timestamp) > opts.now)) return null;
|
||||
const loadedAt = at(results[0]!.timestamp);
|
||||
for (const handoff of ceoModeHandoffs(tools, opts.sessionId)) {
|
||||
if (!handoff.auto || !timely(handoff.timestamp) || at(handoff.timestamp) < loadedAt) continue;
|
||||
const later = messages.filter(m => at(m.timestamp) >= at(handoff.timestamp)).map(m => m.text).join('\n\n');
|
||||
if (withdrawn(later, handoff.option)) continue;
|
||||
return { sessionId: opts.sessionId, skillToolUseId: use.toolUseId, timestamp: handoff.timestamp,
|
||||
summary: 'review mode', option: handoff.option, annotation: handoff.line };
|
||||
}
|
||||
for (const message of messages) {
|
||||
if (at(message.timestamp) < loadedAt) continue;
|
||||
const match = assertedAnnotation(message.text, opts.skillName);
|
||||
|
||||
@@ -317,7 +317,7 @@ export const E2E_TOUCHFILES: Record<string, string[]> = {
|
||||
// written a never-ask preference, AUQ should still auto-decide rather than
|
||||
// surfacing the question. Touches the question-tuning + preference
|
||||
// infrastructure plus the resolvers that own the AUTO_DECIDE preamble.
|
||||
'auto-decide-preserved': ['bin/gstack-state-root.sh', 'lib/state-root.ts', 'lib/claude-public-transcript.ts',
|
||||
'auto-decide-preserved': ['bin/gstack-ceo-mode-handoff', 'bin/gstack-state-root.sh', 'lib/state-root.ts', 'lib/claude-public-transcript.ts',
|
||||
|
||||
'test/fixtures/auto-decide-recommendation-361c.json',
|
||||
|
||||
@@ -343,7 +343,7 @@ export const E2E_TOUCHFILES: Record<string, string[]> = {
|
||||
'test/helpers/eval-store.ts', 'lib/claude-bin.ts', 'lib/eval-model.ts',
|
||||
|
||||
'scripts/resolvers/tasks-section.ts', 'test/helpers/auto-decision-state.ts', 'test/helpers/autoplan-artifact-digest.ts', 'test/helpers/autoplan-artifact-permission.ts', 'test/helpers/autoplan-artifact-recorder.ts', 'test/helpers/claude-pty-runner.ts', 'test/helpers/pty/**', 'test/helpers/dx-selected-navigation.ts', 'test/helpers/e2e-gate.ts', 'test/helpers/eng-cache-writer-decision.ts', 'test/helpers/hermetic-skill-runtime.ts', 'test/helpers/native-auto-decide.ts', 'test/helpers/owned-claude-transcript.ts', 'test/helpers/plan-count-artifacts.ts', 'test/helpers/plan-count-file-permission.ts', 'test/helpers/plan-count-fixture.ts', 'test/helpers/plan-count-pending-exit.ts', 'test/helpers/plan-count-pending-question.ts', 'test/helpers/plan-count-transcript.ts', 'test/helpers/plan-floor-review.ts', 'test/helpers/plan-floor-target.ts', 'test/helpers/plan-scope-selection.ts', 'test/helpers/plan-seed-submission.ts', 'test/helpers/plan-skill-question-events.ts', 'test/helpers/plan-skill-question-hook-scope.ts', 'test/helpers/plan-skill-questions.ts', 'test/helpers/pty-screen.ts', 'test/helpers/pty-trust-dialog.ts', 'test/helpers/skill-census.ts'],
|
||||
'plan-ceo-mode-routing': [
|
||||
'plan-ceo-mode-routing': ['bin/gstack-ceo-mode-handoff',
|
||||
'lib/claude-public-transcript.ts',
|
||||
|
||||
|
||||
|
||||
@@ -140,7 +140,7 @@ test('CEO mode handoff applies the selected mode before the next question', () =
|
||||
expect(routeStart).toBeGreaterThan(handoffStart);
|
||||
const handoff = selection.slice(handoffStart, routeStart);
|
||||
const instruction = handoff.split('\n')[0]!;
|
||||
expect(instruction).toMatch(/before tools or further questions/i);
|
||||
expect(instruction).toMatch(/before other tools or further questions, run `[^`]*gstack-ceo-mode-handoff/i);
|
||||
expect(instruction).toMatch(/chat.*mode.s application and rationale/i);
|
||||
const selectionSteps = compactProse(selection.slice(0, handoffStart));
|
||||
expect(selectionSteps).toMatch(/explicit choice skips steps 2–3/i);
|
||||
|
||||
@@ -178,7 +178,7 @@ const COMMON = {
|
||||
allowedTools: ['Read', 'Bash'],
|
||||
timeout: 240_000,
|
||||
runId,
|
||||
} as const;
|
||||
};
|
||||
|
||||
describeIfSelected('third-party-actions consent gate', [
|
||||
'tpa-present', 'tpa-absent-linux', 'tpa-broken', 'tpa-absent-darwin', 'tpa-apple-ban',
|
||||
|
||||
Reference in new issue
Block a user