wicked-crew 0.7.6 → 0.7.8
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.
- package/dist/api/diagnostics.d.ts +143 -0
- package/dist/api/diagnostics.d.ts.map +1 -0
- package/dist/api/diagnostics.js +417 -0
- package/dist/api/diagnostics.js.map +1 -0
- package/dist/api/multiscope.d.ts +47 -0
- package/dist/api/multiscope.d.ts.map +1 -0
- package/dist/api/multiscope.js +106 -0
- package/dist/api/multiscope.js.map +1 -0
- package/dist/api/routes.d.ts +7 -0
- package/dist/api/routes.d.ts.map +1 -1
- package/dist/api/routes.js +46 -3
- package/dist/api/routes.js.map +1 -1
- package/dist/api/server.d.ts.map +1 -1
- package/dist/api/server.js +17 -3
- package/dist/api/server.js.map +1 -1
- package/dist/api/testing.d.ts +26 -1
- package/dist/api/testing.d.ts.map +1 -1
- package/dist/api/testing.js +137 -2
- package/dist/api/testing.js.map +1 -1
- package/dist/campaigns/plan.d.ts +32 -1
- package/dist/campaigns/plan.d.ts.map +1 -1
- package/dist/campaigns/plan.js +86 -0
- package/dist/campaigns/plan.js.map +1 -1
- package/dist/campaigns/routes.d.ts +104 -0
- package/dist/campaigns/routes.d.ts.map +1 -1
- package/dist/campaigns/routes.js +43 -10
- package/dist/campaigns/routes.js.map +1 -1
- package/dist/studio/assets/index-BdQQv2hz.js +537 -0
- package/dist/studio/assets/index-BmmNnS2d.css +32 -0
- package/dist/studio/index.html +2 -2
- package/dist/studio/testid-inventory.json +541 -222
- package/endpoint-manifest.json +26 -2
- package/package.json +2 -2
- package/dist/studio/assets/index-Bj7TB0yo.js +0 -533
- package/dist/studio/assets/index-DJzyQBx3.css +0 -32
|
@@ -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":"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"}
|
|
@@ -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"}
|