@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
@@ -2551,24 +2551,21 @@ async function createLease({
2551
2551
  now,
2552
2552
  }) {
2553
2553
  let released = false;
2554
- let operationActive = false;
2554
+ let operationTail = Promise.resolve();
2555
2555
  let indeterminateEffectLedgerWrite;
2556
-
2557
- const requireActive = () => {
2558
- if (released) throw new Error('Captain session lease was already released');
2559
- if (operationActive) {
2560
- throw new Error('Captain session lease operation is already in progress');
2561
- }
2562
- };
2563
-
2564
- const runExclusive = async (operation) => {
2565
- requireActive();
2566
- operationActive = true;
2567
- try {
2568
- return await operation();
2569
- } finally {
2570
- operationActive = false;
2571
- }
2556
+ let releasing = false;
2557
+ const ownerChecks = new Set();
2558
+
2559
+ // All lifecycle, step and repository writes share one order, including
2560
+ // calls from parallel players. A failed write does not poison the queue.
2561
+ const runExclusive = (operation) => {
2562
+ if (releasing) return Promise.reject(new Error('Captain session lease is releasing'));
2563
+ const run = operationTail.then(() => {
2564
+ if (released) throw new Error('Captain session lease was already released');
2565
+ return operation();
2566
+ });
2567
+ operationTail = run.then(() => undefined, () => undefined);
2568
+ return run;
2572
2569
  };
2573
2570
 
2574
2571
  const assertOwnerUnchecked = async () => {
@@ -2580,7 +2577,14 @@ async function createLease({
2580
2577
  return current;
2581
2578
  };
2582
2579
 
2583
- const assertOwner = () => runExclusive(assertOwnerUnchecked);
2580
+ const assertOwner = () => {
2581
+ if (releasing) return Promise.reject(new Error('Captain session lease is releasing'));
2582
+ // This reads only the lease token. Parallel players must not compete
2583
+ // for the mutation guard, but release must still join admitted reads.
2584
+ const check = assertOwnerUnchecked();
2585
+ ownerChecks.add(check);
2586
+ return check.finally(() => ownerChecks.delete(check));
2587
+ };
2584
2588
 
2585
2589
  const replayWriter = await createLeaseReplayWriter({
2586
2590
  sessionsDir,
@@ -3113,6 +3117,7 @@ async function createLease({
3113
3117
  ...settledAbandonmentMember(prior),
3114
3118
  uncertain: {
3115
3119
  baseUpdatedAt: prior.uncertain.baseUpdatedAt,
3120
+ ...(prior.uncertain.progress === undefined ? {} : { progress: prior.uncertain.progress }),
3116
3121
  input: prior.uncertain.input,
3117
3122
  attemptId: nextAttemptId,
3118
3123
  attemptNumber: prior.uncertain.attemptNumber + 1,
@@ -3607,6 +3612,49 @@ async function createLease({
3607
3612
  return record.effectLedger;
3608
3613
  });
3609
3614
 
3615
+ const recordProgress = (value) => runExclusive(async () => {
3616
+ const change = requireRecord(snapshotJsonValue(value, 'Captain progress change'), 'Captain progress change');
3617
+ exactOptionalKeys(change, [], ['snapshot', 'step'], 'Captain progress change');
3618
+ await assertOwnerUnchecked();
3619
+ const prior = await readRecord(sessionId, { missing: 'undefined' });
3620
+ if (prior?.state !== 'uncertain' || prior.uncertain.abandonment) {
3621
+ throw new Error('Saving progress requires an active uncertain turn');
3622
+ }
3623
+ const progress = structuredClone(prior.uncertain.progress ?? { snapshot: null, steps: [], positionStepId: null });
3624
+ const newStepStart = change.step && !Object.hasOwn(change.step, 'result') && !progress.steps.some(({ id }) => id === change.step.id);
3625
+ if (Object.hasOwn(change, 'snapshot')) {
3626
+ if (change.snapshot !== null && !isDeepStrictEqual(change.snapshot.effectLedger, prior.effectLedger)) {
3627
+ throw new Error('Saved progress must contain the current effect ledger');
3628
+ }
3629
+ progress.snapshot = change.snapshot;
3630
+ progress.positionStepId = change.snapshot !== null && newStepStart ? change.step.id : null;
3631
+ }
3632
+ if (change.step !== undefined) {
3633
+ const step = snapshotJsonValue(change.step, 'Captain step');
3634
+ const index = progress.steps.findIndex(({ id }) => id === step.id);
3635
+ if (index < 0) {
3636
+ if (!Object.hasOwn(change, 'snapshot')) throw new Error('A step start requires its position or explicit null');
3637
+ if (Object.hasOwn(step, 'result') && !['completion', 'answer'].includes(step.kind)) throw new Error('A step result requires its saved start');
3638
+ progress.steps.push(step);
3639
+ } else {
3640
+ const { result, ...start } = step;
3641
+ const saved = progress.steps[index];
3642
+ const { result: priorResult, ...priorStart } = saved;
3643
+ if (!isDeepStrictEqual(start, priorStart) ||
3644
+ (priorResult !== undefined && !isDeepStrictEqual(priorResult, result))) {
3645
+ throw new Error('A saved step cannot be replaced');
3646
+ }
3647
+ progress.steps[index] = step;
3648
+ }
3649
+ }
3650
+ const record = validateCaptainSessionRecord(projectRecovery({
3651
+ ...prior, uncertain: { ...prior.uncertain, progress },
3652
+ }));
3653
+ await assertOwnerUnchecked();
3654
+ await writeRecord(record, { noReplace: false });
3655
+ await assertOwnerUnchecked();
3656
+ });
3657
+
3610
3658
  const discard = ({ attemptId } = {}) =>
3611
3659
  runExclusive(async () => {
3612
3660
  assertUuid(attemptId, 'Captain session attempt id');
@@ -3620,10 +3668,8 @@ async function createLease({
3620
3668
  'Captain session unresolved-effect abandonment must recover before discard',
3621
3669
  );
3622
3670
  }
3623
- if (!isDeepStrictEqual(prior.effectLedger, prior.snapshot.effectLedger)) {
3624
- throw new Error(
3625
- 'Captain session uncertain effect ledger differs from its pre-turn checkpoint and cannot be discarded',
3626
- );
3671
+ if (!isUncertainTurnDiscardable(prior)) {
3672
+ throw new Error('Recorded work or changed repository evidence must be restored and reported; the turn cannot be discarded');
3627
3673
  }
3628
3674
  await assertOwnerUnchecked();
3629
3675
  if (prior.uncertain.baseUpdatedAt === null) {
@@ -3655,19 +3701,27 @@ async function createLease({
3655
3701
  });
3656
3702
 
3657
3703
  const release = () => {
3704
+ if (released || releasing) return Promise.reject(new Error(`Captain session lease is ${released ? 'released' : 'releasing'}`));
3658
3705
  replayWriter.closeAppendAdmission();
3659
- return runExclusive(async () => {
3660
- await replayWriter.prepareRelease();
3661
- if (replayWriter.status().incomplete && previousManifest?.replay?.incomplete !== true) {
3662
- const record = await readRecord(sessionId, { missing: 'undefined' });
3663
- if (record !== undefined) await writeRecord(record, { noReplace: false });
3706
+ const pending = runExclusive(async () => {
3707
+ try {
3708
+ await Promise.allSettled([...ownerChecks]);
3709
+ await replayWriter.prepareRelease();
3710
+ if (replayWriter.status().incomplete && previousManifest?.replay?.incomplete !== true) {
3711
+ const record = await readRecord(sessionId, { missing: 'undefined' });
3712
+ if (record !== undefined) await writeRecord(record, { noReplace: false });
3713
+ }
3714
+ const current = await assertOwnerUnchecked();
3715
+ await retireObservedLease(sessionId, current);
3716
+ released = true;
3717
+ portableWriters.delete(sessionId);
3718
+ return replayWriter.status();
3719
+ } finally {
3720
+ releasing = false;
3664
3721
  }
3665
- const current = await assertOwnerUnchecked();
3666
- await retireObservedLease(sessionId, current);
3667
- released = true;
3668
- portableWriters.delete(sessionId);
3669
- return replayWriter.status();
3670
3722
  });
3723
+ releasing = true;
3724
+ return pending;
3671
3725
  };
3672
3726
 
3673
3727
  return Object.freeze({
@@ -3691,6 +3745,7 @@ async function createLease({
3691
3745
  completeUnresolvedEffectAbandonment,
3692
3746
  recoverUnresolvedEffectAbandonment,
3693
3747
  writeEffectLedger,
3748
+ recordProgress,
3694
3749
  settle,
3695
3750
  discard,
3696
3751
  assertOwner,
@@ -4034,7 +4089,7 @@ function validateCanonicalCaptainSessionRecord(
4034
4089
  exactOptionalKeys(
4035
4090
  uncertain,
4036
4091
  UNCERTAIN_KEYS,
4037
- ['abandonment'],
4092
+ ['abandonment', 'progress'],
4038
4093
  'Captain session record uncertain',
4039
4094
  );
4040
4095
  if (uncertain.baseUpdatedAt !== null) {
@@ -4112,6 +4167,65 @@ function validateCanonicalCaptainSessionRecord(
4112
4167
  uncertain.attemptedExecutionProjection,
4113
4168
  artifactSchemas,
4114
4169
  );
4170
+ if (uncertain.progress !== undefined) {
4171
+ const progress = requireRecord(uncertain.progress, 'Captain progress');
4172
+ exactOptionalKeys(progress, ['snapshot', 'steps'], ['positionStepId'], 'Captain progress');
4173
+ if (!Array.isArray(progress.steps)) throw new Error('Captain steps must be an array');
4174
+ const ids = new Set();
4175
+ for (const step of progress.steps) {
4176
+ exactOptionalKeys(requireRecord(step, 'Captain step'), ['id', 'kind', 'stateId', 'runtimeSessionId', 'playbookId'], ['result'], 'Captain step');
4177
+ assertUuid(step.id, 'Captain step id');
4178
+ assertUuid(step.runtimeSessionId, 'Captain step runtime');
4179
+ if (ids.has(step.id)) throw new Error('Captain step ids must be unique');
4180
+ ids.add(step.id);
4181
+ if (!['player', 'captain', 'script', 'preparation', 'completion', 'answer'].includes(step.kind)) throw new Error('Unknown Captain step kind');
4182
+ if (['completion', 'answer'].includes(step.kind) && (!step.result || typeof step.result !== 'object' || Array.isArray(step.result))) throw new Error('A recorded completion or answer requires its result');
4183
+ if (step.kind === 'completion') {
4184
+ exactOptionalKeys(step.result, ['state'], ['description', 'terminalOutcome', 'retention'], 'Completed root result');
4185
+ if (step.result.retention !== undefined && !['clear', 'keep'].includes(step.result.retention)) throw new Error('Completed root retention must be clear or keep');
4186
+ if (step.result.state?.status !== 'done' || (step.result.state.stateId !== undefined && step.result.state.stateId !== step.stateId)) throw new Error('Completed root result must name its final state');
4187
+ if (step.result.description !== undefined) requireNonblank(step.result.description, 'Completed root description');
4188
+ if (step.result.terminalOutcome !== undefined) {
4189
+ const terminal = step.result.terminalOutcome;
4190
+ exactOptionalKeys(terminal, ['stateId', 'kind'], ['description'], 'Completed root outcome');
4191
+ if (terminal.stateId !== step.stateId || !['success', 'failure'].includes(terminal.kind)) throw new Error('Completed root outcome must name its final state and kind');
4192
+ if (terminal.description !== undefined) requireNonblank(terminal.description, 'Completed root outcome description');
4193
+ }
4194
+ }
4195
+ if (step.kind === 'answer') {
4196
+ exactOptionalKeys(step.result, ['questions', 'instruction'], [], 'Selected answer');
4197
+ requireNonblank(step.result.instruction, 'Selected answer instruction');
4198
+ if (!Array.isArray(step.result.questions) || !step.result.questions.length) throw new Error('Selected answer requires questions');
4199
+ for (const question of step.result.questions) {
4200
+ exactOptionalKeys(question, ['asker', 'question'], [], 'Selected answer question');
4201
+ if (question.asker?.kind === 'captain') exactOptionalKeys(question.asker, ['kind'], [], 'Selected answer asker');
4202
+ else {
4203
+ exactOptionalKeys(question.asker, ['kind', 'roleId'], [], 'Selected answer asker');
4204
+ if (question.asker.kind !== 'role') throw new Error('Unknown selected answer asker');
4205
+ requireCanonicalNonblank(question.asker.roleId, 'Selected answer role');
4206
+ }
4207
+ requireNonblank(question.question, 'Selected answer question');
4208
+ }
4209
+ }
4210
+ requireCanonicalNonblank(step.stateId, 'Captain step state');
4211
+ if (!Object.hasOwn(structural.catalog, step.playbookId)) throw new Error('Captain step names an unknown playbook');
4212
+ }
4213
+ if (progress.positionStepId !== undefined && progress.positionStepId !== null && !ids.has(progress.positionStepId)) throw new Error('Captain progress positionStepId must name a recorded step');
4214
+ if (progress.snapshot === null && progress.positionStepId != null) throw new Error('Captain progress positionStepId requires a saved position');
4215
+ if (progress.snapshot !== null) {
4216
+ const prepared = assertPlaybookCaptainShellSnapshot(progress.snapshot);
4217
+ assertSnapshotMatchesStructure(prepared, structural, artifactSchemas);
4218
+ if ((Object.hasOwn(progress.snapshot, 'presentedEffectPrefix') && prepared.presentedEffectPrefix !== snapshot.presentedEffectPrefix) ||
4219
+ !isDeepStrictEqual(prepared.captain, snapshot.captain) ||
4220
+ !isDeepStrictEqual(prepared.journal, snapshot.journal) ||
4221
+ !isDeepStrictEqual(prepared.sequences, snapshot.sequences) ||
4222
+ !isPlaybookEffectLedgerMonotonicExtension(snapshot.effectLedger, prepared.effectLedger) ||
4223
+ !isPlaybookEffectLedgerMonotonicExtension(prepared.effectLedger, effectLedger)) {
4224
+ throw new Error('Saved progress must retain the settled controller and current work');
4225
+ }
4226
+
4227
+ }
4228
+ }
4115
4229
  if (uncertain.baseUpdatedAt === null) {
4116
4230
  if (!isDeepStrictEqual(lastApplied, attempted)) {
4117
4231
  throw new Error(
@@ -4436,6 +4550,7 @@ const EFFECT_BOUNDARY_IDENTITY_KEYS = [
4436
4550
  const EFFECT_BOUNDARY_OPTIONAL_KEYS = [
4437
4551
  'after',
4438
4552
  'physicalReceipt',
4553
+ 'restored',
4439
4554
  'finalText',
4440
4555
  'semanticCandidate',
4441
4556
  'initialSemanticCandidate',
@@ -5018,6 +5133,7 @@ function assertMonotonicBoundaryReplacement(expected, next, path) {
5018
5133
  for (const key of [
5019
5134
  'after',
5020
5135
  'physicalReceipt',
5136
+ 'restored',
5021
5137
  'finalText',
5022
5138
  'initialSemanticCandidate',
5023
5139
  ]) {
@@ -6001,9 +6117,13 @@ function validateRetainedGenerationFrame(
6001
6117
  'roleBindings',
6002
6118
  'runtime',
6003
6119
  ],
6004
- ['parentSessionId', 'parentCallId'],
6120
+ ['parentSessionId', 'parentCallId', 'request', 'inputs'],
6005
6121
  path,
6006
6122
  );
6123
+ if (frame.request !== undefined) {
6124
+ if (index !== 0) throw new Error(`${path}.request belongs only to the root`);
6125
+ requireNonblank(frame.request, `${path}.request`);
6126
+ }
6007
6127
  const playbookId = requireCanonicalNonblank(
6008
6128
  frame.playbookId,
6009
6129
  `${path}.playbookId`,
@@ -6014,6 +6134,7 @@ function validateRetainedGenerationFrame(
6014
6134
  `${path}.playbookId names unknown stored playbook ${JSON.stringify(playbookId)}`,
6015
6135
  );
6016
6136
  }
6137
+ if (frame.inputs !== undefined && (index !== 0 || !Array.isArray(frame.inputs) || frame.inputs.some((input) => typeof input !== 'string' || !input.trim()))) throw new Error(`${path}.inputs belongs only to the root and must contain nonempty strings`);
6017
6138
  assertUuid(frame.sessionId, `${path}.sessionId`);
6018
6139
  assertUuid(frame.rootSessionId, `${path}.rootSessionId`);
6019
6140
  if (sessionIds.has(frame.sessionId)) {
@@ -6574,3 +6695,10 @@ async function writePrivateBytes(path, bytes, fs) {
6574
6695
  try { await fs.unlink(temporary); } catch (cause) { if (cause?.code !== 'ENOENT') throw cause; }
6575
6696
  }
6576
6697
  }
6698
+
6699
+ /** One shared decision for discard controls and durable rollback. */
6700
+ export function isUncertainTurnDiscardable(record) {
6701
+ return record?.state === 'uncertain' && !record.uncertain.abandonment &&
6702
+ (record.uncertain.progress?.steps.length ?? 0) === 0 &&
6703
+ isDeepStrictEqual(record.effectLedger, record.snapshot.effectLedger);
6704
+ }
@@ -179,15 +179,15 @@ export declare const codeMachine: import("xstate").StateMachine<CodeContext, {
179
179
  readonly answer: string;
180
180
  readonly questionId?: ResumableStateId;
181
181
  }, {
182
- [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;
182
+ [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;
183
183
  }, {
184
- src: "playbook";
185
- logic: import("xstate").PromiseActorLogic<PlaybookOutput, PlaybookInput, import("xstate").EventObject>;
186
- id: string | undefined;
187
- } | {
188
184
  src: "player";
189
185
  logic: import("xstate").PromiseActorLogic<PlayerOutput, PlayerInput, import("xstate").EventObject>;
190
186
  id: string | undefined;
187
+ } | {
188
+ src: "playbook";
189
+ logic: import("xstate").PromiseActorLogic<PlaybookOutput, PlaybookInput, import("xstate").EventObject>;
190
+ id: string | undefined;
191
191
  }, {
192
192
  type: "playbook.acceptedOutcome";
193
193
  params: {
@@ -86,6 +86,7 @@ export interface PlaybookEffectBoundary {
86
86
  readonly baseline: PlaybookRepositoryObservation;
87
87
  readonly after?: PlaybookRepositoryObservation;
88
88
  readonly physicalReceipt?: PlaybookRepositoryReceipt;
89
+ readonly restored?: PlaybookRepositoryObservation;
89
90
  readonly finalText?: string;
90
91
  readonly semanticCandidate?: JsonValue;
91
92
  readonly initialSemanticCandidate?: JsonValue;
@@ -103,6 +104,7 @@ export type PlaybookEffectBoundaryStart = Omit<
103
104
  | 'attemptNumber'
104
105
  | 'after'
105
106
  | 'physicalReceipt'
107
+ | 'restored'
106
108
  | 'finalText'
107
109
  | 'semanticCandidate'
108
110
  | 'initialSemanticCandidate'
@@ -278,6 +280,7 @@ export interface HostCapabilities {
278
280
  readonly effectLedger: PlaybookEffectLedgerCapability;
279
281
  }
280
282
  export interface WorktreeRepositoryCapability extends RepositoryCapability {
283
+ acquire(options?: { signal?: AbortSignal }): Promise<{ assertOwner(): Promise<void>; release(): Promise<void> }>;
281
284
  readonly identity: RepositoryIdentity;
282
285
  observe(): Promise<PlaybookRepositoryObservation>;
283
286
  }
@@ -1,6 +1,6 @@
1
1
  import { type Captain, type CaptainSession, type TuningSelection } from '@sublang/cligent/tmux-play';
2
2
  import type { Effort, PermissionPolicy } from '@sublang/cligent';
3
- import type { JsonValue, PlaybookEffectLedger, PlaybookEffectLedgerCommandBatch, PlaybookControlAction, PlaybookFailureCause, PlaybookRuntime, PlaybookRuntimeSnapshot } from '@sublang/playbook/runtime';
3
+ import type { JsonValue, PlaybookEffectLedger, PlaybookEffectLedgerCommandBatch, PlaybookControlAction, PlaybookFailureCause, PlaybookRuntime, PlaybookRuntimeSnapshot, PlaybookStepRecord } from '@sublang/playbook/runtime';
4
4
  import { type CaptainControllerPort } from '../captain.playbook/captain.playbook.js';
5
5
  import type { PlaybookSummaryPolicy } from './code.registry.js';
6
6
  interface SessionAgent {
@@ -34,7 +34,24 @@ interface PlaybookCaptainUnresolvedEffectSettlementInput {
34
34
  }
35
35
  type SnapshotAgentEnvelope = DeepReadonly<Omit<SessionAgent, 'model' | 'effort' | 'fastMode'>>;
36
36
  type PlayerLedgerSnapshotEntry = DeepReadonly<PlayerLedgerEntry>;
37
+ export interface ProgressChange {
38
+ snapshot?: PlaybookCaptainShellSnapshot | null;
39
+ step?: Omit<PlaybookStepRecord, 'kind'> & {
40
+ kind: PlaybookStepRecord['kind'] | 'preparation' | 'completion' | 'answer';
41
+ runtimeSessionId: string;
42
+ playbookId: string;
43
+ };
44
+ }
45
+ export interface InterruptedReport {
46
+ text: string;
47
+ effects: readonly PlaybookCaptainUnresolvedEffect[];
48
+ retentionUpdates?: readonly PlaybookCaptainRetentionUpdate[];
49
+ unresolvedEffects?: readonly PlaybookCaptainUnresolvedEffect[];
50
+ }
37
51
  export interface PlaybookCaptainDeps {
52
+ /** Stop the host's active turn, including admitted tool calls, on preparation expiry or a required save failure. */
53
+ abortPreparation?: (reason?: string) => void;
54
+ recordProgress?: (change: ProgressChange) => Promise<void>;
38
55
  continuity?: {
39
56
  beforeCall(participantId: string): Promise<void>;
40
57
  acknowledged(participantId: string, token: string): void;
@@ -137,6 +154,8 @@ export interface PlaybookCaptainFrameSnapshot {
137
154
  readonly depth: number;
138
155
  readonly parentSessionId?: string;
139
156
  readonly parentCallId?: string;
157
+ readonly request?: string;
158
+ readonly inputs?: readonly string[];
140
159
  readonly options: JsonValue;
141
160
  readonly roleBindings: Readonly<Record<string, string>>;
142
161
  readonly runtime: DeepReadonly<PlaybookRuntimeSnapshot>;
@@ -144,6 +163,7 @@ export interface PlaybookCaptainFrameSnapshot {
144
163
  interface PlaybookCaptainShellSnapshotFields {
145
164
  readonly schemaVersion: 4;
146
165
  readonly effectLedger: DeepReadonly<PlaybookEffectLedger>;
166
+ readonly presentedEffectPrefix?: number;
147
167
  readonly captain: {
148
168
  readonly sessionId: string;
149
169
  readonly runtime: DeepReadonly<PlaybookRuntimeSnapshot>;
@@ -158,7 +178,7 @@ interface PlaybookCaptainShellSnapshotFields {
158
178
  readonly journal: number;
159
179
  };
160
180
  readonly journal: readonly PlaybookCaptainJournalRecord[];
161
- readonly lastAction?: 'respond' | 'start' | 'switch' | 'resume' | 'dismiss' | 'deliver' | 'runtime';
181
+ readonly lastAction?: 'respond' | 'start' | 'switch' | 'resume' | 'dismiss' | 'deliver' | 'runtime' | 'recover';
162
182
  readonly lastSettlementStatus?: 'ok' | 'rejected' | 'failed';
163
183
  }
164
184
  /**
@@ -242,6 +262,8 @@ export interface PlaybookCaptainShell extends Captain {
242
262
  * `handleBossTurn` (CAPTAIN-7).
243
263
  */
244
264
  submitShellAction?(actionId: string): string;
265
+ /** Report an interrupted attempt without dispatching work. */
266
+ selectInterruptedReport?(input: string, report: InterruptedReport): void;
245
267
  }
246
268
  /**
247
269
  * Whether words may stand outside a code span in a statement the shell says
@@ -282,6 +304,14 @@ declare function bossReport(statements: readonly (string | BossStatement)[], rea
282
304
  export declare function assertPlaybookCaptainUnresolvedEffects(value: unknown): readonly PlaybookCaptainUnresolvedEffect[];
283
305
  /** Validate, detach, and freeze one untrusted shell snapshot. */
284
306
  export declare function assertPlaybookCaptainShellSnapshot(value: unknown): PlaybookCaptainShellSnapshot;
307
+ export type PlaybookCaptainUnresolvedEffectReference = {
308
+ readonly kind: 'boundary';
309
+ readonly boundaryId: string;
310
+ } | {
311
+ readonly kind: 'logical-operation';
312
+ readonly operationId: string;
313
+ };
314
+ export declare const projectUnresolvedEffects: (ledger: PlaybookEffectLedger, references: readonly PlaybookCaptainUnresolvedEffectReference[]) => readonly PlaybookCaptainUnresolvedEffect[];
285
315
  export declare function createPlaybookCaptainShell(options: unknown, deps?: PlaybookCaptainDeps): PlaybookCaptainShell;
286
316
  export declare const _internal: {
287
317
  failureStatement: typeof failureStatement;