diff --git a/scripts/test-local-llm-harnesses.ts b/scripts/test-local-llm-harnesses.ts index a47b43b4..60acbdd8 100644 --- a/scripts/test-local-llm-harnesses.ts +++ b/scripts/test-local-llm-harnesses.ts @@ -72,7 +72,7 @@ import { mkdtempSync, rmSync, readFileSync, existsSync } from 'node:fs'; import { tmpdir, homedir } from 'node:os'; import { join, delimiter, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; -import { enabledClis } from '../src/config/cli-registry/index.js'; +import { enabledClis } from '../src/config/cli-registry/registry.js'; import type { CliEntry } from '../src/config/cli-registry/types.js'; import { buildCustomModelInjection, diff --git a/src/ai-checker-base.ts b/src/ai-checker-base.ts index 3ade7c2d..18b4c780 100644 --- a/src/ai-checker-base.ts +++ b/src/ai-checker-base.ts @@ -190,11 +190,6 @@ export abstract class AiCheckerBase< */ protected abstract getNegativeVerdict(): V; - /** - * Get the error verdict value. - */ - protected abstract getErrorVerdict(): V; - /** * Create an error result. */ diff --git a/src/ai-idle-checker.ts b/src/ai-idle-checker.ts index 4b462aec..df991dc6 100644 --- a/src/ai-idle-checker.ts +++ b/src/ai-idle-checker.ts @@ -165,10 +165,6 @@ export class AiIdleChecker extends AiCheckerBase -): string { - return renderLaunch(entry.launch, params, engineValues, gatesPassed); -} diff --git a/src/config/cli-registry/index.ts b/src/config/cli-registry/index.ts index d1698967..575b00fc 100644 --- a/src/config/cli-registry/index.ts +++ b/src/config/cli-registry/index.ts @@ -3,59 +3,4 @@ * @module config/cli-registry */ -export type { - ArgSpec, - CliCapabilities, - CliCredStore, - CliDiscovery, - CliEntry, - CliEnv, - CliId, - CliIdentityProbe, - CliLaunch, - CliOverlays, - CliRegistryFile, - CliVariant, - CliVersionProbe, - Cond, - EngineValue, - ParamSpec, - QuoteStyle, -} from './types.js'; -export { - matchesPattern, - TOKEN_PATTERNS, - SAFE_BARE_TOKEN, - compileVersionRegex, - MAX_VERSION_OUTPUT, -} from './patterns.js'; -export type { TokenPattern } from './patterns.js'; -export { renderLaunch, renderCliCommand } from './argv.js'; -export type { EngineValues, ParamValues } from './argv.js'; -export { CliEntrySchema } from './schema.js'; -export type { ValidatedCliEntry } from './schema.js'; -export { STOCK_CLIS } from './stock.js'; -export { - asCliId, - cliIds, - enabledCliIds, - enabledClis, - getCli, - listClis, - loadCliRegistry, - reloadCliRegistry, - resolveInstallCommandForPlatform, - resolveRegistry, -} from './registry.js'; -export type { LoadResult } from './registry.js'; -export { - COMPOSER_ANCHOR_KINDS, - isKnownLauncherProfile, - isKnownPredictProfile, - isKnownSetenvProfile, - LAUNCHER_PROFILE_NAMES, - PREDICT_PROFILES, - SETENV_PROFILE_NAMES, - TRANSCRIPT_READER_NAMES, -} from './profiles.js'; -export type { LauncherProfileName, SetenvProfileName } from './profiles.js'; +export { enabledCliIds, getCli } from './registry.js'; diff --git a/src/config/cli-registry/profiles.ts b/src/config/cli-registry/profiles.ts index 11519142..b05efa88 100644 --- a/src/config/cli-registry/profiles.ts +++ b/src/config/cli-registry/profiles.ts @@ -76,16 +76,6 @@ export const SETENV_PROFILE_NAMES = [ export type LauncherProfileName = (typeof LAUNCHER_PROFILE_NAMES)[number]; export type SetenvProfileName = (typeof SETENV_PROFILE_NAMES)[number]; -/** - * Transcript readers, selected via `capabilities.transcript`. Unlike the profile registries - * above this one is closed over the schema enum itself rather than an open string, since - * transcript format is a small, genuinely fixed set — see CliCapabilities['transcript']. - */ -export const TRANSCRIPT_READER_NAMES = ['claude-jsonl', 'codex-rollout', 'deepseek-zstd', 'none'] as const; - -/** Composer-row finders, selected via `capabilities.echo.anchor.kind`. Also schema-closed. */ -export const COMPOSER_ANCHOR_KINDS = ['glyph', 'cursor', 'none'] as const; - /** True when `name` is a predictive-echo profile this build actually implements. */ export function isKnownPredictProfile(name: string | undefined): boolean { return name !== undefined && Object.prototype.hasOwnProperty.call(PREDICT_PROFILES, name); diff --git a/src/config/cli-registry/registry.ts b/src/config/cli-registry/registry.ts index 8f251eeb..f67a091d 100644 --- a/src/config/cli-registry/registry.ts +++ b/src/config/cli-registry/registry.ts @@ -32,18 +32,10 @@ import { existsSync, readFileSync, renameSync, statSync } from 'node:fs'; import { dataPath } from '../instance.js'; -import type { CliEntry, CliId, CliRegistryFile } from './types.js'; +import type { CliEntry, CliRegistryFile } from './types.js'; import { CliEntrySchema } from './schema.js'; import { STOCK_CLIS } from './stock.js'; -/** Construct a validated CliId. Throws if `raw` is not a well-formed id — call at API boundaries. */ -export function asCliId(raw: string): CliId { - if (!/^[a-z][a-z0-9-]{0,23}$/.test(raw)) { - throw new Error(`invalid CLI id: ${JSON.stringify(raw)}`); - } - return raw as CliId; -} - function filePath(): string { return dataPath('clis.json'); } @@ -227,10 +219,6 @@ export function getCli(id: string): CliEntry | undefined { return listClis().find((e) => (e.id as string) === id); } -export function cliIds(): string[] { - return listClis().map((e) => e.id as string); -} - /** Every enabled entry's id, in registry order. */ export function enabledCliIds(): string[] { return enabledClis().map((e) => e.id as string); diff --git a/src/config/cli-registry/schema.ts b/src/config/cli-registry/schema.ts index 1cc5fef1..ad5185f9 100644 --- a/src/config/cli-registry/schema.ts +++ b/src/config/cli-registry/schema.ts @@ -817,5 +817,3 @@ export const CliEntrySchema = z }); } }); - -export type ValidatedCliEntry = z.infer; diff --git a/src/cron/cron-input.ts b/src/cron/cron-input.ts index 885f993f..9a12f9c7 100644 --- a/src/cron/cron-input.ts +++ b/src/cron/cron-input.ts @@ -7,8 +7,6 @@ import type { ConcurrencyPolicy, InputMode, PromptMode, ScheduleType } from '../types/cron.js'; import type { SessionMode } from '../types/session.js'; -export type { CronJob, CronJobRun, CronJobRunStatus, TriggerType } from '../types/cron.js'; - export interface CronJobInput { name: string; agentType: SessionMode; diff --git a/src/hooks-config.ts b/src/hooks-config.ts index 8c841c7a..70008bc5 100644 --- a/src/hooks-config.ts +++ b/src/hooks-config.ts @@ -13,7 +13,6 @@ * - `applyWorkspaceHooks(workspace, install?)` — the ONE install-vs-refresh decision * point every claude-session create path routes through (see its doc comment) * - `ensureCodemanHooks(casePath)` — safely installs/updates hooks for a managed case - * - `updateCaseEnvVars(casePath, envVars)` — merges env vars into settings * * Hook events generated: `idle_prompt`, `permission_prompt`, `elicitation_dialog`, * `elicitation_complete`, `elicitation_response`, `stop`, `teammate_idle`, @@ -632,37 +631,6 @@ export async function stripCaseEnvKeys(casePath: string, keysToRemove: readonly }); } -/** - * Updates env vars in .claude/settings.local.json for the given case path. - * Merges with existing env field; removes vars set to empty string. - */ -export async function updateCaseEnvVars(casePath: string, envVars: Record): Promise { - await withSafeSettingsWrite(casePath, 'env vars', async (claudeDir, settingsPath) => { - if (!(await pathExistsForWrite(claudeDir))) { - await mkdir(claudeDir, { recursive: true }); - } - - let existing: Record = {}; - try { - existing = JSON.parse(await readFile(settingsPath, 'utf-8')); - } catch { - existing = {}; - } - - const currentEnv = (existing.env as Record) || {}; - for (const [key, value] of Object.entries(envVars)) { - if (value) { - currentEnv[key] = value; - } else { - delete currentEnv[key]; - } - } - existing.env = currentEnv; - - await writeFile(settingsPath, JSON.stringify(existing, null, 2) + '\n'); - }); -} - /** * Updates the `model` field in .claude/settings.local.json for the given case path. * Pass a non-empty string to set, or empty/null to remove. diff --git a/src/prompts/index.ts b/src/prompts/index.ts index 3e91bc30..9d40c145 100644 --- a/src/prompts/index.ts +++ b/src/prompts/index.ts @@ -8,4 +8,4 @@ export { RESEARCH_AGENT_PROMPT } from './research-agent.js'; export { PLANNER_PROMPT } from './planner.js'; export { PHASE_EXECUTION_PROMPT, TEAM_LEAD_PROMPT, REPLAN_PROMPT, SINGLE_TASK_PROMPT } from './orchestrator.js'; -export { RALPH_STATUS_CONTRACT, buildRalphLoopPrompt, type RalphLoopPromptOptions } from './ralph.js'; +export { buildRalphLoopPrompt } from './ralph.js'; diff --git a/src/ralph-tracker.ts b/src/ralph-tracker.ts index 40f25158..239f4f91 100644 --- a/src/ralph-tracker.ts +++ b/src/ralph-tracker.ts @@ -882,13 +882,6 @@ export class RalphTracker extends EventEmitter { } } - /** - * Check if a phrase matches any valid completion phrase (primary or alternate). - */ - isValidCompletionPhrase(phrase: string): boolean { - return this.findMatchingCompletionPhrase(phrase) !== null; - } - /** * Find which completion phrase (primary or alternate) matches the given phrase. */ diff --git a/src/respawn-controller.ts b/src/respawn-controller.ts index c1b775ee..55b20a20 100644 --- a/src/respawn-controller.ts +++ b/src/respawn-controller.ts @@ -30,7 +30,7 @@ * - `DetectionStatus`, `ActiveTimerInfo` — status types * * Key methods: `start()`, `stop()`, `getStatus()`, `getConfig()`, - * `getDetectionStatus()`, `getActiveTimers()`, `getAggregateMetrics()`, + * `getDetectionStatus()`, `getActiveTimers()`, * `getTimingHistory()`, `getHealthScore()` * * @dependencies session (PTY output), ai-idle-checker, ai-plan-checker, @@ -76,14 +76,7 @@ import { AI_PLAN_CHECK_TIMEOUT_MS, AI_PLAN_CHECK_COOLDOWN_MS, } from './config/ai-defaults.js'; -import { - getErrorMessage, - type RespawnCycleMetrics, - type RespawnAggregateMetrics, - type RalphLoopHealthScore, - type TimingHistory, - type CycleOutcome, -} from './types.js'; +import { getErrorMessage, type RalphLoopHealthScore, type TimingHistory, type CycleOutcome } from './types.js'; // ========== Constants ========== @@ -1903,13 +1896,6 @@ export class RespawnController extends EventEmitter { this.emit('actionLog', action); } - /** - * Get recent action log entries. - */ - getRecentActions(): ActionLogEntry[] { - return [...this.recentActions]; - } - // ========== Multi-Layer Detection Methods ========== // Pattern detection delegated to ./respawn-patterns.js (isCompletionMessage, hasWorkingPattern, extractTokenCount) @@ -3178,23 +3164,6 @@ export class RespawnController extends EventEmitter { } } - /** - * Get aggregate metrics for monitoring. - * @returns Copy of aggregate metrics - */ - getAggregateMetrics(): RespawnAggregateMetrics { - return this.cycleMetrics.getAggregate(); - } - - /** - * Get recent cycle metrics for analysis. - * @param limit - Maximum number of metrics to return (default: 20) - * @returns Recent cycle metrics, newest first - */ - getRecentCycleMetrics(limit: number = 20): RespawnCycleMetrics[] { - return this.cycleMetrics.getRecent(limit); - } - // ========== P2-005: Health Score (delegated to respawn-health.ts) ========== /** diff --git a/src/session-task-cache.ts b/src/session-task-cache.ts deleted file mode 100644 index f08252e7..00000000 --- a/src/session-task-cache.ts +++ /dev/null @@ -1,101 +0,0 @@ -/** - * @fileoverview LRU cache for task descriptions parsed from terminal output. - * - * Stores descriptions extracted from Claude Code's Task tool invocations - * (e.g., "Explore(Check files)") keyed by timestamp. Used to correlate - * with SubagentWatcher discoveries for better window titles. - * - * @module session-task-cache - */ - -import { LRUMap } from './utils/lru-map.js'; - -/** Default maximum number of task descriptions to keep */ -const DEFAULT_MAX_SIZE = 100; - -/** Default maximum age for task descriptions (30 seconds) */ -const DEFAULT_MAX_AGE_MS = 30_000; - -/** - * LRU cache for task descriptions parsed from terminal output. - * - * Descriptions are keyed by the timestamp when they were parsed. - * Old entries are automatically cleaned up based on maxAgeMs. - * Size is bounded by the underlying LRUMap. - */ -export class SessionTaskCache { - private readonly cache: LRUMap; - private readonly maxAgeMs: number; - - constructor(maxSize: number = DEFAULT_MAX_SIZE, maxAgeMs: number = DEFAULT_MAX_AGE_MS) { - this.cache = new LRUMap({ maxSize }); - this.maxAgeMs = maxAgeMs; - } - - /** - * Add a task description at the given timestamp. - */ - add(timestamp: number, description: string): void { - this.cache.set(timestamp, description); - } - - /** - * Remove task descriptions older than maxAgeMs. - * LRUMap maintains insertion order, so we can break early - * once we find a non-expired entry. - */ - private cleanupOld(): void { - const cutoff = Date.now() - this.maxAgeMs; - for (const timestamp of this.cache.keysInOrder()) { - if (timestamp < cutoff) { - this.cache.delete(timestamp); - } else { - break; - } - } - } - - /** - * Get all recent task descriptions sorted by timestamp (most recent first). - */ - getAll(): Array<{ timestamp: number; description: string }> { - this.cleanupOld(); - const results: Array<{ timestamp: number; description: string }> = []; - for (const [timestamp, description] of this.cache) { - results.push({ timestamp, description }); - } - return results.sort((a, b) => b.timestamp - a.timestamp); - } - - /** - * Find a task description that was parsed close to a given timestamp. - * Used to correlate with SubagentWatcher discoveries. - * - * @param subagentStartTime - The timestamp when the subagent was discovered - * @param maxAgeMs - Maximum age difference to consider (default 10 seconds) - * @returns The matching description or undefined - */ - findNear(subagentStartTime: number, maxAgeMs: number = 10000): string | undefined { - this.cleanupOld(); - - let bestMatch: { timestamp: number; description: string } | undefined; - let bestDiff = Infinity; - - for (const [timestamp, description] of this.cache) { - const diff = Math.abs(subagentStartTime - timestamp); - if (diff < maxAgeMs && diff < bestDiff) { - bestMatch = { timestamp, description }; - bestDiff = diff; - } - } - - return bestMatch?.description; - } - - /** - * Clear all cached descriptions. - */ - clear(): void { - this.cache.clear(); - } -} diff --git a/src/session.ts b/src/session.ts index e428484f..27e9f9f2 100644 --- a/src/session.ts +++ b/src/session.ts @@ -17,7 +17,7 @@ * * Key methods: `runPrompt()`, `startInteractive()`, `startShell()`, * `writeViaMux()`, `toState()`, `stop()`, `resize()`, `isIdle()`, - * `setAutoCompact()`, `findTaskDescriptionNear()`, `getTerminalBuffer()` + * `setAutoCompact()`, `getTerminalBuffer()` * * @dependencies session-cli-builder (args/env), session-auto-ops (auto-compact/clear), * ralph-tracker (todo/completion parsing), bash-tool-parser (tool invocation tracking), @@ -117,7 +117,6 @@ import { SPINNER_PATTERN, CLAUDE_WORKING_LINE_PATTERN, MAX_SESSION_TOKENS, - execPattern, getClaudeCliVersion, getClaudeBinaryPath, spawnPtyWithHelperRepair, @@ -156,7 +155,6 @@ import { } from './session-cli-builder.js'; import { SessionAutoOps, type ContextReport } from './session-auto-ops.js'; import { detectUsageLimitPause } from './usage-limit-patterns.js'; -import { SessionTaskCache } from './session-task-cache.js'; import { InteractivePtyExitBreaker } from './session-pty-exit-breaker.js'; import { isGeneratedSessionName, SubmittedPromptTracker } from './session-auto-name.js'; import { parseTerminalAttachmentRequests } from './attachment-magic.js'; @@ -222,12 +220,6 @@ const MAX_CLAUDE_SESSION_CHAIN = 32; // eslint-disable-next-line no-control-regex const FOCUS_ESCAPE_FILTER = /\x1b\[\?1004[hl]|\x1b\[[IO]/g; -// Pattern to match Task tool invocations in terminal output -// Matches: "Explore(Description)", "Task(Description)", "Bash(Description)", etc. -// The prefix characters vary (●, ·, ✶, etc.) so we don't require them -// We look for the tool name followed by (description) -const TASK_TOOL_PATTERN = /\b(Explore|Task|Bash|Plan|general-purpose)\(([^)]+)\)/g; - // Pre-compiled patterns for hot paths (avoid regex compilation per call) /** Pattern to strip leading ANSI escapes and whitespace from terminal buffer */ // eslint-disable-next-line no-control-regex @@ -235,8 +227,6 @@ const LEADING_ANSI_WHITESPACE_PATTERN = /^(\x1b\[\??[\d;]*[A-Za-z]|[\s\r\n])+/; /** Pattern to match Ctrl+L (form feed) characters */ // eslint-disable-next-line no-control-regex const CTRL_L_PATTERN = /\x0c/g; -/** Pattern to split by newlines (CR or LF) */ -const NEWLINE_SPLIT_PATTERN = /\r?\n/; /** * True for external-CLI run modes (non-Claude) that use their own TUI and output format: @@ -574,9 +564,6 @@ export class Session extends EventEmitter { readonly createdAt: number; readonly mode: SessionMode; - // Task description cache (extracted to SessionTaskCache) - private _taskCache = new SessionTaskCache(); - private _name: string; private _nameSource: SessionNameSource; /** `pinCliSessionName` as of the last spawn (see {@link _refreshCliNamePin}). */ @@ -834,10 +821,6 @@ export class Session extends EventEmitter { // Ralph tracking (Ralph Wiggum loops and todo lists inside Claude Code) private _ralphTracker: RalphTracker; - // Agent tree tracking - private _parentAgentId: string | null = null; - private _childAgentIds: string[] = []; - // Bounded dedup set for terminal attachment magic-links already requested. private _attachmentMagicSeen = new Set(); private _attachmentHistory: SessionAttachmentHistoryItem[] = []; @@ -961,9 +944,7 @@ export class Session extends EventEmitter { toolsUpdate: (tools: ActiveBashTool[]) => void; } | null = null; - // Task descriptions parsed from terminal output — delegated to SessionTaskCache - - // Throttle expensive PTY processing (Ralph, bash parser, task descriptions) + // Throttle expensive PTY processing (Ralph, bash parser) // Accumulates clean data between processing windows to avoid running regex on every chunk private _lastExpensiveProcessTime: number = 0; private _pendingCleanData: string = ''; @@ -1999,14 +1980,6 @@ export class Session extends EventEmitter { return this._taskTracker; } - get runningTaskCount(): number { - return this._taskTracker.getRunningCount(); - } - - get taskTree(): BackgroundTask[] { - return this._taskTracker.getTaskTree(); - } - get taskStats(): { total: number; running: number; completed: number; failed: number } { return this._taskTracker.getStats(); } @@ -2028,38 +2001,10 @@ export class Session extends EventEmitter { return this._ralphTracker.getTodoStats(); } - // Bash tool tracking getters - get bashToolParser(): BashToolParser { - return this._bashToolParser; - } - get activeTools(): ActiveBashTool[] { return this._bashToolParser.activeTools; } - get parentAgentId(): string | null { - return this._parentAgentId; - } - - set parentAgentId(value: string | null) { - this._parentAgentId = value; - } - - get childAgentIds(): string[] { - return [...this._childAgentIds]; - } - - addChildAgentId(agentId: string): void { - if (!this._childAgentIds.includes(agentId)) { - this._childAgentIds.push(agentId); - } - } - - removeChildAgentId(agentId: string): void { - const idx = this._childAgentIds.indexOf(agentId); - if (idx >= 0) this._childAgentIds.splice(idx, 1); - } - // Nice priority config getters and setters get niceConfig(): NiceConfig { return { ...this._niceConfig }; @@ -2519,8 +2464,6 @@ export class Session extends EventEmitter { ralphEnabled: this._ralphTracker.enabled, ralphAutoEnableDisabled: this._ralphTracker.autoEnableDisabled || undefined, ralphCompletionPhrase: this._ralphTracker.loopState.completionPhrase || undefined, - parentAgentId: this._parentAgentId || undefined, - childAgentIds: this._childAgentIds.length > 0 ? this._childAgentIds : undefined, niceEnabled: this._niceConfig.enabled, niceValue: this._niceConfig.niceValue, color: this._color, @@ -4411,7 +4354,7 @@ export class Session extends EventEmitter { } /** - * Process expensive parsers (ANSI strip, Ralph, bash tool, token, CLI info, task descriptions). + * Process expensive parsers (ANSI strip, Ralph, bash tool, token, CLI info). * Called on a throttled schedule (every EXPENSIVE_PROCESS_INTERVAL_MS) instead of on every * PTY data chunk. Receives accumulated raw data to process in one batch. */ @@ -4464,11 +4407,6 @@ export class Session extends EventEmitter { this.parseClaudeCodeInfo(getCleanData()); } - // Parse task descriptions from terminal output (e.g., "Explore(Check files)") - if (rawData.includes('(') && rawData.includes(')')) { - this.parseTaskDescriptionsFromTerminalData(getCleanData()); - } - // Legacy gerunds, Claude-only. The status-line pattern above already ran for every // CLI that declares one, so this adds only the older wording. Current Claude // randomizes the word ("Actualizing…", "Finagling…"), so these catch a fraction of @@ -4970,67 +4908,10 @@ export class Session extends EventEmitter { } else if (trimmed) { this._textOutput.append(line + '\n'); } - - // Parse task descriptions from terminal output (e.g., "Explore(Description)") - // This captures the short description from Claude Code's Task tool output - // Use direct method since cleanLine is already ANSI-stripped (line 1460) - this.parseTaskDescriptionsDirect(cleanLine); } // Note: BufferAccumulator auto-trims when max size exceeded } - /** - * Parse task descriptions from terminal data (may contain multiple lines). - * Called from interactive mode's onData handler with ANSI-stripped data. - * @param cleanData - Terminal data with ANSI codes already stripped - */ - private parseTaskDescriptionsFromTerminalData(cleanData: string): void { - // Quick pre-check: skip if no parentheses present - if (!cleanData.includes('(') || !cleanData.includes(')')) return; - - // Split by newlines and process each line (data already ANSI-stripped) - const lines = cleanData.split(NEWLINE_SPLIT_PATTERN); - for (const line of lines) { - this.parseTaskDescriptionsDirect(line); - } - } - - /** - * Parse task descriptions from a pre-cleaned line (no ANSI codes). - * Used by both processOutput() and parseTaskDescriptionsFromTerminalData(). - */ - private parseTaskDescriptionsDirect(cleanLine: string): void { - // Quick pre-check: skip expensive regex if no common tool patterns present - if (!cleanLine.includes('(') || !cleanLine.includes(')')) return; - - execPattern(TASK_TOOL_PATTERN, cleanLine, (match) => { - const description = match[2].trim(); - if (description && description.length > 0) { - this._taskCache.add(Date.now(), description); - } - }); - } - - /** - * Get recent task descriptions parsed from terminal output. - * Returns descriptions sorted by timestamp (most recent first). - */ - getRecentTaskDescriptions(): Array<{ timestamp: number; description: string }> { - return this._taskCache.getAll(); - } - - /** - * Find a task description that was parsed close to a given timestamp. - * Used to correlate with SubagentWatcher discoveries. - * - * @param subagentStartTime - The timestamp when the subagent was discovered - * @param maxAgeMs - Maximum age difference to consider (default 10 seconds) - * @returns The matching description or undefined - */ - findTaskDescriptionNear(subagentStartTime: number, maxAgeMs: number = 10000): string | undefined { - return this._taskCache.findNear(subagentStartTime, maxAgeMs); - } - // Parse token count from Claude's status line in interactive mode // Matches patterns like "123.4k tokens", "5234 tokens", "1.2M tokens" // @@ -5981,10 +5862,6 @@ export class Session extends EventEmitter { this._status = killMux ? 'stopped' : 'idle'; this._currentTaskId = null; - // Clear task description cache and agent tree to prevent memory leak - this._taskCache.clear(); - this._childAgentIds = []; - // Kill the associated mux session if requested if (killMux && this._mux) { // Try to kill mux session even if _muxSession is not set (e.g., restored sessions) @@ -6038,6 +5915,5 @@ export class Session extends EventEmitter { this._messages = []; this._taskTracker.clear(); this._ralphTracker.clear(); - this._taskCache.clear(); } } diff --git a/src/state-store.ts b/src/state-store.ts index 88510c93..8e05de5e 100644 --- a/src/state-store.ts +++ b/src/state-store.ts @@ -429,29 +429,6 @@ export class StateStore { } } - /** - * Attempt to recover state from backup file. - * Call this if main state file is corrupt. - */ - recoverFromBackup(): boolean { - const backupPath = this.filePath + '.bak'; - try { - if (existsSync(backupPath)) { - const backupContent = readFileSync(backupPath, 'utf-8'); - const parsed = JSON.parse(backupContent) as Partial; - this.state = this._mergeWithInitialState(parsed); - console.log('[StateStore] Successfully recovered state from backup'); - // Reset circuit breaker after successful recovery - this.circuitBreakerOpen = false; - this.consecutiveSaveFailures = 0; - return true; - } - } catch (err) { - console.error('[StateStore] Failed to recover from backup:', err); - } - return false; - } - /** * Reset the circuit breaker (for manual intervention). */ diff --git a/src/team-watcher.ts b/src/team-watcher.ts index 1822c0e6..d54d1c69 100644 --- a/src/team-watcher.ts +++ b/src/team-watcher.ts @@ -15,7 +15,7 @@ import { join } from 'node:path'; import { watch as chokidarWatch, type FSWatcher as ChokidarWatcher } from 'chokidar'; -import type { TeamConfig, TeamMember, TeamTask, InboxMessage } from './types.js'; +import type { TeamConfig, TeamTask, InboxMessage } from './types.js'; import { LRUMap } from './utils/lru-map.js'; import { TEAM_POLL_INTERVAL_MS, MAX_CACHED_TEAMS, MAX_CACHED_TASKS } from './config/team-config.js'; @@ -175,17 +175,6 @@ export class TeamWatcher extends EventEmitter { return activeTasks > 0; } - /** Get teammates that have real tmux panes (not in-process) */ - getTmuxPaneTeammates(teamName: string): Array { - const team = this.teams.get(teamName); - if (!team) return []; - - return team.members.filter( - (m): m is TeamMember & { tmuxPaneId: string } => - m.agentType !== 'team-lead' && !!m.tmuxPaneId && m.tmuxPaneId !== 'in-process' - ); - } - /** Get count of active teammates for a session */ getActiveTeammateCount(sessionId: string): number { const team = this.getTeamForSession(sessionId); diff --git a/src/tmux-manager.ts b/src/tmux-manager.ts index 7d2e5451..5f77b116 100644 --- a/src/tmux-manager.ts +++ b/src/tmux-manager.ts @@ -4451,21 +4451,6 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer { } } - /** - * Sync mouse mode based on pane count: enable if split (>1 pane), disable if single. - * Called by TeamWatcher when teammates spawn/despawn panes. - * Uses `tmux list-panes` for bulletproof detection — counts actual panes, not config. - */ - async syncMouseMode(muxName: string): Promise { - if (IS_TEST_MODE) return true; - const panes = await this.listPanes(muxName); - if (panes.length > 1) { - return this.enableMouseMode(muxName); - } else { - return this.disableMouseMode(muxName); - } - } - /** * List all panes in a tmux session. * Returns structured info for each pane. @@ -4782,67 +4767,6 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer { } } - /** - * Start piping pane output to a file using tmux pipe-pane. - * Only pipes output direction (-O) to avoid echoing input. - */ - startPipePane(muxName: string, paneTarget: string, outputFile: string): boolean { - if (IS_TEST_MODE) return true; - if (!isValidMuxName(muxName)) { - console.error('[TmuxManager] Invalid session name in startPipePane:', muxName); - return false; - } - if (!SAFE_PANE_TARGET_PATTERN.test(paneTarget)) { - console.error('[TmuxManager] Invalid pane target:', paneTarget); - return false; - } - if (!isValidPath(outputFile)) { - console.error('[TmuxManager] Invalid output file path:', outputFile); - return false; - } - - const target = paneTarget.startsWith('%') ? `${muxName}.${paneTarget}` : `${muxName}.%${paneTarget}`; - - try { - execSync(`${this.tmux()} pipe-pane -O -t ${shellescape(target)} ${shellescape('cat >> ' + outputFile)}`, { - encoding: 'utf-8', - timeout: EXEC_TIMEOUT_MS, - }); - return true; - } catch (err) { - console.error('[TmuxManager] Failed to start pipe-pane:', err); - return false; - } - } - - /** - * Stop piping pane output (calling pipe-pane with no command stops piping). - */ - stopPipePane(muxName: string, paneTarget: string): boolean { - if (IS_TEST_MODE) return true; - if (!isValidMuxName(muxName)) { - console.error('[TmuxManager] Invalid session name in stopPipePane:', muxName); - return false; - } - if (!SAFE_PANE_TARGET_PATTERN.test(paneTarget)) { - console.error('[TmuxManager] Invalid pane target:', paneTarget); - return false; - } - - const target = paneTarget.startsWith('%') ? `${muxName}.${paneTarget}` : `${muxName}.%${paneTarget}`; - - try { - execSync(`${this.tmux()} pipe-pane -t ${shellescape(target)}`, { - encoding: 'utf-8', - timeout: EXEC_TIMEOUT_MS, - }); - return true; - } catch (err) { - console.error('[TmuxManager] Failed to stop pipe-pane:', err); - return false; - } - } - getAttachCommand(): string { return 'tmux'; } diff --git a/src/types/foreign-tmux.ts b/src/types/foreign-tmux.ts index 47955a66..2ad2e2ac 100644 --- a/src/types/foreign-tmux.ts +++ b/src/types/foreign-tmux.ts @@ -29,20 +29,6 @@ import type { SessionMode } from './session.js'; /** Where a foreign tmux server lives. Mirrors Codeman's own three locations. */ export type ForeignTmuxLocationKind = 'local' | 'docker' | 'remote'; -/** - * A location to scan. `hostId` refers to the saved docker-host / remote-host - * registry entry, so the transport can be rebuilt without trusting the browser. - */ -export interface ForeignTmuxLocation { - kind: ForeignTmuxLocationKind; - /** Registry id (`docker-hosts.json` / `remote-hosts.json`). Absent for local. */ - hostId?: string; - /** Display label for the UI. Absent for local. */ - label?: string; - /** Container to `docker exec` into (`kind === 'docker'`). */ - containerName?: string; -} - /** One pane row from the probe script, after parsing. */ export interface ForeignPaneProbe { socketPath: string; diff --git a/src/types/local-model-install.ts b/src/types/local-model-install.ts index 39dc7d83..5e2e4e13 100644 --- a/src/types/local-model-install.ts +++ b/src/types/local-model-install.ts @@ -698,9 +698,6 @@ export interface LocalModelRefusal { message: LocalModelInstallMessage; } -/** What every verb answers on success: the view after the verb. */ -export type LocalModelVerbAnswer = LocalModelInstallView; - /** * Why a verb was refused with 409. * - `job-running`: a job is running (a second install, a resume, a remove or a runtime action). diff --git a/src/types/session.ts b/src/types/session.ts index f3441ff8..edf089a2 100644 --- a/src/types/session.ts +++ b/src/types/session.ts @@ -1103,10 +1103,6 @@ export interface SessionState { ralphAutoEnableDisabled?: boolean; /** Ralph completion phrase (if set) */ ralphCompletionPhrase?: string; - /** Parent agent ID if this session is a spawned agent */ - parentAgentId?: string; - /** Child agent IDs spawned by this session */ - childAgentIds?: string[]; /** Nice priority enabled */ niceEnabled?: boolean; /** Nice value (-20 to 19) */ diff --git a/src/utils/antigravity-cli-resolver.ts b/src/utils/antigravity-cli-resolver.ts index f42197eb..3934ded4 100644 --- a/src/utils/antigravity-cli-resolver.ts +++ b/src/utils/antigravity-cli-resolver.ts @@ -9,11 +9,7 @@ import { getCli } from '../config/cli-registry/registry.js'; import { expandHome } from './cli-resolver.js'; -import { - createCliExecutableResolver, - formatCliNotFoundMessage, - type CliResolverHost, -} from './cli-executable-resolver.js'; +import { createCliExecutableResolver, type CliResolverHost } from './cli-executable-resolver.js'; /** Common directories where the Antigravity CLI binary may be installed */ /** @@ -23,9 +19,6 @@ import { */ const ANTIGRAVITY_SEARCH_DIRS = (): string[] => (getCli('antigravity')?.discovery.searchDirs ?? []).map(expandHome); -const ANTIGRAVITY_NOT_FOUND = - 'Antigravity CLI not found. Install with: curl -fsSL https://antigravity.google/cli/install.sh | bash'; - function createAntigravityResolver(host?: CliResolverHost, now?: () => number) { return createCliExecutableResolver({ binary: 'agy', searchDirs: ANTIGRAVITY_SEARCH_DIRS, now }, host); } @@ -53,7 +46,3 @@ export function resolveAntigravityDir(): string | null { export function isAntigravityAvailable(): boolean { return resolveAntigravityDir() !== null; } - -export function getAntigravityNotFoundMessage(): string { - return formatCliNotFoundMessage(ANTIGRAVITY_NOT_FOUND, antigravityResolver.diagnostics()); -} diff --git a/src/utils/claude-cli-resolver.ts b/src/utils/claude-cli-resolver.ts index 04a1b15f..d84267df 100644 --- a/src/utils/claude-cli-resolver.ts +++ b/src/utils/claude-cli-resolver.ts @@ -12,7 +12,7 @@ import { execFileSync } from 'node:child_process'; import { delimiter, join } from 'node:path'; import { homedir } from 'node:os'; import { EXEC_TIMEOUT_MS } from '../config/exec-timeout.js'; -import { createCliExecutableResolver, formatCliNotFoundMessage } from './cli-executable-resolver.js'; +import { createCliExecutableResolver } from './cli-executable-resolver.js'; /** Common directories where the Claude CLI binary may be installed */ const CLAUDE_SEARCH_DIRS = [ @@ -24,7 +24,6 @@ const CLAUDE_SEARCH_DIRS = [ ]; const claudeResolver = createCliExecutableResolver({ binary: 'claude', searchDirs: CLAUDE_SEARCH_DIRS }); -const CLAUDE_NOT_FOUND = 'Claude CLI not found. Install it with: curl -fsSL https://claude.ai/install.sh | bash'; /** * Returns true if the Claude CLI binary can be located (via `which` or one of @@ -46,10 +45,6 @@ export function findClaudeDir(): string | null { return claudeResolver.resolve()?.directory ?? null; } -export function getClaudeNotFoundMessage(): string { - return formatCliNotFoundMessage(CLAUDE_NOT_FOUND, claudeResolver.diagnostics()); -} - /** * Returns an absolute path to the `claude` binary, falling back to the bare * name `'claude'` when it cannot be located (so PATH resolution still gets a diff --git a/src/utils/cli-launcher.ts b/src/utils/cli-launcher.ts index 2e483c3a..b4a18df9 100644 --- a/src/utils/cli-launcher.ts +++ b/src/utils/cli-launcher.ts @@ -82,26 +82,6 @@ export async function resolveCliLaunchError(mode: string, rawConfig?: Record number -): RegistryResolver { - return createRegistryResolver(entry, probe ?? probeCliCandidate, host, now); -} - /** * One memoized resolver per id, for the process lifetime — the same caching the per-CLI * modules already do for themselves, just keyed by id so generic code holding only a diff --git a/src/utils/codex-cli-resolver.ts b/src/utils/codex-cli-resolver.ts index ccf4dd03..13c75fcb 100644 --- a/src/utils/codex-cli-resolver.ts +++ b/src/utils/codex-cli-resolver.ts @@ -10,7 +10,7 @@ import { spawn } from 'node:child_process'; import { getCli } from '../config/cli-registry/registry.js'; import { expandHome } from './cli-resolver.js'; -import { createCliExecutableResolver, formatCliNotFoundMessage } from './cli-executable-resolver.js'; +import { createCliExecutableResolver } from './cli-executable-resolver.js'; import { parseCodexRateLimitsResponse, type StatusTelemetry } from '../usage-telemetry.js'; /** @@ -22,7 +22,6 @@ const CODEX_SEARCH_DIRS = (): string[] => (getCli('codex')?.discovery.searchDirs const CODEX_BINARY = process.platform === 'win32' ? 'codex.exe' : 'codex'; const codexResolver = createCliExecutableResolver({ binary: CODEX_BINARY, searchDirs: CODEX_SEARCH_DIRS }); -const CODEX_NOT_FOUND = 'Codex CLI not found. Install with: npm install -g @openai/codex'; /** * Finds the directory containing the `codex` binary. @@ -47,10 +46,6 @@ export function isCodexAvailable(): boolean { return resolveCodexDir() !== null; } -export function getCodexNotFoundMessage(): string { - return formatCliNotFoundMessage(CODEX_NOT_FOUND, codexResolver.diagnostics()); -} - type CodexRateLimitsRequest = (binaryPath: string, clientVersion: string) => Promise; const APP_SERVER_TIMEOUT_MS = 10_000; diff --git a/src/utils/copilot-cli-resolver.ts b/src/utils/copilot-cli-resolver.ts index 636dbf2c..c1410fd0 100644 --- a/src/utils/copilot-cli-resolver.ts +++ b/src/utils/copilot-cli-resolver.ts @@ -17,11 +17,7 @@ import { execFileSync } from 'node:child_process'; import { EXEC_TIMEOUT_MS } from '../config/exec-timeout.js'; import { getCli } from '../config/cli-registry/registry.js'; import { expandHome } from './cli-resolver.js'; -import { - createCliExecutableResolver, - formatCliNotFoundMessage, - type CliResolverHost, -} from './cli-executable-resolver.js'; +import { createCliExecutableResolver, type CliResolverHost } from './cli-executable-resolver.js'; /** * Directories probed after `which`, read from this CLI's registry entry so the spawn @@ -36,8 +32,6 @@ const COPILOT_SEARCH_DIRS = (): string[] => (getCli('copilot')?.discovery.search */ export const COPILOT_VERSION_REGEX = /GitHub Copilot CLI (\d+\.\d+\.\d+)/; -const COPILOT_NOT_FOUND = 'GitHub Copilot CLI not found. Install with: npm install -g @github/copilot'; - /** * Run `copilot --version` on a candidate path and return the trimmed version when * it is the GitHub Copilot CLI. Returns null for anything else — a missing @@ -125,10 +119,6 @@ export function isCopilotAvailable(): boolean { return resolveCopilotDir() !== null; } -export function getCopilotNotFoundMessage(): string { - return formatCliNotFoundMessage(COPILOT_NOT_FOUND, copilotResolver.diagnostics()); -} - /** * Version reported by the resolved `copilot` binary, or null when copilot is * unavailable. Surfaced through `GET /api/copilot/status` so a misresolution is diff --git a/src/utils/deepseek-cli-resolver.ts b/src/utils/deepseek-cli-resolver.ts index 498967aa..2bf25433 100644 --- a/src/utils/deepseek-cli-resolver.ts +++ b/src/utils/deepseek-cli-resolver.ts @@ -31,7 +31,7 @@ */ import { execFileSync } from 'node:child_process'; -import { existsSync, readdirSync, readFileSync } from 'node:fs'; +import { readdirSync, readFileSync } from 'node:fs'; import { join } from 'node:path'; import { homedir } from 'node:os'; import { EXEC_TIMEOUT_MS } from '../config/exec-timeout.js'; @@ -379,11 +379,6 @@ export function getDeepSeekCliVersion(): string | null { return deepSeekResolver.resolve()?.metadata ?? null; } -/** Does the named profile exist and can it drive a pane? */ -export function profileExists(name: string): boolean { - return existsSync(join(resolveDshHome(), 'profiles', name, 'package.json')); -} - /** * Why a DeepSeek session cannot start, or null when it can. * diff --git a/src/utils/gemini-cli-resolver.ts b/src/utils/gemini-cli-resolver.ts index a1903a92..bfdf029c 100644 --- a/src/utils/gemini-cli-resolver.ts +++ b/src/utils/gemini-cli-resolver.ts @@ -9,7 +9,7 @@ import { getCli } from '../config/cli-registry/registry.js'; import { expandHome } from './cli-resolver.js'; -import { createCliExecutableResolver, formatCliNotFoundMessage } from './cli-executable-resolver.js'; +import { createCliExecutableResolver } from './cli-executable-resolver.js'; /** Common directories where the Gemini CLI binary may be installed */ /** @@ -20,7 +20,6 @@ import { createCliExecutableResolver, formatCliNotFoundMessage } from './cli-exe const GEMINI_SEARCH_DIRS = (): string[] => (getCli('gemini')?.discovery.searchDirs ?? []).map(expandHome); const geminiResolver = createCliExecutableResolver({ binary: 'gemini', searchDirs: GEMINI_SEARCH_DIRS }); -const GEMINI_NOT_FOUND = 'Gemini CLI not found. Install with: npm install -g @google/gemini-cli'; /** * Finds the directory containing the `gemini` binary. @@ -38,7 +37,3 @@ export function resolveGeminiDir(): string | null { export function isGeminiAvailable(): boolean { return resolveGeminiDir() !== null; } - -export function getGeminiNotFoundMessage(): string { - return formatCliNotFoundMessage(GEMINI_NOT_FOUND, geminiResolver.diagnostics()); -} diff --git a/src/utils/grok-cli-resolver.ts b/src/utils/grok-cli-resolver.ts index 5d5f324e..c63ee589 100644 --- a/src/utils/grok-cli-resolver.ts +++ b/src/utils/grok-cli-resolver.ts @@ -23,11 +23,7 @@ import { execFileSync } from 'node:child_process'; import { EXEC_TIMEOUT_MS } from '../config/exec-timeout.js'; import { getCli } from '../config/cli-registry/registry.js'; import { expandHome } from './cli-resolver.js'; -import { - createCliExecutableResolver, - formatCliNotFoundMessage, - type CliResolverHost, -} from './cli-executable-resolver.js'; +import { createCliExecutableResolver, type CliResolverHost } from './cli-executable-resolver.js'; /** Common directories where the Grok CLI binary may be installed */ /** @@ -49,8 +45,6 @@ const GROK_SEARCH_DIRS = (): string[] => (getCli('grok')?.discovery.searchDirs ? */ export const GROK_VERSION_REGEX = /(?:^|\s)(\d+\.\d+\.\d+)/; -const GROK_NOT_FOUND = 'Grok CLI not found. Install with: curl -fsSL https://x.ai/cli/install.sh | bash'; - /** * Run `grok --version` on a candidate path and return the version token when it * looks like the coding agent. Returns null for anything else: a missing @@ -137,10 +131,6 @@ export function isGrokAvailable(): boolean { return resolveGrokDir() !== null; } -export function getGrokNotFoundMessage(): string { - return formatCliNotFoundMessage(GROK_NOT_FOUND, grokResolver.diagnostics()); -} - /** * Version reported by the resolved `grok` binary, or null when grok is * unavailable. Surfaced through `GET /api/grok/status` so a misresolution is diff --git a/src/utils/index.ts b/src/utils/index.ts index 93124e5a..245b6be1 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -12,7 +12,7 @@ export { Debouncer, KeyedDebouncer } from './debouncer.js'; export { startEventLoopMonitor } from './event-loop-monitor.js'; export type { EventLoopMonitorHandle } from './event-loop-monitor.js'; export { StaleExpirationMap } from './stale-expiration-map.js'; -export { isProcessRunning, waitForProcessesExit, PROCESS_EXIT_POLL_MS } from './process-exit-wait.js'; +export { waitForProcessesExit } from './process-exit-wait.js'; export { ANSI_ESCAPE_PATTERN_FULL, ANSI_ESCAPE_PATTERN_SIMPLE, @@ -30,59 +30,13 @@ export { assertNever } from './type-safety.js'; export { toSingleLine } from './single-line.js'; export { wrapWithNice } from './nice-wrapper.js'; export { resolveLocalShell, loginShellArgs } from './shell-resolver.js'; -export { - findClaudeDir, - getAugmentedPath, - getClaudeCliVersion, - getClaudeBinaryPath, - getClaudeNotFoundMessage, -} from './claude-cli-resolver.js'; +export { getAugmentedPath, getClaudeCliVersion, getClaudeBinaryPath } from './claude-cli-resolver.js'; export { spawnPtyWithHelperRepair } from './node-pty-repair.js'; -export { resolveOpenCodeDir, getOpenCodeNotFoundMessage } from './opencode-cli-resolver.js'; -export { - resolveCodexDir, - resolveCodexBinaryPath, - isCodexAvailable, - getCodexNotFoundMessage, - readCodexPlanUsage, -} from './codex-cli-resolver.js'; -export { resolveGeminiDir, isGeminiAvailable, getGeminiNotFoundMessage } from './gemini-cli-resolver.js'; -export { - resolveAntigravityDir, - isAntigravityAvailable, - getAntigravityNotFoundMessage, -} from './antigravity-cli-resolver.js'; -export { resolvePiDir, isPiAvailable, getPiCliVersion, getPiNotFoundMessage } from './pi-cli-resolver.js'; -export { resolveGrokDir, isGrokAvailable, getGrokCliVersion, getGrokNotFoundMessage } from './grok-cli-resolver.js'; -export { - resolveDeepSeekDir, - isDeepSeekAvailable, - isDeepSeekRunnable, - getDeepSeekCliVersion, - getDeepSeekNotFoundMessage, - listDeepSeekProfiles, - resolveDefaultDeepSeekProfile, - isLaunchableProfile, - resolveDshHome, - profileExists, -} from './deepseek-cli-resolver.js'; -export type { DeepSeekProfile, DeepSeekProfileKind } from './deepseek-cli-resolver.js'; -export { compileFileQuery, matchFileQuery } from './file-query.js'; -export type { FileQueryMatcher } from './file-query.js'; -export { resolveOmpDir, isOmpAvailable, getOmpNotFoundMessage, getOmpCliVersion } from './omp-cli-resolver.js'; -export { - resolveCopilotDir, - isCopilotAvailable, - getCopilotNotFoundMessage, - getCopilotCliVersion, -} from './copilot-cli-resolver.js'; -export { readCopilotPlanUsage, resolveCopilotToken } from './copilot-plan-usage.js'; export { boundedPathExists, describeUnknownPath, probePath, probePathKind, isNearStalledPath, - unknownPathReason, } from './bounded-path-probe.js'; -export type { PathProbeState, PathProbeKind, PathProbeOptions } from './bounded-path-probe.js'; +export type { PathProbeOptions } from './bounded-path-probe.js'; diff --git a/src/utils/omp-cli-resolver.ts b/src/utils/omp-cli-resolver.ts index 9c169366..9fb4d50c 100644 --- a/src/utils/omp-cli-resolver.ts +++ b/src/utils/omp-cli-resolver.ts @@ -17,11 +17,7 @@ import { execFileSync } from 'node:child_process'; import { EXEC_TIMEOUT_MS } from '../config/exec-timeout.js'; import { getCli } from '../config/cli-registry/registry.js'; import { expandHome } from './cli-resolver.js'; -import { - createCliExecutableResolver, - formatCliNotFoundMessage, - type CliResolverHost, -} from './cli-executable-resolver.js'; +import { createCliExecutableResolver, type CliResolverHost } from './cli-executable-resolver.js'; /** * Directories probed after `which`, read from this CLI's registry entry so the spawn @@ -42,8 +38,6 @@ const OMP_SEARCH_DIRS = (): string[] => (getCli('omp')?.discovery.searchDirs ?? */ export const OMP_VERSION_REGEX = /(?:^|\s)omp\/(\d+\.\d+\.\d+)/; -const OMP_NOT_FOUND = 'OMP CLI not found. Install with: curl -fsSL https://omp.sh/install | sh'; - /** * Run `omp --version` on a candidate path and return the trimmed version when * it looks like the coding agent. Returns null for anything else — a missing @@ -125,10 +119,6 @@ export function isOmpAvailable(): boolean { return resolveOmpDir() !== null; } -export function getOmpNotFoundMessage(): string { - return formatCliNotFoundMessage(OMP_NOT_FOUND, ompResolver.diagnostics()); -} - /** * Version reported by the resolved `omp` binary, or null when omp is * unavailable. Surfaced through `GET /api/omp/status` so a misresolution is diff --git a/src/utils/opencode-cli-resolver.ts b/src/utils/opencode-cli-resolver.ts index f1d71012..07bbeb17 100644 --- a/src/utils/opencode-cli-resolver.ts +++ b/src/utils/opencode-cli-resolver.ts @@ -9,7 +9,7 @@ import { getCli } from '../config/cli-registry/registry.js'; import { expandHome } from './cli-resolver.js'; -import { createCliExecutableResolver, formatCliNotFoundMessage } from './cli-executable-resolver.js'; +import { createCliExecutableResolver } from './cli-executable-resolver.js'; /** Common directories where the OpenCode CLI binary may be installed */ /** @@ -20,7 +20,6 @@ import { createCliExecutableResolver, formatCliNotFoundMessage } from './cli-exe const OPENCODE_SEARCH_DIRS = (): string[] => (getCli('opencode')?.discovery.searchDirs ?? []).map(expandHome); const openCodeResolver = createCliExecutableResolver({ binary: 'opencode', searchDirs: OPENCODE_SEARCH_DIRS }); -const OPENCODE_NOT_FOUND = 'OpenCode CLI not found. Install with: curl -fsSL https://opencode.ai/install | bash'; /** * Finds the directory containing the `opencode` binary. @@ -39,7 +38,3 @@ export function resolveOpenCodeDir(): string | null { export function isOpenCodeAvailable(): boolean { return resolveOpenCodeDir() !== null; } - -export function getOpenCodeNotFoundMessage(): string { - return formatCliNotFoundMessage(OPENCODE_NOT_FOUND, openCodeResolver.diagnostics()); -} diff --git a/src/utils/pi-cli-resolver.ts b/src/utils/pi-cli-resolver.ts index b6ad3a35..62a93852 100644 --- a/src/utils/pi-cli-resolver.ts +++ b/src/utils/pi-cli-resolver.ts @@ -19,11 +19,7 @@ import { execFileSync } from 'node:child_process'; import { EXEC_TIMEOUT_MS } from '../config/exec-timeout.js'; import { getCli } from '../config/cli-registry/registry.js'; import { expandHome } from './cli-resolver.js'; -import { - createCliExecutableResolver, - formatCliNotFoundMessage, - type CliResolverHost, -} from './cli-executable-resolver.js'; +import { createCliExecutableResolver, type CliResolverHost } from './cli-executable-resolver.js'; /** Common directories where the Pi CLI binary may be installed */ /** @@ -48,8 +44,6 @@ const PI_SEARCH_DIRS = (): string[] => (getCli('pi')?.discovery.searchDirs ?? [] */ export const PI_VERSION_REGEX = /(?:^|\s)(\d+\.\d+\.\d+)/; -const PI_NOT_FOUND = 'Pi CLI not found. Install with: npm install -g --ignore-scripts @earendil-works/pi-coding-agent'; - /** * Run `pi --version` on a candidate path and return the trimmed version when it * looks like the coding agent. Returns null for anything else — a missing @@ -132,10 +126,6 @@ export function isPiAvailable(): boolean { return resolvePiDir() !== null; } -export function getPiNotFoundMessage(): string { - return formatCliNotFoundMessage(PI_NOT_FOUND, piResolver.diagnostics()); -} - /** * Version reported by the resolved `pi` binary, or null when pi is unavailable. * Surfaced through `GET /api/pi/status` so a misresolution is diagnosable from the UI. diff --git a/src/web/ports/tab-layout-port.ts b/src/web/ports/tab-layout-port.ts index ad42da88..b5e198df 100644 --- a/src/web/ports/tab-layout-port.ts +++ b/src/web/ports/tab-layout-port.ts @@ -1,8 +1,6 @@ /** @fileoverview Owner-scoped tab-layout capabilities exposed to route modules. */ import type { TabLayoutService } from '../../tab-layout-service.js'; -export type { LegacyOrderActor, LegacyOrderPutResult, SessionOrderProjectionChange } from '../../tab-layout-service.js'; - export interface TabLayoutPort { readonly tabLayouts: TabLayoutService; } diff --git a/src/web/routes/index.ts b/src/web/routes/index.ts index 31db304e..472e050c 100644 --- a/src/web/routes/index.ts +++ b/src/web/routes/index.ts @@ -45,10 +45,8 @@ export { closeAllLlamaSwapLogTails, detectCustomModelSwapDisplacements, pruneIdleLlamaSwapLogTails, - type CustomModelSessionLike, - type CustomModelSwapDisplacement, } from './custom-model-routes.js'; -export { registerCliRegistryRoutes, readCliManagementEnabled, type CliListItem } from './cli-registry-routes.js'; +export { registerCliRegistryRoutes } from './cli-registry-routes.js'; export { registerLocalModelRoutes } from './local-model-routes.js'; export { registerAgentSessionRoutes } from './agent-session-routes.js'; diff --git a/src/web/sse-stream-manager.ts b/src/web/sse-stream-manager.ts index 7a2c9c03..66f7a877 100644 --- a/src/web/sse-stream-manager.ts +++ b/src/web/sse-stream-manager.ts @@ -127,10 +127,6 @@ export class SseStreamManager { return this.remoteSseClients.size; } - get isTunnelActive(): boolean { - return this._isTunnelActive; - } - setTunnelActive(active: boolean): void { this._isTunnelActive = active; } diff --git a/test/hooks-config.test.ts b/test/hooks-config.test.ts index 9077fc5d..91291074 100644 --- a/test/hooks-config.test.ts +++ b/test/hooks-config.test.ts @@ -37,7 +37,6 @@ import { resolveStatusLineCliCommand, settingsWriteBlocker, stripCaseEnvKeys, - updateCaseEnvVars, updateCaseModel, writeHooksConfig, } from '../src/hooks-config.js'; @@ -296,7 +295,6 @@ describe('writeHooksConfig', () => { await ensureCodemanHooks(caseDir); await refreshStaleCodemanHooks(caseDir); await updateCaseModel(caseDir, 'opus'); - await updateCaseEnvVars(caseDir, { CLAUDE_CODE_NEW: 'value' }); await stripCaseEnvKeys(caseDir, ['CLAUDE_CODE_KEEP']); await applyStatusLineConfig(caseDir, true); await applyStatusLineConfig(caseDir, false); diff --git a/test/mobile/accessibility.test.ts b/test/mobile/accessibility.test.ts index b96b10a5..95f9a5f5 100644 --- a/test/mobile/accessibility.test.ts +++ b/test/mobile/accessibility.test.ts @@ -10,7 +10,7 @@ import { assertZoomNotDisabled, getCSSProperty, } from './helpers/assertions.js'; import { REPRESENTATIVE_DEVICES } from './devices.js'; -import type { WebServer } from '../src/web/server.js'; +import type { WebServer } from '../../src/web/server.js'; let baseUrl: string; diff --git a/test/mobile/device-matrix.test.ts b/test/mobile/device-matrix.test.ts index b0f78fa0..906ee184 100644 --- a/test/mobile/device-matrix.test.ts +++ b/test/mobile/device-matrix.test.ts @@ -1,7 +1,7 @@ // Ephemeral port - Cross-device parametric tests import { describe, it, expect, beforeAll, afterAll } from 'vitest'; import type { BrowserContext, Page } from 'playwright'; -import type { WebServer } from '../src/web/server.js'; +import type { WebServer } from '../../src/web/server.js'; import { createTestServer, stopTestServer } from './helpers/server.js'; import { createDevicePage, closeAllBrowsers } from './helpers/browser.js'; import { SELECTORS, BREAKPOINTS, BODY_CLASSES } from './helpers/constants.js'; diff --git a/test/mobile/devices.ts b/test/mobile/devices.ts index 4d2864cd..386222ce 100644 --- a/test/mobile/devices.ts +++ b/test/mobile/devices.ts @@ -321,13 +321,6 @@ export const SMALL_TABLETS: DeviceEntry[] = DEVICE_REGISTRY.filter((d) => d.cate export const STANDARD_TABLETS: DeviceEntry[] = DEVICE_REGISTRY.filter((d) => d.category === 'standard-tablet'); export const LARGE_TABLETS: DeviceEntry[] = DEVICE_REGISTRY.filter((d) => d.category === 'large-tablet'); -// --------------------------------------------------------------------------- -// Platform exports -// --------------------------------------------------------------------------- - -export const IOS_DEVICES: DeviceEntry[] = DEVICE_REGISTRY.filter((d) => d.isIOS); -export const ANDROID_DEVICES: DeviceEntry[] = DEVICE_REGISTRY.filter((d) => !d.isIOS); - // --------------------------------------------------------------------------- // Representative devices — one per category for quick smoke tests // --------------------------------------------------------------------------- diff --git a/test/mobile/helpers/assertions.ts b/test/mobile/helpers/assertions.ts index 8521a34e..58314d07 100644 --- a/test/mobile/helpers/assertions.ts +++ b/test/mobile/helpers/assertions.ts @@ -156,21 +156,6 @@ export async function assertNotHasClass(page: Page, selector: string, className: expect(has).toBe(false); } -/** Assert element's computed transform includes a translateY value */ -export async function assertTranslateY( - page: Page, - selector: string, - expectedY: number, - tolerance: number = 2, -): Promise { - const transform = await getCSSProperty(page, selector, 'transform'); - // transform is a matrix(...) string; extract translateY - // matrix(a, b, c, d, tx, ty) — ty is the 6th value - const match = transform.match(/matrix\([^,]+,[^,]+,[^,]+,[^,]+,[^,]+,\s*([^)]+)\)/); - const ty = match ? parseFloat(match[1]) : 0; - expect(Math.abs(ty - expectedY)).toBeLessThanOrEqual(tolerance); -} - /** Check viewport meta tag doesn't disable zoom */ export async function assertZoomNotDisabled(page: Page): Promise { const meta = await page.evaluate(() => { diff --git a/test/mobile/helpers/browser.ts b/test/mobile/helpers/browser.ts index 41091419..0fe68e90 100644 --- a/test/mobile/helpers/browser.ts +++ b/test/mobile/helpers/browser.ts @@ -60,6 +60,3 @@ export async function closeAllBrowsers(): Promise { } } -export function isChromium(engine: 'chromium' | 'webkit'): boolean { - return engine === 'chromium'; -} diff --git a/test/mobile/helpers/cdp.ts b/test/mobile/helpers/cdp.ts index 0407fe33..d55e0462 100644 --- a/test/mobile/helpers/cdp.ts +++ b/test/mobile/helpers/cdp.ts @@ -39,38 +39,3 @@ export async function dispatchTouchEvent( }); } -/** Throttle CPU to simulate mobile hardware (rate = slowdown factor, e.g. 4 = 4x slower) */ -export async function setCPUThrottle(cdp: CDPSession, rate: number): Promise { - await cdp.send('Emulation.setCPUThrottlingRate', { rate }); -} - -/** Clear CPU throttling */ -export async function clearCPUThrottle(cdp: CDPSession): Promise { - await cdp.send('Emulation.setCPUThrottlingRate', { rate: 1 }); -} - -/** Emulate network conditions */ -export async function setNetworkThrottle( - cdp: CDPSession, - downloadKbps: number, - uploadKbps: number, - latencyMs: number, -): Promise { - await cdp.send('Network.enable'); - await cdp.send('Network.emulateNetworkConditions', { - offline: false, - downloadThroughput: (downloadKbps * 1024) / 8, - uploadThroughput: (uploadKbps * 1024) / 8, - latency: latencyMs, - }); -} - -/** Clear network throttling */ -export async function clearNetworkThrottle(cdp: CDPSession): Promise { - await cdp.send('Network.emulateNetworkConditions', { - offline: false, - downloadThroughput: -1, - uploadThroughput: -1, - latency: 0, - }); -} diff --git a/test/mobile/helpers/server.ts b/test/mobile/helpers/server.ts index 2ec0bb3b..17ae0040 100644 --- a/test/mobile/helpers/server.ts +++ b/test/mobile/helpers/server.ts @@ -1,23 +1,13 @@ import { WebServer } from '../../../src/web/server.js'; -const servers = new Set(); - /** A test server on an ephemeral port; read the port from `server.boundPort` once this resolves. */ export async function createTestServer(): Promise { const server = new WebServer(0, false, true); // testMode = true await server.start(); - servers.add(server); return server; } export async function stopTestServer(server: WebServer): Promise { await server.stop(); - servers.delete(server); } -export async function stopAllTestServers(): Promise { - for (const server of servers) { - await server.stop(); - } - servers.clear(); -} diff --git a/test/mobile/helpers/touch-sim.ts b/test/mobile/helpers/touch-sim.ts index 9b57000e..acec9289 100644 --- a/test/mobile/helpers/touch-sim.ts +++ b/test/mobile/helpers/touch-sim.ts @@ -243,19 +243,3 @@ export async function tapViaSynthetic(page: Page, selector: string): Promise { - if (options.isChromium !== false) { - try { - await tapViaCDP(page, selector); - return; - } catch { - // Fall through - } - } - await tapViaSynthetic(page, selector); -} diff --git a/test/mobile/keyboard.test.ts b/test/mobile/keyboard.test.ts index 1dfd4c97..23576bc2 100644 --- a/test/mobile/keyboard.test.ts +++ b/test/mobile/keyboard.test.ts @@ -24,7 +24,7 @@ import { getCSSProperty, } from './helpers/assertions.js'; import { REPRESENTATIVE_DEVICES } from './devices.js'; -import type { WebServer } from '../src/web/server.js'; +import type { WebServer } from '../../src/web/server.js'; let baseUrl: string; diff --git a/test/mobile/layout.test.ts b/test/mobile/layout.test.ts index 46680e6e..04dba598 100644 --- a/test/mobile/layout.test.ts +++ b/test/mobile/layout.test.ts @@ -1,7 +1,7 @@ // Ephemeral port - General mobile layout tests import { describe, it, expect, beforeAll, afterAll } from 'vitest'; import type { BrowserContext, Page } from 'playwright'; -import type { WebServer } from '../src/web/server.js'; +import type { WebServer } from '../../src/web/server.js'; import { createTestServer, stopTestServer } from './helpers/server.js'; import { createDevicePage, closeAllBrowsers } from './helpers/browser.js'; import { SELECTORS, BREAKPOINTS, BODY_CLASSES, WAIT, MIN_TOUCH_TARGET } from './helpers/constants.js'; diff --git a/test/mobile/settings.test.ts b/test/mobile/settings.test.ts index cb6e8b17..129a1a13 100644 --- a/test/mobile/settings.test.ts +++ b/test/mobile/settings.test.ts @@ -7,7 +7,7 @@ import { createDevicePage, closeAllBrowsers } from './helpers/browser.js'; import { showKeyboard, hideKeyboard } from './helpers/keyboard-sim.js'; import { assertVisible, assertHidden, getCSSProperty, getCSSNumericValue } from './helpers/assertions.js'; import { DEVICE_REGISTRY, REPRESENTATIVE_DEVICES } from './devices.js'; -import type { WebServer } from '../src/web/server.js'; +import type { WebServer } from '../../src/web/server.js'; let baseUrl: string; diff --git a/test/mobile/subagent-windows.test.ts b/test/mobile/subagent-windows.test.ts index c51a8df2..3db91480 100644 --- a/test/mobile/subagent-windows.test.ts +++ b/test/mobile/subagent-windows.test.ts @@ -7,7 +7,7 @@ import { createDevicePage, closeAllBrowsers } from './helpers/browser.js'; import { showKeyboard, hideKeyboard } from './helpers/keyboard-sim.js'; import { getCSSProperty, getCSSNumericValue } from './helpers/assertions.js'; import { REPRESENTATIVE_DEVICES } from './devices.js'; -import type { WebServer } from '../src/web/server.js'; +import type { WebServer } from '../../src/web/server.js'; let baseUrl: string; diff --git a/test/mobile/tabs.test.ts b/test/mobile/tabs.test.ts index a751509b..4af8d66e 100644 --- a/test/mobile/tabs.test.ts +++ b/test/mobile/tabs.test.ts @@ -7,7 +7,7 @@ import { createDevicePage, closeAllBrowsers } from './helpers/browser.js'; import { swipe, swipeViaCDP, swipeViaSynthetic } from './helpers/touch-sim.js'; import { assertHidden, assertVisible, getCSSProperty, getCSSNumericValue } from './helpers/assertions.js'; import { REPRESENTATIVE_DEVICES } from './devices.js'; -import type { WebServer } from '../src/web/server.js'; +import type { WebServer } from '../../src/web/server.js'; let baseUrl: string; diff --git a/test/mobile/visual-regression.test.ts b/test/mobile/visual-regression.test.ts index d5d10a49..7a7e4f49 100644 --- a/test/mobile/visual-regression.test.ts +++ b/test/mobile/visual-regression.test.ts @@ -1,7 +1,7 @@ // Ephemeral port - Screenshot comparison tests import { describe, it, expect, beforeAll, afterAll } from 'vitest'; import type { BrowserContext, Page } from 'playwright'; -import type { WebServer } from '../src/web/server.js'; +import type { WebServer } from '../../src/web/server.js'; import { createTestServer, stopTestServer } from './helpers/server.js'; import { createDevicePage, closeAllBrowsers } from './helpers/browser.js'; import { SELECTORS, VISUAL, VISUAL_BREAKPOINTS, WAIT } from './helpers/constants.js'; diff --git a/test/mocks/index.ts b/test/mocks/index.ts index 29bc0c8e..cd9f3802 100644 --- a/test/mocks/index.ts +++ b/test/mocks/index.ts @@ -2,14 +2,11 @@ * Shared test mocks — import from here instead of defining inline. * * @example - * import { MockSession, MockStateStore, terminalOutputs } from './mocks/index.js'; + * import { MockSession, createMockRouteContext } from './mocks/index.js'; */ -export { MockSession, createMockSession, terminalOutputs } from './mock-session.js'; -export { MockStateStore } from './mock-state-store.js'; +export { MockSession, createMockSession } from './mock-session.js'; export { - waitForEvent, - createDeferred, safeRmHomeTree, isUnderTestHome, isSandboxedHome, diff --git a/test/mocks/mock-session.ts b/test/mocks/mock-session.ts index 3918acfd..34e9fd6a 100644 --- a/test/mocks/mock-session.ts +++ b/test/mocks/mock-session.ts @@ -534,55 +534,6 @@ export class MockSession extends EventEmitter { getMessages = vi.fn(() => []); } -/** - * Generate realistic terminal output for testing. - * Must match the patterns used in MockSession's simulate* methods. - */ -export const terminalOutputs = { - /** Standard completion message */ - completion(duration: string = '2m 46s'): string { - return `\n\u273b Worked for ${duration}\n 123.4k tokens used\n`; - }, - - /** Working spinner output */ - working(activity: string = 'Thinking'): string { - return `${activity}... \u280b`; - }, - - /** Plan mode prompt */ - planMode(question: string = 'Would you like to proceed?'): string { - return [`\n${question}\n`, '\u276f 1. Yes\n', ' 2. No\n', ' 3. Type your own\n'].join(''); - }, - - /** Prompt character */ - prompt(): string { - return '\n\u276f '; - }, - - /** Token count display */ - tokens(count: number): string { - const formatted = - count >= 1000000 - ? `${(count / 1000000).toFixed(1)}M` - : count >= 1000 - ? `${(count / 1000).toFixed(1)}k` - : String(count); - return ` ${formatted} tokens\n`; - }, - - /** Large output for buffer testing */ - largeOutput(sizeKb: number = 100): string { - const baseText = 'Lorem ipsum dolor sit amet. '.repeat(100); - const repetitions = Math.ceil((sizeKb * 1024) / baseText.length); - return baseText.repeat(repetitions).slice(0, sizeKb * 1024); - }, - - /** ANSI colored output */ - ansiColored(text: string): string { - return `\x1b[32m${text}\x1b[0m`; - }, -}; - /** * Convenience factory. */ diff --git a/test/mocks/mock-state-store.ts b/test/mocks/mock-state-store.ts deleted file mode 100644 index e9d09bff..00000000 --- a/test/mocks/mock-state-store.ts +++ /dev/null @@ -1,60 +0,0 @@ -/** - * Shared MockStateStore for tests. - * - * Includes methods for both session management and Ralph loop testing. - * All methods are vi.fn() spies — tests can override return values as needed. - * - * NOTE: This is for direct instantiation in new tests. Existing tests that - * use vi.mock('../src/state-store.js') keep their inline definitions. - */ -import { vi } from 'vitest'; - -export class MockStateStore { - state: Record = { - sessions: {} as Record, - config: { maxConcurrentSessions: 5 }, - ralphLoop: { status: 'stopped' }, - tasks: {} as Record, - }; - - // Session methods - getConfig = vi.fn(() => this.state.config); - getSessions = vi.fn(() => this.state.sessions as Record); - getSession = vi.fn((id: string) => (this.state.sessions as Record)[id]); - setSession = vi.fn((id: string, state: unknown) => { - (this.state.sessions as Record)[id] = state; - }); - removeSession = vi.fn((id: string) => { - delete (this.state.sessions as Record)[id]; - }); - - // Ralph state methods - getRalphLoopState = vi.fn(() => this.state.ralphLoop); - setRalphLoopState = vi.fn((update: Record) => { - this.state.ralphLoop = { ...(this.state.ralphLoop as Record), ...update }; - }); - - // Task methods - getTasks = vi.fn(() => this.state.tasks); - setTask = vi.fn(); - removeTask = vi.fn(); - - // Settings methods - getSettings = vi.fn(() => ({})); - setSettings = vi.fn(); - - // Generic persistence - save = vi.fn(); - load = vi.fn(); - - /** Reset all state and mocks for clean test isolation */ - reset(): void { - this.state = { - sessions: {}, - config: { maxConcurrentSessions: 5 }, - ralphLoop: { status: 'stopped' }, - tasks: {}, - }; - vi.clearAllMocks(); - } -} diff --git a/test/mocks/test-helpers.ts b/test/mocks/test-helpers.ts index 12c0a280..924ebdfe 100644 --- a/test/mocks/test-helpers.ts +++ b/test/mocks/test-helpers.ts @@ -1,43 +1,10 @@ /** - * Reusable async test helpers. + * Guards for tests that delete under the throwaway HOME. */ import { rmSync } from 'node:fs'; import { basename, resolve } from 'node:path'; -/** Wait for an EventEmitter to emit a specific event, with timeout */ -export function waitForEvent( - emitter: { once: (event: string, listener: (...args: unknown[]) => void) => void }, - event: string, - timeoutMs = 5000 -): Promise { - return new Promise((resolve, reject) => { - const timer = setTimeout( - () => reject(new Error(`Timed out waiting for event "${event}" after ${timeoutMs}ms`)), - timeoutMs - ); - emitter.once(event, (...args: unknown[]) => { - clearTimeout(timer); - resolve(args.length === 1 ? args[0] : args); - }); - }); -} - -/** Create a deferred promise with external resolve/reject */ -export function createDeferred(): { - promise: Promise; - resolve: (value: T) => void; - reject: (reason?: unknown) => void; -} { - let resolve!: (value: T) => void; - let reject!: (reason?: unknown) => void; - const promise = new Promise((res, rej) => { - resolve = res; - reject = rej; - }); - return { promise, resolve, reject }; -} - /** * Prefix of the throwaway HOME `test/setup.ts` creates (`mkdtempSync(join(tmpdir(), 'codeman-vitest-'))`). * Pinned by test/test-env-isolation.test.ts, so renaming one without the other fails there.