/** * GET /api/v1/diagnostics — the daemon's self-knowledge surface. * * The operator question this answers is "is ACP really working across the CLIs, and what is this * daemon actually running?" — an answer that previously lived only in raw NDJSON under * `/core.db.events/` and in whatever `--version` the operator remembered to run. * Everything here is READ-ONLY and derived from records the daemon already owns: * * - component versions (crew's package.json; the bundled studio's shipped manifest; the * installed wicked-core-ts; engine binaries crew already knows how to invoke) * - store files with sizes (core.db + its sidecars, the durable events dir as a total) * - a bounded tail of the daemon's own error-level log lines (an in-process ring — see * {@link ErrorRing}; empty on a daemon that has logged no errors, never invented) * - the ACP fold: acpSessionStarted / acpFallback per cliKey from the durable run event logs * * The honesty rule is the wire contract's: a field the daemon cannot answer is `null` (or an * empty collection) — NEVER fabricated. A missing studio bundle is `null`, an unprobeable * binary is `null`, a machine with no event logs folds to `{}`. */ /** Per-CLI ACP health, folded from the durable run event logs. Mirrors the wire contract's * `AcpCliDiagnostics` (wicked-crew-api-types). */ export interface AcpCliFold { sessionsStarted: number; fallbacks: number; fallbackKinds: Record; lastStartedTs: number | null; lastFallbackTs: number | null; /** The seat's ACP version pin, probed now (core#581) — present only for a pinned seat. */ versionPin?: SeatVersionPin; } /** * Fold `acpSessionStarted` / `acpFallback` events per cliKey from `/*.ndjson`. * * Stream-parsed (readline over a file stream — never a whole-file read into memory), with a * cheap substring pre-filter so the JSON parser only ever runs on candidate lines. A missing * dir, an unreadable file, or a malformed line each degrade to "contributes nothing" — the * fold is a diagnosis surface and must not itself become a failure mode. */ export declare function foldAcpEvents(eventsDir: string): Promise>; /** * One pinned seat's reading: the build its ACP input-governance admission was proven against * (`pinned`, the registry's `acp.verified_version`), what the binary reports now (`observed`), * and whether the pin holds. `matched: false` on a seat with `governanceClaimed: true` means the * engine fails that seat's governance closed at every spawn. Before this surface it said so only * on a governed turn, so a drifted seat looked healthy everywhere an operator looks between runs. */ export interface SeatVersionPin { cli: string; binary: string; pinned: string; observed: string | null; matched: boolean; governanceClaimed: boolean; disclosure: string | null; } /** A probe's reading. `timedOut`: the daemon stopped `--version` at its bound (a slow start on a * loaded host, crew#997). That is no answer at all, not "did not answer": the cache keeps the seat's * last answered reading, or has none, and asks again soon. Never on the wire. */ export type SeatVersionPinProbe = SeatVersionPin & { timedOut?: true; }; /** Where readings come from: the probe below, or a test's fixture. */ export type SeatVersionPinSource = () => Promise; /** * Read each pinned seat's `--version` the way the engine's spawn-time check does * (`probe_resolved_binary_version`, wicked-core `acp_runner.rs`): stdout then stderr, the pin * matches only the FIRST line trimmed, byte for byte; `observed` is the first non-empty line, for * a human. A probe that cannot run reads `observed: null`, `matched: false`, as in the engine. */ export declare function probeSeatVersionPins(roster: unknown[], exec: ExecLike): Promise; /** * The daemon's source: the engine's production roster, probed with {@link execCapped}. `null` * under a test runner — a test-built server never spawns CLI children by default (the * {@link engineBinaryVersions} rule). */ export declare function defaultSeatVersionPinSource(): SeatVersionPinSource | null; /** * A TTL cache of the pin readings, shared by `/diagnostics` (which awaits a fresh-enough answer) * and the roster (which is synchronous: it reads the last answer and refreshes in the background). * Each refresh spawns one bounded `--version` per pinned seat, so it runs at most once per TTL. A * failed probe keeps the previous answer; with no source at all every read is `null`. * * crew#997: a seat whose probe timed out keeps its last answered reading (for at most * {@link SEAT_VERSION_KEEP_MS}), or has no reading; a timeout never reads as `matched: false`. A * refresh with a timed-out seat is retried after {@link SEAT_VERSION_RETRY_MS}, not a whole TTL. */ export declare class SeatVersionPinCache { private readonly source; private readonly ttlMs; /** `kept`: the seats whose reading was kept over a timed-out probe (crew#997). */ private value; private inFlight; private failedAt; /** When each seat's kept reading was answered (crew#997). */ private readonly answeredAt; constructor(source?: SeatVersionPinSource | null, ttlMs?: number); /** Due a probe: no answer, or the last answer (or the last failed attempt) is a TTL old. */ private stale; /** A fresh-enough answer, probing when stale; `null` when there is no source. */ get(): Promise; /** The last answer for `cli`, without waiting; a stale or empty cache starts a refresh. */ read(cli: string): SeatVersionPin | undefined; /** The cached readings, less a kept reading past {@link SEAT_VERSION_KEEP_MS} (codex r1 on #997): * the window holds whether or not a probe has landed since. */ private served; } /** The daemon's one pin cache: the roster and `/diagnostics` read the same answer. */ export declare const seatVersionPins: SeatVersionPinCache; /** `byCli` with each pinned seat's reading attached — a pinned seat with no ACP events yet still * lists, with zero counts, so a drifted seat nobody has used is visible too. */ export declare function withVersionPins(byCli: Record, pins: SeatVersionPin[] | null): Record; /** * A short-TTL, in-flight-deduplicated cache around {@link foldAcpEvents}. The events dir holds * one file per run (139+ observed), so the fold is real IO — briefly caching keeps a dashboard * polling /diagnostics from turning the daemon into an NDJSON re-reader, while the TTL keeps * the answer honest to within seconds. */ export declare class AcpFoldCache { private readonly ttlMs; /** core#581: each pinned seat's version reading rides its `byCli` entry. */ private readonly pins; private value; private inFlight; constructor(ttlMs?: number, /** core#581: each pinned seat's version reading rides its `byCli` entry. */ pins?: SeatVersionPinCache); get(eventsDir: string): Promise>; private fold; } /** One captured error-level log line — the wire contract's `DiagnosticsRecentError`. */ export interface RecentError { ts: number; source: string; line: string; } /** * A bounded ring of the daemon's own error-level log lines. * * Crew has no persistent log file of its own (pino writes to stdout; `daemon-stdout.log` under * the state home belongs to whatever launched the daemon) — so the "recent errors" record is * an IN-PROCESS ring fed by a tee on the pino stream ({@link teeStreamWithErrorRing}). Empty * on a fresh or healthy daemon; capped so it can never grow with uptime. */ export declare class ErrorRing { private readonly cap; private readonly entries; constructor(cap?: number); push(entry: RecentError): void; /** Newest first — the wire order. */ list(): RecentError[]; } /** * A pino destination that forwards every line to `out` (stdout in the daemon) and folds * error-level lines into `ring`. Passed to Fastify as `logger.stream`, so the ring sees * exactly what the daemon logs — no second logging channel to keep in step. * * The parse is guarded end-to-end: a log line that fails to parse as pino JSON still ships to * `out` verbatim, and a throw in the ring can never break logging (which would turn the * diagnosis surface into an outage). */ export declare function teeStreamWithErrorRing(ring: ErrorRing, out?: { write(chunk: string): unknown; }): { write(chunk: string): void; }; /** * The bundled studio's version, read from the manifest the bundle ALREADY ships: * `testid-inventory.json` carries `studioVersion` (a versioned build artifact, TH-13). No * bundle — a headless daemon — is `null`, and so is a bundle predating the inventory. */ export declare function readStudioBundleVersion(studioRoot: string): string | null; /** * The installed version of a dependency, or `null`. Walks the resolver's candidate dirs and * reads package.json with fs — the same sidestep `apiTypesVersion()` uses, because a * types-only exports map (wicked-core-ts included) refuses `require.resolve` on the package * root and on `/package.json` alike. */ export declare function installedPackageVersion(name: string): string | null; /** First semver-looking token of a `--version` output, else the trimmed first line, else null. */ export declare function parseVersionOutput(stdout: string): string | null; type ExecLike = (file: string, args: string[], opts?: { timeout?: number; }) => Promise<{ stdout: string; stderr: string; }>; /** * `--version` of the engine binaries crew ALREADY knows how to invoke — never a new path * resolution invented for diagnostics: * * - `wicked-core`: the standalone engine binary the adapter resolves at construction for the * gate-hook command and publishes as `WICKED_CORE_EXE`. Env unset = crew never resolved * one = `null`. * - `wicked-estate`: the executable the repo/project indexer resolves — `WICKED_ESTATE_EXE`, else * the bare name on PATH (`projects/graph.ts` `estateExe()`), exactly as the graph-view / * blast-radius routes spawn it. crew#804: a bare `probe('wicked-estate')` reported whatever * older build sat first on PATH while the daemon indexed and grounded with the override, so the * one place an operator reads the installed stack lied about estate. Unresolvable = `null`. * * Under a test runner the probes are SKIPPED (both `null`) unless an exec is injected — the * seat-health-probe posture: a test-built server never spawns CLI children by default. */ export declare function engineBinaryVersions(exec?: ExecLike): Promise>; /** Version probes answer the same for the life of a process — cache the first answer. */ export declare class EngineVersionCache { private value; private inFlight; get(exec?: ExecLike): Promise>; } /** One store file (or dir, sized as a total) — the wire contract's `DiagnosticsStoreFile`. */ export interface StoreFileEntry { name: string; path: string; bytes: number; } /** * The engine's per-repo code graphs live under the state home since wicked-core#406 — * `/repo-graphs/-<12-hex>/estate.db` — and are listed here as one * store entry per graph (`repo-graphs//estate.db`), so the operator can see where every * registered repo's graph is and how big it is. The root is spelled from the state home directly * (the parent of `--db`, exactly how the engine derives it); crew keeps spelling no graph path of * its own beyond this listing. The directory name is the one registered in * `tests/fixtures/state-home-subtrees.json` (owner `engine`). */ export declare const REPO_GRAPHS_DIRNAME = "repo-graphs"; /** * The engine's precedence-1 override for the repo-graph root (`code_graph.rs` ADR): when set, the * engine writes every repo graph under it instead of `/repo-graphs`, so the listing * must look there too or it silently omits every graph. Same name the engine reads; an empty value * means unset, as in the engine. */ export declare const REPO_GRAPH_ROOT_ENV = "WICKED_ESTATE_REPO_GRAPH_ROOT"; /** * Where this daemon's repo graphs live, spelled with the engine's precedence: the env override * when set, else `/repo-graphs` — the state home through the ONE crew resolver * (`stateHomeOfDb`: the parent of the ABSOLUTE `--db`, so a relative `--db ./scratch/core.db` * still yields absolute paths that match the engine's canonical spelling). The two remaining * engine arms (a thread-bound state home; the default `~/.wicked-crew`) collapse to the same * answer here, because the daemon's state home IS the `--db` parent. */ export declare function repoGraphRoot(dbPath: string, env?: NodeJS.ProcessEnv): string; /** * `core.db` and every sidecar sharing its basename (`core.db-wal`, `core.db.knowledge`, * `core.db.mem*`, …), plus the events dir (`core.db.events`) sized as a TOTAL of its * contents, followed by one entry per repo graph under `/repo-graphs//estate.db` * (wicked-core#406 — the store issue #406 asked this surface to list). Paths and sizes only — no * file contents ever ride this wire. */ export declare function listStoreFiles(dbPath: string, env?: NodeJS.ProcessEnv): Promise; /** Where a core db's durable run event logs live — the engine's `.events/` convention. */ export declare function eventsDirOf(dbPath: string): string; /** The OS launcher the engine's repository boundary would arm with on this host (`sandbox-exec` on * macOS, `bwrap` on Linux), found on PATH as an executable file; `null` when none is (crew#742). * Presence only — the engine's own probe decides whether it arms. */ export declare function hostSandboxLauncher(env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform): string | null; /** Where the engine's seat registry reads its user overlay (`wicked-council` `default_user_path`): * `$HOME/.config/wicked-council/clis.toml`. For the local operator only (never GitHub-visible). */ export declare function seatRosterFile(env?: NodeJS.ProcessEnv): { path: string; present: boolean; } | null; export {}; //# sourceMappingURL=diagnostics.d.ts.map