wicked-crew 0.16.4 → 0.16.5

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 (150) hide show
  1. package/dist/api/ask-paths.d.ts +5 -2
  2. package/dist/api/ask-paths.d.ts.map +1 -1
  3. package/dist/api/ask-paths.js +6 -3
  4. package/dist/api/ask-paths.js.map +1 -1
  5. package/dist/api/chat-scope.d.ts +8 -3
  6. package/dist/api/chat-scope.d.ts.map +1 -1
  7. package/dist/api/chat-scope.js +17 -5
  8. package/dist/api/chat-scope.js.map +1 -1
  9. package/dist/api/chat-workflow.d.ts +60 -0
  10. package/dist/api/chat-workflow.d.ts.map +1 -0
  11. package/dist/api/chat-workflow.js +80 -0
  12. package/dist/api/chat-workflow.js.map +1 -0
  13. package/dist/api/endpoint-manifest-live.d.ts.map +1 -1
  14. package/dist/api/endpoint-manifest-live.js +2 -0
  15. package/dist/api/endpoint-manifest-live.js.map +1 -1
  16. package/dist/api/needs.d.ts +116 -0
  17. package/dist/api/needs.d.ts.map +1 -0
  18. package/dist/api/needs.js +618 -0
  19. package/dist/api/needs.js.map +1 -0
  20. package/dist/api/routes.d.ts +23 -1
  21. package/dist/api/routes.d.ts.map +1 -1
  22. package/dist/api/routes.js +288 -23
  23. package/dist/api/routes.js.map +1 -1
  24. package/dist/api/run-timing-index.d.ts +6 -0
  25. package/dist/api/run-timing-index.d.ts.map +1 -1
  26. package/dist/api/run-timing-index.js +18 -0
  27. package/dist/api/run-timing-index.js.map +1 -1
  28. package/dist/api/server.d.ts +8 -0
  29. package/dist/api/server.d.ts.map +1 -1
  30. package/dist/api/server.js +146 -1
  31. package/dist/api/server.js.map +1 -1
  32. package/dist/api/system-snapshot.d.ts +7 -15
  33. package/dist/api/system-snapshot.d.ts.map +1 -1
  34. package/dist/api/system-snapshot.js +7 -115
  35. package/dist/api/system-snapshot.js.map +1 -1
  36. package/dist/core/run-identity.d.ts.map +1 -1
  37. package/dist/core/run-identity.js +3 -0
  38. package/dist/core/run-identity.js.map +1 -1
  39. package/dist/evals/capture.d.ts +93 -0
  40. package/dist/evals/capture.d.ts.map +1 -0
  41. package/dist/evals/capture.js +419 -0
  42. package/dist/evals/capture.js.map +1 -0
  43. package/dist/evals/replay.d.ts +33 -0
  44. package/dist/evals/replay.d.ts.map +1 -0
  45. package/dist/evals/replay.js +87 -0
  46. package/dist/evals/replay.js.map +1 -0
  47. package/dist/evals/routes.d.ts +48 -0
  48. package/dist/evals/routes.d.ts.map +1 -0
  49. package/dist/evals/routes.js +121 -0
  50. package/dist/evals/routes.js.map +1 -0
  51. package/dist/evals/run-cli.d.ts +31 -0
  52. package/dist/evals/run-cli.d.ts.map +1 -0
  53. package/dist/evals/run-cli.js +128 -0
  54. package/dist/evals/run-cli.js.map +1 -0
  55. package/dist/evals/runner.d.ts +34 -0
  56. package/dist/evals/runner.d.ts.map +1 -0
  57. package/dist/evals/runner.js +181 -0
  58. package/dist/evals/runner.js.map +1 -0
  59. package/dist/evals/score.d.ts +31 -0
  60. package/dist/evals/score.d.ts.map +1 -0
  61. package/dist/evals/score.js +191 -0
  62. package/dist/evals/score.js.map +1 -0
  63. package/dist/evals/seed-needs-you.d.ts +17 -0
  64. package/dist/evals/seed-needs-you.d.ts.map +1 -0
  65. package/dist/evals/seed-needs-you.js +57 -0
  66. package/dist/evals/seed-needs-you.js.map +1 -0
  67. package/dist/evals/seed-rig.d.ts +9 -0
  68. package/dist/evals/seed-rig.d.ts.map +1 -0
  69. package/dist/evals/seed-rig.js +156 -0
  70. package/dist/evals/seed-rig.js.map +1 -0
  71. package/dist/evals/stats.d.ts +31 -0
  72. package/dist/evals/stats.d.ts.map +1 -0
  73. package/dist/evals/stats.js +59 -0
  74. package/dist/evals/stats.js.map +1 -0
  75. package/dist/evals/steering.d.ts +9 -0
  76. package/dist/evals/steering.d.ts.map +1 -0
  77. package/dist/evals/steering.js +25 -0
  78. package/dist/evals/steering.js.map +1 -0
  79. package/dist/evals/store.d.ts +26 -0
  80. package/dist/evals/store.d.ts.map +1 -0
  81. package/dist/evals/store.js +96 -0
  82. package/dist/evals/store.js.map +1 -0
  83. package/dist/evals/synthetic.d.ts +17 -0
  84. package/dist/evals/synthetic.d.ts.map +1 -0
  85. package/dist/evals/synthetic.js +71 -0
  86. package/dist/evals/synthetic.js.map +1 -0
  87. package/dist/evals/types.d.ts +29 -0
  88. package/dist/evals/types.d.ts.map +1 -0
  89. package/dist/evals/types.js +25 -0
  90. package/dist/evals/types.js.map +1 -0
  91. package/dist/evals/workflow.d.ts +33 -0
  92. package/dist/evals/workflow.d.ts.map +1 -0
  93. package/dist/evals/workflow.js +66 -0
  94. package/dist/evals/workflow.js.map +1 -0
  95. package/dist/insights/analyze-cli.d.ts +18 -0
  96. package/dist/insights/analyze-cli.d.ts.map +1 -0
  97. package/dist/insights/analyze-cli.js +71 -0
  98. package/dist/insights/analyze-cli.js.map +1 -0
  99. package/dist/insights/analyze.d.ts +136 -0
  100. package/dist/insights/analyze.d.ts.map +1 -0
  101. package/dist/insights/analyze.js +358 -0
  102. package/dist/insights/analyze.js.map +1 -0
  103. package/dist/insights/review-service.d.ts +127 -0
  104. package/dist/insights/review-service.d.ts.map +1 -0
  105. package/dist/insights/review-service.js +392 -0
  106. package/dist/insights/review-service.js.map +1 -0
  107. package/dist/insights/routes.d.ts +15 -0
  108. package/dist/insights/routes.d.ts.map +1 -0
  109. package/dist/insights/routes.js +66 -0
  110. package/dist/insights/routes.js.map +1 -0
  111. package/dist/insights/run-facts.d.ts +64 -0
  112. package/dist/insights/run-facts.d.ts.map +1 -0
  113. package/dist/insights/run-facts.js +152 -0
  114. package/dist/insights/run-facts.js.map +1 -0
  115. package/dist/insights/stats.d.ts +38 -0
  116. package/dist/insights/stats.d.ts.map +1 -0
  117. package/dist/insights/stats.js +102 -0
  118. package/dist/insights/stats.js.map +1 -0
  119. package/dist/insights/store.d.ts +89 -0
  120. package/dist/insights/store.d.ts.map +1 -0
  121. package/dist/insights/store.js +87 -0
  122. package/dist/insights/store.js.map +1 -0
  123. package/dist/insights/workflow.d.ts +22 -0
  124. package/dist/insights/workflow.d.ts.map +1 -0
  125. package/dist/insights/workflow.js +39 -0
  126. package/dist/insights/workflow.js.map +1 -0
  127. package/dist/qe/ledger.d.ts +15 -0
  128. package/dist/qe/ledger.d.ts.map +1 -1
  129. package/dist/qe/ledger.js +24 -0
  130. package/dist/qe/ledger.js.map +1 -1
  131. package/dist/schedules/routes.d.ts +48 -0
  132. package/dist/schedules/routes.d.ts.map +1 -0
  133. package/dist/schedules/routes.js +72 -0
  134. package/dist/schedules/routes.js.map +1 -0
  135. package/dist/schedules/scheduler.d.ts +58 -0
  136. package/dist/schedules/scheduler.d.ts.map +1 -0
  137. package/dist/schedules/scheduler.js +194 -0
  138. package/dist/schedules/scheduler.js.map +1 -0
  139. package/dist/schedules/store.d.ts +67 -0
  140. package/dist/schedules/store.d.ts.map +1 -0
  141. package/dist/schedules/store.js +148 -0
  142. package/dist/schedules/store.js.map +1 -0
  143. package/dist/team/routes.d.ts +10 -0
  144. package/dist/team/routes.d.ts.map +1 -1
  145. package/dist/team/routes.js +3 -0
  146. package/dist/team/routes.js.map +1 -1
  147. package/endpoint-manifest.json +132 -1
  148. package/package.json +4 -3
  149. package/workflows/review-recent-work.json +54 -0
  150. package/workflows/run-evals.json +29 -0
@@ -1,3 +1,4 @@
1
+ import { REVIEW_WORKFLOW } from '../insights/workflow.js';
1
2
  import { busSeamsOffWarning, busUnavailableWarning } from '../core/bus-notice.js';
2
3
  import { vaultBinWarning, vaultDiagnostics } from '../core/vault-bin.js';
3
4
  import { PlanSchema, toLaunchPlan } from './plan-schema.js';
@@ -36,7 +37,8 @@ import { ROSTER_REFRESH_WAIT_MS, rosterWithStandingFactory } from './roster-stan
36
37
  import { SeatContainerStatus, gatewayDirOf, isContainerLoginCli, isContainerSeatFloor, isHostSeatFloor, loginDetail, runSeatLogin, seatLoginDepsFor, } from './seat-container-login.js';
37
38
  import { ChatTurnIndex } from './chat-turns.js';
38
39
  import { ChatScopeIndex, chatScopeDeps, chatScopeStatement, prepareChatScratch, removeChatScratch, namedKindRefusal, resolveChatScope, } from './chat-scope.js';
39
- import { ASK_TURN_BUDGET_SECS, AskPathIndex, answerStep, isAskTurn } from './ask-paths.js';
40
+ import { ASK_TURN_BUDGET_SECS, AskPathIndex, isAskTurn } from './ask-paths.js';
41
+ import { chatTurnStep, resolveChatWorkflow, servedChatWorkflows, writeCapableStep } from './chat-workflow.js';
40
42
  import { reassignRefusedSeats } from '../core/reassign-refusal.js';
41
43
  import { allowedRootsFor, isInsideRoot, openWithSystemDefault } from './open-path.js';
42
44
  import { InvalidDiffBaseError, MERGE_BASE_LITERAL, NotARegularFileError, UnresolvableDiffBaseError, branchDiff, isGitStopped, isPlainRef, readFileCapped, worktreeDiff, } from './run-files.js';
@@ -57,6 +59,10 @@ import { isSteeringAuthorRun, landSteeringProposal } from './steering-landing.js
57
59
  import { inventoryComplete, parseInventoryClaims } from '../core/inventory-claim.js';
58
60
  import { ApproveEditSchema, approveEdited, captureLaunchRoots, editInFlight, FileProposalSchema, registerCaptureRoutes } from './capture.js';
59
61
  import { registerTestingRoutes } from './testing.js';
62
+ import { registerDefaultEvalRoutes, runAndRecord } from '../evals/routes.js';
63
+ import { prepareRunEvalsLaunch, RUN_EVALS_WORKFLOW } from '../evals/workflow.js';
64
+ import { defaultReplaySet } from '../evals/replay.js';
65
+ import { gardenRootFor } from '../learn/scheduler.js';
60
66
  import { DEMO_PRESET, demoLaunchRoots, registerDemoDocumentRoute, registerDemoRoutes, registerWalkthroughRoutes, walkthroughAcceptance } from './recording.js';
61
67
  import { projectDocsRoot } from '../interactive/project-root.js';
62
68
  import { registerSkillsRoutes } from './skills.js';
@@ -94,7 +100,8 @@ import { AcpFoldCache, EngineVersionCache, eventsDirOf, installedPackageVersion,
94
100
  import { GovernanceDiagnostics } from './governance-health.js';
95
101
  import { ATTACHMENT_LIMITS, ATTACHMENT_MAX_MESSAGE_BYTES, AttachmentError, ChatAttachmentStore, attachmentWire, attachmentsBlock, boundaryOf, parseMultipart, stageForSeat } from './chat-attachments.js';
96
102
  import { CHAT_HISTORY_MAX_LIMIT, ChatHistoryStore, decodeCursor } from './chat-history.js';
97
- import { PROBE_TIMEOUT_MS, SYSTEM_BUDGET_MS, SYSTEM_STATS_DAYS, WeekFoldCache, assembleSnapshot, buildAttention, buildClis, buildGovernance, diskRow, estateReadonlyCall, infraRow, judgeGaps, livenessThresholds, loadRow, probeBroker, probeBus, probeEstate, probeGateway, probeInteractive, probeVault, readBridgeLockAsync, readWeek, resolveContainerRuntime, runLiveness, settleWithin, skillsRow, sortRuns, stateHomeRow, unfinished, } from './system-snapshot.js';
103
+ import { PROBE_TIMEOUT_MS, SYSTEM_BUDGET_MS, SYSTEM_STATS_DAYS, SYSTEM_STATS_TTL_MS, WeekFoldCache, assembleSnapshot, buildNeeds, buildClis, buildGovernance, diskRow, estateReadonlyCall, infraRow, judgeGaps, livenessThresholds, loadRow, probeBroker, probeBus, probeEstate, probeGateway, probeInteractive, probeVault, readBridgeLockAsync, readWeek, resolveContainerRuntime, runLiveness, settleWithin, skillsRow, sortRuns, stateHomeRow, unfinished, } from './system-snapshot.js';
104
+ import { RunCostCache } from './needs.js';
98
105
  import { governanceStoreFix, legacyHomeOutboxPath } from '../core/governance-store.js';
99
106
  import { UsageError, replayOutboxInto } from '../cli/governance.js';
100
107
  import { foldDeadletters } from './governance-health.js';
@@ -721,6 +728,9 @@ export const ChatOpenSchema = z.object({
721
728
  primary: z.string().min(1).optional(),
722
729
  /** (api-types 0.137.0, DES-SYSTEM-CHAT) The page the chat was started on, for the history list. */
723
730
  startedOn: z.object({ route: z.string().min(1).max(512), label: z.string().min(1).max(200) }).strict().optional(),
731
+ /** (api-types 0.141.0, crew#1157) The read-only workflow every turn runs (`analyze`); see
732
+ * `chat-workflow.ts`. Omitted ⇒ a plain chat. */
733
+ workflow: z.string().min(1).max(64).optional(),
724
734
  }).strict();
725
735
  /**
726
736
  * The `singleSeat` disclosure (crew#641, hole closed in crew#650) — built in ONE place so the 201
@@ -1235,6 +1245,22 @@ runtime = {}) {
1235
1245
  // crew#1068: the engine has no wicked-vault CLI to record held boundary findings with.
1236
1246
  ...[adapter.vaultBin != null ? vaultBinWarning(adapter.vaultBin) : null].filter((w) => w !== null),
1237
1247
  ];
1248
+ // crew#1157: the engine's built-in presets are compiled in, so the chat workflows it carries are
1249
+ // read once (on the first probe that can read them) and held until restart.
1250
+ let chatWorkflowsHeld = null;
1251
+ const chatWorkflowsNow = async () => {
1252
+ if (chatWorkflowsHeld !== null)
1253
+ return chatWorkflowsHeld;
1254
+ if (typeof adapter.listPresets !== 'function')
1255
+ return [];
1256
+ try {
1257
+ chatWorkflowsHeld = servedChatWorkflows(await adapter.listPresets());
1258
+ return chatWorkflowsHeld;
1259
+ }
1260
+ catch {
1261
+ return [];
1262
+ }
1263
+ };
1238
1264
  app.get(`${V}/health`, { config: { manifest: { statusCodes: [200] } } }, async () => {
1239
1265
  const ping = await enginePing();
1240
1266
  // F-E2E-030: what this deployment can keep. A composer reads `capabilities.deliverGate`
@@ -1268,9 +1294,14 @@ runtime = {}) {
1268
1294
  launchPrimary: true,
1269
1295
  // api-types 0.136.0 (DES-SYSTEM-VIEW): `GET /system`, the Nerve Center snapshot, is served.
1270
1296
  system: true,
1297
+ // api-types 0.140.0: attention[] is the one needs-you list; unresolved[] + toReview; POST /runs/:id/resolve.
1298
+ attentionRanked: true,
1271
1299
  // api-types 0.137.0 (DES-SYSTEM-CHAT): the chat history (list, reopen, close) and attachments.
1272
1300
  chatList: chatHistory !== null,
1273
1301
  chatAttachments: true,
1302
+ // api-types 0.141.0 (crew#1157): the read-only workflows `POST /chats {workflow}` runs on THIS
1303
+ // engine (`analyze` on a core-ts that carries it); `[]` = plain chats only.
1304
+ chatWorkflows: await chatWorkflowsNow(),
1274
1305
  };
1275
1306
  // wicked-core#411 / crew#497: the state-home blocker rides the health probe as a WARNING. The
1276
1307
  // daemon still SERVES (status stays ok — studio must load and show the blocker) but refuses to
@@ -2205,6 +2236,29 @@ runtime = {}) {
2205
2236
  req.log.warn(`linked-issue resolution failed; launching on the intent alone: ${message(err)}`);
2206
2237
  }
2207
2238
  }
2239
+ // DES-RUNS-REVIEW-001 §3: a hand launch of the run review (`/workflow-review-recent-work`) is prepared like a
2240
+ // scheduled one: its snapshot written, repo-less, one seat, its review directory as the only write root.
2241
+ // DES-DEFAULT-EVALS-001 §7: a launch of `run-evals` (`/workflow-run-evals`) is prepared by the daemon, which runs
2242
+ // the evals in-process and writes the report copy its Tool step reads; it delivers nothing.
2243
+ if (b.workflow === RUN_EVALS_WORKFLOW) {
2244
+ try {
2245
+ await runEvalsPrepare(input);
2246
+ }
2247
+ catch (err) {
2248
+ return reply.code(409).send({ error: `the default evals could not be prepared: ${message(err)}` });
2249
+ }
2250
+ deliver = 'none';
2251
+ }
2252
+ const isReview = b.workflow === REVIEW_WORKFLOW && runtime.reviewLaunch !== undefined;
2253
+ if (isReview) {
2254
+ try {
2255
+ await runtime.reviewLaunch(input);
2256
+ }
2257
+ catch (err) {
2258
+ return reply.code(409).send({ error: `the review could not be prepared: ${message(err)}` });
2259
+ }
2260
+ deliver = 'none';
2261
+ }
2208
2262
  try {
2209
2263
  const runId = await adapter.launchRun(input);
2210
2264
  // crew#641/#642 (item 5): when a run is promoted from a chat, record how many seats
@@ -2314,6 +2368,8 @@ runtime = {}) {
2314
2368
  }
2315
2369
  catch (err) {
2316
2370
  const msg = message(err);
2371
+ if (isReview)
2372
+ runtime.reviewLaunchRefused?.(input.sessionId, msg);
2317
2373
  // DES-TEAMING-002 T3: a plan on an engine without the plan approval gate is an "upgrade the
2318
2374
  // engine" 501 — never a free-text run launched in its place.
2319
2375
  if (err instanceof PlanLaunchUnsupportedError) {
@@ -2451,6 +2507,60 @@ runtime = {}) {
2451
2507
  return reply.code(500).send({ error: msg });
2452
2508
  }
2453
2509
  });
2510
+ // ── Resolve an unresolved run (api-types 0.140.0) ─────────────────────────────
2511
+ // Unresolved work (`GET /system.unresolved[]`) is resolved, never hidden: archived WITH a reason,
2512
+ // or closed as superseded naming the run that replaced it (retry is `POST /runs {retryOf}`). A run
2513
+ // parked at a gate is stopped first (no worker is running), then archived; a run that is
2514
+ // executing is refused, so a resolve never ends live work. The note keeps the reason.
2515
+ const ResolveSchema = z.discriminatedUnion('action', [
2516
+ z.object({ action: z.literal('archive'), reason: z.string().trim().min(1, 'a reason is required to archive').max(480) }).strict(),
2517
+ z.object({ action: z.literal('superseded'), by: z.string().min(1), reason: z.string().trim().max(400).optional() }).strict(),
2518
+ ]);
2519
+ app.post(`${V}/runs/:id/resolve`, { config: { manifest: { requestType: 'ResolveRunRequest', responseType: 'ResolveRunResponse', statusCodes: [200, 400, 404, 409, 500] } } }, async (req, reply) => {
2520
+ const { id } = req.params;
2521
+ const parsed = ResolveSchema.safeParse(req.body ?? {});
2522
+ if (!parsed.success)
2523
+ return reply.code(400).send(invalidBody(parsed.error, 'Invalid request body'));
2524
+ const b = parsed.data;
2525
+ const note = b.action === 'archive' ? `resolved: ${b.reason}` : `superseded by ${b.by}${b.reason !== undefined && b.reason !== '' ? `: ${b.reason}` : ''}`;
2526
+ // Inside the run's decision lock (the gate path's): the status read, the stop and the archive
2527
+ // are one step, so a gate answered at the same moment can never be stopped by a resolve. The
2528
+ // status is read again inside the lock, right before the stop.
2529
+ const outcome = await decideExclusively(id, async () => {
2530
+ const views = await adapter.sessionsDetail();
2531
+ const run = views.find((v) => v.session.id === id);
2532
+ if (run === undefined)
2533
+ return { code: 404, body: { error: 'Run not found' } };
2534
+ if (b.action === 'superseded') {
2535
+ if (b.by === id)
2536
+ return { code: 400, body: { error: '`by` must name another run' } };
2537
+ if (!views.some((v) => v.session.id === b.by))
2538
+ return { code: 400, body: { error: `\`by\` names an unknown run: ${b.by}` } };
2539
+ }
2540
+ const status = run.session.status;
2541
+ if (status !== 'awaiting_human' && status !== 'failed' && status !== 'completed' && status !== 'cancelled') {
2542
+ return { code: 409, body: { error: `run ${id} is ${status}: a running job cannot be resolved; stop it first` } };
2543
+ }
2544
+ let cancelled = false;
2545
+ try {
2546
+ if (status === 'awaiting_human') {
2547
+ await adapter.cancelRun(id);
2548
+ cancelled = true;
2549
+ }
2550
+ const found = await adapter.archiveRun(id, true, note);
2551
+ if (!found)
2552
+ return { code: 404, body: { error: 'Run not found' } };
2553
+ }
2554
+ catch (e) {
2555
+ const msg = e instanceof Error ? e.message : String(e);
2556
+ return { code: /only a terminal run/i.test(msg) ? 409 : 500, body: { error: msg } };
2557
+ }
2558
+ audit.record('run.resolved', actorOf(req), { runId: id, detail: { action: b.action, note, cancelled, ...(b.action === 'superseded' ? { by: b.by } : {}) } });
2559
+ const out = { runId: id, action: b.action, archived: true, cancelled, note };
2560
+ return { code: 200, body: out };
2561
+ });
2562
+ return reply.code(outcome.code).send(outcome.body);
2563
+ });
2454
2564
  // Bulk write-off for campaign backlogs — explicit ids only, never implicit age selection.
2455
2565
  const BulkArchiveSchema = z.object({
2456
2566
  ids: z.array(z.string().min(1)).min(1).max(200),
@@ -2926,6 +3036,23 @@ runtime = {}) {
2926
3036
  return reply.code(501).send({ error: message(err) });
2927
3037
  }
2928
3038
  }
3039
+ // crew#1157: a chat that runs a workflow runs a BUILT-IN read-only one, resolved before anything
3040
+ // is reserved or warmed (a 400 for a name chats do not run, a 409 for an engine without it).
3041
+ let workflow;
3042
+ if (b.workflow !== undefined) {
3043
+ let presets = [];
3044
+ try {
3045
+ presets = typeof adapter.listPresets === 'function' ? await adapter.listPresets() : [];
3046
+ }
3047
+ catch {
3048
+ presets = [];
3049
+ }
3050
+ const resolved = resolveChatWorkflow(b.workflow, presets);
3051
+ if (!resolved.ok)
3052
+ return reply.code(resolved.status).send({ error: resolved.error });
3053
+ workflow = resolved.workflow;
3054
+ }
3055
+ const statementOpts = { analysis: workflow !== undefined };
2929
3056
  // A chat id this daemon already holds a scope for is LIVE: re-preparing its scratch root and
2930
3057
  // re-opening it would overwrite the statement its seats are reading and (on a changed scope)
2931
3058
  // evict them mid-conversation (Copilot, #518). Close it first, or let the daemon mint the id.
@@ -2973,7 +3100,7 @@ runtime = {}) {
2973
3100
  if (!chatScopes.owns(chatId, token)) {
2974
3101
  return reply.code(409).send({ error: `chat ${chatId} was closed while it was being opened; open it again` });
2975
3102
  }
2976
- prepareChatScratch(chatId, scope, inForceAtOpen);
3103
+ prepareChatScratch(chatId, scope, inForceAtOpen, statementOpts);
2977
3104
  }
2978
3105
  catch (err) {
2979
3106
  // The helper removes only what it created itself (a pre-existing root is preserved) —
@@ -3044,6 +3171,12 @@ runtime = {}) {
3044
3171
  refused,
3045
3172
  });
3046
3173
  }
3174
+ // crew#1157: a workflow chat is ONE seat — the operator's pick (`primary`), else the first
3175
+ // admitted seat in roster order. The others are not refused; they simply do not sit this chat.
3176
+ if (workflow !== undefined) {
3177
+ const one = b.primary !== undefined && clis.includes(b.primary) ? b.primary : clis[0];
3178
+ clis.splice(0, clis.length, one);
3179
+ }
3047
3180
  try {
3048
3181
  // DES-ASK-TEAM-CHAT-001 §5.1 (ASK-C1): an open resolves the scope and ELIGIBILITY and warms
3049
3182
  // nothing — the chat is a path its first message launches, and the engine picks the PA
@@ -3111,7 +3244,7 @@ runtime = {}) {
3111
3244
  askPaths.open(chatId, clis, b.primary, {
3112
3245
  seats: standing.filter((s) => clis.includes(String(s.key))),
3113
3246
  scopeKind: !scoped ? 'unscoped' : admissionKind === 'path-bound' ? 'scoped_bound' : 'scoped',
3114
- });
3247
+ }, workflow);
3115
3248
  // Register roots for path rewriting (crew#618): absolute host paths in seat replies are
3116
3249
  // rewritten to repo-relative form before being stored in the transcript.
3117
3250
  if (chatTranscripts !== undefined && scope.repos.length > 0) {
@@ -3154,6 +3287,7 @@ runtime = {}) {
3154
3287
  seats,
3155
3288
  scope,
3156
3289
  refused,
3290
+ ...(workflow !== undefined ? { workflow: workflow.name } : {}),
3157
3291
  ...(projectAttachError !== undefined ? { projectAttachError } : {}),
3158
3292
  ...(singleSeat !== undefined ? { singleSeat } : {}),
3159
3293
  });
@@ -3378,7 +3512,7 @@ runtime = {}) {
3378
3512
  }
3379
3513
  if (storedScope !== undefined && scope !== undefined && engine !== undefined &&
3380
3514
  (JSON.stringify(scope) !== JSON.stringify(storedScope) || JSON.stringify(engine) !== JSON.stringify(chatScopes.engineOf(id)))) {
3381
- prepareChatScratch(id, scope, rules);
3515
+ prepareChatScratch(id, scope, rules, { analysis: path.workflow !== undefined });
3382
3516
  if (chatScopes.refresh(id, storedScope, scope, engine)) {
3383
3517
  chatTranscripts?.registerRoots(id, scope.repos.map((r) => ({ absRoot: resolve(r.rootPath), name: r.name })));
3384
3518
  }
@@ -3404,8 +3538,11 @@ runtime = {}) {
3404
3538
  audience = voice.length > 0 ? voice : path.eligible;
3405
3539
  turn = chatTurns.begin(id, audience, text, ASK_TURN_BUDGET_SECS);
3406
3540
  turnId = turn?.turnId ?? turnId;
3407
- // A repo-bound path can deliver; project and multi-repo scopes remain read-only asks.
3541
+ // A repo-bound path can deliver; project and multi-repo scopes remain read-only asks. A workflow
3542
+ // chat (crew#1157) never delivers: it is bound to its repository read-only, with no deliver.
3408
3543
  const repoRef = scope?.kind === 'repos' && scope.repos.length === 1 ? scope.repos[0].id : undefined;
3544
+ const delivers = repoRef !== undefined && path.workflow === undefined;
3545
+ const statementOpts = { analysis: path.workflow !== undefined };
3409
3546
  const projectId = scope?.projectId;
3410
3547
  if (path.runId === undefined) {
3411
3548
  const stepId = 'answer-1';
@@ -3414,14 +3551,18 @@ runtime = {}) {
3414
3551
  // crew#823: the run is titled by the operator's question (every reader titles a run from
3415
3552
  // `problem`); the scope statement grounds the seats from the first answer step's
3416
3553
  // instructions instead of standing in as the run's title.
3417
- const instructions = scope !== undefined ? `${chatScopeStatement(id, scope, rules)}\n\n${withPreface(pending.preface)}` : withPreface(pending.preface);
3554
+ const instructions = scope !== undefined ? `${chatScopeStatement(id, scope, rules, statementOpts)}\n\n${withPreface(pending.preface)}` : withPreface(pending.preface);
3418
3555
  const input = {
3419
3556
  problem: text,
3420
3557
  sessionId: randomUUID(),
3421
3558
  clisJson: JSON.stringify(roster),
3422
- plan: { steps: [answerStep(stepId, instructions)], monitors: { asked: 1 } },
3559
+ // A workflow chat is one seat and asks for no reviewer (crew#1157).
3560
+ plan: {
3561
+ steps: [chatTurnStep(stepId, instructions, path.workflow)],
3562
+ ...(path.workflow === undefined ? { monitors: { asked: 1 } } : {}),
3563
+ },
3423
3564
  ...(path.primary !== undefined ? { primary: path.primary } : {}),
3424
- ...(repoRef !== undefined ? { repoRef, deliver: 'pr' } : {}),
3565
+ ...(repoRef !== undefined ? (delivers ? { repoRef, deliver: 'pr' } : { repoRef }) : {}),
3425
3566
  ...(projectId !== undefined ? { projectId } : {}),
3426
3567
  ...(scope?.kind === 'project' && codeGraphDb !== null ? { projectGraph: { dbPath: codeGraphDb } } : {}),
3427
3568
  // crew#1034: the scope's repositories are the run's read roots, graph or no graph — the
@@ -3456,7 +3597,8 @@ runtime = {}) {
3456
3597
  askPath: true,
3457
3598
  ...(repoRef !== undefined ? { repoRef } : {}),
3458
3599
  ...(projectId !== undefined ? { projectId } : {}),
3459
- deliver: repoRef !== undefined ? 'pr' : 'none',
3600
+ ...(path.workflow !== undefined ? { chatWorkflow: path.workflow.name } : {}),
3601
+ deliver: delivers ? 'pr' : 'none',
3460
3602
  });
3461
3603
  });
3462
3604
  return reply.code(202).send({
@@ -3467,7 +3609,7 @@ runtime = {}) {
3467
3609
  // crew#1033: EVERY answer step carries the scope statement, as the first one does. A seat on a
3468
3610
  // single-shot carrier (codex on the wrapped carrier) remembers nothing of the first turn, so a
3469
3611
  // follow-up told only the question answered "the supplied repository contains only tmp".
3470
- const instructions = scope !== undefined ? `${chatScopeStatement(id, scope, rules)}\n\n${withPreface(pending.preface)}` : withPreface(pending.preface);
3612
+ const instructions = scope !== undefined ? `${chatScopeStatement(id, scope, rules, statementOpts)}\n\n${withPreface(pending.preface)}` : withPreface(pending.preface);
3471
3613
  // The continuation answers the path run's gate: under the per-run decision lock it re-reads the
3472
3614
  // run and is judged by the record before it proposes or confirms anything (crew#1083 round 7).
3473
3615
  // The status read above is a pre-check that changes nothing; this read is the one that counts.
@@ -3485,7 +3627,7 @@ runtime = {}) {
3485
3627
  const decided = await gateForDecision(view, undefined);
3486
3628
  if ('refusal' in decided)
3487
3629
  return decided.refusal;
3488
- await adapter.proposePlan(pathRunId, { steps: [answerStep(stepId, instructions)] }, turnId);
3630
+ await adapter.proposePlan(pathRunId, { steps: [chatTurnStep(stepId, instructions, path.workflow)] }, turnId);
3489
3631
  await adapter.confirmGate(pathRunId, true);
3490
3632
  return null;
3491
3633
  });
@@ -3557,6 +3699,15 @@ runtime = {}) {
3557
3699
  runId: launched.runId,
3558
3700
  });
3559
3701
  }
3702
+ // crew#1157 (xreview on #1160): an analysis chat seats ONE helper; a re-seat may re-warm that
3703
+ // seat, never add another.
3704
+ if (launched?.workflow !== undefined && !(clis.length === 1 && launched.eligible.includes(clis[0]))) {
3705
+ return reply.code(409).send({
3706
+ code: 'analysis_one_seat',
3707
+ error: `chat '${id}' is an analysis (${launched.workflow}) and seats one helper (${launched.eligible.join(', ')}); open a new chat to ask another`,
3708
+ chatId: id,
3709
+ });
3710
+ }
3560
3711
  if (launched?.busy === true) {
3561
3712
  // The first message is launching with the roster as it stands (codex on #808 r3, 4).
3562
3713
  return reply.code(409).send({
@@ -4191,7 +4342,19 @@ runtime = {}) {
4191
4342
  * A decision without one keeps the pre-table resolution (the cache, then the log, then the
4192
4343
  * cursor unit) — an engine, or a run, older than the table.
4193
4344
  */
4194
- async function gateForDecision(run, namedOrd) {
4345
+ async function gateForDecision(run, namedOrd, opts = {}) {
4346
+ const decided = await gateOfRecord(run, namedOrd);
4347
+ // crew#1157: every APPROVING path (the gate route, standing orders, a gated resume or reassign, a
4348
+ // plan edit at the gate, the next message of a chat) is judged here, so no path approves a held
4349
+ // plan change on an analysis run. Declining stays open.
4350
+ if (!('refusal' in decided) && opts.declining !== true) {
4351
+ const analysis = analysisPlanRefusal(run.session.id, { gateKind: decided.gateKind });
4352
+ if (analysis !== null)
4353
+ return { refusal: analysis };
4354
+ }
4355
+ return decided;
4356
+ }
4357
+ async function gateOfRecord(run, namedOrd) {
4195
4358
  const id = run.session.id;
4196
4359
  const unitAt = (ord) => (run.units ?? []).find((u) => u.ord === ord);
4197
4360
  const unknown = (why) => ({ refusal: { code: 409, body: { error: `Gate unknown: ${why} — refresh and decide again.`, code: 'gate_unknown' } } });
@@ -4275,6 +4438,30 @@ runtime = {}) {
4275
4438
  });
4276
4439
  return result;
4277
4440
  }
4441
+ /**
4442
+ * crew#1157 (xreview on #1160): an ANALYSIS chat's run never takes on work. Its seat is told not to
4443
+ * propose any, but a `PLAN+` line in an answer would still hold a creator addition at a
4444
+ * `plan_approval` gate; approving it (or approving with an edited plan) would let the run write.
4445
+ * Refused here, for every decision path, by the durable launch record: declining stays allowed.
4446
+ */
4447
+ function analysisPlanRefusal(runId, held) {
4448
+ const workflow = runTimingIndex.chatWorkflowOf(runId);
4449
+ if (workflow === undefined)
4450
+ return null;
4451
+ // A held plan change (the gate the engine opens on a `PLAN+` line), or a plan edit with any step
4452
+ // that could write (the same read-only predicate the chat open applies to the workflow).
4453
+ const writes = held.plan?.steps.some((s) => writeCapableStep(s) !== null) === true;
4454
+ if (held.gateKind !== 'plan_approval' && !writes)
4455
+ return null;
4456
+ return {
4457
+ code: 409,
4458
+ body: {
4459
+ code: 'analysis_read_only',
4460
+ error: `run ${runId} is an analysis (${workflow}): it answers questions and changes nothing, so it cannot take on ` +
4461
+ 'the work its answer proposed. Decline it here, and start a job for the change.',
4462
+ },
4463
+ };
4464
+ }
4278
4465
  /**
4279
4466
  * THE gate decision path. `POST /runs/:id/gate` answers with it, and so does a standing order
4280
4467
  * (behaviour 10) — the same awaiting check, the same ord check and 409 `gate_changed` /
@@ -4298,9 +4485,12 @@ runtime = {}) {
4298
4485
  // Decision integrity (api-types 0.44.0; crew#1083 rounds 3–5): the gate this decision answers
4299
4486
  // is the RECORD's — read first thing inside the lock, so the gated unit and its delivery
4300
4487
  // checks below are the ones the record confirms ({@link gateForDecision}).
4301
- const decided = await gateForDecision(run, parsed.data.ord);
4488
+ const decided = await gateForDecision(run, parsed.data.ord, { declining: !parsed.data.approve });
4302
4489
  if ('refusal' in decided)
4303
4490
  return decided.refusal;
4491
+ const analysis = parsed.data.approve ? analysisPlanRefusal(id, { plan: parsed.data.plan }) : null;
4492
+ if (analysis !== null)
4493
+ return analysis;
4304
4494
  // The gated unit and its receipt as it stands when the person decides (wicked-core#850: every
4305
4495
  // gate answer carries it). An APPROVE that would run the deliver unit passes the delivery
4306
4496
  // checks first.
@@ -7061,7 +7251,12 @@ runtime = {}) {
7061
7251
  registerPresetRoutes(app, adapter, security);
7062
7252
  // ── Team (DES-TEAMING-002 T8) — reads and commands over the engine; the live stream is the
7063
7253
  // teamEvent relay (team/ws-relay.ts). Crew publishes no team fact (§4.0).
7064
- registerTeamRoutes(app, adapter, security, { exclusive: decideExclusively, judge: gateForDecision });
7254
+ registerTeamRoutes(app, adapter, security, {
7255
+ exclusive: decideExclusively,
7256
+ judge: gateForDecision,
7257
+ // crew#1157: a plan edit on an analysis run may not add a step that could write.
7258
+ readOnlyRefusal: (runId, plan) => analysisPlanRefusal(runId, { plan }),
7259
+ });
7065
7260
  // ── Campaigns (crew#342 + TH-9) — the engine's durable Run-DAG scheduler over REST ──────────
7066
7261
  // Progress streams as campaign* CoreEvents on the existing allowlist-free /ws relay; these
7067
7262
  // routes are launch/resume/cancel + the reads a scoreboard builds from. The GET routes join
@@ -7124,6 +7319,19 @@ runtime = {}) {
7124
7319
  // directly-driven route set records nothing unless it injects one (no `~/.wicked-crew` writes).
7125
7320
  ...(runtime.evalStore !== undefined ? { evalStore: runtime.evalStore } : {}),
7126
7321
  });
7322
+ // ── Default evals (DES-DEFAULT-EVALS-001) — the system's own judgments, scored against what happened ─────
7323
+ // Read-only over the run store (sessions, events, the operator's decisions, QE ledgers, the latest steering
7324
+ // eval); the only write is the report, under the eval store's root.
7325
+ const defaultEvalDeps = {
7326
+ ...(runtime.defaultEvalStore !== undefined ? { store: runtime.defaultEvalStore } : {}),
7327
+ // Served runs, as GET /runs serves them: the derived title and the project membership join (codex r1).
7328
+ runs: async () => (await adapter.sessionsDetail()).map(decorateRun),
7329
+ events: async (id) => (typeof adapter.runEvents === 'function' ? (await adapter.runEvents(id)) : null),
7330
+ decisions: () => (runtime.decisionLedger?.list({}) ?? []),
7331
+ ...(runtime.evalStore !== undefined ? { steeringEvals: runtime.evalStore } : {}),
7332
+ replay: defaultReplaySet({ gardenRoot: gardenRootFor(process.env) }),
7333
+ };
7334
+ registerDefaultEvalRoutes(app, defaultEvalDeps);
7127
7335
  // ── Skills (skills keystone) — the file manager over the daemon-owned garden plugin root ─────
7128
7336
  // Manifest + typed reads, guarded CAS writes, refresh/publish/analyze. `createServer` injects the
7129
7337
  // runtime it booted (store seeded from the installed plugin, snapshot published); a
@@ -7182,6 +7390,8 @@ runtime = {}) {
7182
7390
  // store is kept for SYSTEM_STATS_TTL_MS, with its `asOf`. The whole answer is held to
7183
7391
  // SYSTEM_BUDGET_MS: a part that does not settle in time reads `unknown` (or `null`) with why.
7184
7392
  const weekFoldCache = new WeekFoldCache();
7393
+ const runCosts = new RunCostCache();
7394
+ let toReviewCache = null;
7185
7395
  /** One stat of a run's durable event log: read before the 7-day fold, so the fold can never eat
7186
7396
  * the time a liveness verdict needs. */
7187
7397
  const EVIDENCE_TIMEOUT_MS = 1_000;
@@ -7217,6 +7427,17 @@ runtime = {}) {
7217
7427
  const rules = await adapter.listConformanceRules();
7218
7428
  return rules.filter((r) => r.retired !== true).length;
7219
7429
  }, PROBE_TIMEOUT_MS);
7430
+ // Pending proposals: "N to review", never in the needs-you number (no run waits on them).
7431
+ // Kept for SYSTEM_STATS_TTL_MS: a skin polls /system, and each read would spawn the estate client.
7432
+ const toReviewP = settleWithin(async () => {
7433
+ if (toReviewCache !== null && Date.now() - toReviewCache.at < SYSTEM_STATS_TTL_MS)
7434
+ return toReviewCache.value;
7435
+ const got = (await estateTool('proposal.list', { state: 'pending' }));
7436
+ const value = Array.isArray(got?.proposals) ? got.proposals.length : null;
7437
+ if (value !== null)
7438
+ toReviewCache = { at: Date.now(), value }; // a failed read is never kept
7439
+ return value;
7440
+ }, PROBE_TIMEOUT_MS);
7220
7441
  const gatesByYouP = settleWithin(async () => {
7221
7442
  const since = Date.now() - SYSTEM_STATS_DAYS * 86_400_000;
7222
7443
  const entries = await audit.readAll({ action: 'gate.decided', since });
@@ -7275,7 +7496,14 @@ runtime = {}) {
7275
7496
  if (openGates.kind === 'ok' && openGates.value !== null) {
7276
7497
  const at = (r) => new Date(r.created_at).getTime() || 0;
7277
7498
  for (const r of [...openGates.value].sort((a, b) => at(a) - at(b))) {
7278
- openByRun.set(r.session_id, { interactionId: r.id, ord: r.ord, kind: r.gate_kind ?? (r.kind === 'elicitation' ? 'question' : null), prompt: r.prompt.slice(0, 280) });
7499
+ const arrived = at(r);
7500
+ openByRun.set(r.session_id, {
7501
+ interactionId: r.id,
7502
+ ord: r.ord,
7503
+ kind: r.gate_kind ?? (r.kind === 'elicitation' ? 'question' : null),
7504
+ prompt: r.prompt.slice(0, 280),
7505
+ receivedAt: arrived > 0 ? new Date(arrived).toISOString() : (gateCache.get(r.session_id)?.receivedAt ?? null),
7506
+ });
7279
7507
  }
7280
7508
  }
7281
7509
  const now = Date.now();
@@ -7296,7 +7524,7 @@ runtime = {}) {
7296
7524
  const cached = gateCache.get(view.session.id);
7297
7525
  const gate = openByRun.get(view.session.id) ??
7298
7526
  (view.session.status === 'awaiting_human' && cached !== undefined
7299
- ? { interactionId: null, ord: cached.ord, kind: cached.gateKind ?? null, prompt: cached.prompt.slice(0, 280) }
7527
+ ? { interactionId: null, ord: cached.ord, kind: cached.gateKind ?? null, prompt: cached.prompt.slice(0, 280), receivedAt: cached.receivedAt }
7300
7528
  : null);
7301
7529
  const run = runLiveness({ view, now, evidence: await evidenceOf(view.session.id), gate: view.session.status === 'awaiting_human' ? gate : null, quietAfterMs, stallAfterMs });
7302
7530
  return { view, run };
@@ -7393,16 +7621,50 @@ runtime = {}) {
7393
7621
  steeringRules: rules.kind === 'ok' ? rules.value : null,
7394
7622
  ...(rules.kind === 'ok' && rules.value === null ? { steeringNote: 'this engine cannot list steering rules' } : rules.kind === 'timeout' ? { steeringNote: `the rule store did not answer within ${PROBE_TIMEOUT_MS / 1000} s` } : rules.kind === 'error' ? { steeringNote: `the rule store could not be read: ${rules.error}` } : {}),
7395
7623
  });
7396
- const working = runs.filter((r) => r.liveness !== 'waiting_on_you' && r.liveness !== 'held').length;
7397
- const attention = buildAttention({
7624
+ // The one "needs you" rule (needs.ts): root causes ranked by impact, plus the unresolved work
7625
+ // and what it cost. Per-run cost is read from the run's own log, cached, never waited on past
7626
+ // the budget (an unread run's cost reads unknown and the next read has it).
7627
+ const first = buildNeeds({
7628
+ now,
7629
+ views: allViews,
7630
+ runs,
7631
+ infra,
7632
+ clis,
7633
+ gaps,
7634
+ infraHoldsLive: (key) => (key === 'gateway' || key === 'broker' ? containerFloor : key === 'skills' || key === 'state_home' || key === 'disk' || key === 'daemon'),
7635
+ costOf: (id) => runCosts.get(id),
7636
+ });
7637
+ if (typeof adapter.runEvents === 'function' && first.unresolved.length > 0) {
7638
+ const byId = new Map(allViews.map((v) => [v.session.id, v]));
7639
+ await settleWithin(() => runCosts.fill(first.unresolved.map((u) => {
7640
+ const s = byId.get(u.runId)?.session;
7641
+ return { id: u.runId, final: ['failed', 'completed', 'cancelled'].includes(s?.status ?? ''), stamp: `${s?.status ?? ''}:${s?.ended_at ?? ''}` };
7642
+ }), (id) => adapter.runEvents(id)), Math.min(left(), 2_000));
7643
+ }
7644
+ const needs = buildNeeds({
7645
+ now,
7646
+ views: allViews,
7647
+ runs,
7398
7648
  infra,
7399
7649
  clis,
7650
+ gaps,
7651
+ infraHoldsLive: (key) => (key === 'gateway' || key === 'broker' ? containerFloor : key === 'skills' || key === 'state_home' || key === 'disk' || key === 'daemon'),
7652
+ costOf: (id) => runCosts.get(id),
7653
+ });
7654
+ const toReview = await toReviewP;
7655
+ // A run list that could not be read never reads green: the daemon row (degraded, "did not list
7656
+ // its runs") is in attention as a slowdown, and the level stays at least yellow.
7657
+ return assembleSnapshot({
7658
+ startedAt,
7659
+ clis,
7400
7660
  runs,
7661
+ infra,
7401
7662
  governance,
7402
- gaps,
7403
- blockedByInfra: (key) => key === 'gateway' || key === 'broker' ? (containerFloor ? working : 0) : key === 'skills' || key === 'state_home' || key === 'disk' || key === 'load' ? working : 0,
7663
+ attention: needs.attention,
7664
+ unresolved: needs.unresolved,
7665
+ unresolvedSpend: needs.unresolvedSpend,
7666
+ toReview: toReview.kind === 'ok' ? toReview.value : null,
7404
7667
  });
7405
- return assembleSnapshot({ startedAt, clis, runs, infra, governance, attention });
7406
7668
  });
7407
7669
  registerInteractiveProxy(app, adapter, {
7408
7670
  settings: projectSettings,
@@ -7501,6 +7763,9 @@ runtime = {}) {
7501
7763
  log: (m) => app.log.warn(m),
7502
7764
  });
7503
7765
  registerSearchRoutes(app, search);
7504
- return { decideGate, decisions, chatRecorder, considerations, search };
7766
+ const runEvalsPrepare = async (input) => {
7767
+ await prepareRunEvalsLaunch(input, () => runAndRecord(defaultEvalDeps, { project: null, baseline: false, id: randomUUID(), now: new Date() }), runtime.evalRunsDir);
7768
+ };
7769
+ return { decideGate, decisions, chatRecorder, considerations, search, decorateRun, runEvalsPrepare };
7505
7770
  }
7506
7771
  //# sourceMappingURL=routes.js.map