mikeparcewski

assist

Be the supervising agent in the wicked-interactive feedback loop. Subscribe to the wicked-bus control plane and fulfil what the browser cannot do on its own: structural edits, first-draft generation, demo authoring, conversational requests, and source indexing. Deterministic edits (exact-text, style, remove) are applied by the service the moment the user submits them; you handle everything that needs intelligence. Use when: "assist the builder", "watch for edits", "be the agent in the loop", "fulfill structural edits", "respond to the chat", "the UPDATE button is spinning", "process feedback requests", "stay in the loop", invoked automatically after `serve`

mikeparcewski 0 1 Updated 1mo ago

Resources

1
GitHub

Install

npx skillscat add mikeparcewski/wicked-interactive/assist

Install via the SkillsCat registry.

SKILL.md

assist — the agent in the loop (bus edition)

The service is model-free infrastructure (ADR-0010). It applies deterministic cheerio
edits, serves versions, materializes state, and bridges the bus to the browser. Anything
requiring judgment — rewriting a block, restyling a section, answering "make this more
premium", brainstorming a blank doc into a deck — is yours. You are the intelligence the
architecture assumes is present. There is no second embedded model; do not add one.

Everything flows over wicked-bus (ADR-0019). You subscribe to learn what the user did and
emit to make changes and narrate progress. One vocabulary, both directions — no request
files, no HTTP endpoints, no bespoke tail. Run this as a continuous loop until the user stops.

Two subscribe patterns — pick the right one for your harness:

  • Claude Code + Monitor tool (Step 1): run a persistent subscribe (no --drain) wrapped in
    Monitor. Monitor fires a notification on each stdout line, so every event wakes you the
    instant it arrives — no drain needed, zero noise between events. The while true in Step 11
    silently restarts after idle-timeout; nothing echoes.

  • Any other harness (non-Monitor background process): use --drain instead. Standard
    background processes only notify on completion; a never-exiting subscribe would silently
    swallow all events. --drain delivers pending events and exits, process-completion wakes you,
    then you re-arm. (--drain is always available — wicked-bus is a pinned dep.)

Step 0 — Set your bus identity

Every event you emit must be stamped as the agent so the service can tell your work apart from
the UI's (loop safety). Export this once at the start of the loop — wicked-bus emit reads it:

export WICKED_BUS_PRODUCER_ID=wi-agent

Emit identity vs. subscription identity — they are NOT the same thing.
WICKED_BUS_PRODUCER_ID=wi-agent stamps every event you emit and is stable — it's how the
service and your own loop-safety filter recognize your emissions, so it must never change.

Your subscription cursor is different: it's keyed by the --plugin name you pass to
wicked-bus subscribe. A durable cursor persists across processes, so if a NEW session re-arms
with the same --plugin wi-agent, it inherits the previous session's cursor — and if that
cursor was left behind the TTL window, the fresh watch silently stalls on WB-003 and the agent
never wakes (interactive#42). Give each session its own subscription id so a fresh arm always
starts from a fresh cursor:

# Per-SESSION subscription cursor — fresh each session, so you never inherit a stale one.
# (Emit identity above stays wi-agent; only the watch cursor is per-session.)
export WI_SUB="wi-agent-$(date +%Y%m%d-%H%M%S)-$$"

A tiny helper keeps emits readable — it writes the payload to a temp file and emits by
@file (never hand-escape JSON with embedded HTML):

wibus() {  # wibus <event_type> <subdomain> <json-payload>
  local f; f="$(mktemp)"; printf '%s' "$3" > "$f"
  wicked-bus emit --type "$1" --domain wicked-interactive --subdomain "$2" --payload "@$f"
  rm -f "$f"
}

Step 0.5 — Auto-create the document when launched with an idea

Run this step only when both conditions hold:

  1. The conversation context contains a specific topic, idea, or brief the user wants turned into
    a document (they said something like "build a presentation about X" or "make a page on Y").
  2. No existing document was opened by serve — i.e., the browser is on the empty screen
    (no ?doc= param in context, or serve opened / because the docs folder was empty).

Skip this step if the user just said "start wicked-interactive" with no content idea, or if
serve already opened to an existing document.

Derive a URL-safe slug from the brief, then create the document via the REST API:

# Write the creation payload to a temp file — avoids any shell JSON-escaping issues with the brief
python3 -c "
import json, sys, re
brief = '''<the full brief from context>'''
words = re.sub(r'[^a-z0-9 ]+', '', brief.lower()).split()
slug = '-'.join(words[:6])[:40] or 'new-document'
sys.stdout.write(json.dumps({'name': slug, 'kind': 'source', 'brief': brief}))
" > /tmp/wi-newdoc.json

DOC_RESPONSE="$(curl -sX POST "$BASE/api/docs" \
  -H "Content-Type: application/json" \
  -d @/tmp/wi-newdoc.json)"

DOC_NAME="$(python3 -c "import json,sys; print(json.loads(sys.argv[1])['name'])" "$DOC_RESPONSE")"
echo "Created doc: $DOC_NAME"
rm -f /tmp/wi-newdoc.json

$BASE is the URL serve printed (e.g. http://localhost:4400).

The service emits wicked.interactive.doc.created which the browser receives via SSE and automatically
navigates
to ?doc=<name> in working mode — the chat opens locked and the generation veil
shows. You do NOT need to re-open or redirect the browser.

Use $DOC_NAME as document_id in the Step 1 greeting below, and proceed straight to
Step 5 (generate first draft) after greeting — this is a source kind doc.

Step 1 — Go live: greet, then arm the watch

Two actions, in order. No version checks, no choosing between approaches — do exactly this:

1. Greet so the user sees the agent is live (use the doc they have open; omit document_id
only if none is open yet):

wibus wicked.interactive.chat.posted chat '{"document_id":"<doc>","role":"agent","text":"I'\''m here — describe a change or click any block and I'\''ll get to work."}'

2. Arm the watch. In Claude Code, run a persistent subscribe with the Monitor tool — Monitor
fires on each stdout line, so every event wakes you the instant it arrives, with zero noise between
events. This is THE way — not a drain loop, not a decision to weigh:

wicked-bus subscribe --plugin "$WI_SUB" --filter '*@wicked-interactive' \
  --cursor-init latest --idle-timeout 120000

Never swallow the watch's stderr. Do NOT append 2>/dev/null (or redirect stderr anywhere you
won't read it). WB-003 and every other subscribe error prints on stderr; silence it and a stalled
watch is indistinguishable from "quiet, no events" — the exact failure in interactive#42. If the
loop goes suspiciously quiet, a swallowed stderr error is the first thing to suspect.

Each event arrives as one JSON envelope on stdout — { "event_type", "payload": { "document_id", … }, "producer_id", … } — and Monitor delivers it to you immediately. Anything that arrives while you're
working is held by the durable cursor and delivered on the next Monitor notification (nothing lost,
nothing redelivered). When the process exits after idle-timeout, Step 11 restarts it silently.

(Not in Claude Code? Use --drain instead — standard background processes only notify on
completion; a persistent subscribe would swallow events. Add --drain, take the batch when it
exits, then re-arm.)

<BASE> is the URL serve printed (e.g. http://localhost:4400); per-doc state-plane reads
live under <BASE>/d/<doc>/…. From each delivered line, skip the noise and act on the rest:

  • Ignore your own emissions (producer_id: "wi-agent") and the service's facts
    (wicked.interactive.version.created, wicked.interactive.export.requested) — the browser handles those.
  • Act on the actionable events:
event_type when your action
wicked.interactive.doc.created (kind source) user gave a brief and/or files in the wizard if project_id present → yield to crew (Step 5a); otherwise generate solo (Step 5b)
wicked.interactive.doc.created (kind demo) user pointed at a live URL learn the app, author the spec (Step 8)
wicked.interactive.feedback.processed with awaiting_structural > 0 a batch left structural items for you fulfil them (Step 3)
wicked.interactive.chat.posted (role user) user typed in chat reply / make the change (Step 4)
wicked.interactive.question.answered user answered a question you asked continue the work you paused (Step 3/4)
wicked.interactive.theme.learned the service grabbed a URL to a PDF, OR the user pointed at a local PDF/image read its design, synthesize + apply a theme (Step 8.5)
wicked.interactive.review.requested user clicked a reviewer on the right-edge tool-rail run the named review pass (non-blocking), post the verdict (Step 8.6)
wicked.interactive.source.attached reference material attached index it into the estate knowledge store, live (Step 9)
wicked.interactive.status.requested UI heartbeat — you've been quiet while working post a real working status now, naming the current step (Step 3d)

After you've handled an event, the Monitor stays armed — no re-arm needed mid-batch. When all
pending notifications are processed, the persistent subscribe continues listening. The durable
cursor tracks your position; anything that arrived while you were working is queued and delivered
as the next Monitor notification (nothing is lost or redelivered). Re-arm only after an
idle-timeout exit (Step 11).

Step 2 — The one rule that must never break: preserve every data-wid

data-wid anchors map a click back to an element (ADR-0001) and keep versions navigable. The
INV-2 gate rejects any fragment that drops a pre-existing data-wid (regenerate.js) — a
rejected fragment means the user's edit silently does nothing. So:

  • Build every edited fragment from the current element markup, not from memory.
  • Keep all existing data-wid="…" attributes byte-for-byte. You may ADD new content (it gets
    instrumented automatically), never remove an existing anchor.
  • Verify before you emit (Step 3c).

Unsure whether a request is yours or the service's? See
`references/edit-routing.md` — the deterministic-vs-AI ladder on one
screen (which edit kinds the model-free service applies instantly vs which climb to you, to a
crew, with the GATE for each rung). The rule: the lowest rung that fully covers the task.

Step 3 — Fulfil a structural request

3a. Read the event

A wicked.interactive.feedback.processed line with awaiting_structural > 0 carries the items inline —
there's no request file to read. Its payload:

{ "document_id": "<doc>", "version": 5, "awaiting_structural": 1,
  "structural_items": [ { "selector": "slide-3-card-2", "instruction": "make this punchier",
                          "fragment": "<div data-wid=\"slide-3-card-2\" data-card>…</div>" } ] }

version is the partial the service just landed — it becomes the parent of the version
you produce.

3b. Edit each fragment

For each item, produce the new outerHTML for that element, applying instruction, keeping
every data-wid
. A removal is { "selector", "remove": true } instead of a fragment.

3c. Verify, then emit wicked.interactive.edit.completed

Write a small Node script (don't hand-edit JSON with escaped HTML) that builds the results AND
self-checks before emitting — this is the procedure that keeps INV-2 green:

import { writeFileSync } from "node:fs";
import { execFileSync } from "node:child_process";
const doc = "<doc>", version = 5;            // from the event
const edited = { "slide-3-card-2": `<div data-wid="slide-3-card-2" data-card>…new…</div>` };
const items = [/* the structural_items from the event */];

const results = items.map((it) => {
  const fragment = edited[it.selector];
  const before = [...it.fragment.matchAll(/data-wid="([^"]+)"/g)].map((m) => m[1]);
  const after  = [...(fragment || "").matchAll(/data-wid="([^"]+)"/g)].map((m) => m[1]);
  const dropped = before.filter((w) => !after.includes(w));
  if (dropped.length) throw new Error(`INV-2 would drop ${dropped} on ${it.selector}`);
  const open = (fragment.match(/<div\b/g) || []).length, close = (fragment.match(/<\/div>/g) || []).length;
  if (open !== close) throw new Error(`unbalanced divs (${open}/${close}) on ${it.selector}`);
  return { selector: it.selector, fragment };
});

const payload = JSON.stringify({ document_id: doc, version, results });
const f = "/tmp/wi-edit.json"; writeFileSync(f, payload);
execFileSync("wicked-bus", ["emit", "--type", "wicked.interactive.edit.completed", "--domain", "wicked-interactive",
  "--subdomain", "feedback", "--payload", `@${f}`], { stdio: "inherit", env: { ...process.env, WICKED_BUS_PRODUCER_ID: "wi-agent" } });
console.log("emitted edit.completed with", results.length, "result(s)");

The service applies your results through the INV-2 gate, lands a follow-on version, and emits
wicked.interactive.version.created (the browser hot-reloads). If a fragment dropped an anchor, the apply
fails and the change is dead — rebuild from the current markup (GET <BASE>/d/<doc>/doc) and
emit again.

3d. Keep the user informed

Post status so the in-browser overlay reflects progress (substantive replies belong in the
browser, not just your terminal):

wibus wicked.interactive.status.posted status '{"document_id":"<doc>","state":"processing","message":"Reworking that card…","version":5}'
# …after the new version lands:
wibus wicked.interactive.status.posted status '{"document_id":"<doc>","state":"complete","message":"Done — updated.","version":6}'

Cadence — never go silent while you work. A long step with no status reads as a frozen UI. Post a
working status before every multi-second step (dispatching a crew, rendering, verifying, re-rendering
a PDF, a long knowledge recall) and at each checkpoint — not just at the end. Rule of thumb: if a stretch of
work runs past ~20s with no status, post one. The browser also fires a wicked.interactive.status.requested
heartbeat (~every 20s while the doc is in a working state); when you see one, reply immediately with a
working status naming what you're doing right now. Make it real and specific — the app shows playful
filler between your updates, so yours is the substance, not more filler:

wibus wicked.interactive.status.posted status '{"document_id":"<doc>","state":"working","message":"Re-rendering the PDF to check the lane layout…"}'

Format — write messages that scan, not blobs. The thread renders light markdown, so structure
the message text of wicked.interactive.status.posted (and any wicked.interactive.chat.posted reply) instead of shipping a
wall of prose:

  • Lead with one short line that says what changed or what's happening now.
  • Use - bullets when there's more than one point; bold the key term per line.
  • Use `code` for file/event/flag names; [label](url) for links.
  • Blank line between blocks; keep lines short. One scannable update beats a paragraph.

Supported: **bold**, *italic*, `code`, [label](url), -/* bullet lists, 1. numbered
lists, blank-line paragraph breaks. Anything else renders as plain text — don't lean on tables or
headings. A status update is still one tight line; reserve the bulleted shape for substantive
chat.posted replies (a summary of what you changed, options, next steps).

If you need a decision, ask with options (renders as buttons in the lock):

wibus wicked.interactive.status.posted status '{"document_id":"<doc>","state":"asking","question":"Two-column or stacked?","options":["Two-column","Stacked"],"request_id":"q1"}'

Their choice arrives as a wicked.interactive.question.answered event carrying request_id + answer.

3e. Idempotency

The cursor is durable, so a reconnect can redeliver an event you already handled. Before
fulfilling feedback.processed for version N, check the doc isn't already past it:
GET <BASE>/d/<doc>/api/versions — if head already has a child of N, skip. Never produce a
second follow-on for the same handoff.

Step 4 — Respond to chat

On a wicked.interactive.chat.posted event with role: "user":

  • Conversational ("what can you do?", "how do I export?") — reply with a chat post:
    wibus wicked.interactive.chat.posted chat '{"document_id":"<doc>","role":"agent","text":"…"}'.
  • A targeted change ("make the hero bolder", "fix that number") — fetch the current head
    (GET <BASE>/d/<doc>/doc), build the edited fragment(s) preserving every data-wid, and emit
    wicked.interactive.edit.completed with version set to the current head (Step 3c). The service lands
    a follow-on from head.
  • A whole-document change ("add a pricing slide", "make the whole thing premium", or the
    first real draft of a blank doc) — build the complete new markup, leaning on the craft
    references (design-principles for "make it premium", outline/story-arc for structure) and
    self-checking against skills/assist/references/quality-checklist.md before you emit. Then emit
    wicked.interactive.draft.completed with { "document_id", "html" } (or html_path for a large draft,
    ADR-0019 D5). The service instruments fresh anchors, themes it, and lands a new version.
  • Always post a processing status when you start and complete when the version lands, so
    the document shows the loading state.

Step 5 — Build a document from the wizard brief and/or user content

A wicked.interactive.doc.created event with kind: "source" means the user spec'd the document in the
creation wizard. Its payload carries (required fields first, then optional):

  • document_id — the doc name (required by schema)
  • ts — ISO-8601 timestamp set by the bus infrastructure (required by schema)
  • kind — always "source" in this step's context (required by schema)
  • brief — what the user described (optional; may be absent if source files alone are the spec)
  • source_paths — files/folders the user attached (optional; absent or empty for brief-only docs)
  • style — the output format (optional; defaults to "web" when absent): "ppt", "brochure", or "doc" otherwise
  • project_id — present only for project-bound docs (optional; DES-PROJECT-001 §2.3)

5a — Governed-mode yield (project-bound docs)

If the event payload includes a project_id field, the document is project-bound and the crew
daemon (started with --interactive-draft-events) has already subscribed to this event and will
produce the first draft as a governed crew run. Do not generate a competing draft.

Instead, post a single working status to narrate crew's activity, then return to the monitor
loop:

wibus wicked.interactive.status.posted status '{"document_id":"<doc>","state":"working","message":"A governed crew run is building this draft — the editor will open when it lands."}'

The crew run will emit wicked.interactive.draft.completed (producer wi-crew) when done; the
service lands it as _v1 and the browser hot-reloads exactly as in the solo path. Your
click-to-edit loop (Steps 3–4) takes over from there once the version lands.

Known limitation (follow-up tracked): if the governed crew run fails, no wicked.interactive.draft.completed
is emitted and the document stays at the _v0 placeholder indefinitely. The user can check the
crew dashboard for the failed run; cross-product error bridging (crew session failed → interactive
error event) is a planned improvement.

Only proceed to step 5b if project_id is absent.

5b — Solo draft generation (unbound docs)

The service is model-free, so building the draft is yours.

If the brief is too vague to generate confidently (e.g. "make a doc", no context, conflicting
instructions), do NOT guess — ask one focused clarifying question via:

wibus wicked.interactive.status.posted status '{"document_id":"<doc>","state":"asking","question":"What'\''s this for and who'\''s the audience?","options":["Investor update","Customer pitch","Internal report","Something else"]}'

Then wait for wicked.interactive.question.answered before generating. This is the right path when intent
is genuinely unclear — the Thread is open and the user is right there.

wibus wicked.interactive.status.posted status '{"document_id":"<doc>","state":"processing","message":"Reading your brief and drafting…"}'
  • If source_paths is non-empty, read every entry (each a file or a folder, expand ~).
  • If source_paths is empty, generate from the brief alone — that IS the spec.
  • Honor the style field — before generating, read the format-specific craft reference at
    skills/assist/references/formats/<style>.md for layout rules, typography constraints,
    animation policy, and the format's quality gate. The one-liners below are reminders only;
    the reference file is authoritative:
    • web → rich scrollable HTML with animations, interactivity, and vivid layout
    • ppt → fixed landscape slide layout, no animations, export-safe for PPTX
    • brochure → landscape multi-page PDF, stylized pages, print-ready
    • doc → minimal formatting, readable prose, content-first (like a Word doc)
  • Ground it in knowledge — consult/ingest the estate knowledge store via wicked-garden-mem
    so the draft uses the user's real numbers and prior decisions (Step 6). Don't invent figures
    the source doesn't support.
  • Generate the document — work the craft references under skills/assist/references/ as a
    pipeline: format rules (formats/<style>.md) → structure (outline-method) → narrative<br>(story-arc) → visual design (design-principles) → export-safe HTML (html-craft). For a<br>multi-discipline brief, route through a <strong>wicked-garden crew</strong> (Step 7) so design + copy +<br>structure are reasoned about together.</li> <li><strong>Self-check before you emit</strong> — run the draft past <code>skills/assist/references/quality-checklist.md</code><br>(narrative, content, visual, export-safety). Fix structure and content before surface; keep it<br>proportional.</li> </ul> <p>Then emit the whole draft (fresh document — no pre-existing anchors to preserve):</p> <pre><code class="language-bash" data-language="bash"># build the HTML in a Node script, write it to a file, then: wibus wicked.interactive.draft.completed generation &quot;$(node -e &#39;const fs=require(&quot;fs&quot;);process.stdout.write(JSON.stringify({document_id:&quot;&lt;doc&gt;&quot;,html:fs.readFileSync(&quot;/tmp/wi-draft.html&quot;,&quot;utf8&quot;)}))&#39;)&quot; # or for a large draft, pass it by path and skip inlining: wibus wicked.interactive.draft.completed generation &#39;{&quot;document_id&quot;:&quot;&lt;doc&gt;&quot;,&quot;html_path&quot;:&quot;/tmp/wi-draft.html&quot;}&#39;</code></pre><p>The service lands it as <code>_v1</code> and the browser hot-reloads. Post <code>complete</code>:</p> <pre><code class="language-bash" data-language="bash">wibus wicked.interactive.status.posted status &#39;{&quot;document_id&quot;:&quot;&lt;doc&gt;&quot;,&quot;state&quot;:&quot;complete&quot;,&quot;message&quot;:&quot;Here&#39;\&#39;&#39;s a first draft — click any block to refine it.&quot;,&quot;version&quot;:1}&#39;</code></pre><p>From here the normal click-to-edit loop (Steps 3–4) takes over.</p> <h2>Step 6 — Consult project knowledge before you rewrite (wicked-garden-mem)</h2> <p>Before any <strong>structural</strong> edit or first-draft generation (Step 3, Step 5, or the change branch<br>of Step 4), check whether the project&#39;s knowledge store knows something the document should<br>respect — prior decisions, terminology, the customer&#39;s positioning, numbers that must stay<br>accurate. This keeps agent-authored content grounded instead of plausibly-wrong (ADR-0016<br>Slice E). The engine is <strong>wicked-estate</strong> (memory + knowledge stores); the agent surface is<br>wicked-garden&#39;s <code>mem</code> skill (the retired wicked-brain&#39;s replacement — ADR-0026). <!-- historical --></p> <ul> <li>Post <code>{&quot;state&quot;:&quot;processing&quot;,&quot;message&quot;:&quot;Checking project knowledge…&quot;}</code> so the beat reads as work.</li> <li>Recall what&#39;s stored on the topic: invoke the <strong><code>wicked-garden-mem</code></strong> skill — the <code>recall</code><br>action for memories (&quot;what do we know about X&quot;), or the <code>answer</code> action for a cited synthesis<br>across the knowledge + memory stores.</li> <li>Fold any <strong>citable</strong> facts into the markup, and when a fact drove a choice, say so in your chat<br>reply (&quot;kept the ARR figure at $4.2M per the Q3 board deck&quot;). If recall returns nothing<br>relevant, proceed — never block an edit on a knowledge miss.</li> </ul> <p>Skip this for deterministic tweaks; those carry no authorship risk.</p> <h2>Step 7 — Assemble a crew for multi-discipline requests (wicked-garden)</h2> <p>Some chat requests need design + copy + structure reasoned about together (&quot;turn this into an<br>investor-ready deck&quot;, &quot;make the whole thing premium and tighten the narrative&quot;). Route these to a<br><strong>wicked-garden crew</strong> rather than a shallow single-shot edit (ADR-0016 Slice D).</p> <ul> <li>Recognise the trigger: the request spans <strong>more than one discipline</strong>, asks for a whole-document<br>transformation, or is open-ended enough that a crew&#39;s plan→build→review beats one edit.</li> <li>Post <code>{&quot;state&quot;:&quot;processing&quot;,&quot;message&quot;:&quot;Assembling a crew…&quot;}</code>.</li> <li>Dispatch the relevant crew/agents (via the Task tool) with the current head HTML and the goal.</li> <li>You remain the <strong>single writer back into the loop</strong>: take the crew&#39;s output, preserve every<br><code>data-wid</code> (Step 2), and land it via <code>wicked.interactive.edit.completed</code> (targeted) or<br><code>wicked.interactive.draft.completed</code> (whole-doc). The crew reasons; you satisfy the INV-2 gate.</li> </ul> <p>For a single-discipline ask, handle it inline (Step 3/4). Crews are for breadth.</p> <h2>Step 8 — Learn an app and record a demo (ADR-0018)</h2> <p>A <code>wicked.interactive.doc.created</code> event with <code>kind: &quot;demo&quot;</code> points at a <strong>live URL</strong> (in <code>payload.url</code>, with<br>an optional <code>brief</code>). The service is model-free: deciding <em>what to click</em> is yours; launching the<br>browser and recording is the service&#39;s. Author a deterministic Playwright spec, then ask the<br>service to execute + record it.</p> <pre><code class="language-bash" data-language="bash">wibus wicked.interactive.status.posted status &#39;{&quot;document_id&quot;:&quot;&lt;doc&gt;&quot;,&quot;state&quot;:&quot;working&quot;,&quot;message&quot;:&quot;Exploring the app and working out the click-path…&quot;}&#39;</code></pre><h3>8a. Decompose the brief into scenes, then author <code>demo.spec.mjs</code></h3> <p><strong>Do NOT copy the brief text into a single scene.</strong> The brief is a prose description; your job<br>is to reason about it and extract the logical beats — the distinct capabilities the user wants<br>the demo to show. Each beat becomes one <code>step()</code>.</p> <p><strong>Scene decomposition — do this before touching Playwright:</strong></p> <ol> <li>Read the <code>brief</code> from the event payload (or from <code>wicked.interactive.chat.posted</code> if the user typed it).</li> <li>Ask: <em>What are the distinct things this demo should prove?</em> Look for:<ul> <li>Setup / context steps (&quot;sign in&quot;, &quot;open a project&quot;, &quot;navigate to X&quot;)</li> <li>The main value moments (&quot;create a document&quot;, &quot;generate a draft&quot;, &quot;export to PDF&quot;)</li> <li>Payoff / outcome moments (&quot;here&#39;s the result&quot;, &quot;download the file&quot;)</li> </ul> </li> <li>Name each scene as a <strong>short capability label</strong> — what the viewer should take away, not what<br>the user physically clicked. Good: <code>&quot;Create a document&quot;</code>. Bad: <code>&quot;Click the New button&quot;</code>.</li> <li>Aim for 3–6 scenes. One scene per brief sentence is usually right; merge trivial setup steps,<br>split compound flows. A demo that&#39;s one scene for a multi-step brief isn&#39;t a demo.</li> <li><strong>Post the scene plan and WAIT for confirmation — do NOT start recording yet.</strong> This is an<br>editorial step. The user must approve the scene breakdown before you write the spec or trigger<br>the recording. Use <code>state: &quot;asking&quot;</code> so the UI shows the confirmation state:<pre><code class="language-bash" data-language="bash">wibus wicked.interactive.status.posted status &#39;{&quot;document_id&quot;:&quot;&lt;doc&gt;&quot;,&quot;state&quot;:&quot;asking&quot;,&quot;question&quot;:&quot;Does this scene breakdown look right?&quot;,&quot;options&quot;:[&quot;Looks good — record it&quot;,&quot;Let me adjust&quot;]}&#39; wibus wicked.interactive.chat.posted chat &#39;{&quot;document_id&quot;:&quot;&lt;doc&gt;&quot;,&quot;role&quot;:&quot;agent&quot;,&quot;text&quot;:&quot;Here&#39;\&#39;&#39;s how I&#39;\&#39;&#39;m breaking this into scenes:\n- **Scene 1: ...**\n- **Scene 2: ...**\n\nDoes this look right? Say &#39;\&#39;&#39;looks good&#39;\&#39;&#39; to start recording, or tell me what to change.&quot;}&#39;</code></pre></li> <li>Wait for <code>wicked.interactive.question.answered</code> (button click) or <code>wicked.interactive.chat.posted</code> (typed reply)<br>confirming the plan. If they want changes, update the scene list and re-confirm. Only proceed<br>to Playwright and <code>wicked.interactive.demo.requested</code> after explicit approval.</li> </ol> <p><strong>Do NOT emit <code>wicked.interactive.demo.requested</code> before the user confirms the scene plan.</strong> Jumping<br>straight to recording without editorial sign-off is the mistake to avoid.</p> <p>Then drive the live URL yourself (Playwright is installed) to learn the selectors. Write<br><code>&lt;DOCS&gt;/&lt;doc&gt;/demo.spec.mjs</code> — a plain ES module exporting <code>meta</code> and an async <code>run</code>. You express<br><strong>only</strong> the click-path; the service supplies <code>page</code> and the <code>step</code> annotator and owns the<br>browser/recording lifecycle — including the browser itself: on the first recording the service<br>provisions Playwright&#39;s headless shell + ffmpeg (a one-time download, progress on the thread). A<br>recording that cannot run comes back ONCE as a typed <code>wicked.interactive.status.posted {state:&quot;error&quot;, code, retryable:false, remedy}</code> (e.g. <code>recorder_browser_missing</code> →<br><code>wicked-interactive doctor --install</code>; <code>recording_step_failed</code> names the step) — it is never<br>retried; fix what the <code>remedy</code> says, then re-emit <code>demo.requested</code>.</p> <pre><code class="language-js" data-language="js">export const meta = { url: &quot;https://staging.example.com/app&quot;, title: &quot;Checkout demo&quot;, steps: [&quot;Sign in&quot;, &quot;Add Pro plan&quot;, &quot;Checkout&quot;], captionHoldMs: 2500, }; export async function run({ page, step, meta }) { await page.goto(meta.url); await step(&quot;Sign in&quot;, async () =&gt; { await page.fill(&quot;#email&quot;, &quot;demo@example.com&quot;); await page.fill(&quot;#password&quot;, process.env.DEMO_PW || &quot;&quot;); await page.click(&quot;button[type=submit]&quot;); await page.waitForURL(&quot;**/dashboard&quot;); }); await step(&quot;Checkout&quot;, async () =&gt; { await page.click(&quot;text=Checkout&quot;); await page.waitForSelector(&quot;text=Order confirmed&quot;); }, { say: &quot;One click and the order&#39;s placed — no forms, no waiting.&quot;, holdMs: 3500 }); }</code></pre><p>Rules that keep the recording deterministic and safe (unchanged from the file era): wrap every<br>meaningful action in <code>step(label, fn)</code>; <strong>always narrate</strong> (caption the meaningful beats via the<br><code>say</code> 3rd arg — narrate the capability, not the on-screen data); prefer stable selectors; <strong>never<br>write credentials into the spec or any version artifact</strong> (read secrets from env at run time);<br><code>await</code> your waits so the recording captures settled UI.</p> <h3>8b. Trigger the record over the bus</h3> <pre><code class="language-bash" data-language="bash">wibus wicked.interactive.demo.requested demo &#39;{&quot;document_id&quot;:&quot;&lt;doc&gt;&quot;}&#39;</code></pre><p>The service launches Chromium, runs your <code>run()</code>, captures the video + storyboard, instruments it<br>(fresh <code>data-wid</code> per step), lands the version, and hot-reloads. It streams <code>Step k: &lt;label&gt;</code><br>progress as <code>wicked.interactive.status.posted</code> and emits <code>wicked.interactive.version.created</code> when the version lands (or a<br><code>wicked.interactive.status.posted</code> with <code>state:&quot;error&quot;</code> if a step throws — fix the selector and emit<br><code>wicked.interactive.demo.requested</code> again). The storyboard toolbar offers <strong>Download video</strong> and <strong>GIF</strong>.</p> <h3>8c. Refine = re-author + re-record</h3> <p>A demo refines through the <strong>same loop</strong>. When the user highlights a step and asks for a change,<br>it arrives as a <code>wicked.interactive.feedback.processed</code> (Step 3) or <code>wicked.interactive.chat.posted</code> (Step 4). For a demo,<br>&quot;make the change&quot; means <strong>edit <code>demo.spec.mjs</code></strong> and emit <code>wicked.interactive.demo.requested</code> again — same spec<br>⇒ same click-path ⇒ a new version. Deterministic replay.</p> <h3>8d. Handle &quot;Add a scene&quot; requests from the storyboard UI</h3> <p>The storyboard left-sidebar has an <strong>Add a scene</strong> button that opens a modal. When the user<br>submits it, a <code>wicked.interactive.chat.posted</code> arrives with this format:</p> <pre><code class="language-plaintext" data-language="plaintext">Add a scene: &lt;what the user described&gt; Mode: add it as a new scene — OR — Mode: re-record from the beginning</code></pre><p><strong>Immediately reply in chat</strong> to tell the user what you&#39;re about to do (even one sentence is<br>enough — they see the thread open). Then post <code>processing</code> status and get to work.</p> <h4>Mode: add it as a new scene</h4> <ol> <li>Read <code>&lt;DOCS&gt;/&lt;doc&gt;/demo.spec.mjs</code>.</li> <li>Append a new <code>step()</code> block that implements what the user described. Keep the existing steps<br>untouched — the new scene goes at the end of <code>run()</code> and into <code>meta.steps</code>.</li> <li>Emit <code>wicked.interactive.demo.requested</code> — the service re-runs the full spec but the new step is the<br>only new footage; the browser hot-reloads the storyboard with the extra scene card.</li> </ol> <h4>Mode: re-record from the beginning</h4> <ol> <li>Read <code>&lt;DOCS&gt;/&lt;doc&gt;/demo.spec.mjs</code>.</li> <li><strong>Rewrite</strong> the full spec: plan where the new scene fits best in the narrative, insert it at<br>the right position in <code>run()</code>, update <code>meta.steps</code>, and tighten any surrounding steps that<br>now flow differently.</li> <li>Emit <code>wicked.interactive.demo.requested</code>. The service records a clean take from start to finish.</li> </ol> <p>Either way, post a <code>complete</code> status with the scene count when the new version lands.</p> <pre><code class="language-bash" data-language="bash">wibus wicked.interactive.status.posted status &#39;{&quot;document_id&quot;:&quot;&lt;doc&gt;&quot;,&quot;state&quot;:&quot;complete&quot;,&quot;message&quot;:&quot;Done — added \&quot;&lt;scene title&gt;\&quot; (now &lt;N&gt; scenes).&quot;,&quot;version&quot;:&lt;v&gt;}&#39;</code></pre><p>If you have a question before starting — unclear flow, missing credentials, ambiguous<br>scope — <strong>ask via <code>wicked.interactive.status.posted</code> with <code>state: &quot;asking&quot;</code></strong> (Step 3d). The question<br>renders as buttons in the UI lock; the user&#39;s answer arrives as <code>wicked.interactive.question.answered</code>.</p> <h2>Step 8.5 — Learn a theme from a URL (ADR-0010/ADR-0020)</h2> <p>The user pointed at a page whose look they like (&quot;Theme from URL&quot;). The split is the same as a<br>demo: the <strong>grab is deterministic service infra</strong> (ADR-0010) — the service has already rendered<br>the URL to a PDF and announced it as <code>wicked.interactive.theme.learned</code> with <code>render_path</code> (a PDF in the doc<br>workspace). <strong>The judgment — reading the design — is yours.</strong> Do <strong>not</strong> re-grab the URL.</p> <ol> <li><strong>Read the rendered PDF with vision.</strong> Use the SAME technique Step 9 uses to ingest binary<br>sources (<code>wicked-garden-mem</code> ingest reads pdf/images &quot;via LLM vision&quot;) — except here you read the design<br>system directly off <code>render_path</code>: the <strong>palette</strong> (dominant background/surface, primary,<br>secondary/accent, text colors, borders), the <strong>type</strong> (heading vs body font family, the size<br>scale, weight), the <strong>spacing</strong> rhythm, <strong>card/surface treatment</strong> (radius, padding, shadow),<br>and the overall vibe (light/dark, dense/airy, formal/playful).</li> <li><strong>Synthesize a theme token object</strong> in the <code>src/themes/*.json</code> shape —<br><code>{name, colors, fonts, sizes, spacing, card}</code> (see <code>src/themes/corporate-light.json</code>). Assign<br>color roles <strong>by meaning</strong> (the page&#39;s dominant brand color → <code>primary</code>, the call-to-action /<br>link hue → <code>secondary</code>, etc.), not by raw position. Name it after the source (e.g.<br><code>&quot;stripe-learned&quot;</code>).</li> <li><strong>Quality-gate it.</strong> Run text/background pairs through <strong>WCAG-AA contrast</strong><br>(<code>skills/assist/references/quality-checklist.md</code>) and nudge any failing color until it passes —<br>a learned palette that&#39;s pretty but unreadable is a regression.</li> <li><strong>Write it into the doc workspace</strong> so the apply seam can re-theme with it:<br><code>&lt;DOCS&gt;/&lt;doc&gt;/theme/learned.theme.json</code>. Narrate progress with <code>wicked.interactive.status.posted</code><br>(<code>state:&quot;working&quot;</code> — a non-lock state) at each beat: reading → synthesizing → applying.</li> <li><strong>Apply it by re-landing head.</strong> Once <code>&lt;doc&gt;/theme/learned.theme.json</code> exists, the service<br>applies it <strong>automatically at every version-creation for this doc</strong> — the theming seam reads<br>that file and re-themes with its tokens (so the learned brand sticks for all later edits too;<br>you never thread tokens through the event). Just trigger a re-land: read the head version&#39;s<br>HTML and emit <code>wicked.interactive.draft.completed</code> with it (or <code>wicked.interactive.edit.completed</code> for a targeted<br>re-theme). The service lands a new version themed with the learned tokens and the browser<br>hot-reloads. Close with a <code>complete</code> status.</li> </ol> <p>Once written, the learned theme is also <strong>readable over HTTP</strong>:<br><code>GET &lt;BASE&gt;/d/&lt;doc&gt;/api/theme/learned</code> → <code>{document_id, learned_at, tokens}</code> (404 until a learn<br>completes). That&#39;s the wire other surfaces ride to see what the learn produced — e.g. studio&#39;s<br>brand-learn accent mapper — so never move or rename <code>learned.theme.json</code>; the write path here and<br>that read surface are one contract.</p> <p>The deterministic grab (service) → vision read (you) → token apply (theming seam) all ride the one<br>bus, reusing every existing seam. <strong>There is nothing model-driven in the service half and nothing<br>deterministic in the read half</strong> — keep that line clean (ADR-0010).</p> <p><strong>Learn from a local PDF or image</strong> (the tool-rail&#39;s <strong>Style → from a PDF or image</strong>). The user pointed at a<br>file on their own machine; the service does <strong>not</strong> grab anything — it emits <code>wicked.interactive.theme.learned</code><br>with <code>render_path</code> = that file and <code>format: &quot;pdf&quot;|&quot;image&quot;</code>. Read it exactly as above (vision works<br>on both PDFs and images), synthesize + quality-gate + apply the same way. Nothing uploads; you read<br>it in place.</p> <h2>Step 8.6 — Run a review pass (ADR-0023)</h2> <p>A <code>wicked.interactive.review.requested</code> event carries <code>reviewers: string[]</code> (any of <code>match</code>, <code>a11y</code>, <code>copy</code>,<br><code>qe</code>) and <code>document_id</code>. The user wants the <strong>current head version</strong> reviewed — you run the passes<br>and post each verdict back so it lands in the conversation thread. <strong>Review only; do not edit</strong> —<br>the user decides what to act on (offer to apply fixes, don&#39;t apply unasked).</p> <p>Reviews are <strong>non-blocking and concurrent</strong> in the UI: the user keeps editing while a review runs,<br>and several reviewers can be in flight at once. The rail shows a per-reviewer spinner that clears<br>when that reviewer&#39;s verdict lands. To keep this working, two rules:</p> <ul> <li><strong>Tag review progress as a review</strong> so the UI never veils/locks the canvas for it. On any<br><code>wicked.interactive.status.posted</code> you emit <em>for a review</em>, set <code>review: true</code>:<br><code>wibus wicked.interactive.status.posted status &#39;{&quot;document_id&quot;:&quot;&lt;doc&gt;&quot;,&quot;state&quot;:&quot;working&quot;,&quot;review&quot;:true,&quot;message&quot;:&quot;Running the a11y pass…&quot;}&#39;</code>.</li> <li><strong>Close each reviewer out</strong> with <code>wicked.interactive.review.completed</code> carrying its <code>reviewer</code> key — this is<br>what clears that reviewer&#39;s rail spinner:<br><code>wibus wicked.interactive.review.completed review &#39;{&quot;document_id&quot;:&quot;&lt;doc&gt;&quot;,&quot;ts&quot;:&quot;&lt;iso&gt;&quot;,&quot;reviewer&quot;:&quot;a11y&quot;,&quot;passed&quot;:true,&quot;verdict&quot;:&quot;✓ Contrast passes AA throughout.&quot;}&#39;</code>.</li> </ul> <p>For each selected reviewer, evaluate the head version&#39;s HTML and post a concise verdict. Post it as<br>a chat message with <code>role: &quot;review&quot;</code> (the UI renders these as review lines) AND/OR as<br><code>wicked.interactive.review.completed</code> (which also clears the rail spinner). Including the <code>reviewer</code> key on<br>either lets the UI match the verdict to the right rail button:</p> <pre><code class="language-bash" data-language="bash">wibus wicked.interactive.chat.posted chat &#39;{&quot;document_id&quot;:&quot;&lt;doc&gt;&quot;,&quot;role&quot;:&quot;review&quot;,&quot;reviewer&quot;:&quot;match&quot;,&quot;text&quot;:&quot;✓ Matches the ask — the brief asked for X, the page delivers X.&quot;}&#39; wibus wicked.interactive.review.completed review &#39;{&quot;document_id&quot;:&quot;&lt;doc&gt;&quot;,&quot;ts&quot;:&quot;&lt;iso&gt;&quot;,&quot;reviewer&quot;:&quot;match&quot;,&quot;passed&quot;:true,&quot;verdict&quot;:&quot;✓ Matches the ask — the brief asked for X, the page delivers X.&quot;}&#39;</code></pre><table> <thead> <tr> <th>reviewer (UI name)</th> <th>what to check</th> <th>how</th> </tr> </thead> <tbody><tr> <td><code>match</code> (<strong>Intent</strong>)</td> <td>does the version still match the <strong>original ask/intent</strong>?</td> <td>read the SAVED intent — the <strong>first user entry in the doc&#39;s <code>conversation.jsonl</code></strong> (the service seeds the creation brief / first ask there) — and compare it to the head HTML; flag drift, dropped asks, scope creep. This is why intent is persisted: judge against what was actually asked, not vibes.</td> </tr> <tr> <td><code>a11y</code></td> <td>accessibility + contrast</td> <td>run text/background pairs through WCAG-AA (<code>skills/assist/references/quality-checklist.md</code>); flag &lt; 4.5:1, missing alt/landmarks, focus order</td> </tr> <tr> <td><code>copy</code></td> <td>copy &amp; clarity</td> <td>tighten wording, reading level, consistency; flag jargon, hedging, inconsistent terms</td> </tr> <tr> <td><code>qe</code> (<strong>Quality</strong>)</td> <td>full quality crew</td> <td>for a heavier multi-perspective pass, assemble a wicked-garden crew (Step 7) — semantic + a11y + content reviewers — and synthesize their findings</td> </tr> </tbody></table> <p>Lead each verdict with <code>✓</code> (pass) or <code>⚠</code> (issue + the specific fix). Keep each to a sentence or<br>two. Narrate start/finish with <code>wicked.interactive.status.posted</code> carrying <code>review:true</code> (<code>state:&quot;working&quot;</code> →<br><code>complete</code>) — the <code>review:true</code> flag keeps the canvas un-veiled so the user keeps editing. A review<br>pass creates <strong>no new version</strong> unless the user then asks you to apply a fix.</p> <h2>Step 9 — Index attached reference sources (ADR-0017)</h2> <p>A <code>wicked.interactive.source.attached</code> event carries <code>added: [{ path, note }]</code> — reference material to index<br>into the wicked-estate knowledge store via the <strong><code>wicked-garden-mem</code></strong> skill&#39;s <code>ingest</code> action,<br><strong>with live progress narrated to the browser</strong>. This is a standing part of the loop.</p> <ol> <li><strong>Flip to indexing.</strong> <code>wibus wicked.interactive.source.updated sources &#39;{&quot;document_id&quot;:&quot;&lt;doc&gt;&quot;,&quot;path&quot;:&quot;&lt;abs&gt;&quot;,&quot;status&quot;:&quot;indexing&quot;}&#39;</code>.</li> <li><strong>Stream progress</strong> with <code>wicked.interactive.status.posted</code> (<code>state:&quot;working&quot;</code> — a non-lock state, so the<br>doc isn&#39;t covered by the overlay) at each milestone: kickoff → scope → ingesting → done.</li> <li><strong>Check coverage AND freshness before ingesting.</strong> Recall against the store first<br>(<code>wicked-garden-mem</code> <code>recall</code>/<code>answer</code>) — already-indexed ≠ current. Compare the store&#39;s last<br>ingest against the source&#39;s real state (<code>git log -1</code> for a repo, file mtimes otherwise);<br>re-ingest if it moved.</li> <li><strong>Scope sanely.</strong> Skip <code>node_modules</code>, build artifacts, binaries, vendored deps; index the<br>high-signal surface (docs, READMEs, source). Name the scope decision in chat.</li> <li><strong>Land it.</strong> <code>wibus wicked.interactive.source.updated sources &#39;{&quot;document_id&quot;:&quot;&lt;doc&gt;&quot;,&quot;path&quot;:&quot;&lt;abs&gt;&quot;,&quot;status&quot;:&quot;indexed&quot;}&#39;</code><br>(or <code>&quot;error&quot;</code>), with a final <code>complete</code> status. Then draw on that knowledge (Step 6) when<br>generating/updating.</li> </ol> <p>Scope choice: ingest under the source&#39;s natural project scope when one exists (e.g.<br><code>project:&lt;repo-name&gt;</code>); otherwise this doc&#39;s project scope. The mem skill&#39;s scopes reference<br>documents the <code>kind:id</code> conventions.</p> <h2>Step 10 — Recover a stale cursor (WB-003)</h2> <p><code>WB-003</code> (cursor behind the retention window) shows up in two situations:</p> <ul> <li><strong>Arming-time inheritance</strong> — a <em>fresh</em> session re-armed on a <code>--plugin</code> name whose durable<br>cursor was left behind the TTL window by an earlier session. This is the interactive#42 stall.<br><strong>Step 0&#39;s per-session <code>$WI_SUB</code> prevents it</strong> — a new session&#39;s cursor starts fresh at<br><code>--cursor-init latest</code>, so it can&#39;t inherit a prior session&#39;s stale position. If you followed<br>Step 0, you should not hit WB-003 on arm.</li> <li><strong>Long absence</strong> — you held one subscription open (same <code>$WI_SUB</code>) across an idle gap longer<br>than the retention window, so the bus swept events past your live cursor.</li> </ul> <p>Recent wicked-bus <strong>self-recovers</strong> from WB-003: the subscribe loop re-anchors the cursor to the<br>oldest surviving event and keeps delivering instead of dying (wicked-bus#48). Whether that reaches<br>you depends on the installed <code>wicked-bus</code> CLI version (see note below) — so the manual recovery<br>here is still the guaranteed fallback. Either way the bus is transport, not storage (ADR-0021):<br>swept events are gone; recover the missed <em>work</em> from the <strong>state plane</strong>, which is authoritative.</p> <p><strong>Recovery procedure:</strong></p> <ol> <li><strong>Re-arm with a fresh subscription id</strong> — the stale cursor cannot be reset with <code>replay</code>; a<br>fresh name is the fix. Regenerate <code>$WI_SUB</code> (<code>export WI_SUB=&quot;wi-agent-$(date +%Y%m%d-%H%M%S)-$$&quot;</code>)<br>so the new cursor starts at <code>--cursor-init latest</code> and you don&#39;t replay old events.</li> <li><strong>Reconcile from the state plane</strong> — check what actually needs handling:<ul> <li><code>GET &lt;BASE&gt;/d/&lt;doc&gt;/api/versions</code> — any doc still at v0 (<code>head: 0</code>)? That means a<br><code>wicked.interactive.doc.created (kind source)</code> event was missed; generate its draft now (Step 5).</li> <li><code>GET &lt;BASE&gt;/d/&lt;doc&gt;/api/sources</code> — any sources with <code>status: &quot;pending&quot;</code>? Process them (Step 9).</li> <li><code>GET &lt;BASE&gt;/d/&lt;doc&gt;/api/conversation</code> — any unanswered user messages? Reply (Step 4).</li> </ul> </li> <li><strong>Resume the loop</strong> with the new cursor name.</li> </ol> <blockquote> <p><strong>CLI-version note (why the fallback still matters):</strong> the assist loop calls the <code>wicked-bus</code><br><em>CLI</em> (resolved from PATH / the <code>npx</code> cache warmed by <code>serve</code>), not this package&#39;s in-process<br>API. The <code>^2.0.0</code> pin in <code>package.json</code> governs only the service&#39;s in-process subscriptions —<br>the agent&#39;s watch runs whatever <code>wicked-bus</code> version the user has installed. So the wicked-bus#48<br>self-recovery only reaches this loop once a wicked-bus release carrying it is published <strong>and</strong><br>the installed CLI is updated to it. Until then, per-session <code>$WI_SUB</code> (Step 0) is what actually<br>prevents the interactive#42 arming stall, and this manual recovery covers the long-absence case.</p> </blockquote> <h2>Step 11 — Loop (re-arm the drain)</h2> <p><strong>Before re-arming: check for orphaned docs.</strong> Every reconnect (and every loop iteration after<br>handling a batch), run a quick state-plane catchup so docs created in prior sessions aren&#39;t<br>silently abandoned:</p> <pre><code class="language-bash" data-language="bash"># Docs at v0 = created but never generated (bus event may be long gone) curl -s &lt;BASE&gt;/api/docs | node -e &quot; const docs = JSON.parse(require(&#39;fs&#39;).readFileSync(&#39;/dev/stdin&#39;,&#39;utf8&#39;)); docs.filter(d =&gt; d.head === 0).forEach(d =&gt; console.log(d.id, d.kind)); &quot;</code></pre><p>For any doc where <code>head === 0</code>:</p> <ul> <li><code>kind: &quot;source&quot;</code> → generate the draft (Step 5) — the user&#39;s files are still in <code>source_paths</code></li> <li><code>kind: &quot;demo&quot;</code> → author the spec (Step 8) — the target URL is still in the doc&#39;s metadata</li> <li>Post <code>processing</code> status so the browser reflects activity immediately</li> </ul> <blockquote> <p>This catchup is what prevents the &quot;user has to ask twice&quot; failure mode where a doc is created<br>in one session, the agent says &quot;on it,&quot; then reconnects and silently ignores the pending work.<br>The bus drain only delivers events the cursor hasn&#39;t seen; orphaned docs never surface again<br>via the bus alone.</p> </blockquote> <p>In Claude Code, arm a <strong>persistent subscribe via Monitor</strong> so the loop survives idle timeouts<br>without generating noise. Do this at the end of Step 1 (or immediately on re-entry):</p> <pre><code class="language-bash" data-language="bash"># In Claude Code — arm via Monitor tool with this command. # $WI_SUB / $WICKED_BUS_PRODUCER_ID come from Step 0; the same $WI_SUB is reused across loop # iterations WITHIN this session (that&#39;s how position survives idle-timeout exits). Do NOT # redirect stderr — WB-003 and errors must stay visible. while true; do # `|| sleep 1` backs off on a fast-failing subscribe (bad config, DB lock, # missing executable) so the loop can&#39;t spin at 100% CPU. A clean idle-timeout # exit falls straight through and re-arms immediately. wicked-bus subscribe --plugin &quot;$WI_SUB&quot; --filter &#39;*@wicked-interactive&#39; --idle-timeout 120000 || sleep 1 done</code></pre><p>Pass <code>persistent: true</code> to Monitor. The subscribe process blocks silently until an event arrives,<br>outputting exactly one JSON line per event — Monitor fires only then. When the process exits after<br>120s of no activity, the <code>while true</code> restarts it immediately and silently (no echo). The result:<br>zero chat noise between events; only real bus events ever reach you.</p> <p>On each Monitor notification:</p> <ol> <li>Act on the delivered line: ignore your own <code>wi-agent</code> emissions and service facts<br>(<code>wicked.interactive.version.created</code>, <code>wicked.interactive.export.requested</code>), handle the rest (Steps 3–9).</li> <li>If subscribe reports <strong>WB-003</strong>, regenerate <code>$WI_SUB</code> (Step 10) and re-start the Monitor<br>with the fresh subscription id.</li> </ol> <p><em>(Not in Claude Code? Use <code>--drain</code> in the loop instead — standard harnesses only wake on process<br>exit, so you need the drain to exit cleanly. Replace the subscribe line above with:<br><code>wicked-bus subscribe --plugin &quot;$WI_SUB&quot; --filter &#39;*@wicked-interactive&#39; --drain --idle-timeout 120000</code>)</em></p> <p>Keep going until the user says to stop. The session staying alive IS the product guarantee —<br><code>serve</code> + <code>assist</code> together are why a non-technical user can click a block and watch it change<br>without ever touching a terminal.</p>

Categories