@sublang/playbook 8.0.0 → 10.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 (66) hide show
  1. package/README.md +3 -3
  2. package/docs/cli.md +66 -23
  3. package/docs/configuration.md +13 -8
  4. package/docs/embedding.md +45 -14
  5. package/package.json +7 -3
  6. package/reference/sdlc/captain.md +14 -10
  7. package/reference/sdlc/captain.playbook/captain.fsm.d.ts +33 -13
  8. package/reference/sdlc/captain.playbook/captain.fsm.js +80 -9
  9. package/reference/sdlc/captain.playbook/captain.fsm.ts +137 -18
  10. package/reference/sdlc/captain.playbook/captain.gears.md +10 -6
  11. package/reference/sdlc/captain.playbook/captain.playbook.d.ts +5 -1
  12. package/reference/sdlc/captain.playbook/captain.playbook.js +151 -10
  13. package/reference/sdlc/captain.playbook/captain.playbook.ts +200 -14
  14. package/reference/sdlc/code.md +0 -1
  15. package/reference/sdlc/code.playbook/bin/interactive-session.js +170 -17
  16. package/reference/sdlc/code.playbook/bin/launch-config.js +136 -4
  17. package/reference/sdlc/code.playbook/bin/playbook.js +81 -4
  18. package/reference/sdlc/code.playbook/bin/repository-effects.js +2930 -0
  19. package/reference/sdlc/code.playbook/bin/run.js +365 -63
  20. package/reference/sdlc/code.playbook/bin/session-store.js +2877 -209
  21. package/reference/sdlc/code.playbook/code.fsm.d.ts +11 -1
  22. package/reference/sdlc/code.playbook/code.fsm.js +85 -29
  23. package/reference/sdlc/code.playbook/code.fsm.ts +95 -33
  24. package/reference/sdlc/code.playbook/code.gears.md +0 -2
  25. package/reference/sdlc/code.playbook/code.playbook.d.ts +5 -2
  26. package/reference/sdlc/code.playbook/code.playbook.js +67 -4
  27. package/reference/sdlc/code.playbook/code.playbook.ts +87 -8
  28. package/reference/sdlc/code.playbook/code.registry.d.ts +10 -3
  29. package/reference/sdlc/code.playbook/code.registry.js +10 -3
  30. package/reference/sdlc/code.playbook/code.registry.ts +23 -5
  31. package/reference/sdlc/code.playbook/playbook-captain.d.ts +99 -7
  32. package/reference/sdlc/code.playbook/playbook-captain.js +1894 -82
  33. package/reference/sdlc/code.playbook/playbook-captain.ts +2809 -109
  34. package/reference/sdlc/decide.md +0 -1
  35. package/reference/sdlc/decide.playbook/decide.fsm.d.ts +8 -1
  36. package/reference/sdlc/decide.playbook/decide.fsm.js +80 -29
  37. package/reference/sdlc/decide.playbook/decide.fsm.ts +89 -31
  38. package/reference/sdlc/decide.playbook/decide.gears.md +0 -1
  39. package/reference/sdlc/decide.playbook/decide.playbook.d.ts +15 -5
  40. package/reference/sdlc/decide.playbook/decide.playbook.js +1994 -191
  41. package/reference/sdlc/decide.playbook/decide.playbook.ts +3209 -404
  42. package/reference/sdlc/decide.playbook/decide.registry.d.ts +7 -3
  43. package/reference/sdlc/decide.playbook/decide.registry.js +10 -3
  44. package/reference/sdlc/decide.playbook/decide.registry.ts +20 -5
  45. package/reference/sdlc/review.playbook/review.fsm.d.ts +7 -0
  46. package/reference/sdlc/review.playbook/review.fsm.js +133 -12
  47. package/reference/sdlc/review.playbook/review.fsm.ts +140 -12
  48. package/reference/sdlc/review.playbook/review.playbook.d.ts +5 -2
  49. package/reference/sdlc/review.playbook/review.playbook.js +78 -4
  50. package/reference/sdlc/review.playbook/review.playbook.ts +95 -8
  51. package/reference/sdlc/review.playbook/review.registry.d.ts +10 -3
  52. package/reference/sdlc/review.playbook/review.registry.js +10 -3
  53. package/reference/sdlc/review.playbook/review.registry.ts +23 -5
  54. package/slc/gears2fsm.md +25 -7
  55. package/slc/link.md +727 -82
  56. package/src/accepted-outcome.d.ts +18 -0
  57. package/src/accepted-outcome.js +94 -0
  58. package/src/accepted-outcome.ts +140 -0
  59. package/src/runtime.d.ts +165 -3
  60. package/src/runtime.ts +214 -2
  61. package/src/xstate-playbook-runtime.d.ts +162 -13
  62. package/src/xstate-playbook-runtime.js +3344 -564
  63. package/src/xstate-playbook-runtime.ts +4873 -637
  64. package/src/xstate-runtime.d.ts +76 -8
  65. package/src/xstate-runtime.js +1001 -64
  66. package/src/xstate-runtime.ts +1640 -91
@@ -11,7 +11,14 @@ import { homedir } from 'node:os';
11
11
  import { resolve } from 'node:path';
12
12
  import { isDeepStrictEqual } from 'node:util';
13
13
  import { createTmuxPlayRuntime } from '@sublang/cligent/tmux-play';
14
- import { createPlaybookCaptainShell } from '../playbook-captain.js';
14
+ import {
15
+ assertPlaybookEffectLedger,
16
+ emptyPlaybookEffectLedger,
17
+ } from '../../../../src/xstate-runtime.js';
18
+ import {
19
+ assertPlaybookCaptainShellSnapshot,
20
+ createPlaybookCaptainShell,
21
+ } from '../playbook-captain.js';
15
22
  import {
16
23
  adapterSdkFailureLines,
17
24
  checkAdapterSdks,
@@ -20,11 +27,18 @@ import {
20
27
  } from './adapter-sdk.js';
21
28
  import {
22
29
  checkReadiness,
30
+ invalidRegistryEntryReason,
23
31
  loadLaunchPlan,
24
32
  projectHostAgent,
25
33
  resolveUserConfigPath,
34
+ snapshotRegistryEntry,
26
35
  } from './launch-config.js';
27
36
  import { prepareConfiguredRegistries } from './provision.js';
37
+ import {
38
+ createRepositoryEffectCapabilities,
39
+ refreshRepositoryEffectCapabilities,
40
+ recoverIncompleteRepositoryEffects,
41
+ } from './repository-effects.js';
28
42
  import {
29
43
  assertCaptainSessionExecutionCompatible,
30
44
  captainSessionSelectedMembers,
@@ -122,18 +136,28 @@ export async function runPlaybookRun(options = {}) {
122
136
  if (continuing) {
123
137
  try {
124
138
  if (args.sessionId === undefined) {
139
+ const invokingCwd = resolve(options.cwd ?? process.cwd());
125
140
  const selected = validateCaptainSessionRecord(
126
141
  await awaitWithAbort(
127
142
  store.latest({
128
- onLegacyRecord: ({ sessionId: legacyId, path }) =>
129
- writeStream(
143
+ preferredCwd: invokingCwd,
144
+ onLegacyRecord: (record) =>
145
+ reportSkippedCaptainSession(
130
146
  stderr,
131
- `playbook run: skipping legacy Captain session ${JSON.stringify(legacyId)} at ${JSON.stringify(path)} because schema 2 has incompatible player identity; move it outside the sessions directory or remove it to silence this warning\n`,
147
+ 'playbook run',
148
+ 'legacy',
149
+ record,
132
150
  ),
133
151
  }),
134
152
  options.signal,
135
153
  ),
136
154
  );
155
+ if (selected.cwd !== invokingCwd) {
156
+ await writeStream(
157
+ stderr,
158
+ `playbook run: no same-directory Captain session exists for invoking working directory ${JSON.stringify(invokingCwd)}; selecting globally newest Captain session ${JSON.stringify(selected.sessionId)} with stored working directory ${JSON.stringify(selected.cwd)}\n`,
159
+ );
160
+ }
137
161
  sessionId = selected.sessionId;
138
162
  } else {
139
163
  sessionId = args.sessionId;
@@ -141,7 +165,10 @@ export async function runPlaybookRun(options = {}) {
141
165
  throwIfAborted(options.signal);
142
166
  lease = await store.acquire(sessionId);
143
167
  throwIfAborted(options.signal);
144
- const authoritative = await lease.read();
168
+ const authoritative =
169
+ typeof lease.recoverUnresolvedEffectAbandonment === 'function'
170
+ ? await lease.recoverUnresolvedEffectAbandonment()
171
+ : await lease.read();
145
172
  throwIfAborted(options.signal);
146
173
  if (authoritative === undefined) {
147
174
  throw new Error(
@@ -424,12 +451,14 @@ export async function runPlaybookRun(options = {}) {
424
451
  }
425
452
 
426
453
  let settled;
454
+ let freshLaunchRecord;
427
455
  try {
428
456
  settled = await driveHeadlessCaptainTurn({
429
457
  config,
430
458
  input,
431
459
  sessionId,
432
460
  cwd,
461
+ sessionLease: lease,
433
462
  loadModule,
434
463
  stderr,
435
464
  verbose: args.verbose,
@@ -445,11 +474,62 @@ export async function runPlaybookRun(options = {}) {
445
474
  ...(options.createHostRuntime
446
475
  ? { createHostRuntime: options.createHostRuntime }
447
476
  : {}),
477
+ ...(options.createEffectLedgerWriteAhead
478
+ ? {
479
+ createEffectLedgerWriteAhead:
480
+ options.createEffectLedgerWriteAhead,
481
+ }
482
+ : {}),
448
483
  ...(restoreSnapshot !== undefined
449
484
  ? { restoreSnapshot }
450
485
  : {}),
486
+ ...(args.retryUncertain
487
+ ? { reconcileUncertainTurnReplay: true }
488
+ : {}),
451
489
  ...(options.signal ? { signal: options.signal } : {}),
452
- beforeBossTurn: async (baselineSnapshot) => {
490
+ beforeBossTurn: async (
491
+ baselineSnapshot,
492
+ shell,
493
+ reconcileRepositoryEffects,
494
+ ) => {
495
+ throwIfAborted(options.signal);
496
+ const installForLaunch =
497
+ options.installRetainedGenerationsForLaunch ??
498
+ installRetainedGenerationsForLaunch;
499
+ await installForLaunch({
500
+ lease,
501
+ shell,
502
+ ...(priorRecord === undefined
503
+ ? {
504
+ freshBoundary: {
505
+ cwd,
506
+ structuralProjection:
507
+ projectCaptainSessionStructure(config),
508
+ executionProjection: config,
509
+ snapshot: baselineSnapshot,
510
+ },
511
+ onFreshRecord(record) {
512
+ freshLaunchRecord = record;
513
+ },
514
+ onLegacyRecord: (record) =>
515
+ reportSkippedCaptainSession(
516
+ stderr,
517
+ 'playbook run',
518
+ 'legacy',
519
+ record,
520
+ ),
521
+ onInvalidRecord: (record) =>
522
+ reportSkippedCaptainSession(
523
+ stderr,
524
+ 'playbook run',
525
+ 'invalid',
526
+ record,
527
+ ),
528
+ }
529
+ : {}),
530
+ retainedGenerations: priorRecord?.retainedGenerations ?? {},
531
+ reconcileRepositoryEffects,
532
+ });
453
533
  throwIfAborted(options.signal);
454
534
  return args.retryUncertain
455
535
  ? lease.beginRetry({
@@ -460,22 +540,24 @@ export async function runPlaybookRun(options = {}) {
460
540
  input,
461
541
  attemptId,
462
542
  attemptedExecutionProjection: config,
463
- ...(priorRecord === undefined
464
- ? {
465
- fresh: {
466
- cwd,
467
- structuralProjection:
468
- projectCaptainSessionStructure(config),
469
- snapshot: baselineSnapshot,
470
- },
471
- }
472
- : {}),
473
543
  });
474
544
  },
475
545
  assertBeforeBossTurn: () => lease.assertOwner(),
476
546
  });
477
547
  } catch (error) {
478
548
  const cleanupIncomplete = isCaptainSessionHostCleanupIncomplete(error);
549
+ let abandonmentError;
550
+ if (
551
+ !cleanupIncomplete &&
552
+ freshLaunchRecord !== undefined &&
553
+ Object.keys(freshLaunchRecord.retainedGenerations ?? {}).length === 0
554
+ ) {
555
+ try {
556
+ await lease.abandonFreshSettled({ expected: freshLaunchRecord });
557
+ } catch (cause) {
558
+ abandonmentError = cause;
559
+ }
560
+ }
479
561
  const releaseError = cleanupIncomplete
480
562
  ? undefined
481
563
  : await releaseLease(lease);
@@ -492,9 +574,17 @@ export async function runPlaybookRun(options = {}) {
492
574
  `playbook run: cannot release Captain session lease: ${message(releaseError)}\n`,
493
575
  );
494
576
  }
577
+ if (abandonmentError !== undefined) {
578
+ await writeStream(
579
+ stderr,
580
+ `playbook run: cannot retract unused fresh Captain session: ${message(abandonmentError)}\n`,
581
+ );
582
+ }
495
583
  return {
496
584
  code:
497
- error instanceof HeadlessHostSetupError && releaseError === undefined
585
+ error instanceof HeadlessHostSetupError &&
586
+ abandonmentError === undefined &&
587
+ releaseError === undefined
498
588
  ? EXIT.argument
499
589
  : EXIT.turn,
500
590
  };
@@ -506,6 +596,8 @@ export async function runPlaybookRun(options = {}) {
506
596
  durableRecord = await lease.settle({
507
597
  attemptId: settled.uncertainRecord.uncertain.attemptId,
508
598
  snapshot: settled.snapshot,
599
+ unresolvedEffects: settled.unresolvedEffects,
600
+ retentionUpdates: settled.retentionUpdates,
509
601
  });
510
602
  } catch (error) {
511
603
  let cleanupError;
@@ -593,6 +685,7 @@ export async function driveHeadlessCaptainTurn({
593
685
  input,
594
686
  sessionId,
595
687
  cwd,
688
+ sessionLease,
596
689
  loadModule,
597
690
  stderr,
598
691
  verbose = false,
@@ -600,7 +693,9 @@ export async function driveHeadlessCaptainTurn({
600
693
  createCaptainRuntime,
601
694
  createCaptainSessionId,
602
695
  createHostRuntime = createTmuxPlayRuntime,
696
+ createEffectLedgerWriteAhead,
603
697
  restoreSnapshot,
698
+ reconcileUncertainTurnReplay = false,
604
699
  beforeBossTurn,
605
700
  assertBeforeBossTurn,
606
701
  signal,
@@ -616,6 +711,7 @@ export async function driveHeadlessCaptainTurn({
616
711
  config,
617
712
  sessionId,
618
713
  cwd,
714
+ sessionLease,
619
715
  loadModule,
620
716
  observers: [
621
717
  {
@@ -635,10 +731,20 @@ export async function driveHeadlessCaptainTurn({
635
731
  ...(createCaptainRuntime ? { createCaptainRuntime } : {}),
636
732
  ...(createCaptainSessionId ? { createCaptainSessionId } : {}),
637
733
  createHostRuntime,
734
+ ...(createEffectLedgerWriteAhead
735
+ ? { createEffectLedgerWriteAhead }
736
+ : {}),
638
737
  ...(restoreSnapshot !== undefined ? { restoreSnapshot } : {}),
738
+ ...(reconcileUncertainTurnReplay
739
+ ? { reconcileUncertainTurnReplay: true }
740
+ : {}),
639
741
  });
640
742
  ({ shell, host, snapshot: baselineSnapshot } = created);
641
- uncertainRecord = await beforeBossTurn?.(baselineSnapshot);
743
+ uncertainRecord = await beforeBossTurn?.(
744
+ baselineSnapshot,
745
+ shell,
746
+ created.reconcileRepositoryEffects,
747
+ );
642
748
  } catch (error) {
643
749
  throw new HeadlessHostSetupError(error);
644
750
  }
@@ -660,10 +766,11 @@ export async function driveHeadlessCaptainTurn({
660
766
  if (shell === undefined) {
661
767
  throw new Error('Captain shell host initialized without a shell');
662
768
  }
663
- const snapshot = shell.exportSnapshot();
664
- if (snapshot === undefined) {
665
- throw new Error('Captain turn settled without an exportable session snapshot');
769
+ const settlement = shell.exportSettlement();
770
+ if (settlement === undefined) {
771
+ throw new Error('Captain turn settled without an exportable session settlement');
666
772
  }
773
+ const { snapshot, unresolvedEffects, retentionUpdates } = settlement;
667
774
  if (
668
775
  snapshot.captain?.sessionId === sessionId ||
669
776
  snapshot.issuedSessionIds?.includes(sessionId)
@@ -676,6 +783,8 @@ export async function driveHeadlessCaptainTurn({
676
783
  sessionId,
677
784
  reply: replies[0],
678
785
  snapshot,
786
+ unresolvedEffects,
787
+ retentionUpdates,
679
788
  config: cloneJson(config),
680
789
  cwd,
681
790
  uncertainRecord,
@@ -711,6 +820,48 @@ export class CaptainSessionHostCleanupError extends AggregateError {
711
820
  }
712
821
  }
713
822
 
823
+ async function createStoreEffectLedgerService(lease) {
824
+ let mirror =
825
+ (await lease.read())?.effectLedger ?? emptyPlaybookEffectLedger();
826
+ mirror = assertPlaybookEffectLedger(mirror);
827
+ return Object.freeze({
828
+ snapshot: () => mirror,
829
+ async refresh() {
830
+ mirror = assertPlaybookEffectLedger(
831
+ (await lease.read())?.effectLedger ?? emptyPlaybookEffectLedger(),
832
+ );
833
+ return mirror;
834
+ },
835
+ async writeAhead(authority, commands) {
836
+ mirror = assertPlaybookEffectLedger(
837
+ await lease.writeEffectLedger(authority, commands),
838
+ );
839
+ return mirror;
840
+ },
841
+ });
842
+ }
843
+
844
+ function effectLedgerSnapshotFromCapabilities(hostCapabilities) {
845
+ const capabilities = Object.values(hostCapabilities);
846
+ if (capabilities.length === 0) return emptyPlaybookEffectLedger();
847
+ const mirror = assertPlaybookEffectLedger(
848
+ capabilities[0].effectLedger.snapshot(),
849
+ );
850
+ for (const capability of capabilities.slice(1)) {
851
+ if (
852
+ !isDeepStrictEqual(
853
+ assertPlaybookEffectLedger(capability.effectLedger.snapshot()),
854
+ mirror,
855
+ )
856
+ ) {
857
+ throw new Error(
858
+ 'schema-3 current-host capabilities disagree on their effect ledger',
859
+ );
860
+ }
861
+ }
862
+ return mirror;
863
+ }
864
+
714
865
  function isCaptainSessionHostCleanupIncomplete(error) {
715
866
  if (error instanceof CaptainSessionHostCleanupError) return true;
716
867
  return (
@@ -719,21 +870,145 @@ function isCaptainSessionHostCleanupIncomplete(error) {
719
870
  );
720
871
  }
721
872
 
873
+ function reconcileWholeTurnReplaySnapshot(snapshot, ledger, catalog) {
874
+ const checkpoint = assertPlaybookEffectLedger(snapshot.effectLedger);
875
+ const current = assertPlaybookEffectLedger(ledger);
876
+ const checkpointBoundaryCount = checkpoint.boundaries.length;
877
+ const currentPrefix = current.boundaries.slice(0, checkpointBoundaryCount);
878
+ if (!isDeepStrictEqual(currentPrefix, checkpoint.boundaries)) {
879
+ throw new Error(
880
+ 'Captain uncertain turn repository-effect reconciliation cannot restore a changed pre-turn boundary',
881
+ );
882
+ }
883
+ if (
884
+ !isDeepStrictEqual(
885
+ current.logicalOperations,
886
+ checkpoint.logicalOperations,
887
+ )
888
+ ) {
889
+ throw new Error(
890
+ 'Captain uncertain turn repository-effect reconciliation found deferred logical-operation progress and cannot replay the Boss turn',
891
+ );
892
+ }
893
+ const suffix = current.boundaries.slice(checkpointBoundaryCount);
894
+ const blocking = suffix.find(
895
+ (boundary) =>
896
+ boundary.physicalReceipt?.classification !== 'unchanged',
897
+ );
898
+ if (blocking !== undefined) {
899
+ const classification =
900
+ blocking.physicalReceipt?.classification ?? 'incomplete';
901
+ throw new Error(
902
+ `Captain uncertain turn repository-effect boundary ${JSON.stringify(blocking.boundaryId)} is ${classification}; whole-turn replay remains parked for reconciliation`,
903
+ );
904
+ }
905
+ const frames =
906
+ snapshot.mode !== 'engaged.parked'
907
+ ? undefined
908
+ : snapshot.frames.map((frame) => {
909
+ if (catalog[frame.playbookId]?.artifactSchema !== 3) {
910
+ throw new Error(
911
+ `Captain uncertain turn cannot rebase frame ${JSON.stringify(frame.playbookId)} without exact artifact-schema authority`,
912
+ );
913
+ }
914
+ return {
915
+ ...frame,
916
+ runtime: { ...frame.runtime, effectLedger: current },
917
+ };
918
+ });
919
+
920
+ return assertPlaybookCaptainShellSnapshot({
921
+ ...snapshot,
922
+ effectLedger: current,
923
+ ...(frames === undefined ? {} : { frames }),
924
+ });
925
+ }
926
+
722
927
  export async function createCaptainSessionHost({
723
928
  config,
724
929
  sessionId,
725
930
  cwd,
931
+ sessionLease,
726
932
  loadModule,
727
933
  observers,
728
934
  adapterImports,
729
935
  createCaptainRuntime,
730
936
  createCaptainSessionId,
731
937
  createHostRuntime = createTmuxPlayRuntime,
938
+ createEffectLedgerWriteAhead,
732
939
  restoreSnapshot,
940
+ reconcileUncertainTurnReplay = false,
733
941
  signal,
734
942
  }) {
943
+ const hostCapabilities = await createRepositoryEffectCapabilities({
944
+ cwd,
945
+ catalog: config.catalog,
946
+ sessionId,
947
+ sessionLease,
948
+ createWriteAhead:
949
+ createEffectLedgerWriteAhead ?? createStoreEffectLedgerService,
950
+ });
951
+ const reconcileRepositoryEffects = async () => {
952
+ await refreshRepositoryEffectCapabilities(hostCapabilities);
953
+ await recoverIncompleteRepositoryEffects({
954
+ catalog: config.catalog,
955
+ capabilities: hostCapabilities,
956
+ });
957
+ return effectLedgerSnapshotFromCapabilities(hostCapabilities);
958
+ };
959
+ try {
960
+ await reconcileRepositoryEffects();
961
+ } catch (error) {
962
+ if (!reconcileUncertainTurnReplay) throw error;
963
+ throw new Error(
964
+ `Captain uncertain turn repository-effect reconciliation failed: ${message(error)}`,
965
+ { cause: error },
966
+ );
967
+ }
968
+ const currentEffectLedger = () =>
969
+ effectLedgerSnapshotFromCapabilities(hostCapabilities);
970
+ let sourceSnapshot = restoreSnapshot;
971
+ if (
972
+ sourceSnapshot !== undefined &&
973
+ !isDeepStrictEqual(sourceSnapshot.effectLedger, currentEffectLedger())
974
+ ) {
975
+ const recovered =
976
+ typeof sessionLease.read === 'function'
977
+ ? await sessionLease.read()
978
+ : undefined;
979
+ if (
980
+ recovered?.state === 'settled' &&
981
+ isDeepStrictEqual(recovered.snapshot.effectLedger, currentEffectLedger())
982
+ ) {
983
+ sourceSnapshot = recovered.snapshot;
984
+ }
985
+ }
986
+ if (
987
+ sourceSnapshot !== undefined &&
988
+ reconcileUncertainTurnReplay
989
+ ) {
990
+ sourceSnapshot = reconcileWholeTurnReplaySnapshot(
991
+ sourceSnapshot,
992
+ currentEffectLedger(),
993
+ config.catalog,
994
+ );
995
+ } else if (
996
+ sourceSnapshot !== undefined &&
997
+ !isDeepStrictEqual(sourceSnapshot.effectLedger, currentEffectLedger())
998
+ ) {
999
+ throw new Error(
1000
+ 'Captain session effect ledger requires reconciliation before source-state restoration',
1001
+ );
1002
+ }
735
1003
  const shell = createPlaybookCaptainShell(captainOptionsFromConfig(config), {
736
1004
  loadModule,
1005
+ hostCapabilities,
1006
+ unresolvedEffectSettlement: {
1007
+ begin: (input) =>
1008
+ sessionLease.beginUnresolvedEffectAbandonment(input),
1009
+ complete: (input) =>
1010
+ sessionLease.completeUnresolvedEffectAbandonment(input),
1011
+ },
737
1012
  ...(createCaptainRuntime ? { createCaptainRuntime } : {}),
738
1013
  ...(createCaptainSessionId
739
1014
  ? { createSessionId: createCaptainSessionId }
@@ -741,7 +1016,7 @@ export async function createCaptainSessionHost({
741
1016
  });
742
1017
  let host;
743
1018
  try {
744
- const captain = captainHostBoundary(shell, restoreSnapshot);
1019
+ const captain = captainHostBoundary(shell, sourceSnapshot);
745
1020
  host = await createHostRuntime({
746
1021
  captain,
747
1022
  captainConfig: projectHostAgent(
@@ -763,16 +1038,13 @@ export async function createCaptainSessionHost({
763
1038
  'Captain shell initialized without an exportable session snapshot',
764
1039
  );
765
1040
  }
766
- if (
767
- restoreSnapshot !== undefined &&
768
- !isDeepStrictEqual(snapshot, restoreSnapshot)
769
- ) {
1041
+ if (sourceSnapshot !== undefined && !isDeepStrictEqual(snapshot, sourceSnapshot)) {
770
1042
  throw new Error('restored Captain snapshot changed before the Boss turn');
771
1043
  }
772
1044
  if (sessionId !== undefined) {
773
1045
  assertLogicalSessionIdDistinct({ sessionId, snapshot });
774
1046
  }
775
- return { shell, host, snapshot };
1047
+ return { shell, host, snapshot, reconcileRepositoryEffects };
776
1048
  } catch (error) {
777
1049
  let cleanupError;
778
1050
  try {
@@ -791,6 +1063,37 @@ export async function createCaptainSessionHost({
791
1063
  }
792
1064
  }
793
1065
 
1066
+ // PBCLI-55: both presentations install one authoritative retention map at the
1067
+ // same post-shell, pre-turn boundary. Fresh-session guarded initialization
1068
+ // publishes either the intentional empty boundary or the transferred map.
1069
+ export async function installRetainedGenerationsForLaunch({
1070
+ lease,
1071
+ shell,
1072
+ freshBoundary,
1073
+ onFreshRecord,
1074
+ onLegacyRecord,
1075
+ onInvalidRecord,
1076
+ retainedGenerations,
1077
+ reconcileRepositoryEffects,
1078
+ }) {
1079
+ let authoritative = retainedGenerations;
1080
+ if (freshBoundary !== undefined) {
1081
+ const record = await lease.initializeSettledWithPredecessor({
1082
+ ...freshBoundary,
1083
+ onLegacyRecord,
1084
+ onInvalidRecord,
1085
+ });
1086
+ authoritative = record.retainedGenerations ?? {};
1087
+ }
1088
+ await reconcileRepositoryEffects?.();
1089
+ if (freshBoundary !== undefined) {
1090
+ onFreshRecord?.(await lease.read());
1091
+ }
1092
+ await shell.installRetainedGenerations(authoritative);
1093
+ await lease.assertOwner();
1094
+ return authoritative;
1095
+ }
1096
+
794
1097
  // PBCLI-20: restoration enters through the host's one init boundary. The
795
1098
  // restored shell is fresh and receives restore instead of init, never both.
796
1099
  function captainHostBoundary(shell, restoreSnapshot) {
@@ -871,11 +1174,11 @@ export async function validateFrozenExecutionConfig(
871
1174
  for (const [id, item] of catalogItems) {
872
1175
  let entry;
873
1176
  try {
874
- entry = (await loadModule(item.from))?.default;
1177
+ entry = snapshotRegistryEntry((await loadModule(item.from))?.default);
875
1178
  } catch (cause) {
876
1179
  throw new Error(`stored playbook ${JSON.stringify(id)} failed to import: ${message(cause)}`);
877
1180
  }
878
- if (!isValidRegistryEntry(entry)) {
1181
+ if (invalidRegistryEntryReason(entry) !== undefined) {
879
1182
  throw new Error(`stored playbook ${JSON.stringify(id)} exposes no valid registry entry`);
880
1183
  }
881
1184
  if (
@@ -928,38 +1231,6 @@ function memoizedModuleLoader(loadModule) {
928
1231
  };
929
1232
  }
930
1233
 
931
- function isValidRegistryEntry(value) {
932
- if (
933
- value === null ||
934
- typeof value !== 'object' ||
935
- Array.isArray(value) ||
936
- typeof value.id !== 'string' ||
937
- value.id.trim().length === 0 ||
938
- typeof value.command !== 'string' ||
939
- value.command.trim().length === 0 ||
940
- typeof value.intent !== 'string' ||
941
- value.artifactSchema !== 2 ||
942
- !Array.isArray(value.requiredRoleIds) ||
943
- value.requiredRoleIds.some(
944
- (role) => typeof role !== 'string' || role.trim().length === 0,
945
- ) ||
946
- new Set(value.requiredRoleIds).size !== value.requiredRoleIds.length ||
947
- !Array.isArray(value.concurrentRoleSets) ||
948
- typeof value.validateOptions !== 'function' ||
949
- typeof value.createRuntime !== 'function'
950
- ) {
951
- return false;
952
- }
953
- const roles = new Set(value.requiredRoleIds);
954
- return value.concurrentRoleSets.every(
955
- (set) =>
956
- Array.isArray(set) &&
957
- set.length >= 2 &&
958
- set.every((role) => typeof role === 'string' && roles.has(role)) &&
959
- new Set(set).size === set.length,
960
- );
961
- }
962
-
963
1234
  export function captainOptionsFromConfig(config) {
964
1235
  return {
965
1236
  playbooks: Object.fromEntries(
@@ -1255,6 +1526,35 @@ async function reportUncertainSession(stderr, sessionId) {
1255
1526
  );
1256
1527
  }
1257
1528
 
1529
+ function legacyCaptainSessionReason(schemaVersion) {
1530
+ if (schemaVersion === 2) {
1531
+ return 'schema 2 has incompatible player identity';
1532
+ }
1533
+ if (schemaVersion === 3 || schemaVersion === 4) {
1534
+ return `schema ${JSON.stringify(schemaVersion)} predates the artifact-schema-3 effect-authority cutover and is not resumable`;
1535
+ }
1536
+ 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';
1538
+ }
1539
+ return `schema ${JSON.stringify(schemaVersion)} is unsupported`;
1540
+ }
1541
+
1542
+ export async function reportSkippedCaptainSession(
1543
+ stderr,
1544
+ commandName,
1545
+ kind,
1546
+ { sessionId, path, schemaVersion, reason },
1547
+ ) {
1548
+ const explanation =
1549
+ kind === 'legacy'
1550
+ ? legacyCaptainSessionReason(schemaVersion)
1551
+ : reason;
1552
+ await writeStream(
1553
+ stderr,
1554
+ `${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`,
1555
+ );
1556
+ }
1557
+
1258
1558
  function replayInvocation(argv, args, input) {
1259
1559
  if (args.retryUncertain || args.discardUncertain) {
1260
1560
  return ['run', ...argv];
@@ -1319,14 +1619,16 @@ function runHelpText(userConfigPath) {
1319
1619
  'provider conversation; distinct ids remain isolated.',
1320
1620
  'Legacy playbooks.<id>.players is rejected and is not auto-migrated,',
1321
1621
  'because choosing new ids decides sharing versus isolation.',
1322
- 'An ordinary continued run restores the stored structure and working',
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',
1323
1625
  'directory, then reads current config and overlays for model and effort.',
1324
1626
  'Uncertain retry instead uses its exact recorded input and settings.',
1325
1627
  '',
1326
1628
  'Options:',
1327
1629
  ' --with <path> overlay a generic config fragment (repeatable)',
1328
1630
  ' --no-provision do not provision thin filesystem registry engines',
1329
- ' --continue reply to the latest durable Captain session',
1631
+ ' --continue prefer this working directory, else global newest',
1330
1632
  ' --session <id> reply to one durable Captain session UUID',
1331
1633
  ' --retry-uncertain retry that session\'s exact recorded uncertain input',
1332
1634
  ' --discard-uncertain discard that session\'s uncertain attempt',