/** * `cleo doctor system`: what is hurting this machine, ranked, with the exact * remedy for each finding (T13435). * * On 2026-10-10 one Mac ran at 24 of 25 GB swap with load 24: 49 playwright-mcp, * 33 mcpvault and ~60 agentmbx stdio MCP servers (one set per harness session), * 475 dangling docker volumes from throwaway Postgres runs, long-lived database * containers, and full typechecks started outside `cleo run`. None of it was * visible from one place. This module makes it visible. * * Two halves, so the analysis is testable on any platform: * - {@link collectSystemSnapshot} reads the machine: one resource sample * (reusing the governor's Darwin/Linux backends), one `ps`, the admission * ledger, docker (each call time-boxed, skipped when absent) and the * project's Spotlight/Time Machine state. * - {@link assessSystemHealth} is pure: snapshot in, ranked report out. * * Read-only. It never signals a process. A remedy that deletes data or stops a * process is marked `needsOwnerChoice` so the agent asks the owner first. * * @task T13435 * @epic T13434 */ import type { ResourceSample } from '../resources/backend.js'; import { type MemoryGuardAudit } from '../resources/memory-guard.js'; /** How urgent a finding is. Findings are ranked by this, then by memory impact. */ export type SystemFindingSeverity = 'critical' | 'warning' | 'info'; /** One value in a finding's evidence. */ export type SystemEvidenceValue = string | number | boolean | null | readonly string[]; /** The fix for a finding. */ export interface SystemRemedy { /** The exact command to run, or `null` when the fix is a manual setting. */ readonly command: string | null; /** What the remedy does, and what it costs. */ readonly description: string; } /** One ranked, agent-relayable finding. */ export interface SystemFinding { /** Stable id, e.g. `mcp-fanout:playwright-mcp`. */ readonly id: string; /** Which check produced it. */ readonly category: SystemCheck; readonly severity: SystemFindingSeverity; /** One line for the owner. */ readonly title: string; /** The measurements behind the verdict. */ readonly evidence: Readonly>; /** Resident memory the finding accounts for, in bytes, when known (ranking). */ readonly impactBytes: number | null; readonly remedy: SystemRemedy | null; /** True when the remedy deletes data, stops work or changes machine settings: ask the owner. */ readonly needsOwnerChoice: boolean; } /** The checks the report runs. */ export type SystemCheck = 'memory' | 'mcp-fanout' | 'heavy-ungoverned' | 'containers' | 'sessions' | 'indexing' | 'memory-guard'; /** Whether a check ran. A missing finding means healthy only when its check is `ok`. */ export interface SystemCoverage { readonly check: SystemCheck; readonly status: 'ok' | 'skipped' | 'error'; readonly reason?: string; } /** The `cleo doctor system` result. */ export interface SystemHealthReport { readonly platform: NodeJS.Platform; readonly sampledAt: string; readonly summary: { readonly critical: number; readonly warning: number; readonly info: number; }; /** Worst first. */ readonly findings: readonly SystemFinding[]; readonly coverage: readonly SystemCoverage[]; /** Every agent session, quietest first (T13438). Quiet is not dead: ask, never close on it alone. */ readonly sessions: readonly SessionInfo[]; } /** One agent session (a top-level harness process and its tree). */ export interface SessionInfo { readonly pid: number; /** `claude`, `codex`, `opencode`, `kimi`, … */ readonly harness: string; /** Git root of the session's working directory, or `null` when unknown. */ readonly project: string | null; readonly cwd: string | null; readonly elapsedSec: number; /** RSS of the session and everything it started (MCP servers included), MiB. */ readonly rssMib: number; /** The activity readings behind {@link quiet}; `null` where unmeasured. */ readonly signals: SessionSignals; /** * True only when EVERY signal is quiet: terminal silent, no CPU time used * across a short second sample, no running child besides MCP servers, no * recent writes in its directory (T13460). A quiet session may still be * waiting on a plan or a person, so it is something to ask about, not close. */ readonly quiet: boolean; } /** One session's activity readings (T13460). */ export interface SessionSignals { /** Seconds since its terminal last saw input or output; `null` without a tty. */ readonly ttySilentSec: number | null; /** CPU seconds its process tree used between two samples; `null` when not sampled. */ readonly cpuDeltaSec: number | null; /** Processes running under it other than MCP servers (tool calls, builds, shells). */ readonly activeChildren: number; /** Seconds since the newest write in its cwd or git dir; `null` when unknown. */ readonly lastWriteSec: number | null; } /** Per-session facts the collector reads beyond `ps` (T13438, T13460). */ export interface SessionContext { readonly cwd: string | null; readonly project: string | null; readonly ttyIdleSec: number | null; /** CPU seconds the session's tree used across the collector's two samples. */ readonly cpuDeltaSec?: number | null; /** Seconds since the newest write in the session's cwd or git dir. */ readonly lastWriteSec?: number | null; } /** Docker state, or `null` fields when a call failed. */ export interface DockerSnapshot { /** `docker volume ls -q -f dangling=true` output. */ readonly danglingVolumes: string | null; /** `docker system df --format '{{json .}}'` output. */ readonly systemDf: string | null; /** `docker ps --format '{{.ID}}\t{{.Names}}\t{{.Image}}\t{{.CreatedAt}}\t{{.Labels}}'` output. */ readonly containers: string | null; } /** The project's indexing state (macOS). `null` fields were not measured. */ export interface IndexingSnapshot { /** The project root the checks looked at. */ readonly projectRoot: string; /** `tmutil isexcluded /node_modules` output, `null` when there is no node_modules. */ readonly timeMachine: string | null; /** `mdfind -onlyin /node_modules -count …` output. */ readonly spotlightCount: string | null; } /** Everything {@link assessSystemHealth} needs, gathered by {@link collectSystemSnapshot}. */ export interface SystemSnapshot { readonly platform: NodeJS.Platform; readonly sampledAtMs: number; readonly totalMemBytes: number; readonly cpuCount: number; readonly loadAvg1: number; /** The governor's resource sample, or `null` when it could not be read. */ readonly sample: ResourceSample | null; /** Linux swap from `/proc/meminfo`; macOS carries it on `sample.darwinMemory`. */ readonly linuxSwap: { readonly usedBytes: number; readonly totalBytes: number; } | null; /** `ps -A -o pid=,ppid=,pgid=,rss=,pcpu=,etime=,tty=,args=` output, `null` when ps failed. */ readonly ps: string | null; /** Process groups and pids `cleo run` holds (the admission ledger). */ readonly governedPgids: readonly number[]; readonly governedPids: readonly number[]; /** `null` when docker is not installed or its daemon did not answer. */ readonly docker: DockerSnapshot | null; readonly indexing: IndexingSnapshot | null; /** Linux only. */ readonly memoryGuard: MemoryGuardAudit | null; /** Keyed by session pid; absent entries are reported with unknown project and activity. */ readonly sessionContext?: Readonly>; } /** One `ps` row. */ export interface ProcessRow { readonly pid: number; readonly ppid: number; readonly pgid: number; readonly rssBytes: number; readonly pcpu: number; readonly elapsedSec: number; /** Controlling terminal (`ttys003`, `pts/2`), `null` when none. */ readonly tty: string | null; readonly args: string; /** `args` split on whitespace (ps does not quote; good enough for recognition). */ readonly argv: readonly string[]; } /** `[[dd-]hh:]mm:ss` (both BSD and procps `etime`) to seconds. */ export declare function parseEtime(etime: string): number; /** Parse `ps -A -o pid=,ppid=,pgid=,rss=,pcpu=,etime=,tty=,args=` output. */ export declare function parsePs(output: string): ProcessRow[]; /** * The command words with the interpreter dropped and the script's extension * stripped: `node …/typescript/bin/tsc -b` → `['tsc', '-b']`. */ export declare function commandWords(argv: readonly string[]): string[]; /** An interactive agent session (claude, codex, opencode, kimi, …). */ export declare function isHarnessSession(row: ProcessRow): boolean; /** * The MCP server a process runs, or `null` when it is not one. Recognised by * `mcp` in the command or script name (`playwright-mcp`, `mcpvault`, * `agentmbx mcp`, `railway mcp`, `…/axiom-qa-mcp/dist/server.js`). */ export declare function mcpServerName(row: ProcessRow): string | null; /** * The package a launcher runs (`npx -y @playwright/mcp@latest` → `@playwright/mcp`), * `null` when a launcher names none, `undefined` when `words` is not a launcher. * The scope is kept (`@a/mcp` and `@b/mcp` are different servers); the version is not. */ export declare function launchedPackage(words: readonly string[]): string | null | undefined; /** `2026-10-10 10:23:54 -0700 PDT` (docker CreatedAt) to epoch ms, `NaN` when unparseable. */ export declare function parseDockerCreatedAt(value: string): number; /** Docker's human sizes (`52.3GB`, `16.38kB`, `0B`) to bytes. */ export declare function parseDockerSize(value: string): number; /** Terminal silence and directory writes older than this count as quiet (T13438). */ export declare const SESSION_QUIET_SEC = 3600; /** Top-level agent sessions: harness processes with no harness above them. */ export declare function harnessSessions(rows: readonly ProcessRow[]): ProcessRow[]; /** Quiet only when no signal shows activity; terminal silence alone never decides (T13460). */ export declare function isQuiet(signals: SessionSignals, treePcpu: number): boolean; /** * Turn a snapshot into a ranked report. Pure: no I/O. * * @param s - the raw machine state from {@link collectSystemSnapshot}. * @returns findings worst first, plus which checks ran. * * @example * ```ts * const report = assessSystemHealth(await collectSystemSnapshot({ projectRoot })); * for (const f of report.findings) console.error(f.severity, f.title, f.remedy?.command); * ``` * * @task T13435 */ export declare function assessSystemHealth(s: SystemSnapshot): SystemHealthReport; /** * Run a read-only command, time-boxed. `null` on any failure (absent, timeout, * non-zero). With `keepStdoutOnExit`, a non-zero exit that still printed * returns what it printed: `lsof -p a,b` exits 1 when one pid has vanished but * still reports the others. * * @task T13435 */ export declare function runReadOnly(cmd: string, args: readonly string[], opts?: { readonly timeoutMs?: number; readonly keepStdoutOnExit?: boolean; }): Promise; /** Linux swap from `/proc/meminfo`, `null` when unreadable. */ export declare function parseLinuxSwap(meminfo: string): { usedBytes: number; totalBytes: number; } | null; /** Options for {@link collectSystemSnapshot}. */ export interface CollectSystemSnapshotOptions { /** The project to check Spotlight/Time Machine for; `null` skips those checks. */ readonly projectRoot: string | null; } /** * Read the machine for {@link assessSystemHealth}. Read-only, never throws: * a failed source leaves its field `null` and the report marks the check. * * @task T13435 */ export declare function collectSystemSnapshot(opts: CollectSystemSnapshotOptions): Promise; /** Nearest ancestor of `dir` (itself included) holding `.git`, or `null`. */ export declare function gitRootOf(dir: string, exists?: (p: string) => boolean): string | null; /** pid → cwd from `lsof -a -d cwd -p -Fpn` output. */ export declare function parseLsofCwd(output: string): Map; /** `ps` `time` (BSD `m:ss.cc`, procps `[dd-]hh:mm:ss`) to CPU seconds. */ export declare function parseCpuTime(value: string): number; /** pid → CPU seconds from `ps -A -o pid=,time=`. */ export declare function parsePsTimes(output: string): Map; //# sourceMappingURL=system-health.d.ts.map