/** * The recording views (DES-walkthrough-proof §4.10: `api/demo.ts` renamed `api/recording.ts`) — the * Demo experience and the walkthrough, which share `parseChapters`, `parseMarkers` and * `containedPath`. * * ## Demo * * The Demo experience (wicked-studio#373, migration M9b): a demo of a local app, made by a governed * run of the built-in `demo` preset — the wicked-garden demo skill's plan → record → review. * * POST /projects/:id/demo mints the run's DEMO ROOT (a per-run directory on the daemon host), * writes the brief there, and launches `demo` THROUGH `POST /runs` * (one launch path), with the root as the run's only extra write root. * GET /runs/:id/demo what studio shows: the stage, the presenter script and chapter list * (the plan gate), per-chapter record progress, the contact sheets and * the reviewer's verdicts (the review gate), and the chaptered MP4. * GET /runs/:id/demo/file one file of the root (a contact sheet, the MP4 — with Range). * PUT /runs/:id/demo/script the presenter's edit of `script.md` while the plan gate is open. * POST /runs/:id/demo/export a GIF (`demo-video/demo.gif`) or a poster frame (`demo-video/poster.jpg`) * of the stitched MP4, encoded by ffmpeg with an ASYNC spawn and a * timeout (EP-C3): the daemon keeps answering while it encodes. * * The gates are the run's ordinary gates (`POST /runs/:id/gate`): approve, or `request_changes` with a * note — at the plan gate it re-runs `plan`, at the review gate it rewinds to `record` (re-record one * chapter). Nothing here decides a gate. * * Every file is read from the root the daemon minted for THIS run id, never from a path a caller or a * worker names: `path` is resolved inside it, symlinks included, and only the deliverable types studio * renders are served. * * ## Walkthrough (WT-W1) * * Every repo-bound run gets an EVIDENCE ROOT (`walkthroughRootDir`, minted by `CoreAdapter.launchRun` * and persisted by the engine as `session.evidence_root`). Under it, `author//` is the * walkthrough author's write dir and `/` is the PROOF ROOT only the jailed * `walkthrough_review` Tool writes. * * GET /runs/:id/walkthrough?step= the `WalkthroughView` of the newest (or the named) pair. * GET /runs/:id/walkthrough/file?step=&path= one file of that pair's proof root (Range for the MP4). * GET /runs/:id/walkthrough/storyline?step= the author's storyline, for "Edit the check" (#782). * * What the view reads from a proof root (the recorder's files, DES-walkthrough-proof §4.4): * * result.json `{overall, cause?, reason?, tree?, chapters: [{key, title?, verdict, takes?, * failed_at_sec?, failed_frame?, proves?, legs?, checks?: [{id, kind, sentence, * passed, at_sec, evidence?, vault_entry?, detail?}]}]}` — the engine itself writes * `{overall: INCONCLUSIVE, cause, reason, chapters: []}` when it cannot record. * progress.json `{state: 'starting_app' | 'recording' | 'judging', chapter?}` while it runs. * chapters.json the chapter list (the demo shape), before the judge has written verdicts. * demo-video/ `demo.mp4`, `poster.jpg`, `chapters.md` (markers), `segments//segment.mp4`. * * The proof root is read only when it is a plain directory directly under the evidence root (a link * planted at the step path is never followed — the engine's `check_proof_root` rule), and every file * inside it through `containedPath`. */ import type { FastifyInstance } from 'fastify'; import { z } from 'zod'; import type { DemoDocumentResponse, DemoChapter, DemoExportFormat, DemoFinding, DemoMarker, DemoStage, DemoView, WalkthroughView } from 'wicked-crew-api-types'; import type { CoreAdapter } from '../core/adapter.js'; import type { SessionView, WorkUnit } from '../core/types.js'; import type { AuditLog } from './audit.js'; import { type WalkthroughGate } from '../qe/walkthrough-acceptance.js'; import type { WalkthroughStepState } from 'wicked-crew-api-types'; /** The preset a demo run launches (wicked-core `builtin_presets`). */ export declare const DEMO_PRESET = "demo"; /** The biggest script studio shows or accepts back: a presenter script is prose. */ export declare const DEMO_SCRIPT_MAX_BYTES: number; /** * Where a NEW demo run's files live: one directory per run that FOLLOWS the daemon's state home * (`--db`) but sits OUTSIDE it — the worker must read the brief and write the video there, and * every registered state-home entry is denied to workers by core's Read fence * (`wicked-core src/state_home.rs` pushes `Read(//**)` per entry, `Edit(/**)` is blanket, and `extraWriteRoots` widen a separate boundary — they never lift those * denies), so the root cannot be a state-home entry. * * - `WICKED_DEMO_DIR` set: `/` — the more specific instruction; the boot preflight * keeps it outside the state home (`assertWickedRootsOutsideStateHome`). * - the DEFAULT state home (`~/.wicked-crew`): the historical `~/.wicked/demos/` — that * daemon's home IS the operator's (the crew#756 settings rule). * - any OTHER state home (a lane, a rig, a second install): `-demos/`, a * sibling of the state home — so a `--db` daemon never writes into the operator's home * (crew#813: every demo it launched landed in `~/.wicked/demos/` while all its other stores * followed `--db`, and that absolute home path was baked into the run's problem text). */ export declare function demoRootDir(runId: string): string; /** The demo roots of a NON-default state home: its sibling `-demos` (never inside it — see * {@link demoRootDir}). */ export declare function demoRootsBesideStateHome(): string; /** The historical location, `~/.wicked/demos/` — still the DEFAULT state home's; for any other * state home it is READ for demos recorded before crew#813 (dual-read), never written again. */ export declare function legacyDemoRootDir(runId: string): string; /** * The root an EXISTING demo run's files are read from (and, for its script edit and export, written * in place): this daemon's root when it exists, else the legacy home root when THAT exists (a demo a * non-default daemon recorded before crew#813), else this daemon's root (and a 404 for whatever is * asked of it). `WICKED_DEMO_DIR` set, or the default state home = that root alone. */ export declare function existingDemoRootDir(runId: string): Promise; /** * The demo root of each demo launch in flight, keyed by the run id the demo route minted. `POST /runs` * takes no write roots from its body; it reads this in-process entry, so the root is never something a * caller can ask for (the capture route's pattern). */ export declare const demoLaunchRoots: Map; export declare const DemoLaunchSchema: z.ZodObject<{ url: z.ZodEffects; audience: z.ZodString; show: z.ZodString; clisJson: z.ZodOptional; /** wicked-core#850 — the explicit reduced-assurance opt-in, forwarded to `POST /runs` only when `true`. */ reducedAssurance: z.ZodOptional; }, "strict", z.ZodTypeAny, { url: string; audience: string; show: string; clisJson?: string | undefined; reducedAssurance?: boolean | undefined; }, { url: string; audience: string; show: string; clisJson?: string | undefined; reducedAssurance?: boolean | undefined; }>; export declare const DemoScriptSchema: z.ZodObject<{ content: z.ZodString; }, "strict", z.ZodTypeAny, { content: string; }, { content: string; }>; export declare const DemoExportSchema: z.ZodEffects; /** The poster's frame, in seconds from the start. Poster only. */ atSec: z.ZodOptional; }, "strict", z.ZodTypeAny, { format: "gif" | "poster"; atSec?: number | undefined; }, { format: "gif" | "poster"; atSec?: number | undefined; }>, { format: "gif" | "poster"; atSec?: number | undefined; }, { format: "gif" | "poster"; atSec?: number | undefined; }>; /** * How long one encode may run before it is killed (EP-C3). A GIF re-encodes the whole video (the bound * interactive's encoder uses); a poster decodes one frame. Mutable only so a test can shorten it. */ export declare const DEMO_EXPORT_TIMEOUT_MS: Record; /** Interactive's GIF filter (`src/service/demo.js` `ffmpegGifEncoder`): two-pass palette so colours do not band, 10 fps, 720 px. */ export declare const DEMO_GIF_FILTER = "fps=10,scale=720:-1:flags=lanczos,split[s0][s1];[s0]palettegen=stats_mode=diff[p];[s1][p]paletteuse=dither=bayer:bayer_scale=3"; export declare const DEMO_FFMPEG_HINT = "install ffmpeg (e.g. `brew install ffmpeg` / `apt install ffmpeg`) on the daemon host, or set WICKED_FFMPEG to its path"; type FfmpegOutcome = { kind: 'ok'; } | { kind: 'missing'; } | { kind: 'timeout'; } | { kind: 'failed'; code: number | null; stderr: string; }; /** * Run ffmpeg without blocking the event loop: an async `spawn`, its stderr tail kept for the error, * SIGKILL past `timeoutMs`. Never `spawnSync` (review N6: a 180 s synchronous encode would stall * every route the daemon serves, `/health` included). */ export declare function runFfmpeg(args: string[], timeoutMs: number): Promise; /** The brief the demo run reads (the problem statement stays one short line: worker prompts are capped). */ export declare function demoBrief(root: string, b: { url: string; audience: string; show: string; }): string; /** A demo run: launched from the `demo` preset — read off the engine's plan state, or, before the * launch is scored and `team_plan` exists, off the run's identity (resolved from the launch record). */ export declare function isDemoRun(view: SessionView): boolean; /** Where the run stands, in the Demo experience's words. */ export declare function demoStage(view: SessionView): DemoStage; /** `chapters.json` → the chapter list, or `[]` when it is missing or not the plan's shape. */ export declare function parseChapters(raw: string | null): Omit[]; /** `chapters.md` (record.mjs's `| m:ss | Title |` table) → the MP4's chapter markers. */ export declare function parseMarkers(raw: string | null): DemoMarker[]; /** The reviewer's output → its verdict block (the LAST fenced ```json block), or nothing. */ export declare function parseReview(output: string | null): { verdict: 'accept' | 'changes' | null; findings: DemoFinding[]; }; /** Everything studio renders for a demo run, read from its root and its units. */ export declare function demoView(adapter: CoreAdapter, view: SessionView): Promise; /** Under the state home's registered `daemon-` prefix (state-home-registry.ts): a top-level name the worker * Read fence classifies, so the file never refuses a launch. Its temp sidecars share the prefix. */ export declare const DEMO_DOCS_FILE = "daemon-demo-documents.json"; type DemoDocLink = NonNullable; /** The run's linked document, when one was made (and still reads as one). */ export declare function demoDocumentLink(runId: string): Promise; /** One call to the project's bridge: JSON or raw bytes in, the status and parsed JSON body out. */ export type BridgeCall = (method: 'POST' | 'PUT', path: string, body: Buffer | Record) => Promise<{ status: number; body: Record; }>; export interface DemoDocumentDeps { /** The project's bridge (reuse-or-start), as a caller of its HTTP surface; null = no such project. */ bridgeFor: (projectId: string) => Promise; /** The per-segment read cap (default {@link SEGMENT_MAX_BYTES}). */ segmentMaxBytes?: number; } /** Make (once) and link the run's demo document. */ export declare function makeDemoDocument(view: SessionView, deps: DemoDocumentDeps): Promise<{ status: number; body: DemoDocumentResponse | { error: string; }; }>; /** `POST /runs/:id/demo/document`, registered where the bridge pool exists. One make per run at a time. */ export declare function registerDemoDocumentRoute(app: FastifyInstance, adapter: CoreAdapter, deps: DemoDocumentDeps): void; /** A single `Range: bytes=a-b` → the inclusive span, `null` for no header, `'bad'` for one we cannot serve. */ export declare function parseRange(header: string | undefined, size: number): { start: number; end: number; } | null | 'bad'; export declare function registerDemoRoutes(app: FastifyInstance, adapter: CoreAdapter): void; /** The catalog id of the walkthrough author (an evaluator agent step). */ export declare const WALKTHROUGH_PLAN_CATALOG = "walkthrough_plan"; /** The catalog id of the walkthrough recorder (the jailed, engine-run Tool step). */ export declare const WALKTHROUGH_REVIEW_CATALOG = "walkthrough_review"; export { WALKTHROUGH_AUTHOR_SUBDIR, walkthroughRootDir } from '../core/walkthrough-root.js'; /** One walkthrough pair of a run: the recorder unit and the author unit it records. */ interface WalkthroughPair { review: WorkUnit | null; plan: WorkUnit | null; } /** * The run's walkthrough pairs in plan order. A recorder pairs with the author its `depends_on` names, * else the nearest author before it (the engine's `author_step_for`); an author with no recorder after * it yet is a pair of its own (the walkthrough is still being written). */ export declare function walkthroughPairs(view: SessionView): WalkthroughPair[]; /** * The walkthrough half of `GET /runs/:id/acceptance` (WT-W2): one gate per `walkthrough_review` step * the requirement names (`phases`, step ids), and the per-creator-step check states from the NEWEST * walkthrough (only a sealed one proves anything). */ export declare function walkthroughAcceptance(adapter: Pick, view: SessionView, phases: string[]): Promise<{ gates: WalkthroughGate[]; steps: WalkthroughStepState[]; newest: WalkthroughGate | null; ownedByYou: boolean; }>; /** Everything studio renders for one walkthrough pair (the newest, or the one `step` names). */ export declare function walkthroughView(view: SessionView, step?: string, adapter?: Pick): Promise; /** The biggest storyline the operator may PUT (the demo module shape is a few KB). */ export declare const STORYLINE_MAX_BYTES: number; /** The storyline file the author writes, and crew's edit marker beside it (WT-W3). */ export declare const STORYLINE_FILE = "storyline.mjs"; export declare const STORYLINE_EDIT_FILE = "storyline.edit.json"; /** What crew writes beside an operator-edited storyline: who edited it, when, and what. */ export interface StorylineEditMarker { edited_by: 'human'; actor: string; at: string; sha256: string; /** The escalated unit the edit answers (its `ord`). */ ord: number; } /** A refusal whose message is safe to return (relative names only, never an absolute path). */ export declare class StorylineWriteError extends Error { } /** * WT-W3 (DES-walkthrough-proof §4.8 "Edit the check"): write the operator's storyline for the pair's * author step — `/author//storyline.mjs` plus `storyline.edit.json` * (`edited_by: "human"`) — atomically (temp file + rename), refusing any path that does not resolve * to a plain directory under the evidence root's `author/` (a planted link cannot redirect the write). */ export declare function writeStoryline(evidenceRoot: string, planStepId: string, text: string, marker: Omit): Promise; /** A read refusal whose message is safe to return (relative names only, never an absolute path). */ export declare class StorylineReadError extends Error { readonly code: 'read_refused' | 'too_large'; constructor(message: string, code: 'read_refused' | 'too_large'); } /** * #782: the storyline the operator is asked to edit — `/author//storyline.mjs` * — read the way {@link writeStoryline} writes it: every level lstat-checked (a planted link at * `author/`, `author/` or the file is refused, never followed), the directory's real path pinned * under the evidence root, the file at most {@link STORYLINE_MAX_BYTES}, and the opened inode re-checked * against the pinned path after the read. `null` when the author has not written one. `marker` is the * edit marker only when its `sha256` names this text (the recorder's rule) — a CANDIDATE: the author * directory is the worker's to write, so the route trusts it only with the daemon's own audit row. */ export declare function readStoryline(evidenceRoot: string, planStepId: string): Promise<{ text: string; sha256: string; marker: { at: string | null; } | null; } | null>; export interface WalkthroughRouteDeps { /** The run's open gate (the routes' shared resolution); `'no-log'` = this build cannot say; * `'unstable'` = live gate events kept landing while it was read (retry). */ resolveOpenGate?: (runId: string) => Promise<{ ord: number; } | null | 'no-log' | 'unstable' | 'ambiguous'>; audit?: AuditLog; } export declare function registerWalkthroughRoutes(app: FastifyInstance, adapter: CoreAdapter, deps?: WalkthroughRouteDeps): void; //# sourceMappingURL=recording.d.ts.map