import { tokenizeCommand } from './command-tokens.js'; import type { GitCommandRunner } from '../scope/git.js'; export { tokenizeCommand }; /** * Installation diagnostics (FR-23). * * The rule that shapes every check here: **diagnose the configuration that * exists, not the one the installer would have written.** A hook that Cortex * would install correctly is not evidence about the hook the host actually * runs — and the gap between those two is the entire failure mode this command * exists for, where a broken hook masquerades as an empty memory. * * Two disciplines follow from it and are load-bearing throughout: * * - **Nothing here changes what it observes.** No session, no engagement * write, no `ensureCortexSchema`, no spool flush, no store created where * none exists. A diagnostic that repairs the thing it is checking cannot * report on it: opening the store the normal way would migrate * `schema_version` to the value the check compares against, so the mismatch * could never be seen. Same rule `list-memory` and `inspect-memory` follow, * binding harder. * * One honest exception, which the docs state rather than hide: reading a * **WAL-mode** database creates its `-shm` and `-wal` sidecars if they are * absent. That is SQLite's requirement for reading WAL at all, not a choice * this module makes — `readonly: true` prevents content writes, not sidecar * creation — and the alternative (opening `immutable=1`) would read past the * WAL and could report a stale `schema_version`, which is a wrong answer * rather than a tidier one. * - **Nothing under diagnosis is executed.** `jq` and the hook interpreter are * located on `PATH`, never run. That keeps the 3-second budget (B-7) * structural rather than tuned on a platform where `bash -c 'exit 0'` alone * measures ~36 ms, makes the negative cases testable by pointing `PATH` at a * fixture directory, and avoids executing arbitrary binaries found on a * user's `PATH` while diagnosing. The honest limit: a present-but-broken * `jq` resolves and is reported as available. * * **One subprocess is spawned, and it is not one of the things being * checked:** `git rev-parse`, to locate the store. Store identity is defined * by AD-10 as a hash of `--git-common-dir`, so there is no way to report * where the store lives without asking git — the alternative is not a purer * diagnostic, it is a missing check. Injectable via `DoctorOptions.runGit`, * and measured at ~64 ms. This bullet used to read "no process is spawned" * and was narrowed when it stopped being true, rather than left standing. */ export type CheckStatus = 'pass' | 'warn' | 'fail'; export interface DoctorCheck { id: string; label: string; status: CheckStatus; detail: string; /** Concrete command or edit. Required on every non-passing check (AC #3). */ fix?: string; } export interface DoctorReport { project: string; /** Always set: falls back to the default location when nothing is wired. */ hooks_dir: string; checks: DoctorCheck[]; failures: number; warnings: number; /** No failing check. Drives the exit code; warnings never do. */ ok: boolean; } export interface DoctorOptions { projectDir: string; /** Overridable so tests need neither a real home directory nor a real PATH. */ homeDir?: string; hooksDir?: string; templateDir?: string; env?: NodeJS.ProcessEnv; platform?: NodeJS.Platform; now?: Date; /** * Injected so store-location checks need no real repository. * * This is the one place `doctor` runs a subprocess, and the docstring's * "no process is spawned" rule is narrowed rather than quietly broken: `jq` * and the hook interpreter are still only *located*, never executed. Store * identity has no non-git definition — AD-10 is a hash of what * `git rev-parse --git-common-dir` answers — so the choice is one `git` * invocation or no store-location check at all. Measured at ~64 ms against * B-7's 3-second budget. */ runGit?: GitCommandRunner; } export declare const HOOK_SCRIPTS: readonly ["cortex-capture.sh", "cortex-reflect.sh", "cortex-end-of-turn.sh", "cortex-subagent.sh"]; /** Placeholder the installer substitutes with `hookTemplateDigest`. */ export declare const TEMPLATE_ID_PLACEHOLDER = "__CORTEX_TEMPLATE_ID__"; /** Matches the detached-flush threshold in `cortex-capture.sh`. */ export declare const SPOOL_THRESHOLD_BYTES = 262144; /** A non-empty spool older than this suggests the turn-end flush is not firing. */ export declare const SPOOL_STALE_MS: number; /** * The wirings `install` writes, matched against a command's **tokens**. * * Not a raw substring of the command: the wiring `install` writes * quotes the script path (`bash "…/cortex-reflect.sh" reflect-pre`), so a * needle of `cortex-reflect.sh reflect-pre` never matches the canonical wiring * — the diagnostic would report a correct installation as unwired. Matching on * the script's basename and, where it matters, a separate action token, holds * for quoted and unquoted paths, `~` and absolute alike. */ export interface RequiredWiring { event: string; label: string; script?: string; /** A token that must also be present, e.g. the reflect action. */ action?: string; /** A token that must be present when `script` is not the discriminator. */ token?: string; /** When the action may be omitted because the script defaults to it. */ actionOptionalUnless?: string; /** * The tool matcher this event needs. Carried here rather than in the * installer so that what `install` writes and what `doctor` checks are the * same declaration. */ matcher?: string; } /** * Exported so `install` writes exactly what `doctor` checks for. One source of * truth: an installer and a diagnostic that disagree about what "wired" means * is the shape where a correct installation reports itself broken. */ export declare const REQUIRED_WIRING: readonly RequiredWiring[]; /** * Identity of one required wiring, event PLUS discriminator. * * Exists because `PreToolUse` now carries two entries. `install` records which * wirings the *other* settings files Claude Code merges already provide, so it * does not append a duplicate that would double every hook invocation — and it * kept that record as a set of EVENT NAMES. With two entries on one event, a * machine where `PreToolUse` was wired anywhere else would have had the second * entry silently skipped, after which `doctor` fails `hook-wiring` and names * `cortex install` as the fix that had just declined to help. That is the * installer/diagnostic disagreement this module exists to prevent. * * The discriminator is the same field `commandSatisfiesWiring` distinguishes on, * so the two cannot drift: whatever tells two wirings apart in a command string * is what tells them apart in this key. */ export declare function wiringKey(required: RequiredWiring): string; /** True when a settings command implements the given wiring. */ export declare function commandSatisfiesWiring(command: string, required: RequiredWiring): boolean; /** * Identity of a hook template, stamped into the script at install time and * recompared here. * * Line endings are normalised first: templates are checked out through git and * working files in this repository are CRLF, so an unnormalised digest reports * a false "out of date" against a clone with a different `core.autocrlf`. False * stale is the safe direction — the fix is idempotent — but it is still noise. * * A content digest rather than a hand-maintained version number on purpose: a * number you must remember to bump fails in exactly the shape this check * exists to catch, where the hook is stale while everything about it looks * fine. */ export declare function hookTemplateDigest(templateText: string): string; /** Read the `# cortex-hook-template:` stamp from an installed script. */ export declare function readTemplateStamp(scriptText: string): string | null; /** * Expand the two substitutions a hook command's path can carry that Node does * not resolve on its own. * * `~` is expanded by the shell — the live wiring on this machine is * `bash ~/.claude/hooks/cortex-capture.sh`, so skipping it reports every * configured script as missing. `$CLAUDE_PROJECT_DIR` is expanded by Claude * Code itself and is its documented form for a project-relative hook path; * `install-hooks` never emits it, but this module diagnoses the configuration * that exists rather than the one the installer would have written, and left * unexpanded it produces three false failures plus a nonsense path printed * back at a user whose installation is fine. */ export declare function expandHookPath(target: string, homeDir: string, projectDir: string): string; /** * Locate an executable the way the shell would (N-6). * * `which bash` inside Git Bash answers `/usr/bin/bash`, which Node resolves * against the drive root and does not find — so `existsSync` on the configured * interpreter reports it missing on the exact platform the user runs. A bare * word is therefore resolved through `PATH` honouring `PATHEXT`, and a * POSIX-absolute path that does not exist on win32 falls back to resolving its * basename the same way, because under Git Bash that path does name a real * interpreter. */ export declare function resolveExecutable(name: string, env: NodeJS.ProcessEnv, platform: NodeJS.Platform, homeDir: string, projectDir?: string): string | null; /** * Recover the absolute paths `install-hooks` substituted into a script, by * anchoring on the template line each placeholder sits in. * * Deliberately not a content pattern over the installed file. The obvious * version — "any quoted token containing `node` or `.js`" — matches * `SPOOL="$CWD/.cortex.spool.jsonl"`, because `.jsonl` contains `.js`, and * then reports the spool file as a missing Node installation. Measured against * the live install before this was rewritten. Anchoring on the template makes * the extraction exact: whatever the placeholder expanded to is whatever sits * at that position on that line. * * A stale script may no longer contain the template's line, in which case * nothing is recovered from it — acceptable, because the currency check has * already failed by then and names the same fix. */ export declare function extractBakedPaths(templateText: string, installedText: string): string[]; /** * Every `.hooks.[].hooks[].command` string in a settings object, with * the event it is wired to. Tolerant of shape: a settings file is user-edited * and a malformed branch must not abort discovery of the rest. */ export declare function collectHookCommands(settings: Record): Array<{ event: string; command: string; matcher: string | null; }>; export declare function runDoctor(options: DoctorOptions): DoctorReport; //# sourceMappingURL=doctor.d.ts.map