wicked-crew 0.7.40 → 0.7.45

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 (360) hide show
  1. package/dist/api/audit.d.ts +3 -0
  2. package/dist/api/audit.d.ts.map +1 -1
  3. package/dist/api/audit.js +2 -0
  4. package/dist/api/audit.js.map +1 -1
  5. package/dist/api/capture.d.ts +149 -0
  6. package/dist/api/capture.d.ts.map +1 -0
  7. package/dist/api/capture.js +330 -0
  8. package/dist/api/capture.js.map +1 -0
  9. package/dist/api/chat-citations.d.ts +174 -0
  10. package/dist/api/chat-citations.d.ts.map +1 -0
  11. package/dist/api/chat-citations.js +528 -0
  12. package/dist/api/chat-citations.js.map +1 -0
  13. package/dist/api/chat-transcripts.d.ts +20 -1
  14. package/dist/api/chat-transcripts.d.ts.map +1 -1
  15. package/dist/api/chat-transcripts.js +29 -0
  16. package/dist/api/chat-transcripts.js.map +1 -1
  17. package/dist/api/delivery-cache.d.ts +2 -2
  18. package/dist/api/delivery-freeze.d.ts +66 -0
  19. package/dist/api/delivery-freeze.d.ts.map +1 -0
  20. package/dist/api/delivery-freeze.js +104 -0
  21. package/dist/api/delivery-freeze.js.map +1 -0
  22. package/dist/api/delivery-index.d.ts +53 -9
  23. package/dist/api/delivery-index.d.ts.map +1 -1
  24. package/dist/api/delivery-index.js +115 -22
  25. package/dist/api/delivery-index.js.map +1 -1
  26. package/dist/api/demo.d.ts +90 -0
  27. package/dist/api/demo.d.ts.map +1 -0
  28. package/dist/api/demo.js +492 -0
  29. package/dist/api/demo.js.map +1 -0
  30. package/dist/api/diagnostics.d.ts +64 -1
  31. package/dist/api/diagnostics.d.ts.map +1 -1
  32. package/dist/api/diagnostics.js +129 -1
  33. package/dist/api/diagnostics.js.map +1 -1
  34. package/dist/api/eval-sample.d.ts +6 -6
  35. package/dist/api/governance-steering.d.ts +2 -2
  36. package/dist/api/mcp.d.ts +282 -0
  37. package/dist/api/mcp.d.ts.map +1 -0
  38. package/dist/api/mcp.js +521 -0
  39. package/dist/api/mcp.js.map +1 -0
  40. package/dist/api/plan-schema.d.ts +56 -0
  41. package/dist/api/plan-schema.d.ts.map +1 -0
  42. package/dist/api/plan-schema.js +26 -0
  43. package/dist/api/plan-schema.js.map +1 -0
  44. package/dist/api/roster-standing.d.ts +15 -2
  45. package/dist/api/roster-standing.d.ts.map +1 -1
  46. package/dist/api/roster-standing.js +55 -3
  47. package/dist/api/roster-standing.js.map +1 -1
  48. package/dist/api/routes.d.ts +630 -51
  49. package/dist/api/routes.d.ts.map +1 -1
  50. package/dist/api/routes.js +738 -127
  51. package/dist/api/routes.js.map +1 -1
  52. package/dist/api/run-files.d.ts.map +1 -1
  53. package/dist/api/run-files.js +31 -2
  54. package/dist/api/run-files.js.map +1 -1
  55. package/dist/api/run-liveness.d.ts +41 -0
  56. package/dist/api/run-liveness.d.ts.map +1 -0
  57. package/dist/api/run-liveness.js +166 -0
  58. package/dist/api/run-liveness.js.map +1 -0
  59. package/dist/api/seat-health.d.ts.map +1 -1
  60. package/dist/api/seat-health.js +20 -4
  61. package/dist/api/seat-health.js.map +1 -1
  62. package/dist/api/seat-probe.d.ts +141 -0
  63. package/dist/api/seat-probe.d.ts.map +1 -0
  64. package/dist/api/seat-probe.js +257 -0
  65. package/dist/api/seat-probe.js.map +1 -0
  66. package/dist/api/seat-record.d.ts +40 -0
  67. package/dist/api/seat-record.d.ts.map +1 -0
  68. package/dist/api/seat-record.js +218 -0
  69. package/dist/api/seat-record.js.map +1 -0
  70. package/dist/api/seat-signin.d.ts.map +1 -1
  71. package/dist/api/seat-signin.js +3 -1
  72. package/dist/api/seat-signin.js.map +1 -1
  73. package/dist/api/seat-standing.d.ts +24 -4
  74. package/dist/api/seat-standing.d.ts.map +1 -1
  75. package/dist/api/seat-standing.js +50 -3
  76. package/dist/api/seat-standing.js.map +1 -1
  77. package/dist/api/server.d.ts +34 -48
  78. package/dist/api/server.d.ts.map +1 -1
  79. package/dist/api/server.js +283 -107
  80. package/dist/api/server.js.map +1 -1
  81. package/dist/api/skills.d.ts +3 -1
  82. package/dist/api/skills.d.ts.map +1 -1
  83. package/dist/api/skills.js +7 -2
  84. package/dist/api/skills.js.map +1 -1
  85. package/dist/api/stall-watchdog.d.ts +34 -3
  86. package/dist/api/stall-watchdog.d.ts.map +1 -1
  87. package/dist/api/stall-watchdog.js +107 -8
  88. package/dist/api/stall-watchdog.js.map +1 -1
  89. package/dist/api/steering-landing.d.ts +6 -6
  90. package/dist/api/steering-landing.d.ts.map +1 -1
  91. package/dist/api/steering-landing.js +7 -9
  92. package/dist/api/steering-landing.js.map +1 -1
  93. package/dist/api/testing.d.ts +6 -4
  94. package/dist/api/testing.d.ts.map +1 -1
  95. package/dist/api/testing.js +82 -47
  96. package/dist/api/testing.js.map +1 -1
  97. package/dist/campaign/boot-resume.d.ts +34 -0
  98. package/dist/campaign/boot-resume.d.ts.map +1 -0
  99. package/dist/campaign/boot-resume.js +45 -0
  100. package/dist/campaign/boot-resume.js.map +1 -0
  101. package/dist/campaigns/rollup.d.ts +4 -1
  102. package/dist/campaigns/rollup.d.ts.map +1 -1
  103. package/dist/campaigns/rollup.js +6 -1
  104. package/dist/campaigns/rollup.js.map +1 -1
  105. package/dist/campaigns/routes.d.ts +2 -2
  106. package/dist/cli/bearer.d.ts +27 -1
  107. package/dist/cli/bearer.d.ts.map +1 -1
  108. package/dist/cli/bearer.js +62 -2
  109. package/dist/cli/bearer.js.map +1 -1
  110. package/dist/cli/governance.d.ts +11 -0
  111. package/dist/cli/governance.d.ts.map +1 -1
  112. package/dist/cli/governance.js +8 -2
  113. package/dist/cli/governance.js.map +1 -1
  114. package/dist/cli/index.d.ts.map +1 -1
  115. package/dist/cli/index.js +140 -75
  116. package/dist/cli/index.js.map +1 -1
  117. package/dist/cli/mcp.d.ts +11 -0
  118. package/dist/cli/mcp.d.ts.map +1 -1
  119. package/dist/cli/mcp.js +40 -9
  120. package/dist/cli/mcp.js.map +1 -1
  121. package/dist/cli/service.d.ts +78 -0
  122. package/dist/cli/service.d.ts.map +1 -0
  123. package/dist/cli/service.js +334 -0
  124. package/dist/cli/service.js.map +1 -0
  125. package/dist/core/adapter.d.ts +238 -12
  126. package/dist/core/adapter.d.ts.map +1 -1
  127. package/dist/core/adapter.js +619 -157
  128. package/dist/core/adapter.js.map +1 -1
  129. package/dist/core/bridge-reaper.d.ts +4 -1
  130. package/dist/core/bridge-reaper.d.ts.map +1 -1
  131. package/dist/core/bridge-reaper.js +15 -1
  132. package/dist/core/bridge-reaper.js.map +1 -1
  133. package/dist/core/bus-notice.d.ts +27 -0
  134. package/dist/core/bus-notice.d.ts.map +1 -0
  135. package/dist/core/bus-notice.js +24 -0
  136. package/dist/core/bus-notice.js.map +1 -0
  137. package/dist/core/bus.d.ts +99 -0
  138. package/dist/core/bus.d.ts.map +1 -0
  139. package/dist/core/bus.js +176 -0
  140. package/dist/core/bus.js.map +1 -0
  141. package/dist/core/deliver-exclusions.d.ts +71 -0
  142. package/dist/core/deliver-exclusions.d.ts.map +1 -0
  143. package/dist/core/deliver-exclusions.js +110 -0
  144. package/dist/core/deliver-exclusions.js.map +1 -0
  145. package/dist/core/deliver-text.d.ts +61 -14
  146. package/dist/core/deliver-text.d.ts.map +1 -1
  147. package/dist/core/deliver-text.js +178 -30
  148. package/dist/core/deliver-text.js.map +1 -1
  149. package/dist/core/deliver-triage.d.ts +11 -2
  150. package/dist/core/deliver-triage.d.ts.map +1 -1
  151. package/dist/core/deliver-triage.js +19 -1
  152. package/dist/core/deliver-triage.js.map +1 -1
  153. package/dist/core/deliver.d.ts +161 -3
  154. package/dist/core/deliver.d.ts.map +1 -1
  155. package/dist/core/deliver.js +463 -25
  156. package/dist/core/deliver.js.map +1 -1
  157. package/dist/core/deliverable-floor.d.ts.map +1 -1
  158. package/dist/core/deliverable-floor.js +5 -7
  159. package/dist/core/deliverable-floor.js.map +1 -1
  160. package/dist/core/engine-bus.d.ts +40 -0
  161. package/dist/core/engine-bus.d.ts.map +1 -0
  162. package/dist/core/engine-bus.js +27 -0
  163. package/dist/core/engine-bus.js.map +1 -0
  164. package/dist/core/estate-mcp-client.d.ts +87 -5
  165. package/dist/core/estate-mcp-client.d.ts.map +1 -1
  166. package/dist/core/estate-mcp-client.js +90 -11
  167. package/dist/core/estate-mcp-client.js.map +1 -1
  168. package/dist/core/linked-issues.d.ts +46 -0
  169. package/dist/core/linked-issues.d.ts.map +1 -0
  170. package/dist/core/linked-issues.js +123 -0
  171. package/dist/core/linked-issues.js.map +1 -0
  172. package/dist/core/run-identity.d.ts +100 -0
  173. package/dist/core/run-identity.d.ts.map +1 -0
  174. package/dist/core/run-identity.js +178 -0
  175. package/dist/core/run-identity.js.map +1 -0
  176. package/dist/core/shutdown.d.ts +16 -0
  177. package/dist/core/shutdown.d.ts.map +1 -0
  178. package/dist/core/shutdown.js +39 -0
  179. package/dist/core/shutdown.js.map +1 -0
  180. package/dist/core/types.d.ts +15 -1
  181. package/dist/core/types.d.ts.map +1 -1
  182. package/dist/core/types.js +4 -0
  183. package/dist/core/types.js.map +1 -1
  184. package/dist/interactive/bridge-pool.d.ts +67 -3
  185. package/dist/interactive/bridge-pool.d.ts.map +1 -1
  186. package/dist/interactive/bridge-pool.js +129 -13
  187. package/dist/interactive/bridge-pool.js.map +1 -1
  188. package/dist/interactive/bridge-root.d.ts +9 -0
  189. package/dist/interactive/bridge-root.d.ts.map +1 -1
  190. package/dist/interactive/bridge-root.js +11 -4
  191. package/dist/interactive/bridge-root.js.map +1 -1
  192. package/dist/interactive/chat-events.d.ts +14 -9
  193. package/dist/interactive/chat-events.d.ts.map +1 -1
  194. package/dist/interactive/chat-events.js +55 -57
  195. package/dist/interactive/chat-events.js.map +1 -1
  196. package/dist/interactive/council-outcome.d.ts +1 -0
  197. package/dist/interactive/council-outcome.d.ts.map +1 -1
  198. package/dist/interactive/council-outcome.js +33 -0
  199. package/dist/interactive/council-outcome.js.map +1 -1
  200. package/dist/interactive/draft-events.d.ts +36 -17
  201. package/dist/interactive/draft-events.d.ts.map +1 -1
  202. package/dist/interactive/draft-events.js +121 -92
  203. package/dist/interactive/draft-events.js.map +1 -1
  204. package/dist/interactive/draft-skill.d.ts +59 -0
  205. package/dist/interactive/draft-skill.d.ts.map +1 -1
  206. package/dist/interactive/draft-skill.js +233 -0
  207. package/dist/interactive/draft-skill.js.map +1 -1
  208. package/dist/interactive/edit-events.d.ts +1 -14
  209. package/dist/interactive/edit-events.d.ts.map +1 -1
  210. package/dist/interactive/edit-events.js +37 -70
  211. package/dist/interactive/edit-events.js.map +1 -1
  212. package/dist/interactive/proxy-routes.d.ts +2 -2
  213. package/dist/interactive/proxy-routes.d.ts.map +1 -1
  214. package/dist/interactive/proxy-routes.js +4 -13
  215. package/dist/interactive/proxy-routes.js.map +1 -1
  216. package/dist/interactive/ws-relay.d.ts +9 -10
  217. package/dist/interactive/ws-relay.d.ts.map +1 -1
  218. package/dist/interactive/ws-relay.js +23 -50
  219. package/dist/interactive/ws-relay.js.map +1 -1
  220. package/dist/mcp/broker.d.ts +88 -0
  221. package/dist/mcp/broker.d.ts.map +1 -0
  222. package/dist/mcp/broker.js +307 -0
  223. package/dist/mcp/broker.js.map +1 -0
  224. package/dist/mcp/call-records.d.ts +61 -0
  225. package/dist/mcp/call-records.d.ts.map +1 -0
  226. package/dist/mcp/call-records.js +135 -0
  227. package/dist/mcp/call-records.js.map +1 -0
  228. package/dist/mcp/classify.d.ts +45 -0
  229. package/dist/mcp/classify.d.ts.map +1 -0
  230. package/dist/mcp/classify.js +83 -0
  231. package/dist/mcp/classify.js.map +1 -0
  232. package/dist/mcp/discovery.d.ts +24 -0
  233. package/dist/mcp/discovery.d.ts.map +1 -0
  234. package/dist/mcp/discovery.js +78 -0
  235. package/dist/mcp/discovery.js.map +1 -0
  236. package/dist/mcp/invoke.d.ts +36 -0
  237. package/dist/mcp/invoke.d.ts.map +1 -0
  238. package/dist/mcp/invoke.js +80 -0
  239. package/dist/mcp/invoke.js.map +1 -0
  240. package/dist/mcp/policies.d.ts +110 -0
  241. package/dist/mcp/policies.d.ts.map +1 -0
  242. package/dist/mcp/policies.js +400 -0
  243. package/dist/mcp/policies.js.map +1 -0
  244. package/dist/mcp/probe.d.ts +66 -0
  245. package/dist/mcp/probe.d.ts.map +1 -0
  246. package/dist/mcp/probe.js +116 -0
  247. package/dist/mcp/probe.js.map +1 -0
  248. package/dist/mcp/redact.d.ts +34 -0
  249. package/dist/mcp/redact.d.ts.map +1 -0
  250. package/dist/mcp/redact.js +84 -0
  251. package/dist/mcp/redact.js.map +1 -0
  252. package/dist/mcp/registry-store.d.ts +69 -0
  253. package/dist/mcp/registry-store.d.ts.map +1 -0
  254. package/dist/mcp/registry-store.js +82 -0
  255. package/dist/mcp/registry-store.js.map +1 -0
  256. package/dist/mcp/registry.d.ts +147 -0
  257. package/dist/mcp/registry.d.ts.map +1 -0
  258. package/dist/mcp/registry.js +509 -0
  259. package/dist/mcp/registry.js.map +1 -0
  260. package/dist/mcp/rest.d.ts +111 -0
  261. package/dist/mcp/rest.d.ts.map +1 -0
  262. package/dist/mcp/rest.js +606 -0
  263. package/dist/mcp/rest.js.map +1 -0
  264. package/dist/mcp/secrets.d.ts +66 -0
  265. package/dist/mcp/secrets.d.ts.map +1 -0
  266. package/dist/mcp/secrets.js +180 -0
  267. package/dist/mcp/secrets.js.map +1 -0
  268. package/dist/mcp/tool-list.d.ts +25 -0
  269. package/dist/mcp/tool-list.d.ts.map +1 -0
  270. package/dist/mcp/tool-list.js +62 -0
  271. package/dist/mcp/tool-list.js.map +1 -0
  272. package/dist/mcp/upstream-error.d.ts +12 -0
  273. package/dist/mcp/upstream-error.d.ts.map +1 -0
  274. package/dist/mcp/upstream-error.js +14 -0
  275. package/dist/mcp/upstream-error.js.map +1 -0
  276. package/dist/mcp/usage.d.ts +31 -0
  277. package/dist/mcp/usage.d.ts.map +1 -0
  278. package/dist/mcp/usage.js +159 -0
  279. package/dist/mcp/usage.js.map +1 -0
  280. package/dist/presets/routes.d.ts +53 -0
  281. package/dist/presets/routes.d.ts.map +1 -0
  282. package/dist/presets/routes.js +115 -0
  283. package/dist/presets/routes.js.map +1 -0
  284. package/dist/projects/activity.d.ts +6 -18
  285. package/dist/projects/activity.d.ts.map +1 -1
  286. package/dist/projects/activity.js +17 -60
  287. package/dist/projects/activity.js.map +1 -1
  288. package/dist/projects/events.d.ts +6 -5
  289. package/dist/projects/events.d.ts.map +1 -1
  290. package/dist/projects/events.js +33 -58
  291. package/dist/projects/events.js.map +1 -1
  292. package/dist/projects/routes.d.ts.map +1 -1
  293. package/dist/projects/routes.js +19 -0
  294. package/dist/projects/routes.js.map +1 -1
  295. package/dist/projects/state-home-preflight.d.ts.map +1 -1
  296. package/dist/projects/state-home-preflight.js +7 -0
  297. package/dist/projects/state-home-preflight.js.map +1 -1
  298. package/dist/projects/state-home-registry.d.ts.map +1 -1
  299. package/dist/projects/state-home-registry.js +9 -1
  300. package/dist/projects/state-home-registry.js.map +1 -1
  301. package/dist/qe/acceptance.d.ts +28 -28
  302. package/dist/qe/acceptance.d.ts.map +1 -1
  303. package/dist/qe/acceptance.js +60 -61
  304. package/dist/qe/acceptance.js.map +1 -1
  305. package/dist/qe/test-sets.d.ts +3 -5
  306. package/dist/qe/test-sets.d.ts.map +1 -1
  307. package/dist/qe/test-sets.js +5 -7
  308. package/dist/qe/test-sets.js.map +1 -1
  309. package/dist/skills/installed.d.ts +51 -0
  310. package/dist/skills/installed.d.ts.map +1 -0
  311. package/dist/skills/installed.js +46 -0
  312. package/dist/skills/installed.js.map +1 -0
  313. package/dist/standing-orders/evaluator.d.ts +113 -0
  314. package/dist/standing-orders/evaluator.d.ts.map +1 -0
  315. package/dist/standing-orders/evaluator.js +225 -0
  316. package/dist/standing-orders/evaluator.js.map +1 -0
  317. package/dist/standing-orders/history.d.ts +58 -0
  318. package/dist/standing-orders/history.d.ts.map +1 -0
  319. package/dist/standing-orders/history.js +117 -0
  320. package/dist/standing-orders/history.js.map +1 -0
  321. package/dist/standing-orders/parse.d.ts +19 -0
  322. package/dist/standing-orders/parse.d.ts.map +1 -0
  323. package/dist/standing-orders/parse.js +75 -0
  324. package/dist/standing-orders/parse.js.map +1 -0
  325. package/dist/standing-orders/routes.d.ts +180 -0
  326. package/dist/standing-orders/routes.d.ts.map +1 -0
  327. package/dist/standing-orders/routes.js +95 -0
  328. package/dist/standing-orders/routes.js.map +1 -0
  329. package/dist/standing-orders/store.d.ts +154 -0
  330. package/dist/standing-orders/store.d.ts.map +1 -0
  331. package/dist/standing-orders/store.js +189 -0
  332. package/dist/standing-orders/store.js.map +1 -0
  333. package/dist/studio/assets/index-CvV5qITf.css +32 -0
  334. package/dist/studio/assets/index-jCOy6Ns7.js +568 -0
  335. package/dist/studio/index.html +3 -3
  336. package/dist/studio/testid-inventory.json +3274 -894
  337. package/dist/team/key.d.ts +5 -0
  338. package/dist/team/key.d.ts.map +1 -0
  339. package/dist/team/key.js +21 -0
  340. package/dist/team/key.js.map +1 -0
  341. package/dist/team/routes.d.ts +196 -0
  342. package/dist/team/routes.d.ts.map +1 -0
  343. package/dist/team/routes.js +274 -0
  344. package/dist/team/routes.js.map +1 -0
  345. package/dist/team/ws-relay.d.ts +39 -0
  346. package/dist/team/ws-relay.d.ts.map +1 -0
  347. package/dist/team/ws-relay.js +63 -0
  348. package/dist/team/ws-relay.js.map +1 -0
  349. package/endpoint-manifest.json +502 -6
  350. package/package.json +6 -6
  351. package/dist/interactive/demo-events.d.ts +0 -367
  352. package/dist/interactive/demo-events.d.ts.map +0 -1
  353. package/dist/interactive/demo-events.js +0 -1293
  354. package/dist/interactive/demo-events.js.map +0 -1
  355. package/dist/qe/gate-events.d.ts +0 -113
  356. package/dist/qe/gate-events.d.ts.map +0 -1
  357. package/dist/qe/gate-events.js +0 -172
  358. package/dist/qe/gate-events.js.map +0 -1
  359. package/dist/studio/assets/index-Bg7Z3BR-.css +0 -32
  360. package/dist/studio/assets/index-BvOFMjf9.js +0 -554
@@ -1,3 +1,5 @@
1
+ import { busSeamsOffWarning, busUnavailableWarning } from '../core/bus-notice.js';
2
+ import { PlanSchema, toLaunchPlan } from './plan-schema.js';
1
3
  import { z } from 'zod';
2
4
  import { listRequirements, getRequirement, patchRequirement } from './requirements.js';
3
5
  import { randomUUID } from 'node:crypto';
@@ -5,12 +7,12 @@ import { readFileSync, existsSync } from 'node:fs';
5
7
  import { promises as fsp } from 'node:fs';
6
8
  import { isAbsolute, join, relative, resolve } from 'node:path';
7
9
  import { fileURLToPath } from 'node:url';
8
- import { CampaignsUnsupportedError, ChatUnsupportedError, CoreAdapter, ElicitationUnsupportedError, SteeringUnsupportedError, humanGatePhaseIds, settingsFilePath } from '../core/adapter.js';
10
+ import { CampaignsUnsupportedError, ChatUnsupportedError, CoreAdapter, ElicitationUnsupportedError, GovernanceReplayUnsupportedError, PlanLaunchUnsupportedError, SteeringUnsupportedError, humanGatePhaseIds, settingsFilePath } from '../core/adapter.js';
9
11
  import { codeGraphDb, codeGraphErrorStatus, requirementsGraph } from '../core/repoPaths.js';
10
12
  import { resolveCursorUnit } from '../core/cursor.js';
11
13
  import { detectRefusal } from './gate-cache.js';
12
- import { QeGateCache } from '../qe/gate-events.js';
13
- import { buildAcceptanceView, resolveRunWorkflow } from '../qe/acceptance.js';
14
+ import { acceptanceRequirementOf, buildAcceptanceView } from '../qe/acceptance.js';
15
+ import { runIdentityOf, wireIdentity } from '../core/run-identity.js';
14
16
  import { buildEvidenceBundle, coreUnitId, evidenceFilename } from './evidence.js';
15
17
  import { outputUnavailableReason, resolveUnit, unitKeysFor } from './unit-output.js';
16
18
  import { execCapped, ExecOutputTooLarge } from '../core/exec.js';
@@ -28,12 +30,18 @@ import { listInteractiveDocs } from '../interactive/docs-index.js';
28
30
  import { TestSetIndex } from '../qe/test-sets.js';
29
31
  import { estateExe, parseEstateTotals, resolveProjectGraphBinding } from '../projects/graph.js';
30
32
  import { registerProjectRoutes } from '../projects/routes.js';
33
+ import { registerPresetRoutes } from '../presets/routes.js';
34
+ import { registerTeamRoutes } from '../team/routes.js';
31
35
  import { registerCampaignRoutes } from '../campaigns/routes.js';
32
36
  import { registerGovernanceWikiRoutes } from './governance-wiki.js';
33
37
  import { DEFAULT_STEERING_TYPE, STEERING_TYPE_VALUES, STEERING_TYPES, registerGovernanceSteeringRoutes, } from './governance-steering.js';
34
38
  import { isSteeringAuthorRun, landSteeringProposal } from './steering-landing.js';
39
+ import { ApproveEditSchema, approveEdited, captureLaunchRoots, editInFlight, FileProposalSchema, registerCaptureRoutes } from './capture.js';
35
40
  import { registerTestingRoutes } from './testing.js';
41
+ import { DEMO_PRESET, demoLaunchRoots, registerDemoRoutes } from './demo.js';
36
42
  import { registerSkillsRoutes } from './skills.js';
43
+ import { registerMcpRoutes } from './mcp.js';
44
+ import { McpPolicies } from '../mcp/policies.js';
37
45
  import { disabledSkillsHealth } from '../skills/runtime.js';
38
46
  import { phaseSkillFindings, withPhaseSkillGaps } from '../skills/phase-skill-gaps.js';
39
47
  import { STATE_HOME_BLOCKER_CODE, STATE_HOME_REMEDY, isEngineStateHomeRefusal, stateHomeBlockerBody, } from '../projects/state-home-preflight.js';
@@ -41,22 +49,27 @@ import { BASE_SKILL_POLICIES, BASE_SKILL_REF_SHAPE, baseSkillRemedy } from '../s
41
49
  import { noEligibleSeatBody, parseNoEligibleSeat } from '../core/engine-roster.js';
42
50
  import { ProjectSettingsStore } from '../projects/settings.js';
43
51
  import { boundOrigin, InteractiveBridgePool } from '../interactive/bridge-pool.js';
44
- import { composeDeliverText, factsFromRun, framedDeliverText, runUrlFor } from '../core/deliver-text.js';
45
- import { resolvePullRequest as resolvePullRequestViaGh } from '../core/deliver.js';
52
+ import { composeDeliverText, configuredPublicOrigin, extractFollowUps, factsFromRun, FOLLOW_UPS_MAX, framedDeliverText, runUrlFor, } from '../core/deliver-text.js';
53
+ import { deliverRepoFor, deliverTargetView, isGitHubLogin, readDeliverOriginUrl, resolvePullRequest as resolvePullRequestViaGh, } from '../core/deliver.js';
46
54
  import { registerInteractiveProxy } from '../interactive/proxy-routes.js';
47
55
  import { registerInteractiveDocDelete } from '../interactive/doc-delete-routes.js';
48
56
  import { registerInteractiveDocList } from '../interactive/doc-list-routes.js';
49
57
  import { MembershipIndex } from '../projects/membership-index.js';
50
58
  import { MEMBERSHIP_ATTACHED, membershipAttachedKey } from '../projects/events.js';
59
+ import { resolveLinkedIssues } from '../core/linked-issues.js';
51
60
  import { AuditLog } from './audit.js';
52
61
  import { AcpFoldCache, EngineVersionCache, eventsDirOf, installedPackageVersion, listStoreFiles, readStudioBundleVersion, } from './diagnostics.js';
53
62
  import { GovernanceDiagnostics } from './governance-health.js';
54
63
  import { legacyHomeOutboxPath } from '../core/governance-store.js';
64
+ import { UsageError, replayOutboxInto } from '../cli/governance.js';
65
+ import { foldDeadletters } from './governance-health.js';
55
66
  import { RetryIndex } from './retry-index.js';
67
+ import { SEAT_RECORD_DEFAULT_DAYS, SEAT_RECORD_MAX_DAYS, seatRecord } from './seat-record.js';
68
+ import { DeliveryFreeze, frozenRefusal, isDeliverUnit, registerDeliveryFreezeRoutes } from './delivery-freeze.js';
56
69
  import { GroupIndex } from './group-index.js';
57
70
  import { RunTimingIndex, recordRunLaunched } from './run-timing-index.js';
58
71
  import { GuidanceIndex } from './guidance-index.js';
59
- import { DeliveryIndex, gitRunBranchIsEmpty, gitWorktreeIsClean, isDeliverConflictStranded, prUrlFrom, canDeliverResolver, } from './delivery-index.js';
72
+ import { DeliveryIndex, gitRunBranchIsEmpty, gitWorktreeIsClean, isDeliverConflictStranded, deliveryRecordFrom, pushedState, canDeliverResolver, } from './delivery-index.js';
60
73
  import { DeliveryDerivationCache } from './delivery-cache.js';
61
74
  import { gitReprovisionWorktree, runDeliverScript, } from './post-hoc-deliver.js';
62
75
  import { LOCAL_ACTOR } from './auth.js';
@@ -65,6 +78,10 @@ import { LOCAL_ACTOR } from './auth.js';
65
78
  export { API_PREFIX } from './api-prefix.js';
66
79
  import { API_PREFIX } from './api-prefix.js';
67
80
  const V = API_PREFIX;
81
+ /** How long `GET /health` waits on the engine's actor before answering `ping: "busy"` (crew#471). */
82
+ export const HEALTH_PING_WAIT_MS = 1_000;
83
+ /** `HealthResponse.ping` while the engine's actor is occupied: the daemon serves, the engine is busy. */
84
+ export const ENGINE_BUSY_PING = 'busy';
68
85
  // Daemon version reported by /health — read from package.json so it never drifts
69
86
  // from the shipped version across releases. Resolves the package root from the
70
87
  // compiled module location (dist/api/routes.js → ../../package.json) and works the
@@ -306,6 +323,12 @@ const RegisterRepoSchema = z
306
323
  // defaults to ~/.wicked/repos/<name>.
307
324
  rootPath: z.string().optional(),
308
325
  gitUrl: z.string().optional(),
326
+ /** crew#496: the project the register form had selected — the onboarding run is filed there
327
+ * (membership `crew.run`), not left unfiled while the repo joins the project. */
328
+ projectId: z.string().min(1).max(128).optional(),
329
+ /** crew#496 / crew#632: the launch surface and caller, recorded on the run's launch entry. */
330
+ channel: z.enum(['studio', 'cli', 'api']).optional(),
331
+ actor: z.string().min(1).max(256).optional(),
309
332
  })
310
333
  .strict()
311
334
  .refine((d) => {
@@ -315,6 +338,14 @@ const RegisterRepoSchema = z
315
338
  // or rootPath alone (register existing local repo) — all valid.
316
339
  return hasRemote || hasLocal;
317
340
  }, { message: 'Provide gitUrl (remote clone) or rootPath (local registration), or both.' });
341
+ /** `POST /repos/:id/onboard`'s optional body (crew#496): where to file the run, and who asked. */
342
+ const OnboardBodySchema = z
343
+ .object({
344
+ projectId: z.string().min(1).max(128).optional(),
345
+ channel: z.enum(['studio', 'cli', 'api']).optional(),
346
+ actor: z.string().min(1).max(256).optional(),
347
+ })
348
+ .strict();
318
349
  /**
319
350
  * The launch body. Every field but `problem` is optional, which is what made stripping dangerous:
320
351
  * omitting one is a legitimate request that gets an engine default, so a MISSPELLED one was
@@ -331,6 +362,7 @@ const RegisterRepoSchema = z
331
362
  // The request-body schemas below are exported so `tests/wire-contract.test.ts` can prove, at
332
363
  // compile time, that every body the published contract (`wicked-crew-api-types`) lets a client
333
364
  // send is a body these schemas accept — the request-direction half of the drift guard (task #84).
365
+ export { PlanSchema };
334
366
  export const LaunchSchema = z.object({
335
367
  problem: z.string().min(1),
336
368
  sessionId: z.string().min(1).optional(),
@@ -339,6 +371,10 @@ export const LaunchSchema = z.object({
339
371
  humanConfirm: z.string().min(1).optional(),
340
372
  repoRef: z.string().min(1).optional(),
341
373
  workflow: z.string().min(1).optional(),
374
+ /** DES-TEAMING-002 T3 — a user-composed plan (`LaunchRunBody.plan`), forwarded to the engine,
375
+ * which publishes `plan.proposed{by:"human"}` and holds it at a `plan_approval` gate when the
376
+ * approval matrix says so. Mutually exclusive with `workflow` (refine below). */
377
+ plan: PlanSchema.optional(),
342
378
  /** DES-PROJECT-001 §2.2 — file the run into a project; membership attaches atomically with
343
379
  * the launch record. Unknown/archived ⇒ the launch fails (never a silent unfiled run). */
344
380
  projectId: z.string().min(1).optional(),
@@ -389,7 +425,10 @@ export const LaunchSchema = z.object({
389
425
  * launch (e.g. a CI job name or a studio tab id). Persisted on the `run.launched` audit entry
390
426
  * (`detail.actor`) and served on the run DTO; absent = caller omitted it. */
391
427
  actor: z.string().min(1).max(256).optional(),
392
- }).strict().refine((b) => b.deliver !== 'pr' || b.workflow !== undefined, {
428
+ }).strict().refine((b) => b.plan === undefined || b.workflow === undefined, {
429
+ message: 'plan and workflow are mutually exclusive — a launch carries a plan or names a preset, not both',
430
+ path: ['plan'],
431
+ }).refine((b) => b.plan !== undefined || b.deliver !== 'pr' || b.workflow !== undefined, {
393
432
  message: 'deliver: "pr" requires a workflow — a free-text run has no def to append the deliver phase to',
394
433
  path: ['deliver'],
395
434
  }).refine((b) => b.deliverGate === undefined || b.deliver !== 'none', {
@@ -408,19 +447,43 @@ export const LaunchSchema = z.object({
408
447
  message: 'revisesPr needs deliver: "pr" — a launch that declines delivery has no phase to push the revision with',
409
448
  path: ['revisesPr'],
410
449
  });
450
+ /** The escalation arms (wicked-core#469 / #467): approve-shaped, no `amend` / `amendScope`.
451
+ * `extend` | `targeted` | `accept_partial` answer the gate of a repo-checks floor that did not
452
+ * finish (`repo_checks_timeout`) — the engine re-runs the floor on the tree as it stands with 2×
453
+ * bounds, the targeted test set, or the unfinished checks waived; `accept_suggestion` adopts the
454
+ * evaluator's discarded, pinned edit as the creator's amendment. The ENGINE refuses each at any
455
+ * other gate (a 409 naming the gate it answers). */
456
+ export const ESCALATION_ACTIONS = ['extend', 'targeted', 'accept_partial', 'accept_suggestion'];
457
+ const isEscalationAction = (a) => a !== undefined && ESCALATION_ACTIONS.includes(a);
411
458
  /** `POST /runs/:id/gate` (api-types 0.38.0 `GateDecision`; DES-L1 PR-2). Additive arms: `action`
412
459
  * names the arm (`approve` | `request_changes` | `reject`; absent = today's two-arm mapping of
413
460
  * `approve`), `amendScope` says where an approve's `amend` lands (`cursor` = the gated unit, today's
414
461
  * behaviour | `creator` = the first creator phase at/after the cursor — an intake steer reaches the
415
462
  * phase that implements). `request_changes` sends a NOT-PASS review back to the creator with the
416
463
  * findings in context (`amend` = the operator's note). A disagreement between `action` and
417
- * `approve` is a 400 that names both. */
464
+ * `approve` is a 400 that names both. The escalation arms ({@link ESCALATION_ACTIONS}) require
465
+ * `approve: true` and take no `amend` / `amendScope` / `plan`.
466
+ *
467
+ * `amend_intent` (wicked-core#555) approves the gate AND AMENDS THE RUN'S INTENT: `amend` is
468
+ * what the acceptance list now says (required, non-empty), `approve: true`, and no `amendScope`
469
+ * — an intent amendment reaches every unit at or after the cursor, which is what makes it reach
470
+ * the LATER EVALUATOR. It is the only arm that can descope a run mid-flight. */
418
471
  export const GateSchema = z.object({
419
472
  approve: z.boolean(),
420
473
  amend: z.string().optional(),
421
- action: z.enum(['approve', 'request_changes', 'reject']).optional(),
474
+ action: z.enum(['approve', 'request_changes', 'reject', 'edit_plan', 'amend_intent', ...ESCALATION_ACTIONS]).optional(),
422
475
  amendScope: z.enum(['cursor', 'creator']).optional(),
423
- }).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'] });
476
+ /** DES-TEAMING-002 T3 — approve a `plan_approval` gate WITH AN EDIT (`GateDecision.plan`). */
477
+ plan: PlanSchema.optional(),
478
+ /** api-types 0.44.0 — the gate this decision answers (its unit `ord`). A mismatch with the run's
479
+ * open gate is a 409 `gate_changed`: the decision was made on a gate that is no longer open. */
480
+ ord: z.number().int().nonnegative().optional(),
481
+ }).strict().refine((b) => b.action === undefined
482
+ || (b.action === 'approve' || b.action === 'edit_plan' || b.action === 'amend_intent' || isEscalationAction(b.action)) === b.approve, { message: '`action` disagrees with `approve`: request_changes and reject require approve: false; approve, edit_plan, amend_intent, extend, targeted, accept_partial and accept_suggestion require approve: true', path: ['action'] }).refine(
483
+ // (wicked-core#555) An intent amendment IS its text: an empty one amends nothing, and the arm's
484
+ // scope is every unit at or after the cursor — which is what makes it reach the evaluator — so
485
+ // `amendScope` cannot narrow it. Both are 400s here rather than a 409 from the engine.
486
+ (b) => b.action !== 'amend_intent' || (b.amend !== undefined && b.amend.trim() !== ''), { message: 'amend_intent needs `amend`: what the run\'s acceptance list now says (approve without an action to proceed unamended)', path: ['amend'] }).refine((b) => b.action !== 'amend_intent' || (b.amendScope === undefined && b.plan === undefined), { message: 'amend_intent takes no amendScope or plan — an intent amendment reaches every unit at or after the cursor, which is what makes it reach the evaluator', path: ['amendScope'] }).refine((b) => !isEscalationAction(b.action) || (b.amend === undefined && b.amendScope === undefined && b.plan === undefined), { message: 'extend, targeted, accept_partial and accept_suggestion take no amend, amendScope or plan — the engine re-runs the floor or adopts the pinned edit as it stands', path: ['action'] }).refine((b) => (b.action === 'edit_plan') === (b.plan !== undefined) || (b.action === undefined && b.plan !== undefined), { message: 'an edited plan answers a plan_approval gate: `plan` needs approve: true with `action` omitted or edit_plan, and edit_plan needs `plan`', path: ['plan'] }).refine((b) => b.plan === undefined || (b.approve && b.amend === undefined && b.amendScope === undefined), { message: '`plan` is an approve WITH an edited plan: approve: true, and no amend / amendScope (the plan is the edit)', path: ['plan'] }).refine((b) => b.amendScope === undefined || b.approve, { message: '`amendScope` applies to an approve only (approve: true)', path: ['amendScope'] });
424
487
  /** `PUT /runs/:id/guidance` (DES-UX-002 §7.2, CREW-UX-7) — the durable pre-gate note body.
425
488
  * The empty string is a legal body: it CLEARS the note. The byte cap is checked in the route
426
489
  * (not zod's char-counting `max`) so the 400 names the actual limit. */
@@ -464,6 +527,14 @@ export const OpenPathSchema = z.object({
464
527
  export const RetireMemorySchema = z.object({
465
528
  scope_prefix: z.string(),
466
529
  }).strict();
530
+ /** `POST /memory/retire-item` body (studio#206): ONE memory, by its `memory_id` from `GET /memory`.
531
+ * `.strict()`: a stray `scope_prefix` is a 400, never read as a wider erase than one row. */
532
+ export const RetireMemoryItemSchema = z.object({
533
+ memory_id: z.string().min(1).max(512),
534
+ }).strict();
535
+ /** The error an estate from before wicked-estate#188 answers to `memory.erase {id}`: it knows only the
536
+ * subtree erase, so it refuses the call (deleting nothing) for want of a `scope_prefix`. */
537
+ const ESTATE_NO_ERASE_BY_ID = /scope_prefix \(non-empty\) required/i;
467
538
  /**
468
539
  * A SKIN-OWNED settings key (crew#323): one lowercase segment under the `studio.` namespace,
469
540
  * e.g. `studio.appearance`, `studio.notifications`. The daemon never interprets these values —
@@ -596,11 +667,13 @@ at) {
596
667
  'The single-seat root cause is tracked as wicked-core#563; this chat makes it visible.',
597
668
  };
598
669
  }
670
+ /** N1: why a post-hoc deliver answers 409 for a branch already on a non-GitHub origin. */
671
+ function pushedOnlyRefusal(runId, pushed) {
672
+ return (`run ${runId} is delivered as a pushed branch: ${pushed.branch} is on ${pushed.remote}, and no ` +
673
+ 'pull request can be opened there because that origin is not a GitHub host gh can resolve — ' +
674
+ 'open the pull/merge request on your forge; merge stays human');
675
+ }
599
676
  export function registerRoutes(app, adapter, gateCache, elicitationCache,
600
- // Defaulted so a caller that never arms the bus seam (tests drive this
601
- // function directly) gets the same behavior as an unarmed daemon: an empty
602
- // cache, `busEvent: null`, and the lazy ledger read doing all the work.
603
- qeGateEvents = new QeGateCache(),
604
677
  // Defaulted for the same reason: tests that drive this function directly get
605
678
  // the projects surface with no bus (events skipped) and a fresh index.
606
679
  projects = { bus: null, index: new MembershipIndex(), log: () => undefined },
@@ -614,6 +687,8 @@ security = { audit: AuditLog.noop(), authMode: 'off' },
614
687
  runtime = {}) {
615
688
  const { audit } = security;
616
689
  const seatHealth = runtime.seatHealth ?? new SeatHealthTracker();
690
+ const deliveryFreeze = runtime.deliveryFreeze ?? new DeliveryFreeze();
691
+ registerDeliveryFreezeRoutes(app, { freeze: deliveryFreeze, audit });
617
692
  const openWithOs = runtime.openWithOs ?? openWithSystemDefault;
618
693
  const signedIn = runtime.signedIn ?? signedInHeuristic;
619
694
  const retryIndex = runtime.retryIndex ?? new RetryIndex();
@@ -651,7 +726,7 @@ runtime = {}) {
651
726
  new DeliveryDerivationCache({
652
727
  listViews: () => adapter.sessionsDetail(),
653
728
  probes: vacuityProbes,
654
- isDelivered: (runId) => deliveryIndex.urlFor(runId) !== undefined,
729
+ isDelivered: (runId) => deliveryIndex.isDelivered(runId),
655
730
  canDeliver,
656
731
  });
657
732
  const deliverExec = runtime.deliverExec ?? runDeliverScript;
@@ -670,6 +745,32 @@ runtime = {}) {
670
745
  const repoRootOf = async (repoRef) => (await adapter.listRepos()).find((r) => r.id === repoRef)?.root_path;
671
746
  /** DES-L9: PR number → head branch, through `gh pr view` unless the runtime injected an answerer. */
672
747
  const resolvePullRequest = runtime.resolvePullRequest ?? resolvePullRequestViaGh;
748
+ /** crew#627: the intent's linked issues, read under the daemon's identity at launch. */
749
+ const readLinkedIssues = runtime.resolveLinkedIssues ?? ((p, root, ref) => resolveLinkedIssues(p, root, ref));
750
+ /** crew#550 P-7: the follow-ups the run's evaluator units flagged, read from their captured output.
751
+ * A unit whose output cannot be read contributes nothing (the section then says what WAS read);
752
+ * an adapter without transcripts (a directly-driven route set) reads as "not read" — absent. */
753
+ const evaluatorFollowUps = async (view) => {
754
+ if (typeof adapter.workOutput !== 'function')
755
+ return undefined;
756
+ const evaluators = view.units.filter((u) => u.role === 'evaluator').sort((a, b) => a.ord - b.ord);
757
+ const out = [];
758
+ for (const u of evaluators) {
759
+ let text = null;
760
+ try {
761
+ text = await adapter.workOutput(coreUnitId(view.session.id, u));
762
+ }
763
+ catch {
764
+ text = null;
765
+ }
766
+ if (text === null)
767
+ continue;
768
+ for (const x of extractFollowUps(text))
769
+ if (!out.includes(x))
770
+ out.push(x);
771
+ }
772
+ return out.slice(0, FOLLOW_UPS_MAX);
773
+ };
673
774
  // The run-DTO joins (DES-UX-001 §8.2/§8.3, DES-UX-002 §7.2): `project_id` from the membership
674
775
  // record — `null` = genuinely unfiled, so the field is ALWAYS present on served runs —
675
776
  // `retry_of` from the lineage index and `guidance` from the guidance index, each set only
@@ -717,12 +818,19 @@ runtime = {}) {
717
818
  const url = deliveryIndex.urlFor(view.session.id);
718
819
  if (url !== undefined)
719
820
  return { delivery: 'delivered', deliverUrl: url };
821
+ // N1: a branch pushed to a non-GitHub origin IS the delivery — never 'stranded'.
822
+ const pushed = deliveryIndex.pushedFor(view.session.id);
823
+ if (pushed !== undefined)
824
+ return pushedState(pushed);
720
825
  if (conflictStrand)
721
826
  return { delivery: 'stranded' };
722
827
  return deliveryCache.read(view);
723
828
  };
724
829
  const decorateRun = (view) => {
725
830
  const conflictStrand = normalizeStranded(view);
831
+ // Seam X2: what the run IS — the adapter attaches it from the engine's record; a view from a
832
+ // directly-driven adapter gets it resolved from its record here, so it is always served.
833
+ view.session.run_identity = wireIdentity(runIdentityOf(view));
726
834
  view.session.project_id = projects.index.projectOf(view.session.id) ?? null;
727
835
  // Wave 6 (F-4R2-006): the interactive document this run answered, from the seams' handoff
728
836
  // ledgers — `null` = genuinely not a document run, so the field is ALWAYS present on served runs.
@@ -769,6 +877,10 @@ runtime = {}) {
769
877
  view.session.delivery = state.delivery;
770
878
  if (state.deliverUrl !== undefined)
771
879
  view.session.deliverUrl = state.deliverUrl;
880
+ if (state.deliverBranch !== undefined)
881
+ view.session.deliverBranch = state.deliverBranch;
882
+ if (state.deliverRemote !== undefined)
883
+ view.session.deliverRemote = state.deliverRemote;
772
884
  // crew#641/#642 (item 5): chat promotion provenance — ABSENT when not a chat-promoted run.
773
885
  const chatSeatCount = runTimingIndex.chatSeatCountFor(view.session.id);
774
886
  if (chatSeatCount !== undefined)
@@ -787,13 +899,31 @@ runtime = {}) {
787
899
  // still gets the ONE actor shape via this accessor.
788
900
  const actorOf = (req) => req.actor ?? LOCAL_ACTOR;
789
901
  // Liveness — also proves the actor + event pump are up.
902
+ //
903
+ // crew#471: the engine's ping is a round trip through its single-writer actor, and a launch that
904
+ // plans many units holds that actor for minutes. Awaiting it made `/health` go dark exactly when
905
+ // an operator needed to know the daemon was up, and each waiting probe pinned a libuv worker
906
+ // thread the rest of the read path needs. So at most ONE ping is in flight (later probes join it)
907
+ // and `/health` waits for it at most HEALTH_PING_WAIT_MS: past that it answers `ping: "busy"` —
908
+ // the daemon serves, the engine is occupied.
909
+ let pingInFlight = null;
910
+ const enginePing = () => {
911
+ pingInFlight ??= adapter.ping().finally(() => {
912
+ pingInFlight = null;
913
+ });
914
+ let timer;
915
+ const busy = new Promise((resolve) => {
916
+ timer = setTimeout(() => resolve(ENGINE_BUSY_PING), HEALTH_PING_WAIT_MS);
917
+ });
918
+ return Promise.race([pingInFlight, busy]).finally(() => clearTimeout(timer));
919
+ };
790
920
  // `config.manifest` on the routes below (TH-11): the declaration channel the endpoint manifest
791
921
  // reads — type names bind to `wicked-crew-api-types` exports where one exists, structural
792
922
  // spellings where the contract has no name, statusCodes list every code the route answers on
793
923
  // purpose. Declared on the highest-traffic run-lifecycle routes first; the manifest records
794
924
  // null / [] for the rest ("where declared", never invented). See src/api/endpoint-manifest.ts.
795
925
  app.get(`${V}/health`, { config: { manifest: { statusCodes: [200] } } }, async () => {
796
- const ping = await adapter.ping();
926
+ const ping = await enginePing();
797
927
  // F-E2E-030: what this deployment can keep. A composer reads `capabilities.deliverGate`
798
928
  // before it promises "pauses at the deliver gate"; a stub-driven route set without the
799
929
  // probe honestly reports no gate.
@@ -821,6 +951,12 @@ runtime = {}) {
821
951
  ...(stateHome === null ? [] : stateHome.findings.map((f) => ({ kind: f.kind, severity: f.severity, message: f.message }))),
822
952
  ...(baseSkill?.finding ? [{ kind: baseSkill.finding.kind, severity: baseSkill.finding.severity, message: baseSkill.finding.message }] : []),
823
953
  ...phaseSkillFindings(runtime.phaseSkills?.gaps() ?? []),
954
+ // DES-TEAMING-002 T0: the daemon's bus did not open at boot, so the engine was handed none
955
+ // and runs un-teamed. A daemon-level notice (status stays ok: it serves); the per-run
956
+ // `transport:"none"` record lands with P1.
957
+ ...(adapter.busUnavailable != null ? [busUnavailableWarning(adapter.busUnavailable)] : []),
958
+ // wicked-core#631: the engine has the bus but not the calls crew reaches it through.
959
+ ...(adapter.busSeamsOff != null ? [busSeamsOffWarning(adapter.busSeamsOff)] : []),
824
960
  ];
825
961
  return {
826
962
  status: 'ok',
@@ -894,8 +1030,10 @@ runtime = {}) {
894
1030
  authMode: security.authMode,
895
1031
  }));
896
1032
  // The actor audit trail (task #88): who launched/steered/governed what.
897
- // Read-only (observer trust); newest first; `?runId=` / `?action=` / `?limit=`.
898
- app.get(`${V}/audit`, async (req, reply) => {
1033
+ // Read-only (observer trust); newest first; `?runId=` / `?action=` / `?limit=` / `?since=`.
1034
+ // `since` (unix millis, inclusive) is the lower time bound a skin's "while you were away"
1035
+ // handover reads — the trail since the operator's last visit (studio wave 2b).
1036
+ app.get(`${V}/audit`, { config: { manifest: { responseType: 'AuditPage', statusCodes: [200, 400, 500] } } }, async (req, reply) => {
899
1037
  const q = req.query;
900
1038
  const first = (v) => (Array.isArray(v) ? v[0] : v)?.trim() || undefined;
901
1039
  const runId = first(q.runId);
@@ -907,11 +1045,18 @@ runtime = {}) {
907
1045
  if (limitRaw !== undefined && (!Number.isFinite(limit) || limit < 1 || !Number.isInteger(limit))) {
908
1046
  return reply.code(400).send({ error: '`limit` must be a positive integer' });
909
1047
  }
1048
+ // Same strict parse as `limit`, but zero is a valid clock (the epoch).
1049
+ const sinceRaw = first(q.since);
1050
+ const since = sinceRaw !== undefined ? Number(sinceRaw) : undefined;
1051
+ if (sinceRaw !== undefined && (!Number.isInteger(since) || since < 0)) {
1052
+ return reply.code(400).send({ error: '`since` must be a non-negative integer (unix millis)' });
1053
+ }
910
1054
  try {
911
1055
  const entries = await audit.read({
912
1056
  ...(runId !== undefined ? { runId } : {}),
913
1057
  ...(action !== undefined ? { action } : {}),
914
1058
  ...(limit !== undefined ? { limit } : {}),
1059
+ ...(since !== undefined ? { since } : {}),
915
1060
  });
916
1061
  return { entries };
917
1062
  }
@@ -936,6 +1081,33 @@ runtime = {}) {
936
1081
  // disagree about a seat.
937
1082
  const rosterWithStanding = runtime.rosterWithStanding ?? rosterWithStandingFactory({ seatHealth, signedIn });
938
1083
  app.get(`${V}/roster`, async () => ({ roster: rosterWithStanding() }));
1084
+ // Each seat's week (studio's weekly 1:1 per agent; api-types 0.52.0): units, first pass, rework,
1085
+ // stalls, bench and cost, folded from the runs' durable event logs by `seatRecord` — nothing new
1086
+ // is stored. The daemon's remembered stall frames join the engine's, as on `/runs/:id/events`.
1087
+ app.get(`${V}/roster/record`, { config: { manifest: { responseType: 'SeatRecordResponse', statusCodes: [200, 400, 503] } } }, async (req, reply) => {
1088
+ const raw = req.query.days;
1089
+ let days = SEAT_RECORD_DEFAULT_DAYS;
1090
+ if (raw !== undefined) {
1091
+ const one = Array.isArray(raw) ? undefined : raw;
1092
+ const n = one !== undefined && /^[1-9][0-9]?$/.test(one) ? Number(one) : NaN;
1093
+ if (!(n >= 1 && n <= SEAT_RECORD_MAX_DAYS)) {
1094
+ return reply.code(400).send({ error: `Invalid ?days — expected one whole number from 1 to ${SEAT_RECORD_MAX_DAYS}, got ${JSON.stringify(raw)}` });
1095
+ }
1096
+ days = n;
1097
+ }
1098
+ const views = (await adapter.sessionsDetail()).map(decorateRun);
1099
+ const out = await seatRecord(views, async (id) => {
1100
+ const engine = await adapter.runEvents(id);
1101
+ if (engine === null)
1102
+ return null;
1103
+ const daemon = runtime.stallFrames?.(id) ?? [];
1104
+ return daemon.length === 0 ? engine : [...engine, ...daemon];
1105
+ }, { days, now: Date.now() });
1106
+ if (out === null) {
1107
+ return reply.code(503).send({ error: 'Seat records are unavailable: this wicked-core build has no event-log read binding' });
1108
+ }
1109
+ return out;
1110
+ });
939
1111
  // Open a file/folder with the OS default application (crew#273) — the studio Files tab's
940
1112
  // click-to-open. The open MUST happen daemon-side (the SPA cannot spawn a process), which is
941
1113
  // why the path is validated first: absolute, and inside one of the caller-visible roots — the
@@ -1172,33 +1344,61 @@ runtime = {}) {
1172
1344
  });
1173
1345
  // Registered repos → target-repo picker.
1174
1346
  app.get(`${V}/repos`, async () => ({ repos: await adapter.listRepos() }));
1347
+ /** Filing for an onboarding run: what `POST /runs` does for a launch, for the runs `POST /repos`
1348
+ * and `POST /repos/:id/onboard` start (crew#496). The launch entry (`run.launched`, which dates
1349
+ * the run and names who started it) is always written; with a `projectId` the run joins that
1350
+ * project. The run is LIVE by then, so a filing failure is named in the answer, never a 4xx. */
1351
+ const recordOnboardingLaunch = async (req, runId, repoId, opts) => {
1352
+ recordRunLaunched(audit, runTimingIndex, actorOf(req), runId, {
1353
+ workflow: 'onboarding',
1354
+ repoRef: repoId,
1355
+ ...(opts.projectId !== undefined ? { projectId: opts.projectId } : {}),
1356
+ ...(opts.channel !== undefined ? { channel: opts.channel } : {}),
1357
+ ...(opts.actor !== undefined ? { actor: opts.actor } : {}),
1358
+ });
1359
+ if (opts.projectId === undefined)
1360
+ return undefined;
1361
+ try {
1362
+ const { member } = await adapter.projectMemberAttach(opts.projectId, 'crew.run', runId, { onboarding: true }, actorOf(req).id);
1363
+ projects.index.set(runId, opts.projectId);
1364
+ projects.bus?.emit(MEMBERSHIP_ATTACHED, { project_id: opts.projectId, member: { kind: 'crew.run', ref: runId }, actor: actorOf(req).id }, membershipAttachedKey(opts.projectId, 'crew.run', runId, member.attached_at));
1365
+ return undefined;
1366
+ }
1367
+ catch (err) {
1368
+ req.log.warn({ runId, projectId: opts.projectId }, `onboarding run filing failed: ${message(err)}`);
1369
+ return message(err);
1370
+ }
1371
+ };
1175
1372
  app.post(`${V}/repos`, async (req, reply) => {
1176
1373
  const parsed = RegisterRepoSchema.safeParse(req.body);
1177
1374
  if (!parsed.success) {
1178
1375
  return reply.code(400).send(invalidBody(parsed.error, 'Invalid request body'));
1179
1376
  }
1180
- const { name, rootPath, gitUrl } = parsed.data;
1377
+ const { name, rootPath, gitUrl, projectId, channel, actor } = parsed.data;
1378
+ let repo;
1379
+ let runId;
1181
1380
  try {
1182
1381
  if (gitUrl) {
1183
1382
  // Remote: clone to rootPath (if provided) or default ~/.wicked/repos/<name>,
1184
1383
  // register, and launch onboarding run.
1185
- const { repoId, runId } = await adapter.cloneAndRegisterRepo(name, gitUrl, rootPath);
1384
+ const cloned = await adapter.cloneAndRegisterRepo(name, gitUrl, rootPath);
1385
+ runId = cloned.runId;
1186
1386
  const repos = await adapter.listRepos();
1187
- const repo = repos.find((r) => r.id === repoId);
1387
+ repo = repos.find((r) => r.id === cloned.repoId);
1188
1388
  if (!repo)
1189
1389
  return reply.code(500).send({ error: 'Repo registered but could not be retrieved' });
1190
- return reply.code(201).send({ repo, onboardRunId: runId });
1191
1390
  }
1192
1391
  else {
1193
1392
  // Local: register then launch onboarding run.
1194
- const repo = await adapter.registerRepo(name, rootPath);
1195
- const runId = await adapter.launchOnboardingRun(repo.id, name);
1196
- return reply.code(201).send({ repo, onboardRunId: runId });
1393
+ repo = await adapter.registerRepo(name, rootPath);
1394
+ runId = await adapter.launchOnboardingRun(repo.id, name);
1197
1395
  }
1198
1396
  }
1199
1397
  catch (err) {
1200
1398
  return reply.code(400).send({ error: message(err) });
1201
1399
  }
1400
+ const projectAttachError = await recordOnboardingLaunch(req, runId, repo.id, { projectId, channel, actor });
1401
+ return reply.code(201).send({ repo, onboardRunId: runId, ...(projectAttachError !== undefined ? { projectAttachError } : {}) });
1202
1402
  });
1203
1403
  // Return the onboarding run id for a repo (so the UI can navigate to it).
1204
1404
  app.get(`${V}/repos/:id/onboard`, async (req, reply) => {
@@ -1213,13 +1413,18 @@ runtime = {}) {
1213
1413
  const repo = repos.find((r) => r.id === id);
1214
1414
  if (!repo)
1215
1415
  return reply.code(404).send({ error: `Repo ${id} not found` });
1416
+ const body = OnboardBodySchema.safeParse(req.body ?? {});
1417
+ if (!body.success)
1418
+ return reply.code(400).send(invalidBody(body.error, 'Invalid request body'));
1419
+ let runId;
1216
1420
  try {
1217
- const runId = await adapter.launchOnboardingRun(repo.id, repo.name);
1218
- return reply.code(201).send({ runId });
1421
+ runId = await adapter.launchOnboardingRun(repo.id, repo.name);
1219
1422
  }
1220
1423
  catch (err) {
1221
1424
  return reply.code(400).send({ error: message(err) });
1222
1425
  }
1426
+ const projectAttachError = await recordOnboardingLaunch(req, runId, repo.id, body.data);
1427
+ return reply.code(201).send({ runId, ...(projectAttachError !== undefined ? { projectAttachError } : {}) });
1223
1428
  });
1224
1429
  // Launch a run (replaces POST /sessions). `clisJson` defaults to the roster;
1225
1430
  // `sessionId` is minted if the client omits it.
@@ -1227,7 +1432,7 @@ runtime = {}) {
1227
1432
  config: {
1228
1433
  manifest: {
1229
1434
  requestType: 'LaunchRunBody',
1230
- responseType: '{ runId: string }',
1435
+ responseType: 'LaunchRunResponse',
1231
1436
  // 404/409: unknown project / unknown campaignId (wicked-studio#27) /
1232
1437
  // archived-or-synthesized project + busy engine + the state-home blocker
1233
1438
  // (`state_home_unregistered`) + a roster with no eligible seat (`no_eligible_seat`,
@@ -1244,6 +1449,13 @@ runtime = {}) {
1244
1449
  return reply.code(400).send(invalidBody(parsed.error, 'Invalid request body'));
1245
1450
  }
1246
1451
  const b = parsed.data;
1452
+ // The demo preset launches only from POST /projects/:id/demo (api/demo.ts), which mints the run's
1453
+ // demo root and brief first: launched here directly it would have neither.
1454
+ if (b.workflow === DEMO_PRESET && (b.sessionId === undefined || !demoLaunchRoots.has(b.sessionId))) {
1455
+ return reply.code(400).send({
1456
+ error: 'the demo preset launches from POST /projects/:id/demo, which makes its demo root and brief; POST /runs does not take it directly',
1457
+ });
1458
+ }
1247
1459
  // wicked-core#411 / crew#497: the state-home blocker, judged BEFORE anything is resolved or
1248
1460
  // committed. While the handed skills snapshot derives a state home with an entry the worker
1249
1461
  // Read fence cannot classify, the engine refuses this launch at intake (and an engine before
@@ -1256,6 +1468,10 @@ runtime = {}) {
1256
1468
  return reply.code(409).send(stateHomeBlockerBody(stateHome));
1257
1469
  }
1258
1470
  }
1471
+ // crew#645: an expired login reads signed out BEFORE routing — wait (bounded) for every seat's
1472
+ // missing or stale login check, so the standing below is the seats' own answer.
1473
+ if (b.clisJson === undefined)
1474
+ await rosterWithStanding.ready?.();
1259
1475
  const input = {
1260
1476
  problem: b.problem,
1261
1477
  sessionId: b.sessionId ?? randomUUID(),
@@ -1264,6 +1480,15 @@ runtime = {}) {
1264
1480
  // (`core/engine-roster.ts`), so a signed-out seat is never convened, never a judge.
1265
1481
  clisJson: b.clisJson ?? JSON.stringify(rosterWithStanding()),
1266
1482
  };
1483
+ // A capture launch (api/capture.ts) reads its materials from a per-run inbox outside every
1484
+ // sandbox: the capture route registered it under the run id it minted.
1485
+ const captureRoot = captureLaunchRoots.get(input.sessionId);
1486
+ if (captureRoot !== undefined)
1487
+ input.extraWriteRoots = [captureRoot];
1488
+ // A demo launch (api/demo.ts) writes every file into the demo root the demo route minted for it.
1489
+ const demoRoot = demoLaunchRoots.get(input.sessionId);
1490
+ if (demoRoot !== undefined)
1491
+ input.extraWriteRoots = [demoRoot];
1267
1492
  if (b.entityMode !== undefined)
1268
1493
  input.entityMode = b.entityMode;
1269
1494
  if (b.humanConfirm !== undefined)
@@ -1277,6 +1502,10 @@ runtime = {}) {
1277
1502
  input.repoRef = b.repoRef;
1278
1503
  if (b.workflow !== undefined)
1279
1504
  input.workflow = b.workflow;
1505
+ // DES-TEAMING-002 T3: the plan is the ENGINE's to propose, score, floor and gate — crew
1506
+ // forwards the command and publishes nothing (§4.0).
1507
+ if (b.plan !== undefined)
1508
+ input.plan = toLaunchPlan(b.plan);
1280
1509
  if (b.projectId !== undefined) {
1281
1510
  input.projectId = b.projectId;
1282
1511
  // A project is a CONTEXT (crew#326): a run filed into one should see the project's whole
@@ -1395,6 +1624,26 @@ runtime = {}) {
1395
1624
  throw err;
1396
1625
  }
1397
1626
  }
1627
+ // crew#627: a workflow run's intent may name its work only by issue ("fix #541"), and the
1628
+ // worker cannot read GitHub. The daemon reads each linked issue now and appends its text to the
1629
+ // problem, inside a marked block the deliver text takes back out. Workflow launches only: the
1630
+ // free-text planner turns sentences into units, so issue text there would become work units.
1631
+ let linkedIssues;
1632
+ if (b.workflow !== undefined) {
1633
+ // Best-effort by contract: reading issues never fails the launch it decorates. A resolver
1634
+ // fault (the repo list unreadable, say) launches the run on its intent alone and is logged.
1635
+ try {
1636
+ const root = b.repoRef !== undefined ? await repoRootOf(b.repoRef) : undefined;
1637
+ const linked = await readLinkedIssues(b.problem, root, b.repoRef);
1638
+ if (linked.block !== null) {
1639
+ input.problem = `${b.problem.trimEnd()}\n\n${linked.block}`;
1640
+ linkedIssues = linked.issues;
1641
+ }
1642
+ }
1643
+ catch (err) {
1644
+ req.log.warn(`linked-issue resolution failed; launching on the intent alone: ${message(err)}`);
1645
+ }
1646
+ }
1398
1647
  try {
1399
1648
  const runId = await adapter.launchRun(input);
1400
1649
  // crew#641/#642 (item 5): when a run is promoted from a chat, record how many seats
@@ -1452,6 +1701,8 @@ runtime = {}) {
1452
1701
  // crew#632: launch surface and caller identity — human-readable provenance on the audit trail.
1453
1702
  ...(b.channel !== undefined ? { channel: b.channel } : {}),
1454
1703
  ...(b.actor !== undefined ? { actor: b.actor } : {}),
1704
+ // crew#627: which linked issues the daemon read into the problem, and which it could not.
1705
+ ...(linkedIssues !== undefined ? { linkedIssues } : {}),
1455
1706
  });
1456
1707
  // Stamp the index live so the field is served without waiting for a daemon restart.
1457
1708
  if (chatSeatCount !== undefined && chatGrounded !== undefined) {
@@ -1479,10 +1730,15 @@ runtime = {}) {
1479
1730
  // is always reproducible even if the chat is idle-reclaimed before the run finishes.
1480
1731
  if (b.chatId !== undefined)
1481
1732
  runtime.linkChatRun?.(b.chatId, runId);
1482
- return reply.code(201).send({ runId });
1733
+ return reply.code(201).send({ runId, ...(linkedIssues !== undefined ? { linkedIssues } : {}) });
1483
1734
  }
1484
1735
  catch (err) {
1485
1736
  const msg = message(err);
1737
+ // DES-TEAMING-002 T3: a plan on an engine without the plan approval gate is an "upgrade the
1738
+ // engine" 501 — never a free-text run launched in its place.
1739
+ if (err instanceof PlanLaunchUnsupportedError) {
1740
+ return reply.code(501).send({ error: msg });
1741
+ }
1486
1742
  // The engine's own intake refusal (wicked-core#411 `StateHomeConfigError`) is a configuration
1487
1743
  // error, not a malformed request: the same typed 409 the pre-check above answers, over a fresh
1488
1744
  // survey — the pre-check saw a clean state home (or an addon that could not classify), the
@@ -1645,8 +1901,6 @@ runtime = {}) {
1645
1901
  return reply.code(404).send({ error: 'Run not found' });
1646
1902
  return { run: decorateRun(run) };
1647
1903
  });
1648
- /** The full workflow registry, or `[]` on a directly-driven route set whose fake adapter has none. */
1649
- const listWorkflowsSafe = () => typeof adapter.listWorkflows === 'function' ? adapter.listWorkflows() : [];
1650
1904
  // ── Deliver text (crew#524 / F-3R2-014) — the PR title + body a run's delivery carries ──
1651
1905
  // `gh pr create --fill` gave wicked-studio#249 a mid-word title and an EMPTY body. The deliver
1652
1906
  // script now asks this route for the text composed from the PERSISTED RUN RECORD: the intent,
@@ -1661,12 +1915,12 @@ runtime = {}) {
1661
1915
  const run = views.find((v) => v.session.id === id);
1662
1916
  if (!run)
1663
1917
  return reply.code(404).send({ error: 'Run not found' });
1664
- const origin = boundOrigin(app.server.address());
1665
- // The workflow DEFINITION (a user-registered workflow's view carries the engine instance id).
1666
- const def = resolveRunWorkflow(run, listWorkflowsSafe());
1667
- const text = composeDeliverText(factsFromRun(decorateRun(run), runUrlFor(origin, id), {
1668
- workflowId: def?.id ?? null,
1918
+ const followUps = await evaluatorFollowUps(run);
1919
+ // The run's preset or workflow NAME, as the engine recorded it (seam X2).
1920
+ const text = composeDeliverText(factsFromRun(decorateRun(run), runUrlFor(configuredPublicOrigin(), id), {
1921
+ workflowId: runIdentityOf(run).name,
1669
1922
  revisesPr: retryIndex.revisesPrFor(id) ?? null,
1923
+ ...(followUps !== undefined ? { followUps } : {}),
1670
1924
  }));
1671
1925
  return reply.type('text/plain; charset=utf-8').send(framedDeliverText(text));
1672
1926
  });
@@ -1727,10 +1981,19 @@ runtime = {}) {
1727
1981
  const existing = deliveryIndex.urlFor(id);
1728
1982
  if (existing !== undefined)
1729
1983
  return { prUrl: existing };
1984
+ // N1: a run whose branch is already on a non-GitHub origin has nothing left to lift, and no
1985
+ // pull request can be opened from here — say so instead of pushing the branch again.
1986
+ const pushedAlready = deliveryIndex.pushedFor(id);
1987
+ if (pushedAlready !== undefined) {
1988
+ return reply.code(409).send({ error: pushedOnlyRefusal(id, pushedAlready) });
1989
+ }
1730
1990
  const views = await adapter.sessionsDetail();
1731
1991
  const run = views.find((v) => v.session.id === id);
1732
1992
  if (!run)
1733
1993
  return reply.code(404).send({ error: 'Run not found' });
1994
+ // Freeze deliveries (idea 15): nothing pushes while the switch is on.
1995
+ if (deliveryFreeze.state().frozen)
1996
+ return reply.code(409).send(frozenRefusal(deliveryFreeze.state()));
1734
1997
  // crew#418/#432: a run stranded by a recoverable deliver lift failure reads `failed` from the
1735
1998
  // engine but IS liftable post-hoc — normalize its status to `completed` so this route
1736
1999
  // accepts it, exactly as the run wire and the resume route see it.
@@ -1794,7 +2057,7 @@ runtime = {}) {
1794
2057
  // crew#524: the post-hoc lift composes its PR text from the run record it already
1795
2058
  // holds (the fallback), and names this daemon so the script can re-ask at delivery.
1796
2059
  const origin = boundOrigin(app.server.address());
1797
- const def = resolveRunWorkflow(run, listWorkflowsSafe());
2060
+ const workflowName = runIdentityOf(run).name;
1798
2061
  const revising = retryIndex.revisesPrFor(id);
1799
2062
  let revisesPr = null;
1800
2063
  if (revising !== undefined) {
@@ -1808,14 +2071,20 @@ runtime = {}) {
1808
2071
  }
1809
2072
  revisesPr = { number: revising.number, headRef: again.pr.headRef, url: again.pr.url };
1810
2073
  }
2074
+ const followUps = await evaluatorFollowUps(run);
1811
2075
  result = await deliverExec(workdir, s.problem ?? undefined, {
1812
2076
  runId: id,
1813
2077
  apiOrigin: origin,
1814
- facts: factsFromRun(run, runUrlFor(origin, id), {
1815
- workflowId: def?.id ?? null,
2078
+ facts: factsFromRun(run, runUrlFor(configuredPublicOrigin(), id), {
2079
+ workflowId: workflowName,
1816
2080
  revisesPr: revisesPr === null ? null : { number: revisesPr.number, url: revisesPr.url },
2081
+ ...(followUps !== undefined ? { followUps } : {}),
1817
2082
  }),
1818
2083
  revisesPr,
2084
+ // R1 (Copilot on crew#736): the post-hoc delivery binds `gh pr create --repo` to the
2085
+ // origin's GitHub repository too, read now from the worktree (it shares its repo's
2086
+ // remotes), so the PR opens where origin points rather than where gh's default does.
2087
+ originUrl: await readDeliverOriginUrl(workdir),
1819
2088
  });
1820
2089
  }
1821
2090
  finally {
@@ -1857,7 +2126,19 @@ runtime = {}) {
1857
2126
  error: `deliver failed (exit ${result.status}): ${deliverErrorTail(result.output)}`,
1858
2127
  });
1859
2128
  }
1860
- const url = prUrlFrom(result.output);
2129
+ // The SAME parser the in-run record uses: the script's push-only verdict outranks a URL a
2130
+ // remote hook echoed earlier in the transcript (Copilot on crew#734).
2131
+ const record = deliveryRecordFrom(result.output);
2132
+ const url = record !== null && 'url' in record ? record.url : null;
2133
+ const pushedOnly = record !== null && 'pushed' in record ? record.pushed : null;
2134
+ if (pushedOnly !== null) {
2135
+ // N1: the script pushed the branch and gh could resolve no GitHub repository for the
2136
+ // origin. That IS a delivery — record it, so the run reads `delivery: 'pushed'` — but there
2137
+ // is no PR URL to answer with, so the reply says what happened instead of a 200 `prUrl`.
2138
+ audit.record('run.delivered', actorOf(req), { runId: id, detail: { pushed: pushedOnly, via: 'post-hoc' } });
2139
+ deliveryIndex.setPushed(id, pushedOnly);
2140
+ return reply.code(409).send({ error: pushedOnlyRefusal(id, pushedOnly) });
2141
+ }
1861
2142
  if (url === null) {
1862
2143
  // Exit 0 with no URL should be unreachable (the script re-derives its own success),
1863
2144
  // but a delivery nothing can be pointed at is never recorded (crew#317).
@@ -2497,11 +2778,11 @@ runtime = {}) {
2497
2778
  const run = views.find((v) => v.session.id === id);
2498
2779
  if (!run)
2499
2780
  return reply.code(404).send({ error: 'Run not found' });
2500
- // `sessionsDetail()` patches workflow_id back to the definition name for
2501
- // BUILT-INS; runs of user-registered workflows still carry the instance id,
2502
- // so resolve by phase sequence over the full registry. A free-text run
2503
- // resolves to no workflow, which reads as "declares no requirement".
2504
- const workflow = resolveRunWorkflow(run, adapter.listWorkflows());
2781
+ // What the run must prove, from what it CONTAINS (seam X2): a plan or preset run's steps whose
2782
+ // catalog entry re-verifies evidence, a registered workflow's def, nothing for free text — and
2783
+ // an unknown run (or one the daemon cannot read) is declared and denied with the reason.
2784
+ const verifiedCatalog = typeof adapter.verifiedEvidenceCatalog === 'function' ? await adapter.verifiedEvidenceCatalog() : null;
2785
+ const requirement = acceptanceRequirementOf(run, adapter.listWorkflows(), verifiedCatalog);
2505
2786
  let repo = null;
2506
2787
  if (run.session.repo_ref !== null) {
2507
2788
  const repos = await adapter.listRepos();
@@ -2510,8 +2791,7 @@ runtime = {}) {
2510
2791
  return buildAcceptanceView({
2511
2792
  runId: id,
2512
2793
  repo,
2513
- workflow,
2514
- gateEvents: qeGateEvents,
2794
+ requirement,
2515
2795
  ...(qeRunId !== undefined && qeRunId !== '' ? { qeRunId } : {}),
2516
2796
  // AW-14 (arch-R13a + R16): the conformance section reads the same wires the standalone
2517
2797
  // `/governance/claims` and `/runs/:id/events` routes serve, but run-scoped and resolved
@@ -2521,55 +2801,122 @@ runtime = {}) {
2521
2801
  events: (rid) => adapter.runEvents(rid),
2522
2802
  });
2523
2803
  });
2524
- // The steering gate (§11.1). approve+amend = approve-with-steer; approve:false = reject (cancels).
2525
- app.post(`${V}/runs/:id/gate`, {
2526
- config: {
2527
- manifest: {
2528
- requestType: 'GateDecision',
2529
- responseType: '{ status: SessionStatus; landing?: SteeringLandingResult }',
2530
- // 409 twice over: a run not awaiting a human gate, and an engine refusal at confirm.
2531
- statusCodes: [200, 400, 404, 409],
2532
- },
2533
- },
2534
- }, async (req, reply) => {
2535
- const { id } = req.params;
2536
- const parsed = GateSchema.safeParse(req.body);
2804
+ /**
2805
+ * The run's OPEN gate, resolved the way `GET /runs/:id/gate` serves it: the cache, then the
2806
+ * engine's durable `interaction_requests` row (adopted into the cache), then the event-log replay.
2807
+ * The caller has already checked `awaiting_human`. `null` = the log records no open gate;
2808
+ * `'no-log'` = this build cannot read the log, so nothing can be said.
2809
+ */
2810
+ async function resolveOpenGate(id) {
2811
+ const cached = gateCache.get(id);
2812
+ if (cached)
2813
+ return cached;
2814
+ const durableRows = typeof adapter.interactionRequests === 'function'
2815
+ ? await adapter.interactionRequests(id, 'open')
2816
+ : null;
2817
+ const durableGate = durableRows?.find((r) => r.kind === 'gate');
2818
+ if (durableGate !== undefined) {
2819
+ // This path builds the entry inline (not through `fold`), so it must run the SAME refusal
2820
+ // detection (issue #419) — otherwise a gate served from the durable row after a restart would
2821
+ // silently drop the warning that the live/replay paths carry.
2822
+ const refusal = detectRefusal(durableGate.prompt);
2823
+ const entry = {
2824
+ ord: durableGate.ord ?? 0,
2825
+ prompt: durableGate.prompt,
2826
+ lifecycle: 'open',
2827
+ receivedAt: new Date(durableGate.created_at).toISOString(),
2828
+ ...(refusal !== undefined ? { refusal } : {}),
2829
+ };
2830
+ gateCache.adopt(id, entry);
2831
+ return entry;
2832
+ }
2833
+ const events = await adapter.runEvents(id);
2834
+ if (events === null)
2835
+ return 'no-log';
2836
+ return gateCache.rebuild(id, events) ?? null;
2837
+ }
2838
+ /**
2839
+ * THE gate decision path. `POST /runs/:id/gate` answers with it, and so does a standing order
2840
+ * (behaviour 10) — the same awaiting check, the same ord check and 409 `gate_changed` /
2841
+ * `gate_unknown`, the same `confirmGate`, the same `gate.decided` audit. There is no side path:
2842
+ * an order is just another actor, named on the audit line (`extraDetail.standingOrder`).
2843
+ */
2844
+ async function decideGate(id, decision, actor, extraDetail = {}) {
2845
+ const parsed = GateSchema.safeParse(decision);
2537
2846
  if (!parsed.success) {
2538
- return reply.code(400).send(invalidBody(parsed.error, 'Invalid request body'));
2847
+ return { code: 400, body: invalidBody(parsed.error, 'Invalid request body') };
2539
2848
  }
2540
2849
  const views = await adapter.sessionsDetail();
2541
2850
  const run = views.find((v) => v.session.id === id);
2542
2851
  if (!run)
2543
- return reply.code(404).send({ error: 'Run not found' });
2852
+ return { code: 404, body: { error: 'Run not found' } };
2544
2853
  if (run.session.status !== 'awaiting_human') {
2545
- return reply
2546
- .code(409)
2547
- .send({ error: `Run is not awaiting a human gate (status: ${run.session.status})` });
2854
+ return { code: 409, body: { error: `Run is not awaiting a human gate (status: ${run.session.status})` } };
2855
+ }
2856
+ // api-types 0.44.0: a decision that names its gate answers THAT gate or nothing. When the
2857
+ // daemon cannot tell which gate is open (no cache, no durable row, no log binding, or a log
2858
+ // recording no open gate) it cannot confirm the named one either, so it refuses — the skin
2859
+ // re-reads the gate and the person decides again. A request without `ord` is unchanged.
2860
+ if (parsed.data.ord !== undefined) {
2861
+ const open = await resolveOpenGate(id);
2862
+ if (open === null || open === 'no-log') {
2863
+ return { code: 409, body: {
2864
+ error: `Gate unknown: this decision names the gate before unit ${parsed.data.ord}, but the daemon cannot `
2865
+ + 'tell which gate is open on this run — refresh and decide again.',
2866
+ code: 'gate_unknown',
2867
+ } };
2868
+ }
2869
+ if (open.ord !== parsed.data.ord) {
2870
+ return { code: 409, body: {
2871
+ error: `Gate changed: this decision was made on the gate before unit ${parsed.data.ord}, but the open gate `
2872
+ + `is before unit ${open.ord} — it was answered or replaced. Read the open gate before deciding.`,
2873
+ code: 'gate_changed',
2874
+ openOrd: open.ord,
2875
+ } };
2876
+ }
2877
+ }
2878
+ // Freeze deliveries (idea 15): an APPROVE that would run the run's deliver unit is held while
2879
+ // the switch is on — the gate stays open, so unfreezing lets the same approve through. The gate
2880
+ // is the named one, else the open one, else the unit the run's cursor points at.
2881
+ if (parsed.data.approve && deliveryFreeze.state().frozen) {
2882
+ let gateOrd = parsed.data.ord;
2883
+ if (gateOrd === undefined) {
2884
+ const open = await resolveOpenGate(id);
2885
+ gateOrd = open !== null && open !== 'no-log' ? open.ord : undefined;
2886
+ }
2887
+ const units = run.units ?? [];
2888
+ const gated = gateOrd !== undefined
2889
+ ? units.find((u) => u.ord === gateOrd)
2890
+ : [...units].sort((a, b) => a.ord - b.ord)[run.session.unit_ix];
2891
+ if (gated !== undefined && isDeliverUnit(gated)) {
2892
+ return { code: 409, body: frozenRefusal(deliveryFreeze.state()) };
2893
+ }
2548
2894
  }
2549
2895
  // The steering-author landing (crew#388): decided — and the gate prompt captured — BEFORE
2550
2896
  // the confirm, because a terminal-phase approve prunes the gate cache and moves the run out
2551
- // of `awaiting_human`. The landing itself runs AFTER a successful approve only.
2552
- // (`listWorkflows` is presence-guarded for partial-stub adapters — a registry-less adapter
2553
- // cannot be hosting a steering-author run, and the legacy gate path must not 500 over it.)
2554
- const steeringPropose = parsed.data.approve &&
2555
- typeof adapter.listWorkflows === 'function' &&
2556
- isSteeringAuthorRun(run, adapter.listWorkflows());
2897
+ // of `awaiting_human`. The landing itself runs AFTER a successful approve only. The run is
2898
+ // classified by its recorded name (seam X2), so a partial-stub adapter with no registry cannot
2899
+ // 500 the legacy gate path here.
2900
+ const steeringPropose = parsed.data.approve && isSteeringAuthorRun(run);
2557
2901
  const gatePrompt = steeringPropose ? gateCache.get(id)?.prompt : undefined;
2558
2902
  try {
2559
2903
  // All five positionals, explicit `undefined` for the absent ones (DES-L1 PR-2; the
2560
2904
  // gate-arms tripwire pins the arity). An engine refusal (no creator to send back to,
2561
2905
  // an engine older than the arm) surfaces as the existing 409 below.
2562
- const status = await adapter.confirmGate(id, parsed.data.approve, parsed.data.amend, parsed.data.action, parsed.data.amendScope);
2906
+ const status = await adapter.confirmGate(id, parsed.data.approve, parsed.data.amend, parsed.data.action, parsed.data.amendScope, parsed.data.plan !== undefined ? toLaunchPlan(parsed.data.plan) : undefined);
2563
2907
  // WHO approved/rejected — the gate-decision audit (task #88). The engine
2564
2908
  // records THAT the gate resolved (interaction_requests / gateDecided);
2565
2909
  // only this HTTP layer knows the authenticated principal behind it.
2566
- audit.record('gate.decided', actorOf(req), {
2910
+ audit.record('gate.decided', actor, {
2567
2911
  runId: id,
2568
2912
  detail: {
2569
2913
  approve: parsed.data.approve,
2570
2914
  ...(parsed.data.amend !== undefined ? { amend: parsed.data.amend } : {}),
2571
2915
  ...(parsed.data.action !== undefined ? { action: parsed.data.action } : {}),
2572
2916
  ...(parsed.data.amendScope !== undefined ? { amendScope: parsed.data.amendScope } : {}),
2917
+ ...(parsed.data.plan !== undefined ? { planSteps: parsed.data.plan.steps.length } : {}),
2918
+ ...(parsed.data.ord !== undefined ? { ord: parsed.data.ord } : {}),
2919
+ ...extraDetail,
2573
2920
  status,
2574
2921
  },
2575
2922
  });
@@ -2582,13 +2929,32 @@ runtime = {}) {
2582
2929
  // text (an operator who wants different rules rejects and re-authors). A reject lands
2583
2930
  // nothing — the run cancels and the proposal stays an artifact.
2584
2931
  const landing = steeringPropose
2585
- ? await landSteeringProposal({ adapter, audit, actor: actorOf(req) }, run, gatePrompt)
2932
+ ? await landSteeringProposal({ adapter, audit, actor }, run, gatePrompt)
2586
2933
  : undefined;
2587
- return reply.send({ status, ...(landing !== undefined ? { landing } : {}) });
2934
+ return { code: 200, body: { status, ...(landing !== undefined ? { landing } : {}) } };
2588
2935
  }
2589
2936
  catch (err) {
2590
- return reply.code(409).send({ error: message(err) });
2937
+ if (err instanceof PlanLaunchUnsupportedError) {
2938
+ return { code: 501, body: { error: message(err) } };
2939
+ }
2940
+ return { code: 409, body: { error: message(err) } };
2591
2941
  }
2942
+ }
2943
+ // The steering gate (§11.1). approve+amend = approve-with-steer; approve:false = reject (cancels).
2944
+ app.post(`${V}/runs/:id/gate`, {
2945
+ config: {
2946
+ manifest: {
2947
+ requestType: 'GateDecision',
2948
+ responseType: '{ status: SessionStatus; landing?: SteeringLandingResult }',
2949
+ // 409 twice over: a run not awaiting a human gate, and an engine refusal at confirm.
2950
+ // 501: an edited plan on an addon without the plan approval gate (DES-TEAMING-002 T3).
2951
+ statusCodes: [200, 400, 404, 409, 501],
2952
+ },
2953
+ },
2954
+ }, async (req, reply) => {
2955
+ const { id } = req.params;
2956
+ const out = await decideGate(id, req.body, actorOf(req));
2957
+ return reply.code(out.code).send(out.body);
2592
2958
  });
2593
2959
  // Cancel a running or paused run (distinct third action, §11.1).
2594
2960
  app.post(`${V}/runs/:id/cancel`, async (req, reply) => {
@@ -2635,9 +3001,11 @@ runtime = {}) {
2635
3001
  ? `run ${id} is cancelled — a terminal run cannot be resumed; relaunch the work as a new run with POST /runs {"retryOf":"${id}"}`
2636
3002
  : state.delivery === 'stranded'
2637
3003
  ? `run ${id} is already completed — nothing to resume; its unlifted work is on the wicked/${id} branch: deliver it with POST /runs/${id}/deliver`
2638
- : state.delivery === 'vacuous'
2639
- ? `run ${id} is already completed, but VACUOUSLY — its units produced no work to resume or deliver; relaunch with POST /runs {"retryOf":"${id}"}`
2640
- : `run ${id} is already completed — nothing to resume; relaunch the work as a new run with POST /runs {"retryOf":"${id}"}`;
3004
+ : state.delivery === 'pushed'
3005
+ ? `run ${id} is already completed and delivered — its branch ${state.deliverBranch ?? `wicked/${id}`} is on ${state.deliverRemote ?? 'origin'} (no pull request: that origin is not a GitHub host gh can resolve); nothing to resume`
3006
+ : state.delivery === 'vacuous'
3007
+ ? `run ${id} is already completed, but VACUOUSLY — its units produced no work to resume or deliver; relaunch with POST /runs {"retryOf":"${id}"}`
3008
+ : `run ${id} is already completed — nothing to resume; relaunch the work as a new run with POST /runs {"retryOf":"${id}"}`;
2641
3009
  return reply.code(409).send({ error: why, recovery });
2642
3010
  }
2643
3011
  try {
@@ -2645,9 +3013,7 @@ runtime = {}) {
2645
3013
  // A gated resume IS a gate approval, so it lands a steering-author proposal exactly like
2646
3014
  // POST /runs/:id/gate would — the "no side door" rule (task #88) holds for the landing
2647
3015
  // write too (crew#388). Prompt captured pre-confirm; see the gate route.
2648
- const steeringPropose = gated &&
2649
- typeof adapter.listWorkflows === 'function' &&
2650
- isSteeringAuthorRun(run, adapter.listWorkflows());
3016
+ const steeringPropose = gated && isSteeringAuthorRun(run);
2651
3017
  const gatePrompt = steeringPropose ? gateCache.get(id)?.prompt : undefined;
2652
3018
  const status = gated ? await adapter.confirmGate(id, true) : await adapter.resumeRun(id);
2653
3019
  // A resume of a gated run IS a gate approval — audit it as one, so the
@@ -2729,7 +3095,7 @@ runtime = {}) {
2729
3095
  `this build cannot approve the gate on your behalf (no confirmGate); approve via POST /runs/${id}/gate, then reassign`,
2730
3096
  });
2731
3097
  }
2732
- if (gated && typeof adapter.listWorkflows === 'function' && isSteeringAuthorRun(run, adapter.listWorkflows())) {
3098
+ if (gated && isSteeringAuthorRun(run)) {
2733
3099
  return reply.code(409).send({
2734
3100
  error: `run ${id} is awaiting a steering-author propose gate — approve or reject it via POST /runs/${id}/gate (the approve lands the proposal); reassign is not a review of the rules`,
2735
3101
  });
@@ -2797,34 +3163,12 @@ runtime = {}) {
2797
3163
  return reply.code(404).send({ error: 'No open gate for this run' });
2798
3164
  }
2799
3165
  // DURABLE TRUTH FIRST (DES-PROJECT-001 §5.3): the engine persists the open prompt in
2800
- // `interaction_requests`, written in the same transaction as the `awaiting_human` pause —
2801
- // so a daemon restart reads it back directly instead of replaying the event log. The cache
2802
- // adopts the row (latency layer over durable truth — its comments finally true). The replay
2803
- // below stays as the FALLBACK, not just for engines predating the binding: a run parked
2804
- // BEFORE the engine grew the table is awaiting_human with no row, and answering 404 there
2805
- // would re-open FINDING-051 for exactly the runs mid-upgrade. Row → serve it; no row →
2806
- // fall through and let the log speak.
2807
- const durableRows = typeof adapter.interactionRequests === 'function'
2808
- ? await adapter.interactionRequests(id, 'open')
2809
- : null;
2810
- const durableGate = durableRows?.find((r) => r.kind === 'gate');
2811
- if (durableGate !== undefined) {
2812
- // This path builds the entry inline (not through `fold`), so it must run the SAME refusal
2813
- // detection (issue #419) — otherwise a gate served from the durable row after a restart would
2814
- // silently drop the warning that the live/replay paths carry.
2815
- const refusal = detectRefusal(durableGate.prompt);
2816
- const entry = {
2817
- ord: durableGate.ord ?? 0,
2818
- prompt: durableGate.prompt,
2819
- lifecycle: 'open',
2820
- receivedAt: new Date(durableGate.created_at).toISOString(),
2821
- ...(refusal !== undefined ? { refusal } : {}),
2822
- };
2823
- gateCache.adopt(id, entry);
2824
- return { runId: id, ...entry };
2825
- }
2826
- const events = await adapter.runEvents(id);
2827
- if (events === null) {
3166
+ // `interaction_requests`, written in the same transaction as the `awaiting_human` pause — so a
3167
+ // daemon restart reads it back instead of replaying the log; the replay stays the FALLBACK for
3168
+ // runs parked before the engine grew the table. `resolveOpenGate` is that one resolution, shared
3169
+ // with the POST route's `ord` check.
3170
+ const open = await resolveOpenGate(id);
3171
+ if (open === 'no-log') {
2828
3172
  // Now — and only now — 503 is the honest answer: this run really is holding for a human, and
2829
3173
  // this build cannot say what it is asking. Distinct cause, distinct message; answering "no
2830
3174
  // gate" here would report a capability gap as a fact about the run (the FINDING-050 shape).
@@ -2832,13 +3176,12 @@ runtime = {}) {
2832
3176
  error: 'Gate history is unavailable: this wicked-core build has no event-log read binding',
2833
3177
  });
2834
3178
  }
2835
- const replayed = gateCache.rebuild(id, events);
2836
- if (!replayed) {
3179
+ if (open === null) {
2837
3180
  // Parked, with a history that records no open gate. Pre-log runs land here (their prompt is
2838
3181
  // genuinely lost), as does a gate whose `awaitingHuman` predates the log's retention.
2839
3182
  return reply.code(404).send({ error: 'No open gate for this run' });
2840
3183
  }
2841
- return { runId: id, ...replayed };
3184
+ return { runId: id, ...open };
2842
3185
  });
2843
3186
  // ── Elicitation (DES-002) ────────────────────────────────────────────────────
2844
3187
  //
@@ -3128,6 +3471,74 @@ runtime = {}) {
3128
3471
  const claims = await adapter.listConformanceClaims();
3129
3472
  return { claims };
3130
3473
  });
3474
+ // Replay THIS daemon's governance dead-letter outbox into its own store — the
3475
+ // `wicked-crew governance replay` CLI (crew#495) as a route, so a skin can offer the repair
3476
+ // beside the count it shows (`/diagnostics.governance.deadletters`). `dryRun: true` folds the
3477
+ // outbox and moves nothing, and says up front whether a real replay could run at all
3478
+ // (`blocker`); a real replay drains exactly as the CLI does (archive first, entries that fail
3479
+ // go back onto the live outbox and stay dead letters). One replay at a time per daemon.
3480
+ let deadletterReplayInFlight = false;
3481
+ app.post(`${V}/governance/deadletters/replay`, {
3482
+ config: {
3483
+ manifest: {
3484
+ requestType: 'GovernanceReplayBody',
3485
+ responseType: 'GovernanceReplayOutcome',
3486
+ statusCodes: [200, 400, 409, 500, 501],
3487
+ },
3488
+ },
3489
+ }, async (req, reply) => {
3490
+ const parsed = z.object({ dryRun: z.boolean().optional() }).strict().safeParse(req.body ?? {});
3491
+ if (!parsed.success)
3492
+ return reply.code(400).send({ error: parsed.error.message });
3493
+ const dryRun = parsed.data.dryRun === true;
3494
+ const store = adapter.governanceStore ?? null;
3495
+ if (store === null) {
3496
+ return reply.code(409).send({ error: 'this daemon resolved no governance store — there is nothing to replay into' });
3497
+ }
3498
+ const blocker = store.dbPath === ':memory:'
3499
+ ? 'the governance store is in-memory — name a durable store before replaying'
3500
+ : !CoreAdapter.replayEmitOutboxSupported()
3501
+ ? 'the installed engine cannot replay a dead-letter outbox — upgrade wicked-core-ts'
3502
+ : null;
3503
+ // Checked FIRST, for both modes: a running replay has renamed the live outbox to its archive,
3504
+ // so any read now would report a false zero while those dead letters are still in flight.
3505
+ if (deadletterReplayInFlight)
3506
+ return reply.code(409).send({ error: 'a dead-letter replay is already running' });
3507
+ const outbox = store.outboxPath;
3508
+ if (!existsSync(outbox)) {
3509
+ // No outbox = no dead letters: a zero outcome, never an error.
3510
+ const fold = await foldDeadletters(outbox);
3511
+ return {
3512
+ outbox, store: { path: store.displayPath, source: store.source }, archive: null,
3513
+ read: 0, replayed: 0, alreadyPresent: null, failed: 0, dryRun, note: null,
3514
+ ...(dryRun ? { fold } : {}), blocker,
3515
+ };
3516
+ }
3517
+ if (!dryRun && blocker !== null)
3518
+ return reply.code(blocker.startsWith('the installed engine') ? 501 : 409).send({ error: blocker });
3519
+ if (!dryRun)
3520
+ deadletterReplayInFlight = true;
3521
+ try {
3522
+ const { outcome } = await replayOutboxInto(outbox, store, { dryRun });
3523
+ if (!dryRun) {
3524
+ audit.record('governance.deadletters.replayed', actorOf(req), {
3525
+ detail: { read: outcome.read, replayed: outcome.replayed, failed: outcome.failed },
3526
+ });
3527
+ }
3528
+ return { ...outcome, blocker: dryRun ? blocker : null };
3529
+ }
3530
+ catch (err) {
3531
+ if (err instanceof GovernanceReplayUnsupportedError)
3532
+ return reply.code(501).send({ error: message(err) });
3533
+ if (err instanceof UsageError)
3534
+ return reply.code(409).send({ error: message(err) });
3535
+ return reply.code(500).send({ error: message(err) });
3536
+ }
3537
+ finally {
3538
+ if (!dryRun)
3539
+ deadletterReplayInFlight = false;
3540
+ }
3541
+ });
3131
3542
  // Normalize a `?repo=` query: Fastify parses a repeated `?repo=a&repo=b` into a `string[]` (take
3132
3543
  // the first), and the value is TRIMMED so it's both validated and PASSED trimmed — a `?repo=%20x%20`
3133
3544
  // must not pass the guard then miss the registry lookup on whitespace (Copilot #227).
@@ -3297,15 +3708,41 @@ runtime = {}) {
3297
3708
  }
3298
3709
  });
3299
3710
  // ── Workflow viewer + builder (crew#44) ───────────────────────────────────
3711
+ // (crew#718) Start the engine's verdict pass at registration, so every SYNC reader of
3712
+ // `listWorkflows()` / `getWorkflow()` (the deliver resolver, the acceptance fold, the launch
3713
+ // lookup) sees the judged catalog from the first request rather than after the first catalog GET.
3714
+ // The pass is memoized: the handlers below await the same promise.
3715
+ // Guarded like `listWorkflows` at :1110 — the endpoint-manifest collector registers these routes
3716
+ // against a partial adapter that carries neither.
3717
+ if (typeof adapter.workflowCatalog === 'function') {
3718
+ void adapter.workflowCatalog().catch((err) => {
3719
+ app.log.warn(`wicked-crew: the workflow overlay could not be judged (${message(err)}); the catalog lists what parsed`);
3720
+ });
3721
+ }
3722
+ // (crew#718) The catalog is what the ENGINE will honour, not what parses as `{id, phases[]}`.
3723
+ // A drop-in core refused at boot ("gate evaluates nothing: …") used to be listed here and
3724
+ // offered in studio's selector, and the launch then 400'd `unknown workflow` — the operator
3725
+ // learnt about it only from the failed launch. `workflowCatalog()` asks core's own validator:
3726
+ // the refused def leaves `workflows` (so it cannot be launched) and appears in `unavailable`
3727
+ // with core's verbatim reason (so its author is told why, and what to fix).
3300
3728
  app.get(`${V}/workflows`, async () => {
3301
- const workflows = adapter.listWorkflows();
3302
- return { workflows };
3729
+ const { workflows, unavailable } = await adapter.workflowCatalog();
3730
+ return { workflows, unavailable };
3303
3731
  });
3304
3732
  app.get(`${V}/workflows/:id`, async (req, reply) => {
3305
3733
  const { id } = req.params;
3734
+ // (crew#718) JUDGE FIRST (review of PR #724, MEDIUM). `getWorkflow` only hydrates the overlay
3735
+ // dir; `workflowRefusal` is what runs the engine's verdict pass and drops the defs it refused.
3736
+ // Read the other way round, the FIRST request for a refused drop-in answered 200 with the def
3737
+ // the engine had thrown away — and this route is where the launch looks it up.
3738
+ const refusal = await adapter.workflowRefusal(id);
3739
+ if (refusal !== null) {
3740
+ return reply.code(404).send({ error: `workflow '${id}' was refused by the engine: ${refusal}`, reason: refusal });
3741
+ }
3306
3742
  const workflow = adapter.getWorkflow(id);
3307
- if (!workflow)
3743
+ if (!workflow) {
3308
3744
  return reply.code(404).send({ error: `workflow '${id}' not found` });
3745
+ }
3309
3746
  // humanGates: the phases that will PAUSE for a person even under humanConfirm:none (core#208).
3310
3747
  // Surfaced so an operator can see a workflow's gates BEFORE launching it (FINDING-023).
3311
3748
  return { workflow, humanGates: humanGatePhaseIds(workflow) };
@@ -3627,6 +4064,18 @@ runtime = {}) {
3627
4064
  return reply.code(500).send({ error: message(err) });
3628
4065
  }
3629
4066
  });
4067
+ // R3 (ship-prove-3): what a delivering launch on this repo would push and where — read by the
4068
+ // SAME origin preflight the deliver gate card uses (crew#730), so the launch composer cannot
4069
+ // promise "opens a PR" on an origin that can never carry one. Read-only: one
4070
+ // `git remote get-url --push origin` in the registered checkout; nothing is pushed or probed.
4071
+ app.get(`${V}/repos/:id/deliver-target`, { config: { manifest: { responseType: 'DeliverTargetResponse', statusCodes: [200, 404] } } }, async (req, reply) => {
4072
+ const { id } = req.params;
4073
+ const repo = deliverRepoFor(await adapter.listRepos(), id);
4074
+ if (repo === undefined)
4075
+ return reply.code(404).send({ error: `Repo ${id} not found` });
4076
+ const originUrl = repo.root_path === '' ? null : await readDeliverOriginUrl(repo.root_path);
4077
+ return reply.send({ repo: repo.id, ...deliverTargetView(originUrl) });
4078
+ });
3630
4079
  app.get(`${V}/repos/:id/git-history`, async (req, reply) => {
3631
4080
  const { id } = req.params;
3632
4081
  const repos = await adapter.listRepos();
@@ -3809,6 +4258,22 @@ runtime = {}) {
3809
4258
  .send({ error: 'workerStallMaxEscalations must be an integer between 1 and 10' });
3810
4259
  }
3811
4260
  }
4261
+ // deliverIdentityLogin (crew#549): the login the deliver phase must push as. A typo here
4262
+ // would be an identity the script can never match — every delivery would refuse — so an
4263
+ // invalid value is a 400, never a silently-dropped key. `''` clears it (back to GH_ACCOUNT).
4264
+ // A LOGIN, never a token: the same validator the deliver script's splice uses.
4265
+ if (Object.hasOwn(patch, 'deliverIdentityLogin')) {
4266
+ const login = patch.deliverIdentityLogin;
4267
+ if (typeof login !== 'string' || (login !== '' && !isGitHubLogin(login.trim()))) {
4268
+ return reply.code(400).send({
4269
+ error: 'deliverIdentityLogin must be a GitHub login (letters, digits and single hyphens, '
4270
+ + 'up to 39 characters), or "" to fall back to the GH_ACCOUNT environment variable. '
4271
+ + 'It is a login, never a token — the credential stays in gh\'s keyring or GH_TOKEN.',
4272
+ });
4273
+ }
4274
+ if (typeof login === 'string')
4275
+ patch.deliverIdentityLogin = login.trim();
4276
+ }
3812
4277
  // deliverDefault (crew#393): the repo-scoped launch delivery default. Two values only —
3813
4278
  // this knob decides whether completed code runs open PRs, so a typo must be a 400, never
3814
4279
  // a silently-dropped key that leaves the operator believing they flipped it.
@@ -3880,6 +4345,7 @@ runtime = {}) {
3880
4345
  'workerStallEscalateAction',
3881
4346
  'workerStallMaxEscalations',
3882
4347
  'deliverDefault',
4348
+ 'deliverIdentityLogin',
3883
4349
  'baseSkillRef',
3884
4350
  'baseSkillPolicy',
3885
4351
  ];
@@ -3964,6 +4430,41 @@ runtime = {}) {
3964
4430
  return estateUpstreamError(reply, err);
3965
4431
  }
3966
4432
  });
4433
+ // POST /proposals {content, project?, source?} → proposal.submit → 201 { id } (api-types 0.54.0).
4434
+ // A skin files ONE preference into the same review queue — studio's "takes": the person picks one
4435
+ // of two candidate outputs, and the pick becomes "prefers takes like X", learned only once they
4436
+ // accept it here. The kind (`memory`), tier and `capture: "preference"` marker are the daemon's; a
4437
+ // caller cannot file a policy through this door (the schema is strict). `project` becomes the
4438
+ // `project` facet, so the preference is recalled on that project only.
4439
+ app.post(`${V}/proposals`, {
4440
+ config: {
4441
+ manifest: { requestType: 'FileProposalBody', responseType: 'FileProposalResponse', statusCodes: [201, 400, 502] },
4442
+ },
4443
+ }, async (req, reply) => {
4444
+ const parsed = FileProposalSchema.safeParse(req.body ?? {});
4445
+ if (!parsed.success) {
4446
+ return reply.code(400).send(invalidBody(parsed.error, 'Invalid proposal body'));
4447
+ }
4448
+ const { content, project, source } = parsed.data;
4449
+ const payload = { content, tier: 'semantic', capture: 'preference' };
4450
+ if (source !== undefined)
4451
+ payload.source = source;
4452
+ let submitted;
4453
+ try {
4454
+ submitted = (await estateTool('proposal.submit', {
4455
+ kind_type: 'memory',
4456
+ payload,
4457
+ facets: project !== undefined ? { project } : {},
4458
+ }));
4459
+ }
4460
+ catch (err) {
4461
+ return estateUpstreamError(reply, err);
4462
+ }
4463
+ if (typeof submitted?.id !== 'string' || submitted.id === '') {
4464
+ return reply.code(502).send({ error: 'proposal.submit returned no id' });
4465
+ }
4466
+ return reply.code(201).send({ id: submitted.id });
4467
+ });
3967
4468
  // POST /proposals/:id/approve → proposal.approve →
3968
4469
  // { outcome:'promoted', active_id } — a MEMORY proposal, now an active memory (complete);
3969
4470
  // { outcome:'handed_off', payload, landing } — a POLICY proposal, LANDED here as a steering rule.
@@ -3978,7 +4479,16 @@ runtime = {}) {
3978
4479
  // 500, never the silent no-op that crew#388 exists to end — and `payload` is still returned for
3979
4480
  // backward compatibility. The MEMORY path is UNTOUCHED: a `promoted` outcome returns immediately
3980
4481
  // with no extra estate call and no landing.
3981
- app.post(`${V}/proposals/:id/approve`, { config: { manifest: { responseType: 'ApproveProposalResponse', statusCodes: [200, 400, 502] } } }, async (req, reply) => {
4482
+ app.post(`${V}/proposals/:id/approve`, {
4483
+ config: {
4484
+ manifest: {
4485
+ requestType: 'ApproveProposalBody',
4486
+ responseType: 'ApproveProposalResponse',
4487
+ // 404: an accept-with-edit naming no pending proposal; 409: one already in flight for it.
4488
+ statusCodes: [200, 400, 404, 409, 502],
4489
+ },
4490
+ },
4491
+ }, async (req, reply) => {
3982
4492
  // Normalize ONCE and use the trimmed value throughout — an id like `%20pol1%20`
3983
4493
  // decodes to a padded, non-empty string that would otherwise ride upstream as-is
3984
4494
  // and into `proposal:<id>` derivations.
@@ -3986,6 +4496,24 @@ runtime = {}) {
3986
4496
  if (id === '') {
3987
4497
  return reply.code(400).send({ error: '`id` is required' });
3988
4498
  }
4499
+ // An accept WITH an edit (capture review, api-types 0.49.0): the edited copy replaces the
4500
+ // original in the same queue — see `approveEdited`. No body / `{}` is the plain approve below.
4501
+ const edit = ApproveEditSchema.safeParse(req.body ?? {});
4502
+ if (!edit.success) {
4503
+ return reply.code(400).send(invalidBody(edit.error, 'Invalid approve body'));
4504
+ }
4505
+ if (edit.data.content !== undefined || edit.data.reach !== undefined) {
4506
+ try {
4507
+ const done = await approveEdited(estateTool, id, edit.data, (runId) => projects.index.projectOf(runId));
4508
+ return reply.code(done.status).send(done.body);
4509
+ }
4510
+ catch (err) {
4511
+ return estateUpstreamError(reply, err);
4512
+ }
4513
+ }
4514
+ if (editInFlight(id)) {
4515
+ return reply.code(409).send({ error: `proposal ${id} is already being accepted` });
4516
+ }
3989
4517
  let approved;
3990
4518
  try {
3991
4519
  approved = (await estateTool('proposal.approve', { id }));
@@ -4056,11 +4584,16 @@ runtime = {}) {
4056
4584
  return { outcome: 'handed_off', payload: approved.payload, landing };
4057
4585
  });
4058
4586
  // POST /proposals/:id/reject → proposal.reject → { ok: true }.
4059
- app.post(`${V}/proposals/:id/reject`, { config: { manifest: { responseType: 'RejectProposalResponse', statusCodes: [200, 400, 502] } } }, async (req, reply) => {
4587
+ app.post(`${V}/proposals/:id/reject`,
4588
+ // 409: an accept-with-edit of this proposal is in flight (api/capture.ts).
4589
+ { config: { manifest: { responseType: 'RejectProposalResponse', statusCodes: [200, 400, 409, 502] } } }, async (req, reply) => {
4060
4590
  const { id } = req.params;
4061
4591
  if (id.trim() === '') {
4062
4592
  return reply.code(400).send({ error: '`id` is required' });
4063
4593
  }
4594
+ if (editInFlight(id.trim())) {
4595
+ return reply.code(409).send({ error: `proposal ${id.trim()} is being accepted` });
4596
+ }
4064
4597
  try {
4065
4598
  return (await estateTool('proposal.reject', { id }));
4066
4599
  }
@@ -4086,10 +4619,12 @@ runtime = {}) {
4086
4619
  // applied HERE as post-filters over that complete set (see below) — filtering, not retrieval.
4087
4620
  // • list returns { memory_id, scope, content, tier, facets, created_at }; facets ride through to
4088
4621
  // MemoryItem.facets, powering the surface's facet filter.
4089
- // • RETIRE is SUBTREE-scoped, never per-id. estate exposes NO per-memory delete: `memory.erase`
4090
- // hard-deletes EVERY memory whose scope equals or descends from `scope_prefix`, and refuses an
4091
- // empty prefix (a total-wipe guard). So retire takes a `scope_prefix` and reports how many
4092
- // memories the subtree wipe removed — the UI must show the operator the subtree, not one row.
4622
+ // • RETIRE comes in two widths. `POST /memory/retire { scope_prefix }` is SUBTREE-scoped:
4623
+ // `memory.erase {scope_prefix}` hard-deletes EVERY memory whose scope equals or descends from
4624
+ // the prefix, and refuses an empty prefix (a total-wipe guard) — the UI must show the operator
4625
+ // the subtree. `POST /memory/retire-item { memory_id }` (studio#206) removes exactly ONE memory
4626
+ // through `memory.erase {id}` (wicked-estate#188); an older estate refuses that call without
4627
+ // deleting anything, and the route answers 501 naming the upgrade — never a subtree fallback.
4093
4628
  // GET /memory?query=&scope_prefix=&facets=<json>&limit= → memory.list + post-filter → { memories }.
4094
4629
  app.get(`${V}/memory`, { config: { manifest: { responseType: 'ListMemoriesResponse', statusCodes: [200, 400, 502] } } }, async (req, reply) => {
4095
4630
  const q = req.query;
@@ -4192,9 +4727,9 @@ runtime = {}) {
4192
4727
  return estateUpstreamError(reply, err);
4193
4728
  }
4194
4729
  });
4195
- // POST /memory/retire { scope_prefix } → memory.erase → { erased }. SUBTREE-scoped (no per-id
4196
- // delete in estate); an empty scope_prefix is refused at the route (a 400 BEFORE any spawn) — the
4197
- // same total-wipe guard estate enforces with -32602.
4730
+ // POST /memory/retire { scope_prefix } → memory.erase → { erased }. SUBTREE-scoped (the per-item
4731
+ // delete is /memory/retire-item below); an empty scope_prefix is refused at the route (a 400
4732
+ // BEFORE any spawn) — the same total-wipe guard estate enforces with -32602.
4198
4733
  app.post(`${V}/memory/retire`, {
4199
4734
  config: {
4200
4735
  manifest: {
@@ -4211,7 +4746,7 @@ runtime = {}) {
4211
4746
  const scopePrefix = parsed.data.scope_prefix.trim();
4212
4747
  if (scopePrefix === '') {
4213
4748
  return reply.code(400).send({
4214
- error: '`scope_prefix` must not be empty — memory.erase is subtree-scoped (no per-id delete) and refuses a total wipe',
4749
+ error: '`scope_prefix` must not be empty — this retire is subtree-scoped and refuses a total wipe (one memory: POST /memory/retire-item)',
4215
4750
  });
4216
4751
  }
4217
4752
  try {
@@ -4227,8 +4762,64 @@ runtime = {}) {
4227
4762
  return estateUpstreamError(reply, err);
4228
4763
  }
4229
4764
  });
4765
+ // POST /memory/retire-item { memory_id } → memory.erase { id } → { erased: 1 } (studio#206). Removes
4766
+ // exactly ONE memory: the id is the only target sent, so no reading of it can widen to a subtree.
4767
+ // 404 when estate holds no memory with that id (it erased 0). 501 when the estate predates
4768
+ // wicked-estate#188: it refuses an id-only erase for want of `scope_prefix`, deleting nothing.
4769
+ app.post(`${V}/memory/retire-item`, {
4770
+ config: {
4771
+ manifest: {
4772
+ requestType: 'RetireMemoryItemBody',
4773
+ responseType: 'RetireMemoryResponse',
4774
+ statusCodes: [200, 400, 404, 501, 502],
4775
+ },
4776
+ },
4777
+ }, async (req, reply) => {
4778
+ const parsed = RetireMemoryItemSchema.safeParse(req.body);
4779
+ if (!parsed.success) {
4780
+ return reply.code(400).send(invalidBody(parsed.error, 'Invalid retire request'));
4781
+ }
4782
+ const memoryId = parsed.data.memory_id.trim();
4783
+ if (memoryId === '')
4784
+ return reply.code(400).send({ error: '`memory_id` must not be empty' });
4785
+ try {
4786
+ const raw = await estateTool('memory.erase', { id: memoryId });
4787
+ const deleted = typeof raw === 'object' && raw !== null ? raw['deleted_count'] : undefined;
4788
+ if (typeof deleted !== 'number') {
4789
+ throw new EstateMcpError('memory.erase returned an unexpected shape');
4790
+ }
4791
+ if (deleted === 0)
4792
+ return reply.code(404).send({ error: `no memory ${memoryId}` });
4793
+ if (deleted !== 1) {
4794
+ // An id names one memory; any other count is estate misbehaving, and it is reported as such.
4795
+ return reply.code(502).send({ error: `memory.erase {id} removed ${deleted} memories, expected 1` });
4796
+ }
4797
+ const body = { erased: 1 };
4798
+ return body;
4799
+ }
4800
+ catch (err) {
4801
+ if (err instanceof EstateMcpError && err.code === -32602 && ESTATE_NO_ERASE_BY_ID.test(err.message)) {
4802
+ return reply.code(501).send({
4803
+ error: 'this wicked-estate cannot erase one memory by id (it needs wicked-estate#188); upgrade wicked-estate-mcp — nothing was deleted',
4804
+ code: 'estate_upgrade_required',
4805
+ });
4806
+ }
4807
+ return estateUpstreamError(reply, err);
4808
+ }
4809
+ });
4230
4810
  // ── Projects (DES-PROJECT-001) — the 9-route experience-plane surface ────────
4231
4811
  registerProjectRoutes(app, adapter, { ...projects, settings: projectSettings }, security);
4812
+ // ── Capture (Studio OS behaviour 8) — notes and photos → a project-filed run whose output is
4813
+ // proposals in the queue above; the launch is POST /runs itself (api/capture.ts).
4814
+ registerCaptureRoutes(app);
4815
+ // The Demo experience (studio#373): the demo preset's launch, view, files and script edit.
4816
+ registerDemoRoutes(app, adapter);
4817
+ // ── Presets (DES-TEAMING-002 §8.4, seam C2) — saved phase selections in the engine's store;
4818
+ // a launch names one via `workflow`, and the ENGINE resolves it.
4819
+ registerPresetRoutes(app, adapter, security);
4820
+ // ── Team (DES-TEAMING-002 T8) — reads and commands over the engine; the live stream is the
4821
+ // teamEvent relay (team/ws-relay.ts). Crew publishes no team fact (§4.0).
4822
+ registerTeamRoutes(app, adapter, security);
4232
4823
  // ── Campaigns (crew#342 + TH-9) — the engine's durable Run-DAG scheduler over REST ──────────
4233
4824
  // Progress streams as campaign* CoreEvents on the existing allowlist-free /ws relay; these
4234
4825
  // routes are launch/resume/cancel + the reads a scoreboard builds from. The GET routes join
@@ -4246,6 +4837,7 @@ runtime = {}) {
4246
4837
  // Wave 6 (F-7R2-014): the registered test sets ride beside the campaigns + groups.
4247
4838
  testSets,
4248
4839
  deliveryUrlFor: (runId) => deliveryIndex.urlFor(runId),
4840
+ deliveryPushedFor: (runId) => deliveryIndex.pushedFor(runId),
4249
4841
  vacuity: vacuityProbes,
4250
4842
  // A non-probe derivation throw in the rollup is a defect — error level, loud in diagnostics.
4251
4843
  logDefect: (m) => app.log.error(m),
@@ -4299,6 +4891,24 @@ runtime = {}) {
4299
4891
  audit,
4300
4892
  actorOf,
4301
4893
  });
4894
+ // ── MCP tools registry (DES-MCP-TOOLS-001 S2) — upstream servers, their tools, their secrets ─
4895
+ // Preview-bound saves, the tool diff on test, and a secret route that never answers the value.
4896
+ // S6: the policy preview and the approvals, over the same registry and the engine's rule store.
4897
+ const mcpPolicies = runtime.mcp !== undefined
4898
+ ? new McpPolicies(runtime.mcp, {
4899
+ listRules: () => adapter.listConformanceRules(),
4900
+ upsertRule: (rule) => adapter.upsertConformanceRule(rule),
4901
+ previewCalls: (json) => adapter.previewMcpCalls(json),
4902
+ })
4903
+ : undefined;
4904
+ registerMcpRoutes(app, {
4905
+ ...(runtime.mcp !== undefined ? { registry: runtime.mcp, toolLister: () => CoreAdapter.mcpToolLister() } : {}),
4906
+ ...(runtime.mcpBroker !== undefined ? { broker: runtime.mcpBroker } : {}),
4907
+ ...(mcpPolicies !== undefined ? { policies: mcpPolicies } : {}),
4908
+ ...(runtime.mcpCalls !== undefined ? { usage: runtime.mcpCalls } : {}),
4909
+ audit,
4910
+ actorOf,
4911
+ });
4302
4912
  // ── The wicked-interactive bridge, reverse-proxied (DES-MERGE-001 §5.3/§7.2) ──
4303
4913
  // Mounted BESIDE the routes above and under the same `${V}` prefix, so it inherits one
4304
4914
  // origin, one auth hook, and one CORS posture — the whole point of slice 1.
@@ -4349,5 +4959,6 @@ runtime = {}) {
4349
4959
  pool: interactiveBridges,
4350
4960
  log: (m) => app.log.warn(m),
4351
4961
  });
4962
+ return { decideGate };
4352
4963
  }
4353
4964
  //# sourceMappingURL=routes.js.map