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,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
- }
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)');
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)
815
885
  process.env['WICKED_BUS_EXEC'] = '1';
816
- process.env['WICKED_BUS_DB'] = opts.busDbPath;
817
- }
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')
@@ -1036,8 +1166,84 @@ export class CoreAdapter {
1036
1166
  seatChipOnCreate: true,
1037
1167
  };
1038
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
+ }
1039
1239
  /** Launch an interactive, resumable run → the run id. */
1040
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;
1041
1247
  const opts = {
1042
1248
  problem: input.problem,
1043
1249
  sessionId: input.sessionId,
@@ -1132,7 +1338,45 @@ export class CoreAdapter {
1132
1338
  }
1133
1339
  opts.projectGraph = input.projectGraph;
1134
1340
  }
1135
- 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) {
1136
1380
  let workflowId = input.workflow;
1137
1381
  // Both per-run compositions (deliver + deliverable floor) fold into ONE def and ONE
1138
1382
  // registration: composing twice would arm two ids and launch the second, leaving the
@@ -1194,6 +1438,12 @@ export class CoreAdapter {
1194
1438
  revisesPr: input.revisesPr ?? null,
1195
1439
  ghAccount: process.env['GH_ACCOUNT'] ?? null,
1196
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,
1197
1447
  });
1198
1448
  }
1199
1449
  }
@@ -1226,8 +1476,8 @@ export class CoreAdapter {
1226
1476
  }
1227
1477
  }
1228
1478
  }
1229
- else if (input.deliver === 'pr') {
1230
- // 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
1231
1481
  // believing a PR opens at the end — the exact operator gap crew#293 closes.
1232
1482
  throw new Error('deliver: "pr" requires a workflow — a free-text run has no def to append the deliver phase to');
1233
1483
  }
@@ -1250,8 +1500,18 @@ export class CoreAdapter {
1250
1500
  * would silently DROP them — a `request_changes` would run as a plain reject — so they fail
1251
1501
  * CLOSED on version (the `extraWriteRoots` / `projectGraph` doctrine); absent, the call is the
1252
1502
  * three-arg one every engine understands. */
1253
- confirmGate(runId, approve, amend, action, amendScope) {
1503
+ confirmGate(runId, approve, amend, action, amendScope, plan) {
1254
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
+ }
1255
1515
  if (action === undefined && amendScope === undefined) {
1256
1516
  return this.core.confirmGate(runId, approve, amend);
1257
1517
  }
@@ -1414,48 +1674,62 @@ export class CoreAdapter {
1414
1674
  async sessions() {
1415
1675
  return JSON.parse(await this.core.sessions());
1416
1676
  }
1417
- /** 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
+ */
1418
1715
  async sessionsDetail() {
1419
1716
  const views = JSON.parse(await this.core.sessionsDetail());
1420
- // The Rust core always stores workflow_id as 'wf-<session-uuid>' (an instance ID, not the
1421
- // definition name). Patch it back to the definition name so the studio's chat/work filters work.
1422
- // phase_ref is only set on executed units and uses format 'wf-<uuid>:unit-N' (not the phase id).
1423
- // 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
+ }
1424
1728
  for (const view of views) {
1425
- if (view.session.workflow_id?.startsWith('wf-')) {
1426
- const phases = [...view.units].sort((a, b) => a.ord - b.ord).map((u) => {
1427
- const colonIdx = u.id.indexOf(':');
1428
- return colonIdx >= 0 ? u.id.slice(colonIdx + 1) : '';
1429
- });
1430
- if (view.units.length === 1) {
1431
- // Single-unit chat sessions have phase id 'explore' (from the chat workflow def).
1432
- // 'u1' is ambiguous — it appears on any single-unit run without an explicit workflow,
1433
- // including Do Work runs, so we leave those unpatched rather than misclassify them.
1434
- const phase = phases[0] ?? '';
1435
- if (phase === 'explore')
1436
- view.session.workflow_id = 'chat';
1437
- }
1438
- else {
1439
- // Multi-unit: match against builtin workflow defs by phase sequence. A delivered run
1440
- // (crew#293, default-on for code-work launches since crew#393) carries run-scoped
1441
- // appendages the def never had — [verify-deliverables,] [deliver] at the tail — so
1442
- // when the exact sequence matches nothing they are stripped and the match retried:
1443
- // a feature run must not lose its name on the wire because it also delivered.
1444
- const bySequence = (seq) => BUILTIN_WORKFLOWS.find((def) => def.phases.length === seq.length &&
1445
- def.phases.every((p, i) => p.id === seq[i]));
1446
- let match = bySequence(phases);
1447
- if (!match) {
1448
- const stripped = [...phases];
1449
- if (stripped[stripped.length - 1] === DELIVER_PHASE_ID)
1450
- stripped.pop();
1451
- if (stripped[stripped.length - 1] === DELIVERABLE_FLOOR_PHASE_ID)
1452
- stripped.pop();
1453
- if (stripped.length < phases.length)
1454
- match = bySequence(stripped);
1455
- }
1456
- if (match)
1457
- view.session.workflow_id = match.id;
1458
- }
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;
1459
1733
  }
1460
1734
  }
1461
1735
  return views;
@@ -1464,6 +1738,117 @@ export class CoreAdapter {
1464
1738
  async workOutput(unitId) {
1465
1739
  return JSON.parse(await this.core.workOutput(unitId));
1466
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
+ }
1467
1852
  // ── Projects (DES-PROJECT-001) ──────────────────────────────────────────────
1468
1853
  // 1:1 maps of the 0.6.0 engine surface. Writes ride the single-writer actor;
1469
1854
  // reads are read-only store opens inside the addon. Every method throws
@@ -1779,9 +2164,10 @@ export class CoreAdapter {
1779
2164
  problem: `Onboard repository: ${repoName}`,
1780
2165
  sessionId: runId,
1781
2166
  // The run's seat pool is the seats the WORKFLOW can use (F-2R2-010): onboarding is two tool
1782
- // phases routed to the `wicked-estate` executor, so its pool is empty — not the whole roster
1783
- // dressed up as a 5-seat run with four signed-out seats.
1784
- 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')),
1785
2171
  workflow: 'onboarding',
1786
2172
  repoRef: repoId,
1787
2173
  });
@@ -1799,20 +2185,24 @@ export class CoreAdapter {
1799
2185
  }
1800
2186
  }
1801
2187
  /**
1802
- * The roster a run of `workflowId` should carry as its seat pool. A workflow whose every phase
1803
- * runs a TOOL executor convenes no council and dispatches no seat, so its pool is `[]` — the
1804
- * engine routes each unit `tool` without consulting the pool (verified against the engine: an
1805
- * onboarding launch with `clis: []` distributes both units to `wicked-estate`). Any workflow
1806
- * 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).
1807
2195
  */
1808
- seatsForWorkflow(workflowId) {
2196
+ async seatsForWorkflow(workflowId) {
1809
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']);
1810
2201
  // The roster WITH standing when the daemon wired one (F-RECON-002/003) — `launchRun` then
1811
2202
  // benches `council_eligible: false` seats through `engineRosterJson`.
1812
- if (def === null || def.phases.length === 0)
1813
- return this.launchRoster();
1814
- const toolOnly = def.phases.every((p) => p.executor?.type === 'tool');
1815
- return toolOnly ? [] : this.launchRoster();
2203
+ if (executors.length === 0)
2204
+ return this.readyLaunchRoster();
2205
+ return executors.every((e) => e?.type === 'tool') ? [] : this.readyLaunchRoster();
1816
2206
  }
1817
2207
  /** Return the onboarding run id for a repo (undefined if not launched this session). */
1818
2208
  getOnboardRunId(repoId) {
@@ -1966,6 +2356,18 @@ export class CoreAdapter {
1966
2356
  steeringSupported() {
1967
2357
  return typeof this.core.steeringImport === 'function';
1968
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
+ }
1969
2371
  /**
1970
2372
  * Batch-import steering rules (frontmattered markdown docs and/or ready rule JSON) through the
1971
2373
  * engine's ingest normalize/validate path — the SAME path `rules ingest --dir` runs, on the
@@ -2086,6 +2488,47 @@ export class CoreAdapter {
2086
2488
  this.userWorkflows.set(def.id, def);
2087
2489
  }
2088
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
+ }
2089
2532
  listWorkflows() {
2090
2533
  this.hydrateFromOverlay();
2091
2534
  // Builtins first (stable ordering), but user-registered workflows take precedence when
@@ -2096,20 +2539,21 @@ export class CoreAdapter {
2096
2539
  const override = this.userWorkflows.get(w.id);
2097
2540
  if (!seen.has(w.id)) {
2098
2541
  seen.add(w.id);
2099
- result.push(override ?? w);
2542
+ result.push(withSystemFlag(override ?? w));
2100
2543
  }
2101
2544
  }
2102
2545
  for (const w of this.userWorkflows.values()) {
2103
2546
  if (!seen.has(w.id)) {
2104
2547
  seen.add(w.id);
2105
- result.push(w);
2548
+ result.push(withSystemFlag(w));
2106
2549
  }
2107
2550
  }
2108
2551
  return result;
2109
2552
  }
2110
2553
  getWorkflow(id) {
2111
2554
  this.hydrateFromOverlay();
2112
- 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);
2113
2557
  }
2114
2558
  /** Write a built-in workflow definition to the Rust overlay dir (and hot-register when possible).
2115
2559
  * Unlike registerWorkflow(), this does NOT touch userWorkflows, avoiding duplicates in listWorkflows(). */
@@ -2216,6 +2660,9 @@ export class CoreAdapter {
2216
2660
  await register.call(this.core, json);
2217
2661
  await writeFile(path, JSON.stringify(overlayDef, null, 2), 'utf8');
2218
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);
2219
2666
  return def.id;
2220
2667
  }
2221
2668
  /**
@@ -2270,6 +2717,19 @@ export class CoreAdapter {
2270
2717
  if (typeof v !== 'number' || !Number.isInteger(v) || v < 1 || v > 10000)
2271
2718
  delete parsed.graphNodeLimit;
2272
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
+ }
2273
2733
  // workerStallMinutes (crew#287): positive minutes; a hand-edited zero/negative/NaN would
2274
2734
  // make the stall watchdog fire on every sweep, so drop it and fall back to the default.
2275
2735
  if ('workerStallMinutes' in parsed) {
@@ -2400,6 +2860,8 @@ export class CoreAdapter {
2400
2860
  if (this.closed)
2401
2861
  return;
2402
2862
  this.closed = true;
2863
+ if (this.engineBus !== null && this.busDbPath !== undefined)
2864
+ detachEngineBus(this.busDbPath, this.engineBus);
2403
2865
  this.listeners.clear();
2404
2866
  this.subscription.close();
2405
2867
  }