wicked-crew 0.7.39 → 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 (380) 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-scope.d.ts +37 -1
  14. package/dist/api/chat-scope.d.ts.map +1 -1
  15. package/dist/api/chat-scope.js +131 -7
  16. package/dist/api/chat-scope.js.map +1 -1
  17. package/dist/api/chat-transcripts.d.ts +20 -1
  18. package/dist/api/chat-transcripts.d.ts.map +1 -1
  19. package/dist/api/chat-transcripts.js +29 -0
  20. package/dist/api/chat-transcripts.js.map +1 -1
  21. package/dist/api/delivery-cache.d.ts +2 -2
  22. package/dist/api/delivery-freeze.d.ts +66 -0
  23. package/dist/api/delivery-freeze.d.ts.map +1 -0
  24. package/dist/api/delivery-freeze.js +104 -0
  25. package/dist/api/delivery-freeze.js.map +1 -0
  26. package/dist/api/delivery-index.d.ts +53 -9
  27. package/dist/api/delivery-index.d.ts.map +1 -1
  28. package/dist/api/delivery-index.js +115 -22
  29. package/dist/api/delivery-index.js.map +1 -1
  30. package/dist/api/demo.d.ts +90 -0
  31. package/dist/api/demo.d.ts.map +1 -0
  32. package/dist/api/demo.js +492 -0
  33. package/dist/api/demo.js.map +1 -0
  34. package/dist/api/diagnostics.d.ts +64 -1
  35. package/dist/api/diagnostics.d.ts.map +1 -1
  36. package/dist/api/diagnostics.js +129 -1
  37. package/dist/api/diagnostics.js.map +1 -1
  38. package/dist/api/eval-sample.d.ts +6 -6
  39. package/dist/api/governance-steering.d.ts +2 -2
  40. package/dist/api/mcp.d.ts +282 -0
  41. package/dist/api/mcp.d.ts.map +1 -0
  42. package/dist/api/mcp.js +521 -0
  43. package/dist/api/mcp.js.map +1 -0
  44. package/dist/api/plan-schema.d.ts +56 -0
  45. package/dist/api/plan-schema.d.ts.map +1 -0
  46. package/dist/api/plan-schema.js +26 -0
  47. package/dist/api/plan-schema.js.map +1 -0
  48. package/dist/api/roster-standing.d.ts +15 -2
  49. package/dist/api/roster-standing.d.ts.map +1 -1
  50. package/dist/api/roster-standing.js +55 -3
  51. package/dist/api/roster-standing.js.map +1 -1
  52. package/dist/api/routes.d.ts +647 -59
  53. package/dist/api/routes.d.ts.map +1 -1
  54. package/dist/api/routes.js +906 -146
  55. package/dist/api/routes.js.map +1 -1
  56. package/dist/api/run-files.d.ts.map +1 -1
  57. package/dist/api/run-files.js +31 -2
  58. package/dist/api/run-files.js.map +1 -1
  59. package/dist/api/run-liveness.d.ts +41 -0
  60. package/dist/api/run-liveness.d.ts.map +1 -0
  61. package/dist/api/run-liveness.js +166 -0
  62. package/dist/api/run-liveness.js.map +1 -0
  63. package/dist/api/run-timing-index.d.ts +10 -0
  64. package/dist/api/run-timing-index.d.ts.map +1 -1
  65. package/dist/api/run-timing-index.js +24 -0
  66. package/dist/api/run-timing-index.js.map +1 -1
  67. package/dist/api/seat-health.d.ts.map +1 -1
  68. package/dist/api/seat-health.js +20 -4
  69. package/dist/api/seat-health.js.map +1 -1
  70. package/dist/api/seat-probe.d.ts +141 -0
  71. package/dist/api/seat-probe.d.ts.map +1 -0
  72. package/dist/api/seat-probe.js +257 -0
  73. package/dist/api/seat-probe.js.map +1 -0
  74. package/dist/api/seat-record.d.ts +40 -0
  75. package/dist/api/seat-record.d.ts.map +1 -0
  76. package/dist/api/seat-record.js +218 -0
  77. package/dist/api/seat-record.js.map +1 -0
  78. package/dist/api/seat-signin.d.ts.map +1 -1
  79. package/dist/api/seat-signin.js +3 -1
  80. package/dist/api/seat-signin.js.map +1 -1
  81. package/dist/api/seat-standing.d.ts +24 -4
  82. package/dist/api/seat-standing.d.ts.map +1 -1
  83. package/dist/api/seat-standing.js +50 -3
  84. package/dist/api/seat-standing.js.map +1 -1
  85. package/dist/api/server.d.ts +34 -48
  86. package/dist/api/server.d.ts.map +1 -1
  87. package/dist/api/server.js +316 -116
  88. package/dist/api/server.js.map +1 -1
  89. package/dist/api/skills.d.ts +3 -1
  90. package/dist/api/skills.d.ts.map +1 -1
  91. package/dist/api/skills.js +7 -2
  92. package/dist/api/skills.js.map +1 -1
  93. package/dist/api/stall-watchdog.d.ts +34 -3
  94. package/dist/api/stall-watchdog.d.ts.map +1 -1
  95. package/dist/api/stall-watchdog.js +107 -8
  96. package/dist/api/stall-watchdog.js.map +1 -1
  97. package/dist/api/steering-landing.d.ts +6 -6
  98. package/dist/api/steering-landing.d.ts.map +1 -1
  99. package/dist/api/steering-landing.js +7 -9
  100. package/dist/api/steering-landing.js.map +1 -1
  101. package/dist/api/testing.d.ts +6 -4
  102. package/dist/api/testing.d.ts.map +1 -1
  103. package/dist/api/testing.js +82 -47
  104. package/dist/api/testing.js.map +1 -1
  105. package/dist/campaign/boot-resume.d.ts +34 -0
  106. package/dist/campaign/boot-resume.d.ts.map +1 -0
  107. package/dist/campaign/boot-resume.js +45 -0
  108. package/dist/campaign/boot-resume.js.map +1 -0
  109. package/dist/campaigns/rollup.d.ts +4 -1
  110. package/dist/campaigns/rollup.d.ts.map +1 -1
  111. package/dist/campaigns/rollup.js +6 -1
  112. package/dist/campaigns/rollup.js.map +1 -1
  113. package/dist/campaigns/routes.d.ts +2 -2
  114. package/dist/cli/bearer.d.ts +27 -1
  115. package/dist/cli/bearer.d.ts.map +1 -1
  116. package/dist/cli/bearer.js +62 -2
  117. package/dist/cli/bearer.js.map +1 -1
  118. package/dist/cli/governance.d.ts +11 -0
  119. package/dist/cli/governance.d.ts.map +1 -1
  120. package/dist/cli/governance.js +8 -2
  121. package/dist/cli/governance.js.map +1 -1
  122. package/dist/cli/index.d.ts.map +1 -1
  123. package/dist/cli/index.js +140 -75
  124. package/dist/cli/index.js.map +1 -1
  125. package/dist/cli/mcp.d.ts +11 -0
  126. package/dist/cli/mcp.d.ts.map +1 -1
  127. package/dist/cli/mcp.js +40 -9
  128. package/dist/cli/mcp.js.map +1 -1
  129. package/dist/cli/service.d.ts +78 -0
  130. package/dist/cli/service.d.ts.map +1 -0
  131. package/dist/cli/service.js +334 -0
  132. package/dist/cli/service.js.map +1 -0
  133. package/dist/core/adapter.d.ts +247 -14
  134. package/dist/core/adapter.d.ts.map +1 -1
  135. package/dist/core/adapter.js +638 -161
  136. package/dist/core/adapter.js.map +1 -1
  137. package/dist/core/bridge-reaper.d.ts +4 -1
  138. package/dist/core/bridge-reaper.d.ts.map +1 -1
  139. package/dist/core/bridge-reaper.js +15 -1
  140. package/dist/core/bridge-reaper.js.map +1 -1
  141. package/dist/core/bus-notice.d.ts +27 -0
  142. package/dist/core/bus-notice.d.ts.map +1 -0
  143. package/dist/core/bus-notice.js +24 -0
  144. package/dist/core/bus-notice.js.map +1 -0
  145. package/dist/core/bus.d.ts +99 -0
  146. package/dist/core/bus.d.ts.map +1 -0
  147. package/dist/core/bus.js +176 -0
  148. package/dist/core/bus.js.map +1 -0
  149. package/dist/core/deliver-exclusions.d.ts +71 -0
  150. package/dist/core/deliver-exclusions.d.ts.map +1 -0
  151. package/dist/core/deliver-exclusions.js +110 -0
  152. package/dist/core/deliver-exclusions.js.map +1 -0
  153. package/dist/core/deliver-text.d.ts +61 -14
  154. package/dist/core/deliver-text.d.ts.map +1 -1
  155. package/dist/core/deliver-text.js +178 -30
  156. package/dist/core/deliver-text.js.map +1 -1
  157. package/dist/core/deliver-triage.d.ts +11 -2
  158. package/dist/core/deliver-triage.d.ts.map +1 -1
  159. package/dist/core/deliver-triage.js +19 -1
  160. package/dist/core/deliver-triage.js.map +1 -1
  161. package/dist/core/deliver.d.ts +161 -3
  162. package/dist/core/deliver.d.ts.map +1 -1
  163. package/dist/core/deliver.js +463 -25
  164. package/dist/core/deliver.js.map +1 -1
  165. package/dist/core/deliverable-floor.d.ts.map +1 -1
  166. package/dist/core/deliverable-floor.js +5 -7
  167. package/dist/core/deliverable-floor.js.map +1 -1
  168. package/dist/core/engine-bus.d.ts +40 -0
  169. package/dist/core/engine-bus.d.ts.map +1 -0
  170. package/dist/core/engine-bus.js +27 -0
  171. package/dist/core/engine-bus.js.map +1 -0
  172. package/dist/core/estate-mcp-client.d.ts +87 -5
  173. package/dist/core/estate-mcp-client.d.ts.map +1 -1
  174. package/dist/core/estate-mcp-client.js +90 -11
  175. package/dist/core/estate-mcp-client.js.map +1 -1
  176. package/dist/core/linked-issues.d.ts +46 -0
  177. package/dist/core/linked-issues.d.ts.map +1 -0
  178. package/dist/core/linked-issues.js +123 -0
  179. package/dist/core/linked-issues.js.map +1 -0
  180. package/dist/core/run-identity.d.ts +100 -0
  181. package/dist/core/run-identity.d.ts.map +1 -0
  182. package/dist/core/run-identity.js +178 -0
  183. package/dist/core/run-identity.js.map +1 -0
  184. package/dist/core/shutdown.d.ts +16 -0
  185. package/dist/core/shutdown.d.ts.map +1 -0
  186. package/dist/core/shutdown.js +39 -0
  187. package/dist/core/shutdown.js.map +1 -0
  188. package/dist/core/types.d.ts +15 -1
  189. package/dist/core/types.d.ts.map +1 -1
  190. package/dist/core/types.js +4 -0
  191. package/dist/core/types.js.map +1 -1
  192. package/dist/interactive/bridge-pool.d.ts +67 -3
  193. package/dist/interactive/bridge-pool.d.ts.map +1 -1
  194. package/dist/interactive/bridge-pool.js +129 -13
  195. package/dist/interactive/bridge-pool.js.map +1 -1
  196. package/dist/interactive/bridge-root.d.ts +9 -0
  197. package/dist/interactive/bridge-root.d.ts.map +1 -1
  198. package/dist/interactive/bridge-root.js +11 -4
  199. package/dist/interactive/bridge-root.js.map +1 -1
  200. package/dist/interactive/chat-events.d.ts +14 -9
  201. package/dist/interactive/chat-events.d.ts.map +1 -1
  202. package/dist/interactive/chat-events.js +55 -57
  203. package/dist/interactive/chat-events.js.map +1 -1
  204. package/dist/interactive/council-outcome.d.ts +1 -0
  205. package/dist/interactive/council-outcome.d.ts.map +1 -1
  206. package/dist/interactive/council-outcome.js +33 -0
  207. package/dist/interactive/council-outcome.js.map +1 -1
  208. package/dist/interactive/draft-events.d.ts +36 -17
  209. package/dist/interactive/draft-events.d.ts.map +1 -1
  210. package/dist/interactive/draft-events.js +121 -92
  211. package/dist/interactive/draft-events.js.map +1 -1
  212. package/dist/interactive/draft-skill.d.ts +59 -0
  213. package/dist/interactive/draft-skill.d.ts.map +1 -1
  214. package/dist/interactive/draft-skill.js +233 -0
  215. package/dist/interactive/draft-skill.js.map +1 -1
  216. package/dist/interactive/edit-events.d.ts +1 -14
  217. package/dist/interactive/edit-events.d.ts.map +1 -1
  218. package/dist/interactive/edit-events.js +37 -70
  219. package/dist/interactive/edit-events.js.map +1 -1
  220. package/dist/interactive/proxy-routes.d.ts +2 -2
  221. package/dist/interactive/proxy-routes.d.ts.map +1 -1
  222. package/dist/interactive/proxy-routes.js +4 -13
  223. package/dist/interactive/proxy-routes.js.map +1 -1
  224. package/dist/interactive/ws-relay.d.ts +9 -10
  225. package/dist/interactive/ws-relay.d.ts.map +1 -1
  226. package/dist/interactive/ws-relay.js +23 -50
  227. package/dist/interactive/ws-relay.js.map +1 -1
  228. package/dist/mcp/broker.d.ts +88 -0
  229. package/dist/mcp/broker.d.ts.map +1 -0
  230. package/dist/mcp/broker.js +307 -0
  231. package/dist/mcp/broker.js.map +1 -0
  232. package/dist/mcp/call-records.d.ts +61 -0
  233. package/dist/mcp/call-records.d.ts.map +1 -0
  234. package/dist/mcp/call-records.js +135 -0
  235. package/dist/mcp/call-records.js.map +1 -0
  236. package/dist/mcp/classify.d.ts +45 -0
  237. package/dist/mcp/classify.d.ts.map +1 -0
  238. package/dist/mcp/classify.js +83 -0
  239. package/dist/mcp/classify.js.map +1 -0
  240. package/dist/mcp/discovery.d.ts +24 -0
  241. package/dist/mcp/discovery.d.ts.map +1 -0
  242. package/dist/mcp/discovery.js +78 -0
  243. package/dist/mcp/discovery.js.map +1 -0
  244. package/dist/mcp/invoke.d.ts +36 -0
  245. package/dist/mcp/invoke.d.ts.map +1 -0
  246. package/dist/mcp/invoke.js +80 -0
  247. package/dist/mcp/invoke.js.map +1 -0
  248. package/dist/mcp/policies.d.ts +110 -0
  249. package/dist/mcp/policies.d.ts.map +1 -0
  250. package/dist/mcp/policies.js +400 -0
  251. package/dist/mcp/policies.js.map +1 -0
  252. package/dist/mcp/probe.d.ts +66 -0
  253. package/dist/mcp/probe.d.ts.map +1 -0
  254. package/dist/mcp/probe.js +116 -0
  255. package/dist/mcp/probe.js.map +1 -0
  256. package/dist/mcp/redact.d.ts +34 -0
  257. package/dist/mcp/redact.d.ts.map +1 -0
  258. package/dist/mcp/redact.js +84 -0
  259. package/dist/mcp/redact.js.map +1 -0
  260. package/dist/mcp/registry-store.d.ts +69 -0
  261. package/dist/mcp/registry-store.d.ts.map +1 -0
  262. package/dist/mcp/registry-store.js +82 -0
  263. package/dist/mcp/registry-store.js.map +1 -0
  264. package/dist/mcp/registry.d.ts +147 -0
  265. package/dist/mcp/registry.d.ts.map +1 -0
  266. package/dist/mcp/registry.js +509 -0
  267. package/dist/mcp/registry.js.map +1 -0
  268. package/dist/mcp/rest.d.ts +111 -0
  269. package/dist/mcp/rest.d.ts.map +1 -0
  270. package/dist/mcp/rest.js +606 -0
  271. package/dist/mcp/rest.js.map +1 -0
  272. package/dist/mcp/secrets.d.ts +66 -0
  273. package/dist/mcp/secrets.d.ts.map +1 -0
  274. package/dist/mcp/secrets.js +180 -0
  275. package/dist/mcp/secrets.js.map +1 -0
  276. package/dist/mcp/tool-list.d.ts +25 -0
  277. package/dist/mcp/tool-list.d.ts.map +1 -0
  278. package/dist/mcp/tool-list.js +62 -0
  279. package/dist/mcp/tool-list.js.map +1 -0
  280. package/dist/mcp/upstream-error.d.ts +12 -0
  281. package/dist/mcp/upstream-error.d.ts.map +1 -0
  282. package/dist/mcp/upstream-error.js +14 -0
  283. package/dist/mcp/upstream-error.js.map +1 -0
  284. package/dist/mcp/usage.d.ts +31 -0
  285. package/dist/mcp/usage.d.ts.map +1 -0
  286. package/dist/mcp/usage.js +159 -0
  287. package/dist/mcp/usage.js.map +1 -0
  288. package/dist/presets/routes.d.ts +53 -0
  289. package/dist/presets/routes.d.ts.map +1 -0
  290. package/dist/presets/routes.js +115 -0
  291. package/dist/presets/routes.js.map +1 -0
  292. package/dist/projects/activity.d.ts +6 -18
  293. package/dist/projects/activity.d.ts.map +1 -1
  294. package/dist/projects/activity.js +17 -60
  295. package/dist/projects/activity.js.map +1 -1
  296. package/dist/projects/events.d.ts +6 -5
  297. package/dist/projects/events.d.ts.map +1 -1
  298. package/dist/projects/events.js +33 -58
  299. package/dist/projects/events.js.map +1 -1
  300. package/dist/projects/graph.d.ts +11 -0
  301. package/dist/projects/graph.d.ts.map +1 -1
  302. package/dist/projects/graph.js +12 -0
  303. package/dist/projects/graph.js.map +1 -1
  304. package/dist/projects/routes.d.ts.map +1 -1
  305. package/dist/projects/routes.js +19 -0
  306. package/dist/projects/routes.js.map +1 -1
  307. package/dist/projects/state-home-preflight.d.ts.map +1 -1
  308. package/dist/projects/state-home-preflight.js +7 -0
  309. package/dist/projects/state-home-preflight.js.map +1 -1
  310. package/dist/projects/state-home-registry.d.ts.map +1 -1
  311. package/dist/projects/state-home-registry.js +9 -1
  312. package/dist/projects/state-home-registry.js.map +1 -1
  313. package/dist/qe/acceptance.d.ts +28 -28
  314. package/dist/qe/acceptance.d.ts.map +1 -1
  315. package/dist/qe/acceptance.js +60 -61
  316. package/dist/qe/acceptance.js.map +1 -1
  317. package/dist/qe/test-sets.d.ts +3 -5
  318. package/dist/qe/test-sets.d.ts.map +1 -1
  319. package/dist/qe/test-sets.js +5 -7
  320. package/dist/qe/test-sets.js.map +1 -1
  321. package/dist/skills/installed.d.ts +51 -0
  322. package/dist/skills/installed.d.ts.map +1 -0
  323. package/dist/skills/installed.js +46 -0
  324. package/dist/skills/installed.js.map +1 -0
  325. package/dist/skills/phase-skill-gaps.d.ts +108 -0
  326. package/dist/skills/phase-skill-gaps.d.ts.map +1 -0
  327. package/dist/skills/phase-skill-gaps.js +199 -0
  328. package/dist/skills/phase-skill-gaps.js.map +1 -0
  329. package/dist/skills/runtime.d.ts +1 -1
  330. package/dist/skills/runtime.d.ts.map +1 -1
  331. package/dist/skills/runtime.js +2 -1
  332. package/dist/skills/runtime.js.map +1 -1
  333. package/dist/standing-orders/evaluator.d.ts +113 -0
  334. package/dist/standing-orders/evaluator.d.ts.map +1 -0
  335. package/dist/standing-orders/evaluator.js +225 -0
  336. package/dist/standing-orders/evaluator.js.map +1 -0
  337. package/dist/standing-orders/history.d.ts +58 -0
  338. package/dist/standing-orders/history.d.ts.map +1 -0
  339. package/dist/standing-orders/history.js +117 -0
  340. package/dist/standing-orders/history.js.map +1 -0
  341. package/dist/standing-orders/parse.d.ts +19 -0
  342. package/dist/standing-orders/parse.d.ts.map +1 -0
  343. package/dist/standing-orders/parse.js +75 -0
  344. package/dist/standing-orders/parse.js.map +1 -0
  345. package/dist/standing-orders/routes.d.ts +180 -0
  346. package/dist/standing-orders/routes.d.ts.map +1 -0
  347. package/dist/standing-orders/routes.js +95 -0
  348. package/dist/standing-orders/routes.js.map +1 -0
  349. package/dist/standing-orders/store.d.ts +154 -0
  350. package/dist/standing-orders/store.d.ts.map +1 -0
  351. package/dist/standing-orders/store.js +189 -0
  352. package/dist/standing-orders/store.js.map +1 -0
  353. package/dist/studio/assets/index-CvV5qITf.css +32 -0
  354. package/dist/studio/assets/index-jCOy6Ns7.js +568 -0
  355. package/dist/studio/index.html +3 -3
  356. package/dist/studio/testid-inventory.json +3307 -897
  357. package/dist/team/key.d.ts +5 -0
  358. package/dist/team/key.d.ts.map +1 -0
  359. package/dist/team/key.js +21 -0
  360. package/dist/team/key.js.map +1 -0
  361. package/dist/team/routes.d.ts +196 -0
  362. package/dist/team/routes.d.ts.map +1 -0
  363. package/dist/team/routes.js +274 -0
  364. package/dist/team/routes.js.map +1 -0
  365. package/dist/team/ws-relay.d.ts +39 -0
  366. package/dist/team/ws-relay.d.ts.map +1 -0
  367. package/dist/team/ws-relay.js +63 -0
  368. package/dist/team/ws-relay.js.map +1 -0
  369. package/endpoint-manifest.json +502 -6
  370. package/package.json +6 -6
  371. package/dist/interactive/demo-events.d.ts +0 -367
  372. package/dist/interactive/demo-events.d.ts.map +0 -1
  373. package/dist/interactive/demo-events.js +0 -1293
  374. package/dist/interactive/demo-events.js.map +0 -1
  375. package/dist/qe/gate-events.d.ts +0 -113
  376. package/dist/qe/gate-events.d.ts.map +0 -1
  377. package/dist/qe/gate-events.js +0 -172
  378. package/dist/qe/gate-events.js.map +0 -1
  379. package/dist/studio/assets/index-BRDAN35i.js +0 -548
  380. package/dist/studio/assets/index-Drfcsy8b.css +0 -32
@@ -1,4 +1,5 @@
1
1
  import { createRequire } from 'node:module';
2
+ import { attachEngineBus, detachEngineBus } from './bus.js';
2
3
  import { mkdir, access, readFile, writeFile, chmod, rm } from 'node:fs/promises';
3
4
  import { existsSync, readdirSync, readFileSync, renameSync } from 'node:fs';
4
5
  import { join, dirname, resolve, isAbsolute, relative, sep } from 'node:path';
@@ -8,11 +9,14 @@ import { randomUUID } from 'node:crypto';
8
9
  import { DEFAULT_SETTINGS } from './types.js';
9
10
  import { BASE_SKILL_REF_SHAPE } from '../skills/base-skill.js';
10
11
  import { execCapped } from './exec.js';
11
- import { BUG_FIX_SWEEP_INSTRUCTIONS, composeDeliverWorkflow, DELIVER_PHASE_ID, EVIDENCE_FLOOR_PIN } from './deliver.js';
12
+ import { BUG_FIX_SWEEP_INSTRUCTIONS, composeDeliverWorkflow, DELIVER_PHASE_ID, deliverPresetStep, deliverRepoFor, EVIDENCE_FLOOR_PIN, isGitHubLogin, readDeliverOriginUrl } from './deliver.js';
12
13
  import { engineCampaignDef, engineRosterJson } from './engine-roster.js';
13
14
  import { QE_AUTHOR_TESTS_WORKFLOW_DEF } from '../qe/author-workflow.js';
14
15
  import { CAMPAIGN_WORKFLOW_PREFIX } from '../campaigns/plan.js';
15
- import { composeDeliverableFloor, DELIVERABLE_FLOOR_PHASE_ID } from './deliverable-floor.js';
16
+ import { composeDeliverableFloor } from './deliverable-floor.js';
17
+ import { isSyntheticWorkflowId, resolveRunIdentity, verifiedEvidenceCatalog, wireIdentity, withPresetSystemFlag, withSystemFlag, } from './run-identity.js';
18
+ /** A run in one of these statuses never records another launch frame. */
19
+ const TERMINAL_RUN_STATUSES = new Set(['completed', 'cancelled', 'failed']);
16
20
  import { resolveProjectGraphBinding } from '../projects/graph.js';
17
21
  import { applyGovernanceStoreEnv, isStoreSpec } from './governance-store.js';
18
22
  /** Resolved path under the user's home directory. */
@@ -115,6 +119,44 @@ export function readOverlayWorkflows(dir, builtinIds) {
115
119
  }
116
120
  return out;
117
121
  }
122
+ /** Ask CORE's parser which drop-in defs it will actually honour (crew#718).
123
+ *
124
+ * {@link readOverlayWorkflows} checks a `{id, phases[]}` shape, which is not the engine's contract:
125
+ * `WorkflowRegistry::load_dir` runs `refuse_reserved_id` + `WorkflowDef::validate()` and SKIPS the
126
+ * file it rejects, loudly, on the daemon's stderr. So the catalog listed defs the engine had thrown
127
+ * away at boot and the launch then 400'd `unknown workflow` — the MCP S8 dogfood's F-3, on a
128
+ * hand-authored drop-in whose `executes_code` phase pinned no validator ("gate evaluates nothing").
129
+ *
130
+ * The same doctrine {@link CoreAdapter.registerWorkflow} states for the WRITE path applies here:
131
+ * core's parser is the authority, so it is what we ask. Enumerating `validate()`'s rules in
132
+ * TypeScript would be a second copy of core's schema — the drift that produced this defect.
133
+ * `register` is the engine's `registerWorkflow` binding, which validates before it registers and
134
+ * is idempotent on id, so re-offering a def the engine already loaded at boot is a same-content
135
+ * overwrite; a def added to the dir after boot becomes launchable rather than being listed and
136
+ * refused.
137
+ *
138
+ * `is_system` is stripped first, exactly as the two write paths do: it is crew's own display flag
139
+ * and core's strict def parser rejects the key.
140
+ *
141
+ * Pure (no adapter state) so it is unit-testable without spawning a Core, like
142
+ * {@link readOverlayWorkflows}.
143
+ */
144
+ export async function judgeOverlayWorkflows(defs, register) {
145
+ const accepted = [];
146
+ const refused = [];
147
+ for (const def of defs) {
148
+ const overlayDef = { ...def };
149
+ delete overlayDef.is_system;
150
+ try {
151
+ await register(JSON.stringify(overlayDef));
152
+ accepted.push(def);
153
+ }
154
+ catch (err) {
155
+ refused.push({ id: def.id, reason: err instanceof Error ? err.message : String(err) });
156
+ }
157
+ }
158
+ return { accepted, refused };
159
+ }
118
160
  // The native addon is a CommonJS cdylib (`index.node`); load it with a CJS
119
161
  // require even though this daemon is ESM. This module is the ONLY place that
120
162
  // touches wicked-core-ts (DES-STUDIO-001 §5.2/§5.3), so the FINALIZING
@@ -188,6 +230,23 @@ function locateWickedCoreExe() {
188
230
  return candidates.find((p) => existsSync(p));
189
231
  }
190
232
  const { Core } = require('wicked-core-ts');
233
+ /**
234
+ * Does the linked engine carry DES-TEAMING-002 T3's plan launch (`Core.supportsPlanLaunch`)? A
235
+ * plan handed to an addon without it would be silently ignored — the run launches as free text,
236
+ * with no `plan.proposed` and no `plan_approval` gate — so every plan path fails CLOSED on this.
237
+ * Read at call time (not cached): the addon is resolved once, but tests swap the static.
238
+ */
239
+ export function engineSupportsPlanLaunch() {
240
+ return typeof Core.supportsPlanLaunch === 'function' && Core.supportsPlanLaunch() === true;
241
+ }
242
+ /** A plan path on an engine without the plan approval gate (maps to 501 at the HTTP boundary). */
243
+ export class PlanLaunchUnsupportedError extends Error {
244
+ constructor(what) {
245
+ super(`${what} needs a wicked-core-ts that carries the plan approval gate (Core.supportsPlanLaunch, ` +
246
+ `DES-TEAMING-002 T3); the installed addon would drop the plan and run the launch ungated`);
247
+ this.name = 'PlanLaunchUnsupportedError';
248
+ }
249
+ }
191
250
  /**
192
251
  * Does the installed wicked-core-ts addon understand `LaunchOptions.extraWriteRoots` (≥ 0.6.1)?
193
252
  *
@@ -267,15 +326,16 @@ function addonAtLeast(maj, min, pat) {
267
326
  }
268
327
  }
269
328
  // ── Built-in workflow definitions (crew#44) ──────────────────────────────────
270
- // Static mirrors of wicked-core workflow defs: feature, bug, migration, survey-repo,
271
- // domain-graph-slice, memories, collab, onboarding, chat, and domain-extraction.
329
+ // Static mirrors of wicked-core workflow defs: feature, bug, migration and domain-extraction, plus
330
+ // crew's own defs. `chat` and `onboarding` are engine built-in PRESETS (DES-TEAMING-002 M3/M4),
331
+ // launched by the same name with no def here; `survey-repo`, `memories`, `domain-graph-slice` and
332
+ // `collab` are deleted (nothing launched them).
272
333
  // Swap for `this.core.listWorkflowsJson()` / `this.core.getWorkflowJson(id)` once
273
334
  // the wicked-core-ts NAPI methods land.
274
335
  /**
275
336
  * The ids wicked-core seeds itself, in `WorkflowRegistry::with_defaults()`.
276
337
  *
277
- * `launchRun`'s generic drop-in overlay write SKIPS every id in this set (`onboarding` is written
278
- * by the onboarding path instead — see the end of this comment, it is the one deliberate exception).
338
+ * `launchRun`'s generic drop-in overlay write SKIPS every id in this set.
279
339
  * A file in that dir shadows the compiled built-in
280
340
  * *wholesale* — `register` overwrites by id and `load_dir` runs after `with_defaults` — so writing
281
341
  * this hand-transcribed mirror over the real def silently replaces it with a copy missing whatever
@@ -284,16 +344,10 @@ function addonAtLeast(maj, min, pat) {
284
344
  * `bug.verify` and `migration.verify` — the entire content of core's gate-floor change, undone by a
285
345
  * file write, with no error and a workflow still reporting the right id and phases (FINDING-049).
286
346
  *
287
- * The write exists for the ids core does NOT seed (chat, survey-repo, domain-graph-slice,
288
- * memories, domain-extraction): for those the overlay is the only reason they resolve at all, so
289
- * it stays.
290
- *
291
- * The exception: `onboarding` is core-seeded AND still written, by the onboarding path rather than
292
- * by the generic one. Deliberate — that def's executor cmds are baked with runtime `--db` paths, so
293
- * it shadows core's copy with a real customization rather than a stale transcription. It is the one
294
- * shadow that earns its keep, and the reason this set gates the generic write specifically.
347
+ * The write exists for the ids core does NOT seed (domain-extraction and crew's own defs): for
348
+ * those the overlay is the only reason they resolve at all, so it stays.
295
349
  */
296
- const CORE_SEEDED_WORKFLOWS = new Set(['feature', 'bug', 'migration', 'onboarding', 'collab']);
350
+ const CORE_SEEDED_WORKFLOWS = new Set(['feature', 'bug', 'migration']);
297
351
  // `EVIDENCE_FLOOR_PIN` (imported from ./deliver.js, defined once) is carried on the Evaluator phase
298
352
  // of feature/bug/migration AND, since wicked-core F-039, on their code-writing Creator phases
299
353
  // (`build`/`fix`/`execute`) — so the gate that was supposed to make the change re-derives its diff
@@ -305,33 +359,9 @@ const CORE_SEEDED_WORKFLOWS = new Set(['feature', 'bug', 'migration', 'onboardin
305
359
  // `tests/armed-workflow-served.test.ts` and `tests/builtin-overlay-shadow.test.ts` fail loudly
306
360
  // the moment core's defs move. As of FINDING-049 these defs are never written to core's overlay dir
307
361
  // (see CORE_SEEDED_WORKFLOWS).
362
+ // `is_system` is NOT spelled on these defs: `withSystemFlag` stamps it from `SYSTEM_WORKFLOWS`
363
+ // (core/run-identity.ts), the one list keyed by name that also classifies served runs (seam X2).
308
364
  export const BUILTIN_WORKFLOWS = [
309
- {
310
- id: 'chat',
311
- is_system: true,
312
- phases: [
313
- { id: 'explore', kind: 'recon', gate_type: 'value', gate: 'auto', executes_code: false, verified_evidence: false, required_deliverables: [], depends_on: [], role: 'neutral', skill_ref: null, allowed_skills: [], validator_pin: null },
314
- ],
315
- },
316
- {
317
- id: 'onboarding',
318
- is_system: true,
319
- phases: [
320
- { id: 'index', executor: { type: 'tool', cmd: ['wicked-estate', 'index', '{repo_root}', '--db', '{code_graph_db}'] }, kind: 'recon', gate_type: 'value', gate: 'auto', executes_code: false, verified_evidence: false, required_deliverables: [], depends_on: [], role: 'neutral', skill_ref: null, allowed_skills: [], validator_pin: null },
321
- { id: 'annotate', executor: { type: 'tool', cmd: ['wicked-estate', 'clusters', '--annotate', '--db', '{code_graph_db}'] }, kind: 'recon', gate_type: 'value', gate: 'auto', executes_code: false, verified_evidence: false, required_deliverables: [], depends_on: ['index'], role: 'neutral', skill_ref: null, allowed_skills: [], validator_pin: null },
322
- // index → annotate, and NOT a third `domain` phase running `wicked-core domain-graph`. That
323
- // phase could never pass: domain-graph fails closed below 1.0 front-half coverage, and nothing
324
- // in this workflow annotates a single symbol, so coverage was 0.0 on every repo — every
325
- // registration ended sessionFailed after the two phases that matter had both succeeded
326
- // (FINDING-068). domain-graph belongs to `domain-extraction`, downstream of the agentic
327
- // extract+coverage phases that produce its precondition. Mirrors core's `onboarding_def()`.
328
- //
329
- // The `{repo_root}` / `{code_graph_db}` placeholders are core's, substituted per run from the
330
- // launch's `repoRef` (wicked-core#179). This package used to bake absolute paths in here and
331
- // write the result to one shared overlay file per launch — which concurrent registrations
332
- // raced, indexing one repo's tree under another repo's name (FINDING-075, #196).
333
- ],
334
- },
335
365
  {
336
366
  id: 'feature',
337
367
  phases: [
@@ -365,22 +395,6 @@ export const BUILTIN_WORKFLOWS = [
365
395
  { id: 'cleanup', kind: 'build', gate_type: null, gate: 'auto', executes_code: false, verified_evidence: false, required_deliverables: [], depends_on: ['verify'], role: 'neutral', skill_ref: null, allowed_skills: [], validator_pin: null },
366
396
  ],
367
397
  },
368
- {
369
- // MUST stay byte-identical to wicked-core/workflows/survey-repo.json — crew's overlay write is the
370
- // ONLY def the engine resolves at runtime (core does not seed survey-repo), so a stale mirror here
371
- // silently runs the OLD def. The pre-fix mirror carried 3 phases with no `instructions` and no
372
- // `synthesize`, so survey-repo ran 3 near-identical prompts and produced no run-level synthesis —
373
- // exactly FINDING-011, still live because the fix only landed in the core JSON the runtime ignores.
374
- // Guarded by builtin-overlay-shadow.test.ts (survey-repo is now in MIRRORED_IDS).
375
- id: 'survey-repo',
376
- is_system: true,
377
- phases: [
378
- { id: 'structure', kind: 'recon', instructions: 'Map the repository layout only: top-level directories, entry points, and where source, tests, config, and docs live. Do not analyze languages, dependencies, or conventions — later phases cover those.', gate_type: 'value', gate: 'auto', executes_code: false, verified_evidence: false, required_deliverables: [], depends_on: [], role: 'neutral', skill_ref: null, allowed_skills: [], validator_pin: null },
379
- { id: 'stack', kind: 'recon', instructions: 'Identify the technology stack from the manifests (package.json, Cargo.toml, pyproject.toml, ...): languages, frameworks, build tools, key dependencies. Build on the structure summary provided as prior context; do not re-map the layout.', gate_type: 'value', gate: 'auto', executes_code: false, verified_evidence: false, required_deliverables: [], depends_on: ['structure'], role: 'neutral', skill_ref: null, allowed_skills: [], validator_pin: null },
380
- { id: 'conventions', kind: 'recon', instructions: 'Identify the working conventions: naming, module boundaries, test placement and style, lint/format configuration, CI expectations. Build on the prior phases\' outputs provided as context; do not re-survey structure or stack.', gate_type: 'value', gate: 'auto', executes_code: false, verified_evidence: false, required_deliverables: [], depends_on: ['stack'], role: 'neutral', skill_ref: null, allowed_skills: [], validator_pin: null },
381
- { id: 'synthesize', kind: 'recon', instructions: 'Do not re-survey the repository. Merge the three prior phase outputs provided as context into one coherent survey — structure, then stack, then conventions — resolving overlaps and flagging any contradictions between them.', gate_type: 'value', gate: 'auto', executes_code: false, verified_evidence: false, required_deliverables: [], depends_on: ['structure', 'stack', 'conventions'], role: 'neutral', skill_ref: null, allowed_skills: [], validator_pin: null },
382
- ],
383
- },
384
398
  {
385
399
  // capture-learnings (DES-MEM-FACETED-001 write side, onboarding): survey a just-indexed repo,
386
400
  // then propose its durable learnings — BOTH faceted MEMORIES and repo POLICIES — as inert estate
@@ -400,7 +414,7 @@ export const BUILTIN_WORKFLOWS = [
400
414
  // SINGLE `capture` phase emits both from the one shared understanding — splitting derive-
401
415
  // memories / derive-policies would re-run a council over the same context for no new evidence.
402
416
  // • Reuse already lives below the run: the reusable unit is the SKILL (and hotspot-read is
403
- // already a reusable capability via `wicked-garden-search`; survey via `survey-repo`).
417
+ // already a reusable capability via `wicked-garden-search`).
404
418
  //
405
419
  // The METHOD lives in the garden skill `wicked-garden-repo-learn`, referenced per-phase by
406
420
  // `skill_ref` — the engine emits only a short `Invoke your skill "wicked-garden:repo-learn"…`
@@ -419,28 +433,16 @@ export const BUILTIN_WORKFLOWS = [
419
433
  // so proposals land in the same queue the studio Memories/Policies surfaces review. Onboarding IS
420
434
  // about the repo, so the skill tags learnings `repo:`/`project:`.
421
435
  id: 'capture-learnings',
422
- is_system: true,
423
436
  phases: [
424
437
  { id: 'churn', kind: 'recon', instructions: "Phase 1/3 CHURN: produce a ranked list of this repo's most actively-changed files and directories over the last ~12 months, plus the repo's real name (manifest or git remote) and parent project. Use the skill's bounded/sampled git-churn method — never stream the whole history. Do not read code deeply yet; the next phase targets these areas.", gate_type: 'value', gate: 'auto', executes_code: false, verified_evidence: false, required_deliverables: [], depends_on: [], role: 'neutral', skill_ref: 'wicked-garden-repo-learn', allowed_skills: [], validator_pin: null },
425
438
  { id: 'hotspots', kind: 'recon', instructions: "Phase 2/3 HOTSPOTS: cross-reference the prior churn ranking with wicked-estate hotspot / blast-radius signals to find the load-bearing code, then READ it through the estate shim (`wicked-garden run scripts/_estate_client.py --readonly call …`, the skill's grounding path) to build a real technical understanding of how the system fits together — not a file listing. Reuse wicked-garden-search for the hotspot signals; follow the skill.", gate_type: 'value', gate: 'auto', executes_code: false, verified_evidence: false, required_deliverables: [], depends_on: ['churn'], role: 'neutral', skill_ref: 'wicked-garden-repo-learn', allowed_skills: [], validator_pin: null },
426
- { id: 'capture', kind: 'build', instructions: "Phase 3/3 CAPTURE: from the prior churn + hotspot understanding, submit durable learnings as estate proposals through the shim's `propose` per the skill's capture contract — BOTH memories (facts / how-it-works) and policies (enforced conventions), one proposal per item, tagged repo/project. Each is inert until human review; never include secrets or personal data; capturing nothing is acceptable.", gate_type: 'value', gate: 'auto', executes_code: false, verified_evidence: false, required_deliverables: [], depends_on: ['hotspots'], role: 'creator', skill_ref: 'wicked-garden-repo-learn', allowed_skills: [], validator_pin: null },
427
- ],
428
- },
429
- {
430
- id: 'domain-graph-slice',
431
- is_system: true,
432
- phases: [
433
- { id: 'identify', kind: 'recon', gate_type: 'value', gate: 'auto', executes_code: false, verified_evidence: false, required_deliverables: [], depends_on: [], role: 'neutral', skill_ref: null, allowed_skills: [], validator_pin: null },
434
- { id: 'extract', kind: 'build', gate_type: 'value', gate: 'auto', executes_code: false, verified_evidence: false, required_deliverables: [], depends_on: ['identify'], role: 'creator', skill_ref: null, allowed_skills: [], validator_pin: null },
435
- { id: 'validate', kind: 'review', gate_type: 'value', gate: 'auto', executes_code: false, verified_evidence: false, required_deliverables: [], depends_on: ['extract'], role: 'evaluator', skill_ref: null, allowed_skills: [], validator_pin: null },
436
- ],
437
- },
438
- {
439
- id: 'memories',
440
- is_system: true,
441
- phases: [
442
- { id: 'gather', kind: 'recon', gate_type: 'value', gate: 'auto', executes_code: false, verified_evidence: false, required_deliverables: [], depends_on: [], role: 'neutral', skill_ref: null, allowed_skills: [], validator_pin: null },
443
- { id: 'store', kind: 'build', gate_type: 'value', gate: 'auto', executes_code: false, verified_evidence: false, required_deliverables: [], depends_on: ['gather'], role: 'creator', skill_ref: null, allowed_skills: [], validator_pin: null },
439
+ // (BC-80, wicked-core#535) `requires_capture_report`: the engine's capture-report floor reads
440
+ // the phase's output marker, so a capture run whose skill loaded and never ran can no longer
441
+ // report `completed` with 0 proposals — a missing marker, a failed submission and proposals
442
+ // derived but never submitted each deny into the human gate. An honest 0 passes. The
443
+ // instruction MANDATES the marker; garden's repo-learn skill carries the same contract, and
444
+ // the counts land on the unit (`capture_report`), which `GET /runs/:id` serves.
445
+ { id: 'capture', kind: 'build', instructions: "Phase 3/3 CAPTURE: from the prior churn + hotspot understanding, submit durable learnings as estate proposals through the shim's `propose` per the skill's capture contract — BOTH memories (facts / how-it-works) and policies (enforced conventions), one proposal per item, tagged repo/project. Each is inert until human review; never include secrets or personal data. END with `wicked-capture-report {\"derived\": N, \"submitted\": M, \"failed\": K}` — always, even on a degrade or a legitimate 0 (which is acceptable).", gate_type: 'value', gate: 'auto', executes_code: false, requires_capture_report: true, verified_evidence: false, required_deliverables: [], depends_on: ['hotspots'], role: 'creator', skill_ref: 'wicked-garden-repo-learn', allowed_skills: [], validator_pin: null },
444
446
  ],
445
447
  },
446
448
  {
@@ -458,26 +460,15 @@ export const BUILTIN_WORKFLOWS = [
458
460
  // as the launch's extra write root) — because parsing the proposal out of prose transcript
459
461
  // is a fallback, not a design (the landing reads the file first).
460
462
  //
461
- // Crew-authored drop-in (like `chat`): NOT in CORE_SEEDED_WORKFLOWS, so launchRun's
463
+ // Crew-authored drop-in: NOT in CORE_SEEDED_WORKFLOWS, so launchRun's
462
464
  // `_writeBuiltinOverlay` write is the only way core resolves the id — the same delivery
463
465
  // mechanism every crew drop-in uses.
464
466
  id: 'steering-author',
465
- is_system: true,
466
467
  phases: [
467
468
  { id: 'analyze', kind: 'recon', instructions: 'Read the operator intent and every file or directory listed in the problem statement. Identify candidate steering rules: durable, prescriptive statements a coding agent must follow, each classified into one steering type (architecture, development, security, testing, operations, compliance, design-ux). For each candidate note the statement, steering type, severity, and the evidence in the source material. Analysis only — do not write any rule to any store, and do not emit final rule JSON yet.', gate_type: 'value', gate: 'auto', executes_code: false, verified_evidence: false, required_deliverables: [], depends_on: [], role: 'neutral', skill_ref: null, allowed_skills: [], validator_pin: null },
468
469
  { id: 'propose', kind: 'recon', instructions: 'From the prior analysis, emit the PROPOSED steering rules as one JSON array. Each entry is a conformance-rule object: id (PAT-<digits> for rule_type "pattern", POL-<digits> for "policy"), rule_type, statement, severity (info|warn|error|critical), confidence (a NUMBER 0..1), steering_type (default to the type named in the problem statement), provenance {"source":"chat"}, and — only where the source material supports them — the enforcement fields applies_to (array of phase tokens or globs), excludes, weight, obligations (array of strings), criteria (ONE string, never a list). Omit targets, effect and trigger unless you can express them in the store schema exactly: targets is a {language, layer, framework} facet OBJECT (never a file list — files belong in applies_to), and trigger is a structured condition object (never prose). SAVE that JSON array (bare array, no prose, no code fences) to the absolute proposal file path named in the problem statement (create parent directories if needed, overwrite if present), AND include the same array in your reply for the human reviewer. This output is a PROPOSAL for the human gate: the proposal file is an artifact for review, and rules land in the governance store only after approval, written crew-side — do not write any rule to any store yourself.', gate_type: 'value', gate: { human_confirm: { unconditional: true } }, executes_code: false, verified_evidence: false, required_deliverables: [], depends_on: ['analyze'], role: 'creator', skill_ref: null, allowed_skills: [], validator_pin: null },
469
470
  ],
470
471
  },
471
- {
472
- id: 'collab',
473
- is_system: true,
474
- phases: [
475
- { id: 'propose', kind: 'recon', gate_type: 'value', gate: 'auto', executes_code: false, verified_evidence: false, required_deliverables: [], depends_on: [], role: 'creator', skill_ref: null, allowed_skills: [], validator_pin: null },
476
- { id: 'critique', kind: 'review', gate_type: 'value', gate: 'auto', executes_code: false, verified_evidence: false, required_deliverables: [], depends_on: ['propose'], role: 'evaluator', skill_ref: null, allowed_skills: [], validator_pin: null },
477
- { id: 'revise', kind: 'recon', gate_type: 'strategy', gate: 'auto', executes_code: false, verified_evidence: false, required_deliverables: [], depends_on: ['critique'], role: 'creator', skill_ref: null, allowed_skills: [], validator_pin: null },
478
- { id: 'verdict', kind: 'review', gate_type: 'value', gate: { human_confirm: { unconditional: false } }, executes_code: false, verified_evidence: false, required_deliverables: [], depends_on: ['revise'], role: 'evaluator', skill_ref: null, allowed_skills: [], validator_pin: null },
479
- ],
480
- },
481
472
  // The governed test-authoring workflow (wave 6 — F-7R2-003/004/005/012/014/015, R4-r2): recon →
482
473
  // author (creator, evidence-floor pinned) → verify (a TOOL phase that RUNS every produced test
483
474
  // under the repository's own harness and fails the unit when one fails or never ran) → review
@@ -527,7 +518,7 @@ export const BUILTIN_WORKFLOWS = [
527
518
  { id: 'domain-graph', executor: { type: 'tool', cmd: ['wicked-core', 'domain-graph', '--db', '{code_graph_db}', '--out', 'requirements_graph.json'] }, kind: 'build', gate_type: 'strategy', gate: { human_confirm: { unconditional: false } }, executes_code: false, verified_evidence: false, required_deliverables: [], depends_on: ['coverage'], role: 'neutral', skill_ref: null, allowed_skills: [], validator_pin: null },
528
519
  ],
529
520
  },
530
- ];
521
+ ].map(withSystemFlag);
531
522
  /**
532
523
  * Chat is not available in this deployment at all — a capability gap, never a bad request.
533
524
  *
@@ -557,6 +548,26 @@ export class ProjectsUnsupportedError extends Error {
557
548
  this.name = 'ProjectsUnsupportedError';
558
549
  }
559
550
  }
551
+ /**
552
+ * The preset bindings (DES-TEAMING-002 seam C2) are not in the installed `wicked-core-ts`. The
553
+ * routes answer 501 ("upgrade the engine"), never 400.
554
+ */
555
+ export class PresetsUnsupportedError extends Error {
556
+ constructor(what) {
557
+ super(`${what} is not supported by this wicked-core build (needs the wicked-core-ts release carrying presets)`);
558
+ this.name = 'PresetsUnsupportedError';
559
+ }
560
+ }
561
+ /**
562
+ * A team binding (DES-TEAMING-002 T8) is not in the installed `wicked-core-ts`. The routes answer
563
+ * 501 ("upgrade the engine"), never 400.
564
+ */
565
+ export class TeamUnsupportedError extends Error {
566
+ constructor(what, binding) {
567
+ super(`${what} is not supported by this wicked-core build (needs the wicked-core-ts binding Core.${binding})`);
568
+ this.name = 'TeamUnsupportedError';
569
+ }
570
+ }
560
571
  /**
561
572
  * `resolveElicitation` is not available in this deployment — the installed `wicked-core-ts`
562
573
  * predates the binding (< 0.7.2). Feature-detected by method presence, the
@@ -704,6 +715,38 @@ function quarantineStaleOnboardingOverlay() {
704
715
  `whatever repo the run names (FINDING-075).`);
705
716
  }
706
717
  }
718
+ /**
719
+ * The drop-in ids crew wrote into the overlay dir before DES-TEAMING-002 wave 1 retired them:
720
+ * `chat` is the engine's built-in preset now, and `survey-repo`, `memories` and
721
+ * `domain-graph-slice` are deleted. Exported for the quarantine test.
722
+ */
723
+ export const RETIRED_OVERLAY_IDS = ['chat', 'survey-repo', 'memories', 'domain-graph-slice'];
724
+ /**
725
+ * Park the overlay files older crews wrote for {@link RETIRED_OVERLAY_IDS}.
726
+ *
727
+ * `_writeBuiltinOverlay` wrote each of these on first launch, and the overlay dir is PERSISTENT: on
728
+ * an upgraded host the engine's startup `load_dir` would keep the deleted workflows launchable, and
729
+ * `hydrateFromOverlay` would serve them on `GET /workflows` as user workflows, where they are no
730
+ * longer on the system list (review of wicked-crew#688). Renamed, not deleted, like the onboarding
731
+ * quarantine above: the evidence stays, and a name retired by operator decision (2026-09-26) is
732
+ * not a workflow any more — save a preset instead.
733
+ */
734
+ function quarantineRetiredOverlays() {
735
+ for (const id of RETIRED_OVERLAY_IDS) {
736
+ const stale = join(workflowOverlayDir(), `${id}.json`);
737
+ if (!existsSync(stale))
738
+ continue;
739
+ const parked = `${stale}.retired-des-teaming-002`;
740
+ try {
741
+ renameSync(stale, parked);
742
+ console.warn(`[workflows] parked ${stale} → ${parked}: \`${id}\` is retired (DES-TEAMING-002 wave 1` +
743
+ `${id === 'chat' ? '; chat is the engine\'s built-in preset' : '; the workflow is deleted'}).`);
744
+ }
745
+ catch (err) {
746
+ console.error(`[workflows] FAILED to park the retired overlay ${stale}: ${err instanceof Error ? err.message : String(err)}. The engine will keep resolving \`${id}\` from it until it is removed by hand.`);
747
+ }
748
+ }
749
+ }
707
750
  /** The ids of a workflow's phases that carry a HUMAN gate (`human_confirm` unconditional, or the
708
751
  * conditional `human_confirm_if`) — i.e. the phases that will PAUSE for a person.
709
752
  *
@@ -737,8 +780,17 @@ export class CoreAdapter {
737
780
  dbPath;
738
781
  /** `true` when this adapter armed the event-driven exec seam (for readiness/reporting). */
739
782
  engineExec;
740
- /** The bus db the exec seam runs over when armed (else `undefined`). */
783
+ /** The bus db handed to the engine (`WICKED_BUS_DB`; exec mediation runs over it when armed), or
784
+ * `undefined` when this adapter handed none. */
741
785
  busDbPath;
786
+ /** Why the engine has no usable bus — the boot probe's failure, or the engine could not arm its
787
+ * bus bridge on the handed bus within its bound — else `null`. */
788
+ busUnavailable;
789
+ /** The bus file whose seams are off because the linked engine has no `Core.busEmit`/`busRead`
790
+ * (wicked-core#631) — `/health.warnings` says so — else `null`. */
791
+ busSeamsOff = null;
792
+ /** The engine's bus calls, attached for `busDbPath` (core/bus.ts); `null` without a bus. */
793
+ engineBus = null;
742
794
  /**
743
795
  * `true` when this adapter drives the DETERMINISTIC OFFLINE engine (`Core.spawnStub`) rather
744
796
  * than the production one — i.e. the `StubDispatcher` (every seat votes for the first roster
@@ -797,6 +849,12 @@ export class CoreAdapter {
797
849
  launchRoster() {
798
850
  return this.rosterProvider !== null ? this.rosterProvider() : CoreAdapter.roster();
799
851
  }
852
+ /** {@link launchRoster} after the seats' login checks answered (bounded, crew#645): an expired
853
+ * login reads signed out before the launch routes work to it. */
854
+ async readyLaunchRoster() {
855
+ await this.rosterProvider?.ready?.();
856
+ return this.launchRoster();
857
+ }
800
858
  constructor(opts) {
801
859
  // Arm the EVENT-DRIVEN execution-mediation seam BEFORE spawning the Core: the Rust actor reads
802
860
  // `WICKED_BUS_EXEC` + `WICKED_BUS_DB` from the process env at spawn time (wicked-core actor::run),
@@ -807,16 +865,29 @@ export class CoreAdapter {
807
865
  // BEFORE the Core spawns: the actor reads the overlay dir at startup, so a stale
808
866
  // `onboarding.json` has to be out of the way by then or it shadows the built-in def.
809
867
  quarantineStaleOnboardingOverlay();
868
+ quarantineRetiredOverlays();
869
+ // DES-TEAMING-002 T0: the engine gets the daemon's bus on EVERY boot (`WICKED_BUS_DB`); exec
870
+ // mediation stays a separate switch (`WICKED_BUS_EXEC`), set only under `engineExec`, and the
871
+ // engine's bus gate-judge path follows that switch — never the mere presence of a bus.
810
872
  const armExec = opts.engineExec === true;
811
- if (armExec) {
812
- if (!opts.busDbPath || opts.busDbPath.length === 0) {
813
- throw new Error('engineExec requires busDbPath (the wicked-bus db to mediate execution over)');
814
- }
815
- process.env['WICKED_BUS_EXEC'] = '1';
816
- process.env['WICKED_BUS_DB'] = opts.busDbPath;
873
+ const busDbPath = opts.busDbPath !== undefined && opts.busDbPath.length > 0 ? opts.busDbPath : undefined;
874
+ if (armExec && busDbPath === undefined) {
875
+ throw new Error('engineExec requires busDbPath (the wicked-bus db to mediate execution over)');
817
876
  }
877
+ // process.env matches the decision EXACTLY before the engine spawns: the engine reads both
878
+ // variables itself, so an inherited value the decision did not hand it (an unopenable bus crew
879
+ // declined, an exec switch crew did not arm) would otherwise reach it anyway — fail-open.
880
+ if (busDbPath !== undefined)
881
+ process.env['WICKED_BUS_DB'] = busDbPath;
882
+ else
883
+ delete process.env['WICKED_BUS_DB'];
884
+ if (armExec)
885
+ process.env['WICKED_BUS_EXEC'] = '1';
886
+ else
887
+ delete process.env['WICKED_BUS_EXEC'];
818
888
  this.engineExec = armExec;
819
- this.busDbPath = armExec ? opts.busDbPath : undefined;
889
+ this.busDbPath = busDbPath;
890
+ this.busUnavailable = busDbPath === undefined ? (opts.busUnavailable ?? null) : null;
820
891
  // Give the Rust actor the path to the wicked-core standalone binary so the gate-hook
821
892
  // command works when wicked-core is loaded as a napi-rs addon (where current_exe()
822
893
  // returns the Node.js interpreter, not wicked-core). The actor checks WICKED_CORE_EXE
@@ -836,6 +907,34 @@ export class CoreAdapter {
836
907
  this.stub = opts.stub === true;
837
908
  this.dbPath = opts.dbPath;
838
909
  this.core = this.stub ? Core.spawnStub(opts.dbPath) : Core.spawn(opts.dbPath);
910
+ // DES-TEAMING-002 T0: `spawn` returns only once the engine's bus bridge is armed, or has given up
911
+ // within its bound. Not armed = the engine launches nothing from the bus: say so on the same
912
+ // `bus.unavailable` path as a bus crew could not open.
913
+ if (this.busDbPath !== undefined && typeof this.core.busBridgeState === 'function') {
914
+ const state = JSON.parse(this.core.busBridgeState());
915
+ if (state.state === 'not-armed') {
916
+ const reason = `the engine could not arm its bus bridge: ${state.reason ?? 'no reason given'}`;
917
+ this.busUnavailable = { dbPath: this.busDbPath, reason: state.reason ?? 'no reason given', kind: 'bridge_not_armed' };
918
+ console.error(`[crew] bus unavailable: ${this.busDbPath} (${reason}) — the engine launches nothing from the bus`);
919
+ }
920
+ }
921
+ // wicked-core#631: crew's seams write and read this bus through the engine that holds it —
922
+ // crew opens no SQLite of its own (core/bus.ts). An engine without the bus calls leaves every
923
+ // seam unarmed (each logs why); crew does not fall back to a second library.
924
+ if (this.busDbPath !== undefined) {
925
+ const core = this.core;
926
+ if (typeof core.busEmit === 'function' && typeof core.busRead === 'function') {
927
+ this.engineBus = {
928
+ busEmit: (eventJson) => core.busEmit(eventJson),
929
+ busRead: (afterId, limit, typePrefix, includeExpired) => core.busRead(afterId, limit, typePrefix, includeExpired),
930
+ };
931
+ attachEngineBus(this.busDbPath, this.engineBus);
932
+ }
933
+ else {
934
+ this.busSeamsOff = this.busDbPath;
935
+ console.error(`[crew] the linked wicked-core-ts has no Core.busEmit/busRead (wicked-core#631) — crew's bus seams on ${this.busDbPath} stay off; upgrade the engine`);
936
+ }
937
+ }
839
938
  // The ONE subscribe() for the process. Error-first callback (index.d.ts:56):
840
939
  // one JSON string per CoreEvent, in emission order. A throw in a listener is
841
940
  // isolated so one bad consumer can never stall the pump or the others.
@@ -866,6 +965,37 @@ export class CoreAdapter {
866
965
  * store FILE that does not exist yet counts as 0: nothing has landed, which is a number, not an
867
966
  * unknown (the engine's read-only open refuses a missing file, and that refusal is not "unknown").
868
967
  */
968
+ /**
969
+ * The engine's MCP broker gate (DES-MCP-TOOLS-001 §6 steps 3 and 8), or `null` on an addon without
970
+ * BOTH `Core.evaluateMcpCall` and `Core.evaluateMcpOutput`: the broker then refuses every call
971
+ * (`guard_error`), never runs one unjudged. Read at call time: tests swap the statics.
972
+ */
973
+ static mcpEngineGate() {
974
+ const call = Core.evaluateMcpCall;
975
+ const output = Core.evaluateMcpOutput;
976
+ if (typeof call !== 'function' || typeof output !== 'function')
977
+ return null;
978
+ return {
979
+ evaluateCall: (requestJson) => call.call(Core, requestJson),
980
+ evaluateOutput: (requestJson) => output.call(Core, requestJson),
981
+ };
982
+ }
983
+ /**
984
+ * The engine's tool list for a token's unit (`Core.listMcpTools`, DES-MCP-TOOLS-001 §8, slice S4),
985
+ * or `null` on an addon without it: `POST /mcp/tools` then answers 503, never an unjudged list.
986
+ * Read at call time: tests swap the static.
987
+ */
988
+ static mcpToolLister() {
989
+ const list = Core.listMcpTools;
990
+ if (typeof list !== 'function')
991
+ return null;
992
+ return (requestJson) => list.call(Core, requestJson);
993
+ }
994
+ /** Whether the linked engine follows the one-connection bus rule (`Core.busConnectionStats`,
995
+ * DES-TEAMING-002 T0) and may therefore be handed the daemon's bus on every boot. */
996
+ static engineHoldsBusConnection() {
997
+ return typeof Core.busConnectionStats === 'function';
998
+ }
869
999
  static eventStoreCounter() {
870
1000
  const fn = Core.eventStoreCount;
871
1001
  if (typeof fn !== 'function')
@@ -967,21 +1097,36 @@ export class CoreAdapter {
967
1097
  * never swallowed (codex round 6). A listener failing on `rejected` cannot un-launch anything;
968
1098
  * the primary error keeps precedence and the secondary one is logged, not lost.
969
1099
  */
970
- async handedToEngine(kind, id, call) {
1100
+ async handedToEngine(kind, id, call, workflow) {
971
1101
  try {
972
- this.notifyLaunch({ kind, id, status: 'handed' });
1102
+ this.notifyLaunch({ kind, id, status: 'handed', ...(workflow !== undefined ? { workflow } : {}) });
973
1103
  }
974
1104
  catch (err) {
975
1105
  this.releaseAfterFailure(kind, id, err);
976
1106
  throw err;
977
1107
  }
1108
+ let out;
978
1109
  try {
979
- return await call();
1110
+ out = await call();
980
1111
  }
981
1112
  catch (err) {
982
1113
  this.releaseAfterFailure(kind, id, err);
983
1114
  throw err;
984
1115
  }
1116
+ this.announceAccepted({ kind, id, status: 'accepted', ...(workflow !== undefined ? { workflow } : {}) });
1117
+ return out;
1118
+ }
1119
+ /** Deliver `accepted` once the engine TOOK the launch. Never propagates: the launch already happened,
1120
+ * so a listener failure is logged and must not turn an accepted run into a rejected call. */
1121
+ announceAccepted(notice) {
1122
+ for (const listener of this.launchListeners) {
1123
+ try {
1124
+ listener(notice);
1125
+ }
1126
+ catch (err) {
1127
+ console.warn(`[crew] launch ${notice.kind}:${notice.id} accepted, but a launch listener failed on it: ${err instanceof Error ? err.message : String(err)}`);
1128
+ }
1129
+ }
985
1130
  }
986
1131
  /** Deliver `rejected` while a launch is already failing with `primary`: a secondary listener failure is logged, never masks the primary. */
987
1132
  releaseAfterFailure(kind, id, primary) {
@@ -1021,8 +1166,84 @@ export class CoreAdapter {
1021
1166
  seatChipOnCreate: true,
1022
1167
  };
1023
1168
  }
1169
+ /** Whether this engine carries the plan approval gate (DES-TEAMING-002 T3) — see
1170
+ * [`engineSupportsPlanLaunch`]. An instance method so a partial-stub adapter can say either. */
1171
+ supportsPlanLaunch() {
1172
+ return engineSupportsPlanLaunch();
1173
+ }
1174
+ /** (crew#549) The CONFIGURED deliver identity — the `deliverIdentityLogin` setting, read at
1175
+ * launch. Cached on the instance for the synchronous compose paths (`deliverStep`), refreshed
1176
+ * by every `launchRun`, so a `PUT /settings` takes effect on the next launch and a settings
1177
+ * read never sits on the launch path twice. `''` ⇒ nothing configured (`GH_ACCOUNT` decides).
1178
+ * A LOGIN only: no token is ever read here. */
1179
+ deliverIdentityLogin = '';
1180
+ async refreshDeliverIdentity() {
1181
+ try {
1182
+ this.deliverIdentityLogin = (await this.getSettings()).deliverIdentityLogin ?? '';
1183
+ }
1184
+ catch {
1185
+ // A settings file that cannot be read leaves the identity unconfigured rather than
1186
+ // failing the launch — the deliver script still cross-checks gh against git's credential.
1187
+ this.deliverIdentityLogin = '';
1188
+ }
1189
+ }
1190
+ /**
1191
+ * The origin the deliver push will actually go to (F2) so the deliver GATE CARD says what will
1192
+ * actually happen. The card used to promise "pushes its branch and opens a pull request"
1193
+ * whatever the origin was — on a local path, an SSH, GitLab, ADO or Gitea remote, the phase
1194
+ * pushed the branch and then died on `gh pr create`, so the operator had consented to a pull
1195
+ * request that could not exist.
1196
+ *
1197
+ * RETURNED, never stashed on the adapter (codex review of the F2 PR, HIGH): two concurrent
1198
+ * delivering launches on different repos would otherwise interleave their `await` with the
1199
+ * synchronous composition that reads it, and one launch's card would name the other repo's
1200
+ * origin. The value is launch-local from here on.
1201
+ *
1202
+ * ONE `git remote get-url --push` per delivering launch, in the registered repo's root (a run
1203
+ * worktree shares its repo's remotes, and the worktree does not exist yet at compose time).
1204
+ * `null` for everything that can go wrong — "could not read it" is not a licence to claim
1205
+ * anything about it either way, and the card keeps its generic sentence. Only git's own "no such
1206
+ * remote" becomes `''`, the one answer that licenses "there is no origin".
1207
+ */
1208
+ async resolveDeliverOrigin(repoRef) {
1209
+ if (repoRef === null || repoRef === undefined || repoRef === '')
1210
+ return null;
1211
+ try {
1212
+ const repos = await this.listRepos();
1213
+ const hit = deliverRepoFor(repos, repoRef);
1214
+ if (hit === undefined || hit.root_path === '')
1215
+ return null;
1216
+ return await readDeliverOriginUrl(hit.root_path);
1217
+ }
1218
+ catch {
1219
+ // An engine that cannot list repos leaves the origin UNKNOWN — the card keeps its generic
1220
+ // sentence rather than claim anything. `readDeliverOriginUrl` owns the git-side verdicts.
1221
+ return null;
1222
+ }
1223
+ }
1224
+ /** The launcher's `deliver` step for a plan (`name` null) or a preset launch (DES-TEAMING-002
1225
+ * §8.5): the engine appends it to the plan and puts it in the floor. */
1226
+ deliverStep(name, phases, input,
1227
+ /** (F2) The origin this launch resolved — launch-local, never adapter state. */
1228
+ originUrl) {
1229
+ return deliverPresetStep(name, phases, input.sessionId, input.problem, {
1230
+ repoRef: input.repoRef ?? null,
1231
+ apiOrigin: this.deliverApiOrigin?.() ?? null,
1232
+ revisesPr: input.revisesPr ?? null,
1233
+ ghAccount: process.env['GH_ACCOUNT'] ?? null,
1234
+ ghTokenPinned: typeof process.env['GH_TOKEN'] === 'string' && process.env['GH_TOKEN'] !== '',
1235
+ originUrl,
1236
+ deliverIdentity: this.deliverIdentityLogin,
1237
+ });
1238
+ }
1024
1239
  /** Launch an interactive, resumable run → the run id. */
1025
1240
  async launchRun(input) {
1241
+ // (crew#549) Read the configured deliver identity BEFORE anything composes the deliver
1242
+ // phase: the compose paths below are synchronous and bake it into the script.
1243
+ await this.refreshDeliverIdentity();
1244
+ // (F2) And the origin the push would go to — read once, for the gate card's target sentence.
1245
+ // Only a delivering launch composes a deliver phase, so only a delivering launch pays for it.
1246
+ const deliverOriginUrl = input.deliver === 'pr' ? await this.resolveDeliverOrigin(input.repoRef) : null;
1026
1247
  const opts = {
1027
1248
  problem: input.problem,
1028
1249
  sessionId: input.sessionId,
@@ -1117,7 +1338,45 @@ export class CoreAdapter {
1117
1338
  }
1118
1339
  opts.projectGraph = input.projectGraph;
1119
1340
  }
1120
- if (input.workflow !== undefined) {
1341
+ if (input.plan !== undefined) {
1342
+ // DES-TEAMING-002 T3: a user-composed plan is a COMMAND the engine owns end to end — it
1343
+ // publishes `plan.proposed`, scores, floor-fills and gates it. Crew only forwards it, and
1344
+ // refuses what it cannot forward faithfully rather than dropping it.
1345
+ if (input.workflow !== undefined) {
1346
+ throw new Error('a launch carries a plan or names a workflow (a preset), not both');
1347
+ }
1348
+ if ((input.requireDeliverables ?? []).length > 0) {
1349
+ throw new Error('requireDeliverables with a plan is refused: the deliverable floor has no engine-side ' +
1350
+ 'step yet — launch the plan without it');
1351
+ }
1352
+ if (!this.supportsPlanLaunch())
1353
+ throw new PlanLaunchUnsupportedError('A plan launch');
1354
+ opts.planJson = JSON.stringify(input.plan);
1355
+ // T8: a delivering plan hands the engine its deliver step, exactly as a preset launch does.
1356
+ if (input.deliver === 'pr') {
1357
+ opts.deliverStepJson = JSON.stringify(this.deliverStep(null, [], input, deliverOriginUrl));
1358
+ }
1359
+ }
1360
+ // DES-TEAMING-002 T3: a workflow that names a PRESET is a plan the engine floor-fills and gates.
1361
+ // Crew never composes a per-run def over one (that def is no preset, so the launch would skip
1362
+ // `plan_approval`): delivery rides the launch as the deliver STEP (`deliverStepJson`), and a
1363
+ // deliverable floor — which has no engine-side step yet — is refused rather than composed.
1364
+ const requireDeliverablesAll = input.requireDeliverables ?? [];
1365
+ const namesPreset = input.workflow !== undefined &&
1366
+ (input.deliver === 'pr' || requireDeliverablesAll.length > 0) &&
1367
+ (await this.presetNamed(input.workflow, input.projectId)) !== null;
1368
+ if (namesPreset && input.workflow !== undefined) {
1369
+ if (requireDeliverablesAll.length > 0) {
1370
+ throw new Error(`requireDeliverables on a preset launch ('${input.workflow}') is refused: the deliverable ` +
1371
+ 'floor would have to be composed into a per-run def, which skips the plan_approval gate');
1372
+ }
1373
+ if (!this.supportsPlanLaunch())
1374
+ throw new PlanLaunchUnsupportedError('Delivering a preset launch');
1375
+ const step = this.deliverStep(input.workflow, this.getWorkflow(input.workflow)?.phases ?? [], input, deliverOriginUrl);
1376
+ opts.deliverStepJson = JSON.stringify(step);
1377
+ opts.workflow = input.workflow;
1378
+ }
1379
+ else if (input.workflow !== undefined) {
1121
1380
  let workflowId = input.workflow;
1122
1381
  // Both per-run compositions (deliver + deliverable floor) fold into ONE def and ONE
1123
1382
  // registration: composing twice would arm two ids and launch the second, leaving the
@@ -1179,6 +1438,12 @@ export class CoreAdapter {
1179
1438
  revisesPr: input.revisesPr ?? null,
1180
1439
  ghAccount: process.env['GH_ACCOUNT'] ?? null,
1181
1440
  ghTokenPinned: typeof process.env['GH_TOKEN'] === 'string' && process.env['GH_TOKEN'] !== '',
1441
+ // F2 — the origin the push will actually go to, so the gate card cannot promise a
1442
+ // pull request on a remote that can never carry one.
1443
+ originUrl: deliverOriginUrl,
1444
+ // crew#549 — the configured deliver identity, baked into the script so the refusal
1445
+ // holds on a daemon started without GH_ACCOUNT exported.
1446
+ deliverIdentity: this.deliverIdentityLogin,
1182
1447
  });
1183
1448
  }
1184
1449
  }
@@ -1211,8 +1476,8 @@ export class CoreAdapter {
1211
1476
  }
1212
1477
  }
1213
1478
  }
1214
- else if (input.deliver === 'pr') {
1215
- // Fail loud, not silent: dropping the option would run to completion with the caller
1479
+ else if (input.deliver === 'pr' && input.plan === undefined) {
1480
+ // (A plan carries its deliver step above.) Fail loud, not silent: dropping the option would run to completion with the caller
1216
1481
  // believing a PR opens at the end — the exact operator gap crew#293 closes.
1217
1482
  throw new Error('deliver: "pr" requires a workflow — a free-text run has no def to append the deliver phase to');
1218
1483
  }
@@ -1224,7 +1489,7 @@ export class CoreAdapter {
1224
1489
  throw new Error('requireDeliverables requires a workflow — a free-text run has no def to append the ' +
1225
1490
  'deliverable floor to');
1226
1491
  }
1227
- return this.handedToEngine('run', input.sessionId, () => this.core.launchRun(opts));
1492
+ return this.handedToEngine('run', input.sessionId, () => this.core.launchRun(opts), input.workflow);
1228
1493
  }
1229
1494
  /** Resume a run from its persisted cursor → the status token. */
1230
1495
  resumeRun(runId) {
@@ -1235,8 +1500,18 @@ export class CoreAdapter {
1235
1500
  * would silently DROP them — a `request_changes` would run as a plain reject — so they fail
1236
1501
  * CLOSED on version (the `extraWriteRoots` / `projectGraph` doctrine); absent, the call is the
1237
1502
  * three-arg one every engine understands. */
1238
- confirmGate(runId, approve, amend, action, amendScope) {
1503
+ confirmGate(runId, approve, amend, action, amendScope, plan) {
1239
1504
  return this.handedToEngine('run', runId, () => {
1505
+ if (plan !== undefined) {
1506
+ // DES-TEAMING-002 T3: approve a `plan_approval` gate WITH AN EDIT — the engine proposes,
1507
+ // floor-fills and accepts it (or refuses it and re-opens the gate). Fail CLOSED on an
1508
+ // addon without the gate: it would drop the edit and approve the held plan unedited.
1509
+ if (!this.supportsPlanLaunch()) {
1510
+ return Promise.reject(new PlanLaunchUnsupportedError('An edited plan at the gate'));
1511
+ }
1512
+ const core = this.core;
1513
+ return core.confirmGate(runId, true, undefined, 'edit_plan', undefined, JSON.stringify(plan));
1514
+ }
1240
1515
  if (action === undefined && amendScope === undefined) {
1241
1516
  return this.core.confirmGate(runId, approve, amend);
1242
1517
  }
@@ -1399,48 +1674,62 @@ export class CoreAdapter {
1399
1674
  async sessions() {
1400
1675
  return JSON.parse(await this.core.sessions());
1401
1676
  }
1402
- /** Every run + its ordered units. */
1677
+ /**
1678
+ * Each run's launch name as the engine RECORDED it — `sessionStarted.workflowId` from its event
1679
+ * log (the preset, the registered def, `<run>:plan-<rev>`, or `null` for free text). Immutable
1680
+ * once written, so it is read once per run: a run whose log holds no `sessionStarted` is memoized
1681
+ * only once it is terminal (a live run's frame may not be flushed yet; a finished run's never
1682
+ * will be). Seam X2 — the one input `resolveRunIdentity` needs that the session record lacks.
1683
+ */
1684
+ launchedWorkflows = new Map();
1685
+ async launchedWorkflowOf(view) {
1686
+ const id = view.session.id;
1687
+ if (this.launchedWorkflows.has(id))
1688
+ return this.launchedWorkflows.get(id);
1689
+ let events;
1690
+ try {
1691
+ events = await this.runEvents(id);
1692
+ }
1693
+ catch {
1694
+ return undefined;
1695
+ }
1696
+ if (events === null)
1697
+ return undefined;
1698
+ const started = events.find((e) => e.type === 'sessionStarted');
1699
+ // The engine spells it `workflowId` on the frame (core-ts `CoreEventJson`).
1700
+ const raw = started?.['workflowId'];
1701
+ const launched = started === undefined ? undefined : typeof raw === 'string' ? raw : null;
1702
+ if (launched !== undefined || TERMINAL_RUN_STATUSES.has(view.session.status)) {
1703
+ this.launchedWorkflows.set(id, launched);
1704
+ }
1705
+ return launched;
1706
+ }
1707
+ /**
1708
+ * Every run + its ordered units, each with its `run_identity` (seam X2) resolved from the engine's
1709
+ * record — its plan state, else its recorded launch — never from its phase sequence.
1710
+ *
1711
+ * `workflow_id` is still rewritten from the engine's synthetic `wf-<run>` to the resolved NAME
1712
+ * for skins that read it (studio's run-kind list, until it reads `run_identity.system`); a run
1713
+ * with no name (a user plan, free text, unknown) keeps the engine's id.
1714
+ */
1403
1715
  async sessionsDetail() {
1404
1716
  const views = JSON.parse(await this.core.sessionsDetail());
1405
- // The Rust core always stores workflow_id as 'wf-<session-uuid>' (an instance ID, not the
1406
- // definition name). Patch it back to the definition name so the studio's chat/work filters work.
1407
- // phase_ref is only set on executed units and uses format 'wf-<uuid>:unit-N' (not the phase id).
1408
- // The phase id is reliably embedded in the unit id as '<session-uuid>:<phase-id>'.
1717
+ const needsLaunch = (v) => v.session.team_plan == null &&
1718
+ typeof v.session.workflow_id === 'string' &&
1719
+ isSyntheticWorkflowId(v.session.workflow_id);
1720
+ const pending = views.filter(needsLaunch);
1721
+ const launched = new Map();
1722
+ // Bounded fan-out: the first read after boot visits each such run's log once (memoized after).
1723
+ for (let i = 0; i < pending.length; i += 8) {
1724
+ const batch = pending.slice(i, i + 8);
1725
+ const got = await Promise.all(batch.map((v) => this.launchedWorkflowOf(v)));
1726
+ batch.forEach((v, k) => launched.set(v.session.id, got[k]));
1727
+ }
1409
1728
  for (const view of views) {
1410
- if (view.session.workflow_id?.startsWith('wf-')) {
1411
- const phases = [...view.units].sort((a, b) => a.ord - b.ord).map((u) => {
1412
- const colonIdx = u.id.indexOf(':');
1413
- return colonIdx >= 0 ? u.id.slice(colonIdx + 1) : '';
1414
- });
1415
- if (view.units.length === 1) {
1416
- // Single-unit chat sessions have phase id 'explore' (from the chat workflow def).
1417
- // 'u1' is ambiguous — it appears on any single-unit run without an explicit workflow,
1418
- // including Do Work runs, so we leave those unpatched rather than misclassify them.
1419
- const phase = phases[0] ?? '';
1420
- if (phase === 'explore')
1421
- view.session.workflow_id = 'chat';
1422
- }
1423
- else {
1424
- // Multi-unit: match against builtin workflow defs by phase sequence. A delivered run
1425
- // (crew#293, default-on for code-work launches since crew#393) carries run-scoped
1426
- // appendages the def never had — [verify-deliverables,] [deliver] at the tail — so
1427
- // when the exact sequence matches nothing they are stripped and the match retried:
1428
- // a feature run must not lose its name on the wire because it also delivered.
1429
- const bySequence = (seq) => BUILTIN_WORKFLOWS.find((def) => def.phases.length === seq.length &&
1430
- def.phases.every((p, i) => p.id === seq[i]));
1431
- let match = bySequence(phases);
1432
- if (!match) {
1433
- const stripped = [...phases];
1434
- if (stripped[stripped.length - 1] === DELIVER_PHASE_ID)
1435
- stripped.pop();
1436
- if (stripped[stripped.length - 1] === DELIVERABLE_FLOOR_PHASE_ID)
1437
- stripped.pop();
1438
- if (stripped.length < phases.length)
1439
- match = bySequence(stripped);
1440
- }
1441
- if (match)
1442
- view.session.workflow_id = match.id;
1443
- }
1729
+ const identity = wireIdentity(resolveRunIdentity(view, launched.get(view.session.id)));
1730
+ view.session.run_identity = identity;
1731
+ if (identity.name !== null && isSyntheticWorkflowId(view.session.workflow_id ?? '')) {
1732
+ view.session.workflow_id = identity.name;
1444
1733
  }
1445
1734
  }
1446
1735
  return views;
@@ -1449,6 +1738,117 @@ export class CoreAdapter {
1449
1738
  async workOutput(unitId) {
1450
1739
  return JSON.parse(await this.core.workOutput(unitId));
1451
1740
  }
1741
+ // ── Presets (DES-TEAMING-002 §8.4, seam C2) ─────────────────────────────────
1742
+ // 1:1 maps of the engine's preset commands. The engine owns the store AND resolution: a launch
1743
+ // naming a preset (`workflow`) is resolved inside the engine, so crew never expands one itself.
1744
+ // Every method throws PresetsUnsupportedError on an addon without the bindings (routes: 501).
1745
+ /** True when the installed addon carries the preset bindings. */
1746
+ presetsSupported() {
1747
+ return typeof this.core.putPreset === 'function';
1748
+ }
1749
+ requirePresets(fn, what) {
1750
+ if (typeof fn !== 'function')
1751
+ throw new PresetsUnsupportedError(what);
1752
+ return fn;
1753
+ }
1754
+ async putPreset(name, steps, projectId, createdBy) {
1755
+ const fn = this.requirePresets(this.core.putPreset, 'Saving a preset');
1756
+ return withPresetSystemFlag(JSON.parse(await fn.call(this.core, name, JSON.stringify(steps), projectId ?? null, createdBy ?? null)));
1757
+ }
1758
+ async deletePreset(name, projectId) {
1759
+ const fn = this.requirePresets(this.core.deletePreset, 'Deleting a preset');
1760
+ return JSON.parse(await fn.call(this.core, name, projectId ?? null));
1761
+ }
1762
+ /** The preset `name` resolves to for a launch in `projectId` (the project's row shadows the
1763
+ * global one), or `null` — including on an addon without presets, whose engine resolves every
1764
+ * workflow id as a registered def. */
1765
+ async presetNamed(name, projectId) {
1766
+ try {
1767
+ return (await this.listPresets(projectId)).find((p) => p.name === name) ?? null;
1768
+ }
1769
+ catch (err) {
1770
+ if (err instanceof PresetsUnsupportedError)
1771
+ return null;
1772
+ throw err;
1773
+ }
1774
+ }
1775
+ async listPresets(projectId) {
1776
+ const fn = this.requirePresets(this.core.listPresets, 'Listing presets');
1777
+ return JSON.parse(await fn.call(this.core, projectId ?? null)).map(withPresetSystemFlag);
1778
+ }
1779
+ // ── Team (DES-TEAMING-002 T8) ───────────────────────────────────────────────
1780
+ // Reads and commands over the engine; crew publishes no team fact (§4.0). Every method throws
1781
+ // TeamUnsupportedError on an addon without its binding (routes: 501).
1782
+ requireTeam(fn, what, binding) {
1783
+ if (typeof fn !== 'function')
1784
+ throw new TeamUnsupportedError(what, binding);
1785
+ return fn;
1786
+ }
1787
+ /** The run's team transport and per-unit snapshots, or `null` for a run that is not a team run. */
1788
+ async runTeam(runId) {
1789
+ const fn = this.requireTeam(this.core.runTeam, 'Reading a run\'s team', 'runTeam');
1790
+ return JSON.parse(await fn.call(this.core, runId));
1791
+ }
1792
+ /** Replay `<state home>/team-outbox.ndjson` onto the bus. */
1793
+ async replayTeamOutbox() {
1794
+ const fn = this.requireTeam(this.core.replayTeamOutbox, 'Replaying the team outbox', 'replayTeamOutbox');
1795
+ return JSON.parse(await fn.call(this.core));
1796
+ }
1797
+ /**
1798
+ * The catalog ids whose entry declares `verified_evidence` (seam X2: a plan run's acceptance is
1799
+ * the steps it contains that re-verify evidence). `null` when the engine has no catalog binding
1800
+ * or its catalog does not carry the flag — callers fail closed on it. The catalog is compiled
1801
+ * into the engine, so a known answer is read once.
1802
+ */
1803
+ async verifiedEvidenceCatalog() {
1804
+ if (this._verifiedEvidenceCatalog !== undefined)
1805
+ return this._verifiedEvidenceCatalog;
1806
+ let entries;
1807
+ try {
1808
+ entries = await this.catalog();
1809
+ }
1810
+ catch {
1811
+ return null;
1812
+ }
1813
+ const set = verifiedEvidenceCatalog(entries);
1814
+ if (set !== null)
1815
+ this._verifiedEvidenceCatalog = set;
1816
+ return set;
1817
+ }
1818
+ _verifiedEvidenceCatalog;
1819
+ /** The engine's phase catalog. */
1820
+ async catalog() {
1821
+ const fn = this.requireTeam(this.core.catalog, 'Reading the phase catalog', 'catalog');
1822
+ return JSON.parse(await fn.call(this.core));
1823
+ }
1824
+ /** What a `POST /runs {plan}` launch with the same fields would decide, persisting nothing
1825
+ * (`Core.previewPlan`): `repoRef` is the repo the launch runs on (its graph scores the touch
1826
+ * set), and `deliver` hands the engine the launch's deliver step, as `launchRun` does. */
1827
+ async previewPlan(plan, opts = {}) {
1828
+ const fn = this.requireTeam(this.core.previewPlan, 'Previewing a plan', 'previewPlan');
1829
+ // (F2) The preview shows the deliver step's gate-card text, so it reads the same origin the
1830
+ // launch would. Local to this call, like the launch's.
1831
+ const previewOriginUrl = opts.deliver === true ? await this.resolveDeliverOrigin(opts.repoRef) : null;
1832
+ const deliverStep = opts.deliver === true
1833
+ ? JSON.stringify(
1834
+ // The launch's own step for a plan (`deliverStep(null, [], input)`); the run id is a
1835
+ // placeholder: it only shapes the push command, which a preview never runs.
1836
+ this.deliverStep(null, [], {
1837
+ sessionId: 'plan-preview',
1838
+ problem: '',
1839
+ clisJson: '[]',
1840
+ ...(opts.repoRef !== undefined ? { repoRef: opts.repoRef } : {}),
1841
+ }, previewOriginUrl))
1842
+ : null;
1843
+ return JSON.parse(await fn.call(this.core, JSON.stringify(plan), opts.projectId ?? null, opts.humanConfirm ?? null, opts.repoRef ?? null, deliverStep));
1844
+ }
1845
+ /** A mid-run plan edit (`Core.proposePlan`): the steps to add, held for the run's next step
1846
+ * boundary and approved by its author. Idempotent by `requestId`; rejects with the engine's
1847
+ * refusal (a plan awaiting approval, a started deliver step, a finished or unplanned run, …). */
1848
+ async proposePlan(runId, plan, requestId) {
1849
+ const fn = this.requireTeam(this.core.proposePlan, 'Editing a running plan', 'proposePlan');
1850
+ return JSON.parse(await fn.call(this.core, runId, JSON.stringify(plan), requestId));
1851
+ }
1452
1852
  // ── Projects (DES-PROJECT-001) ──────────────────────────────────────────────
1453
1853
  // 1:1 maps of the 0.6.0 engine surface. Writes ride the single-writer actor;
1454
1854
  // reads are read-only store opens inside the addon. Every method throws
@@ -1764,9 +2164,10 @@ export class CoreAdapter {
1764
2164
  problem: `Onboard repository: ${repoName}`,
1765
2165
  sessionId: runId,
1766
2166
  // The run's seat pool is the seats the WORKFLOW can use (F-2R2-010): onboarding is two tool
1767
- // phases routed to the `wicked-estate` executor, so its pool is empty — not the whole roster
1768
- // dressed up as a 5-seat run with four signed-out seats.
1769
- clisJson: JSON.stringify(this.seatsForWorkflow('onboarding')),
2167
+ // steps routed to the `wicked-estate` executor, so its pool is empty — not the whole roster
2168
+ // dressed up as a 5-seat run with four signed-out seats. `onboarding` is the engine's
2169
+ // built-in PRESET (DES-TEAMING-002 M4), read from the engine's store.
2170
+ clisJson: JSON.stringify(await this.seatsForWorkflow('onboarding')),
1770
2171
  workflow: 'onboarding',
1771
2172
  repoRef: repoId,
1772
2173
  });
@@ -1784,20 +2185,24 @@ export class CoreAdapter {
1784
2185
  }
1785
2186
  }
1786
2187
  /**
1787
- * The roster a run of `workflowId` should carry as its seat pool. A workflow whose every phase
1788
- * runs a TOOL executor convenes no council and dispatches no seat, so its pool is `[]` — the
1789
- * engine routes each unit `tool` without consulting the pool (verified against the engine: an
1790
- * onboarding launch with `clis: []` distributes both units to `wicked-estate`). Any workflow
1791
- * with an agent phase — or one this daemon cannot read — gets the full roster, as before.
2188
+ * The roster a run of `workflowId` should carry as its seat pool. A workflow or preset whose
2189
+ * every phase (step) runs a TOOL executor convenes no council and dispatches no seat, so its pool
2190
+ * is `[]` — the engine routes each unit `tool` without consulting the pool (verified against the
2191
+ * engine: an onboarding launch with `clis: []` distributes both units to `wicked-estate`). Any
2192
+ * one with an agent phase — or one this daemon cannot read — gets the full roster, as before.
2193
+ * A registered def is read first; otherwise the preset the name launches (the engine resolves a
2194
+ * launch the same way: DES-TEAMING-002 §8.4).
1792
2195
  */
1793
- seatsForWorkflow(workflowId) {
2196
+ async seatsForWorkflow(workflowId) {
1794
2197
  const def = this.getWorkflow(workflowId);
2198
+ const executors = def !== null
2199
+ ? def.phases.map((p) => p.executor)
2200
+ : ((await this.presetNamed(workflowId))?.steps ?? []).map((s) => s['executor']);
1795
2201
  // The roster WITH standing when the daemon wired one (F-RECON-002/003) — `launchRun` then
1796
2202
  // benches `council_eligible: false` seats through `engineRosterJson`.
1797
- if (def === null || def.phases.length === 0)
1798
- return this.launchRoster();
1799
- const toolOnly = def.phases.every((p) => p.executor?.type === 'tool');
1800
- return toolOnly ? [] : this.launchRoster();
2203
+ if (executors.length === 0)
2204
+ return this.readyLaunchRoster();
2205
+ return executors.every((e) => e?.type === 'tool') ? [] : this.readyLaunchRoster();
1801
2206
  }
1802
2207
  /** Return the onboarding run id for a repo (undefined if not launched this session). */
1803
2208
  getOnboardRunId(repoId) {
@@ -1951,6 +2356,18 @@ export class CoreAdapter {
1951
2356
  steeringSupported() {
1952
2357
  return typeof this.core.steeringImport === 'function';
1953
2358
  }
2359
+ /**
2360
+ * The MCP policy preview (DES-MCP-TOOLS-001 S6): `{calls, cells}` → `[{subject, cells}]` JSON,
2361
+ * judged by the same evaluation the broker's `evaluateMcpCall` runs, and recorded nowhere.
2362
+ * Throws `mcp_preview_unsupported` on an addon that predates the binding (the route answers 501).
2363
+ */
2364
+ async previewMcpCalls(requestJson) {
2365
+ const preview = this.core.previewMcpCalls;
2366
+ if (typeof preview !== 'function') {
2367
+ throw new Error('mcp_preview_unsupported: the installed wicked-core-ts predates the MCP policy preview; upgrade the engine');
2368
+ }
2369
+ return preview.call(this.core, requestJson);
2370
+ }
1954
2371
  /**
1955
2372
  * Batch-import steering rules (frontmattered markdown docs and/or ready rule JSON) through the
1956
2373
  * engine's ingest normalize/validate path — the SAME path `rules ingest --dir` runs, on the
@@ -2071,6 +2488,47 @@ export class CoreAdapter {
2071
2488
  this.userWorkflows.set(def.id, def);
2072
2489
  }
2073
2490
  }
2491
+ /** Core's verdict on each hydrated drop-in, run ONCE per process (the same lifetime as
2492
+ * {@link hydrateFromOverlay} and as core's own boot-time `load_dir`). */
2493
+ overlayVerdicts = null;
2494
+ /** id → core's refusal reason, for the drop-ins `judgeOverlayOnce` dropped. */
2495
+ overlayRefusals = new Map();
2496
+ /** Drop the refused defs from `userWorkflows` and remember why, asking core's parser (crew#718).
2497
+ *
2498
+ * A build with no `registerWorkflow` binding has no validator to ask, so nothing is judged and
2499
+ * the catalog reads exactly as it did before — the same floor {@link registerWorkflow} draws,
2500
+ * on the read side where there is no state to corrupt. `registerWorkflow` has been declared
2501
+ * (non-optional) in wicked-core-ts since 0.4.0. */
2502
+ judgeOverlayOnce() {
2503
+ this.overlayVerdicts ??= (async () => {
2504
+ this.hydrateFromOverlay();
2505
+ const core = this.core;
2506
+ const register = core['registerWorkflow'];
2507
+ if (typeof register !== 'function')
2508
+ return;
2509
+ const { refused } = await judgeOverlayWorkflows([...this.userWorkflows.values()], (json) => register.call(this.core, json));
2510
+ for (const r of refused) {
2511
+ this.userWorkflows.delete(r.id);
2512
+ this.overlayRefusals.set(r.id, r.reason);
2513
+ console.warn(`wicked-crew: drop-in workflow '${r.id}' is NOT in the catalog — the engine refused it: ${r.reason}`);
2514
+ }
2515
+ })();
2516
+ return this.overlayVerdicts;
2517
+ }
2518
+ /** The workflow catalog `GET /workflows` serves: the defs the ENGINE accepted, plus the drop-ins
2519
+ * it refused with its own reason, so an unlaunchable def is named instead of offered (crew#718). */
2520
+ async workflowCatalog() {
2521
+ await this.judgeOverlayOnce();
2522
+ return {
2523
+ workflows: this.listWorkflows(),
2524
+ unavailable: [...this.overlayRefusals].map(([id, reason]) => ({ id, reason })),
2525
+ };
2526
+ }
2527
+ /** Core's refusal reason for `id`, or `null` when the engine never refused it (crew#718). */
2528
+ async workflowRefusal(id) {
2529
+ await this.judgeOverlayOnce();
2530
+ return this.overlayRefusals.get(id) ?? null;
2531
+ }
2074
2532
  listWorkflows() {
2075
2533
  this.hydrateFromOverlay();
2076
2534
  // Builtins first (stable ordering), but user-registered workflows take precedence when
@@ -2081,20 +2539,21 @@ export class CoreAdapter {
2081
2539
  const override = this.userWorkflows.get(w.id);
2082
2540
  if (!seen.has(w.id)) {
2083
2541
  seen.add(w.id);
2084
- result.push(override ?? w);
2542
+ result.push(withSystemFlag(override ?? w));
2085
2543
  }
2086
2544
  }
2087
2545
  for (const w of this.userWorkflows.values()) {
2088
2546
  if (!seen.has(w.id)) {
2089
2547
  seen.add(w.id);
2090
- result.push(w);
2548
+ result.push(withSystemFlag(w));
2091
2549
  }
2092
2550
  }
2093
2551
  return result;
2094
2552
  }
2095
2553
  getWorkflow(id) {
2096
2554
  this.hydrateFromOverlay();
2097
- return this.userWorkflows.get(id) ?? BUILTIN_WORKFLOWS.find((w) => w.id === id) ?? null;
2555
+ const def = this.userWorkflows.get(id) ?? BUILTIN_WORKFLOWS.find((w) => w.id === id) ?? null;
2556
+ return def === null ? null : withSystemFlag(def);
2098
2557
  }
2099
2558
  /** Write a built-in workflow definition to the Rust overlay dir (and hot-register when possible).
2100
2559
  * Unlike registerWorkflow(), this does NOT touch userWorkflows, avoiding duplicates in listWorkflows(). */
@@ -2201,6 +2660,9 @@ export class CoreAdapter {
2201
2660
  await register.call(this.core, json);
2202
2661
  await writeFile(path, JSON.stringify(overlayDef, null, 2), 'utf8');
2203
2662
  this.userWorkflows.set(def.id, def);
2663
+ // (crew#718) This def just passed core's own validator, so any earlier refusal under the same
2664
+ // id is stale — the catalog must offer the replacement, not keep naming the version it refused.
2665
+ this.overlayRefusals.delete(def.id);
2204
2666
  return def.id;
2205
2667
  }
2206
2668
  /**
@@ -2255,6 +2717,19 @@ export class CoreAdapter {
2255
2717
  if (typeof v !== 'number' || !Number.isInteger(v) || v < 1 || v > 10000)
2256
2718
  delete parsed.graphNodeLimit;
2257
2719
  }
2720
+ // deliverIdentityLogin (crew#549): a LOGIN, and the same charset PUT /settings enforces —
2721
+ // a hand-edited settings.json must not enable a value the API rejects (and must never be
2722
+ // able to put anything but a login into the deliver script's shell literal). Anything else
2723
+ // is dropped, which leaves the identity unconfigured rather than unpinned-and-unsaid.
2724
+ if ('deliverIdentityLogin' in parsed) {
2725
+ const v = parsed.deliverIdentityLogin;
2726
+ if (typeof v !== 'string' || (v.trim() !== '' && !isGitHubLogin(v.trim()))) {
2727
+ delete parsed.deliverIdentityLogin;
2728
+ }
2729
+ else {
2730
+ parsed.deliverIdentityLogin = v.trim();
2731
+ }
2732
+ }
2258
2733
  // workerStallMinutes (crew#287): positive minutes; a hand-edited zero/negative/NaN would
2259
2734
  // make the stall watchdog fire on every sweep, so drop it and fall back to the default.
2260
2735
  if ('workerStallMinutes' in parsed) {
@@ -2385,6 +2860,8 @@ export class CoreAdapter {
2385
2860
  if (this.closed)
2386
2861
  return;
2387
2862
  this.closed = true;
2863
+ if (this.engineBus !== null && this.busDbPath !== undefined)
2864
+ detachEngineBus(this.busDbPath, this.engineBus);
2388
2865
  this.listeners.clear();
2389
2866
  this.subscription.close();
2390
2867
  }