@sublang/playbook 10.0.0 → 12.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 (59) hide show
  1. package/README.md +1 -1
  2. package/docs/cli.md +67 -19
  3. package/docs/configuration.md +103 -40
  4. package/docs/embedding.md +88 -0
  5. package/package.json +30 -4
  6. package/reference/sdlc/code.md +35 -15
  7. package/reference/sdlc/code.playbook/bin/interactive-session.js +58 -6
  8. package/reference/sdlc/code.playbook/bin/launch-config.js +499 -241
  9. package/reference/sdlc/code.playbook/bin/playbook.js +236 -187
  10. package/reference/sdlc/code.playbook/bin/replay-observer.js +221 -0
  11. package/reference/sdlc/code.playbook/bin/run.js +355 -203
  12. package/reference/sdlc/code.playbook/bin/session-store.js +1512 -136
  13. package/reference/sdlc/code.playbook/code.fsm.d.ts +22 -19
  14. package/reference/sdlc/code.playbook/code.fsm.js +116 -52
  15. package/reference/sdlc/code.playbook/code.fsm.ts +149 -64
  16. package/reference/sdlc/code.playbook/code.gears.md +40 -20
  17. package/reference/sdlc/code.playbook/code.playbook.js +23 -2
  18. package/reference/sdlc/code.playbook/code.playbook.ts +23 -2
  19. package/reference/sdlc/code.playbook/playbook-captain.d.ts +4 -1
  20. package/reference/sdlc/code.playbook/playbook-captain.js +21 -3
  21. package/reference/sdlc/code.playbook/playbook-captain.ts +42 -6
  22. package/reference/sdlc/code.playbook/playbook.config.template.yaml +31 -11
  23. package/reference/sdlc/code.playbook/session-store.d.ts +82 -0
  24. package/reference/sdlc/code.playbook/session-store.js +113 -0
  25. package/reference/sdlc/decide.md +24 -15
  26. package/reference/sdlc/decide.playbook/decide.fsm.d.ts +13 -6
  27. package/reference/sdlc/decide.playbook/decide.fsm.js +54 -27
  28. package/reference/sdlc/decide.playbook/decide.fsm.ts +68 -29
  29. package/reference/sdlc/decide.playbook/decide.gears.md +25 -19
  30. package/reference/sdlc/decide.playbook/decide.playbook.js +11 -3
  31. package/reference/sdlc/decide.playbook/decide.playbook.ts +11 -3
  32. package/reference/sdlc/decide.playbook/decide.registry.js +1 -1
  33. package/reference/sdlc/decide.playbook/decide.registry.ts +1 -1
  34. package/reference/sdlc/dev.md +52 -0
  35. package/reference/sdlc/dev.playbook/dev.fsm.d.ts +261 -0
  36. package/reference/sdlc/dev.playbook/dev.fsm.js +723 -0
  37. package/reference/sdlc/dev.playbook/dev.fsm.ts +988 -0
  38. package/reference/sdlc/dev.playbook/dev.gears.md +91 -0
  39. package/reference/sdlc/dev.playbook/dev.playbook.d.ts +21 -0
  40. package/reference/sdlc/dev.playbook/dev.playbook.js +143 -0
  41. package/reference/sdlc/dev.playbook/dev.playbook.ts +246 -0
  42. package/reference/sdlc/dev.playbook/dev.registry.d.ts +40 -0
  43. package/reference/sdlc/dev.playbook/dev.registry.js +64 -0
  44. package/reference/sdlc/dev.playbook/dev.registry.ts +120 -0
  45. package/reference/sdlc/review.md +36 -18
  46. package/reference/sdlc/review.playbook/review.fsm.d.ts +15 -2
  47. package/reference/sdlc/review.playbook/review.fsm.js +77 -27
  48. package/reference/sdlc/review.playbook/review.fsm.ts +96 -30
  49. package/reference/sdlc/review.playbook/review.gears.md +52 -26
  50. package/reference/sdlc/review.playbook/review.playbook.js +17 -7
  51. package/reference/sdlc/review.playbook/review.playbook.ts +17 -7
  52. package/reference/sdlc/review.playbook/review.registry.js +1 -1
  53. package/reference/sdlc/review.playbook/review.registry.ts +1 -1
  54. package/slc/link.md +12 -5
  55. package/slc/text2gears.md +3 -0
  56. package/src/xstate-playbook-runtime.js +5 -2
  57. package/src/xstate-playbook-runtime.ts +5 -2
  58. package/src/xstate-runtime.js +13 -1
  59. package/src/xstate-runtime.ts +13 -1
@@ -6,65 +6,75 @@
6
6
  // in tmux. The core below uses cligent's ordinary tmux-play runtime without a
7
7
  // presenter; it does not construct a registry runtime or PlaybookPorts itself.
8
8
 
9
- import { randomUUID } from 'node:crypto';
10
- import { homedir } from 'node:os';
11
- import { resolve } from 'node:path';
12
- import { isDeepStrictEqual } from 'node:util';
13
- import { createTmuxPlayRuntime } from '@sublang/cligent/tmux-play';
9
+ import { randomUUID } from "node:crypto";
10
+ import { homedir } from "node:os";
11
+ import { resolve } from "node:path";
12
+ import { isDeepStrictEqual } from "node:util";
13
+ import { assertFastModeSupported } from "@sublang/cligent";
14
+ import { createTmuxPlayRuntime } from "@sublang/cligent/tmux-play";
14
15
  import {
15
16
  assertPlaybookEffectLedger,
16
17
  emptyPlaybookEffectLedger,
17
- } from '../../../../src/xstate-runtime.js';
18
+ } from "../../../../src/xstate-runtime.js";
18
19
  import {
19
20
  assertPlaybookCaptainShellSnapshot,
20
21
  createPlaybookCaptainShell,
21
- } from '../playbook-captain.js';
22
+ } from "../playbook-captain.js";
22
23
  import {
23
24
  adapterSdkFailureLines,
24
25
  checkAdapterSdks,
25
26
  mappedSdksFor,
26
27
  probeAdapterSdk,
27
- } from './adapter-sdk.js';
28
+ } from "./adapter-sdk.js";
28
29
  import {
29
30
  checkReadiness,
30
31
  invalidRegistryEntryReason,
31
32
  loadLaunchPlan,
32
33
  projectHostAgent,
34
+ resolveLaunchSessionsDir,
35
+ relocateLegacyUserConfig,
36
+ resolveLegacyUserConfigPath,
33
37
  resolveUserConfigPath,
34
38
  snapshotRegistryEntry,
35
- } from './launch-config.js';
36
- import { prepareConfiguredRegistries } from './provision.js';
39
+ } from "./launch-config.js";
40
+ import { prepareConfiguredRegistries } from "./provision.js";
41
+ import {
42
+ createReplayRecordObserver,
43
+ replayIncompleteMessage,
44
+ } from "./replay-observer.js";
37
45
  import {
38
46
  createRepositoryEffectCapabilities,
39
47
  refreshRepositoryEffectCapabilities,
40
48
  recoverIncompleteRepositoryEffects,
41
- } from './repository-effects.js';
49
+ } from "./repository-effects.js";
42
50
  import {
43
51
  assertCaptainSessionExecutionCompatible,
52
+ assertCaptainSessionsDirectoryUsable,
44
53
  captainSessionSelectedMembers,
45
54
  createCaptainSessionStore,
46
55
  projectCaptainSessionStructure,
47
56
  SESSION_ID_PATTERN,
48
57
  validateCaptainSessionExecutionProjection,
49
58
  validateCaptainSessionRecord,
50
- } from './session-store.js';
59
+ } from "./session-store.js";
51
60
 
52
61
  const EXIT = { ok: 0, argument: 1, turn: 2 };
53
62
  const UUID_PATTERN = SESSION_ID_PATTERN;
63
+ const HEADLESS_REPLAY_CHANNELS = new WeakMap();
54
64
  class HeadlessHostSetupError extends Error {
55
65
  constructor(cause) {
56
66
  super(message(cause));
57
- this.name = 'HeadlessHostSetupError';
67
+ this.name = "HeadlessHostSetupError";
58
68
  this.cause = cause;
59
69
  }
60
70
  }
61
71
  const RETIRED_FLAGS = new Set([
62
- '--player',
63
- '--captain',
64
- '--option',
65
- '--cwd',
66
- '--last',
67
- '--config',
72
+ "--player",
73
+ "--captain",
74
+ "--option",
75
+ "--cwd",
76
+ "--last",
77
+ "--config",
68
78
  ]);
69
79
 
70
80
  export async function runPlaybookRun(options = {}) {
@@ -81,15 +91,25 @@ export async function runPlaybookRun(options = {}) {
81
91
  }
82
92
  if (args.help) {
83
93
  const env = options.env ?? process.env;
84
- const home = options.homeDir ?? env.HOME ?? homedir();
94
+ const home =
95
+ options.homeDir ??
96
+ (typeof env.HOME === "string" && env.HOME.trim().length > 0
97
+ ? env.HOME
98
+ : homedir());
85
99
  const userConfigPath =
86
100
  options.userConfigPath ?? resolveUserConfigPath(env, home);
101
+ // PBCLI-17: help resolves the path but writes nothing, so it neither
102
+ // seeds nor relocates.
87
103
  await writeStream(stdout, runHelpText(userConfigPath));
88
104
  return { code: EXIT.ok };
89
105
  }
90
106
 
91
107
  const env = options.env ?? process.env;
92
- const home = options.homeDir ?? env.HOME ?? homedir();
108
+ const home =
109
+ options.homeDir ??
110
+ (typeof env.HOME === "string" && env.HOME.trim().length > 0
111
+ ? env.HOME
112
+ : homedir());
93
113
  const recovering = args.retryUncertain || args.discardUncertain;
94
114
  const continuing = args.continue || args.sessionId !== undefined;
95
115
  let input = args.input;
@@ -104,6 +124,47 @@ export async function runPlaybookRun(options = {}) {
104
124
  input = resolvedInput.input;
105
125
  }
106
126
 
127
+ const userConfigPath =
128
+ options.userConfigPath ?? resolveUserConfigPath(env, home);
129
+ // DR-043: move a pre-relocation config to the canonical path before any
130
+ // read, seed, or plan work observes its absence.
131
+ if (options.userConfigPath === undefined) {
132
+ try {
133
+ relocateLegacyUserConfig(
134
+ userConfigPath,
135
+ resolveLegacyUserConfigPath(env, home),
136
+ (line) => stderr.write(line),
137
+ );
138
+ } catch (error) {
139
+ await writeStream(stderr, `playbook run: ${message(error)}\n`);
140
+ return { code: EXIT.argument };
141
+ }
142
+ }
143
+ const bootstrapConfigNotices = [];
144
+ let resolvedSessionsDir;
145
+ if (options.sessionStore === undefined) {
146
+ try {
147
+ resolvedSessionsDir = resolveLaunchSessionsDir({
148
+ userConfigPath,
149
+ overlayPaths: args.withPaths,
150
+ env,
151
+ homeDir: home,
152
+ ...(options.sessionsDir !== undefined
153
+ ? { sessionsDir: options.sessionsDir }
154
+ : {}),
155
+ preparePrimary: !continuing,
156
+ onNotice: (line) => bootstrapConfigNotices.push(line),
157
+ });
158
+ await assertCaptainSessionsDirectoryUsable(resolvedSessionsDir);
159
+ } catch (error) {
160
+ for (const line of bootstrapConfigNotices) {
161
+ await writeStream(stderr, line);
162
+ }
163
+ await writeStream(stderr, `playbook run: ${message(error)}\n`);
164
+ return { code: EXIT.argument };
165
+ }
166
+ }
167
+
107
168
  let store;
108
169
  try {
109
170
  store =
@@ -111,7 +172,7 @@ export async function runPlaybookRun(options = {}) {
111
172
  createCaptainSessionStore({
112
173
  env,
113
174
  homeDir: home,
114
- ...(options.sessionsDir ? { sessionsDir: options.sessionsDir } : {}),
175
+ sessionsDir: resolvedSessionsDir,
115
176
  ...(options.now ? { now: options.now } : {}),
116
177
  ...(options.createSessionTempId
117
178
  ? { createTempId: options.createSessionTempId }
@@ -128,6 +189,7 @@ export async function runPlaybookRun(options = {}) {
128
189
  let config;
129
190
  let cwd;
130
191
  let restoreSnapshot;
192
+ let replayChannel;
131
193
  const loadModule = memoizedModuleLoader(
132
194
  options.loadModule ?? ((specifier) => import(specifier)),
133
195
  );
@@ -144,8 +206,8 @@ export async function runPlaybookRun(options = {}) {
144
206
  onLegacyRecord: (record) =>
145
207
  reportSkippedCaptainSession(
146
208
  stderr,
147
- 'playbook run',
148
- 'legacy',
209
+ "playbook run",
210
+ "legacy",
149
211
  record,
150
212
  ),
151
213
  }),
@@ -164,9 +226,15 @@ export async function runPlaybookRun(options = {}) {
164
226
  }
165
227
  throwIfAborted(options.signal);
166
228
  lease = await store.acquire(sessionId);
229
+ replayChannel = createHeadlessReplayChannel({
230
+ lease,
231
+ sessionId,
232
+ stderr,
233
+ });
234
+ await reportHeadlessReplay(replayChannel);
167
235
  throwIfAborted(options.signal);
168
236
  const authoritative =
169
- typeof lease.recoverUnresolvedEffectAbandonment === 'function'
237
+ typeof lease.recoverUnresolvedEffectAbandonment === "function"
170
238
  ? await lease.recoverUnresolvedEffectAbandonment()
171
239
  : await lease.read();
172
240
  throwIfAborted(options.signal);
@@ -190,7 +258,7 @@ export async function runPlaybookRun(options = {}) {
190
258
  return { code: EXIT.argument };
191
259
  }
192
260
 
193
- if (priorRecord.state === 'uncertain') {
261
+ if (priorRecord.state === "uncertain") {
194
262
  if (args.discardUncertain) {
195
263
  try {
196
264
  throwIfAborted(options.signal);
@@ -271,10 +339,8 @@ export async function runPlaybookRun(options = {}) {
271
339
  }
272
340
 
273
341
  if (!continuing || !args.retryUncertain) {
274
- const userConfigPath =
275
- options.userConfigPath ?? resolveUserConfigPath(env, home);
276
342
  let plan;
277
- const configNotices = [];
343
+ const configNotices = [...bootstrapConfigNotices];
278
344
  try {
279
345
  throwIfAborted(options.signal);
280
346
  plan = await loadLaunchPlan({
@@ -316,7 +382,7 @@ export async function runPlaybookRun(options = {}) {
316
382
  );
317
383
  } else {
318
384
  sessionId = (options.createLogicalSessionId ?? randomUUID)();
319
- if (typeof sessionId !== 'string' || !UUID_PATTERN.test(sessionId)) {
385
+ if (typeof sessionId !== "string" || !UUID_PATTERN.test(sessionId)) {
320
386
  throw new Error(
321
387
  `logical session id generator returned a non-UUID value: ${JSON.stringify(sessionId)}`,
322
388
  );
@@ -419,6 +485,12 @@ export async function runPlaybookRun(options = {}) {
419
485
  try {
420
486
  throwIfAborted(options.signal);
421
487
  lease = await store.acquire(sessionId);
488
+ replayChannel = createHeadlessReplayChannel({
489
+ lease,
490
+ sessionId,
491
+ stderr,
492
+ });
493
+ await reportHeadlessReplay(replayChannel);
422
494
  throwIfAborted(options.signal);
423
495
  } catch (error) {
424
496
  const releaseError = await releaseLease(lease);
@@ -459,6 +531,7 @@ export async function runPlaybookRun(options = {}) {
459
531
  sessionId,
460
532
  cwd,
461
533
  sessionLease: lease,
534
+ replayObserver: replayChannel?.observer,
462
535
  loadModule,
463
536
  stderr,
464
537
  verbose: args.verbose,
@@ -476,16 +549,11 @@ export async function runPlaybookRun(options = {}) {
476
549
  : {}),
477
550
  ...(options.createEffectLedgerWriteAhead
478
551
  ? {
479
- createEffectLedgerWriteAhead:
480
- options.createEffectLedgerWriteAhead,
552
+ createEffectLedgerWriteAhead: options.createEffectLedgerWriteAhead,
481
553
  }
482
554
  : {}),
483
- ...(restoreSnapshot !== undefined
484
- ? { restoreSnapshot }
485
- : {}),
486
- ...(args.retryUncertain
487
- ? { reconcileUncertainTurnReplay: true }
488
- : {}),
555
+ ...(restoreSnapshot !== undefined ? { restoreSnapshot } : {}),
556
+ ...(args.retryUncertain ? { reconcileUncertainTurnReplay: true } : {}),
489
557
  ...(options.signal ? { signal: options.signal } : {}),
490
558
  beforeBossTurn: async (
491
559
  baselineSnapshot,
@@ -503,8 +571,7 @@ export async function runPlaybookRun(options = {}) {
503
571
  ? {
504
572
  freshBoundary: {
505
573
  cwd,
506
- structuralProjection:
507
- projectCaptainSessionStructure(config),
574
+ structuralProjection: projectCaptainSessionStructure(config),
508
575
  executionProjection: config,
509
576
  snapshot: baselineSnapshot,
510
577
  },
@@ -514,15 +581,15 @@ export async function runPlaybookRun(options = {}) {
514
581
  onLegacyRecord: (record) =>
515
582
  reportSkippedCaptainSession(
516
583
  stderr,
517
- 'playbook run',
518
- 'legacy',
584
+ "playbook run",
585
+ "legacy",
519
586
  record,
520
587
  ),
521
588
  onInvalidRecord: (record) =>
522
589
  reportSkippedCaptainSession(
523
590
  stderr,
524
- 'playbook run',
525
- 'invalid',
591
+ "playbook run",
592
+ "invalid",
526
593
  record,
527
594
  ),
528
595
  }
@@ -544,7 +611,9 @@ export async function runPlaybookRun(options = {}) {
544
611
  },
545
612
  assertBeforeBossTurn: () => lease.assertOwner(),
546
613
  });
614
+ await reportHeadlessReplay(replayChannel);
547
615
  } catch (error) {
616
+ await reportHeadlessReplay(replayChannel);
548
617
  const cleanupIncomplete = isCaptainSessionHostCleanupIncomplete(error);
549
618
  let abandonmentError;
550
619
  if (
@@ -565,7 +634,7 @@ export async function runPlaybookRun(options = {}) {
565
634
  if (cleanupIncomplete) {
566
635
  await writeStream(
567
636
  stderr,
568
- 'playbook run: writer lease retained until process exit because host cleanup was incomplete\n',
637
+ "playbook run: writer lease retained until process exit because host cleanup was incomplete\n",
569
638
  );
570
639
  }
571
640
  if (releaseError !== undefined) {
@@ -599,7 +668,9 @@ export async function runPlaybookRun(options = {}) {
599
668
  unresolvedEffects: settled.unresolvedEffects,
600
669
  retentionUpdates: settled.retentionUpdates,
601
670
  });
671
+ await reportHeadlessReplay(replayChannel);
602
672
  } catch (error) {
673
+ await reportHeadlessReplay(replayChannel);
603
674
  let cleanupError;
604
675
  try {
605
676
  await settled.dispose();
@@ -615,13 +686,10 @@ export async function runPlaybookRun(options = {}) {
615
686
  [error, cleanupError],
616
687
  `Captain session settlement failed (${message(error)}) and host cleanup also failed: ${message(cleanupError)}`,
617
688
  );
689
+ await writeStream(stderr, `playbook run: ${message(cleanupFailure)}\n`);
618
690
  await writeStream(
619
691
  stderr,
620
- `playbook run: ${message(cleanupFailure)}\n`,
621
- );
622
- await writeStream(
623
- stderr,
624
- 'playbook run: writer lease retained until process exit because host cleanup was incomplete\n',
692
+ "playbook run: writer lease retained until process exit because host cleanup was incomplete\n",
625
693
  );
626
694
  return { code: EXIT.turn };
627
695
  }
@@ -647,7 +715,7 @@ export async function runPlaybookRun(options = {}) {
647
715
  if (options.signal?.aborted) {
648
716
  await writeStream(
649
717
  stderr,
650
- 'playbook run: Captain turn was interrupted; reply withheld\n',
718
+ "playbook run: Captain turn was interrupted; reply withheld\n",
651
719
  );
652
720
  return { code: EXIT.turn, sessionId, record: durableRecord };
653
721
  }
@@ -686,6 +754,7 @@ export async function driveHeadlessCaptainTurn({
686
754
  sessionId,
687
755
  cwd,
688
756
  sessionLease,
757
+ replayObserver,
689
758
  loadModule,
690
759
  stderr,
691
760
  verbose = false,
@@ -716,15 +785,16 @@ export async function driveHeadlessCaptainTurn({
716
785
  observers: [
717
786
  {
718
787
  async onRecord(record) {
719
- if (record.type === 'captain_reply') {
788
+ if (record.type === "captain_reply") {
720
789
  replies.push(record.text);
721
- } else if (record.type === 'captain_status') {
790
+ } else if (record.type === "captain_status") {
722
791
  await writeStream(stderr, `${record.message}\n`);
723
- } else if (verbose && record.type === 'captain_telemetry') {
792
+ } else if (verbose && record.type === "captain_telemetry") {
724
793
  await writeStream(stderr, `\u00b7 ${record.topic}\n`);
725
794
  }
726
795
  },
727
796
  },
797
+ ...(replayObserver === undefined ? [] : [replayObserver]),
728
798
  ],
729
799
  ...(signal ? { signal } : {}),
730
800
  ...(adapterImports ? { adapterImports } : {}),
@@ -750,13 +820,13 @@ export async function driveHeadlessCaptainTurn({
750
820
  }
751
821
  await assertBeforeBossTurn?.();
752
822
  if (signal?.aborted) {
753
- throw signal.reason ?? new Error('Captain turn aborted');
823
+ throw signal.reason ?? new Error("Captain turn aborted");
754
824
  }
755
825
  await host.runBossTurn(input);
756
826
  throwIfAborted(signal);
757
827
  if (
758
828
  replies.length !== 1 ||
759
- typeof replies[0] !== 'string' ||
829
+ typeof replies[0] !== "string" ||
760
830
  replies[0].trim().length === 0
761
831
  ) {
762
832
  throw new Error(
@@ -764,11 +834,13 @@ export async function driveHeadlessCaptainTurn({
764
834
  );
765
835
  }
766
836
  if (shell === undefined) {
767
- throw new Error('Captain shell host initialized without a shell');
837
+ throw new Error("Captain shell host initialized without a shell");
768
838
  }
769
839
  const settlement = shell.exportSettlement();
770
840
  if (settlement === undefined) {
771
- throw new Error('Captain turn settled without an exportable session settlement');
841
+ throw new Error(
842
+ "Captain turn settled without an exportable session settlement",
843
+ );
772
844
  }
773
845
  const { snapshot, unresolvedEffects, retentionUpdates } = settlement;
774
846
  if (
@@ -776,7 +848,7 @@ export async function driveHeadlessCaptainTurn({
776
848
  snapshot.issuedSessionIds?.includes(sessionId)
777
849
  ) {
778
850
  throw new Error(
779
- 'logical session id collided with an internal Captain session id',
851
+ "logical session id collided with an internal Captain session id",
780
852
  );
781
853
  }
782
854
  return {
@@ -815,8 +887,8 @@ export async function driveHeadlessCaptainTurn({
815
887
  export class CaptainSessionHostCleanupError extends AggregateError {
816
888
  constructor(errors, messageText) {
817
889
  super(errors, messageText);
818
- this.name = 'CaptainSessionHostCleanupError';
819
- this.code = 'PLAYBOOK_CAPTAIN_HOST_CLEANUP_INCOMPLETE';
890
+ this.name = "CaptainSessionHostCleanupError";
891
+ this.code = "PLAYBOOK_CAPTAIN_HOST_CLEANUP_INCOMPLETE";
820
892
  }
821
893
  }
822
894
 
@@ -855,7 +927,7 @@ function effectLedgerSnapshotFromCapabilities(hostCapabilities) {
855
927
  )
856
928
  ) {
857
929
  throw new Error(
858
- 'schema-3 current-host capabilities disagree on their effect ledger',
930
+ "schema-3 current-host capabilities disagree on their effect ledger",
859
931
  );
860
932
  }
861
933
  }
@@ -877,33 +949,29 @@ function reconcileWholeTurnReplaySnapshot(snapshot, ledger, catalog) {
877
949
  const currentPrefix = current.boundaries.slice(0, checkpointBoundaryCount);
878
950
  if (!isDeepStrictEqual(currentPrefix, checkpoint.boundaries)) {
879
951
  throw new Error(
880
- 'Captain uncertain turn repository-effect reconciliation cannot restore a changed pre-turn boundary',
952
+ "Captain uncertain turn repository-effect reconciliation cannot restore a changed pre-turn boundary",
881
953
  );
882
954
  }
883
955
  if (
884
- !isDeepStrictEqual(
885
- current.logicalOperations,
886
- checkpoint.logicalOperations,
887
- )
956
+ !isDeepStrictEqual(current.logicalOperations, checkpoint.logicalOperations)
888
957
  ) {
889
958
  throw new Error(
890
- 'Captain uncertain turn repository-effect reconciliation found deferred logical-operation progress and cannot replay the Boss turn',
959
+ "Captain uncertain turn repository-effect reconciliation found deferred logical-operation progress and cannot replay the Boss turn",
891
960
  );
892
961
  }
893
962
  const suffix = current.boundaries.slice(checkpointBoundaryCount);
894
963
  const blocking = suffix.find(
895
- (boundary) =>
896
- boundary.physicalReceipt?.classification !== 'unchanged',
964
+ (boundary) => boundary.physicalReceipt?.classification !== "unchanged",
897
965
  );
898
966
  if (blocking !== undefined) {
899
967
  const classification =
900
- blocking.physicalReceipt?.classification ?? 'incomplete';
968
+ blocking.physicalReceipt?.classification ?? "incomplete";
901
969
  throw new Error(
902
970
  `Captain uncertain turn repository-effect boundary ${JSON.stringify(blocking.boundaryId)} is ${classification}; whole-turn replay remains parked for reconciliation`,
903
971
  );
904
972
  }
905
973
  const frames =
906
- snapshot.mode !== 'engaged.parked'
974
+ snapshot.mode !== "engaged.parked"
907
975
  ? undefined
908
976
  : snapshot.frames.map((frame) => {
909
977
  if (catalog[frame.playbookId]?.artifactSchema !== 3) {
@@ -973,20 +1041,17 @@ export async function createCaptainSessionHost({
973
1041
  !isDeepStrictEqual(sourceSnapshot.effectLedger, currentEffectLedger())
974
1042
  ) {
975
1043
  const recovered =
976
- typeof sessionLease.read === 'function'
1044
+ typeof sessionLease.read === "function"
977
1045
  ? await sessionLease.read()
978
1046
  : undefined;
979
1047
  if (
980
- recovered?.state === 'settled' &&
1048
+ recovered?.state === "settled" &&
981
1049
  isDeepStrictEqual(recovered.snapshot.effectLedger, currentEffectLedger())
982
1050
  ) {
983
1051
  sourceSnapshot = recovered.snapshot;
984
1052
  }
985
1053
  }
986
- if (
987
- sourceSnapshot !== undefined &&
988
- reconcileUncertainTurnReplay
989
- ) {
1054
+ if (sourceSnapshot !== undefined && reconcileUncertainTurnReplay) {
990
1055
  sourceSnapshot = reconcileWholeTurnReplaySnapshot(
991
1056
  sourceSnapshot,
992
1057
  currentEffectLedger(),
@@ -997,15 +1062,14 @@ export async function createCaptainSessionHost({
997
1062
  !isDeepStrictEqual(sourceSnapshot.effectLedger, currentEffectLedger())
998
1063
  ) {
999
1064
  throw new Error(
1000
- 'Captain session effect ledger requires reconciliation before source-state restoration',
1065
+ "Captain session effect ledger requires reconciliation before source-state restoration",
1001
1066
  );
1002
1067
  }
1003
1068
  const shell = createPlaybookCaptainShell(captainOptionsFromConfig(config), {
1004
1069
  loadModule,
1005
1070
  hostCapabilities,
1006
1071
  unresolvedEffectSettlement: {
1007
- begin: (input) =>
1008
- sessionLease.beginUnresolvedEffectAbandonment(input),
1072
+ begin: (input) => sessionLease.beginUnresolvedEffectAbandonment(input),
1009
1073
  complete: (input) =>
1010
1074
  sessionLease.completeUnresolvedEffectAbandonment(input),
1011
1075
  },
@@ -1021,7 +1085,7 @@ export async function createCaptainSessionHost({
1021
1085
  captain,
1022
1086
  captainConfig: projectHostAgent(
1023
1087
  config.captain,
1024
- 'Captain execution config.captain',
1088
+ "Captain execution config.captain",
1025
1089
  ),
1026
1090
  players: config.players.map(({ id, ...agent }) => ({
1027
1091
  id,
@@ -1035,11 +1099,14 @@ export async function createCaptainSessionHost({
1035
1099
  const snapshot = shell.exportSnapshot();
1036
1100
  if (snapshot === undefined) {
1037
1101
  throw new Error(
1038
- 'Captain shell initialized without an exportable session snapshot',
1102
+ "Captain shell initialized without an exportable session snapshot",
1039
1103
  );
1040
1104
  }
1041
- if (sourceSnapshot !== undefined && !isDeepStrictEqual(snapshot, sourceSnapshot)) {
1042
- throw new Error('restored Captain snapshot changed before the Boss turn');
1105
+ if (
1106
+ sourceSnapshot !== undefined &&
1107
+ !isDeepStrictEqual(snapshot, sourceSnapshot)
1108
+ ) {
1109
+ throw new Error("restored Captain snapshot changed before the Boss turn");
1043
1110
  }
1044
1111
  if (sessionId !== undefined) {
1045
1112
  assertLogicalSessionIdDistinct({ sessionId, snapshot });
@@ -1147,6 +1214,7 @@ export async function validateFrozenExecutionConfig(
1147
1214
  structuralProjection,
1148
1215
  executionProjection,
1149
1216
  );
1217
+ assertFrozenFastModesSupported(config);
1150
1218
  const catalogItems = Object.entries(config.catalog);
1151
1219
 
1152
1220
  // Preserve the complete-catalog preparation transaction: prepare every
@@ -1162,7 +1230,9 @@ export async function validateFrozenExecutionConfig(
1162
1230
  authoredFrom: item.from,
1163
1231
  });
1164
1232
  } catch (cause) {
1165
- throw new Error(`stored playbook ${JSON.stringify(id)} failed to prepare: ${message(cause)}`);
1233
+ throw new Error(
1234
+ `stored playbook ${JSON.stringify(id)} failed to prepare: ${message(cause)}`,
1235
+ );
1166
1236
  }
1167
1237
  if (prepared !== undefined && prepared !== item.from) {
1168
1238
  throw new Error(
@@ -1176,10 +1246,14 @@ export async function validateFrozenExecutionConfig(
1176
1246
  try {
1177
1247
  entry = snapshotRegistryEntry((await loadModule(item.from))?.default);
1178
1248
  } catch (cause) {
1179
- throw new Error(`stored playbook ${JSON.stringify(id)} failed to import: ${message(cause)}`);
1249
+ throw new Error(
1250
+ `stored playbook ${JSON.stringify(id)} failed to import: ${message(cause)}`,
1251
+ );
1180
1252
  }
1181
1253
  if (invalidRegistryEntryReason(entry) !== undefined) {
1182
- throw new Error(`stored playbook ${JSON.stringify(id)} exposes no valid registry entry`);
1254
+ throw new Error(
1255
+ `stored playbook ${JSON.stringify(id)} exposes no valid registry entry`,
1256
+ );
1183
1257
  }
1184
1258
  if (
1185
1259
  entry.id !== id ||
@@ -1187,10 +1261,7 @@ export async function validateFrozenExecutionConfig(
1187
1261
  entry.intent !== item.intent ||
1188
1262
  entry.artifactSchema !== item.artifactSchema ||
1189
1263
  !isDeepStrictEqual(entry.requiredRoleIds, item.requiredRoleIds) ||
1190
- !isDeepStrictEqual(
1191
- entry.concurrentRoleSets,
1192
- item.concurrentRoleSets,
1193
- )
1264
+ !isDeepStrictEqual(entry.concurrentRoleSets, item.concurrentRoleSets)
1194
1265
  ) {
1195
1266
  throw new Error(
1196
1267
  `stored playbook ${JSON.stringify(id)} no longer matches its recorded manifest identity`,
@@ -1200,6 +1271,44 @@ export async function validateFrozenExecutionConfig(
1200
1271
  return config;
1201
1272
  }
1202
1273
 
1274
+ function assertFrozenFastModesSupported(config) {
1275
+ if (config.captain.fastMode !== undefined) {
1276
+ assertFastModeSupported(
1277
+ config.captain.adapter,
1278
+ "stored Captain fastMode",
1279
+ );
1280
+ }
1281
+
1282
+ const playerAdapters = new Map();
1283
+ for (const player of config.players) {
1284
+ playerAdapters.set(player.id, player.adapter);
1285
+ if (player.fastMode !== undefined) {
1286
+ assertFastModeSupported(
1287
+ player.adapter,
1288
+ `stored player ${JSON.stringify(player.id)} fastMode`,
1289
+ );
1290
+ }
1291
+ }
1292
+
1293
+ for (const [playbookId, item] of Object.entries(config.catalog)) {
1294
+ for (const [roleId, binding] of Object.entries(item.roles)) {
1295
+ if (binding.fastMode === undefined) continue;
1296
+ const adapter = playerAdapters.get(binding.playerId);
1297
+ if (adapter === undefined) {
1298
+ throw new Error(
1299
+ `stored playbook ${JSON.stringify(playbookId)} role ` +
1300
+ `${JSON.stringify(roleId)} names an unknown player`,
1301
+ );
1302
+ }
1303
+ assertFastModeSupported(
1304
+ adapter,
1305
+ `stored playbook ${JSON.stringify(playbookId)} role ` +
1306
+ `${JSON.stringify(roleId)} fastMode`,
1307
+ );
1308
+ }
1309
+ }
1310
+ }
1311
+
1203
1312
  function adaptersFromExecutionConfig(config) {
1204
1313
  return [
1205
1314
  ...new Set([
@@ -1216,7 +1325,7 @@ function assertLogicalSessionIdDistinct(record) {
1216
1325
  record.snapshot.issuedSessionIds.includes(record.sessionId))
1217
1326
  ) {
1218
1327
  throw new Error(
1219
- 'logical session id collides with an internal Captain session id',
1328
+ "logical session id collides with an internal Captain session id",
1220
1329
  );
1221
1330
  }
1222
1331
  }
@@ -1225,7 +1334,10 @@ function memoizedModuleLoader(loadModule) {
1225
1334
  const modules = new Map();
1226
1335
  return (specifier) => {
1227
1336
  if (!modules.has(specifier)) {
1228
- modules.set(specifier, Promise.resolve().then(() => loadModule(specifier)));
1337
+ modules.set(
1338
+ specifier,
1339
+ Promise.resolve().then(() => loadModule(specifier)),
1340
+ );
1229
1341
  }
1230
1342
  return modules.get(specifier);
1231
1343
  };
@@ -1250,7 +1362,7 @@ export function captainOptionsFromConfig(config) {
1250
1362
  config.players.map(({ id, ...agent }) => [id, cloneJson(agent)]),
1251
1363
  ),
1252
1364
  },
1253
- ...(typeof config.captain.adapter === 'string' &&
1365
+ ...(typeof config.captain.adapter === "string" &&
1254
1366
  config.captain.adapter.length > 0
1255
1367
  ? { captainAdapter: config.captain.adapter }
1256
1368
  : {}),
@@ -1284,65 +1396,65 @@ export function parseRunArgs(argv) {
1284
1396
  const positionals = [];
1285
1397
  for (let index = 0; index < argv.length; index += 1) {
1286
1398
  const arg = argv[index];
1287
- if (arg === '--') {
1399
+ if (arg === "--") {
1288
1400
  parsed.terminated = true;
1289
1401
  positionals.push(...argv.slice(index + 1));
1290
1402
  break;
1291
1403
  }
1292
- if (arg === '--help' || arg === '-h') parsed.help = true;
1293
- else if (arg === '--json') parsed.json = true;
1294
- else if (arg === '--verbose') parsed.verbose = true;
1295
- else if (arg === '--no-provision') parsed.noProvision = true;
1296
- else if (arg === '--retry-uncertain') {
1404
+ if (arg === "--help" || arg === "-h") parsed.help = true;
1405
+ else if (arg === "--json") parsed.json = true;
1406
+ else if (arg === "--verbose") parsed.verbose = true;
1407
+ else if (arg === "--no-provision") parsed.noProvision = true;
1408
+ else if (arg === "--retry-uncertain") {
1297
1409
  if (parsed.retryUncertain) {
1298
- throw new Error('--retry-uncertain may be specified only once');
1410
+ throw new Error("--retry-uncertain may be specified only once");
1299
1411
  }
1300
1412
  parsed.retryUncertain = true;
1301
- } else if (arg === '--discard-uncertain') {
1413
+ } else if (arg === "--discard-uncertain") {
1302
1414
  if (parsed.discardUncertain) {
1303
- throw new Error('--discard-uncertain may be specified only once');
1415
+ throw new Error("--discard-uncertain may be specified only once");
1304
1416
  }
1305
1417
  parsed.discardUncertain = true;
1306
- }
1307
- else if (arg === '--continue') {
1308
- if (parsed.continue) throw new Error('--continue may be specified only once');
1418
+ } else if (arg === "--continue") {
1419
+ if (parsed.continue)
1420
+ throw new Error("--continue may be specified only once");
1309
1421
  parsed.continue = true;
1310
- } else if (arg === '--session') {
1422
+ } else if (arg === "--session") {
1311
1423
  const value = argv[index + 1];
1312
- if (value === undefined || value === '') {
1313
- throw new Error('--session needs a UUID value');
1424
+ if (value === undefined || value === "") {
1425
+ throw new Error("--session needs a UUID value");
1314
1426
  }
1315
1427
  if (parsed.sessionId !== undefined) {
1316
- throw new Error('--session may be specified only once');
1428
+ throw new Error("--session may be specified only once");
1317
1429
  }
1318
1430
  parsed.sessionId = value;
1319
1431
  index += 1;
1320
- } else if (arg.startsWith('--session=')) {
1321
- const value = arg.slice('--session='.length);
1322
- if (value === '') throw new Error('--session needs a UUID value');
1432
+ } else if (arg.startsWith("--session=")) {
1433
+ const value = arg.slice("--session=".length);
1434
+ if (value === "") throw new Error("--session needs a UUID value");
1323
1435
  if (parsed.sessionId !== undefined) {
1324
- throw new Error('--session may be specified only once');
1436
+ throw new Error("--session may be specified only once");
1325
1437
  }
1326
1438
  parsed.sessionId = value;
1327
- } else if (arg === '--with') {
1439
+ } else if (arg === "--with") {
1328
1440
  const value = argv[index + 1];
1329
- if (value === undefined || value === '') {
1330
- throw new Error('--with needs a value');
1441
+ if (value === undefined || value === "") {
1442
+ throw new Error("--with needs a value");
1331
1443
  }
1332
1444
  parsed.withPaths.push(value);
1333
1445
  index += 1;
1334
- } else if (arg.startsWith('--with=')) {
1335
- const value = arg.slice('--with='.length);
1336
- if (value === '') throw new Error('--with needs a value');
1446
+ } else if (arg.startsWith("--with=")) {
1447
+ const value = arg.slice("--with=".length);
1448
+ if (value === "") throw new Error("--with needs a value");
1337
1449
  parsed.withPaths.push(value);
1338
1450
  } else if (
1339
1451
  RETIRED_FLAGS.has(arg) ||
1340
1452
  [...RETIRED_FLAGS].some((flag) => arg.startsWith(`${flag}=`))
1341
1453
  ) {
1342
1454
  throw new Error(
1343
- `${arg.split('=')[0]} was removed; configure the shared Captain session in playbook.config.yaml or a --with overlay`,
1455
+ `${arg.split("=")[0]} was removed; configure the shared Captain session in playbook.config.yaml or a --with overlay`,
1344
1456
  );
1345
- } else if (arg.startsWith('-')) {
1457
+ } else if (arg.startsWith("-")) {
1346
1458
  throw new Error(`unknown option ${arg}`);
1347
1459
  } else {
1348
1460
  positionals.push(arg);
@@ -1350,15 +1462,15 @@ export function parseRunArgs(argv) {
1350
1462
  }
1351
1463
  if (positionals.length > 1) {
1352
1464
  throw new Error(
1353
- 'expected at most one [input] argument; quote multi-word input as one shell argument',
1465
+ "expected at most one [input] argument; quote multi-word input as one shell argument",
1354
1466
  );
1355
1467
  }
1356
1468
  if (parsed.continue && parsed.sessionId !== undefined) {
1357
- throw new Error('--continue and --session are mutually exclusive');
1469
+ throw new Error("--continue and --session are mutually exclusive");
1358
1470
  }
1359
1471
  if (parsed.retryUncertain && parsed.discardUncertain) {
1360
1472
  throw new Error(
1361
- '--retry-uncertain and --discard-uncertain are mutually exclusive',
1473
+ "--retry-uncertain and --discard-uncertain are mutually exclusive",
1362
1474
  );
1363
1475
  }
1364
1476
  if (
@@ -1366,7 +1478,7 @@ export function parseRunArgs(argv) {
1366
1478
  parsed.sessionId === undefined
1367
1479
  ) {
1368
1480
  throw new Error(
1369
- '--retry-uncertain and --discard-uncertain require --session <id>',
1481
+ "--retry-uncertain and --discard-uncertain require --session <id>",
1370
1482
  );
1371
1483
  }
1372
1484
  if (
@@ -1374,7 +1486,7 @@ export function parseRunArgs(argv) {
1374
1486
  (parsed.continue || positionals.length > 0)
1375
1487
  ) {
1376
1488
  throw new Error(
1377
- 'uncertain-turn recovery accepts only an explicit --session and no input',
1489
+ "uncertain-turn recovery accepts only an explicit --session and no input",
1378
1490
  );
1379
1491
  }
1380
1492
  if (
@@ -1382,20 +1494,20 @@ export function parseRunArgs(argv) {
1382
1494
  (parsed.json || parsed.verbose || parsed.noProvision)
1383
1495
  ) {
1384
1496
  throw new Error(
1385
- '--discard-uncertain does not accept --json, --verbose, or --no-provision',
1497
+ "--discard-uncertain does not accept --json, --verbose, or --no-provision",
1386
1498
  );
1387
1499
  }
1388
1500
  if (
1389
1501
  (parsed.retryUncertain || parsed.discardUncertain) &&
1390
1502
  parsed.withPaths.length > 0
1391
1503
  ) {
1392
- throw new Error('--with is unavailable during uncertain-turn recovery');
1504
+ throw new Error("--with is unavailable during uncertain-turn recovery");
1393
1505
  }
1394
1506
  if (
1395
1507
  parsed.sessionId !== undefined &&
1396
1508
  !SESSION_ID_PATTERN.test(parsed.sessionId)
1397
1509
  ) {
1398
- throw new Error('--session needs a canonical UUID value');
1510
+ throw new Error("--session needs a canonical UUID value");
1399
1511
  }
1400
1512
  parsed.input = positionals[0];
1401
1513
  return parsed;
@@ -1418,18 +1530,15 @@ async function reportReadinessFailure({
1418
1530
  const [first, ...rest] = lines;
1419
1531
  await writeStream(
1420
1532
  stderr,
1421
- [
1422
- ...(first ? [`playbook run: ${first}`] : []),
1423
- ...rest,
1424
- ]
1533
+ [...(first ? [`playbook run: ${first}`] : []), ...rest]
1425
1534
  .map((line) => `${line}\n`)
1426
- .join(''),
1535
+ .join(""),
1427
1536
  );
1428
1537
  }
1429
1538
  if (failingAdapters.length > 0) {
1430
1539
  await writeStream(
1431
1540
  stderr,
1432
- `playbook run: adapters not ready: ${failingAdapters.join(', ')}\n`,
1541
+ `playbook run: adapters not ready: ${failingAdapters.join(", ")}\n`,
1433
1542
  );
1434
1543
  }
1435
1544
  }
@@ -1453,7 +1562,7 @@ async function resolveBossInput(input, options, stderr) {
1453
1562
  if (resolved.trim().length === 0) {
1454
1563
  await writeStream(
1455
1564
  stderr,
1456
- 'playbook run: empty input; pass one argument or pipe a Boss message on stdin\n',
1565
+ "playbook run: empty input; pass one argument or pipe a Boss message on stdin\n",
1457
1566
  );
1458
1567
  return { ok: false };
1459
1568
  }
@@ -1462,22 +1571,22 @@ async function resolveBossInput(input, options, stderr) {
1462
1571
 
1463
1572
  async function awaitWithAbort(value, signal) {
1464
1573
  if (signal === undefined) return value;
1465
- if (signal.aborted) throw signal.reason ?? new Error('operation aborted');
1574
+ if (signal.aborted) throw signal.reason ?? new Error("operation aborted");
1466
1575
  let onAbort;
1467
1576
  const aborted = new Promise((_, reject) => {
1468
- onAbort = () => reject(signal.reason ?? new Error('operation aborted'));
1469
- signal.addEventListener('abort', onAbort, { once: true });
1577
+ onAbort = () => reject(signal.reason ?? new Error("operation aborted"));
1578
+ signal.addEventListener("abort", onAbort, { once: true });
1470
1579
  });
1471
1580
  try {
1472
1581
  return await Promise.race([value, aborted]);
1473
1582
  } finally {
1474
- signal.removeEventListener('abort', onAbort);
1583
+ signal.removeEventListener("abort", onAbort);
1475
1584
  }
1476
1585
  }
1477
1586
 
1478
1587
  function throwIfAborted(signal) {
1479
1588
  if (signal?.aborted) {
1480
- throw signal.reason ?? new Error('operation aborted');
1589
+ throw signal.reason ?? new Error("operation aborted");
1481
1590
  }
1482
1591
  }
1483
1592
 
@@ -1488,14 +1597,14 @@ function registryPreparer(args, options, stderr) {
1488
1597
  enabled: !args.noProvision,
1489
1598
  stderr,
1490
1599
  hostRoots: options.hostRoots,
1491
- commandName: 'playbook run',
1600
+ commandName: "playbook run",
1492
1601
  })
1493
1602
  );
1494
1603
  }
1495
1604
 
1496
1605
  function createAttemptId(options) {
1497
1606
  const attemptId = (options.createAttemptId ?? randomUUID)();
1498
- if (typeof attemptId !== 'string' || !UUID_PATTERN.test(attemptId)) {
1607
+ if (typeof attemptId !== "string" || !UUID_PATTERN.test(attemptId)) {
1499
1608
  throw new Error(
1500
1609
  `uncertain turn attempt id generator returned a non-UUID value: ${JSON.stringify(attemptId)}`,
1501
1610
  );
@@ -1505,36 +1614,80 @@ function createAttemptId(options) {
1505
1614
 
1506
1615
  async function releaseLease(lease) {
1507
1616
  if (lease === undefined) return undefined;
1617
+ const replayChannel = HEADLESS_REPLAY_CHANNELS.get(lease);
1508
1618
  try {
1509
- await lease.release();
1619
+ const status = await lease.release();
1620
+ await reportHeadlessReplay(replayChannel, status);
1621
+ HEADLESS_REPLAY_CHANNELS.delete(lease);
1510
1622
  return undefined;
1511
1623
  } catch (error) {
1624
+ await reportHeadlessReplay(replayChannel);
1512
1625
  return error;
1513
1626
  }
1514
1627
  }
1515
1628
 
1629
+ function createHeadlessReplayChannel({ lease, sessionId, stderr }) {
1630
+ if (
1631
+ typeof lease?.append !== "function" ||
1632
+ typeof lease?.streamStatus !== "function"
1633
+ ) {
1634
+ return undefined;
1635
+ }
1636
+ let warningPending = false;
1637
+ let warningAttempted = false;
1638
+ const replay = createReplayRecordObserver({
1639
+ lease,
1640
+ onIncomplete() {
1641
+ warningPending = true;
1642
+ },
1643
+ });
1644
+ const channel = Object.freeze({
1645
+ ...replay,
1646
+ async flushWarning() {
1647
+ if (!warningPending || warningAttempted) return;
1648
+ warningAttempted = true;
1649
+ try {
1650
+ await writeStream(
1651
+ stderr,
1652
+ `playbook run: ${replayIncompleteMessage(sessionId)}\n`,
1653
+ );
1654
+ } catch {
1655
+ // The bounded warning is best-effort and never changes run outcome.
1656
+ }
1657
+ },
1658
+ });
1659
+ HEADLESS_REPLAY_CHANNELS.set(lease, channel);
1660
+ return channel;
1661
+ }
1662
+
1663
+ async function reportHeadlessReplay(channel, status) {
1664
+ if (channel === undefined) return;
1665
+ await channel.reportIfIncomplete(status);
1666
+ await channel.flushWarning();
1667
+ }
1668
+
1516
1669
  async function reportUncertainSession(stderr, sessionId) {
1517
1670
  await writeStream(
1518
1671
  stderr,
1519
1672
  [
1520
1673
  `playbook run: Captain session ${JSON.stringify(sessionId)} has an uncertain turn and will not be replayed automatically`,
1521
- 'Retry may duplicate external effects from the interrupted attempt; discard abandons that attempted turn.',
1674
+ "Retry may duplicate external effects from the interrupted attempt; discard abandons that attempted turn.",
1522
1675
  `playbook run --session ${sessionId} --retry-uncertain`,
1523
1676
  `playbook run --session ${sessionId} --discard-uncertain`,
1524
- '',
1525
- ].join('\n'),
1677
+ "",
1678
+ ].join("\n"),
1526
1679
  );
1527
1680
  }
1528
1681
 
1529
1682
  function legacyCaptainSessionReason(schemaVersion) {
1530
1683
  if (schemaVersion === 2) {
1531
- return 'schema 2 has incompatible player identity';
1684
+ return "schema 2 has incompatible player identity";
1532
1685
  }
1533
1686
  if (schemaVersion === 3 || schemaVersion === 4) {
1534
1687
  return `schema ${JSON.stringify(schemaVersion)} predates the artifact-schema-3 effect-authority cutover and is not resumable`;
1535
1688
  }
1536
1689
  if (schemaVersion === 5) {
1537
- return 'schema 5 predates the canonical schema-6 unresolved-effect settlement boundary for the artifact-schema-3 effect-authority cutover and is not resumable';
1690
+ return "schema 5 predates the canonical schema-6 unresolved-effect settlement boundary for the artifact-schema-3 effect-authority cutover and is not resumable";
1538
1691
  }
1539
1692
  return `schema ${JSON.stringify(schemaVersion)} is unsupported`;
1540
1693
  }
@@ -1546,9 +1699,7 @@ export async function reportSkippedCaptainSession(
1546
1699
  { sessionId, path, schemaVersion, reason },
1547
1700
  ) {
1548
1701
  const explanation =
1549
- kind === 'legacy'
1550
- ? legacyCaptainSessionReason(schemaVersion)
1551
- : reason;
1702
+ kind === "legacy" ? legacyCaptainSessionReason(schemaVersion) : reason;
1552
1703
  await writeStream(
1553
1704
  stderr,
1554
1705
  `${commandName}: skipping ${kind} Captain session ${JSON.stringify(sessionId)} at ${JSON.stringify(path)} because ${explanation}; move it outside the sessions directory or remove it to silence this warning\n`,
@@ -1557,12 +1708,12 @@ export async function reportSkippedCaptainSession(
1557
1708
 
1558
1709
  function replayInvocation(argv, args, input) {
1559
1710
  if (args.retryUncertain || args.discardUncertain) {
1560
- return ['run', ...argv];
1711
+ return ["run", ...argv];
1561
1712
  }
1562
- if (args.input !== undefined) return ['run', ...argv];
1713
+ if (args.input !== undefined) return ["run", ...argv];
1563
1714
  return args.terminated
1564
- ? ['run', ...argv, input]
1565
- : ['run', ...argv, '--', input];
1715
+ ? ["run", ...argv, input]
1716
+ : ["run", ...argv, "--", input];
1566
1717
  }
1567
1718
 
1568
1719
  function cloneJson(value) {
@@ -1574,69 +1725,70 @@ async function readAllStdin() {
1574
1725
  for await (const chunk of process.stdin) {
1575
1726
  chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
1576
1727
  }
1577
- return Buffer.concat(chunks).toString('utf8');
1728
+ return Buffer.concat(chunks).toString("utf8");
1578
1729
  }
1579
1730
 
1580
1731
  async function writeStream(stream, text) {
1581
1732
  const ready = stream.write(text);
1582
- if (ready !== false || typeof stream.once !== 'function') return;
1733
+ if (ready !== false || typeof stream.once !== "function") return;
1583
1734
  await new Promise((resolvePromise, rejectPromise) => {
1584
1735
  const onDrain = () => {
1585
- stream.off?.('error', onError);
1736
+ stream.off?.("error", onError);
1586
1737
  resolvePromise();
1587
1738
  };
1588
1739
  const onError = (error) => {
1589
- stream.off?.('drain', onDrain);
1740
+ stream.off?.("drain", onDrain);
1590
1741
  rejectPromise(error);
1591
1742
  };
1592
- stream.once('drain', onDrain);
1593
- stream.once('error', onError);
1743
+ stream.once("drain", onDrain);
1744
+ stream.once("error", onError);
1594
1745
  });
1595
1746
  }
1596
1747
 
1597
1748
  function runHelpText(userConfigPath) {
1598
1749
  return [
1599
- 'Usage:',
1600
- ' playbook run [--with <path>]... [--no-provision] [--json]',
1601
- ' [--verbose] [--] [input]',
1602
- ' playbook run (--continue | --session <id>) [--with <path>]...',
1603
- ' [--no-provision] [--json] [--verbose] [--] [reply]',
1604
- ' playbook run --session <id> --retry-uncertain [--no-provision]',
1605
- ' playbook run --session <id> --discard-uncertain',
1606
- '',
1607
- ' [input] one exact Boss message; read verbatim from stdin when omitted',
1608
- ' -- end options so a flag-shaped input remains Boss text',
1609
- '',
1750
+ "Usage:",
1751
+ " playbook run [--with <path>]... [--no-provision] [--json]",
1752
+ " [--verbose] [--] [input]",
1753
+ " playbook run (--continue | --session <id>) [--with <path>]...",
1754
+ " [--no-provision] [--json] [--verbose] [--] [reply]",
1755
+ " playbook run --session <id> --retry-uncertain [--no-provision]",
1756
+ " playbook run --session <id> --discard-uncertain",
1757
+ "",
1758
+ " [input] one exact Boss message; read verbatim from stdin when omitted",
1759
+ " -- end options so a flag-shaped input remains Boss text",
1760
+ "",
1610
1761
  `Default config: ${userConfigPath}`,
1611
- '',
1612
- 'A new run uses the same configured Captain, enabled playbooks, players,',
1613
- 'options, overlays, provisioning, and readiness gate as interactive',
1614
- '`playbook`. Enable an external registry in that config, then invoke its',
1615
- 'effective /command through Captain. The former positional registry,',
1616
- 'resume, and run-only binding surfaces have been removed.',
1617
- 'Stable agents live under top-level players; every playbook-local role',
1618
- 'binds explicitly under playbooks.<id>.roles. Equal player ids share one',
1619
- 'provider conversation; distinct ids remain isolated.',
1620
- 'Legacy playbooks.<id>.players is rejected and is not auto-migrated,',
1621
- 'because choosing new ids decides sharing versus isolation.',
1622
- 'Bare continuation prefers the newest session stored for the invoking',
1623
- 'working directory and reports when it uses the global newest fallback.',
1624
- 'An ordinary continued run restores that stored structure and working',
1625
- 'directory, then reads current config and overlays for model and effort.',
1626
- 'Uncertain retry instead uses its exact recorded input and settings.',
1627
- '',
1628
- 'Options:',
1629
- ' --with <path> overlay a generic config fragment (repeatable)',
1630
- ' --no-provision do not provision thin filesystem registry engines',
1631
- ' --continue prefer this working directory, else global newest',
1632
- ' --session <id> reply to one durable Captain session UUID',
1633
- ' --retry-uncertain retry that session\'s exact recorded uncertain input',
1634
- ' --discard-uncertain discard that session\'s uncertain attempt',
1762
+ "",
1763
+ "A new run uses the same configured Captain, enabled playbooks, players,",
1764
+ "options, overlays, provisioning, and readiness gate as interactive",
1765
+ "`playbook`. Enable an external registry in that config, then invoke its",
1766
+ "effective /command through Captain. The former positional registry,",
1767
+ "resume, and run-only binding surfaces have been removed.",
1768
+ "Stable agents live under top-level players; every playbook-local role",
1769
+ "binds explicitly under playbooks.<id>.roles. Equal player ids share one",
1770
+ "provider conversation; distinct ids remain isolated.",
1771
+ "Legacy playbooks.<id>.players is rejected and is not auto-migrated,",
1772
+ "because choosing new ids decides sharing versus isolation.",
1773
+ "Bare continuation prefers the newest session stored for the invoking",
1774
+ "working directory and reports when it uses the global newest fallback.",
1775
+ "An ordinary continued run restores that stored structure and working",
1776
+ "directory, then reads current config and overlays for model, effort,",
1777
+ "and fast mode.",
1778
+ "Uncertain retry instead uses its exact recorded input and settings.",
1779
+ "",
1780
+ "Options:",
1781
+ " --with <path> overlay a generic config fragment (repeatable)",
1782
+ " --no-provision do not provision thin filesystem registry engines",
1783
+ " --continue prefer this working directory, else global newest",
1784
+ " --session <id> reply to one durable Captain session UUID",
1785
+ " --retry-uncertain retry that session's exact recorded uncertain input",
1786
+ " --discard-uncertain discard that session's uncertain attempt",
1635
1787
  ' --json print exactly {"sessionId", "reply"}',
1636
- ' --verbose print Captain telemetry topics to stderr',
1637
- ' -h, --help print this help without reading input or config',
1638
- '',
1639
- ].join('\n');
1788
+ " --verbose print Captain telemetry topics to stderr",
1789
+ " -h, --help print this help without reading input or config",
1790
+ "",
1791
+ ].join("\n");
1640
1792
  }
1641
1793
 
1642
1794
  function message(error) {