wicked-crew 0.6.0 → 0.7.0

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 (99) hide show
  1. package/dist/api/audit.d.ts +13 -0
  2. package/dist/api/audit.d.ts.map +1 -1
  3. package/dist/api/audit.js +18 -2
  4. package/dist/api/audit.js.map +1 -1
  5. package/dist/api/guidance-index.d.ts +39 -0
  6. package/dist/api/guidance-index.d.ts.map +1 -0
  7. package/dist/api/guidance-index.js +67 -0
  8. package/dist/api/guidance-index.js.map +1 -0
  9. package/dist/api/open-path.d.ts +16 -0
  10. package/dist/api/open-path.d.ts.map +1 -1
  11. package/dist/api/open-path.js +22 -0
  12. package/dist/api/open-path.js.map +1 -1
  13. package/dist/api/retry-index.d.ts +30 -0
  14. package/dist/api/retry-index.d.ts.map +1 -0
  15. package/dist/api/retry-index.js +45 -0
  16. package/dist/api/retry-index.js.map +1 -0
  17. package/dist/api/routes.d.ts +53 -1
  18. package/dist/api/routes.d.ts.map +1 -1
  19. package/dist/api/routes.js +352 -23
  20. package/dist/api/routes.js.map +1 -1
  21. package/dist/api/run-files.d.ts +63 -0
  22. package/dist/api/run-files.d.ts.map +1 -0
  23. package/dist/api/run-files.js +271 -0
  24. package/dist/api/run-files.js.map +1 -0
  25. package/dist/api/server.d.ts +79 -0
  26. package/dist/api/server.d.ts.map +1 -1
  27. package/dist/api/server.js +135 -6
  28. package/dist/api/server.js.map +1 -1
  29. package/dist/api/stall-watchdog.d.ts +62 -0
  30. package/dist/api/stall-watchdog.d.ts.map +1 -0
  31. package/dist/api/stall-watchdog.js +138 -0
  32. package/dist/api/stall-watchdog.js.map +1 -0
  33. package/dist/cli/index.js +78 -13
  34. package/dist/cli/index.js.map +1 -1
  35. package/dist/core/adapter.d.ts +24 -10
  36. package/dist/core/adapter.d.ts.map +1 -1
  37. package/dist/core/adapter.js +191 -30
  38. package/dist/core/adapter.js.map +1 -1
  39. package/dist/core/bridge-reaper.d.ts +134 -0
  40. package/dist/core/bridge-reaper.d.ts.map +1 -0
  41. package/dist/core/bridge-reaper.js +286 -0
  42. package/dist/core/bridge-reaper.js.map +1 -0
  43. package/dist/core/deliver.d.ts +118 -0
  44. package/dist/core/deliver.d.ts.map +1 -0
  45. package/dist/core/deliver.js +241 -0
  46. package/dist/core/deliver.js.map +1 -0
  47. package/dist/core/deliverable-floor.d.ts +103 -0
  48. package/dist/core/deliverable-floor.d.ts.map +1 -0
  49. package/dist/core/deliverable-floor.js +173 -0
  50. package/dist/core/deliverable-floor.js.map +1 -0
  51. package/dist/core/exec.d.ts +2 -0
  52. package/dist/core/exec.d.ts.map +1 -1
  53. package/dist/core/exec.js.map +1 -1
  54. package/dist/core/types.d.ts +79 -1
  55. package/dist/core/types.d.ts.map +1 -1
  56. package/dist/core/types.js +3 -0
  57. package/dist/core/types.js.map +1 -1
  58. package/dist/interactive/bridge-pool.d.ts +28 -0
  59. package/dist/interactive/bridge-pool.d.ts.map +1 -1
  60. package/dist/interactive/bridge-pool.js +67 -10
  61. package/dist/interactive/bridge-pool.js.map +1 -1
  62. package/dist/interactive/chat-events.d.ts +207 -0
  63. package/dist/interactive/chat-events.d.ts.map +1 -0
  64. package/dist/interactive/chat-events.js +769 -0
  65. package/dist/interactive/chat-events.js.map +1 -0
  66. package/dist/interactive/demo-events.d.ts +283 -0
  67. package/dist/interactive/demo-events.d.ts.map +1 -0
  68. package/dist/interactive/demo-events.js +889 -0
  69. package/dist/interactive/demo-events.js.map +1 -0
  70. package/dist/interactive/draft-events.d.ts +87 -7
  71. package/dist/interactive/draft-events.d.ts.map +1 -1
  72. package/dist/interactive/draft-events.js +352 -49
  73. package/dist/interactive/draft-events.js.map +1 -1
  74. package/dist/interactive/edit-events.d.ts +22 -0
  75. package/dist/interactive/edit-events.d.ts.map +1 -1
  76. package/dist/interactive/edit-events.js +73 -2
  77. package/dist/interactive/edit-events.js.map +1 -1
  78. package/dist/interactive/repo-snapshot.d.ts +100 -0
  79. package/dist/interactive/repo-snapshot.d.ts.map +1 -0
  80. package/dist/interactive/repo-snapshot.js +289 -0
  81. package/dist/interactive/repo-snapshot.js.map +1 -0
  82. package/dist/projects/graph-paths.d.ts +92 -0
  83. package/dist/projects/graph-paths.d.ts.map +1 -0
  84. package/dist/projects/graph-paths.js +130 -0
  85. package/dist/projects/graph-paths.js.map +1 -0
  86. package/dist/projects/graph.d.ts +179 -0
  87. package/dist/projects/graph.d.ts.map +1 -0
  88. package/dist/projects/graph.js +775 -0
  89. package/dist/projects/graph.js.map +1 -0
  90. package/dist/projects/routes.d.ts +7 -0
  91. package/dist/projects/routes.d.ts.map +1 -1
  92. package/dist/projects/routes.js +122 -0
  93. package/dist/projects/routes.js.map +1 -1
  94. package/dist/studio/assets/index-8p8uwCxG.js +530 -0
  95. package/dist/studio/assets/index-D6S9zUtO.css +32 -0
  96. package/dist/studio/index.html +5 -3
  97. package/package.json +3 -3
  98. package/dist/studio/assets/index-CCwXa1cn.js +0 -428
  99. package/dist/studio/assets/index-HWxo0h41.css +0 -32
@@ -0,0 +1,889 @@
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, writeFileSync } from 'node:fs';
78
+ import { join } from 'node:path';
79
+ import { homedir } from 'node:os';
80
+ import { randomUUID } from 'node:crypto';
81
+ import { DOC_CREATED, DOC_NAME, INTERACTIVE_DOMAIN, INTERACTIVE_PRODUCER, STATUS_POSTED, oneLine, } from './draft-events.js';
82
+ import { FEEDBACK_PROCESSED, handoffKey, parseStructuralFeedback, } from './edit-events.js';
83
+ import { readDocHead } from './chat-events.js';
84
+ import { resolveInteractiveRoot } from './bridge-root.js';
85
+ import { InteractiveHandoffLedger } from './ledger.js';
86
+ // ── Vocabulary constants (interactive's, verbatim — src/service/events.js is the truth) ──────
87
+ export const DEMO_REQUESTED = 'wicked.interactive.demo.requested';
88
+ /** Exact-type filters with a domain guard — no wildcards. The seam listens on TWO topics:
89
+ * doc.created (kind:demo — author the first spec) and feedback.processed (demo-kind docs —
90
+ * re-author it). */
91
+ export const INTERACTIVE_DEMO_BUS_FILTER = `${DOC_CREATED}@${INTERACTIVE_DOMAIN}`;
92
+ export const INTERACTIVE_DEMO_FEEDBACK_BUS_FILTER = `${FEEDBACK_PROCESSED}@${INTERACTIVE_DOMAIN}`;
93
+ /** Dedicated durable-cursor identities — NOT the draft/edit/chat seams', so every interactive
94
+ * seam advances an independent cursor and stopping one never strands another. The two demo
95
+ * subscriptions get their own cursors too: they filter different types. */
96
+ export const INTERACTIVE_DEMO_BUS_PLUGIN = 'wicked-crew-interactive-demo';
97
+ export const INTERACTIVE_DEMO_FEEDBACK_BUS_PLUGIN = 'wicked-crew-interactive-demo-feedback';
98
+ /** The one file the whole demo pipeline pivots on (interactive demo.js `DEMO_SPEC`):
99
+ * `recordDemo` refuses to record until `<docDir>/demo.spec.mjs` exists. */
100
+ export const DEMO_SPEC_FILE = 'demo.spec.mjs';
101
+ // ── The workflows (workflows-as-data) ─────────────────────────────────────────────────────────
102
+ export const INTERACTIVE_DEMO_WORKFLOW = 'interactive-demo';
103
+ export const INTERACTIVE_DEMO_REAUTHOR_WORKFLOW = 'interactive-demo-reauthor';
104
+ /**
105
+ * The governed workflow that authors a demo's FIRST spec. TWO agent phases — `scenes` (recon,
106
+ * neutral: plan from the brief and the URL string, TOUCHING NOTHING) then `spec` (build,
107
+ * creator: inspect the live app, write the Playwright spec, report).
108
+ *
109
+ * THE PHASE SPLIT IS LOAD-BEARING AND MEASURED — do not "simplify" it in either direction.
110
+ * wicked-core#293: A TOOL-USING TURN POISONS A LATER TOOL-PERMISSION REQUEST — the request is
111
+ * never answered, so the worker's turn ends where it stood. Three shapes have been run against
112
+ * a real seat on a real stack (adversarial + build verification, 2026-08-24), and only one
113
+ * survives:
114
+ *
115
+ * 1. recon phase that INSPECTS THE APP, then a spec phase — DEAD 4/4. The recon turn fetches
116
+ * the target, and the spec phase's first permission request goes unanswered; nothing is
117
+ * ever written.
118
+ * 2. recon phase forbidden from using ANY TOOL, then a spec phase that curls the app and
119
+ * writes — GREEN 15/15 end-to-end: spec authored, installed, a 436KB webm recorded, the
120
+ * storyboard landed, `version.created kind:demo` on the bus. THIS IS THE SHAPE BELOW.
121
+ * 3. ONE phase doing inspect + plan + write together (the obvious "no next turn to poison"
122
+ * simplification) — DEAD 3/3, and this is the counter-intuitive result worth writing down:
123
+ * the worker curls the pages, narrates "now writing the spec", and the turn ends AT the
124
+ * write. Two runs died with an empty inbox and 161-345 byte outputs (one rejected by the
125
+ * substance floor as `substanceRejected`, one "completed" with no deliverable and caught
126
+ * by finalize). Collapsing the phases does NOT dodge #293 — it just moves the unanswered
127
+ * permission request from the next turn into this one.
128
+ *
129
+ * A fourth data point narrows what "tool use" means here: the re-author leg's single phase
130
+ * does `Read` + `Read` + `Write` in one turn and lands green (7/7, run 0704b910 — revised spec
131
+ * installed, v2 re-recorded). So a turn that reads LOCAL FILES and then writes is fine; it is
132
+ * the NETWORK fetch that costs the following write, whether that write is in the next turn
133
+ * (shape 1) or the same one (shape 3). Treat that as the observed discriminator, not a proven
134
+ * mechanism — #293 is wicked-core's to diagnose; this def only has to survive it.
135
+ *
136
+ * So the rule this workflow encodes is narrower than "one phase": THE PHASE THAT INSPECTS THE
137
+ * APP MUST BE THE PHASE THAT WRITES THE DELIVERABLE, and nothing before it may touch a tool.
138
+ * Phase 1 therefore plans from the brief and the URL STRING alone and is told so explicitly (a
139
+ * soft constraint, and the model has broken a weaker one — the shipped wording said only "do
140
+ * not write any files" and the worker fetched the app anyway — so the prohibition names tools
141
+ * outright and says what it protects).
142
+ *
143
+ * The phase `instructions` adapt the demo-authoring contract from interactive's assist skill
144
+ * (Step 8a/8b) and demo.js `recordDemo`'s executable contract: ESM exporting `meta` + `async
145
+ * run({page, step, meta})`, every meaningful action wrapped in `step(label, fn, {say,
146
+ * holdMs})`, capability narration, stable selectors, awaited waits, and NEVER credentials in
147
+ * the file. SINGLE-LINE by contract (PTY seat runner, wicked-core FINDING-011).
148
+ *
149
+ * THE SUBSTANCE FLOOR (wicked-core `actor.rs`, "phase produced no reviewable substance"): a
150
+ * governed creator/neutral phase whose fold carries neither a worktree diff nor >=200 trimmed
151
+ * chars of prose is REJECTED and fails the run. These runs are UNBOUND — `workdir: null`, so
152
+ * there is never a worktree diff by construction — and the deliverable is a FILE in the inbox,
153
+ * invisible to that check. A worker told only to "end your reply with the absolute path" answers
154
+ * in ~50 bytes and the whole demo dies at the gate (observed 3/3 on the real stack before this
155
+ * wording). So both workflows' build phases demand a prose report of what they wrote: the
156
+ * phase's own reviewable substance, not decoration.
157
+ *
158
+ * All gates are `auto` with `validator_pin: null` — no human gate — because the acceptance
159
+ * gate for a demo is the RECORDING itself: the service executes the spec (a broken selector
160
+ * fails the record with a step-precise error status) and the user judges the video on the
161
+ * storyboard, then refines through the same loop. The assist skill's interactive scene-plan
162
+ * confirmation (Step 8a.5) was an editorial affordance of a conversational session; the
163
+ * governed loop's editorial channel is the step-feedback path below.
164
+ */
165
+ export const INTERACTIVE_DEMO_WORKFLOW_DEF = {
166
+ id: INTERACTIVE_DEMO_WORKFLOW,
167
+ is_system: true,
168
+ phases: [
169
+ {
170
+ id: 'scenes',
171
+ kind: 'recon',
172
+ 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.',
173
+ gate_type: 'value',
174
+ gate: 'auto',
175
+ executes_code: false,
176
+ verified_evidence: false,
177
+ required_deliverables: [],
178
+ depends_on: [],
179
+ role: 'neutral',
180
+ skill_ref: null,
181
+ allowed_skills: [],
182
+ validator_pin: null,
183
+ },
184
+ {
185
+ id: 'spec',
186
+ kind: 'build',
187
+ 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.',
188
+ gate_type: 'execution',
189
+ gate: 'auto',
190
+ executes_code: false,
191
+ verified_evidence: false,
192
+ required_deliverables: [],
193
+ depends_on: ['scenes'],
194
+ role: 'creator',
195
+ skill_ref: null,
196
+ allowed_skills: [],
197
+ validator_pin: null,
198
+ },
199
+ ],
200
+ };
201
+ /**
202
+ * The governed workflow that RE-authors a spec per step feedback (Step 8c). ONE agent phase —
203
+ * `respec` (build, creator role) — the edit leg's rationale: the current spec and the user's
204
+ * instructions are already extracted into files the task names, so a recon phase would only
205
+ * burn a council turn re-reading what the build phase reads anyway.
206
+ *
207
+ * SAME v2 TREATMENT AS THE FIRST-SPEC WORKFLOW (wicked-core#293), which for this leg means
208
+ * LEAVING IT ALONE and saying why. Nothing runs before the phase that writes, so no predecessor
209
+ * turn can poison its permissions — and its own tool use is LOCAL FILE READS inside the run's
210
+ * declared write root (the same shape as the interactive-edit workflow's single phase, which
211
+ * reads a handoff JSON and writes fragments in one turn, in production, today). What this leg
212
+ * must NOT grow is a live re-fetch of the app: the first-spec measurements above show a turn
213
+ * that fetches the network and then writes losing the write outright (shape 3, dead 3/3),
214
+ * while this leg's Read+Read+Write turn lands green (7/7 on a real seat, run 0704b910). If a
215
+ * feedback item ever genuinely needs a selector the current spec lacks, the fix is a preceding
216
+ * TOOL-FREE phase plus inspection moved into this one — the shape 2 arrangement — not a fetch
217
+ * bolted onto the writing turn.
218
+ */
219
+ export const INTERACTIVE_DEMO_REAUTHOR_WORKFLOW_DEF = {
220
+ id: INTERACTIVE_DEMO_REAUTHOR_WORKFLOW,
221
+ is_system: true,
222
+ phases: [
223
+ {
224
+ id: 'respec',
225
+ kind: 'build',
226
+ 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.',
227
+ gate_type: 'execution',
228
+ gate: 'auto',
229
+ executes_code: false,
230
+ verified_evidence: false,
231
+ required_deliverables: [],
232
+ depends_on: [],
233
+ role: 'creator',
234
+ skill_ref: null,
235
+ allowed_skills: [],
236
+ validator_pin: null,
237
+ },
238
+ ],
239
+ };
240
+ /** URL budget for the problem statement: the URL must ride VERBATIM (a respelled URL targets
241
+ * the wrong app), so an over-budget one refuses the parse rather than truncating. */
242
+ export const DEMO_URL_MAX = 1000;
243
+ /**
244
+ * Parse a bus frame into a {@link DemoDocCreated}, or `null` when it is not an actionable
245
+ * demo creation: wrong type, non-`demo` kind (source/html docs are the draft seam's and the
246
+ * user's own business), missing/malformed document_id, or an unusable URL. The URL must be a
247
+ * parseable http(s) URL with no whitespace/control characters — it rides the single-line PTY
248
+ * problem verbatim (interactive's server validates this at creation, so a violation here is
249
+ * producer drift, not a user path).
250
+ */
251
+ export function parseDemoDocCreated(eventType, payload) {
252
+ if (eventType !== DOC_CREATED)
253
+ return null;
254
+ if (typeof payload !== 'object' || payload === null)
255
+ return null;
256
+ const p = payload;
257
+ if (p['kind'] !== 'demo')
258
+ return null;
259
+ const documentId = typeof p['document_id'] === 'string' ? p['document_id'] : '';
260
+ if (!DOC_NAME.test(documentId))
261
+ return null;
262
+ const url = typeof p['url'] === 'string' ? p['url'].trim() : '';
263
+ if (url.length === 0 || url.length > DEMO_URL_MAX || /[\s\u0000-\u001f]/.test(url))
264
+ return null;
265
+ try {
266
+ const parsed = new URL(url);
267
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:')
268
+ return null;
269
+ }
270
+ catch {
271
+ return null;
272
+ }
273
+ const brief = typeof p['brief'] === 'string' ? p['brief'] : '';
274
+ const projectId = typeof p['project_id'] === 'string' && p['project_id'].length > 0 ? p['project_id'] : undefined;
275
+ return { documentId, url, brief, ...(projectId !== undefined ? { projectId } : {}) };
276
+ }
277
+ /**
278
+ * The first-spec run's problem statement (the engine scopes it per phase and folds each
279
+ * phase's instructions on top). Carries the doc identity, the target URL VERBATIM, the brief
280
+ * (flattened + capped: PTY, wicked-core FINDING-011), and the absolute inbox path the spec
281
+ * must land at. Deliberately NO doc-workspace path: the unbound worker could not read or
282
+ * write it anyway (wicked-core#294) — crew installs the spec at finalize.
283
+ */
284
+ export function demoProblem(doc, outPath) {
285
+ const brief = doc.brief.trim().length > 0
286
+ ? oneLine(doc.brief, 2000)
287
+ : "(no brief provided — demonstrate the app's core flow)";
288
+ return (`Author the Playwright demo spec for the wicked-interactive demo document "${doc.documentId}". ` +
289
+ `Target application URL: ${doc.url} The user's brief: ${brief} ` +
290
+ `The finished spec MUST be written to exactly this absolute file path: ${outPath}`);
291
+ }
292
+ /**
293
+ * The re-author run's problem statement. Single-line; the bulky parts (the current spec, the
294
+ * feedback fragments) ride in FILES inside the inbox — write roots are readable
295
+ * (wicked-core#259) — so only identity, the capped instruction gist, and the three paths ride
296
+ * here (the edit leg's handoff-by-file discipline).
297
+ */
298
+ export function demoReauthorProblem(handoff, currentSpecPath, feedbackPath, outPath) {
299
+ const gist = oneLine(handoff.items.map((i) => i.instruction).filter((s) => s.length > 0).join('; '), 600);
300
+ return (`Re-author the Playwright demo spec for the wicked-interactive demo document ` +
301
+ `"${handoff.documentId}" per the user's step feedback (${handoff.items.length} item(s) on ` +
302
+ `version ${handoff.version}). The CURRENT spec is the file at this absolute path — read it ` +
303
+ `first: ${currentSpecPath} The feedback items (instruction + the targeted storyboard step's ` +
304
+ `HTML fragment, per item) are in the JSON file at: ${feedbackPath} ` +
305
+ `The user asked: ${gist.length > 0 ? gist : '(no instruction text)'} ` +
306
+ `The revised spec MUST be written to exactly this absolute file path: ${outPath}`);
307
+ }
308
+ /** Deterministic bus idempotency key for the FIRST record request this seam may trigger per
309
+ * document. Distinct from every re-author key: a demo legitimately re-records many times, so
310
+ * the record trigger must never dedupe across authoring generations. */
311
+ export function demoIdempotencyKey(documentId) {
312
+ return `crew:interactive.demo:${documentId}:spec`;
313
+ }
314
+ /** Deterministic bus idempotency key for the one re-record a given feedback handoff earns. */
315
+ export function demoReauthorIdempotencyKey(documentId, version) {
316
+ return `crew:interactive.demo:${documentId}:v${version}`;
317
+ }
318
+ /**
319
+ * Deterministic pre-install self-check on the worker's spec (the demo leg's sibling of the
320
+ * edit leg's INV-2 check): `recordDemo` imports the file and throws on a missing `run` export
321
+ * (interactive demo.js), which would surface as a recording error AFTER crew claimed success.
322
+ * Fail honest crew-side instead: a spec that plainly cannot satisfy the module contract is
323
+ * never installed. Returns the human-readable violation, or `null` when the spec looks like a
324
+ * real module. Deliberately shallow — a static check cannot execute the module, so it pins
325
+ * only the export shape the contract mandates; a wrong-but-well-formed spec is the recording's
326
+ * (and the user's) to judge, exactly like a wrong-but-anchor-safe edit on the edit leg.
327
+ */
328
+ export function specSelfCheck(spec) {
329
+ if (spec.trim().length === 0)
330
+ return 'the spec file is empty';
331
+ const exportsMeta = /export\s+(const|let|var)\s+meta\b/.test(spec) || /export\s*\{[^}]*\bmeta\b/.test(spec);
332
+ if (!exportsMeta)
333
+ return 'the spec does not export `meta`';
334
+ // `run` must be AWAITABLE, not merely present (Copilot, #316): recordDemo awaits it, and a
335
+ // synchronous `run` returns before its `step()` calls settle — the recorder would then film
336
+ // an empty page and land a valid-looking but contentless video. A silent bad artifact is
337
+ // exactly what this seam must not produce, so the async marker is part of the shape check
338
+ // for the two forms a static read can see. The re-export form (`export { run }`) still
339
+ // cannot be judged here — its asyncness lives at the declaration — so it stays accepted and
340
+ // the recording remains its judge.
341
+ const asyncRunDecl = /export\s+async\s+function\s+run\b/.test(spec);
342
+ const asyncRunBinding = /export\s+(const|let|var)\s+run\s*=\s*async\b/.test(spec);
343
+ const runReExport = /export\s*\{[^}]*\brun\b/.test(spec);
344
+ const syncRunDecl = /export\s+function\s+run\b/.test(spec);
345
+ const syncRunBinding = /export\s+(const|let|var)\s+run\s*=/.test(spec) && !asyncRunBinding;
346
+ if (syncRunDecl || syncRunBinding) {
347
+ return 'the spec exports `run` but not as an async function (the recorder awaits it)';
348
+ }
349
+ if (!asyncRunDecl && !asyncRunBinding && !runReExport) {
350
+ return 'the spec does not export an async `run` function';
351
+ }
352
+ return null;
353
+ }
354
+ function defaultStateDir() {
355
+ return join(homedir(), '.wicked-crew');
356
+ }
357
+ /** The production council roster, resolved lazily through the adapter's own class so this module
358
+ * never imports the native addon at runtime (unit tests pass `clisJson` and a fake adapter). */
359
+ function rosterOf(adapter) {
360
+ return adapter.constructor.roster();
361
+ }
362
+ /**
363
+ * Arm the seam: register the demo workflows, open durable subscriptions on
364
+ * `wicked.interactive.doc.created` (kind:demo) and `wicked.interactive.feedback.processed`
365
+ * (demo-kind docs), and answer each with a governed run that authors/installs
366
+ * `demo.spec.mjs` and ends in `wicked.interactive.demo.requested` — the model-free service
367
+ * then records the video and lands the storyboard version itself.
368
+ *
369
+ * Graceful degradation mirrors the sibling seams: a missing wicked-bus package or an
370
+ * unopenable db LOGS and returns `null` — the daemon must still boot on a machine whose bus
371
+ * is broken; the demo doc then simply keeps its placeholder (the pre-CREW-UX-9 state).
372
+ */
373
+ export async function startInteractiveDemoSubscriber(adapter, opts = {}) {
374
+ const log = opts.log ?? ((m) => console.error(m));
375
+ let bus;
376
+ try {
377
+ bus = await import('wicked-bus');
378
+ }
379
+ catch (err) {
380
+ log(`[interactive-demo] wicked-bus is not importable — governed demo authoring disabled: ${err instanceof Error ? err.message : String(err)}`);
381
+ return null;
382
+ }
383
+ let db;
384
+ let config;
385
+ try {
386
+ config = bus.loadConfig(opts.dbPath !== undefined ? { db_path: opts.dbPath } : {});
387
+ db = bus.openDb(opts.dbPath !== undefined ? { db_path: opts.dbPath } : {});
388
+ }
389
+ catch (err) {
390
+ 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)}`);
391
+ return null;
392
+ }
393
+ // Both workflows ride the normal registration path — core validates each def BEFORE it is
394
+ // persisted/hot-registered (FINDING-002 ordering), so a drifted def fails the arm loudly
395
+ // instead of failing the first launch obscurely.
396
+ try {
397
+ await adapter.registerWorkflow(INTERACTIVE_DEMO_WORKFLOW_DEF);
398
+ await adapter.registerWorkflow(INTERACTIVE_DEMO_REAUTHOR_WORKFLOW_DEF);
399
+ }
400
+ catch (err) {
401
+ log(`[interactive-demo] could not register the demo workflows — governed demo authoring ` +
402
+ `disabled: ${err instanceof Error ? err.message : String(err)}`);
403
+ return null;
404
+ }
405
+ const ledger = new InteractiveHandoffLedger(opts.ledgerPath ?? join(defaultStateDir(), 'interactive-demo-ledger.json'));
406
+ const demoDir = opts.demoDir ?? join(defaultStateDir(), 'interactive-demos');
407
+ const heartbeatMs = opts.heartbeatMs ?? 15_000;
408
+ const resolveDocsRoot = opts.resolveDocsRoot ?? (() => resolveInteractiveRoot(null));
409
+ const inFlight = new Map(); // runId → live state
410
+ /** Emit onto interactive's vocabulary as the `wi-crew` producer. Never throws into the
411
+ * caller: narration/announce failures are logged — a lost status line must not kill the
412
+ * subscription, and a duplicate demo.requested (WB-002) is the idempotency key WORKING. */
413
+ function emitInteractive(type, payload, idempotencyKey) {
414
+ try {
415
+ bus.emit(db, config, {
416
+ event_type: type,
417
+ // Subdomains per interactive's events.js table: demo.requested rides `demo`,
418
+ // status.posted rides `status`.
419
+ domain: INTERACTIVE_DOMAIN,
420
+ subdomain: type === DEMO_REQUESTED ? 'demo' : 'status',
421
+ payload: { ts: new Date().toISOString(), ...payload },
422
+ producer_id: INTERACTIVE_PRODUCER,
423
+ ...(idempotencyKey !== undefined ? { idempotency_key: idempotencyKey } : {}),
424
+ });
425
+ return true;
426
+ }
427
+ catch (err) {
428
+ const code = err.error;
429
+ if (code === 'WB-002') {
430
+ // Duplicate idempotency key — the emit already happened (redelivery race). Success.
431
+ return true;
432
+ }
433
+ log(`[interactive-demo] emit ${type} failed: ${err instanceof Error ? err.message : String(err)}`);
434
+ return false;
435
+ }
436
+ }
437
+ function narrate(flight, message) {
438
+ flight.narration = message;
439
+ emitInteractive(STATUS_POSTED, {
440
+ document_id: flight.documentId,
441
+ state: 'working',
442
+ message,
443
+ });
444
+ }
445
+ function endFlight(runId) {
446
+ const flight = inFlight.get(runId);
447
+ if (flight) {
448
+ clearInterval(flight.heartbeat);
449
+ inFlight.delete(runId);
450
+ }
451
+ return flight;
452
+ }
453
+ /** Terminal-event fold: turn the governed run's own events into interactive narration, and
454
+ * close the loop (copy spec → emit demo.requested) when the run lands. */
455
+ const offCoreEvents = adapter.onEvent((event) => {
456
+ const runId = typeof event.session === 'string' ? event.session : undefined;
457
+ if (runId === undefined)
458
+ return;
459
+ const flight = inFlight.get(runId);
460
+ if (flight === undefined)
461
+ return;
462
+ // Narration ladder — same rationale as the sibling folds: the heartbeat repeats the LATEST
463
+ // line and the transcript dedups repeats, so advancing the line = visible progress.
464
+ const authoring = flight.leg === 'spec' ? 'the click-path spec' : 'the revised click-path spec';
465
+ // The crew#311 deliverable floor is a DETERMINISTIC tool phase — no seat, no council. Core
466
+ // still emits the seat-selection events for it (its `cli` is the node interpreter's absolute
467
+ // path), so narrating them verbatim put "Council picked /opt/homebrew/.../node to write the
468
+ // click-path spec…" in the reader's thread. Drop those two lines for the floor ord; the
469
+ // `unitDispatched` line that follows immediately says what the phase actually is.
470
+ const isFloorOrd = (e) => typeof e.ord === 'number' &&
471
+ e.ord > flight.agentPhaseCount;
472
+ if (event.type === 'councilConvened') {
473
+ if (isFloorOrd(event))
474
+ return;
475
+ const seats = Array.isArray(event.clis) ? event.clis.length : 0;
476
+ // "0-seat council" reads like a bug — generic phrasing whenever clis is missing or empty
477
+ // (Copilot, #269).
478
+ const council = seats > 0 ? `a ${seats}-seat council` : 'a council';
479
+ narrate(flight, `Convening ${council} to pick who writes ${authoring}…`);
480
+ return;
481
+ }
482
+ if (event.type === 'unitDistributed') {
483
+ if (isFloorOrd(event))
484
+ return;
485
+ const who = typeof event.cli === 'string' ? event.cli : 'a worker';
486
+ const pct = typeof event.agreement_pct === 'number' ? ` (${event.agreement_pct}% agreement)` : '';
487
+ narrate(flight, `Council picked ${who} to write ${authoring}${pct}…`);
488
+ return;
489
+ }
490
+ if (event.type === 'unitDispatched') {
491
+ // The first-spec leg plans (tool-free) then inspects-and-writes; the re-author leg is one
492
+ // writing phase; both then run the crew#311 deliverable floor. Narrate whichever the ord
493
+ // actually is rather than assuming a count.
494
+ const ord = typeof event.ord === 'number' ? event.ord : 0;
495
+ const runPhaseCount = flight.agentPhaseCount + 1;
496
+ const line = ord > flight.agentPhaseCount
497
+ ? 'checking the spec file was actually written…'
498
+ : ord >= flight.agentPhaseCount
499
+ ? `inspecting the app and writing ${authoring}…`
500
+ : 'planning the demo scenes…';
501
+ narrate(flight, `Crew phase ${ord}/${runPhaseCount}: ${line}`);
502
+ return;
503
+ }
504
+ if (event.type === 'toolInvoked') {
505
+ const tools = Array.isArray(event.tools) ? [...new Set(event.tools)].join(', ') : '';
506
+ if (tools)
507
+ narrate(flight, `Worker is using ${tools} on your demo…`);
508
+ return;
509
+ }
510
+ if (event.type === 'unitOutputCaptured') {
511
+ narrate(flight, `Spec work finished — the governance gate is reviewing it…`);
512
+ return;
513
+ }
514
+ if (event.type === 'gateDecided' && event.allow === true) {
515
+ const ord = typeof event.ord === 'number' ? event.ord : 0;
516
+ narrate(flight, ord > flight.agentPhaseCount
517
+ ? 'Spec file verified on disk — installing it and starting the recording…'
518
+ : 'Gate approved the spec — checking the file landed…');
519
+ return;
520
+ }
521
+ if (event.type === 'acpFallback') {
522
+ const who = typeof event.cliKey === 'string' ? event.cliKey : 'the worker';
523
+ narrate(flight, `${who}'s live session dropped — continuing in single-shot mode…`);
524
+ return;
525
+ }
526
+ if (event.type === 'sessionCompleted') {
527
+ endFlight(runId);
528
+ finalize(flight, runId);
529
+ return;
530
+ }
531
+ if (event.type === 'stepFailed') {
532
+ // Remember the engine's own reason (crew#311) so the terminal status can name it.
533
+ const detail = typeof event.detail === 'string' ? event.detail.trim() : '';
534
+ if (detail.length > 0)
535
+ flight.failureDetail = detail;
536
+ return;
537
+ }
538
+ if (event.type === 'sessionFailed' || event.type === 'runCancelled') {
539
+ endFlight(runId);
540
+ ledger.recordFailure(flight.key);
541
+ const why = flight.failureDetail !== undefined ? ` Reason: ${oneLine(flight.failureDetail, 600)}` : '';
542
+ emitInteractive(STATUS_POSTED, {
543
+ document_id: flight.documentId,
544
+ state: 'error',
545
+ message: `The crew run authoring this demo's spec ${event.type === 'runCancelled' ? 'was cancelled' : 'failed'} ` +
546
+ `(run ${runId}).${why} Inspect it via the crew API (GET /api/v1/runs/${runId}); no recording was triggered.`,
547
+ });
548
+ log(`[interactive-demo] run ${runId} for ${flight.key} ended: ${event.type}`);
549
+ }
550
+ });
551
+ /**
552
+ * The run is terminal: verify the spec (exists, non-empty, passes the static module
553
+ * self-check), COPY it into the doc workspace, THEN emit demo.requested — strictly in that
554
+ * order (the service reads `<docDir>/demo.spec.mjs` when the request arrives; an emit-first
555
+ * ordering would record a missing or stale spec). Every failure is honest: error status,
556
+ * failure row, and NO demo.requested — the doc keeps its current state instead of the
557
+ * service throwing "no demo.spec.mjs authored yet" at a request crew knew was hollow.
558
+ */
559
+ function finalize(flight, runId) {
560
+ const { documentId, outPath, key } = flight;
561
+ const fail = (message) => {
562
+ ledger.recordFailure(key);
563
+ emitInteractive(STATUS_POSTED, { document_id: documentId, state: 'error', message });
564
+ log(`[interactive-demo] run ${runId} for ${key} failed at finalize: ${message}`);
565
+ };
566
+ let spec = '';
567
+ try {
568
+ spec = readFileSync(outPath, 'utf8');
569
+ }
570
+ catch {
571
+ /* missing file → the empty-spec failure below */
572
+ }
573
+ if (spec.trim().length === 0) {
574
+ fail(`The crew run completed but produced no demo spec at ${outPath} (run ${runId}); no recording was triggered.`);
575
+ return;
576
+ }
577
+ const violation = specSelfCheck(spec);
578
+ if (violation !== null) {
579
+ // The pre-install self-check (recordDemo would reject the module at import time —
580
+ // interactive demo.js: `demo.spec.mjs must export an async run(...)`). Fail loud crew-side
581
+ // instead of letting the service fail the recording after crew claimed success.
582
+ fail(`The crew's demo spec failed its pre-install self-check and was NOT installed — ${violation} ` +
583
+ `(run ${runId}). The document is unchanged; resubmit the request.`);
584
+ return;
585
+ }
586
+ // Crew (not the worker) installs the spec: the worker's boundary cannot touch the doc
587
+ // workspace (wicked-core#293/#294) — crew's own process can. The docs root resolves the
588
+ // same way the chat seam and the interactive proxy resolve it (per-project setting via the
589
+ // server wiring; shared default otherwise), so the copy lands where the service will look.
590
+ const docsRoot = resolveDocsRoot(flight.projectId);
591
+ const docDir = join(docsRoot, documentId);
592
+ if (!existsSync(join(docDir, 'versions.json'))) {
593
+ fail(`Crew authored the demo spec (at ${outPath}) but found no doc workspace at ${docDir} — ` +
594
+ `the spec was NOT installed and no recording was triggered. Check the interactive docs root, then replay the request.`);
595
+ return;
596
+ }
597
+ try {
598
+ copyFileSync(outPath, join(docDir, DEMO_SPEC_FILE));
599
+ }
600
+ catch (err) {
601
+ 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.`);
602
+ return;
603
+ }
604
+ // Spec installed — NOW ask the (model-free) service to record it. The deterministic key
605
+ // makes a re-announce a WB-002 no-op; distinct keys per authoring generation keep a
606
+ // legitimate re-record from deduping against the first one.
607
+ const idemKey = flight.leg === 'spec'
608
+ ? demoIdempotencyKey(documentId)
609
+ : demoReauthorIdempotencyKey(documentId, flight.version ?? 0);
610
+ const emitted = emitInteractive(DEMO_REQUESTED, { document_id: documentId }, idemKey);
611
+ if (!emitted) {
612
+ // The bus refused the announce (non-WB-002): the spec IS installed but the recording was
613
+ // never requested. Fail HONEST — and say exactly where things stand, because unlike the
614
+ // sibling seams the doc is half-advanced (spec on disk, no video).
615
+ ledger.recordFailure(key);
616
+ emitInteractive(STATUS_POSTED, {
617
+ document_id: documentId,
618
+ state: 'error',
619
+ message: `Crew installed the demo spec at ${join(docDir, DEMO_SPEC_FILE)} but could not request ` +
620
+ `the recording on the bus (run ${runId}). Inspect the crew daemon log, then emit ` +
621
+ `wicked.interactive.demo.requested for this document to record it.`,
622
+ });
623
+ log(`[interactive-demo] demo.requested emit FAILED for ${key} (run ${runId}) — recorded as failure`);
624
+ return;
625
+ }
626
+ ledger.recordEmitted(key);
627
+ emitInteractive(STATUS_POSTED, {
628
+ document_id: documentId,
629
+ state: 'complete',
630
+ message: flight.leg === 'spec'
631
+ ? 'Demo spec authored — recording now. Step-by-step progress will follow; highlight any step to refine it.'
632
+ : 'Demo spec re-authored per your feedback — recording the new take now.',
633
+ });
634
+ log(`[interactive-demo] spec installed + demo.requested emitted for ${key} (run ${runId})`);
635
+ }
636
+ /** `true` when any run (either leg) is in flight for the doc — one demo doc gets one
637
+ * authoring run at a time, so a re-author can never race the spec it revises. */
638
+ function docBusy(documentId) {
639
+ for (const f of inFlight.values())
640
+ if (f.documentId === documentId)
641
+ return true;
642
+ return false;
643
+ }
644
+ async function launchFlight(input) {
645
+ const runId = randomUUID();
646
+ // Resolved BEFORE the launch and never indexing — a refresh is `wicked-estate index` per
647
+ // member at up to 600s EACH, so doing it here would turn "record a demo" into an
648
+ // unannounced multi-repo job. Missing or stale degrades to no binding; the run is unaffected.
649
+ //
650
+ // The decision is RECORDED on both outcomes, like the API launch path (`api/routes.ts`):
651
+ // "this demo sees the project" and "this demo sees nothing, because X" are equally facts about
652
+ // what the run could observe. An unexpected failure degrades the same way but says so — a
653
+ // silent `catch(() => null)` would make a broken binding look identical to a project that
654
+ // simply has no graph yet.
655
+ let projectGraphBinding = null;
656
+ if (input.projectId !== undefined) {
657
+ const decision = await resolveProjectGraphBinding(adapter, input.projectId, undefined).catch((err) => ({
658
+ binding: null,
659
+ reason: `the project graph binding could not be resolved ` +
660
+ `(${err instanceof Error ? err.message : String(err)}). ` +
661
+ `This repo-less run gets no code graph.`,
662
+ }));
663
+ projectGraphBinding = decision.binding;
664
+ log(`run ${runId}: ${decision.reason}`);
665
+ }
666
+ return adapter
667
+ .launchRun({
668
+ problem: input.problem,
669
+ sessionId: runId,
670
+ clisJson: opts.clisJson ?? JSON.stringify(rosterOf(adapter)),
671
+ workflow: input.workflow,
672
+ // A project-bound doc's governed run is FILED (the engine attaches the crew.run
673
+ // membership atomically with the launch); an unfiled doc launches with the key OMITTED
674
+ // — never a fabricated 'default' membership (CREW-UX-2).
675
+ ...(input.projectId !== undefined ? { projectId: input.projectId } : {}),
676
+ // A project-filed run sees the PROJECT's graph, like any other (verification
677
+ // found this seam launching filed but unbound). These launches are repo-LESS,
678
+ // which is exactly the case that gets a graph where it previously got none.
679
+ ...(projectGraphBinding !== null ? { projectGraph: projectGraphBinding } : {}),
680
+ // THE WRITE-BOUNDARY LESSON, applied (wicked-core#293/#294): deliberately NO `repoRef`
681
+ // and NO doc-workspace path — the run is UNBOUND and its worker can only read/write
682
+ // the per-run inbox declared here (write roots are readable, wicked-core#259). The
683
+ // task names `<inbox>/demo.spec.mjs` as the deliverable; crew copies it into the doc
684
+ // workspace at finalize. Per-run isolation (Copilot, crew#313): ONLY this run's own
685
+ // subdirectory, never the shared demoDir.
686
+ extraWriteRoots: [input.runDir],
687
+ // THE DELIVERABLE FLOOR (crew#311): the spec file IS the deliverable — and this leg is
688
+ // the one that PROVED prose can stand in for it (the module doc above records a run
689
+ // that "completed" with no deliverable at 161-345 bytes and was caught only by
690
+ // finalize, after the gate had passed it). The floor fails the RUN instead.
691
+ requireDeliverables: [input.outPath],
692
+ })
693
+ .then(() => {
694
+ // Record AFTER the launch resolved: a failed launch leaves no ledger row, so a replayed
695
+ // delivery retries. The crash window between launch and this write is the reason the
696
+ // demo.requested emit ALSO carries a deterministic idempotency key.
697
+ ledger.recordLaunch(input.key, runId);
698
+ if (input.projectId !== undefined)
699
+ opts.onRunFiled?.(runId, input.projectId);
700
+ const flight = {
701
+ key: input.key,
702
+ leg: input.leg,
703
+ documentId: input.documentId,
704
+ projectId: input.projectId,
705
+ version: input.version,
706
+ outPath: input.outPath,
707
+ agentPhaseCount: input.agentPhaseCount,
708
+ narration: input.leg === 'spec'
709
+ ? 'Crew run launched — authoring your demo…'
710
+ : 'Crew run launched — re-authoring your demo…',
711
+ heartbeat: setInterval(() => {
712
+ // Repeat the last real narration so the ~20s status.requested window is always
713
+ // fed, even mid-phase when the engine is quiet.
714
+ emitInteractive(STATUS_POSTED, {
715
+ document_id: flight.documentId,
716
+ state: 'working',
717
+ message: flight.narration,
718
+ });
719
+ }, heartbeatMs),
720
+ };
721
+ // Do not keep the daemon alive for narration alone.
722
+ flight.heartbeat.unref?.();
723
+ inFlight.set(runId, flight);
724
+ log(`[interactive-demo] ${input.key} → governed run ${runId} (${input.leg}, spec → ${input.outPath})`);
725
+ })
726
+ .catch((err) => {
727
+ // The 'processing' status is already on the thread — close it out honestly so the
728
+ // canvas never sits in an in-between state on a launch that went nowhere.
729
+ const reason = err instanceof Error ? err.message : String(err);
730
+ emitInteractive(STATUS_POSTED, {
731
+ document_id: input.documentId,
732
+ state: 'error',
733
+ message: `Crew could not start a run for this demo: ${reason}.`,
734
+ });
735
+ // DELIBERATELY no ledger write: only an answered trigger earns a row, so an operator
736
+ // can replay the dead-lettered frame after fixing the daemon and get a real retry.
737
+ // Re-throw so the bus (maxRetries 0) dead-letters the frame — visible, replayable,
738
+ // and incapable of hot-looping.
739
+ throw err instanceof Error ? err : new Error(reason);
740
+ });
741
+ }
742
+ async function handleDocCreated(event) {
743
+ const doc = parseDemoDocCreated(event.event_type, event.payload);
744
+ if (doc === null)
745
+ return;
746
+ // Replay-dedup: the ledger is the durable gate (redelivery after crash/restart), the
747
+ // in-flight scan the live one. Keyed by DOCUMENT — one first spec per document lifetime
748
+ // (refinement is the feedback leg's business).
749
+ if (ledger.has(doc.documentId)) {
750
+ log(`[interactive-demo] doc ${doc.documentId} already answered (run ${ledger.get(doc.documentId)?.runId}) — replay ignored`);
751
+ return;
752
+ }
753
+ if (docBusy(doc.documentId))
754
+ return;
755
+ const runDir = join(demoDir, doc.documentId);
756
+ const outPath = join(runDir, DEMO_SPEC_FILE);
757
+ mkdirSync(runDir, { recursive: true });
758
+ emitInteractive(STATUS_POSTED, {
759
+ document_id: doc.documentId,
760
+ state: 'processing',
761
+ message: 'A governed crew picked up your demo brief — planning the scenes and authoring the click-path…',
762
+ });
763
+ await launchFlight({
764
+ key: doc.documentId,
765
+ leg: 'spec',
766
+ documentId: doc.documentId,
767
+ projectId: doc.projectId,
768
+ version: undefined,
769
+ problem: demoProblem(doc, outPath),
770
+ workflow: INTERACTIVE_DEMO_WORKFLOW,
771
+ runDir,
772
+ outPath,
773
+ agentPhaseCount: INTERACTIVE_DEMO_WORKFLOW_DEF.phases.length,
774
+ });
775
+ }
776
+ async function handleFeedbackProcessed(event) {
777
+ const handoff = parseStructuralFeedback(event.event_type, event.payload);
778
+ if (handoff === null)
779
+ return;
780
+ // THE KIND GATE (the disk truth, shared with the edit seam): this leg answers ONLY docs
781
+ // whose manifest says `kind: "demo"` — interactive records the kind for demo docs
782
+ // (initWorkspace(dir, html, {kind:'demo'}), server.js). Everything else — including a doc
783
+ // whose manifest this daemon cannot read — is the edit seam's business, and the edit seam
784
+ // applies the same gate inverted, so exactly one seam answers any given handoff.
785
+ const docsRoot = resolveDocsRoot(handoff.projectId);
786
+ const head = readDocHead(docsRoot, handoff.documentId);
787
+ if (head === null || head.kind !== 'demo')
788
+ return;
789
+ const key = handoffKey(handoff.documentId, handoff.version);
790
+ if (ledger.has(key)) {
791
+ log(`[interactive-demo] handoff ${key} already answered (run ${ledger.get(key)?.runId}) — replay ignored`);
792
+ return;
793
+ }
794
+ for (const f of inFlight.values()) {
795
+ if (f.key === key)
796
+ return;
797
+ }
798
+ if (docBusy(handoff.documentId)) {
799
+ // Another authoring run (usually the FIRST spec) is still in flight for this doc — a
800
+ // re-author launched now would revise a spec that is still being written. Dead-letter
801
+ // the frame (throw → maxRetries 0 → DLQ) so it is visible and replayable once the
802
+ // in-flight run lands, and say so on the thread instead of eating the user's feedback.
803
+ const message = `Crew is still authoring this demo's spec — your step feedback was set aside; ` +
804
+ `replay it (or resubmit) once the current run lands.`;
805
+ emitInteractive(STATUS_POSTED, { document_id: handoff.documentId, state: 'error', message });
806
+ log(`[interactive-demo] handoff ${key} arrived while doc ${handoff.documentId} is busy — dead-lettered`);
807
+ throw new Error(message);
808
+ }
809
+ // The current spec is the re-author's ground truth: copy it INTO the inbox crew-side so
810
+ // the unbound worker can read it (write roots are readable, wicked-core#259 — the doc
811
+ // workspace itself is boundary-denied, wicked-core#294). No spec on disk = nothing to
812
+ // re-author (a demo doc that never had its first spec authored): dead-letter, replayable
813
+ // after the first authoring lands.
814
+ const srcSpec = join(docsRoot, handoff.documentId, DEMO_SPEC_FILE);
815
+ if (!existsSync(srcSpec)) {
816
+ const message = `This demo has no ${DEMO_SPEC_FILE} to revise yet — the step feedback was set aside; ` +
817
+ `replay it once the first spec is authored.`;
818
+ emitInteractive(STATUS_POSTED, { document_id: handoff.documentId, state: 'error', message });
819
+ log(`[interactive-demo] handoff ${key}: no spec at ${srcSpec} — dead-lettered`);
820
+ throw new Error(message);
821
+ }
822
+ const runDir = join(demoDir, key.replace(':', '-'));
823
+ const outPath = join(runDir, DEMO_SPEC_FILE);
824
+ mkdirSync(runDir, { recursive: true });
825
+ const currentSpecPath = join(runDir, 'current.spec.mjs');
826
+ copyFileSync(srcSpec, currentSpecPath);
827
+ // Handoff by file (the edit leg's discipline): fragments can be arbitrarily large and the
828
+ // PTY prompt is single-line, so the items ride a JSON file in the inbox, not the problem.
829
+ const feedbackPath = join(runDir, 'feedback.json');
830
+ writeFileSync(feedbackPath, JSON.stringify({ document_id: handoff.documentId, version: handoff.version, items: handoff.items }, null, 2), 'utf8');
831
+ emitInteractive(STATUS_POSTED, {
832
+ document_id: handoff.documentId,
833
+ state: 'processing',
834
+ message: `A governed crew picked up your demo feedback — re-authoring the click-path (${handoff.items.length} change${handoff.items.length === 1 ? '' : 's'})…`,
835
+ });
836
+ await launchFlight({
837
+ key,
838
+ leg: 'reauthor',
839
+ documentId: handoff.documentId,
840
+ projectId: handoff.projectId,
841
+ version: handoff.version,
842
+ problem: demoReauthorProblem(handoff, currentSpecPath, feedbackPath, outPath),
843
+ workflow: INTERACTIVE_DEMO_REAUTHOR_WORKFLOW,
844
+ runDir,
845
+ outPath,
846
+ agentPhaseCount: INTERACTIVE_DEMO_REAUTHOR_WORKFLOW_DEF.phases.length,
847
+ });
848
+ }
849
+ const subCreated = bus.subscribe({
850
+ db,
851
+ plugin: INTERACTIVE_DEMO_BUS_PLUGIN,
852
+ filter: INTERACTIVE_DEMO_BUS_FILTER,
853
+ // Live triggers only: replaying a bus backlog would answer docs whose demos the assist
854
+ // loop long since produced. History reconciliation belongs to the state plane, not here.
855
+ cursor_init: 'latest',
856
+ pollIntervalMs: opts.pollIntervalMs ?? 2000,
857
+ // Our own ledger + idempotency key are the dedupe; a bus-level retry of a failed launch
858
+ // would double-launch precisely because the ledger row is only written on success.
859
+ maxRetries: 0,
860
+ handler: (event) => handleDocCreated(event),
861
+ onError: (err, event) => {
862
+ log(`[interactive-demo] handler error on event ${String(event?.event_id ?? '?')}: ${err.message}`);
863
+ },
864
+ });
865
+ const subFeedback = bus.subscribe({
866
+ db,
867
+ plugin: INTERACTIVE_DEMO_FEEDBACK_BUS_PLUGIN,
868
+ filter: INTERACTIVE_DEMO_FEEDBACK_BUS_FILTER,
869
+ cursor_init: 'latest',
870
+ pollIntervalMs: opts.pollIntervalMs ?? 2000,
871
+ maxRetries: 0,
872
+ handler: (event) => handleFeedbackProcessed(event),
873
+ onError: (err, event) => {
874
+ log(`[interactive-demo] feedback handler error on event ${String(event?.event_id ?? '?')}: ${err.message}`);
875
+ },
876
+ });
877
+ return {
878
+ ledger,
879
+ inFlightDocs: () => [...new Set([...inFlight.values()].map((f) => f.documentId))],
880
+ stop: async () => {
881
+ offCoreEvents();
882
+ for (const runId of [...inFlight.keys()])
883
+ endFlight(runId);
884
+ await subCreated.stop();
885
+ await subFeedback.stop();
886
+ },
887
+ };
888
+ }
889
+ //# sourceMappingURL=demo-events.js.map