Files
Codeman/src/ai-plan-checker.ts
T
arkonandClaude Opus 4.7 257695ff8e chore: version packages
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-31 05:17:07 +02:00

154 lines
5.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* @fileoverview AI-Powered Plan Mode Checker for Auto-Accept
*
* Spawns a fresh Claude CLI session in a tmux session to analyze terminal output
* and determine if Claude Code is showing a plan mode approval prompt.
* Used as a confirmation gate before auto-accepting prompts.
*
* ## How It Works
*
* 1. Generate temp file path for output capture
* 2. Spawn tmux: `tmux new-session -d -s codeman-plancheck-<short> bash -c 'claude -p ...'`
* 3. Poll the temp file every 500ms for `__PLANCHECK_DONE__` marker
* 4. Parse the file content for PLAN_MODE/NOT_PLAN_MODE on the first line
* 5. Kill tmux session and delete temp file
*
* ## Error Handling
*
* - Tmux spawn fails: 30s cooldown, increment error counter
* - Check times out (60s): Kill session, 30s cooldown
* - Can't parse verdict: Treat as NOT_PLAN_MODE, 30s cooldown
* - 3 consecutive errors: Disable AI plan check
*
* @module ai-plan-checker
*/
import {
AiCheckerBase,
type AiCheckerConfigBase,
type AiCheckerResultBase,
type AiCheckerStateBase,
} from './ai-checker-base.js';
import {
AI_CHECK_MODEL,
AI_PLAN_CHECK_MAX_CONTEXT,
AI_PLAN_CHECK_TIMEOUT_MS,
AI_PLAN_CHECK_COOLDOWN_MS,
AI_PLAN_CHECK_ERROR_COOLDOWN_MS,
AI_CHECK_MAX_CONSECUTIVE_ERRORS,
} from './config/ai-defaults.js';
// ========== Types ==========
type AiPlanCheckConfig = AiCheckerConfigBase;
export type AiPlanCheckVerdict = 'PLAN_MODE' | 'NOT_PLAN_MODE' | 'ERROR';
export type AiPlanCheckResult = AiCheckerResultBase<AiPlanCheckVerdict>;
type AiPlanCheckState = AiCheckerStateBase<AiPlanCheckVerdict>;
// ========== Constants ==========
const DEFAULT_PLAN_CHECK_CONFIG: AiPlanCheckConfig = {
enabled: true,
model: AI_CHECK_MODEL,
maxContextChars: AI_PLAN_CHECK_MAX_CONTEXT,
checkTimeoutMs: AI_PLAN_CHECK_TIMEOUT_MS,
cooldownMs: AI_PLAN_CHECK_COOLDOWN_MS,
errorCooldownMs: AI_PLAN_CHECK_ERROR_COOLDOWN_MS,
maxConsecutiveErrors: AI_CHECK_MAX_CONSECUTIVE_ERRORS,
};
/** Pattern to match PLAN_MODE or NOT_PLAN_MODE as the first word(s) of output */
const VERDICT_PATTERN = /^\s*(PLAN_MODE|NOT_PLAN_MODE)\b/i;
/** The prompt sent to the AI plan checker */
const AI_PLAN_CHECK_PROMPT = `Analyze this terminal output from a running Claude Code session. Determine if the terminal is currently showing a NUMBERED SELECTION MENU that is waiting for the user to press Enter on the highlighted default option.
A qualifying menu has all of these characteristics:
- A numbered list of options (e.g., "1. Yes", "2. No", "3. Type your own")
- A selection indicator arrow (❯ or >) pointing to one of the options (the default)
- The menu appears at the BOTTOM of the output (most recent content)
- It is asking the user to choose, not just displaying numbered information
This includes BOTH:
- Plan-mode approval prompts ("Would you like to proceed?" / "Ready to implement?")
- AskUserQuestion / elicitation dialogs (Claude Code's numbered question menus)
NOT a qualifying menu:
- Claude actively working (spinners, "Thinking", tool execution)
- A completed response with no selection menu visible
- A free-text input field with no numbered options
- A numbered LIST in the assistant's prose with no selection arrow
- Network lag or mid-output pause
- Any state without a visible selector arrow on a numbered option
The verdict name PLAN_MODE is historical — it now means "auto-accept this selection menu by pressing Enter on the default".
Terminal output (most recent at bottom):
---
{TERMINAL_BUFFER}
---
Answer with EXACTLY one of these on the first line: PLAN_MODE or NOT_PLAN_MODE
Then optionally explain briefly why.`;
// ========== AiPlanChecker Class ==========
/**
* Manages AI-powered plan mode detection by spawning a fresh Claude CLI session
* to analyze terminal output and confirm plan mode approval prompts.
*/
export class AiPlanChecker extends AiCheckerBase<
AiPlanCheckVerdict,
AiPlanCheckConfig,
AiPlanCheckResult,
AiPlanCheckState
> {
protected readonly muxNamePrefix = 'codeman-plancheck-';
protected readonly doneMarker = '__PLANCHECK_DONE__';
protected readonly tempFilePrefix = 'codeman-plancheck';
protected readonly logPrefix = '[AiPlanChecker]';
protected readonly checkDescription = 'AI plan check';
constructor(sessionId: string, config: Partial<AiPlanCheckConfig> = {}) {
super(sessionId, DEFAULT_PLAN_CHECK_CONFIG, config);
}
protected buildPrompt(terminalBuffer: string): string {
return AI_PLAN_CHECK_PROMPT.replace('{TERMINAL_BUFFER}', terminalBuffer);
}
protected parseVerdict(output: string): { verdict: AiPlanCheckVerdict; reasoning: string } | null {
const match = output.match(VERDICT_PATTERN);
if (!match) return null;
const verdict = match[1].toUpperCase() as 'PLAN_MODE' | 'NOT_PLAN_MODE';
const lines = output.split('\n');
const reasoning = lines.slice(1).join('\n').trim() || `AI determined: ${verdict}`;
return { verdict, reasoning };
}
protected getPositiveVerdict(): AiPlanCheckVerdict {
return 'PLAN_MODE';
}
protected getNegativeVerdict(): AiPlanCheckVerdict {
return 'NOT_PLAN_MODE';
}
protected getErrorVerdict(): AiPlanCheckVerdict {
return 'ERROR';
}
protected createErrorResult(reasoning: string, durationMs: number): AiPlanCheckResult {
return { verdict: 'ERROR', reasoning, durationMs };
}
protected createResult(verdict: AiPlanCheckVerdict, reasoning: string, durationMs: number): AiPlanCheckResult {
return { verdict, reasoning, durationMs };
}
}