{"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/core/doctor.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Doctor diagnostics module for system health checks.\n */\n\nimport { execSync, spawnSync } from \"node:child_process\";\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport { getPowerShellConfig } from \"../utils/shell.js\";\nimport { parseWorktreeList } from \"./footer-data-provider.js\";\n\n/** Status levels for doctor checks */\nexport type DoctorStatus = \"ok\" | \"warn\" | \"error\";\n\n/** Individual check result */\nexport interface DoctorCheckResult {\n\tname: string;\n\tstatus: DoctorStatus;\n\tmessage: string;\n}\n\n/** Complete doctor check result */\nexport interface DoctorResult {\n\tchecks: DoctorCheckResult[];\n\tsummary: string;\n}\n\n/**\n * Options for running doctor checks.\n */\nexport interface DoctorOptions {\n\t/** Current working directory (defaults to process.cwd()) */\n\tcwd?: string;\n\t/** Resource loader for getting loaded resources */\n\tresourceLoader?: {\n\t\tgetExtensions: () => { extensions: unknown[] };\n\t\tgetSkills: () => { skills: unknown[] };\n\t\tgetThemes: () => { themes: unknown[] };\n\t};\n\t/** Model registry for checking model configuration */\n\tmodelRegistry?: {\n\t\tgetAvailable: () => Array<{ id: string; provider: string }>;\n\t\tgetApiKey: (model: { provider: string }) => Promise<string | null | undefined>;\n\t};\n\t/** Current model (if set) */\n\tcurrentModel?: {\n\t\tid: string;\n\t\tprovider: string;\n\t};\n\t/** Extension runner for checking loaded extensions */\n\textensionRunner?: {\n\t\tgetExtensionPaths: () => string[];\n\t\tgetRegisteredCommands: () => unknown[];\n\t};\n}\n\n/**\n * Find config files (JENSEN.md, AGENTS.md, CLAUDE.md) walking up from cwd to root.\n */\nasync function findConfigFiles(cwd: string): Promise<string[]> {\n\tconst configFiles = [\"JENSEN.md\", \"AGENTS.md\", \"CLAUDE.md\"];\n\tconst found: string[] = [];\n\tlet current = cwd;\n\n\twhile (true) {\n\t\tfor (const file of configFiles) {\n\t\t\tconst filePath = path.join(current, file);\n\t\t\ttry {\n\t\t\t\tawait fs.promises.access(filePath);\n\t\t\t\tfound.push(filePath);\n\t\t\t} catch {\n\t\t\t\t// File doesn't exist\n\t\t\t}\n\t\t}\n\n\t\t// Check if we've reached the root\n\t\tconst parent = path.dirname(current);\n\t\tif (parent === current) break;\n\t\tcurrent = parent;\n\t}\n\n\treturn found;\n}\n\n/**\n * Check if .pi/settings.json exists and is valid JSON.\n */\nasync function checkSettingsFile(cwd: string): Promise<DoctorCheckResult> {\n\tconst settingsPath = path.join(cwd, \".pi\", \"settings.json\");\n\n\ttry {\n\t\tawait fs.promises.access(settingsPath);\n\t\tconst content = await fs.promises.readFile(settingsPath, \"utf-8\");\n\t\tJSON.parse(content); // Validate JSON\n\t\treturn {\n\t\t\tname: \"settings\",\n\t\t\tstatus: \"ok\",\n\t\t\tmessage: \".pi/settings.json exists and is valid JSON\",\n\t\t};\n\t} catch (error) {\n\t\tif (error instanceof SyntaxError) {\n\t\t\treturn {\n\t\t\t\tname: \"settings\",\n\t\t\t\tstatus: \"error\",\n\t\t\t\tmessage: \".pi/settings.json exists but is invalid JSON\",\n\t\t\t};\n\t\t}\n\t\t// File doesn't exist or other error\n\t\treturn {\n\t\t\tname: \"settings\",\n\t\t\tstatus: \"warn\",\n\t\t\tmessage: \".pi/settings.json not found\",\n\t\t};\n\t}\n}\n\n/**\n * Check loaded extensions and skills.\n */\nfunction checkResources(options: DoctorOptions): DoctorCheckResult[] {\n\tconst results: DoctorCheckResult[] = [];\n\n\t// Check extensions\n\tif (options.resourceLoader) {\n\t\tconst extensions = options.resourceLoader.getExtensions().extensions;\n\t\tresults.push({\n\t\t\tname: \"extensions\",\n\t\t\tstatus: extensions.length > 0 ? \"ok\" : \"warn\",\n\t\t\tmessage: `${extensions.length} extension${extensions.length !== 1 ? \"s\" : \"\"} loaded`,\n\t\t});\n\t} else if (options.extensionRunner) {\n\t\tconst extPaths = options.extensionRunner.getExtensionPaths();\n\t\tresults.push({\n\t\t\tname: \"extensions\",\n\t\t\tstatus: extPaths.length > 0 ? \"ok\" : \"warn\",\n\t\t\tmessage: `${extPaths.length} extension${extPaths.length !== 1 ? \"s\" : \"\"} loaded`,\n\t\t});\n\t} else {\n\t\tresults.push({\n\t\t\tname: \"extensions\",\n\t\t\tstatus: \"warn\",\n\t\t\tmessage: \"Extension info not available\",\n\t\t});\n\t}\n\n\t// Check skills\n\tif (options.resourceLoader) {\n\t\tconst skills = options.resourceLoader.getSkills().skills;\n\t\tresults.push({\n\t\t\tname: \"skills\",\n\t\t\tstatus: skills.length > 0 ? \"ok\" : \"warn\",\n\t\t\tmessage: `${skills.length} skill${skills.length !== 1 ? \"s\" : \"\"} loaded`,\n\t\t});\n\t} else {\n\t\tresults.push({\n\t\t\tname: \"skills\",\n\t\t\tstatus: \"warn\",\n\t\t\tmessage: \"Skills info not available\",\n\t\t});\n\t}\n\n\t// Check themes\n\tif (options.resourceLoader) {\n\t\tconst themes = options.resourceLoader.getThemes().themes;\n\t\tresults.push({\n\t\t\tname: \"themes\",\n\t\t\tstatus: themes.length > 0 ? \"ok\" : \"warn\",\n\t\t\tmessage: `${themes.length} theme${themes.length !== 1 ? \"s\" : \"\"} loaded`,\n\t\t});\n\t} else {\n\t\tresults.push({\n\t\t\tname: \"themes\",\n\t\t\tstatus: \"warn\",\n\t\t\tmessage: \"Themes info not available\",\n\t\t});\n\t}\n\n\treturn results;\n}\n\n/**\n * Check current model configuration.\n */\nasync function checkModel(options: DoctorOptions): Promise<DoctorCheckResult> {\n\tif (!options.currentModel) {\n\t\treturn {\n\t\t\tname: \"model\",\n\t\t\tstatus: \"warn\",\n\t\t\tmessage: \"No model selected\",\n\t\t};\n\t}\n\n\tconst model = options.currentModel;\n\n\t// Check if API key is available\n\tif (options.modelRegistry) {\n\t\tconst apiKey = await options.modelRegistry.getApiKey(model);\n\t\tif (apiKey) {\n\t\t\treturn {\n\t\t\t\tname: \"model\",\n\t\t\t\tstatus: \"ok\",\n\t\t\t\tmessage: `${model.provider}/${model.id} (API key available)`,\n\t\t\t};\n\t\t}\n\t\treturn {\n\t\t\tname: \"model\",\n\t\t\tstatus: \"error\",\n\t\t\tmessage: `${model.provider}/${model.id} (no API key)`,\n\t\t};\n\t}\n\n\treturn {\n\t\tname: \"model\",\n\t\tstatus: \"ok\",\n\t\tmessage: `${model.provider}/${model.id}`,\n\t};\n}\n\n/**\n * Check git worktrees for potential conflicts or issues.\n */\nfunction checkWorktrees(cwd: string): DoctorCheckResult {\n\ttry {\n\t\t// Verify we're in a git repo first\n\t\texecSync(\"git rev-parse --git-dir\", { cwd, stdio: \"ignore\" });\n\t} catch {\n\t\treturn {\n\t\t\tname: \"worktrees\",\n\t\t\tstatus: \"ok\",\n\t\t\tmessage: \"Not a git repository — worktree check skipped\",\n\t\t};\n\t}\n\n\tconst worktrees = parseWorktreeList(cwd);\n\n\tif (worktrees.length === 0) {\n\t\treturn {\n\t\t\tname: \"worktrees\",\n\t\t\tstatus: \"ok\",\n\t\t\tmessage: \"1 worktree (no additional worktrees)\",\n\t\t};\n\t}\n\n\tconst currentBranch = (() => {\n\t\ttry {\n\t\t\treturn execSync(\"git branch --show-current\", { cwd, encoding: \"utf-8\" }).trim() || null;\n\t\t} catch {\n\t\t\treturn null;\n\t\t}\n\t})();\n\n\tconst warnings: string[] = [];\n\n\t// Multiple worktrees\n\tif (worktrees.length > 1) {\n\t\twarnings.push(`${worktrees.length} worktrees total`);\n\t}\n\n\t// Detached HEAD\n\tconst currentWt = worktrees.find((wt) => {\n\t\tconst normCwd = cwd.replace(/[/\\\\]$/, \"\");\n\t\tconst normPath = wt.path.replace(/[/\\\\]$/, \"\");\n\t\treturn normCwd === normPath || cwd.startsWith(`${wt.path}/`) || cwd.startsWith(`${wt.path}\\\\`);\n\t});\n\n\tif (currentWt && currentWt.branch === null) {\n\t\twarnings.push(\"current worktree is detached HEAD\");\n\t}\n\n\t// Locked worktrees\n\tfor (const wt of worktrees) {\n\t\tif (wt.locked) {\n\t\t\twarnings.push(`worktree ${path.basename(wt.path)} is locked${wt.lockReason ? `: ${wt.lockReason}` : \"\"}`);\n\t\t}\n\t}\n\n\t// Prunable worktrees\n\tfor (const wt of worktrees) {\n\t\tif (wt.prunable) {\n\t\t\twarnings.push(`worktree ${path.basename(wt.path)} is prunable`);\n\t\t}\n\t}\n\n\t// Same branch in multiple worktrees\n\tif (currentBranch) {\n\t\tconst sameBranchWorktrees = worktrees.filter((wt) => wt.branch === currentBranch);\n\t\tif (sameBranchWorktrees.length > 1) {\n\t\t\twarnings.push(\n\t\t\t\t`branch \"${currentBranch}\" is checked out in ${sameBranchWorktrees.length} worktrees — potential for conflicting edits`,\n\t\t\t);\n\t\t}\n\t}\n\n\tif (warnings.length > 0) {\n\t\treturn {\n\t\t\tname: \"worktrees\",\n\t\t\tstatus: \"warn\",\n\t\t\tmessage: warnings.join(\"; \"),\n\t\t};\n\t}\n\n\treturn {\n\t\tname: \"worktrees\",\n\t\tstatus: \"ok\",\n\t\tmessage: `${worktrees.length} worktree${worktrees.length !== 1 ? \"s\" : \"\"}, no issues detected`,\n\t};\n}\n\n/**\n * Check git repository status.\n */\nfunction checkGit(cwd: string): DoctorCheckResult {\n\ttry {\n\t\texecSync(\"git rev-parse --git-dir\", { cwd, stdio: \"ignore\" });\n\n\t\t// Get current branch\n\t\tconst branch = execSync(\"git rev-parse --abbrev-ref HEAD\", { cwd, encoding: \"utf-8\" }).trim();\n\n\t\t// Check for uncommitted changes\n\t\tconst status = execSync(\"git status --porcelain\", { cwd, encoding: \"utf-8\" }).trim();\n\t\tconst hasChanges = status.length > 0;\n\n\t\treturn {\n\t\t\tname: \"git\",\n\t\t\tstatus: hasChanges ? \"warn\" : \"ok\",\n\t\t\tmessage: `Branch: ${branch}${hasChanges ? \" (uncommitted changes)\" : \"\"}`,\n\t\t};\n\t} catch {\n\t\treturn {\n\t\t\tname: \"git\",\n\t\t\tstatus: \"warn\",\n\t\t\tmessage: \"Not a git repository\",\n\t\t};\n\t}\n}\n\n/**\n * Check shell configuration and basic commands.\n */\nfunction checkShell(): DoctorCheckResult {\n\tconst shell = process.env.SHELL || \"unknown\";\n\tconst shellName = path.basename(shell);\n\n\t// Check if basic commands work\n\ttry {\n\t\texecSync(\"echo\", { stdio: \"ignore\" });\n\t\treturn {\n\t\t\tname: \"shell\",\n\t\t\tstatus: \"ok\",\n\t\t\tmessage: `Shell: ${shellName}`,\n\t\t};\n\t} catch {\n\t\treturn {\n\t\t\tname: \"shell\",\n\t\t\tstatus: \"error\",\n\t\t\tmessage: `Shell: ${shellName} (basic commands not working)`,\n\t\t};\n\t}\n}\n\n/**\n * Result of a PowerShell validation probe.\n */\nexport interface PowerShellDoctorResult {\n\t/** Whether getPowerShellConfig() succeeded */\n\tconfigured: boolean;\n\t/** Whether the pwsh executable was found on PATH */\n\texecutableFound: boolean;\n\t/** Absolute path to the pwsh executable, or null */\n\texecutablePath: string | null;\n\t/** Whether pwsh --version succeeded */\n\tversionDetected: boolean;\n\t/** The detected version string, or null */\n\tversion: string | null;\n\t/** Whether the non-interactive probe (pwsh -NoProfile -NonInteractive -Command 'exit 0') passed */\n\tnonInteractiveProbePassed: boolean;\n\t/** Whether the exit code probe (exit 0) correctly returned exit code 0 */\n\texitCodeProbePassed: boolean;\n}\n\n/**\n * Probe PowerShell version by executing \"pwsh --version\".\n * Returns the version string or null if it fails.\n */\nfunction probePowerShellVersion(pwshPath: string): string | null {\n\ttry {\n\t\tconst result = spawnSync(pwshPath, [\"--version\"], {\n\t\t\tencoding: \"utf-8\",\n\t\t\ttimeout: 5000,\n\t\t\tstdio: [\"ignore\", \"pipe\", \"pipe\"],\n\t\t});\n\t\tif (result.status === 0 && result.stdout) {\n\t\t\treturn result.stdout.trim();\n\t\t}\n\t\treturn null;\n\t} catch {\n\t\treturn null;\n\t}\n}\n\n/**\n * Probe PowerShell non-interactive execution.\n * Runs: pwsh -NoLogo -NoProfile -NonInteractive -Command 'exit 0'\n */\nfunction probePowerShellNonInteractive(pwshPath: string): boolean {\n\ttry {\n\t\tconst result = spawnSync(pwshPath, [\"-NoLogo\", \"-NoProfile\", \"-NonInteractive\", \"-Command\", \"exit 0\"], {\n\t\t\tencoding: \"utf-8\",\n\t\t\ttimeout: 5000,\n\t\t\tstdio: [\"ignore\", \"pipe\", \"pipe\"],\n\t\t});\n\t\treturn result.status === 0;\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/**\n * Check PowerShell Core (pwsh) availability for cross-platform integration tests.\n * Not a requirement for all Linux users — only for PowerShell Core integration.\n *\n * Reports structured diagnostics: configured, executableFound, executablePath,\n * versionDetected, version, nonInteractiveProbePassed, exitCodeProbePassed.\n */\nfunction checkPowerShell(): { result: DoctorCheckResult; details: PowerShellDoctorResult } {\n\tconst details: PowerShellDoctorResult = {\n\t\tconfigured: false,\n\t\texecutableFound: false,\n\t\texecutablePath: null,\n\t\tversionDetected: false,\n\t\tversion: null,\n\t\tnonInteractiveProbePassed: false,\n\t\texitCodeProbePassed: false,\n\t};\n\n\ttry {\n\t\t// Do NOT reset the shell config cache. Doctor must not alter global state.\n\t\t// getPowerShellConfig() uses its own cache; we read it without invalidation.\n\t\tconst config = getPowerShellConfig();\n\t\tdetails.configured = true;\n\t\tdetails.executableFound = true;\n\t\tdetails.executablePath = config.shell;\n\n\t\t// Probe version\n\t\tconst version = probePowerShellVersion(config.shell);\n\t\tif (version) {\n\t\t\tdetails.versionDetected = true;\n\t\t\tdetails.version = version;\n\t\t}\n\n\t\t// Probe non-interactive execution\n\t\tconst nonInteractiveOk = probePowerShellNonInteractive(config.shell);\n\t\tdetails.nonInteractiveProbePassed = nonInteractiveOk;\n\t\tdetails.exitCodeProbePassed = nonInteractiveOk;\n\n\t\tconst versionInfo = details.version ? ` (${details.version})` : \"\";\n\t\tconst probeStatus = nonInteractiveOk ? \"\" : \" — non-interactive probe failed\";\n\n\t\treturn {\n\t\t\tresult: {\n\t\t\t\tname: \"pwsh\",\n\t\t\t\tstatus: nonInteractiveOk ? \"ok\" : \"warn\",\n\t\t\t\tmessage: `pwsh ${config.flavor}${versionInfo} available at ${config.shell}${probeStatus}`,\n\t\t\t},\n\t\t\tdetails,\n\t\t};\n\t} catch {\n\t\treturn {\n\t\t\tresult: {\n\t\t\t\tname: \"pwsh\",\n\t\t\t\tstatus: \"warn\",\n\t\t\t\tmessage: \"pwsh not found — PowerShell Core integration tests will be skipped\",\n\t\t\t},\n\t\t\tdetails,\n\t\t};\n\t}\n}\n\n/**\n * Check the TODO subsystem health. Read-only.\n * Accepts an optional diagnostics snapshot from an active TodoEngine.\n */\nexport function checkTodoHealth(diagnostics?: {\n\tscopeId?: string;\n\tprogressEpoch?: number;\n\tsnapshotCount?: number;\n\tledgerCount?: number;\n\teventCount?: number;\n\tchainActive?: boolean;\n\tchainConsecutive?: number;\n\tisLoopBlocked?: boolean;\n}): DoctorCheckResult {\n\tif (!diagnostics) {\n\t\t// No active session TODOs to inspect — this is a warning, not an error.\n\t\treturn {\n\t\t\tname: \"todo\",\n\t\t\tstatus: \"warn\",\n\t\t\tmessage: \"No active TODO session to inspect (this is expected outside a session)\",\n\t\t};\n\t}\n\n\tconst problems: string[] = [];\n\tif (diagnostics.chainConsecutive && diagnostics.chainConsecutive >= 3) {\n\t\tproblems.push(`no-progress loop chain active (${diagnostics.chainConsecutive} consecutive)`);\n\t}\n\tif (diagnostics.ledgerCount && diagnostics.ledgerCount > 200) {\n\t\tproblems.push(\"idempotency ledger near retention bound\");\n\t}\n\tif (diagnostics.eventCount && diagnostics.eventCount > 400) {\n\t\tproblems.push(\"event log near retention bound\");\n\t}\n\n\tif (problems.length > 0) {\n\t\treturn {\n\t\t\tname: \"todo\",\n\t\t\tstatus: \"warn\",\n\t\t\tmessage: problems.join(\"; \"),\n\t\t};\n\t}\n\n\treturn {\n\t\tname: \"todo\",\n\t\tstatus: \"ok\",\n\t\tmessage: `TODO subsystem healthy (${diagnostics.scopeId ?? \"session\"}): ${String(diagnostics.snapshotCount ?? 0)} snapshots, ${String(diagnostics.ledgerCount ?? 0)} applied intents`,\n\t};\n}\n\n/**\n * Run all doctor diagnostics checks.\n */\nexport async function runDoctorChecks(options: DoctorOptions = {}): Promise<DoctorResult> {\n\tconst cwd = options.cwd || process.cwd();\n\tconst checks: DoctorCheckResult[] = [];\n\n\t// 1. Config files check\n\tconst configFiles = await findConfigFiles(cwd);\n\tchecks.push({\n\t\tname: \"config\",\n\t\tstatus: configFiles.length > 0 ? \"ok\" : \"warn\",\n\t\tmessage:\n\t\t\tconfigFiles.length > 0\n\t\t\t\t? `Found ${configFiles.length} config file(s): ${configFiles.map((f) => `${path.basename(path.dirname(f))}/${path.basename(f)}`).join(\", \")}`\n\t\t\t\t: \"No config files (JENSEN.md, AGENTS.md, CLAUDE.md) found\",\n\t});\n\n\t// 2. Settings file check\n\tconst settingsCheck = await checkSettingsFile(cwd);\n\tchecks.push(settingsCheck);\n\n\t// 3. Extensions, skills, themes check\n\tconst resourceChecks = checkResources(options);\n\tchecks.push(...resourceChecks);\n\n\t// 4. Model check\n\tconst modelCheck = await checkModel(options);\n\tchecks.push(modelCheck);\n\n\t// 5. Git check\n\tconst gitCheck = checkGit(cwd);\n\tchecks.push(gitCheck);\n\n\t// 6. Worktree check\n\tconst worktreeCheck = checkWorktrees(cwd);\n\tchecks.push(worktreeCheck);\n\n\t// 7. Shell check\n\tconst shellCheck = checkShell();\n\tchecks.push(shellCheck);\n\n\t// 8. PowerShell Core check\n\tconst psCheck = checkPowerShell();\n\tchecks.push(psCheck.result);\n\n\t// Generate summary\n\tconst okCount = checks.filter((c) => c.status === \"ok\").length;\n\tconst warnCount = checks.filter((c) => c.status === \"warn\").length;\n\tconst errorCount = checks.filter((c) => c.status === \"error\").length;\n\n\tconst summary = `Diagnostics complete: ${okCount} OK, ${warnCount} warning${warnCount !== 1 ? \"s\" : \"\"}, ${errorCount} error${errorCount !== 1 ? \"s\" : \"\"}`;\n\n\treturn {\n\t\tchecks,\n\t\tsummary,\n\t};\n}\n\n/**\n * Format doctor result as colored text for display in TUI.\n * The theme object should have fg(color, text) and bold(text) methods.\n */\n// Using `any` for the color parameter to allow both string and ThemeColor types\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function formatDoctorResult(\n\tresult: DoctorResult,\n\ttheme: { fg: (color: any, text: string) => string; bold: (text: string) => string },\n): string {\n\tconst lines: string[] = [];\n\n\tlines.push(theme.bold(\"System Diagnostics\"));\n\tlines.push(\"\");\n\n\tfor (const check of result.checks) {\n\t\tlet statusIcon: string;\n\t\tlet statusColor: string;\n\n\t\tswitch (check.status) {\n\t\t\tcase \"ok\":\n\t\t\t\tstatusIcon = \"✓\";\n\t\t\t\tstatusColor = \"success\";\n\t\t\t\tbreak;\n\t\t\tcase \"warn\":\n\t\t\t\tstatusIcon = \"⚠\";\n\t\t\t\tstatusColor = \"warning\";\n\t\t\t\tbreak;\n\t\t\tcase \"error\":\n\t\t\t\tstatusIcon = \"✗\";\n\t\t\t\tstatusColor = \"error\";\n\t\t\t\tbreak;\n\t\t}\n\n\t\tlines.push(`${theme.fg(statusColor, statusIcon)} ${check.name}: ${check.message}`);\n\t}\n\n\tlines.push(\"\");\n\tlines.push(theme.fg(\"dim\", result.summary));\n\n\treturn lines.join(\"\\n\");\n}\n"]}