@sublang/playbook 9.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.
- package/docs/cli.md +51 -8
- package/docs/embedding.md +38 -12
- package/package.json +7 -3
- package/reference/sdlc/captain.md +14 -10
- package/reference/sdlc/captain.playbook/captain.fsm.d.ts +33 -13
- package/reference/sdlc/captain.playbook/captain.fsm.js +80 -9
- package/reference/sdlc/captain.playbook/captain.fsm.ts +137 -18
- package/reference/sdlc/captain.playbook/captain.gears.md +10 -6
- package/reference/sdlc/captain.playbook/captain.playbook.d.ts +5 -1
- package/reference/sdlc/captain.playbook/captain.playbook.js +140 -10
- package/reference/sdlc/captain.playbook/captain.playbook.ts +188 -16
- package/reference/sdlc/code.md +0 -1
- package/reference/sdlc/code.playbook/bin/interactive-session.js +170 -17
- package/reference/sdlc/code.playbook/bin/launch-config.js +136 -4
- package/reference/sdlc/code.playbook/bin/playbook.js +81 -4
- package/reference/sdlc/code.playbook/bin/repository-effects.js +2930 -0
- package/reference/sdlc/code.playbook/bin/run.js +365 -63
- package/reference/sdlc/code.playbook/bin/session-store.js +2877 -209
- package/reference/sdlc/code.playbook/code.fsm.d.ts +7 -0
- package/reference/sdlc/code.playbook/code.fsm.js +74 -25
- package/reference/sdlc/code.playbook/code.fsm.ts +83 -29
- package/reference/sdlc/code.playbook/code.gears.md +0 -2
- package/reference/sdlc/code.playbook/code.playbook.d.ts +5 -2
- package/reference/sdlc/code.playbook/code.playbook.js +54 -2
- package/reference/sdlc/code.playbook/code.playbook.ts +75 -6
- package/reference/sdlc/code.playbook/code.registry.d.ts +10 -3
- package/reference/sdlc/code.playbook/code.registry.js +10 -3
- package/reference/sdlc/code.playbook/code.registry.ts +23 -5
- package/reference/sdlc/code.playbook/playbook-captain.d.ts +99 -7
- package/reference/sdlc/code.playbook/playbook-captain.js +1850 -72
- package/reference/sdlc/code.playbook/playbook-captain.ts +2759 -96
- package/reference/sdlc/decide.md +0 -1
- package/reference/sdlc/decide.playbook/decide.fsm.d.ts +7 -0
- package/reference/sdlc/decide.playbook/decide.fsm.js +80 -29
- package/reference/sdlc/decide.playbook/decide.fsm.ts +89 -31
- package/reference/sdlc/decide.playbook/decide.gears.md +0 -1
- package/reference/sdlc/decide.playbook/decide.playbook.d.ts +13 -5
- package/reference/sdlc/decide.playbook/decide.playbook.js +1712 -91
- package/reference/sdlc/decide.playbook/decide.playbook.ts +2677 -136
- package/reference/sdlc/decide.playbook/decide.registry.d.ts +7 -3
- package/reference/sdlc/decide.playbook/decide.registry.js +10 -3
- package/reference/sdlc/decide.playbook/decide.registry.ts +20 -5
- package/reference/sdlc/review.playbook/review.fsm.d.ts +7 -0
- package/reference/sdlc/review.playbook/review.fsm.js +133 -12
- package/reference/sdlc/review.playbook/review.fsm.ts +140 -12
- package/reference/sdlc/review.playbook/review.playbook.d.ts +5 -2
- package/reference/sdlc/review.playbook/review.playbook.js +65 -2
- package/reference/sdlc/review.playbook/review.playbook.ts +83 -6
- package/reference/sdlc/review.playbook/review.registry.d.ts +10 -3
- package/reference/sdlc/review.playbook/review.registry.js +10 -3
- package/reference/sdlc/review.playbook/review.registry.ts +23 -5
- package/slc/gears2fsm.md +6 -5
- package/slc/link.md +544 -41
- package/src/accepted-outcome.d.ts +18 -0
- package/src/accepted-outcome.js +94 -0
- package/src/accepted-outcome.ts +140 -0
- package/src/runtime.d.ts +164 -3
- package/src/runtime.ts +213 -2
- package/src/xstate-playbook-runtime.d.ts +149 -10
- package/src/xstate-playbook-runtime.js +2569 -270
- package/src/xstate-playbook-runtime.ts +4133 -490
- package/src/xstate-runtime.d.ts +59 -1
- package/src/xstate-runtime.js +866 -7
- package/src/xstate-runtime.ts +1397 -7
|
@@ -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 {
|
|
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
|
-
|
|
129
|
-
|
|
143
|
+
preferredCwd: invokingCwd,
|
|
144
|
+
onLegacyRecord: (record) =>
|
|
145
|
+
reportSkippedCaptainSession(
|
|
130
146
|
stderr,
|
|
131
|
-
|
|
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 =
|
|
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 (
|
|
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 &&
|
|
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?.(
|
|
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
|
|
664
|
-
if (
|
|
665
|
-
throw new Error('Captain turn settled without an exportable session
|
|
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,
|
|
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 (
|
|
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
|
-
'
|
|
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
|
|
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',
|