@sublang/playbook 10.0.0 → 11.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/README.md +1 -1
- package/docs/cli.md +58 -13
- package/docs/configuration.md +89 -40
- package/docs/embedding.md +88 -0
- package/package.json +9 -2
- package/reference/sdlc/code.md +35 -15
- package/reference/sdlc/code.playbook/bin/interactive-session.js +58 -6
- package/reference/sdlc/code.playbook/bin/launch-config.js +499 -241
- package/reference/sdlc/code.playbook/bin/playbook.js +236 -187
- package/reference/sdlc/code.playbook/bin/replay-observer.js +221 -0
- package/reference/sdlc/code.playbook/bin/run.js +355 -203
- package/reference/sdlc/code.playbook/bin/session-store.js +1512 -136
- package/reference/sdlc/code.playbook/playbook-captain.d.ts +4 -1
- package/reference/sdlc/code.playbook/playbook-captain.js +21 -3
- package/reference/sdlc/code.playbook/playbook-captain.ts +42 -6
- package/reference/sdlc/code.playbook/playbook.config.template.yaml +14 -10
- package/reference/sdlc/code.playbook/session-store.d.ts +82 -0
- package/reference/sdlc/code.playbook/session-store.js +113 -0
- package/reference/sdlc/decide.md +24 -15
- package/reference/sdlc/review.md +36 -18
|
@@ -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
|
|
10
|
-
import { homedir } from
|
|
11
|
-
import { resolve } from
|
|
12
|
-
import { isDeepStrictEqual } from
|
|
13
|
-
import {
|
|
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
|
|
18
|
+
} from "../../../../src/xstate-runtime.js";
|
|
18
19
|
import {
|
|
19
20
|
assertPlaybookCaptainShellSnapshot,
|
|
20
21
|
createPlaybookCaptainShell,
|
|
21
|
-
} from
|
|
22
|
+
} from "../playbook-captain.js";
|
|
22
23
|
import {
|
|
23
24
|
adapterSdkFailureLines,
|
|
24
25
|
checkAdapterSdks,
|
|
25
26
|
mappedSdksFor,
|
|
26
27
|
probeAdapterSdk,
|
|
27
|
-
} from
|
|
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
|
|
36
|
-
import { prepareConfiguredRegistries } from
|
|
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
|
|
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
|
|
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 =
|
|
67
|
+
this.name = "HeadlessHostSetupError";
|
|
58
68
|
this.cause = cause;
|
|
59
69
|
}
|
|
60
70
|
}
|
|
61
71
|
const RETIRED_FLAGS = new Set([
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
148
|
-
|
|
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 ===
|
|
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 ===
|
|
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 !==
|
|
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
|
-
|
|
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
|
-
|
|
518
|
-
|
|
584
|
+
"playbook run",
|
|
585
|
+
"legacy",
|
|
519
586
|
record,
|
|
520
587
|
),
|
|
521
588
|
onInvalidRecord: (record) =>
|
|
522
589
|
reportSkippedCaptainSession(
|
|
523
590
|
stderr,
|
|
524
|
-
|
|
525
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 ===
|
|
788
|
+
if (record.type === "captain_reply") {
|
|
720
789
|
replies.push(record.text);
|
|
721
|
-
} else if (record.type ===
|
|
790
|
+
} else if (record.type === "captain_status") {
|
|
722
791
|
await writeStream(stderr, `${record.message}\n`);
|
|
723
|
-
} else if (verbose && record.type ===
|
|
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(
|
|
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] !==
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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 =
|
|
819
|
-
this.code =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 ??
|
|
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 !==
|
|
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 ===
|
|
1044
|
+
typeof sessionLease.read === "function"
|
|
977
1045
|
? await sessionLease.read()
|
|
978
1046
|
: undefined;
|
|
979
1047
|
if (
|
|
980
|
-
recovered?.state ===
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1102
|
+
"Captain shell initialized without an exportable session snapshot",
|
|
1039
1103
|
);
|
|
1040
1104
|
}
|
|
1041
|
-
if (
|
|
1042
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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 ===
|
|
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 ===
|
|
1293
|
-
else if (arg ===
|
|
1294
|
-
else if (arg ===
|
|
1295
|
-
else if (arg ===
|
|
1296
|
-
else if (arg ===
|
|
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(
|
|
1410
|
+
throw new Error("--retry-uncertain may be specified only once");
|
|
1299
1411
|
}
|
|
1300
1412
|
parsed.retryUncertain = true;
|
|
1301
|
-
} else if (arg ===
|
|
1413
|
+
} else if (arg === "--discard-uncertain") {
|
|
1302
1414
|
if (parsed.discardUncertain) {
|
|
1303
|
-
throw new Error(
|
|
1415
|
+
throw new Error("--discard-uncertain may be specified only once");
|
|
1304
1416
|
}
|
|
1305
1417
|
parsed.discardUncertain = true;
|
|
1306
|
-
}
|
|
1307
|
-
|
|
1308
|
-
|
|
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 ===
|
|
1422
|
+
} else if (arg === "--session") {
|
|
1311
1423
|
const value = argv[index + 1];
|
|
1312
|
-
if (value === undefined || value ===
|
|
1313
|
-
throw new Error(
|
|
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(
|
|
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(
|
|
1321
|
-
const value = arg.slice(
|
|
1322
|
-
if (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(
|
|
1436
|
+
throw new Error("--session may be specified only once");
|
|
1325
1437
|
}
|
|
1326
1438
|
parsed.sessionId = value;
|
|
1327
|
-
} else if (arg ===
|
|
1439
|
+
} else if (arg === "--with") {
|
|
1328
1440
|
const value = argv[index + 1];
|
|
1329
|
-
if (value === undefined || value ===
|
|
1330
|
-
throw new Error(
|
|
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(
|
|
1335
|
-
const value = arg.slice(
|
|
1336
|
-
if (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(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
1469
|
-
signal.addEventListener(
|
|
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(
|
|
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(
|
|
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:
|
|
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 !==
|
|
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
|
-
|
|
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(
|
|
1677
|
+
"",
|
|
1678
|
+
].join("\n"),
|
|
1526
1679
|
);
|
|
1527
1680
|
}
|
|
1528
1681
|
|
|
1529
1682
|
function legacyCaptainSessionReason(schemaVersion) {
|
|
1530
1683
|
if (schemaVersion === 2) {
|
|
1531
|
-
return
|
|
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
|
|
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 ===
|
|
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 [
|
|
1711
|
+
return ["run", ...argv];
|
|
1561
1712
|
}
|
|
1562
|
-
if (args.input !== undefined) return [
|
|
1713
|
+
if (args.input !== undefined) return ["run", ...argv];
|
|
1563
1714
|
return args.terminated
|
|
1564
|
-
? [
|
|
1565
|
-
: [
|
|
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(
|
|
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 !==
|
|
1733
|
+
if (ready !== false || typeof stream.once !== "function") return;
|
|
1583
1734
|
await new Promise((resolvePromise, rejectPromise) => {
|
|
1584
1735
|
const onDrain = () => {
|
|
1585
|
-
stream.off?.(
|
|
1736
|
+
stream.off?.("error", onError);
|
|
1586
1737
|
resolvePromise();
|
|
1587
1738
|
};
|
|
1588
1739
|
const onError = (error) => {
|
|
1589
|
-
stream.off?.(
|
|
1740
|
+
stream.off?.("drain", onDrain);
|
|
1590
1741
|
rejectPromise(error);
|
|
1591
1742
|
};
|
|
1592
|
-
stream.once(
|
|
1593
|
-
stream.once(
|
|
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
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
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
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
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
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
].join(
|
|
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) {
|