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.
@@ -0,0 +1,106 @@
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
+ /**
23
+ * Resolve the launch scope. Throws only what the adapter throws (notably
24
+ * `ProjectsUnsupportedError` on a pre-projects addon — the caller maps it to 501, the same
25
+ * "upgrade the engine" posture every project route holds).
26
+ */
27
+ export async function resolveScopeRepos(adapter, fields) {
28
+ const { projectId, repoRefs } = fields;
29
+ if (projectId === undefined && repoRefs === undefined) {
30
+ return { ok: true, repos: [] };
31
+ }
32
+ const registry = await adapter.listRepos();
33
+ const byId = new Map(registry.map((r) => [r.id, r]));
34
+ const byName = new Map(registry.map((r) => [r.name, r]));
35
+ const repos = [];
36
+ const seen = new Set();
37
+ const admit = (id, name) => {
38
+ if (!seen.has(id)) {
39
+ seen.add(id);
40
+ repos.push({ id, name });
41
+ }
42
+ };
43
+ for (const ref of repoRefs ?? []) {
44
+ const hit = byId.get(ref) ?? byName.get(ref);
45
+ if (hit === undefined) {
46
+ return {
47
+ ok: false,
48
+ status: 400,
49
+ error: `repoRefs: '${ref}' does not name a registered repo — register it first ` +
50
+ `(POST /api/v1/repos) or pick a ref from GET /api/v1/repos`,
51
+ };
52
+ }
53
+ admit(hit.id, hit.name);
54
+ }
55
+ if (projectId !== undefined) {
56
+ if (projectId === 'default') {
57
+ // The synthesized "Unfiled" container (ADR §7) is computed, never stored — it has no
58
+ // membership rows to resolve, and `projectGet` answering null would mislabel it unknown.
59
+ return {
60
+ ok: false,
61
+ status: 400,
62
+ error: `projectId: 'default' is the synthesized unfiled container and has no repo members ` +
63
+ `to resolve — name a real project or pass repoRefs explicitly`,
64
+ };
65
+ }
66
+ const project = await adapter.projectGet(projectId);
67
+ if (project === null) {
68
+ return { ok: false, status: 404, error: `unknown project: ${projectId}` };
69
+ }
70
+ if (project.status === 'archived') {
71
+ return {
72
+ ok: false,
73
+ status: 409,
74
+ error: `project '${projectId}' is archived — restore it (PATCH /api/v1/projects/` +
75
+ `${projectId} with status "active") or pass repoRefs explicitly`,
76
+ };
77
+ }
78
+ const members = (await adapter.projectMembers(projectId)).filter((m) => m.member_kind === 'crew.repo');
79
+ if (members.length === 0 && (repoRefs === undefined || repoRefs.length === 0)) {
80
+ return {
81
+ ok: false,
82
+ status: 400,
83
+ error: `project '${projectId}' has no repo members — attach one ` +
84
+ `(POST /api/v1/projects/${projectId}/members with kind "crew.repo") ` +
85
+ `or pass repoRefs explicitly`,
86
+ };
87
+ }
88
+ for (const m of members) {
89
+ const hit = byId.get(m.member_ref);
90
+ if (hit === undefined) {
91
+ // A member ref pointing at nothing in the registry would launch a run that fails on a
92
+ // repo the CALLER never named — refuse loudly, with the dangling ref and the fix named.
93
+ return {
94
+ ok: false,
95
+ status: 400,
96
+ error: `project '${projectId}' has a repo member '${m.member_ref}' that is not in the ` +
97
+ `repo registry — detach the stale member (DELETE /api/v1/projects/${projectId}` +
98
+ `/members/${m.id}) or re-register the repo, then retry`,
99
+ };
100
+ }
101
+ admit(hit.id, hit.name);
102
+ }
103
+ }
104
+ return { ok: true, repos };
105
+ }
106
+ //# sourceMappingURL=multiscope.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"multiscope.js","sourceRoot":"","sources":["../../src/api/multiscope.ts"],"names":[],"mappings":"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"}
@@ -8,6 +8,7 @@ import { SeatHealthTracker } from './seat-health.js';
8
8
  import { type ProjectRoutesDeps } from '../projects/routes.js';
9
9
  import { InteractiveBridgePool } from '../interactive/bridge-pool.js';
10
10
  import { AuditLog } from './audit.js';
11
+ import { type ErrorRing } from './diagnostics.js';
11
12
  import { RetryIndex } from './retry-index.js';
12
13
  import { GuidanceIndex } from './guidance-index.js';
13
14
  import { DeliveryIndex } from './delivery-index.js';
@@ -170,6 +171,12 @@ export interface RuntimeDeps {
170
171
  * slice 1). Injectable so the integration suite proxies to a FAKE bridge instead of
171
172
  * spawning a real `npx wicked-interactive serve`. */
172
173
  interactiveBridges?: InteractiveBridgePool;
174
+ /** The daemon's in-process error-level log ring (diagnostics) — `createServer` tees the pino
175
+ * stream into one; a directly-driven route set (tests) gets an honestly-empty tail. */
176
+ errorRing?: ErrorRing;
177
+ /** The studio asset root `createServer` resolved (bundled or overridden) — diagnostics reads
178
+ * the bundle's shipped version manifest from it. Absent = headless = `studioBundle: null`. */
179
+ studioRoot?: string;
173
180
  }
174
181
  /**
175
182
  * The daemon REST surface. Every endpoint is a thin wrapper over one adapter /
@@ -1 +1 @@
1
- {"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/api/routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAgB,MAAM,SAAS,CAAC;AAC7D,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAOxB,OAAO,EAAwB,WAAW,EAA4E,MAAM,oBAAoB,CAAC;AAEjJ,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC/D,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAMnD,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAWrD,OAAO,EAAyB,KAAK,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAWtF,OAAO,EAAe,qBAAqB,EAAE,MAAM,+BAA+B,CAAC;AAInF,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAe,KAAK,QAAQ,EAAE,MAAM,WAAW,CAAC;AAGvD,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAgG7C;;;;;;;;;;;;GAYG;AAIH,eAAO,MAAM,YAAY;;;;;;;;IAQvB;+FAC2F;;IAE3F;;wFAEoF;;IAEpF;;0FAEsF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAKtF,CAAC;AAEH,eAAO,MAAM,UAAU;;;;;;;;;EAGZ,CAAC;AAEZ;;yEAEyE;AACzE,eAAO,MAAM,cAAc;;;;;;EAEhB,CAAC;AAUZ,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;EAQpB,CAAC;AAOZ,4FAA4F;AAC5F,eAAO,MAAM,cAAc;;;;;;;;;EAGhB,CAAC;AAwBZ,sEAAsE;AACtE,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,QAAQ,CAAC;IAChB,QAAQ,EAAE,QAAQ,CAAC;CACpB;AAED;;;;GAIG;AACH,MAAM,WAAW,WAAW;IAC1B,UAAU,CAAC,EAAE,iBAAiB,CAAC;IAC/B;oGACgG;IAChG,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB;uGACmG;IACnG,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B;;qBAEiB;IACjB,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,UAAU,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/C;2FACuF;IACvF,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,gBAAgB,CAAC,EAAE,MAAM,KAAK,OAAO,GAAG,IAAI,CAAC;IAC1E;;0DAEsD;IACtD,kBAAkB,CAAC,EAAE,qBAAqB,CAAC;CAC5C;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAC5B,GAAG,EAAE,eAAe,EACpB,OAAO,EAAE,WAAW,EACpB,SAAS,EAAE,SAAS,EACpB,gBAAgB,EAAE,gBAAgB,EAIlC,YAAY,GAAE,WAA+B,EAG7C,QAAQ,GAAE,iBAAqF,EAK/F,QAAQ,GAAE,YAA0D,EAGpE,OAAO,GAAE,WAAgB,GACxB,IAAI,CAk+DN"}
1
+ {"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/api/routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAgB,MAAM,SAAS,CAAC;AAC7D,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAOxB,OAAO,EAAwB,WAAW,EAA4E,MAAM,oBAAoB,CAAC;AAEjJ,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC/D,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAMnD,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAWrD,OAAO,EAAyB,KAAK,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAWtF,OAAO,EAAe,qBAAqB,EAAE,MAAM,+BAA+B,CAAC;AAInF,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,OAAO,EAOL,KAAK,SAAS,EACf,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAe,KAAK,QAAQ,EAAE,MAAM,WAAW,CAAC;AAGvD,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAgG7C;;;;;;;;;;;;GAYG;AAIH,eAAO,MAAM,YAAY;;;;;;;;IAQvB;+FAC2F;;IAE3F;;wFAEoF;;IAEpF;;0FAEsF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAKtF,CAAC;AAEH,eAAO,MAAM,UAAU;;;;;;;;;EAGZ,CAAC;AAEZ;;yEAEyE;AACzE,eAAO,MAAM,cAAc;;;;;;EAEhB,CAAC;AAUZ,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;EAQpB,CAAC;AAOZ,4FAA4F;AAC5F,eAAO,MAAM,cAAc;;;;;;;;;EAGhB,CAAC;AAwBZ,sEAAsE;AACtE,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,QAAQ,CAAC;IAChB,QAAQ,EAAE,QAAQ,CAAC;CACpB;AAED;;;;GAIG;AACH,MAAM,WAAW,WAAW;IAC1B,UAAU,CAAC,EAAE,iBAAiB,CAAC;IAC/B;oGACgG;IAChG,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB;uGACmG;IACnG,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B;;qBAEiB;IACjB,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,UAAU,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/C;2FACuF;IACvF,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,gBAAgB,CAAC,EAAE,MAAM,KAAK,OAAO,GAAG,IAAI,CAAC;IAC1E;;0DAEsD;IACtD,kBAAkB,CAAC,EAAE,qBAAqB,CAAC;IAC3C;4FACwF;IACxF,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB;mGAC+F;IAC/F,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAC5B,GAAG,EAAE,eAAe,EACpB,OAAO,EAAE,WAAW,EACpB,SAAS,EAAE,SAAS,EACpB,gBAAgB,EAAE,gBAAgB,EAIlC,YAAY,GAAE,WAA+B,EAG7C,QAAQ,GAAE,iBAAqF,EAK/F,QAAQ,GAAE,YAA0D,EAGpE,OAAO,GAAE,WAAgB,GACxB,IAAI,CAkhEN"}
@@ -28,6 +28,7 @@ import { registerInteractiveProxy } from '../interactive/proxy-routes.js';
28
28
  import { MembershipIndex } from '../projects/membership-index.js';
29
29
  import { MEMBERSHIP_ATTACHED, membershipAttachedKey } from '../projects/events.js';
30
30
  import { AuditLog } from './audit.js';
31
+ import { AcpFoldCache, EngineVersionCache, eventsDirOf, installedPackageVersion, listStoreFiles, readStudioBundleVersion, } from './diagnostics.js';
31
32
  import { RetryIndex } from './retry-index.js';
32
33
  import { GuidanceIndex } from './guidance-index.js';
33
34
  import { DeliveryIndex } from './delivery-index.js';
@@ -267,6 +268,41 @@ runtime = {}) {
267
268
  const ping = await adapter.ping();
268
269
  return { status: 'ok', version: PKG_VERSION, ping };
269
270
  });
271
+ // The daemon's self-knowledge surface (diagnostics): what is deployed, what it stores, what
272
+ // has gone wrong recently, and whether ACP is really working across the CLIs — the answer
273
+ // that previously lived only in raw NDJSON under the state home. READ-ONLY throughout, and
274
+ // honest throughout: a field the daemon cannot answer is null/empty, never fabricated.
275
+ // The ACP fold and the binary probes are cached (briefly / per-process) so a polling skin
276
+ // can never turn this GET into an event-log re-reader or a `--version` spawner per request.
277
+ const acpFoldCache = new AcpFoldCache();
278
+ const engineVersionCache = new EngineVersionCache();
279
+ app.get(`${V}/diagnostics`, { config: { manifest: { responseType: 'DiagnosticsResponse', statusCodes: [200] } } }, async () => {
280
+ // `dbPath` is a readonly field of the real adapter; a stub-driven route set may lack it,
281
+ // and diagnostics over an unknown store honestly reports no stores and no ACP record.
282
+ const dbPath = typeof adapter.dbPath === 'string' && adapter.dbPath !== '' ? adapter.dbPath : null;
283
+ const [stores, byCli, engineBinaries] = await Promise.all([
284
+ dbPath !== null ? listStoreFiles(dbPath) : Promise.resolve([]),
285
+ dbPath !== null
286
+ ? acpFoldCache.get(eventsDirOf(dbPath))
287
+ : Promise.resolve({}),
288
+ engineVersionCache.get(),
289
+ ]);
290
+ const uptimeMs = Math.round(process.uptime() * 1000);
291
+ const addr = app.server.address();
292
+ const port = typeof addr === 'object' && addr !== null ? addr.port : 7701;
293
+ return {
294
+ components: {
295
+ crew: PKG_VERSION,
296
+ studioBundle: runtime.studioRoot !== undefined ? readStudioBundleVersion(runtime.studioRoot) : null,
297
+ coreTs: installedPackageVersion('wicked-core-ts'),
298
+ engineBinaries,
299
+ },
300
+ daemon: { uptimeMs, startedAt: Date.now() - uptimeMs, port },
301
+ stores,
302
+ recentErrors: runtime.errorRing?.list() ?? [],
303
+ acp: { byCli },
304
+ };
305
+ });
270
306
  // Who am I talking to the daemon as? (task #88). In local mode this is
271
307
  // always the implicit full-trust local actor; in required mode it names the
272
308
  // token's actor — the cheap probe a skin uses to decide what to render.
@@ -2103,10 +2139,17 @@ runtime = {}) {
2103
2139
  actorOf,
2104
2140
  roster: () => CoreAdapter.roster(),
2105
2141
  });
2106
- // ── Testing (crew-testing) — governance evals + eval corpora ────────────────
2142
+ // ── Testing (crew-testing) — governance evals + eval corpora + the recon trigger ────────────
2107
2143
  // Run the evals (does the steering corpus catch what it claims to?) and import a named eval
2108
- // corpus. Both presence-gated on the engine's evals bindings (501 on wicked-core-ts < 0.7.5).
2109
- registerTestingRoutes(app, adapter, { audit, actorOf });
2144
+ // corpus — both presence-gated on the engine's evals bindings (501 on wicked-core-ts < 0.7.5).
2145
+ // POST /testing/recon launches governed recon runs (the multiscope fan-out), so it gets the
2146
+ // roster and the SAME membership plumbing POST /runs files projectId launches through.
2147
+ registerTestingRoutes(app, adapter, {
2148
+ audit,
2149
+ actorOf,
2150
+ roster: () => CoreAdapter.roster(),
2151
+ projects: { bus: projects.bus, index: projects.index },
2152
+ });
2110
2153
  // ── The wicked-interactive bridge, reverse-proxied (DES-MERGE-001 §5.3/§7.2) ──
2111
2154
  // Mounted BESIDE the routes above and under the same `${V}` prefix, so it inherits one
2112
2155
  // origin, one auth hook, and one CORS posture — the whole point of slice 1.