@sublang/playbook 0.9.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +190 -151
  2. package/package.json +50 -6
  3. package/reference/sdlc/captain.md +102 -0
  4. package/reference/sdlc/captain.playbook/captain.fsm.d.ts +227 -0
  5. package/reference/sdlc/captain.playbook/captain.fsm.js +628 -0
  6. package/reference/sdlc/captain.playbook/captain.fsm.ts +851 -0
  7. package/reference/sdlc/captain.playbook/captain.gears.md +60 -0
  8. package/reference/sdlc/captain.playbook/captain.playbook.d.ts +23 -0
  9. package/reference/sdlc/captain.playbook/captain.playbook.js +1053 -0
  10. package/reference/sdlc/captain.playbook/captain.playbook.ts +1144 -0
  11. package/reference/sdlc/code.playbook/bin/playbook.js +158 -12
  12. package/reference/sdlc/code.playbook/bin/run.js +999 -0
  13. package/reference/sdlc/code.playbook/code.fsm.d.ts +11 -4
  14. package/reference/sdlc/code.playbook/code.fsm.introspect.d.ts +2 -2
  15. package/reference/sdlc/code.playbook/code.fsm.introspect.js +1 -1
  16. package/reference/sdlc/code.playbook/code.fsm.introspect.ts +6 -6
  17. package/reference/sdlc/code.playbook/code.fsm.js +334 -102
  18. package/reference/sdlc/code.playbook/code.fsm.ts +467 -180
  19. package/reference/sdlc/code.playbook/code.gears.md +11 -10
  20. package/reference/sdlc/code.playbook/code.playbook.d.ts +16 -19
  21. package/reference/sdlc/code.playbook/code.playbook.js +199 -488
  22. package/reference/sdlc/code.playbook/code.playbook.ts +327 -566
  23. package/reference/sdlc/code.playbook/code.registry.d.ts +0 -3
  24. package/reference/sdlc/code.playbook/code.registry.js +0 -3
  25. package/reference/sdlc/code.playbook/code.registry.ts +0 -6
  26. package/reference/sdlc/code.playbook/playbook-captain.d.ts +9 -4
  27. package/reference/sdlc/code.playbook/playbook-captain.js +889 -210
  28. package/reference/sdlc/code.playbook/playbook-captain.ts +1136 -257
  29. package/reference/sdlc/code.playbook/playbook.config.template.yaml +21 -0
  30. package/reference/sdlc/discuss.playbook/discuss.fsm.d.ts +396 -0
  31. package/reference/sdlc/discuss.playbook/discuss.fsm.js +2066 -0
  32. package/reference/sdlc/discuss.playbook/discuss.fsm.ts +2464 -0
  33. package/reference/sdlc/discuss.playbook/discuss.gears.md +251 -0
  34. package/reference/sdlc/discuss.playbook/discuss.playbook.d.ts +113 -0
  35. package/reference/sdlc/discuss.playbook/discuss.playbook.js +1514 -0
  36. package/reference/sdlc/discuss.playbook/discuss.playbook.ts +1926 -0
  37. package/reference/sdlc/discuss.playbook/discuss.registry.d.ts +58 -0
  38. package/reference/sdlc/discuss.playbook/discuss.registry.js +97 -0
  39. package/reference/sdlc/discuss.playbook/discuss.registry.ts +153 -0
  40. package/slc/gears2fsm.md +557 -57
  41. package/slc/link.md +1165 -89
  42. package/slc/optimize.md +92 -0
  43. package/slc/text2gears.md +255 -7
  44. package/src/runtime.d.ts +146 -3
  45. package/src/runtime.ts +201 -2
  46. package/src/xstate-playbook-runtime.d.ts +201 -0
  47. package/src/xstate-playbook-runtime.js +2058 -0
  48. package/src/xstate-playbook-runtime.ts +2792 -0
  49. package/src/xstate-runtime.d.ts +95 -0
  50. package/src/xstate-runtime.js +1258 -0
  51. package/src/xstate-runtime.ts +1816 -0
package/slc/link.md CHANGED
@@ -8,28 +8,30 @@ Compiles the [gears2fsm](gears2fsm.md) artifact into a **`PlaybookRuntime`**: a
8
8
 
9
9
  - Drives the FSM.
10
10
  - Classifies Boss input into typed events.
11
- - Runs the Captain-actor against the playbook's players.
12
- - Adjudicates player output into FSM guards.
11
+ - Runs direct-Captain, delegated-player, and nested-playbook actors.
12
+ - Executes deterministic script actors locally, without any agent.
13
+ - Adjudicates Captain and player output into FSM guards.
13
14
  - Surfaces transitions as status/telemetry.
14
15
 
15
16
  The runtime is invoked through the stable `PlaybookPorts` contract.
16
- Presentation layers (tmux-play, web, CLI, tests) implement the four ports once and inherit every playbook.
17
+ Presentation layers (tmux-play, web, CLI, tests) implement the six ports once
18
+ and inherit every playbook.
17
19
 
18
20
  - Source: an XState v5 machine artifact (`.fsm.ts`) produced by gears2fsm.
19
21
  - Target: a `PlaybookRuntime` factory module — TypeScript, host-agnostic.
20
22
 
21
23
  Hosts are out of scope for this phase.
22
- Each host has a small adapter (~30 lines) that loads a `PlaybookRuntime` module and supplies the host's primitives as `PlaybookPorts`.
24
+ Each host has an adapter that loads a `PlaybookRuntime` module and supplies the host's primitives as `PlaybookPorts`.
23
25
  The adapter shall speak only `PlaybookPorts` to the runtime and shall not leak host types back into it.
24
26
 
25
27
  The link compiler shall not modify the FSM artifact and shall not re-derive Captain prompts, result keys, or guard semantics — those are fixed by the FSM.
26
28
 
27
29
  ## Formats
28
30
 
29
- | Role | Format | Extension |
30
- | --- | --- | --- |
31
- | source | fsm | .ts |
32
- | target | playbook | .ts |
31
+ | Role | Format | Extension |
32
+ | ------ | -------- | --------- |
33
+ | source | fsm | .ts |
34
+ | target | playbook | .ts |
33
35
 
34
36
  ## PlaybookRuntime contract
35
37
 
@@ -37,11 +39,80 @@ The emitted module shall default-export a factory of the following shape:
37
39
 
38
40
  ```typescript
39
41
  interface PlaybookRuntime {
40
- init(ports: PlaybookPorts): Promise<void>;
41
- handleBossInput(turn: { text: string; signal: AbortSignal }): Promise<void>;
42
+ init(session: PlaybookSession): Promise<void>;
43
+ handleBossInput(turn: {
44
+ text: string;
45
+ signal: AbortSignal;
46
+ }): Promise<PlaybookRunResult>;
47
+ resumePlaybookCall(input: {
48
+ callId: string;
49
+ result: PlaybookCallResult;
50
+ signal: AbortSignal;
51
+ }): Promise<PlaybookRunResult>;
42
52
  dispose(): Promise<void>;
43
53
  }
44
54
 
55
+ interface PlaybookSession {
56
+ sessionId: string;
57
+ playbookId: string;
58
+ rootSessionId: string;
59
+ parentSessionId?: string;
60
+ parentCallId?: string;
61
+ depth: number;
62
+ ports: PlaybookPorts;
63
+ }
64
+
65
+ type JsonValue =
66
+ | null
67
+ | boolean
68
+ | number
69
+ | string
70
+ | readonly JsonValue[]
71
+ | { readonly [key: string]: JsonValue };
72
+
73
+ interface NormalizedError {
74
+ name: string;
75
+ message: string;
76
+ stack?: string;
77
+ }
78
+
79
+ type PlaybookStateValue =
80
+ | string
81
+ | { readonly [key: string]: PlaybookStateValue };
82
+
83
+ interface PlaybookState {
84
+ value: PlaybookStateValue;
85
+ activeStateIds: readonly string[];
86
+ tags: readonly string[];
87
+ status: 'active' | 'done' | 'error' | 'stopped';
88
+ quiescent: boolean;
89
+ stateId?: string;
90
+ }
91
+
92
+ interface PlaybookPendingCall {
93
+ callId: string;
94
+ playbookId: string;
95
+ childSessionId: string;
96
+ }
97
+
98
+ type PlaybookRunResult =
99
+ | { outcome: 'quiescent' | 'no-action'; state: PlaybookState }
100
+ | {
101
+ outcome: 'failed' | 'aborted';
102
+ state: PlaybookState;
103
+ error?: NormalizedError;
104
+ }
105
+ | {
106
+ outcome: 'terminal';
107
+ state: PlaybookState;
108
+ output?: JsonValue;
109
+ }
110
+ | {
111
+ outcome: 'suspended';
112
+ state: PlaybookState;
113
+ pendingCall: PlaybookPendingCall;
114
+ };
115
+
45
116
  type PlaybookRuntimeFactory<Options = unknown> = (
46
117
  options: Options,
47
118
  ) => PlaybookRuntime;
@@ -53,46 +124,330 @@ export default function createPlaybookRuntime(
53
124
 
54
125
  The default export conforms to `PlaybookRuntimeFactory<PlaybookRuntimeOptions>`, the generic factory type the shared contract module exposes (§Output).
55
126
 
56
- `init` receives the host's ports, constructs the XState actor with FSM `input` derived from `options`, and starts the actor.
127
+ `init` receives the host-owned playbook session identity and ports, constructs the XState actor with FSM `input` derived from `options`, and starts the actor.
57
128
  The runtime owns the actor for its lifetime; `handleBossInput` runs one turn, and `dispose` stops the actor and drains pending port emissions.
58
-
59
- `PlaybookRuntimeOptions` is host-agnostic and carries only *per-run* knobs such as identity strings (e.g., model names a playbook substitutes into prompt placeholders) and strategy overrides the linker exposes.
129
+ The host shall generate a non-empty, globally unique `sessionId` for each init-to-dispose lifecycle and shall supply the stable registry or authored playbook id as `playbookId`.
130
+ The runtime shall validate non-empty session, playbook, and root ids, a safe
131
+ non-negative integer depth, root identity (`depth === 0` and
132
+ `rootSessionId === sessionId` with no parent fields), and child identity
133
+ (`depth > 0` with non-empty parent session and call ids). It shall copy those
134
+ identity scalars and the port references into its own immutable record rather
135
+ than retaining the caller's mutable session object. A child `sessionId` shall
136
+ differ from both its `rootSessionId` and `parentSessionId`.
137
+
138
+ Run outcomes are exact: `no-action` means no FSM event was sent;
139
+ `quiescent` means a non-failure parked/idle state; `failed` means the FSM is in
140
+ a recoverable failure state; `terminal` means top-level final with optional
141
+ JSON output; `aborted` means the turn signal ended work; and `suspended` means
142
+ exactly one `pendingCall` is active.
143
+ Control-plane exceptions reject the runtime method rather than masquerade as a
144
+ recoverable workflow `failed` result.
145
+
146
+ `PlaybookRuntimeOptions` is host-agnostic and carries only _per-run_ knobs such as identity strings (e.g., model names a playbook substitutes into prompt placeholders) and strategy overrides the linker exposes.
60
147
  The link compiler emits a typed options interface per playbook based on the FSM's `CodingInput` (or equivalent).
61
-
62
- Player binding is a *linker-time* input baked into the emitted runtime by default.
148
+ The CLI's absence of `--link-option` values does not mean that
149
+ `PlaybookRuntimeOptions` is empty. CLI link options are compile-time inputs;
150
+ the runtime options interface is independently derived from every required FSM
151
+ input field that is not supplied by `PlaybookSession` or another linker-owned
152
+ source. In particular, a required immutable `enabledPlaybooks` catalog shall
153
+ remain a required readonly runtime option passed through to machine input; the
154
+ linker shall neither invent an empty catalog nor require it to be baked into a
155
+ CLI link option.
156
+
157
+ Player binding is a _linker-time_ input baked into the emitted runtime by default.
63
158
  A linker may also expose it via `PlaybookRuntimeOptions` for per-run remapping; the contract requires only that the runtime ship with a deterministic binding it applies at every `callPlayer` site.
64
159
 
65
160
  ## PlaybookPorts contract
66
161
 
67
162
  ```typescript
68
163
  interface PlaybookPorts {
69
- callPlayer(playerId: string, prompt: string, signal: AbortSignal):
70
- Promise<PlayerResult>;
71
- callJudge(prompt: string, signal: AbortSignal):
72
- Promise<string>;
164
+ callPlayer(
165
+ playerId: string,
166
+ prompt: string,
167
+ signal: AbortSignal,
168
+ options: PlayerCallOptions,
169
+ ): Promise<PlayerResult>;
170
+ callCaptain(
171
+ prompt: string,
172
+ signal: AbortSignal,
173
+ options: CaptainCallOptions,
174
+ ): Promise<CaptainResult>;
175
+ callJudge(prompt: string, signal: AbortSignal): Promise<string>;
176
+ callPlaybook(
177
+ request: PlaybookCallRequest,
178
+ signal: AbortSignal,
179
+ ): Promise<PlaybookCallStart>;
73
180
  emitStatus(message: string, data?: unknown): Promise<void>;
74
181
  emitTelemetry(event: { topic: string; payload: unknown }): Promise<void>;
75
182
  }
76
183
 
184
+ interface PlayerCallOptions {
185
+ resume: string | false;
186
+ }
187
+
188
+ interface CaptainCallOptions {
189
+ visibility: 'visible' | 'hidden';
190
+ resume: string | false;
191
+ allowedTools?: readonly string[];
192
+ }
193
+
77
194
  interface PlayerResult {
195
+ status: 'ok' | 'aborted' | 'error';
196
+ resumeToken?: string;
197
+ finalText?: string;
198
+ error?: string;
199
+ }
200
+
201
+ interface CaptainResult {
78
202
  status: 'ok' | 'aborted' | 'error';
79
203
  finalText?: string;
80
204
  error?: string;
81
205
  }
206
+
207
+ interface PlaybookCallRequest {
208
+ callId: string;
209
+ playbookId: string;
210
+ text: string;
211
+ }
212
+
213
+ type PlaybookCallResult =
214
+ | {
215
+ status: 'ok';
216
+ playbookId: string;
217
+ childSessionId: string;
218
+ state?: PlaybookState;
219
+ output?: JsonValue;
220
+ }
221
+ | {
222
+ status: 'aborted';
223
+ playbookId: string;
224
+ childSessionId?: string;
225
+ state?: PlaybookState;
226
+ error?: NormalizedError;
227
+ }
228
+ | {
229
+ status: 'error';
230
+ playbookId: string;
231
+ childSessionId?: string;
232
+ state?: PlaybookState;
233
+ error: NormalizedError;
234
+ };
235
+
236
+ type PlaybookCallStart =
237
+ | { state: 'settled'; result: PlaybookCallResult }
238
+ | { state: 'suspended'; childSessionId: string };
82
239
  ```
83
240
 
84
- `PlayerResult` mirrors cligent's `PlayerRunResult` shape ([TMUX-033](https://github.com/sublang-ai/cligent/blob/main/specs/user/tmux-play.md#tmux-033)), so a tmux-play port adapter is direct assignment.
241
+ `PlayerResult` mirrors the status, resume token, final text, and error fields of cligent's `PlayerRunResult` ([TMUX-033](https://github.com/sublang-ai/cligent/blob/main/specs/user/tmux-play.md#tmux-033)).
85
242
  The runtime treats `status !== 'ok'` as a player failure and routes it through the FSM's error path (§Abort).
86
243
 
244
+ `callCaptain` runs a direct-Captain FSM actor against the host's Captain
245
+ agent. The linked runtime shall pass
246
+ `{ visibility: 'visible', resume: false }` for authored workflow calls so
247
+ XState context, rather than an agent conversation, owns workflow continuity.
248
+ The tool restriction is source-owned: the runtime shall additionally pass
249
+ `allowedTools: []` exactly when the GEARS source itself restricts the acting
250
+ Captain from tools (a routing-only Captain policy such as the default generic
251
+ Captain). A transformation-performing Captain — e.g. a compiler phase compiled
252
+ from a transformation-spec source, whose behavior writes a declared target
253
+ artifact — works through the host Captain's own tools, so its calls shall
254
+ carry no `allowedTools` restriction.
255
+ Accordingly, `CaptainCallOptions.allowedTools` is optional: an explicit empty
256
+ array requests a tool-free call, while omission preserves the host Captain's
257
+ configured tools.
258
+ `CaptainResult` carries no resume token or player-continuation selection.
259
+ A non-`ok`
260
+ result, or an `ok` result without `finalText`, shall reject the actor through
261
+ the FSM's error path.
262
+ Outside a signal-driven abort, those invalid direct-Captain results are
263
+ latched control-plane failures. The runtime shall let the actor take `onError`,
264
+ drive it to quiescence, and drain ordered emissions before the public method
265
+ rejects with the original failure. It shall never translate either case into
266
+ a recoverable workflow `{ outcome: 'failed' }` result. If the combined signal
267
+ has aborted, an aborted host result follows the ordinary abort settlement
268
+ instead of being promoted to a control-plane failure.
269
+
270
+ Every linked runtime owns a map from resolved player id to its latest non-empty `resumeToken`.
271
+ Before reading a resolved direct-Captain or delegated-player result, the
272
+ runtime shall validate, detach, and freeze it through the shared
273
+ `validateCaptainResult` or `validatePlayerResult` helper. The accepted object
274
+ shape is exact: only the declared status and optional string fields are
275
+ allowed, JSON-unsafe members reject, and caller mutation after resolution
276
+ cannot change trace evidence or player continuity. Validation happens before
277
+ adopting a resume token or reading final text.
278
+ The first call to each player in a playbook session shall pass `{ resume: false }`; later calls shall pass the exact stored token.
279
+ After a resolved call, the runtime shall replace the token when the result carries one or clear it when absent before interpreting `status`; a rejected call with no result leaves the prior token unchanged.
280
+ After awaiting a host Captain or player promise, the runtime shall re-check the
281
+ combined invocation/public-boundary signal before validating the result,
282
+ adopting a resume token, or emitting a successful finish. A host promise that
283
+ ignores cancellation and resolves late shall be paired as aborted and shall
284
+ not mutate continuity or masquerade as success.
285
+ The map survives actor reconstruction inside the same runtime and is discarded at `dispose`.
286
+ The runtime shall keep an in-flight set keyed by resolved player id and reject
287
+ a second concurrent call to the same id before crossing the host port. Calls
288
+ to distinct resolved player ids may overlap.
289
+
87
290
  `callJudge` returns free-form text.
88
291
  The runtime parses it per the state's adjudication strategy (§Captain adjudication).
89
292
  One port serves both classifier and adjudicator — they vary only in prompt.
293
+ Concurrent `callJudge` attempts within one linked runtime shall pass through
294
+ one abort-aware local FIFO. After the host promise resolves, the runtime shall
295
+ require a string reply and re-check the combined signal before tracing or
296
+ parsing success, so a non-cooperative late judge cannot outlive cancellation.
297
+ The host shall serialize `callCaptain` and `callJudge` together through one
298
+ shared abort-aware concurrency-one FIFO because both use the same single-flight
299
+ Captain lane, even when distinct player ports overlap [[4]]. A direct Captain
300
+ call's subsequent adjudication shall enter that same queue only after the
301
+ visible call has settled; the linked runtime shall not hold one queue lease
302
+ while requesting the other port.
303
+ Use one shared `PQueue({ concurrency: 1 })` for the individual host
304
+ `callCaptain` and `callJudge` promises. Do not pass an invocation or public
305
+ boundary signal as `PQueue.add(..., { signal })`: PQueue may release a running
306
+ slot as soon as that signal aborts even though a non-cooperative host promise
307
+ is still executing, which permits overlap. Instead check the combined signal
308
+ inside the queued task before crossing the host port, await the host promise
309
+ without releasing the queue lease, and check the signal again afterward.
310
+
311
+ `callPlaybook` starts a function-style child call.
312
+ The caller runtime supplies its stable call id and the XState invocation's
313
+ lifetime signal.
314
+ The host drives the child's initial text before resolving the port with either
315
+ an immediate settled result or a suspended child session.
316
+ Suspension is resumed later through `PlaybookRuntime.resumePlaybookCall`; the
317
+ port promise itself shall not remain pending across Boss turns.
90
318
 
91
319
  `emitStatus` is human-readable; `emitTelemetry` is structured.
92
320
  Both are async and shall be ordered, awaited, and never-dropped; the runtime awaits each emission before issuing the next.
93
321
 
94
322
  The runtime never speaks to LLMs directly and never touches host types beyond `PlaybookPorts`.
95
323
 
324
+ ## Playbook trace
325
+
326
+ Every linked runtime shall emit a boundary-complete, ordered trace through `emitTelemetry` topic `playbook.trace`.
327
+ Each payload shall carry `schemaVersion: 2`, the immutable session identity and
328
+ causality, a contiguous one-based `sequence`, a Unix-millisecond `timestamp`, a
329
+ trace `type`, event `payload`, and the runtime-local `turnId` / paired `callId`
330
+ where applicable.
331
+
332
+ ```typescript
333
+ type PlaybookTraceType =
334
+ | 'session.started'
335
+ | 'boss.input.received'
336
+ | 'judge.call.started'
337
+ | 'judge.call.finished'
338
+ | 'player.call.started'
339
+ | 'player.call.finished'
340
+ | 'captain.call.started'
341
+ | 'captain.call.finished'
342
+ | 'playbook.call.started'
343
+ | 'playbook.call.finished'
344
+ | 'fsm.transition'
345
+ | 'status.emitted'
346
+ | 'boss.input.settled'
347
+ | 'session.disposed';
348
+
349
+ interface PlaybookTraceEvent {
350
+ schemaVersion: 2;
351
+ sessionId: string;
352
+ playbookId: string;
353
+ rootSessionId: string;
354
+ parentSessionId?: string;
355
+ parentCallId?: string;
356
+ depth: number;
357
+ sequence: number;
358
+ timestamp: number;
359
+ type: PlaybookTraceType;
360
+ turnId?: number;
361
+ callId?: string;
362
+ payload: JsonValue;
363
+ }
364
+ ```
365
+
366
+ The trace types are `session.started`, `boss.input.received`,
367
+ `judge.call.started`, `judge.call.finished`, `player.call.started`,
368
+ `player.call.finished`, `captain.call.started`, `captain.call.finished`,
369
+ `playbook.call.started`,
370
+ `playbook.call.finished`, `fsm.transition`, `status.emitted`,
371
+ `boss.input.settled`, and `session.disposed`.
372
+ Call pairs carry exact prompts and replies, normalized failures, actor and state
373
+ identity, and their boundary-specific options.
374
+ Direct-Captain start and finish payloads shall carry `allowedTools` exactly when
375
+ the originating `CaptainCallOptions` selects it and shall omit the member when
376
+ the call preserves the host Captain's configured tools.
377
+ `session.started` and `session.disposed` carry their descriptor as top-level
378
+ `state` and its singular `stateId` when present. Every judge start and finish
379
+ carries the working snapshot's singular `stateId` when one exists;
380
+ classification uses the current descriptor and adjudication uses the invoking
381
+ actor input. The default Captain always has such a singular id, while a
382
+ parallel snapshot may omit it. Every judge finish also carries
383
+ `status: 'ok' | 'aborted' | 'error'`. Every `status.emitted` carries the
384
+ described top-level `state` and its singular `stateId` when present, as well as
385
+ its message and optional data; consumers shall not have to recover state
386
+ identity from a nested ad hoc object.
387
+ Judge results use `reply`; player start and finish payloads both carry the
388
+ selected `resume`; Captain start and finish payloads both carry
389
+ the exact composed prompt, `visibility: 'visible'`, the direct invocation's
390
+ `stateId` and `sourceItem`, and no player resume selection or resume token;
391
+ judge `purpose` is
392
+ `boss-input-classification`, `player-output-adjudication`, or
393
+ `captain-output-adjudication`; and every error uses
394
+ `{ name, message, stack? }` rather than a raw string or `Error` instance.
395
+ The Captain finish payload shall preserve the exact `CaptainResult` status and
396
+ final text when present, while carrying any failure in normalized form.
397
+ An `ok` result without `finalText` therefore retains status `ok` but also
398
+ carries the normalized missing-text failure that makes the actor reject.
399
+ If the Captain port rejects before returning a result, the finish instead
400
+ carries explicit `status: 'aborted'` when the combined signal has aborted or
401
+ `status: 'error'` otherwise. A finish boundary never omits status merely
402
+ because there was no structured host result.
403
+ The pair obligation also applies when a port promise rejects or throws: the
404
+ linked runtime shall emit and drain one normalized finish boundary before it
405
+ propagates the failure. No started call boundary may be left without its
406
+ matching finished boundary.
407
+ If a started-boundary sink records the event and then rejects, the runtime
408
+ shall make one best-effort normalized error-finish attempt with the same call
409
+ id and then reject the original start error. It shall not retry either event or
410
+ let a failure of that finish attempt replace the start error.
411
+ When a call boundary carries `callId`, that id shall be unique within the
412
+ runtime session. A stable FSM `stateId` is identity metadata in the payload,
413
+ not a call id and shall not be reused as one across repeated invocations.
414
+ Optional trace and run-result members shall be omitted when absent; the runtime
415
+ shall not create own `turnId`, `callId`, parent identity, output, or error
416
+ properties with value `undefined` and then rely on JSON serialization to drop
417
+ them.
418
+ A `boss.input.settled` payload shall project the complete structured run
419
+ result: its outcome must be one of the `PlaybookRunResult` discriminants (never
420
+ an invented `error` outcome), and it shall include `state`, singular
421
+ `stateId`, `pendingCall`, `output`, and normalized `error` whenever the matching
422
+ result arm carries them.
423
+ One runtime-owned concurrency-one emission queue shall serialize every trace,
424
+ human status, and state telemetry call. Sequence allocation and enqueueing
425
+ shall occur atomically, and every public method shall drain that queue before
426
+ resolving or rejecting. A state transition emission queued on entry shall be
427
+ observed before the invoked boundary's `*.started` event, even when a host
428
+ delays `emitTelemetry`.
429
+ The linked module shall use `PQueue({ concurrency: 1 })` from `p-queue` for
430
+ this ordering and drain it with `onIdle()` rather than recreate a promise-queue
431
+ implementation in every generated artifact.
432
+ An XState inspection callback shall synchronously enqueue the transition
433
+ trace, state telemetry, status trace, and human status in that order before it
434
+ returns. `emitStatus` likewise enqueues its trace and port emission in the same
435
+ synchronous call. Do not enqueue state telemetry or the status port from a
436
+ `trace(...).then(...)` continuation: the queue can become momentarily idle,
437
+ letting an invoked actor's `await drain()` overtake those dependent enqueues.
438
+ All validation happens before these synchronous enqueues; later sink failures
439
+ are caught into the appropriate latch without changing their queue position.
440
+ FSM trace events carry the same transition, pending-question, and normalized-error fields as state telemetry.
441
+ Trace emissions are awaited and sequenced before the boundary operation or human status/state telemetry they describe.
442
+ Every event in one session carries the same root/parent/depth identity.
443
+ A parent call start precedes its child `session.started`; the child's
444
+ `session.disposed` precedes the parent call finish.
445
+ Parallel call finishes may occur in either order, so consumers shall use call
446
+ ids for pairing and sequence for the observed total order.
447
+
448
+ This trace covers everything observable through `PlaybookRuntime`; host-specific adapter streaming remains in the host record stream.
449
+ Trace payloads never become Boss-visible status or prompt text.
450
+
96
451
  ## Linker inputs
97
452
 
98
453
  The link compiler shall accept:
@@ -101,6 +456,9 @@ The link compiler shall accept:
101
456
  - A **player binding** mapping GEARS players (declared in the
102
457
  [text2gears](text2gears.md#players) source) to opaque player-identifier
103
458
  strings.
459
+ Where no binding is supplied, the linker shall apply the default
460
+ binding — each player to its lowercased name (e.g. `Coder` → `coder`)
461
+ — and record the applied binding in the emitted header.
104
462
  - An **adjudication strategy** (default: LLM-judge per state) and a
105
463
  **Boss-event mapping** (default: free-text judge classification).
106
464
  Both strategies are host-agnostic.
@@ -109,12 +467,21 @@ The host's identity does not enter compilation; the linked module runs unchanged
109
467
 
110
468
  ## Player binding
111
469
 
112
- Each GEARS state names exactly one player (`invoke.input.player`).
113
- The linker shall map every named player to a `playerId` string used in `PlaybookPorts.callPlayer(playerId, …)`.
470
+ Each delegated GEARS state names exactly one player
471
+ (`player` actor `invoke.input.player`).
472
+ The linker shall map every named player to a `playerId` string used in
473
+ `PlaybookPorts.callPlayer(playerId, …)`.
114
474
  The host adapter routes that opaque string to its concrete primitive.
475
+ Every direct-Captain and delegated-player invocation shall also carry its
476
+ working leaf's explicit
477
+ `stateId`; a linked runtime shall use that field for call identity and shall
478
+ not infer one leaf from a structured root snapshot.
479
+ Direct `captain` actor states bypass player binding and call
480
+ `PlaybookPorts.callCaptain`; the linker shall not synthesize a player id named
481
+ `captain` for them.
115
482
 
116
483
  For composite players declared with aliases (e.g., `Committer = Coder | Reviewer`), the linker shall resolve the alias **per source item**.
117
- Resolution inspects the `CaptainInput` fields populated at that state:
484
+ Resolution inspects the `PlayerInput` fields populated at that state:
118
485
 
119
486
  - If only one `<playerName>Player` field is present, bind to that player.
120
487
  - If multiple are present, prefer the first-listed alternative in the alias declaration order.
@@ -122,20 +489,28 @@ Resolution inspects the `CaptainInput` fields populated at that state:
122
489
 
123
490
  Resolution shall be deterministic and recorded in the emitted module so future maintainers can audit it without re-running the linker.
124
491
 
125
- The linker shall not invent player identifiers and shall not silently collapse aliases at the FSM level — composite players keep their `player: 'Committer'` value on `CaptainInput`; resolution decides only the `callPlayer` invocation.
492
+ The linker shall not invent player identifiers beyond the recorded default
493
+ binding, and shall not silently collapse aliases at the FSM level — composite
494
+ players keep their `player: 'Committer'` value on `PlayerInput`; resolution
495
+ decides only the `callPlayer` invocation.
126
496
 
127
497
  ## Player prompt composition
128
498
 
129
- The runtime shall compose the actual player prompt from the state's `CaptainInput`.
499
+ The runtime shall compose the actual player prompt from the state's
500
+ `PlayerInput`.
130
501
  `input.prompt` is the GEARS-derived domain prompt body and shall not be mutated, re-flowed, or treated as a place to store framework control instructions.
131
502
 
132
- The composer may prepend structured labelled blocks from typed `CaptainInput` fields the FSM exposes (for example `Boss intent:`, `Review items:`, `Rebuttals:`, or `Task description:`).
503
+ The composer may prepend structured labelled blocks from typed `PlayerInput`
504
+ fields the FSM exposes (for example `Boss intent:`, `Review items:`,
505
+ `Rebuttals:`, or `Task description:`).
133
506
  Those blocks are outside the domain prompt body.
134
507
 
135
508
  The composer shall not inject a player-visible Boss-question instruction.
136
509
  Boss-question detection is adjudicator-facing: it comes from the state's `needsBossReply` result description, not from extra prompt text.
137
510
 
138
- When `CaptainInput` carries both `pendingBossQuestion` and `bossReply`, the composer shall prepend the continuation preamble and labelled Q&A blocks before ordinary structured blocks and before the domain prompt body:
511
+ When `PlayerInput` carries both `pendingBossQuestion` and `bossReply`, the
512
+ composer shall prepend the continuation preamble and labelled Q&A blocks before
513
+ ordinary structured blocks and before the domain prompt body:
139
514
 
140
515
  ```text
141
516
  You previously paused this task to ask Boss a question; Boss has now replied. Continue the same task using the reply below.
@@ -145,52 +520,222 @@ Boss question:
145
520
 
146
521
  Boss reply:
147
522
  <bossReply>
523
+
148
524
  ```
149
525
 
150
526
  The continuation preamble is framework text supplied by the runtime.
151
527
  It is not part of the GEARS blockquote and shall not appear in `invoke.input.prompt`.
528
+ The composer shall retain the blank line after the Boss reply before the next
529
+ structured block or domain prompt, producing exactly two newline characters at
530
+ that boundary.
531
+ When implementing the prefix as an array joined with `"\n"`, the array needs
532
+ two trailing empty strings after `bossReply`; one trailing empty string emits
533
+ only one newline and is nonconformant. Equivalently, append `"\n\n"` exactly
534
+ once before the following block or domain body.
535
+
536
+ ## Captain prompt composition
537
+
538
+ The runtime shall compose a direct Captain prompt from the state's
539
+ `CaptainInput` under the same prompt-integrity rules: `input.prompt` remains the
540
+ verbatim GEARS domain body, while specific typed fields may be supplied as
541
+ labelled blocks and substituted for their declared placeholders.
542
+ It shall not introduce a player binding or player resume instruction.
543
+ String fields substitute verbatim. Arrays and objects such as the sanitized
544
+ enabled-playbook catalog, remaining plan, and completed child results shall be
545
+ validated as JSON-safe and rendered as deterministic JSON; they shall never be
546
+ coerced through default JavaScript string conversion or expose untyped context.
547
+ Deterministic rendering shall sort object keys lexicographically at every
548
+ depth while preserving array order, so equivalent JSON values produce the same
549
+ prompt independent of host property insertion order.
550
+ At construction, a structured host-owned catalog shall be validated against its
551
+ declared exact entry shape, copied, and frozen recursively so later caller
552
+ mutation or extra properties cannot alter a prompt or machine decision.
553
+ For the default Captain catalog, every entry has exactly the own enumerable
554
+ data keys `id`, `command`, and `intent`; all three values are non-empty strings,
555
+ and `id` values are unique. Empty values, duplicate ids, extra keys, accessors,
556
+ non-plain objects, and non-JSON data reject runtime construction rather than
557
+ being silently repaired or discarded.
558
+
559
+ When a direct Captain task resumes from its own Boss question, the composer
560
+ shall prepend the same continuation preamble and labelled Q&A blocks defined in
561
+ §Player prompt composition. The runtime shall pass the complete composed prompt
562
+ once to `callCaptain` with `{ visibility: 'visible', resume: false }` and the
563
+ same source-owned tool restriction as the originating call; it shall not
564
+ expose the
565
+ subsequent adjudicator prompt or structured judge reply through that visible
566
+ call.
567
+ The composed prompt shall contain only the GEARS blockquote, typed runtime
568
+ evidence blocks, and the continuation preamble.
569
+ It shall not append the state's result map, guard names, result-property
570
+ schema, adjudication request, workspace context, or tool instructions.
571
+ The shared Captain composer shall replace every known placeholder whose
572
+ matching typed field is present in the supplied input. It shall not choose one
573
+ exclusive replacement set from `stateId`, source-item identity, or another
574
+ variant discriminator. Verification may deliberately combine catalog,
575
+ intent, plan, result, question, and reply fields in one synthetic input; every
576
+ matching placeholder in that template still has to be rendered.
577
+ Construct the replacement table from field presence alone. In particular,
578
+ populate `<remaining-plan>` when `remainingPlan` is supplied and
579
+ `<completed-call-results>` when `completedCallResults` is supplied, regardless
580
+ of the input's `stateId` or `sourceItem`. An implementation branch such as
581
+ `if (input.stateId === 'reassessment')` around either replacement is
582
+ nonconformant.
152
583
 
153
584
  ## Boss-event mapping
154
585
 
155
586
  The FSM's `events` union enumerates every Boss-originated event.
156
- The runtime receives Boss input as a free-form string (`handleBossInput.text`) and shall classify each non-empty turn into one of the FSM's events plus its payload, or no FSM action, by invoking `callJudge`.
157
- Empty or whitespace-only text produces no event and no port call.
158
-
159
- The classifier prompt shall demand JSON against the FSM's typed event union and any state-specific Boss input contract, including the payload fields required for each event.
160
- When the FSM supports a Boss-reply suspension state, the prompt shall include the current state and the pending Boss question so the judge can distinguish a reply from a fresh directive.
587
+ The runtime receives Boss input as a free-form string
588
+ (`handleBossInput.text`).
589
+ Where the current ready or reconstructed terminal machine accepts exactly one
590
+ ordinary textual entry event and no Boss question is pending, the runtime
591
+ shall send that event deterministically and attach the exact original text to
592
+ its declared textual payload field without invoking `callJudge`.
593
+ The default Captain's ready entry is
594
+ `{ type: 'BOSS_INTENT', bossIntent: turn.text }`.
595
+ All other non-empty turns shall use `callJudge` only to choose one of the FSM's
596
+ event kinds and non-text routing fields, or no FSM action.
597
+ The classifier prompt shall include the exact, unmodified `turn.text` in a
598
+ clearly labelled Boss-message block so the judge can make that choice. Omitting
599
+ the message makes a parked-state classifier unable to distinguish an answer,
600
+ a fresh directive, and no action; including it does not authorize the judge to
601
+ rewrite the runtime-owned textual payload fields.
602
+ For `BOSS_INTENT` and `BOSS_INTERRUPT`, the runtime shall attach the exact
603
+ original text as `bossIntent`; for `BOSS_REPLY`, it shall attach the exact
604
+ original text as `answer`.
605
+ The classifier prompt shall neither request nor accept a copy of those fields,
606
+ and classifier-authored paraphrases shall never become machine context.
607
+ Empty or whitespace-only text produces no event, judge call, Captain call,
608
+ player call, status emission, or FSM transition; its received and settled
609
+ session-trace events are still emitted.
610
+
611
+ The classifier prompt shall demand JSON against the FSM's typed event union and any state-specific Boss input contract, including non-text routing payload fields required for each event but excluding the runtime-owned textual fields above.
612
+ Fields the FSM's event union declares optional shall stay optional in the classifier contract and the reply parser; the classifier shall not promote them to required.
613
+ The runtime shall parse the judge reply tolerantly before validating the
614
+ event. It shall recover the intended JSON object from surrounding prose or a
615
+ Markdown fence, ignore earlier non-JSON bracketed prose, remove a trailing
616
+ comma before a closing brace or bracket, and complete a truncated
617
+ unterminated string or unclosed object/array. When several values are
618
+ recoverable, it shall choose the first object in document order, preferring a
619
+ strict parse at each candidate position before repairing that same candidate.
620
+ For each opening-brace position, first scan strings and nesting to find that
621
+ candidate's earliest balanced closing boundary. Both the strict parse and the
622
+ trailing-comma repair shall operate on only that bounded substring. If no
623
+ closing boundary exists, repair may complete the unterminated suffix. The
624
+ implementation shall never repair the entire remaining document after a
625
+ balanced candidate, because later prose or a later clean object would make the
626
+ earlier repair fail. Advance to the next opening brace only after strict and
627
+ repaired parsing of the current bounded candidate both fail; an earlier
628
+ repairable object therefore wins over every later strict object.
629
+ When no object is recoverable or the recovered event/payload is invalid, the
630
+ runtime shall emit exactly one status and send no FSM event; a malformed
631
+ classification is recoverable control input, not a public boundary rejection.
632
+ If a recovered `BOSS_REPLY` names no question that is currently pending, it is
633
+ such a malformed classification: emit the one recovery status, send no event,
634
+ leave the actor unchanged, and return `no-action` after emissions drain.
635
+ Host-owned runtime options, player bindings, and enabled-playbook catalogs are
636
+ not Boss-event payload. The classifier schema and parser shall not invite or
637
+ accept them, and classified prose shall never overwrite their machine context.
638
+ Every recovered classifier object shall have exactly `type` plus the declared
639
+ non-text routing keys for its selected event arm. Extra own keys, including a
640
+ classifier-authored `bossIntent` or `answer`, reject the classification; the
641
+ parser shall not accept and discard injected catalog, option, state, or
642
+ routing fields.
643
+ `NO_ACTION` in particular is exactly `{ type: 'NO_ACTION' }`.
644
+ A valid `NO_ACTION` returns `no-action` without an invalid-classification
645
+ status and leaves the actor untouched. It is a successful classifier choice,
646
+ not the same parser result as malformed or unrecoverable classifier output.
647
+ After any successful classifier call drains, re-check the active Boss signal
648
+ before reconstructing a terminal actor or sending the selected event. If it
649
+ aborted while the classifier finish emission was pending, return and trace the
650
+ same structured `aborted` result against the unchanged actor.
651
+ When the FSM supports a Boss-reply suspension state, the prompt shall inspect
652
+ the actor snapshot context and include each exact pending Boss question,
653
+ question id, and asking player so the judge can distinguish a reply from a
654
+ fresh directive. With one pending question, a classified `BOSS_REPLY` that
655
+ omits its optional id shall be filled with that sole id. With several pending
656
+ questions, the classifier shall require a known id. A reply shall re-enter only
657
+ its recorded resume state and preserve the original intent, plan, prior child
658
+ results, and Q+A continuation context.
659
+ The classifier-facing pending-question block contains only `questionId`,
660
+ `player`, and `question`. Internal `resumeStateId`, source-item identity, and
661
+ other machine-routing fields remain authoritative in snapshot context and
662
+ shall not be serialized into the judge prompt.
663
+ The allowed fresh directives while parked include every applicable root entry
664
+ event and `BOSS_INTERRUPT`; accepting one shall abandon and clear the pending
665
+ question and reply context before new work begins.
161
666
 
162
667
  A playbook runtime shall not define slash-prefix commands for states or features inside that playbook.
163
668
  The `/command` namespace is reserved for host-level or playbook-selection UX before a turn reaches `handleBossInput`.
164
- If a host forwards text beginning with `/` to `handleBossInput`, the runtime treats it as ordinary Boss text and classifies it through `callJudge`.
669
+ If a host forwards text beginning with `/` to `handleBossInput`, the runtime treats it as ordinary Boss text and maps it through the same deterministic-or-classified Boss-event rules.
165
670
 
166
671
  Hosts that receive structured control input shall resolve host-level concerns before choosing a playbook runtime.
167
672
  Once they call `handleBossInput`, they shall pass the Boss content as text and shall not pre-classify in-playbook FSM events or rely on slash forms as a runtime protocol.
168
673
 
169
674
  `BOSS_INTERRUPT` (or the FSM's equivalent explicit-state-jump event) is reached only by the judge choosing it and supplying its required target payload.
170
- It is *not* an abort surface; aborts go through the abort signal and the strategies in §Abort.
675
+ It is _not_ an abort surface; aborts go through the abort signal and the strategies in §Abort.
171
676
  Hosts where the abort signal is terminal (e.g., SIGINT runs shutdown) shall not route abort to `BOSS_INTERRUPT`.
172
677
 
173
678
  ## Captain adjudication
174
679
 
175
- After a player call returns, the runtime shall coerce `result.finalText` into one of the **per-state** `invoke.input.result` keys.
680
+ After a direct Captain or delegated player call returns, the runtime shall
681
+ coerce `result.finalText` into one of the **per-state**
682
+ `invoke.input.result` keys.
176
683
  It shall also extract any payload fields the state's `result` description names as required.
684
+ Required-field extraction shall recognize both an exact backticked property
685
+ name such as `` `question` `` and the standard annotated form
686
+ `` `question: <verbatim question text>` ``; in either form only `question` is
687
+ the JSON property name.
688
+ Extraction is limited to the description's explicit `Output shall include`
689
+ clause (or equivalent typed output metadata). Backticked prose before that
690
+ clause can name statuses, guards, or concepts such as `ok`, `aborted`, and
691
+ `error`; those names are not output properties and shall never become required
692
+ judge fields.
693
+ For a direct Captain result, `question` and `response` are human-presentation
694
+ fields owned by the visible call rather than fields authored by the hidden
695
+ judge.
696
+ The adjudicator shall select the guard and supply only other structural fields
697
+ required by that guard.
698
+ After validating that selection, the runtime shall inject the exact non-empty
699
+ `CaptainResult.finalText` as the selected output's `question` or `response`.
700
+ It shall reject a judge reply that supplies either presentation field as an
701
+ undeclared extra key, so hidden adjudication cannot replace, paraphrase, or
702
+ decorate prose Boss already saw.
703
+ Delegated-player adjudication retains extraction of every required field from
704
+ the judge reply, including a player-authored Boss question.
705
+ The adjudicator shall use the same document-order tolerant JSON recovery as
706
+ the Boss classifier. Unlike invalid classification, a reply from which no
707
+ object can be recovered, an undeclared guard, or a missing required field is a
708
+ control-plane error and shall throw after the invocation reaches its FSM error
709
+ path and ordered emissions drain.
177
710
 
178
711
  Two default adjudication strategies, in selection order:
179
712
 
180
713
  - **LLM-judge** (default): construct a fresh prompt for `callJudge` that
181
- names the source item's player, includes the player's verbatim output,
714
+ names the source item's actor (and delegated player where applicable),
715
+ includes the actor's verbatim output,
182
716
  lists the `result` keys with their descriptions, and demands a JSON
183
- `{ guard, …payloadFields }` answer keyed to exactly one of the
184
- declared guards. The judge prompt shall not interpret the player's
717
+ `{ guard, …structuralPayloadFields }` answer keyed to exactly one of the
718
+ declared guards, excluding the runtime-owned direct-Captain `question` and
719
+ `response` fields above. The judge prompt shall not interpret the player's
185
720
  output, paraphrase it, or alter the FSM's `result` text — it carries
186
721
  the description verbatim.
187
- - **Marker-parse** (alternative): a deterministic parser that scans the
188
- player output for a terminal control line such as
722
+ - **Marker-parse** (delegated-player alternative): a deterministic parser that
723
+ scans the player output for a terminal control line such as
189
724
  `FSM-RESULT: { "guard": "...", ... }`. Useful when player adapters can
190
725
  be steered to emit structured trailers and the operator wants to avoid
191
726
  the extra LLM call.
192
727
 
193
- The linker may select different strategies per state; the default is **LLM-judge for every state**.
728
+ The linker may select different strategies per delegated-player state; the
729
+ default is **LLM-judge for every state**. Direct-Captain states shall use the
730
+ LLM judge so their visible prose remains human-readable and carries no marker
731
+ or control JSON. Their adjudicator call uses purpose
732
+ `captain-output-adjudication` and remains hidden at the host adapter.
733
+
734
+ When the direct Captain result selects a terminal `response`, the exact
735
+ already-visible `CaptainResult.finalText` is the machine response and Boss
736
+ presentation. The linked
737
+ runtime shall not make a second visible Captain call or expose the hidden
738
+ structured adjudication merely to present the same response.
194
739
 
195
740
  The adjudicator shall fail loudly on:
196
741
 
@@ -198,49 +743,450 @@ The adjudicator shall fail loudly on:
198
743
  - A missing payload field the state's `result` description requires,
199
744
  - An empty / malformed response.
200
745
 
746
+ These cases shall remain distinguishable in the thrown error: malformed JSON
747
+ recovery shall identify the missing JSON object, an unknown selection shall
748
+ identify an undeclared guard, and an incomplete selection shall identify the
749
+ missing required field. A generic “no declared guard selected” error for all
750
+ three cases is nonconformant.
751
+
201
752
  Adjudicator failures are control-plane errors.
202
753
  The runtime shall propagate them by throwing out of `handleBossInput` after attempting cleanup.
203
754
  The host adapter surfaces the throw on its control-plane channel (cligent surfaces such throws as `runtime_error` per [TMUX-025](https://github.com/sublang-ai/cligent/blob/main/specs/user/tmux-play.md#tmux-025)).
204
755
  The host's player-result channels (`player_finished` and equivalents) are reserved for failures the player itself produced; the host emits them when `callPlayer` resolves with `status !== 'ok'`.
756
+ Captain call failures stay on the Captain/control boundary and shall not be
757
+ reported as player failures.
758
+ Because XState still needs the invoked promise to settle, the linked runtime
759
+ shall latch an adjudicator, actor-output JSON-validation, or nested-boundary
760
+ control error outside machine context, allow the invocation's `onError` path to
761
+ reach quiescence, drain all emissions, and then reject the public runtime
762
+ method with that original error. It shall not return such a failure as a
763
+ recoverable `{ outcome: 'failed' }` workflow result.
764
+ The first latched non-abort control error takes precedence over a coincident
765
+ boundary-signal abort. Read and clear the latch only in the public boundary's
766
+ `finally` cleanup after XState and emissions have settled, so it cannot leak
767
+ into a later Boss turn or be erased before rejection.
768
+ An `AbortError`-named transport, validation, or trace-sink failure is still a
769
+ non-abort control error unless it is causally identical to the applicable
770
+ signal reason. Error names shall never change original-error or first-latch
771
+ precedence.
772
+ When a host port or structured-result validator fails after a call-start
773
+ boundary, latch that original error before attempting the required finish
774
+ trace. If the finish sink records the event and then rejects, do not emit a
775
+ second finish and do not let the sink failure replace the earlier control
776
+ error returned by the public boundary; retain the sink failure only as
777
+ independent cleanup evidence.
778
+
779
+ ## Script execution
780
+
781
+ Where the FSM declares the typed `script` actor from
782
+ [gears2fsm "Setup"](gears2fsm.md#setup), the linked runtime shall provide
783
+ its implementation through the shared factory (§Output); the linker shall
784
+ not regenerate a script executor inside each emitted module.
785
+ A script invocation is the one actor kind that runs without any agent:
786
+ it makes no `callPlayer`, `callCaptain`, or `callJudge` call and needs no
787
+ adjudication.
788
+
789
+ The provided actor shall:
790
+
791
+ - Execute `input.command` verbatim through the platform's POSIX shell
792
+ (`sh -c`), with the working directory taken from the emitted
793
+ `PlaybookRuntimeOptions.cwd` when the caller supplies it, else the process
794
+ working directory. The linker shall declare the optional `cwd` option on the
795
+ emitted options interface whenever the FSM contains a script state; the
796
+ validated option reaches the shared script actor through the spec.
797
+ - Resolve deterministically from the child's exit status: status zero resolves
798
+ `{ guard: <first declared guard>, exitStatus: 0 }`; any nonzero status
799
+ resolves the second declared guard with that status. Guard selection is
800
+ mechanical; the runtime shall not route script output through the judge.
801
+ - Reject only when the command cannot be spawned at all, routing through the
802
+ state's ordinary `onError` path.
803
+ - Honor the active turn's abort signal by terminating the child process and
804
+ rejecting per §Abort.
805
+ - Emit, after the child settles and before the invocation resolves, one status
806
+ line `Executed script for <stateId> (exit <status>).` and one telemetry
807
+ event under topic `playbook.script` with payload
808
+ `{ stateId, sourceItem, exitStatus }`, through the ordinary serialized
809
+ emission channel.
810
+
811
+ Script execution emits no `*.call.*` trace pair: the surrounding FSM
812
+ transition trace and the `playbook.script` telemetry are its record, so trace
813
+ schema consumers see no new event types.
814
+ Script stdout and stderr are not workflow data: the runtime shall not place
815
+ them in machine context, prompts, or trace payloads.
816
+
817
+ ## Nested playbook bridge
818
+
819
+ Where the FSM declares the typed `playbook` actor from
820
+ [gears2fsm](gears2fsm.md#nested-playbook-calls), the linked runtime shall provide
821
+ it with the shared `createNestedPlaybookBridge(...).actorLogic` — wired by the
822
+ shared factory per §Output — and shall not regenerate a second pending-call,
823
+ identity-validation, or abort-cleanup substrate inside each linked artifact.
824
+ Instantiate the generic bridge with the FSM-exported `PlaybookInput` type so
825
+ XState `.provide(...)` receives the exact declared actor input rather than a
826
+ structurally similar local type.
827
+ Construct one bridge per runtime and wire every integration hook: allocate ids
828
+ with `nextCallId`; return the currently active public-boundary signal from
829
+ `getBoundarySignal`; bind `resumePlaybookCall.signal` before settling the
830
+ deferred actor through `bindResumeSignal`; enqueue the exact start/finish trace
831
+ through `emitStarted` / `emitFinished`; drain the global emission queue through
832
+ `drain`; latch the original control error through `onControlPlaneError`; and
833
+ retain any cleanup/observer failure through `onBackgroundError` for the next
834
+ public boundary or disposal rejection. The runtime shall not leave these
835
+ optional API hooks unwired merely because their TypeScript properties are
836
+ optional for simpler bridge consumers.
837
+ On invocation the bridge allocates a runtime-local call id, traces the start,
838
+ and calls `PlaybookPorts.callPlaybook` with the composed target/text and the
839
+ bridge signal combined from the XState invocation lifetime, the active public
840
+ boundary, and the bridge's own disposal controller.
841
+
842
+ For a literal invocation, target and text retain their existing static/composed
843
+ values. For a dynamic invocation, the bridge shall use the evaluated
844
+ `PlaybookInput.playbookId` and `PlaybookInput.text` values, require both to be
845
+ strings with non-empty target and text, and preserve the exact resolved values in the
846
+ request and trace. The linker shall preserve the FSM's static
847
+ `playbookIdContext` and `textContext` metadata for conformance; it shall not
848
+ parse function source, treat either metadata name as the runtime value, or
849
+ freeze a dynamic call to the value observed during artifact inspection.
850
+
851
+ If the port returns `state: 'settled'`, the bridge validates the result,
852
+ emits and drains `playbook.call.finished`, then resolves successful output or
853
+ rejects an aborted/error result.
854
+ If the port returns `state: 'suspended'`, the bridge records one pending call
855
+ and awaits a runtime-owned deferred result.
856
+ Only after that pending record exists may the drive boundary treat the call
857
+ state's `playbook.suspended` tag as quiescent.
858
+ One runtime supports at most one pending child call; a second shall reject.
859
+ The pending record shall also retain the call-start `turnId`. A resumed finish
860
+ and every parent transition, Captain reassessment, and status caused by that
861
+ return shall use this retained id, not an absent or newly allocated
862
+ current-turn value. The finish callback shall receive or close over that stored
863
+ id rather than read a mutable global turn id at resume time.
864
+ The bridge shall strictly validate the start discriminant, non-empty suspended
865
+ child session id, settled target identity, optional state descriptor,
866
+ normalized error, and JSON-safe output. A malformed start, malformed result,
867
+ identity mismatch, or non-JSON value is a control-plane error. Once a start
868
+ trace exists, every thrown port, validation failure, immediate result,
869
+ suspension resume, invocation abort, and disposal path shall emit and drain
870
+ exactly one matching finish trace; malformed data shall neither create a
871
+ pending identity nor be reassessed as ordinary child evidence.
872
+ The bridge shall detach and recursively freeze a validated start/result before
873
+ tracing it or delivering it to the FSM, so caller mutation after port
874
+ resolution cannot alter identity, evidence, or trace payloads. A non-abort
875
+ `callPlaybook` throw/rejection is a control-plane failure: pair its finish,
876
+ latch and rethrow the original error, and take the FSM fallback error path. A
877
+ rejection caused by the combined abort signal remains an authored `aborted`
878
+ child result.
879
+ The optional output field may be absent from an otherwise valid successful
880
+ child result. Generated event and trace descriptors shall omit an absent or
881
+ `undefined` output instead of attempting to snapshot it as a JSON value.
882
+ When cancellation wins while the host's opening promise is still pending, the
883
+ shared bridge shall retain and drain that exact promise before emitting the
884
+ matching finish boundary. It shall ignore an abort-reason rejection from that
885
+ opening promise, surface any other late rejection as a control-plane cleanup
886
+ failure, and recover a child session identity from a late resolved start when
887
+ available. Generated runtimes shall pass the host port directly to the shared
888
+ bridge rather than recreate this opening-promise drainage locally.
889
+ In particular, aborting a public turn during that opening promise shall abort
890
+ the combined bridge signal, wait for opening cleanup and the paired finish,
891
+ let the promise actor reach its `onError` quiescent state, and only then return
892
+ an aborted run result. It shall neither hang waiting for a child-resume path
893
+ that was never registered nor return while the opening promise or finish
894
+ emission remains live.
895
+ The pending record shall retain a one-shot invocation-signal listener. If the
896
+ call state is stopped, that listener shall settle and clear the deferred call
897
+ as an aborted `NestedPlaybookCallError`, drain the matching finish boundary after
898
+ host abort cleanup, and make a later nested invocation possible; it shall not
899
+ leave a permanently pending record merely because XState stopped observing the
900
+ promise actor.
901
+
902
+ `resumePlaybookCall` shall accept only the matching pending call id, target
903
+ playbook id, and child session id; bind its new turn signal for work resumed in
904
+ the parent; emit and drain the call-finish trace; settle the bridge deferred;
905
+ and use XState `waitFor` to drive the parent to its next
906
+ quiescent, suspended, failed, aborted, or terminal result.
907
+ An `ok` result resolves the actor and reaches `invoke.onDone`; `aborted` and
908
+ `error` results reject it and reach `invoke.onError`.
909
+ The rejection shall be an `Error` whose public readonly `result` property is
910
+ the exact normalized `PlaybookCallResult`; throwing the result object directly
911
+ or discarding its status prevents the FSM from distinguishing abort from
912
+ failure during recovery.
913
+ Unknown, duplicate, or stale call ids reject without changing actor state.
914
+ The finish trace shall therefore precede any parent FSM transition caused by
915
+ the child return.
916
+ The host independently validates every evaluated target against its enabled
917
+ registry; linker-time metadata is not authorization to call a target.
918
+
919
+ Disposal shall settle an outstanding call as aborted and drain its finish
920
+ trace before `session.disposed`.
921
+ If registered child abort cleanup rejects, the bridge shall emit the paired
922
+ finish with an error result and reject `abortPending` or disposal with that
923
+ original cleanup error; it shall not swallow the failure merely because the
924
+ promise actor also observes a `NestedPlaybookCallError`. Parent disposal shall
925
+ still drain, emit its one `session.disposed` boundary, and clear the bound
926
+ session before rejecting with that preserved cleanup error.
927
+ Child output and errors must be JSON-safe; a non-JSON-safe result is a
928
+ control-plane error.
205
929
 
206
930
  ## Session lifecycle
207
931
 
208
932
  The `PlaybookRuntime` shall:
209
933
 
210
- - In `init`, construct the XState actor with FSM `input` derived from
211
- `options`. The actor is session-scoped, not turn-scoped. Subscribe to
212
- actor snapshots so each transition can be surfaced via `emitStatus`
213
- and `emitTelemetry` before the next event fires. Start the actor.
934
+ - Reject use before `init`, a second active turn or resume, and re-initializing
935
+ a live session. `handleBossInput` and `resumePlaybookCall` share one active
936
+ turn sentinel; neither may overlap the other, and disposal shall not race a
937
+ live boundary. A dispose request made during an active public boundary shall
938
+ reject without beginning teardown. Idle concurrent dispose requests shall
939
+ share one disposal promise; later calls after disposal shall return that
940
+ settled disposal outcome without emitting another boundary. Once disposal
941
+ begins, no new turn or resume may start.
942
+ Disposal requested during initialization shall retain one teardown promise,
943
+ wait for initialization's success or failure cleanup, and emit at most one
944
+ `session.disposed` boundary. Disposal before initialization is terminal and
945
+ coalesced: later initialization rejects and every later disposal call
946
+ returns the first retained promise.
947
+ Represent in-flight initialization with a cleanup-complete latch resolved by
948
+ `init`'s outer `finally`, after either successful startup or the complete
949
+ failed-start cleanup. Do not expose the fallible inner startup promise as
950
+ that latch: it rejects before the outer cleanup and lets concurrent disposal
951
+ race the cleanup's own `session.disposed` attempt.
952
+ Put session validation and snapshotting, bridge/actor construction, initial
953
+ state reads, and startup emissions inside that guarded outer `try`; none may
954
+ throw before the cleanup-complete latch's `finally` can resolve. A rejected
955
+ session identity must not leave later disposal waiting forever.
956
+ The generated `dispose` method shall not be declared `async`, because an
957
+ async wrapper returns a distinct promise and breaks identity coalescing; it
958
+ shall return the retained teardown promise directly and use
959
+ `Promise.reject(...)` for precondition failures.
960
+
961
+ - In `init`, bind the immutable `PlaybookSession`, emit
962
+ `session.started` with the initial normalized state descriptor, and
963
+ construct the XState actor with FSM `input` derived
964
+ from `options`. The actor is session-scoped, not turn-scoped. Use XState v5's
965
+ public actor inspection `@xstate.snapshot` event for the root actor so each
966
+ transition's triggering event and snapshot can be surfaced via `emitStatus`
967
+ and `emitTelemetry` before the next event fires; do not consult private actor
968
+ nodes or infer the event later from context. Filter inspection events by
969
+ `inspectionEvent.actorRef === rootActor`, not merely by the actor-system root
970
+ id, so promise-child snapshots are not emitted as root FSM transitions. The
971
+ inspection callback shall only validate and synchronously enqueue emission
972
+ work, catching validation/enqueue failures into the control/background-error
973
+ latch; it shall not let an exception escape or call an async port directly.
974
+ Its transition `event` field shall be a detached JSON-safe descriptor, never
975
+ the raw XState inspection event. Preserve the string `type` (or use
976
+ `unknown` when absent); copy only declared Boss-union payload fields and a
977
+ validated actor `output`, and normalize an `error` member before inclusion.
978
+ Omit `input`, `actorId`, system/ref data, and every other XState-internal
979
+ field even when it happens to be JSON-safe, so `xstate.init.input` cannot
980
+ leak the host catalog into transition telemetry. In particular, do not call
981
+ `snapshotJsonValue(event)` on an `xstate.error.actor.*` event that contains a
982
+ raw `Error`.
983
+ Construct the actor without starting it, read its public initial snapshot,
984
+ emit and drain `session.started`, and only then call `actor.start()`. The
985
+ initial inspection-driven transition/status emissions shall not precede the
986
+ session-start trace. Have any actor-construction helper return the actor and
987
+ assign it at the call site; TypeScript does not narrow a captured optional
988
+ actor variable from assignment hidden inside a helper. Retain a non-optional
989
+ local actor reference across terminal reconstruction and event sending.
990
+ An actor-construction helper may read the already-bound immutable session
991
+ directly for machine input such as `session.playbookId`, but it shall not
992
+ call a lifecycle assertion that also requires the actor to exist. The actor
993
+ does not exist until that helper returns, so coupling session access to actor
994
+ availability makes every valid `init` fail before construction completes.
995
+ Generated code shall pass the repository's full strict `tsc` build with no
996
+ unused helper or destructured parameter, not only a transpile-only or
997
+ target-local syntax check.
998
+ For the default Captain runtime, the initial quiescent `ready` snapshot may
999
+ emit the ordinary structured transition trace and telemetry, but it is not a
1000
+ Boss-relevant transition and shall emit no human status. Any initial
1001
+ transition-trace or telemetry sink failure is part of `init`: initialization
1002
+ shall reject, stop the actor, and perform the failed-start cleanup below
1003
+ rather than swallowing it as a later background error.
1004
+ Where the FSM input declares `selfPlaybookId`, seed it from the immutable
1005
+ `session.playbookId`; do not expose a caller option or reuse a working leaf's
1006
+ `stateId` as the self-call identity.
1007
+ - If initialization fails after attempting `session.started`, stop the actor,
1008
+ abort/drain nested and host work, and make one best-effort
1009
+ `session.disposed` attempt before clearing the bound session. Preserve the
1010
+ original initialization error if cleanup or disposal emission also fails.
1011
+ Suppress root inspection emissions before stopping the failed actor because
1012
+ XState emits a stop snapshot; that teardown snapshot shall not retry a
1013
+ transition/status sink that already failed initialization. Reset the
1014
+ inspection gate, queues, error latches, prior state, and all per-session
1015
+ sequence counters so a permitted retry starts with trace sequence `1`.
214
1016
  - Per `handleBossInput`:
215
- 1. Classify `turn.text` through the Boss-event mapping.
216
- If it produces no event, return after draining any port emissions.
217
- 2. If the actor is in a `final` state, dispose and reconstruct it —
218
- `final` is terminal and cannot accept new events.
219
- 3. Send the classified event to the actor.
220
- 4. **Drive to quiescence**: each time the actor invokes its `captain`
221
- actor, await the invoke's input, build a player prompt, call
222
- `callPlayer`, adjudicate, and resolve the invoke. Repeat until the
223
- actor's snapshot value is a state that takes a Boss event
224
- (typically `ready` or `failed`) or a `final` state.
225
- - In `dispose`, stop the actor and drain pending port emissions.
1017
+ 1. Allocate a runtime-local turn id and trace the exact Boss text.
1018
+ 2. Map `turn.text` through the Boss-event mapping, using deterministic exact
1019
+ entry where applicable and classification otherwise.
1020
+ If mapping produces no event, return after draining any port emissions.
1021
+ If the classifier port rejects, emit and drain the Boss-settled error
1022
+ boundary, send no event, leave the actor unchanged (including a terminal
1023
+ actor), and reject the original error. If that rejection is caused by the
1024
+ active Boss abort signal, return and trace the same structured `aborted`
1025
+ result instead of tracing `no-action`. If the port resolves but its reply
1026
+ cannot be recovered or validated, emit the one recovery status required
1027
+ by §Boss-event mapping, send no event, leave the actor unchanged, and
1028
+ return `no-action` after the ordinary settled boundary drains.
1029
+ 3. Only after classification produces a real event, if the actor is in a
1030
+ `final` state, dispose and reconstruct it — `final` is terminal and cannot
1031
+ accept new events. `NO_ACTION`, classifier rejection, and malformed
1032
+ classification shall leave a terminal actor untouched.
1033
+ 4. Bind the active public-boundary signal and send the classified event to
1034
+ the actor.
1035
+ 5. **Drive to quiescence**: provide each invoked actor according to its
1036
+ declared kind. For `player`, build a player prompt, call `callPlayer`,
1037
+ adjudicate, and resolve the invoke. For `captain`, build a direct Captain
1038
+ prompt, call `callCaptain` visibly, adjudicate through the shared hidden
1039
+ judge path, and resolve the invoke. For `playbook`, use §Nested playbook
1040
+ bridge. For `script`, use §Script execution — no port call and no
1041
+ adjudication. Parallel regions may run distinct resolved players independently;
1042
+ Captain and judge work remains serialized by the shared host queue. Use
1043
+ XState `waitFor` over public tags/status until no `playbook.busy` state is
1044
+ active, a registered child call is suspended, or the actor is
1045
+ terminal/error. Pass `pendingCalls: nestedBridge` so a suspended tag is
1046
+ quiescent only after its child identity exists. Under natural rejection,
1047
+ do not pass the already-aborted public turn signal as wait cancellation:
1048
+ it has already been combined into the invoked boundary, and the runtime
1049
+ must now wait for XState's `onError` transition and quiescence.
1050
+ 6. Return a structured `PlaybookRunResult` after all in-flight calls and
1051
+ ordered emissions caused by the turn drain.
1052
+ - Per `resumePlaybookCall`, follow §Nested playbook bridge and return the same
1053
+ structured run-result boundary without classifying new Boss text. Drain the
1054
+ transition/status/telemetry queue before returning, just as
1055
+ `handleBossInput` does. A resume shall not allocate a new Boss-input
1056
+ `turnId`; retain the original call-start turn id for its matching finish and
1057
+ for the parent continuation caused by that return. Every success and
1058
+ exceptional path shall drain ordered emissions, select the first latched
1059
+ non-abort control error before considering abort, and clear its boundary
1060
+ latches in `finally`, so a failed resume cannot leak an emission error into a
1061
+ later turn.
1062
+ A resume is not a Boss-input turn and shall emit neither
1063
+ `boss.input.received` nor `boss.input.settled`; the structured result is the
1064
+ method return. Reusing the originating turn id on the child finish and
1065
+ continuation emissions does not create a second Boss trace pair.
1066
+ This quiescence and drain path is mandatory even when
1067
+ `nestedBridge.resume(...)` rejects: capture that operation error, allow the
1068
+ promise actor's `onError` transition to settle, and select the first latched
1069
+ control error only after all ordered emissions have drained.
1070
+ - In `dispose`, capture the final public state and stop the root actor before
1071
+ settling or aborting a suspended nested bridge, so the bridge rejection
1072
+ cannot reenter the FSM and start new actor work during disposal. Then drain
1073
+ pending port emissions and every in-flight Captain/player/judge/child opening, emit
1074
+ `session.disposed` with the final descriptor, and discard player resume
1075
+ tokens. Host child abort cleanup and child `session.disposed` shall drain
1076
+ before the parent call finish, which shall drain before parent
1077
+ `session.disposed`. Use cleanup/finally structure so a bridge or emission
1078
+ failure cannot skip the parent disposal boundary or leave the runtime bound.
226
1079
 
227
1080
  The actor's `lastError` field shall be surfaced via `emitStatus` when the machine enters its `failed` state.
1081
+ For the default Captain runtime, an initial `ready` state and a terminal `done`
1082
+ state shall not emit human status. The terminal response is already visible
1083
+ Captain prose; a synthetic “entered done” message would present it twice.
1084
+ Structured transition trace and telemetry still apply to both states.
1085
+ Every provided actor boundary shall first drain the queued state-entry
1086
+ transition/status/telemetry caused by entering its working leaf, so a call's
1087
+ `*.started` trace cannot overtake the transition that explains it. Every public
1088
+ runtime method shall drain that queue before it resolves or rejects.
1089
+ This initial `await drain()` is required inside each provided `fromPromise`
1090
+ body: XState may begin that body before publishing the root snapshot, and the
1091
+ await yields so the synchronous inspection callback can enqueue the entering
1092
+ transition first.
1093
+
1094
+ If a `*.call.started` trace records and then its sink rejects, no host call may
1095
+ begin. The runtime shall still enqueue exactly one synthetic paired
1096
+ `*.call.finished` trace with `status: 'error'`, preserving the original call
1097
+ id, turn id, actor visibility, state/source identity, and prompt or request
1098
+ metadata from the start boundary. It shall then follow the same latched
1099
+ control-error, FSM settlement, and ordered-drain path as any other call-start
1100
+ failure; the synthetic finish must not replace the original sink error.
1101
+
1102
+ ## Parked-session snapshot (optional)
1103
+
1104
+ A linked runtime may implement the optional durable-session capability of
1105
+ `@sublang/playbook/runtime` — `exportSnapshot()` and
1106
+ `restore(session, snapshot)` — so a host can persist a parked session and
1107
+ rehydrate it in a later process (DR-014). A runtime that implements either
1108
+ member shall implement both. When generated for a runtime whose host needs
1109
+ durability, the pair shall behave as follows.
1110
+
1111
+ `exportSnapshot()` shall return `undefined` unless the runtime is at a safe
1112
+ capture point: initialized, not disposing or disposed, no active
1113
+ `handleBossInput`/`resumePlaybookCall` boundary, no pending nested playbook
1114
+ call, and the root actor at a quiescent state with actor status `active`.
1115
+ At a safe capture point it shall return a JSON-safe
1116
+ `PlaybookRuntimeSnapshot` carrying:
1117
+
1118
+ - `schemaVersion`: literal `1`.
1119
+ - `playbookId`: the bound session's playbook id.
1120
+ - `machine`: the root actor's `getPersistedSnapshot()` result, passed
1121
+ through the shared JSON detachment with any raw `Error` context value
1122
+ (for example FSM `lastError`) normalized to `{ name, message, stack? }`
1123
+ first. The value is opaque to hosts.
1124
+ - `playerResumeTokens`: the resume-token map as a plain object
1125
+ (§PlaybookPorts contract).
1126
+ - `sequences`: the live `trace`, `turn`, `judgeCall`, `playerCall`, and
1127
+ `playbookCall` counters, plus `captainCall` when the runtime supports direct
1128
+ Captain calls. `captainCall` remains optional under schema version `1` for
1129
+ backward compatibility; a direct-Captain-capable runtime shall persist it.
1130
+ - `state`: the current normalized state descriptor.
1131
+ - `pendingBossQuestions`: the pending Boss question(s) from FSM context as
1132
+ a list of `{ questionId, player, question, sourceItem? }`, empty when the
1133
+ parked state awaits no reply. This list exists so hosts can surface the
1134
+ question without parsing status lines or telemetry.
1135
+
1136
+ `restore(session, snapshot)` is an alternative to `init` under the same
1137
+ lifecycle guards (§Session lifecycle): it shall reject when already
1138
+ initialized, disposing, or disposed, and shall validate
1139
+ `snapshot.schemaVersion` and that `snapshot.playbookId` equals
1140
+ `session.playbookId` before touching state.
1141
+ The host supplies the same immutable `PlaybookSession` identity the
1142
+ snapshot was exported under and recreates the runtime through the same
1143
+ factory with equivalent options; the runtime does not diff options, and
1144
+ module identity — that the factory constructing this runtime still
1145
+ belongs to the snapshot's playbook — is likewise the host's check to
1146
+ make before calling `restore`.
1147
+ `restore` shall bind the session, restore the resume-token map, the
1148
+ sequence counters (using the persisted global `trace` counter as a
1149
+ collision-safe floor for an absent legacy `captainCall`), and the
1150
+ prior-state descriptor from the snapshot,
1151
+ construct the actor with the persisted `machine` snapshot, and start it
1152
+ with root inspection emissions suppressed so rehydration emits no
1153
+ `session.started` trace, no transition trace, and no human status — the
1154
+ session already started, and the next public boundary continues the
1155
+ contiguous trace sequence. After start, a restored actor whose status is
1156
+ not `active` or whose state descriptor cannot be normalized shall fail
1157
+ `restore` through the same failed-start cleanup path as `init`.
1158
+ A restore failure shall leave the runtime unbound so `dispose` remains
1159
+ callable and terminal.
228
1160
 
229
1161
  ## Abort
230
1162
 
231
1163
  `handleBossInput.signal` is the abort surface.
232
- The runtime shall honor it at every `callPlayer`/`callJudge` and at every poll between transitions.
233
- On abort, the runtime shall drive the actor to a quiescent state before returning from the turn.
1164
+ The runtime shall honor it at every `callPlayer`/`callCaptain`/`callJudge` and
1165
+ at every poll between transitions.
1166
+ Each provided Captain, player, judge, or nested-playbook boundary shall receive
1167
+ a signal combined from its XState invocation-lifetime signal and the currently
1168
+ active `handleBossInput` or `resumePlaybookCall` signal (for example with
1169
+ the shared `combineAbortSignals`). Classify a rejection as cancellation by its
1170
+ causal identity with the applicable signal reason, not by an `AbortError` name
1171
+ or by observing only that the signal is also aborted. Signals may carry an
1172
+ ordinary `Error`, while a distinct transport or sink failure that occurs after
1173
+ abort remains a non-abort control error and takes precedence. On abort, the
1174
+ runtime shall not merely race the imperative
1175
+ wait and return while an invocation remains live: it shall let the selected
1176
+ rejection path settle and drive the actor to a quiescent state before returning
1177
+ from the turn. No trace, status, state, or call completion caused by that turn
1178
+ may appear after the public method returns.
234
1179
  Three strategies are permitted; the linker selects per FSM:
235
1180
 
236
- - **Natural rejection** — the runtime's Captain actor (e.g.,
1181
+ - **Natural rejection** — the runtime's Captain or player actor (e.g.,
237
1182
  `fromPromise`) ends the invocation by rejecting, and the FSM routes
238
1183
  the rejection through `onError` to a quiescent sink. The cancelled
239
- port call may *itself* reject, or it may resolve with
240
- `PlayerResult { status: 'aborted' | 'error' }` that the runtime
241
- inspects and converts into a Captain-actor rejection. Either shape
242
- is permitted the contract is on the Captain-actor boundary, not on
243
- the port's promise behavior. Preferred when every Captain-invoking
1184
+ port call may _itself_ reject, or it may resolve with
1185
+ `PlayerResult` or `CaptainResult` with
1186
+ `{ status: 'aborted' | 'error' }` that the runtime
1187
+ inspects and converts into an actor rejection. Either shape
1188
+ is permitted — the contract is on the actor boundary, not on
1189
+ the port's promise behavior. Preferred when every Captain- or player-invoking
244
1190
  state's `onError` lands somewhere quiescent; the FSM's own error
245
1191
  wiring is the abort path.
246
1192
  - **Synthetic pre-emption to a quiescent target** — send the FSM's
@@ -249,7 +1195,7 @@ Three strategies are permitted; the linker selects per FSM:
249
1195
  The runtime shall not pick the active state as the target:
250
1196
  `gears2fsm.md` prescribes `reenter: true` for `bossInterrupts`, so
251
1197
  re-entering the active state restarts its `invoke` and spawns a
252
- fresh player call.
1198
+ fresh agent call.
253
1199
  - **Programmatic stop** — `actor.stop()` and report the turn as aborted
254
1200
  via `emitStatus`. Reserved for FSMs with neither `onError` wiring nor
255
1201
  a pre-emption event.
@@ -266,33 +1212,154 @@ The runtime shall emit, at minimum:
266
1212
  default is to emit on every transition and let the host filter; hosts
267
1213
  may bind a stricter rule.
268
1214
  - One `emitTelemetry` per state transition under a namespaced topic
269
- (recommended `playbook.fsm.state`), with payload `{ from, to, event }`.
1215
+ (recommended `playbook.fsm.state`), with structured `from`, `to`, `event`,
1216
+ `previousState`, and `state` fields. Descriptors carry the JSON-safe XState
1217
+ value, active stable ids from public state metadata, tags, status, and
1218
+ quiescence; they do not inspect private XState nodes.
1219
+ The payload shall additionally carry the exact pending Boss question or
1220
+ keyed questions selected from public snapshot context and normalize any
1221
+ transition error without retaining a raw `Error` instance.
1222
+ Do not reduce this payload to the current state: `from` and `previousState`
1223
+ are the authoritative prior descriptor, while `to` and `state` are the new
1224
+ descriptor. On the first observed transition, use the initialized state as
1225
+ both the prior and new descriptor when no earlier transition exists.
1226
+ Snapshot and recursively freeze the complete described telemetry payload
1227
+ independently from the state retained as `previousState`, so an observer
1228
+ cannot mutate a later transition's authoritative `from` state.
270
1229
  Observers consume telemetry; the runtime never interprets the topic.
271
1230
 
272
- Player prompts and adjudicator JSON ride the host's own record channels when the host has them (cligent's `captain_*` / `player_*`).
273
- The runtime shall not duplicate them into `emitTelemetry`.
1231
+ Player prompts and adjudicator JSON may additionally ride the host's own record channels when the host has them (cligent's `captain_*` / `player_*`).
1232
+ The `playbook.trace` copies are the host-agnostic runtime-boundary record required by §Playbook trace.
274
1233
 
275
1234
  ## Output
276
1235
 
277
- The link compiler emits **one** TypeScript module that:
278
-
279
- - Imports the FSM artifact by relative path.
280
- - Imports XState's actor primitives (`createActor`, `fromPromise`,
281
- `setup`'s `.provide`).
282
- - Exports `createPlaybookRuntime` and the typed `PlaybookRuntimeOptions`
283
- interface for that playbook.
1236
+ The link compiler emits **one thin** TypeScript module per playbook.
1237
+ The FSM-interpreter machinery — actor wiring, boundary tracing, Boss-event
1238
+ mapping, adjudication, script execution, nested-playbook bridging, session
1239
+ lifecycle, abort handling, and the optional parked-session snapshot
1240
+ capability — is not regenerated per artifact: it ships once as the shared
1241
+ `createXStatePlaybookRuntime(machine, spec)` factory exported by
1242
+ `@sublang/playbook/xstate-runtime`, and the emitted module hands its FSM and
1243
+ a small per-playbook `spec` to that factory. Every behavioral section of
1244
+ this definition still binds the emitted module's runtime; the shared factory
1245
+ is how the emitted module satisfies them, so a runtime fix ships as a
1246
+ package release instead of a re-link of every artifact.
1247
+
1248
+ The emitted module:
1249
+
1250
+ - Imports the FSM artifact by relative path with an extension-bearing
1251
+ runtime specifier. When the linked TypeScript is part of a package that
1252
+ compiles and ships JavaScript siblings, the source shall use the
1253
+ NodeNext-compatible `.js` specifier (for example `./code.fsm.js`), never a
1254
+ `.ts` specifier that the package's supported Node versions cannot load.
1255
+ An explicitly source-only host may instead retain `.ts` only when that
1256
+ host supports direct TypeScript loading and no JavaScript build is shipped.
1257
+ - Restricts itself to erasable TypeScript syntax — type annotations
1258
+ that strip cleanly, no constructor parameter properties, `enum`s, or
1259
+ namespaces — so a host running under type stripping loads it
1260
+ directly.
1261
+ - Imports `createXStatePlaybookRuntime` (plus any shared strategy defaults
1262
+ its `_internal` surface re-exports) from the shared engine module through
1263
+ its bare package specifier `@sublang/playbook/xstate-runtime`, and the
1264
+ contract types through `@sublang/playbook/runtime`. It shall not copy,
1265
+ inline, or re-derive interpreter machinery — actor bridges, trace
1266
+ emission, judge-JSON recovery, lifecycle guards — beside the factory
1267
+ call, and shall not import `xstate`, `p-queue`, or `node:child_process`
1268
+ itself; those are the shared engine's dependencies.
1269
+ - Declares and exports the typed `PlaybookRuntimeOptions` interface for that
1270
+ playbook, derived from every required FSM input field that is not supplied
1271
+ by `PlaybookSession` or another linker-owned source (§PlaybookRuntime
1272
+ contract), plus the optional `cwd` option whenever the FSM contains a
1273
+ `script` state (§Script execution).
1274
+ - Supplies the spec's `snapshotOptions` with the same options-validation
1275
+ semantics previously generated inline: validate and JSON-snapshot the
1276
+ caller's options, rejecting undeclared keys and non-conforming values, so
1277
+ the factory binds an immutable options record before constructing any
1278
+ actor.
1279
+ - Supplies in `spec` only what the factory cannot read from the FSM
1280
+ artifact's own data: the deterministic textual entry event where
1281
+ §Boss-event mapping prescribes deterministic entry; compact `bossEvents`
1282
+ metadata for each additional Boss-union arm whose exact required/optional
1283
+ judge fields, runtime-owned text fields, or closed string values disappear
1284
+ under TypeScript erasure; `placeholderFields` only for authored token/field
1285
+ exceptions not covered by the canonical kebab-token-to-camel-field mapping
1286
+ and the canonical `<#>` → `irNumber` special case; the
1287
+ transition-event payload fields the FSM's Boss union declares; a
1288
+ non-default player binding where the linker inputs supplied one; and any
1289
+ per-playbook strategy override (classifier, prompt composers,
1290
+ required-field extraction, status formatting) an earlier section of this
1291
+ definition requires for that playbook. The metadata shall keep the shared
1292
+ classifier's reply contract exactly flat `{ type, ...declaredFields }` and
1293
+ distinguish judge-authored routing fields from exact-text fields the
1294
+ runtime attaches itself. Everything else — player/script/captain/nested actor
1295
+ provisioning, prompt composition, classification, adjudication, statuses,
1296
+ resumable-state derivation — comes from the factory's generic defaults,
1297
+ which implement the behavioral sections of this definition.
1298
+
1299
+ ```ts
1300
+ interface XStateBossEventFieldSpec {
1301
+ source: 'judge' | 'text';
1302
+ required?: boolean;
1303
+ values?: readonly string[];
1304
+ }
1305
+
1306
+ interface XStateBossEventSpec {
1307
+ type: string;
1308
+ fields?: Readonly<Record<string, XStateBossEventFieldSpec>>;
1309
+ }
1310
+
1311
+ bossEvents?: readonly XStateBossEventSpec[];
1312
+ placeholderFields?: Readonly<Record<string, string>>;
1313
+ ```
1314
+
1315
+ Supplied `bossEvents` metadata shall merge with, and shall not replace or
1316
+ weaken, runtime-derived entry text ownership or closed interrupt targets.
1317
+ A conflicting duplicate field contract is a linker/runtime construction
1318
+ error.
1319
+ - Default-exports the factory call as `createPlaybookRuntime`, typed
1320
+ `PlaybookRuntimeFactory<PlaybookRuntimeOptions>`.
1321
+ - Exposes, under an `_internal` export, the pure helpers verification
1322
+ needs — at least the player-prompt and Captain-prompt composers
1323
+ (`composePlayerPrompt` and `composeCaptainPrompt`), which may re-export
1324
+ the shared defaults when the spec does not override composition — so
1325
+ compilation-correctness tests can exercise composition without a host.
284
1326
  - Holds no host-specific types and no host primitive calls. The runtime
285
- speaks only `PlaybookPorts`.
1327
+ speaks only `PlaybookPorts` for every agent and host concern; the
1328
+ `node:child_process` dependency of §Script execution lives in the shared
1329
+ factory, not in the emitted module.
286
1330
  - Records the linker inputs (FSM path, player binding, strategies) in a
287
1331
  top-of-file header comment so the file is reproducible from the same
288
1332
  inputs.
289
- - Sources the contract types (`PlayerResult`, `PlaybookPorts`,
290
- `PlaybookRuntime`, `PlaybookRuntimeFactory`) from a single shared
1333
+ - Sources the contract types (`PlayerResult`, `PlayerCallOptions`,
1334
+ `CaptainResult`, `CaptainCallOptions`, `PlaybookPorts`, `PlaybookSession`,
1335
+ `PlaybookTraceEvent`,
1336
+ `PlaybookCallRequest`, `PlaybookCallResult`, `PlaybookCallStart`,
1337
+ `PlaybookStateValue`, `PlaybookState`, `PlaybookRunResult`,
1338
+ `PlaybookRuntime`, `PlaybookRuntimeFactory`) from the single shared
291
1339
  type-only module instead of redefining them, and re-exports the names
292
1340
  its consumers import, so every linked playbook shares one contract
293
- definition. The shared module imports no FSM or host types, so the
1341
+ definition. The shared modules import no FSM or host types, so the
294
1342
  dependency runs one way — from each linked module to the shared
295
- contract, never the reverse.
1343
+ engine and contract, never the reverse.
1344
+
1345
+ When a co-located integration test for the linked runtime already exists, the
1346
+ link compiler shall run it before reporting success and treat any failure as a
1347
+ generation failure. It shall not delete, skip, or weaken that suite to make a
1348
+ new artifact pass; the suite is executable evidence for lifecycle, ordering,
1349
+ error-propagation, and host-boundary requirements that static artifact checks
1350
+ cannot establish.
1351
+
1352
+ Internal trace/status helpers may accept `unknown`, validate it with the same
1353
+ JSON-safety rules as the public boundary, and only then emit a `JsonValue`.
1354
+ They shall not require nominally typed public interfaces such as
1355
+ `NormalizedError`, `PlaybookState`, `PlaybookCallRequest`, or
1356
+ `PlaybookCallResult` to satisfy a `JsonValue` index signature at compile time,
1357
+ and they shall not silence that mismatch with an unchecked cast.
1358
+ Prompt placeholder substitution shall make one callback-based pass over the
1359
+ original template. Replacement strings are literal: placeholder-looking text
1360
+ inside Boss/catalog/plan/result values and JavaScript replacement tokens such
1361
+ as `$&`, `$$`, dollar-backtick, and `$'` shall not be interpreted or
1362
+ substituted again.
296
1363
 
297
1364
  ## Host adaptation (informative, not normative)
298
1365
 
@@ -304,14 +1371,18 @@ A host integrates with playbooks via a small adapter that:
304
1371
  2. Imports the module and constructs the runtime with options forwarded
305
1372
  verbatim from the host config.
306
1373
  3. Implements `PlaybookPorts` by wrapping the host's own primitives —
307
- for cligent/tmux-play this is `callPlayer ← context.callPlayer`,
308
- `callJudge ← context.callCaptain`, `emitStatus`/`emitTelemetry` ←
309
- `session.emitStatus`/`session.emitTelemetry`.
310
- 4. Calls `runtime.init(ports)` once at session start, forwards each
311
- Boss turn to `runtime.handleBossInput`, and calls
1374
+ for cligent/tmux-play this is `callPlayer ← context.callPlayer`, visible
1375
+ `callCaptain ← context.callCaptain`, hidden
1376
+ `callJudge ← context.callCaptain`, nested `callPlaybook ←` the Captain
1377
+ session stack, and `emitStatus`/`emitTelemetry`
1378
+ `session.emitStatus`/`session.emitTelemetry`. The two Captain-backed ports
1379
+ share one abort-aware concurrency-one queue.
1380
+ 4. Generates a unique playbook-session id, calls
1381
+ `runtime.init({ sessionId, rootSessionId: sessionId, depth: 0,
1382
+ playbookId, ports })` once at session start, forwards each Boss turn to
1383
+ `runtime.handleBossInput`, and calls
312
1384
  `runtime.dispose()` at session end.
313
1385
 
314
- The adapter is ~30 lines regardless of which playbook is loaded.
315
1386
  Its location is a project-organization choice:
316
1387
 
317
1388
  - **Playbook repo** — simplest when the playbook author owns the integration; keeps host primitives a lower-layer dependency.
@@ -328,9 +1399,13 @@ This spec is silent on the choice; the contract is the same in any location.
328
1399
  layouts — where these live is a per-project decision (see
329
1400
  §Host adaptation); this spec only constrains the `PlaybookPorts`
330
1401
  contract they satisfy.
331
- - Persisting FSM context across sessions, multi-Boss orchestration, or
332
- visualizer rendering separate hosts/observers may add them without
333
- changing this spec.
1402
+ - Trace persistence, multiple Boss-selected root engagements, recursive
1403
+ playbook calls, multi-Boss orchestration, or visualizer rendering
1404
+ separate hosts/observers may add them without changing this spec. A host
1405
+ may persist the emitted trace, but the runtime does not rehydrate a
1406
+ disposed actor from it. Parked-session durability is in scope only
1407
+ through the optional snapshot surface of §Parked-session snapshot
1408
+ (DR-014); everything beyond it remains out of scope.
334
1409
 
335
1410
  New behavior in any of these areas requires a separate slc spec.
336
1411
 
@@ -339,3 +1414,4 @@ New behavior in any of these areas requires a separate slc spec.
339
1414
  [1]: [text2gears](text2gears.md) "First phase: text → GEARS spec items."
340
1415
  [2]: [gears2fsm](gears2fsm.md) "Second phase: GEARS items → FSM artifact."
341
1416
  [3]: https://stately.ai/docs/actors "XState actors — `createActor`, snapshots, abort signal handling."
1417
+ [4]: https://github.com/sindresorhus/p-queue#readme "p-queue concurrency and AbortSignal support."