@sublang/playbook 9.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 +109 -21
- package/docs/configuration.md +89 -40
- package/docs/embedding.md +126 -12
- package/package.json +14 -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 +35 -16
- package/reference/sdlc/code.playbook/bin/interactive-session.js +228 -23
- package/reference/sdlc/code.playbook/bin/launch-config.js +611 -221
- package/reference/sdlc/code.playbook/bin/playbook.js +304 -178
- package/reference/sdlc/code.playbook/bin/replay-observer.js +221 -0
- package/reference/sdlc/code.playbook/bin/repository-effects.js +2930 -0
- package/reference/sdlc/code.playbook/bin/run.js +669 -215
- package/reference/sdlc/code.playbook/bin/session-store.js +4546 -502
- 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 +103 -8
- package/reference/sdlc/code.playbook/playbook-captain.js +1871 -75
- package/reference/sdlc/code.playbook/playbook-captain.ts +2801 -102
- 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 -16
- 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.md +36 -18
- 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
|
@@ -6,51 +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 {
|
|
14
|
-
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";
|
|
15
|
+
import {
|
|
16
|
+
assertPlaybookEffectLedger,
|
|
17
|
+
emptyPlaybookEffectLedger,
|
|
18
|
+
} from "../../../../src/xstate-runtime.js";
|
|
19
|
+
import {
|
|
20
|
+
assertPlaybookCaptainShellSnapshot,
|
|
21
|
+
createPlaybookCaptainShell,
|
|
22
|
+
} from "../playbook-captain.js";
|
|
15
23
|
import {
|
|
16
24
|
adapterSdkFailureLines,
|
|
17
25
|
checkAdapterSdks,
|
|
18
26
|
mappedSdksFor,
|
|
19
27
|
probeAdapterSdk,
|
|
20
|
-
} from
|
|
28
|
+
} from "./adapter-sdk.js";
|
|
21
29
|
import {
|
|
22
30
|
checkReadiness,
|
|
31
|
+
invalidRegistryEntryReason,
|
|
23
32
|
loadLaunchPlan,
|
|
24
33
|
projectHostAgent,
|
|
34
|
+
resolveLaunchSessionsDir,
|
|
35
|
+
relocateLegacyUserConfig,
|
|
36
|
+
resolveLegacyUserConfigPath,
|
|
25
37
|
resolveUserConfigPath,
|
|
26
|
-
|
|
27
|
-
|
|
38
|
+
snapshotRegistryEntry,
|
|
39
|
+
} from "./launch-config.js";
|
|
40
|
+
import { prepareConfiguredRegistries } from "./provision.js";
|
|
41
|
+
import {
|
|
42
|
+
createReplayRecordObserver,
|
|
43
|
+
replayIncompleteMessage,
|
|
44
|
+
} from "./replay-observer.js";
|
|
45
|
+
import {
|
|
46
|
+
createRepositoryEffectCapabilities,
|
|
47
|
+
refreshRepositoryEffectCapabilities,
|
|
48
|
+
recoverIncompleteRepositoryEffects,
|
|
49
|
+
} from "./repository-effects.js";
|
|
28
50
|
import {
|
|
29
51
|
assertCaptainSessionExecutionCompatible,
|
|
52
|
+
assertCaptainSessionsDirectoryUsable,
|
|
30
53
|
captainSessionSelectedMembers,
|
|
31
54
|
createCaptainSessionStore,
|
|
32
55
|
projectCaptainSessionStructure,
|
|
33
56
|
SESSION_ID_PATTERN,
|
|
34
57
|
validateCaptainSessionExecutionProjection,
|
|
35
58
|
validateCaptainSessionRecord,
|
|
36
|
-
} from
|
|
59
|
+
} from "./session-store.js";
|
|
37
60
|
|
|
38
61
|
const EXIT = { ok: 0, argument: 1, turn: 2 };
|
|
39
62
|
const UUID_PATTERN = SESSION_ID_PATTERN;
|
|
63
|
+
const HEADLESS_REPLAY_CHANNELS = new WeakMap();
|
|
40
64
|
class HeadlessHostSetupError extends Error {
|
|
41
65
|
constructor(cause) {
|
|
42
66
|
super(message(cause));
|
|
43
|
-
this.name =
|
|
67
|
+
this.name = "HeadlessHostSetupError";
|
|
44
68
|
this.cause = cause;
|
|
45
69
|
}
|
|
46
70
|
}
|
|
47
71
|
const RETIRED_FLAGS = new Set([
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
72
|
+
"--player",
|
|
73
|
+
"--captain",
|
|
74
|
+
"--option",
|
|
75
|
+
"--cwd",
|
|
76
|
+
"--last",
|
|
77
|
+
"--config",
|
|
54
78
|
]);
|
|
55
79
|
|
|
56
80
|
export async function runPlaybookRun(options = {}) {
|
|
@@ -67,15 +91,25 @@ export async function runPlaybookRun(options = {}) {
|
|
|
67
91
|
}
|
|
68
92
|
if (args.help) {
|
|
69
93
|
const env = options.env ?? process.env;
|
|
70
|
-
const home =
|
|
94
|
+
const home =
|
|
95
|
+
options.homeDir ??
|
|
96
|
+
(typeof env.HOME === "string" && env.HOME.trim().length > 0
|
|
97
|
+
? env.HOME
|
|
98
|
+
: homedir());
|
|
71
99
|
const userConfigPath =
|
|
72
100
|
options.userConfigPath ?? resolveUserConfigPath(env, home);
|
|
101
|
+
// PBCLI-17: help resolves the path but writes nothing, so it neither
|
|
102
|
+
// seeds nor relocates.
|
|
73
103
|
await writeStream(stdout, runHelpText(userConfigPath));
|
|
74
104
|
return { code: EXIT.ok };
|
|
75
105
|
}
|
|
76
106
|
|
|
77
107
|
const env = options.env ?? process.env;
|
|
78
|
-
const home =
|
|
108
|
+
const home =
|
|
109
|
+
options.homeDir ??
|
|
110
|
+
(typeof env.HOME === "string" && env.HOME.trim().length > 0
|
|
111
|
+
? env.HOME
|
|
112
|
+
: homedir());
|
|
79
113
|
const recovering = args.retryUncertain || args.discardUncertain;
|
|
80
114
|
const continuing = args.continue || args.sessionId !== undefined;
|
|
81
115
|
let input = args.input;
|
|
@@ -90,6 +124,47 @@ export async function runPlaybookRun(options = {}) {
|
|
|
90
124
|
input = resolvedInput.input;
|
|
91
125
|
}
|
|
92
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
|
+
|
|
93
168
|
let store;
|
|
94
169
|
try {
|
|
95
170
|
store =
|
|
@@ -97,7 +172,7 @@ export async function runPlaybookRun(options = {}) {
|
|
|
97
172
|
createCaptainSessionStore({
|
|
98
173
|
env,
|
|
99
174
|
homeDir: home,
|
|
100
|
-
|
|
175
|
+
sessionsDir: resolvedSessionsDir,
|
|
101
176
|
...(options.now ? { now: options.now } : {}),
|
|
102
177
|
...(options.createSessionTempId
|
|
103
178
|
? { createTempId: options.createSessionTempId }
|
|
@@ -114,6 +189,7 @@ export async function runPlaybookRun(options = {}) {
|
|
|
114
189
|
let config;
|
|
115
190
|
let cwd;
|
|
116
191
|
let restoreSnapshot;
|
|
192
|
+
let replayChannel;
|
|
117
193
|
const loadModule = memoizedModuleLoader(
|
|
118
194
|
options.loadModule ?? ((specifier) => import(specifier)),
|
|
119
195
|
);
|
|
@@ -122,26 +198,45 @@ export async function runPlaybookRun(options = {}) {
|
|
|
122
198
|
if (continuing) {
|
|
123
199
|
try {
|
|
124
200
|
if (args.sessionId === undefined) {
|
|
201
|
+
const invokingCwd = resolve(options.cwd ?? process.cwd());
|
|
125
202
|
const selected = validateCaptainSessionRecord(
|
|
126
203
|
await awaitWithAbort(
|
|
127
204
|
store.latest({
|
|
128
|
-
|
|
129
|
-
|
|
205
|
+
preferredCwd: invokingCwd,
|
|
206
|
+
onLegacyRecord: (record) =>
|
|
207
|
+
reportSkippedCaptainSession(
|
|
130
208
|
stderr,
|
|
131
|
-
|
|
209
|
+
"playbook run",
|
|
210
|
+
"legacy",
|
|
211
|
+
record,
|
|
132
212
|
),
|
|
133
213
|
}),
|
|
134
214
|
options.signal,
|
|
135
215
|
),
|
|
136
216
|
);
|
|
217
|
+
if (selected.cwd !== invokingCwd) {
|
|
218
|
+
await writeStream(
|
|
219
|
+
stderr,
|
|
220
|
+
`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`,
|
|
221
|
+
);
|
|
222
|
+
}
|
|
137
223
|
sessionId = selected.sessionId;
|
|
138
224
|
} else {
|
|
139
225
|
sessionId = args.sessionId;
|
|
140
226
|
}
|
|
141
227
|
throwIfAborted(options.signal);
|
|
142
228
|
lease = await store.acquire(sessionId);
|
|
229
|
+
replayChannel = createHeadlessReplayChannel({
|
|
230
|
+
lease,
|
|
231
|
+
sessionId,
|
|
232
|
+
stderr,
|
|
233
|
+
});
|
|
234
|
+
await reportHeadlessReplay(replayChannel);
|
|
143
235
|
throwIfAborted(options.signal);
|
|
144
|
-
const authoritative =
|
|
236
|
+
const authoritative =
|
|
237
|
+
typeof lease.recoverUnresolvedEffectAbandonment === "function"
|
|
238
|
+
? await lease.recoverUnresolvedEffectAbandonment()
|
|
239
|
+
: await lease.read();
|
|
145
240
|
throwIfAborted(options.signal);
|
|
146
241
|
if (authoritative === undefined) {
|
|
147
242
|
throw new Error(
|
|
@@ -163,7 +258,7 @@ export async function runPlaybookRun(options = {}) {
|
|
|
163
258
|
return { code: EXIT.argument };
|
|
164
259
|
}
|
|
165
260
|
|
|
166
|
-
if (priorRecord.state ===
|
|
261
|
+
if (priorRecord.state === "uncertain") {
|
|
167
262
|
if (args.discardUncertain) {
|
|
168
263
|
try {
|
|
169
264
|
throwIfAborted(options.signal);
|
|
@@ -244,10 +339,8 @@ export async function runPlaybookRun(options = {}) {
|
|
|
244
339
|
}
|
|
245
340
|
|
|
246
341
|
if (!continuing || !args.retryUncertain) {
|
|
247
|
-
const userConfigPath =
|
|
248
|
-
options.userConfigPath ?? resolveUserConfigPath(env, home);
|
|
249
342
|
let plan;
|
|
250
|
-
const configNotices = [];
|
|
343
|
+
const configNotices = [...bootstrapConfigNotices];
|
|
251
344
|
try {
|
|
252
345
|
throwIfAborted(options.signal);
|
|
253
346
|
plan = await loadLaunchPlan({
|
|
@@ -289,7 +382,7 @@ export async function runPlaybookRun(options = {}) {
|
|
|
289
382
|
);
|
|
290
383
|
} else {
|
|
291
384
|
sessionId = (options.createLogicalSessionId ?? randomUUID)();
|
|
292
|
-
if (typeof sessionId !==
|
|
385
|
+
if (typeof sessionId !== "string" || !UUID_PATTERN.test(sessionId)) {
|
|
293
386
|
throw new Error(
|
|
294
387
|
`logical session id generator returned a non-UUID value: ${JSON.stringify(sessionId)}`,
|
|
295
388
|
);
|
|
@@ -392,6 +485,12 @@ export async function runPlaybookRun(options = {}) {
|
|
|
392
485
|
try {
|
|
393
486
|
throwIfAborted(options.signal);
|
|
394
487
|
lease = await store.acquire(sessionId);
|
|
488
|
+
replayChannel = createHeadlessReplayChannel({
|
|
489
|
+
lease,
|
|
490
|
+
sessionId,
|
|
491
|
+
stderr,
|
|
492
|
+
});
|
|
493
|
+
await reportHeadlessReplay(replayChannel);
|
|
395
494
|
throwIfAborted(options.signal);
|
|
396
495
|
} catch (error) {
|
|
397
496
|
const releaseError = await releaseLease(lease);
|
|
@@ -424,12 +523,15 @@ export async function runPlaybookRun(options = {}) {
|
|
|
424
523
|
}
|
|
425
524
|
|
|
426
525
|
let settled;
|
|
526
|
+
let freshLaunchRecord;
|
|
427
527
|
try {
|
|
428
528
|
settled = await driveHeadlessCaptainTurn({
|
|
429
529
|
config,
|
|
430
530
|
input,
|
|
431
531
|
sessionId,
|
|
432
532
|
cwd,
|
|
533
|
+
sessionLease: lease,
|
|
534
|
+
replayObserver: replayChannel?.observer,
|
|
433
535
|
loadModule,
|
|
434
536
|
stderr,
|
|
435
537
|
verbose: args.verbose,
|
|
@@ -445,11 +547,56 @@ export async function runPlaybookRun(options = {}) {
|
|
|
445
547
|
...(options.createHostRuntime
|
|
446
548
|
? { createHostRuntime: options.createHostRuntime }
|
|
447
549
|
: {}),
|
|
448
|
-
...(
|
|
449
|
-
? {
|
|
550
|
+
...(options.createEffectLedgerWriteAhead
|
|
551
|
+
? {
|
|
552
|
+
createEffectLedgerWriteAhead: options.createEffectLedgerWriteAhead,
|
|
553
|
+
}
|
|
450
554
|
: {}),
|
|
555
|
+
...(restoreSnapshot !== undefined ? { restoreSnapshot } : {}),
|
|
556
|
+
...(args.retryUncertain ? { reconcileUncertainTurnReplay: true } : {}),
|
|
451
557
|
...(options.signal ? { signal: options.signal } : {}),
|
|
452
|
-
beforeBossTurn: async (
|
|
558
|
+
beforeBossTurn: async (
|
|
559
|
+
baselineSnapshot,
|
|
560
|
+
shell,
|
|
561
|
+
reconcileRepositoryEffects,
|
|
562
|
+
) => {
|
|
563
|
+
throwIfAborted(options.signal);
|
|
564
|
+
const installForLaunch =
|
|
565
|
+
options.installRetainedGenerationsForLaunch ??
|
|
566
|
+
installRetainedGenerationsForLaunch;
|
|
567
|
+
await installForLaunch({
|
|
568
|
+
lease,
|
|
569
|
+
shell,
|
|
570
|
+
...(priorRecord === undefined
|
|
571
|
+
? {
|
|
572
|
+
freshBoundary: {
|
|
573
|
+
cwd,
|
|
574
|
+
structuralProjection: projectCaptainSessionStructure(config),
|
|
575
|
+
executionProjection: config,
|
|
576
|
+
snapshot: baselineSnapshot,
|
|
577
|
+
},
|
|
578
|
+
onFreshRecord(record) {
|
|
579
|
+
freshLaunchRecord = record;
|
|
580
|
+
},
|
|
581
|
+
onLegacyRecord: (record) =>
|
|
582
|
+
reportSkippedCaptainSession(
|
|
583
|
+
stderr,
|
|
584
|
+
"playbook run",
|
|
585
|
+
"legacy",
|
|
586
|
+
record,
|
|
587
|
+
),
|
|
588
|
+
onInvalidRecord: (record) =>
|
|
589
|
+
reportSkippedCaptainSession(
|
|
590
|
+
stderr,
|
|
591
|
+
"playbook run",
|
|
592
|
+
"invalid",
|
|
593
|
+
record,
|
|
594
|
+
),
|
|
595
|
+
}
|
|
596
|
+
: {}),
|
|
597
|
+
retainedGenerations: priorRecord?.retainedGenerations ?? {},
|
|
598
|
+
reconcileRepositoryEffects,
|
|
599
|
+
});
|
|
453
600
|
throwIfAborted(options.signal);
|
|
454
601
|
return args.retryUncertain
|
|
455
602
|
? lease.beginRetry({
|
|
@@ -460,22 +607,26 @@ export async function runPlaybookRun(options = {}) {
|
|
|
460
607
|
input,
|
|
461
608
|
attemptId,
|
|
462
609
|
attemptedExecutionProjection: config,
|
|
463
|
-
...(priorRecord === undefined
|
|
464
|
-
? {
|
|
465
|
-
fresh: {
|
|
466
|
-
cwd,
|
|
467
|
-
structuralProjection:
|
|
468
|
-
projectCaptainSessionStructure(config),
|
|
469
|
-
snapshot: baselineSnapshot,
|
|
470
|
-
},
|
|
471
|
-
}
|
|
472
|
-
: {}),
|
|
473
610
|
});
|
|
474
611
|
},
|
|
475
612
|
assertBeforeBossTurn: () => lease.assertOwner(),
|
|
476
613
|
});
|
|
614
|
+
await reportHeadlessReplay(replayChannel);
|
|
477
615
|
} catch (error) {
|
|
616
|
+
await reportHeadlessReplay(replayChannel);
|
|
478
617
|
const cleanupIncomplete = isCaptainSessionHostCleanupIncomplete(error);
|
|
618
|
+
let abandonmentError;
|
|
619
|
+
if (
|
|
620
|
+
!cleanupIncomplete &&
|
|
621
|
+
freshLaunchRecord !== undefined &&
|
|
622
|
+
Object.keys(freshLaunchRecord.retainedGenerations ?? {}).length === 0
|
|
623
|
+
) {
|
|
624
|
+
try {
|
|
625
|
+
await lease.abandonFreshSettled({ expected: freshLaunchRecord });
|
|
626
|
+
} catch (cause) {
|
|
627
|
+
abandonmentError = cause;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
479
630
|
const releaseError = cleanupIncomplete
|
|
480
631
|
? undefined
|
|
481
632
|
: await releaseLease(lease);
|
|
@@ -483,7 +634,7 @@ export async function runPlaybookRun(options = {}) {
|
|
|
483
634
|
if (cleanupIncomplete) {
|
|
484
635
|
await writeStream(
|
|
485
636
|
stderr,
|
|
486
|
-
|
|
637
|
+
"playbook run: writer lease retained until process exit because host cleanup was incomplete\n",
|
|
487
638
|
);
|
|
488
639
|
}
|
|
489
640
|
if (releaseError !== undefined) {
|
|
@@ -492,9 +643,17 @@ export async function runPlaybookRun(options = {}) {
|
|
|
492
643
|
`playbook run: cannot release Captain session lease: ${message(releaseError)}\n`,
|
|
493
644
|
);
|
|
494
645
|
}
|
|
646
|
+
if (abandonmentError !== undefined) {
|
|
647
|
+
await writeStream(
|
|
648
|
+
stderr,
|
|
649
|
+
`playbook run: cannot retract unused fresh Captain session: ${message(abandonmentError)}\n`,
|
|
650
|
+
);
|
|
651
|
+
}
|
|
495
652
|
return {
|
|
496
653
|
code:
|
|
497
|
-
error instanceof HeadlessHostSetupError &&
|
|
654
|
+
error instanceof HeadlessHostSetupError &&
|
|
655
|
+
abandonmentError === undefined &&
|
|
656
|
+
releaseError === undefined
|
|
498
657
|
? EXIT.argument
|
|
499
658
|
: EXIT.turn,
|
|
500
659
|
};
|
|
@@ -506,8 +665,12 @@ export async function runPlaybookRun(options = {}) {
|
|
|
506
665
|
durableRecord = await lease.settle({
|
|
507
666
|
attemptId: settled.uncertainRecord.uncertain.attemptId,
|
|
508
667
|
snapshot: settled.snapshot,
|
|
668
|
+
unresolvedEffects: settled.unresolvedEffects,
|
|
669
|
+
retentionUpdates: settled.retentionUpdates,
|
|
509
670
|
});
|
|
671
|
+
await reportHeadlessReplay(replayChannel);
|
|
510
672
|
} catch (error) {
|
|
673
|
+
await reportHeadlessReplay(replayChannel);
|
|
511
674
|
let cleanupError;
|
|
512
675
|
try {
|
|
513
676
|
await settled.dispose();
|
|
@@ -523,13 +686,10 @@ export async function runPlaybookRun(options = {}) {
|
|
|
523
686
|
[error, cleanupError],
|
|
524
687
|
`Captain session settlement failed (${message(error)}) and host cleanup also failed: ${message(cleanupError)}`,
|
|
525
688
|
);
|
|
689
|
+
await writeStream(stderr, `playbook run: ${message(cleanupFailure)}\n`);
|
|
526
690
|
await writeStream(
|
|
527
691
|
stderr,
|
|
528
|
-
|
|
529
|
-
);
|
|
530
|
-
await writeStream(
|
|
531
|
-
stderr,
|
|
532
|
-
'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",
|
|
533
693
|
);
|
|
534
694
|
return { code: EXIT.turn };
|
|
535
695
|
}
|
|
@@ -555,7 +715,7 @@ export async function runPlaybookRun(options = {}) {
|
|
|
555
715
|
if (options.signal?.aborted) {
|
|
556
716
|
await writeStream(
|
|
557
717
|
stderr,
|
|
558
|
-
|
|
718
|
+
"playbook run: Captain turn was interrupted; reply withheld\n",
|
|
559
719
|
);
|
|
560
720
|
return { code: EXIT.turn, sessionId, record: durableRecord };
|
|
561
721
|
}
|
|
@@ -593,6 +753,8 @@ export async function driveHeadlessCaptainTurn({
|
|
|
593
753
|
input,
|
|
594
754
|
sessionId,
|
|
595
755
|
cwd,
|
|
756
|
+
sessionLease,
|
|
757
|
+
replayObserver,
|
|
596
758
|
loadModule,
|
|
597
759
|
stderr,
|
|
598
760
|
verbose = false,
|
|
@@ -600,7 +762,9 @@ export async function driveHeadlessCaptainTurn({
|
|
|
600
762
|
createCaptainRuntime,
|
|
601
763
|
createCaptainSessionId,
|
|
602
764
|
createHostRuntime = createTmuxPlayRuntime,
|
|
765
|
+
createEffectLedgerWriteAhead,
|
|
603
766
|
restoreSnapshot,
|
|
767
|
+
reconcileUncertainTurnReplay = false,
|
|
604
768
|
beforeBossTurn,
|
|
605
769
|
assertBeforeBossTurn,
|
|
606
770
|
signal,
|
|
@@ -616,41 +780,53 @@ export async function driveHeadlessCaptainTurn({
|
|
|
616
780
|
config,
|
|
617
781
|
sessionId,
|
|
618
782
|
cwd,
|
|
783
|
+
sessionLease,
|
|
619
784
|
loadModule,
|
|
620
785
|
observers: [
|
|
621
786
|
{
|
|
622
787
|
async onRecord(record) {
|
|
623
|
-
if (record.type ===
|
|
788
|
+
if (record.type === "captain_reply") {
|
|
624
789
|
replies.push(record.text);
|
|
625
|
-
} else if (record.type ===
|
|
790
|
+
} else if (record.type === "captain_status") {
|
|
626
791
|
await writeStream(stderr, `${record.message}\n`);
|
|
627
|
-
} else if (verbose && record.type ===
|
|
792
|
+
} else if (verbose && record.type === "captain_telemetry") {
|
|
628
793
|
await writeStream(stderr, `\u00b7 ${record.topic}\n`);
|
|
629
794
|
}
|
|
630
795
|
},
|
|
631
796
|
},
|
|
797
|
+
...(replayObserver === undefined ? [] : [replayObserver]),
|
|
632
798
|
],
|
|
633
799
|
...(signal ? { signal } : {}),
|
|
634
800
|
...(adapterImports ? { adapterImports } : {}),
|
|
635
801
|
...(createCaptainRuntime ? { createCaptainRuntime } : {}),
|
|
636
802
|
...(createCaptainSessionId ? { createCaptainSessionId } : {}),
|
|
637
803
|
createHostRuntime,
|
|
804
|
+
...(createEffectLedgerWriteAhead
|
|
805
|
+
? { createEffectLedgerWriteAhead }
|
|
806
|
+
: {}),
|
|
638
807
|
...(restoreSnapshot !== undefined ? { restoreSnapshot } : {}),
|
|
808
|
+
...(reconcileUncertainTurnReplay
|
|
809
|
+
? { reconcileUncertainTurnReplay: true }
|
|
810
|
+
: {}),
|
|
639
811
|
});
|
|
640
812
|
({ shell, host, snapshot: baselineSnapshot } = created);
|
|
641
|
-
uncertainRecord = await beforeBossTurn?.(
|
|
813
|
+
uncertainRecord = await beforeBossTurn?.(
|
|
814
|
+
baselineSnapshot,
|
|
815
|
+
shell,
|
|
816
|
+
created.reconcileRepositoryEffects,
|
|
817
|
+
);
|
|
642
818
|
} catch (error) {
|
|
643
819
|
throw new HeadlessHostSetupError(error);
|
|
644
820
|
}
|
|
645
821
|
await assertBeforeBossTurn?.();
|
|
646
822
|
if (signal?.aborted) {
|
|
647
|
-
throw signal.reason ?? new Error(
|
|
823
|
+
throw signal.reason ?? new Error("Captain turn aborted");
|
|
648
824
|
}
|
|
649
825
|
await host.runBossTurn(input);
|
|
650
826
|
throwIfAborted(signal);
|
|
651
827
|
if (
|
|
652
828
|
replies.length !== 1 ||
|
|
653
|
-
typeof replies[0] !==
|
|
829
|
+
typeof replies[0] !== "string" ||
|
|
654
830
|
replies[0].trim().length === 0
|
|
655
831
|
) {
|
|
656
832
|
throw new Error(
|
|
@@ -658,24 +834,29 @@ export async function driveHeadlessCaptainTurn({
|
|
|
658
834
|
);
|
|
659
835
|
}
|
|
660
836
|
if (shell === undefined) {
|
|
661
|
-
throw new Error(
|
|
837
|
+
throw new Error("Captain shell host initialized without a shell");
|
|
662
838
|
}
|
|
663
|
-
const
|
|
664
|
-
if (
|
|
665
|
-
throw new Error(
|
|
839
|
+
const settlement = shell.exportSettlement();
|
|
840
|
+
if (settlement === undefined) {
|
|
841
|
+
throw new Error(
|
|
842
|
+
"Captain turn settled without an exportable session settlement",
|
|
843
|
+
);
|
|
666
844
|
}
|
|
845
|
+
const { snapshot, unresolvedEffects, retentionUpdates } = settlement;
|
|
667
846
|
if (
|
|
668
847
|
snapshot.captain?.sessionId === sessionId ||
|
|
669
848
|
snapshot.issuedSessionIds?.includes(sessionId)
|
|
670
849
|
) {
|
|
671
850
|
throw new Error(
|
|
672
|
-
|
|
851
|
+
"logical session id collided with an internal Captain session id",
|
|
673
852
|
);
|
|
674
853
|
}
|
|
675
854
|
return {
|
|
676
855
|
sessionId,
|
|
677
856
|
reply: replies[0],
|
|
678
857
|
snapshot,
|
|
858
|
+
unresolvedEffects,
|
|
859
|
+
retentionUpdates,
|
|
679
860
|
config: cloneJson(config),
|
|
680
861
|
cwd,
|
|
681
862
|
uncertainRecord,
|
|
@@ -706,9 +887,51 @@ export async function driveHeadlessCaptainTurn({
|
|
|
706
887
|
export class CaptainSessionHostCleanupError extends AggregateError {
|
|
707
888
|
constructor(errors, messageText) {
|
|
708
889
|
super(errors, messageText);
|
|
709
|
-
this.name =
|
|
710
|
-
this.code =
|
|
890
|
+
this.name = "CaptainSessionHostCleanupError";
|
|
891
|
+
this.code = "PLAYBOOK_CAPTAIN_HOST_CLEANUP_INCOMPLETE";
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
async function createStoreEffectLedgerService(lease) {
|
|
896
|
+
let mirror =
|
|
897
|
+
(await lease.read())?.effectLedger ?? emptyPlaybookEffectLedger();
|
|
898
|
+
mirror = assertPlaybookEffectLedger(mirror);
|
|
899
|
+
return Object.freeze({
|
|
900
|
+
snapshot: () => mirror,
|
|
901
|
+
async refresh() {
|
|
902
|
+
mirror = assertPlaybookEffectLedger(
|
|
903
|
+
(await lease.read())?.effectLedger ?? emptyPlaybookEffectLedger(),
|
|
904
|
+
);
|
|
905
|
+
return mirror;
|
|
906
|
+
},
|
|
907
|
+
async writeAhead(authority, commands) {
|
|
908
|
+
mirror = assertPlaybookEffectLedger(
|
|
909
|
+
await lease.writeEffectLedger(authority, commands),
|
|
910
|
+
);
|
|
911
|
+
return mirror;
|
|
912
|
+
},
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
function effectLedgerSnapshotFromCapabilities(hostCapabilities) {
|
|
917
|
+
const capabilities = Object.values(hostCapabilities);
|
|
918
|
+
if (capabilities.length === 0) return emptyPlaybookEffectLedger();
|
|
919
|
+
const mirror = assertPlaybookEffectLedger(
|
|
920
|
+
capabilities[0].effectLedger.snapshot(),
|
|
921
|
+
);
|
|
922
|
+
for (const capability of capabilities.slice(1)) {
|
|
923
|
+
if (
|
|
924
|
+
!isDeepStrictEqual(
|
|
925
|
+
assertPlaybookEffectLedger(capability.effectLedger.snapshot()),
|
|
926
|
+
mirror,
|
|
927
|
+
)
|
|
928
|
+
) {
|
|
929
|
+
throw new Error(
|
|
930
|
+
"schema-3 current-host capabilities disagree on their effect ledger",
|
|
931
|
+
);
|
|
932
|
+
}
|
|
711
933
|
}
|
|
934
|
+
return mirror;
|
|
712
935
|
}
|
|
713
936
|
|
|
714
937
|
function isCaptainSessionHostCleanupIncomplete(error) {
|
|
@@ -719,21 +942,137 @@ function isCaptainSessionHostCleanupIncomplete(error) {
|
|
|
719
942
|
);
|
|
720
943
|
}
|
|
721
944
|
|
|
945
|
+
function reconcileWholeTurnReplaySnapshot(snapshot, ledger, catalog) {
|
|
946
|
+
const checkpoint = assertPlaybookEffectLedger(snapshot.effectLedger);
|
|
947
|
+
const current = assertPlaybookEffectLedger(ledger);
|
|
948
|
+
const checkpointBoundaryCount = checkpoint.boundaries.length;
|
|
949
|
+
const currentPrefix = current.boundaries.slice(0, checkpointBoundaryCount);
|
|
950
|
+
if (!isDeepStrictEqual(currentPrefix, checkpoint.boundaries)) {
|
|
951
|
+
throw new Error(
|
|
952
|
+
"Captain uncertain turn repository-effect reconciliation cannot restore a changed pre-turn boundary",
|
|
953
|
+
);
|
|
954
|
+
}
|
|
955
|
+
if (
|
|
956
|
+
!isDeepStrictEqual(current.logicalOperations, checkpoint.logicalOperations)
|
|
957
|
+
) {
|
|
958
|
+
throw new Error(
|
|
959
|
+
"Captain uncertain turn repository-effect reconciliation found deferred logical-operation progress and cannot replay the Boss turn",
|
|
960
|
+
);
|
|
961
|
+
}
|
|
962
|
+
const suffix = current.boundaries.slice(checkpointBoundaryCount);
|
|
963
|
+
const blocking = suffix.find(
|
|
964
|
+
(boundary) => boundary.physicalReceipt?.classification !== "unchanged",
|
|
965
|
+
);
|
|
966
|
+
if (blocking !== undefined) {
|
|
967
|
+
const classification =
|
|
968
|
+
blocking.physicalReceipt?.classification ?? "incomplete";
|
|
969
|
+
throw new Error(
|
|
970
|
+
`Captain uncertain turn repository-effect boundary ${JSON.stringify(blocking.boundaryId)} is ${classification}; whole-turn replay remains parked for reconciliation`,
|
|
971
|
+
);
|
|
972
|
+
}
|
|
973
|
+
const frames =
|
|
974
|
+
snapshot.mode !== "engaged.parked"
|
|
975
|
+
? undefined
|
|
976
|
+
: snapshot.frames.map((frame) => {
|
|
977
|
+
if (catalog[frame.playbookId]?.artifactSchema !== 3) {
|
|
978
|
+
throw new Error(
|
|
979
|
+
`Captain uncertain turn cannot rebase frame ${JSON.stringify(frame.playbookId)} without exact artifact-schema authority`,
|
|
980
|
+
);
|
|
981
|
+
}
|
|
982
|
+
return {
|
|
983
|
+
...frame,
|
|
984
|
+
runtime: { ...frame.runtime, effectLedger: current },
|
|
985
|
+
};
|
|
986
|
+
});
|
|
987
|
+
|
|
988
|
+
return assertPlaybookCaptainShellSnapshot({
|
|
989
|
+
...snapshot,
|
|
990
|
+
effectLedger: current,
|
|
991
|
+
...(frames === undefined ? {} : { frames }),
|
|
992
|
+
});
|
|
993
|
+
}
|
|
994
|
+
|
|
722
995
|
export async function createCaptainSessionHost({
|
|
723
996
|
config,
|
|
724
997
|
sessionId,
|
|
725
998
|
cwd,
|
|
999
|
+
sessionLease,
|
|
726
1000
|
loadModule,
|
|
727
1001
|
observers,
|
|
728
1002
|
adapterImports,
|
|
729
1003
|
createCaptainRuntime,
|
|
730
1004
|
createCaptainSessionId,
|
|
731
1005
|
createHostRuntime = createTmuxPlayRuntime,
|
|
1006
|
+
createEffectLedgerWriteAhead,
|
|
732
1007
|
restoreSnapshot,
|
|
1008
|
+
reconcileUncertainTurnReplay = false,
|
|
733
1009
|
signal,
|
|
734
1010
|
}) {
|
|
1011
|
+
const hostCapabilities = await createRepositoryEffectCapabilities({
|
|
1012
|
+
cwd,
|
|
1013
|
+
catalog: config.catalog,
|
|
1014
|
+
sessionId,
|
|
1015
|
+
sessionLease,
|
|
1016
|
+
createWriteAhead:
|
|
1017
|
+
createEffectLedgerWriteAhead ?? createStoreEffectLedgerService,
|
|
1018
|
+
});
|
|
1019
|
+
const reconcileRepositoryEffects = async () => {
|
|
1020
|
+
await refreshRepositoryEffectCapabilities(hostCapabilities);
|
|
1021
|
+
await recoverIncompleteRepositoryEffects({
|
|
1022
|
+
catalog: config.catalog,
|
|
1023
|
+
capabilities: hostCapabilities,
|
|
1024
|
+
});
|
|
1025
|
+
return effectLedgerSnapshotFromCapabilities(hostCapabilities);
|
|
1026
|
+
};
|
|
1027
|
+
try {
|
|
1028
|
+
await reconcileRepositoryEffects();
|
|
1029
|
+
} catch (error) {
|
|
1030
|
+
if (!reconcileUncertainTurnReplay) throw error;
|
|
1031
|
+
throw new Error(
|
|
1032
|
+
`Captain uncertain turn repository-effect reconciliation failed: ${message(error)}`,
|
|
1033
|
+
{ cause: error },
|
|
1034
|
+
);
|
|
1035
|
+
}
|
|
1036
|
+
const currentEffectLedger = () =>
|
|
1037
|
+
effectLedgerSnapshotFromCapabilities(hostCapabilities);
|
|
1038
|
+
let sourceSnapshot = restoreSnapshot;
|
|
1039
|
+
if (
|
|
1040
|
+
sourceSnapshot !== undefined &&
|
|
1041
|
+
!isDeepStrictEqual(sourceSnapshot.effectLedger, currentEffectLedger())
|
|
1042
|
+
) {
|
|
1043
|
+
const recovered =
|
|
1044
|
+
typeof sessionLease.read === "function"
|
|
1045
|
+
? await sessionLease.read()
|
|
1046
|
+
: undefined;
|
|
1047
|
+
if (
|
|
1048
|
+
recovered?.state === "settled" &&
|
|
1049
|
+
isDeepStrictEqual(recovered.snapshot.effectLedger, currentEffectLedger())
|
|
1050
|
+
) {
|
|
1051
|
+
sourceSnapshot = recovered.snapshot;
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
if (sourceSnapshot !== undefined && reconcileUncertainTurnReplay) {
|
|
1055
|
+
sourceSnapshot = reconcileWholeTurnReplaySnapshot(
|
|
1056
|
+
sourceSnapshot,
|
|
1057
|
+
currentEffectLedger(),
|
|
1058
|
+
config.catalog,
|
|
1059
|
+
);
|
|
1060
|
+
} else if (
|
|
1061
|
+
sourceSnapshot !== undefined &&
|
|
1062
|
+
!isDeepStrictEqual(sourceSnapshot.effectLedger, currentEffectLedger())
|
|
1063
|
+
) {
|
|
1064
|
+
throw new Error(
|
|
1065
|
+
"Captain session effect ledger requires reconciliation before source-state restoration",
|
|
1066
|
+
);
|
|
1067
|
+
}
|
|
735
1068
|
const shell = createPlaybookCaptainShell(captainOptionsFromConfig(config), {
|
|
736
1069
|
loadModule,
|
|
1070
|
+
hostCapabilities,
|
|
1071
|
+
unresolvedEffectSettlement: {
|
|
1072
|
+
begin: (input) => sessionLease.beginUnresolvedEffectAbandonment(input),
|
|
1073
|
+
complete: (input) =>
|
|
1074
|
+
sessionLease.completeUnresolvedEffectAbandonment(input),
|
|
1075
|
+
},
|
|
737
1076
|
...(createCaptainRuntime ? { createCaptainRuntime } : {}),
|
|
738
1077
|
...(createCaptainSessionId
|
|
739
1078
|
? { createSessionId: createCaptainSessionId }
|
|
@@ -741,12 +1080,12 @@ export async function createCaptainSessionHost({
|
|
|
741
1080
|
});
|
|
742
1081
|
let host;
|
|
743
1082
|
try {
|
|
744
|
-
const captain = captainHostBoundary(shell,
|
|
1083
|
+
const captain = captainHostBoundary(shell, sourceSnapshot);
|
|
745
1084
|
host = await createHostRuntime({
|
|
746
1085
|
captain,
|
|
747
1086
|
captainConfig: projectHostAgent(
|
|
748
1087
|
config.captain,
|
|
749
|
-
|
|
1088
|
+
"Captain execution config.captain",
|
|
750
1089
|
),
|
|
751
1090
|
players: config.players.map(({ id, ...agent }) => ({
|
|
752
1091
|
id,
|
|
@@ -760,19 +1099,19 @@ export async function createCaptainSessionHost({
|
|
|
760
1099
|
const snapshot = shell.exportSnapshot();
|
|
761
1100
|
if (snapshot === undefined) {
|
|
762
1101
|
throw new Error(
|
|
763
|
-
|
|
1102
|
+
"Captain shell initialized without an exportable session snapshot",
|
|
764
1103
|
);
|
|
765
1104
|
}
|
|
766
1105
|
if (
|
|
767
|
-
|
|
768
|
-
!isDeepStrictEqual(snapshot,
|
|
1106
|
+
sourceSnapshot !== undefined &&
|
|
1107
|
+
!isDeepStrictEqual(snapshot, sourceSnapshot)
|
|
769
1108
|
) {
|
|
770
|
-
throw new Error(
|
|
1109
|
+
throw new Error("restored Captain snapshot changed before the Boss turn");
|
|
771
1110
|
}
|
|
772
1111
|
if (sessionId !== undefined) {
|
|
773
1112
|
assertLogicalSessionIdDistinct({ sessionId, snapshot });
|
|
774
1113
|
}
|
|
775
|
-
return { shell, host, snapshot };
|
|
1114
|
+
return { shell, host, snapshot, reconcileRepositoryEffects };
|
|
776
1115
|
} catch (error) {
|
|
777
1116
|
let cleanupError;
|
|
778
1117
|
try {
|
|
@@ -791,6 +1130,37 @@ export async function createCaptainSessionHost({
|
|
|
791
1130
|
}
|
|
792
1131
|
}
|
|
793
1132
|
|
|
1133
|
+
// PBCLI-55: both presentations install one authoritative retention map at the
|
|
1134
|
+
// same post-shell, pre-turn boundary. Fresh-session guarded initialization
|
|
1135
|
+
// publishes either the intentional empty boundary or the transferred map.
|
|
1136
|
+
export async function installRetainedGenerationsForLaunch({
|
|
1137
|
+
lease,
|
|
1138
|
+
shell,
|
|
1139
|
+
freshBoundary,
|
|
1140
|
+
onFreshRecord,
|
|
1141
|
+
onLegacyRecord,
|
|
1142
|
+
onInvalidRecord,
|
|
1143
|
+
retainedGenerations,
|
|
1144
|
+
reconcileRepositoryEffects,
|
|
1145
|
+
}) {
|
|
1146
|
+
let authoritative = retainedGenerations;
|
|
1147
|
+
if (freshBoundary !== undefined) {
|
|
1148
|
+
const record = await lease.initializeSettledWithPredecessor({
|
|
1149
|
+
...freshBoundary,
|
|
1150
|
+
onLegacyRecord,
|
|
1151
|
+
onInvalidRecord,
|
|
1152
|
+
});
|
|
1153
|
+
authoritative = record.retainedGenerations ?? {};
|
|
1154
|
+
}
|
|
1155
|
+
await reconcileRepositoryEffects?.();
|
|
1156
|
+
if (freshBoundary !== undefined) {
|
|
1157
|
+
onFreshRecord?.(await lease.read());
|
|
1158
|
+
}
|
|
1159
|
+
await shell.installRetainedGenerations(authoritative);
|
|
1160
|
+
await lease.assertOwner();
|
|
1161
|
+
return authoritative;
|
|
1162
|
+
}
|
|
1163
|
+
|
|
794
1164
|
// PBCLI-20: restoration enters through the host's one init boundary. The
|
|
795
1165
|
// restored shell is fresh and receives restore instead of init, never both.
|
|
796
1166
|
function captainHostBoundary(shell, restoreSnapshot) {
|
|
@@ -844,6 +1214,7 @@ export async function validateFrozenExecutionConfig(
|
|
|
844
1214
|
structuralProjection,
|
|
845
1215
|
executionProjection,
|
|
846
1216
|
);
|
|
1217
|
+
assertFrozenFastModesSupported(config);
|
|
847
1218
|
const catalogItems = Object.entries(config.catalog);
|
|
848
1219
|
|
|
849
1220
|
// Preserve the complete-catalog preparation transaction: prepare every
|
|
@@ -859,7 +1230,9 @@ export async function validateFrozenExecutionConfig(
|
|
|
859
1230
|
authoredFrom: item.from,
|
|
860
1231
|
});
|
|
861
1232
|
} catch (cause) {
|
|
862
|
-
throw new Error(
|
|
1233
|
+
throw new Error(
|
|
1234
|
+
`stored playbook ${JSON.stringify(id)} failed to prepare: ${message(cause)}`,
|
|
1235
|
+
);
|
|
863
1236
|
}
|
|
864
1237
|
if (prepared !== undefined && prepared !== item.from) {
|
|
865
1238
|
throw new Error(
|
|
@@ -871,12 +1244,16 @@ export async function validateFrozenExecutionConfig(
|
|
|
871
1244
|
for (const [id, item] of catalogItems) {
|
|
872
1245
|
let entry;
|
|
873
1246
|
try {
|
|
874
|
-
entry = (await loadModule(item.from))?.default;
|
|
1247
|
+
entry = snapshotRegistryEntry((await loadModule(item.from))?.default);
|
|
875
1248
|
} catch (cause) {
|
|
876
|
-
throw new Error(
|
|
1249
|
+
throw new Error(
|
|
1250
|
+
`stored playbook ${JSON.stringify(id)} failed to import: ${message(cause)}`,
|
|
1251
|
+
);
|
|
877
1252
|
}
|
|
878
|
-
if (
|
|
879
|
-
throw new Error(
|
|
1253
|
+
if (invalidRegistryEntryReason(entry) !== undefined) {
|
|
1254
|
+
throw new Error(
|
|
1255
|
+
`stored playbook ${JSON.stringify(id)} exposes no valid registry entry`,
|
|
1256
|
+
);
|
|
880
1257
|
}
|
|
881
1258
|
if (
|
|
882
1259
|
entry.id !== id ||
|
|
@@ -884,10 +1261,7 @@ export async function validateFrozenExecutionConfig(
|
|
|
884
1261
|
entry.intent !== item.intent ||
|
|
885
1262
|
entry.artifactSchema !== item.artifactSchema ||
|
|
886
1263
|
!isDeepStrictEqual(entry.requiredRoleIds, item.requiredRoleIds) ||
|
|
887
|
-
!isDeepStrictEqual(
|
|
888
|
-
entry.concurrentRoleSets,
|
|
889
|
-
item.concurrentRoleSets,
|
|
890
|
-
)
|
|
1264
|
+
!isDeepStrictEqual(entry.concurrentRoleSets, item.concurrentRoleSets)
|
|
891
1265
|
) {
|
|
892
1266
|
throw new Error(
|
|
893
1267
|
`stored playbook ${JSON.stringify(id)} no longer matches its recorded manifest identity`,
|
|
@@ -897,6 +1271,44 @@ export async function validateFrozenExecutionConfig(
|
|
|
897
1271
|
return config;
|
|
898
1272
|
}
|
|
899
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
|
+
|
|
900
1312
|
function adaptersFromExecutionConfig(config) {
|
|
901
1313
|
return [
|
|
902
1314
|
...new Set([
|
|
@@ -913,7 +1325,7 @@ function assertLogicalSessionIdDistinct(record) {
|
|
|
913
1325
|
record.snapshot.issuedSessionIds.includes(record.sessionId))
|
|
914
1326
|
) {
|
|
915
1327
|
throw new Error(
|
|
916
|
-
|
|
1328
|
+
"logical session id collides with an internal Captain session id",
|
|
917
1329
|
);
|
|
918
1330
|
}
|
|
919
1331
|
}
|
|
@@ -922,44 +1334,15 @@ function memoizedModuleLoader(loadModule) {
|
|
|
922
1334
|
const modules = new Map();
|
|
923
1335
|
return (specifier) => {
|
|
924
1336
|
if (!modules.has(specifier)) {
|
|
925
|
-
modules.set(
|
|
1337
|
+
modules.set(
|
|
1338
|
+
specifier,
|
|
1339
|
+
Promise.resolve().then(() => loadModule(specifier)),
|
|
1340
|
+
);
|
|
926
1341
|
}
|
|
927
1342
|
return modules.get(specifier);
|
|
928
1343
|
};
|
|
929
1344
|
}
|
|
930
1345
|
|
|
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
1346
|
export function captainOptionsFromConfig(config) {
|
|
964
1347
|
return {
|
|
965
1348
|
playbooks: Object.fromEntries(
|
|
@@ -979,7 +1362,7 @@ export function captainOptionsFromConfig(config) {
|
|
|
979
1362
|
config.players.map(({ id, ...agent }) => [id, cloneJson(agent)]),
|
|
980
1363
|
),
|
|
981
1364
|
},
|
|
982
|
-
...(typeof config.captain.adapter ===
|
|
1365
|
+
...(typeof config.captain.adapter === "string" &&
|
|
983
1366
|
config.captain.adapter.length > 0
|
|
984
1367
|
? { captainAdapter: config.captain.adapter }
|
|
985
1368
|
: {}),
|
|
@@ -1013,65 +1396,65 @@ export function parseRunArgs(argv) {
|
|
|
1013
1396
|
const positionals = [];
|
|
1014
1397
|
for (let index = 0; index < argv.length; index += 1) {
|
|
1015
1398
|
const arg = argv[index];
|
|
1016
|
-
if (arg ===
|
|
1399
|
+
if (arg === "--") {
|
|
1017
1400
|
parsed.terminated = true;
|
|
1018
1401
|
positionals.push(...argv.slice(index + 1));
|
|
1019
1402
|
break;
|
|
1020
1403
|
}
|
|
1021
|
-
if (arg ===
|
|
1022
|
-
else if (arg ===
|
|
1023
|
-
else if (arg ===
|
|
1024
|
-
else if (arg ===
|
|
1025
|
-
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") {
|
|
1026
1409
|
if (parsed.retryUncertain) {
|
|
1027
|
-
throw new Error(
|
|
1410
|
+
throw new Error("--retry-uncertain may be specified only once");
|
|
1028
1411
|
}
|
|
1029
1412
|
parsed.retryUncertain = true;
|
|
1030
|
-
} else if (arg ===
|
|
1413
|
+
} else if (arg === "--discard-uncertain") {
|
|
1031
1414
|
if (parsed.discardUncertain) {
|
|
1032
|
-
throw new Error(
|
|
1415
|
+
throw new Error("--discard-uncertain may be specified only once");
|
|
1033
1416
|
}
|
|
1034
1417
|
parsed.discardUncertain = true;
|
|
1035
|
-
}
|
|
1036
|
-
|
|
1037
|
-
|
|
1418
|
+
} else if (arg === "--continue") {
|
|
1419
|
+
if (parsed.continue)
|
|
1420
|
+
throw new Error("--continue may be specified only once");
|
|
1038
1421
|
parsed.continue = true;
|
|
1039
|
-
} else if (arg ===
|
|
1422
|
+
} else if (arg === "--session") {
|
|
1040
1423
|
const value = argv[index + 1];
|
|
1041
|
-
if (value === undefined || value ===
|
|
1042
|
-
throw new Error(
|
|
1424
|
+
if (value === undefined || value === "") {
|
|
1425
|
+
throw new Error("--session needs a UUID value");
|
|
1043
1426
|
}
|
|
1044
1427
|
if (parsed.sessionId !== undefined) {
|
|
1045
|
-
throw new Error(
|
|
1428
|
+
throw new Error("--session may be specified only once");
|
|
1046
1429
|
}
|
|
1047
1430
|
parsed.sessionId = value;
|
|
1048
1431
|
index += 1;
|
|
1049
|
-
} else if (arg.startsWith(
|
|
1050
|
-
const value = arg.slice(
|
|
1051
|
-
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");
|
|
1052
1435
|
if (parsed.sessionId !== undefined) {
|
|
1053
|
-
throw new Error(
|
|
1436
|
+
throw new Error("--session may be specified only once");
|
|
1054
1437
|
}
|
|
1055
1438
|
parsed.sessionId = value;
|
|
1056
|
-
} else if (arg ===
|
|
1439
|
+
} else if (arg === "--with") {
|
|
1057
1440
|
const value = argv[index + 1];
|
|
1058
|
-
if (value === undefined || value ===
|
|
1059
|
-
throw new Error(
|
|
1441
|
+
if (value === undefined || value === "") {
|
|
1442
|
+
throw new Error("--with needs a value");
|
|
1060
1443
|
}
|
|
1061
1444
|
parsed.withPaths.push(value);
|
|
1062
1445
|
index += 1;
|
|
1063
|
-
} else if (arg.startsWith(
|
|
1064
|
-
const value = arg.slice(
|
|
1065
|
-
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");
|
|
1066
1449
|
parsed.withPaths.push(value);
|
|
1067
1450
|
} else if (
|
|
1068
1451
|
RETIRED_FLAGS.has(arg) ||
|
|
1069
1452
|
[...RETIRED_FLAGS].some((flag) => arg.startsWith(`${flag}=`))
|
|
1070
1453
|
) {
|
|
1071
1454
|
throw new Error(
|
|
1072
|
-
`${arg.split(
|
|
1455
|
+
`${arg.split("=")[0]} was removed; configure the shared Captain session in playbook.config.yaml or a --with overlay`,
|
|
1073
1456
|
);
|
|
1074
|
-
} else if (arg.startsWith(
|
|
1457
|
+
} else if (arg.startsWith("-")) {
|
|
1075
1458
|
throw new Error(`unknown option ${arg}`);
|
|
1076
1459
|
} else {
|
|
1077
1460
|
positionals.push(arg);
|
|
@@ -1079,15 +1462,15 @@ export function parseRunArgs(argv) {
|
|
|
1079
1462
|
}
|
|
1080
1463
|
if (positionals.length > 1) {
|
|
1081
1464
|
throw new Error(
|
|
1082
|
-
|
|
1465
|
+
"expected at most one [input] argument; quote multi-word input as one shell argument",
|
|
1083
1466
|
);
|
|
1084
1467
|
}
|
|
1085
1468
|
if (parsed.continue && parsed.sessionId !== undefined) {
|
|
1086
|
-
throw new Error(
|
|
1469
|
+
throw new Error("--continue and --session are mutually exclusive");
|
|
1087
1470
|
}
|
|
1088
1471
|
if (parsed.retryUncertain && parsed.discardUncertain) {
|
|
1089
1472
|
throw new Error(
|
|
1090
|
-
|
|
1473
|
+
"--retry-uncertain and --discard-uncertain are mutually exclusive",
|
|
1091
1474
|
);
|
|
1092
1475
|
}
|
|
1093
1476
|
if (
|
|
@@ -1095,7 +1478,7 @@ export function parseRunArgs(argv) {
|
|
|
1095
1478
|
parsed.sessionId === undefined
|
|
1096
1479
|
) {
|
|
1097
1480
|
throw new Error(
|
|
1098
|
-
|
|
1481
|
+
"--retry-uncertain and --discard-uncertain require --session <id>",
|
|
1099
1482
|
);
|
|
1100
1483
|
}
|
|
1101
1484
|
if (
|
|
@@ -1103,7 +1486,7 @@ export function parseRunArgs(argv) {
|
|
|
1103
1486
|
(parsed.continue || positionals.length > 0)
|
|
1104
1487
|
) {
|
|
1105
1488
|
throw new Error(
|
|
1106
|
-
|
|
1489
|
+
"uncertain-turn recovery accepts only an explicit --session and no input",
|
|
1107
1490
|
);
|
|
1108
1491
|
}
|
|
1109
1492
|
if (
|
|
@@ -1111,20 +1494,20 @@ export function parseRunArgs(argv) {
|
|
|
1111
1494
|
(parsed.json || parsed.verbose || parsed.noProvision)
|
|
1112
1495
|
) {
|
|
1113
1496
|
throw new Error(
|
|
1114
|
-
|
|
1497
|
+
"--discard-uncertain does not accept --json, --verbose, or --no-provision",
|
|
1115
1498
|
);
|
|
1116
1499
|
}
|
|
1117
1500
|
if (
|
|
1118
1501
|
(parsed.retryUncertain || parsed.discardUncertain) &&
|
|
1119
1502
|
parsed.withPaths.length > 0
|
|
1120
1503
|
) {
|
|
1121
|
-
throw new Error(
|
|
1504
|
+
throw new Error("--with is unavailable during uncertain-turn recovery");
|
|
1122
1505
|
}
|
|
1123
1506
|
if (
|
|
1124
1507
|
parsed.sessionId !== undefined &&
|
|
1125
1508
|
!SESSION_ID_PATTERN.test(parsed.sessionId)
|
|
1126
1509
|
) {
|
|
1127
|
-
throw new Error(
|
|
1510
|
+
throw new Error("--session needs a canonical UUID value");
|
|
1128
1511
|
}
|
|
1129
1512
|
parsed.input = positionals[0];
|
|
1130
1513
|
return parsed;
|
|
@@ -1147,18 +1530,15 @@ async function reportReadinessFailure({
|
|
|
1147
1530
|
const [first, ...rest] = lines;
|
|
1148
1531
|
await writeStream(
|
|
1149
1532
|
stderr,
|
|
1150
|
-
[
|
|
1151
|
-
...(first ? [`playbook run: ${first}`] : []),
|
|
1152
|
-
...rest,
|
|
1153
|
-
]
|
|
1533
|
+
[...(first ? [`playbook run: ${first}`] : []), ...rest]
|
|
1154
1534
|
.map((line) => `${line}\n`)
|
|
1155
|
-
.join(
|
|
1535
|
+
.join(""),
|
|
1156
1536
|
);
|
|
1157
1537
|
}
|
|
1158
1538
|
if (failingAdapters.length > 0) {
|
|
1159
1539
|
await writeStream(
|
|
1160
1540
|
stderr,
|
|
1161
|
-
`playbook run: adapters not ready: ${failingAdapters.join(
|
|
1541
|
+
`playbook run: adapters not ready: ${failingAdapters.join(", ")}\n`,
|
|
1162
1542
|
);
|
|
1163
1543
|
}
|
|
1164
1544
|
}
|
|
@@ -1182,7 +1562,7 @@ async function resolveBossInput(input, options, stderr) {
|
|
|
1182
1562
|
if (resolved.trim().length === 0) {
|
|
1183
1563
|
await writeStream(
|
|
1184
1564
|
stderr,
|
|
1185
|
-
|
|
1565
|
+
"playbook run: empty input; pass one argument or pipe a Boss message on stdin\n",
|
|
1186
1566
|
);
|
|
1187
1567
|
return { ok: false };
|
|
1188
1568
|
}
|
|
@@ -1191,22 +1571,22 @@ async function resolveBossInput(input, options, stderr) {
|
|
|
1191
1571
|
|
|
1192
1572
|
async function awaitWithAbort(value, signal) {
|
|
1193
1573
|
if (signal === undefined) return value;
|
|
1194
|
-
if (signal.aborted) throw signal.reason ?? new Error(
|
|
1574
|
+
if (signal.aborted) throw signal.reason ?? new Error("operation aborted");
|
|
1195
1575
|
let onAbort;
|
|
1196
1576
|
const aborted = new Promise((_, reject) => {
|
|
1197
|
-
onAbort = () => reject(signal.reason ?? new Error(
|
|
1198
|
-
signal.addEventListener(
|
|
1577
|
+
onAbort = () => reject(signal.reason ?? new Error("operation aborted"));
|
|
1578
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1199
1579
|
});
|
|
1200
1580
|
try {
|
|
1201
1581
|
return await Promise.race([value, aborted]);
|
|
1202
1582
|
} finally {
|
|
1203
|
-
signal.removeEventListener(
|
|
1583
|
+
signal.removeEventListener("abort", onAbort);
|
|
1204
1584
|
}
|
|
1205
1585
|
}
|
|
1206
1586
|
|
|
1207
1587
|
function throwIfAborted(signal) {
|
|
1208
1588
|
if (signal?.aborted) {
|
|
1209
|
-
throw signal.reason ?? new Error(
|
|
1589
|
+
throw signal.reason ?? new Error("operation aborted");
|
|
1210
1590
|
}
|
|
1211
1591
|
}
|
|
1212
1592
|
|
|
@@ -1217,14 +1597,14 @@ function registryPreparer(args, options, stderr) {
|
|
|
1217
1597
|
enabled: !args.noProvision,
|
|
1218
1598
|
stderr,
|
|
1219
1599
|
hostRoots: options.hostRoots,
|
|
1220
|
-
commandName:
|
|
1600
|
+
commandName: "playbook run",
|
|
1221
1601
|
})
|
|
1222
1602
|
);
|
|
1223
1603
|
}
|
|
1224
1604
|
|
|
1225
1605
|
function createAttemptId(options) {
|
|
1226
1606
|
const attemptId = (options.createAttemptId ?? randomUUID)();
|
|
1227
|
-
if (typeof attemptId !==
|
|
1607
|
+
if (typeof attemptId !== "string" || !UUID_PATTERN.test(attemptId)) {
|
|
1228
1608
|
throw new Error(
|
|
1229
1609
|
`uncertain turn attempt id generator returned a non-UUID value: ${JSON.stringify(attemptId)}`,
|
|
1230
1610
|
);
|
|
@@ -1234,35 +1614,106 @@ function createAttemptId(options) {
|
|
|
1234
1614
|
|
|
1235
1615
|
async function releaseLease(lease) {
|
|
1236
1616
|
if (lease === undefined) return undefined;
|
|
1617
|
+
const replayChannel = HEADLESS_REPLAY_CHANNELS.get(lease);
|
|
1237
1618
|
try {
|
|
1238
|
-
await lease.release();
|
|
1619
|
+
const status = await lease.release();
|
|
1620
|
+
await reportHeadlessReplay(replayChannel, status);
|
|
1621
|
+
HEADLESS_REPLAY_CHANNELS.delete(lease);
|
|
1239
1622
|
return undefined;
|
|
1240
1623
|
} catch (error) {
|
|
1624
|
+
await reportHeadlessReplay(replayChannel);
|
|
1241
1625
|
return error;
|
|
1242
1626
|
}
|
|
1243
1627
|
}
|
|
1244
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
|
+
|
|
1245
1669
|
async function reportUncertainSession(stderr, sessionId) {
|
|
1246
1670
|
await writeStream(
|
|
1247
1671
|
stderr,
|
|
1248
1672
|
[
|
|
1249
1673
|
`playbook run: Captain session ${JSON.stringify(sessionId)} has an uncertain turn and will not be replayed automatically`,
|
|
1250
|
-
|
|
1674
|
+
"Retry may duplicate external effects from the interrupted attempt; discard abandons that attempted turn.",
|
|
1251
1675
|
`playbook run --session ${sessionId} --retry-uncertain`,
|
|
1252
1676
|
`playbook run --session ${sessionId} --discard-uncertain`,
|
|
1253
|
-
|
|
1254
|
-
].join(
|
|
1677
|
+
"",
|
|
1678
|
+
].join("\n"),
|
|
1679
|
+
);
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
function legacyCaptainSessionReason(schemaVersion) {
|
|
1683
|
+
if (schemaVersion === 2) {
|
|
1684
|
+
return "schema 2 has incompatible player identity";
|
|
1685
|
+
}
|
|
1686
|
+
if (schemaVersion === 3 || schemaVersion === 4) {
|
|
1687
|
+
return `schema ${JSON.stringify(schemaVersion)} predates the artifact-schema-3 effect-authority cutover and is not resumable`;
|
|
1688
|
+
}
|
|
1689
|
+
if (schemaVersion === 5) {
|
|
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";
|
|
1691
|
+
}
|
|
1692
|
+
return `schema ${JSON.stringify(schemaVersion)} is unsupported`;
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
export async function reportSkippedCaptainSession(
|
|
1696
|
+
stderr,
|
|
1697
|
+
commandName,
|
|
1698
|
+
kind,
|
|
1699
|
+
{ sessionId, path, schemaVersion, reason },
|
|
1700
|
+
) {
|
|
1701
|
+
const explanation =
|
|
1702
|
+
kind === "legacy" ? legacyCaptainSessionReason(schemaVersion) : reason;
|
|
1703
|
+
await writeStream(
|
|
1704
|
+
stderr,
|
|
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`,
|
|
1255
1706
|
);
|
|
1256
1707
|
}
|
|
1257
1708
|
|
|
1258
1709
|
function replayInvocation(argv, args, input) {
|
|
1259
1710
|
if (args.retryUncertain || args.discardUncertain) {
|
|
1260
|
-
return [
|
|
1711
|
+
return ["run", ...argv];
|
|
1261
1712
|
}
|
|
1262
|
-
if (args.input !== undefined) return [
|
|
1713
|
+
if (args.input !== undefined) return ["run", ...argv];
|
|
1263
1714
|
return args.terminated
|
|
1264
|
-
? [
|
|
1265
|
-
: [
|
|
1715
|
+
? ["run", ...argv, input]
|
|
1716
|
+
: ["run", ...argv, "--", input];
|
|
1266
1717
|
}
|
|
1267
1718
|
|
|
1268
1719
|
function cloneJson(value) {
|
|
@@ -1274,67 +1725,70 @@ async function readAllStdin() {
|
|
|
1274
1725
|
for await (const chunk of process.stdin) {
|
|
1275
1726
|
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
|
|
1276
1727
|
}
|
|
1277
|
-
return Buffer.concat(chunks).toString(
|
|
1728
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
1278
1729
|
}
|
|
1279
1730
|
|
|
1280
1731
|
async function writeStream(stream, text) {
|
|
1281
1732
|
const ready = stream.write(text);
|
|
1282
|
-
if (ready !== false || typeof stream.once !==
|
|
1733
|
+
if (ready !== false || typeof stream.once !== "function") return;
|
|
1283
1734
|
await new Promise((resolvePromise, rejectPromise) => {
|
|
1284
1735
|
const onDrain = () => {
|
|
1285
|
-
stream.off?.(
|
|
1736
|
+
stream.off?.("error", onError);
|
|
1286
1737
|
resolvePromise();
|
|
1287
1738
|
};
|
|
1288
1739
|
const onError = (error) => {
|
|
1289
|
-
stream.off?.(
|
|
1740
|
+
stream.off?.("drain", onDrain);
|
|
1290
1741
|
rejectPromise(error);
|
|
1291
1742
|
};
|
|
1292
|
-
stream.once(
|
|
1293
|
-
stream.once(
|
|
1743
|
+
stream.once("drain", onDrain);
|
|
1744
|
+
stream.once("error", onError);
|
|
1294
1745
|
});
|
|
1295
1746
|
}
|
|
1296
1747
|
|
|
1297
1748
|
function runHelpText(userConfigPath) {
|
|
1298
1749
|
return [
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
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
|
+
"",
|
|
1310
1761
|
`Default config: ${userConfigPath}`,
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
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",
|
|
1333
1787
|
' --json print exactly {"sessionId", "reply"}',
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
].join(
|
|
1788
|
+
" --verbose print Captain telemetry topics to stderr",
|
|
1789
|
+
" -h, --help print this help without reading input or config",
|
|
1790
|
+
"",
|
|
1791
|
+
].join("\n");
|
|
1338
1792
|
}
|
|
1339
1793
|
|
|
1340
1794
|
function message(error) {
|