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,1293 +0,0 @@
1
- /**
2
- * Opt-in governed answering of wicked-interactive's DEMO docs (CREW-UX-9 — the fourth
3
- * interactive leg, beside draft-events.ts, edit-events.ts, and chat-events.ts).
4
- *
5
- * wicked-interactive's demo pipeline (ADR-0018) is deliberately split: the SERVICE is the
6
- * model-free recorder — `recordDemo` (src/service/demo.js) launches Chromium, executes the
7
- * doc's `demo.spec.mjs` with Playwright, records video + storyboard, and lands the version —
8
- * while *something with intelligence* must AUTHOR that spec. A demo doc's creation emits
9
- * `wicked.interactive.doc.created` with `kind: "demo"` carrying the live target `url` and an
10
- * optional `brief` (recon: server.js `POST /api/docs`, the isDemo branch), and the doc then
11
- * sits on its "Learning <url>…" placeholder until someone writes `<docDir>/demo.spec.mjs` and
12
- * emits `wicked.interactive.demo.requested` — the retired assist agent's Step 8 (assist skill
13
- * SKILL.md). Since the assist session retired, NOTHING authored the spec: video generation had
14
- * hands (the recorder is proven working) but no brain. This module makes a crew-governed run
15
- * the answerer.
16
- *
17
- * Shape mirrors draft-events.ts (dynamic wicked-bus import, graceful degradation, durable
18
- * cursor `cursor_init: 'latest'` under a dedicated plugin name, durable replay-dedup ledger,
19
- * `wi-crew` narration via status.posted, heartbeat inside the UI's ~20s window). The
20
- * demo-specific deltas:
21
- *
22
- * - THE DELIVERABLE IS A SPEC, NOT HTML. The worker authors `demo.spec.mjs` — a plain ES
23
- * module exporting `meta` ({url, title, and optionally steps/captions/captionHoldMs}) and
24
- * `async run({ page, step, meta })` wrapping every meaningful action in
25
- * `step(label, fn, { say, holdMs })` — the exact contract `recordDemo` executes (recon:
26
- * interactive's demo.js + assist SKILL.md Step 8a). Crew never records anything; the
27
- * BRIDGE (interactive's service) owns Playwright + ffmpeg, and the storyboard version it
28
- * lands is the proven, already-working half of the loop.
29
- *
30
- * - THE WRITE-BOUNDARY LESSON (wicked-core#293/#294, learned the hard way on the draft leg):
31
- * the governed worker CANNOT write into the doc workspace and CANNOT be repo-bound. A
32
- * repoRef-bound run's tool-permission stream closes on the first prompt-needing call
33
- * (wicked-core#293), and an UNBOUND worker's governance boundary is {sandbox,
34
- * extraWriteRoots, ~/.claude/plugins} — reads/writes of the live doc workspace are DENIED
35
- * (wicked-core#294). So the launch is UNBOUND with the per-run inbox as the SOLE extra
36
- * write root; the task names `<inbox>/demo.spec.mjs` as the deliverable and carries the
37
- * url + brief in the (PTY-capped, single-line) problem text. At finalize CREW copies the
38
- * spec into the doc workspace — crew's own process is not the worker and is bound by no
39
- * run boundary — THEN emits `demo.requested`, then narrates complete. Copy-then-emit,
40
- * strictly: an emit before the copy would have the service record a missing/stale spec.
41
- *
42
- * - EMIT ACCEPTANCE (recon): interactive's command loop consumes `demo.requested` into
43
- * `materializeDemo` and drops ONLY frames produced by itself (`producer_id ===
44
- * PRODUCERS.SERVICE`, server.js runCommand loop) — the events.js ownership table
45
- * ([UI, AGENT] for demo.requested) is enforced on interactive's own emit paths, not at
46
- * consume — so a `wi-crew`-produced demo.requested is executed. The additive CREW owner
47
- * row in interactive's events.js is the same follow-up courtesy the draft/edit legs got.
48
- *
49
- * - THE STEP-FEEDBACK LOOP (assist SKILL.md Step 8c): a demo refines by RE-AUTHORING the
50
- * spec and re-emitting demo.requested — never by editing storyboard HTML. When the user
51
- * highlights a storyboard step and asks for a change, the service applies the deterministic
52
- * remainder to the storyboard and hands the structural items off on
53
- * `wicked.interactive.feedback.processed` — the SAME frame the edit seam answers. That
54
- * frame carries no `kind`, and until CREW-UX-9 the edit seam answered demo docs' handoffs
55
- * too: it rewrote the storyboard FRAGMENT (chapter list text), landed it as a structural
56
- * version, and the recording + spec never changed — the user's "change the demo" died as a
57
- * cosmetic caption edit the next re-record would overwrite. Now both seams gate on the SAME
58
- * disk truth (the doc manifest's `kind`, readable because interactive records kind for demo
59
- * docs — chat-events.ts readDocHead): manifest says `demo` → THIS seam re-authors the spec
60
- * per the feedback items, copies, re-emits demo.requested; anything else (including an
61
- * unreadable manifest) → the edit seam keeps it. One reader each way = no double-answer.
62
- * The edit seam's skip is conditional on THIS seam actually being armed (it is handed a
63
- * `demoSeamArmed` probe): with the demo seam off, a demo doc's handoff would otherwise be
64
- * answered by nobody, silently — so the edit seam posts an honest error status instead.
65
- *
66
- * - THE PHASE SHAPE IS MEASURED, NOT CHOSEN (wicked-core#293). A tool-using turn poisons a
67
- * later tool-permission request, which is never answered; the worker's turn then ends where
68
- * it stood. The rule that survives on real seats: THE PHASE THAT INSPECTS THE APP MUST BE
69
- * THE PHASE THAT WRITES THE SPEC, and nothing before it may touch a tool. So the first-spec
70
- * workflow is a TOOL-FREE planning phase then an inspect-and-write phase, and the re-author
71
- * workflow is one local-files-only writing phase. Both the "let recon look at the app" and
72
- * the "collapse it all into one phase" shapes were run against a real seat and died. The
73
- * numbers and the failure signatures are on INTERACTIVE_DEMO_WORKFLOW_DEF — read them
74
- * before reshaping either def.
75
- */
76
- import { resolveProjectGraphBinding } from '../projects/graph.js';
77
- import { copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
78
- import { basename, dirname, join } from 'node:path';
79
- import { randomUUID } from 'node:crypto';
80
- import { DOC_CREATED, DOC_NAME, GROUNDING_BINDING_WAIT_MS, INTERACTIVE_DOMAIN, INTERACTIVE_PRODUCER, STATUS_POSTED, docScope, groundablePath, oneLine, narrationStamps, } from './draft-events.js';
81
- import { runDirInsideRepo, snapshotRepo } from './repo-snapshot.js';
82
- import { groundingNarration, resolveGroundingRepos, snapshotDirName, } from './doc-grounding.js';
83
- import { FEEDBACK_PROCESSED, handoffKey, parseStructuralFeedback, } from './edit-events.js';
84
- import { readDocHead } from './chat-events.js';
85
- import { resolveInteractiveRoot } from './bridge-root.js';
86
- import { InteractiveHandoffLedger } from './ledger.js';
87
- import { crewStateHome } from '../projects/state-home.js';
88
- import { acpFallbackLine, councilAgreementPct, councilOutcomeSuffix, ungatedGateNote, workerToolCallDeniedLine, } from './council-outcome.js';
89
- import { busSubscriberErrorReporter } from './bus-subscriber-errors.js';
90
- // ── Vocabulary constants (interactive's, verbatim — src/service/events.js is the truth) ──────
91
- export const DEMO_REQUESTED = 'wicked.interactive.demo.requested';
92
- /**
93
- * The recorder's FAILURE, typed (F-RECON-013 companion; wicked-interactive PR #224 — F-RECON-012):
94
- * ONE `RecorderError` shape the service serialises identically on three surfaces, and the one crew
95
- * consumes is the bus frame — `wicked.interactive.status.posted` with the existing SeamStatusPayload
96
- * fields plus the error FLATTENED beside `state: "error"`:
97
- *
98
- * { document_id, project_id?, ts, state: "error", message,
99
- * code, source: "recorder", retryable: false, error, remedy,
100
- * browser?, missing?, install_command?, playwright_version?, browsers_path?, executable_path?,
101
- * step?: { index, label }, // recording_step_failed only
102
- * cause? }
103
- *
104
- * Codes: `recorder_browser_missing` · `recorder_browser_install_failed` · `recorder_launch_failed` ·
105
- * `recording_spec_missing` · `recording_spec_invalid` · `recording_step_failed` · `recording_failed`
106
- * (all `retryable: false`) · `recording_in_flight` (the ONLY `retryable: true`). Crew relays `code` +
107
- * `remedy` (+ `step`) into the thread-adjacent diagnostics — an error-level line for
108
- * `GET /diagnostics.recentErrors` naming the doc's spec run — and keeps the last failure per doc on
109
- * the subscription handle. Crew never replays: this seam emits no `demo.requested` of its own on a
110
- * failure (a `retryable: false` frame is terminal by contract; "Re-record" is the user's one retry).
111
- */
112
- export const RECORDER_ERROR_SOURCE = 'recorder';
113
- /** Exact-type filters with a domain guard — no wildcards. The seam listens on TWO topics:
114
- * doc.created (kind:demo — author the first spec) and feedback.processed (demo-kind docs —
115
- * re-author it). */
116
- export const INTERACTIVE_DEMO_BUS_FILTER = `${DOC_CREATED}@${INTERACTIVE_DOMAIN}`;
117
- export const INTERACTIVE_DEMO_FEEDBACK_BUS_FILTER = `${FEEDBACK_PROCESSED}@${INTERACTIVE_DOMAIN}`;
118
- export const INTERACTIVE_DEMO_RECORDER_BUS_FILTER = `${STATUS_POSTED}@${INTERACTIVE_DOMAIN}`;
119
- /** Dedicated durable-cursor identities — NOT the draft/edit/chat seams', so every interactive
120
- * seam advances an independent cursor and stopping one never strands another. The two demo
121
- * subscriptions get their own cursors too: they filter different types. */
122
- export const INTERACTIVE_DEMO_BUS_PLUGIN = 'wicked-crew-interactive-demo';
123
- export const INTERACTIVE_DEMO_FEEDBACK_BUS_PLUGIN = 'wicked-crew-interactive-demo-feedback';
124
- export const INTERACTIVE_DEMO_RECORDER_BUS_PLUGIN = 'wicked-crew-interactive-demo-recorder';
125
- /** The one file the whole demo pipeline pivots on (interactive demo.js `DEMO_SPEC`):
126
- * `recordDemo` refuses to record until `<docDir>/demo.spec.mjs` exists. */
127
- export const DEMO_SPEC_FILE = 'demo.spec.mjs';
128
- // ── The workflows (workflows-as-data) ─────────────────────────────────────────────────────────
129
- export const INTERACTIVE_DEMO_WORKFLOW = 'interactive-demo';
130
- export const INTERACTIVE_DEMO_REAUTHOR_WORKFLOW = 'interactive-demo-reauthor';
131
- /**
132
- * The governed workflow that authors a demo's FIRST spec. TWO agent phases — `scenes` (recon,
133
- * neutral: plan from the brief and the URL string, TOUCHING NOTHING) then `spec` (build,
134
- * creator: inspect the live app, write the Playwright spec, report).
135
- *
136
- * THE PHASE SPLIT IS LOAD-BEARING AND MEASURED — do not "simplify" it in either direction.
137
- * wicked-core#293: A TOOL-USING TURN POISONS A LATER TOOL-PERMISSION REQUEST — the request is
138
- * never answered, so the worker's turn ends where it stood. Three shapes have been run against
139
- * a real seat on a real stack (adversarial + build verification, 2026-08-24), and only one
140
- * survives:
141
- *
142
- * 1. recon phase that INSPECTS THE APP, then a spec phase — DEAD 4/4. The recon turn fetches
143
- * the target, and the spec phase's first permission request goes unanswered; nothing is
144
- * ever written.
145
- * 2. recon phase forbidden from using ANY TOOL, then a spec phase that curls the app and
146
- * writes — GREEN 15/15 end-to-end: spec authored, installed, a 436KB webm recorded, the
147
- * storyboard landed, `version.created kind:demo` on the bus. THIS IS THE SHAPE BELOW.
148
- * 3. ONE phase doing inspect + plan + write together (the obvious "no next turn to poison"
149
- * simplification) — DEAD 3/3, and this is the counter-intuitive result worth writing down:
150
- * the worker curls the pages, narrates "now writing the spec", and the turn ends AT the
151
- * write. Two runs died with an empty inbox and 161-345 byte outputs (one rejected by the
152
- * substance floor as `substanceRejected`, one "completed" with no deliverable and caught
153
- * by finalize). Collapsing the phases does NOT dodge #293 — it just moves the unanswered
154
- * permission request from the next turn into this one.
155
- *
156
- * A fourth data point narrows what "tool use" means here: the re-author leg's single phase
157
- * does `Read` + `Read` + `Write` in one turn and lands green (7/7, run 0704b910 — revised spec
158
- * installed, v2 re-recorded). So a turn that reads LOCAL FILES and then writes is fine; it is
159
- * the NETWORK fetch that costs the following write, whether that write is in the next turn
160
- * (shape 1) or the same one (shape 3). Treat that as the observed discriminator, not a proven
161
- * mechanism — #293 is wicked-core's to diagnose; this def only has to survive it.
162
- *
163
- * So the rule this workflow encodes is narrower than "one phase": THE PHASE THAT INSPECTS THE
164
- * APP MUST BE THE PHASE THAT WRITES THE DELIVERABLE, and nothing before it may touch a tool.
165
- * Phase 1 therefore plans from the brief and the URL STRING alone and is told so explicitly (a
166
- * soft constraint, and the model has broken a weaker one — the shipped wording said only "do
167
- * not write any files" and the worker fetched the app anyway — so the prohibition names tools
168
- * outright and says what it protects).
169
- *
170
- * The phase `instructions` adapt the demo-authoring contract from interactive's assist skill
171
- * (Step 8a/8b) and demo.js `recordDemo`'s executable contract: ESM exporting `meta` + `async
172
- * run({page, step, meta})`, every meaningful action wrapped in `step(label, fn, {say,
173
- * holdMs})`, capability narration, stable selectors, awaited waits, and NEVER credentials in
174
- * the file. SINGLE-LINE by contract (PTY seat runner, wicked-core FINDING-011).
175
- *
176
- * THE SUBSTANCE FLOOR (wicked-core `actor.rs`, "phase produced no reviewable substance"): a
177
- * governed creator/neutral phase whose fold carries neither a worktree diff nor >=200 trimmed
178
- * chars of prose is REJECTED and fails the run. These runs are UNBOUND — `workdir: null`, so
179
- * there is never a worktree diff by construction — and the deliverable is a FILE in the inbox,
180
- * invisible to that check. A worker told only to "end your reply with the absolute path" answers
181
- * in ~50 bytes and the whole demo dies at the gate (observed 3/3 on the real stack before this
182
- * wording). So both workflows' build phases demand a prose report of what they wrote: the
183
- * phase's own reviewable substance, not decoration.
184
- *
185
- * All gates are `auto` with `validator_pin: null` — no human gate — because the acceptance
186
- * gate for a demo is the RECORDING itself: the service executes the spec (a broken selector
187
- * fails the record with a step-precise error status) and the user judges the video on the
188
- * storyboard, then refines through the same loop. The assist skill's interactive scene-plan
189
- * confirmation (Step 8a.5) was an editorial affordance of a conversational session; the
190
- * governed loop's editorial channel is the step-feedback path below.
191
- */
192
- export const INTERACTIVE_DEMO_WORKFLOW_DEF = {
193
- id: INTERACTIVE_DEMO_WORKFLOW,
194
- is_system: true,
195
- phases: [
196
- {
197
- id: 'scenes',
198
- kind: 'recon',
199
- instructions: 'Plan the demo — and in this phase USE NO TOOLS AT ALL: no web fetch, no shell, no file reads, no writes. Work from the task text alone (the target application URL and the user\'s brief); inspecting the app is the NEXT phase\'s job, and a tool use here breaks that phase\'s ability to write the spec, which kills the whole demo. Decompose the brief into 3-6 named scenes — each a short capability label stating what the viewer should take away (good: "Create a document"; bad: "Click the New button"), covering setup/context beats, the main value moments, and the payoff. Merge trivial setup steps, split compound flows — a demo that is one scene for a multi-step brief is not a demo. For each scene note the click-path you EXPECT (navigation, the kind of control to look for, waits) and a one-sentence narration line that states the capability, not the on-screen data; say plainly that the selectors are expectations for the next phase to confirm against the live page. Never invent app features the brief and the URL do not support. Output the scene plan as plain text.',
200
- gate_type: 'value',
201
- gate: 'auto',
202
- executes_code: false,
203
- verified_evidence: false,
204
- required_deliverables: [],
205
- depends_on: [],
206
- role: 'neutral',
207
- skill_ref: null,
208
- allowed_skills: [],
209
- validator_pin: null,
210
- },
211
- {
212
- id: 'spec',
213
- kind: 'build',
214
- instructions: 'FIRST inspect the live target application yourself: curl the HTML of the URL named in the task and of every further page the scene plan visits (use curl — the target is often a plain-HTTP or localhost app a web-fetch tool cannot reach), so every selector you use provably exists on the page; never guess a selector, and correct the scene plan wherever the real page contradicts it. THEN, using the scene plan from the prior phase, write the COMPLETE Playwright demo spec and SAVE it to the absolute output file named in the task (create parent directories if needed, overwrite if present) — the file on disk is the deliverable, so write it before you finish. THEN REPORT IN PROSE, in at least 120 words, what you inspected and what you wrote: name the pages you fetched and the selectors they gave you, walk the reader through every step in order (its label, the selectors and waits it uses, and the capability its narration states), call out any scene-plan beat you merged, split, or dropped and why, and end with the absolute path you wrote — a reply that is only the path is an unreviewable phase and will be rejected. Contract (wicked-interactive demo.spec.mjs): a plain, standalone ES module with NO imports that exports const meta = { url, title, and optionally steps, captions, captionHoldMs } and export async function run({ page, step, meta }) — the service supplies page (Playwright) and step; begin run() with await page.goto(meta.url); wrap EVERY meaningful action in await step(label, async () => { ... }, { say, holdMs }) with one step per scene and the scene\'s capability name as the label; narrate the meaningful beats via say (the capability, not the on-screen data); prefer stable selectors (roles, text, ids) and await your waits (waitForURL, waitForSelector) so the recording captures settled UI; NEVER write credentials or secrets into the spec — read them from process.env at run time.',
215
- gate_type: 'execution',
216
- gate: 'auto',
217
- executes_code: false,
218
- verified_evidence: false,
219
- required_deliverables: [],
220
- depends_on: ['scenes'],
221
- role: 'creator',
222
- skill_ref: null,
223
- allowed_skills: [],
224
- validator_pin: null,
225
- },
226
- ],
227
- };
228
- /**
229
- * The governed workflow that RE-authors a spec per step feedback (Step 8c). ONE agent phase —
230
- * `respec` (build, creator role) — the edit leg's rationale: the current spec and the user's
231
- * instructions are already extracted into files the task names, so a recon phase would only
232
- * burn a council turn re-reading what the build phase reads anyway.
233
- *
234
- * SAME v2 TREATMENT AS THE FIRST-SPEC WORKFLOW (wicked-core#293), which for this leg means
235
- * LEAVING IT ALONE and saying why. Nothing runs before the phase that writes, so no predecessor
236
- * turn can poison its permissions — and its own tool use is LOCAL FILE READS inside the run's
237
- * declared write root (the same shape as the interactive-edit workflow's single phase, which
238
- * reads a handoff JSON and writes fragments in one turn, in production, today). What this leg
239
- * must NOT grow is a live re-fetch of the app: the first-spec measurements above show a turn
240
- * that fetches the network and then writes losing the write outright (shape 3, dead 3/3),
241
- * while this leg's Read+Read+Write turn lands green (7/7 on a real seat, run 0704b910). If a
242
- * feedback item ever genuinely needs a selector the current spec lacks, the fix is a preceding
243
- * TOOL-FREE phase plus inspection moved into this one — the shape 2 arrangement — not a fetch
244
- * bolted onto the writing turn.
245
- */
246
- export const INTERACTIVE_DEMO_REAUTHOR_WORKFLOW_DEF = {
247
- id: INTERACTIVE_DEMO_REAUTHOR_WORKFLOW,
248
- is_system: true,
249
- phases: [
250
- {
251
- id: 'respec',
252
- kind: 'build',
253
- instructions: 'Read the current demo spec and the feedback JSON named in the task; re-author the spec so the recorded demo fulfils every feedback item, and SAVE the complete revised spec to the exact absolute output file named in the task (create parent directories if needed, overwrite if present) — the file on disk is the deliverable, so write it before you finish. Work from the two files and the current spec\'s own selectors — do NOT fetch the live application in this phase; if a feedback item needs a control the current spec never touches, choose the most robust selector the item\'s storyboard fragment supports and say so in your report rather than guessing silently. THEN REPORT IN PROSE, in at least 120 words, what changed: take each feedback item in turn, say which step(s) you altered and how the revised selectors/waits/narration answer it, note anything you deliberately left untouched, and end with the absolute path you wrote — a reply that is only the path is an unreviewable phase and will be rejected. Each feedback item carries the user\'s instruction plus the storyboard fragment of the step it targets (the step\'s label appears in the fragment text) — change the matching step(s) and leave unrelated steps untouched unless the instruction asks for a restructure. Keep the same executable contract as the current spec: a plain, standalone ES module with NO imports exporting const meta = { url, title, ... } and export async function run({ page, step, meta }); begin run() with await page.goto(meta.url); wrap every meaningful action in await step(label, fn, { say, holdMs }); narrate capabilities via say; prefer stable selectors and await your waits; NEVER write credentials or secrets into the spec — read them from process.env at run time.',
254
- gate_type: 'execution',
255
- gate: 'auto',
256
- executes_code: false,
257
- verified_evidence: false,
258
- required_deliverables: [],
259
- depends_on: [],
260
- role: 'creator',
261
- skill_ref: null,
262
- allowed_skills: [],
263
- validator_pin: null,
264
- },
265
- ],
266
- };
267
- /** URL budget for the problem statement: the URL must ride VERBATIM (a respelled URL targets
268
- * the wrong app), so an over-budget one refuses the parse rather than truncating. */
269
- export const DEMO_URL_MAX = 1000;
270
- /**
271
- * Parse a bus frame into a {@link DemoDocCreated}, or `null` when it is not an actionable
272
- * demo creation: wrong type, non-`demo` kind (source/html docs are the draft seam's and the
273
- * user's own business), missing/malformed document_id, or an unusable URL. The URL must be a
274
- * parseable http(s) URL with no whitespace/control characters — it rides the single-line PTY
275
- * problem verbatim (interactive's server validates this at creation, so a violation here is
276
- * producer drift, not a user path).
277
- */
278
- export function parseDemoDocCreated(eventType, payload) {
279
- if (eventType !== DOC_CREATED)
280
- return null;
281
- if (typeof payload !== 'object' || payload === null)
282
- return null;
283
- const p = payload;
284
- if (p['kind'] !== 'demo')
285
- return null;
286
- const documentId = typeof p['document_id'] === 'string' ? p['document_id'] : '';
287
- if (!DOC_NAME.test(documentId))
288
- return null;
289
- const url = typeof p['url'] === 'string' ? p['url'].trim() : '';
290
- if (url.length === 0 || url.length > DEMO_URL_MAX || /[\s\u0000-\u001f]/.test(url))
291
- return null;
292
- try {
293
- const parsed = new URL(url);
294
- if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:')
295
- return null;
296
- }
297
- catch {
298
- return null;
299
- }
300
- const brief = typeof p['brief'] === 'string' ? p['brief'] : '';
301
- const projectId = typeof p['project_id'] === 'string' && p['project_id'].length > 0 ? p['project_id'] : undefined;
302
- return { documentId, url, brief, ...(projectId !== undefined ? { projectId } : {}) };
303
- }
304
- /** The recorder's own error kind for the fresh-install cause (matches interactive's preflight). */
305
- export const RECORDER_BROWSER_MISSING = 'recorder_browser_missing';
306
- /**
307
- * Read a `status.posted` frame as a recorder failure: `state: "error"` + `source: "recorder"` + a
308
- * string `code`. Anything else — a seam's own narration (crew's `wi-crew` producer included), a
309
- * plain error status without the typed fields — is `null`.
310
- */
311
- export function parseRecorderError(eventType, payload) {
312
- if (eventType !== STATUS_POSTED)
313
- return null;
314
- if (typeof payload !== 'object' || payload === null)
315
- return null;
316
- const p = payload;
317
- if (p['state'] !== 'error' || p['source'] !== RECORDER_ERROR_SOURCE || typeof p['code'] !== 'string' || p['code'].trim() === '')
318
- return null;
319
- const documentId = p['document_id'];
320
- if (typeof documentId !== 'string' || !DOC_NAME.test(documentId))
321
- return null;
322
- const rawMessage = typeof p['message'] === 'string' ? p['message'] : '';
323
- const message = typeof p['error'] === 'string' && p['error'].trim() !== ''
324
- ? oneLine(p['error'], 600)
325
- : rawMessage.trim() !== ''
326
- ? oneLine(rawMessage.replace(/^Recording failed:\s*/i, ''), 600)
327
- : 'the recorder failed';
328
- const step = p['step'];
329
- const stepOut = typeof step === 'object' && step !== null && typeof step.index === 'number' && typeof step.label === 'string'
330
- ? { index: step.index, label: oneLine(step.label, 200) }
331
- : undefined;
332
- const missing = Array.isArray(p['missing']) ? p['missing'].filter((m) => typeof m === 'string') : undefined;
333
- const out = {
334
- documentId,
335
- error: {
336
- kind: p['code'].trim(),
337
- message,
338
- retryable: p['retryable'] === true,
339
- ...(typeof p['remedy'] === 'string' && p['remedy'].trim() !== '' ? { remedy: oneLine(p['remedy'], 400) } : {}),
340
- ...(typeof p['install_command'] === 'string' && p['install_command'].trim() !== '' ? { installCommand: oneLine(p['install_command'], 400) } : {}),
341
- ...(typeof p['browser'] === 'string' && p['browser'] !== '' ? { browser: p['browser'] } : {}),
342
- ...(missing !== undefined && missing.length > 0 ? { missing } : {}),
343
- ...(stepOut !== undefined ? { step: stepOut } : {}),
344
- ...(typeof p['cause'] === 'string' && p['cause'].trim() !== '' ? { cause: oneLine(p['cause'], 400) } : {}),
345
- },
346
- };
347
- if (typeof p['project_id'] === 'string' && p['project_id'] !== '')
348
- out.projectId = p['project_id'];
349
- if (typeof p['version'] === 'number' && Number.isInteger(p['version']) && p['version'] >= 0)
350
- out.version = p['version'];
351
- return out;
352
- }
353
- /** The one diagnostics line for a relayed recorder failure: doc, spec run, code, cause, step, remedy. */
354
- export function recorderFailureLine(f, runId) {
355
- return (`[interactive-demo] recording FAILED for doc ${f.documentId}` +
356
- (runId !== undefined ? ` (spec run ${runId})` : '') +
357
- `: ${f.error.kind} — ${f.error.message}` +
358
- (f.error.step !== undefined ? ` [step ${f.error.step.index}: ${f.error.step.label}]` : '') +
359
- (f.error.remedy !== undefined ? `. Remedy: ${f.error.remedy}` : '') +
360
- (f.error.installCommand !== undefined && f.error.installCommand !== f.error.remedy ? ` (${f.error.installCommand})` : '') +
361
- (f.error.retryable ? '. Retryable.' : '. Not retryable — Re-record fails the same way until this is fixed.'));
362
- }
363
- /**
364
- * The first-spec run's problem statement (the engine scopes it per phase and folds each
365
- * phase's instructions on top). Carries the doc identity, the target URL VERBATIM, the brief
366
- * (flattened + capped: PTY, wicked-core FINDING-011), and the absolute inbox path the spec
367
- * must land at. Deliberately NO doc-workspace path: the unbound worker could not read or
368
- * write it anyway (wicked-core#294) — crew installs the spec at finalize.
369
- */
370
- export function demoProblem(doc, outPath, grounding) {
371
- const brief = doc.brief.trim().length > 0
372
- ? oneLine(doc.brief, 2000)
373
- : "(no brief provided — demonstrate the app's core flow)";
374
- // F-046: the demo's SUBJECT — the application's own source, snapshotted into the inbox. LOCAL
375
- // reads only (the re-author leg's Read+Read+Write shape lands green; it is the NETWORK fetch
376
- // that costs the following write, wicked-core#293), and offered as context for routes and
377
- // selectors, never as a substitute for inspecting the live page.
378
- const subjects = grounding?.subjects ?? [];
379
- const source = subjects.length > 0
380
- ? `The application's source is the ${subjects.length === 1 ? 'repository' : 'repositories'} ` +
381
- `${subjects.map((s) => (s.snapshotDir !== undefined ? `${s.name} (offline snapshot at ${s.snapshotDir})` : s.name)).join(', ')}` +
382
- ` — you may read ${subjects.some((s) => s.snapshotDir !== undefined) ? 'the snapshot files locally' : 'it through the estate tools'} ` +
383
- `to learn its routes and stable selectors before you inspect the live page; never treat a sibling ` +
384
- `repository in the same project as this app. `
385
- : '';
386
- return (`Author the Playwright demo spec for the wicked-interactive demo document "${doc.documentId}". ` +
387
- `Target application URL: ${doc.url} The user's brief: ${brief} ${source}` +
388
- `The finished spec MUST be written to exactly this absolute file path: ${outPath}`);
389
- }
390
- /**
391
- * The re-author run's problem statement. Single-line; the bulky parts (the current spec, the
392
- * feedback fragments) ride in FILES inside the inbox — write roots are readable
393
- * (wicked-core#259) — so only identity, the capped instruction gist, and the three paths ride
394
- * here (the edit leg's handoff-by-file discipline).
395
- */
396
- export function demoReauthorProblem(handoff, currentSpecPath, feedbackPath, outPath) {
397
- const gist = oneLine(handoff.items.map((i) => i.instruction).filter((s) => s.length > 0).join('; '), 600);
398
- return (`Re-author the Playwright demo spec for the wicked-interactive demo document ` +
399
- `"${handoff.documentId}" per the user's step feedback (${handoff.items.length} item(s) on ` +
400
- `version ${handoff.version}). The CURRENT spec is the file at this absolute path — read it ` +
401
- `first: ${currentSpecPath} The feedback items (instruction + the targeted storyboard step's ` +
402
- `HTML fragment, per item) are in the JSON file at: ${feedbackPath} ` +
403
- `The user asked: ${gist.length > 0 ? gist : '(no instruction text)'} ` +
404
- `The revised spec MUST be written to exactly this absolute file path: ${outPath}`);
405
- }
406
- /** Deterministic bus idempotency key for the FIRST record request this seam may trigger per
407
- * document. Distinct from every re-author key: a demo legitimately re-records many times, so
408
- * the record trigger must never dedupe across authoring generations. */
409
- export function demoIdempotencyKey(documentId) {
410
- return `crew:interactive.demo:${documentId}:spec`;
411
- }
412
- /** Deterministic bus idempotency key for the one re-record a given feedback handoff earns. */
413
- export function demoReauthorIdempotencyKey(documentId, version) {
414
- return `crew:interactive.demo:${documentId}:v${version}`;
415
- }
416
- /**
417
- * Deterministic pre-install self-check on the worker's spec (the demo leg's sibling of the
418
- * edit leg's INV-2 check): `recordDemo` imports the file and throws on a missing `run` export
419
- * (interactive demo.js), which would surface as a recording error AFTER crew claimed success.
420
- * Fail honest crew-side instead: a spec that plainly cannot satisfy the module contract is
421
- * never installed. Returns the human-readable violation, or `null` when the spec looks like a
422
- * real module. Deliberately shallow — a static check cannot execute the module, so it pins
423
- * only the export shape the contract mandates; a wrong-but-well-formed spec is the recording's
424
- * (and the user's) to judge, exactly like a wrong-but-anchor-safe edit on the edit leg.
425
- */
426
- export function specSelfCheck(spec) {
427
- if (spec.trim().length === 0)
428
- return 'the spec file is empty';
429
- const exportsMeta = /export\s+(const|let|var)\s+meta\b/.test(spec) || /export\s*\{[^}]*\bmeta\b/.test(spec);
430
- if (!exportsMeta)
431
- return 'the spec does not export `meta`';
432
- // `run` must be AWAITABLE, not merely present (Copilot, #316): recordDemo awaits it, and a
433
- // synchronous `run` returns before its `step()` calls settle — the recorder would then film
434
- // an empty page and land a valid-looking but contentless video. A silent bad artifact is
435
- // exactly what this seam must not produce, so the async marker is part of the shape check
436
- // for the two forms a static read can see. The re-export form (`export { run }`) still
437
- // cannot be judged here — its asyncness lives at the declaration — so it stays accepted and
438
- // the recording remains its judge.
439
- const asyncRunDecl = /export\s+async\s+function\s+run\b/.test(spec);
440
- const asyncRunBinding = /export\s+(const|let|var)\s+run\s*=\s*async\b/.test(spec);
441
- const runReExport = /export\s*\{[^}]*\brun\b/.test(spec);
442
- const syncRunDecl = /export\s+function\s+run\b/.test(spec);
443
- const syncRunBinding = /export\s+(const|let|var)\s+run\s*=/.test(spec) && !asyncRunBinding;
444
- if (syncRunDecl || syncRunBinding) {
445
- return 'the spec exports `run` but not as an async function (the recorder awaits it)';
446
- }
447
- if (!asyncRunDecl && !asyncRunBinding && !runReExport) {
448
- return 'the spec does not export an async `run` function';
449
- }
450
- return null;
451
- }
452
- /** The seam's durable state (handoff ledger + working dirs) follows the daemon's state home —
453
- * the `--db` parent when configured, `~/.wicked-crew` otherwise (crew#353): an isolated
454
- * daemon's interactive handoffs must not land in the operator's real home. */
455
- function defaultStateDir() {
456
- return crewStateHome();
457
- }
458
- /** The council roster a launch carries when no `clisJson` override is set. F-RECON-002/003: the
459
- * server injects `roster` — the daemon's roster WITH standing (`api/roster-standing.ts`), so
460
- * `launchRun`'s `engineRosterJson` benches signed-out seats instead of convening (or electing)
461
- * them. Without an injected accessor the adapter's own `launchRoster()` is asked (the same
462
- * standing when the daemon wired it); the raw registry, resolved lazily through the adapter's
463
- * class so this module never imports the native addon at runtime, is the last resort (unit tests
464
- * pass `clisJson` and a fake adapter). */
465
- function rosterOf(adapter, roster) {
466
- if (roster !== undefined)
467
- return roster();
468
- const own = adapter.launchRoster;
469
- if (typeof own === 'function')
470
- return own.call(adapter);
471
- return adapter.constructor.roster();
472
- }
473
- /**
474
- * Arm the seam: register the demo workflows, open durable subscriptions on
475
- * `wicked.interactive.doc.created` (kind:demo) and `wicked.interactive.feedback.processed`
476
- * (demo-kind docs), and answer each with a governed run that authors/installs
477
- * `demo.spec.mjs` and ends in `wicked.interactive.demo.requested` — the model-free service
478
- * then records the video and lands the storyboard version itself.
479
- *
480
- * Graceful degradation mirrors the sibling seams: a missing wicked-bus package or an
481
- * unopenable db LOGS and returns `null` — the daemon must still boot on a machine whose bus
482
- * is broken; the demo doc then simply keeps its placeholder (the pre-CREW-UX-9 state).
483
- */
484
- export async function startInteractiveDemoSubscriber(adapter, opts = {}) {
485
- const log = opts.log ?? ((m) => console.error(m));
486
- let bus;
487
- try {
488
- bus = await import('wicked-bus');
489
- }
490
- catch (err) {
491
- log(`[interactive-demo] wicked-bus is not importable — governed demo authoring disabled: ${err instanceof Error ? err.message : String(err)}`);
492
- return null;
493
- }
494
- let db;
495
- let config;
496
- try {
497
- config = bus.loadConfig(opts.dbPath !== undefined ? { db_path: opts.dbPath } : {});
498
- db = bus.openDb(opts.dbPath !== undefined ? { db_path: opts.dbPath } : {});
499
- }
500
- catch (err) {
501
- log(`[interactive-demo] could not open the bus db${opts.dbPath !== undefined ? ` at ${opts.dbPath}` : ''} — governed demo authoring disabled: ${err instanceof Error ? err.message : String(err)}`);
502
- return null;
503
- }
504
- // Both workflows ride the normal registration path — core validates each def BEFORE it is
505
- // persisted/hot-registered (FINDING-002 ordering), so a drifted def fails the arm loudly
506
- // instead of failing the first launch obscurely.
507
- try {
508
- await adapter.registerWorkflow(INTERACTIVE_DEMO_WORKFLOW_DEF);
509
- await adapter.registerWorkflow(INTERACTIVE_DEMO_REAUTHOR_WORKFLOW_DEF);
510
- }
511
- catch (err) {
512
- log(`[interactive-demo] could not register the demo workflows — governed demo authoring ` +
513
- `disabled: ${err instanceof Error ? err.message : String(err)}`);
514
- return null;
515
- }
516
- const ledger = new InteractiveHandoffLedger(opts.ledgerPath ?? join(defaultStateDir(), 'interactive-demo-ledger.json'));
517
- const demoDir = opts.demoDir ?? join(defaultStateDir(), 'interactive-demos');
518
- const heartbeatMs = opts.heartbeatMs ?? 15_000;
519
- const resolveDocsRoot = opts.resolveDocsRoot ?? (() => resolveInteractiveRoot(null));
520
- const groundingStore = opts.groundingStore;
521
- const inFlight = new Map(); // runId → live state (pre-launch placeholders included)
522
- let closed = false; // set by stop(): a handler mid-snapshot must never launch after shutdown
523
- /** Best-effort removal of a run's app-source snapshots (F-046) — a leftover is a disk-space
524
- * wart, never a correctness one. Empties the array it is handed. */
525
- function removeSnapshots(dirs) {
526
- const parents = new Set();
527
- for (const dir of dirs.splice(0)) {
528
- try {
529
- rmSync(dir, { recursive: true, force: true });
530
- log(`[interactive-demo] removed repo snapshot ${dir}`);
531
- parents.add(dirname(dir));
532
- }
533
- catch (err) {
534
- log(`[interactive-demo] could not remove repo snapshot ${dir}: ${err instanceof Error ? err.message : String(err)}`);
535
- }
536
- }
537
- // The per-run `repos/` parent held nothing but this run's snapshots — it goes too.
538
- for (const parent of parents) {
539
- if (basename(parent) !== 'repos')
540
- continue;
541
- try {
542
- rmSync(parent, { recursive: true, force: true });
543
- }
544
- catch {
545
- /* best-effort */
546
- }
547
- }
548
- }
549
- /** Emit onto interactive's vocabulary as the `wi-crew` producer. Never throws into the
550
- * caller: narration/announce failures are logged — a lost status line must not kill the
551
- * subscription, and a duplicate demo.requested (WB-002) is the idempotency key WORKING. */
552
- function emitInteractive(type, payload, idempotencyKey) {
553
- try {
554
- bus.emit(db, config, {
555
- event_type: type,
556
- // Subdomains per interactive's events.js table: demo.requested rides `demo`,
557
- // status.posted rides `status`.
558
- domain: INTERACTIVE_DOMAIN,
559
- subdomain: type === DEMO_REQUESTED ? 'demo' : 'status',
560
- payload: { ts: new Date().toISOString(), ...payload },
561
- producer_id: INTERACTIVE_PRODUCER,
562
- ...(idempotencyKey !== undefined ? { idempotency_key: idempotencyKey } : {}),
563
- });
564
- return true;
565
- }
566
- catch (err) {
567
- const code = err.error;
568
- if (code === 'WB-002') {
569
- // Duplicate idempotency key — the emit already happened (redelivery race). Success.
570
- return true;
571
- }
572
- log(`[interactive-demo] emit ${type} failed: ${err instanceof Error ? err.message : String(err)}`);
573
- return false;
574
- }
575
- }
576
- /** Every `status.posted` this seam emits is typed as the published frame's payload (codex on
577
- * crew#506: the wire type at the real boundary, not a detached alias) — `emitInteractive` adds `ts`. */
578
- function emitStatus(payload) {
579
- return emitInteractive(STATUS_POSTED, { ...payload });
580
- }
581
- function narrate(flight, message) {
582
- flight.narration = message;
583
- emitStatus({
584
- ...docScope(flight.documentId, flight.projectId),
585
- state: 'working',
586
- message,
587
- ...narrationStamps(flight),
588
- });
589
- }
590
- function endFlight(runId) {
591
- const flight = inFlight.get(runId);
592
- if (flight) {
593
- if (flight.heartbeat !== undefined)
594
- clearInterval(flight.heartbeat);
595
- inFlight.delete(runId);
596
- }
597
- return flight;
598
- }
599
- /**
600
- * The inbox must never sit inside a registered repository (codex on crew#506, CRITICAL): the
601
- * run declares it as the worker's extra write root, so a demo dir configured inside a checkout
602
- * would hand the unbound worker write access to live source whatever the run is about. Checked
603
- * against the WHOLE registry before anything is created; a hit refuses the launch — error status,
604
- * no directory, no run, no ledger row, the frame dead-lettered (replayable once the config is
605
- * fixed). `null` when clear.
606
- */
607
- async function refuseRunDirInsideRepo(runDir, documentId, projectId) {
608
- let inside;
609
- try {
610
- inside = await runDirInsideRepo(adapter, runDir);
611
- }
612
- catch (err) {
613
- // A path that cannot be RESOLVED (EACCES/ELOOP on a component — never plain ENOENT, which
614
- // the resolver walks past) cannot be proven clear of the registry either: fail closed WITH a
615
- // user-facing status naming the cause, then dead-letter (Copilot on crew#506).
616
- const why = err instanceof Error ? err.message : String(err);
617
- const message = `Crew refused to author this demo: the configured demo directory (${demoDir}) could not be verified ` +
618
- `against the registered repositories (${why}). Fix the path or its permissions, then replay the request.`;
619
- emitStatus({ ...docScope(documentId, projectId), state: 'error', message });
620
- log(`[interactive-demo] ${documentId}: REFUSING launch — run dir ${runDir} unverifiable: ${why}`);
621
- throw new Error(message);
622
- }
623
- if (inside === null)
624
- return;
625
- const message = `Crew refused to author this demo: the configured demo directory (${demoDir}) overlaps the ` +
626
- `registered repository ${inside}, so launching would give the worker write access inside live ` +
627
- `source. Point the crew demo directory outside every registered repository, then replay the request.`;
628
- emitStatus({ ...docScope(documentId, projectId), state: 'error', message });
629
- log(`[interactive-demo] ${documentId}: REFUSING launch — run dir ${runDir} is inside repo ${inside}`);
630
- throw new Error(message);
631
- }
632
- /** Terminal-event fold: turn the governed run's own events into interactive narration, and
633
- * close the loop (copy spec → emit demo.requested) when the run lands. */
634
- const offCoreEvents = adapter.onEvent((event) => {
635
- const runId = typeof event.session === 'string' ? event.session : undefined;
636
- if (runId === undefined)
637
- return;
638
- const flight = inFlight.get(runId);
639
- if (flight === undefined)
640
- return;
641
- // F-4R2-005: every narration line and heartbeat from here carries the run id and the ord of the
642
- // latest unit-scoped frame (`narrationStamps`), so a skin keys the thread per run and per unit.
643
- flight.runId ??= runId;
644
- if (typeof event.ord === 'number')
645
- flight.narrationOrd = event.ord;
646
- // Narration ladder — same rationale as the sibling folds: the heartbeat repeats the LATEST
647
- // line and the transcript dedups repeats, so advancing the line = visible progress.
648
- const authoring = flight.leg === 'spec' ? 'the click-path spec' : 'the revised click-path spec';
649
- // The crew#311 deliverable floor is a DETERMINISTIC tool phase — no seat, no council. Core
650
- // still emits the seat-selection events for it (its `cli` is the node interpreter's absolute
651
- // path), so narrating them verbatim put "Council picked /opt/homebrew/.../node to write the
652
- // click-path spec…" in the reader's thread. Drop those two lines for the floor ord; the
653
- // `unitDispatched` line that follows immediately says what the phase actually is.
654
- const isFloorOrd = (e) => typeof e.ord === 'number' &&
655
- e.ord > flight.agentPhaseCount;
656
- if (event.type === 'councilConvened') {
657
- if (isFloorOrd(event))
658
- return;
659
- const seats = Array.isArray(event.clis) ? event.clis.length : 0;
660
- // "0-seat council" reads like a bug — generic phrasing whenever clis is missing or empty
661
- // (Copilot, #269).
662
- const council = seats > 0 ? `a ${seats}-seat council` : 'a council';
663
- narrate(flight, `Convening ${council} to pick who writes ${authoring}…`);
664
- return;
665
- }
666
- if (event.type === 'unitDistributed') {
667
- if (isFloorOrd(event))
668
- return;
669
- const who = typeof event.cli === 'string' ? event.cli : 'a worker';
670
- const agreement = councilAgreementPct(event);
671
- const pct = agreement !== null ? ` (${agreement}% agreement)` : '';
672
- // Honest about a council that held on a fraction of its seats (F-4R2-007).
673
- narrate(flight, `Council picked ${who} to write ${authoring}${pct}${councilOutcomeSuffix(event)}…`);
674
- return;
675
- }
676
- if (event.type === 'unitDispatched') {
677
- // The first-spec leg plans (tool-free) then inspects-and-writes; the re-author leg is one
678
- // writing phase; both then run the crew#311 deliverable floor. Narrate whichever the ord
679
- // actually is rather than assuming a count.
680
- const ord = typeof event.ord === 'number' ? event.ord : 0;
681
- const runPhaseCount = flight.agentPhaseCount + 1;
682
- const line = ord > flight.agentPhaseCount
683
- ? 'checking the spec file was actually written…'
684
- : ord >= flight.agentPhaseCount
685
- ? `inspecting the app and writing ${authoring}…`
686
- : 'planning the demo scenes…';
687
- narrate(flight, `Crew phase ${ord}/${runPhaseCount}: ${line}`);
688
- return;
689
- }
690
- if (event.type === 'toolInvoked') {
691
- const tools = Array.isArray(event.tools) ? [...new Set(event.tools)].join(', ') : '';
692
- if (tools)
693
- narrate(flight, `Worker is using ${tools} on your demo…`);
694
- return;
695
- }
696
- if (event.type === 'unitOutputCaptured') {
697
- narrate(flight, `Spec work finished — the governance gate is reviewing it…`);
698
- return;
699
- }
700
- if (event.type === 'gateDecided' && event.allow === true) {
701
- const ord = typeof event.ord === 'number' ? event.ord : 0;
702
- narrate(flight, ord > flight.agentPhaseCount
703
- ? 'Spec file verified on disk — installing it and starting the recording…'
704
- : 'Gate approved the spec — checking the file landed…');
705
- return;
706
- }
707
- // Wave 6 — the honest gate (F-7R2-005): a unit NOTHING gated must read as UNGATED in the thread,
708
- // never as approved; the engine says so on `gateEvaluated.ungated` and names the missing layers.
709
- if (event.type === 'gateEvaluated') {
710
- const note = ungatedGateNote(event);
711
- if (note !== null) {
712
- const ord = typeof event.ord === 'number' ? event.ord : 0;
713
- narrate(flight, `Gate for unit ${ord}: ${note}`);
714
- }
715
- return;
716
- }
717
- // Wave 6 — the fenced worker (F-7R2-012): a seat that tried to push or open a PR itself was
718
- // refused; the thread names who, what, and that delivery belongs to the run's deliver phase.
719
- if (event.type === 'workerToolCallDenied') {
720
- narrate(flight, workerToolCallDeniedLine(event));
721
- return;
722
- }
723
- if (event.type === 'acpFallback') {
724
- narrate(flight, acpFallbackLine(event));
725
- return;
726
- }
727
- if (event.type === 'sessionCompleted') {
728
- endFlight(runId);
729
- finalize(flight, runId);
730
- return;
731
- }
732
- if (event.type === 'stepFailed') {
733
- // Remember the engine's own reason (crew#311) so the terminal status can name it.
734
- const detail = typeof event.detail === 'string' ? event.detail.trim() : '';
735
- if (detail.length > 0)
736
- flight.failureDetail = detail;
737
- return;
738
- }
739
- if (event.type === 'sessionFailed' || event.type === 'runCancelled') {
740
- endFlight(runId);
741
- removeSnapshots(flight.snapshotDirs);
742
- ledger.recordFailure(flight.key);
743
- const why = flight.failureDetail !== undefined ? ` Reason: ${oneLine(flight.failureDetail, 600)}` : '';
744
- emitStatus({
745
- ...docScope(flight.documentId, flight.projectId),
746
- state: 'error',
747
- message: `The crew run authoring this demo's spec ${event.type === 'runCancelled' ? 'was cancelled' : 'failed'} ` +
748
- `(run ${runId}).${why} Inspect it via the crew API (GET /api/v1/runs/${runId}); no recording was triggered.`,
749
- });
750
- log(`[interactive-demo] run ${runId} for ${flight.key} ended: ${event.type}`);
751
- }
752
- });
753
- /**
754
- * The run is terminal: verify the spec (exists, non-empty, passes the static module
755
- * self-check), COPY it into the doc workspace, THEN emit demo.requested — strictly in that
756
- * order (the service reads `<docDir>/demo.spec.mjs` when the request arrives; an emit-first
757
- * ordering would record a missing or stale spec). Every failure is honest: error status,
758
- * failure row, and NO demo.requested — the doc keeps its current state instead of the
759
- * service throwing "no demo.spec.mjs authored yet" at a request crew knew was hollow.
760
- */
761
- function finalize(flight, runId) {
762
- // The run is terminal — its app-source snapshots are done serving reads, on every branch below.
763
- removeSnapshots(flight.snapshotDirs);
764
- const { documentId, projectId, outPath, key } = flight;
765
- const fail = (message) => {
766
- ledger.recordFailure(key);
767
- emitStatus({ ...docScope(documentId, projectId), state: 'error', message });
768
- log(`[interactive-demo] run ${runId} for ${key} failed at finalize: ${message}`);
769
- };
770
- let spec = '';
771
- try {
772
- spec = readFileSync(outPath, 'utf8');
773
- }
774
- catch {
775
- /* missing file → the empty-spec failure below */
776
- }
777
- if (spec.trim().length === 0) {
778
- fail(`The crew run completed but produced no demo spec at ${outPath} (run ${runId}); no recording was triggered.`);
779
- return;
780
- }
781
- const violation = specSelfCheck(spec);
782
- if (violation !== null) {
783
- // The pre-install self-check (recordDemo would reject the module at import time —
784
- // interactive demo.js: `demo.spec.mjs must export an async run(...)`). Fail loud crew-side
785
- // instead of letting the service fail the recording after crew claimed success.
786
- fail(`The crew's demo spec failed its pre-install self-check and was NOT installed — ${violation} ` +
787
- `(run ${runId}). The document is unchanged; resubmit the request.`);
788
- return;
789
- }
790
- // Crew (not the worker) installs the spec: the worker's boundary cannot touch the doc
791
- // workspace (wicked-core#293/#294) — crew's own process can. The docs root resolves the
792
- // same way the chat seam and the interactive proxy resolve it (per-project setting via the
793
- // server wiring; shared default otherwise), so the copy lands where the service will look.
794
- const docsRoot = resolveDocsRoot(flight.projectId);
795
- const docDir = join(docsRoot, documentId);
796
- if (!existsSync(join(docDir, 'versions.json'))) {
797
- fail(`Crew authored the demo spec (at ${outPath}) but found no doc workspace at ${docDir} — ` +
798
- `the spec was NOT installed and no recording was triggered. Check the interactive docs root, then replay the request.`);
799
- return;
800
- }
801
- try {
802
- copyFileSync(outPath, join(docDir, DEMO_SPEC_FILE));
803
- }
804
- catch (err) {
805
- fail(`Crew authored the demo spec but could not install it into ${docDir}: ${err instanceof Error ? err.message : String(err)} (run ${runId}). No recording was triggered.`);
806
- return;
807
- }
808
- // Spec installed — NOW ask the (model-free) service to record it. The deterministic key
809
- // makes a re-announce a WB-002 no-op; distinct keys per authoring generation keep a
810
- // legitimate re-record from deduping against the first one.
811
- const idemKey = flight.leg === 'spec'
812
- ? demoIdempotencyKey(documentId)
813
- : demoReauthorIdempotencyKey(documentId, flight.version ?? 0);
814
- const emitted = emitInteractive(DEMO_REQUESTED, docScope(documentId, projectId), idemKey);
815
- if (!emitted) {
816
- // The bus refused the announce (non-WB-002): the spec IS installed but the recording was
817
- // never requested. Fail HONEST — and say exactly where things stand, because unlike the
818
- // sibling seams the doc is half-advanced (spec on disk, no video).
819
- ledger.recordFailure(key);
820
- emitStatus({
821
- ...docScope(documentId, projectId),
822
- state: 'error',
823
- message: `Crew installed the demo spec at ${join(docDir, DEMO_SPEC_FILE)} but could not request ` +
824
- `the recording on the bus (run ${runId}). Inspect the crew daemon log, then emit ` +
825
- `wicked.interactive.demo.requested for this document to record it.`,
826
- });
827
- log(`[interactive-demo] demo.requested emit FAILED for ${key} (run ${runId}) — recorded as failure`);
828
- return;
829
- }
830
- ledger.recordEmitted(key);
831
- emitStatus({
832
- ...docScope(documentId, projectId),
833
- state: 'complete',
834
- message: flight.leg === 'spec'
835
- ? 'Demo spec authored — recording now. Step-by-step progress will follow; highlight any step to refine it.'
836
- : 'Demo spec re-authored per your feedback — recording the new take now.',
837
- });
838
- log(`[interactive-demo] spec installed + demo.requested emitted for ${key} (run ${runId})`);
839
- }
840
- /** `true` when any run (either leg) is in flight for the doc — one demo doc gets one
841
- * authoring run at a time, so a re-author can never race the spec it revises. */
842
- function docBusy(documentId) {
843
- for (const f of inFlight.values())
844
- if (f.documentId === documentId)
845
- return true;
846
- return false;
847
- }
848
- async function launchFlight(input) {
849
- const { runId } = input;
850
- const placeholder = inFlight.get(runId);
851
- const snapshotDirs = placeholder?.snapshotDirs ?? [];
852
- if (closed) {
853
- // stop() ran during the caller's awaits: never launch after the subscriber detached.
854
- endFlight(runId);
855
- removeSnapshots(snapshotDirs);
856
- log(`[interactive-demo] ${input.key}: subscriber stopped before launch — abandoned (a replay retries)`);
857
- return;
858
- }
859
- // Resolved BEFORE the launch and never indexing — a refresh is `wicked-estate index` per
860
- // member at up to 600s EACH, so doing it here would turn "record a demo" into an
861
- // unannounced multi-repo job. Missing or stale degrades to no binding; the run is unaffected.
862
- //
863
- // The decision is RECORDED on both outcomes, like the API launch path (`api/routes.ts`):
864
- // "this demo sees the project" and "this demo sees nothing, because X" are equally facts about
865
- // what the run could observe. An unexpected failure degrades the same way but says so — a
866
- // silent `catch(() => null)` would make a broken binding look identical to a project that
867
- // simply has no graph yet.
868
- let projectGraphBinding = null;
869
- if (input.projectId !== undefined) {
870
- const decision = await resolveProjectGraphBinding(adapter, input.projectId, undefined).catch((err) => ({
871
- binding: null,
872
- reason: `the project graph binding could not be resolved ` +
873
- `(${err instanceof Error ? err.message : String(err)}). ` +
874
- `This repo-less run gets no code graph.`,
875
- }));
876
- projectGraphBinding = decision.binding;
877
- log(`run ${runId}: ${decision.reason}`);
878
- }
879
- return adapter
880
- .launchRun({
881
- problem: input.problem,
882
- sessionId: runId,
883
- clisJson: input.clisJson ?? opts.clisJson ?? JSON.stringify(rosterOf(adapter, opts.roster)),
884
- workflow: input.workflow,
885
- // A project-bound doc's governed run is FILED (the engine attaches the crew.run
886
- // membership atomically with the launch); an unfiled doc launches with the key OMITTED
887
- // — never a fabricated 'default' membership (CREW-UX-2).
888
- ...(input.projectId !== undefined ? { projectId: input.projectId } : {}),
889
- // A project-filed run sees the PROJECT's graph, like any other (verification
890
- // found this seam launching filed but unbound). These launches are repo-LESS,
891
- // which is exactly the case that gets a graph where it previously got none.
892
- ...(projectGraphBinding !== null ? { projectGraph: projectGraphBinding } : {}),
893
- // THE WRITE-BOUNDARY LESSON, applied (wicked-core#293/#294): deliberately NO `repoRef`
894
- // and NO doc-workspace path — the run is UNBOUND and its worker can only read/write
895
- // the per-run inbox declared here (write roots are readable, wicked-core#259). The
896
- // task names `<inbox>/demo.spec.mjs` as the deliverable; crew copies it into the doc
897
- // workspace at finalize. Per-run isolation (Copilot, crew#313): ONLY this run's own
898
- // subdirectory, never the shared demoDir.
899
- extraWriteRoots: [input.runDir],
900
- // THE DELIVERABLE FLOOR (crew#311): the spec file IS the deliverable — and this leg is
901
- // the one that PROVED prose can stand in for it (the module doc above records a run
902
- // that "completed" with no deliverable at 161-345 bytes and was caught only by
903
- // finalize, after the gate had passed it). The floor fails the RUN instead.
904
- requireDeliverables: [input.outPath],
905
- })
906
- .then(() => {
907
- // Record AFTER the launch resolved: a failed launch leaves no ledger row, so a replayed
908
- // delivery retries. The crash window between launch and this write is the reason the
909
- // demo.requested emit ALSO carries a deterministic idempotency key.
910
- ledger.recordLaunch(input.key, runId);
911
- if (closed) {
912
- // stop() ran while the engine was accepting the launch: its sweep already dropped the
913
- // placeholder and the snapshots; the ledger row keeps a post-restart redelivery from
914
- // double-launching, and the engine's workers die with the daemon.
915
- return;
916
- }
917
- if (input.projectId !== undefined)
918
- opts.onRunFiled?.(runId, input.projectId);
919
- opts.onRunLaunched?.(runId, {
920
- ...(input.channel !== undefined ? { channel: input.channel } : {}),
921
- ...(input.actor !== undefined ? { actor: input.actor } : {}),
922
- });
923
- // Upgrade the placeholder to a live flight: the heartbeat starts once the run exists.
924
- const flight = placeholder ?? {
925
- key: input.key,
926
- leg: input.leg,
927
- documentId: input.documentId,
928
- projectId: input.projectId,
929
- version: input.version,
930
- outPath: input.outPath,
931
- snapshotDirs,
932
- agentPhaseCount: input.agentPhaseCount,
933
- narration: '',
934
- };
935
- flight.narration =
936
- input.leg === 'spec' ? 'Crew run launched — authoring your demo…' : 'Crew run launched — re-authoring your demo…';
937
- flight.heartbeat = setInterval(() => {
938
- // Repeat the last real narration so the ~20s status.requested window is always
939
- // fed, even mid-phase when the engine is quiet.
940
- emitStatus({
941
- ...docScope(flight.documentId, flight.projectId),
942
- state: 'working',
943
- message: flight.narration,
944
- ...narrationStamps(flight),
945
- });
946
- }, heartbeatMs);
947
- // Do not keep the daemon alive for narration alone.
948
- flight.heartbeat.unref?.();
949
- inFlight.set(runId, flight);
950
- log(`[interactive-demo] ${input.key} → governed run ${runId} (${input.leg}, spec → ${input.outPath})`);
951
- })
952
- .catch((err) => {
953
- // A launch that never happened keeps no flight and no snapshot (a replayed frame
954
- // re-registers and re-snapshots fresh).
955
- endFlight(runId);
956
- removeSnapshots(snapshotDirs);
957
- // The 'processing' status is already on the thread — close it out honestly so the
958
- // canvas never sits in an in-between state on a launch that went nowhere.
959
- const reason = err instanceof Error ? err.message : String(err);
960
- emitStatus({
961
- ...docScope(input.documentId, input.projectId),
962
- state: 'error',
963
- message: `Crew could not start a run for this demo: ${reason}.`,
964
- });
965
- // DELIBERATELY no ledger write: only an answered trigger earns a row, so an operator
966
- // can replay the dead-lettered frame after fixing the daemon and get a real retry.
967
- // Re-throw so the bus (maxRetries 0) dead-letters the frame — visible, replayable,
968
- // and incapable of hot-looping.
969
- throw err instanceof Error ? err : new Error(reason);
970
- });
971
- }
972
- async function handleDocCreated(event) {
973
- const doc = parseDemoDocCreated(event.event_type, event.payload);
974
- if (doc === null)
975
- return;
976
- // Replay-dedup: the ledger is the durable gate (redelivery after crash/restart), the
977
- // in-flight scan the live one. Keyed by DOCUMENT — one first spec per document lifetime
978
- // (refinement is the feedback leg's business).
979
- if (ledger.has(doc.documentId)) {
980
- log(`[interactive-demo] doc ${doc.documentId} already answered (run ${ledger.get(doc.documentId)?.runId}) — replay ignored`);
981
- return;
982
- }
983
- if (docBusy(doc.documentId))
984
- return;
985
- const runDir = join(demoDir, doc.documentId);
986
- const outPath = join(runDir, DEMO_SPEC_FILE);
987
- const runId = randomUUID();
988
- // PRE-LAUNCH placeholder (Copilot on crew#506): the awaits below (registry check, grounding,
989
- // snapshots) open a window in which this doc has no `inFlight` entry — a replayed doc.created
990
- // could start a second snapshot+launch, and stop()'s sweep could not find a half-made clone.
991
- // Register the flight FIRST; every exit path below must endFlight() it.
992
- const placeholder = {
993
- key: doc.documentId,
994
- leg: 'spec',
995
- documentId: doc.documentId,
996
- projectId: doc.projectId,
997
- version: undefined,
998
- outPath,
999
- snapshotDirs: [],
1000
- agentPhaseCount: INTERACTIVE_DEMO_WORKFLOW_DEF.phases.length,
1001
- narration: 'Crew run launched — authoring your demo…',
1002
- };
1003
- inFlight.set(runId, placeholder);
1004
- try {
1005
- // NOTHING is created before the inbox is known to be clear of every registered repository.
1006
- await refuseRunDirInsideRepo(runDir, doc.documentId, doc.projectId);
1007
- }
1008
- catch (err) {
1009
- endFlight(runId);
1010
- throw err;
1011
- }
1012
- mkdirSync(runDir, { recursive: true });
1013
- emitStatus({
1014
- ...docScope(doc.documentId, doc.projectId),
1015
- state: 'processing',
1016
- message: 'A governed crew picked up your demo brief — planning the scenes and authoring the click-path…',
1017
- });
1018
- // F-046: WHICH repository is this app? The create request's `repo_ref(s)` (recorded by the
1019
- // proxy, waited for while the create is still answering), else the ones the brief names, else
1020
- // the project's sole repo, else none — never the project's first member. Each resolved repo is
1021
- // snapshotted into the run's own inbox (readable by the unbound worker, wicked-core#259) and
1022
- // named in the task; the thread hears where the demo is grounded and why.
1023
- const subjects = [];
1024
- const snapshotDirs = placeholder.snapshotDirs; // tracked on the flight so stop() sweeps them
1025
- let docClisJson;
1026
- let docChannel;
1027
- let docActor;
1028
- if (doc.projectId !== undefined) {
1029
- let binding;
1030
- let decision;
1031
- try {
1032
- binding =
1033
- groundingStore !== undefined
1034
- ? await groundingStore.waitFor(resolveDocsRoot(doc.projectId), doc.documentId, doc.projectId, GROUNDING_BINDING_WAIT_MS)
1035
- : undefined;
1036
- decision = await resolveGroundingRepos(adapter, doc.projectId, doc.brief, binding?.repo_refs, log);
1037
- docClisJson = binding?.clis_json;
1038
- docChannel = binding?.channel;
1039
- docActor = binding?.actor;
1040
- }
1041
- catch (err) {
1042
- endFlight(runId);
1043
- throw err;
1044
- }
1045
- const snapshotted = [];
1046
- const taken = new Set();
1047
- for (const repo of decision.repos) {
1048
- const dest = join(runDir, 'repos', snapshotDirName(repo, taken));
1049
- if (!groundablePath(dest)) {
1050
- log(`[interactive-demo] ${doc.documentId}: snapshot dest ${dest} cannot ride the problem — no snapshot for ${repo.repoRef}`);
1051
- subjects.push({ name: repo.name });
1052
- continue;
1053
- }
1054
- snapshotDirs.push(dest); // BEFORE the await: a stop() mid-clone must find it
1055
- const snap = await snapshotRepo(repo.rootPath, dest, { maxBytes: opts.repoSnapshotMaxBytes, log });
1056
- if (snap.ok) {
1057
- snapshotted.push(repo);
1058
- subjects.push({ name: repo.name, snapshotDir: dest });
1059
- continue;
1060
- }
1061
- snapshotDirs.splice(snapshotDirs.indexOf(dest), 1); // nothing landed — snapshotRepo cleans its partials
1062
- if (snap.reason === 'dest-overlap') {
1063
- // FAIL CLOSED, exactly like the draft seam (Copilot on #506): the configured demo dir places
1064
- // this run's write root inside the live application repository (or the repo is registered
1065
- // at the inbox). The spec itself lands in the inbox, but `extraWriteRoots: [runDir]` would
1066
- // still hand the unbound worker write access INSIDE the live repo — so the launch is
1067
- // refused outright: sibling snapshots removed, no run, no ledger row. The status names the
1068
- // CONFIG problem; the thrown error dead-letters the frame, replayable once it is fixed.
1069
- endFlight(runId);
1070
- removeSnapshots(snapshotDirs);
1071
- const message = `Crew refused to author this demo: the configured demo directory (${demoDir}) overlaps the ` +
1072
- `application's repository (${repo.rootPath}), so launching would give the worker write access ` +
1073
- `inside the live repo. Point the crew demo directory outside every registered repository, then ` +
1074
- `replay the request.`;
1075
- emitStatus({ ...docScope(doc.documentId, doc.projectId), state: 'error', message });
1076
- log(`[interactive-demo] ${doc.documentId}: REFUSING launch — demo dir ${demoDir} overlaps repo ${repo.rootPath} (dest-overlap)`);
1077
- throw new Error(message);
1078
- }
1079
- // Any other failure degrades honestly: the subject is still NAMED, only its snapshot is
1080
- // missing, and the app is still inspected live.
1081
- log(`[interactive-demo] ${doc.documentId}: repo ${repo.rootPath} could not be snapshotted (${snap.reason}) — no snapshot for ${repo.repoRef}`);
1082
- subjects.push({ name: repo.name });
1083
- }
1084
- const line = groundingNarration(decision, snapshotted, 'demo');
1085
- if (line !== null)
1086
- emitStatus({ ...docScope(doc.documentId, doc.projectId), state: 'working', message: line });
1087
- }
1088
- await launchFlight({
1089
- key: doc.documentId,
1090
- leg: 'spec',
1091
- documentId: doc.documentId,
1092
- projectId: doc.projectId,
1093
- version: undefined,
1094
- problem: demoProblem(doc, outPath, subjects.length > 0 ? { subjects } : undefined),
1095
- workflow: INTERACTIVE_DEMO_WORKFLOW,
1096
- runDir,
1097
- outPath,
1098
- agentPhaseCount: INTERACTIVE_DEMO_WORKFLOW_DEF.phases.length,
1099
- clisJson: docClisJson,
1100
- channel: docChannel,
1101
- actor: docActor,
1102
- runId,
1103
- });
1104
- }
1105
- async function handleFeedbackProcessed(event) {
1106
- const handoff = parseStructuralFeedback(event.event_type, event.payload);
1107
- if (handoff === null)
1108
- return;
1109
- // THE KIND GATE (the disk truth, shared with the edit seam): this leg answers ONLY docs
1110
- // whose manifest says `kind: "demo"` — interactive records the kind for demo docs
1111
- // (initWorkspace(dir, html, {kind:'demo'}), server.js). Everything else — including a doc
1112
- // whose manifest this daemon cannot read — is the edit seam's business, and the edit seam
1113
- // applies the same gate inverted, so exactly one seam answers any given handoff.
1114
- const docsRoot = resolveDocsRoot(handoff.projectId);
1115
- const head = readDocHead(docsRoot, handoff.documentId);
1116
- if (head === null || head.kind !== 'demo')
1117
- return;
1118
- const key = handoffKey(handoff.documentId, handoff.version);
1119
- if (ledger.has(key)) {
1120
- log(`[interactive-demo] handoff ${key} already answered (run ${ledger.get(key)?.runId}) — replay ignored`);
1121
- return;
1122
- }
1123
- for (const f of inFlight.values()) {
1124
- if (f.key === key)
1125
- return;
1126
- }
1127
- if (docBusy(handoff.documentId)) {
1128
- // Another authoring run (usually the FIRST spec) is still in flight for this doc — a
1129
- // re-author launched now would revise a spec that is still being written. Dead-letter
1130
- // the frame (throw → maxRetries 0 → DLQ) so it is visible and replayable once the
1131
- // in-flight run lands, and say so on the thread instead of eating the user's feedback.
1132
- const message = `Crew is still authoring this demo's spec — your step feedback was set aside; ` +
1133
- `replay it (or resubmit) once the current run lands.`;
1134
- emitStatus({ ...docScope(handoff.documentId, handoff.projectId), state: 'error', message });
1135
- log(`[interactive-demo] handoff ${key} arrived while doc ${handoff.documentId} is busy — dead-lettered`);
1136
- throw new Error(message);
1137
- }
1138
- // The current spec is the re-author's ground truth: copy it INTO the inbox crew-side so
1139
- // the unbound worker can read it (write roots are readable, wicked-core#259 — the doc
1140
- // workspace itself is boundary-denied, wicked-core#294). No spec on disk = nothing to
1141
- // re-author (a demo doc that never had its first spec authored): dead-letter, replayable
1142
- // after the first authoring lands.
1143
- const srcSpec = join(docsRoot, handoff.documentId, DEMO_SPEC_FILE);
1144
- if (!existsSync(srcSpec)) {
1145
- const message = `This demo has no ${DEMO_SPEC_FILE} to revise yet — the step feedback was set aside; ` +
1146
- `replay it once the first spec is authored.`;
1147
- emitStatus({ ...docScope(handoff.documentId, handoff.projectId), state: 'error', message });
1148
- log(`[interactive-demo] handoff ${key}: no spec at ${srcSpec} — dead-lettered`);
1149
- throw new Error(message);
1150
- }
1151
- const runDir = join(demoDir, key.replace(':', '-'));
1152
- const outPath = join(runDir, DEMO_SPEC_FILE);
1153
- const runId = randomUUID();
1154
- inFlight.set(runId, {
1155
- key,
1156
- leg: 'reauthor',
1157
- documentId: handoff.documentId,
1158
- projectId: handoff.projectId,
1159
- version: handoff.version,
1160
- outPath,
1161
- snapshotDirs: [],
1162
- agentPhaseCount: INTERACTIVE_DEMO_REAUTHOR_WORKFLOW_DEF.phases.length,
1163
- narration: 'Crew run launched — re-authoring your demo…',
1164
- });
1165
- try {
1166
- await refuseRunDirInsideRepo(runDir, handoff.documentId, handoff.projectId);
1167
- }
1168
- catch (err) {
1169
- endFlight(runId);
1170
- throw err;
1171
- }
1172
- mkdirSync(runDir, { recursive: true });
1173
- const currentSpecPath = join(runDir, 'current.spec.mjs');
1174
- copyFileSync(srcSpec, currentSpecPath);
1175
- // Handoff by file (the edit leg's discipline): fragments can be arbitrarily large and the
1176
- // PTY prompt is single-line, so the items ride a JSON file in the inbox, not the problem.
1177
- const feedbackPath = join(runDir, 'feedback.json');
1178
- writeFileSync(feedbackPath, JSON.stringify({ document_id: handoff.documentId, version: handoff.version, items: handoff.items }, null, 2), 'utf8');
1179
- emitStatus({
1180
- ...docScope(handoff.documentId, handoff.projectId),
1181
- state: 'processing',
1182
- message: `A governed crew picked up your demo feedback — re-authoring the click-path (${handoff.items.length} change${handoff.items.length === 1 ? '' : 's'})…`,
1183
- });
1184
- await launchFlight({
1185
- key,
1186
- leg: 'reauthor',
1187
- documentId: handoff.documentId,
1188
- projectId: handoff.projectId,
1189
- version: handoff.version,
1190
- problem: demoReauthorProblem(handoff, currentSpecPath, feedbackPath, outPath),
1191
- workflow: INTERACTIVE_DEMO_REAUTHOR_WORKFLOW,
1192
- runDir,
1193
- outPath,
1194
- agentPhaseCount: INTERACTIVE_DEMO_REAUTHOR_WORKFLOW_DEF.phases.length,
1195
- runId,
1196
- });
1197
- }
1198
- // F-RECON-013 companion: the recorder's typed failure (wicked-interactive PR #224 — one
1199
- // `RecorderError` flattened beside `state: "error"` on `status.posted`). Before it, a failed
1200
- // recording was a bus dead letter (3 identical retries in 4 s) plus Playwright's raw ASCII box on
1201
- // the thread — nothing reached the run's diagnostics and nothing named the one-line remedy. The
1202
- // thread already carries the service's own error status (the frame IS the thread line), so crew
1203
- // relays it where the thread cannot reach: `GET /diagnostics.recentErrors` (an error-level line
1204
- // naming the doc's spec run, the code, the step and the remedy) and the handle
1205
- // (`recorderFailure(doc)`). Crew emits NOTHING back for it — no `demo.requested`, no second
1206
- // status: a `retryable: false` frame is terminal by contract and must never be replayed.
1207
- const recorderFailures = new Map();
1208
- const logError = opts.logError ?? log;
1209
- function handleRecorderStatus(event) {
1210
- // Our own narration rides the same topic: never read a `wi-crew` frame as the recorder's.
1211
- if (event.producer_id === INTERACTIVE_PRODUCER)
1212
- return;
1213
- const failure = parseRecorderError(event.event_type, event.payload);
1214
- if (failure === null)
1215
- return;
1216
- // The first spec authoring is ledgered under the bare doc id (a re-author under `<doc>:v<N>`).
1217
- const specRun = ledger.get(failure.documentId);
1218
- const record = {
1219
- ...failure,
1220
- at: new Date().toISOString(),
1221
- ...(specRun !== undefined ? { runId: specRun.runId } : {}),
1222
- };
1223
- recorderFailures.set(failure.documentId, record);
1224
- logError(recorderFailureLine(failure, record.runId));
1225
- }
1226
- const subRecorder = bus.subscribe({
1227
- db,
1228
- plugin: INTERACTIVE_DEMO_RECORDER_BUS_PLUGIN,
1229
- filter: INTERACTIVE_DEMO_RECORDER_BUS_FILTER,
1230
- cursor_init: 'latest',
1231
- pollIntervalMs: opts.pollIntervalMs ?? 2000,
1232
- maxRetries: 0,
1233
- handler: (event) => handleRecorderStatus(event),
1234
- onError: busSubscriberErrorReporter({
1235
- describe: (err, event) => `[interactive-demo] recorder status handler error on event ${String(event?.event_id ?? '?')}: ${err.message}`,
1236
- log,
1237
- logError: opts.logError,
1238
- pollIntervalMs: opts.pollIntervalMs ?? 2000,
1239
- }),
1240
- });
1241
- const subCreated = bus.subscribe({
1242
- db,
1243
- plugin: INTERACTIVE_DEMO_BUS_PLUGIN,
1244
- filter: INTERACTIVE_DEMO_BUS_FILTER,
1245
- // Live triggers only: replaying a bus backlog would answer docs whose demos the assist
1246
- // loop long since produced. History reconciliation belongs to the state plane, not here.
1247
- cursor_init: 'latest',
1248
- pollIntervalMs: opts.pollIntervalMs ?? 2000,
1249
- // Our own ledger + idempotency key are the dedupe; a bus-level retry of a failed launch
1250
- // would double-launch precisely because the ledger row is only written on success.
1251
- maxRetries: 0,
1252
- handler: (event) => handleDocCreated(event),
1253
- onError: busSubscriberErrorReporter({
1254
- describe: (err, event) => `[interactive-demo] handler error on event ${String(event?.event_id ?? '?')}: ${err.message}`,
1255
- log,
1256
- logError: opts.logError,
1257
- pollIntervalMs: opts.pollIntervalMs ?? 2000,
1258
- }),
1259
- });
1260
- const subFeedback = bus.subscribe({
1261
- db,
1262
- plugin: INTERACTIVE_DEMO_FEEDBACK_BUS_PLUGIN,
1263
- filter: INTERACTIVE_DEMO_FEEDBACK_BUS_FILTER,
1264
- cursor_init: 'latest',
1265
- pollIntervalMs: opts.pollIntervalMs ?? 2000,
1266
- maxRetries: 0,
1267
- handler: (event) => handleFeedbackProcessed(event),
1268
- onError: busSubscriberErrorReporter({
1269
- describe: (err, event) => `[interactive-demo] feedback handler error on event ${String(event?.event_id ?? '?')}: ${err.message}`,
1270
- log,
1271
- logError: opts.logError,
1272
- pollIntervalMs: opts.pollIntervalMs ?? 2000,
1273
- }),
1274
- });
1275
- return {
1276
- ledger,
1277
- inFlightDocs: () => [...new Set([...inFlight.values()].map((f) => f.documentId))],
1278
- recorderFailure: (documentId) => recorderFailures.get(documentId),
1279
- stop: async () => {
1280
- closed = true; // a handler mid-snapshot sees this and never launches
1281
- offCoreEvents();
1282
- for (const runId of [...inFlight.keys()]) {
1283
- const flight = endFlight(runId);
1284
- if (flight !== undefined)
1285
- removeSnapshots(flight.snapshotDirs);
1286
- }
1287
- await subCreated.stop();
1288
- await subFeedback.stop();
1289
- await subRecorder.stop();
1290
- },
1291
- };
1292
- }
1293
- //# sourceMappingURL=demo-events.js.map