@sublang/playbook 7.0.0 → 9.0.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 (62) hide show
  1. package/README.md +20 -7
  2. package/docs/cli.md +88 -43
  3. package/docs/configuration.md +221 -119
  4. package/docs/embedding.md +78 -27
  5. package/package.json +4 -3
  6. package/reference/sdlc/captain.playbook/captain.playbook.js +16 -5
  7. package/reference/sdlc/captain.playbook/captain.playbook.ts +20 -6
  8. package/reference/sdlc/code.md +1 -1
  9. package/reference/sdlc/code.playbook/bin/interactive-session.js +816 -0
  10. package/reference/sdlc/code.playbook/bin/launch-config.js +1078 -116
  11. package/reference/sdlc/code.playbook/bin/playbook.js +489 -34
  12. package/reference/sdlc/code.playbook/bin/run.js +283 -298
  13. package/reference/sdlc/code.playbook/bin/session-store.js +818 -26
  14. package/reference/sdlc/code.playbook/code.fsm.d.ts +9 -6
  15. package/reference/sdlc/code.playbook/code.fsm.introspect.js +2 -2
  16. package/reference/sdlc/code.playbook/code.fsm.introspect.ts +2 -2
  17. package/reference/sdlc/code.playbook/code.fsm.js +18 -15
  18. package/reference/sdlc/code.playbook/code.fsm.ts +21 -21
  19. package/reference/sdlc/code.playbook/code.gears.md +1 -1
  20. package/reference/sdlc/code.playbook/code.playbook.d.ts +2 -1
  21. package/reference/sdlc/code.playbook/code.playbook.js +25 -15
  22. package/reference/sdlc/code.playbook/code.playbook.ts +34 -17
  23. package/reference/sdlc/code.playbook/code.registry.d.ts +5 -13
  24. package/reference/sdlc/code.playbook/code.registry.js +3 -10
  25. package/reference/sdlc/code.playbook/code.registry.ts +7 -32
  26. package/reference/sdlc/code.playbook/playbook-captain.d.ts +39 -14
  27. package/reference/sdlc/code.playbook/playbook-captain.js +1014 -299
  28. package/reference/sdlc/code.playbook/playbook-captain.ts +1450 -406
  29. package/reference/sdlc/code.playbook/playbook.config.template.yaml +41 -49
  30. package/reference/sdlc/decide.md +4 -4
  31. package/reference/sdlc/decide.playbook/decide.fsm.d.ts +10 -10
  32. package/reference/sdlc/decide.playbook/decide.fsm.js +21 -14
  33. package/reference/sdlc/decide.playbook/decide.fsm.ts +27 -23
  34. package/reference/sdlc/decide.playbook/decide.gears.md +3 -5
  35. package/reference/sdlc/decide.playbook/decide.playbook.d.ts +11 -13
  36. package/reference/sdlc/decide.playbook/decide.playbook.js +465 -246
  37. package/reference/sdlc/decide.playbook/decide.playbook.ts +623 -283
  38. package/reference/sdlc/decide.playbook/decide.registry.d.ts +5 -13
  39. package/reference/sdlc/decide.playbook/decide.registry.js +3 -9
  40. package/reference/sdlc/decide.playbook/decide.registry.ts +7 -31
  41. package/reference/sdlc/review.md +4 -5
  42. package/reference/sdlc/review.playbook/review.fsm.d.ts +9 -11
  43. package/reference/sdlc/review.playbook/review.fsm.js +30 -24
  44. package/reference/sdlc/review.playbook/review.fsm.ts +39 -35
  45. package/reference/sdlc/review.playbook/review.gears.md +6 -5
  46. package/reference/sdlc/review.playbook/review.playbook.d.ts +2 -1
  47. package/reference/sdlc/review.playbook/review.playbook.js +29 -23
  48. package/reference/sdlc/review.playbook/review.playbook.ts +38 -28
  49. package/reference/sdlc/review.playbook/review.registry.d.ts +5 -13
  50. package/reference/sdlc/review.playbook/review.registry.js +3 -16
  51. package/reference/sdlc/review.playbook/review.registry.ts +7 -38
  52. package/slc/gears2fsm.md +45 -24
  53. package/slc/link.md +297 -135
  54. package/slc/text2gears.md +19 -18
  55. package/src/runtime.d.ts +21 -16
  56. package/src/runtime.ts +20 -23
  57. package/src/xstate-playbook-runtime.d.ts +34 -20
  58. package/src/xstate-playbook-runtime.js +973 -400
  59. package/src/xstate-playbook-runtime.ts +1203 -457
  60. package/src/xstate-runtime.d.ts +17 -7
  61. package/src/xstate-runtime.js +198 -81
  62. package/src/xstate-runtime.ts +339 -112
package/slc/link.md CHANGED
@@ -59,13 +59,27 @@ interface PlaybookSession {
59
59
  parentSessionId?: string;
60
60
  parentCallId?: string;
61
61
  depth: number;
62
+ roleBindings?: Readonly<Record<string, PlaybookRoleBinding>>;
62
63
  playerSessions?: PlayerSessionStore;
63
64
  ports: PlaybookPorts;
64
65
  }
65
66
 
67
+ interface PlaybookRoleBinding {
68
+ readonly playerId: string;
69
+ readonly promptIdentity: string;
70
+ }
71
+
72
+ interface PlaybookPendingBossQuestion {
73
+ questionId: string;
74
+ asker: { kind: 'captain' } | { kind: 'role'; roleId: string };
75
+ question: string;
76
+ sourceItem?: string;
77
+ }
78
+
66
79
  interface PlayerSessionStore {
67
- select(playerId: string): string | false;
68
- update(playerId: string, resumeToken?: string): void;
80
+ select(roleId: string): string | false;
81
+ // Called only for a replacement token or an authorized ok-status clear.
82
+ update(roleId: string, resumeToken?: string): void;
69
83
  snapshot(): Readonly<Record<string, string>>;
70
84
  restore(tokens: Readonly<Record<string, string>>): void;
71
85
  }
@@ -103,6 +117,30 @@ interface PlaybookPendingCall {
103
117
  childSessionId: string;
104
118
  }
105
119
 
120
+ interface PlaybookSuspendedCall extends PlaybookPendingCall {
121
+ stateId: string;
122
+ text: string;
123
+ turnId?: number;
124
+ }
125
+
126
+ interface PlaybookRuntimeSnapshot {
127
+ schemaVersion: 3;
128
+ playbookId: string;
129
+ machine: JsonValue;
130
+ roleResumeTokens: { readonly [roleId: string]: string };
131
+ sequences: {
132
+ trace: number;
133
+ turn: number;
134
+ judgeCall: number;
135
+ playerCall: number;
136
+ playbookCall: number;
137
+ captainCall?: number;
138
+ };
139
+ state: PlaybookState;
140
+ pendingBossQuestions: readonly PlaybookPendingBossQuestion[];
141
+ suspendedCall?: PlaybookSuspendedCall;
142
+ }
143
+
106
144
  type PlaybookRunResult =
107
145
  | { outcome: 'quiescent' | 'no-action'; state: PlaybookState }
108
146
  | {
@@ -113,6 +151,7 @@ type PlaybookRunResult =
113
151
  | {
114
152
  outcome: 'terminal';
115
153
  state: PlaybookState;
154
+ stateDescription?: string;
116
155
  output?: JsonValue;
117
156
  }
118
157
  | {
@@ -146,12 +185,14 @@ differ from both its `rootSessionId` and `parentSessionId`.
146
185
  Run outcomes are exact: `no-action` means no FSM event was sent;
147
186
  `quiescent` means a non-failure parked/idle state; `failed` means the FSM is in
148
187
  a recoverable failure state; `terminal` means top-level final with optional
149
- JSON output; `aborted` means the turn signal ended work; and `suspended` means
188
+ JSON output and the exact authored `stateDescription` of the reached final
189
+ state when one is declared; `aborted` means the turn signal ended work; and `suspended` means
150
190
  exactly one `pendingCall` is active.
191
+ Only the terminal variant may carry `stateDescription`; the runtime shall omit it when the final state declares none and shall never substitute a state id or derive it from opaque output ([DR-037](../specs/decisions/037-terminal-result-meaning.md)).
151
192
  Control-plane exceptions reject the runtime method rather than masquerade as a
152
193
  recoverable workflow `failed` result.
153
194
 
154
- `PlaybookRuntimeOptions` is host-agnostic and carries only _per-run_ knobs such as identity strings (e.g., model names a playbook substitutes into prompt placeholders), strategy overrides the linker exposes, and — where the compiled playbook's policy needs a host seam — host-supplied port-shaped callbacks the linker exposes as option members whose types the artifact itself declares, so the six-member `PlaybookPorts` contract and the shared contract module stay free of host types.
195
+ `PlaybookRuntimeOptions` is host-agnostic and carries only _per-run_ workflow knobs, strategy overrides the linker exposes, and — where the compiled playbook's policy needs a host seam — host-supplied port-shaped callbacks the linker exposes as option members whose types the artifact itself declares, so the six-member `PlaybookPorts` contract and the shared contract module stay free of host types.
155
196
  The link compiler emits a typed options interface per playbook based on the FSM's `CodingInput` (or equivalent).
156
197
  The CLI's absence of `--link-option` values does not mean that
157
198
  `PlaybookRuntimeOptions` is empty. CLI link options are compile-time inputs;
@@ -162,15 +203,16 @@ remain a required readonly runtime option passed through to machine input; the
162
203
  linker shall neither invent an empty catalog nor require it to be baked into a
163
204
  CLI link option.
164
205
 
165
- Player binding is a _linker-time_ input baked into the emitted runtime by default.
166
- 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.
206
+ Concrete player binding and prompt identity are host policy and shall not enter `PlaybookRuntimeOptions`, machine input, or the emitted artifact.
207
+ For a shell-hosted runtime, `PlaybookSession.roleBindings` shall carry exactly the runtime's local roles, map each to its resolved player id and current prompt identity, and be the sole source for call targeting, player-facing prompt identity, concurrency keys, and trace player ids.
208
+ The host shall derive `promptIdentity` from the current effective model when present and the established adapter otherwise; a standalone runtime may omit the map and retain only its local role identity.
167
209
 
168
210
  ## PlaybookPorts contract
169
211
 
170
212
  ```typescript
171
213
  interface PlaybookPorts {
172
214
  callPlayer(
173
- playerId: string,
215
+ roleId: string,
174
216
  prompt: string,
175
217
  signal: AbortSignal,
176
218
  options: PlayerCallOptions,
@@ -289,7 +331,7 @@ emission drain. Absent such a control-plane failure, if the combined signal
289
331
  has aborted, ordinary abort settlement remains authoritative after the actor
290
332
  reaches its error path.
291
333
 
292
- Every linked runtime owns a map from resolved player id to its latest non-empty `resumeToken`.
334
+ Every standalone linked runtime owns a private continuation map keyed by resolved player id when `roleBindings` is supplied and by local role id otherwise.
293
335
  Before reading a resolved direct-Captain or delegated-player result, the
294
336
  runtime shall validate, detach, and freeze it through the shared
295
337
  `validateCaptainResult` or `validatePlayerResult` helper. The accepted object
@@ -297,17 +339,18 @@ shape is exact: only the declared status and optional string fields are
297
339
  allowed, JSON-unsafe members reject, and caller mutation after resolution
298
340
  cannot change trace evidence or player continuity. Validation happens before
299
341
  adopting a resume token or reading final text.
300
- The first call to each player in a playbook session shall pass `{ resume: false }`; later calls shall pass the exact stored token.
301
- 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.
342
+ The first call to each private continuation key in a standalone playbook session shall pass `{ resume: false }`; later calls shall pass the exact stored token, so two sequential roles explicitly bound to one player id share one token even without a supplied `PlayerSessionStore`.
343
+ After a validated resolved call, the runtime shall replace the token when the result carries one, clear it only for an `ok` result that omits one, and preserve it for an `aborted` or `error` result that omits one; a rejected call with no result likewise leaves the prior token unchanged.
302
344
  After awaiting a host Captain or player promise, the runtime shall re-check the
303
345
  combined invocation/public-boundary signal before validating the result,
304
346
  adopting a resume token, or emitting a successful finish. A host promise that
305
347
  ignores cancellation and resolves late shall be paired as aborted and shall
306
348
  not mutate continuity or masquerade as success.
307
349
  The map survives actor reconstruction inside the same runtime and is discarded at `dispose`.
308
- The runtime shall keep an in-flight set keyed by resolved player id and reject
309
- a second concurrent call to the same id before crossing the host port. Calls
310
- to distinct resolved player ids may overlap.
350
+ The runtime shall keep an in-flight set keyed by resolved player id when the
351
+ host supplies binding metadata, otherwise by local role id, and reject a
352
+ second concurrent call to the same key before crossing the host port.
353
+ Calls to distinct keys may overlap.
311
354
 
312
355
  `callJudge` returns free-form text.
313
356
  The runtime parses it per the state's adjudication strategy (§Captain adjudication).
@@ -346,7 +389,7 @@ The runtime never speaks to LLMs directly and never touches host types beyond `P
346
389
  ## Playbook trace
347
390
 
348
391
  Every linked runtime shall emit a boundary-complete, ordered trace through `emitTelemetry` topic `playbook.trace`.
349
- Each payload shall carry `schemaVersion: 2`, the immutable session identity and
392
+ Each payload shall carry `schemaVersion: 3`, the immutable session identity and
350
393
  causality, a contiguous one-based `sequence`, a Unix-millisecond `timestamp`, a
351
394
  trace `type`, event `payload`, and the runtime-local `turnId` / paired `callId`
352
395
  where applicable.
@@ -371,7 +414,7 @@ type PlaybookTraceType =
371
414
  | 'session.disposed';
372
415
 
373
416
  interface PlaybookTraceEvent {
374
- schemaVersion: 2;
417
+ schemaVersion: 3;
375
418
  sessionId: string;
376
419
  playbookId: string;
377
420
  rootSessionId: string;
@@ -387,7 +430,7 @@ interface PlaybookTraceEvent {
387
430
  }
388
431
  ```
389
432
 
390
- A composing host may supply `playerSessions` as a frame-local view of player continuation owned by the root engagement tree.
433
+ A composing host may supply `playerSessions` as a frame-local view of player continuation owned by the logical Captain session.
391
434
  The runtime shall select through that view before allocating or tracing a player call and shall update or clear it from a validated player result before emitting the matching finish trace.
392
435
  Snapshot export and restore shall use the same view, failed restore shall leave its prior contents unchanged, and child disposal shall not clear host-owned continuation.
393
436
  A host that omits the view retains the runtime's private per-session continuation behavior.
@@ -401,7 +444,7 @@ The trace types are `session.started`, `boss.input.received`,
401
444
  `boss.input.settled`, and `session.disposed`.
402
445
  Call pairs carry exact prompts and replies, normalized failures, actor and state
403
446
  identity, and their boundary-specific options.
404
- `apply.started` and `apply.finished` are the paired schema-2 boundary of an
447
+ `apply.started` and `apply.finished` are the paired apply boundary of a
405
448
  executed `apply()` call on a runtime implementing the optional control surface
406
449
  (§Control surface): both carry the action id and idempotency `key` (plus the
407
450
  singular `stateId` on start when one exists), the pair shares one
@@ -423,7 +466,8 @@ described top-level `state` and its singular `stateId` when present, as well as
423
466
  its message and optional data; consumers shall not have to recover state
424
467
  identity from a nested ad hoc object.
425
468
  Judge results use `reply`; player start and finish payloads both carry the
426
- selected `resume`; Captain start and finish payloads both carry
469
+ local `roleId`, the resolved `playerId` when host binding metadata is
470
+ available, and the selected `resume`; Captain start and finish payloads both carry
427
471
  the exact composed prompt, the boundary's selected `visibility`, the direct
428
472
  invocation's `stateId` and `sourceItem`, and no player resume selection or
429
473
  resume token — a visible workflow call carries its runtime-owned
@@ -453,6 +497,15 @@ If a started-boundary sink records the event and then rejects, the runtime
453
497
  shall make one best-effort normalized error-finish attempt with the same call
454
498
  id and then reject the original start error. It shall not retry either event or
455
499
  let a failure of that finish attempt replace the start error.
500
+ A start-sink rejection causally identical to the applicable signal reason is
501
+ the cancellation itself, not a control error: no host call begins, the
502
+ best-effort paired finish carries the boundary's canonical aborted evidence —
503
+ `status: 'aborted'` for a host call, or the rejected-before-effect disposition
504
+ and reason for apply — and nothing is latched. An ordinary run boundary settles
505
+ as §Abort prescribes. At the apply boundary the same event remains
506
+ pre-acceptance: `apply` rejects with that exact reason, records no receipt, and
507
+ leaves the key reusable
508
+ ([DR-036](../specs/decisions/036-coherent-abort-settlement.md)).
456
509
  When a call boundary carries `callId`, that id shall be unique within the
457
510
  runtime session. A stable FSM `stateId` is identity metadata in the payload,
458
511
  not a call id and shall not be reused as one across repeated invocations.
@@ -498,51 +551,36 @@ Trace payloads never become Boss-visible status or prompt text.
498
551
  The link compiler shall accept:
499
552
 
500
553
  - The FSM artifact (path to a `.fsm.ts`).
501
- - A **player binding** mapping GEARS players (declared in the
502
- [text2gears](text2gears.md#players) source) to opaque player-identifier
503
- strings.
504
- Where no binding is supplied, the linker shall apply the default
505
- binding — each player to its lowercased name (e.g. `Coder` → `coder`)
506
- — and record the applied binding in the emitted header.
507
554
  - An **adjudication strategy** (default: LLM-judge per state) and a
508
555
  **Boss-event mapping** (default: free-text judge classification).
509
556
  Both strategies are host-agnostic.
510
557
 
511
558
  The host's identity does not enter compilation; the linked module runs unchanged under any host that implements `PlaybookPorts`.
512
559
 
513
- ## Player binding
560
+ ## Role identity
514
561
 
515
- Each delegated GEARS state names exactly one player
516
- (`player` actor `invoke.input.player`).
517
- The linker shall map every named player to a `playerId` string used in
518
- `PlaybookPorts.callPlayer(playerId, …)`.
519
- The host adapter routes that opaque string to its concrete primitive.
562
+ Each delegated GEARS state names exactly one canonical local role id (`player` actor `invoke.input.role`).
563
+ The linker shall retain that id in `PlaybookPorts.callPlayer(roleId, …)` without selecting a concrete player.
564
+ The host shall bind that local role id explicitly when it constructs the runtime.
520
565
  Every direct-Captain and delegated-player invocation shall also carry its
521
566
  working leaf's explicit
522
567
  `stateId`; a linked runtime shall use that field for call identity and shall
523
568
  not infer one leaf from a structured root snapshot.
524
- Direct `captain` actor states bypass player binding and call
525
- `PlaybookPorts.callCaptain`; the linker shall not synthesize a player id named
526
- `captain` for them.
527
-
528
- For composite players declared with aliases (e.g., `Committer = Coder | Reviewer`), the linker shall resolve the alias **per source item**.
529
- Resolution inspects the `PlayerInput` fields populated at that state:
530
-
531
- - If only one `<playerName>Player` field is present, bind to that player.
532
- - If multiple are present, prefer the first-listed alternative in the alias declaration order.
533
- - If none are present, fall back to the alias's first alternative.
534
-
535
- Resolution shall be deterministic and recorded in the emitted module so future maintainers can audit it without re-running the linker.
536
-
537
- The linker shall not invent player identifiers beyond the recorded default
538
- binding, and shall not silently collapse aliases at the FSM level — composite
539
- players keep their `player: 'Committer'` value on `PlayerInput`; resolution
540
- decides only the `callPlayer` invocation.
569
+ Direct `captain` actor states call `PlaybookPorts.callCaptain`; the linker shall not synthesize a local role or concrete player id named `captain` for them.
570
+ The linker shall reject an alias-shaped role declaration rather than choose a runtime identity.
541
571
 
542
572
  ## Player prompt composition
543
573
 
544
574
  The runtime shall compose the actual player prompt from the state's
545
575
  `PlayerInput`.
576
+ The shared-factory `composePlayerPrompt` seam shall receive an
577
+ invocation-scoped `promptIdentity(roleId)` lookup as its second argument.
578
+ The lookup shall return the current detached session binding's prompt identity,
579
+ or the canonical local role id when bindings are absent, and shall reject an
580
+ undeclared role.
581
+ It shall expose neither the resolved player id nor the binding map, and the
582
+ runtime shall not place the lookup or any value read through it in options,
583
+ machine input, FSM context, or a persisted snapshot.
546
584
  `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.
547
585
  A leading `>` inside that body is authored quoted-context content and shall reach the player unchanged.
548
586
 
@@ -632,10 +670,14 @@ nonconformant.
632
670
  The FSM's `events` union enumerates every Boss-originated event.
633
671
  The runtime receives Boss input as a free-form string
634
672
  (`handleBossInput.text`).
635
- Where the current ready or reconstructed terminal machine accepts exactly one
673
+ Where the current ready, recoverable-failure (`failed`), or reconstructed
674
+ terminal machine accepts exactly one
636
675
  ordinary textual entry event and no Boss question is pending, the runtime
637
676
  shall send that event deterministically and attach the exact original text to
638
- its declared textual payload field without invoking `callJudge`.
677
+ its declared textual payload field without invoking `callJudge`: each of the
678
+ three is an entry awaiting a fresh intent, so delivered text has exactly one
679
+ meaning there and a judge call could only spend budget or settle the restart
680
+ as no action.
639
681
  The default Captain — the controller playbook of
640
682
  [gears2fsm "Setup"](gears2fsm.md#setup) — is deterministic at every parked
641
683
  entry: the runtime maps each Boss turn from the exact text and the host's
@@ -687,7 +729,7 @@ classification is recoverable control input, not a public boundary rejection.
687
729
  If a recovered `BOSS_REPLY` names no question that is currently pending, it is
688
730
  such a malformed classification: emit the one recovery status, send no event,
689
731
  leave the actor unchanged, and return `no-action` after emissions drain.
690
- Host-owned runtime options, player bindings, and enabled-playbook catalogs are
732
+ Host-owned runtime options, role-to-player bindings, and enabled-playbook catalogs are
691
733
  not Boss-event payload. The classifier schema and parser shall not invite or
692
734
  accept them, and classified prose shall never overwrite their machine context.
693
735
  Every recovered classifier object shall have exactly `type` plus the declared
@@ -705,14 +747,14 @@ aborted while the classifier finish emission was pending, return and trace the
705
747
  same structured `aborted` result against the unchanged actor.
706
748
  When the FSM supports a Boss-reply suspension state, the prompt shall inspect
707
749
  the actor snapshot context and include each exact pending Boss question,
708
- question id, and asking player so the judge can distinguish a reply from a
709
- fresh directive. With one pending question, a classified `BOSS_REPLY` that
710
- omits its optional id shall be filled with that sole id. With several pending
711
- questions, the classifier shall require a known id. A reply shall re-enter only
750
+ question id, and discriminated Captain-or-role asker so the judge can distinguish a reply from a fresh directive.
751
+ With one pending question, a classified `BOSS_REPLY` that omits its optional id shall be filled with that sole id.
752
+ With several pending questions, the classifier shall require a known id.
753
+ A reply shall re-enter only
712
754
  its recorded resume state and preserve the original intent, plan, prior child
713
755
  results, and Q+A continuation context.
714
- The classifier-facing pending-question block contains only `questionId`,
715
- `player`, and `question`. Internal `resumeStateId`, source-item identity, and
756
+ The classifier-facing pending-question block contains only `questionId`, `asker`, and `question`.
757
+ Internal `resumeStateId`, source-item identity, and
716
758
  other machine-routing fields remain authoritative in snapshot context and
717
759
  shall not be serialized into the judge prompt.
718
760
  The allowed fresh directives while parked include every applicable root entry
@@ -911,10 +953,34 @@ The provided actor shall:
911
953
  `{ guard: <first declared guard>, exitStatus: 0 }`; any nonzero status
912
954
  resolves the second declared guard with that status. Guard selection is
913
955
  mechanical; the runtime shall not route script output through the judge.
914
- - Reject only when the command cannot be spawned at all, routing through the
915
- state's ordinary `onError` path.
916
- - Honor the active turn's abort signal by terminating the child process and
917
- rejecting per §Abort.
956
+ - Reject when the command cannot be spawned at all, routing through the
957
+ state's ordinary `onError` path. Beyond spawn failure, the invocation
958
+ rejects only per the abort bullet below or when one of its own script
959
+ emissions rejects; a completed command's exit status itself never rejects.
960
+ - Honor the active turn's abort signal per §Abort: the actor shall reject
961
+ without spawning when the combined signal is already aborted; shall run the
962
+ shell detached as its own process-group leader; and on abort — whenever it
963
+ lands before the invocation settles, including only after the shell's own
964
+ exit — shall deliver
965
+ `SIGTERM` to the entire group, escalate to `SIGKILL` after a bounded grace,
966
+ and settle only after the shell process itself has exited and the group has
967
+ stopped being signalable, confirmed by an `ESRCH` liveness probe, rejecting
968
+ with the signal's reason. The same
969
+ bounded grace caps the post-`SIGKILL` wait for kernel teardown, so an
970
+ unreaped member outside the runtime's control cannot stall settlement. If
971
+ the group remains signalable through that bound, or confirmation fails
972
+ without `ESRCH`, the boundary rejects with a distinct teardown control error
973
+ rather than reporting a clean abort over unconfirmed cleanup. The kill is
974
+ always posted before the actor settles. Abort ownership — the
975
+ listener and its escalation — spans the whole invocation, not the
976
+ spawn-to-exit window
977
+ ([DR-036](../specs/decisions/036-coherent-abort-settlement.md)). An abort
978
+ observed only after the shell's exit shall additionally reject before guard
979
+ resolution and before starting any script emission not already in flight; an
980
+ emission already started when the abort lands completes through the
981
+ ordinary serialized channel and the rejection follows it. A
982
+ descendant that leaves the process group is beyond the runtime's kill
983
+ scope.
918
984
  - Emit, after the child settles and before the invocation resolves, one status
919
985
  line `Executed script for <stateId> (exit <status>).` and one telemetry
920
986
  event under topic `playbook.script` with payload
@@ -939,14 +1005,18 @@ XState `.provide(...)` receives the exact declared actor input rather than a
939
1005
  structurally similar local type.
940
1006
  Construct one bridge per runtime and wire every integration hook: allocate ids
941
1007
  with `nextCallId`; return the currently active public-boundary signal from
942
- `getBoundarySignal`; bind `resumePlaybookCall.signal` before settling the
943
- deferred actor through `bindResumeSignal`; enqueue the exact start/finish trace
944
- through `emitStarted` / `emitFinished`; drain the global emission queue through
945
- `drain`; latch the original control error through `onControlPlaneError`; and
946
- retain any cleanup/observer failure through `onBackgroundError` for the next
947
- public boundary or disposal rejection. The runtime shall not leave these
948
- optional API hooks unwired merely because their TypeScript properties are
949
- optional for simpler bridge consumers.
1008
+ `getBoundarySignal`; capture an immutable cancellation classifier for the
1009
+ invocation's signal identities; compose `resumePlaybookCall.signal` into that
1010
+ classifier through `bindResumeSignal`; pass the applicable classifier through
1011
+ `emitStarted`, `emitFinished`, and `drain`; bind it to the root transition
1012
+ caused by child settlement through `bindActorSettlement`; and pass it through
1013
+ `onControlPlaneError` and `onBackgroundError`. Each receiving latch shall drop
1014
+ only a failure the supplied classifier identifies as exact cancellation and
1015
+ shall retain every distinct cleanup or observer failure for the owning public
1016
+ boundary, the next drain, or disposal rejection as applicable. A stored
1017
+ distinct failure shall never be reclassified against a later boundary. The
1018
+ runtime shall not leave these optional API hooks unwired merely because their
1019
+ TypeScript properties are optional for simpler bridge consumers.
950
1020
  On invocation the bridge allocates a runtime-local call id, traces the start,
951
1021
  and calls `PlaybookPorts.callPlaybook` with the composed target/text and the
952
1022
  bridge signal combined from the XState invocation lifetime, the active public
@@ -1031,12 +1101,15 @@ registry; linker-time metadata is not authorization to call a target.
1031
1101
 
1032
1102
  Disposal shall settle an outstanding call as aborted and drain its finish
1033
1103
  trace before `session.disposed`.
1034
- If registered child abort cleanup rejects, the bridge shall emit the paired
1035
- finish with an error result and reject `abortPending` or disposal with that
1036
- original cleanup error; it shall not swallow the failure merely because the
1037
- promise actor also observes a `NestedPlaybookCallError`. Parent disposal shall
1038
- still drain, emit its one `session.disposed` boundary, and clear the bound
1039
- session before rejecting with that preserved cleanup error.
1104
+ If registered child abort cleanup rejects with a failure distinct from every
1105
+ applicable abort reason, the bridge shall emit the paired finish with an error
1106
+ result and reject `abortPending` or disposal with that original cleanup error,
1107
+ or with an aggregate containing every distinct failure when more than one
1108
+ remains;
1109
+ an exact abort-reason rejection is cancellation evidence and shall not be
1110
+ retained as a control failure. Parent disposal shall still drain, emit its one
1111
+ `session.disposed` boundary, and clear the bound session before rejecting with
1112
+ any preserved distinct cleanup error.
1040
1113
  Child output and errors must be JSON-safe; a non-JSON-safe result is a
1041
1114
  control-plane error.
1042
1115
 
@@ -1114,6 +1187,12 @@ The `PlaybookRuntime` shall:
1114
1187
  transition-trace or telemetry sink failure is part of `init`: initialization
1115
1188
  shall reject, stop the actor, and perform the failed-start cleanup below
1116
1189
  rather than swallowing it as a later background error.
1190
+ A root-actor error observed during startup — an initial entry action or a
1191
+ synchronously failing initial invocation — is equally part of `init` and
1192
+ `restore`: the boundary shall reject with that original error after the
1193
+ failed-start cleanup, and shall never resolve leaving the errored actor as
1194
+ later background state
1195
+ ([DR-036](../specs/decisions/036-coherent-abort-settlement.md)).
1117
1196
  Where the FSM input declares `selfPlaybookId`, seed it from the immutable
1118
1197
  `session.playbookId`; do not expose a caller option or reuse a working leaf's
1119
1198
  `stateId` as the self-call identity.
@@ -1167,7 +1246,14 @@ The `PlaybookRuntime` shall:
1167
1246
  transition/status/telemetry queue before returning, just as
1168
1247
  `handleBossInput` does. A resume shall not allocate a new Boss-input
1169
1248
  `turnId`; retain the original call-start turn id for its matching finish and
1170
- for the parent continuation caused by that return. Every success and
1249
+ for the parent continuation caused by that return.
1250
+ A resume whose signal is already aborted after identity and result
1251
+ validation shall deliver nothing: bind no resume signal, settle no deferred,
1252
+ emit no call finish, and preserve the pending call — the boundary settles
1253
+ `{ outcome: 'aborted' }` with the signal's reason while the suspended state
1254
+ and pending identity survive, so a later resume with the same call id and a
1255
+ fresh signal still delivers
1256
+ ([DR-036](../specs/decisions/036-coherent-abort-settlement.md)). Every success and
1171
1257
  exceptional path shall drain ordered emissions, select the first latched
1172
1258
  non-abort control error before considering abort, and clear its boundary
1173
1259
  latches in `finally`, so a failed resume cannot leak an emission error into a
@@ -1191,23 +1277,20 @@ The `PlaybookRuntime` shall:
1191
1277
  failure cannot skip the parent disposal boundary or leave the runtime bound.
1192
1278
 
1193
1279
  The actor's `lastError` field shall be surfaced via `emitStatus` when the machine enters its `failed` state.
1194
- Presence of linker-emitted `playerStates` selects the canonical
1195
- factory-backed status profile. That profile shall emit the selected Boss event type
1280
+ Presence of linker-emitted `roleStates` selects the canonical factory-backed status profile.
1281
+ That profile shall emit the selected Boss event type
1196
1282
  before sending that event, exactly `→ <guard>` (with no payload-count or tally
1197
1283
  rider) when a settling actor output carries a guard, and
1198
- `⤷ <Player>: <label>` only when the entered state appears in the
1199
- linked module's `playerStates` metadata. It shall emit no raw state-id fallback
1200
- for any other state. `playerStates` shall be a complete map of the FSM states
1201
- that invoke the typed `player` actor; each value carries the exact player from
1202
- that state's source-derived `meta.playbook.player` and the state's exact FSM
1203
- description as `{ player, label }`. The factory shall reject an incomplete
1204
- entry, a non-player state, or a player or label that differs from the FSM
1205
- metadata. A schema-1 legacy module that predates this metadata
1206
- shall preserve the prior factory status defaults, including no classification
1207
- line, `Entered <stateId>.` for ordinary state entry, the single question line,
1208
- and the unglyphed failure line. The new profile is opt-in so adding the seam
1209
- does not silently reinterpret an already-linked artifact without an ABI or
1210
- artifact-schema bump.
1284
+ `⤷ <Role>: <label>` only when the entered state appears in the linked module's `roleStates` metadata.
1285
+ It shall emit no raw state-id fallback for any other state.
1286
+ `roleStates` shall be a complete map of the FSM states
1287
+ that invoke the typed `player` actor; each schema-2 value carries the exact
1288
+ local role from that state's source-derived `meta.playbook.role` and the state's exact FSM description as `{ role, label }`.
1289
+ The factory shall reject an
1290
+ incomplete entry, a non-player state, or a role or label that differs from the FSM metadata.
1291
+ Artifact schema `1` and a missing compatibility declaration
1292
+ shall reject before interpretation because their legacy `player` values may
1293
+ encode bindings or aliases rather than canonical local roles.
1211
1294
  For the default Captain runtime, an initial `ready` state and a terminal `done`
1212
1295
  state shall not emit human status. The terminal response is already visible
1213
1296
  Captain prose; a synthetic “entered done” message would present it twice.
@@ -1223,9 +1306,14 @@ transition first.
1223
1306
 
1224
1307
  If a `*.call.started` trace records and then its sink rejects, no host call may
1225
1308
  begin. The runtime shall still enqueue exactly one synthetic paired
1226
- `*.call.finished` trace with `status: 'error'`, preserving the original call
1309
+ `*.call.finished` trace with `status: 'error'`, or `status: 'aborted'` when
1310
+ the sink rejection is causally identical to the applicable signal reason, in
1311
+ which case nothing is latched and the turn follows abort settlement
1312
+ ([DR-036](../specs/decisions/036-coherent-abort-settlement.md)) — preserving
1313
+ the original call
1227
1314
  id, turn id, actor visibility, state/source identity, and prompt or request
1228
- metadata from the start boundary. It shall then follow the same latched
1315
+ metadata from the start boundary. A distinct rejection shall then follow the
1316
+ same latched
1229
1317
  control-error, FSM settlement, and ordered-drain path as any other call-start
1230
1318
  failure; the synthetic finish must not replace the original sink error.
1231
1319
 
@@ -1245,22 +1333,22 @@ quiescent state with actor status `active`.
1245
1333
  At a safe capture point it shall return a JSON-safe
1246
1334
  `PlaybookRuntimeSnapshot` carrying:
1247
1335
 
1248
- - `schemaVersion`: literal `2`.
1336
+ - `schemaVersion`: literal `3`.
1249
1337
  - `playbookId`: the bound session's playbook id.
1250
1338
  - `machine`: the root actor's `getPersistedSnapshot()` result, passed
1251
1339
  through the shared JSON detachment with any raw `Error` context value
1252
1340
  (for example FSM `lastError`) normalized to `{ name, message, stack? }`
1253
1341
  first. The value is opaque to hosts.
1254
- - `playerResumeTokens`: the resume-token map as a plain object
1342
+ - `roleResumeTokens`: the local-role resume-token projection as a plain object
1255
1343
  (§PlaybookPorts contract).
1256
1344
  - `sequences`: the live `trace`, `turn`, `judgeCall`, `playerCall`, and
1257
1345
  `playbookCall` counters, plus `captainCall` when the runtime supports direct
1258
- Captain calls. A direct-Captain-capable runtime shall persist it in current
1259
- schema-version-2 exports; it remains optional in legacy schema-version-1
1260
- input, where restore uses `trace` as its collision-safe floor.
1346
+ Captain calls.
1347
+ A direct-Captain-capable runtime shall persist it in every schema-version-3 export.
1261
1348
  - `state`: the current normalized state descriptor.
1262
1349
  - `pendingBossQuestions`: the pending Boss question(s) from FSM context as
1263
- a list of `{ questionId, player, question, sourceItem? }`, empty when the
1350
+ a list of `{ questionId, asker, question, sourceItem? }`, where `asker` is
1351
+ `{ kind: 'captain' }` or `{ kind: 'role', roleId }`, empty when the
1264
1352
  parked state awaits no reply. This list exists so hosts can surface the
1265
1353
  question without parsing status lines or telemetry.
1266
1354
  - `suspendedCall`: omitted when no nested call is pending; otherwise the
@@ -1278,19 +1366,16 @@ unsafe and returns `undefined`.
1278
1366
  `restore(session, snapshot)` is an alternative to `init` under the same
1279
1367
  lifecycle guards (§Session lifecycle): it shall reject when already
1280
1368
  initialized, disposing, or disposed, and shall validate
1281
- schema version `1` or `2` and that `snapshot.playbookId` equals
1282
- `session.playbookId` before touching state. Schema version `1` remains a
1283
- descriptor-free legacy input; schema version `2` may carry the suspended-call
1284
- descriptor above.
1369
+ schema version `3` and that `snapshot.playbookId` equals `session.playbookId` before touching state.
1370
+ Runtime snapshot schemas `1` and `2` shall reject before state binding because their token and pending-question fields conflate local roles, concrete players, and Captain identity.
1285
1371
  The host supplies the same immutable `PlaybookSession` identity the
1286
1372
  snapshot was exported under and recreates the runtime through the same
1287
1373
  factory with equivalent options; the runtime does not diff options, and
1288
1374
  module identity — that the factory constructing this runtime still
1289
1375
  belongs to the snapshot's playbook — is likewise the host's check to
1290
1376
  make before calling `restore`.
1291
- `restore` shall bind the session, restore the resume-token map, the
1292
- sequence counters (using the persisted global `trace` counter as a
1293
- collision-safe floor for an absent legacy `captainCall`), and the
1377
+ `restore` shall bind the session and its current detached role bindings, restore the local-role token projection, the
1378
+ sequence counters, and the
1294
1379
  prior-state descriptor from the snapshot,
1295
1380
  prepare the shared nested bridge with the suspended-call descriptor or its
1296
1381
  explicit absence, restore a descriptor's call-to-turn map entry, construct
@@ -1401,11 +1486,18 @@ still describes the state. Two families exist, labeled from source state
1401
1486
  descriptions:
1402
1487
 
1403
1488
  - **Failure-state retry** — while the singular state id is the recoverable
1404
- failure state and the runtime holds a recorded last classified event (the
1405
- event a public Boss boundary sent that drove the run into `failed`, kept
1406
- with its recorded payload), and the live snapshot accepts that event, the
1407
- runtime shall advertise `retry:<EVENT_TYPE>` replaying exactly that
1408
- recorded event.
1489
+ failure state and the live snapshot accepts the retry event sourced below,
1490
+ the runtime shall advertise `retry:<EVENT_TYPE>` replaying exactly that
1491
+ event. Where the emitted module's entry-event declaration names the FSM
1492
+ context member the machine's entry action copies the exact Boss text into
1493
+ (DR-034), the retry event is that deterministic entry event built from the
1494
+ live snapshot's member — excluded when the member is absent, not a string,
1495
+ or blank, and never falling back to the record. Where it names no member,
1496
+ the retry event is the recorded last classified event (the event a public
1497
+ Boss boundary sent that drove the run into `failed`, kept with its recorded
1498
+ payload), and there is none while the runtime holds none. The member is
1499
+ declared, never inferred from a context member that happens to match the
1500
+ entry event's text field.
1409
1501
  - **Jump entries** — for each registered resumable state id whose
1410
1502
  explicit-state-jump event (`BOSS_INTERRUPT` with that `targetId`, optional
1411
1503
  textual fields omitted) the live snapshot accepts, guards included, the
@@ -1460,12 +1552,17 @@ trace and the return agree, and a receipt states what happened to the effect
1460
1552
  rather than what happened to its telemetry. The published receipt stands, is
1461
1553
  returned and replayed verbatim, and the delivery failure travels on the
1462
1554
  runtime's emission-failure channel to surface from the next public boundary
1463
- that drains.
1555
+ that drains — unless the delivery failure is causally identical to the apply
1556
+ signal's own abort reason, in which case it evidences the cancellation and is
1557
+ dropped, not latched
1558
+ ([DR-036](../specs/decisions/036-coherent-abort-settlement.md)).
1464
1559
 
1465
1560
  The recorded receipts and the recorded last classified event are
1466
- process-local: the durable runtime snapshot persists neither, and a
1467
- restored runtime advertises a retry again only after its next classified
1468
- event.
1561
+ process-local: the durable runtime snapshot persists neither. A restored
1562
+ runtime therefore advertises the retry of a declared entry-event source
1563
+ immediately — that payload rides the persisted machine snapshot — while a
1564
+ module declaring no source advertises a retry again only after its next
1565
+ classified event.
1469
1566
 
1470
1567
  ## Abort
1471
1568
 
@@ -1479,7 +1576,61 @@ the shared `combineAbortSignals`). Classify a rejection as cancellation by its
1479
1576
  causal identity with the applicable signal reason, not by an `AbortError` name
1480
1577
  or by observing only that the signal is also aborted. Signals may carry an
1481
1578
  ordinary `Error`, while a distinct transport or sink failure that occurs after
1482
- abort remains a non-abort control error and takes precedence. On abort, the
1579
+ abort remains a non-abort control error and takes precedence. Classification
1580
+ lives at each latch or report site, against the boundary signal applicable
1581
+ there — the invocation-lifetime combined signal, and during a resume that
1582
+ boundary's own signal — so a failure causally identical to the applicable
1583
+ reason is the cancellation's own evidence: it is handled there under the phase
1584
+ rules below, never mislabeled as a distinct failure and never carried to an
1585
+ unrelated later boundary
1586
+ ([DR-036](../specs/decisions/036-coherent-abort-settlement.md)).
1587
+ A failure already latched as distinct retains that ownership; a later drain
1588
+ shall not reinterpret it against another boundary whose abort signal happens
1589
+ to use the same object as its reason.
1590
+ A public boundary settles on the machine's state at its quiescence point,
1591
+ in this precedence: a suspended pending call, then a distinct actor error,
1592
+ then terminal completion, then a coincident abort, then the recoverable
1593
+ failure state — a completed machine settles `terminal` even when the signal
1594
+ also aborted, because an `aborted` settlement over a terminal machine hides
1595
+ work the next turn would silently restart.
1596
+ An abort observed after the outcome is computed does not rewrite it, and a
1597
+ settlement-channel rejection causally identical to the abort reason is
1598
+ forgiven, so the returned result and the settlement trace state one fact.
1599
+ A boundary entered with an already-aborted signal delivers nothing.
1600
+ That entry refusal precedes the ordinary settlement order: a pre-aborted
1601
+ resume reports `aborted` while preserving its suspended pending call rather
1602
+ than reporting `suspended` for work it did not deliver.
1603
+ Cancellation-coupled channel rejections obey this phase matrix:
1604
+
1605
+ - **Before a host call or effect starts (and before apply acceptance):** an
1606
+ identical start-channel rejection starts no host call or effect and latches
1607
+ no control error. A recorded start receives one best-effort `aborted` finish.
1608
+ An ordinary run boundary then settles by the precedence above; a
1609
+ pre-acceptance `apply` instead rejects with that exact reason, records no
1610
+ receipt, and leaves its key reusable.
1611
+ - **After a host call or effect starts but before its finish or outcome is
1612
+ recorded:** an identical host, cleanup, observer, or in-flight-emission
1613
+ rejection is cancellation evidence. Invocation-owned cleanup completes, a
1614
+ started trace pair receives one `aborted` finish, and the ordinary boundary
1615
+ settles by the precedence above. A distinct rejection remains a control
1616
+ failure, produces the applicable error finish, and takes distinct-error
1617
+ precedence.
1618
+ - **After a call finish is recorded but before the enclosing non-apply outcome
1619
+ is computed:** an identical finish-sink or drain rejection leaves the
1620
+ recorded finish unchanged, emits no corrective second finish, latches
1621
+ nothing, and lets the enclosing boundary settle by the precedence above.
1622
+ - **After apply acceptance but before receipt publication:** every settlement
1623
+ failure, the exact apply abort reason included, is folded into the current
1624
+ `failed` receipt. Acceptance forbids throwing; the replacement receipt is
1625
+ published, returned, and replayed, and the failure is not carried as a later
1626
+ delivery error.
1627
+ - **After a non-apply outcome is computed or an apply receipt is published:**
1628
+ an identical rejection is dropped without rewriting the outcome or receipt
1629
+ and without poisoning a later boundary. A distinct non-apply settlement
1630
+ rejection retains current-boundary control-error precedence; a distinct
1631
+ post-publication apply rejection retains the published receipt and travels
1632
+ on the delivery-failure channel to the next boundary that drains.
1633
+ On abort, the
1483
1634
  runtime shall not merely race the imperative
1484
1635
  wait and return while an invocation remains live: it shall let the selected
1485
1636
  rejection path settle and drive the actor to a quiescent state before returning
@@ -1543,12 +1694,13 @@ The `playbook.trace` copies are the host-agnostic runtime-boundary record requir
1543
1694
  ## Output
1544
1695
 
1545
1696
  The link compiler emits one TypeScript module per playbook.
1546
- For an FSM that declares no `type: 'parallel'` state, it shall emit the thin
1547
- shared-factory module defined below.
1697
+ For an FSM that declares no `type: 'parallel'` state necessarily flat
1698
+ under [gears2fsm.md](gears2fsm.md)'s one-state-per-item mapping it shall
1699
+ emit the thin shared-factory module defined below.
1548
1700
  For an FSM that declares a parallel state, it shall emit bespoke linked
1549
1701
  machinery satisfying this document's runtime contract and shall not invoke
1550
- `createXStatePlaybookRuntime`, whose supported domain is single-region FSMs
1551
- under [DR-019](../specs/decisions/019-shared-linked-runtime-factory.md).
1702
+ `createXStatePlaybookRuntime`, whose supported domain is flat single-region
1703
+ FSMs under [DR-019](../specs/decisions/019-shared-linked-runtime-factory.md).
1552
1704
  The FSM-interpreter machinery — actor wiring, boundary tracing, Boss-event
1553
1705
  mapping, adjudication, script execution, nested-playbook bridging, session
1554
1706
  lifecycle, abort handling, and the optional parked-session snapshot
@@ -1593,17 +1745,19 @@ The thin emitted module:
1593
1745
  actor.
1594
1746
  - Supplies in `spec` only what the factory cannot read from the FSM
1595
1747
  artifact's own data: the deterministic textual entry event where
1596
- §Boss-event mapping prescribes deterministic entry; compact `bossEvents`
1748
+ §Boss-event mapping prescribes deterministic entry, naming with it the FSM
1749
+ context member that event's own transition action copies the exact Boss
1750
+ text into wherever the machine keeps one, so the failure-state retry of
1751
+ §Control surface survives a restore; compact `bossEvents`
1597
1752
  metadata for each additional Boss-union arm whose exact required/optional
1598
1753
  judge fields, runtime-owned text fields, or closed string values disappear
1599
1754
  under TypeScript erasure; `placeholderFields` only for authored token/field
1600
1755
  exceptions not covered by the canonical kebab-token-to-camel-field mapping
1601
1756
  and the canonical `<#>` → `irNumber` special case; the
1602
- transition-event payload fields the FSM's Boss union declares; a
1603
- non-default player binding where the linker inputs supplied one; the
1604
- complete `playerStates` status map derived from every FSM state that invokes
1605
- the typed `player` actor, with each `player` copied from that state's
1606
- source-derived `meta.playbook.player` (an empty map when there is no such
1757
+ transition-event payload fields the FSM's Boss union declares; the
1758
+ complete `roleStates` status map derived from every FSM state that invokes
1759
+ the typed `player` actor, with each `role` copied from that state's
1760
+ source-derived `meta.playbook.role` (an empty map when there is no such
1607
1761
  state); the
1608
1762
  `verbatimPayloadFields` set derived from annotated result fields above; the
1609
1763
  `controlContextFields` projection of §Control surface; and any
@@ -1641,13 +1795,13 @@ The thin emitted module:
1641
1795
  fields?: Readonly<Record<string, XStateBossEventFieldSpec>>;
1642
1796
  }
1643
1797
 
1644
- interface XStatePlayerStateStatus {
1645
- player: string;
1798
+ interface XStateRoleStateStatus {
1799
+ role: string;
1646
1800
  label: string;
1647
1801
  }
1648
1802
 
1649
1803
  bossEvents?: readonly XStateBossEventSpec[];
1650
- playerStates?: Readonly<Record<string, XStatePlayerStateStatus>>;
1804
+ roleStates?: Readonly<Record<string, XStateRoleStateStatus>>;
1651
1805
  placeholderFields?: Readonly<Record<string, string>>;
1652
1806
  ```
1653
1807
 
@@ -1663,8 +1817,8 @@ The thin emitted module:
1663
1817
  runtime-owned arm to have lost payload detail under erasure shall report
1664
1818
  that gap rather than emit the entry.
1665
1819
  - Supplies `spec.compat` with the compatibility values current at link time:
1666
- `{ artifactSchema, runtimeAbi }`, where `artifactSchema` is `1` — the
1667
- schema number of the thin-module format this §Output defines — and
1820
+ `{ artifactSchema, runtimeAbi }`, where `artifactSchema` is `2` — the
1821
+ schema number of the local-role thin-module format this §Output defines — and
1668
1822
  `runtimeAbi` is the installed shared engine's `RUNTIME_ABI` self-report.
1669
1823
  The linker shall verify that the installed engine lists the emitted
1670
1824
  schema in `SUPPORTED_ARTIFACT_SCHEMAS` and treat its absence as a
@@ -1675,9 +1829,17 @@ The thin emitted module:
1675
1829
  declaration against the engine instance that actually loads the emitted
1676
1830
  module and fails construction on a mismatch, so an artifact linked under
1677
1831
  one engine cannot run silently skewed under another. Modules emitted
1678
- before this contract carry no `compat` member and remain loadable.
1832
+ before this contract carry no `compat` member and shall reject before interpretation.
1833
+ - Requires the containing public registry manifest to advertise the same `artifactSchema: 2`; the Captain host shall reject a missing or disagreeing registry value before constructing this runtime, and a bespoke runtime profile shall advertise the same schema without claiming this shared factory's `runtimeAbi`.
1679
1834
  - Default-exports the factory call as `createPlaybookRuntime`, typed
1680
- `PlaybookRuntimeFactory<PlaybookRuntimeOptions>`.
1835
+ `PlaybookRuntimeFactory<PlaybookRuntimeOptions>`. A registry module loads
1836
+ dynamically inside the host's caught boundary, so its eager module-scope
1837
+ factory call fails fast there. The compiled session Captain module is the
1838
+ exception: the shell and both CLI front ends import it statically, so it
1839
+ shall defer its factory call to the first runtime request — an eager call
1840
+ would turn a future `spec.compat` rejection into an uncaught module-load
1841
+ error that takes even `--help` down, instead of the caught
1842
+ host-construction boundary's setup diagnostic.
1681
1843
  - Exposes, under an `_internal` export, the pure helpers verification
1682
1844
  needs — at least the prompt composers its own machine uses, which may
1683
1845
  re-export the shared defaults when the spec does not override composition —
@@ -1693,7 +1855,7 @@ The thin emitted module:
1693
1855
  speaks only `PlaybookPorts` for every agent and host concern; the
1694
1856
  `node:child_process` dependency of §Script execution lives in the shared
1695
1857
  factory, not in the emitted module.
1696
- - Records the linker inputs (FSM path, player binding, strategies) in a
1858
+ - Records the linker inputs (FSM path and strategies) in a
1697
1859
  top-of-file header comment so the file is reproducible from the same
1698
1860
  inputs.
1699
1861
  - Sources the contract types (`PlayerResult`, `PlayerCallOptions`,
@@ -1781,7 +1943,7 @@ New behavior in any of these areas requires a separate slc spec.
1781
1943
 
1782
1944
  ## References
1783
1945
 
1784
- [1]: [text2gears](text2gears.md) "First phase: text → GEARS spec items."
1785
- [2]: [gears2fsm](gears2fsm.md) "Second phase: GEARS items → FSM artifact."
1946
+ [1]: text2gears.md "First phase: text → GEARS spec items."
1947
+ [2]: gears2fsm.md "Second phase: GEARS items → FSM artifact."
1786
1948
  [3]: https://stately.ai/docs/actors "XState actors — `createActor`, snapshots, abort signal handling."
1787
1949
  [4]: https://github.com/sindresorhus/p-queue#readme "p-queue concurrency and AbortSignal support."