wicked-crew 0.7.13 → 0.7.14

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.
Files changed (42) hide show
  1. package/dist/api/delivery-cache.d.ts +135 -0
  2. package/dist/api/delivery-cache.d.ts.map +1 -0
  3. package/dist/api/delivery-cache.js +245 -0
  4. package/dist/api/delivery-cache.js.map +1 -0
  5. package/dist/api/delivery-index.d.ts +42 -11
  6. package/dist/api/delivery-index.d.ts.map +1 -1
  7. package/dist/api/delivery-index.js +104 -52
  8. package/dist/api/delivery-index.js.map +1 -1
  9. package/dist/api/endpoint-manifest-live.d.ts.map +1 -1
  10. package/dist/api/endpoint-manifest-live.js +0 -1
  11. package/dist/api/endpoint-manifest-live.js.map +1 -1
  12. package/dist/api/gate-cache.d.ts +24 -0
  13. package/dist/api/gate-cache.d.ts.map +1 -1
  14. package/dist/api/gate-cache.js +35 -0
  15. package/dist/api/gate-cache.js.map +1 -1
  16. package/dist/api/routes.d.ts +14 -7
  17. package/dist/api/routes.d.ts.map +1 -1
  18. package/dist/api/routes.js +60 -13
  19. package/dist/api/routes.js.map +1 -1
  20. package/dist/api/seat-health.d.ts +0 -25
  21. package/dist/api/seat-health.d.ts.map +1 -1
  22. package/dist/api/seat-health.js +6 -81
  23. package/dist/api/seat-health.js.map +1 -1
  24. package/dist/api/server.d.ts +18 -15
  25. package/dist/api/server.d.ts.map +1 -1
  26. package/dist/api/server.js +87 -36
  27. package/dist/api/server.js.map +1 -1
  28. package/dist/api/stall-watchdog.d.ts +40 -3
  29. package/dist/api/stall-watchdog.d.ts.map +1 -1
  30. package/dist/api/stall-watchdog.js +94 -15
  31. package/dist/api/stall-watchdog.js.map +1 -1
  32. package/dist/campaigns/rollup.d.ts +3 -0
  33. package/dist/campaigns/rollup.d.ts.map +1 -1
  34. package/dist/campaigns/rollup.js +17 -2
  35. package/dist/campaigns/rollup.js.map +1 -1
  36. package/dist/campaigns/routes.d.ts +4 -4
  37. package/dist/core/types.d.ts +20 -4
  38. package/dist/core/types.d.ts.map +1 -1
  39. package/dist/core/types.js +20 -1
  40. package/dist/core/types.js.map +1 -1
  41. package/endpoint-manifest.json +3 -2
  42. package/package.json +2 -2
@@ -7,6 +7,7 @@ import { isAbsolute, join, relative, resolve } from 'node:path';
7
7
  import { fileURLToPath } from 'node:url';
8
8
  import { CampaignsUnsupportedError, ChatUnsupportedError, CoreAdapter, ElicitationUnsupportedError, SteeringUnsupportedError, humanGatePhaseIds } from '../core/adapter.js';
9
9
  import { codeGraphDb, requirementsGraph } from '../core/repoPaths.js';
10
+ import { detectRefusal } from './gate-cache.js';
10
11
  import { QeGateCache } from '../qe/gate-events.js';
11
12
  import { buildAcceptanceView, resolveRunWorkflow } from '../qe/acceptance.js';
12
13
  import { buildEvidenceBundle, coreUnitId, evidenceFilename } from './evidence.js';
@@ -34,7 +35,8 @@ import { AcpFoldCache, EngineVersionCache, eventsDirOf, installedPackageVersion,
34
35
  import { RetryIndex } from './retry-index.js';
35
36
  import { GroupIndex } from './group-index.js';
36
37
  import { GuidanceIndex } from './guidance-index.js';
37
- import { DeliveryIndex, deliveryStateWithVacuity, gitRunBranchIsEmpty, gitWorktreeIsClean, isDeliverConflictStranded, prUrlFrom, } from './delivery-index.js';
38
+ import { DeliveryIndex, gitRunBranchIsEmpty, gitWorktreeIsClean, isDeliverConflictStranded, prUrlFrom, } from './delivery-index.js';
39
+ import { DeliveryDerivationCache } from './delivery-cache.js';
38
40
  import { gitReprovisionWorktree, runDeliverScript, } from './post-hoc-deliver.js';
39
41
  import { LOCAL_ACTOR } from './auth.js';
40
42
  // Re-exported so existing `import { API_PREFIX } from './routes.js'` callers keep working; the
@@ -262,6 +264,16 @@ runtime = {}) {
262
264
  return repos.find((r) => r.id === repoRef)?.root_path;
263
265
  }),
264
266
  };
267
+ // The background layer that owns the git probes above (GET /runs p99): the run DTOs read it,
268
+ // the daemon's sweep/warm feed it. The default (a directly-driven route set) is COLD and
269
+ // unstarted — no background timer under a unit test, reads degrade to the stat-only
270
+ // tri-state — while `createServer` injects a started one over the production probes.
271
+ const deliveryCache = runtime.deliveryCache ??
272
+ new DeliveryDerivationCache({
273
+ listViews: () => adapter.sessionsDetail(),
274
+ probes: vacuityProbes,
275
+ isDelivered: (runId) => deliveryIndex.urlFor(runId) !== undefined,
276
+ });
265
277
  const deliverExec = runtime.deliverExec ?? runDeliverScript;
266
278
  const reprovisionWorktree = runtime.reprovisionWorktree ?? gitReprovisionWorktree;
267
279
  /** Repo root for a repo ref, from the registry — shared by the reprovision path below. */
@@ -275,7 +287,10 @@ runtime = {}) {
275
287
  // completed code run whose work is sitting unlifted in its worktree is VISIBLE on the wire,
276
288
  // legacy records included. The would-be-stranded runs are further split (crew#311): a
277
289
  // completed run whose worktree carries NO contribution at all reads `'vacuous'` — units all
278
- // "done" with nothing produced must be LOUD on the wire, never silently green.
290
+ // "done" with nothing produced must be LOUD on the wire, never silently green. The vacuity
291
+ // split's git probes run in the BACKGROUND (delivery-cache.ts — GET /runs p99): DTO assembly
292
+ // reads the cache and degrades to the stat-only stranded/none label for at most one sweep
293
+ // tick on a miss, so the list fan-out never spawns git.
279
294
  // Applied at DTO assembly on exactly the two endpoints that serve the run DTO
280
295
  // (GET /runs + GET /runs/:id); the internal sessionsDetail() consumers are untouched.
281
296
  // crew#418 A: a `failed` run whose ONLY failure was the deliver phase's LIFT collision (a
@@ -303,16 +318,18 @@ runtime = {}) {
303
318
  };
304
319
  /** The run's delivery state, honest by construction (crew#393/#311/#418): a recorded PR wins
305
320
  * (`'delivered'`); else a lift-conflict strand reads `'stranded'` from the branch; else the
306
- * worktree-stat derivation (stranded / vacuous / none). */
307
- const resolveDelivery = async (view, conflictStrand) => {
321
+ * delivery-derivation CACHE (a background-derived stranded / vacuous / none — GET /runs p99).
322
+ * A cache miss answers the stat-only tri-state (one existsSync, the pre-crew#311 label) for
323
+ * at most one sweep tick; the request path never spawns git and never awaits a derivation. */
324
+ const resolveDelivery = (view, conflictStrand) => {
308
325
  const url = deliveryIndex.urlFor(view.session.id);
309
326
  if (url !== undefined)
310
327
  return { delivery: 'delivered', deliverUrl: url };
311
328
  if (conflictStrand)
312
329
  return { delivery: 'stranded' };
313
- return deliveryStateWithVacuity(view.session, undefined, vacuityProbes);
330
+ return deliveryCache.read(view.session);
314
331
  };
315
- const decorateRun = async (view) => {
332
+ const decorateRun = (view) => {
316
333
  const conflictStrand = normalizeStranded(view);
317
334
  view.session.project_id = projects.index.projectOf(view.session.id) ?? null;
318
335
  const retryOf = retryIndex.retryOfFor(view.session.id);
@@ -330,7 +347,7 @@ runtime = {}) {
330
347
  const guidance = guidanceIndex.guidanceFor(view.session.id);
331
348
  if (guidance !== undefined)
332
349
  view.session.guidance = guidance;
333
- const state = await resolveDelivery(view, conflictStrand);
350
+ const state = resolveDelivery(view, conflictStrand);
334
351
  view.session.delivery = state.delivery;
335
352
  if (state.deliverUrl !== undefined)
336
353
  view.session.deliverUrl = state.deliverUrl;
@@ -858,7 +875,7 @@ runtime = {}) {
858
875
  });
859
876
  // Run list (replaces GET /sessions). Actionable-first; reconciles the gate and elicitation caches
860
877
  // so that terminal-run entries are pruned even when their terminal CoreEvent was missed.
861
- app.get(`${V}/runs`, { config: { manifest: { responseType: '{ runs: SessionView[] }', statusCodes: [200] } } }, async (req) => {
878
+ app.get(`${V}/runs`, { config: { manifest: { responseType: '{ runs: SessionView[] }', statusCodes: [200, 400] } } }, async (req, reply) => {
862
879
  const views = await adapter.sessionsDetail();
863
880
  gateCache.reconcile(views);
864
881
  elicitationCache.reconcile(views);
@@ -868,12 +885,35 @@ runtime = {}) {
868
885
  // resolve, not leak.
869
886
  // Fastify parses a REPEATED query param as string[] — normalize so `?include=archived`
870
887
  // and `?include=archived&include=archived` behave identically (Copilot).
871
- const { include } = req.query;
888
+ const { include, limit } = req.query;
872
889
  const includeArchived = (Array.isArray(include) ? include : [include]).includes('archived');
890
+ // `?limit=N` — the top N AFTER the actionable-first sort below, so a capped poll still sees
891
+ // the runs that need a human before the terminal sediment. The default stays UNBOUNDED: the
892
+ // param used to be read by nobody (silently ignored — the full payload regardless), so an
893
+ // implicit cap would silently change results for un-migrated callers. A malformed value is
894
+ // refused loudly instead of ignored (the FINDING-031 posture: ignoring a field runs a
895
+ // different request than the caller sent) — and a repeated `?limit` is malformed too, since
896
+ // two caps are an ambiguity, not an idempotent repetition like `include`.
897
+ let cap;
898
+ if (limit !== undefined) {
899
+ // STRICT canonical decimal only (Copilot on #435): `Number(...)` also admits hex
900
+ // (`0x10`), exponent (`2e3`), signed (`+1`), and whitespace-padded spellings — surprising
901
+ // aliases a 400-on-malformed contract must refuse, not quietly honor. `0` is deliberately
902
+ // VALID and answers the empty list — a real non-negative integer, not a malformation —
903
+ // while non-canonical zeros (`00`, `01`) are refused with the rest.
904
+ const canonical = !Array.isArray(limit) && /^(0|[1-9][0-9]*)$/.test(limit);
905
+ if (!canonical) {
906
+ return reply.code(400).send({
907
+ error: `Invalid ?limit — expected one non-negative decimal integer, got ${JSON.stringify(limit)}`,
908
+ });
909
+ }
910
+ cap = Number(limit);
911
+ }
873
912
  const visible = includeArchived
874
913
  ? views
875
914
  : views.filter((v) => v.session.archived_at == null);
876
- return { runs: await Promise.all(sortActionableFirst(visible).map(decorateRun)) };
915
+ const ordered = sortActionableFirst(visible);
916
+ return { runs: (cap !== undefined ? ordered.slice(0, cap) : ordered).map(decorateRun) };
877
917
  });
878
918
  // ── Run archival (crew#265) — write-off, not delete ────────────────────────
879
919
  const ArchiveSchema = z.object({
@@ -933,7 +973,7 @@ runtime = {}) {
933
973
  const run = views.find((v) => v.session.id === id);
934
974
  if (!run)
935
975
  return reply.code(404).send({ error: 'Run not found' });
936
- return { run: await decorateRun(run) };
976
+ return { run: decorateRun(run) };
937
977
  });
938
978
  // ── Post-hoc delivery (crew#393) — lift a stranded run's worktree into a PR ──
939
979
  // The recovery path for the run 83052f0b class: a COMPLETED repo-scoped run whose reviewable
@@ -1463,7 +1503,7 @@ runtime = {}) {
1463
1503
  // The 409 body carries the machine-readable pointer (`ResumeRefusal` in api-types).
1464
1504
  const terminal = run.session.status;
1465
1505
  if (terminal === 'completed' || terminal === 'cancelled') {
1466
- const state = await resolveDelivery(run, conflictStrand);
1506
+ const state = resolveDelivery(run, conflictStrand);
1467
1507
  const recovery = terminal === 'completed' && state.delivery === 'stranded'
1468
1508
  ? 'deliver'
1469
1509
  : 'retry';
@@ -1562,11 +1602,16 @@ runtime = {}) {
1562
1602
  : null;
1563
1603
  const durableGate = durableRows?.find((r) => r.kind === 'gate');
1564
1604
  if (durableGate !== undefined) {
1605
+ // This path builds the entry inline (not through `fold`), so it must run the SAME refusal
1606
+ // detection (issue #419) — otherwise a gate served from the durable row after a restart would
1607
+ // silently drop the warning that the live/replay paths carry.
1608
+ const refusal = detectRefusal(durableGate.prompt);
1565
1609
  const entry = {
1566
1610
  ord: durableGate.ord ?? 0,
1567
1611
  prompt: durableGate.prompt,
1568
1612
  lifecycle: 'open',
1569
1613
  receivedAt: new Date(durableGate.created_at).toISOString(),
1614
+ ...(refusal !== undefined ? { refusal } : {}),
1570
1615
  };
1571
1616
  gateCache.adopt(id, entry);
1572
1617
  return { runId: id, ...entry };
@@ -2434,7 +2479,7 @@ runtime = {}) {
2434
2479
  // The escalation ladder's knobs (crew#341). This trio lets the PLATFORM touch runs, so a
2435
2480
  // typo must be a 400, never a silently-dropped key that leaves the operator believing they
2436
2481
  // armed (or disarmed) automatic recovery. `workerStallEscalateMinutes: 0` is the explicit
2437
- // OFF spelling — and the shipped default is off (absent).
2482
+ // OFF spelling — the shipped default is ON at 30 (perf#4), so 0 is how an operator disarms.
2438
2483
  if (Object.hasOwn(patch, 'workerStallEscalateMinutes')) {
2439
2484
  const mins = patch.workerStallEscalateMinutes;
2440
2485
  if (typeof mins !== 'number' || !Number.isInteger(mins) || mins < 0 || mins > 1440) {
@@ -2556,6 +2601,8 @@ runtime = {}) {
2556
2601
  groupIndex,
2557
2602
  deliveryUrlFor: (runId) => deliveryIndex.urlFor(runId),
2558
2603
  vacuity: vacuityProbes,
2604
+ // A non-probe derivation throw in the rollup is a defect — error level, loud in diagnostics.
2605
+ logDefect: (m) => app.log.error(m),
2559
2606
  });
2560
2607
  // ── Governance wiki management (wiki-mgmt) — scoreboard + honest empty-state meta ──────────
2561
2608
  // Browse (`GET /governance/rules` + facets) and retire (`DELETE /governance/rules/:id`) stay