Skills › gstack › plan-ceo-review › mode-handoff-hook.ts
mode-handoff-hook.ts
34 lines · plan-ceo-review/bin/mode-handoff-hook.ts at 3f79d81
1#!/usr/bin/env -S bun --no-env-file --config=/dev/null
2/**
3 * PostToolUse (Bash) hook declared in /plan-ceo-review's frontmatter.
4 *
5 * Step 0E's mode handoff line (from bin/gstack-ceo-mode-handoff) must reach
6 * the user, but Claude Code collapses Bash output, and models often open the
7 * next chat with a paraphrase instead of the line (periodic red 37272185151;
8 * 1 of 10 trials led with it even with a reminder beside the line). This hook
9 * shows the helper's line to the user as a system message, so the user sees
10 * it whatever the model writes next.
11 *
12 * Reads only the helper's own output: the command must be a plain invocation
13 * of gstack-ceo-mode-handoff and the first stdout line must have the helper's
14 * exact shape. Anything else prints nothing. Always exits 0.
15 */
16const HANDOFF_LINE = /^(?:Auto-decided review mode → (?:HOLD SCOPE|SCOPE EXPANSION|SELECTIVE EXPANSION|SCOPE REDUCTION) \(your preference\)\. Change with \/plan-tune\. Approved decisions: .+\.|Mode: (?:HOLD SCOPE|SCOPE EXPANSION|SELECTIVE EXPANSION|SCOPE REDUCTION); approved decisions: .+\.)$/;
18export function handoffMessage(input: unknown): string | null {
19 if (!input || typeof input !== 'object') return null;
20 const { tool_name: tool, tool_input: toolInput, tool_response: response } = input as Record<string, any>;
21 if (tool !== 'Bash' || typeof toolInput?.command !== 'string') return null;
22 if (!/(?:^|[\s"'/])gstack-ceo-mode-handoff["']?\s/.test(toolInput.command)) return null;
23 const stdout: string = typeof response === 'string' ? response : typeof response?.stdout === 'string' ? response.stdout : '';
24 const line = stdout.split(/\r?\n/).find((l: string) => l.trim())?.trim() ?? '';
25 return HANDOFF_LINE.test(line) ? line : null;
26}
28if (import.meta.main) {
29 try {
30 const message = handoffMessage(JSON.parse(await Bun.stdin.text()));
31 if (message) process.stdout.write(JSON.stringify({ systemMessage: message }) + '\n');
32 } catch { /* A hook failure never blocks the session. */ }
33 process.exit(0);
34}