Files
Codeman/src/ai-plan-checker.ts
T
arkonandClaude Opus 4.7 02e2f3e8b5 refactor: remove dead code and narrow internal exports (knip sweep)
Knip-driven cleanup. All changes verified with tsc --noEmit, lint, and
build.

Removed (zero consumers):
- VERIFICATION_PROMPT constant + its barrel re-export
- createInitialOrchestratorPersistState factory
- transcriptWatcher singleton export
- createAnsiPatternFull / createAnsiPatternSimple factories
- TimerInfo interface + unused AiCheckResult/AiPlanCheckResult imports
  in respawn-controller.ts
- 35 unused Zod z.infer \`*Input\` types in schemas.ts
- Dead re-exports: SessionMode from session.ts, AuthSessionRecord from
  web/ports/index.ts, EnhancedPlanTask/CheckpointReview from
  ralph-tracker.ts, 7 unused entries in utils/index.ts
- 14 event/config interfaces that lived only as JSDoc hints (no TS type
  position usage): Session/Respawn/RalphLoop/RalphTracker/
  SessionManager/SessionAutoOps/Subagent/TaskQueue/TaskTracker/
  TranscriptWatcher/Image/OrchestratorLoop Events + RespawnPreset +
  SessionOutput

Narrowed to module scope (kept but no longer exported):
- buildPermissionArgs in session-cli-builder.ts
- 28 type/interface declarations used only within their own file:
  Ai{Idle,Plan}Check{Config,State}, BashToolParser{Events,Config},
  FileStream/CreateStream{Options,Result}, PlanSubagentEvent,
  SubagentCallback, RalphLoopConfig, RalphLoop{Events,Options},
  ActiveTimerInfo, DetectionStatus, ActionLogEntry, AutoOpsCallbacks,
  TunnelStatus, Timer/LRUMap/StaleExpirationMap Options, AuthState,
  SessionListenerDeps, SseStreamManagerDeps, and 8 more

Docs: CLAUDE.md advice for global-regex `lastIndex` now points to the
remaining `execPattern()` helper instead of the deleted factories.

Knip delta: unused files 42→0, unused exports 161→16, unused types 92→0.
The 16 remaining exports are a mobile-test helper toolkit intentionally
kept for upcoming tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 11:57:00 +02:00

147 lines
5.1 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 PLAN MODE APPROVAL PROMPT or not.
A plan mode approval prompt is a numbered selection menu that Claude Code shows when it wants the user to approve a plan before proceeding. It typically has 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
- Text asking for approval like "Would you like to proceed?" or "Ready to implement?"
- The prompt appears at the BOTTOM of the output (most recent content)
NOT a plan mode prompt:
- Claude actively working (spinners, "Thinking", tool execution)
- A completed response with no selection menu
- An AskUserQuestion/elicitation dialog (different format, free-text input)
- Network lag or mid-output pause
- Any state without a visible numbered selection menu
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 };
}
}