@sublang/playbook 16.0.0 → 17.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 (40) hide show
  1. package/README.md +2 -0
  2. package/docs/cli.md +36 -18
  3. package/docs/configuration.md +7 -5
  4. package/docs/embedding.md +26 -5
  5. package/package.json +4 -4
  6. package/reference/sdlc/captain.md +19 -10
  7. package/reference/sdlc/captain.playbook/captain.fsm.d.ts +6 -2
  8. package/reference/sdlc/captain.playbook/captain.fsm.js +19 -7
  9. package/reference/sdlc/captain.playbook/captain.fsm.ts +25 -10
  10. package/reference/sdlc/captain.playbook/captain.gears.md +16 -8
  11. package/reference/sdlc/captain.playbook/captain.playbook.d.ts +1 -1
  12. package/reference/sdlc/captain.playbook/captain.playbook.js +3 -2
  13. package/reference/sdlc/captain.playbook/captain.playbook.ts +4 -3
  14. package/reference/sdlc/code.playbook/bin/interactive-session.js +14 -1
  15. package/reference/sdlc/code.playbook/bin/portable-codec.js +3 -0
  16. package/reference/sdlc/code.playbook/bin/repository-effects.js +1 -0
  17. package/reference/sdlc/code.playbook/bin/run.js +128 -85
  18. package/reference/sdlc/code.playbook/bin/session-host.js +40 -8
  19. package/reference/sdlc/code.playbook/bin/session-store.js +162 -34
  20. package/reference/sdlc/code.playbook/code.fsm.d.ts +5 -5
  21. package/reference/sdlc/code.playbook/host-capabilities.d.ts +3 -0
  22. package/reference/sdlc/code.playbook/playbook-captain.d.ts +32 -2
  23. package/reference/sdlc/code.playbook/playbook-captain.js +729 -284
  24. package/reference/sdlc/code.playbook/playbook-captain.ts +771 -358
  25. package/reference/sdlc/code.playbook/session-host.d.ts +4 -1
  26. package/reference/sdlc/code.playbook/session-store.d.ts +17 -0
  27. package/reference/sdlc/code.playbook/session-store.js +1 -0
  28. package/reference/sdlc/decide.playbook/decide.fsm.d.ts +5 -5
  29. package/reference/sdlc/dev.playbook/dev.fsm.d.ts +5 -5
  30. package/reference/sdlc/pr.playbook/pr.fsm.d.ts +5 -5
  31. package/slc/gears2fsm.md +5 -5
  32. package/slc/link.md +77 -33
  33. package/src/runtime.d.ts +46 -2
  34. package/src/runtime.ts +42 -2
  35. package/src/xstate-playbook-runtime.d.ts +9 -8
  36. package/src/xstate-playbook-runtime.js +471 -215
  37. package/src/xstate-playbook-runtime.ts +525 -238
  38. package/src/xstate-runtime.d.ts +3 -4
  39. package/src/xstate-runtime.js +52 -0
  40. package/src/xstate-runtime.ts +59 -4
@@ -48,7 +48,7 @@ export interface OpenSessionHostOptions {
48
48
  readonly store?: SharedSessionStore;
49
49
  readonly sessionsDir?: string;
50
50
  readonly sessionId?: string;
51
- readonly mode?: 'new' | 'continue' | 'retry';
51
+ readonly mode?: 'new' | 'continue' | 'retry' | 'recover';
52
52
  readonly cwd?: string;
53
53
  readonly config?: SessionExecutionProjection;
54
54
  readonly plan?: any;
@@ -98,6 +98,9 @@ export interface SessionHostController {
98
98
  */
99
99
  submitShellAction(actionId: string): Promise<SessionRecovery>;
100
100
  retry(): Promise<SessionRecovery>;
101
+ /** Restore and report recorded uncertainty without running work; settled pauses require Boss input. Never discards work.
102
+ * After this live controller becomes uncertain, dispose and reopen with mode:'recover' first. */
103
+ recover(input?: string): Promise<SessionRecovery>;
101
104
  dispose(): Promise<void>;
102
105
  }
103
106
  export declare function openSessionHost(options: OpenSessionHostOptions): Promise<SessionHostController>;
@@ -113,6 +113,18 @@ export interface SessionStructuralProjection {
113
113
  readonly players: readonly Readonly<Record<string, any>>[];
114
114
  readonly catalog: Readonly<Record<string, any>>;
115
115
  }
116
+ export interface SessionStep {
117
+ readonly id: string;
118
+ readonly kind: 'player' | 'captain' | 'script' | 'preparation' | 'completion' | 'answer';
119
+ readonly stateId: string;
120
+ readonly runtimeSessionId: string;
121
+ readonly playbookId: string;
122
+ readonly result?: unknown;
123
+ }
124
+ export interface SessionProgressChange {
125
+ readonly snapshot?: SessionSnapshot | null;
126
+ readonly step?: SessionStep;
127
+ }
116
128
  export interface SessionRecovery {
117
129
  readonly schemaVersion: 6;
118
130
  readonly kind: 'captain-session';
@@ -135,6 +147,7 @@ export interface SessionRecovery {
135
147
  readonly markedAt: string;
136
148
  readonly attemptedExecutionProjection: SessionExecutionProjection;
137
149
  readonly abandonment?: Readonly<Record<string, any>>;
150
+ readonly progress?: { readonly snapshot: SessionSnapshot | null; readonly steps: readonly SessionStep[]; readonly positionStepId?: string | null };
138
151
  };
139
152
  }
140
153
  export interface SessionReplayCheckpoint {
@@ -215,6 +228,7 @@ export interface PlaybookSessionLifecycle extends PlaybookSessionLease {
215
228
  abandonFreshSettled(options: { expected: SessionRecovery }): Promise<boolean>;
216
229
  beginTurn(options: { input: string; attemptId: string; attemptedExecutionProjection: SessionExecutionProjection }): Promise<SessionRecovery>;
217
230
  beginRetry(options: { expectedAttemptId: string; nextAttemptId: string }): Promise<SessionRecovery>;
231
+ recordProgress(change: SessionProgressChange): Promise<void>;
218
232
  settle(options: { attemptId: string; snapshot: SessionSnapshot; unresolvedEffects: readonly SessionUnresolvedEffect[]; retentionUpdates?: readonly SessionRetentionUpdate[] }): Promise<SessionRecovery>;
219
233
  discard(options: { attemptId: string }): Promise<SessionRecovery | undefined>;
220
234
  beginUnresolvedEffectAbandonment(options: any): Promise<any>;
@@ -255,3 +269,6 @@ export declare function validateCaptainSessionExecutionProjection(value: unknown
255
269
  export declare function validateCaptainSessionStructuralProjection(value: unknown): SessionStructuralProjection;
256
270
  export declare function assertCaptainSessionExecutionCompatible(structural: SessionStructuralProjection, execution: SessionExecutionProjection): SessionExecutionProjection;
257
271
  export declare function attachSessionHints(snapshot: SessionSnapshot, hints: SessionHints): SessionSnapshot;
272
+
273
+ /** Whether discarding an uncertain turn restores its untouched baseline. */
274
+ export declare function isUncertainTurnDiscardable(record: SessionRecovery): boolean;
@@ -117,6 +117,7 @@ function projectStatus(status) {
117
117
 
118
118
  export {
119
119
  createCaptainSessionStore as createSessionStore,
120
+ isUncertainTurnDiscardable,
120
121
  projectCaptainSessionStructure,
121
122
  validateCaptainSessionExecutionProjection,
122
123
  validateCaptainSessionStructuralProjection,
@@ -203,15 +203,15 @@ export type DecideOutput = {
203
203
  */
204
204
  export declare function authoredChildResult(error: unknown, expectedPlaybookId: string): PlaybookCallResult | undefined;
205
205
  export declare const decideMachine: import("xstate").StateMachine<DecideContext, import("xstate").AnyEventObject, {
206
- [x: string]: import("xstate").ActorRefFromLogic<import("xstate").PromiseActorLogic<PlaybookOutput, PlaybookInput, import("xstate").EventObject>> | import("xstate").ActorRefFromLogic<import("xstate").PromiseActorLogic<PlayerOutput, PlayerInput, import("xstate").EventObject>> | undefined;
206
+ [x: string]: import("xstate").ActorRefFromLogic<import("xstate").PromiseActorLogic<PlayerOutput, PlayerInput, import("xstate").EventObject>> | import("xstate").ActorRefFromLogic<import("xstate").PromiseActorLogic<PlaybookOutput, PlaybookInput, import("xstate").EventObject>> | undefined;
207
207
  }, {
208
- src: "playbook";
209
- logic: import("xstate").PromiseActorLogic<PlaybookOutput, PlaybookInput, import("xstate").EventObject>;
210
- id: string | undefined;
211
- } | {
212
208
  src: "player";
213
209
  logic: import("xstate").PromiseActorLogic<PlayerOutput, PlayerInput, import("xstate").EventObject>;
214
210
  id: string | undefined;
211
+ } | {
212
+ src: "playbook";
213
+ logic: import("xstate").PromiseActorLogic<PlaybookOutput, PlaybookInput, import("xstate").EventObject>;
214
+ id: string | undefined;
215
215
  }, {
216
216
  type: "playbook.acceptedOutcome";
217
217
  params: {
@@ -152,15 +152,15 @@ export declare const devMachine: import("xstate").StateMachine<DevContext, {
152
152
  readonly answer: string;
153
153
  readonly questionId?: string;
154
154
  }, {
155
- [x: string]: import("xstate").ActorRefFromLogic<import("xstate").PromiseActorLogic<PlaybookOutput, PlaybookInput, import("xstate").EventObject>> | import("xstate").ActorRefFromLogic<import("xstate").PromiseActorLogic<PlayerOutput, PlayerInput, import("xstate").EventObject>> | undefined;
155
+ [x: string]: import("xstate").ActorRefFromLogic<import("xstate").PromiseActorLogic<PlayerOutput, PlayerInput, import("xstate").EventObject>> | import("xstate").ActorRefFromLogic<import("xstate").PromiseActorLogic<PlaybookOutput, PlaybookInput, import("xstate").EventObject>> | undefined;
156
156
  }, {
157
- src: "playbook";
158
- logic: import("xstate").PromiseActorLogic<PlaybookOutput, PlaybookInput, import("xstate").EventObject>;
159
- id: string | undefined;
160
- } | {
161
157
  src: "player";
162
158
  logic: import("xstate").PromiseActorLogic<PlayerOutput, PlayerInput, import("xstate").EventObject>;
163
159
  id: string | undefined;
160
+ } | {
161
+ src: "playbook";
162
+ logic: import("xstate").PromiseActorLogic<PlaybookOutput, PlaybookInput, import("xstate").EventObject>;
163
+ id: string | undefined;
164
164
  }, {
165
165
  type: "playbook.acceptedOutcome";
166
166
  params: {
@@ -162,12 +162,8 @@ export declare const prMachine: import("xstate").StateMachine<PrContext, {
162
162
  readonly answer: string;
163
163
  readonly questionId?: "openPullRequest";
164
164
  }, {
165
- [x: string]: import("xstate").ActorRefFromLogic<import("xstate").PromiseActorLogic<PlaybookOutput, PlaybookInput, import("xstate").EventObject>> | import("xstate").ActorRefFromLogic<import("xstate").PromiseActorLogic<PlayerOutput, PlayerInput, import("xstate").EventObject>> | import("xstate").ActorRefFromLogic<import("xstate").PromiseActorLogic<ScriptOutput, ScriptInput, import("xstate").EventObject>> | undefined;
165
+ [x: string]: import("xstate").ActorRefFromLogic<import("xstate").PromiseActorLogic<PlayerOutput, PlayerInput, import("xstate").EventObject>> | import("xstate").ActorRefFromLogic<import("xstate").PromiseActorLogic<ScriptOutput, ScriptInput, import("xstate").EventObject>> | import("xstate").ActorRefFromLogic<import("xstate").PromiseActorLogic<PlaybookOutput, PlaybookInput, import("xstate").EventObject>> | undefined;
166
166
  }, {
167
- src: "playbook";
168
- logic: import("xstate").PromiseActorLogic<PlaybookOutput, PlaybookInput, import("xstate").EventObject>;
169
- id: string | undefined;
170
- } | {
171
167
  src: "player";
172
168
  logic: import("xstate").PromiseActorLogic<PlayerOutput, PlayerInput, import("xstate").EventObject>;
173
169
  id: string | undefined;
@@ -175,6 +171,10 @@ export declare const prMachine: import("xstate").StateMachine<PrContext, {
175
171
  src: "script";
176
172
  logic: import("xstate").PromiseActorLogic<ScriptOutput, ScriptInput, import("xstate").EventObject>;
177
173
  id: string | undefined;
174
+ } | {
175
+ src: "playbook";
176
+ logic: import("xstate").PromiseActorLogic<PlaybookOutput, PlaybookInput, import("xstate").EventObject>;
177
+ id: string | undefined;
178
178
  }, {
179
179
  type: "playbook.acceptedOutcome";
180
180
  params: {
package/slc/gears2fsm.md CHANGED
@@ -243,15 +243,16 @@ teardown event. The completion rule of
243
243
  output clause binds only where Source declares a terminal result, which a
244
244
  controller Source does not.
245
245
  The controller decision state's direct-Captain result contract discriminates
246
- the closed action set of DR-029 as extended by DR-038. Its guard discriminants
246
+ the closed action set of DR-029 as extended by DR-038 and DR-069. Its guard discriminants
247
247
  are a stable compiler contract, not names the compiler may invent — `respond`, `resume`,
248
- `start`, `switch`, `dismiss`, `deliver`, and `runtime` — with each guard's
248
+ `start`, `switch`, `dismiss`, `deliver`, and `runtime`, plus `recover` when Source declares recovery preparation — with each guard's
249
249
  required payload fields:
250
250
 
251
251
  - `respond` requires `text`;
252
252
  - `resume` requires `playbookId` and carries no `input`;
253
253
  - `start` and `switch` each require `playbookId` and `input`;
254
254
  - `runtime` requires `actionId`;
255
+ - `recover`, when declared, requires no payload and selects host-owned preparation followed by runtime-validated continuation;
255
256
  - `dismiss` and `deliver` require none — a `deliver` result in particular
256
257
  carries no text payload: the host is authoritative for the delivered text,
257
258
  so the contract declares no field for it.
@@ -779,8 +780,7 @@ current event, not solely from the context that action will populate.
779
780
  For an omitted event value, use an existing context seed only where Source
780
781
  permits it; do not manufacture a required constructor value to satisfy a
781
782
  context-only entry guard. The linked runtime sends the entry event normally;
782
- its `entryEvent.contextField` declaration supports failure retry and does not
783
- copy fresh text into context before the guard.
783
+ entry metadata does not copy fresh text into context before the guard.
784
784
  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.
785
785
  The two surfaces shall not be collapsed. `BOSS_INTERRUPT` always carries its
786
786
  target id and may additionally carry typed Boss-supplied fields such as an
@@ -803,7 +803,7 @@ Boss-reply suspension, because its hub already receives every Boss turn and a
803
803
  clarifying question to Boss is a `respond` selection over the closed action
804
804
  set. The rule below is therefore universal over workflow states and silent
805
805
  about that class; in particular, adding `needsBossReply` to the controller
806
- decision state would add an eighth outcome to a closed seven-action contract
806
+ decision state would add an unauthorized outcome to its closed controller contract
807
807
  whose guard discriminants [Setup](#setup) fixes, and is nonconformant.
808
808
  There is no source-level opt-in annotation and no `needsBossReply` result metadata in GEARS output.
809
809
  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`.
package/slc/link.md CHANGED
@@ -65,7 +65,7 @@ node "<definition-directory>/materialize-link.mjs" --fsm "<source.fsm.ts>" --out
65
65
  "label": "EXAMPLE",
66
66
  "options": {},
67
67
  "inputMapping": {},
68
- "entryEvent": { "type": "BOSS_TASK", "textField": "bossIntent", "contextField": "bossIntent" },
68
+ "entryEvent": { "type": "BOSS_TASK", "textField": "bossIntent" },
69
69
  "bossEvents": [],
70
70
  "outcomeAuthority": {
71
71
  "governedPlayerStates": {
@@ -196,6 +196,8 @@ interface PlaybookRuntime {
196
196
  handleBossInput(turn: {
197
197
  text: string;
198
198
  signal: AbortSignal;
199
+ /** Called synchronously just before accepting the event, at most once. */
200
+ onAccepted?: () => void;
199
201
  }): Promise<PlaybookRunResult>;
200
202
  resumePlaybookCall(input: {
201
203
  callId: string;
@@ -366,6 +368,7 @@ interface PlaybookEffectBoundary {
366
368
  readonly baseline: PlaybookRepositoryObservation;
367
369
  readonly after?: PlaybookRepositoryObservation;
368
370
  readonly physicalReceipt?: PlaybookRepositoryReceipt;
371
+ readonly restored?: PlaybookRepositoryObservation;
369
372
  readonly finalText?: string;
370
373
  readonly semanticCandidate?: JsonValue;
371
374
  readonly initialSemanticCandidate?: JsonValue;
@@ -405,6 +408,7 @@ type PlaybookEffectBoundaryStart = Omit<
405
408
  | 'finalText'
406
409
  | 'semanticCandidate'
407
410
  | 'initialSemanticCandidate'
411
+ | 'restored'
408
412
  >;
409
413
 
410
414
  type PlaybookEffectLogicalOperationStart = Omit<
@@ -466,6 +470,23 @@ interface PlaybookEffectLedgerCapability {
466
470
  ): Promise<PlaybookEffectLedger>;
467
471
  }
468
472
 
473
+ interface PlaybookRecoveryCheckpoint {
474
+ readonly id?: string;
475
+ readonly stateId: string;
476
+ readonly prompt: string;
477
+ readonly machine: JsonValue;
478
+ readonly boundaryPrefix: number;
479
+ readonly result?: JsonValue;
480
+ readonly delivered?: true;
481
+ }
482
+
483
+ interface PlaybookStepRecord {
484
+ readonly id: string;
485
+ readonly kind: 'player' | 'captain' | 'script';
486
+ readonly stateId: string;
487
+ readonly result?: JsonValue;
488
+ }
489
+
469
490
  interface PlaybookRuntimeSnapshot {
470
491
  schemaVersion: 4;
471
492
  playbookId: string;
@@ -496,6 +517,8 @@ interface PlaybookRuntimeSnapshot {
496
517
  readonly boundaryPrefix: number;
497
518
  readonly attemptId: string | null;
498
519
  };
520
+ /** Interrupted invocation, captured before its external call (DR-069). */
521
+ recoveryCheckpoint?: PlaybookRecoveryCheckpoint;
499
522
  suspendedCall?: PlaybookSuspendedCall;
500
523
  }
501
524
 
@@ -580,7 +603,7 @@ interface XStatePlaybookRuntimeConstruction<
580
603
 
581
604
  The shared type-only contract module shall export `PlaybookRepositoryDisposition`, `PlaybookRepositoryObservation`, `PlaybookRepositoryReceipt`, `PlaybookEffectBoundary`, `PlaybookEffectBoundaryStart`, `PlaybookEffectLogicalOperation`, `PlaybookEffectLedger`, `PlaybookEffectLedgerCommand`, `PlaybookEffectLedgerCommandBatch`, and `PlaybookEffectLedgerCapability`; the executable `@sublang/playbook/xstate-runtime` module shall export `assertPlaybookEffectLedger`, `emptyPlaybookEffectLedger`, and `isPlaybookEffectLedgerMonotonicExtension` over those types.
582
605
  That executable module shall also export the centralized schema-3 semantic surface: `PlaybookSemanticFieldAuthority`, `PlaybookSemanticOutcomeSpec`, `PlaybookSemanticEvidenceInput`, `PlaybookReconciledSemanticOutput`, `PlaybookRetainedSemanticEvidence`, `PlaybookSemanticReconciliationReason`, `PlaybookSemanticReconciliation`, `PlaybookSemanticCandidateStructureError`, and `reconcilePlaybookSemanticEvidence`.
583
- The pure reconciler shall accept the declared state-local outcomes, an unknown semantic candidate, and optional unknown `finalText`, repository receipt, and runtime-field evidence; shall return a detached frozen `resolved` or `deferred` decision with exact output and retained evidence, or an `unresolved` decision with retained evidence and one closed reason from `missing-presentation-evidence`, `missing-repository-receipt`, `invalid-repository-receipt`, `repository-disposition-mismatch`, `missing-effect-evidence`, `missing-runtime-evidence`, and `inconsistent-runtime-evidence`; and shall reserve `PlaybookSemanticCandidateStructureError` for candidate defects eligible for the bounded correction path rather than effect-evidence disagreement.
606
+ The pure reconciler shall accept the declared state-local outcomes, an unknown semantic candidate, and optional unknown `finalText`, repository receipt, and runtime-field evidence; shall return a detached frozen `resolved` or `deferred` decision with exact output and retained evidence, or an `unresolved` decision with retained evidence and one closed reason from `no-matching-outcome`, `missing-presentation-evidence`, `missing-repository-receipt`, `invalid-repository-receipt`, `repository-disposition-mismatch`, `missing-effect-evidence`, `missing-runtime-evidence`, and `inconsistent-runtime-evidence`; and shall reserve `PlaybookSemanticCandidateStructureError` for candidate defects eligible for the bounded correction path rather than effect-evidence disagreement.
584
607
  The empty ledger shall be exactly `{ schemaVersion: 1, revision: 0, boundaries: [], logicalOperations: [] }`, and revision shall be zero if and only if both ordered ledgers are empty.
585
608
  The validator shall capture the complete supplied ledger once as detached frozen JSON and enforce every closed member, identity, ordering, receipt, cross-reference, correction-budget, and logical-operation invariant represented above.
586
609
  One optional host-owned UUID `cohortId` shall identify every member of exactly one contiguous, distinct-role, all-`unchanged` physical cohort in declared role order; every member shall share attempt, playbook, runtime-session, turn, canonical-worktree, and baseline identity and shall be uniformly started or uniformly complete, complete members shall carry the identical after observation and receipt, and the id shall never be reused by another group.
@@ -642,8 +665,8 @@ Boss text supplied by the entry event is not a required startup option unless So
642
665
  A generated entry guard that requires the text already in context is likewise
643
666
  not independent Source evidence; it is a producer defect when the entry action
644
667
  has yet to copy the event's text. Do not compensate with a required option or
645
- invented startup task. `entryEvent.contextField` supplies failure-retry text;
646
- it does not populate fresh entry context before FSM guards execute.
668
+ invented startup task. Entry metadata does not populate fresh context before
669
+ FSM guards execute. Interrupted-step checkpoints retain accepted input.
647
670
  A source-appropriate optional seed may remain, and genuine required bootstrap catalogs or other options shall not be erased or filled with invented defaults.
648
671
 
649
672
  Concrete player binding and prompt identity are host policy and shall not enter `PlaybookRuntimeOptions`, machine input, or the emitted artifact.
@@ -654,6 +677,7 @@ The host shall derive `promptIdentity` from the current effective model when pre
654
677
 
655
678
  ```typescript
656
679
  interface PlaybookPorts {
680
+ recordStep?(step: PlaybookStepRecord, position?: PlaybookRuntimeSnapshot): Promise<void>;
657
681
  callPlayer(
658
682
  roleId: string,
659
683
  prompt: string,
@@ -1283,6 +1307,13 @@ field.
1283
1307
  It shall reject an absent required field, an undeclared or extra field, a
1284
1308
  field supplied by the wrong authority, an invalid value, or any mutually
1285
1309
  inconsistent candidate before FSM delivery.
1310
+ When no outcome matches, a governed judge may instead return exactly
1311
+ `{ blocked: <nonempty explanation> }`.
1312
+ The shared reconciler retains this candidate and returns `no-matching-outcome`
1313
+ with its explanation as a `runtime-defect` cause; it authorizes neither a
1314
+ corrective judge nor an FSM outcome.
1315
+ Both shared and bespoke judge prompts shall state this alternative explicitly
1316
+ instead of forcing a match when the declared results are incomplete.
1286
1317
 
1287
1318
  For a non-deferred candidate, reconciliation shall require a complete durable
1288
1319
  physical receipt, or the complete cumulative logical receipt of a deferred
@@ -1356,7 +1387,8 @@ Two default adjudication strategies, in selection order:
1356
1387
  answer keyed to exactly one of the declared guards: a nongoverned player uses
1357
1388
  `{ guard, …structuralPayloadFields }`, while a governed schema-3 player uses
1358
1389
  `{ guard, …semanticOwnedPayloadFields }` and explicitly forbids every other
1359
- payload field. Both forms exclude the runtime-injected direct-Captain
1390
+ payload field. The governed form also permits the blocked report above when no outcome matches.
1391
+ Both forms exclude the runtime-injected direct-Captain
1360
1392
  `question` and `response` fields above. The prompt shall identify hidden control work,
1361
1393
  prohibit tool use, file inspection, and external evidence, direct the judge
1362
1394
  to decide only from the supplied actor output and declared outcomes, and
@@ -1887,7 +1919,8 @@ rehydrate it in a later process (DR-014). A runtime that implements either
1887
1919
  member shall implement both. When generated for a runtime whose host needs
1888
1920
  durability, the pair shall behave as follows.
1889
1921
 
1890
- `exportSnapshot()` shall return `undefined` unless the runtime is at a safe
1922
+ A shared runtime may also receive the internal step-start snapshot for its current invocation or `exportSnapshot({child})` for a bridge-owned child during startup; these capture stopped positions without execution.
1923
+ Ordinary `exportSnapshot()` shall return `undefined` unless the runtime is at a safe
1891
1924
  capture point: initialized, not disposing or disposed, no active
1892
1925
  `handleBossInput`/`resumePlaybookCall` boundary, and the root actor at a
1893
1926
  quiescent state with actor status `active`.
@@ -2096,6 +2129,14 @@ interface PlaybookControlAction {
2096
2129
  reason?: PlaybookControlActionReason; // why it is not `ready`
2097
2130
  }
2098
2131
 
2132
+ interface PlaybookRecoveryOffer {
2133
+ prompt: string;
2134
+ description?: string;
2135
+ preparation?: string;
2136
+ evidence?: JsonValue;
2137
+ continuation: { kind: 'reply' } | { kind: 'runtime'; actionId: string };
2138
+ }
2139
+
2099
2140
  interface PlaybookControlView {
2100
2141
  state: PlaybookState;
2101
2142
  stateDescription?: string; // runtime-published meaning of the current state
@@ -2103,12 +2144,13 @@ interface PlaybookControlView {
2103
2144
  pendingQuestions: readonly PlaybookPendingBossQuestion[];
2104
2145
  lastError?: NormalizedError;
2105
2146
  actions: readonly PlaybookControlAction[];
2147
+ recovery?: PlaybookRecoveryOffer;
2106
2148
  }
2107
2149
 
2108
2150
  type PlaybookControlReceipt =
2109
2151
  | { disposition: 'rejected'; reason: string } // before any effect
2110
2152
  | { disposition: 'executed'; run: PlaybookRunResult }
2111
- | { disposition: 'failed'; error: NormalizedError }; // effects may exist
2153
+ | { disposition: 'failed'; error: NormalizedError; run?: PlaybookRunResult }; // effects may exist
2112
2154
 
2113
2155
  // Optional PlaybookRuntime members — both or neither:
2114
2156
  describe?(): PlaybookControlView;
@@ -2129,6 +2171,7 @@ throw. The view carries the current normalized state descriptor, the state
2129
2171
  description defined below, the authored context projection defined below, the
2130
2172
  pending Boss questions with their stable ids, the last recorded error in
2131
2173
  normalized form, and the currently valid actions.
2174
+ The shared runtime owns the optional recovery context and invocation checkpoint; linked artifacts need no recovery-specific source, event, or actor.
2132
2175
 
2133
2176
  `stateDescription` is the runtime's own Boss-facing statement of what its
2134
2177
  current state means, taken from the same source state descriptions the action
@@ -2181,25 +2224,17 @@ parked-session snapshot uses (actor status `active`, quiescent, no pending
2181
2224
  nested call); anywhere else `actions` is empty while the rest of the view
2182
2225
  still describes the state.
2183
2226
  While effect-possible outcome evidence remains unresolved, the view shall omit its pending Boss questions and state description and shall replace every ordinary action with exactly `reconcile:unresolved-effect` labeled `Retry unresolved effect reconciliation` and `abandon:unresolved-effect` labeled `Abandon unresolved workflow attempt`.
2184
- Otherwise two ordinary families exist, labeled from source state descriptions:
2185
-
2186
- - **Failure-state retry** — while the singular state id is the recoverable
2187
- failure state and the live snapshot accepts the retry event sourced below,
2188
- the runtime shall advertise `retry:<EVENT_TYPE>` replaying exactly that
2189
- event. Where the emitted module's entry-event declaration names the FSM
2190
- context member the machine's entry action copies the exact Boss text into
2191
- (DR-034), the retry event is that deterministic entry event built from the
2192
- live snapshot's member — excluded when the member is absent, not a string,
2193
- or blank, and never falling back to the record. Where it names no member,
2194
- the retry event is the recorded last classified event (the event a public
2195
- Boss boundary sent that drove the run into `failed`, kept with its recorded
2196
- payload), and there is none while the runtime holds none. The member is
2197
- declared, never inferred from a context member that happens to match the
2198
- entry event's text field.
2227
+ A valid invocation checkpoint is the only source of a retry: it retries only the stopped invocation after its effect checks, using `retry:<EVENT_TYPE>` for the same entry target or `retry:step` otherwise.
2228
+ Saved-result assessment (`retry:adjudication`) never repeats the player and is not offered for an output the machine has already received (checkpoint `delivered: true`), and verified read-only restoration (`retry:restored-step`) records an exact baseline check before replay.
2229
+ These retries have standing `ready`; an unsafe checkpoint grants no replay. Saved-result assessment withholds ordinary state jumps. Reconciliation and abandonment remain available whenever effects are unresolved, even alongside an eligible assessment or restoration.
2230
+ Ordinary retry and jump actions use source state descriptions and require assessment and unresolved-effect fences to be absent:
2231
+
2232
+ - **Failure-state retry** — a valid invocation checkpoint retries that step only; a completed saved result is consumed without executing the operation again. No checkpoint means no retry. Legacy `entryEvent.contextField` metadata is ignored.
2199
2233
  - **Jump entries** — for each registered resumable state id whose
2200
2234
  explicit-state-jump event (`BOSS_INTERRUPT` with that `targetId`, optional
2201
2235
  textual fields omitted) the live snapshot accepts, guards included, the
2202
- runtime shall advertise `jump:<stateId>`.
2236
+ runtime shall advertise `jump:<stateId>`, except that a jump out of a governed
2237
+ failure must also pass the full failed-attempt effect check above.
2203
2238
 
2204
2239
  A candidate whose event requires a payload the runtime cannot source from
2205
2240
  recorded state shall be excluded from `actions` — `apply` never invents free
@@ -2209,9 +2244,8 @@ back to a target id or to the replayed event type, because a controller host
2209
2244
  names an executed or refused action by its label and never by its id, so an
2210
2245
  identifier used as a label defeats that substitution. A jump whose target
2211
2246
  publishes no description is therefore not advertised — borrowing another
2212
- state's description would name the wrong state — and a retry falls back from
2213
- its target's description to its own source state's, and is not advertised when
2214
- neither exists.
2247
+ state's description would name the wrong state — and a retry requires the
2248
+ interrupted invocation's source description.
2215
2249
 
2216
2250
  `apply({ actionId, key, signal })` shall revalidate the action against the
2217
2251
  live state and settle `{ disposition: 'rejected', reason }` with no effect
@@ -2227,7 +2261,7 @@ pair, and may execute once the action is advertised — and a key whose call
2227
2261
  threw before reaching acceptance (lifecycle misuse, invalid input, a
2228
2262
  pre-acceptance abort, a rejected start-boundary sink) likewise records
2229
2263
  nothing, so a later call with that key may execute.
2230
- Executing an ordinary retry or jump sends the validated event through the same actor drive as `handleBossInput` — state
2264
+ Executing a jump sends the validated event through the same actor drive as `handleBossInput` — state
2231
2265
  transitions, player/judge boundaries, statuses, and traces flow unchanged —
2232
2266
  and settles `executed` with the projected run result, or `failed` with the
2233
2267
  normalized error when the run settles in the failure state, aborts, or a
@@ -2260,12 +2294,18 @@ signal's own abort reason, in which case it evidences the cancellation and is
2260
2294
  dropped, not latched
2261
2295
  ([DR-036](../specs/decisions/036-coherent-abort-settlement.md)).
2262
2296
 
2263
- The recorded receipts and the recorded last classified event are
2264
- process-local: the durable runtime snapshot persists neither. A restored
2265
- runtime therefore advertises the retry of a declared entry-event source
2266
- immediately — that payload rides the persisted machine snapshot — while a
2267
- module declaring no source advertises a retry again only after its next
2268
- classified event.
2297
+ Recorded control receipts are process-local. Invocation checkpoints retain the original input and accepted output across restoration; a restored runtime offers only the currently valid step controls.
2298
+
2299
+ The optional `PlaybookPorts.recordStep(step, position?)` lets a durable host save ordinary work without understanding machine internals.
2300
+ The shared runtime records player, direct-Captain and script starts before execution and their actor results before advancing.
2301
+ The runtime owns the complete stopped position, including accepted Boss input; the host joins nested frames and atomically stores starts and results; retention changes wait for settlement.
2302
+ An invocation inside a parallel region records its start and result without a single-invocation recovery position, including when only its last region is resuming.
2303
+ Sequential invocations after the join use ordinary checkpoints.
2304
+ A custom runtime that cannot provide a position remains runnable, but a crash without a supported position returns to Captain with recorded work preserved.
2305
+ Automatic recovery requires a failed or quiescent outcome produced by an operation in the same turn; resuming or adopting an older stop starts no preparation.
2306
+ The store identifies the step that owns a saved position, so a consumed result cannot be attached to a later stopped snapshot.
2307
+ Only internal step-start capture creates a synthetic failed position; public snapshot capture returns the actual stopped state.
2308
+ Opening or reporting an interrupted run executes no work; Boss chooses the next action after checking outside effects and stopping any surviving worker.
2269
2309
 
2270
2310
  ## Abort
2271
2311
 
@@ -2374,6 +2414,10 @@ The runtime shall emit, at minimum:
2374
2414
  semantics matter to Boss — e.g., `respondToReview`, `failed`). The
2375
2415
  default is to emit on every transition and let the host filter; hosts
2376
2416
  may bind a stricter rule.
2417
+ Mark both a player-question status and its waiting marker with status data
2418
+ `{ kind: "boss-question" }`; the session Captain replaces those statuses with
2419
+ its own clear reply, using the complete pending question from the control view.
2420
+ Keep the original question in state and telemetry, and deliver Boss input unchanged.
2377
2421
  - One `emitTelemetry` per state transition under a namespaced topic
2378
2422
  (recommended `playbook.fsm.state`), with structured `from`, `to`, `event`,
2379
2423
  `previousState`, and `state` fields. Descriptors carry the JSON-safe XState
package/src/runtime.d.ts CHANGED
@@ -173,10 +173,13 @@ export type PlaybookRunResult = {
173
173
  pendingCall: PlaybookPendingCall;
174
174
  };
175
175
  export interface PlaybookPorts {
176
+ /** Acknowledge the durable start before work and its result before transition. */
177
+ recordStep?(step: PlaybookStepRecord, position?: PlaybookRuntimeSnapshot): Promise<void>;
176
178
  callPlayer(roleId: string, prompt: string, signal: AbortSignal, options: PlayerCallOptions): Promise<PlayerResult>;
177
179
  callCaptain(prompt: string, signal: AbortSignal, options: CaptainCallOptions): Promise<CaptainResult>;
178
180
  callJudge(prompt: string, signal: AbortSignal): Promise<string>;
179
181
  callPlaybook(request: PlaybookCallRequest, signal: AbortSignal): Promise<PlaybookCallStart>;
182
+ /** Mark question presentation with data.kind = "boss-question"; Captain relays it from pendingQuestions. */
180
183
  emitStatus(message: string, data?: unknown): Promise<void>;
181
184
  emitTelemetry(event: {
182
185
  topic: string;
@@ -275,6 +278,8 @@ export interface PlaybookEffectBoundary {
275
278
  readonly baseline: PlaybookRepositoryObservation;
276
279
  readonly after?: PlaybookRepositoryObservation;
277
280
  readonly physicalReceipt?: PlaybookRepositoryReceipt;
281
+ /** A later exact restoration permits replay of this standalone read-only call. */
282
+ readonly restored?: PlaybookRepositoryObservation;
278
283
  readonly finalText?: string;
279
284
  readonly semanticCandidate?: JsonValue;
280
285
  readonly initialSemanticCandidate?: JsonValue;
@@ -286,7 +291,7 @@ export interface PlaybookEffectBoundary {
286
291
  readonly logicalOperationId?: string;
287
292
  }
288
293
  /** One physical boundary before the host assigns attempt and sequence data. */
289
- export type PlaybookEffectBoundaryStart = Omit<PlaybookEffectBoundary, 'sequence' | 'attemptId' | 'attemptNumber' | 'after' | 'physicalReceipt' | 'finalText' | 'semanticCandidate' | 'initialSemanticCandidate'>;
294
+ export type PlaybookEffectBoundaryStart = Omit<PlaybookEffectBoundary, 'sequence' | 'attemptId' | 'attemptNumber' | 'after' | 'physicalReceipt' | 'restored' | 'finalText' | 'semanticCandidate' | 'initialSemanticCandidate'>;
290
295
  /** One deferred logical operation spanning its ordered physical boundaries. */
291
296
  export interface PlaybookEffectLogicalOperation {
292
297
  readonly sequence: number;
@@ -388,8 +393,26 @@ export interface PlaybookRuntimeSnapshot {
388
393
  readonly boundaryPrefix: number;
389
394
  readonly attemptId: string | null;
390
395
  };
396
+ /** Interrupted invocation, captured before its external call (DR-069). */
397
+ recoveryCheckpoint?: PlaybookRecoveryCheckpoint;
391
398
  suspendedCall?: PlaybookSuspendedCall;
392
399
  }
400
+ export interface PlaybookRecoveryCheckpoint {
401
+ readonly id?: string;
402
+ readonly stateId: string;
403
+ readonly prompt: string;
404
+ readonly machine: JsonValue;
405
+ readonly boundaryPrefix: number;
406
+ /** Completed actor output supplied by the execution journal on restore. */
407
+ readonly result?: JsonValue;
408
+ readonly delivered?: true;
409
+ }
410
+ export interface PlaybookStepRecord {
411
+ readonly id: string;
412
+ readonly kind: 'player' | 'captain' | 'script';
413
+ readonly stateId: string;
414
+ readonly result?: JsonValue;
415
+ }
393
416
  export type PlaybookControlStanding = 'ready' | 'no-op' | 'blocked';
394
417
  export type PlaybookControlActionReason = 'receipt-complete';
395
418
  export interface PlaybookControlAction {
@@ -398,6 +421,20 @@ export interface PlaybookControlAction {
398
421
  standing?: PlaybookControlStanding;
399
422
  reason?: PlaybookControlActionReason;
400
423
  }
424
+ export interface PlaybookRecoveryOffer {
425
+ prompt: string;
426
+ description?: string;
427
+ /** Runtime-owned conditions to satisfy; these never authorize task completion. */
428
+ preparation?: string;
429
+ /** Saved observations and result choices for diagnosing the stopped step. */
430
+ evidence?: JsonValue;
431
+ continuation: {
432
+ kind: 'reply';
433
+ } | {
434
+ kind: 'runtime';
435
+ actionId: string;
436
+ };
437
+ }
401
438
  export interface PlaybookControlView {
402
439
  state: PlaybookState;
403
440
  stateDescription?: string;
@@ -405,6 +442,8 @@ export interface PlaybookControlView {
405
442
  pendingQuestions: readonly PlaybookPendingBossQuestion[];
406
443
  lastError?: NormalizedError;
407
444
  actions: readonly PlaybookControlAction[];
445
+ /** Context only for an explicitly authorized Captain preparation call. */
446
+ recovery?: PlaybookRecoveryOffer;
408
447
  }
409
448
  export type PlaybookControlReceipt = {
410
449
  disposition: 'rejected';
@@ -415,13 +454,16 @@ export type PlaybookControlReceipt = {
415
454
  } | {
416
455
  disposition: 'failed';
417
456
  error: NormalizedError;
457
+ run?: PlaybookRunResult;
418
458
  };
419
459
  export interface PlaybookRetainedGenerationMetadata {
420
460
  readonly unfinishedFinalStateIds: readonly string[];
421
461
  }
422
462
  export interface PlaybookRuntime {
423
463
  init(session: PlaybookSession): Promise<void>;
424
- exportSnapshot?(): PlaybookRuntimeSnapshot | undefined;
464
+ exportSnapshot?(checkpoint?: {
465
+ child?: PlaybookPendingCall;
466
+ }): PlaybookRuntimeSnapshot | undefined;
425
467
  restore?(session: PlaybookSession, snapshot: PlaybookRuntimeSnapshot): Promise<void>;
426
468
  adopt?(session: PlaybookSession, snapshot: PlaybookRuntimeSnapshot, context: PlaybookAdoptionContext): Promise<void>;
427
469
  readonly retainedGenerationMetadata?: PlaybookRetainedGenerationMetadata;
@@ -447,6 +489,8 @@ export interface PlaybookRuntime {
447
489
  handleBossInput(turn: {
448
490
  text: string;
449
491
  signal: AbortSignal;
492
+ /** Called synchronously just before accepting the event, at most once. */
493
+ onAccepted?: () => void;
450
494
  }): Promise<PlaybookRunResult>;
451
495
  resumePlaybookCall(input: {
452
496
  callId: string;