@sublang/playbook 4.0.0 → 6.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 (73) hide show
  1. package/README.md +69 -122
  2. package/docs/assets/playbook-venn.svg +13 -0
  3. package/docs/cli.md +43 -26
  4. package/docs/configuration.md +63 -18
  5. package/docs/embedding.md +24 -16
  6. package/package.json +43 -22
  7. package/reference/sdlc/captain.md +70 -83
  8. package/reference/sdlc/captain.playbook/captain.fsm.d.ts +127 -142
  9. package/reference/sdlc/captain.playbook/captain.fsm.js +349 -470
  10. package/reference/sdlc/captain.playbook/captain.fsm.ts +535 -598
  11. package/reference/sdlc/captain.playbook/captain.gears.md +37 -41
  12. package/reference/sdlc/captain.playbook/captain.playbook.d.ts +90 -15
  13. package/reference/sdlc/captain.playbook/captain.playbook.js +466 -976
  14. package/reference/sdlc/captain.playbook/captain.playbook.ts +698 -1001
  15. package/reference/sdlc/code.md +55 -97
  16. package/reference/sdlc/code.playbook/code.fsm.d.ts +229 -94
  17. package/reference/sdlc/code.playbook/code.fsm.introspect.d.ts +26 -44
  18. package/reference/sdlc/code.playbook/code.fsm.introspect.js +61 -66
  19. package/reference/sdlc/code.playbook/code.fsm.introspect.ts +100 -149
  20. package/reference/sdlc/code.playbook/code.fsm.js +587 -1347
  21. package/reference/sdlc/code.playbook/code.fsm.ts +809 -1650
  22. package/reference/sdlc/code.playbook/code.gears.md +51 -263
  23. package/reference/sdlc/code.playbook/code.playbook.d.ts +8 -47
  24. package/reference/sdlc/code.playbook/code.playbook.js +69 -639
  25. package/reference/sdlc/code.playbook/code.playbook.ts +90 -850
  26. package/reference/sdlc/code.playbook/code.registry.d.ts +9 -25
  27. package/reference/sdlc/code.playbook/code.registry.js +20 -78
  28. package/reference/sdlc/code.playbook/code.registry.ts +58 -122
  29. package/reference/sdlc/code.playbook/playbook-captain.d.ts +2 -0
  30. package/reference/sdlc/code.playbook/playbook-captain.js +1877 -251
  31. package/reference/sdlc/code.playbook/playbook-captain.ts +2385 -352
  32. package/reference/sdlc/code.playbook/playbook.config.template.yaml +33 -22
  33. package/reference/sdlc/decide.md +54 -0
  34. package/reference/sdlc/decide.playbook/decide.fsm.d.ts +261 -0
  35. package/reference/sdlc/decide.playbook/decide.fsm.js +894 -0
  36. package/reference/sdlc/decide.playbook/decide.fsm.ts +1152 -0
  37. package/reference/sdlc/decide.playbook/decide.gears.md +88 -0
  38. package/reference/sdlc/decide.playbook/decide.playbook.d.ts +67 -0
  39. package/reference/sdlc/{discuss.playbook/discuss.playbook.js → decide.playbook/decide.playbook.js} +511 -370
  40. package/reference/sdlc/{discuss.playbook/discuss.playbook.ts → decide.playbook/decide.playbook.ts} +616 -451
  41. package/reference/sdlc/decide.playbook/decide.registry.d.ts +41 -0
  42. package/reference/sdlc/decide.playbook/decide.registry.js +60 -0
  43. package/reference/sdlc/decide.playbook/decide.registry.ts +125 -0
  44. package/reference/sdlc/review.md +81 -0
  45. package/reference/sdlc/review.playbook/review.fsm.d.ts +183 -0
  46. package/reference/sdlc/review.playbook/review.fsm.js +524 -0
  47. package/reference/sdlc/review.playbook/review.fsm.ts +652 -0
  48. package/reference/sdlc/review.playbook/review.gears.md +112 -0
  49. package/reference/sdlc/review.playbook/review.playbook.d.ts +12 -0
  50. package/reference/sdlc/review.playbook/review.playbook.js +112 -0
  51. package/reference/sdlc/review.playbook/review.playbook.ts +201 -0
  52. package/reference/sdlc/review.playbook/review.registry.d.ts +43 -0
  53. package/reference/sdlc/review.playbook/review.registry.js +73 -0
  54. package/reference/sdlc/review.playbook/review.registry.ts +138 -0
  55. package/slc/gears2fsm.md +67 -6
  56. package/slc/link.md +339 -25
  57. package/slc/text2gears.md +22 -2
  58. package/src/runtime.d.ts +36 -1
  59. package/src/runtime.ts +59 -0
  60. package/src/xstate-playbook-runtime.d.ts +96 -7
  61. package/src/xstate-playbook-runtime.js +1018 -49
  62. package/src/xstate-playbook-runtime.ts +1283 -59
  63. package/src/xstate-runtime.js +25 -0
  64. package/src/xstate-runtime.ts +51 -0
  65. package/reference/sdlc/discuss.md +0 -93
  66. package/reference/sdlc/discuss.playbook/discuss.fsm.d.ts +0 -396
  67. package/reference/sdlc/discuss.playbook/discuss.fsm.js +0 -2067
  68. package/reference/sdlc/discuss.playbook/discuss.fsm.ts +0 -2465
  69. package/reference/sdlc/discuss.playbook/discuss.gears.md +0 -258
  70. package/reference/sdlc/discuss.playbook/discuss.playbook.d.ts +0 -113
  71. package/reference/sdlc/discuss.playbook/discuss.registry.d.ts +0 -58
  72. package/reference/sdlc/discuss.playbook/discuss.registry.js +0 -97
  73. package/reference/sdlc/discuss.playbook/discuss.registry.ts +0 -153
package/slc/gears2fsm.md CHANGED
@@ -167,6 +167,7 @@ The default never applies to an item carrying a `Results:` label, and it does
167
167
  not license inferring any richer contract from prose.
168
168
  The item's blockquote alone becomes `invoke.input.prompt`; the `Results:`
169
169
  label and bullets shall never enter that prompt.
170
+ A `>` that remains at the start of a prompt line after removing the outer GEARS blockquote marker is literal quoted-context content and shall remain in `invoke.input.prompt` unchanged.
170
171
  Each result description shall name every additional output field its accepting
171
172
  guard requires, using the exact case-sensitive property names. For example, a
172
173
  delegation or continuing-call description whose guard reads the planned child
@@ -193,6 +194,49 @@ Both direct-Captain states additionally receive the universal
193
194
  `needsBossReply` result. Their guards and actions shall use those exact
194
195
  case-sensitive names so the compiled adjudication contract remains stable.
195
196
 
197
+ For a controller playbook — one whose Source declares DR-029's session-scoped
198
+ controller policy: a session Captain that runs for the whole host session,
199
+ receives every Boss turn, and operates the working playbooks from outside the
200
+ engagement stack (DR-029) — the compiler shall apply the additive
201
+ controller decision-state class below. The class joins the stable compiler
202
+ contract beside the decide-call-observe vocabulary above; that vocabulary and
203
+ the universal `needsBossReply` rule stay untouched for the artifacts that
204
+ consume them.
205
+ The controller machine shall be a session loop, not a finite errand: a
206
+ quiescent conversational hub (tag `playbook.parked`) receives each Boss turn;
207
+ the controller decision state decides it over the closed action set; a
208
+ `respond` selection settles its turn in the decision call itself, its
209
+ validated `text` being the turn's captain speech; an acting selection's host
210
+ settlement becomes the outcome report grounding one closing-reply call; and
211
+ the machine returns to the hub for the next turn. Returning to the hub after
212
+ a settled turn completes the session loop's turn; it is not the idle-hub
213
+ routing that [Transitions](#transitions) reserves for recovery.
214
+ Because the hub receives every Boss turn, a controller state carries no
215
+ Boss-reply suspension: the compiler shall not add `needsBossReply` to a
216
+ controller machine's `invoke.input.result` maps — a clarifying question to
217
+ Boss is a `respond` selection.
218
+ The machine shall declare no terminal result output and shall keep exactly
219
+ one reachable `type: 'final'` shutdown state entered only by the host's
220
+ teardown event. The completion rule of
221
+ [Errors and termination](#errors-and-termination) applies unamended: its
222
+ output clause binds only where Source declares a terminal result, which a
223
+ controller Source does not.
224
+ The controller decision state's direct-Captain result contract discriminates
225
+ the closed action set of DR-029. Its guard discriminants are a stable
226
+ compiler contract, not names the compiler may invent — `respond`, `start`,
227
+ `switch`, `dismiss`, `deliver`, and `runtime` — with each guard's required
228
+ payload fields:
229
+
230
+ - `respond` requires `text`;
231
+ - `start` and `switch` each require `playbookId` and `input`;
232
+ - `runtime` requires `actionId`;
233
+ - `dismiss` and `deliver` require none — a `deliver` result in particular
234
+ carries no text payload: the host is authoritative for the delivered text,
235
+ so the contract declares no field for it.
236
+
237
+ The decision state's guards and actions shall use those exact case-sensitive
238
+ names so the compiled controller contract remains stable.
239
+
196
240
  ## States
197
241
 
198
242
  Each state shall declare:
@@ -200,13 +244,15 @@ Each state shall declare:
200
244
  - a stable `id` (for `#id` targeting and Boss interrupts);
201
245
  - an intuitive state key (the property name under `states: { ... }`);
202
246
  - a one-line `description` (for inspector tools and documentation);
203
- - JSON-safe `meta: { playbook: { stateId, description } }` repeating its
204
- stable id and description so linked runtimes can discover active public
205
- identities through `snapshot.getMeta()` without private XState nodes;
247
+ - JSON-safe `meta: { playbook: { stateId, description, player? } }` repeating
248
+ its stable id and description so linked runtimes can discover active public
249
+ identities through `snapshot.getMeta()` without private XState nodes. A
250
+ delegated-player state shall also carry the exact source player in
251
+ `meta.playbook.player`; every other state shall omit `player`;
206
252
  - if it invokes the direct `captain` actor: `invoke.input` carrying
207
253
  `sourceItem`, `prompt`, and `result` (per [Setup](#setup));
208
254
  - if it invokes the delegated `player` actor: `invoke.input` additionally
209
- carrying `player`;
255
+ carrying the same source-derived `player` as `meta.playbook.player`;
210
256
  - if it invokes the `script` actor: `invoke.input` carrying `stateId`,
211
257
  `sourceItem`, `command`, and `result` (per [Setup](#setup)) — no `prompt`
212
258
  and no `player`.
@@ -555,6 +601,11 @@ context precondition required to enter that target safely. It shall not jump
555
601
  into a working or reassessment state with missing intent, prior result, plan,
556
602
  or other required context and shall not invent defaults merely to make an
557
603
  interrupt target executable.
604
+ Control-action discovery probes these guards with optional textual fields
605
+ omitted. Before applying a string operation such as `trim()`, a generated
606
+ guard shall narrow the field to a string; a missing required textual field
607
+ shall make the guard return false, never throw, so the control view remains
608
+ total and omits an action whose payload the runtime cannot source.
558
609
  XState automatically stops the current state's invoked actor on transition [[2]].
559
610
  Where the default Captain's routing state accepts a fresh intent while another
560
611
  state or Boss-reply wait is active, its `BOSS_INTERRUPT` event shall carry a
@@ -588,11 +639,21 @@ cannot supply alone, the machine shall suspend that task in a quiescent wait
588
639
  state and resume the same task with the Q+A in the next prompt.
589
640
  This is a third Boss surface alongside `BOSS_INTERRUPT` and Boss entry events.
590
641
 
591
- Every captain- and player-invoking state supports this path.
642
+ Every captain- and player-invoking state supports this path, with one
643
+ exception the compiler shall apply, not infer: the states of a controller
644
+ machine ([Setup](#setup), controller decision-state class) carry no
645
+ Boss-reply suspension, because its hub already receives every Boss turn and a
646
+ clarifying question to Boss is a `respond` selection over the closed action
647
+ set. The rule below is therefore universal over workflow states and silent
648
+ about that class; in particular, adding `needsBossReply` to the controller
649
+ decision state would add a seventh outcome to a closed six-action contract
650
+ whose guard discriminants [Setup](#setup) fixes, and is nonconformant.
592
651
  There is no source-level opt-in annotation and no `needsBossReply` result metadata in GEARS output.
593
652
  The FSM compiler shall preserve the GEARS blockquote as the state's domain `prompt` body and shall not inject any Boss-question instruction into `invoke.input.prompt`.
653
+ This preservation includes every literal leading `>` carried inside the outer GEARS blockquote for quoted runtime context.
594
654
 
595
- For every captain- and player-invoking state, the compiler shall add
655
+ For every captain- and player-invoking state outside a controller machine,
656
+ the compiler shall add
596
657
  `needsBossReply` to the state's `invoke.input.result` map.
597
658
  The description shall be the standard adjudicator-facing text:
598
659
 
package/slc/link.md CHANGED
@@ -59,9 +59,17 @@ interface PlaybookSession {
59
59
  parentSessionId?: string;
60
60
  parentCallId?: string;
61
61
  depth: number;
62
+ playerSessions?: PlayerSessionStore;
62
63
  ports: PlaybookPorts;
63
64
  }
64
65
 
66
+ interface PlayerSessionStore {
67
+ select(playerId: string): string | false;
68
+ update(playerId: string, resumeToken?: string): void;
69
+ snapshot(): Readonly<Record<string, string>>;
70
+ restore(tokens: Readonly<Record<string, string>>): void;
71
+ }
72
+
65
73
  type JsonValue =
66
74
  | null
67
75
  | boolean
@@ -143,7 +151,7 @@ exactly one `pendingCall` is active.
143
151
  Control-plane exceptions reject the runtime method rather than masquerade as a
144
152
  recoverable workflow `failed` result.
145
153
 
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.
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.
147
155
  The link compiler emits a typed options interface per playbook based on the FSM's `CodingInput` (or equivalent).
148
156
  The CLI's absence of `--link-option` values does not mean that
149
157
  `PlaybookRuntimeOptions` is empty. CLI link options are compile-time inputs;
@@ -240,6 +248,10 @@ type PlaybookCallStart =
240
248
 
241
249
  `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)).
242
250
  The runtime treats `status !== 'ok'` as a player failure and routes it through the FSM's error path (§Abort).
251
+ An `ok` player result whose `finalText` is missing, empty, or whitespace-only
252
+ earns exactly one corrective re-ask: the same player call repeated under the
253
+ stored resume selection, traced as its own player-call pair, before a second
254
+ such result routes through the same error path.
243
255
 
244
256
  `callCaptain` runs a direct-Captain FSM actor against the host's Captain
245
257
  agent. The linked runtime shall pass
@@ -257,9 +269,13 @@ array requests a tool-free call, while omission preserves the host Captain's
257
269
  configured tools.
258
270
  `CaptainResult` carries no resume token or player-continuation selection.
259
271
  A non-`ok`
260
- result, or an `ok` result without non-empty `finalText`, shall record that
261
- failure on the call's single finish trace and reject the actor through the
262
- FSM's error path. These structured host-result failures are recoverable
272
+ result, or an `ok` result whose `finalText` is missing, empty, or
273
+ whitespace-only, shall record that failure on the call's single finish trace.
274
+ A non-`ok` result shall then reject the actor through the FSM's error path
275
+ with no corrective re-ask; an empty `ok` result shall first earn exactly one
276
+ corrective re-ask — the same call repeated, traced as its own
277
+ started/finished pair — and only a second such result shall reject the actor
278
+ the same way. These structured host-result failures are recoverable
263
279
  workflow failures, not control-plane failures: the runtime shall let the actor
264
280
  take `onError`, drive it to quiescence, drain ordered emissions, and resolve
265
281
  the public method with `{ outcome: 'failed' }` carrying the failure state's
@@ -347,6 +363,8 @@ type PlaybookTraceType =
347
363
  | 'captain.call.finished'
348
364
  | 'playbook.call.started'
349
365
  | 'playbook.call.finished'
366
+ | 'apply.started'
367
+ | 'apply.finished'
350
368
  | 'fsm.transition'
351
369
  | 'status.emitted'
352
370
  | 'boss.input.settled'
@@ -369,14 +387,28 @@ interface PlaybookTraceEvent {
369
387
  }
370
388
  ```
371
389
 
390
+ A composing host may supply `playerSessions` as a frame-local view of player continuation owned by the root engagement tree.
391
+ 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
+ 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
+ A host that omits the view retains the runtime's private per-session continuation behavior.
394
+
372
395
  The trace types are `session.started`, `boss.input.received`,
373
396
  `judge.call.started`, `judge.call.finished`, `player.call.started`,
374
397
  `player.call.finished`, `captain.call.started`, `captain.call.finished`,
375
398
  `playbook.call.started`,
376
- `playbook.call.finished`, `fsm.transition`, `status.emitted`,
399
+ `playbook.call.finished`, `apply.started`, `apply.finished`,
400
+ `fsm.transition`, `status.emitted`,
377
401
  `boss.input.settled`, and `session.disposed`.
378
402
  Call pairs carry exact prompts and replies, normalized failures, actor and state
379
403
  identity, and their boundary-specific options.
404
+ `apply.started` and `apply.finished` are the paired schema-2 boundary of an
405
+ executed `apply()` call on a runtime implementing the optional control surface
406
+ (§Control surface): both carry the action id and idempotency `key` (plus the
407
+ singular `stateId` on start when one exists), the pair shares one
408
+ session-unique `apply-<n>` call id and the boundary's turn id, and the finish
409
+ adds the receipt `disposition` with its `reason`, normalized `error`, or
410
+ projected `run` result, all JSON-safe. A repeated idempotency key returns the
411
+ recorded receipt without a new pair.
380
412
  Direct-Captain start and finish payloads shall carry `allowedTools` exactly when
381
413
  the originating `CaptainCallOptions` selects it and shall omit the member when
382
414
  the call preserves the host Captain's configured tools.
@@ -392,16 +424,23 @@ its message and optional data; consumers shall not have to recover state
392
424
  identity from a nested ad hoc object.
393
425
  Judge results use `reply`; player start and finish payloads both carry the
394
426
  selected `resume`; Captain start and finish payloads both carry
395
- the exact composed prompt, `visibility: 'visible'`, the direct invocation's
396
- `stateId` and `sourceItem`, and no player resume selection or resume token;
427
+ the exact composed prompt, the boundary's selected `visibility`, the direct
428
+ invocation's `stateId` and `sourceItem`, and no player resume selection or
429
+ resume token — a visible workflow call carries its runtime-owned
430
+ `resume: false` selection, while a hidden controller call
431
+ (§Captain adjudication) omits the `resume` member altogether, its
432
+ durable-conversation selection being host-owned;
397
433
  judge `purpose` is
398
434
  `boss-input-classification`, `player-output-adjudication`, or
399
435
  `captain-output-adjudication`; and every error uses
400
436
  `{ name, message, stack? }` rather than a raw string or `Error` instance.
401
437
  The Captain finish payload shall preserve the exact `CaptainResult` status and
402
438
  final text when present, while carrying any failure in normalized form.
403
- An `ok` result without `finalText` therefore retains status `ok` but also
404
- carries the normalized missing-text failure that makes the actor reject.
439
+ An `ok` result without non-empty `finalText` therefore retains status `ok` but
440
+ also carries the normalized missing-text failure; the corrective re-ask that
441
+ follows (§PlaybookPorts contract) traces as its own started/finished pair, and,
442
+ when that corrective call happens, only a second such finish makes the actor
443
+ reject.
405
444
  If the Captain port rejects before returning a result, the finish instead
406
445
  carries explicit `status: 'aborted'` when the combined signal has aborted or
407
446
  `status: 'error'` otherwise. A finish boundary never omits status merely
@@ -505,6 +544,7 @@ decides only the `callPlayer` invocation.
505
544
  The runtime shall compose the actual player prompt from the state's
506
545
  `PlayerInput`.
507
546
  `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
+ A leading `>` inside that body is authored quoted-context content and shall reach the player unchanged.
508
548
 
509
549
  The composer may prepend structured labelled blocks from typed `PlayerInput`
510
550
  fields the FSM exposes (for example `Boss intent:`, `Review items:`,
@@ -596,8 +636,17 @@ Where the current ready or reconstructed terminal machine accepts exactly one
596
636
  ordinary textual entry event and no Boss question is pending, the runtime
597
637
  shall send that event deterministically and attach the exact original text to
598
638
  its declared textual payload field without invoking `callJudge`.
599
- The default Captain's ready entry is
600
- `{ type: 'BOSS_INTENT', bossIntent: turn.text }`.
639
+ The default Captain the controller playbook of
640
+ [gears2fsm "Setup"](gears2fsm.md#setup) is deterministic at every parked
641
+ entry: the runtime maps each Boss turn from the exact text and the host's
642
+ deterministic command-parse resolution, supplied through the linked options'
643
+ controller port, to the rewritten machine's hub entry union —
644
+ `{ type: 'BOSS_TURN', bossText: turn.text }` for an undecided turn,
645
+ `{ type: 'PARSED_RESPOND', bossText: turn.text }` for a parse-resolved
646
+ `respond`, `{ type: 'PARSED_ACTION', bossText: turn.text, decision }` carrying
647
+ the injected parse-resolved decision object, and `{ type: 'SHUTDOWN' }` for the
648
+ host's teardown resolution — and invokes no classifier judge call; the exact
649
+ original text still rides only the runtime-owned textual payload field.
601
650
  All other non-empty turns shall use `callJudge` only to choose one of the FSM's
602
651
  event kinds and non-text routing fields, or no FSM action.
603
652
  The classifier prompt shall include the exact, unmodified `turn.text` in a
@@ -677,7 +726,7 @@ If a host forwards text beginning with `/` to `handleBossInput`, the runtime tre
677
726
  Hosts that receive structured control input shall resolve host-level concerns before choosing a playbook runtime.
678
727
  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.
679
728
 
680
- `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.
729
+ Within `handleBossInput` classification, `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; `apply()` of a runtime-advertised action (§Control surface) is the second, runtime-validated path to the same events, and on neither path does the host fabricate an FSM event itself.
681
730
  It is _not_ an abort surface; aborts go through the abort signal and the strategies in §Abort.
682
731
  Hosts where the abort signal is terminal (e.g., SIGINT runs shutdown) shall not route abort to `BOSS_INTERRUPT`.
683
732
 
@@ -696,6 +745,9 @@ clause (or equivalent typed output metadata). Backticked prose before that
696
745
  clause can name statuses, guards, or concepts such as `ok`, `aborted`, and
697
746
  `error`; those names are not output properties and shall never become required
698
747
  judge fields.
748
+ For a delegated-player field annotated exactly `` `<field>: <verbatim final text>` ``, the judge shall select the guard but the runtime shall replace any judge-supplied value with the player's canonical non-empty final text before returning the actor output.
749
+ The linker shall derive the complete `verbatimPayloadFields` set from those annotations across the FSM result maps.
750
+ A field name that is annotated in one result map and unannotated in another is a link error because the shared adjudication strategy cannot give one property both ownership policies.
699
751
  For a direct Captain result, `question` and `response` are human-presentation
700
752
  fields owned by the visible call rather than fields authored by the hidden
701
753
  judge.
@@ -746,6 +798,55 @@ presentation. The linked
746
798
  runtime shall not make a second visible Captain call or expose the hidden
747
799
  structured adjudication merely to present the same response.
748
800
 
801
+ That visible-call presentation — visible Captain prose as the Boss
802
+ presentation, a separate hidden adjudicator that never authors the
803
+ `question`/`response` fields, and runtime injection of the visible
804
+ `finalText` — stays scoped to a working playbook whose FSM declares a visible
805
+ direct-Captain state. For a controller playbook, whose FSM declares the controller
806
+ decision-state class of [gears2fsm "Setup"](gears2fsm.md#setup), the
807
+ Captain-call presentation admits the hidden controller form instead
808
+ (DR-029): the decision and closing-reply Captain calls run
809
+ `{ visibility: 'hidden' }` on the host's durable conversation, whose resume
810
+ token the host pins and rotates (DR-029), and the decision call's reply
811
+ is the `{ action, … }` control JSON itself — validated by the linked runtime
812
+ against the declared decision-state contract rather than adjudicated through
813
+ a separate judge call, with exactly one corrective re-ask appending the
814
+ rejection reason and the restated reply contract (the DR-025 corrective
815
+ pattern). Because the host owns that conversation, the `resume` member the
816
+ runtime is required to pass on a controller call carries no continuity
817
+ meaning: the runtime passes `resume: false` because it holds no token, and
818
+ the host's pinned durable selection overrides it — a controller call shall
819
+ never be read as a request for a fresh conversation.
820
+
821
+ The validated selection is not itself an effect. The linked runtime shall
822
+ submit it through the host-supplied controller port the linker exposed as an
823
+ option member (§PlaybookRuntime contract) and shall take the returned
824
+ settlement as the only evidence of what happened. That settlement, carried
825
+ beside the selected action as the guard discriminant, is the decision
826
+ invocation's own result — the actor output the decision state's `onDone` arms
827
+ select on and its evidence action records — so the effect reports through the
828
+ same invocation that decided it, with no second boundary and no host-sent
829
+ event. A controller prose state (a parse-resolved `respond` reply, an acting
830
+ turn's closing reply) settles on its declared single outcome and returns no
831
+ prose to the machine at all.
832
+
833
+ That reply is control data, never Boss presentation: the runtime
834
+ shall not inject the `question` or `response` presentation fields into a
835
+ controller result — no visible Captain call exists to own them. Controller
836
+ prose reaches the Boss only as host-validated captain speech surfaced
837
+ through the host's presentation seam, cligent `CaptainContext.emitReply`
838
+ (DR-029). Because no prose returns to the machine and no presentation
839
+ field is injected, the host's own `callCaptain` implementation is that seam:
840
+ it already holds the `CaptainResult` of the call it just served, and it
841
+ identifies which call that is from the paired `captain.call.started` boundary
842
+ the runtime emits before invoking the port, whose identity carries the
843
+ invoking `stateId` and `sourceItem` (§Playbook trace). No prose therefore
844
+ needs a return path through the machine, and none exists.
845
+ The visible-call `question`/`response` injection rule above and
846
+ the `{ visibility: 'visible', resume: false }` workflow-call selection
847
+ (§PlaybookPorts contract, §Captain prompt composition) stay the
848
+ visible-presentation shape for non-controller playbooks.
849
+
749
850
  The adjudicator shall fail loudly on:
750
851
 
751
852
  - A guard the state does not declare,
@@ -791,9 +892,9 @@ independent cleanup evidence.
791
892
  ## Script execution
792
893
 
793
894
  Where the FSM declares the typed `script` actor from
794
- [gears2fsm "Setup"](gears2fsm.md#setup), the linked runtime shall provide
795
- its implementation through the shared factory (§Output); the linker shall
796
- not regenerate a script executor inside each emitted module.
895
+ [gears2fsm "Setup"](gears2fsm.md#setup), a factory-backed linked runtime shall
896
+ provide its implementation through the shared factory (§Output); the linker
897
+ shall not regenerate a script executor inside a factory-backed emitted module.
797
898
  A script invocation is the one actor kind that runs without any agent:
798
899
  it makes no `callPlayer`, `callCaptain`, or `callJudge` call and needs no
799
900
  adjudication.
@@ -1090,6 +1191,23 @@ The `PlaybookRuntime` shall:
1090
1191
  failure cannot skip the parent disposal boundary or leave the runtime bound.
1091
1192
 
1092
1193
  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
1196
+ before sending that event, exactly `→ <guard>` (with no payload-count or tally
1197
+ 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.
1093
1211
  For the default Captain runtime, an initial `ready` state and a terminal `done`
1094
1212
  state shall not emit human status. The terminal response is already visible
1095
1213
  Captain prose; a synthetic “entered done” message would present it twice.
@@ -1170,6 +1288,162 @@ not `active` or whose state descriptor cannot be normalized shall fail
1170
1288
  A restore failure shall leave the runtime unbound so `dispose` remains
1171
1289
  callable and terminal.
1172
1290
 
1291
+ ## Control surface (optional)
1292
+
1293
+ A linked runtime may implement the optional control-surface capability of
1294
+ `@sublang/playbook/runtime` — `describe()` and `apply(...)` — so a host can
1295
+ observe the parked machine and execute a runtime-advertised recovery or jump
1296
+ action without fabricating an FSM event (DR-029). A runtime that
1297
+ implements either member shall implement both; every runtime the shared
1298
+ `createXStatePlaybookRuntime` factory constructs implements the pair. A
1299
+ runtime lacking the pair advertises no actions, and plain text delivery is
1300
+ the only verb against it. Presence is feature-detected like the
1301
+ parked-session snapshot capability; the pair changes no runtime ABI and no
1302
+ artifact or snapshot schema.
1303
+
1304
+ ```typescript
1305
+ interface PlaybookControlAction {
1306
+ id: string; // stable within the returned view
1307
+ label: string; // runtime-written, Boss-appropriate
1308
+ }
1309
+
1310
+ interface PlaybookControlView {
1311
+ state: PlaybookState;
1312
+ stateDescription?: string; // runtime-published meaning of the current state
1313
+ context?: JsonValue; // the runtime's authored projection, sanitized
1314
+ pendingQuestions: readonly PlaybookPendingBossQuestion[];
1315
+ lastError?: NormalizedError;
1316
+ actions: readonly PlaybookControlAction[];
1317
+ }
1318
+
1319
+ type PlaybookControlReceipt =
1320
+ | { disposition: 'rejected'; reason: string } // before any effect
1321
+ | { disposition: 'executed'; run: PlaybookRunResult }
1322
+ | { disposition: 'failed'; error: NormalizedError }; // effects may exist
1323
+
1324
+ // Optional PlaybookRuntime members — both or neither:
1325
+ describe?(): PlaybookControlView;
1326
+ apply?(input: { actionId: string; key: string; signal: AbortSignal }): Promise<PlaybookControlReceipt>;
1327
+ ```
1328
+
1329
+ `describe()` shall be side-effect free — it emits no trace, status, or
1330
+ telemetry and moves no machine state — and is valid at parked quiescence
1331
+ outside an active `handleBossInput`/`resumePlaybookCall`/`apply` boundary;
1332
+ during an active boundary, before `init`, or after disposal begins it shall
1333
+ throw. The view carries the current normalized state descriptor, the state
1334
+ description defined below, the authored context projection defined below, the
1335
+ pending Boss questions with their stable ids, the last recorded error in
1336
+ normalized form, and the currently valid actions.
1337
+
1338
+ `stateDescription` is the runtime's own Boss-facing statement of what its
1339
+ current state means, taken from the same source state descriptions the action
1340
+ labels below are written from. A controller host has no other grounding for a
1341
+ status answer, and an internal state id is not text a reply may repeat, so the
1342
+ runtime publishes the meaning rather than leaving the host to substitute the
1343
+ identifier for it. A state whose source declares no description carries no
1344
+ `stateDescription`: an id is never promoted into a description, so a host is
1345
+ never handed an identifier dressed as meaning.
1346
+
1347
+ The view's `context` is an explicit projection the linked runtime **authors**,
1348
+ never an allow-by-default serialization of the FSM context (PBRT-52).
1349
+ Only the runtime knows which of its context members are safe and relevant
1350
+ for a controller prompt, while the host receiving the view cannot inspect an
1351
+ opaque blob for the player rosters, option values, and raw player output its
1352
+ own prompts must exclude; exporting by default makes the two obligations
1353
+ unsatisfiable together and gives every member added to an FSM later the wrong
1354
+ default. The rules:
1355
+
1356
+ - The emitted module declares the projection in its `spec` as
1357
+ `controlContextFields` — the FSM context member names its view exposes, in
1358
+ the order it names them — and the factory exports those and nothing else.
1359
+ - A runtime naming no member carries no `context` at all, so a member is
1360
+ private until an artifact names it and extending an FSM leaks nothing by
1361
+ omission.
1362
+ - Sanitization sits on top of the projection, not in place of it: a named
1363
+ member is still normalized (raw `Error` values normalized) and dropped when
1364
+ it cannot be made JSON-safe, rather than thrown, since `describe` stays
1365
+ side-effect free and total.
1366
+ - The two members the view surfaces first-class — the pending Boss question
1367
+ and the last error — cannot be named. A projection naming either is a
1368
+ construction error, failing runtime construction rather than being silently
1369
+ ignored.
1370
+ - The host still composes its own prompt block from the projection rather than
1371
+ pasting the projection in, so no runtime's exported value can forge a block
1372
+ into an envelope the host owns.
1373
+
1374
+ Actions derive from the live snapshot, only at the same safe point the
1375
+ parked-session snapshot uses (actor status `active`, quiescent, no pending
1376
+ nested call); anywhere else `actions` is empty while the rest of the view
1377
+ still describes the state. Two families exist, labeled from source state
1378
+ descriptions:
1379
+
1380
+ - **Failure-state retry** — while the singular state id is the recoverable
1381
+ failure state and the runtime holds a recorded last classified event (the
1382
+ event a public Boss boundary sent that drove the run into `failed`, kept
1383
+ with its recorded payload), and the live snapshot accepts that event, the
1384
+ runtime shall advertise `retry:<EVENT_TYPE>` replaying exactly that
1385
+ recorded event.
1386
+ - **Jump entries** — for each registered resumable state id whose
1387
+ explicit-state-jump event (`BOSS_INTERRUPT` with that `targetId`, optional
1388
+ textual fields omitted) the live snapshot accepts, guards included, the
1389
+ runtime shall advertise `jump:<stateId>`.
1390
+
1391
+ A candidate whose event requires a payload the runtime cannot source from
1392
+ recorded state shall be excluded from `actions` — `apply` never invents free
1393
+ text and never enters Boss-input classification. A candidate whose *label*
1394
+ could only be an identifier is excluded on the same terms: a label never falls
1395
+ back to a target id or to the replayed event type, because a controller host
1396
+ names an executed or refused action by its label and never by its id, so an
1397
+ identifier used as a label defeats that substitution. A jump whose target
1398
+ publishes no description is therefore not advertised — borrowing another
1399
+ state's description would name the wrong state — and a retry falls back from
1400
+ its target's description to its own source state's, and is not advertised when
1401
+ neither exists.
1402
+
1403
+ `apply({ actionId, key, signal })` shall revalidate the action against the
1404
+ live state and settle `{ disposition: 'rejected', reason }` with no effect
1405
+ when it is no longer advertised. It shall execute an accepted action at most
1406
+ once per idempotency `key`: the receipt is recorded at acceptance, before
1407
+ the settlement emissions, and a repeated key returns the recorded receipt
1408
+ verbatim with no revalidation, no execution, and no new trace pair within the
1409
+ runtime instance.
1410
+ Only accepted receipts (`executed` or `failed`) are recorded and final for
1411
+ their key. A rejection settles before acceptance and records nothing under
1412
+ its key — a later call with that key revalidates afresh, traces its own
1413
+ pair, and may execute once the action is advertised — and a key whose call
1414
+ threw before reaching acceptance (lifecycle misuse, invalid input, a
1415
+ pre-acceptance abort, a rejected start-boundary sink) likewise records
1416
+ nothing, so a later call with that key may execute. Executing sends the
1417
+ validated event through the same actor drive as `handleBossInput` — state
1418
+ transitions, player/judge boundaries, statuses, and traces flow unchanged —
1419
+ and settles `executed` with the projected run result, or `failed` with the
1420
+ normalized error when the run settles in the failure state, aborts, or a
1421
+ post-acceptance control-plane error lands (effects may exist). `signal`
1422
+ follows §Abort exactly as a Boss-turn signal does; an abort after acceptance
1423
+ settles the `failed` receipt rather than rejecting. The boundary traces as
1424
+ the paired `apply.started` / `apply.finished` events of §Playbook trace, and
1425
+ `apply` shares the single active-boundary sentinel with `handleBossInput`
1426
+ and `resumePlaybookCall`.
1427
+
1428
+ Acceptance is also the line past which `apply` does not throw, and
1429
+ publication — the `apply.finished` emission — is the line past which its
1430
+ receipt no longer changes. A settlement failure after acceptance but before
1431
+ publication (a rejecting emission drain) settles the `failed` receipt carrying
1432
+ its normalized error, replacing the one recorded at acceptance so the finish
1433
+ trace, the returned receipt, and any replay of the key report one settlement.
1434
+ A settlement failure at or after publication (a rejecting `apply.finished`
1435
+ sink) does not: the disposition is already emitted, so no rewrite can make the
1436
+ trace and the return agree, and a receipt states what happened to the effect
1437
+ rather than what happened to its telemetry. The published receipt stands, is
1438
+ returned and replayed verbatim, and the delivery failure travels on the
1439
+ runtime's emission-failure channel to surface from the next public boundary
1440
+ that drains.
1441
+
1442
+ The recorded receipts and the recorded last classified event are
1443
+ process-local: the schema-1 parked-session snapshot persists neither, and a
1444
+ restored runtime advertises a retry again only after its next classified
1445
+ event.
1446
+
1173
1447
  ## Abort
1174
1448
 
1175
1449
  `handleBossInput.signal` is the abort surface.
@@ -1245,19 +1519,25 @@ The `playbook.trace` copies are the host-agnostic runtime-boundary record requir
1245
1519
 
1246
1520
  ## Output
1247
1521
 
1248
- The link compiler emits **one thin** TypeScript module per playbook.
1522
+ The link compiler emits one TypeScript module per playbook.
1523
+ For an FSM that declares no `type: 'parallel'` state, it shall emit the thin
1524
+ shared-factory module defined below.
1525
+ For an FSM that declares a parallel state, it shall emit bespoke linked
1526
+ machinery satisfying this document's runtime contract and shall not invoke
1527
+ `createXStatePlaybookRuntime`, whose supported domain is single-region FSMs
1528
+ under [DR-019](../specs/decisions/019-shared-linked-runtime-factory.md).
1249
1529
  The FSM-interpreter machinery — actor wiring, boundary tracing, Boss-event
1250
1530
  mapping, adjudication, script execution, nested-playbook bridging, session
1251
1531
  lifecycle, abort handling, and the optional parked-session snapshot
1252
- capability — is not regenerated per artifact: it ships once as the shared
1253
- `createXStatePlaybookRuntime(machine, spec)` factory exported by
1532
+ capability — is not regenerated for a factory-backed artifact: it ships once
1533
+ as the shared `createXStatePlaybookRuntime(machine, spec)` factory exported by
1254
1534
  `@sublang/playbook/xstate-runtime`, and the emitted module hands its FSM and
1255
1535
  a small per-playbook `spec` to that factory. Every behavioral section of
1256
1536
  this definition still binds the emitted module's runtime; the shared factory
1257
1537
  is how the emitted module satisfies them, so a runtime fix ships as a
1258
1538
  package release instead of a re-link of every artifact.
1259
1539
 
1260
- The emitted module:
1540
+ The thin emitted module:
1261
1541
 
1262
1542
  - Imports the FSM artifact by relative path with an extension-bearing
1263
1543
  runtime specifier. When the linked TypeScript is part of a package that
@@ -1297,10 +1577,28 @@ The emitted module:
1297
1577
  exceptions not covered by the canonical kebab-token-to-camel-field mapping
1298
1578
  and the canonical `<#>` → `irNumber` special case; the
1299
1579
  transition-event payload fields the FSM's Boss union declares; a
1300
- non-default player binding where the linker inputs supplied one; and any
1580
+ non-default player binding where the linker inputs supplied one; the
1581
+ complete `playerStates` status map derived from every FSM state that invokes
1582
+ the typed `player` actor, with each `player` copied from that state's
1583
+ source-derived `meta.playbook.player` (an empty map when there is no such
1584
+ state); the
1585
+ `verbatimPayloadFields` set derived from annotated result fields above; the
1586
+ `controlContextFields` projection of §Control surface; and any
1301
1587
  per-playbook strategy override (classifier, prompt composers,
1302
1588
  required-field extraction, status formatting) an earlier section of this
1303
- definition requires for that playbook. The metadata shall keep the shared
1589
+ definition requires for that playbook.
1590
+ `controlContextFields` is authored, not derived: the linker names the FSM
1591
+ context members the playbook's controller view exposes and no others, in the
1592
+ order the view should render them, omitting the member entirely where the
1593
+ playbook exposes no context. It is the one spec member whose default is
1594
+ *nothing* rather than everything — the factory exports no context for a
1595
+ module that supplies none — so a module emitted without it advertises a
1596
+ playbook with no Boss-visible context rather than one whose whole FSM
1597
+ context is Boss-visible. The linker shall not name a member the view
1598
+ surfaces first-class (the pending Boss question, the last error), which is a
1599
+ construction error, and shall not name a member carrying a resolved player
1600
+ roster, an option value, or player-authored text, which a controller host's
1601
+ prompts are required to exclude or to fence. The metadata shall keep the shared
1304
1602
  classifier's reply contract exactly flat `{ type, ...declaredFields }` and
1305
1603
  distinguish judge-authored routing fields from exact-text fields the
1306
1604
  runtime attaches itself. Everything else — player/script/captain/nested actor
@@ -1320,7 +1618,13 @@ The emitted module:
1320
1618
  fields?: Readonly<Record<string, XStateBossEventFieldSpec>>;
1321
1619
  }
1322
1620
 
1621
+ interface XStatePlayerStateStatus {
1622
+ player: string;
1623
+ label: string;
1624
+ }
1625
+
1323
1626
  bossEvents?: readonly XStateBossEventSpec[];
1627
+ playerStates?: Readonly<Record<string, XStatePlayerStateStatus>>;
1324
1628
  placeholderFields?: Readonly<Record<string, string>>;
1325
1629
  ```
1326
1630
 
@@ -1352,10 +1656,16 @@ The emitted module:
1352
1656
  - Default-exports the factory call as `createPlaybookRuntime`, typed
1353
1657
  `PlaybookRuntimeFactory<PlaybookRuntimeOptions>`.
1354
1658
  - Exposes, under an `_internal` export, the pure helpers verification
1355
- needs — at least the player-prompt and Captain-prompt composers
1356
- (`composePlayerPrompt` and `composeCaptainPrompt`), which may re-export
1357
- the shared defaults when the spec does not override composition — so
1358
- compilation-correctness tests can exercise composition without a host.
1659
+ needs — at least the prompt composers its own machine uses, which may
1660
+ re-export the shared defaults when the spec does not override composition —
1661
+ so compilation-correctness tests can exercise composition without a host.
1662
+ A playbook that calls players exposes `composePlayerPrompt`; a playbook
1663
+ whose states make direct-Captain calls exposes `composeCaptainPrompt`. A
1664
+ controller playbook that calls no players exposes no player composer:
1665
+ there is no composition to verify, and a stub under that name would
1666
+ describe work the module cannot do. `_internal` is not a public API — the
1667
+ leading underscore says so — and nothing in it is semver-stable; a helper
1668
+ a host is meant to call is a top-level export and is governed as one.
1359
1669
  - Holds no host-specific types and no host primitive calls. The runtime
1360
1670
  speaks only `PlaybookPorts` for every agent and host concern; the
1361
1671
  `node:child_process` dependency of §Script execution lives in the shared
@@ -1364,6 +1674,7 @@ The emitted module:
1364
1674
  top-of-file header comment so the file is reproducible from the same
1365
1675
  inputs.
1366
1676
  - Sources the contract types (`PlayerResult`, `PlayerCallOptions`,
1677
+ `PlayerSessionStore`,
1367
1678
  `CaptainResult`, `CaptainCallOptions`, `PlaybookPorts`, `PlaybookSession`,
1368
1679
  `PlaybookTraceEvent`,
1369
1680
  `PlaybookCallRequest`, `PlaybookCallResult`, `PlaybookCallStart`,
@@ -1375,6 +1686,9 @@ The emitted module:
1375
1686
  dependency runs one way — from each linked module to the shared
1376
1687
  engine and contract, never the reverse.
1377
1688
 
1689
+ Both output profiles remain subject to the behavioral sections above and the
1690
+ verification requirements below.
1691
+
1378
1692
  When a co-located integration test for the linked runtime already exists, the
1379
1693
  link compiler shall run it before reporting success and treat any failure as a
1380
1694
  generation failure. It shall not delete, skip, or weaken that suite to make a