wicked-crew 0.7.34 → 0.7.36

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 (185) hide show
  1. package/dist/api/chat-scope.d.ts +29 -1
  2. package/dist/api/chat-scope.d.ts.map +1 -1
  3. package/dist/api/chat-scope.js +67 -12
  4. package/dist/api/chat-scope.js.map +1 -1
  5. package/dist/api/chat-transcripts.d.ts +60 -0
  6. package/dist/api/chat-transcripts.d.ts.map +1 -0
  7. package/dist/api/chat-transcripts.js +139 -0
  8. package/dist/api/chat-transcripts.js.map +1 -0
  9. package/dist/api/chat-turns.d.ts +43 -4
  10. package/dist/api/chat-turns.d.ts.map +1 -1
  11. package/dist/api/chat-turns.js +73 -6
  12. package/dist/api/chat-turns.js.map +1 -1
  13. package/dist/api/delivery-cache.d.ts +23 -6
  14. package/dist/api/delivery-cache.d.ts.map +1 -1
  15. package/dist/api/delivery-cache.js +27 -17
  16. package/dist/api/delivery-cache.js.map +1 -1
  17. package/dist/api/delivery-index.d.ts +45 -3
  18. package/dist/api/delivery-index.d.ts.map +1 -1
  19. package/dist/api/delivery-index.js +79 -4
  20. package/dist/api/delivery-index.js.map +1 -1
  21. package/dist/api/requirements.d.ts +3 -5
  22. package/dist/api/requirements.d.ts.map +1 -1
  23. package/dist/api/requirements.js +30 -146
  24. package/dist/api/requirements.js.map +1 -1
  25. package/dist/api/retry-index.d.ts +14 -0
  26. package/dist/api/retry-index.d.ts.map +1 -1
  27. package/dist/api/retry-index.js +29 -0
  28. package/dist/api/retry-index.js.map +1 -1
  29. package/dist/api/roster-standing.d.ts +4 -4
  30. package/dist/api/roster-standing.d.ts.map +1 -1
  31. package/dist/api/roster-standing.js +24 -12
  32. package/dist/api/roster-standing.js.map +1 -1
  33. package/dist/api/routes.d.ts +142 -10
  34. package/dist/api/routes.d.ts.map +1 -1
  35. package/dist/api/routes.js +292 -44
  36. package/dist/api/routes.js.map +1 -1
  37. package/dist/api/run-timing-index.d.ts +22 -0
  38. package/dist/api/run-timing-index.d.ts.map +1 -1
  39. package/dist/api/run-timing-index.js +34 -0
  40. package/dist/api/run-timing-index.js.map +1 -1
  41. package/dist/api/seat-health.d.ts +2 -46
  42. package/dist/api/seat-health.d.ts.map +1 -1
  43. package/dist/api/seat-health.js +28 -111
  44. package/dist/api/seat-health.js.map +1 -1
  45. package/dist/api/seat-standing.d.ts +9 -8
  46. package/dist/api/seat-standing.d.ts.map +1 -1
  47. package/dist/api/seat-standing.js +22 -32
  48. package/dist/api/seat-standing.js.map +1 -1
  49. package/dist/api/server.d.ts.map +1 -1
  50. package/dist/api/server.js +114 -12
  51. package/dist/api/server.js.map +1 -1
  52. package/dist/api/skills.d.ts +2 -2
  53. package/dist/api/skills.d.ts.map +1 -1
  54. package/dist/api/skills.js +5 -2
  55. package/dist/api/skills.js.map +1 -1
  56. package/dist/api/stall-frame-index.d.ts +68 -0
  57. package/dist/api/stall-frame-index.d.ts.map +1 -0
  58. package/dist/api/stall-frame-index.js +106 -0
  59. package/dist/api/stall-frame-index.js.map +1 -0
  60. package/dist/api/stall-watchdog.d.ts +22 -2
  61. package/dist/api/stall-watchdog.d.ts.map +1 -1
  62. package/dist/api/stall-watchdog.js +41 -11
  63. package/dist/api/stall-watchdog.js.map +1 -1
  64. package/dist/campaigns/plan.d.ts +12 -2
  65. package/dist/campaigns/plan.d.ts.map +1 -1
  66. package/dist/campaigns/plan.js +3 -0
  67. package/dist/campaigns/plan.js.map +1 -1
  68. package/dist/campaigns/rollup.d.ts +5 -0
  69. package/dist/campaigns/rollup.d.ts.map +1 -1
  70. package/dist/campaigns/rollup.js +3 -2
  71. package/dist/campaigns/rollup.js.map +1 -1
  72. package/dist/campaigns/routes.d.ts +5 -2
  73. package/dist/campaigns/routes.d.ts.map +1 -1
  74. package/dist/campaigns/routes.js +13 -3
  75. package/dist/campaigns/routes.js.map +1 -1
  76. package/dist/cli/index.js +137 -23
  77. package/dist/cli/index.js.map +1 -1
  78. package/dist/cli/port.d.ts +19 -0
  79. package/dist/cli/port.d.ts.map +1 -0
  80. package/dist/cli/port.js +39 -0
  81. package/dist/cli/port.js.map +1 -0
  82. package/dist/core/adapter.d.ts +68 -2
  83. package/dist/core/adapter.d.ts.map +1 -1
  84. package/dist/core/adapter.js +176 -23
  85. package/dist/core/adapter.js.map +1 -1
  86. package/dist/core/bridge-reaper.d.ts +57 -1
  87. package/dist/core/bridge-reaper.d.ts.map +1 -1
  88. package/dist/core/bridge-reaper.js +143 -21
  89. package/dist/core/bridge-reaper.js.map +1 -1
  90. package/dist/core/cursor.d.ts +7 -0
  91. package/dist/core/cursor.d.ts.map +1 -1
  92. package/dist/core/cursor.js +7 -1
  93. package/dist/core/cursor.js.map +1 -1
  94. package/dist/core/deliver-text.d.ts +35 -2
  95. package/dist/core/deliver-text.d.ts.map +1 -1
  96. package/dist/core/deliver-text.js +127 -17
  97. package/dist/core/deliver-text.js.map +1 -1
  98. package/dist/core/deliver-triage.d.ts.map +1 -1
  99. package/dist/core/deliver-triage.js +5 -2
  100. package/dist/core/deliver-triage.js.map +1 -1
  101. package/dist/core/deliver.d.ts +76 -2
  102. package/dist/core/deliver.d.ts.map +1 -1
  103. package/dist/core/deliver.js +209 -16
  104. package/dist/core/deliver.js.map +1 -1
  105. package/dist/core/engine-roster.d.ts +4 -3
  106. package/dist/core/engine-roster.d.ts.map +1 -1
  107. package/dist/core/engine-roster.js +10 -14
  108. package/dist/core/engine-roster.js.map +1 -1
  109. package/dist/core/types.d.ts +30 -0
  110. package/dist/core/types.d.ts.map +1 -1
  111. package/dist/core/types.js +6 -4
  112. package/dist/core/types.js.map +1 -1
  113. package/dist/core/versions.d.ts +17 -0
  114. package/dist/core/versions.d.ts.map +1 -0
  115. package/dist/core/versions.js +35 -0
  116. package/dist/core/versions.js.map +1 -0
  117. package/dist/interactive/bridge-pool.d.ts +66 -14
  118. package/dist/interactive/bridge-pool.d.ts.map +1 -1
  119. package/dist/interactive/bridge-pool.js +130 -17
  120. package/dist/interactive/bridge-pool.js.map +1 -1
  121. package/dist/interactive/bridge-root.d.ts +47 -16
  122. package/dist/interactive/bridge-root.d.ts.map +1 -1
  123. package/dist/interactive/bridge-root.js +102 -31
  124. package/dist/interactive/bridge-root.js.map +1 -1
  125. package/dist/interactive/doc-delete-routes.d.ts +3 -1
  126. package/dist/interactive/doc-delete-routes.d.ts.map +1 -1
  127. package/dist/interactive/doc-delete-routes.js.map +1 -1
  128. package/dist/interactive/doc-list-routes.d.ts +3 -1
  129. package/dist/interactive/doc-list-routes.d.ts.map +1 -1
  130. package/dist/interactive/doc-list-routes.js.map +1 -1
  131. package/dist/interactive/docs-index.d.ts +3 -1
  132. package/dist/interactive/docs-index.d.ts.map +1 -1
  133. package/dist/interactive/docs-index.js +1 -1
  134. package/dist/interactive/docs-index.js.map +1 -1
  135. package/dist/interactive/project-root.d.ts +8 -2
  136. package/dist/interactive/project-root.d.ts.map +1 -1
  137. package/dist/interactive/project-root.js +1 -1
  138. package/dist/interactive/project-root.js.map +1 -1
  139. package/dist/interactive/proxy-routes.d.ts +3 -1
  140. package/dist/interactive/proxy-routes.d.ts.map +1 -1
  141. package/dist/interactive/proxy-routes.js.map +1 -1
  142. package/dist/projects/graph.d.ts +4 -0
  143. package/dist/projects/graph.d.ts.map +1 -1
  144. package/dist/projects/graph.js +4 -2
  145. package/dist/projects/graph.js.map +1 -1
  146. package/dist/projects/routes.d.ts +2 -2
  147. package/dist/projects/state-home-preflight.d.ts +30 -7
  148. package/dist/projects/state-home-preflight.d.ts.map +1 -1
  149. package/dist/projects/state-home-preflight.js +59 -16
  150. package/dist/projects/state-home-preflight.js.map +1 -1
  151. package/dist/projects/state-home-registry.d.ts.map +1 -1
  152. package/dist/projects/state-home-registry.js +2 -0
  153. package/dist/projects/state-home-registry.js.map +1 -1
  154. package/dist/qe/acceptance.d.ts +9 -0
  155. package/dist/qe/acceptance.d.ts.map +1 -1
  156. package/dist/qe/acceptance.js +16 -1
  157. package/dist/qe/acceptance.js.map +1 -1
  158. package/dist/qe/author-workflow.d.ts.map +1 -1
  159. package/dist/qe/author-workflow.js +2 -1
  160. package/dist/qe/author-workflow.js.map +1 -1
  161. package/dist/skills/base-skill.d.ts +32 -10
  162. package/dist/skills/base-skill.d.ts.map +1 -1
  163. package/dist/skills/base-skill.js +36 -27
  164. package/dist/skills/base-skill.js.map +1 -1
  165. package/dist/skills/guards.d.ts.map +1 -1
  166. package/dist/skills/guards.js +3 -0
  167. package/dist/skills/guards.js.map +1 -1
  168. package/dist/skills/runtime.d.ts +9 -5
  169. package/dist/skills/runtime.d.ts.map +1 -1
  170. package/dist/skills/runtime.js +11 -7
  171. package/dist/skills/runtime.js.map +1 -1
  172. package/dist/skills/store.d.ts +30 -0
  173. package/dist/skills/store.d.ts.map +1 -1
  174. package/dist/skills/store.js +159 -3
  175. package/dist/skills/store.js.map +1 -1
  176. package/dist/skills/tree.d.ts +12 -0
  177. package/dist/skills/tree.d.ts.map +1 -1
  178. package/dist/skills/tree.js +26 -0
  179. package/dist/skills/tree.js.map +1 -1
  180. package/dist/studio/assets/index-Dk98qXKv.js +546 -0
  181. package/dist/studio/index.html +1 -1
  182. package/dist/studio/testid-inventory.json +89 -5
  183. package/endpoint-manifest.json +13 -1
  184. package/package.json +4 -4
  185. package/dist/studio/assets/index-DhojBZ1T.js +0 -544
@@ -5,7 +5,7 @@ import { readFileSync, existsSync } from 'node:fs';
5
5
  import { promises as fsp } from 'node:fs';
6
6
  import { isAbsolute, join, relative, resolve } from 'node:path';
7
7
  import { fileURLToPath } from 'node:url';
8
- import { CampaignsUnsupportedError, ChatUnsupportedError, CoreAdapter, ElicitationUnsupportedError, SteeringUnsupportedError, humanGatePhaseIds } from '../core/adapter.js';
8
+ import { CampaignsUnsupportedError, ChatUnsupportedError, CoreAdapter, ElicitationUnsupportedError, SteeringUnsupportedError, humanGatePhaseIds, settingsFilePath } from '../core/adapter.js';
9
9
  import { codeGraphDb, codeGraphErrorStatus, requirementsGraph } from '../core/repoPaths.js';
10
10
  import { resolveCursorUnit } from '../core/cursor.js';
11
11
  import { detectRefusal } from './gate-cache.js';
@@ -26,7 +26,7 @@ import { InvalidDiffBaseError, MERGE_BASE_LITERAL, NotARegularFileError, Unresol
26
26
  import { DocRunIndex } from '../interactive/doc-run-index.js';
27
27
  import { listInteractiveDocs } from '../interactive/docs-index.js';
28
28
  import { TestSetIndex } from '../qe/test-sets.js';
29
- import { resolveProjectGraphBinding } from '../projects/graph.js';
29
+ import { estateExe, resolveProjectGraphBinding } from '../projects/graph.js';
30
30
  import { registerProjectRoutes } from '../projects/routes.js';
31
31
  import { registerCampaignRoutes } from '../campaigns/routes.js';
32
32
  import { registerGovernanceWikiRoutes } from './governance-wiki.js';
@@ -36,11 +36,12 @@ import { registerTestingRoutes } from './testing.js';
36
36
  import { registerSkillsRoutes } from './skills.js';
37
37
  import { disabledSkillsHealth } from '../skills/runtime.js';
38
38
  import { STATE_HOME_BLOCKER_CODE, STATE_HOME_REMEDY, isEngineStateHomeRefusal, stateHomeBlockerBody, } from '../projects/state-home-preflight.js';
39
- import { BASE_SKILL_POLICIES, BASE_SKILL_REF_SHAPE } from '../skills/base-skill.js';
39
+ import { BASE_SKILL_POLICIES, BASE_SKILL_REF_SHAPE, baseSkillRemedy } from '../skills/base-skill.js';
40
40
  import { noEligibleSeatBody, parseNoEligibleSeat } from '../core/engine-roster.js';
41
41
  import { ProjectSettingsStore } from '../projects/settings.js';
42
42
  import { boundOrigin, InteractiveBridgePool } from '../interactive/bridge-pool.js';
43
43
  import { composeDeliverText, factsFromRun, framedDeliverText, runUrlFor } from '../core/deliver-text.js';
44
+ import { resolvePullRequest as resolvePullRequestViaGh } from '../core/deliver.js';
44
45
  import { registerInteractiveProxy } from '../interactive/proxy-routes.js';
45
46
  import { registerInteractiveDocDelete } from '../interactive/doc-delete-routes.js';
46
47
  import { registerInteractiveDocList } from '../interactive/doc-list-routes.js';
@@ -54,7 +55,7 @@ import { RetryIndex } from './retry-index.js';
54
55
  import { GroupIndex } from './group-index.js';
55
56
  import { RunTimingIndex, recordRunLaunched } from './run-timing-index.js';
56
57
  import { GuidanceIndex } from './guidance-index.js';
57
- import { DeliveryIndex, gitRunBranchIsEmpty, gitWorktreeIsClean, isDeliverConflictStranded, prUrlFrom, } from './delivery-index.js';
58
+ import { DeliveryIndex, gitRunBranchIsEmpty, gitWorktreeIsClean, isDeliverConflictStranded, prUrlFrom, canDeliverResolver, } from './delivery-index.js';
58
59
  import { DeliveryDerivationCache } from './delivery-cache.js';
59
60
  import { gitReprovisionWorktree, runDeliverScript, } from './post-hoc-deliver.js';
60
61
  import { LOCAL_ACTOR } from './auth.js';
@@ -82,6 +83,17 @@ const STATUS_ORDER = {
82
83
  function sortActionableFirst(views) {
83
84
  return [...views].sort((a, b) => (STATUS_ORDER[a.session.status] ?? 9) - (STATUS_ORDER[b.session.status] ?? 9));
84
85
  }
86
+ /**
87
+ * The whole-graph counts from `wicked-estate stats --db` stdout (`nodes=N edges=M files=F …`, the
88
+ * first line estate prints — `main.rs`'s `stats` summary). `undefined` when the line does not parse
89
+ * (an older/newer estate, an error message): the caller leaves `CodeGraphData.totals` ABSENT.
90
+ */
91
+ export function parseEstateTotals(stdout) {
92
+ const m = /^nodes=(\d+) edges=(\d+) files=(\d+)/m.exec(stdout);
93
+ if (m === null)
94
+ return undefined;
95
+ return { nodes: Number(m[1]), edges: Number(m[2]), files: Number(m[3]) };
96
+ }
85
97
  function codeGraphDbOr503(repo, reply) {
86
98
  try {
87
99
  return codeGraphDb(repo);
@@ -364,6 +376,13 @@ export const LaunchSchema = z.object({
364
376
  * sharing a label form one `RunGroup` on `GET /campaigns`. Persisted/echoed like
365
377
  * `campaignId` (as `AgentSession.group_label`). */
366
378
  groupLabel: z.string().min(1).max(200).optional(),
379
+ /** DES-L9 / crew#550 (api-types 0.38.0) — REVISE an OPEN same-repository pull request: its head
380
+ * branch becomes the run's base and the deliver phase pushes the run's commits onto it (no
381
+ * second PR), commenting the run record. Needs `repoRef` + `workflow` and `deliver: "pr"`
382
+ * (explicit `"none"` is a 400; a daemon that resolves the omitted field to `none` is a 409).
383
+ * Resolved via `gh pr view` at launch — not OPEN / a fork / gh failure ⇒ 409, nothing launched.
384
+ * Send it only when `GET /health.capabilities.revisesPr === true` (engine ≥ 0.7.27). */
385
+ revisesPr: z.number().int().positive().optional(),
367
386
  }).strict().refine((b) => b.deliver !== 'pr' || b.workflow !== undefined, {
368
387
  message: 'deliver: "pr" requires a workflow — a free-text run has no def to append the deliver phase to',
369
388
  path: ['deliver'],
@@ -376,11 +395,26 @@ export const LaunchSchema = z.object({
376
395
  }).refine((b) => b.campaignId === undefined || b.groupLabel === undefined, {
377
396
  message: 'campaignId and groupLabel are mutually exclusive — a run files onto ONE grouping surface (an existing campaign, or a label group)',
378
397
  path: ['groupLabel'],
398
+ }).refine((b) => b.revisesPr === undefined || (b.repoRef !== undefined && b.workflow !== undefined), {
399
+ message: 'revisesPr needs repoRef and workflow — a revision bases a repo-scoped, def-driven run on the pull request\'s branch',
400
+ path: ['revisesPr'],
401
+ }).refine((b) => b.revisesPr === undefined || b.deliver !== 'none', {
402
+ message: 'revisesPr needs deliver: "pr" — a launch that declines delivery has no phase to push the revision with',
403
+ path: ['revisesPr'],
379
404
  });
405
+ /** `POST /runs/:id/gate` (api-types 0.38.0 `GateDecision`; DES-L1 PR-2). Additive arms: `action`
406
+ * names the arm (`approve` | `request_changes` | `reject`; absent = today's two-arm mapping of
407
+ * `approve`), `amendScope` says where an approve's `amend` lands (`cursor` = the gated unit, today's
408
+ * behaviour | `creator` = the first creator phase at/after the cursor — an intake steer reaches the
409
+ * phase that implements). `request_changes` sends a NOT-PASS review back to the creator with the
410
+ * findings in context (`amend` = the operator's note). A disagreement between `action` and
411
+ * `approve` is a 400 that names both. */
380
412
  export const GateSchema = z.object({
381
413
  approve: z.boolean(),
382
414
  amend: z.string().optional(),
383
- }).strict();
415
+ action: z.enum(['approve', 'request_changes', 'reject']).optional(),
416
+ amendScope: z.enum(['cursor', 'creator']).optional(),
417
+ }).strict().refine((b) => b.action === undefined || (b.action === 'approve') === b.approve, { message: '`action` disagrees with `approve`: request_changes and reject require approve: false; approve requires approve: true', path: ['action'] }).refine((b) => b.amendScope === undefined || b.approve, { message: '`amendScope` applies to an approve only (approve: true)', path: ['amendScope'] });
384
418
  /** `PUT /runs/:id/guidance` (DES-UX-002 §7.2, CREW-UX-7) — the durable pre-gate note body.
385
419
  * The empty string is a legal body: it CLEARS the note. The byte cap is checked in the route
386
420
  * (not zod's char-counting `max`) so the 400 names the actual limit. */
@@ -444,6 +478,24 @@ const STUDIO_SETTINGS_KEY = /^studio\.[a-z][a-z0-9-]*$/;
444
478
  * ceiling on ONE key, not on the patch.
445
479
  */
446
480
  const STUDIO_SETTINGS_MAX_BYTES = 512 * 1024;
481
+ /**
482
+ * Stamp the required `seq` onto the daemon-authored frames of a ts-sorted merge. A watchdog frame
483
+ * has no engine `seq` — the engine's log never saw it — so each one rides the seq of the engine
484
+ * record it FOLLOWS (0 before the first). The served array is therefore monotonic non-decreasing in
485
+ * `seq`, and a consumer that re-sorts by seq (studio's narrator) keeps every frame beside the event
486
+ * it was captured after, instead of dropping it to the front on a missing key.
487
+ */
488
+ function withServedSeq(merged) {
489
+ let seq = 0;
490
+ return merged.map((e) => {
491
+ if (!('daemon' in e)) {
492
+ if (typeof e.seq === 'number')
493
+ seq = e.seq;
494
+ return e;
495
+ }
496
+ return { ...e, seq };
497
+ });
498
+ }
447
499
  /**
448
500
  * The daemon REST surface. Every endpoint is a thin wrapper over one adapter /
449
501
  * core-ts call (DES-STUDIO-001 §2). `session`/`phase` nouns are now `run`/`unit`.
@@ -489,6 +541,7 @@ runtime = {}) {
489
541
  const guidanceIndex = runtime.guidanceIndex ?? new GuidanceIndex();
490
542
  const chatScopes = runtime.chatScopes ?? new ChatScopeIndex();
491
543
  const chatTurns = runtime.chatTurns ?? new ChatTurnIndex();
544
+ const chatTranscripts = runtime.chatTranscripts;
492
545
  const deliveryIndex = runtime.deliveryIndex ?? new DeliveryIndex();
493
546
  // Wave 6: the doc↔run binding and the registered test sets — `createServer` injects the real
494
547
  // ones; a directly-driven route set gets an empty index (every run `document_id: null`, no sets).
@@ -508,11 +561,17 @@ runtime = {}) {
508
561
  // the daemon's sweep/warm feed it. The default (a directly-driven route set) is COLD and
509
562
  // unstarted — no background timer under a unit test, reads degrade to the stat-only
510
563
  // tri-state — while `createServer` injects a started one over the production probes.
564
+ // Def-awareness (crew#481 / D-14): the ONE predicate the run DTOs' cache, the campaigns rollup and
565
+ // the resume 409 classify from. A fake adapter with no registry (`listWorkflows` absent) resolves
566
+ // no def ⇒ `runCanDeliver(view, null)` ⇒ candidate — byte-for-byte today's read.
567
+ const canDeliver = runtime.canDeliver ??
568
+ canDeliverResolver(() => (typeof adapter.listWorkflows === 'function' ? adapter.listWorkflows() : []), (m) => app.log.warn(m));
511
569
  const deliveryCache = runtime.deliveryCache ??
512
570
  new DeliveryDerivationCache({
513
571
  listViews: () => adapter.sessionsDetail(),
514
572
  probes: vacuityProbes,
515
573
  isDelivered: (runId) => deliveryIndex.urlFor(runId) !== undefined,
574
+ canDeliver,
516
575
  });
517
576
  const deliverExec = runtime.deliverExec ?? runDeliverScript;
518
577
  // crew#524: the deliver phase's script asks THIS daemon for the run-derived PR text, and the PR
@@ -528,6 +587,8 @@ runtime = {}) {
528
587
  const estateTool = runtime.callEstateTool ?? callEstateTool;
529
588
  /** Repo root for a repo ref, from the registry — shared by the reprovision path below. */
530
589
  const repoRootOf = async (repoRef) => (await adapter.listRepos()).find((r) => r.id === repoRef)?.root_path;
590
+ /** DES-L9: PR number → head branch, through `gh pr view` unless the runtime injected an answerer. */
591
+ const resolvePullRequest = runtime.resolvePullRequest ?? resolvePullRequestViaGh;
531
592
  // The run-DTO joins (DES-UX-001 §8.2/§8.3, DES-UX-002 §7.2): `project_id` from the membership
532
593
  // record — `null` = genuinely unfiled, so the field is ALWAYS present on served runs —
533
594
  // `retry_of` from the lineage index and `guidance` from the guidance index, each set only
@@ -577,7 +638,7 @@ runtime = {}) {
577
638
  return { delivery: 'delivered', deliverUrl: url };
578
639
  if (conflictStrand)
579
640
  return { delivery: 'stranded' };
580
- return deliveryCache.read(view.session);
641
+ return deliveryCache.read(view);
581
642
  };
582
643
  const decorateRun = (view) => {
583
644
  const conflictStrand = normalizeStranded(view);
@@ -607,6 +668,11 @@ runtime = {}) {
607
668
  const createdAt = runTimingIndex.createdAtFor(view.session.id);
608
669
  if (createdAt !== undefined)
609
670
  view.session.created_at = createdAt;
671
+ // `ended_at` (crew#496 / studio#230; api-types 0.38.0): the same posture from the `run.ended`
672
+ // entry the daemon records at the run's terminal frame — ABSENT when it has none.
673
+ const endedAt = runTimingIndex.endedAtFor(view.session.id);
674
+ if (endedAt !== undefined)
675
+ view.session.ended_at = endedAt;
610
676
  const state = resolveDelivery(view, conflictStrand);
611
677
  view.session.delivery = state.delivery;
612
678
  if (state.deliverUrl !== undefined)
@@ -634,19 +700,26 @@ runtime = {}) {
634
700
  // probe honestly reports no gate.
635
701
  const capabilities = typeof adapter.engineCapabilities === 'function'
636
702
  ? adapter.engineCapabilities()
637
- : { deliverGate: false };
703
+ : { deliverGate: false, revisesPr: false };
638
704
  // wicked-core#411 / crew#497: the state-home blocker rides the health probe as a WARNING. The
639
705
  // daemon still SERVES (status stays ok — studio must load and show the blocker) but refuses to
640
706
  // launch while the state home holds an entry the worker Read fence cannot classify. Re-surveyed
641
707
  // per probe (two readdirs) so an entry that appears after boot is reported without a restart;
642
708
  // the field is ABSENT when there is nothing to say, and on a route set booted without the watch.
643
709
  const stateHome = runtime.stateHome !== undefined ? await runtime.stateHome.refresh() : null;
644
- const warnings = stateHome === null
645
- ? []
646
- : stateHome.findings.map((f) => ({ kind: f.kind, severity: f.severity, message: f.message }));
647
710
  // crew#554: the BASE skill posture for the next launch — the composer's confirm line
648
711
  // ("discipline skill: <name> gen N" / "MISSING — …"). Cached by the skills runtime: no I/O.
649
712
  const baseSkill = runtime.skills?.baseSkill() ?? null;
713
+ // F-W1-102: the base skill's ERROR rides `warnings` too. A daemon under `require` with no published
714
+ // generation holding the skill (the crew-only install: no wicked-garden) REFUSES every launch at
715
+ // intake — it must never read "status ok, no warnings". The SAME finding object as
716
+ // `baseSkill.finding` and `/diagnostics.skills.findings[]` (one surface, no new field;
717
+ // `HealthWarning.kind` is open); status stays ok because the daemon serves and studio must load
718
+ // and show it — exactly the state-home blocker's precedent above.
719
+ const warnings = [
720
+ ...(stateHome === null ? [] : stateHome.findings.map((f) => ({ kind: f.kind, severity: f.severity, message: f.message }))),
721
+ ...(baseSkill?.finding ? [{ kind: baseSkill.finding.kind, severity: baseSkill.finding.severity, message: baseSkill.finding.message }] : []),
722
+ ];
650
723
  return {
651
724
  status: 'ok',
652
725
  version: PKG_VERSION,
@@ -1157,6 +1230,36 @@ runtime = {}) {
1157
1230
  }
1158
1231
  if (deliver === 'pr')
1159
1232
  input.deliver = 'pr';
1233
+ // DES-L9 / crew#550 — REVISION: `revisesPr` names an OPEN same-repository pull request whose
1234
+ // head branch becomes the run's base (`baseRef`, crew-internal → the engine's
1235
+ // `LaunchSpec.base_ref`) and the push target of the composed deliver phase — the PR gains
1236
+ // exactly the run's commits, no second PR. Resolved HERE via `gh pr view` (5 s; the engine has
1237
+ // no GitHub client) and refused by name on anything but OPEN + same repo. Judged AFTER the
1238
+ // deliver resolution (F5): a launch this daemon resolved to `none` (deliverDefault) has no
1239
+ // phase to push with — a revision that pushes nothing would be a silent no-op.
1240
+ let revisesPr;
1241
+ if (b.revisesPr !== undefined) {
1242
+ if (deliver !== 'pr') {
1243
+ return reply.code(409).send({
1244
+ error: `revisesPr needs deliver: pr — this daemon resolved the launch to none (deliverDefault); send deliver: "pr"`,
1245
+ });
1246
+ }
1247
+ const revisedRepo = b.repoRef;
1248
+ if (revisedRepo === undefined) {
1249
+ // Unreachable past the schema refine; kept so the type narrows honestly.
1250
+ return reply.code(400).send({ error: 'revisesPr needs repoRef' });
1251
+ }
1252
+ const root = await repoRootOf(revisedRepo);
1253
+ if (root === undefined) {
1254
+ return reply.code(404).send({ error: `repoRef names an unknown repo: ${revisedRepo}` });
1255
+ }
1256
+ const resolved = await resolvePullRequest(root, b.revisesPr);
1257
+ if (!resolved.ok)
1258
+ return reply.code(409).send({ error: resolved.error });
1259
+ revisesPr = { number: b.revisesPr, headRef: resolved.pr.headRef, url: resolved.pr.url };
1260
+ input.baseRef = revisesPr.headRef;
1261
+ input.revisesPr = revisesPr;
1262
+ }
1160
1263
  // Retry lineage (DES-UX-001 §8.3): `retryOf` must name an EXISTING run — recording lineage
1161
1264
  // to a run that never existed would be provenance pointing at nothing, so the launch fails
1162
1265
  // loudly (400, before anything is committed) rather than filing a dangling edge.
@@ -1212,6 +1315,9 @@ runtime = {}) {
1212
1315
  // CREW-UX-3: the trail is the durable record of lineage — the retry index (and a
1213
1316
  // restarted daemon's hydrate) reads it back from exactly this entry.
1214
1317
  ...(b.retryOf !== undefined ? { retryOf: b.retryOf } : {}),
1318
+ // DES-L9: the revised PR (number, head branch, URL) — the retry index (and a restarted
1319
+ // daemon's hydrate) reads it back from here so a stranded revision re-pushes to THAT PR.
1320
+ ...(revisesPr !== undefined ? { revisesPr, baseRef: revisesPr.headRef } : {}),
1215
1321
  // wicked-studio#27: the trail is likewise the durable record of the group attach —
1216
1322
  // the group index (and a restarted daemon's hydrate) reads it back from here.
1217
1323
  ...(b.campaignId !== undefined ? { campaignId: b.campaignId } : {}),
@@ -1219,6 +1325,8 @@ runtime = {}) {
1219
1325
  });
1220
1326
  if (b.retryOf !== undefined)
1221
1327
  retryIndex.set(runId, b.retryOf);
1328
+ if (revisesPr !== undefined)
1329
+ retryIndex.setRevisesPr(runId, revisesPr);
1222
1330
  if (b.campaignId !== undefined)
1223
1331
  groupIndex.set(runId, { campaignId: b.campaignId });
1224
1332
  else if (b.groupLabel !== undefined)
@@ -1279,9 +1387,8 @@ runtime = {}) {
1279
1387
  // composer renders a clear card (skill, policy, remedy) instead of a generic launch 400.
1280
1388
  if (/\bbase skill\b.*\brefused at intake\b/is.test(msg)) {
1281
1389
  const posture = runtime.skills?.baseSkill() ?? null;
1282
- const remedy = posture !== null && posture.inCatalog
1283
- ? `the skill is in the catalog — POST /skills/publish hands it to the next launch; or set baseSkillPolicy "warn" (PUT /settings) to run without the discipline directive`
1284
- : `install a wicked-garden that ships the skill, POST /skills/refresh-baseline, then POST /skills/publish; or set baseSkillPolicy "warn" (PUT /settings) to run without the discipline directive`;
1390
+ // F-W1-102: the SAME remedy sentence the /health finding and `wicked-crew status` carry.
1391
+ const remedy = baseSkillRemedy(posture?.inCatalog ?? false);
1285
1392
  return reply.code(422).send({ code: 'base_skill_refused', error: msg, baseSkill: posture, remedy });
1286
1393
  }
1287
1394
  const busy = /busy|in flight|already/i.test(msg);
@@ -1419,7 +1526,10 @@ runtime = {}) {
1419
1526
  const origin = boundOrigin(app.server.address());
1420
1527
  // The workflow DEFINITION (a user-registered workflow's view carries the engine instance id).
1421
1528
  const def = resolveRunWorkflow(run, listWorkflowsSafe());
1422
- const text = composeDeliverText(factsFromRun(decorateRun(run), runUrlFor(origin, id), { workflowId: def?.id ?? null }));
1529
+ const text = composeDeliverText(factsFromRun(decorateRun(run), runUrlFor(origin, id), {
1530
+ workflowId: def?.id ?? null,
1531
+ revisesPr: retryIndex.revisesPrFor(id) ?? null,
1532
+ }));
1423
1533
  return reply.type('text/plain; charset=utf-8').send(framedDeliverText(text));
1424
1534
  });
1425
1535
  // ── Post-hoc delivery (crew#393) — lift a stranded run's worktree into a PR ──
@@ -1520,6 +1630,9 @@ runtime = {}) {
1520
1630
  // the throwaway down after the lift.
1521
1631
  let result;
1522
1632
  let worktreeGone = false;
1633
+ // DES-L9: a stranded REVISION re-pushes onto its PR's branch (re-checked OPEN via the same
1634
+ // resolver), never a new PR; a PR that closed meanwhile is a named 409.
1635
+ let revisionRefused;
1523
1636
  try {
1524
1637
  // The worktree admin (reprovision), the deliver spawn, AND the throwaway teardown all run
1525
1638
  // under the per-repo lock, so a second stranded run in this repo cannot touch
@@ -1544,10 +1657,27 @@ runtime = {}) {
1544
1657
  // holds (the fallback), and names this daemon so the script can re-ask at delivery.
1545
1658
  const origin = boundOrigin(app.server.address());
1546
1659
  const def = resolveRunWorkflow(run, listWorkflowsSafe());
1660
+ const revising = retryIndex.revisesPrFor(id);
1661
+ let revisesPr = null;
1662
+ if (revising !== undefined) {
1663
+ const revisedRoot = await repoRootOf(repoRef);
1664
+ const again = revisedRoot === undefined
1665
+ ? { ok: false, error: `repo ${repoRef} is no longer registered — cannot re-check pull request #${revising.number}` }
1666
+ : await resolvePullRequest(revisedRoot, revising.number);
1667
+ if (!again.ok) {
1668
+ revisionRefused = again.error;
1669
+ return;
1670
+ }
1671
+ revisesPr = { number: revising.number, headRef: again.pr.headRef, url: again.pr.url };
1672
+ }
1547
1673
  result = await deliverExec(workdir, s.problem ?? undefined, {
1548
1674
  runId: id,
1549
1675
  apiOrigin: origin,
1550
- facts: factsFromRun(run, runUrlFor(origin, id), { workflowId: def?.id ?? null }),
1676
+ facts: factsFromRun(run, runUrlFor(origin, id), {
1677
+ workflowId: def?.id ?? null,
1678
+ revisesPr: revisesPr === null ? null : { number: revisesPr.number, url: revisesPr.url },
1679
+ }),
1680
+ revisesPr,
1551
1681
  });
1552
1682
  }
1553
1683
  finally {
@@ -1567,6 +1697,9 @@ runtime = {}) {
1567
1697
  error: `run ${id}'s worktree is gone (${s.workdir}) — nothing left to deliver`,
1568
1698
  });
1569
1699
  }
1700
+ if (revisionRefused !== undefined) {
1701
+ return reply.code(409).send({ error: revisionRefused });
1702
+ }
1570
1703
  if (result === undefined) {
1571
1704
  // Unreachable: the lock body assigns `result` on every path that is not `worktreeGone`.
1572
1705
  return reply.code(500).send({ error: `deliver for run ${id} produced no result` });
@@ -1783,7 +1916,8 @@ runtime = {}) {
1783
1916
  // taken out by the council-bench / dispatch-timeout path, not by admission, so admission's
1784
1917
  // list never named it. Every requested-or-defaulted seat that is not in `seats` is named
1785
1918
  // here with the most specific cause the daemon knows: its own "no credential" report
1786
- // (`auth`), this daemon's council bench (`bench`), else the dispatch budget (`budget`).
1919
+ // (`auth`), else the dispatch budget (`budget`). (R5b: crew keeps no council bench of its
1920
+ // own any more — the engine benches per run and says so in `unitDistributed.degradedReason`.)
1787
1921
  for (const key of clis) {
1788
1922
  if (seats.some((s) => s.cliKey === key) || refused.some((r) => r.cliKey === key))
1789
1923
  continue;
@@ -1798,13 +1932,6 @@ runtime = {}) {
1798
1932
  source: 'auth',
1799
1933
  });
1800
1934
  }
1801
- else if (st?.council_bench !== undefined) {
1802
- refused.push({
1803
- cliKey: key,
1804
- reason: `not seated — ${st.council_ineligible_reason ?? 'benched by this daemon’s recent councils'}`,
1805
- source: 'bench',
1806
- });
1807
- }
1808
1935
  else {
1809
1936
  refused.push({
1810
1937
  cliKey: key,
@@ -1878,7 +2005,7 @@ runtime = {}) {
1878
2005
  projectAttachError = message(err);
1879
2006
  }
1880
2007
  }
1881
- if (!chatScopes.set(chatId, scope, token, refused)) {
2008
+ if (!chatScopes.set(chatId, scope, token, refused, engine)) {
1882
2009
  // The reservation was cancelled while the open was in flight — an engine `chatClosed` or a
1883
2010
  // `DELETE` for this id (Copilot, #518). Nothing was recorded; tear the chat down (engine
1884
2011
  // session, scratch root, and the filing just made) instead of returning a stale 201.
@@ -1927,6 +2054,19 @@ runtime = {}) {
1927
2054
  chatScopes.release(chatId, token);
1928
2055
  }
1929
2056
  });
2057
+ /**
2058
+ * The ONE 409 `turn_in_flight` body (review NIT): the send route and the re-seat route refuse for
2059
+ * the same reason and promise the same shape, so they build it in one place and cannot drift.
2060
+ * `nothingHappened` names what did NOT happen on this route — their only difference.
2061
+ */
2062
+ const turnInFlightBody = (chatId, inFlight, nothingHappened) => ({
2063
+ code: 'turn_in_flight',
2064
+ error: `${inFlight.busy.join(', ')} ${inFlight.busy.length === 1 ? 'is' : 'are'} still answering the previous ` +
2065
+ `message in this chat (turn ${inFlight.turnId}, ${Math.round(inFlight.ageMs / 1000)}s ago: ` +
2066
+ `“${inFlight.excerpt}”) — wait for the reply, or target only idle seats. ${nothingHappened}`,
2067
+ chatId,
2068
+ turn: inFlight,
2069
+ });
1930
2070
  const ChatMessageSchema = z.object({
1931
2071
  text: z.string().min(1).max(65536),
1932
2072
  targets: z.array(z.string().min(1)).min(1).max(8).optional(),
@@ -1944,28 +2084,103 @@ runtime = {}) {
1944
2084
  if (!parsed.success) {
1945
2085
  return reply.code(400).send(invalidBody(parsed.error, 'Invalid request body'));
1946
2086
  }
1947
- const inFlight = chatTurns.inFlight(id, parsed.data.targets);
2087
+ // DES-L5 §5-c (F-E2E-041): the audience is decided HERE — the named targets, else the engine's
2088
+ // warm seats — so the turn can be opened BEFORE the engine call, in the same tick as the
2089
+ // predicate below: two racing sends can no longer both pass `inFlight` during the await, and a
2090
+ // `chatDelta` that lands before `chatSend` resolves is already stamped with its `turn_id`.
2091
+ const audience = parsed.data.targets ?? (await adapter.chatSeats(id));
2092
+ if (audience.length === 0) {
2093
+ return reply.code(409).send({ error: `chat '${id}' has no warm seats — open it first` });
2094
+ }
2095
+ const inFlight = chatTurns.inFlight(id, audience);
1948
2096
  if (inFlight !== null) {
1949
- const ageS = Math.round(inFlight.ageMs / 1000);
1950
- return reply.code(409).send({
1951
- code: 'turn_in_flight',
1952
- error: `${inFlight.busy.join(', ')} ${inFlight.busy.length === 1 ? 'is' : 'are'} still answering the previous ` +
1953
- `message in this chat (turn ${inFlight.turnId}, ${ageS}s ago: “${inFlight.excerpt}”) — wait for the ` +
1954
- `reply, or target only idle seats. Nothing was sent.`,
1955
- chatId: id,
1956
- turn: inFlight,
1957
- });
2097
+ return reply.code(409).send(turnInFlightBody(id, inFlight, 'Nothing was sent.'));
1958
2098
  }
2099
+ // Sync, same tick as `inFlight`: the window is closed before anything yields.
2100
+ const turn = chatTurns.begin(id, audience, parsed.data.text);
1959
2101
  try {
1960
2102
  const seats = await adapter.chatSend(id, parsed.data.text, parsed.data.targets);
1961
- const turn = chatTurns.begin(id, seats, parsed.data.text);
2103
+ if (turn !== null) {
2104
+ // The engine's answer is the truth: the reserved audience is squared with the seats it
2105
+ // reached, and the operator's message joins the transcript with exactly those seats.
2106
+ chatTurns.reconcile(id, turn.turnId, seats);
2107
+ chatTranscripts?.appendUser(id, turn.turnId, parsed.data.text, seats);
2108
+ }
1962
2109
  return reply.code(202).send({ seats, ...(turn !== null ? { turnId: turn.turnId } : {}) });
1963
2110
  }
1964
2111
  catch (err) {
2112
+ // Nothing went out: the reservation is retracted so the next send is not refused for it.
2113
+ if (turn !== null)
2114
+ chatTurns.abort(id, turn.turnId);
1965
2115
  const msg = message(err);
1966
2116
  return reply.code(/no warm seats/.test(msg) ? 409 : 400).send({ error: msg });
1967
2117
  }
1968
2118
  });
2119
+ // F-W1-005 (wave-1 P6): re-seat NAMED seats on a LIVE chat — the retry lever for a seat the open
2120
+ // refused (or whose session died). Re-runs the engine's per-seat `chat_ensure` through `chatOpen`
2121
+ // with the IDENTICAL scope recorded at open (a different scope would evict every warm seat —
2122
+ // Copilot, #426; the index retains what the engine was handed). Explicit seats bypass the daemon's
2123
+ // default pre-filter exactly as `clis` on `POST /chats` does: the engine refuses per seat and its
2124
+ // reason rides the outcome, then `refused[]` and a `chatSeatRefused` frame — the same shapes the
2125
+ // 201 carries, so the thread and the picker read one story. Never a new chat, never a new pool key.
2126
+ const ChatSeatsSchema = z.object({
2127
+ clis: z.array(z.string().min(1)).min(1).max(8),
2128
+ }).strict();
2129
+ app.post(`${V}/chats/:id/seats`,
2130
+ // Typed like every neighbouring chat route (review HIGH-1): the names bind the 0.39.0 api-types
2131
+ // mirror to this row — `ChatSeatsBody { clis }` in, `ChatSeatsResponse { chatId, seats, refused }`
2132
+ // out (the 201's own `ChatSeatOutcome` / `ChatSeatRefusal` shapes, which 0.38.0 already ships).
2133
+ {
2134
+ config: {
2135
+ manifest: {
2136
+ requestType: 'ChatSeatsBody',
2137
+ responseType: 'ChatSeatsResponse',
2138
+ statusCodes: [200, 400, 404, 409],
2139
+ },
2140
+ },
2141
+ }, async (req, reply) => {
2142
+ const { id } = req.params;
2143
+ const parsed = ChatSeatsSchema.safeParse(req.body);
2144
+ if (!parsed.success) {
2145
+ return reply.code(400).send(invalidBody(parsed.error, 'Invalid request body'));
2146
+ }
2147
+ const engine = chatScopes.engineOf(id);
2148
+ if (engine === undefined) {
2149
+ return reply.code(404).send({
2150
+ error: `chat ${id} is not open on this daemon — open a chat first, then re-seat into it`,
2151
+ });
2152
+ }
2153
+ const clis = [...new Set(parsed.data.clis)];
2154
+ // A seat mid-turn is not re-warmed under its own reply (the same rule as a send).
2155
+ const inFlight = chatTurns.inFlight(id, clis);
2156
+ if (inFlight !== null) {
2157
+ return reply.code(409).send(turnInFlightBody(id, inFlight, 'No seat was re-warmed.'));
2158
+ }
2159
+ try {
2160
+ const seats = await adapter.chatOpen(id, clis, engine.cwd, {
2161
+ codeGraphDb: engine.codeGraphDb,
2162
+ readRoots: engine.readRoots,
2163
+ });
2164
+ const refused = chatScopes.foldSeats(id, seats);
2165
+ const projectId = projects.index.projectOf(id) ?? undefined;
2166
+ for (const s of seats) {
2167
+ if (s.ok)
2168
+ continue;
2169
+ runtime.broadcast?.({
2170
+ type: 'chatSeatRefused',
2171
+ chat: id,
2172
+ cliKey: s.cliKey,
2173
+ reason: s.error ?? 'the engine refused the seat',
2174
+ source: 'engine',
2175
+ ...(projectId !== undefined ? { project_id: projectId } : {}),
2176
+ });
2177
+ }
2178
+ return { chatId: id, seats, refused };
2179
+ }
2180
+ catch (err) {
2181
+ return reply.code(400).send({ error: message(err) });
2182
+ }
2183
+ });
1969
2184
  // Enumerate live chats (FINDING-027 gap 4). Chat sessions deliberately outlive the page, and
1970
2185
  // their ids are minted client-side — so before this route the only record of an orphaned seat
1971
2186
  // lived in the tab that abandoned it, and an operator could not reclaim one without restarting
@@ -2012,6 +2227,9 @@ runtime = {}) {
2012
2227
  seats: await adapter.chatSeats(id),
2013
2228
  scope: chatScopes.get(id) ?? null,
2014
2229
  refused: chatScopes.refusedOf(id) ?? null,
2230
+ // DES-L5 (D-13): the transcript so far, append order — `[]` before the first persisted
2231
+ // turn; the file goes with the chat on `chatClosed`, so a reclaimed id answers `[]` too.
2232
+ ...(chatTranscripts !== undefined ? { messages: chatTranscripts.read(id) } : {}),
2015
2233
  };
2016
2234
  }
2017
2235
  catch (err) {
@@ -2154,7 +2372,10 @@ runtime = {}) {
2154
2372
  isSteeringAuthorRun(run, adapter.listWorkflows());
2155
2373
  const gatePrompt = steeringPropose ? gateCache.get(id)?.prompt : undefined;
2156
2374
  try {
2157
- const status = await adapter.confirmGate(id, parsed.data.approve, parsed.data.amend);
2375
+ // All five positionals, explicit `undefined` for the absent ones (DES-L1 PR-2; the
2376
+ // gate-arms tripwire pins the arity). An engine refusal (no creator to send back to,
2377
+ // an engine older than the arm) surfaces as the existing 409 below.
2378
+ const status = await adapter.confirmGate(id, parsed.data.approve, parsed.data.amend, parsed.data.action, parsed.data.amendScope);
2158
2379
  // WHO approved/rejected — the gate-decision audit (task #88). The engine
2159
2380
  // records THAT the gate resolved (interaction_requests / gateDecided);
2160
2381
  // only this HTTP layer knows the authenticated principal behind it.
@@ -2163,6 +2384,8 @@ runtime = {}) {
2163
2384
  detail: {
2164
2385
  approve: parsed.data.approve,
2165
2386
  ...(parsed.data.amend !== undefined ? { amend: parsed.data.amend } : {}),
2387
+ ...(parsed.data.action !== undefined ? { action: parsed.data.action } : {}),
2388
+ ...(parsed.data.amendScope !== undefined ? { amendScope: parsed.data.amendScope } : {}),
2166
2389
  status,
2167
2390
  },
2168
2391
  });
@@ -2575,14 +2798,22 @@ runtime = {}) {
2575
2798
  if (!ids.includes(id)) {
2576
2799
  return reply.code(404).send({ error: 'Run not found' });
2577
2800
  }
2578
- const events = await adapter.runEvents(id);
2579
- if (events === null) {
2801
+ const engineEvents = await adapter.runEvents(id);
2802
+ if (engineEvents === null) {
2580
2803
  // Same shape as the gate route's 503, and for the same reason: "no events" would report a
2581
2804
  // missing binding as a fact about the run. The run may well have a rich history.
2582
2805
  return reply.code(503).send({
2583
2806
  error: 'Run history is unavailable: this wicked-core build has no event-log read binding',
2584
2807
  });
2585
2808
  }
2809
+ // wicked-studio#284: the stall watchdog's frames are daemon-authored — the engine's log never
2810
+ // saw them — so a reloaded page used to lose the "needs you" facts the live socket carried.
2811
+ // Merge the remembered frames in capture order beside the engine's; they carry `daemon: true`
2812
+ // and no engine `seq`.
2813
+ const daemonFrames = runtime.stallFrames?.(id) ?? [];
2814
+ const events = daemonFrames.length === 0
2815
+ ? engineEvents
2816
+ : withServedSeq([...engineEvents, ...daemonFrames].sort((a, b) => a.ts - b.ts));
2586
2817
  // An empty array here is a real answer, not a failure: runs that predate the log have no
2587
2818
  // history, and saying so is the honest response.
2588
2819
  // Both spellings of "several types" mean the same thing: `?type=a,b` and `?type=a&type=b`.
@@ -3152,17 +3383,28 @@ runtime = {}) {
3152
3383
  if (q.focus !== undefined && q.focus.trim() !== '') {
3153
3384
  args.push('--focus', q.focus.trim());
3154
3385
  }
3155
- const { stdout } = await execCapped('wicked-estate', args, {
3156
- timeout: 30_000,
3157
- cwd: repo.root_path,
3158
- });
3386
+ // Whole-graph `totals` beside the served slice (crew#505 / F-RC1-100; api-types 0.38.0):
3387
+ // `graph-view --limit` emits only the slice, so `stats` alone read "my repo has 150 symbols".
3388
+ // `wicked-estate stats --db` prints `nodes=N edges=M files=F …`; both spawns run under
3389
+ // `Promise.all` (one round trip, not two), the SAME daemon-side CLI posture as `graph-view`.
3390
+ // A stats spawn that fails or prints something else leaves `totals` ABSENT and the slice
3391
+ // still 200 — the tile then knows only the slice, never a substituted count.
3392
+ const exe = estateExe();
3393
+ const [{ stdout }, statsOut] = await Promise.all([
3394
+ execCapped(exe, args, { timeout: 30_000, cwd: repo.root_path }),
3395
+ execCapped(exe, ['stats', '--db', dbPath], { timeout: 30_000, cwd: repo.root_path })
3396
+ .then((r) => r.stdout)
3397
+ .catch(() => null),
3398
+ ]);
3159
3399
  const raw = JSON.parse(stdout);
3160
3400
  const fileCount = new Set(raw.nodes.map((n) => n.file)).size;
3401
+ const totals = statsOut === null ? undefined : parseEstateTotals(statsOut);
3161
3402
  const built = {
3162
3403
  graph: {
3163
3404
  nodes: raw.nodes,
3164
3405
  edges: raw.edges,
3165
3406
  stats: { nodeCount: raw.nodes.length, edgeCount: raw.edges.length, fileCount },
3407
+ ...(totals !== undefined ? { totals } : {}),
3166
3408
  },
3167
3409
  };
3168
3410
  return reply.send(built);
@@ -3316,7 +3558,11 @@ runtime = {}) {
3316
3558
  // The store is SHARED with the skin (crew#323): beside the engine's own keys it round-trips
3317
3559
  // the studio's `studio.*` preference blobs verbatim — see `CrewSystemSettings`'s index
3318
3560
  // signature in core/types.ts, which states that rather than leaving it to a client comment.
3319
- app.get(`${V}/settings`, async () => ({ settings: await adapter.getSettings() }));
3561
+ // crew#494 (F-007 — FIX-IT-ALL L10-6): the System page showed a LITERAL settings path; the daemon
3562
+ // now says where its settings file actually is (`WICKED_CREW_SYSTEM_SETTINGS` honoured) as the
3563
+ // additive `path` (api-types 0.38.0 `SettingsResponse.path?`; adjudicated §4.6 — `path`, not
3564
+ // `settings_path`).
3565
+ app.get(`${V}/settings`, async () => ({ settings: await adapter.getSettings(), path: settingsFilePath() }));
3320
3566
  app.put(`${V}/settings`, async (req, reply) => {
3321
3567
  const patch = req.body;
3322
3568
  if (typeof patch !== 'object' || patch === null || Array.isArray(patch)) {
@@ -3406,7 +3652,7 @@ runtime = {}) {
3406
3652
  const p = patch.baseSkillPolicy;
3407
3653
  if (typeof p !== 'string' || !BASE_SKILL_POLICIES.has(p)) {
3408
3654
  return reply.code(400).send({
3409
- error: "baseSkillPolicy must be 'warn' (runs proceed without a missing base skill, with a diagnostics warning) or 'require' (the engine refuses launches at intake until the published snapshot holds it)",
3655
+ error: "baseSkillPolicy must be 'require' — the only policy (the engine refuses launches at intake until the published snapshot holds the base skill); 'warn' was deleted in crew 0.7.35: it ran seats UNGROUNDED. Set baseSkillRef \"\" to turn the base skill off explicitly",
3410
3656
  });
3411
3657
  }
3412
3658
  }
@@ -3819,6 +4065,8 @@ runtime = {}) {
3819
4065
  vacuity: vacuityProbes,
3820
4066
  // A non-probe derivation throw in the rollup is a defect — error level, loud in diagnostics.
3821
4067
  logDefect: (m) => app.log.error(m),
4068
+ // crew#481: the SAME def-awareness the run DTOs apply — one predicate, every surface.
4069
+ canDeliver,
3822
4070
  });
3823
4071
  // ── Governance wiki management (wiki-mgmt) — scoreboard + honest empty-state meta ──────────
3824
4072
  // Browse (`GET /governance/rules` + facets) and retire (`DELETE /governance/rules/:id`) stay