@sublang/playbook 17.4.0 → 17.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/cli.md +30 -3
- package/docs/configuration.md +32 -2
- package/docs/embedding.md +29 -0
- package/package.json +2 -2
- package/reference/sdlc/code.md +24 -4
- package/reference/sdlc/code.playbook/bin/launch-config.js +249 -39
- package/reference/sdlc/code.playbook/bin/playbook.js +43 -6
- package/reference/sdlc/code.playbook/bin/run.js +37 -3
- package/reference/sdlc/code.playbook/bin/session-store.js +11 -5
- package/reference/sdlc/code.playbook/code.fsm.d.ts +12 -0
- package/reference/sdlc/code.playbook/code.fsm.js +40 -6
- package/reference/sdlc/code.playbook/code.fsm.ts +53 -6
- package/reference/sdlc/code.playbook/code.gears.md +32 -4
- package/reference/sdlc/code.playbook/playbook-captain.js +45 -4
- package/reference/sdlc/code.playbook/playbook-captain.ts +50 -3
- package/reference/sdlc/code.playbook/session-assets.js +7 -6
- package/reference/sdlc/code.playbook/session-host.d.ts +9 -2
- package/slc/gears2fsm.md +17 -3
- package/slc/text2gears.md +7 -0
- package/src/xstate-playbook-runtime.js +5 -0
- package/src/xstate-playbook-runtime.ts +7 -0
|
@@ -84,6 +84,73 @@ export function extractWithFlags(argv) {
|
|
|
84
84
|
return { withPaths, rest };
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
// PBCLI-94 (DR-083): one `--module` value names `<id>=<specifier>`, split at
|
|
88
|
+
// its first `=` so a specifier may itself carry one.
|
|
89
|
+
export function parseModuleFlagValue(value) {
|
|
90
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
91
|
+
throw new Error("--module needs an <id>=<specifier> value");
|
|
92
|
+
}
|
|
93
|
+
const separator = value.indexOf("=");
|
|
94
|
+
if (separator < 0) {
|
|
95
|
+
throw new Error(
|
|
96
|
+
`--module ${JSON.stringify(value)} must be <id>=<specifier>`,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
const id = value.slice(0, separator);
|
|
100
|
+
const specifier = value.slice(separator + 1);
|
|
101
|
+
if (id.length === 0) {
|
|
102
|
+
throw new Error(`--module ${JSON.stringify(value)} names no playbook id`);
|
|
103
|
+
}
|
|
104
|
+
if (specifier.length === 0) {
|
|
105
|
+
throw new Error(
|
|
106
|
+
`--module ${JSON.stringify(value)} names no module specifier`,
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
return [id, specifier];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// PBCLI-94 (DR-083): split ordered `--module <id>=<specifier>` pairs out of an
|
|
113
|
+
// argument vector, beside `--with`. The caller's vector is never changed.
|
|
114
|
+
export function extractModuleFlags(argv) {
|
|
115
|
+
const modulePairs = [];
|
|
116
|
+
const rest = [];
|
|
117
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
118
|
+
const arg = argv[i];
|
|
119
|
+
if (arg === "--module") {
|
|
120
|
+
modulePairs.push(parseModuleFlagValue(argv[i + 1]));
|
|
121
|
+
i += 1;
|
|
122
|
+
} else if (arg.startsWith("--module=")) {
|
|
123
|
+
modulePairs.push(parseModuleFlagValue(arg.slice("--module=".length)));
|
|
124
|
+
} else {
|
|
125
|
+
rest.push(arg);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return { modulePairs, rest };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// PBCLI-94 (DR-083): the CLI's supplied modules, one per playbook id. A
|
|
132
|
+
// relative filesystem path resolves against the invocation's working
|
|
133
|
+
// directory here, because the loader refuses relative paths.
|
|
134
|
+
export function suppliedModulesFromFlags(modulePairs, cwd = process.cwd()) {
|
|
135
|
+
const seen = new Set();
|
|
136
|
+
const entries = [];
|
|
137
|
+
for (const [id, specifier] of modulePairs) {
|
|
138
|
+
if (seen.has(id)) {
|
|
139
|
+
throw new Error(
|
|
140
|
+
`--module names playbook ${JSON.stringify(id)} more than once`,
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
seen.add(id);
|
|
144
|
+
entries.push([
|
|
145
|
+
id,
|
|
146
|
+
isRelativeFilesystemRegistrySpecifier(specifier)
|
|
147
|
+
? resolve(cwd, specifier)
|
|
148
|
+
: specifier,
|
|
149
|
+
]);
|
|
150
|
+
}
|
|
151
|
+
return Object.freeze(Object.fromEntries(entries));
|
|
152
|
+
}
|
|
153
|
+
|
|
87
154
|
// PBCLI-25: an overlay fragment is a top-level-format YAML map.
|
|
88
155
|
export function loadOverlayFragment(overlayPath) {
|
|
89
156
|
const resolved = resolve(overlayPath);
|
|
@@ -244,6 +311,76 @@ export function canonicalizeRegistrySpecifier(from, configPath) {
|
|
|
244
311
|
return from;
|
|
245
312
|
}
|
|
246
313
|
|
|
314
|
+
// PBCLI-94 (DR-083): validate the loader's `modules` option — a read-only
|
|
315
|
+
// record from playbook id to module specifier — and canonicalize each value by
|
|
316
|
+
// the configured-module rules, except that a relative path has no config
|
|
317
|
+
// directory to anchor it and is refused. Every id must name a playbook of the
|
|
318
|
+
// current config; an ordinary reopen skips one the stored catalog lacks.
|
|
319
|
+
function resolveSuppliedModules(modules, enabledIds, currentIds) {
|
|
320
|
+
const supplied = new Map();
|
|
321
|
+
if (modules === undefined) return supplied;
|
|
322
|
+
const record = cloneJson(modules, "modules");
|
|
323
|
+
if (!isPlainObject(record)) {
|
|
324
|
+
throw new Error(
|
|
325
|
+
"modules must be a record from playbook id to module specifier",
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
for (const [id, specifier] of Object.entries(record)) {
|
|
329
|
+
if (!currentIds.has(id)) {
|
|
330
|
+
throw new Error(
|
|
331
|
+
`a module was supplied for ${JSON.stringify(id)}, which names no enabled playbook`,
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
if (
|
|
335
|
+
typeof specifier !== "string" ||
|
|
336
|
+
specifier.trim().length === 0 ||
|
|
337
|
+
specifier !== specifier.trim()
|
|
338
|
+
) {
|
|
339
|
+
throw new Error(
|
|
340
|
+
`the module supplied for ${id} must be a canonical trimmed module specifier`,
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
if (isRelativeFilesystemRegistrySpecifier(specifier)) {
|
|
344
|
+
throw new Error(
|
|
345
|
+
`the module supplied for ${id} is a relative path; supply an absolute path, a file URL, or a package specifier`,
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
// A current playbook outside the stored catalog is ignored on a reopen,
|
|
349
|
+
// once its value has passed the checks above.
|
|
350
|
+
if (!enabledIds.includes(id)) continue;
|
|
351
|
+
supplied.set(
|
|
352
|
+
id,
|
|
353
|
+
!specifier.startsWith("file:") && isAbsolute(specifier)
|
|
354
|
+
? pathToFileURL(specifier).href
|
|
355
|
+
: specifier,
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
return supplied;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// PBCLI-94 (DR-083): add the ids a layer's own `playbooks` map holds, read as
|
|
362
|
+
// keys only, so a reopen can name a stray supplied id without inspecting any
|
|
363
|
+
// member outside its stored selection.
|
|
364
|
+
function addCurrentPlaybookIds(layer, ids = new Set()) {
|
|
365
|
+
if (!isPlainObject(layer)) return ids;
|
|
366
|
+
const descriptor = Object.getOwnPropertyDescriptor(layer, "playbooks");
|
|
367
|
+
if (
|
|
368
|
+
descriptor === undefined ||
|
|
369
|
+
!hasOwn(descriptor, "value") ||
|
|
370
|
+
!isPlainObject(descriptor.value)
|
|
371
|
+
) {
|
|
372
|
+
return ids;
|
|
373
|
+
}
|
|
374
|
+
for (const id of Object.keys(descriptor.value)) ids.add(id);
|
|
375
|
+
return ids;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function missingModuleError(id) {
|
|
379
|
+
return new Error(
|
|
380
|
+
`playbooks.${id} names no module: set playbooks.${id}.from or supply a module for ${id} (--module ${id}=<specifier>)`,
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
|
|
247
384
|
function isRelativeFilesystemRegistrySpecifier(from) {
|
|
248
385
|
return (
|
|
249
386
|
from.startsWith("./") ||
|
|
@@ -264,6 +401,7 @@ export async function loadLaunchPlan({
|
|
|
264
401
|
templatePath = DEFAULT_TEMPLATE_PATH,
|
|
265
402
|
onNotice = () => {},
|
|
266
403
|
selectedMembers,
|
|
404
|
+
modules,
|
|
267
405
|
env = process.env,
|
|
268
406
|
homeDir = env.HOME ?? homedir(),
|
|
269
407
|
}) {
|
|
@@ -280,19 +418,26 @@ export async function loadLaunchPlan({
|
|
|
280
418
|
`the top-level config at ${userConfigPath} must be a YAML map before --with can overlay it`,
|
|
281
419
|
);
|
|
282
420
|
}
|
|
421
|
+
const currentPlaybookIds = addCurrentPlaybookIds(top);
|
|
283
422
|
for (const overlayPath of overlayPaths) {
|
|
284
423
|
const overlay = loadOverlayFragment(overlayPath);
|
|
424
|
+
addCurrentPlaybookIds(overlay, currentPlaybookIds);
|
|
285
425
|
top =
|
|
286
426
|
selectedMembers === undefined
|
|
287
427
|
? mergeConfigs(top, overlay)
|
|
288
428
|
: mergeSelectedConfigs(top, overlay, selectedMembers);
|
|
289
429
|
}
|
|
290
|
-
return await
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
430
|
+
return await normalizeLaunchPlanWith(
|
|
431
|
+
top,
|
|
432
|
+
{
|
|
433
|
+
loadModule,
|
|
434
|
+
configPath: userConfigPath,
|
|
435
|
+
prepareRegistryModule,
|
|
436
|
+
selectedMembers,
|
|
437
|
+
modules,
|
|
438
|
+
},
|
|
439
|
+
currentPlaybookIds,
|
|
440
|
+
);
|
|
296
441
|
}
|
|
297
442
|
|
|
298
443
|
// PBCLI-22/49: an interactive selected launch may project current tuning and
|
|
@@ -304,6 +449,7 @@ export async function loadSelectedLaunchPlanDataOnly({
|
|
|
304
449
|
userConfigPath,
|
|
305
450
|
overlayPaths = [],
|
|
306
451
|
structuralProjection,
|
|
452
|
+
modules,
|
|
307
453
|
templatePath = DEFAULT_TEMPLATE_PATH,
|
|
308
454
|
onNotice = () => {},
|
|
309
455
|
env = process.env,
|
|
@@ -319,28 +465,38 @@ export async function loadSelectedLaunchPlanDataOnly({
|
|
|
319
465
|
`the top-level config at ${userConfigPath} must be a YAML map before --with can overlay it`,
|
|
320
466
|
);
|
|
321
467
|
}
|
|
468
|
+
const currentPlaybookIds = addCurrentPlaybookIds(top);
|
|
322
469
|
top = projectSelectedLayer(
|
|
323
470
|
top,
|
|
324
471
|
validateSelectedMembers(selectedMembers),
|
|
325
472
|
"config",
|
|
326
473
|
);
|
|
327
474
|
for (const overlayPath of overlayPaths) {
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
selectedMembers,
|
|
332
|
-
);
|
|
475
|
+
const overlay = loadOverlayFragment(overlayPath);
|
|
476
|
+
addCurrentPlaybookIds(overlay, currentPlaybookIds);
|
|
477
|
+
top = mergeSelectedConfigs(top, overlay, selectedMembers);
|
|
333
478
|
}
|
|
334
|
-
return await
|
|
335
|
-
|
|
336
|
-
stored,
|
|
337
|
-
|
|
338
|
-
|
|
479
|
+
return await normalizeSelectedLaunchPlanDataOnlyWith(
|
|
480
|
+
top,
|
|
481
|
+
{ configPath: userConfigPath, stored, selectedMembers, modules },
|
|
482
|
+
currentPlaybookIds,
|
|
483
|
+
);
|
|
339
484
|
}
|
|
340
485
|
|
|
341
|
-
export async function normalizeSelectedLaunchPlanDataOnly(
|
|
486
|
+
export async function normalizeSelectedLaunchPlanDataOnly(top, options = {}) {
|
|
487
|
+
return await normalizeSelectedLaunchPlanDataOnlyWith(
|
|
488
|
+
top,
|
|
489
|
+
options,
|
|
490
|
+
addCurrentPlaybookIds(top),
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// `currentPlaybookIds` names every playbook of the current merged config,
|
|
495
|
+
// gathered before the layers were projected to the stored selection.
|
|
496
|
+
async function normalizeSelectedLaunchPlanDataOnlyWith(
|
|
342
497
|
top,
|
|
343
|
-
{ configPath, stored, selectedMembers } = {},
|
|
498
|
+
{ configPath, stored, selectedMembers, modules } = {},
|
|
499
|
+
currentPlaybookIds,
|
|
344
500
|
) {
|
|
345
501
|
stored = validateStoredStructuralProjection(stored);
|
|
346
502
|
const expectedMembers = selectedMembersFromStoredStructure(stored);
|
|
@@ -390,6 +546,11 @@ export async function normalizeSelectedLaunchPlanDataOnly(
|
|
|
390
546
|
);
|
|
391
547
|
|
|
392
548
|
const playbooksCfg = requireObject(top.playbooks, "playbooks");
|
|
549
|
+
const suppliedModules = resolveSuppliedModules(
|
|
550
|
+
modules,
|
|
551
|
+
expectedMembers.playbookIds,
|
|
552
|
+
currentPlaybookIds,
|
|
553
|
+
);
|
|
393
554
|
const tuningChecks = [];
|
|
394
555
|
let tuningCheckIndex = 0;
|
|
395
556
|
const authored = new Map();
|
|
@@ -400,20 +561,30 @@ export async function normalizeSelectedLaunchPlanDataOnly(
|
|
|
400
561
|
throw legacyPlayersError(`playbooks.${id}.players`, configPath);
|
|
401
562
|
}
|
|
402
563
|
rejectConfiguredHostCapabilities(block, `playbooks.${id}`);
|
|
564
|
+
const hasFrom = hasOwn(block, "from");
|
|
403
565
|
if (
|
|
404
|
-
|
|
405
|
-
block.from
|
|
406
|
-
|
|
566
|
+
hasFrom &&
|
|
567
|
+
(typeof block.from !== "string" ||
|
|
568
|
+
block.from.trim().length === 0 ||
|
|
569
|
+
block.from !== block.from.trim())
|
|
407
570
|
) {
|
|
408
571
|
throw new Error(
|
|
409
572
|
`playbooks.${id}.from must be a canonical trimmed module specifier`,
|
|
410
573
|
);
|
|
411
574
|
}
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
)
|
|
416
|
-
|
|
575
|
+
// PBCLI-22/94 (DR-083): a supplied module wins over `from`; with neither,
|
|
576
|
+
// the reopen takes the module the session already records.
|
|
577
|
+
const suppliedModule = suppliedModules.get(id);
|
|
578
|
+
if (suppliedModule !== undefined) {
|
|
579
|
+
if (suppliedModule !== storedItem.from) {
|
|
580
|
+
throw new Error(
|
|
581
|
+
`the module supplied for ${id} differs from the stored structural projection`,
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
} else if (
|
|
585
|
+
hasFrom &&
|
|
586
|
+
canonicalizeRegistrySpecifier(block.from, configPath) !== storedItem.from
|
|
587
|
+
) {
|
|
417
588
|
throw new Error(
|
|
418
589
|
`playbooks.${id}.from changed from the stored structural projection`,
|
|
419
590
|
);
|
|
@@ -663,9 +834,26 @@ function rejectConfiguredHostCapabilities(value, path) {
|
|
|
663
834
|
// PBCLI-46: normalize into a detached, deeply frozen JSON plan. The plan has
|
|
664
835
|
// only execution data and presentation data; imported registry functions are
|
|
665
836
|
// consulted for validation and then discarded.
|
|
666
|
-
export async function normalizeLaunchPlan(
|
|
837
|
+
export async function normalizeLaunchPlan(top, options = {}) {
|
|
838
|
+
return await normalizeLaunchPlanWith(
|
|
839
|
+
top,
|
|
840
|
+
options,
|
|
841
|
+
addCurrentPlaybookIds(top),
|
|
842
|
+
);
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
// `currentPlaybookIds` names every playbook of the current merged config,
|
|
846
|
+
// gathered before a reopen projected the layers to its selection.
|
|
847
|
+
async function normalizeLaunchPlanWith(
|
|
667
848
|
top,
|
|
668
|
-
{
|
|
849
|
+
{
|
|
850
|
+
loadModule,
|
|
851
|
+
configPath,
|
|
852
|
+
prepareRegistryModule,
|
|
853
|
+
selectedMembers,
|
|
854
|
+
modules,
|
|
855
|
+
} = {},
|
|
856
|
+
currentPlaybookIds,
|
|
669
857
|
) {
|
|
670
858
|
const importModule = loadModule ?? ((specifier) => import(specifier));
|
|
671
859
|
top = projectSelectedMembers(top, selectedMembers);
|
|
@@ -714,6 +902,13 @@ export async function normalizeLaunchPlan(
|
|
|
714
902
|
if (ids.some((id) => id.trim().length === 0 || id !== id.trim())) {
|
|
715
903
|
throw new Error("playbooks keys must be canonical trimmed nonblank ids");
|
|
716
904
|
}
|
|
905
|
+
// PBCLI-94 (DR-083): supplied modules are checked before any preparation
|
|
906
|
+
// or import; an ordinary reopen skips a current playbook it does not hold.
|
|
907
|
+
const suppliedModules = resolveSuppliedModules(
|
|
908
|
+
modules,
|
|
909
|
+
ids,
|
|
910
|
+
selectedMembers === undefined ? new Set(ids) : currentPlaybookIds,
|
|
911
|
+
);
|
|
717
912
|
|
|
718
913
|
let captain = resolveDelegationDefault(
|
|
719
914
|
resolveAgent(top.captain, "captain", ["from", "options"]),
|
|
@@ -741,16 +936,28 @@ export async function normalizeLaunchPlan(
|
|
|
741
936
|
throw legacyPlayersError(`playbooks.${id}.players`, configPath);
|
|
742
937
|
}
|
|
743
938
|
rejectConfiguredHostCapabilities(block, `playbooks.${id}`);
|
|
744
|
-
const
|
|
939
|
+
const hasFrom = hasOwn(block, "from");
|
|
745
940
|
if (
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
941
|
+
hasFrom &&
|
|
942
|
+
(typeof block.from !== "string" ||
|
|
943
|
+
block.from.trim().length === 0 ||
|
|
944
|
+
block.from !== block.from.trim())
|
|
749
945
|
) {
|
|
750
946
|
throw new Error(
|
|
751
947
|
`playbooks.${id}.from must be a canonical trimmed module specifier`,
|
|
752
948
|
);
|
|
753
949
|
}
|
|
950
|
+
// PBCLI-9/94 (DR-083): the supplied module wins over `from`, as a --with
|
|
951
|
+
// fragment wins over the file; with neither, the playbook names no module.
|
|
952
|
+
const suppliedModule = suppliedModules.get(id);
|
|
953
|
+
if (!hasFrom && suppliedModule === undefined) {
|
|
954
|
+
throw missingModuleError(id);
|
|
955
|
+
}
|
|
956
|
+
const from = suppliedModule ?? block.from;
|
|
957
|
+
const fromLabel =
|
|
958
|
+
suppliedModule === undefined
|
|
959
|
+
? `playbooks.${id}.from`
|
|
960
|
+
: `the module supplied for ${id}`;
|
|
754
961
|
if (
|
|
755
962
|
block.command !== undefined &&
|
|
756
963
|
(typeof block.command !== "string" ||
|
|
@@ -816,10 +1023,12 @@ export async function normalizeLaunchPlan(
|
|
|
816
1023
|
([key]) => !PLAYBOOK_LAUNCHER_KEYS.includes(key),
|
|
817
1024
|
),
|
|
818
1025
|
);
|
|
819
|
-
const configuredFrom =
|
|
1026
|
+
const configuredFrom =
|
|
1027
|
+
suppliedModule ?? canonicalizeRegistrySpecifier(from, configPath);
|
|
820
1028
|
configuredPlaybooks.push({
|
|
821
1029
|
id,
|
|
822
1030
|
from,
|
|
1031
|
+
fromLabel,
|
|
823
1032
|
configuredFrom,
|
|
824
1033
|
commandOverride: block.command,
|
|
825
1034
|
roles,
|
|
@@ -906,7 +1115,7 @@ export async function normalizeLaunchPlan(
|
|
|
906
1115
|
// registry modules evaluated and others untouched.
|
|
907
1116
|
const preparedPlaybooks = [];
|
|
908
1117
|
for (const configured of configuredPlaybooks) {
|
|
909
|
-
const { id, from, configuredFrom } = configured;
|
|
1118
|
+
const { id, from, fromLabel, configuredFrom } = configured;
|
|
910
1119
|
let preparedFrom = configuredFrom;
|
|
911
1120
|
if (prepareRegistryModule !== undefined) {
|
|
912
1121
|
try {
|
|
@@ -919,7 +1128,7 @@ export async function normalizeLaunchPlan(
|
|
|
919
1128
|
if (prepared !== undefined) preparedFrom = prepared;
|
|
920
1129
|
} catch (cause) {
|
|
921
1130
|
throw new Error(
|
|
922
|
-
|
|
1131
|
+
`${fromLabel} "${from}" failed to prepare: ${errorMessage(cause)}`,
|
|
923
1132
|
);
|
|
924
1133
|
}
|
|
925
1134
|
if (
|
|
@@ -927,13 +1136,13 @@ export async function normalizeLaunchPlan(
|
|
|
927
1136
|
preparedFrom.trim().length === 0
|
|
928
1137
|
) {
|
|
929
1138
|
throw new Error(
|
|
930
|
-
|
|
1139
|
+
`${fromLabel} "${from}" preparation must return a module specifier`,
|
|
931
1140
|
);
|
|
932
1141
|
}
|
|
933
1142
|
}
|
|
934
1143
|
preparedFrom = canonicalizePreparedRegistrySpecifier(
|
|
935
1144
|
preparedFrom,
|
|
936
|
-
|
|
1145
|
+
fromLabel,
|
|
937
1146
|
);
|
|
938
1147
|
preparedPlaybooks.push({ ...configured, preparedFrom });
|
|
939
1148
|
}
|
|
@@ -945,6 +1154,7 @@ export async function normalizeLaunchPlan(
|
|
|
945
1154
|
for (const {
|
|
946
1155
|
id,
|
|
947
1156
|
from,
|
|
1157
|
+
fromLabel,
|
|
948
1158
|
preparedFrom,
|
|
949
1159
|
commandOverride,
|
|
950
1160
|
roles,
|
|
@@ -956,14 +1166,14 @@ export async function normalizeLaunchPlan(
|
|
|
956
1166
|
mod = await importModule(preparedFrom);
|
|
957
1167
|
} catch (cause) {
|
|
958
1168
|
throw new Error(
|
|
959
|
-
|
|
1169
|
+
`${fromLabel} "${from}" failed to import: ${errorMessage(cause)}`,
|
|
960
1170
|
);
|
|
961
1171
|
}
|
|
962
1172
|
const entry = snapshotRegistryEntry(mod?.default);
|
|
963
1173
|
const registryProblem = invalidRegistryEntryReason(entry);
|
|
964
1174
|
if (registryProblem !== undefined) {
|
|
965
1175
|
throw new Error(
|
|
966
|
-
|
|
1176
|
+
`${fromLabel} "${from}" exposes no valid registry entry: ` +
|
|
967
1177
|
registryProblem,
|
|
968
1178
|
);
|
|
969
1179
|
}
|
|
@@ -18,10 +18,12 @@ import {
|
|
|
18
18
|
} from "./adapter-sdk.js";
|
|
19
19
|
import {
|
|
20
20
|
adaptersFromLaunchPlan,
|
|
21
|
+
extractModuleFlags,
|
|
21
22
|
extractWithFlags,
|
|
22
23
|
loadLaunchPlan,
|
|
23
24
|
loadSelectedLaunchPlanDataOnly,
|
|
24
25
|
projectTmuxConfig,
|
|
26
|
+
suppliedModulesFromFlags,
|
|
25
27
|
resolveLaunchSessionsDir,
|
|
26
28
|
relocateLegacyUserConfig,
|
|
27
29
|
resolveFormerUserConfigPaths,
|
|
@@ -54,15 +56,18 @@ export {
|
|
|
54
56
|
checkReadiness,
|
|
55
57
|
composeGenericConfig,
|
|
56
58
|
deriveLaunchReadiness,
|
|
59
|
+
extractModuleFlags,
|
|
57
60
|
extractWithFlags,
|
|
58
61
|
loadLaunchPlan,
|
|
59
62
|
loadOverlayFragment,
|
|
60
63
|
mergeConfigs,
|
|
61
64
|
migrateRetiredProfiles,
|
|
62
65
|
normalizeLaunchPlan,
|
|
66
|
+
parseModuleFlagValue,
|
|
63
67
|
projectTmuxConfig,
|
|
64
68
|
resolveAgent,
|
|
65
69
|
resolveUserConfigPath,
|
|
70
|
+
suppliedModulesFromFlags,
|
|
66
71
|
} from "./launch-config.js";
|
|
67
72
|
|
|
68
73
|
const READINESS_FAILURE_EXIT_CODE = 2;
|
|
@@ -188,10 +193,15 @@ export async function runPlaybookCli(options = {}) {
|
|
|
188
193
|
// PBCLI-25/26: `--with <path>` overlays are launcher-owned — consumed
|
|
189
194
|
// here, never forwarded to tmux-play, and incompatible with a raw
|
|
190
195
|
// `--config` launch, which bypasses the composition they target.
|
|
196
|
+
// PBCLI-94 (DR-083): `--module <id>=<specifier>` is launcher-owned in the
|
|
197
|
+
// same way: consumed, never forwarded, and refused beside raw `--config`.
|
|
191
198
|
let withPaths;
|
|
199
|
+
let modulePairs;
|
|
200
|
+
let modules;
|
|
192
201
|
let forwardArgv;
|
|
193
202
|
try {
|
|
194
203
|
({ withPaths, rest: forwardArgv } = extractWithFlags(argv));
|
|
204
|
+
({ modulePairs, rest: forwardArgv } = extractModuleFlags(forwardArgv));
|
|
195
205
|
} catch (error) {
|
|
196
206
|
stderr.write(`playbook: ${errorMessage(error)}\n`);
|
|
197
207
|
return { code: COMPOSITION_FAILURE_EXIT_CODE };
|
|
@@ -203,6 +213,22 @@ export async function runPlaybookCli(options = {}) {
|
|
|
203
213
|
);
|
|
204
214
|
return { code: COMPOSITION_FAILURE_EXIT_CODE };
|
|
205
215
|
}
|
|
216
|
+
if (modulePairs.length > 0 && hasExplicitConfig(argv)) {
|
|
217
|
+
stderr.write(
|
|
218
|
+
"playbook: --module supplies a playbook module for the top-level " +
|
|
219
|
+
"config and cannot combine with a raw --config launch\n",
|
|
220
|
+
);
|
|
221
|
+
return { code: COMPOSITION_FAILURE_EXIT_CODE };
|
|
222
|
+
}
|
|
223
|
+
try {
|
|
224
|
+
modules = suppliedModulesFromFlags(
|
|
225
|
+
modulePairs,
|
|
226
|
+
options.cwd ?? process.cwd(),
|
|
227
|
+
);
|
|
228
|
+
} catch (error) {
|
|
229
|
+
stderr.write(`playbook: ${errorMessage(error)}\n`);
|
|
230
|
+
return { code: COMPOSITION_FAILURE_EXIT_CODE };
|
|
231
|
+
}
|
|
206
232
|
const noProvision = forwardArgv.includes("--no-provision");
|
|
207
233
|
forwardArgv = forwardArgv.filter((arg) => arg !== "--no-provision");
|
|
208
234
|
if (noProvision && hasExplicitConfig(argv)) {
|
|
@@ -349,6 +375,7 @@ export async function runPlaybookCli(options = {}) {
|
|
|
349
375
|
? await loadSelectedLaunchPlanDataOnly({
|
|
350
376
|
userConfigPath,
|
|
351
377
|
overlayPaths: withPaths,
|
|
378
|
+
modules,
|
|
352
379
|
structuralProjection: selectedRecord.structuralProjection,
|
|
353
380
|
onNotice: (line) => stderr.write(line),
|
|
354
381
|
env,
|
|
@@ -357,6 +384,7 @@ export async function runPlaybookCli(options = {}) {
|
|
|
357
384
|
: await loadLaunchPlan({
|
|
358
385
|
userConfigPath,
|
|
359
386
|
overlayPaths: withPaths,
|
|
387
|
+
modules,
|
|
360
388
|
env,
|
|
361
389
|
homeDir: home,
|
|
362
390
|
loadModule,
|
|
@@ -923,13 +951,17 @@ function helpText({
|
|
|
923
951
|
...sdkFailureLines,
|
|
924
952
|
...failures,
|
|
925
953
|
"Usage:",
|
|
926
|
-
" playbook [--with <path>]... [--
|
|
927
|
-
"
|
|
928
|
-
" playbook --
|
|
929
|
-
"
|
|
954
|
+
" playbook [--with <path>]... [--module <id>=<specifier>]...",
|
|
955
|
+
" [--no-provision] [--cwd <path>]",
|
|
956
|
+
" playbook --session <id> [--with <path>]...",
|
|
957
|
+
" [--module <id>=<specifier>]... [--no-provision]",
|
|
958
|
+
" playbook --list [--with <path>]... [--module <id>=<specifier>]...",
|
|
959
|
+
" [--no-provision]",
|
|
960
|
+
" playbook --theme-diagnostics [--with <path>]...",
|
|
961
|
+
" [--module <id>=<specifier>]... [--cwd <path>]",
|
|
930
962
|
" playbook --config <path> [tmux-play arguments...]",
|
|
931
|
-
" playbook run [--with <path>]... [--
|
|
932
|
-
" [--verbose] [--] [input]",
|
|
963
|
+
" playbook run [--with <path>]... [--module <id>=<specifier>]...",
|
|
964
|
+
" [--no-provision] [--json] [--verbose] [--] [input]",
|
|
933
965
|
" playbook run (--continue | --session <id>) [reply]",
|
|
934
966
|
" playbook run --session <id> --retry-uncertain",
|
|
935
967
|
" playbook run --session <id> --discard-uncertain",
|
|
@@ -947,6 +979,11 @@ function helpText({
|
|
|
947
979
|
" the default config) for a fresh launch or compatible ordinary reopen —",
|
|
948
980
|
" maps merge recursively, other values replace, later files win, and the",
|
|
949
981
|
" default config file is never modified.",
|
|
982
|
+
" --module <id>=<specifier> supplies playbook <id>'s registry module",
|
|
983
|
+
" ahead of its playbooks.<id>.from (repeatable, one per id); a relative",
|
|
984
|
+
" path resolves against the current directory, and the module is never",
|
|
985
|
+
" written back to your config files. Without --module or from, a",
|
|
986
|
+
" reopened session uses the module it already records.",
|
|
950
987
|
" --no-provision keeps configured filesystem registries read-only;",
|
|
951
988
|
" any missing engine links remain a launch error.",
|
|
952
989
|
" `playbook run --verbose` prints Captain telemetry topics to stderr.",
|
|
@@ -39,12 +39,14 @@ import {
|
|
|
39
39
|
invalidRegistryEntryReason,
|
|
40
40
|
loadLaunchPlan,
|
|
41
41
|
loadSelectedLaunchPlanDataOnly,
|
|
42
|
+
parseModuleFlagValue,
|
|
42
43
|
projectHostAgent,
|
|
43
44
|
resolveLaunchSessionsDir,
|
|
44
45
|
relocateLegacyUserConfig,
|
|
45
46
|
resolveFormerUserConfigPaths,
|
|
46
47
|
resolveUserConfigPath,
|
|
47
48
|
snapshotRegistryEntry,
|
|
49
|
+
suppliedModulesFromFlags,
|
|
48
50
|
} from "./launch-config.js";
|
|
49
51
|
import { prepareConfiguredRegistries } from "./provision.js";
|
|
50
52
|
import {
|
|
@@ -122,6 +124,12 @@ export async function runPlaybookRun(options = {}) {
|
|
|
122
124
|
const recovering = args.retryUncertain || args.discardUncertain;
|
|
123
125
|
const continuing = args.continue || args.sessionId !== undefined;
|
|
124
126
|
let input = args.input;
|
|
127
|
+
// PBCLI-94 (DR-083): a relative supplied module resolves against the
|
|
128
|
+
// invocation's working directory before the loader sees it.
|
|
129
|
+
const modules = suppliedModulesFromFlags(
|
|
130
|
+
args.modulePairs,
|
|
131
|
+
options.cwd ?? process.cwd(),
|
|
132
|
+
);
|
|
125
133
|
|
|
126
134
|
// PBCLI-18/40: a fresh piped producer is drained before config,
|
|
127
135
|
// preparation, import, or readiness. Continuations first inspect the
|
|
@@ -369,6 +377,7 @@ export async function runPlaybookRun(options = {}) {
|
|
|
369
377
|
? await loadSelectedLaunchPlanDataOnly({
|
|
370
378
|
userConfigPath,
|
|
371
379
|
overlayPaths: args.withPaths,
|
|
380
|
+
modules,
|
|
372
381
|
structuralProjection: priorRecord.structuralProjection,
|
|
373
382
|
onNotice: (line) => configNotices.push(line),
|
|
374
383
|
env,
|
|
@@ -377,6 +386,7 @@ export async function runPlaybookRun(options = {}) {
|
|
|
377
386
|
: await loadLaunchPlan({
|
|
378
387
|
userConfigPath,
|
|
379
388
|
overlayPaths: args.withPaths,
|
|
389
|
+
modules,
|
|
380
390
|
env,
|
|
381
391
|
homeDir: home,
|
|
382
392
|
loadModule,
|
|
@@ -1561,6 +1571,7 @@ export function parseRunArgs(argv) {
|
|
|
1561
1571
|
const parsed = {
|
|
1562
1572
|
input: undefined,
|
|
1563
1573
|
withPaths: [],
|
|
1574
|
+
modulePairs: [],
|
|
1564
1575
|
noProvision: false,
|
|
1565
1576
|
json: false,
|
|
1566
1577
|
verbose: false,
|
|
@@ -1625,6 +1636,18 @@ export function parseRunArgs(argv) {
|
|
|
1625
1636
|
const value = arg.slice("--with=".length);
|
|
1626
1637
|
if (value === "") throw new Error("--with needs a value");
|
|
1627
1638
|
parsed.withPaths.push(value);
|
|
1639
|
+
} else if (arg === "--module" || arg.startsWith("--module=")) {
|
|
1640
|
+
// PBCLI-94 (DR-083): one `<id>=<specifier>` per playbook id.
|
|
1641
|
+
const pair = parseModuleFlagValue(
|
|
1642
|
+
arg === "--module" ? argv[index + 1] : arg.slice("--module=".length),
|
|
1643
|
+
);
|
|
1644
|
+
if (arg === "--module") index += 1;
|
|
1645
|
+
if (parsed.modulePairs.some(([id]) => id === pair[0])) {
|
|
1646
|
+
throw new Error(
|
|
1647
|
+
`--module names playbook ${JSON.stringify(pair[0])} more than once`,
|
|
1648
|
+
);
|
|
1649
|
+
}
|
|
1650
|
+
parsed.modulePairs.push(pair);
|
|
1628
1651
|
} else if (
|
|
1629
1652
|
RETIRED_FLAGS.has(arg) ||
|
|
1630
1653
|
[...RETIRED_FLAGS].some((flag) => arg.startsWith(`${flag}=`))
|
|
@@ -1681,6 +1704,12 @@ export function parseRunArgs(argv) {
|
|
|
1681
1704
|
) {
|
|
1682
1705
|
throw new Error("--with is unavailable during uncertain-turn recovery");
|
|
1683
1706
|
}
|
|
1707
|
+
if (
|
|
1708
|
+
(parsed.retryUncertain || parsed.discardUncertain) &&
|
|
1709
|
+
parsed.modulePairs.length > 0
|
|
1710
|
+
) {
|
|
1711
|
+
throw new Error("--module is unavailable during uncertain-turn recovery");
|
|
1712
|
+
}
|
|
1684
1713
|
if (
|
|
1685
1714
|
parsed.sessionId !== undefined &&
|
|
1686
1715
|
!SESSION_ID_PATTERN.test(parsed.sessionId)
|
|
@@ -1928,10 +1957,11 @@ async function writeStream(stream, text) {
|
|
|
1928
1957
|
function runHelpText(userConfigPath) {
|
|
1929
1958
|
return [
|
|
1930
1959
|
"Usage:",
|
|
1931
|
-
" playbook run [--with <path>]... [--
|
|
1932
|
-
" [--verbose] [--] [input]",
|
|
1960
|
+
" playbook run [--with <path>]... [--module <id>=<specifier>]...",
|
|
1961
|
+
" [--no-provision] [--json] [--verbose] [--] [input]",
|
|
1933
1962
|
" playbook run (--continue | --session <id>) [--with <path>]...",
|
|
1934
|
-
" [--
|
|
1963
|
+
" [--module <id>=<specifier>]... [--no-provision] [--json]",
|
|
1964
|
+
" [--verbose] [--] [reply]",
|
|
1935
1965
|
" playbook run --session <id> --retry-uncertain [--no-provision]",
|
|
1936
1966
|
" playbook run --session <id> --discard-uncertain",
|
|
1937
1967
|
"",
|
|
@@ -1959,6 +1989,10 @@ function runHelpText(userConfigPath) {
|
|
|
1959
1989
|
"",
|
|
1960
1990
|
"Options:",
|
|
1961
1991
|
" --with <path> overlay a generic config fragment (repeatable)",
|
|
1992
|
+
" --module <id>=<specifier>",
|
|
1993
|
+
" supply playbook <id>'s module ahead of its",
|
|
1994
|
+
" playbooks.<id>.from (repeatable, one per id); a",
|
|
1995
|
+
" continuation without either uses the stored module",
|
|
1962
1996
|
" --no-provision do not provision thin filesystem registry engines",
|
|
1963
1997
|
" --continue prefer this working directory, else global newest",
|
|
1964
1998
|
" --session <id> reply to one durable Captain session UUID",
|