import type { Platform, PlatformContext, PlatformCapabilities } from "../platform/types.js"; import { existsSync, writeFileSync, unlinkSync } from "node:fs"; import { join } from "node:path"; import { DEFAULT_CONFIG } from "../config/defaults.js"; import { formatConfigErrors, inspectConfig } from "../config/loader.js"; import { detectContextMode } from "../context-mode/detector.js"; import { isLspAvailable } from "../lsp/detector.js"; import { summarizeEnabledGates } from "../quality/setup.js"; import { DEPENDENCIES } from "../deps/registry.js"; import { formatReliabilitySection, loadReliabilitySummaries } from "../storage/reliability-metrics.js"; import { getMetricsStore, getSessionId } from "../context-mode/hooks.js"; import { getProjectStatePath, getProjectStateDir } from "../workspace/state-paths.js"; import { basename } from "node:path"; import { execCli } from "../utils/exec-cli.js"; export interface CheckResult { name: string; presence: { ok: boolean; detail: string }; functional?: { ok: boolean; detail: string }; } export interface SectionResult { title: string; checks: CheckResult[]; } const LABEL_WIDTH = 19; // " Context Mode ... " padded width export function formatCheckResult(check: CheckResult): string[] { const lines: string[] = []; const icon = (ok: boolean) => ok ? "✓" : "✗"; const dots = ".".repeat(Math.max(2, LABEL_WIDTH - check.name.length - 2)); const label = ` ${check.name} ${dots} `; const indent = " ".repeat(label.length); lines.push(`${label}${icon(check.presence.ok)} ${check.presence.detail}`); if (check.functional) { lines.push(`${indent}${icon(check.functional.ok)} ${check.functional.detail}`); } return lines; } export async function checkPlatform(platform: Platform): Promise { const name = platform.name.toUpperCase(); const presence = { ok: true, detail: `${name} detected` }; try { const r = await platform.exec("node", ["--version"]); return { name: "Platform", presence, functional: { ok: r.code === 0, detail: r.code === 0 ? "exec works" : "exec failed" } }; } catch { return { name: "Platform", presence, functional: { ok: false, detail: "exec failed" } }; } } export async function checkConfig(platform: Platform, cwd: string): Promise { const projectPath = platform.paths.project(cwd, "config.json"); const globalPath = platform.paths.global("config.json"); const projectExists = existsSync(projectPath); const globalExists = existsSync(globalPath); const inspection = inspectConfig(platform.paths, cwd); const config = inspection.effectiveConfig ?? DEFAULT_CONFIG; if (!projectExists && !globalExists) { return { name: "Config", presence: { ok: false, detail: "No config.json found (using defaults)" }, functional: { ok: true, detail: `quality.gates: ${summarizeEnabledGates(config.quality.gates)}` }, }; } const foundPath = projectExists ? `${platform.paths.dotDir}/supipowers/config.json (project)` : `~/${platform.paths.dotDir}/supipowers/config.json (global)`; if (inspection.parseErrors.length > 0 || inspection.validationErrors.length > 0) { return { name: "Config", presence: { ok: true, detail: `Found ${foundPath}` }, functional: { ok: false, detail: formatConfigErrors(inspection) }, }; } return { name: "Config", presence: { ok: true, detail: `Found ${foundPath}` }, functional: { ok: true, detail: `quality.gates: ${summarizeEnabledGates(config.quality.gates)}` }, }; } export async function checkStorage(platform: Platform, cwd: string): Promise { const dir = platform.paths.project(cwd); if (!existsSync(dir)) { return { name: "Storage", presence: { ok: false, detail: `${platform.paths.dotDir}/supipowers/ not found` } }; } const tmpFile = join(dir, `.doctor-check-${Date.now()}`); try { writeFileSync(tmpFile, "ok"); unlinkSync(tmpFile); return { name: "Storage", presence: { ok: true, detail: `${platform.paths.dotDir}/supipowers/ exists` }, functional: { ok: true, detail: "Writable" }, }; } catch { return { name: "Storage", presence: { ok: true, detail: `${platform.paths.dotDir}/supipowers/ exists` }, functional: { ok: false, detail: "Not writable" }, }; } } export async function checkEventStore(): Promise { const bunSqliteDep = DEPENDENCIES.find((d) => d.binary === "__bun_sqlite__"); if (!bunSqliteDep) { return { name: "EventStore", presence: { ok: false, detail: "bun:sqlite dependency not registered" } }; } const { installed } = await bunSqliteDep.checkFn(async () => ({ stdout: "", stderr: "", code: 1 })); if (installed) { return { name: "EventStore", presence: { ok: true, detail: "bun:sqlite available" }, functional: { ok: true, detail: "SQLite + FTS5 functional" } }; } return { name: "EventStore", presence: { ok: false, detail: "bun:sqlite not available (requires Bun runtime)" } }; } export async function checkGit(platform: Platform): Promise { try { const versionResult = await platform.exec("git", ["--version"]); if (versionResult.code !== 0) { return { name: "Git", presence: { ok: false, detail: "git not found" } }; } const version = versionResult.stdout.trim().replace("git version ", ""); const presence = { ok: true, detail: `v${version}` }; const repoResult = await platform.exec("git", ["rev-parse", "--is-inside-work-tree"]); if (repoResult.code !== 0) { return { name: "Git", presence, functional: { ok: false, detail: "Not inside a git repo" } }; } const branchResult = await platform.exec("git", ["branch", "--show-current"]); const branch = branchResult.stdout.trim() || "detached HEAD"; return { name: "Git", presence, functional: { ok: true, detail: `Repo detected (${branch})` } }; } catch { return { name: "Git", presence: { ok: false, detail: "git not found" } }; } } /** Core infra checks where a presence failure is critical (blocks the extension) */ const CRITICAL_CHECKS = new Set(["Platform", "Config", "Git"]); export function formatSummary(sections: SectionResult[]): string { let passed = 0; let warnings = 0; let critical = 0; for (const section of sections) { for (const check of section.checks) { const presenceOk = check.presence.ok; const functionalOk = check.functional ? check.functional.ok : true; if (presenceOk && functionalOk) { passed++; } else if (!presenceOk && CRITICAL_CHECKS.has(check.name)) { critical++; } else { warnings++; } } } return `Summary: ${passed} passed, ${warnings} warning${warnings !== 1 ? "s" : ""}, ${critical} critical`; } export async function checkGitHubCli(platform: Platform): Promise { try { const vResult = await platform.exec("gh", ["--version"]); if (vResult.code !== 0) { return { name: "GitHub CLI", presence: { ok: false, detail: "gh not found" } }; } const vMatch = vResult.stdout.match(/gh version ([\d.]+)/); const version = vMatch ? vMatch[1] : "unknown"; const presence = { ok: true, detail: `v${version}` }; const authResult = await platform.exec("gh", ["auth", "status"]); const output = authResult.stderr || authResult.stdout; const userMatch = output.match(/account\s+(\S+)/); if (authResult.code === 0 && userMatch) { return { name: "GitHub CLI", presence, functional: { ok: true, detail: `Authenticated (${userMatch[1]})` } }; } return { name: "GitHub CLI", presence, functional: { ok: false, detail: "Not authenticated" } }; } catch { return { name: "GitHub CLI", presence: { ok: false, detail: "gh not found" } }; } } export function checkLsp(activeTools: string[]): CheckResult { const available = isLspAvailable(activeTools); return { name: "LSP", presence: { ok: available, detail: available ? "LSP tool detected" : "LSP tool not available" }, }; } export function checkMcp(activeTools: string[]): CheckResult { const mcpTools = activeTools.filter((t) => t.startsWith("mcp__")); if (mcpTools.length === 0) { return { name: "MCP", presence: { ok: false, detail: "No MCP tools detected" } }; } const servers = new Set(); for (const tool of mcpTools) { const withoutPrefix = tool.slice("mcp__".length); const lastSep = withoutPrefix.lastIndexOf("__"); if (lastSep > 0) { servers.add(withoutPrefix.slice(0, lastSep)); } } return { name: "MCP", presence: { ok: true, detail: "MCP tools detected" }, functional: { ok: true, detail: `${mcpTools.length} tools, ${servers.size} server${servers.size !== 1 ? "s" : ""}` }, }; } const CTX_TOOL_NAMES: Record = { ctxExecute: "ctx_execute", ctxBatchExecute: "ctx_batch_execute", ctxExecuteFile: "ctx_execute_file", ctxIndex: "ctx_index", ctxSearch: "ctx_search", ctxFetchAndIndex: "ctx_fetch_and_index", ctxStats: "ctx_stats", ctxPurge: "ctx_purge", ctxRepomap: "ctx_repomap", ctxSymbol: "ctx_symbol", }; export function checkContextMode(activeTools: string[]): CheckResult { // Reflect the same active-tool-aware view the runtime routing/prompt layers // use, so the doctor cannot claim ctx_* tools are available when the model // can't actually call them. const status = detectContextMode(activeTools); const foundNames = Object.entries(status.tools) .filter(([, value]) => value) .map(([key]) => CTX_TOOL_NAMES[key] || key); if (!status.available) { return { name: "Context Mode", presence: { ok: false, detail: "No ctx_* tools active in this session" }, functional: { ok: false, detail: "Inactive — routing rescue path will be skipped this turn" }, }; } return { name: "Context Mode", presence: { ok: true, detail: "Native tools active" }, functional: { ok: true, detail: foundNames.join(", ") }, }; } export function checkLazyTools(platform: Platform, cwd: string): CheckResult { const inspection = (() => { try { return inspectConfig(platform.paths, cwd); } catch (error) { return error instanceof Error ? error : new Error(String(error)); } })(); if (inspection instanceof Error) { return { name: "Lazy Tools", presence: { ok: false, detail: `Unable to inspect config: ${inspection.message}` }, }; } if (!inspection.effectiveConfig) { return { name: "Lazy Tools", presence: { ok: false, detail: "Skipped: config is invalid" }, functional: { ok: false, detail: formatConfigErrors(inspection) }, }; } const config = inspection.effectiveConfig; if (!config.contextMode.enabled) { return { name: "Lazy Tools", presence: { ok: false, detail: "Disabled (contextMode.enabled = false)" }, }; } if (!config.contextMode.lazyTools.enabled) { return { name: "Lazy Tools", presence: { ok: false, detail: "Disabled (contextMode.lazyTools.enabled = false)" }, }; } if (!platform.capabilities.activeToolFiltering) { return { name: "Lazy Tools", presence: { ok: false, detail: "Degraded: contextMode.lazyTools.enabled is true but active-tool filtering is unavailable; upgrade OMP/supipowers adapter or disable contextMode.lazyTools.enabled.", }, }; } return { name: "Lazy Tools", presence: { ok: true, detail: "L7 active-tool filtering available" }, functional: { ok: true, detail: `mode: ${config.contextMode.lazyTools.mode}` }, }; } /** Metrics health checks for the L1 measurement layer. */ export function checkMetrics( platform: Platform, cwd: string, now = Date.now(), ): { section: SectionResult; absDbPath: string } { const absDbPath = getProjectStatePath( platform.paths, cwd, "sessions", "metrics.db", ); const store = getMetricsStore(); const sessionId = getSessionId(); const reachable: CheckResult = store ? { name: "metrics-db-reachable", presence: { ok: true, detail: `Metrics DB: ${absDbPath}` }, functional: { ok: true, detail: "Open" }, } : (() => { // Differentiate the two reasons `getMetricsStore()` returns null: // 1. `contextMode.enabled = false` — the hook never registered, so the // remediation is to flip the config flag. // 2. Init failed (FS perms, sqlite error, etc.) — the hook registered // but `ensureMetricsStore` returned null and logged the error. // Saying "run /supi:context to bootstrap" was wrong: that command only // reads the ref and never initializes the store. const enabled = (() => { try { return inspectConfig(platform.paths, cwd).effectiveConfig?.contextMode?.enabled ?? true; } catch { return true; } })(); const detail = enabled ? `Failed to initialize \u2014 check session_start logs (${absDbPath})` : `Disabled (contextMode.enabled = false) \u2014 set it to true in config to enable metrics (${absDbPath})`; return { name: "metrics-db-reachable", presence: { ok: false, detail }, }; })(); const writeFailures = (() => { try { return store?.getSessionWriteFailures(sessionId) ?? 0; } catch { return 0; } })(); const writeHealthy: CheckResult = store ? { name: "metrics-write-healthy", presence: { ok: writeFailures === 0, detail: writeFailures === 0 ? "No write failures this session" : `${writeFailures} write_failures \u2014 see /supi:clear or filesystem health`, }, } : { name: "metrics-write-healthy", presence: { ok: false, detail: "Skipped \u2014 metrics store unavailable" }, }; const lastPruneAt = (() => { try { // Use the store's bound slug rather than re-deriving from cwd; the store // is the source of truth for which project_meta_metrics row matters. const slug = store?.projectSlug ?? basename(getProjectStateDir(platform.paths, cwd)); return store?.getProjectMeta(slug)?.last_prune_at ?? null; } catch { return null; } })(); const ONE_DAY_MS = 24 * 60 * 60 * 1000; const pruneFresh: CheckResult = !store ? { name: "metrics-prune-fresh", presence: { ok: false, detail: "Skipped \u2014 metrics store unavailable" }, } : lastPruneAt === null ? { name: "metrics-prune-fresh", presence: { ok: false, detail: "Never pruned; runs on session_shutdown", }, } : now - lastPruneAt <= ONE_DAY_MS ? { name: "metrics-prune-fresh", presence: { ok: true, detail: "Pruned within 24h" }, } : { name: "metrics-prune-fresh", presence: { ok: false, detail: `Stale (${Math.round((now - lastPruneAt) / ONE_DAY_MS)}d ago)`, }, }; return { section: { title: `Metrics (${absDbPath})`, checks: [reachable, writeHealthy, pruneFresh], }, absDbPath, }; } export async function checkNpm(platform: Platform): Promise { try { const vResult = await execCli((cmd, args, opts) => platform.exec(cmd, args, opts), "npm", ["--version"]); if (vResult.code !== 0) { return { name: "npm", presence: { ok: false, detail: "npm not found" } }; } const version = vResult.stdout.trim(); const presence = { ok: true, detail: `v${version}` }; const pingResult = await execCli((cmd, args, opts) => platform.exec(cmd, args, opts), "npm", ["ping"]); if (pingResult.code === 0) { return { name: "npm", presence, functional: { ok: true, detail: "Registry reachable" } }; } return { name: "npm", presence, functional: { ok: false, detail: "Registry unreachable" } }; } catch { return { name: "npm", presence: { ok: false, detail: "npm not found" } }; } } export async function runDoctorChecks(platform: Platform, cwd: string): Promise { const activeTools = platform.getActiveTools(); // Core Infrastructure const core: CheckResult[] = [ await checkPlatform(platform), await checkConfig(platform, cwd), await checkStorage(platform, cwd), await checkEventStore(), await checkGit(platform), ]; // Integrations const mcpResult = checkMcp(activeTools); const integrations: CheckResult[] = [ await checkGitHubCli(platform), checkLsp(activeTools), mcpResult, checkContextMode(activeTools), checkLazyTools(platform, cwd), await checkNpm(platform), ]; // Platform Capabilities const mcpAvailable = mcpResult.presence.ok; const capChecks = checkCapabilities(platform.capabilities, mcpAvailable); const metrics = checkMetrics(platform, cwd); return [ { title: "Core Infrastructure", checks: core }, { title: "Integrations", checks: integrations }, { title: "Platform Capabilities", checks: capChecks }, metrics.section, ]; } /** * Recommended OMP settings that materially improve supipowers UX. * * These are *suggestions*, not health checks — supipowers cannot read OMP's * `settings.json` directly, so we cannot detect whether the user has the * setting on. We surface them at the bottom of `/supi:doctor` as a * non-failing tip block. */ export function getDoctorRecommendations(): string[] { return [ "Set `tools.elideFileMutationInputs: true` (OMP ≥14.7.0) — elides `write`/`edit`/`apply_patch` payloads from history after success. Saves significant context on long sessions like `/supi:ultraplan execute` and `/supi:harness implement`.", "Update to OMP ≥14.7.2 — fixes the `Working…` spinner staying active after read-only commands such as `/supi:status`, `/supi:doctor`, `/supi:context`, and `/supi:clear`. (oh-my-pi#927)", "Use OMP ≥15.1.7 for best reliability — includes ACP fixes for command-driven agent handoffs and permission prompts, plus accurate provider-scoped `/fast` status-line indicators.", ]; } function formatRecommendationsSection(recommendations: string[]): string[] { if (recommendations.length === 0) return []; const lines: string[] = ["Recommendations"]; for (const tip of recommendations) { lines.push(` → ${tip}`); } lines.push(""); return lines; } function formatReport(sections: SectionResult[]): string { const lines: string[] = ["/supi:doctor", ""]; for (const section of sections) { lines.push(section.title); for (const check of section.checks) { lines.push(...formatCheckResult(check)); } lines.push(""); } lines.push(...formatRecommendationsSection(getDoctorRecommendations())); lines.push(formatSummary(sections)); return lines.join("\n"); } export function formatReliabilityReportLines(platform: Platform, cwd: string): string[] { return formatReliabilitySection(loadReliabilitySummaries(platform.paths, cwd)); } export function handleDoctor(platform: Platform, ctx: PlatformContext): void { if (!ctx.hasUI) return; void (async () => { try { const sections = await runDoctorChecks(platform, ctx.cwd); const reliabilityLines = formatReliabilityReportLines(platform, ctx.cwd); const report = `${formatReport(sections)}\n\n${reliabilityLines.join("\n")}`; ctx.ui.notify(report, "info"); } catch (err) { ctx.ui.notify(`Doctor failed: ${(err as Error).message}`, "error"); } })(); } export function registerDoctorCommand(platform: Platform): void { platform.registerCommand("supi:doctor", { description: "Run health checks on supipowers features and integrations", async handler(_args: string | undefined, ctx: any) { handleDoctor(platform, ctx); }, }); } const CAPABILITY_LABELS: Partial> = { compactionHooks: "Context compression", customWidgets: "Progress widgets", registerTool: "Custom tool registration", activeToolFiltering: "Active-tool filtering", }; export function checkCapabilities( capabilities: PlatformCapabilities, mcpAvailable: boolean, ): CheckResult[] { const results: CheckResult[] = []; for (const [key, label] of Object.entries(CAPABILITY_LABELS)) { const ok = capabilities[key as keyof PlatformCapabilities]; results.push({ name: key, presence: { ok, detail: ok ? label : `Not available — ${label}` }, }); } results.push({ name: "MCP", presence: { ok: mcpAvailable, detail: mcpAvailable ? "Detected via active tools" : "Not detected", }, }); return results; }