@sublang/playbook 0.9.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +190 -151
  2. package/package.json +50 -6
  3. package/reference/sdlc/captain.md +102 -0
  4. package/reference/sdlc/captain.playbook/captain.fsm.d.ts +227 -0
  5. package/reference/sdlc/captain.playbook/captain.fsm.js +628 -0
  6. package/reference/sdlc/captain.playbook/captain.fsm.ts +851 -0
  7. package/reference/sdlc/captain.playbook/captain.gears.md +60 -0
  8. package/reference/sdlc/captain.playbook/captain.playbook.d.ts +23 -0
  9. package/reference/sdlc/captain.playbook/captain.playbook.js +1053 -0
  10. package/reference/sdlc/captain.playbook/captain.playbook.ts +1144 -0
  11. package/reference/sdlc/code.playbook/bin/playbook.js +158 -12
  12. package/reference/sdlc/code.playbook/bin/run.js +999 -0
  13. package/reference/sdlc/code.playbook/code.fsm.d.ts +11 -4
  14. package/reference/sdlc/code.playbook/code.fsm.introspect.d.ts +2 -2
  15. package/reference/sdlc/code.playbook/code.fsm.introspect.js +1 -1
  16. package/reference/sdlc/code.playbook/code.fsm.introspect.ts +6 -6
  17. package/reference/sdlc/code.playbook/code.fsm.js +334 -102
  18. package/reference/sdlc/code.playbook/code.fsm.ts +467 -180
  19. package/reference/sdlc/code.playbook/code.gears.md +11 -10
  20. package/reference/sdlc/code.playbook/code.playbook.d.ts +16 -19
  21. package/reference/sdlc/code.playbook/code.playbook.js +199 -488
  22. package/reference/sdlc/code.playbook/code.playbook.ts +327 -566
  23. package/reference/sdlc/code.playbook/code.registry.d.ts +0 -3
  24. package/reference/sdlc/code.playbook/code.registry.js +0 -3
  25. package/reference/sdlc/code.playbook/code.registry.ts +0 -6
  26. package/reference/sdlc/code.playbook/playbook-captain.d.ts +9 -4
  27. package/reference/sdlc/code.playbook/playbook-captain.js +889 -210
  28. package/reference/sdlc/code.playbook/playbook-captain.ts +1136 -257
  29. package/reference/sdlc/code.playbook/playbook.config.template.yaml +21 -0
  30. package/reference/sdlc/discuss.playbook/discuss.fsm.d.ts +396 -0
  31. package/reference/sdlc/discuss.playbook/discuss.fsm.js +2066 -0
  32. package/reference/sdlc/discuss.playbook/discuss.fsm.ts +2464 -0
  33. package/reference/sdlc/discuss.playbook/discuss.gears.md +251 -0
  34. package/reference/sdlc/discuss.playbook/discuss.playbook.d.ts +113 -0
  35. package/reference/sdlc/discuss.playbook/discuss.playbook.js +1514 -0
  36. package/reference/sdlc/discuss.playbook/discuss.playbook.ts +1926 -0
  37. package/reference/sdlc/discuss.playbook/discuss.registry.d.ts +58 -0
  38. package/reference/sdlc/discuss.playbook/discuss.registry.js +97 -0
  39. package/reference/sdlc/discuss.playbook/discuss.registry.ts +153 -0
  40. package/slc/gears2fsm.md +557 -57
  41. package/slc/link.md +1165 -89
  42. package/slc/optimize.md +92 -0
  43. package/slc/text2gears.md +255 -7
  44. package/src/runtime.d.ts +146 -3
  45. package/src/runtime.ts +201 -2
  46. package/src/xstate-playbook-runtime.d.ts +201 -0
  47. package/src/xstate-playbook-runtime.js +2058 -0
  48. package/src/xstate-playbook-runtime.ts +2792 -0
  49. package/src/xstate-runtime.d.ts +95 -0
  50. package/src/xstate-runtime.js +1258 -0
  51. package/src/xstate-runtime.ts +1816 -0
package/slc/gears2fsm.md CHANGED
@@ -13,29 +13,185 @@ Target is an object artifact only: it defines the machine, actor contracts, and
13
13
 
14
14
  ## Formats
15
15
 
16
- | Role | Format | Extension |
17
- | --- | --- | --- |
18
- | source | gears | .md |
19
- | target | fsm | .ts |
16
+ | Role | Format | Extension |
17
+ | ------ | ------ | --------- |
18
+ | source | gears | .md |
19
+ | target | fsm | .ts |
20
20
 
21
21
  ## Setup
22
22
 
23
23
  The artifact shall use XState v5's `setup(...)` then `.createMachine(...)` [[10]].
24
- The `types` block shall declare `context`, `events`, machine `input`, and a typed `Captain` actor contract [[11]].
25
- The artifact shall not import a runner or bake in a concrete Captain implementation; any actor placeholder shall fail explicitly (e.g., throw `'captain actor must be provided by the runner'`).
24
+ The artifact shall restrict itself to erasable TypeScript syntax type annotations that strip cleanly, no constructor parameter properties, `enum`s, or namespaces — so a host running under type stripping loads it directly.
25
+ It shall also pass the repository's strict `noUnusedLocals` and
26
+ `noUnusedParameters` checks. Helper signatures and XState callbacks shall omit
27
+ values they do not read; for example, a fresh-context helper that uses only
28
+ `bossIntent` shall not also accept an unused `context`, and an assign callback
29
+ that reads only `event` shall destructure only `event`.
30
+ The `types` block shall declare only `context`, `events`, machine `input`, and
31
+ machine `output`. XState v5's `SetupTypes` has no `actors` property; emitting
32
+ `types: { actors: ... }` is invalid and prevents registered action and actor
33
+ names from type-checking.
34
+ Declare a distinct typed actor contract in `setup(...)`'s top-level `actors`
35
+ map for every actor kind the GEARS artifact uses, using typed actor logic such
36
+ as `fromPromise<Output, Input>(...)` [[11]]:
37
+
38
+ - `captain` for direct work performed by Captain;
39
+ - `player` for work Captain delegates to a named player;
40
+ - `playbook` for a nested playbook call; and
41
+ - `script` for a deterministic shell script an
42
+ [optimizer-introduced script item](text2gears.md#script-behaviors-optimizer-introduced)
43
+ runs without any agent.
44
+
45
+ Do not declare, register, export, or import an actor kind the GEARS artifact
46
+ does not use. A playbook with direct Captain work and nested calls but no
47
+ delegated player therefore has `captain` and `playbook` contracts only.
48
+
49
+ XState may expose output from heterogeneous invoked actors as `unknown` in
50
+ shared guards and actions. Generated helpers shall accept an unknown event and
51
+ narrow its `output` or `error` structurally to the declared actor contract
52
+ before reading fields; they shall not rely on unchecked `event.output`
53
+ inference. Helpers that construct transition arrays shall preserve guard,
54
+ action, and target literals with `as const`, `satisfies`, or typed action/guard
55
+ functions rather than widening registered names to plain `string`.
56
+
57
+ The artifact shall not import a runner or bake in concrete actor
58
+ implementations. Each actor placeholder shall fail explicitly (for example,
59
+ throw `'captain actor must be provided by the runner'`).
60
+ Where the Source artifact begins with an SPDX comment block, the generated
61
+ artifact shall preserve its license and copyright text before the imports
62
+ using valid TypeScript line comments. It shall never copy Markdown HTML
63
+ comment delimiters into a TypeScript target.
26
64
 
27
65
  `CaptainInput` shall be a typed object with at least:
28
66
 
67
+ - `stateId`: the stable id of the invoking working leaf;
68
+ - `sourceItem`: the GEARS item ID this state realizes;
69
+ - `prompt`: the source item's full final prompt, verbatim;
70
+ - `result`: a record whose keys are the valid guard names this invocation may
71
+ return.
72
+
73
+ `PlayerInput` shall be a typed object with at least:
74
+
75
+ - `stateId`: the stable id of the invoking working leaf;
29
76
  - `player`: the [player](text2gears.md#players) Captain is to invoke;
30
77
  - `sourceItem`: the GEARS item ID this state realizes;
31
78
  - `prompt`: the source item's full final prompt, verbatim;
32
79
  - `result`: a record whose keys are the valid guard names this invocation may return.
33
80
 
34
- `CaptainOutput` shall be a discriminated object with `guard: string` and any extracted fields downstream states need.
81
+ `ScriptInput` shall be a typed object with at least:
82
+
83
+ - `stateId`: the stable id of the invoking working leaf;
84
+ - `sourceItem`: the GEARS item ID this state realizes;
85
+ - `command`: the script item's blockquote text, verbatim after Markdown
86
+ unescaping;
87
+ - `result`: a record whose keys are the item's two declared guard names, first
88
+ the zero-exit guard, then the nonzero-exit guard.
89
+
90
+ `ScriptOutput` shall be a discriminated union with one literal `guard` member
91
+ per declared result key and a required `exitStatus: number` property.
92
+ The script contract carries no prose output: downstream prompts shall not
93
+ depend on text a script produces.
94
+
95
+ `CaptainOutput` and `PlayerOutput` shall each be a discriminated union with one
96
+ literal `guard` member per authored result key and every payload field required
97
+ by that result as a required property. A catch-all `guard: string` interface
98
+ with optional look-alike fields is not a discriminated contract and is
99
+ malformed.
100
+ The artifact shall export the machine input plus every Captain, player, and
101
+ playbook actor input/output type that the linker must provide. The linked
102
+ module imports those exact types; it shall not redeclare near-duplicates that
103
+ can drift in optional fields, dynamic-call metadata, question ids, or child
104
+ result shapes.
105
+ Any recursive JSON value type in the artifact shall exactly preserve the
106
+ shared boundary's readonly variance:
107
+
108
+ ```typescript
109
+ type JsonValue =
110
+ | null
111
+ | boolean
112
+ | number
113
+ | string
114
+ | readonly JsonValue[]
115
+ | { readonly [key: string]: JsonValue };
116
+ ```
117
+
118
+ Nested-playbook output, completed-result evidence, plans, context, and machine
119
+ output shall use that readonly type rather than a mutable array/record
120
+ near-duplicate. The linker shall not cast or copy around a variance mismatch.
121
+
122
+ Every runtime-value placeholder established by Source in a direct-Captain or
123
+ delegated-player prompt shall be backed by a typed actor-input field populated
124
+ from typed machine context, so the linker can substitute it with the exact
125
+ runtime value. Angle-bracketed metavariables quoted inside domain instructions
126
+ (for example the literal `<model>` in a commit-message format) remain ordinary
127
+ prompt text and are not runtime-value placeholders. For the generic Captain
128
+ forms, wire `<boss-intent>` from `bossIntent`,
129
+ `<enabled-playbooks>` from `enabledPlaybooks`, `<remaining-plan>` from
130
+ `remainingPlan`, and `<completed-call-results>` from
131
+ `completedCallResults`. Other placeholders shall retain the semantic typed
132
+ field established by Source (for example `<#>` from `irNumber`). Leaving a
133
+ placeholder literal, replacing it with an empty default because its field was
134
+ omitted, or making the linker recover it from untyped context is malformed.
135
+ The sole blockquote placeholder of a dynamic nested-playbook item is instead
136
+ the child `textContext` field specified in §Nested playbook calls.
137
+
138
+ The generic Captain's `enabledPlaybooks` field shall be an immutable array of
139
+ exact entries `{ id: string, command: string, intent: string }`, not an array
140
+ of ids or an open record. Its dynamic-call guard checks `entry.id`, while the
141
+ linked runtime validates, snapshots, and deterministically renders all three
142
+ fields.
35
143
 
36
144
  Guard names shall be specified and interpreted **per state**, not as a global union.
37
145
  A global union encourages name reuse with divergent semantics and couples unrelated states.
38
146
  Shared helpers may accept `string`, but each state's `invoke.input.result` is the authoritative local contract.
147
+ For an acting GEARS item, the compiler shall derive that contract only from
148
+ the ordered bullets under the item's out-of-blockquote `Results:` label.
149
+ Every declared guard name shall match `[A-Za-z_$][A-Za-z0-9_$]*`.
150
+ It shall preserve every guard name, order, and description verbatim, reject a
151
+ missing, duplicate, blank, or malformed declaration, and shall not infer a
152
+ result contract from acting-prompt prose or transition implementation.
153
+
154
+ An acting item that declares no `Results:` label has exactly one outcome
155
+ (text2gears emits result contracts only for behaviors with more than one
156
+ outcome, or whose output a later item consumes).
157
+ The compiler shall give such a state the **default single-outcome contract**:
158
+ one result `done` with the fixed description
159
+ `The acting agent completed the behavior.`, plus the universal
160
+ `needsBossReply` below.
161
+ A single-outcome item may instead carry exactly one authored `Results:`
162
+ bullet when a later prompt consumes its output; the compiler derives the
163
+ one-guard contract from that bullet as usual.
164
+ The `done` transition is self-driving per §Transitions: it targets the next
165
+ workflow obligation, or a `final` state when the item is the last one.
166
+ The default never applies to an item carrying a `Results:` label, and it does
167
+ not license inferring any richer contract from prose.
168
+ The item's blockquote alone becomes `invoke.input.prompt`; the `Results:`
169
+ label and bullets shall never enter that prompt.
170
+ Each result description shall name every additional output field its accepting
171
+ guard requires, using the exact case-sensitive property names. For example, a
172
+ delegation or continuing-call description whose guard reads the planned child
173
+ call shall say that output includes `remainingPlan`, `nextPlaybookId`, and
174
+ `nextPlaybookInput`; a direct or final response shall name `response`; and an
175
+ authored question shall name `question`. A vague description such as
176
+ "selected the next call" is malformed when its guard also requires structured
177
+ fields. Deterministic verification synthesizes valid actor output from this
178
+ local result contract and shall not infer hidden guard payloads from guard
179
+ source text.
180
+
181
+ For the default generic Captain decide-call-observe pattern, the local guard
182
+ discriminants are a stable compiler contract, not names the compiler may
183
+ invent:
184
+
185
+ - initial routing uses `question` with required `question` and `delegation`
186
+ with required `remainingPlan`, `nextPlaybookId`, and
187
+ `nextPlaybookInput`; it has no direct or terminal result;
188
+ - post-child reassessment uses `final` with required `response`,
189
+ `followUpQuestion` with required `question`, and `continuing` with required
190
+ `remainingPlan`, `nextPlaybookId`, and `nextPlaybookInput`.
191
+
192
+ Both direct-Captain states additionally receive the universal
193
+ `needsBossReply` result. Their guards and actions shall use those exact
194
+ case-sensitive names so the compiled adjudication contract remains stable.
39
195
 
40
196
  ## States
41
197
 
@@ -44,20 +200,41 @@ Each state shall declare:
44
200
  - a stable `id` (for `#id` targeting and Boss interrupts);
45
201
  - an intuitive state key (the property name under `states: { ... }`);
46
202
  - a one-line `description` (for inspector tools and documentation);
47
- - if it invokes Captain: `invoke.input` carrying `player`, `sourceItem`, `prompt`, `result` (per [Setup](#setup)).
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;
206
+ - if it invokes the direct `captain` actor: `invoke.input` carrying
207
+ `sourceItem`, `prompt`, and `result` (per [Setup](#setup));
208
+ - if it invokes the delegated `player` actor: `invoke.input` additionally
209
+ carrying `player`;
210
+ - if it invokes the `script` actor: `invoke.input` carrying `stateId`,
211
+ `sourceItem`, `command`, and `result` (per [Setup](#setup)) — no `prompt`
212
+ and no `player`.
48
213
 
49
214
  The source item ID shall live in `invoke.input.sourceItem`, not in a comment — this keeps the GEARS-to-state mapping machine-readable.
50
- A state's `invoke.input.player` shall match its source item's player.
215
+ A delegated state's `invoke.input.player` shall match its source item's named
216
+ player. A direct Captain state shall not invent a `Captain` player binding.
51
217
 
52
- Captain returns a discriminated result with `guard` set to one of `input.result`'s keys [[4]].
218
+ Every invoking working leaf sequential or parallel, whatever its actor
219
+ kind — shall carry the tag `playbook.busy`: the shared quiescence helper
220
+ derives busyness strictly from active-state tags, so an untagged working leaf
221
+ reads as quiescent while its call is still in flight.
222
+
223
+ The machine's initial state shall be a quiescent idle hub (no `invoke`) — typically `ready` — that accepts the Boss entry events and carries the `playbook.parked` tag because it can return control to Boss.
224
+ Captain- and player-invoking work begins only on a Boss-originated event, so
225
+ constructing and starting the machine performs no agent call.
226
+
227
+ Each direct Captain or delegated player actor returns a discriminated result
228
+ with `guard` set to one of `input.result`'s keys [[4]].
53
229
  Guards [[5]] on `onDone` transitions inspect `event.output.guard` to route.
54
230
 
55
- Example:
231
+ Delegated-player example:
56
232
 
57
233
  ```typescript
58
234
  invoke: {
59
- src: 'captain',
60
- input: ({ context }): CaptainInput => ({
235
+ src: 'player',
236
+ input: ({ context }): PlayerInput => ({
237
+ stateId: '<stable-state-id>',
61
238
  player: 'Reviewer',
62
239
  sourceItem: '<ITEM-A>',
63
240
  prompt: [
@@ -74,25 +251,268 @@ invoke: {
74
251
  }
75
252
  ```
76
253
 
254
+ For an item in which Captain acts directly, the corresponding invocation uses
255
+ `src: 'captain'` and a `CaptainInput` with the same static mapping fields but no
256
+ `player` field.
257
+
77
258
  ## Mapping
78
259
 
79
260
  Each Source spec item shall map to exactly one state in Target.
80
261
  A state's `invoke.input.sourceItem` shall be that item's ID, and `invoke.input.prompt` shall carry the item's prompt verbatim.
81
262
 
263
+ An item written as direct Captain work shall map to exactly one `captain`
264
+ invocation. An item that prompts or relays to a named player shall map to
265
+ exactly one `player` invocation. A nested-call item shall map to exactly one
266
+ `playbook` invocation. A script item (`Captain shall run:`) shall map to
267
+ exactly one `script` invocation whose `input.command` carries the blockquote
268
+ verbatim and whose `result` preserves the item's two guards in declared order.
269
+ The compiler shall not infer one actor kind from a
270
+ runtime player name or encode Captain as a player.
271
+ A script state is not agent-invoking: the compiler shall not add
272
+ `needsBossReply` to a script state's result map and shall not register it with
273
+ `resumableStates(ids)`.
274
+ A script state's success guard shall target the next workflow step and its
275
+ failure guard shall route to `failed` unless the source items define a
276
+ different recovery.
277
+
82
278
  Per text2gears [composition](text2gears.md#composition), each spec item already carries the full final prompt for one state behavior, with no duplicate lines.
83
279
  The FSM compiler shall not concatenate prompts across items, re-compose them, or silently dedupe.
84
280
  A spec item that still contains duplicate prompt lines is malformed; the compiler shall reject or flag it rather than silently propagate the duplication into `invoke.input.prompt`.
85
281
 
282
+ ## Parallel groups
283
+
284
+ Items carrying the same `Parallel group: <id>` metadata shall compile into one
285
+ compound state with `type: 'parallel'` and one region per item [[12]].
286
+ Each member shall be a delegated-player item; a direct-Captain or nested-call
287
+ member is malformed because those actor kinds share one Captain control lane or one
288
+ pending-child slot. Each region shall contain a delegated-player working leaf
289
+ and a local final state; the working leaf retains the item's stable state id,
290
+ `sourceItem`, player, prompt, and result contract.
291
+ The parallel parent shall use `onDone` as the join, which XState takes only
292
+ after every region reaches final.
293
+
294
+ Each branch shall assign only its own staged result.
295
+ The join shall promote all staged results atomically before later work begins,
296
+ so branch completion order cannot change downstream inputs.
297
+ Transitions between sibling regions are forbidden.
298
+
299
+ Working leaves shall carry tag `playbook.busy`.
300
+ A branch that supports Boss-reply suspension shall use a local waiting leaf
301
+ tagged `playbook.parked` rather than exit the parallel parent; `BOSS_REPLY`
302
+ shall identify and reenter only the waiting branch.
303
+ If several branch questions are pending, the event shall carry a stable
304
+ question id and the classifier shall not guess among them.
305
+ A fresh entry event or root interrupt may exit the complete parallel parent and
306
+ shall clear its staged results and branch questions.
307
+ Treat a fixed parallel parent as one jumpable unit. Generate a stable id and
308
+ root `BOSS_INTERRUPT` target for the parallel parent, not for any working leaf
309
+ inside its regions. Branch working ids remain valid internal resume targets for
310
+ their branch-local `BOSS_REPLY`; they shall not appear in the interrupt target
311
+ union or classifier catalog. This prevents a nominal one-branch jump from
312
+ implicitly entering or restarting the parallel parent's other regions.
313
+ An invoke error shall exit to the root failure state, allowing XState to stop
314
+ the sibling invocations automatically.
315
+
316
+ ## Nested playbook calls
317
+
318
+ An item whose behavior is a literal or dynamic
319
+ `Captain shall call playbook ...:` shall compile to a state that invokes a
320
+ typed `playbook` actor, not the `captain` or `player` actor.
321
+ The setup types shall declare `PlaybookInput` with stable `stateId`, target
322
+ `playbookId`, composed `text`, and optional `sourceItem`. The playbook actor's
323
+ successful output is the child's JSON-safe machine output itself (or
324
+ `undefined`), not a second wrapper carrying a synthetic status or `output`
325
+ field. `invoke.onDone` shall therefore record `event.output` as the successful
326
+ child output. Aborted and error call results reject the actor and reach
327
+ `invoke.onError`.
328
+ The artifact shall supply a failing placeholder for `playbook`, just as it does
329
+ for `captain` and `player`; the linked runtime provides the actor
330
+ implementation.
331
+
332
+ A literal call shall retain the existing representation: `playbookId` is the
333
+ literal target and `text` is the composed GEARS blockquote.
334
+
335
+ A dynamic call written
336
+ ``Captain shall call playbook selected by `<target-field>`:`` shall declare the
337
+ named target field and the blockquote's text field as typed string fields in
338
+ FSM context. The dynamic `PlaybookInput` variant shall require string-valued
339
+ `playbookIdContext` and `textContext` metadata fields. Its `invoke.input` shall
340
+ read the runtime values from those exact context fields and shall also carry
341
+ the following static metadata:
342
+
343
+ ```typescript
344
+ {
345
+ stateId: '<stable-state-id>',
346
+ sourceItem: '<ITEM-A>',
347
+ playbookId: context.nextPlaybookId,
348
+ text: context.nextPlaybookInput,
349
+ playbookIdContext: 'nextPlaybookId',
350
+ textContext: 'nextPlaybookInput',
351
+ }
352
+ ```
353
+
354
+ `playbookIdContext` and `textContext` name context fields; they never contain
355
+ runtime target or text values. The compiler shall emit them as explicit string
356
+ literals so conformance tools can verify context wiring without evaluating or
357
+ parsing the `invoke.input` function's source. The evaluated `playbookId` and
358
+ `text` shall each be strings and shall come from the context field named by its
359
+ corresponding metadata property. Literal calls need not carry these dynamic
360
+ metadata properties and retain their existing behavior.
361
+
362
+ The call state shall carry tag `playbook.suspended` and shall route
363
+ `invoke.onDone` from child output and `invoke.onError` from child failure.
364
+ The child call shall remain state-scoped: leaving the call state stops the
365
+ invoked actor and aborts the host call through XState's invocation signal
366
+ [[2]].
367
+ The FSM shall not allocate runtime call ids, construct child sessions, retain
368
+ runtime promises, or route Boss text to the child.
369
+
370
+ When Source explicitly continues one downstream behavior after a child
371
+ success, abort, or failure, both `invoke.onDone` and `invoke.onError` shall
372
+ record the corresponding JSON-safe child result and target that downstream
373
+ behavior. The generic `failed` state is the default only when Source declares
374
+ no recovery or reassessment path for a rejected child.
375
+ That recovering `onError` shall be an ordered transition array. Its first arm
376
+ shall use a typed structural guard that accepts only an `Error` carrying a
377
+ validated public child `result` with `status: 'aborted' | 'error'`; only that
378
+ arm appends sanitized child evidence and continues. A fallback arm shall retain
379
+ the control error normalized as JSON-safe `{ name, message, stack? }` in
380
+ `lastError` and route to `failed` without appending a completed child result;
381
+ the linked runtime alone retains the original error in its out-of-machine
382
+ latch. Non-abort port rejection, malformed port data, JSON, identity, bridge,
383
+ and other control-plane errors are not authored child outcomes even though
384
+ XState delivers both kinds through `invoke.onError`.
385
+ Where the rejected error structurally carries the runtime's normalized child
386
+ result, the error action shall inspect whether its status was `aborted` or
387
+ `error`; it shall not collapse both into an invented success/failure enum. The
388
+ FSM may inspect that public structural data without importing the runner or
389
+ constructing runtime call identities.
390
+ For a workflow that reassesses child results, use a typed JSON-safe record such
391
+ as `{ playbookId, status: 'ok', output }` on `onDone` and
392
+ `{ playbookId, status: 'aborted' | 'error', error }` on `onError`. Because the
393
+ runtime rejection is an `Error` with a public `result` property, normalization
394
+ shall inspect `result.status` and `result.error` before applying a generic
395
+ `Error` normalizer. It shall persist only the current context target id, the
396
+ status, and a compact `{ name, message }` error; it shall never persist the
397
+ whole runtime result, child session id, child state, call identity, or stack.
398
+ An abort without an error gets a compact generic abort description. The current
399
+ target id remains available in typed context until the sanitized record has
400
+ been created. On success, persist only `event.output`, which is the actual child
401
+ machine output returned by the bridge, not a runtime call-result envelope.
402
+ When that optional output is absent, omit the `output` property from the
403
+ completed-result record rather than storing `undefined`.
404
+ The outer trusted error is an actual `Error` instance and therefore is not a
405
+ plain JSON object. The structural guard shall inspect its public `.result`
406
+ property directly, then validate only that nested result before sanitizing it;
407
+ it shall not require the outer error itself to pass a plain-object/JSON guard.
408
+ Validation of that nested public result includes its status-specific required
409
+ members and target identity: `playbookId` shall equal the current selected
410
+ target, an `error` result shall carry a normalized error, and every optional
411
+ member that is present shall have the public contract's declared shape. A
412
+ look-alike such as `{ status: 'error' }` is malformed control data, not an
413
+ authored child failure, and shall take the fallback `failed` arm without
414
+ appending evidence. The guard shall not fabricate missing identity or error
415
+ members merely because the status string happens to be recognized.
416
+ The public result's declared optional `childSessionId` and `state` members are
417
+ valid when their shapes satisfy the shared contract; validate and then discard
418
+ them when building compact Captain evidence. They are not undeclared extras.
419
+ Likewise, the public normalized error may carry its declared optional string
420
+ `stack`; validate it and omit it from the compact `{ name, message }` evidence
421
+ rather than rejecting an otherwise valid authored child result.
422
+ Apply the public union exactly: an `aborted` or `error` result shall reject an
423
+ `output` member; `childSessionId`, when present, shall be non-empty; `error`
424
+ shall contain only non-empty `name`, string `message`, and optional string
425
+ `stack`; and `state`, when present, shall validate every declared
426
+ `PlaybookState` member and reject unknown or missing members. Treating an
427
+ arbitrary JSON-safe object as a valid `state`, or checking only that these
428
+ members have broad string/object types, is not complete public-result
429
+ validation.
430
+ In other words, the guard validates the complete public result it received,
431
+ while the action retains only the current selected playbook id, status, and
432
+ compact error. Do not implement evidence minimization by accepting only the
433
+ three keys that survive that projection.
434
+
435
+ Before entering a dynamic call, the machine shall reject an empty target and
436
+ empty input text, any target equal to `selfPlaybookId`, and any target that
437
+ Source requires to belong to an input catalog but that catalog does not
438
+ contain. Rejection shall occur before invoking the `playbook` actor; the host
439
+ remains responsible for its independent registry validation.
440
+ Where Source forbids repeating an equivalent completed or failed call without
441
+ new information, the machine shall also keep a private deterministic history
442
+ of target-and-input signatures and reject a continuation whose target and
443
+ complete input exactly match a prior call. Encode each signature as the
444
+ collision-free `JSON.stringify([playbookId, text])` tuple of exact JavaScript
445
+ strings, not delimiter concatenation, and append it before invocation so
446
+ success, abort, and authored failure all count. That history shall not be
447
+ included in a Captain or player prompt; a revised input containing new
448
+ information is a different call. The exact machine check is a safety floor;
449
+ the acting Captain remains responsible for Source's broader semantic
450
+ equivalence policy.
451
+ That validation belongs on the guarded transition into the call state. The
452
+ call state's `invoke.input` mapper shall be a pure read of the already-validated
453
+ typed context fields; it shall not call an assertion helper or throw while
454
+ XState resolves actor input. This keeps state restoration, inspection, and
455
+ scripted coverage from crashing outside the invocation's `onError` boundary.
456
+ For the default Captain decide-call-observe loop, the delegation and
457
+ continuing `onDone` arms shall transition directly into the invoking call
458
+ state. Each arm's single guard validates its applicable actor-output and
459
+ context constraints: both validate JSON shape, catalog membership,
460
+ self-target, and duplicate history, while strict plan shrink applies only to
461
+ `continuing`. Its actions store the selected target/input and append the
462
+ signature before state entry. Do not interpose an eventless preparation or
463
+ validation state between the Captain actor and the call state: it obscures the
464
+ authored Captain entry edge from deterministic coverage and adds no XState
465
+ safety beyond the guarded direct transition.
466
+
86
467
  ## Context and prompts
87
468
 
88
469
  Context fields used to drive guards or compose prompts shall be **typed and named**.
89
470
  The compiler shall not branch on untyped properties of `lastResult`; persistent routing decisions belong in typed context fields. (`lastResult` is for inspection only.)
471
+ Where Source declares a finite ordered plan, represent it as a typed readonly
472
+ JSON-safe array and validate that shape on the actor-output transition; an
473
+ unconstrained `JsonValue` does not establish that a plan is ordered or finite.
474
+ Where a decide-call-observe loop carries the calls after the selected next call
475
+ as `remainingPlan`, its continuing-call guard shall additionally require the
476
+ new plan to be strictly shorter than the current plan. The Captain may revise
477
+ or remove remaining entries as evidence arrives, but it cannot grow or retain
478
+ the same-length plan indefinitely; the initial finite array therefore bounds
479
+ the number of sequential child calls without an arbitrary runtime call limit.
90
480
 
91
481
  Prompts shall pass only the **specific extracted fields** the player needs.
92
482
  The compiler shall not dump `JSON.stringify(lastResult)` or any opaque blob: it leaks internal `guard` strings, wastes tokens, and confuses the LLM.
93
483
 
94
484
  Player bindings and per-run parameters shall flow in via the machine's `input` and be copied into context at start-up.
95
485
  The artifact shall not bake in player bindings, model names, or per-run values.
486
+ Host-owned configuration such as an enabled-playbook catalog shall remain
487
+ immutable machine input/context for the session. Boss events and actor outputs
488
+ shall not carry, replace, append to, or otherwise overwrite that catalog.
489
+ Every machine with a dynamic call shall receive its own registered or authored
490
+ playbook id as immutable machine input/context named `selfPlaybookId`, and its
491
+ dynamic-call guard shall reject that target. The leaf-level `stateId` name is
492
+ reserved for actor invocation identity and shall not be reused for a playbook
493
+ id.
494
+ JSON-safe context and output records shall omit absent optional members instead
495
+ of creating own properties whose value is `undefined`.
496
+ JSON validation shall accept only null, booleans, finite numbers, strings,
497
+ arrays, and plain own enumerable data-property objects. It shall reject cycles,
498
+ non-plain instances (`Error`, `Date`, `Map`, and class instances), accessors,
499
+ symbol keys, sparse/undefined values, `NaN`, and infinities rather than silently
500
+ changing them during serialization.
501
+ An accepted array shall have prototype exactly `Array.prototype`, no holes,
502
+ symbols, accessors, or extra own string properties, and enumerable own data
503
+ descriptors for every canonical index; its standard non-enumerable `length`
504
+ descriptor is the sole exception. That data descriptor shall be
505
+ non-configurable and carry the exact array length, but its `writable` flag may
506
+ be either `true` on an ordinary array or `false` after the shared runtime
507
+ recursively freezes a validated boundary value. `Reflect.ownKeys(array)` shall
508
+ contain exactly `length + 1` keys: the `length` property and every canonical
509
+ index from `0` through `length - 1`. A digit string whose numeric value is not
510
+ less than `length` is an extra property, not an array index. An accepted record
511
+ shall have prototype exactly `Object.prototype` or `null`, and every key returned by
512
+ `Reflect.ownKeys` shall be a string whose own descriptor is enumerable and a
513
+ data descriptor. Cycle detection shall track only the active recursion path
514
+ and remove a container on unwind, so a shared acyclic array or record is valid
515
+ while an actual back-edge is rejected.
96
516
 
97
517
  ## Transitions
98
518
 
@@ -123,87 +543,167 @@ Phases may set typed routing fields so terminal outcomes return to the originati
123
543
 
124
544
  ### Boss interrupts
125
545
 
126
- Boss may interrupt any active state at any time. Every jumpable state shall have a stable `id` [[9]].
546
+ Boss may interrupt any active state that can itself receive a Boss turn. Every
547
+ jumpable state shall have a stable `id` [[9]]. A final state is not jumpable.
548
+ A `playbook.suspended` call state with an outstanding child is also not a Boss
549
+ interrupt target: the host routes Boss input to the active child leaf and
550
+ resumes the parent only from the matching child result.
127
551
  The runtime sends `{ type: 'BOSS_INTERRUPT', targetId: '<id>' }`; the root machine handles it with one guarded transition per jumpable state targeting `#<id>` with `reenter: true` [[7]][[8]][[9]], so invoked actors restart cleanly.
128
552
  The compiler shall emit a `bossInterrupts(ids)` helper rather than hand-writing one transition per state.
553
+ Each generated arm shall guard both the selected `targetId` and every typed
554
+ context precondition required to enter that target safely. It shall not jump
555
+ into a working or reassessment state with missing intent, prior result, plan,
556
+ or other required context and shall not invent defaults merely to make an
557
+ interrupt target executable.
129
558
  XState automatically stops the current state's invoked actor on transition [[2]].
559
+ Where the default Captain's routing state accepts a fresh intent while another
560
+ state or Boss-reply wait is active, its `BOSS_INTERRUPT` event shall carry a
561
+ required non-empty `bossIntent`. The guarded routing arm shall copy that value,
562
+ clear the prior plan, child evidence, exact-call history, selected call,
563
+ response, error, and consumed question/reply context, then reenter routing.
564
+ It shall not restart the old intent or retain a stale pending question.
565
+ For this default Captain, `routing` is the sole `BOSS_INTERRUPT` target; a
566
+ fresh directive always returns to routing and shall not jump directly into
567
+ reassessment or the Boss-reply wait. The typed event union and classifier
568
+ contract shall require exactly `targetId: 'routing'` plus the fresh
569
+ `bossIntent`.
130
570
 
131
571
  ### Boss entry events vs. BOSS_INTERRUPT
132
572
 
133
573
  `BOSS_INTERRUPT` jumps into an **active** machine, pre-empting whichever state is running.
134
574
  **Boss entry events** start or resume from idle or recoverable states when Boss-supplied parameters can't be inferred from machine state alone.
135
575
  Entry events shall be typed alongside `BOSS_INTERRUPT` and populate context via a dedicated action.
136
- The two surfaces shall not be collapsed: `BOSS_INTERRUPT` cannot carry payload, and a parameterless entry event may collapse to interrupt-style routing only when state-jump semantics are identical.
576
+ An entry event's copy action shall not clear per-run parameters the event omits: an absent optional field falls back to the existing (input-seeded) context value.
577
+ The two surfaces shall not be collapsed. `BOSS_INTERRUPT` always carries its
578
+ target id and may additionally carry typed Boss-supplied fields such as an
579
+ intent or IR number only where Source requires the pre-empted target to consume
580
+ them; a parameterless entry event may collapse to interrupt-style routing only
581
+ when state-jump and context-update semantics are identical.
137
582
  Entry events shall not be root-level transitions from every active state unless the workflow supports pre-emption; they belong on idle and recoverable states (e.g., `failed`).
138
583
 
139
584
  ### Boss-reply suspension
140
585
 
141
- When a captain-invoking state needs a Boss decision the player cannot supply alone, the machine shall suspend in a dedicated quiescent state and resume the same state with the Q+A in the next prompt.
586
+ When a captain- or player-invoking state needs a Boss decision the acting agent
587
+ cannot supply alone, the machine shall suspend that task in a quiescent wait
588
+ state and resume the same task with the Q+A in the next prompt.
142
589
  This is a third Boss surface alongside `BOSS_INTERRUPT` and Boss entry events.
143
590
 
144
- Every captain-invoking state supports this path.
591
+ Every captain- and player-invoking state supports this path.
145
592
  There is no source-level opt-in annotation and no `needsBossReply` result metadata in GEARS output.
146
593
  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`.
147
594
 
148
- For every captain-invoking state, the compiler shall add `needsBossReply` to the state's `invoke.input.result` map.
595
+ For every captain- and player-invoking state, the compiler shall add
596
+ `needsBossReply` to the state's `invoke.input.result` map.
149
597
  The description shall be the standard adjudicator-facing text:
150
598
 
151
599
  ```text
152
- The player's prose surfaces a clarifying question for Boss that the player cannot answer alone. Output shall include `question: <verbatim question text from the player's prose>`.
600
+ The acting agent's prose surfaces a clarifying question for Boss that the agent cannot answer alone. Output shall include `question: <verbatim question text from the acting agent's prose>`.
153
601
  ```
154
602
 
155
- It shall include the load-bearing substring ``Output shall include `question:`` so the runtime's adjudicator requires `question` in the JSON reply.
603
+ The standard annotated backtick form names the exact `question` property; the
604
+ linker's required-field extractor shall interpret only the identifier before
605
+ the colon as the JSON field name.
156
606
  The linked runtime composes player prompts per [link.md "Player prompt composition"](link.md#player-prompt-composition), without adding a player-visible Boss-question instruction.
157
607
 
158
- The machine shall declare:
159
-
160
- - An `awaitBossReply` state with stable `id: 'awaitBossReply'` and `description: 'Waiting for Boss to answer a player question.'`.
161
- - A `BOSS_REPLY` event carrying `{ answer: string }`.
162
- - Context fields `pendingBossQuestion?: { resumeStateId, sourceItem, player, question }` and `bossReply?: string`.
163
- Field provenance is normative: `resumeStateId`, `sourceItem`, and `player` shall come from the suspended state's invocation metadata; only `question` shall come from adjudicated player output.
164
-
165
- The compiler shall emit three helpers:
166
-
167
- - `resumableStates(ids)` emits one `BOSS_REPLY` arm per registered state on `awaitBossReply.on.BOSS_REPLY`, each guarded on `context.pendingBossQuestion?.resumeStateId === '<id>'` and targeting `'#<id>'` with `reenter: true`.
168
- The compiler shall register every captain-invoking state id with this helper.
169
- The helper is analogous to `bossInterrupts(ids)`.
170
- - `setPendingBossQuestion` `assign({ pendingBossQuestion: <new>, bossReply: undefined })`.
171
- Used on every `needsBossReply` arm; clearing `bossReply` here prevents a follow-up question from inheriting the prior answer.
172
- - `clearBossReplyContext` `assign({ pendingBossQuestion: undefined, bossReply: undefined })`.
173
- Used on every transition out of `awaitBossReply` other than the resume arm, and on every non-`needsBossReply` outcome of a captain-invoking state.
174
-
175
- `awaitBossReply` is a quiescent state for the runtime's drive loop.
176
- It shall declare the standard `bossInterrupts(ids)` handler with `actions: clearBossReplyContext`, so a Boss interrupt event abandons a pending question.
177
- The machine's root-level Boss entry events shall be re-declared on `awaitBossReply` with `actions: clearBossReplyContext`, so a fresh Boss directive while waiting starts a fresh turn and clears stale context.
178
-
179
- A captain-invoking state's `invoke.input` function shall carry `pendingBossQuestion` and `bossReply` fields when present so the linked runtime can compose the continuation prompt.
608
+ The question record shall be
609
+ `{ questionId, resumeStateId, sourceItem, player, question }`.
610
+ `questionId` and `resumeStateId` shall both equal the stable working-leaf
611
+ `stateId`.
612
+ `questionId`, `resumeStateId`, and `sourceItem` shall come from the suspended
613
+ working leaf's stable invocation metadata. `player` shall come from a delegated
614
+ `PlayerInput`, or be the literal `Captain` for a direct-Captain state. Only
615
+ `question` shall come from adjudicated actor output.
616
+
617
+ A machine with at most one active Captain or player task may use the scalar
618
+ form:
619
+
620
+ - An `awaitBossReply` state with stable `id: 'awaitBossReply'`, tag
621
+ `playbook.parked`, and description
622
+ `Waiting for Boss to answer the acting agent's question.`.
623
+ - A `BOSS_REPLY` event carrying `{ answer: string; questionId?: string }`.
624
+ - Context fields `pendingBossQuestion?: PendingBossQuestion` and
625
+ `bossReply?: string`.
626
+ - `resumableStates(ids)`, `setPendingBossQuestion`, and
627
+ `clearBossReplyContext` helpers with the existing single-question behavior.
628
+
629
+ A machine with parallel delegated-player tasks shall use the keyed form:
630
+
631
+ - One local waiting leaf per branch, tagged `playbook.parked`.
632
+ - A `BOSS_REPLY` event carrying `{ questionId: string; answer: string }`.
633
+ - Context fields
634
+ `pendingBossQuestions: Partial<Record<ResumableStateId, PendingBossQuestion>>`
635
+ and `bossReplies: Partial<Record<ResumableStateId, string>>`.
636
+ - Helpers that set, answer, and clear only the named branch record; exiting the
637
+ complete parallel group for a fresh directive or interrupt clears every
638
+ record owned by that group.
639
+
640
+ Where exactly one question is pending, a linked runtime may accept a classifier
641
+ reply that omits `questionId` and fill that sole id.
642
+ Where several questions are pending, the classifier prompt and event shall
643
+ require `questionId` and shall reject an omitted or unknown id without moving
644
+ the FSM.
645
+
646
+ The scalar `awaitBossReply` state and every local branch wait are quiescent for
647
+ the runtime drive boundary.
648
+ They shall allow a fresh root entry event or interrupt to abandon the relevant
649
+ pending question data before starting new work.
650
+ The wait state or branch-wait leaf itself shall not be an interrupt target:
651
+ re-entering it after the interrupt clears its pending question would create an
652
+ unresumable parked state. Its recorded working leaf remains the sole
653
+ `BOSS_REPLY` resume destination.
654
+
655
+ A captain- or player-invoking state's `invoke.input` function shall carry the
656
+ pending question and reply selected for that working leaf as singular
657
+ `pendingBossQuestion` and `bossReply` fields, regardless of the scalar or keyed
658
+ context representation, so prompt composition has one stable contract.
180
659
  When both fields are present, the linked runtime shall compose the continuation preamble and labelled Q&A blocks per [link.md "Player prompt composition"](link.md#player-prompt-composition).
181
660
  The FSM artifact shall not bake the continuation preamble into the GEARS-derived `prompt` body.
182
661
 
183
662
  The following malformed states shall route to `failed` per [Errors and termination](#errors-and-termination):
184
663
 
185
- - Captain output has `guard: 'needsBossReply'` but no `question` field.
186
- - Captain output declares `needsBossReply` from a state not registered with `resumableStates(ids)`.
664
+ - Captain or player output has `guard: 'needsBossReply'` but no `question`
665
+ field.
666
+ - Captain or player output declares `needsBossReply` from a state without a
667
+ registered scalar or branch-local resume route.
187
668
  - `BOSS_REPLY` fired with empty or whitespace-only `answer`.
669
+ - A keyed `BOSS_REPLY` names no pending question.
188
670
 
189
671
  ## Errors and termination
190
672
 
191
- Every `invoke` shall declare an `onError` handler routing to a dedicated `failed` state, with the error captured in `context.lastError` for inspection.
192
- `failed` is not `final`: Boss may interrupt out of it to recover.
673
+ Every `invoke` shall declare an `onError` handler with a fallback routing to a
674
+ dedicated `failed` state and capturing the error in `context.lastError` for
675
+ inspection. A nested playbook invoke may place its validated authored-child
676
+ recovery arm before that fallback as described in §Nested playbook calls.
677
+ `failed` is not `final`: it shall carry tag `playbook.parked`, retain enough
678
+ typed context for Boss recovery, and accept the workflow's recovery entry or
679
+ interrupt surface. The parked tag distinguishes a recoverable failure from a
680
+ busy state so the host retains the session instead of treating the outcome as
681
+ an unhandled runtime error.
193
682
 
194
683
  Every machine shall declare at least one `type: 'final'` state (typically `done`) reachable on completion.
195
684
  A never-terminating machine is a defect: the runner has no completion signal.
196
685
 
686
+ Where Source declares a JSON-safe terminal result, the setup types shall
687
+ declare that output and the root machine shall derive it from typed context
688
+ through XState's machine `output` function. A final-state transition alone does
689
+ not satisfy a declared output contract.
690
+ Fields that Source requires in every terminal output shall be required in the
691
+ TypeScript output type. In particular, a declared `{ response }` result shall
692
+ compile as `{ response: string }`, not `{ response?: string }`; reaching the
693
+ final state without a non-empty response shall be guarded out before the
694
+ machine output is constructed.
695
+
197
696
  ## References
198
697
 
199
- [1]: https://stately.ai/docs/xstate "XState Official Documentation"
200
- [2]: https://stately.ai/docs/invoke "Invoke — invoking actors from states"
201
- [3]: https://stately.ai/docs/input "Input — passing data to invoked actors"
202
- [4]: https://stately.ai/docs/output "Output — receiving actor results via onDone"
203
- [5]: https://stately.ai/docs/guards "Guards — synchronous transition conditions"
204
- [6]: https://stately.ai/docs/context "Context — persistent state and assign"
205
- [7]: https://stately.ai/docs/transitions "Transitions — reenter, root-level routing"
206
- [8]: https://stately.ai/docs/parent-states "Parent states — root-level event handling"
207
- [9]: https://stately.ai/docs/finite-states "Finite states — state IDs"
208
- [10]: https://stately.ai/docs/setup "Setup — typed machine setup"
209
- [11]: https://stately.ai/docs/actors "Actors — typed actor contracts"
698
+ [1]: https://stately.ai/docs/xstate 'XState Official Documentation'
699
+ [2]: https://stately.ai/docs/invoke 'Invoke — invoking actors from states'
700
+ [3]: https://stately.ai/docs/input 'Input — passing data to invoked actors'
701
+ [4]: https://stately.ai/docs/output 'Output — receiving actor results via onDone'
702
+ [5]: https://stately.ai/docs/guards 'Guards — synchronous transition conditions'
703
+ [6]: https://stately.ai/docs/context 'Context — persistent state and assign'
704
+ [7]: https://stately.ai/docs/transitions 'Transitions — reenter, root-level routing'
705
+ [8]: https://stately.ai/docs/parent-states 'Parent states — root-level event handling'
706
+ [9]: https://stately.ai/docs/finite-states 'Finite states — state IDs'
707
+ [10]: https://stately.ai/docs/setup 'Setup — typed machine setup'
708
+ [11]: https://stately.ai/docs/actors 'Actors — typed actor contracts'
709
+ [12]: https://stately.ai/docs/parallel-states 'Parallel states — concurrent regions and onDone joins'