TourController#startTour(script, {autoGates}) |
apps/workstation/view/TourController.mjs |
Plays one screenplay visibly; autoGates given sets the controller's gate mode (true = gates settle without a viewer), omitted keeps it |
A running playback returns the running promise and a caption |
class JSDoc, Workstation README |
TourController.spec.mjs gate-mode test; WorkstationFilmTourNL.spec.mjs viewer and autoGates arms |
TourController#startFilmTour(options) |
same |
startTour(fiveBeatFilmScript, options) |
as above |
README |
e2e film arms |
TourController#continueTour() |
same |
Resolves the pending gate with {applied: true, continued: 'viewer', prompt}; the resolving click is the user activation the next birth runs inside; returns false when nothing waits |
— |
README, TourToolbar Continue button |
unit gate test; e2e viewer arm (three Continue clicks) |
TourController#getPendingGate() |
same |
{prompt} while a gate waits, else null |
— |
JSDoc |
unit gate test; e2e hold test |
TourController#getTourState() |
same |
{caption, completedCount, gate, running, totalBeats} read from the root provider's tour.* keys — the transport oracle (the bridge resolves property paths only) |
null fields when unset |
JSDoc |
e2e polls |
TourController#getCueLog() |
same |
{cueErrors, cues, pending, vessels}: settled cues with applied / cancelled / reentered / documentsUnchanged / errors, promise pending-ness probes, compact vessel states — forensics without proof documents |
— |
JSDoc |
e2e diagnostics on timeout |
autoGates (controller field) |
same |
Gate mode; settable directly before Start by a transport (set_instance_properties), or through startTour |
false |
JSDoc |
e2e autoGates arm sets it through the fixture, then presses the ordinary button |
Gate cue {type: 'pause', ms: 0, cue: {type: 'gate', prompt}} |
apps/workstation/tour/fiveBeatFilm.mjs, cueVocabulary.mjs |
Holds the beat at the host settlement boundary until continueTour(); under autoGates settles at once with continued: 'auto' |
— |
screenplay header |
fiveBeatFilm.spec.mjs (a gate precedes every birth) |
Provider key tour.gatePrompt |
denseWorkstation.mjs initial state, TourToolbar binds |
A string while a gate waits (the Continue button's text and visibility), null otherwise |
null |
README |
unit gate test reads the provider |
| Cue settlement (fail closed) |
TourController#onTourBeat / #settleTourStep |
A cue receipt with errors, or applied: false without a cancelled/reentered terminal, or an un-applied terminal without proof.documentsUnchanged === true, fails the beat: the runner aborts before the next beat; the receipt and the cue error stay recorded |
— |
class JSDoc, README |
TourController.spec.mjs negative controls; Workspace.spec.mjs truth-binding tests |
| Cue receipt fields |
driver executors and controller cues |
applied, errors[], optional cancelled, reentered, continued, proof.{born, documentsUnchanged, …}, tabs (membership after restore/undo/redo), transactionId |
— |
JSDoc per cue method |
e2e assertFilmReceipt |
WORKSTATION_CUE_TYPES |
apps/workstation/tour/cueVocabulary.mjs |
The exact cue list executeCue handles; screenplays are validated against it |
unknown cue → false → failed cue |
file header |
fiveBeatFilm.spec.mjs |
Context
Operator, 2026-09-23 ~00:30 CEST: the tour inside
apps/workstation"looks boring" now that multi-window, tear-out and rails were polished, and a generated video for the README is wanted. Measured ondev@a186e92cfa:TourToolbar"Start dense tour" →TourController#startTour→TourRunnerwithworkstationTourScriptfromapps/workstation/tour/denseWorkstation.mjs) plays four scenes — dense overview, dropzone choreography, promote, theme handoff. None births a window.apps/workstation/tour/fiveBeatFilm.mjs(#16311 / #16376; witness authoritytest/playwright/e2e/workstation/WorkstationFiveBeatNL.spec.mjs) has eight scenes: cold open, showcase, tear-out, re-entry, second-window docking, stack return, perspectives + undo/redo, signature. Its scenes carrynarrationandbeats(strings) and no runnablesteps; nothing consumes it at runtime.apps/workstation/tour/NativeGestureDriver.mjs(executeTearOutStepwithcancel/reenter, the convert-while-dragging executor,executeStackReturnStep, the vessel birth / commit / retirement gates) — driven today only from Playwright.The Problem
Two authorities for one journey: the viewer sees the dense script; the film and its witness use another. The film's window scenes cannot run from a button as they stand, for one mechanical reason: a scripted vessel birth (
Neo.Main.windowOpenatapps/workstation/view/VesselWorkspace.mjs:1035) needs transient user activation — browsers blockwindow.openoutside a user gesture, and the app already reports that outcome fail-closed aslastVesselOpen.stage = 'windowOpen-false'. The runner's v1 vocabulary isop | pause | topology-assert | cross-window(src/ai/client/tourScript.mjs:66); it has no step that waits for the viewer.The Architectural Reality
Neo.ai.client.TourRunner: modesdemo(paced),record(script-authored pacing),spec(waits skipped);crossWindowExecutor.executeCrossWindowStep(step)returns a settlement receipt validated byvalidateCrossWindowResult;pauseis viewer pacing only — and the host settlement boundary (stepSettlement) already holds the next beat until a cue resolves.TourController#runTourSpec(script, options)already accepts any script;startTourhardcodes the dense one (TourController.mjs:67).DockService(capturePerspective/listPerspectives/restorePerspective), not in theOperationsvocabulary;'perspective'is aRESERVED_STEP_TYPESentry. Dock undo/redo run on one Group cursor through the Neural Link (#19063 is the proof).NEO_FILM_TAKE=1) records the same journey drive per page. One runnable script therefore serves both the viewer's tour and the README film.The Fix
{type: 'pause', ms: 0, cue: {type: 'gate', prompt}}: the controller publishes the prompt and holds the beat through its existing settlement boundary until the viewer clicks Continue;autoGatesresolves gates at once for replays and takes. The runner stays document-tier (delta from the first draft of this ticket, which put agatestep type into the runner: the app already owns the seam).apps/workstation/tour/fiveBeatFilm.mjs: every scene gainssteps: the cold open and showcase reuse the dense script'sop/pause+ cue shapes; the morph, the committed tear-out, the second-window conversion and the stack return becomegate+ real-pointer cues over the existing executors; perspectives and undo/redo become cues over the workspace's own seams; the signature is atopology-assert.narrationstays the caption source;beatsname what each step shows.TourController: the cue vocabulary (gate,tear-out,convert-while-dragging,stack-return,perspective-capture,perspective-restore,undo,redo),startFilmTour,continueTour, read-only oracles for whitebox specs.TourToolbar: a "Start film tour" button and a Continue affordance bound to the gate (a click or Space is the activation the birth needs; the gate caption says where to look).fiveBeatFilmScriptvalidates, names only cues the controller handles, gates every birth, keeps its budget, and runs its document tier twice with identical logs; e2e — the film plays from the toolbar under Playwright with real Continue clicks and the executors' own receipts for scenes 3–7; the dense tour and its specs stay untouched.Contract Ledger Matrix
Backfilled 2026-09-23 after review (PR #19083 RA-4), matching the shipped implementation.
TourController#startTour(script, {autoGates})apps/workstation/view/TourController.mjsautoGatesgiven sets the controller's gate mode (true = gates settle without a viewer), omitted keeps itTourController.spec.mjsgate-mode test;WorkstationFilmTourNL.spec.mjsviewer and autoGates armsTourController#startFilmTour(options)startTour(fiveBeatFilmScript, options)TourController#continueTour(){applied: true, continued: 'viewer', prompt}; the resolving click is the user activation the next birth runs inside; returnsfalsewhen nothing waitsTourToolbarContinue buttonTourController#getPendingGate(){prompt}while a gate waits, elsenullTourController#getTourState(){caption, completedCount, gate, running, totalBeats}read from the root provider'stour.*keys — the transport oracle (the bridge resolves property paths only)nullfields when unsetTourController#getCueLog(){cueErrors, cues, pending, vessels}: settled cues withapplied / cancelled / reentered / documentsUnchanged / errors, promise pending-ness probes, compact vessel states — forensics without proof documentsautoGates(controller field)set_instance_properties), or throughstartTourfalse{type: 'pause', ms: 0, cue: {type: 'gate', prompt}}apps/workstation/tour/fiveBeatFilm.mjs,cueVocabulary.mjscontinueTour(); underautoGatessettles at once withcontinued: 'auto'fiveBeatFilm.spec.mjs(a gate precedes every birth)tour.gatePromptdenseWorkstation.mjsinitial state,TourToolbarbindsnullotherwisenullTourController#onTourBeat/#settleTourSteperrors, orapplied: falsewithout acancelled/reenteredterminal, or an un-applied terminal withoutproof.documentsUnchanged === true, fails the beat: the runner aborts before the next beat; the receipt and the cue error stay recordedTourController.spec.mjsnegative controls;Workspace.spec.mjstruth-binding testsapplied,errors[], optionalcancelled,reentered,continued,proof.{born, documentsUnchanged, …},tabs(membership after restore/undo/redo),transactionIdassertFilmReceiptWORKSTATION_CUE_TYPESapps/workstation/tour/cueVocabulary.mjsexecuteCuehandles; screenplays are validated against itfalse→ failed cuefiveBeatFilm.spec.mjsAcceptance Criteria
autoGates; unit- and e2e-witnessed.fiveBeatFilmScriptvalidates and runs end to end — its document tier inspecmode (unit), the full screenplay including scenes 3–7 as the visible tour under Playwright with the same executor receiptsWorkstationFiveBeatNL.spec.mjsasserts (proof.born,reentered,applied, phase order).windowOpen-false) — measured in Chromium; a second engine is a residual owned by #15243.denseWorkstation.spec.mjsare unchanged and green, andWorkstationNL.spec.mjskeeps baseline parity with cleandev: the same 8/9, whose one red is #19086 (the dark splitter's:activeread at:1294, pre-existing, owned there). Restated 2026-09-23 after review: the control's red is deterministic on cleandevand is not this leaf's to fix.AC-5Struck 2026-09-23: the take's recording of the runnable screenplay is production work on the parent #15252, not this leaf's wiring; the screenplay header states the one-authority intent.NEO_FILM_TAKE=1records the film script's run (per-page videos), so the README film derives from the runnable script — one authority, stated in the file header.Out of Scope
Avoided Traps
windowOpen-falseis the receipt that proves the trap. The gate is the honest carriage, not a workaround.targetSecondsbudget the cut, andrecordmode pins the authored pacing.topology-assert: the document-tier replay executes no cues, so cue effects are witnessed through their receipts (each carries the tabs membership it produced).Related
#15252 (parent) · #16311 · #16376 · #17871 · #16498 · #19063 · #18461 · D#18730 · #15243
Sweeps: live latest-20 open at 2026-09-23T00:26:32Z — none; tour/film title search — #17871 and #16498 are the epic's open subs, neither makes the film runnable; A2A herd window — the film epic is mine,
[lane-claim]followed the filing; Memory Core: the 2026-08-08 treatment and Emmy's 2026-08-09 disposition (SCALE folded into the cold open, L4 narrowed to the two-window climax unless a bounded probe composes a third vessel) — this leaf implements that shape; own-assignment: #15252 (parent), #19055, #19059. Structure map: N/A (one new data-only sibling,cueVocabulary.mjs, beside the two screenplays).Origin Session ID: 610d2189-0b28-46e2-89f3-3c42eae5de21 (earlier in the same session: 28a95670-a9dc-4214-8387-189e996c16b6) Retrieval Hint:
query_raw_memories("Workstation film tour gate cue cross-window runnable fiveBeatFilm NativeGestureDriver windowOpen-false stored home")Ownership: self-assigned; the parent epic is mine.
🪢 Mnemosyne (Claude Fable 5.1 · Claude Code) · session 610d2189-0b28-46e2-89f3-3c42eae5de21