/** * Opt-in governed answering of wicked-interactive's conversational ITERATION asks (CREW-UX-5 — * the third interactive leg, beside draft-events.ts and edit-events.ts). * * A finished doc's thread send in wicked-studio does `postFork` (mints a version) and injects * `wicked.interactive.chat.posted` — and NOTHING consumed that topic since the ad-hoc assist * agent retired: crew answers `doc.created` (the first draft) and `feedback.processed` (the * structural handoff), but the plain conversational ask ("make the intro punchier") had no * answerer, so BRIEF-UX-001 J3's "iterate twice" was impossible. This module makes a * crew-governed run the answerer: understand-the-ask → revise, announced back on the SAME * `draft.completed` wire the first draft rides — the service lands the revised full HTML as a * generated version (`materializeDraft` → `applyGeneratedHtml`), which is exactly what an * iteration is. * * Shape mirrors draft-events.ts (dynamic wicked-bus import, graceful degradation, durable * cursor `cursor_init: 'latest'` under a dedicated plugin name, durable replay-dedup ledger, * `wi-crew` narration via status.posted). The chat-specific deltas: * * - THE TRIGGER IS A CONVERSATION LINE, so the actionable filter is layered (the contract): * (a) `role: "user"` only — agent narration echoes also ride chat.posted; * (b) the doc must EXIST under the resolved docs root and not belong to a foreign loop — * and the disk truth here is subtle: interactive's `initManifest` only ever RECORDS a * `kind` for demo docs (a real `kind: "source"` doc's versions.json carries NO kind * field at all — the "source" spelling rides the doc.created EVENT only; verified * against interactive 0.8.0 and main, src/service/server.js `initWorkspace(dir, html)`). * So the gate accepts `source` (future manifests may record it) and the absent-default * `doc`, and rejects explicit foreign kinds (`demo`) — a doc we cannot see is not ours * to answer either; * (c) per-doc serialization — an ask on a doc whose draft/edit/chat run is still in flight * is QUEUED (FIFO per doc), never raced: two concurrent revisions of one doc would * land as two forks of the same parent and the second would silently drop the first; * (d) the text must be an ASK, not an echo of a machine-composed message — the feedback * overlay injects its batch as a chat.posted TOO (same `source_message_id` as its * `feedback.submitted`), and that batch is already the edit seam's business. * - VERSION SNAPSHOT AT LAUNCH: the seam copies the doc's CURRENT HEAD html into the chat * inbox and names the COPY in the task, so (1) the worker never needs read access to the * doc workspace (the one declared write root covers input + deliverable — crew#263 / * wicked-core#259: write roots are readable) and (2) a queued second ask snapshots the * head AFTER the first revision landed — which is what "iterate twice" means. * - THE LANDING GATE: `draft.completed` is announced by path and the service lands the new * version ASYNCHRONOUSLY. A queued ask drained the instant our run completes would snapshot * the stale head and silently drop the revision the user just watched land. So a successful * completion arms a per-doc gate — drain only once the manifest head ADVANCES past the head * we launched from, or a timeout passes (the service may be down; waiting forever would * strand the queue). * - IDEMPOTENCY KEYING: one doc legitimately produces many asks over its life, so the dedupe * unit is the ASK — `source_message_id` when the frame carries one (the studio reuses the * SAME message id on a user-driven resend, so a retry that reached the bus twice dedupes), * else the bus `event_id` (pure redelivery). Never the doc lifetime. */ import { type RecallIntent } from './draft-events.js'; import { InteractiveHandoffLedger } from './ledger.js'; import { type SkillHeld } from './draft-skill.js'; import type { CoreAdapter } from '../core/adapter.js'; export declare const CHAT_POSTED = "wicked.interactive.chat.posted"; /** Exact-type filter with a domain guard — no wildcard, one event type is the whole trigger. */ export declare const INTERACTIVE_CHAT_BUS_FILTER = "wicked.interactive.chat.posted@wicked-interactive"; /** * The governed workflow that fulfils an iteration ask: the wicked-core built-in preset * `interactive-chat` (X-MIG M9, wicked-core#860; crew#935). understand, then revise (the creator): * the reviser builds on a stated plan instead of one-shotting a rewrite. The steps carry the * ITERATION contract (start from the CURRENT document, change only what the ask touches, KEEP every * existing `data-wid` and mint none) and run `wicked-garden-draft`. The engine owns the steps: the * PA's RISK rating (`pa-scope`) first, its floor additions, and the deliverable floor on the * creator step. Crew registers no def; its narration reads the run's planned units. */ export declare const INTERACTIVE_CHAT_WORKFLOW = "interactive-chat"; /** The chat-posted fields this seam acts on. */ export interface ChatAsk { documentId: string; /** The user's ask, verbatim (flattened later — the problem statement is single-line). */ text: string; /** The thread message behind the ask (studio inject wire, §7.7). The studio REUSES the same * id on a user-driven resend, so this is the dedupe unit when present. */ sourceMessageId?: string; /** Present when the doc is project-bound (interactive's DES-PROJECT-001 enrichment). */ projectId?: string; } /** * Parse a bus frame into a {@link ChatAsk}, or `null` when it is not an actionable ask: * wrong type, malformed payload, non-`user` role (contract (a) — agent narration and any * transcript echo ride the same topic), slug-invalid document_id, or empty text. Kind and * existence checks (contract (b)) happen against the filesystem, not the payload — the frame * does not carry `kind`. */ export declare function parseChatPosted(eventType: string, payload: unknown): ChatAsk | null; /** Contract (d): `true` when the text is a conversational ask this seam should answer — * not the feedback overlay's batch echo (already answered elsewhere). */ export declare function isIterationAsk(text: string): boolean; /** The one dedupe unit of this seam: the ASK — the studio's message id when the frame carries * one (a resend reuses it), else the bus event id (pure redelivery). Never the doc lifetime. */ export declare function chatKey(documentId: string, eventId: number, sourceMessageId?: string): string; /** Deterministic bus idempotency key for the one revision this seam may land per ask. */ export declare function chatIdempotencyKey(documentId: string, eventId: number, sourceMessageId?: string): string; /** What the versions.json read yields — enough to gate (kind) and snapshot (head html path). */ export interface DocHead { kind: string; head: number; /** Absolute path of the head version's html artifact. */ headHtmlPath: string; } /** * Read a doc workspace's manifest (interactive's `versions.json`, fsstore.js shape) and resolve * its head html path. `null` when the doc does not exist under this root or the manifest is * unreadable — not ours to answer, per contract (b). Follows interactive's own tolerant read: * `kind` defaults to `"doc"` when absent (listDocs does the same) — and ABSENT is what a real * source doc looks like on disk (interactive records kind only for demo docs), so the caller * gates through {@link isAnswerableDocKind}, never a `=== 'source'` comparison. */ export declare function readDocHead(docsRoot: string, documentId: string): DocHead | null; /** * Contract (b)'s kind half: `true` when a doc of this manifest kind is THIS seam's to answer. * `source` is the spec's spelling (recorded by no released interactive yet, accepted for the * day a manifest carries it); `doc` is the absent-default every REAL source (and plain html) * doc reads as — interactive's initManifest keeps non-demo kinds implicit. Explicit foreign * kinds belong to their own loops. */ export declare function chatSeamAnswers(kind: string): boolean; /** * `true` when SOME seam answers a thread ask on a doc of this kind — the proxy refuses the rest * with a typed 422 (`askRefusalFor`). A `demo` document has no answerer: demos are made by a * governed run of the `demo` preset, started from studio's New job › Demo video (studio#664). */ export declare function isAnswerableDocKind(kind: string): boolean; /** Whether a document is a run's working document: crew's sidecar (also before interactive wrote a manifest), or a * manifest that says `phase-report`. Every seam and every write path asks this one question. */ export declare function isWorkingDoc(docsRoot: string, documentId: string): boolean; /** Why a run's WORKING DOCUMENT (kind `phase-report`, spike 3) is answered by no seam: it is written only through the * run's working-document block, a person's edit steering the run's next step. No edit, ask, review or theme of it * ever launches a governed run. */ export declare const WORKING_DOC_REFUSED_MESSAGE: string; /** Why a demo DOCUMENT's feedback or ask has no answerer (studio#373, M9b): demos are made by a * governed run of the `demo` preset, which re-records at its review gate. Studio's Demo mode is * gone (studio#609); the way in is New job › Demo video, or ⌘K "New Demo" (studio#664). */ export declare const DEMO_DOC_MOVED_MESSAGE: string; /** The thread's answer to an ask on a doc no seam answers (F-RECON-013). Mirrors the proxy's 422 * (`askRefusalFor`). */ export declare function foreignKindAskMessage(kind: string): string; /** * The run's problem statement (the engine scopes it per phase and folds each phase's * instructions on top). Carries everything ask-specific: identity, the flattened ask, the * CURRENT-version snapshot to read, and the absolute path the revised HTML must land at. * * CREW-UX-8 SPLIT — the revision leg carries NO repo grounding, deliberately. The draft leg * (draft-events.ts) grounds its v1 in a launch-scoped repo snapshot and that shape is proven * live; the SAME shape on the revise turn wedged 2/2 on the real engine (crew#288 comment — * the repo-grounded revise turn hits the known second-turn wedge), while UNGROUNDED revisions * are proven to land (the CREW-UX-5 verification). So bound asks launch exactly like unfiled * ones — head-copy in the external inbox, one write root, no repoRef (wicked-core#293), no * live-repo path in the task (wicked-core#294). Revision grounding returns when either of * those is fixed. */ export declare function chatProblem(ask: ChatAsk, currentPath: string, outPath: string, intent?: RecallIntent): string; /** Options for {@link startInteractiveChatSubscriber}. */ export interface InteractiveChatOptions { /** The bus db the daemon handed its engine (core/bus.ts); without one the seam does not arm. */ dbPath?: string; /** Poll cadence, ms (default 2000; tests shorten it). */ pollIntervalMs?: number; /** Heartbeat narration cadence while a run is in flight, ms (default 15000 — inside the * UI's ~20s `status.requested` window so the canvas never reads frozen; the studio's own * 90s silence budget makes the FIRST pickup narration the load-bearing one). */ heartbeatMs?: number; /** Ledger file (default `~/.wicked-crew/interactive-chat-ledger.json`). */ ledgerPath?: string; /** Root under which each ask gets its own per-run subdirectory (`//`) * holding the head snapshot and the revised deliverable; only that subdirectory is declared * as the run's extra write root (per-run isolation — Copilot, crew#313: declaring the shared * dir wholesale let one doc's worker read every other doc's head and revision). * Default `~/.wicked-crew/interactive-chats`. */ chatDir?: string; /** Seat roster JSON for the governed run (default: the production council roster). * The functional-test harness passes a deterministic stub seat here. */ clisJson?: string; /** The roster accessor used when `clisJson` is not set — the server wires the daemon's roster * WITH crew's standing (`api/roster-standing.ts`, F-RECON-002/003) so a signed-out seat reaches * the engine benched (`health {usable: false, reason}`) instead of being convened or elected. */ roster?: () => unknown[]; /** Does the daemon's PUBLISHED skills snapshot hold (and enable) a skill? Consulted ONCE at arm time * for `wicked-garden-draft` (interactive/draft-skill.ts), only for the arm log line: the preset * always runs the skill, and a snapshot without it fails the run before the document work starts (crew#935). * Default: `() => false` (a caller without a skills runtime has no snapshot to hold anything). */ skillHeld?: SkillHeld; /** crew#1145: the daemon's background skills ladder — the arm line waits for it. Absent = say it now. */ skillsSettled?: Promise; /** The docs root an ask's doc is read from. Default: the shared-default resolution * (`WICKED_INTERACTIVE_ROOT` › `~/wicked-interactive/docs`); the server wires the * per-project `interactiveRoot` setting through here so a project on its own root * resolves correctly. */ resolveDocsRoot?: (projectId: string | undefined) => string; /** Contract (c)'s cross-seam half: `true` while a DRAFT or EDIT run is in flight for the * doc. The server wires the sibling subscriptions' in-flight sets through here; the chat * seam's own in-flight runs are tracked internally. */ isDocBusy?: (documentId: string) => boolean; /** Queue-drain sweep cadence, ms (default 1000; tests shorten it). */ queueSweepMs?: number; /** How long a drained ask waits for the PREVIOUS revision's version to land before * proceeding on the stale head anyway, ms (default 60000; tests shorten it). */ landingGateMs?: number; /** Called after a launch that FILED the run into a project (chat.posted carried * `project_id`). Same wiring as the sibling seams: the server points this at the launch * route's post-commit half (membership index tag + membership.attached emit). */ onRunFiled?: (runId: string, projectId: string) => void; /** Diagnostics sink (default: console.error). */ log?: (message: string) => void; /** Error-level logger for connection-fatal subscriber errors (the /diagnostics ring folds it); defaults to `log`. */ logError?: (message: string) => void; } /** Handle for a running subscription. */ export interface InteractiveChatSubscription { stop(): Promise | void; /** The durable ledger (diagnostics / tests). */ ledger: InteractiveHandoffLedger; /** Documents with a chat run currently in flight (diagnostics / cross-seam wiring). */ inFlightDocs(): string[]; /** Asks queued behind a busy doc, per doc (diagnostics / tests). */ queuedCount(documentId: string): number; } /** * Arm the seam (it registers nothing: the run is the engine's `interactive-chat` preset), open a durable * `wicked.interactive.chat.posted` subscription, and answer each user ask on an existing * answerable doc (contract (b)) with a governed run that ends in `wicked.interactive.draft.completed` * (the service lands the revised full HTML as a generated version). * * Graceful degradation mirrors the sibling seams: a missing wicked-bus package or an * unopenable db LOGS and returns `null` — the daemon must still boot on a machine whose bus * is broken. */ export declare function startInteractiveChatSubscriber(adapter: CoreAdapter, opts?: InteractiveChatOptions): Promise; //# sourceMappingURL=chat-events.d.ts.map