@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
package/README.md CHANGED
@@ -81,7 +81,7 @@ playbook run --session 4f2c0000-0000-4000-8000-000000009ab1 "continue"
81
81
 
82
82
  `playbook run` prints the one Boss-visible Captain reply to stdout and operational status to stderr; CODE and DECIDE can complete their nested REVIEW calls there too.
83
83
 
84
- See [Using the CLI](docs/cli.md) for flags and durable continuation, [Configuring agents](docs/configuration.md) for the shared lineup, [Embedding](docs/embedding.md) for custom hosts, and the [changelog](CHANGELOG.md) for releases.
84
+ See [Using the CLI](docs/cli.md) for flags and durable continuation, [Configuring agents](docs/configuration.md) for the shared lineup, [Embedding](docs/embedding.md) for custom hosts, and the [changelog](https://github.com/sublang-ai/playbook/blob/main/CHANGELOG.md) for releases.
85
85
 
86
86
  ## Create your own playbook
87
87
 
@@ -129,8 +129,8 @@ pnpm playbook # drive a Boss turn against the source tree
129
129
  ```
130
130
 
131
131
  Playbook is itself spec-driven: the compiler phases are specs in [`slc/`](slc), and the reference playbooks are regenerated from their prose sources.
132
- Edit a source, regenerate its GEARS, FSM, and runtime artifacts, sync the tests and downstream specs until `pnpm test` passes, and commit with co-author trailers per [`specs/packages/git.md`](specs/packages/git.md).
133
- The gears↔FSM contract ([the playbook package](specs/packages/playbook.md)) and runtime contract ([the playbook-runtime package](specs/packages/playbook-runtime.md)) are pinned in [`specs/packages/`](specs/packages) and verified by the test suite.
132
+ Edit a source, regenerate its GEARS, FSM, and runtime artifacts, sync the tests and downstream specs until `pnpm test` passes, and commit with co-author trailers per [`specs/packages/git.md`](https://github.com/sublang-ai/playbook/blob/main/specs/packages/git.md).
133
+ The gears↔FSM contract ([the playbook package](https://github.com/sublang-ai/playbook/blob/main/specs/packages/playbook.md)) and runtime contract ([the playbook-runtime package](https://github.com/sublang-ai/playbook/blob/main/specs/packages/playbook-runtime.md)) are pinned in [`specs/packages/`](https://github.com/sublang-ai/playbook/tree/main/specs/packages) and verified by the test suite.
134
134
 
135
135
  ## License
136
136
 
package/docs/cli.md CHANGED
@@ -18,7 +18,7 @@ Each adapter is backed by a vendor runtime that installing
18
18
  agent stack you did not ask for. Which versions each adapter supports
19
19
  is [cligent](https://github.com/sublang-ai/cligent)'s knowledge and
20
20
  ships with it
21
- ([DR-027](../specs/decisions/027-runtime-compatibility-from-cligent.md));
21
+ ([DR-027](https://github.com/sublang-ai/playbook/blob/main/specs/decisions/027-runtime-compatibility-from-cligent.md));
22
22
  the commands below install the latest, which cligent accepts from its
23
23
  supported floor up. Install the SDKs your config names, each as its
24
24
  own top-level install root:
@@ -45,7 +45,7 @@ finds a bare specifier by walking *up* from there — which reaches the
45
45
  install prefix's own `node_modules`, but never into a sibling package's
46
46
  subtree. An SDK that landed inside some other package is invisible to
47
47
  the adapter even though it is on disk
48
- ([DR-026](../specs/decisions/026-optional-adapter-sdks.md)).
48
+ ([DR-026](https://github.com/sublang-ai/playbook/blob/main/specs/decisions/026-optional-adapter-sdks.md)).
49
49
 
50
50
  Both surfaces check this before doing any work: a declared adapter
51
51
  whose runtime is not loadable — or is installed below the version
@@ -54,7 +54,7 @@ runtime is reported as not installed; a stale one with its installed
54
54
  and required versions, never as absent. Either way the remedy printed
55
55
  is cligent's pinned install, `npm install -g <package>@<version>`, so
56
56
  following it cannot install a version the gate refuses again
57
- ([[playbook-cli-40](../specs/packages/playbook-cli.md#playbook-cli-40)]).
57
+ ([[playbook-cli-40](https://github.com/sublang-ai/playbook/blob/main/specs/packages/playbook-cli.md#playbook-cli-40)]).
58
58
 
59
59
  ## Interactive
60
60
 
@@ -98,8 +98,8 @@ later signals use native client detach or termination semantics and do not
98
98
  retire the pane child's session. Only `--config` and composed
99
99
  `--theme-diagnostics` use the stock subprocess boundary: those forms mirror
100
100
  its exit status or signal and exit `127` when it cannot be spawned
101
- ([[playbook-cli-1](../specs/packages/playbook-cli.md#playbook-cli-1)],
102
- [[playbook-cli-2](../specs/packages/playbook-cli.md#playbook-cli-2)]).
101
+ ([[playbook-cli-1](https://github.com/sublang-ai/playbook/blob/main/specs/packages/playbook-cli.md#playbook-cli-1)],
102
+ [[playbook-cli-2](https://github.com/sublang-ai/playbook/blob/main/specs/packages/playbook-cli.md#playbook-cli-2)]).
103
103
 
104
104
  ### Running a Boss turn
105
105
 
@@ -118,8 +118,8 @@ currently offers. It never does the specialized work itself, and a
118
118
  conversational turn — including a progress or status question — leaves
119
119
  the engagement, its parked state, and any pending player question
120
120
  untouched
121
- ([[playbook-captain-1](../specs/packages/playbook-captain.md#playbook-captain-1)],
122
- [[playbook-captain-2](../specs/packages/playbook-captain.md#playbook-captain-2)]).
121
+ ([[playbook-captain-1](https://github.com/sublang-ai/playbook/blob/main/specs/packages/playbook-captain.md#playbook-captain-1)],
122
+ [[playbook-captain-2](https://github.com/sublang-ai/playbook/blob/main/specs/packages/playbook-captain.md#playbook-captain-2)]).
123
123
 
124
124
  The current CODE, REVIEW, and DECIDE workflows take their deterministic
125
125
  initial event from the selecting Boss turn. CODE and DECIDE then call REVIEW
@@ -131,16 +131,16 @@ identical. When a player surfaces a
131
131
  clarifying question the FSM parks, the pane shows the question, and a
132
132
  judge classifies your next turn as its reply or a fresh directive that
133
133
  abandons it
134
- ([[playbook-runtime-2](../specs/packages/playbook-runtime.md#playbook-runtime-2)]).
134
+ ([[playbook-runtime-2](https://github.com/sublang-ai/playbook/blob/main/specs/packages/playbook-runtime.md#playbook-runtime-2)]).
135
135
 
136
136
  The Captain pane shows start/stop/finished status with `◇` lines and
137
137
  streams progress with captain-speech classification and questions
138
- ([[playbook-runtime-3](../specs/packages/playbook-runtime.md#playbook-runtime-3)]), while player
138
+ ([[playbook-runtime-3](https://github.com/sublang-ai/playbook/blob/main/specs/packages/playbook-runtime.md#playbook-runtime-3)]), while player
139
139
  prompts ride their own panes. A turn that actually did something ends
140
140
  with one Captain reply summarizing what changed, composed only from that
141
141
  turn's reported outcome; a turn that changed nothing ends with an
142
142
  ordinary reply and no saved-counts line
143
- ([[playbook-captain-19](../specs/packages/playbook-captain.md#playbook-captain-19)]).
143
+ ([[playbook-captain-19](https://github.com/sublang-ai/playbook/blob/main/specs/packages/playbook-captain.md#playbook-captain-19)]).
144
144
 
145
145
  ## Headless
146
146
 
@@ -182,7 +182,7 @@ Captain reply. Argument, config, catalog, readiness, or pre-turn setup errors
182
182
  exit `1`; a started-turn, persistence, lease-release, or presentation failure
183
183
  exits `2` with stdout empty. SIGINT, SIGTERM, and SIGHUP preserve the
184
184
  uncertain boundary, withhold stdout, and are re-raised after lease retirement
185
- ([[playbook-cli-18](../specs/packages/playbook-cli.md#playbook-cli-18)]).
185
+ ([[playbook-cli-18](https://github.com/sublang-ai/playbook/blob/main/specs/packages/playbook-cli.md#playbook-cli-18)]).
186
186
 
187
187
  The former positional `<from>`, `resume`, `--player`, `--captain`,
188
188
  `--option`, `--cwd`, `--last`, run-only `--config`, and top-level `run:`
@@ -221,8 +221,8 @@ the primary config file. Before either front end imports a configured
221
221
  filesystem registry, the shared launcher checks whether that module can
222
222
  resolve `xstate` and `@sublang/playbook/xstate-runtime`. When needed, it
223
223
  creates engine symlinks beside the module and prints one provisioning line
224
- ([[playbook-cli-36](../specs/packages/playbook-cli.md#playbook-cli-36)],
225
- [DR-024](../specs/decisions/024-runtime-engine-provisioning.md)).
224
+ ([[playbook-cli-36](https://github.com/sublang-ai/playbook/blob/main/specs/packages/playbook-cli.md#playbook-cli-36)],
225
+ [DR-024](https://github.com/sublang-ai/playbook/blob/main/specs/decisions/024-runtime-engine-provisioning.md)).
226
226
 
227
227
  A directory where both imports already resolve is untouched, and
228
228
  `--no-provision` disables new links for either fresh front end. If the
@@ -261,8 +261,8 @@ current `model` and `effort` selections apply to the next call, including an
261
261
  explicit boolean `false` provider-default reset. The retained provider token
262
262
  is never silently replaced by a fresh conversation if that selection is not
263
263
  supported
264
- ([[playbook-cli-22](../specs/packages/playbook-cli.md#playbook-cli-22)],
265
- [DR-032](../specs/decisions/032-explicit-roles-session-players.md)).
264
+ ([[playbook-cli-22](https://github.com/sublang-ai/playbook/blob/main/specs/packages/playbook-cli.md#playbook-cli-22)],
265
+ [DR-032](https://github.com/sublang-ai/playbook/blob/main/specs/decisions/032-explicit-roles-session-players.md)).
266
266
 
267
267
  ### Recovering an uncertain turn
268
268
 
@@ -30,7 +30,7 @@ names, nesting, and ancestry never infer a binding.
30
30
  Each `captain` or `players.<player-id>` value is either an adapter shorthand
31
31
  (`claude`, `codex`) or a block carrying that agent's own `adapter`, `model`,
32
32
  `effort`, `instruction`, and `permissions`. Settings are inline per stable
33
- agent ([DR-021](../specs/decisions/021-inline-agent-settings.md)). Dots in a
33
+ agent ([DR-021](https://github.com/sublang-ai/playbook/blob/main/specs/decisions/021-inline-agent-settings.md)). Dots in a
34
34
  player ID are literal characters, not YAML hierarchy. Other adapter IDs pass
35
35
  through to `tmux-play` with a warning because `playbook` cannot preflight their
36
36
  auth.
@@ -91,7 +91,7 @@ playbooks:
91
91
  The current bundled workflows accept no workflow-specific options.
92
92
  Each role's per-call prompt names its current `model`, else its player's
93
93
  `adapter`
94
- ([[playbook-runtime-4](../specs/packages/playbook-runtime.md#playbook-runtime-4)]),
94
+ ([[playbook-runtime-4](https://github.com/sublang-ai/playbook/blob/main/specs/packages/playbook-runtime.md#playbook-runtime-4)]),
95
95
  so commit trailers credit the concrete model rather than the adapter
96
96
  family.
97
97
 
@@ -160,7 +160,7 @@ instead of doing the work itself. Claude and Gemini enforce that at the
160
160
  provider level. The Codex, Kimi, and OpenCode adapters cannot — they
161
161
  reject any tool list — so a `captain:` using one of them falls back to a
162
162
  prompt-level restriction
163
- ([DR-013](../specs/decisions/013-routing-only-captain-control.md) A1).
163
+ ([DR-013](https://github.com/sublang-ai/playbook/blob/main/specs/decisions/013-routing-only-captain-control.md) A1).
164
164
  Those adapters remain good choices for *players*, where full tools are wanted.
165
165
 
166
166
  Adapter readiness is intentionally light: `claude` is ready with local
@@ -203,7 +203,7 @@ playbooks:
203
203
  ```
204
204
 
205
205
  The global file is never modified, and `--with` is not forwarded to
206
- `tmux-play` ([[playbook-cli-25](../specs/packages/playbook-cli.md#playbook-cli-25)]).
206
+ `tmux-play` ([[playbook-cli-25](https://github.com/sublang-ai/playbook/blob/main/specs/packages/playbook-cli.md#playbook-cli-25)]).
207
207
  Overlays apply when creating a fresh session and as current-config input for a
208
208
  compatible ordinary reopen. A selected session keeps its stored catalog,
209
209
  player roster, role bindings, adapter, instruction, permissions, and working
@@ -250,6 +250,11 @@ playbooks:
250
250
  playbook run "/my-workflow perform the task"
251
251
  ```
252
252
 
253
+ Importing a `.ts` registry uses Node's native type stripping, available
254
+ unflagged on Node 22.18+ and 23.6+; on the older Node versions this
255
+ package supports (>= 20.6), compile the registry and point `from` at the
256
+ emitted `.js` module instead.
257
+
253
258
  A relative path-shaped `from` is resolved relative to the primary config
254
259
  file, not the invocation directory; an absolute path is clearest for an SLC
255
260
  entry emitted in a project working tree.
@@ -318,9 +323,9 @@ or `--session`.
318
323
 
319
324
  The JSON response is now exactly `{ "sessionId": "…", "reply": "…" }`.
320
325
  Released direct-run session records are not complete Captain sessions and
321
- cannot be continued by the new host ([[playbook-cli-19](../specs/packages/playbook-cli.md#playbook-cli-19)],
322
- [[playbook-cli-22](../specs/packages/playbook-cli.md#playbook-cli-22)],
323
- [[playbook-cli-28](../specs/packages/playbook-cli.md#playbook-cli-28)]).
326
+ cannot be continued by the new host ([[playbook-cli-19](https://github.com/sublang-ai/playbook/blob/main/specs/packages/playbook-cli.md#playbook-cli-19)],
327
+ [[playbook-cli-22](https://github.com/sublang-ai/playbook/blob/main/specs/packages/playbook-cli.md#playbook-cli-22)],
328
+ [[playbook-cli-28](https://github.com/sublang-ai/playbook/blob/main/specs/packages/playbook-cli.md#playbook-cli-28)]).
324
329
 
325
330
  ## Migrating from `profiles`
326
331
 
@@ -334,7 +339,7 @@ do by hand.
334
339
 
335
340
  For a one-off, pass a raw `tmux-play` config explicitly. This bypasses
336
341
  the seed, composition, and readiness gate, forwarding arguments to
337
- `tmux-play` verbatim ([[playbook-cli-1](../specs/packages/playbook-cli.md#playbook-cli-1)]):
342
+ `tmux-play` verbatim ([[playbook-cli-1](https://github.com/sublang-ai/playbook/blob/main/specs/packages/playbook-cli.md#playbook-cli-1)]):
338
343
 
339
344
  ```sh
340
345
  playbook --config ./tmux-play.config.yaml
package/docs/embedding.md CHANGED
@@ -9,7 +9,7 @@ app) is another. This guide shows how to wire a playbook runtime into
9
9
  your own host.
10
10
 
11
11
  > **Release note:** this guide targets the current semver-stable six-port
12
- > contract; see the [CHANGELOG](../CHANGELOG.md) for migration details.
12
+ > contract; see the [CHANGELOG](https://github.com/sublang-ai/playbook/blob/main/CHANGELOG.md) for migration details.
13
13
 
14
14
  ## The runtime contract
15
15
 
@@ -31,6 +31,11 @@ nested-playbook bridge.
31
31
 
32
32
  ## Constructing a runtime against your own ports
33
33
 
34
+ `p-queue` is your host's own dependency here — declare it in your
35
+ application's `dependencies` (the same library `@sublang/playbook` itself
36
+ depends on) rather than relying on it resolving through the package's
37
+ tree, which pnpm's strict linking will not allow.
38
+
34
39
  ```ts
35
40
  import createPlaybookRuntime from '@sublang/playbook/review/playbook';
36
41
  import type {
@@ -190,7 +195,7 @@ Boss-visible status text. Because trace observers do receive opaque resume
190
195
  tokens, persisted traces should be protected as sensitive data.
191
196
 
192
197
  See
193
- [`code.playbook.test.ts`](../reference/sdlc/code.playbook/code.playbook.test.ts)
198
+ [`code.playbook.test.ts`](https://github.com/sublang-ai/playbook/blob/main/reference/sdlc/code.playbook/code.playbook.test.ts)
194
199
  for the full range of port shapes (classifier, judge, abort, interrupt,
195
200
  status/telemetry) the runtime is contract-tested against.
196
201
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sublang/playbook",
3
- "version": "8.0.0",
3
+ "version": "9.0.0",
4
4
  "type": "module",
5
5
  "description": "Composable XState v5 playbook runtime with compiled Captain, CODE, REVIEW, and DECIDE workflows driven by GEARS specs.",
6
6
  "license": "Apache-2.0",
@@ -33,7 +33,7 @@
33
33
  // controller-port submission, and status formatting.
34
34
  // Compat: spec.compat = { artifactSchema: 2, runtimeAbi: 1 }
35
35
  // (DR-022; checked at construction by the loading engine).
36
- import { createXStatePlaybookRuntime, defaultComposeCaptainPrompt, normalizeError, normalizeErrorCompact, parseJudgeJson, snapshotJsonValue, RUNTIME_ABI, } from '../../../src/xstate-runtime.js';
36
+ import { createXStatePlaybookRuntime, defaultComposeCaptainPrompt, normalizeError, normalizeErrorCompact, parseJudgeJson, snapshotJsonValue, } from '../../../src/xstate-runtime.js';
37
37
  import { captainMachine, } from './captain.fsm.js';
38
38
  function assertNonEmptyString(value, label) {
39
39
  if (typeof value !== 'string' || value.trim().length === 0) {
@@ -516,7 +516,11 @@ export const _internal = {
516
516
  // describe/apply control surface — lives in @sublang/playbook/xstate-runtime.
517
517
  const runtimeSpec = {
518
518
  label: 'CAPTAIN',
519
- compat: { artifactSchema: 2, runtimeAbi: RUNTIME_ABI },
519
+ // DR-022 / slc/link.md: the declaration carries the value current at link
520
+ // time — a literal, never the loading engine's RUNTIME_ABI self-report,
521
+ // which would follow whatever engine loads the module and make the
522
+ // factory's skew check compare that engine with itself.
523
+ compat: { artifactSchema: 2, runtimeAbi: 1 },
520
524
  snapshotOptions: snapshotCaptainOptions,
521
525
  machineInput: (options) => ({ enabledPlaybooks: options.enabledPlaybooks }),
522
526
  classifyBossText: (text, ports, signal, snapshotOrState, boundary, options) => classifyControllerTurn(text, ports, signal, snapshotOrState, boundary, options),
@@ -543,8 +547,15 @@ const runtimeSpec = {
543
547
  ],
544
548
  statusesForState,
545
549
  };
546
- const createCaptainPlaybookRuntime = createXStatePlaybookRuntime(captainMachine, runtimeSpec);
550
+ // DR-022's compat check fails fast at factory construction. The Captain is
551
+ // statically imported by the shell and both CLI front ends, so constructing
552
+ // here at module evaluation would turn a future compat mismatch into an
553
+ // uncaught ESM-load error that takes even `--help` down; constructing on
554
+ // the first runtime request keeps the failure inside the caught
555
+ // host-construction boundary that owes the Boss a setup diagnostic.
556
+ let createCaptainPlaybookRuntime;
547
557
  export function createPlaybookRuntime(options) {
558
+ createCaptainPlaybookRuntime ??= createXStatePlaybookRuntime(captainMachine, runtimeSpec);
548
559
  return createCaptainPlaybookRuntime(options);
549
560
  }
550
561
  const factory = createPlaybookRuntime;
@@ -41,7 +41,6 @@ import {
41
41
  normalizeErrorCompact,
42
42
  parseJudgeJson,
43
43
  snapshotJsonValue,
44
- RUNTIME_ABI,
45
44
  type PlaybookActorOutput,
46
45
  type PlaybookCaptainInput,
47
46
  type ScheduledStatus,
@@ -798,7 +797,11 @@ export const _internal = {
798
797
  // describe/apply control surface — lives in @sublang/playbook/xstate-runtime.
799
798
  const runtimeSpec: XStatePlaybookRuntimeSpec<ValidatedCaptainOptions> = {
800
799
  label: 'CAPTAIN',
801
- compat: { artifactSchema: 2, runtimeAbi: RUNTIME_ABI },
800
+ // DR-022 / slc/link.md: the declaration carries the value current at link
801
+ // time — a literal, never the loading engine's RUNTIME_ABI self-report,
802
+ // which would follow whatever engine loads the module and make the
803
+ // factory's skew check compare that engine with itself.
804
+ compat: { artifactSchema: 2, runtimeAbi: 1 },
802
805
  snapshotOptions: snapshotCaptainOptions,
803
806
  machineInput: (options) => ({ enabledPlaybooks: options.enabledPlaybooks }),
804
807
  classifyBossText: (text, ports, signal, snapshotOrState, boundary, options) =>
@@ -834,12 +837,23 @@ const runtimeSpec: XStatePlaybookRuntimeSpec<ValidatedCaptainOptions> = {
834
837
  statusesForState,
835
838
  };
836
839
 
837
- const createCaptainPlaybookRuntime: PlaybookRuntimeFactory<ValidatedCaptainOptions> =
838
- createXStatePlaybookRuntime(captainMachine, runtimeSpec);
840
+ // DR-022's compat check fails fast at factory construction. The Captain is
841
+ // statically imported by the shell and both CLI front ends, so constructing
842
+ // here at module evaluation would turn a future compat mismatch into an
843
+ // uncaught ESM-load error that takes even `--help` down; constructing on
844
+ // the first runtime request keeps the failure inside the caught
845
+ // host-construction boundary that owes the Boss a setup diagnostic.
846
+ let createCaptainPlaybookRuntime:
847
+ | PlaybookRuntimeFactory<ValidatedCaptainOptions>
848
+ | undefined;
839
849
 
840
850
  export function createPlaybookRuntime(
841
851
  options: PlaybookRuntimeOptions,
842
852
  ): PlaybookRuntime {
853
+ createCaptainPlaybookRuntime ??= createXStatePlaybookRuntime(
854
+ captainMachine,
855
+ runtimeSpec,
856
+ );
843
857
  return createCaptainPlaybookRuntime(options);
844
858
  }
845
859
 
@@ -202,7 +202,7 @@ export declare const codingMachine: import("xstate").StateMachine<CodingContext,
202
202
  } | {
203
203
  type: "resumesIrTask";
204
204
  params: unknown;
205
- }, never, "done" | "failed" | "awaitBossReply" | "ready" | "runFirstPhase" | "reviewFirstCommit" | "runIrTask" | "reviewIrTask", string, CodingInput, {
205
+ }, never, "done" | "failed" | "awaitBossReply" | "ready" | "runFirstPhase" | "reviewFirstCommit" | "runIrTask" | "reviewIrTask" | "reportedReviewFailure", string, CodingInput, {
206
206
  readonly status: "complete";
207
207
  /** Exact identity of the latest CODE-owned commit. */
208
208
  readonly lastCodeCommit: string;
@@ -239,6 +239,9 @@ export declare const codingMachine: import("xstate").StateMachine<CodingContext,
239
239
  readonly failed: {
240
240
  id: "failed";
241
241
  };
242
+ readonly reportedReviewFailure: {
243
+ id: "reportedReviewFailure";
244
+ };
242
245
  readonly done: {
243
246
  id: "done";
244
247
  };
@@ -53,6 +53,7 @@ const STATE_DESCRIPTIONS = {
53
53
  reviewIrTask: 'The REVIEW playbook is checking the latest task commit.',
54
54
  awaitBossReply: 'Waiting for Boss to answer Coder.',
55
55
  failed: 'The coding workflow failed and is waiting for a new coding intent.',
56
+ reportedReviewFailure: 'The coding workflow reported a REVIEW failure and the last code-owned commit.',
56
57
  done: 'The coding workflow completed after REVIEW found no unsettled findings.',
57
58
  };
58
59
  function playbookMeta(stateId, role) {
@@ -530,14 +531,14 @@ export const codingMachine = machineSetup.createMachine({
530
531
  target: 'runIrTask',
531
532
  },
532
533
  {
533
- target: 'done',
534
+ target: 'reportedReviewFailure',
534
535
  actions: 'completeWithInvalidReviewOutput',
535
536
  },
536
537
  ],
537
538
  onError: [
538
539
  {
539
540
  guard: 'authoredReviewFailure',
540
- target: 'done',
541
+ target: 'reportedReviewFailure',
541
542
  actions: 'completeWithReviewFailure',
542
543
  },
543
544
  { target: 'failed', actions: 'rememberActorError' },
@@ -619,14 +620,14 @@ export const codingMachine = machineSetup.createMachine({
619
620
  actions: 'completeSuccessfully',
620
621
  },
621
622
  {
622
- target: 'done',
623
+ target: 'reportedReviewFailure',
623
624
  actions: 'completeWithInvalidReviewOutput',
624
625
  },
625
626
  ],
626
627
  onError: [
627
628
  {
628
629
  guard: 'authoredReviewFailure',
629
- target: 'done',
630
+ target: 'reportedReviewFailure',
630
631
  actions: 'completeWithReviewFailure',
631
632
  },
632
633
  { target: 'failed', actions: 'rememberActorError' },
@@ -670,6 +671,12 @@ export const codingMachine = machineSetup.createMachine({
670
671
  START_CODE: { target: 'runFirstPhase', actions: 'startCoding' },
671
672
  },
672
673
  },
674
+ reportedReviewFailure: {
675
+ id: 'reportedReviewFailure',
676
+ description: STATE_DESCRIPTIONS.reportedReviewFailure,
677
+ meta: playbookMeta('reportedReviewFailure'),
678
+ type: 'final',
679
+ },
673
680
  done: {
674
681
  id: 'done',
675
682
  description: STATE_DESCRIPTIONS.done,
@@ -202,6 +202,8 @@ const STATE_DESCRIPTIONS = {
202
202
  reviewIrTask: 'The REVIEW playbook is checking the latest task commit.',
203
203
  awaitBossReply: 'Waiting for Boss to answer Coder.',
204
204
  failed: 'The coding workflow failed and is waiting for a new coding intent.',
205
+ reportedReviewFailure:
206
+ 'The coding workflow reported a REVIEW failure and the last code-owned commit.',
205
207
  done: 'The coding workflow completed after REVIEW found no unsettled findings.',
206
208
  } as const;
207
209
 
@@ -768,14 +770,14 @@ export const codingMachine = machineSetup.createMachine({
768
770
  target: 'runIrTask',
769
771
  },
770
772
  {
771
- target: 'done',
773
+ target: 'reportedReviewFailure',
772
774
  actions: 'completeWithInvalidReviewOutput',
773
775
  },
774
776
  ],
775
777
  onError: [
776
778
  {
777
779
  guard: 'authoredReviewFailure',
778
- target: 'done',
780
+ target: 'reportedReviewFailure',
779
781
  actions: 'completeWithReviewFailure',
780
782
  },
781
783
  { target: 'failed', actions: 'rememberActorError' },
@@ -857,14 +859,14 @@ export const codingMachine = machineSetup.createMachine({
857
859
  actions: 'completeSuccessfully',
858
860
  },
859
861
  {
860
- target: 'done',
862
+ target: 'reportedReviewFailure',
861
863
  actions: 'completeWithInvalidReviewOutput',
862
864
  },
863
865
  ],
864
866
  onError: [
865
867
  {
866
868
  guard: 'authoredReviewFailure',
867
- target: 'done',
869
+ target: 'reportedReviewFailure',
868
870
  actions: 'completeWithReviewFailure',
869
871
  },
870
872
  { target: 'failed', actions: 'rememberActorError' },
@@ -908,6 +910,12 @@ export const codingMachine = machineSetup.createMachine({
908
910
  START_CODE: { target: 'runFirstPhase', actions: 'startCoding' },
909
911
  },
910
912
  },
913
+ reportedReviewFailure: {
914
+ id: 'reportedReviewFailure',
915
+ description: STATE_DESCRIPTIONS.reportedReviewFailure,
916
+ meta: playbookMeta('reportedReviewFailure'),
917
+ type: 'final',
918
+ },
911
919
  done: {
912
920
  id: 'done',
913
921
  description: STATE_DESCRIPTIONS.done,
@@ -9,7 +9,7 @@
9
9
  // Adjudication: LLM judge per player state; coderOutput is carried verbatim
10
10
  // Nested call: literal review target through the shared bridge
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 { codingMachine, } from './code.fsm.js';
14
14
  const OPTION_KEYS = new Set(['runResults']);
15
15
  const PLACEHOLDER = /<(#|[A-Za-z_$][A-Za-z0-9_$-]*)>/g;
@@ -81,9 +81,20 @@ export const _internal = {
81
81
  };
82
82
  const runtimeSpec = {
83
83
  label: 'CODE',
84
- compat: { artifactSchema: 2, runtimeAbi: RUNTIME_ABI },
84
+ // DR-022 / slc/link.md: the declaration carries the value current at link
85
+ // time — a literal, never the loading engine's RUNTIME_ABI self-report,
86
+ // which would follow whatever engine loads the module and make the
87
+ // factory's skew check compare that engine with itself.
88
+ compat: { artifactSchema: 2, runtimeAbi: 1 },
85
89
  snapshotOptions: snapshotCodeOptions,
86
- entryEvent: { type: 'START_CODE', textField: 'callerInput' },
90
+ entryEvent: {
91
+ type: 'START_CODE',
92
+ textField: 'callerInput',
93
+ // `startCoding` copies the entry text here, so the failure-state retry
94
+ // reads it back from the persisted machine snapshot and survives a
95
+ // continued session (DR-034).
96
+ contextField: 'callerInput',
97
+ },
87
98
  roleStates: {
88
99
  runFirstPhase: {
89
100
  role: 'coder',
@@ -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,
@@ -168,9 +167,20 @@ export const _internal = {
168
167
 
169
168
  const runtimeSpec = {
170
169
  label: 'CODE',
171
- compat: { artifactSchema: 2, runtimeAbi: RUNTIME_ABI },
170
+ // DR-022 / slc/link.md: the declaration carries the value current at link
171
+ // time — a literal, never the loading engine's RUNTIME_ABI self-report,
172
+ // which would follow whatever engine loads the module and make the
173
+ // factory's skew check compare that engine with itself.
174
+ compat: { artifactSchema: 2, runtimeAbi: 1 },
172
175
  snapshotOptions: snapshotCodeOptions,
173
- entryEvent: { type: 'START_CODE', textField: 'callerInput' },
176
+ entryEvent: {
177
+ type: 'START_CODE',
178
+ textField: 'callerInput',
179
+ // `startCoding` copies the entry text here, so the failure-state retry
180
+ // reads it back from the persisted machine snapshot and survives a
181
+ // continued session (DR-034).
182
+ contextField: 'callerInput',
183
+ },
174
184
  roleStates: {
175
185
  runFirstPhase: {
176
186
  role: 'coder',
@@ -126,6 +126,34 @@ function stateDigestLine(state, description) {
126
126
  digestLine `status ${state.status}`,
127
127
  ].join('; ');
128
128
  }
129
+ // CAPTAIN-5's mirrored ledger member holds the runtime snapshot's
130
+ // pending-question projection — entries of `{ questionId, asker, question,
131
+ // sourceItem }` — never a raw telemetry payload: the linked runtime's state
132
+ // telemetry carries the singular full-context question, whose extra
133
+ // runtime-internal fields (`resumeStateId`) fail the durable snapshot's
134
+ // leaf-projection equality (CAPTAIN-41) and with it headless settlement.
135
+ function mirroredBossQuestions(value) {
136
+ if (value === undefined || value === null)
137
+ return undefined;
138
+ const entries = Array.isArray(value) ? value : [value];
139
+ return entries.map((entry) => {
140
+ if (typeof entry !== 'object' || entry === null)
141
+ return entry;
142
+ const record = entry;
143
+ const projected = {};
144
+ if (record.questionId !== undefined) {
145
+ projected.questionId = record.questionId;
146
+ }
147
+ if (record.asker !== undefined)
148
+ projected.asker = record.asker;
149
+ if (record.question !== undefined)
150
+ projected.question = record.question;
151
+ if (record.sourceItem !== undefined) {
152
+ projected.sourceItem = record.sourceItem;
153
+ }
154
+ return projected;
155
+ });
156
+ }
129
157
  function pendingQuestionLines(pending) {
130
158
  const list = Array.isArray(pending)
131
159
  ? pending
@@ -1491,8 +1519,7 @@ export function createPlaybookCaptainShell(options, deps = {}) {
1491
1519
  }
1492
1520
  }
1493
1521
  if (leafFrame() === frame) {
1494
- pendingBossQuestions =
1495
- record.pendingBossQuestions ?? record.pendingBossQuestion;
1522
+ pendingBossQuestions = mirroredBossQuestions(record.pendingBossQuestions ?? record.pendingBossQuestion);
1496
1523
  lastError = normalizeErrorCompact(record.lastError);
1497
1524
  if (state.quiescent && state.tags.includes('playbook.parked')) {
1498
1525
  await setMode('engaged.parked', `sub-runtime:${state.stateId ?? 'structured'}`);
@@ -2308,7 +2335,7 @@ export function createPlaybookCaptainShell(options, deps = {}) {
2308
2335
  // runtime publishes before disposal removes the frame. The opaque run
2309
2336
  // output remains runtime-to-runtime data and never becomes Captain
2310
2337
  // evidence (CAPPLAY-10).
2311
- activeTurn?.settlementFacts.push(rootCompletionFact(frame));
2338
+ activeTurn?.settlementFacts.push(rootCompletionFact(frame, result));
2312
2339
  await runEffect(() => disposeStack('final'));
2313
2340
  }
2314
2341
  return;
@@ -3200,10 +3227,10 @@ export function createPlaybookCaptainShell(options, deps = {}) {
3200
3227
  // -------------------------------------------------------------------------
3201
3228
  // The controller port (DR-029): host validation is the sole effector.
3202
3229
  // -------------------------------------------------------------------------
3203
- // The leaf's published state description, read from its control view the
3204
- // same way the digest reads it. A leaf without the pair or one whose view
3205
- // cannot be read at this moment publishes none, and the summary then says
3206
- // so instead of falling back to the state id.
3230
+ // The legacy state-description channel, read from the live control view the
3231
+ // same way the digest reads it. DR-037 makes the terminal result authoritative
3232
+ // for completion; this remains only for an older runtime that omits the new
3233
+ // optional member.
3207
3234
  const leafStateDescription = (frame) => {
3208
3235
  if (typeof frame.runtime.describe !== 'function')
3209
3236
  return undefined;
@@ -3214,9 +3241,16 @@ export function createPlaybookCaptainShell(options, deps = {}) {
3214
3241
  return undefined;
3215
3242
  }
3216
3243
  };
3217
- const rootCompletionFact = (frame) => {
3218
- const published = leafStateDescription(frame);
3219
- const description = published === undefined ? '' : compactEvidence(published);
3244
+ const rootCompletionFact = (frame, result) => {
3245
+ const returned = result.stateDescription === undefined
3246
+ ? ''
3247
+ : compactEvidence(result.stateDescription);
3248
+ const legacy = returned === '' ? leafStateDescription(frame) : undefined;
3249
+ const description = returned !== ''
3250
+ ? returned
3251
+ : legacy === undefined
3252
+ ? ''
3253
+ : compactEvidence(legacy);
3220
3254
  return description === ''
3221
3255
  ? `${frameLabel(frame)} completed; its runtime published no result description.`
3222
3256
  : `${frameLabel(frame)} completed; its runtime-published result meaning was ${quoteEvidence(description)}.`;