wicked-crew 0.7.5 → 0.7.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,143 @@
1
+ /**
2
+ * GET /api/v1/diagnostics — the daemon's self-knowledge surface.
3
+ *
4
+ * The operator question this answers is "is ACP really working across the CLIs, and what is this
5
+ * daemon actually running?" — an answer that previously lived only in raw NDJSON under
6
+ * `<state-home>/core.db.events/` and in whatever `--version` the operator remembered to run.
7
+ * Everything here is READ-ONLY and derived from records the daemon already owns:
8
+ *
9
+ * - component versions (crew's package.json; the bundled studio's shipped manifest; the
10
+ * installed wicked-core-ts; engine binaries crew already knows how to invoke)
11
+ * - store files with sizes (core.db + its sidecars, the durable events dir as a total)
12
+ * - a bounded tail of the daemon's own error-level log lines (an in-process ring — see
13
+ * {@link ErrorRing}; empty on a daemon that has logged no errors, never invented)
14
+ * - the ACP fold: acpSessionStarted / acpFallback per cliKey from the durable run event logs
15
+ *
16
+ * The honesty rule is the wire contract's: a field the daemon cannot answer is `null` (or an
17
+ * empty collection) — NEVER fabricated. A missing studio bundle is `null`, an unprobeable
18
+ * binary is `null`, a machine with no event logs folds to `{}`.
19
+ */
20
+ /** Per-CLI ACP health, folded from the durable run event logs. Mirrors the wire contract's
21
+ * `AcpCliDiagnostics` (wicked-crew-api-types). */
22
+ export interface AcpCliFold {
23
+ sessionsStarted: number;
24
+ fallbacks: number;
25
+ fallbackKinds: Record<string, number>;
26
+ lastStartedTs: number | null;
27
+ lastFallbackTs: number | null;
28
+ }
29
+ /**
30
+ * Fold `acpSessionStarted` / `acpFallback` events per cliKey from `<dir>/*.ndjson`.
31
+ *
32
+ * Stream-parsed (readline over a file stream — never a whole-file read into memory), with a
33
+ * cheap substring pre-filter so the JSON parser only ever runs on candidate lines. A missing
34
+ * dir, an unreadable file, or a malformed line each degrade to "contributes nothing" — the
35
+ * fold is a diagnosis surface and must not itself become a failure mode.
36
+ */
37
+ export declare function foldAcpEvents(eventsDir: string): Promise<Record<string, AcpCliFold>>;
38
+ /**
39
+ * A short-TTL, in-flight-deduplicated cache around {@link foldAcpEvents}. The events dir holds
40
+ * one file per run (139+ observed), so the fold is real IO — briefly caching keeps a dashboard
41
+ * polling /diagnostics from turning the daemon into an NDJSON re-reader, while the TTL keeps
42
+ * the answer honest to within seconds.
43
+ */
44
+ export declare class AcpFoldCache {
45
+ private readonly ttlMs;
46
+ private value;
47
+ private inFlight;
48
+ constructor(ttlMs?: number);
49
+ get(eventsDir: string): Promise<Record<string, AcpCliFold>>;
50
+ }
51
+ /** One captured error-level log line — the wire contract's `DiagnosticsRecentError`. */
52
+ export interface RecentError {
53
+ ts: number;
54
+ source: string;
55
+ line: string;
56
+ }
57
+ /**
58
+ * A bounded ring of the daemon's own error-level log lines.
59
+ *
60
+ * Crew has no persistent log file of its own (pino writes to stdout; `daemon-stdout.log` under
61
+ * the state home belongs to whatever launched the daemon) — so the "recent errors" record is
62
+ * an IN-PROCESS ring fed by a tee on the pino stream ({@link teeStreamWithErrorRing}). Empty
63
+ * on a fresh or healthy daemon; capped so it can never grow with uptime.
64
+ */
65
+ export declare class ErrorRing {
66
+ private readonly cap;
67
+ private readonly entries;
68
+ constructor(cap?: number);
69
+ push(entry: RecentError): void;
70
+ /** Newest first — the wire order. */
71
+ list(): RecentError[];
72
+ }
73
+ /**
74
+ * A pino destination that forwards every line to `out` (stdout in the daemon) and folds
75
+ * error-level lines into `ring`. Passed to Fastify as `logger.stream`, so the ring sees
76
+ * exactly what the daemon logs — no second logging channel to keep in step.
77
+ *
78
+ * The parse is guarded end-to-end: a log line that fails to parse as pino JSON still ships to
79
+ * `out` verbatim, and a throw in the ring can never break logging (which would turn the
80
+ * diagnosis surface into an outage).
81
+ */
82
+ export declare function teeStreamWithErrorRing(ring: ErrorRing, out?: {
83
+ write(chunk: string): unknown;
84
+ }): {
85
+ write(chunk: string): void;
86
+ };
87
+ /**
88
+ * The bundled studio's version, read from the manifest the bundle ALREADY ships:
89
+ * `testid-inventory.json` carries `studioVersion` (a versioned build artifact, TH-13). No
90
+ * bundle — a headless daemon — is `null`, and so is a bundle predating the inventory.
91
+ */
92
+ export declare function readStudioBundleVersion(studioRoot: string): string | null;
93
+ /**
94
+ * The installed version of a dependency, or `null`. Walks the resolver's candidate dirs and
95
+ * reads package.json with fs — the same sidestep `apiTypesVersion()` uses, because a
96
+ * types-only exports map (wicked-core-ts included) refuses `require.resolve` on the package
97
+ * root and on `/package.json` alike.
98
+ */
99
+ export declare function installedPackageVersion(name: string): string | null;
100
+ /** First semver-looking token of a `--version` output, else the trimmed first line, else null. */
101
+ export declare function parseVersionOutput(stdout: string): string | null;
102
+ type ExecLike = (file: string, args: string[], opts?: {
103
+ timeout?: number;
104
+ }) => Promise<{
105
+ stdout: string;
106
+ stderr: string;
107
+ }>;
108
+ /**
109
+ * `--version` of the engine binaries crew ALREADY knows how to invoke — never a new path
110
+ * resolution invented for diagnostics:
111
+ *
112
+ * - `wicked-core`: the standalone engine binary the adapter resolves at construction for the
113
+ * gate-hook command and publishes as `WICKED_CORE_EXE`. Env unset = crew never resolved
114
+ * one = `null`.
115
+ * - `wicked-estate`: invoked by bare name on PATH, exactly as the graph-view/blast-radius
116
+ * routes do. Not on PATH = `null`.
117
+ *
118
+ * Under a test runner the probes are SKIPPED (both `null`) unless an exec is injected — the
119
+ * seat-health-probe posture: a test-built server never spawns CLI children by default.
120
+ */
121
+ export declare function engineBinaryVersions(exec?: ExecLike): Promise<Record<string, string | null>>;
122
+ /** Version probes answer the same for the life of a process — cache the first answer. */
123
+ export declare class EngineVersionCache {
124
+ private value;
125
+ private inFlight;
126
+ get(exec?: ExecLike): Promise<Record<string, string | null>>;
127
+ }
128
+ /** One store file (or dir, sized as a total) — the wire contract's `DiagnosticsStoreFile`. */
129
+ export interface StoreFileEntry {
130
+ name: string;
131
+ path: string;
132
+ bytes: number;
133
+ }
134
+ /**
135
+ * `core.db` and every sidecar sharing its basename (`core.db-wal`, `core.db.knowledge`,
136
+ * `core.db.mem*`, …), plus the events dir (`core.db.events`) sized as a TOTAL of its
137
+ * contents. Paths and sizes only — no file contents ever ride this wire.
138
+ */
139
+ export declare function listStoreFiles(dbPath: string): Promise<StoreFileEntry[]>;
140
+ /** Where a core db's durable run event logs live — the engine's `<db>.events/` convention. */
141
+ export declare function eventsDirOf(dbPath: string): string;
142
+ export {};
143
+ //# sourceMappingURL=diagnostics.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diagnostics.d.ts","sourceRoot":"","sources":["../../src/api/diagnostics.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAYH;mDACmD;AACnD,MAAM,WAAW,UAAU;IACzB,eAAe,EAAE,MAAM,CAAC;IACxB,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B;AASD;;;;;;;GAOG;AACH,wBAAsB,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC,CA8B1F;AAkED;;;;;GAKG;AACH,qBAAa,YAAY;IAIX,OAAO,CAAC,QAAQ,CAAC,KAAK;IAHlC,OAAO,CAAC,KAAK,CAAkE;IAC/E,OAAO,CAAC,QAAQ,CAAoD;gBAEvC,KAAK,GAAE,MAAe;IAE7C,GAAG,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;CAclE;AAID,wFAAwF;AACxF,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;CACd;AAMD;;;;;;;GAOG;AACH,qBAAa,SAAS;IAGR,OAAO,CAAC,QAAQ,CAAC,GAAG;IAFhC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAqB;gBAEhB,GAAG,GAAE,MAAW;IAE7C,IAAI,CAAC,KAAK,EAAE,WAAW,GAAG,IAAI;IAS9B,qCAAqC;IACrC,IAAI,IAAI,WAAW,EAAE;CAGtB;AAKD;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,SAAS,EACf,GAAG,GAAE;IAAE,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAA;CAAmB,GACtD;IAAE,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CA2BhC;AAID;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAQzE;AAED;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAenE;AAED,kGAAkG;AAClG,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAKhE;AAED,KAAK,QAAQ,GAAG,CACd,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EAAE,EACd,IAAI,CAAC,EAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,KACxB,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AAEjD;;;;;;;;;;;;GAYG;AACH,wBAAsB,oBAAoB,CAAC,IAAI,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC,CAAC,CAqBlG;AAED,yFAAyF;AACzF,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,KAAK,CAA8C;IAC3D,OAAO,CAAC,QAAQ,CAAuD;IAEjE,GAAG,CAAC,IAAI,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC,CAAC;CAanE;AAID,8FAA8F;AAC9F,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf;AAkCD;;;;GAIG;AACH,wBAAsB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC,CAuB9E;AAED,8FAA8F;AAC9F,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAElD"}
@@ -0,0 +1,417 @@
1
+ /**
2
+ * GET /api/v1/diagnostics — the daemon's self-knowledge surface.
3
+ *
4
+ * The operator question this answers is "is ACP really working across the CLIs, and what is this
5
+ * daemon actually running?" — an answer that previously lived only in raw NDJSON under
6
+ * `<state-home>/core.db.events/` and in whatever `--version` the operator remembered to run.
7
+ * Everything here is READ-ONLY and derived from records the daemon already owns:
8
+ *
9
+ * - component versions (crew's package.json; the bundled studio's shipped manifest; the
10
+ * installed wicked-core-ts; engine binaries crew already knows how to invoke)
11
+ * - store files with sizes (core.db + its sidecars, the durable events dir as a total)
12
+ * - a bounded tail of the daemon's own error-level log lines (an in-process ring — see
13
+ * {@link ErrorRing}; empty on a daemon that has logged no errors, never invented)
14
+ * - the ACP fold: acpSessionStarted / acpFallback per cliKey from the durable run event logs
15
+ *
16
+ * The honesty rule is the wire contract's: a field the daemon cannot answer is `null` (or an
17
+ * empty collection) — NEVER fabricated. A missing studio bundle is `null`, an unprobeable
18
+ * binary is `null`, a machine with no event logs folds to `{}`.
19
+ */
20
+ import { createReadStream, existsSync, readFileSync } from 'node:fs';
21
+ import { promises as fsp } from 'node:fs';
22
+ import { createRequire } from 'node:module';
23
+ import { basename, dirname, join } from 'node:path';
24
+ import { createInterface } from 'node:readline';
25
+ import { execCapped } from '../core/exec.js';
26
+ /** Bounds on the fold's work — the events dir grows one file per run forever, and this fold
27
+ * runs inside a request handler, so both dimensions are capped. At the caps this is still
28
+ * hundreds of runs' worth of history; past them the answer is the bounded subset (newest
29
+ * files first), which stays honest for "is ACP working" — the recent record is what matters. */
30
+ const ACP_FOLD_MAX_FILES = 512;
31
+ const ACP_FOLD_MAX_LINE_BYTES = 1024 * 1024;
32
+ /**
33
+ * Fold `acpSessionStarted` / `acpFallback` events per cliKey from `<dir>/*.ndjson`.
34
+ *
35
+ * Stream-parsed (readline over a file stream — never a whole-file read into memory), with a
36
+ * cheap substring pre-filter so the JSON parser only ever runs on candidate lines. A missing
37
+ * dir, an unreadable file, or a malformed line each degrade to "contributes nothing" — the
38
+ * fold is a diagnosis surface and must not itself become a failure mode.
39
+ */
40
+ export async function foldAcpEvents(eventsDir) {
41
+ const byCli = {};
42
+ let names;
43
+ try {
44
+ names = await fsp.readdir(eventsDir);
45
+ }
46
+ catch {
47
+ return byCli; // no events dir yet — nothing recorded, honestly empty
48
+ }
49
+ let files = names.filter((n) => n.endsWith('.ndjson'));
50
+ if (files.length > ACP_FOLD_MAX_FILES) {
51
+ // Newest runs first when the cap bites: mtime is one stat per file, paid only past the cap.
52
+ const stamped = await Promise.all(files.map(async (n) => {
53
+ try {
54
+ const st = await fsp.stat(join(eventsDir, n));
55
+ return { n, mtime: st.mtimeMs };
56
+ }
57
+ catch {
58
+ return { n, mtime: 0 };
59
+ }
60
+ }));
61
+ files = stamped
62
+ .sort((a, b) => b.mtime - a.mtime)
63
+ .slice(0, ACP_FOLD_MAX_FILES)
64
+ .map((s) => s.n);
65
+ }
66
+ for (const name of files) {
67
+ await foldOneFile(join(eventsDir, name), byCli);
68
+ }
69
+ return byCli;
70
+ }
71
+ function entryFor(byCli, cliKey) {
72
+ let e = byCli[cliKey];
73
+ if (e === undefined) {
74
+ e = {
75
+ sessionsStarted: 0,
76
+ fallbacks: 0,
77
+ fallbackKinds: {},
78
+ lastStartedTs: null,
79
+ lastFallbackTs: null,
80
+ };
81
+ byCli[cliKey] = e;
82
+ }
83
+ return e;
84
+ }
85
+ async function foldOneFile(path, byCli) {
86
+ let rl;
87
+ let stream;
88
+ try {
89
+ stream = createReadStream(path, { encoding: 'utf8' });
90
+ rl = createInterface({ input: stream, crlfDelay: Infinity });
91
+ }
92
+ catch {
93
+ return; // unreadable file contributes nothing
94
+ }
95
+ try {
96
+ for await (const line of rl) {
97
+ // Substring pre-filter: almost every line in a run log is NOT an ACP event, and
98
+ // JSON.parse on megabytes of narration is exactly the stall this fold must not cause.
99
+ const isStart = line.includes('"acpSessionStarted"');
100
+ const isFallback = !isStart && line.includes('"acpFallback"');
101
+ if (!isStart && !isFallback)
102
+ continue;
103
+ if (line.length > ACP_FOLD_MAX_LINE_BYTES)
104
+ continue; // pathological line — skip, don't parse
105
+ let event;
106
+ try {
107
+ event = JSON.parse(line);
108
+ }
109
+ catch {
110
+ continue; // torn tail line of a crashed run — not this surface's problem
111
+ }
112
+ if (typeof event.cliKey !== 'string')
113
+ continue;
114
+ const ts = typeof event.ts === 'number' ? event.ts : null;
115
+ if (event.type === 'acpSessionStarted') {
116
+ const e = entryFor(byCli, event.cliKey);
117
+ e.sessionsStarted += 1;
118
+ if (ts !== null && (e.lastStartedTs === null || ts > e.lastStartedTs)) {
119
+ e.lastStartedTs = ts;
120
+ }
121
+ }
122
+ else if (event.type === 'acpFallback') {
123
+ const e = entryFor(byCli, event.cliKey);
124
+ e.fallbacks += 1;
125
+ const kind = typeof event.fallbackKind === 'string' ? event.fallbackKind : 'unknown';
126
+ e.fallbackKinds[kind] = (e.fallbackKinds[kind] ?? 0) + 1;
127
+ if (ts !== null && (e.lastFallbackTs === null || ts > e.lastFallbackTs)) {
128
+ e.lastFallbackTs = ts;
129
+ }
130
+ }
131
+ }
132
+ }
133
+ catch {
134
+ // A read error mid-stream (file rotated away, disk hiccup): keep what folded so far.
135
+ }
136
+ finally {
137
+ rl.close();
138
+ stream.destroy();
139
+ }
140
+ }
141
+ /**
142
+ * A short-TTL, in-flight-deduplicated cache around {@link foldAcpEvents}. The events dir holds
143
+ * one file per run (139+ observed), so the fold is real IO — briefly caching keeps a dashboard
144
+ * polling /diagnostics from turning the daemon into an NDJSON re-reader, while the TTL keeps
145
+ * the answer honest to within seconds.
146
+ */
147
+ export class AcpFoldCache {
148
+ ttlMs;
149
+ value = null;
150
+ inFlight = null;
151
+ constructor(ttlMs = 15_000) {
152
+ this.ttlMs = ttlMs;
153
+ }
154
+ async get(eventsDir) {
155
+ const now = Date.now();
156
+ if (this.value !== null && now - this.value.at < this.ttlMs)
157
+ return this.value.byCli;
158
+ if (this.inFlight !== null)
159
+ return this.inFlight;
160
+ this.inFlight = foldAcpEvents(eventsDir)
161
+ .then((byCli) => {
162
+ this.value = { at: Date.now(), byCli };
163
+ return byCli;
164
+ })
165
+ .finally(() => {
166
+ this.inFlight = null;
167
+ });
168
+ return this.inFlight;
169
+ }
170
+ }
171
+ /** Keep captured lines readable, not exhaustive: the tail is a diagnosis pointer, the full
172
+ * narrative stays wherever the operator ships the daemon's stdout. */
173
+ const RING_LINE_MAX_CHARS = 2000;
174
+ /**
175
+ * A bounded ring of the daemon's own error-level log lines.
176
+ *
177
+ * Crew has no persistent log file of its own (pino writes to stdout; `daemon-stdout.log` under
178
+ * the state home belongs to whatever launched the daemon) — so the "recent errors" record is
179
+ * an IN-PROCESS ring fed by a tee on the pino stream ({@link teeStreamWithErrorRing}). Empty
180
+ * on a fresh or healthy daemon; capped so it can never grow with uptime.
181
+ */
182
+ export class ErrorRing {
183
+ cap;
184
+ entries = [];
185
+ constructor(cap = 20) {
186
+ this.cap = cap;
187
+ }
188
+ push(entry) {
189
+ const line = entry.line.length > RING_LINE_MAX_CHARS
190
+ ? `${entry.line.slice(0, RING_LINE_MAX_CHARS)}…`
191
+ : entry.line;
192
+ this.entries.push({ ...entry, line });
193
+ if (this.entries.length > this.cap)
194
+ this.entries.shift();
195
+ }
196
+ /** Newest first — the wire order. */
197
+ list() {
198
+ return [...this.entries].reverse();
199
+ }
200
+ }
201
+ /** Pino numeric levels: 50 = error, 60 = fatal. Anything below stays out of the ring. */
202
+ const PINO_ERROR_LEVEL = 50;
203
+ /**
204
+ * A pino destination that forwards every line to `out` (stdout in the daemon) and folds
205
+ * error-level lines into `ring`. Passed to Fastify as `logger.stream`, so the ring sees
206
+ * exactly what the daemon logs — no second logging channel to keep in step.
207
+ *
208
+ * The parse is guarded end-to-end: a log line that fails to parse as pino JSON still ships to
209
+ * `out` verbatim, and a throw in the ring can never break logging (which would turn the
210
+ * diagnosis surface into an outage).
211
+ */
212
+ export function teeStreamWithErrorRing(ring, out = process.stdout) {
213
+ return {
214
+ write(chunk) {
215
+ try {
216
+ out.write(chunk);
217
+ }
218
+ catch {
219
+ /* a broken stdout must not also kill the ring */
220
+ }
221
+ try {
222
+ // Cheap pre-filter before JSON.parse — most lines are info/debug.
223
+ if (!/"level":\s*(50|60)\b/.test(chunk))
224
+ return;
225
+ for (const line of chunk.split('\n')) {
226
+ if (line === '')
227
+ continue;
228
+ const parsed = JSON.parse(line);
229
+ const level = typeof parsed.level === 'number' ? parsed.level : 0;
230
+ if (level < PINO_ERROR_LEVEL)
231
+ continue;
232
+ ring.push({
233
+ ts: typeof parsed.time === 'number' ? parsed.time : Date.now(),
234
+ source: 'daemon',
235
+ line: typeof parsed.msg === 'string' ? parsed.msg : line,
236
+ });
237
+ }
238
+ }
239
+ catch {
240
+ /* an unparseable line is not an error record — skip it */
241
+ }
242
+ },
243
+ };
244
+ }
245
+ // ── Component versions ───────────────────────────────────────────────────────
246
+ /**
247
+ * The bundled studio's version, read from the manifest the bundle ALREADY ships:
248
+ * `testid-inventory.json` carries `studioVersion` (a versioned build artifact, TH-13). No
249
+ * bundle — a headless daemon — is `null`, and so is a bundle predating the inventory.
250
+ */
251
+ export function readStudioBundleVersion(studioRoot) {
252
+ try {
253
+ const raw = readFileSync(join(studioRoot, 'testid-inventory.json'), 'utf8');
254
+ const parsed = JSON.parse(raw);
255
+ return typeof parsed.studioVersion === 'string' ? parsed.studioVersion : null;
256
+ }
257
+ catch {
258
+ return null;
259
+ }
260
+ }
261
+ /**
262
+ * The installed version of a dependency, or `null`. Walks the resolver's candidate dirs and
263
+ * reads package.json with fs — the same sidestep `apiTypesVersion()` uses, because a
264
+ * types-only exports map (wicked-core-ts included) refuses `require.resolve` on the package
265
+ * root and on `/package.json` alike.
266
+ */
267
+ export function installedPackageVersion(name) {
268
+ try {
269
+ const require = createRequire(import.meta.url);
270
+ for (const dir of require.resolve.paths(name) ?? []) {
271
+ const pkgPath = join(dir, name, 'package.json');
272
+ if (existsSync(pkgPath)) {
273
+ const version = JSON.parse(readFileSync(pkgPath, 'utf8'))
274
+ .version;
275
+ return typeof version === 'string' ? version : null;
276
+ }
277
+ }
278
+ }
279
+ catch {
280
+ /* fall through to null */
281
+ }
282
+ return null;
283
+ }
284
+ /** First semver-looking token of a `--version` output, else the trimmed first line, else null. */
285
+ export function parseVersionOutput(stdout) {
286
+ const firstLine = stdout.split('\n', 1)[0]?.trim() ?? '';
287
+ if (firstLine === '')
288
+ return null;
289
+ const semver = firstLine.match(/\d+\.\d+\.\d+[^\s]*/);
290
+ return semver !== null ? semver[0] : firstLine;
291
+ }
292
+ /**
293
+ * `--version` of the engine binaries crew ALREADY knows how to invoke — never a new path
294
+ * resolution invented for diagnostics:
295
+ *
296
+ * - `wicked-core`: the standalone engine binary the adapter resolves at construction for the
297
+ * gate-hook command and publishes as `WICKED_CORE_EXE`. Env unset = crew never resolved
298
+ * one = `null`.
299
+ * - `wicked-estate`: invoked by bare name on PATH, exactly as the graph-view/blast-radius
300
+ * routes do. Not on PATH = `null`.
301
+ *
302
+ * Under a test runner the probes are SKIPPED (both `null`) unless an exec is injected — the
303
+ * seat-health-probe posture: a test-built server never spawns CLI children by default.
304
+ */
305
+ export async function engineBinaryVersions(exec) {
306
+ const underTestRunner = process.env['VITEST'] !== undefined || process.env['NODE_ENV'] === 'test';
307
+ if (exec === undefined && underTestRunner) {
308
+ return { 'wicked-core': null, 'wicked-estate': null };
309
+ }
310
+ const run = exec ?? execCapped;
311
+ const probe = async (file) => {
312
+ try {
313
+ const { stdout } = await run(file, ['--version'], { timeout: 5_000 });
314
+ return parseVersionOutput(stdout);
315
+ }
316
+ catch {
317
+ return null;
318
+ }
319
+ };
320
+ const coreExe = process.env['WICKED_CORE_EXE'];
321
+ const [core, estate] = await Promise.all([
322
+ coreExe !== undefined && coreExe !== '' ? probe(coreExe) : Promise.resolve(null),
323
+ probe('wicked-estate'),
324
+ ]);
325
+ return { 'wicked-core': core, 'wicked-estate': estate };
326
+ }
327
+ /** Version probes answer the same for the life of a process — cache the first answer. */
328
+ export class EngineVersionCache {
329
+ value = null;
330
+ inFlight = null;
331
+ async get(exec) {
332
+ if (this.value !== null)
333
+ return this.value;
334
+ if (this.inFlight !== null)
335
+ return this.inFlight;
336
+ this.inFlight = engineBinaryVersions(exec)
337
+ .then((v) => {
338
+ this.value = v;
339
+ return v;
340
+ })
341
+ .finally(() => {
342
+ this.inFlight = null;
343
+ });
344
+ return this.inFlight;
345
+ }
346
+ }
347
+ /** Directory totals stop counting past this many entries — a runaway dir must not turn a
348
+ * diagnostics GET into a filesystem crawl. The observed events dir is ~139 files. */
349
+ const DIR_SIZE_MAX_ENTRIES = 10_000;
350
+ async function dirBytes(path) {
351
+ let total = 0;
352
+ let seen = 0;
353
+ const stack = [path];
354
+ while (stack.length > 0) {
355
+ const dir = stack.pop();
356
+ let names;
357
+ try {
358
+ names = await fsp.readdir(dir);
359
+ }
360
+ catch {
361
+ continue;
362
+ }
363
+ for (const name of names) {
364
+ if (seen >= DIR_SIZE_MAX_ENTRIES)
365
+ return total;
366
+ seen += 1;
367
+ const child = join(dir, name);
368
+ try {
369
+ const st = await fsp.stat(child);
370
+ if (st.isDirectory())
371
+ stack.push(child);
372
+ else
373
+ total += st.size;
374
+ }
375
+ catch {
376
+ /* raced deletion — skip */
377
+ }
378
+ }
379
+ }
380
+ return total;
381
+ }
382
+ /**
383
+ * `core.db` and every sidecar sharing its basename (`core.db-wal`, `core.db.knowledge`,
384
+ * `core.db.mem*`, …), plus the events dir (`core.db.events`) sized as a TOTAL of its
385
+ * contents. Paths and sizes only — no file contents ever ride this wire.
386
+ */
387
+ export async function listStoreFiles(dbPath) {
388
+ const home = dirname(dbPath);
389
+ const base = basename(dbPath);
390
+ let names;
391
+ try {
392
+ names = await fsp.readdir(home);
393
+ }
394
+ catch {
395
+ return [];
396
+ }
397
+ const matches = names
398
+ .filter((n) => n === base || n.startsWith(`${base}.`) || n.startsWith(`${base}-`))
399
+ .sort();
400
+ const out = [];
401
+ for (const name of matches) {
402
+ const path = join(home, name);
403
+ try {
404
+ const st = await fsp.stat(path);
405
+ out.push({ name, path, bytes: st.isDirectory() ? await dirBytes(path) : st.size });
406
+ }
407
+ catch {
408
+ /* raced deletion — skip */
409
+ }
410
+ }
411
+ return out;
412
+ }
413
+ /** Where a core db's durable run event logs live — the engine's `<db>.events/` convention. */
414
+ export function eventsDirOf(dbPath) {
415
+ return `${dbPath}.events`;
416
+ }
417
+ //# sourceMappingURL=diagnostics.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diagnostics.js","sourceRoot":"","sources":["../../src/api/diagnostics.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Multi-codebase scope resolution — the PINNED multiscope wire shared by the testing launch
3
+ * surfaces (`POST /testing/recon`, `POST /campaigns`).
4
+ *
5
+ * Both launch bodies accept two OPTIONAL camelCase fields:
6
+ *
7
+ * - `repoRefs?: string[]` — explicit codebase attachments. Each ref must resolve in the repo
8
+ * registry (by id, or by unique name — resolved TO the id) or the whole request fails with a
9
+ * 400 that NAMES the bad ref. Deduped, first occurrence wins.
10
+ * - `projectId?: string` — crew resolves the project's `crew.repo` members server-side.
11
+ * Unknown project ⇒ 404; a project with zero repo members (and no explicit `repoRefs` to
12
+ * fall back on) ⇒ 400 naming the fix.
13
+ *
14
+ * BOTH given ⇒ the union (explicit `repoRefs` order first, then project members not already
15
+ * named). NEITHER ⇒ `repos: []` and the launch behaves exactly as it did before this wire
16
+ * existed — backward compatible by construction.
17
+ *
18
+ * Fail-closed by doctrine (FINDING-031 posture): a ref that would not launch is refused HERE,
19
+ * before anything is persisted, with the offending token named — never a silently narrowed
20
+ * scope or a mid-run "no such repo" a worker discovers for the operator.
21
+ */
22
+ import type { CoreAdapter } from '../core/adapter.js';
23
+ /** The two optional launch-body fields, as parsed by the route's zod schema. */
24
+ export interface MultiscopeFields {
25
+ projectId?: string | undefined;
26
+ repoRefs?: string[] | undefined;
27
+ }
28
+ /** A resolved repo — id is what launches carry (`repoRef`/`repo_ref`), name is for display. */
29
+ export interface ScopedRepo {
30
+ id: string;
31
+ name: string;
32
+ }
33
+ export type ScopeResolution = {
34
+ ok: true;
35
+ repos: ScopedRepo[];
36
+ } | {
37
+ ok: false;
38
+ status: 400 | 404 | 409;
39
+ error: string;
40
+ };
41
+ /**
42
+ * Resolve the launch scope. Throws only what the adapter throws (notably
43
+ * `ProjectsUnsupportedError` on a pre-projects addon — the caller maps it to 501, the same
44
+ * "upgrade the engine" posture every project route holds).
45
+ */
46
+ export declare function resolveScopeRepos(adapter: CoreAdapter, fields: MultiscopeFields): Promise<ScopeResolution>;
47
+ //# sourceMappingURL=multiscope.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"multiscope.d.ts","sourceRoot":"","sources":["../../src/api/multiscope.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAEtD,gFAAgF;AAChF,MAAM,WAAW,gBAAgB;IAC/B,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC/B,QAAQ,CAAC,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC;CACjC;AAED,+FAA+F;AAC/F,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,MAAM,eAAe,GACvB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,UAAU,EAAE,CAAA;CAAE,GACjC;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,GAAG,GAAG,GAAG,GAAG,GAAG,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAE1D;;;;GAIG;AACH,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,WAAW,EACpB,MAAM,EAAE,gBAAgB,GACvB,OAAO,CAAC,eAAe,CAAC,CA0F1B"}