@sublang/playbook 8.0.0 → 10.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/docs/cli.md +66 -23
- package/docs/configuration.md +13 -8
- package/docs/embedding.md +45 -14
- package/package.json +7 -3
- package/reference/sdlc/captain.md +14 -10
- package/reference/sdlc/captain.playbook/captain.fsm.d.ts +33 -13
- package/reference/sdlc/captain.playbook/captain.fsm.js +80 -9
- package/reference/sdlc/captain.playbook/captain.fsm.ts +137 -18
- package/reference/sdlc/captain.playbook/captain.gears.md +10 -6
- package/reference/sdlc/captain.playbook/captain.playbook.d.ts +5 -1
- package/reference/sdlc/captain.playbook/captain.playbook.js +151 -10
- package/reference/sdlc/captain.playbook/captain.playbook.ts +200 -14
- package/reference/sdlc/code.md +0 -1
- package/reference/sdlc/code.playbook/bin/interactive-session.js +170 -17
- package/reference/sdlc/code.playbook/bin/launch-config.js +136 -4
- package/reference/sdlc/code.playbook/bin/playbook.js +81 -4
- package/reference/sdlc/code.playbook/bin/repository-effects.js +2930 -0
- package/reference/sdlc/code.playbook/bin/run.js +365 -63
- package/reference/sdlc/code.playbook/bin/session-store.js +2877 -209
- package/reference/sdlc/code.playbook/code.fsm.d.ts +11 -1
- package/reference/sdlc/code.playbook/code.fsm.js +85 -29
- package/reference/sdlc/code.playbook/code.fsm.ts +95 -33
- 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 +67 -4
- package/reference/sdlc/code.playbook/code.playbook.ts +87 -8
- package/reference/sdlc/code.playbook/code.registry.d.ts +10 -3
- package/reference/sdlc/code.playbook/code.registry.js +10 -3
- package/reference/sdlc/code.playbook/code.registry.ts +23 -5
- package/reference/sdlc/code.playbook/playbook-captain.d.ts +99 -7
- package/reference/sdlc/code.playbook/playbook-captain.js +1894 -82
- package/reference/sdlc/code.playbook/playbook-captain.ts +2809 -109
- package/reference/sdlc/decide.md +0 -1
- package/reference/sdlc/decide.playbook/decide.fsm.d.ts +8 -1
- 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 +15 -5
- package/reference/sdlc/decide.playbook/decide.playbook.js +1994 -191
- package/reference/sdlc/decide.playbook/decide.playbook.ts +3209 -404
- package/reference/sdlc/decide.playbook/decide.registry.d.ts +7 -3
- package/reference/sdlc/decide.playbook/decide.registry.js +10 -3
- package/reference/sdlc/decide.playbook/decide.registry.ts +20 -5
- package/reference/sdlc/review.playbook/review.fsm.d.ts +7 -0
- package/reference/sdlc/review.playbook/review.fsm.js +133 -12
- package/reference/sdlc/review.playbook/review.fsm.ts +140 -12
- package/reference/sdlc/review.playbook/review.playbook.d.ts +5 -2
- package/reference/sdlc/review.playbook/review.playbook.js +78 -4
- package/reference/sdlc/review.playbook/review.playbook.ts +95 -8
- 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 +25 -7
- package/slc/link.md +727 -82
- 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 +165 -3
- package/src/runtime.ts +214 -2
- package/src/xstate-playbook-runtime.d.ts +162 -13
- package/src/xstate-playbook-runtime.js +3344 -564
- package/src/xstate-playbook-runtime.ts +4873 -637
- package/src/xstate-runtime.d.ts +76 -8
- package/src/xstate-runtime.js +1001 -64
- package/src/xstate-runtime.ts +1640 -91
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import { randomUUID } from 'node:crypto';
|
|
10
10
|
import { constants, realpathSync } from 'node:fs';
|
|
11
|
-
import { lstat, open,
|
|
11
|
+
import { lstat, open, realpath, unlink } from 'node:fs/promises';
|
|
12
12
|
import {
|
|
13
13
|
basename,
|
|
14
14
|
dirname,
|
|
@@ -30,6 +30,8 @@ import {
|
|
|
30
30
|
CaptainSessionHostCleanupError,
|
|
31
31
|
captainOptionsFromConfig,
|
|
32
32
|
createCaptainSessionHost,
|
|
33
|
+
installRetainedGenerationsForLaunch,
|
|
34
|
+
reportSkippedCaptainSession,
|
|
33
35
|
validateFrozenExecutionConfig,
|
|
34
36
|
} from './run.js';
|
|
35
37
|
import { prepareConfiguredRegistries } from './provision.js';
|
|
@@ -47,6 +49,8 @@ export const MANAGED_INTERACTIVE_PAYLOAD_FILE =
|
|
|
47
49
|
export const MANAGED_INTERACTIVE_PAYLOAD_SCHEMA_VERSION = 1;
|
|
48
50
|
export const MANAGED_INTERACTIVE_PAYLOAD_KIND =
|
|
49
51
|
'playbook-managed-interactive-launch';
|
|
52
|
+
export const MANAGED_INTERACTIVE_READINESS_WITNESS_FILE =
|
|
53
|
+
'playbook-managed-readiness';
|
|
50
54
|
|
|
51
55
|
const PAYLOAD_KEYS = [
|
|
52
56
|
'schemaVersion',
|
|
@@ -156,6 +160,18 @@ export async function writeManagedInteractivePayload(workDir, value) {
|
|
|
156
160
|
return path;
|
|
157
161
|
}
|
|
158
162
|
|
|
163
|
+
export async function publishManagedInteractiveReadinessWitness(
|
|
164
|
+
workDir,
|
|
165
|
+
sessionId,
|
|
166
|
+
) {
|
|
167
|
+
return writeManagedInteractiveReadinessClaim({
|
|
168
|
+
workDir,
|
|
169
|
+
sessionId,
|
|
170
|
+
claim: 'ready',
|
|
171
|
+
rejectExisting: true,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
159
175
|
export async function readManagedInteractivePayload(path) {
|
|
160
176
|
assertCanonicalAbsolutePath(path, 'managed interactive launch descriptor');
|
|
161
177
|
assertManagedDescriptorPath(path);
|
|
@@ -300,6 +316,7 @@ export function createManagedInteractiveLifecycle(payloadValue, options = {}) {
|
|
|
300
316
|
let leaseQuarantined = false;
|
|
301
317
|
let activeTurn;
|
|
302
318
|
let executionProjection;
|
|
319
|
+
let freshLaunchRecord;
|
|
303
320
|
|
|
304
321
|
const release = async () => {
|
|
305
322
|
if (released || lease === undefined) return;
|
|
@@ -309,6 +326,30 @@ export function createManagedInteractiveLifecycle(payloadValue, options = {}) {
|
|
|
309
326
|
released = true;
|
|
310
327
|
};
|
|
311
328
|
|
|
329
|
+
const abandonFreshLaunchRecord = async ({ claim = false } = {}) => {
|
|
330
|
+
const record = freshLaunchRecord;
|
|
331
|
+
if (
|
|
332
|
+
record === undefined ||
|
|
333
|
+
lease === undefined ||
|
|
334
|
+
Object.keys(record.retainedGenerations ?? {}).length !== 0
|
|
335
|
+
) {
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
if (
|
|
339
|
+
claim &&
|
|
340
|
+
!(await writeManagedInteractiveReadinessClaim({
|
|
341
|
+
workDir: payload.workDir,
|
|
342
|
+
sessionId: payload.sessionId,
|
|
343
|
+
claim: 'abandon',
|
|
344
|
+
rejectExisting: false,
|
|
345
|
+
}))
|
|
346
|
+
) {
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
freshLaunchRecord = undefined;
|
|
350
|
+
await lease.abandonFreshSettled({ expected: record });
|
|
351
|
+
};
|
|
352
|
+
|
|
312
353
|
return Object.freeze({
|
|
313
354
|
async initializeRuntime(context) {
|
|
314
355
|
if (initialized || lease !== undefined || released) {
|
|
@@ -328,7 +369,11 @@ export function createManagedInteractiveLifecycle(payloadValue, options = {}) {
|
|
|
328
369
|
let host;
|
|
329
370
|
try {
|
|
330
371
|
lease = await store.acquire(payload.sessionId);
|
|
331
|
-
const authoritative =
|
|
372
|
+
const authoritative =
|
|
373
|
+
typeof lease.recoverUnresolvedEffectAbandonment === 'function'
|
|
374
|
+
? await lease.recoverUnresolvedEffectAbandonment()
|
|
375
|
+
: await lease.read();
|
|
376
|
+
let retainedGenerations = {};
|
|
332
377
|
let restoreSnapshot;
|
|
333
378
|
const prepareRegistryModule =
|
|
334
379
|
options.prepareRegistryModule ??
|
|
@@ -381,12 +426,14 @@ export function createManagedInteractiveLifecycle(payloadValue, options = {}) {
|
|
|
381
426
|
{ loadModule, prepareRegistryModule },
|
|
382
427
|
);
|
|
383
428
|
restoreSnapshot = record.snapshot;
|
|
429
|
+
retainedGenerations = record.retainedGenerations ?? {};
|
|
384
430
|
}
|
|
385
431
|
|
|
386
432
|
const created = await createSessionHost({
|
|
387
433
|
config: executionProjection,
|
|
388
434
|
sessionId: payload.sessionId,
|
|
389
435
|
cwd: payload.cwd,
|
|
436
|
+
sessionLease: lease,
|
|
390
437
|
loadModule,
|
|
391
438
|
observers: context.observers,
|
|
392
439
|
...(options.adapterImports
|
|
@@ -401,18 +448,49 @@ export function createManagedInteractiveLifecycle(payloadValue, options = {}) {
|
|
|
401
448
|
...(options.createHostRuntime
|
|
402
449
|
? { createHostRuntime: options.createHostRuntime }
|
|
403
450
|
: {}),
|
|
451
|
+
...(options.createEffectLedgerWriteAhead
|
|
452
|
+
? {
|
|
453
|
+
createEffectLedgerWriteAhead:
|
|
454
|
+
options.createEffectLedgerWriteAhead,
|
|
455
|
+
}
|
|
456
|
+
: {}),
|
|
404
457
|
...(restoreSnapshot !== undefined ? { restoreSnapshot } : {}),
|
|
405
458
|
});
|
|
406
459
|
({ host, shell } = created);
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
460
|
+
await installRetainedGenerationsForLaunch({
|
|
461
|
+
lease,
|
|
462
|
+
shell,
|
|
463
|
+
...(payload.mode === 'fresh'
|
|
464
|
+
? {
|
|
465
|
+
freshBoundary: {
|
|
466
|
+
cwd: payload.cwd,
|
|
467
|
+
structuralProjection:
|
|
468
|
+
projectCaptainSessionStructure(executionProjection),
|
|
469
|
+
executionProjection,
|
|
470
|
+
snapshot: created.snapshot,
|
|
471
|
+
},
|
|
472
|
+
onFreshRecord(record) {
|
|
473
|
+
freshLaunchRecord = record;
|
|
474
|
+
},
|
|
475
|
+
onLegacyRecord: (record) =>
|
|
476
|
+
reportSkippedCaptainSession(
|
|
477
|
+
options.stderr ?? process.stderr,
|
|
478
|
+
'playbook',
|
|
479
|
+
'legacy',
|
|
480
|
+
record,
|
|
481
|
+
),
|
|
482
|
+
onInvalidRecord: (record) =>
|
|
483
|
+
reportSkippedCaptainSession(
|
|
484
|
+
options.stderr ?? process.stderr,
|
|
485
|
+
'playbook',
|
|
486
|
+
'invalid',
|
|
487
|
+
record,
|
|
488
|
+
),
|
|
489
|
+
}
|
|
490
|
+
: {}),
|
|
491
|
+
retainedGenerations,
|
|
492
|
+
reconcileRepositoryEffects: created.reconcileRepositoryEffects,
|
|
493
|
+
});
|
|
416
494
|
initialized = true;
|
|
417
495
|
return {
|
|
418
496
|
abortActiveTurn: (...args) => host.abortActiveTurn(...args),
|
|
@@ -445,6 +523,13 @@ export function createManagedInteractiveLifecycle(payloadValue, options = {}) {
|
|
|
445
523
|
failures.push(disposeError);
|
|
446
524
|
}
|
|
447
525
|
}
|
|
526
|
+
if (!leaseQuarantined) {
|
|
527
|
+
try {
|
|
528
|
+
await abandonFreshLaunchRecord();
|
|
529
|
+
} catch (abandonmentError) {
|
|
530
|
+
failures.push(abandonmentError);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
448
533
|
shell = undefined;
|
|
449
534
|
if (!leaseQuarantined) {
|
|
450
535
|
try {
|
|
@@ -503,25 +588,93 @@ export function createManagedInteractiveLifecycle(payloadValue, options = {}) {
|
|
|
503
588
|
`managed interactive Captain turn produced ${replies.length} usable Boss-visible replies; expected exactly one`,
|
|
504
589
|
);
|
|
505
590
|
}
|
|
506
|
-
const
|
|
507
|
-
if (
|
|
591
|
+
const settlement = shell?.exportSettlement();
|
|
592
|
+
if (settlement === undefined) {
|
|
508
593
|
throw new Error(
|
|
509
|
-
'managed interactive Captain turn settled without an exportable session
|
|
594
|
+
'managed interactive Captain turn settled without an exportable session settlement',
|
|
510
595
|
);
|
|
511
596
|
}
|
|
512
|
-
await lease.settle({
|
|
597
|
+
await lease.settle({
|
|
598
|
+
attemptId: activeTurn.attemptId,
|
|
599
|
+
snapshot: settlement.snapshot,
|
|
600
|
+
unresolvedEffects: settlement.unresolvedEffects,
|
|
601
|
+
retentionUpdates: settlement.retentionUpdates,
|
|
602
|
+
});
|
|
513
603
|
activeTurn = undefined;
|
|
514
604
|
},
|
|
515
605
|
|
|
516
|
-
async shutdown() {
|
|
606
|
+
async shutdown(context) {
|
|
517
607
|
// Cligent invokes lifecycle shutdown only after runtime disposal and the
|
|
518
608
|
// complete turn transaction, so ownership retirement cannot race either.
|
|
519
609
|
if (leaseQuarantined) return;
|
|
520
|
-
|
|
610
|
+
const failures = [];
|
|
611
|
+
if (context !== undefined) {
|
|
612
|
+
try {
|
|
613
|
+
await abandonFreshLaunchRecord({ claim: true });
|
|
614
|
+
} catch (error) {
|
|
615
|
+
failures.push(error);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
try {
|
|
619
|
+
await release();
|
|
620
|
+
} catch (error) {
|
|
621
|
+
failures.push(error);
|
|
622
|
+
}
|
|
623
|
+
if (failures.length === 1) throw failures[0];
|
|
624
|
+
if (failures.length > 1) {
|
|
625
|
+
throw new AggregateError(
|
|
626
|
+
failures,
|
|
627
|
+
'managed interactive shutdown could not retract its unused fresh session and retire its lease',
|
|
628
|
+
);
|
|
629
|
+
}
|
|
521
630
|
},
|
|
522
631
|
});
|
|
523
632
|
}
|
|
524
633
|
|
|
634
|
+
async function writeManagedInteractiveReadinessClaim({
|
|
635
|
+
workDir,
|
|
636
|
+
sessionId,
|
|
637
|
+
claim,
|
|
638
|
+
rejectExisting,
|
|
639
|
+
}) {
|
|
640
|
+
assertCanonicalAbsolutePath(
|
|
641
|
+
workDir,
|
|
642
|
+
'managed interactive readiness work directory',
|
|
643
|
+
);
|
|
644
|
+
assertUuid(sessionId, 'managed interactive readiness session id');
|
|
645
|
+
const path = join(workDir, MANAGED_INTERACTIVE_READINESS_WITNESS_FILE);
|
|
646
|
+
let handle;
|
|
647
|
+
try {
|
|
648
|
+
handle = await open(path, 'wx', 0o600);
|
|
649
|
+
} catch (cause) {
|
|
650
|
+
if (cause?.code === 'EEXIST' && !rejectExisting) return false;
|
|
651
|
+
throw new Error(
|
|
652
|
+
`managed interactive readiness claim could not be created: ${message(cause)}`,
|
|
653
|
+
{ cause },
|
|
654
|
+
);
|
|
655
|
+
}
|
|
656
|
+
try {
|
|
657
|
+
await handle.chmod(0o600);
|
|
658
|
+
const stat = await handle.stat();
|
|
659
|
+
if (!stat.isFile() || (stat.mode & 0o7777) !== 0o600) {
|
|
660
|
+
throw new Error(
|
|
661
|
+
'managed interactive readiness claim is not a private regular file',
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
if (
|
|
665
|
+
typeof process.getuid === 'function' &&
|
|
666
|
+
stat.uid !== process.getuid()
|
|
667
|
+
) {
|
|
668
|
+
throw new Error('managed interactive readiness claim has a foreign owner');
|
|
669
|
+
}
|
|
670
|
+
await handle.writeFile(`${claim} ${sessionId}\n`, 'utf8');
|
|
671
|
+
await handle.sync();
|
|
672
|
+
} finally {
|
|
673
|
+
await handle.close();
|
|
674
|
+
}
|
|
675
|
+
return true;
|
|
676
|
+
}
|
|
677
|
+
|
|
525
678
|
export function parseManagedInteractiveChildArgs(argv) {
|
|
526
679
|
if (argv.length !== 1) {
|
|
527
680
|
throw new Error(
|
|
@@ -37,6 +37,7 @@ const DEFAULT_TEMPLATE_PATH = resolve(
|
|
|
37
37
|
export const PLAYBOOK_CAPTAIN_MODULE =
|
|
38
38
|
'@sublang/playbook/playbook-captain';
|
|
39
39
|
const PLAYBOOK_LAUNCHER_KEYS = ['from', 'command', 'roles'];
|
|
40
|
+
const HOST_CAPABILITIES_OPTION_KEY = 'hostCapabilities';
|
|
40
41
|
const PLAYBOOK_TOP_LEVEL_KEYS = new Set([
|
|
41
42
|
'captain',
|
|
42
43
|
'players',
|
|
@@ -291,6 +292,7 @@ export async function normalizeSelectedLaunchPlanDataOnly(
|
|
|
291
292
|
if (hasOwn(block, 'players')) {
|
|
292
293
|
throw legacyPlayersError(`playbooks.${id}.players`, configPath);
|
|
293
294
|
}
|
|
295
|
+
rejectConfiguredHostCapabilities(block, `playbooks.${id}`);
|
|
294
296
|
if (
|
|
295
297
|
typeof block.from !== 'string' ||
|
|
296
298
|
block.from.trim().length === 0 ||
|
|
@@ -519,6 +521,19 @@ export function resolveAgent(value, path, reservedKeys = []) {
|
|
|
519
521
|
throw new Error(`${path} must be an adapter shorthand or an agent block`);
|
|
520
522
|
}
|
|
521
523
|
|
|
524
|
+
function rejectConfiguredHostCapabilities(value, path) {
|
|
525
|
+
if (
|
|
526
|
+
value !== null &&
|
|
527
|
+
typeof value === 'object' &&
|
|
528
|
+
!Array.isArray(value) &&
|
|
529
|
+
hasOwn(value, HOST_CAPABILITIES_OPTION_KEY)
|
|
530
|
+
) {
|
|
531
|
+
throw new Error(
|
|
532
|
+
`${path}.${HOST_CAPABILITIES_OPTION_KEY} is host-owned and cannot be configured`,
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
522
537
|
// PBCLI-46: normalize into a detached, deeply frozen JSON plan. The plan has
|
|
523
538
|
// only execution data and presentation data; imported registry functions are
|
|
524
539
|
// consulted for validation and then discarded.
|
|
@@ -597,6 +612,7 @@ export async function normalizeLaunchPlan(
|
|
|
597
612
|
if (hasOwn(block, 'players')) {
|
|
598
613
|
throw legacyPlayersError(`playbooks.${id}.players`, configPath);
|
|
599
614
|
}
|
|
615
|
+
rejectConfiguredHostCapabilities(block, `playbooks.${id}`);
|
|
600
616
|
const from = block.from;
|
|
601
617
|
if (
|
|
602
618
|
typeof from !== 'string' ||
|
|
@@ -806,7 +822,7 @@ export async function normalizeLaunchPlan(
|
|
|
806
822
|
`playbooks.${id}.from "${from}" failed to import: ${errorMessage(cause)}`,
|
|
807
823
|
);
|
|
808
824
|
}
|
|
809
|
-
const entry = mod?.default;
|
|
825
|
+
const entry = snapshotRegistryEntry(mod?.default);
|
|
810
826
|
const registryProblem = invalidRegistryEntryReason(entry);
|
|
811
827
|
if (registryProblem !== undefined) {
|
|
812
828
|
throw new Error(
|
|
@@ -1252,7 +1268,26 @@ function assertNoRetiredProfiles(top, configPath) {
|
|
|
1252
1268
|
}
|
|
1253
1269
|
}
|
|
1254
1270
|
|
|
1255
|
-
|
|
1271
|
+
// Imported manifests remain live JavaScript objects. Capture every member this
|
|
1272
|
+
// host consumes once, then validate and project only the detached record so a
|
|
1273
|
+
// getter or later mutation cannot make those two phases observe different
|
|
1274
|
+
// compatibility declarations or identities.
|
|
1275
|
+
export function snapshotRegistryEntry(value) {
|
|
1276
|
+
if (!isObject(value)) return value;
|
|
1277
|
+
return {
|
|
1278
|
+
id: value.id,
|
|
1279
|
+
command: value.command,
|
|
1280
|
+
intent: value.intent,
|
|
1281
|
+
artifactSchema: value.artifactSchema,
|
|
1282
|
+
runtimeProfile: value.runtimeProfile,
|
|
1283
|
+
requiredRoleIds: value.requiredRoleIds,
|
|
1284
|
+
concurrentRoleSets: value.concurrentRoleSets,
|
|
1285
|
+
validateOptions: value.validateOptions,
|
|
1286
|
+
createRuntime: value.createRuntime,
|
|
1287
|
+
};
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
export function invalidRegistryEntryReason(value) {
|
|
1256
1291
|
if (!isObject(value)) return 'the default export must be an object';
|
|
1257
1292
|
if (
|
|
1258
1293
|
typeof value.id !== 'string' ||
|
|
@@ -1269,9 +1304,14 @@ function invalidRegistryEntryReason(value) {
|
|
|
1269
1304
|
return 'command must be a canonical trimmed nonblank string';
|
|
1270
1305
|
}
|
|
1271
1306
|
if (typeof value.intent !== 'string') return 'intent must be a string';
|
|
1272
|
-
if (value.artifactSchema !==
|
|
1273
|
-
return 'artifactSchema must be
|
|
1307
|
+
if (value.artifactSchema !== 3) {
|
|
1308
|
+
return 'artifactSchema must be 3';
|
|
1274
1309
|
}
|
|
1310
|
+
const runtimeProfileProblem = invalidRuntimeProfileReason(
|
|
1311
|
+
value.runtimeProfile,
|
|
1312
|
+
value.artifactSchema,
|
|
1313
|
+
);
|
|
1314
|
+
if (runtimeProfileProblem !== undefined) return runtimeProfileProblem;
|
|
1275
1315
|
const roleProblem = invalidManifestRoles(value.requiredRoleIds);
|
|
1276
1316
|
if (roleProblem !== undefined) return `requiredRoleIds ${roleProblem}`;
|
|
1277
1317
|
const concurrentProblem = invalidConcurrentRoleSets(
|
|
@@ -1290,6 +1330,89 @@ function invalidRegistryEntryReason(value) {
|
|
|
1290
1330
|
return undefined;
|
|
1291
1331
|
}
|
|
1292
1332
|
|
|
1333
|
+
function invalidRuntimeProfileReason(value, advertisedArtifactSchema) {
|
|
1334
|
+
if (!isPlainObject(value)) {
|
|
1335
|
+
return 'runtimeProfile must be a plain object';
|
|
1336
|
+
}
|
|
1337
|
+
const kindDescriptor = Object.getOwnPropertyDescriptor(value, 'kind');
|
|
1338
|
+
if (
|
|
1339
|
+
kindDescriptor === undefined ||
|
|
1340
|
+
kindDescriptor.get !== undefined ||
|
|
1341
|
+
kindDescriptor.set !== undefined ||
|
|
1342
|
+
kindDescriptor.enumerable !== true
|
|
1343
|
+
) {
|
|
1344
|
+
return 'runtimeProfile.kind must be an enumerable data property';
|
|
1345
|
+
}
|
|
1346
|
+
const kind = kindDescriptor.value;
|
|
1347
|
+
if (kind === 'shared-factory') {
|
|
1348
|
+
const profile = exactPlainDataRecord(value, ['kind', 'compat']);
|
|
1349
|
+
if (profile === undefined) {
|
|
1350
|
+
return 'shared-factory runtimeProfile must contain exactly kind and compat data properties';
|
|
1351
|
+
}
|
|
1352
|
+
const compat = exactPlainDataRecord(profile.compat, [
|
|
1353
|
+
'artifactSchema',
|
|
1354
|
+
'runtimeAbi',
|
|
1355
|
+
]);
|
|
1356
|
+
if (compat === undefined) {
|
|
1357
|
+
return 'shared-factory runtimeProfile.compat must contain exactly artifactSchema and runtimeAbi data properties';
|
|
1358
|
+
}
|
|
1359
|
+
if (!Number.isSafeInteger(compat.artifactSchema)) {
|
|
1360
|
+
return 'shared-factory runtimeProfile.compat.artifactSchema must be an integer';
|
|
1361
|
+
}
|
|
1362
|
+
if (!Number.isSafeInteger(compat.runtimeAbi)) {
|
|
1363
|
+
return 'shared-factory runtimeProfile.compat.runtimeAbi must be an integer';
|
|
1364
|
+
}
|
|
1365
|
+
if (compat.artifactSchema !== advertisedArtifactSchema) {
|
|
1366
|
+
return 'artifactSchema must match runtimeProfile.compat.artifactSchema';
|
|
1367
|
+
}
|
|
1368
|
+
return undefined;
|
|
1369
|
+
}
|
|
1370
|
+
if (kind === 'bespoke') {
|
|
1371
|
+
const profile = exactPlainDataRecord(value, ['kind', 'artifactSchema']);
|
|
1372
|
+
if (profile === undefined) {
|
|
1373
|
+
return 'bespoke runtimeProfile must contain exactly kind and artifactSchema data properties';
|
|
1374
|
+
}
|
|
1375
|
+
if (!Number.isSafeInteger(profile.artifactSchema)) {
|
|
1376
|
+
return 'bespoke runtimeProfile.artifactSchema must be an integer';
|
|
1377
|
+
}
|
|
1378
|
+
if (profile.artifactSchema !== advertisedArtifactSchema) {
|
|
1379
|
+
return 'artifactSchema must match runtimeProfile.artifactSchema';
|
|
1380
|
+
}
|
|
1381
|
+
return undefined;
|
|
1382
|
+
}
|
|
1383
|
+
return 'runtimeProfile.kind must be shared-factory or bespoke';
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
function exactPlainDataRecord(value, expectedKeys) {
|
|
1387
|
+
if (!isPlainObject(value)) return undefined;
|
|
1388
|
+
const keys = Reflect.ownKeys(value);
|
|
1389
|
+
if (
|
|
1390
|
+
keys.length !== expectedKeys.length ||
|
|
1391
|
+
keys.some(
|
|
1392
|
+
(key) => typeof key !== 'string' || !expectedKeys.includes(key),
|
|
1393
|
+
)
|
|
1394
|
+
) {
|
|
1395
|
+
return undefined;
|
|
1396
|
+
}
|
|
1397
|
+
const descriptors = Object.getOwnPropertyDescriptors(value);
|
|
1398
|
+
if (
|
|
1399
|
+
expectedKeys.some((key) => {
|
|
1400
|
+
const descriptor = descriptors[key];
|
|
1401
|
+
return (
|
|
1402
|
+
descriptor === undefined ||
|
|
1403
|
+
descriptor.get !== undefined ||
|
|
1404
|
+
descriptor.set !== undefined ||
|
|
1405
|
+
descriptor.enumerable !== true
|
|
1406
|
+
);
|
|
1407
|
+
})
|
|
1408
|
+
) {
|
|
1409
|
+
return undefined;
|
|
1410
|
+
}
|
|
1411
|
+
return Object.fromEntries(
|
|
1412
|
+
expectedKeys.map((key) => [key, descriptors[key].value]),
|
|
1413
|
+
);
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1293
1416
|
function validateStoredStructuralProjection(value) {
|
|
1294
1417
|
const stored = cloneJson(value, 'stored structural projection');
|
|
1295
1418
|
if (!isPlainObject(stored) || stored.schemaVersion !== 1) {
|
|
@@ -1340,6 +1463,15 @@ function validateStoredStructuralProjection(value) {
|
|
|
1340
1463
|
) {
|
|
1341
1464
|
throw new Error(`stored structural catalog.${id} is malformed`);
|
|
1342
1465
|
}
|
|
1466
|
+
if (item.artifactSchema !== 3) {
|
|
1467
|
+
throw new Error(
|
|
1468
|
+
`stored structural catalog.${id}.artifactSchema must be 3`,
|
|
1469
|
+
);
|
|
1470
|
+
}
|
|
1471
|
+
rejectConfiguredHostCapabilities(
|
|
1472
|
+
item.options,
|
|
1473
|
+
`stored structural catalog.${id}.options`,
|
|
1474
|
+
);
|
|
1343
1475
|
if (
|
|
1344
1476
|
JSON.stringify(Object.keys(item.roles)) !==
|
|
1345
1477
|
JSON.stringify(item.requiredRoleIds)
|
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
createManagedInteractiveSessionCommand,
|
|
35
35
|
MANAGED_INTERACTIVE_PAYLOAD_KIND,
|
|
36
36
|
MANAGED_INTERACTIVE_PAYLOAD_SCHEMA_VERSION,
|
|
37
|
+
publishManagedInteractiveReadinessWitness,
|
|
37
38
|
} from './interactive-session.js';
|
|
38
39
|
import { prepareConfiguredRegistries } from './provision.js';
|
|
39
40
|
import {
|
|
@@ -115,6 +116,12 @@ export async function runPlaybookCli(options = {}) {
|
|
|
115
116
|
...(options.createHostRuntime
|
|
116
117
|
? { createHostRuntime: options.createHostRuntime }
|
|
117
118
|
: {}),
|
|
119
|
+
...(options.createEffectLedgerWriteAhead
|
|
120
|
+
? {
|
|
121
|
+
createEffectLedgerWriteAhead:
|
|
122
|
+
options.createEffectLedgerWriteAhead,
|
|
123
|
+
}
|
|
124
|
+
: {}),
|
|
118
125
|
...(options.sessionStore
|
|
119
126
|
? { sessionStore: options.sessionStore }
|
|
120
127
|
: {}),
|
|
@@ -126,6 +133,12 @@ export async function runPlaybookCli(options = {}) {
|
|
|
126
133
|
...(options.createAttemptId
|
|
127
134
|
? { createAttemptId: options.createAttemptId }
|
|
128
135
|
: {}),
|
|
136
|
+
...(options.installRetainedGenerationsForLaunch
|
|
137
|
+
? {
|
|
138
|
+
installRetainedGenerationsForLaunch:
|
|
139
|
+
options.installRetainedGenerationsForLaunch,
|
|
140
|
+
}
|
|
141
|
+
: {}),
|
|
129
142
|
...(options.signal ? { signal: options.signal } : {}),
|
|
130
143
|
// PBCLI-39: the run path gates on SDK availability too.
|
|
131
144
|
...(options.probeAdapterSdk
|
|
@@ -213,11 +226,45 @@ export async function runPlaybookCli(options = {}) {
|
|
|
213
226
|
selectedRecord = validateCaptainSessionRecord(
|
|
214
227
|
await store.read(interactiveArgs.sessionId),
|
|
215
228
|
);
|
|
216
|
-
|
|
229
|
+
const needsAbandonmentRecovery =
|
|
230
|
+
(selectedRecord.state === 'uncertain' &&
|
|
231
|
+
Object.hasOwn(selectedRecord.uncertain, 'abandonment')) ||
|
|
232
|
+
(selectedRecord.state === 'settled' &&
|
|
233
|
+
Object.hasOwn(selectedRecord, 'settledAbandonment'));
|
|
234
|
+
if (selectedRecord.state !== 'settled' && !needsAbandonmentRecovery) {
|
|
217
235
|
throw new Error(
|
|
218
236
|
`Captain session ${JSON.stringify(interactiveArgs.sessionId)} has an uncertain turn; recover it with playbook run before reopening interactively`,
|
|
219
237
|
);
|
|
220
238
|
}
|
|
239
|
+
if (needsAbandonmentRecovery) {
|
|
240
|
+
const recoveryLease = await store.acquire(interactiveArgs.sessionId);
|
|
241
|
+
let recoveryError;
|
|
242
|
+
try {
|
|
243
|
+
selectedRecord = validateCaptainSessionRecord(
|
|
244
|
+
await recoveryLease.recoverUnresolvedEffectAbandonment(),
|
|
245
|
+
);
|
|
246
|
+
} catch (error) {
|
|
247
|
+
recoveryError = error;
|
|
248
|
+
}
|
|
249
|
+
try {
|
|
250
|
+
await recoveryLease.release();
|
|
251
|
+
} catch (error) {
|
|
252
|
+
if (recoveryError !== undefined) {
|
|
253
|
+
throw aggregateOperationalFailures(
|
|
254
|
+
recoveryError,
|
|
255
|
+
error,
|
|
256
|
+
'Captain session abandonment recovery failed and its lease could not be released',
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
throw error;
|
|
260
|
+
}
|
|
261
|
+
if (recoveryError !== undefined) throw recoveryError;
|
|
262
|
+
if (selectedRecord.state !== 'settled') {
|
|
263
|
+
throw new Error(
|
|
264
|
+
`Captain session ${JSON.stringify(interactiveArgs.sessionId)} abandonment recovery did not settle its turn`,
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
221
268
|
} catch (error) {
|
|
222
269
|
stderr.write(`playbook: ${errorMessage(error)}\n`);
|
|
223
270
|
return { code: COMPOSITION_FAILURE_EXIT_CODE };
|
|
@@ -358,6 +405,7 @@ export async function runPlaybookCli(options = {}) {
|
|
|
358
405
|
projectTmuxConfig(plan),
|
|
359
406
|
);
|
|
360
407
|
let prepared;
|
|
408
|
+
let managedChildWorkDir;
|
|
361
409
|
try {
|
|
362
410
|
throwIfSignalAborted(options.signal);
|
|
363
411
|
prepared = await awaitManagedPreparation(
|
|
@@ -372,8 +420,14 @@ export async function runPlaybookCli(options = {}) {
|
|
|
372
420
|
...(options.adapterImports
|
|
373
421
|
? { adapterImports: options.adapterImports }
|
|
374
422
|
: {}),
|
|
375
|
-
createSessionCommand: (context) =>
|
|
376
|
-
|
|
423
|
+
createSessionCommand: (context) => {
|
|
424
|
+
if (managedChildWorkDir !== undefined) {
|
|
425
|
+
throw new Error(
|
|
426
|
+
'managed tmux-play requested more than one session command',
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
managedChildWorkDir = context.workDir;
|
|
430
|
+
return createManagedInteractiveSessionCommand(
|
|
377
431
|
context,
|
|
378
432
|
{
|
|
379
433
|
schemaVersion: MANAGED_INTERACTIVE_PAYLOAD_SCHEMA_VERSION,
|
|
@@ -393,7 +447,8 @@ export async function runPlaybookCli(options = {}) {
|
|
|
393
447
|
),
|
|
394
448
|
...(options.execPath ? { execPath: options.execPath } : {}),
|
|
395
449
|
},
|
|
396
|
-
)
|
|
450
|
+
);
|
|
451
|
+
},
|
|
397
452
|
}),
|
|
398
453
|
options.signal,
|
|
399
454
|
);
|
|
@@ -403,6 +458,28 @@ export async function runPlaybookCli(options = {}) {
|
|
|
403
458
|
new Error('managed tmux-play prepared a mismatched session id'),
|
|
404
459
|
);
|
|
405
460
|
}
|
|
461
|
+
if (managedChildWorkDir === undefined) {
|
|
462
|
+
await cancelPreparedAfterFailure(
|
|
463
|
+
prepared,
|
|
464
|
+
new Error('managed tmux-play did not request a session command'),
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
if (prepared?.workDir !== managedChildWorkDir) {
|
|
468
|
+
await cancelPreparedAfterFailure(
|
|
469
|
+
prepared,
|
|
470
|
+
new Error('managed tmux-play prepared a mismatched work directory'),
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
if (!selectedRecord) {
|
|
474
|
+
try {
|
|
475
|
+
await (
|
|
476
|
+
options.publishManagedReadinessWitness ??
|
|
477
|
+
publishManagedInteractiveReadinessWitness
|
|
478
|
+
)(managedChildWorkDir, sessionId);
|
|
479
|
+
} catch (error) {
|
|
480
|
+
await cancelPreparedAfterFailure(prepared, error);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
406
483
|
try {
|
|
407
484
|
await writeStream(
|
|
408
485
|
stderr,
|