wicked-crew 0.7.46 → 0.7.48

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 (263) hide show
  1. package/dist/api/capture.d.ts +2 -2
  2. package/dist/api/chat-citations.d.ts +10 -1
  3. package/dist/api/chat-citations.d.ts.map +1 -1
  4. package/dist/api/chat-citations.js +28 -0
  5. package/dist/api/chat-citations.js.map +1 -1
  6. package/dist/api/chat-scope.d.ts +4 -2
  7. package/dist/api/chat-scope.d.ts.map +1 -1
  8. package/dist/api/chat-scope.js +35 -3
  9. package/dist/api/chat-scope.js.map +1 -1
  10. package/dist/api/chat-transcripts.d.ts +23 -1
  11. package/dist/api/chat-transcripts.d.ts.map +1 -1
  12. package/dist/api/chat-transcripts.js +61 -6
  13. package/dist/api/chat-transcripts.js.map +1 -1
  14. package/dist/api/delivery-index.d.ts +21 -0
  15. package/dist/api/delivery-index.d.ts.map +1 -1
  16. package/dist/api/delivery-index.js +36 -0
  17. package/dist/api/delivery-index.js.map +1 -1
  18. package/dist/api/elicitation-cache.d.ts +12 -0
  19. package/dist/api/elicitation-cache.d.ts.map +1 -1
  20. package/dist/api/elicitation-cache.js +17 -1
  21. package/dist/api/elicitation-cache.js.map +1 -1
  22. package/dist/api/eval-sample.d.ts +12 -12
  23. package/dist/api/governance-steering.d.ts.map +1 -1
  24. package/dist/api/governance-steering.js +1 -0
  25. package/dist/api/governance-steering.js.map +1 -1
  26. package/dist/api/mcp.d.ts +4 -4
  27. package/dist/api/policy-landing.d.ts +55 -0
  28. package/dist/api/policy-landing.d.ts.map +1 -0
  29. package/dist/api/policy-landing.js +174 -0
  30. package/dist/api/policy-landing.js.map +1 -0
  31. package/dist/api/recording.d.ts +228 -0
  32. package/dist/api/recording.d.ts.map +1 -0
  33. package/dist/api/recording.js +1140 -0
  34. package/dist/api/recording.js.map +1 -0
  35. package/dist/api/routes.d.ts +42 -39
  36. package/dist/api/routes.d.ts.map +1 -1
  37. package/dist/api/routes.js +362 -107
  38. package/dist/api/routes.js.map +1 -1
  39. package/dist/api/run-timing-index.d.ts +17 -0
  40. package/dist/api/run-timing-index.d.ts.map +1 -1
  41. package/dist/api/run-timing-index.js +43 -1
  42. package/dist/api/run-timing-index.js.map +1 -1
  43. package/dist/api/server.d.ts +27 -0
  44. package/dist/api/server.d.ts.map +1 -1
  45. package/dist/api/server.js +168 -4
  46. package/dist/api/server.js.map +1 -1
  47. package/dist/api/testing.d.ts +4 -4
  48. package/dist/api/testing.d.ts.map +1 -1
  49. package/dist/api/testing.js +3 -0
  50. package/dist/api/testing.js.map +1 -1
  51. package/dist/api/watch-routes.d.ts +30 -0
  52. package/dist/api/watch-routes.d.ts.map +1 -0
  53. package/dist/api/watch-routes.js +104 -0
  54. package/dist/api/watch-routes.js.map +1 -0
  55. package/dist/campaigns/routes.d.ts +2 -2
  56. package/dist/cli/index.d.ts.map +1 -1
  57. package/dist/cli/index.js +20 -2
  58. package/dist/cli/index.js.map +1 -1
  59. package/dist/core/adapter.d.ts +52 -1
  60. package/dist/core/adapter.d.ts.map +1 -1
  61. package/dist/core/adapter.js +193 -5
  62. package/dist/core/adapter.js.map +1 -1
  63. package/dist/core/run-identity.js +1 -1
  64. package/dist/core/run-identity.js.map +1 -1
  65. package/dist/core/walkthrough-root.d.ts +21 -0
  66. package/dist/core/walkthrough-root.d.ts.map +1 -0
  67. package/dist/core/walkthrough-root.js +66 -0
  68. package/dist/core/walkthrough-root.js.map +1 -0
  69. package/dist/decisions/chat-recorder.d.ts +129 -0
  70. package/dist/decisions/chat-recorder.d.ts.map +1 -0
  71. package/dist/decisions/chat-recorder.js +367 -0
  72. package/dist/decisions/chat-recorder.js.map +1 -0
  73. package/dist/decisions/consider.d.ts +102 -0
  74. package/dist/decisions/consider.d.ts.map +1 -0
  75. package/dist/decisions/consider.js +207 -0
  76. package/dist/decisions/consider.js.map +1 -0
  77. package/dist/decisions/derive.d.ts +79 -0
  78. package/dist/decisions/derive.d.ts.map +1 -0
  79. package/dist/decisions/derive.js +282 -0
  80. package/dist/decisions/derive.js.map +1 -0
  81. package/dist/decisions/events.d.ts +25 -0
  82. package/dist/decisions/events.d.ts.map +1 -0
  83. package/dist/decisions/events.js +24 -0
  84. package/dist/decisions/events.js.map +1 -0
  85. package/dist/decisions/ingest.d.ts +51 -0
  86. package/dist/decisions/ingest.d.ts.map +1 -0
  87. package/dist/decisions/ingest.js +100 -0
  88. package/dist/decisions/ingest.js.map +1 -0
  89. package/dist/decisions/land.d.ts +100 -0
  90. package/dist/decisions/land.d.ts.map +1 -0
  91. package/dist/decisions/land.js +466 -0
  92. package/dist/decisions/land.js.map +1 -0
  93. package/dist/decisions/ledger.d.ts +60 -0
  94. package/dist/decisions/ledger.d.ts.map +1 -0
  95. package/dist/decisions/ledger.js +246 -0
  96. package/dist/decisions/ledger.js.map +1 -0
  97. package/dist/decisions/routes.d.ts +10 -0
  98. package/dist/decisions/routes.d.ts.map +1 -0
  99. package/dist/decisions/routes.js +141 -0
  100. package/dist/decisions/routes.js.map +1 -0
  101. package/dist/decisions/rules-text.d.ts +21 -0
  102. package/dist/decisions/rules-text.d.ts.map +1 -0
  103. package/dist/decisions/rules-text.js +50 -0
  104. package/dist/decisions/rules-text.js.map +1 -0
  105. package/dist/decisions/types.d.ts +120 -0
  106. package/dist/decisions/types.d.ts.map +1 -0
  107. package/dist/decisions/types.js +19 -0
  108. package/dist/decisions/types.js.map +1 -0
  109. package/dist/editors/csp.d.ts +31 -0
  110. package/dist/editors/csp.d.ts.map +1 -0
  111. package/dist/editors/csp.js +46 -0
  112. package/dist/editors/csp.js.map +1 -0
  113. package/dist/editors/pack-install.d.ts +28 -0
  114. package/dist/editors/pack-install.d.ts.map +1 -0
  115. package/dist/editors/pack-install.js +153 -0
  116. package/dist/editors/pack-install.js.map +1 -0
  117. package/dist/editors/registry.d.ts +142 -0
  118. package/dist/editors/registry.d.ts.map +1 -0
  119. package/dist/editors/registry.js +255 -0
  120. package/dist/editors/registry.js.map +1 -0
  121. package/dist/editors/routes.d.ts +46 -0
  122. package/dist/editors/routes.d.ts.map +1 -0
  123. package/dist/editors/routes.js +166 -0
  124. package/dist/editors/routes.js.map +1 -0
  125. package/dist/interactive/bridge-pool.d.ts +7 -2
  126. package/dist/interactive/bridge-pool.d.ts.map +1 -1
  127. package/dist/interactive/bridge-pool.js +6 -1
  128. package/dist/interactive/bridge-pool.js.map +1 -1
  129. package/dist/interactive/draft-events.d.ts.map +1 -1
  130. package/dist/interactive/draft-events.js +1 -0
  131. package/dist/interactive/draft-events.js.map +1 -1
  132. package/dist/interactive/theme-events.d.ts +119 -0
  133. package/dist/interactive/theme-events.d.ts.map +1 -0
  134. package/dist/interactive/theme-events.js +606 -0
  135. package/dist/interactive/theme-events.js.map +1 -0
  136. package/dist/projects/events.d.ts.map +1 -1
  137. package/dist/projects/events.js +2 -1
  138. package/dist/projects/events.js.map +1 -1
  139. package/dist/projects/routes.d.ts +2 -2
  140. package/dist/projects/state-home-preflight.d.ts.map +1 -1
  141. package/dist/projects/state-home-preflight.js +3 -0
  142. package/dist/projects/state-home-preflight.js.map +1 -1
  143. package/dist/projects/state-home-registry.d.ts.map +1 -1
  144. package/dist/projects/state-home-registry.js +2 -0
  145. package/dist/projects/state-home-registry.js.map +1 -1
  146. package/dist/projects/state-home.d.ts +6 -0
  147. package/dist/projects/state-home.d.ts.map +1 -1
  148. package/dist/projects/state-home.js +28 -1
  149. package/dist/projects/state-home.js.map +1 -1
  150. package/dist/qe/acceptance.d.ts +45 -4
  151. package/dist/qe/acceptance.d.ts.map +1 -1
  152. package/dist/qe/acceptance.js +93 -4
  153. package/dist/qe/acceptance.js.map +1 -1
  154. package/dist/qe/ledger.d.ts +20 -0
  155. package/dist/qe/ledger.d.ts.map +1 -1
  156. package/dist/qe/ledger.js +34 -0
  157. package/dist/qe/ledger.js.map +1 -1
  158. package/dist/qe/walkthrough-acceptance.d.ts +82 -0
  159. package/dist/qe/walkthrough-acceptance.d.ts.map +1 -0
  160. package/dist/qe/walkthrough-acceptance.js +248 -0
  161. package/dist/qe/walkthrough-acceptance.js.map +1 -0
  162. package/dist/skills/base-skill.d.ts.map +1 -1
  163. package/dist/skills/base-skill.js +3 -2
  164. package/dist/skills/base-skill.js.map +1 -1
  165. package/dist/skills/plugin-source.d.ts +19 -1
  166. package/dist/skills/plugin-source.d.ts.map +1 -1
  167. package/dist/skills/plugin-source.js +31 -2
  168. package/dist/skills/plugin-source.js.map +1 -1
  169. package/dist/skills/runtime.d.ts +11 -2
  170. package/dist/skills/runtime.d.ts.map +1 -1
  171. package/dist/skills/runtime.js +29 -1
  172. package/dist/skills/runtime.js.map +1 -1
  173. package/dist/skills/store.d.ts +14 -1
  174. package/dist/skills/store.d.ts.map +1 -1
  175. package/dist/skills/store.js +19 -5
  176. package/dist/skills/store.js.map +1 -1
  177. package/dist/standing-orders/routes.d.ts +22 -22
  178. package/dist/standing-orders/store.d.ts +10 -10
  179. package/dist/watch/checks/check-failed.d.ts +24 -0
  180. package/dist/watch/checks/check-failed.d.ts.map +1 -0
  181. package/dist/watch/checks/check-failed.js +56 -0
  182. package/dist/watch/checks/check-failed.js.map +1 -0
  183. package/dist/watch/checks/claim-vs-evidence.d.ts +30 -0
  184. package/dist/watch/checks/claim-vs-evidence.d.ts.map +1 -0
  185. package/dist/watch/checks/claim-vs-evidence.js +152 -0
  186. package/dist/watch/checks/claim-vs-evidence.js.map +1 -0
  187. package/dist/watch/checks/deliver-audit.d.ts +19 -0
  188. package/dist/watch/checks/deliver-audit.d.ts.map +1 -0
  189. package/dist/watch/checks/deliver-audit.js +129 -0
  190. package/dist/watch/checks/deliver-audit.js.map +1 -0
  191. package/dist/watch/checks/index.d.ts +8 -0
  192. package/dist/watch/checks/index.d.ts.map +1 -0
  193. package/dist/watch/checks/index.js +24 -0
  194. package/dist/watch/checks/index.js.map +1 -0
  195. package/dist/watch/checks/lag.d.ts +24 -0
  196. package/dist/watch/checks/lag.d.ts.map +1 -0
  197. package/dist/watch/checks/lag.js +55 -0
  198. package/dist/watch/checks/lag.js.map +1 -0
  199. package/dist/watch/checks/quiet-after-claim.d.ts +17 -0
  200. package/dist/watch/checks/quiet-after-claim.d.ts.map +1 -0
  201. package/dist/watch/checks/quiet-after-claim.js +89 -0
  202. package/dist/watch/checks/quiet-after-claim.js.map +1 -0
  203. package/dist/watch/checks/ungated.d.ts +12 -0
  204. package/dist/watch/checks/ungated.d.ts.map +1 -0
  205. package/dist/watch/checks/ungated.js +64 -0
  206. package/dist/watch/checks/ungated.js.map +1 -0
  207. package/dist/watch/checks/warned-rule.d.ts +26 -0
  208. package/dist/watch/checks/warned-rule.d.ts.map +1 -0
  209. package/dist/watch/checks/warned-rule.js +126 -0
  210. package/dist/watch/checks/warned-rule.js.map +1 -0
  211. package/dist/watch/emit.d.ts +122 -0
  212. package/dist/watch/emit.d.ts.map +1 -0
  213. package/dist/watch/emit.js +443 -0
  214. package/dist/watch/emit.js.map +1 -0
  215. package/dist/watch/events.d.ts +21 -0
  216. package/dist/watch/events.d.ts.map +1 -0
  217. package/dist/watch/events.js +21 -0
  218. package/dist/watch/events.js.map +1 -0
  219. package/dist/watch/keys.d.ts +15 -0
  220. package/dist/watch/keys.d.ts.map +1 -0
  221. package/dist/watch/keys.js +23 -0
  222. package/dist/watch/keys.js.map +1 -0
  223. package/dist/watch/lanes.d.ts +41 -0
  224. package/dist/watch/lanes.d.ts.map +1 -0
  225. package/dist/watch/lanes.js +73 -0
  226. package/dist/watch/lanes.js.map +1 -0
  227. package/dist/watch/loader.d.ts +47 -0
  228. package/dist/watch/loader.d.ts.map +1 -0
  229. package/dist/watch/loader.js +178 -0
  230. package/dist/watch/loader.js.map +1 -0
  231. package/dist/watch/registry.d.ts +166 -0
  232. package/dist/watch/registry.d.ts.map +1 -0
  233. package/dist/watch/registry.js +589 -0
  234. package/dist/watch/registry.js.map +1 -0
  235. package/dist/watch/ring.d.ts +32 -0
  236. package/dist/watch/ring.d.ts.map +1 -0
  237. package/dist/watch/ring.js +90 -0
  238. package/dist/watch/ring.js.map +1 -0
  239. package/dist/watch/router.d.ts +25 -0
  240. package/dist/watch/router.d.ts.map +1 -0
  241. package/dist/watch/router.js +87 -0
  242. package/dist/watch/router.js.map +1 -0
  243. package/dist/watch/sources.d.ts +42 -0
  244. package/dist/watch/sources.d.ts.map +1 -0
  245. package/dist/watch/sources.js +104 -0
  246. package/dist/watch/sources.js.map +1 -0
  247. package/dist/watch/types.d.ts +129 -0
  248. package/dist/watch/types.d.ts.map +1 -0
  249. package/dist/watch/types.js +6 -0
  250. package/dist/watch/types.js.map +1 -0
  251. package/endpoint-manifest.json +272 -1
  252. package/package.json +3 -2
  253. package/watch/entries/claim-vs-evidence.json +13 -0
  254. package/watch/entries/deliver-audit.json +17 -0
  255. package/watch/entries/quiet-after-claim.json +16 -0
  256. package/watch/entries/registry-check-failed.json +12 -0
  257. package/watch/entries/registry-lagging.json +12 -0
  258. package/watch/entries/risky-call.json +12 -0
  259. package/watch/entries/ungated.json +13 -0
  260. package/dist/api/demo.d.ts +0 -90
  261. package/dist/api/demo.d.ts.map +0 -1
  262. package/dist/api/demo.js +0 -492
  263. package/dist/api/demo.js.map +0 -1
@@ -0,0 +1,606 @@
1
+ /**
2
+ * The interactive-theme seam (DES-artifact-editor-plugins §7.6 "Theme learning", EP-C4) — the
3
+ * sibling of `edit-events.ts`, same subscriber shape, ledger and filter pattern.
4
+ *
5
+ * 1. interactive grabs a page (or the operator points at a local PDF/image) and emits
6
+ * `wicked.interactive.theme.learned {document_id, render_path, format, url?}` (handlers.js).
7
+ * 2. Crew answers with ONE governed `interactive-theme` run: one creator phase that reads the
8
+ * render in place (nothing uploads) and writes a token JSON in interactive's `DEFAULT_THEME`
9
+ * shape to a handoff output path (the handoff-by-file pattern).
10
+ * 3. Crew validates the SHAPE, EVERY value's grammar (interactive's `theme-grammar.js`, ported
11
+ * field for field) and the CONTRAST (text on background ≥ 4.5:1), and refuses honestly with
12
+ * `status.posted {state:'error'}` when any check fails. Why the grammar (review S8): the
13
+ * tokens are interpolated raw into CSS custom properties and applied at every later version,
14
+ * and the creator phase read a grabbed third-party page — prompt-injection material. A value
15
+ * like `red;}body{background:url(https://x/?…)` must never ride into a version.
16
+ * 4. Valid tokens are written THROUGH interactive — `PUT /d/:doc/api/theme/learned {tokens,
17
+ * apply: true}` (EP-I2) — which lands one re-themed version (`version.created {kind:'theme'}`).
18
+ * Interactive stays the only writer of its files (§9.1); it re-checks the grammar at the reader.
19
+ *
20
+ * Replay dedup and the handoff directory ride the edit seam's ledger and `interactive-edits` root
21
+ * (keys `<doc>:theme:<ts>` beside the edit leg's `<doc>:v<n>`), so no new state-home entry is
22
+ * introduced — the worker fence is unchanged.
23
+ */
24
+ import { randomUUID } from 'node:crypto';
25
+ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
26
+ import { request as httpRequest } from 'node:http';
27
+ import { isAbsolute, join } from 'node:path';
28
+ import { emitOnBus, requireEngineBus, tapBus } from '../core/bus.js';
29
+ import { resolveProjectGraphBinding } from '../projects/graph.js';
30
+ import { crewStateHome } from '../projects/state-home.js';
31
+ import { busSubscriberErrorReporter } from './bus-subscriber-errors.js';
32
+ import { unitDistributedLine } from './council-outcome.js';
33
+ import { DOC_NAME, INTERACTIVE_DOMAIN, INTERACTIVE_PRODUCER, STATUS_POSTED, docScope, narrationStamps, oneLine, recallClause, } from './draft-events.js';
34
+ import { InteractiveHandoffLedger } from './ledger.js';
35
+ // ── Vocabulary (interactive's, verbatim — src/service/events.js is the truth) ───────────────────
36
+ export const THEME_LEARNED = 'wicked.interactive.theme.learned';
37
+ export const INTERACTIVE_THEME_BUS_FILTER = `${THEME_LEARNED}@${INTERACTIVE_DOMAIN}`;
38
+ export const INTERACTIVE_THEME_WORKFLOW = 'interactive-theme';
39
+ /** The token groups and keys of interactive's `DEFAULT_THEME` (theme.js) — the shape a run must write. */
40
+ export const THEME_SHAPE = {
41
+ colors: ['background', 'surface', 'primary', 'secondary', 'accent', 'text_primary', 'text_secondary', 'text_muted', 'border', 'success', 'warning', 'error'],
42
+ fonts: ['heading', 'body', 'mono'],
43
+ sizes: ['title', 'subtitle', 'heading', 'subheading', 'body', 'caption', 'small'],
44
+ spacing: ['margin', 'gap_large', 'gap_medium', 'gap_small', 'gap_xs'],
45
+ layout: ['viewport_width', 'viewport_height', 'content_width', 'content_start_x', 'content_start_y'],
46
+ card: ['background', 'border_radius', 'padding', 'shadow'],
47
+ };
48
+ export const INTERACTIVE_THEME_WORKFLOW_DEF = {
49
+ id: INTERACTIVE_THEME_WORKFLOW,
50
+ phases: [
51
+ {
52
+ id: 'learn',
53
+ kind: 'build',
54
+ instructions: 'Read the handoff JSON file named in the task. Open the render at its "render_path" (a PDF or an image already on this machine — read it in place, upload nothing) and learn the design: palette, type, spacing, card treatment. Then SAVE one JSON object of design tokens to the exact absolute "output_path" (create parent directories if needed, overwrite if present) in EXACTLY this shape — {"name":"<theme name>","colors":{"background","surface","primary","secondary","accent","text_primary","text_secondary","text_muted","border","success","warning","error"},"fonts":{"heading","body","mono"},"sizes":{"title","subtitle","heading","subheading","body","caption","small"},"spacing":{"margin","gap_large","gap_medium","gap_small","gap_xs"},"card":{"background","border_radius","padding","shadow"}} — with every key present. Grammar, NON-NEGOTIABLE: a colour is #rgb/#rrggbb/#rrggbbaa or rgb()/rgba()/hsl()/hsla() with numbers; a size or spacing is <number>px|rem|em|pt; a font is a comma-separated list of family names (letters, digits, spaces, hyphens; quotes allowed); the card shadow is at most two comma-separated "<x> <y> [blur] <colour>" entries. No value may contain a semicolon, braces, angle brackets, a backslash, url( or a newline. Text (text_primary) on background must read at 4.5:1 contrast or better. Never fabricate a brand you did not see. Write the file before you finish and end your reply with its absolute path.',
55
+ gate_type: 'execution',
56
+ gate: 'auto',
57
+ executes_code: false,
58
+ verified_evidence: false,
59
+ required_deliverables: [],
60
+ depends_on: [],
61
+ role: 'creator',
62
+ skill_ref: null,
63
+ allowed_skills: [],
64
+ validator_pin: null,
65
+ },
66
+ ],
67
+ };
68
+ export function parseThemeLearned(eventType, payload) {
69
+ if (eventType !== THEME_LEARNED)
70
+ return null;
71
+ if (typeof payload !== 'object' || payload === null)
72
+ return null;
73
+ const p = payload;
74
+ const documentId = typeof p['document_id'] === 'string' ? p['document_id'] : '';
75
+ if (!DOC_NAME.test(documentId))
76
+ return null;
77
+ const renderPath = typeof p['render_path'] === 'string' ? p['render_path'] : '';
78
+ if (renderPath === '' || !isAbsolute(renderPath))
79
+ return null;
80
+ const format = p['format'] === 'image' ? 'image' : p['format'] === 'pdf' ? 'pdf' : null;
81
+ if (format === null)
82
+ return null;
83
+ const ts = typeof p['ts'] === 'string' && p['ts'] !== '' ? p['ts'] : '';
84
+ if (ts === '')
85
+ return null;
86
+ const url = typeof p['url'] === 'string' && p['url'] !== '' ? p['url'] : undefined;
87
+ const projectId = typeof p['project_id'] === 'string' && p['project_id'] !== '' ? p['project_id'] : undefined;
88
+ return { documentId, renderPath, format, ts, ...(url !== undefined ? { url } : {}), ...(projectId !== undefined ? { projectId } : {}) };
89
+ }
90
+ export function themeHandoffKey(documentId, ts) {
91
+ return `${documentId}:theme:${ts}`;
92
+ }
93
+ export function themeProblem(learned, handoffPath) {
94
+ const from = learned.url !== undefined ? `grabbed from ${oneLine(learned.url, 200)}` : `a local ${learned.format} the operator pointed at`;
95
+ return (`Learn the design tokens of the wicked-interactive document "${learned.documentId}" from its render (${from}). ` +
96
+ `Read the handoff file at ${handoffPath} — a JSON file naming render_path (the render to read, in place) and ` +
97
+ `output_path (the exact absolute file the token JSON must be saved to). ` +
98
+ `${recallClause(learned.projectId !== undefined ? { project: learned.projectId } : undefined)}` +
99
+ `Write the tokens in the shape the handoff describes, obey its grammar to the letter, and end with the path you wrote.`);
100
+ }
101
+ // ── The grammar (interactive's src/core/theme-grammar.js, field for field) ─────────────────────
102
+ const BANNED = /[;{}<\\\n\r]|url\(/iu;
103
+ const NUM = String.raw `(?:\d{1,3}(?:\.\d+)?%?|\.\d+%?)`;
104
+ const HEX = /^#(?:[0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/iu;
105
+ const COLOR_FN = new RegExp(String.raw `^(?:rgba?|hsla?)\(\s*${NUM}(?:\s*,\s*${NUM}){2,3}\s*\)$`, 'iu');
106
+ const WI_VAR = /^var\(--wi-[a-z0-9-]{1,40}\)$/u;
107
+ const LENGTH = /^\d{1,3}(?:\.\d{1,2})?(?:px|rem|em|pt)$/u;
108
+ const SHADOW_OFFSET = /^(?:0|-?\d{1,3}(?:\.\d{1,2})?(?:px|rem|em|pt))$/u;
109
+ const SHADOW_BLUR = /^(?:0|\d{1,3}(?:\.\d{1,2})?(?:px|rem|em|pt))$/u;
110
+ const FAMILY = /^(?:[A-Za-z0-9 -]{1,64}|'[A-Za-z0-9 -]{1,64}'|"[A-Za-z0-9 -]{1,64}")$/u;
111
+ const NAME = /^[A-Za-z0-9 _-]{1,64}$/u;
112
+ const PROSE = /^[^<>{};\\\n\r]{0,200}$/u;
113
+ export function isColor(v) {
114
+ return typeof v === 'string' && (HEX.test(v) || COLOR_FN.test(v) || WI_VAR.test(v));
115
+ }
116
+ export function isLength(v) {
117
+ return typeof v === 'string' && LENGTH.test(v);
118
+ }
119
+ export function isFontList(v) {
120
+ if (typeof v !== 'string' || v.length > 200)
121
+ return false;
122
+ const parts = v.split(',').map((p) => p.trim());
123
+ return parts.length > 0 && parts.every((p) => p !== '' && FAMILY.test(p));
124
+ }
125
+ export function isShadow(v) {
126
+ if (typeof v !== 'string' || BANNED.test(v))
127
+ return false;
128
+ if (v.trim() === 'none')
129
+ return true;
130
+ const shadows = v.split(/,(?![^(]*\))/u).map((s) => s.trim());
131
+ if (shadows.length === 0 || shadows.length > 2)
132
+ return false;
133
+ return shadows.every((s) => {
134
+ const parts = s.match(/(?:[a-z]+\([^)]*\)|\S+)/giu) ?? [];
135
+ if (parts.length < 3 || parts.length > 4)
136
+ return false;
137
+ const colour = parts[parts.length - 1];
138
+ const lengths = parts.slice(0, -1);
139
+ if (!isColor(colour))
140
+ return false;
141
+ if (!SHADOW_OFFSET.test(lengths[0]) || !SHADOW_OFFSET.test(lengths[1]))
142
+ return false;
143
+ return lengths.length === 2 || SHADOW_BLUR.test(lengths[2]);
144
+ });
145
+ }
146
+ const isNumber = (v) => typeof v === 'number' && Number.isFinite(v);
147
+ const GROUPS = {
148
+ colors: Object.fromEntries(THEME_SHAPE.colors.map((k) => [k, isColor])),
149
+ fonts: Object.fromEntries(THEME_SHAPE.fonts.map((k) => [k, isFontList])),
150
+ sizes: Object.fromEntries(THEME_SHAPE.sizes.map((k) => [k, isLength])),
151
+ spacing: Object.fromEntries(THEME_SHAPE.spacing.map((k) => [k, isLength])),
152
+ layout: Object.fromEntries(THEME_SHAPE.layout.map((k) => [k, isNumber])),
153
+ card: { background: isColor, border_radius: isLength, padding: isLength, shadow: isShadow },
154
+ };
155
+ const isPlainObject = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
156
+ const reject = (detail) => ({ ok: false, reason: `theme-rejected:${detail}` });
157
+ /** Shape + per-field grammar, the first failing field decides (interactive's `checkThemeTokens`). */
158
+ export function checkThemeTokens(tokens) {
159
+ if (!isPlainObject(tokens))
160
+ return reject('not-an-object');
161
+ for (const [key, val] of Object.entries(tokens)) {
162
+ if (key === 'name') {
163
+ if (typeof val !== 'string' || !NAME.test(val))
164
+ return reject('value-outside-grammar:name');
165
+ continue;
166
+ }
167
+ if (key === 'display_name' || key === 'description') {
168
+ if (typeof val !== 'string' || !PROSE.test(val) || BANNED.test(val))
169
+ return reject(`value-outside-grammar:${key}`);
170
+ continue;
171
+ }
172
+ const group = Object.hasOwn(GROUPS, key) ? GROUPS[key] : null;
173
+ if (group === null)
174
+ return reject(`unknown-key:${String(key).slice(0, 40)}`);
175
+ if (!isPlainObject(val))
176
+ return reject(`not-an-object:${key}`);
177
+ for (const [sub, v] of Object.entries(val)) {
178
+ const field = `${key}.${String(sub).slice(0, 40)}`;
179
+ const check = Object.hasOwn(group, sub) ? group[sub] : null;
180
+ if (check === null)
181
+ return reject(`unknown-key:${field}`);
182
+ if (typeof v === 'string' && BANNED.test(v))
183
+ return reject(`value-outside-grammar:${field}`);
184
+ if (!check(v))
185
+ return reject(`value-outside-grammar:${field}`);
186
+ }
187
+ }
188
+ // The groups a theme needs to render at all (DEFAULT_THEME): a learned theme that omits them
189
+ // would wear the bundled fallback for those tokens — surprising, so it is refused here.
190
+ for (const group of ['colors', 'fonts', 'sizes', 'spacing', 'card']) {
191
+ const got = tokens[group];
192
+ if (!isPlainObject(got))
193
+ return reject(`missing-group:${group}`);
194
+ for (const k of THEME_SHAPE[group])
195
+ if (!(k in got))
196
+ return reject(`missing-key:${group}.${k}`);
197
+ }
198
+ return { ok: true };
199
+ }
200
+ // ── Contrast (WCAG 2.x relative luminance) ────────────────────────────────────────────────────
201
+ /**
202
+ * `[r, g, b]` in 0..255 for an OPAQUE hex, rgb()/rgba() or hsl()/hsla() colour; `null` when not
203
+ * computable — `var()`, or any alpha below 1 (codex r1: a translucent text colour composites over
204
+ * whatever is behind it, so its contrast cannot be known here and must not pass).
205
+ */
206
+ export function parseColor(v) {
207
+ const s = v.trim();
208
+ const hex = /^#([0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/iu.exec(s);
209
+ if (hex !== null) {
210
+ const h = hex[1];
211
+ if (h.length === 3)
212
+ return [parseInt(h[0] + h[0], 16), parseInt(h[1] + h[1], 16), parseInt(h[2] + h[2], 16)];
213
+ if (h.length === 8 && parseInt(h.slice(6, 8), 16) < 255)
214
+ return null;
215
+ return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)];
216
+ }
217
+ const fn = /^(rgba?|hsla?)\(\s*([^)]*)\)$/iu.exec(s);
218
+ if (fn === null)
219
+ return null;
220
+ const nums = fn[2].split(',').map((x) => x.trim());
221
+ if (nums.length < 3)
222
+ return null;
223
+ if (nums.length >= 4) {
224
+ const a = nums[3];
225
+ const alpha = a.endsWith('%') ? Number(a.slice(0, -1)) / 100 : Number(a);
226
+ if (!Number.isFinite(alpha) || alpha < 1)
227
+ return null;
228
+ }
229
+ const n = (x, max) => {
230
+ const pct = x.endsWith('%');
231
+ const val = Number(pct ? x.slice(0, -1) : x);
232
+ if (!Number.isFinite(val))
233
+ return null;
234
+ return pct ? (val / 100) * max : val;
235
+ };
236
+ if (fn[1].toLowerCase().startsWith('rgb')) {
237
+ const rgb = [n(nums[0], 255), n(nums[1], 255), n(nums[2], 255)];
238
+ if (rgb.some((c) => c === null))
239
+ return null;
240
+ return rgb.map((c) => Math.max(0, Math.min(255, c)));
241
+ }
242
+ const h = n(nums[0], 360);
243
+ const sat = n(nums[1], 1);
244
+ const light = n(nums[2], 1);
245
+ if (h === null || sat === null || light === null)
246
+ return null;
247
+ const hue = ((h % 360) + 360) % 360;
248
+ const sN = Math.max(0, Math.min(1, nums[1].endsWith('%') ? sat : sat / 100));
249
+ const lN = Math.max(0, Math.min(1, nums[2].endsWith('%') ? light : light / 100));
250
+ const c = (1 - Math.abs(2 * lN - 1)) * sN;
251
+ const x = c * (1 - Math.abs(((hue / 60) % 2) - 1));
252
+ const m = lN - c / 2;
253
+ const [r1, g1, b1] = hue < 60 ? [c, x, 0] : hue < 120 ? [x, c, 0] : hue < 180 ? [0, c, x] : hue < 240 ? [0, x, c] : hue < 300 ? [x, 0, c] : [c, 0, x];
254
+ return [Math.round((r1 + m) * 255), Math.round((g1 + m) * 255), Math.round((b1 + m) * 255)];
255
+ }
256
+ function luminance([r, g, b]) {
257
+ const lin = (c) => {
258
+ const s = c / 255;
259
+ return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
260
+ };
261
+ return 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b);
262
+ }
263
+ /** The WCAG contrast ratio of two colours (≥ 1), or `null` when either is not computable. */
264
+ export function contrastRatio(a, b) {
265
+ const ca = parseColor(a);
266
+ const cb = parseColor(b);
267
+ if (ca === null || cb === null)
268
+ return null;
269
+ const la = luminance(ca);
270
+ const lb = luminance(cb);
271
+ const [hi, lo] = la >= lb ? [la, lb] : [lb, la];
272
+ return (hi + 0.05) / (lo + 0.05);
273
+ }
274
+ export const MIN_TEXT_CONTRAST = 4.5;
275
+ /** Text on background must read: `colors.text_primary` over `colors.background` ≥ 4.5:1. */
276
+ export function checkThemeContrast(tokens) {
277
+ const colors = tokens['colors'];
278
+ if (!isPlainObject(colors))
279
+ return reject('missing-group:colors');
280
+ const text = colors['text_primary'];
281
+ const bg = colors['background'];
282
+ if (typeof text !== 'string' || typeof bg !== 'string')
283
+ return reject('missing-key:colors.text_primary|background');
284
+ const ratio = contrastRatio(text, bg);
285
+ if (ratio === null)
286
+ return { ok: false, reason: 'contrast-not-checkable:colors.text_primary on colors.background (use an opaque #hex, rgb() or hsl(); no var(), no alpha below 1)' };
287
+ if (ratio < MIN_TEXT_CONTRAST)
288
+ return { ok: false, reason: `contrast-too-low:${ratio.toFixed(2)}:1 for colors.text_primary on colors.background (needs ${MIN_TEXT_CONTRAST}:1)` };
289
+ return { ok: true };
290
+ }
291
+ /** Shape, grammar, then contrast. The first failure is the honest refusal. */
292
+ export function validateThemeTokens(tokens) {
293
+ const shape = checkThemeTokens(tokens);
294
+ if (!shape.ok)
295
+ return shape;
296
+ return checkThemeContrast(tokens);
297
+ }
298
+ /** Read the worker's output file. `null` tokens with the reason when it is missing or not JSON. */
299
+ export function collectThemeResult(outputPath) {
300
+ let raw;
301
+ try {
302
+ raw = readFileSync(outputPath, 'utf8');
303
+ }
304
+ catch {
305
+ return { error: `no token file at ${outputPath}` };
306
+ }
307
+ try {
308
+ return { tokens: JSON.parse(raw) };
309
+ }
310
+ catch (err) {
311
+ return { error: `the token file is not JSON: ${err instanceof Error ? err.message : String(err)}` };
312
+ }
313
+ }
314
+ /**
315
+ * The default writer: the doc's bridge (reuse-or-start, per docs root) ← `PUT /d/:doc/api/theme/learned
316
+ * {tokens, apply: true}`. Interactive validates the grammar again (400), writes `learned.theme.json`
317
+ * and lands one re-themed version of the head inside its FIFO.
318
+ */
319
+ export function putLearnedThemeViaBridge(pool, resolveDocsRoot) {
320
+ return async (documentId, projectId, tokens) => {
321
+ const bridge = await pool.ensure(resolveDocsRoot(projectId));
322
+ const body = JSON.stringify({ tokens, apply: true });
323
+ return new Promise((resolvePromise, rejectPromise) => {
324
+ const req = httpRequest({
325
+ host: bridge.host,
326
+ port: bridge.port,
327
+ method: 'PUT',
328
+ path: `/d/${encodeURIComponent(documentId)}/api/theme/learned`,
329
+ headers: { 'content-type': 'application/json', 'content-length': Buffer.byteLength(body) },
330
+ timeout: 30_000,
331
+ }, (res) => {
332
+ const chunks = [];
333
+ res.on('data', (c) => chunks.push(c));
334
+ res.on('end', () => {
335
+ const text = Buffer.concat(chunks).toString('utf8');
336
+ let parsed = {};
337
+ try {
338
+ parsed = JSON.parse(text);
339
+ }
340
+ catch {
341
+ parsed = {};
342
+ }
343
+ if ((res.statusCode ?? 500) >= 300) {
344
+ rejectPromise(new Error(`interactive refused the theme (${res.statusCode}): ${typeof parsed['error'] === 'string' ? parsed['error'] : oneLine(text, 200)}`));
345
+ return;
346
+ }
347
+ resolvePromise({
348
+ version: typeof parsed['version'] === 'number' ? parsed['version'] : null,
349
+ ...(typeof parsed['parent'] === 'number' || parsed['parent'] === null ? { parent: parsed['parent'] } : {}),
350
+ ...(parsed['unchanged'] === true ? { unchanged: true } : {}),
351
+ });
352
+ });
353
+ });
354
+ req.on('timeout', () => req.destroy(new Error('interactive did not answer the theme write in 30 s')));
355
+ req.on('error', rejectPromise);
356
+ req.end(body);
357
+ });
358
+ };
359
+ }
360
+ function rosterOf(adapter, roster) {
361
+ if (roster !== undefined)
362
+ return roster();
363
+ const own = adapter.launchRoster;
364
+ if (typeof own === 'function')
365
+ return own.call(adapter);
366
+ return adapter.constructor.roster();
367
+ }
368
+ export async function startInteractiveThemeSubscriber(adapter, opts) {
369
+ const log = opts.log ?? ((m) => console.error(m));
370
+ let busDbPath;
371
+ try {
372
+ busDbPath = requireEngineBus(opts.dbPath);
373
+ }
374
+ catch (err) {
375
+ log(`[interactive-theme] has no bus${opts.dbPath !== undefined ? ` at ${opts.dbPath}` : ''} — theme learning disabled: ${err instanceof Error ? err.message : String(err)}`);
376
+ return null;
377
+ }
378
+ try {
379
+ await adapter.registerWorkflow(INTERACTIVE_THEME_WORKFLOW_DEF);
380
+ }
381
+ catch (err) {
382
+ log(`[interactive-theme] could not register the '${INTERACTIVE_THEME_WORKFLOW}' workflow — theme learning disabled: ${err instanceof Error ? err.message : String(err)}`);
383
+ return null;
384
+ }
385
+ const ledger = opts.ledger ?? new InteractiveHandoffLedger(opts.ledgerPath ?? join(crewStateHome(), 'interactive-edit-ledger.json'));
386
+ const editDir = opts.editDir ?? join(crewStateHome(), 'interactive-edits');
387
+ const heartbeatMs = opts.heartbeatMs ?? 15_000;
388
+ const inFlight = new Map();
389
+ async function emitInteractive(type, payload) {
390
+ try {
391
+ await emitOnBus(busDbPath, {
392
+ event_type: type,
393
+ domain: INTERACTIVE_DOMAIN,
394
+ subdomain: 'status',
395
+ payload: { ts: new Date().toISOString(), ...payload },
396
+ producer_id: INTERACTIVE_PRODUCER,
397
+ });
398
+ return true;
399
+ }
400
+ catch (err) {
401
+ log(`[interactive-theme] emit ${type} failed: ${err instanceof Error ? err.message : String(err)}`);
402
+ return false;
403
+ }
404
+ }
405
+ const emitStatus = (payload) => emitInteractive(STATUS_POSTED, { ...payload });
406
+ const narrate = (flight, message) => {
407
+ flight.narration = message;
408
+ void emitStatus({ ...docScope(flight.documentId, flight.projectId), state: 'working', message, ...narrationStamps(flight) });
409
+ };
410
+ /** The ledger row a flight folds onto; a run that ended inside the launch call needs it first. */
411
+ const ensureRow = (flight, runId) => {
412
+ if (!ledger.has(flight.key))
413
+ ledger.recordLaunch(flight.key, runId);
414
+ };
415
+ const endFlight = (runId) => {
416
+ const flight = inFlight.get(runId);
417
+ if (flight !== undefined) {
418
+ clearInterval(flight.heartbeat);
419
+ inFlight.delete(runId);
420
+ }
421
+ return flight;
422
+ };
423
+ const offCoreEvents = adapter.onEvent((event) => {
424
+ const runId = typeof event.session === 'string' ? event.session : undefined;
425
+ if (runId === undefined)
426
+ return;
427
+ const flight = inFlight.get(runId);
428
+ if (flight === undefined)
429
+ return;
430
+ flight.runId ??= runId;
431
+ if (typeof event.ord === 'number')
432
+ flight.narrationOrd = event.ord;
433
+ switch (event.type) {
434
+ case 'councilConvened':
435
+ narrate(flight, 'Convening a council to pick who reads the design…');
436
+ return;
437
+ case 'unitDistributed':
438
+ narrate(flight, unitDistributedLine(event, 'to read the design'));
439
+ return;
440
+ case 'unitDispatched':
441
+ narrate(flight, 'Crew is reading the grabbed design and writing its tokens…');
442
+ return;
443
+ case 'unitOutputCaptured':
444
+ narrate(flight, 'Tokens written — checking the shape, the grammar and the contrast…');
445
+ return;
446
+ case 'stepFailed': {
447
+ const detail = typeof event.detail === 'string' ? event.detail.trim() : '';
448
+ if (detail.length > 0)
449
+ flight.failureDetail = detail;
450
+ return;
451
+ }
452
+ case 'sessionCompleted':
453
+ endFlight(runId);
454
+ finalize(flight, runId).catch((err) => log(`[interactive-theme] finalizing run ${runId} failed: ${err instanceof Error ? err.message : String(err)}`));
455
+ return;
456
+ case 'sessionFailed':
457
+ case 'runCancelled': {
458
+ endFlight(runId);
459
+ ensureRow(flight, runId);
460
+ ledger.recordFailure(flight.key);
461
+ const why = flight.failureDetail !== undefined ? ` Reason: ${oneLine(flight.failureDetail, 600)}` : '';
462
+ void emitStatus({
463
+ ...docScope(flight.documentId, flight.projectId),
464
+ state: 'error',
465
+ message: `The crew run learning this theme ${event.type === 'runCancelled' ? 'was cancelled' : 'failed'} (run ${runId}).${why} Nothing was applied.`,
466
+ });
467
+ return;
468
+ }
469
+ default:
470
+ return;
471
+ }
472
+ });
473
+ async function finalize(flight, runId) {
474
+ const { documentId, projectId, key, outputPath } = flight;
475
+ ensureRow(flight, runId);
476
+ const result = collectThemeResult(outputPath);
477
+ if ('error' in result) {
478
+ ledger.recordFailure(key);
479
+ await emitStatus({ ...docScope(documentId, projectId), state: 'error', message: `The crew's theme run wrote no usable tokens — ${result.error} (run ${runId}). Nothing was applied.` });
480
+ return;
481
+ }
482
+ const check = validateThemeTokens(result.tokens);
483
+ if (!check.ok) {
484
+ ledger.recordFailure(key);
485
+ await emitStatus({ ...docScope(documentId, projectId), state: 'error', message: `The learned theme was refused — ${check.reason} (run ${runId}). Nothing was applied; the document is unchanged.` });
486
+ log(`[interactive-theme] run ${runId} refused for ${key}: ${check.reason}`);
487
+ return;
488
+ }
489
+ try {
490
+ const written = await opts.putLearnedTheme(documentId, projectId, result.tokens);
491
+ ledger.recordEmitted(key);
492
+ await emitStatus({
493
+ ...docScope(documentId, projectId),
494
+ state: 'complete',
495
+ ...(written.version !== null ? { version: written.version } : {}),
496
+ message: written.version !== null
497
+ ? `Theme learned — version ${written.version} wears it now. Undo = fork from its parent.`
498
+ : 'Theme learned and saved; the head already wore it, so no new version was made.',
499
+ });
500
+ log(`[interactive-theme] theme written for ${key} (run ${runId}, version ${written.version ?? 'unchanged'})`);
501
+ }
502
+ catch (err) {
503
+ ledger.recordFailure(key);
504
+ const reason = err instanceof Error ? err.message : String(err);
505
+ await emitStatus({ ...docScope(documentId, projectId), state: 'error', message: `Crew learned the theme but interactive did not take it: ${oneLine(reason, 400)} (run ${runId}). The document is unchanged.` });
506
+ log(`[interactive-theme] write for ${key} failed: ${reason}`);
507
+ }
508
+ }
509
+ async function handleThemeLearned(event) {
510
+ const learned = parseThemeLearned(event.event_type, event.payload);
511
+ if (learned === null)
512
+ return;
513
+ const key = themeHandoffKey(learned.documentId, learned.ts);
514
+ if (ledger.has(key)) {
515
+ log(`[interactive-theme] handoff ${key} already answered (run ${ledger.get(key)?.runId}) — replay ignored`);
516
+ return;
517
+ }
518
+ for (const f of inFlight.values())
519
+ if (f.key === key)
520
+ return;
521
+ const safeKey = key.replace(/[^a-zA-Z0-9_-]/gu, '-');
522
+ const runDir = join(editDir, safeKey);
523
+ mkdirSync(runDir, { recursive: true });
524
+ const outputPath = join(runDir, 'theme.tokens.json');
525
+ const handoffPath = join(runDir, 'handoff.json');
526
+ writeFileSync(handoffPath, JSON.stringify({ document_id: learned.documentId, render_path: learned.renderPath, format: learned.format, ...(learned.url !== undefined ? { url: learned.url } : {}), output_path: outputPath, shape: THEME_SHAPE }, null, 2), 'utf8');
527
+ const runId = randomUUID();
528
+ await emitStatus({ ...docScope(learned.documentId, learned.projectId), state: 'processing', message: 'A governed crew is reading the design you grabbed…' });
529
+ let projectGraphBinding = null;
530
+ if (learned.projectId !== undefined) {
531
+ const decision = await resolveProjectGraphBinding(adapter, learned.projectId, undefined).catch((err) => ({
532
+ binding: null,
533
+ reason: `the project graph binding could not be resolved (${err instanceof Error ? err.message : String(err)}). This repo-less run gets no code graph.`,
534
+ }));
535
+ projectGraphBinding = decision.binding;
536
+ log(`run ${runId}: ${decision.reason}`);
537
+ }
538
+ // The flight is registered BEFORE the engine is called (codex r1): a run whose terminal frame
539
+ // lands during the launch call must still find its flight. A refused launch retracts it.
540
+ const flight = {
541
+ key,
542
+ documentId: learned.documentId,
543
+ projectId: learned.projectId,
544
+ outputPath,
545
+ narration: 'Reading the grabbed design…',
546
+ runId,
547
+ heartbeat: setInterval(() => {
548
+ const f = inFlight.get(runId);
549
+ if (f !== undefined)
550
+ void emitStatus({ ...docScope(f.documentId, f.projectId), state: 'working', message: f.narration, ...narrationStamps(f) });
551
+ }, heartbeatMs),
552
+ };
553
+ inFlight.set(runId, flight);
554
+ try {
555
+ const input = {
556
+ problem: themeProblem(learned, handoffPath),
557
+ sessionId: runId,
558
+ clisJson: opts.clisJson ?? JSON.stringify(rosterOf(adapter, opts.roster)),
559
+ workflow: INTERACTIVE_THEME_WORKFLOW,
560
+ ...(learned.projectId !== undefined ? { projectId: learned.projectId } : {}),
561
+ ...(projectGraphBinding !== null ? { projectGraph: projectGraphBinding } : {}),
562
+ extraWriteRoots: [runDir],
563
+ requireDeliverables: [outputPath],
564
+ };
565
+ await adapter.launchRun(input);
566
+ }
567
+ catch (err) {
568
+ endFlight(runId);
569
+ const reason = err instanceof Error ? err.message : String(err);
570
+ await emitStatus({ ...docScope(learned.documentId, learned.projectId), state: 'error', message: `Crew could not start a run to learn this theme: ${reason}.` });
571
+ log(`[interactive-theme] launch for ${key} failed: ${reason}`);
572
+ return;
573
+ }
574
+ // A run that already ended inside the launch call was finalized by the event fold and holds its
575
+ // row (with its outcome) already; otherwise the row is written now, so a replay never launches twice.
576
+ if (!ledger.has(key))
577
+ ledger.recordLaunch(key, runId);
578
+ if (learned.projectId !== undefined)
579
+ opts.onRunFiled?.(runId, learned.projectId);
580
+ log(`[interactive-theme] launched run ${runId} for ${key}`);
581
+ }
582
+ const tap = await tapBus({
583
+ dbPath: busDbPath,
584
+ filter: INTERACTIVE_THEME_BUS_FILTER,
585
+ pollIntervalMs: opts.pollIntervalMs,
586
+ handler: handleThemeLearned,
587
+ onError: busSubscriberErrorReporter({
588
+ describe: (err, event) => `[interactive-theme] handler error on event ${String(event?.event_id ?? '?')}: ${err.message}`,
589
+ log,
590
+ logError: opts.logError,
591
+ pollIntervalMs: opts.pollIntervalMs ?? 2000,
592
+ }),
593
+ });
594
+ return {
595
+ ledger,
596
+ inFlightDocs: () => [...new Set([...inFlight.values()].map((f) => f.documentId))],
597
+ stop: async () => {
598
+ offCoreEvents();
599
+ for (const f of inFlight.values())
600
+ clearInterval(f.heartbeat);
601
+ inFlight.clear();
602
+ await tap.stop();
603
+ },
604
+ };
605
+ }
606
+ //# sourceMappingURL=theme-events.js.map