@sublang/playbook 8.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 (30) hide show
  1. package/README.md +3 -3
  2. package/docs/cli.md +15 -15
  3. package/docs/configuration.md +13 -8
  4. package/docs/embedding.md +7 -2
  5. package/package.json +1 -1
  6. package/reference/sdlc/captain.playbook/captain.playbook.js +14 -3
  7. package/reference/sdlc/captain.playbook/captain.playbook.ts +18 -4
  8. package/reference/sdlc/code.playbook/code.fsm.d.ts +4 -1
  9. package/reference/sdlc/code.playbook/code.fsm.js +11 -4
  10. package/reference/sdlc/code.playbook/code.fsm.ts +12 -4
  11. package/reference/sdlc/code.playbook/code.playbook.js +14 -3
  12. package/reference/sdlc/code.playbook/code.playbook.ts +13 -3
  13. package/reference/sdlc/code.playbook/playbook-captain.js +44 -10
  14. package/reference/sdlc/code.playbook/playbook-captain.ts +47 -10
  15. package/reference/sdlc/decide.playbook/decide.fsm.d.ts +1 -1
  16. package/reference/sdlc/decide.playbook/decide.playbook.d.ts +2 -0
  17. package/reference/sdlc/decide.playbook/decide.playbook.js +299 -117
  18. package/reference/sdlc/decide.playbook/decide.playbook.ts +395 -131
  19. package/reference/sdlc/review.playbook/review.playbook.js +14 -3
  20. package/reference/sdlc/review.playbook/review.playbook.ts +13 -3
  21. package/slc/gears2fsm.md +19 -2
  22. package/slc/link.md +184 -42
  23. package/src/runtime.d.ts +1 -0
  24. package/src/runtime.ts +1 -0
  25. package/src/xstate-playbook-runtime.d.ts +13 -3
  26. package/src/xstate-playbook-runtime.js +732 -251
  27. package/src/xstate-playbook-runtime.ts +873 -280
  28. package/src/xstate-runtime.d.ts +17 -7
  29. package/src/xstate-runtime.js +135 -57
  30. package/src/xstate-runtime.ts +243 -84
@@ -9,7 +9,7 @@
9
9
  // Adjudication: LLM judge per state; coderOutput and reviewerOutput are
10
10
  // carried verbatim
11
11
  // Compat: artifact schema 2 / runtime ABI 1
12
- import { RUNTIME_ABI, createXStatePlaybookRuntime, snapshotJsonValue, } from '@sublang/playbook/xstate-runtime';
12
+ import { createXStatePlaybookRuntime, snapshotJsonValue, } from '@sublang/playbook/xstate-runtime';
13
13
  import { reviewMachine, } from './review.fsm.js';
14
14
  const PLACEHOLDER = /<(#|[A-Za-z_$][A-Za-z0-9_$-]*)>/g;
15
15
  const CONTINUATION_PREAMBLE = 'You previously paused this task to ask Boss a question; Boss has now replied. Continue the same task using the reply below.';
@@ -71,9 +71,20 @@ export const _internal = {
71
71
  };
72
72
  const runtimeSpec = {
73
73
  label: 'REVIEW',
74
- compat: { artifactSchema: 2, runtimeAbi: RUNTIME_ABI },
74
+ // DR-022 / slc/link.md: the declaration carries the value current at link
75
+ // time — a literal, never the loading engine's RUNTIME_ABI self-report,
76
+ // which would follow whatever engine loads the module and make the
77
+ // factory's skew check compare that engine with itself.
78
+ compat: { artifactSchema: 2, runtimeAbi: 1 },
75
79
  snapshotOptions: snapshotReviewOptions,
76
- entryEvent: { type: 'START_REVIEW', textField: 'callerInput' },
80
+ entryEvent: {
81
+ type: 'START_REVIEW',
82
+ textField: 'callerInput',
83
+ // `copyStartInput` copies the entry text here, so the failure-state
84
+ // retry reads it back from the persisted machine snapshot and survives
85
+ // a continued session (DR-034).
86
+ contextField: 'callerInput',
87
+ },
77
88
  roleStates: {
78
89
  reviewInitial: {
79
90
  role: 'reviewer',
@@ -11,7 +11,6 @@
11
11
  // Compat: artifact schema 2 / runtime ABI 1
12
12
 
13
13
  import {
14
- RUNTIME_ABI,
15
14
  createXStatePlaybookRuntime,
16
15
  snapshotJsonValue,
17
16
  type PlaybookPlayerInput,
@@ -156,9 +155,20 @@ export const _internal = {
156
155
 
157
156
  const runtimeSpec = {
158
157
  label: 'REVIEW',
159
- compat: { artifactSchema: 2, runtimeAbi: RUNTIME_ABI },
158
+ // DR-022 / slc/link.md: the declaration carries the value current at link
159
+ // time — a literal, never the loading engine's RUNTIME_ABI self-report,
160
+ // which would follow whatever engine loads the module and make the
161
+ // factory's skew check compare that engine with itself.
162
+ compat: { artifactSchema: 2, runtimeAbi: 1 },
160
163
  snapshotOptions: snapshotReviewOptions,
161
- entryEvent: { type: 'START_REVIEW', textField: 'callerInput' },
164
+ entryEvent: {
165
+ type: 'START_REVIEW',
166
+ textField: 'callerInput',
167
+ // `copyStartInput` copies the entry text here, so the failure-state
168
+ // retry reads it back from the persisted machine snapshot and survives
169
+ // a continued session (DR-034).
170
+ contextField: 'callerInput',
171
+ },
162
172
  roleStates: {
163
173
  reviewInitial: {
164
174
  role: 'reviewer',
package/slc/gears2fsm.md CHANGED
@@ -244,8 +244,9 @@ Each state shall declare:
244
244
  - a stable `id` (for `#id` targeting and Boss interrupts);
245
245
  - an intuitive state key (the property name under `states: { ... }`);
246
246
  - a one-line `description` (for inspector tools and documentation);
247
- - JSON-safe `meta: { playbook: { stateId, description, role? } }` repeating
248
- its stable id and description so linked runtimes can discover active public
247
+ - JSON-safe `meta: { playbook: { stateId, description, role? } }` naming the
248
+ state's public playbook identity per the identity rule below and repeating
249
+ its description so linked runtimes can discover active public
249
250
  identities through `snapshot.getMeta()` without private XState nodes. A
250
251
  delegated-role state shall also carry the canonical lowercase source role id in
251
252
  `meta.playbook.role`; every other state shall omit `role`;
@@ -258,6 +259,8 @@ Each state shall declare:
258
259
  and no `role`.
259
260
 
260
261
  The source item ID shall live in `invoke.input.sourceItem`, not in a comment — this keeps the GEARS-to-state mapping machine-readable.
262
+
263
+ Outside a parallel group's regions, a state's `meta.playbook.stateId` shall equal its state key — the one identity a factory-backed linked runtime indexes by.
261
264
  A delegated state's `invoke.input.role` shall match the canonical lowercase id of its source item's named role.
262
265
  A direct Captain state shall not invent a `Captain` role binding.
263
266
 
@@ -748,6 +751,20 @@ an unhandled runtime error.
748
751
  Every machine shall declare at least one `type: 'final'` state (typically `done`) reachable on completion.
749
752
  A never-terminating machine is a defect: the runner has no completion signal.
750
753
 
754
+ At least one is a floor, not a ceiling. A final state's `description` is the
755
+ machine's published terminal meaning: a host that cannot read the machine's
756
+ output quotes that description to report what the run did. It shall therefore
757
+ be true of every arm that enters the state and of no other terminal outcome.
758
+ Where Source declares more than one terminal outcome — an approval that
759
+ completes the workflow and a failure the workflow reports to its caller
760
+ instead of parking — each outcome shall get its own `type: 'final'` state
761
+ whose description names it. Routing an approval arm and a failure, abort, or
762
+ invalid-result arm into one final state is a defect of the same kind as a
763
+ wrong result field, because the quoting host cannot detect the difference.
764
+ This constrains only published meaning: the declared machine `output` still
765
+ derives its status and fields from typed context, so a caller that does read
766
+ the output is unaffected.
767
+
751
768
  Where Source declares a JSON-safe terminal result, the setup types shall
752
769
  declare that output and the root machine shall derive it from typed context
753
770
  through XState's machine `output` function. A final-state transition alone does
package/slc/link.md CHANGED
@@ -151,6 +151,7 @@ type PlaybookRunResult =
151
151
  | {
152
152
  outcome: 'terminal';
153
153
  state: PlaybookState;
154
+ stateDescription?: string;
154
155
  output?: JsonValue;
155
156
  }
156
157
  | {
@@ -184,8 +185,10 @@ differ from both its `rootSessionId` and `parentSessionId`.
184
185
  Run outcomes are exact: `no-action` means no FSM event was sent;
185
186
  `quiescent` means a non-failure parked/idle state; `failed` means the FSM is in
186
187
  a recoverable failure state; `terminal` means top-level final with optional
187
- 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
188
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)).
189
192
  Control-plane exceptions reject the runtime method rather than masquerade as a
190
193
  recoverable workflow `failed` result.
191
194
 
@@ -494,6 +497,15 @@ If a started-boundary sink records the event and then rejects, the runtime
494
497
  shall make one best-effort normalized error-finish attempt with the same call
495
498
  id and then reject the original start error. It shall not retry either event or
496
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)).
497
509
  When a call boundary carries `callId`, that id shall be unique within the
498
510
  runtime session. A stable FSM `stateId` is identity metadata in the payload,
499
511
  not a call id and shall not be reused as one across repeated invocations.
@@ -658,10 +670,14 @@ nonconformant.
658
670
  The FSM's `events` union enumerates every Boss-originated event.
659
671
  The runtime receives Boss input as a free-form string
660
672
  (`handleBossInput.text`).
661
- 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
662
675
  ordinary textual entry event and no Boss question is pending, the runtime
663
676
  shall send that event deterministically and attach the exact original text to
664
- 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.
665
681
  The default Captain — the controller playbook of
666
682
  [gears2fsm "Setup"](gears2fsm.md#setup) — is deterministic at every parked
667
683
  entry: the runtime maps each Boss turn from the exact text and the host's
@@ -937,10 +953,34 @@ The provided actor shall:
937
953
  `{ guard: <first declared guard>, exitStatus: 0 }`; any nonzero status
938
954
  resolves the second declared guard with that status. Guard selection is
939
955
  mechanical; the runtime shall not route script output through the judge.
940
- - Reject only when the command cannot be spawned at all, routing through the
941
- state's ordinary `onError` path.
942
- - Honor the active turn's abort signal by terminating the child process and
943
- 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.
944
984
  - Emit, after the child settles and before the invocation resolves, one status
945
985
  line `Executed script for <stateId> (exit <status>).` and one telemetry
946
986
  event under topic `playbook.script` with payload
@@ -965,14 +1005,18 @@ XState `.provide(...)` receives the exact declared actor input rather than a
965
1005
  structurally similar local type.
966
1006
  Construct one bridge per runtime and wire every integration hook: allocate ids
967
1007
  with `nextCallId`; return the currently active public-boundary signal from
968
- `getBoundarySignal`; bind `resumePlaybookCall.signal` before settling the
969
- deferred actor through `bindResumeSignal`; enqueue the exact start/finish trace
970
- through `emitStarted` / `emitFinished`; drain the global emission queue through
971
- `drain`; latch the original control error through `onControlPlaneError`; and
972
- retain any cleanup/observer failure through `onBackgroundError` for the next
973
- public boundary or disposal rejection. The runtime shall not leave these
974
- optional API hooks unwired merely because their TypeScript properties are
975
- 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.
976
1020
  On invocation the bridge allocates a runtime-local call id, traces the start,
977
1021
  and calls `PlaybookPorts.callPlaybook` with the composed target/text and the
978
1022
  bridge signal combined from the XState invocation lifetime, the active public
@@ -1057,12 +1101,15 @@ registry; linker-time metadata is not authorization to call a target.
1057
1101
 
1058
1102
  Disposal shall settle an outstanding call as aborted and drain its finish
1059
1103
  trace before `session.disposed`.
1060
- If registered child abort cleanup rejects, the bridge shall emit the paired
1061
- finish with an error result and reject `abortPending` or disposal with that
1062
- original cleanup error; it shall not swallow the failure merely because the
1063
- promise actor also observes a `NestedPlaybookCallError`. Parent disposal shall
1064
- still drain, emit its one `session.disposed` boundary, and clear the bound
1065
- 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.
1066
1113
  Child output and errors must be JSON-safe; a non-JSON-safe result is a
1067
1114
  control-plane error.
1068
1115
 
@@ -1140,6 +1187,12 @@ The `PlaybookRuntime` shall:
1140
1187
  transition-trace or telemetry sink failure is part of `init`: initialization
1141
1188
  shall reject, stop the actor, and perform the failed-start cleanup below
1142
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)).
1143
1196
  Where the FSM input declares `selfPlaybookId`, seed it from the immutable
1144
1197
  `session.playbookId`; do not expose a caller option or reuse a working leaf's
1145
1198
  `stateId` as the self-call identity.
@@ -1193,7 +1246,14 @@ The `PlaybookRuntime` shall:
1193
1246
  transition/status/telemetry queue before returning, just as
1194
1247
  `handleBossInput` does. A resume shall not allocate a new Boss-input
1195
1248
  `turnId`; retain the original call-start turn id for its matching finish and
1196
- 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
1197
1257
  exceptional path shall drain ordered emissions, select the first latched
1198
1258
  non-abort control error before considering abort, and clear its boundary
1199
1259
  latches in `finally`, so a failed resume cannot leak an emission error into a
@@ -1246,9 +1306,14 @@ transition first.
1246
1306
 
1247
1307
  If a `*.call.started` trace records and then its sink rejects, no host call may
1248
1308
  begin. The runtime shall still enqueue exactly one synthetic paired
1249
- `*.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
1250
1314
  id, turn id, actor visibility, state/source identity, and prompt or request
1251
- 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
1252
1317
  control-error, FSM settlement, and ordered-drain path as any other call-start
1253
1318
  failure; the synthetic finish must not replace the original sink error.
1254
1319
 
@@ -1421,11 +1486,18 @@ still describes the state. Two families exist, labeled from source state
1421
1486
  descriptions:
1422
1487
 
1423
1488
  - **Failure-state retry** — while the singular state id is the recoverable
1424
- failure state and the runtime holds a recorded last classified event (the
1425
- event a public Boss boundary sent that drove the run into `failed`, kept
1426
- with its recorded payload), and the live snapshot accepts that event, the
1427
- runtime shall advertise `retry:<EVENT_TYPE>` replaying exactly that
1428
- 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.
1429
1501
  - **Jump entries** — for each registered resumable state id whose
1430
1502
  explicit-state-jump event (`BOSS_INTERRUPT` with that `targetId`, optional
1431
1503
  textual fields omitted) the live snapshot accepts, guards included, the
@@ -1480,12 +1552,17 @@ trace and the return agree, and a receipt states what happened to the effect
1480
1552
  rather than what happened to its telemetry. The published receipt stands, is
1481
1553
  returned and replayed verbatim, and the delivery failure travels on the
1482
1554
  runtime's emission-failure channel to surface from the next public boundary
1483
- 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)).
1484
1559
 
1485
1560
  The recorded receipts and the recorded last classified event are
1486
- process-local: the durable runtime snapshot persists neither, and a
1487
- restored runtime advertises a retry again only after its next classified
1488
- 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.
1489
1566
 
1490
1567
  ## Abort
1491
1568
 
@@ -1499,7 +1576,61 @@ the shared `combineAbortSignals`). Classify a rejection as cancellation by its
1499
1576
  causal identity with the applicable signal reason, not by an `AbortError` name
1500
1577
  or by observing only that the signal is also aborted. Signals may carry an
1501
1578
  ordinary `Error`, while a distinct transport or sink failure that occurs after
1502
- 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
1503
1634
  runtime shall not merely race the imperative
1504
1635
  wait and return while an invocation remains live: it shall let the selected
1505
1636
  rejection path settle and drive the actor to a quiescent state before returning
@@ -1563,12 +1694,13 @@ The `playbook.trace` copies are the host-agnostic runtime-boundary record requir
1563
1694
  ## Output
1564
1695
 
1565
1696
  The link compiler emits one TypeScript module per playbook.
1566
- For an FSM that declares no `type: 'parallel'` state, it shall emit the thin
1567
- 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.
1568
1700
  For an FSM that declares a parallel state, it shall emit bespoke linked
1569
1701
  machinery satisfying this document's runtime contract and shall not invoke
1570
- `createXStatePlaybookRuntime`, whose supported domain is single-region FSMs
1571
- 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).
1572
1704
  The FSM-interpreter machinery — actor wiring, boundary tracing, Boss-event
1573
1705
  mapping, adjudication, script execution, nested-playbook bridging, session
1574
1706
  lifecycle, abort handling, and the optional parked-session snapshot
@@ -1613,7 +1745,10 @@ The thin emitted module:
1613
1745
  actor.
1614
1746
  - Supplies in `spec` only what the factory cannot read from the FSM
1615
1747
  artifact's own data: the deterministic textual entry event where
1616
- §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`
1617
1752
  metadata for each additional Boss-union arm whose exact required/optional
1618
1753
  judge fields, runtime-owned text fields, or closed string values disappear
1619
1754
  under TypeScript erasure; `placeholderFields` only for authored token/field
@@ -1697,7 +1832,14 @@ The thin emitted module:
1697
1832
  before this contract carry no `compat` member and shall reject before interpretation.
1698
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`.
1699
1834
  - Default-exports the factory call as `createPlaybookRuntime`, typed
1700
- `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.
1701
1843
  - Exposes, under an `_internal` export, the pure helpers verification
1702
1844
  needs — at least the prompt composers its own machine uses, which may
1703
1845
  re-export the shared defaults when the spec does not override composition —
@@ -1801,7 +1943,7 @@ New behavior in any of these areas requires a separate slc spec.
1801
1943
 
1802
1944
  ## References
1803
1945
 
1804
- [1]: [text2gears](text2gears.md) "First phase: text → GEARS spec items."
1805
- [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."
1806
1948
  [3]: https://stately.ai/docs/actors "XState actors — `createActor`, snapshots, abort signal handling."
1807
1949
  [4]: https://github.com/sindresorhus/p-queue#readme "p-queue concurrency and AbortSignal support."
package/src/runtime.d.ts CHANGED
@@ -97,6 +97,7 @@ export type PlaybookRunResult = {
97
97
  } | {
98
98
  outcome: 'terminal';
99
99
  state: PlaybookState;
100
+ stateDescription?: string;
100
101
  output?: JsonValue;
101
102
  } | {
102
103
  outcome: 'suspended';
package/src/runtime.ts CHANGED
@@ -132,6 +132,7 @@ export type PlaybookRunResult =
132
132
  | {
133
133
  outcome: 'terminal';
134
134
  state: PlaybookState;
135
+ stateDescription?: string;
135
136
  output?: JsonValue;
136
137
  }
137
138
  | {
@@ -168,6 +168,15 @@ export interface XStatePlaybookRuntimeSpec<TOptions> {
168
168
  entryEvent?: {
169
169
  type: string;
170
170
  textField: string;
171
+ /**
172
+ * DR-034: the FSM context member this machine's entry action copies the
173
+ * exact Boss text into. Where it is named, the failure-state retry
174
+ * builds its payload from that member of the live snapshot instead of
175
+ * from the process-local recorded event, so the action derives the same
176
+ * before and after `restore`. Absent: the recorded event stays the
177
+ * source and the action lives only as long as the process.
178
+ */
179
+ contextField?: string;
171
180
  };
172
181
  /**
173
182
  * Exact flat Boss-event contracts whose non-text fields the judge may
@@ -313,9 +322,10 @@ export declare function stateDescriptionsFromMachine(machine: AnyStateMachine):
313
322
  * (literal and dynamic) — and implements the full runtime lifecycle including
314
323
  * the optional parked-session snapshot capability (DR-014).
315
324
  *
316
- * Scope: machines that declare no parallel state (each snapshot exposes
317
- * exactly one playbook state id). Parallel-region FSMs keep their own linked
318
- * runtimes.
325
+ * Scope: flat single-region machines no parallel state, no compound
326
+ * child states, and every root state's `meta.playbook.stateId` equal to its
327
+ * state key — so each snapshot exposes exactly one playbook state id.
328
+ * Parallel-region FSMs keep their own linked runtimes.
319
329
  */
320
330
  export declare function createXStatePlaybookRuntime<TOptions>(machine: AnyStateMachine, spec: XStatePlaybookRuntimeSpec<TOptions>): PlaybookRuntimeFactory<TOptions>;
321
331
  export {};