@sublang/playbook 7.0.0 → 8.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 +17 -4
- package/docs/cli.md +74 -29
- package/docs/configuration.md +209 -112
- package/docs/embedding.md +71 -25
- package/package.json +4 -3
- package/reference/sdlc/captain.playbook/captain.playbook.js +3 -3
- package/reference/sdlc/captain.playbook/captain.playbook.ts +3 -3
- package/reference/sdlc/code.md +1 -1
- package/reference/sdlc/code.playbook/bin/interactive-session.js +816 -0
- package/reference/sdlc/code.playbook/bin/launch-config.js +1078 -116
- package/reference/sdlc/code.playbook/bin/playbook.js +489 -34
- package/reference/sdlc/code.playbook/bin/run.js +283 -298
- package/reference/sdlc/code.playbook/bin/session-store.js +818 -26
- package/reference/sdlc/code.playbook/code.fsm.d.ts +5 -5
- package/reference/sdlc/code.playbook/code.fsm.introspect.js +2 -2
- package/reference/sdlc/code.playbook/code.fsm.introspect.ts +2 -2
- package/reference/sdlc/code.playbook/code.fsm.js +7 -11
- package/reference/sdlc/code.playbook/code.fsm.ts +9 -17
- package/reference/sdlc/code.playbook/code.gears.md +1 -1
- package/reference/sdlc/code.playbook/code.playbook.d.ts +2 -1
- package/reference/sdlc/code.playbook/code.playbook.js +12 -13
- package/reference/sdlc/code.playbook/code.playbook.ts +22 -15
- package/reference/sdlc/code.playbook/code.registry.d.ts +5 -13
- package/reference/sdlc/code.playbook/code.registry.js +3 -10
- package/reference/sdlc/code.playbook/code.registry.ts +7 -32
- package/reference/sdlc/code.playbook/playbook-captain.d.ts +39 -14
- package/reference/sdlc/code.playbook/playbook-captain.js +970 -289
- package/reference/sdlc/code.playbook/playbook-captain.ts +1403 -396
- package/reference/sdlc/code.playbook/playbook.config.template.yaml +41 -49
- package/reference/sdlc/decide.md +4 -4
- package/reference/sdlc/decide.playbook/decide.fsm.d.ts +9 -9
- package/reference/sdlc/decide.playbook/decide.fsm.js +21 -14
- package/reference/sdlc/decide.playbook/decide.fsm.ts +27 -23
- package/reference/sdlc/decide.playbook/decide.gears.md +3 -5
- package/reference/sdlc/decide.playbook/decide.playbook.d.ts +9 -13
- package/reference/sdlc/decide.playbook/decide.playbook.js +171 -134
- package/reference/sdlc/decide.playbook/decide.playbook.ts +238 -162
- package/reference/sdlc/decide.playbook/decide.registry.d.ts +5 -13
- package/reference/sdlc/decide.playbook/decide.registry.js +3 -9
- package/reference/sdlc/decide.playbook/decide.registry.ts +7 -31
- package/reference/sdlc/review.md +4 -5
- package/reference/sdlc/review.playbook/review.fsm.d.ts +9 -11
- package/reference/sdlc/review.playbook/review.fsm.js +30 -24
- package/reference/sdlc/review.playbook/review.fsm.ts +39 -35
- package/reference/sdlc/review.playbook/review.gears.md +6 -5
- package/reference/sdlc/review.playbook/review.playbook.d.ts +2 -1
- package/reference/sdlc/review.playbook/review.playbook.js +16 -21
- package/reference/sdlc/review.playbook/review.playbook.ts +26 -26
- package/reference/sdlc/review.playbook/review.registry.d.ts +5 -13
- package/reference/sdlc/review.playbook/review.registry.js +3 -16
- package/reference/sdlc/review.playbook/review.registry.ts +7 -38
- package/slc/gears2fsm.md +27 -23
- package/slc/link.md +113 -93
- package/slc/text2gears.md +19 -18
- package/src/runtime.d.ts +20 -16
- package/src/runtime.ts +19 -23
- package/src/xstate-playbook-runtime.d.ts +21 -17
- package/src/xstate-playbook-runtime.js +241 -149
- package/src/xstate-playbook-runtime.ts +331 -178
- package/src/xstate-runtime.js +63 -24
- package/src/xstate-runtime.ts +96 -28
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
import { homedir, tmpdir } from 'node:os';
|
|
19
19
|
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
20
20
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
21
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
21
22
|
import {
|
|
22
23
|
parse as parseYaml,
|
|
23
24
|
parseDocument as parseYamlDocument,
|
|
@@ -35,9 +36,10 @@ const DEFAULT_TEMPLATE_PATH = resolve(
|
|
|
35
36
|
// PBCLI-1/8: the tmux projection uses the Playbook Captain shell adapter.
|
|
36
37
|
export const PLAYBOOK_CAPTAIN_MODULE =
|
|
37
38
|
'@sublang/playbook/playbook-captain';
|
|
38
|
-
const PLAYBOOK_LAUNCHER_KEYS = ['from', 'command', '
|
|
39
|
+
const PLAYBOOK_LAUNCHER_KEYS = ['from', 'command', 'roles'];
|
|
39
40
|
const PLAYBOOK_TOP_LEVEL_KEYS = new Set([
|
|
40
41
|
'captain',
|
|
42
|
+
'players',
|
|
41
43
|
'playbooks',
|
|
42
44
|
'layout',
|
|
43
45
|
'notifications',
|
|
@@ -45,6 +47,8 @@ const PLAYBOOK_TOP_LEVEL_KEYS = new Set([
|
|
|
45
47
|
]);
|
|
46
48
|
const RESERVED_CAPTAIN_PLAYBOOK_ID = 'captain';
|
|
47
49
|
const RESERVED_CAPTAIN_ROLE_ID = 'captain';
|
|
50
|
+
const PLAYER_ID_PATTERN = /^[a-z][a-z0-9_-]*(?:\.[a-z][a-z0-9_-]*)*$/;
|
|
51
|
+
const ROLE_ID_PATTERN = /^[a-z][a-z0-9_-]*$/;
|
|
48
52
|
|
|
49
53
|
// PBCLI-26: split ordered `--with <path>` pairs out of an argument vector.
|
|
50
54
|
// The returned arrays are new values; the caller's vector is never changed.
|
|
@@ -110,6 +114,18 @@ export function mergeConfigs(base, overlay) {
|
|
|
110
114
|
]);
|
|
111
115
|
}
|
|
112
116
|
|
|
117
|
+
// PBCLI-22/46: ordinary reopen must merge only durable catalog members.
|
|
118
|
+
// A selected member may first appear in an overlay, so each layer is pruned
|
|
119
|
+
// permissively before merge and the final strict projection below owns the
|
|
120
|
+
// missing-member diagnostic. Unselected accessors are never observed.
|
|
121
|
+
export function mergeSelectedConfigs(base, overlay, selectedMembers) {
|
|
122
|
+
const selected = validateSelectedMembers(selectedMembers);
|
|
123
|
+
return mergeConfigs(
|
|
124
|
+
projectSelectedLayer(base, selected, 'config'),
|
|
125
|
+
projectSelectedLayer(overlay, selected, 'overlay'),
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
113
129
|
export function resolveConfigHome(env = process.env, home = homedir()) {
|
|
114
130
|
return env.XDG_CONFIG_HOME || join(home, '.config');
|
|
115
131
|
}
|
|
@@ -145,9 +161,14 @@ export async function loadLaunchPlan({
|
|
|
145
161
|
prepareRegistryModule,
|
|
146
162
|
templatePath = DEFAULT_TEMPLATE_PATH,
|
|
147
163
|
onNotice = () => {},
|
|
164
|
+
selectedMembers,
|
|
148
165
|
}) {
|
|
149
166
|
seedUserConfigIfMissing(userConfigPath, templatePath, onNotice);
|
|
150
|
-
|
|
167
|
+
// PBCLI-22/46: a reopen must not rewrite, validate, or otherwise inspect
|
|
168
|
+
// config members outside the stored projection.
|
|
169
|
+
if (selectedMembers === undefined) {
|
|
170
|
+
migrateUserConfigIfRetired(userConfigPath, onNotice);
|
|
171
|
+
}
|
|
151
172
|
|
|
152
173
|
let top = parseYaml(readFileSync(userConfigPath, 'utf8')) ?? {};
|
|
153
174
|
if (overlayPaths.length > 0 && !isObject(top)) {
|
|
@@ -156,15 +177,328 @@ export async function loadLaunchPlan({
|
|
|
156
177
|
);
|
|
157
178
|
}
|
|
158
179
|
for (const overlayPath of overlayPaths) {
|
|
159
|
-
|
|
180
|
+
const overlay = loadOverlayFragment(overlayPath);
|
|
181
|
+
top =
|
|
182
|
+
selectedMembers === undefined
|
|
183
|
+
? mergeConfigs(top, overlay)
|
|
184
|
+
: mergeSelectedConfigs(top, overlay, selectedMembers);
|
|
160
185
|
}
|
|
161
186
|
return await normalizeLaunchPlan(top, {
|
|
162
187
|
loadModule,
|
|
163
188
|
configPath: userConfigPath,
|
|
164
189
|
prepareRegistryModule,
|
|
190
|
+
selectedMembers,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// PBCLI-22/49: an interactive selected launch may project current tuning and
|
|
195
|
+
// presentation before its pane child owns the writer lease, but it must not
|
|
196
|
+
// prepare or import a registry from an unlocked advisory read. Rebuild the
|
|
197
|
+
// durable catalog from the validated stored structural projection and use the
|
|
198
|
+
// current config only for compatible Captain/player tuning and presentation.
|
|
199
|
+
export async function loadSelectedLaunchPlanDataOnly({
|
|
200
|
+
userConfigPath,
|
|
201
|
+
overlayPaths = [],
|
|
202
|
+
structuralProjection,
|
|
203
|
+
templatePath = DEFAULT_TEMPLATE_PATH,
|
|
204
|
+
onNotice = () => {},
|
|
205
|
+
}) {
|
|
206
|
+
const stored = validateStoredStructuralProjection(structuralProjection);
|
|
207
|
+
const selectedMembers = selectedMembersFromStoredStructure(stored);
|
|
208
|
+
seedUserConfigIfMissing(userConfigPath, templatePath, onNotice);
|
|
209
|
+
|
|
210
|
+
let top = parseYaml(readFileSync(userConfigPath, 'utf8')) ?? {};
|
|
211
|
+
if (overlayPaths.length > 0 && !isObject(top)) {
|
|
212
|
+
throw new Error(
|
|
213
|
+
`the top-level config at ${userConfigPath} must be a YAML map before --with can overlay it`,
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
top = projectSelectedLayer(
|
|
217
|
+
top,
|
|
218
|
+
validateSelectedMembers(selectedMembers),
|
|
219
|
+
'config',
|
|
220
|
+
);
|
|
221
|
+
for (const overlayPath of overlayPaths) {
|
|
222
|
+
top = mergeSelectedConfigs(
|
|
223
|
+
top,
|
|
224
|
+
loadOverlayFragment(overlayPath),
|
|
225
|
+
selectedMembers,
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
return await normalizeSelectedLaunchPlanDataOnly(top, {
|
|
229
|
+
configPath: userConfigPath,
|
|
230
|
+
stored,
|
|
231
|
+
selectedMembers,
|
|
165
232
|
});
|
|
166
233
|
}
|
|
167
234
|
|
|
235
|
+
export async function normalizeSelectedLaunchPlanDataOnly(
|
|
236
|
+
top,
|
|
237
|
+
{ configPath, stored, selectedMembers } = {},
|
|
238
|
+
) {
|
|
239
|
+
stored = validateStoredStructuralProjection(stored);
|
|
240
|
+
const expectedMembers = selectedMembersFromStoredStructure(stored);
|
|
241
|
+
if (selectedMembers !== undefined) {
|
|
242
|
+
const supplied = validateSelectedMembers(selectedMembers);
|
|
243
|
+
if (
|
|
244
|
+
!isDeepStrictEqual(supplied.playbookIds, expectedMembers.playbookIds) ||
|
|
245
|
+
!isDeepStrictEqual(supplied.playerIds, expectedMembers.playerIds)
|
|
246
|
+
) {
|
|
247
|
+
throw new Error(
|
|
248
|
+
'selected launch members do not match the stored structural projection',
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
top = projectSelectedMembers(top, expectedMembers);
|
|
253
|
+
top = cloneJson(top, 'config');
|
|
254
|
+
assertNoRetiredProfiles(top, configPath);
|
|
255
|
+
if (hasOwn(top, 'run')) {
|
|
256
|
+
throw new Error(
|
|
257
|
+
'top-level "run" was removed: configure the shared Captain under ' +
|
|
258
|
+
'captain, top-level players, and playbooks.<id>.roles instead',
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
const unknownTopLevel = Object.keys(top).filter(
|
|
262
|
+
(key) => !PLAYBOOK_TOP_LEVEL_KEYS.has(key),
|
|
263
|
+
);
|
|
264
|
+
if (unknownTopLevel.length > 0) {
|
|
265
|
+
throw new Error(
|
|
266
|
+
`config has unknown top-level ${formatKeyList(unknownTopLevel)}`,
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
if (top.layout !== undefined && !isObject(top.layout)) {
|
|
270
|
+
throw new Error('layout must be a map');
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const playersCfg = requireObject(top.players, 'players');
|
|
274
|
+
const configuredAgents = new Map();
|
|
275
|
+
for (const playerId of expectedMembers.playerIds) {
|
|
276
|
+
assertPlayerId(playerId, `players.${playerId}`);
|
|
277
|
+
const agent = resolveAgent(playersCfg[playerId], `players.${playerId}`, [
|
|
278
|
+
'id',
|
|
279
|
+
]);
|
|
280
|
+
configuredAgents.set(playerId, agent);
|
|
281
|
+
}
|
|
282
|
+
let captain = resolveAgent(top.captain, 'captain', ['from', 'options']);
|
|
283
|
+
|
|
284
|
+
const playbooksCfg = requireObject(top.playbooks, 'playbooks');
|
|
285
|
+
const tuningChecks = [];
|
|
286
|
+
let tuningCheckIndex = 0;
|
|
287
|
+
const authored = new Map();
|
|
288
|
+
for (const id of expectedMembers.playbookIds) {
|
|
289
|
+
const storedItem = stored.catalog[id];
|
|
290
|
+
const block = requireObject(playbooksCfg[id], `playbooks.${id}`);
|
|
291
|
+
if (hasOwn(block, 'players')) {
|
|
292
|
+
throw legacyPlayersError(`playbooks.${id}.players`, configPath);
|
|
293
|
+
}
|
|
294
|
+
if (
|
|
295
|
+
typeof block.from !== 'string' ||
|
|
296
|
+
block.from.trim().length === 0 ||
|
|
297
|
+
block.from !== block.from.trim()
|
|
298
|
+
) {
|
|
299
|
+
throw new Error(
|
|
300
|
+
`playbooks.${id}.from must be a canonical trimmed module specifier`,
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
const configuredFrom = canonicalizeRegistrySpecifier(block.from, configPath);
|
|
304
|
+
if (configuredFrom !== storedItem.from) {
|
|
305
|
+
throw new Error(
|
|
306
|
+
`playbooks.${id}.from changed from the stored structural projection`,
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
const command = block.command ?? storedItem.manifestCommand;
|
|
310
|
+
if (command !== storedItem.command) {
|
|
311
|
+
throw new Error(
|
|
312
|
+
`playbooks.${id}.command changed from the stored structural projection`,
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
const rolesMap = requireObject(block.roles, `playbooks.${id}.roles`);
|
|
316
|
+
assertExactRoleBindings(
|
|
317
|
+
id,
|
|
318
|
+
Object.keys(rolesMap),
|
|
319
|
+
storedItem.requiredRoleIds,
|
|
320
|
+
);
|
|
321
|
+
const bindings = {};
|
|
322
|
+
for (const role of storedItem.requiredRoleIds) {
|
|
323
|
+
const binding = resolveRoleBinding(
|
|
324
|
+
rolesMap[role],
|
|
325
|
+
`playbooks.${id}.roles.${role}`,
|
|
326
|
+
);
|
|
327
|
+
if (binding.playerId !== storedItem.roles[role].playerId) {
|
|
328
|
+
throw new Error(
|
|
329
|
+
`playbooks.${id}.roles.${role} changed its stored player binding`,
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
bindings[role] = binding;
|
|
333
|
+
if (binding.model !== undefined || binding.effort !== undefined) {
|
|
334
|
+
let checkId;
|
|
335
|
+
do {
|
|
336
|
+
checkId = `binding-check-${tuningCheckIndex}`;
|
|
337
|
+
tuningCheckIndex += 1;
|
|
338
|
+
} while (configuredAgents.has(checkId));
|
|
339
|
+
tuningChecks.push({
|
|
340
|
+
id: checkId,
|
|
341
|
+
agent: applyTuningOverrides(
|
|
342
|
+
configuredAgents.get(binding.playerId),
|
|
343
|
+
binding,
|
|
344
|
+
),
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
const optionSlice = Object.fromEntries(
|
|
349
|
+
Object.entries(block).filter(
|
|
350
|
+
([key]) => !PLAYBOOK_LAUNCHER_KEYS.includes(key),
|
|
351
|
+
),
|
|
352
|
+
);
|
|
353
|
+
if (!isDeepStrictEqual(optionSlice, storedItem.options)) {
|
|
354
|
+
throw new Error(
|
|
355
|
+
`playbooks.${id} options changed from the stored structural projection`,
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
authored.set(id, { bindings });
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const firstRoleful = expectedMembers.playbookIds
|
|
362
|
+
.map((id) => stored.catalog[id])
|
|
363
|
+
.find((item) => item.requiredRoleIds.length > 0);
|
|
364
|
+
const initialVisible =
|
|
365
|
+
firstRoleful === undefined
|
|
366
|
+
? []
|
|
367
|
+
: distinct(
|
|
368
|
+
firstRoleful.requiredRoleIds.map(
|
|
369
|
+
(role) => firstRoleful.roles[role].playerId,
|
|
370
|
+
),
|
|
371
|
+
);
|
|
372
|
+
const validationVisible =
|
|
373
|
+
initialVisible.length === 0
|
|
374
|
+
? expectedMembers.playerIds.slice(0, 1)
|
|
375
|
+
: initialVisible;
|
|
376
|
+
const provisional = {
|
|
377
|
+
captain: { ...captain, from: PLAYBOOK_CAPTAIN_MODULE, options: {} },
|
|
378
|
+
players: [
|
|
379
|
+
...expectedMembers.playerIds.map((id) => ({
|
|
380
|
+
id,
|
|
381
|
+
...configuredAgents.get(id),
|
|
382
|
+
})),
|
|
383
|
+
...tuningChecks.map(({ id, agent }) => ({ id, ...agent })),
|
|
384
|
+
],
|
|
385
|
+
layout: {
|
|
386
|
+
...(isObject(top.layout) ? top.layout : {}),
|
|
387
|
+
initialVisible: validationVisible,
|
|
388
|
+
},
|
|
389
|
+
...(top.notifications === undefined
|
|
390
|
+
? {}
|
|
391
|
+
: { notifications: top.notifications }),
|
|
392
|
+
...(top.theme === undefined ? {} : { theme: top.theme }),
|
|
393
|
+
};
|
|
394
|
+
const normalizedHost = await normalizeHostConfig(provisional);
|
|
395
|
+
const normalizedPresentationHost =
|
|
396
|
+
initialVisible.length === 0 && expectedMembers.playerIds.length > 0
|
|
397
|
+
? await normalizeHostConfig({
|
|
398
|
+
...provisional,
|
|
399
|
+
players: [],
|
|
400
|
+
layout: { ...provisional.layout, initialVisible: [] },
|
|
401
|
+
})
|
|
402
|
+
: normalizedHost;
|
|
403
|
+
const { from: _captainFrom, options: _captainOptions, ...normalizedCaptain } =
|
|
404
|
+
normalizedHost.captain;
|
|
405
|
+
captain = sessionAgentFromHostAgent(normalizedCaptain, 'captain');
|
|
406
|
+
const hostAgents = new Map(
|
|
407
|
+
normalizedHost.players.map(({ id, ...agent }) => [id, agent]),
|
|
408
|
+
);
|
|
409
|
+
const normalizedPlayerAgents = new Map(
|
|
410
|
+
expectedMembers.playerIds.map((id) => [
|
|
411
|
+
id,
|
|
412
|
+
sessionAgentFromHostAgent(hostAgents.get(id), `players.${id}`),
|
|
413
|
+
]),
|
|
414
|
+
);
|
|
415
|
+
|
|
416
|
+
const catalog = Object.fromEntries(
|
|
417
|
+
expectedMembers.playbookIds.map((id) => {
|
|
418
|
+
const storedItem = stored.catalog[id];
|
|
419
|
+
const bindings = authored.get(id).bindings;
|
|
420
|
+
return [
|
|
421
|
+
id,
|
|
422
|
+
{
|
|
423
|
+
...cloneJson(storedItem, `stored catalog.${id}`),
|
|
424
|
+
roles: Object.fromEntries(
|
|
425
|
+
storedItem.requiredRoleIds.map((role) => {
|
|
426
|
+
const binding = bindings[role];
|
|
427
|
+
const agent = normalizedPlayerAgents.get(binding.playerId);
|
|
428
|
+
return [
|
|
429
|
+
role,
|
|
430
|
+
{
|
|
431
|
+
playerId: binding.playerId,
|
|
432
|
+
model:
|
|
433
|
+
binding.model === undefined
|
|
434
|
+
? agent.model
|
|
435
|
+
: overrideTuningSelection(binding.model),
|
|
436
|
+
effort:
|
|
437
|
+
binding.effort === undefined
|
|
438
|
+
? agent.effort
|
|
439
|
+
: overrideTuningSelection(binding.effort),
|
|
440
|
+
},
|
|
441
|
+
];
|
|
442
|
+
}),
|
|
443
|
+
),
|
|
444
|
+
},
|
|
445
|
+
];
|
|
446
|
+
}),
|
|
447
|
+
);
|
|
448
|
+
const candidateStructure = {
|
|
449
|
+
schemaVersion: stored.schemaVersion,
|
|
450
|
+
captain: fixedAgentProjection(captain),
|
|
451
|
+
players: expectedMembers.playerIds.map((id) => ({
|
|
452
|
+
id,
|
|
453
|
+
...fixedAgentProjection(normalizedPlayerAgents.get(id)),
|
|
454
|
+
})),
|
|
455
|
+
catalog: Object.fromEntries(
|
|
456
|
+
Object.entries(catalog).map(([id, item]) => [
|
|
457
|
+
id,
|
|
458
|
+
{
|
|
459
|
+
...cloneJson(stored.catalog[id], `stored catalog.${id}`),
|
|
460
|
+
roles: Object.fromEntries(
|
|
461
|
+
Object.entries(item.roles).map(([role, binding]) => [
|
|
462
|
+
role,
|
|
463
|
+
{ playerId: binding.playerId },
|
|
464
|
+
]),
|
|
465
|
+
),
|
|
466
|
+
},
|
|
467
|
+
]),
|
|
468
|
+
),
|
|
469
|
+
};
|
|
470
|
+
if (!isDeepStrictEqual(candidateStructure, stored)) {
|
|
471
|
+
throw new Error(
|
|
472
|
+
'current selected config does not reproduce the stored structural projection',
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
return deepFreeze(
|
|
477
|
+
cloneJson(
|
|
478
|
+
{
|
|
479
|
+
schemaVersion: 1,
|
|
480
|
+
captain,
|
|
481
|
+
players: expectedMembers.playerIds.map((id) => ({
|
|
482
|
+
id,
|
|
483
|
+
agent: normalizedPlayerAgents.get(id),
|
|
484
|
+
})),
|
|
485
|
+
catalog,
|
|
486
|
+
presentation: {
|
|
487
|
+
layout: {
|
|
488
|
+
...normalizedPresentationHost.layout,
|
|
489
|
+
initialVisible,
|
|
490
|
+
},
|
|
491
|
+
notifications: normalizedPresentationHost.notifications,
|
|
492
|
+
...(normalizedPresentationHost.theme === undefined
|
|
493
|
+
? {}
|
|
494
|
+
: { theme: normalizedPresentationHost.theme }),
|
|
495
|
+
},
|
|
496
|
+
},
|
|
497
|
+
'selected launch config',
|
|
498
|
+
),
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
|
|
168
502
|
// PBCLI-8 (DR-021): scalar agents are adapter shorthands and full blocks
|
|
169
503
|
// carry their own settings without profile indirection.
|
|
170
504
|
export function resolveAgent(value, path, reservedKeys = []) {
|
|
@@ -190,15 +524,16 @@ export function resolveAgent(value, path, reservedKeys = []) {
|
|
|
190
524
|
// consulted for validation and then discarded.
|
|
191
525
|
export async function normalizeLaunchPlan(
|
|
192
526
|
top,
|
|
193
|
-
{ loadModule, configPath, prepareRegistryModule } = {},
|
|
527
|
+
{ loadModule, configPath, prepareRegistryModule, selectedMembers } = {},
|
|
194
528
|
) {
|
|
195
529
|
const importModule = loadModule ?? ((specifier) => import(specifier));
|
|
530
|
+
top = projectSelectedMembers(top, selectedMembers);
|
|
196
531
|
top = cloneJson(top, 'config');
|
|
197
532
|
assertNoRetiredProfiles(top, configPath);
|
|
198
533
|
if (hasOwn(top, 'run')) {
|
|
199
534
|
throw new Error(
|
|
200
535
|
'top-level "run" was removed: configure the shared Captain under ' +
|
|
201
|
-
'captain and playbooks.<id>.
|
|
536
|
+
'captain, top-level players, and playbooks.<id>.roles instead',
|
|
202
537
|
);
|
|
203
538
|
}
|
|
204
539
|
const unknownTopLevel = Object.keys(top).filter(
|
|
@@ -213,13 +548,30 @@ export async function normalizeLaunchPlan(
|
|
|
213
548
|
throw new Error('layout must be a map');
|
|
214
549
|
}
|
|
215
550
|
|
|
551
|
+
const playersCfg = requireObject(top.players, 'players');
|
|
552
|
+
const allPlayerIds = Object.keys(playersCfg);
|
|
553
|
+
const configuredAgents = new Map();
|
|
554
|
+
for (const playerId of allPlayerIds) {
|
|
555
|
+
assertPlayerId(playerId, `players.${playerId}`);
|
|
556
|
+
const agent = resolveAgent(playersCfg[playerId], `players.${playerId}`, [
|
|
557
|
+
'id',
|
|
558
|
+
]);
|
|
559
|
+
if (
|
|
560
|
+
typeof agent.adapter !== 'string' ||
|
|
561
|
+
agent.adapter.trim().length === 0
|
|
562
|
+
) {
|
|
563
|
+
throw new Error(`players.${playerId} must resolve an adapter`);
|
|
564
|
+
}
|
|
565
|
+
configuredAgents.set(playerId, agent);
|
|
566
|
+
}
|
|
567
|
+
|
|
216
568
|
const playbooksCfg = requireObject(top.playbooks, 'playbooks');
|
|
217
569
|
const ids = Object.keys(playbooksCfg);
|
|
218
570
|
if (ids.length === 0) {
|
|
219
571
|
throw new Error('playbooks must enable at least one playbook');
|
|
220
572
|
}
|
|
221
|
-
if (ids.some((id) => id.trim().length === 0)) {
|
|
222
|
-
throw new Error('playbooks keys must be nonblank ids');
|
|
573
|
+
if (ids.some((id) => id.trim().length === 0 || id !== id.trim())) {
|
|
574
|
+
throw new Error('playbooks keys must be canonical trimmed nonblank ids');
|
|
223
575
|
}
|
|
224
576
|
|
|
225
577
|
let captain = resolveAgent(top.captain, 'captain', ['from', 'options']);
|
|
@@ -230,10 +582,11 @@ export async function normalizeLaunchPlan(
|
|
|
230
582
|
throw new Error('captain must resolve an adapter');
|
|
231
583
|
}
|
|
232
584
|
|
|
233
|
-
// Validate and detach every config-owned value before provisioning
|
|
234
|
-
// importing any registry. That keeps malformed config side-effect free.
|
|
585
|
+
// Validate and detach every retained config-owned value before provisioning
|
|
586
|
+
// or importing any registry. That keeps malformed config side-effect free.
|
|
235
587
|
const configuredPlaybooks = [];
|
|
236
|
-
const
|
|
588
|
+
const tuningChecks = [];
|
|
589
|
+
let tuningCheckIndex = 0;
|
|
237
590
|
for (const id of ids) {
|
|
238
591
|
if (id === RESERVED_CAPTAIN_PLAYBOOK_ID) {
|
|
239
592
|
throw new Error(
|
|
@@ -241,66 +594,72 @@ export async function normalizeLaunchPlan(
|
|
|
241
594
|
);
|
|
242
595
|
}
|
|
243
596
|
const block = requireObject(playbooksCfg[id], `playbooks.${id}`);
|
|
597
|
+
if (hasOwn(block, 'players')) {
|
|
598
|
+
throw legacyPlayersError(`playbooks.${id}.players`, configPath);
|
|
599
|
+
}
|
|
244
600
|
const from = block.from;
|
|
245
|
-
if (
|
|
246
|
-
|
|
601
|
+
if (
|
|
602
|
+
typeof from !== 'string' ||
|
|
603
|
+
from.trim().length === 0 ||
|
|
604
|
+
from !== from.trim()
|
|
605
|
+
) {
|
|
606
|
+
throw new Error(
|
|
607
|
+
`playbooks.${id}.from must be a canonical trimmed module specifier`,
|
|
608
|
+
);
|
|
247
609
|
}
|
|
248
610
|
if (
|
|
249
611
|
block.command !== undefined &&
|
|
250
|
-
(typeof block.command !== 'string' ||
|
|
612
|
+
(typeof block.command !== 'string' ||
|
|
613
|
+
block.command.trim().length === 0 ||
|
|
614
|
+
block.command !== block.command.trim())
|
|
251
615
|
) {
|
|
252
|
-
throw new Error(
|
|
616
|
+
throw new Error(
|
|
617
|
+
`playbooks.${id}.command must be a canonical trimmed nonblank string`,
|
|
618
|
+
);
|
|
253
619
|
}
|
|
254
620
|
if (block.command === RESERVED_CAPTAIN_PLAYBOOK_ID) {
|
|
255
621
|
throw new Error(
|
|
256
622
|
`playbooks.${id}.command collides with the reserved internal Captain command`,
|
|
257
623
|
);
|
|
258
624
|
}
|
|
259
|
-
const
|
|
260
|
-
const roles = Object.keys(
|
|
261
|
-
if (roles.length === 0) {
|
|
262
|
-
throw new Error(`playbooks.${id} resolves no visible local role`);
|
|
263
|
-
}
|
|
625
|
+
const rolesMap = requireObject(block.roles, `playbooks.${id}.roles`);
|
|
626
|
+
const roles = Object.keys(rolesMap);
|
|
264
627
|
if (roles.some((role) => role.trim().length === 0)) {
|
|
265
|
-
throw new Error(`playbooks.${id}.
|
|
628
|
+
throw new Error(`playbooks.${id}.roles keys must be nonblank role ids`);
|
|
266
629
|
}
|
|
267
630
|
if (roles.includes(RESERVED_CAPTAIN_ROLE_ID)) {
|
|
268
631
|
throw new Error(
|
|
269
|
-
`playbooks.${id}.
|
|
632
|
+
`playbooks.${id}.roles.${RESERVED_CAPTAIN_ROLE_ID} binds local ` +
|
|
270
633
|
`role "${RESERVED_CAPTAIN_ROLE_ID}", which is reserved for the ` +
|
|
271
634
|
'tmux-play Captain',
|
|
272
635
|
);
|
|
273
636
|
}
|
|
274
|
-
const
|
|
275
|
-
const generated = [];
|
|
276
|
-
const playerIdEntries = [];
|
|
637
|
+
const bindings = Object.create(null);
|
|
277
638
|
for (const role of roles) {
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
639
|
+
assertRoleId(role, `playbooks.${id}.roles.${role}`);
|
|
640
|
+
const binding = resolveRoleBinding(
|
|
641
|
+
rolesMap[role],
|
|
642
|
+
`playbooks.${id}.roles.${role}`,
|
|
282
643
|
);
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
agent.adapter.trim().length === 0
|
|
286
|
-
) {
|
|
644
|
+
const agent = configuredAgents.get(binding.playerId);
|
|
645
|
+
if (agent === undefined) {
|
|
287
646
|
throw new Error(
|
|
288
|
-
`playbooks.${id}.
|
|
647
|
+
`playbooks.${id}.roles.${role} names unknown player ` +
|
|
648
|
+
JSON.stringify(binding.playerId),
|
|
289
649
|
);
|
|
290
650
|
}
|
|
291
|
-
|
|
292
|
-
if (
|
|
293
|
-
|
|
651
|
+
bindings[role] = binding;
|
|
652
|
+
if (binding.model !== undefined || binding.effort !== undefined) {
|
|
653
|
+
let checkId;
|
|
654
|
+
do {
|
|
655
|
+
checkId = `binding-check-${tuningCheckIndex}`;
|
|
656
|
+
tuningCheckIndex += 1;
|
|
657
|
+
} while (configuredAgents.has(checkId));
|
|
658
|
+
tuningChecks.push({
|
|
659
|
+
id: checkId,
|
|
660
|
+
agent: applyTuningOverrides(agent, binding),
|
|
661
|
+
});
|
|
294
662
|
}
|
|
295
|
-
seenHostIds.add(hostId);
|
|
296
|
-
normalizedPlayers.push({
|
|
297
|
-
id: hostId,
|
|
298
|
-
playbookId: id,
|
|
299
|
-
roleId: role,
|
|
300
|
-
agent,
|
|
301
|
-
});
|
|
302
|
-
playerIdEntries.push([role, hostId]);
|
|
303
|
-
generated.push(hostId);
|
|
304
663
|
}
|
|
305
664
|
const optionSlice = Object.fromEntries(
|
|
306
665
|
Object.entries(block).filter(
|
|
@@ -314,9 +673,7 @@ export async function normalizeLaunchPlan(
|
|
|
314
673
|
configuredFrom,
|
|
315
674
|
commandOverride: block.command,
|
|
316
675
|
roles,
|
|
317
|
-
|
|
318
|
-
generated,
|
|
319
|
-
playerIds: Object.fromEntries(playerIdEntries),
|
|
676
|
+
bindings,
|
|
320
677
|
optionSlice,
|
|
321
678
|
});
|
|
322
679
|
}
|
|
@@ -325,22 +682,40 @@ export async function normalizeLaunchPlan(
|
|
|
325
682
|
// detached provisional projection before any registry preparation/import,
|
|
326
683
|
// then feed its agent and presentation fields back into the authoritative
|
|
327
684
|
// plan. The interactive child will only revalidate these same values.
|
|
328
|
-
const
|
|
685
|
+
const firstRoleful = configuredPlaybooks.find(
|
|
686
|
+
(configured) => configured.roles.length > 0,
|
|
687
|
+
);
|
|
688
|
+
const provisionalVisible =
|
|
689
|
+
firstRoleful === undefined
|
|
690
|
+
? []
|
|
691
|
+
: distinct(
|
|
692
|
+
firstRoleful.roles.map(
|
|
693
|
+
(role) => firstRoleful.bindings[role].playerId,
|
|
694
|
+
),
|
|
695
|
+
);
|
|
696
|
+
// An all-roleless catalog still validates every authored player through
|
|
697
|
+
// cligent, but the validation-only projection must respect tmux-play's
|
|
698
|
+
// invariant that a nonempty roster has at least one visible pane.
|
|
699
|
+
const validationVisible =
|
|
700
|
+
provisionalVisible.length === 0
|
|
701
|
+
? allPlayerIds.slice(0, 1)
|
|
702
|
+
: provisionalVisible;
|
|
329
703
|
const provisional = {
|
|
330
704
|
captain: {
|
|
331
705
|
...captain,
|
|
332
706
|
from: PLAYBOOK_CAPTAIN_MODULE,
|
|
333
707
|
options: {},
|
|
334
708
|
},
|
|
335
|
-
players:
|
|
336
|
-
|
|
709
|
+
players: [
|
|
710
|
+
...allPlayerIds.map((id) => ({
|
|
337
711
|
id,
|
|
338
|
-
...
|
|
712
|
+
...configuredAgents.get(id),
|
|
339
713
|
})),
|
|
340
|
-
|
|
714
|
+
...tuningChecks.map(({ id, agent }) => ({ id, ...agent })),
|
|
715
|
+
],
|
|
341
716
|
layout: {
|
|
342
717
|
...(isObject(top.layout) ? top.layout : {}),
|
|
343
|
-
initialVisible:
|
|
718
|
+
initialVisible: validationVisible,
|
|
344
719
|
},
|
|
345
720
|
...(top.notifications === undefined
|
|
346
721
|
? {}
|
|
@@ -348,17 +723,30 @@ export async function normalizeLaunchPlan(
|
|
|
348
723
|
...(top.theme === undefined ? {} : { theme: top.theme }),
|
|
349
724
|
};
|
|
350
725
|
const normalizedHost = await normalizeHostConfig(provisional);
|
|
726
|
+
// The authoritative all-roleless projection is Boss-only. Normalize that
|
|
727
|
+
// exact empty host shape separately so derived presentation aliases (most
|
|
728
|
+
// notably active `columnWeights`) match zero visible player panes rather
|
|
729
|
+
// than the temporary validation pane above.
|
|
730
|
+
const normalizedPresentationHost =
|
|
731
|
+
provisionalVisible.length === 0 && allPlayerIds.length > 0
|
|
732
|
+
? await normalizeHostConfig({
|
|
733
|
+
...provisional,
|
|
734
|
+
players: [],
|
|
735
|
+
layout: { ...provisional.layout, initialVisible: [] },
|
|
736
|
+
})
|
|
737
|
+
: normalizedHost;
|
|
351
738
|
const { from: _captainFrom, options: _captainOptions, ...normalizedCaptain } =
|
|
352
739
|
normalizedHost.captain;
|
|
353
|
-
captain = normalizedCaptain;
|
|
740
|
+
captain = sessionAgentFromHostAgent(normalizedCaptain, 'captain');
|
|
354
741
|
const hostAgents = new Map(
|
|
355
742
|
normalizedHost.players.map(({ id, ...agent }) => [id, agent]),
|
|
356
743
|
);
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
744
|
+
const normalizedPlayerAgents = new Map(
|
|
745
|
+
allPlayerIds.map((id) => [
|
|
746
|
+
id,
|
|
747
|
+
sessionAgentFromHostAgent(hostAgents.get(id), `players.${id}`),
|
|
748
|
+
]),
|
|
749
|
+
);
|
|
362
750
|
|
|
363
751
|
// Preparation is a transaction-like pre-import phase across the complete
|
|
364
752
|
// configured catalog. A provisioning failure therefore cannot leave some
|
|
@@ -390,11 +778,14 @@ export async function normalizeLaunchPlan(
|
|
|
390
778
|
);
|
|
391
779
|
}
|
|
392
780
|
}
|
|
781
|
+
preparedFrom = canonicalizePreparedRegistrySpecifier(
|
|
782
|
+
preparedFrom,
|
|
783
|
+
`playbooks.${id}.from`,
|
|
784
|
+
);
|
|
393
785
|
preparedPlaybooks.push({ ...configured, preparedFrom });
|
|
394
786
|
}
|
|
395
787
|
|
|
396
788
|
const catalogEntries = [];
|
|
397
|
-
const players = [];
|
|
398
789
|
const seenCommands = new Map();
|
|
399
790
|
const seenIds = new Set();
|
|
400
791
|
|
|
@@ -404,8 +795,7 @@ export async function normalizeLaunchPlan(
|
|
|
404
795
|
preparedFrom,
|
|
405
796
|
commandOverride,
|
|
406
797
|
roles,
|
|
407
|
-
|
|
408
|
-
playerIds,
|
|
798
|
+
bindings,
|
|
409
799
|
optionSlice,
|
|
410
800
|
} of preparedPlaybooks) {
|
|
411
801
|
let mod;
|
|
@@ -417,9 +807,11 @@ export async function normalizeLaunchPlan(
|
|
|
417
807
|
);
|
|
418
808
|
}
|
|
419
809
|
const entry = mod?.default;
|
|
420
|
-
|
|
810
|
+
const registryProblem = invalidRegistryEntryReason(entry);
|
|
811
|
+
if (registryProblem !== undefined) {
|
|
421
812
|
throw new Error(
|
|
422
|
-
`playbooks.${id}.from "${from}" exposes no valid registry entry
|
|
813
|
+
`playbooks.${id}.from "${from}" exposes no valid registry entry: ` +
|
|
814
|
+
registryProblem,
|
|
423
815
|
);
|
|
424
816
|
}
|
|
425
817
|
if (entry.id !== id) {
|
|
@@ -443,21 +835,29 @@ export async function normalizeLaunchPlan(
|
|
|
443
835
|
}
|
|
444
836
|
seenCommands.set(command, id);
|
|
445
837
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
838
|
+
assertExactRoleBindings(id, roles, entry.requiredRoleIds);
|
|
839
|
+
const resolvedRoles = Object.fromEntries(
|
|
840
|
+
entry.requiredRoleIds.map((role) => {
|
|
841
|
+
const binding = bindings[role];
|
|
842
|
+
const agent = normalizedPlayerAgents.get(binding.playerId);
|
|
843
|
+
return [
|
|
844
|
+
role,
|
|
845
|
+
{
|
|
846
|
+
playerId: binding.playerId,
|
|
847
|
+
model:
|
|
848
|
+
binding.model === undefined
|
|
849
|
+
? agent.model
|
|
850
|
+
: overrideTuningSelection(binding.model),
|
|
851
|
+
effort:
|
|
852
|
+
binding.effort === undefined
|
|
853
|
+
? agent.effort
|
|
854
|
+
: overrideTuningSelection(binding.effort),
|
|
855
|
+
},
|
|
856
|
+
];
|
|
857
|
+
}),
|
|
858
|
+
);
|
|
859
|
+
assertConcurrentPlayers(id, entry.concurrentRoleSets, resolvedRoles);
|
|
459
860
|
|
|
460
|
-
players.push(...normalizedPlayers);
|
|
461
861
|
catalogEntries.push([
|
|
462
862
|
id,
|
|
463
863
|
{
|
|
@@ -470,19 +870,43 @@ export async function normalizeLaunchPlan(
|
|
|
470
870
|
command,
|
|
471
871
|
...(commandOverride === undefined ? {} : { commandOverride }),
|
|
472
872
|
intent: entry.intent,
|
|
873
|
+
artifactSchema: entry.artifactSchema,
|
|
473
874
|
requiredRoleIds: [...entry.requiredRoleIds],
|
|
474
|
-
|
|
875
|
+
concurrentRoleSets: entry.concurrentRoleSets.map((set) => [...set]),
|
|
876
|
+
roles: resolvedRoles,
|
|
475
877
|
options: optionSlice,
|
|
476
878
|
},
|
|
477
879
|
]);
|
|
478
880
|
}
|
|
479
881
|
|
|
882
|
+
// Registry role order is the canonical role order. Derive the one roster
|
|
883
|
+
// union only after every exact role map is closed against its manifest.
|
|
884
|
+
const referencedPlayerIds = distinct(
|
|
885
|
+
catalogEntries.flatMap(([, item]) =>
|
|
886
|
+
Object.values(item.roles).map((binding) => binding.playerId),
|
|
887
|
+
),
|
|
888
|
+
);
|
|
889
|
+
|
|
890
|
+
const firstVisibleCatalog = catalogEntries.find(
|
|
891
|
+
([, item]) => Object.keys(item.roles).length > 0,
|
|
892
|
+
);
|
|
893
|
+
const initialVisible =
|
|
894
|
+
firstVisibleCatalog === undefined
|
|
895
|
+
? []
|
|
896
|
+
: distinct(
|
|
897
|
+
Object.values(firstVisibleCatalog[1].roles).map(
|
|
898
|
+
(binding) => binding.playerId,
|
|
899
|
+
),
|
|
900
|
+
);
|
|
480
901
|
const presentation = {
|
|
481
|
-
layout:
|
|
482
|
-
|
|
483
|
-
|
|
902
|
+
layout: {
|
|
903
|
+
...normalizedPresentationHost.layout,
|
|
904
|
+
initialVisible,
|
|
905
|
+
},
|
|
906
|
+
notifications: normalizedPresentationHost.notifications,
|
|
907
|
+
...(normalizedPresentationHost.theme === undefined
|
|
484
908
|
? {}
|
|
485
|
-
: { theme:
|
|
909
|
+
: { theme: normalizedPresentationHost.theme }),
|
|
486
910
|
};
|
|
487
911
|
|
|
488
912
|
return deepFreeze(
|
|
@@ -490,7 +914,10 @@ export async function normalizeLaunchPlan(
|
|
|
490
914
|
{
|
|
491
915
|
schemaVersion: 1,
|
|
492
916
|
captain,
|
|
493
|
-
players
|
|
917
|
+
players: referencedPlayerIds.map((id) => ({
|
|
918
|
+
id,
|
|
919
|
+
agent: normalizedPlayerAgents.get(id),
|
|
920
|
+
})),
|
|
494
921
|
catalog: Object.fromEntries(catalogEntries),
|
|
495
922
|
presentation,
|
|
496
923
|
},
|
|
@@ -522,16 +949,26 @@ export function projectTmuxConfig(plan) {
|
|
|
522
949
|
{
|
|
523
950
|
from: item.from,
|
|
524
951
|
command: item.command,
|
|
952
|
+
roles: cloneJson(item.roles, `catalog.${id}.roles`),
|
|
525
953
|
options: cloneJson(item.options, `catalog.${id}.options`),
|
|
526
954
|
},
|
|
527
955
|
]),
|
|
528
956
|
);
|
|
529
957
|
const captain = {
|
|
530
|
-
...
|
|
958
|
+
...projectHostAgent(plan.captain, 'captain'),
|
|
531
959
|
from: PLAYBOOK_CAPTAIN_MODULE,
|
|
532
960
|
};
|
|
533
961
|
captain.options = {
|
|
534
962
|
playbooks,
|
|
963
|
+
sessionAgents: {
|
|
964
|
+
captain: cloneJson(plan.captain, 'captain'),
|
|
965
|
+
players: Object.fromEntries(
|
|
966
|
+
plan.players.map(({ id, agent }) => [
|
|
967
|
+
id,
|
|
968
|
+
cloneJson(agent, `players.${id}.agent`),
|
|
969
|
+
]),
|
|
970
|
+
),
|
|
971
|
+
},
|
|
535
972
|
...(typeof captain.adapter === 'string' && captain.adapter.length > 0
|
|
536
973
|
? { captainAdapter: captain.adapter }
|
|
537
974
|
: {}),
|
|
@@ -539,7 +976,7 @@ export function projectTmuxConfig(plan) {
|
|
|
539
976
|
const config = {
|
|
540
977
|
captain,
|
|
541
978
|
players: plan.players.map(({ id, agent }) => ({
|
|
542
|
-
...
|
|
979
|
+
...projectHostAgent(agent, `players.${id}.agent`),
|
|
543
980
|
id,
|
|
544
981
|
})),
|
|
545
982
|
layout: projectHostLayout(plan.presentation.layout),
|
|
@@ -653,6 +1090,9 @@ function migrateUserConfigIfRetired(userConfigPath, onNotice) {
|
|
|
653
1090
|
try {
|
|
654
1091
|
migrated = migrateRetiredProfiles(text);
|
|
655
1092
|
} catch (error) {
|
|
1093
|
+
if (error?.code === 'PLAYBOOK_LEGACY_PLAYERS') {
|
|
1094
|
+
throw legacyPlayersError(error.legacyPath, userConfigPath);
|
|
1095
|
+
}
|
|
656
1096
|
throw new Error(
|
|
657
1097
|
`cannot migrate the retired profiles config at ${userConfigPath}: ` +
|
|
658
1098
|
`${errorMessage(error)} — edit it by hand: each agent takes its own ` +
|
|
@@ -684,19 +1124,23 @@ export function migrateRetiredProfiles(text) {
|
|
|
684
1124
|
const doc = parseYamlDocument(text);
|
|
685
1125
|
const contents = doc.contents;
|
|
686
1126
|
if (!contents || !Array.isArray(contents.items)) return undefined;
|
|
687
|
-
const profiles = doc.get('profiles');
|
|
688
|
-
const agentPaths = [['captain']];
|
|
689
1127
|
const playbooks = doc.get('playbooks');
|
|
690
1128
|
if (playbooks && Array.isArray(playbooks.items)) {
|
|
691
1129
|
for (const entry of playbooks.items) {
|
|
692
1130
|
const id = String(entry.key);
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
for (const player of players.items) {
|
|
696
|
-
agentPaths.push(['playbooks', id, 'players', String(player.key)]);
|
|
1131
|
+
if (doc.getIn(['playbooks', id, 'players']) !== undefined) {
|
|
1132
|
+
throw legacyPlayersError(`playbooks.${id}.players`);
|
|
697
1133
|
}
|
|
698
1134
|
}
|
|
699
1135
|
}
|
|
1136
|
+
const profiles = doc.get('profiles');
|
|
1137
|
+
const agentPaths = [['captain']];
|
|
1138
|
+
const players = doc.get('players');
|
|
1139
|
+
if (players && Array.isArray(players.items)) {
|
|
1140
|
+
for (const player of players.items) {
|
|
1141
|
+
agentPaths.push(['players', String(player.key)]);
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
700
1144
|
|
|
701
1145
|
const profileSettings = (name) =>
|
|
702
1146
|
profiles && typeof profiles.get === 'function'
|
|
@@ -787,18 +1231,16 @@ function assertNoRetiredProfiles(top, configPath) {
|
|
|
787
1231
|
if (top.profiles !== undefined) {
|
|
788
1232
|
throw new Error(
|
|
789
1233
|
`top-level "profiles" was removed${where}: write each agent's settings ` +
|
|
790
|
-
'inline under captain and each
|
|
1234
|
+
'inline under captain and each top-level players.<player-id> ' +
|
|
791
1235
|
'(adapter, model, effort, permissions)',
|
|
792
1236
|
);
|
|
793
1237
|
}
|
|
1238
|
+
const legacyPath = findLegacyPlayersPath(top);
|
|
1239
|
+
if (legacyPath !== undefined) throw legacyPlayersError(legacyPath, configPath);
|
|
794
1240
|
const blocks = [['captain', top.captain]];
|
|
795
|
-
const
|
|
796
|
-
for (const [
|
|
797
|
-
|
|
798
|
-
isObject(block) && isObject(block.players) ? block.players : {};
|
|
799
|
-
for (const [role, agent] of Object.entries(playersMap)) {
|
|
800
|
-
blocks.push([`playbooks.${id}.players.${role}`, agent]);
|
|
801
|
-
}
|
|
1241
|
+
const playersCfg = isObject(top.players) ? top.players : {};
|
|
1242
|
+
for (const [playerId, agent] of Object.entries(playersCfg)) {
|
|
1243
|
+
blocks.push([`players.${playerId}`, agent]);
|
|
802
1244
|
}
|
|
803
1245
|
for (const [path, block] of blocks) {
|
|
804
1246
|
if (isObject(block) && block.profile !== undefined) {
|
|
@@ -810,22 +1252,536 @@ function assertNoRetiredProfiles(top, configPath) {
|
|
|
810
1252
|
}
|
|
811
1253
|
}
|
|
812
1254
|
|
|
813
|
-
function
|
|
814
|
-
if (!isObject(value)) return
|
|
815
|
-
|
|
816
|
-
typeof value.id
|
|
817
|
-
value.id.trim().length
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
)
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
1255
|
+
function invalidRegistryEntryReason(value) {
|
|
1256
|
+
if (!isObject(value)) return 'the default export must be an object';
|
|
1257
|
+
if (
|
|
1258
|
+
typeof value.id !== 'string' ||
|
|
1259
|
+
value.id.trim().length === 0 ||
|
|
1260
|
+
value.id !== value.id.trim()
|
|
1261
|
+
) {
|
|
1262
|
+
return 'id must be a canonical trimmed nonblank string';
|
|
1263
|
+
}
|
|
1264
|
+
if (
|
|
1265
|
+
typeof value.command !== 'string' ||
|
|
1266
|
+
value.command.trim().length === 0 ||
|
|
1267
|
+
value.command !== value.command.trim()
|
|
1268
|
+
) {
|
|
1269
|
+
return 'command must be a canonical trimmed nonblank string';
|
|
1270
|
+
}
|
|
1271
|
+
if (typeof value.intent !== 'string') return 'intent must be a string';
|
|
1272
|
+
if (value.artifactSchema !== 2) {
|
|
1273
|
+
return 'artifactSchema must be exactly 2';
|
|
1274
|
+
}
|
|
1275
|
+
const roleProblem = invalidManifestRoles(value.requiredRoleIds);
|
|
1276
|
+
if (roleProblem !== undefined) return `requiredRoleIds ${roleProblem}`;
|
|
1277
|
+
const concurrentProblem = invalidConcurrentRoleSets(
|
|
1278
|
+
value.concurrentRoleSets,
|
|
1279
|
+
value.requiredRoleIds,
|
|
1280
|
+
);
|
|
1281
|
+
if (concurrentProblem !== undefined) {
|
|
1282
|
+
return `concurrentRoleSets ${concurrentProblem}`;
|
|
1283
|
+
}
|
|
1284
|
+
if (typeof value.validateOptions !== 'function') {
|
|
1285
|
+
return 'validateOptions must be a function';
|
|
1286
|
+
}
|
|
1287
|
+
if (typeof value.createRuntime !== 'function') {
|
|
1288
|
+
return 'createRuntime must be a function';
|
|
1289
|
+
}
|
|
1290
|
+
return undefined;
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
function validateStoredStructuralProjection(value) {
|
|
1294
|
+
const stored = cloneJson(value, 'stored structural projection');
|
|
1295
|
+
if (!isPlainObject(stored) || stored.schemaVersion !== 1) {
|
|
1296
|
+
throw new Error('stored structural projection schema 1 is required');
|
|
1297
|
+
}
|
|
1298
|
+
const captain = requireObject(stored.captain, 'stored structural captain');
|
|
1299
|
+
if (typeof captain.adapter !== 'string' || captain.adapter.length === 0) {
|
|
1300
|
+
throw new Error('stored structural captain must name an adapter');
|
|
1301
|
+
}
|
|
1302
|
+
if (!Array.isArray(stored.players) || !isPlainObject(stored.catalog)) {
|
|
1303
|
+
throw new Error(
|
|
1304
|
+
'stored structural projection must contain players and catalog',
|
|
1305
|
+
);
|
|
1306
|
+
}
|
|
1307
|
+
const playerIds = stored.players.map((player, index) => {
|
|
1308
|
+
const record = requireObject(player, `stored structural players.${index}`);
|
|
1309
|
+
assertPlayerId(record.id, `stored structural players.${index}.id`);
|
|
1310
|
+
if (typeof record.adapter !== 'string' || record.adapter.length === 0) {
|
|
1311
|
+
throw new Error(
|
|
1312
|
+
`stored structural players.${index} must name an adapter`,
|
|
1313
|
+
);
|
|
1314
|
+
}
|
|
1315
|
+
return record.id;
|
|
1316
|
+
});
|
|
1317
|
+
if (new Set(playerIds).size !== playerIds.length) {
|
|
1318
|
+
throw new Error('stored structural player ids must be unique');
|
|
1319
|
+
}
|
|
1320
|
+
for (const [id, itemValue] of Object.entries(stored.catalog)) {
|
|
1321
|
+
const item = requireObject(itemValue, `stored structural catalog.${id}`);
|
|
1322
|
+
if (item.id !== id || id === RESERVED_CAPTAIN_PLAYBOOK_ID) {
|
|
1323
|
+
throw new Error(`stored structural catalog.${id}.id is invalid`);
|
|
1324
|
+
}
|
|
1325
|
+
for (const field of [
|
|
1326
|
+
'from',
|
|
1327
|
+
'manifestCommand',
|
|
1328
|
+
'command',
|
|
1329
|
+
'intent',
|
|
1330
|
+
]) {
|
|
1331
|
+
if (typeof item[field] !== 'string') {
|
|
1332
|
+
throw new Error(`stored structural catalog.${id}.${field} is invalid`);
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
if (
|
|
1336
|
+
!Array.isArray(item.requiredRoleIds) ||
|
|
1337
|
+
!Array.isArray(item.concurrentRoleSets) ||
|
|
1338
|
+
!isPlainObject(item.roles) ||
|
|
1339
|
+
!isPlainObject(item.options)
|
|
1340
|
+
) {
|
|
1341
|
+
throw new Error(`stored structural catalog.${id} is malformed`);
|
|
1342
|
+
}
|
|
1343
|
+
if (
|
|
1344
|
+
JSON.stringify(Object.keys(item.roles)) !==
|
|
1345
|
+
JSON.stringify(item.requiredRoleIds)
|
|
1346
|
+
) {
|
|
1347
|
+
throw new Error(
|
|
1348
|
+
`stored structural catalog.${id}.roles must follow requiredRoleIds`,
|
|
1349
|
+
);
|
|
1350
|
+
}
|
|
1351
|
+
for (const role of item.requiredRoleIds) {
|
|
1352
|
+
assertRoleId(role, `stored structural catalog.${id}.roles.${role}`);
|
|
1353
|
+
const binding = requireObject(
|
|
1354
|
+
item.roles[role],
|
|
1355
|
+
`stored structural catalog.${id}.roles.${role}`,
|
|
1356
|
+
);
|
|
1357
|
+
if (!playerIds.includes(binding.playerId)) {
|
|
1358
|
+
throw new Error(
|
|
1359
|
+
`stored structural catalog.${id}.roles.${role} names an absent player`,
|
|
1360
|
+
);
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
return stored;
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
function selectedMembersFromStoredStructure(stored) {
|
|
1368
|
+
return {
|
|
1369
|
+
playbookIds: Object.keys(stored.catalog),
|
|
1370
|
+
playerIds: stored.players.map((player) => player.id),
|
|
1371
|
+
};
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
function fixedAgentProjection(agent) {
|
|
1375
|
+
return {
|
|
1376
|
+
adapter: agent.adapter,
|
|
1377
|
+
...(agent.instruction === undefined
|
|
1378
|
+
? {}
|
|
1379
|
+
: { instruction: agent.instruction }),
|
|
1380
|
+
...(agent.permissions === undefined
|
|
1381
|
+
? {}
|
|
1382
|
+
: { permissions: agent.permissions }),
|
|
1383
|
+
};
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
function projectSelectedMembers(top, selectedMembers) {
|
|
1387
|
+
if (selectedMembers === undefined) return top;
|
|
1388
|
+
const { playbookIds, playerIds } = validateSelectedMembers(selectedMembers);
|
|
1389
|
+
if (!isPlainObject(top)) {
|
|
1390
|
+
throw new Error('config must contain only plain JSON objects');
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
const descriptors = Object.getOwnPropertyDescriptors(top);
|
|
1394
|
+
const keys = Reflect.ownKeys(top);
|
|
1395
|
+
for (const key of keys) {
|
|
1396
|
+
const descriptor = descriptors[key];
|
|
1397
|
+
if (
|
|
1398
|
+
typeof key === 'symbol' ||
|
|
1399
|
+
descriptor?.get !== undefined ||
|
|
1400
|
+
descriptor?.set !== undefined ||
|
|
1401
|
+
descriptor?.enumerable !== true
|
|
1402
|
+
) {
|
|
1403
|
+
throw new Error(
|
|
1404
|
+
`config.${String(key)} must be an enumerable data property`,
|
|
1405
|
+
);
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
const projected = Object.fromEntries(
|
|
1409
|
+
keys.map((key) => [key, descriptors[key].value]),
|
|
1410
|
+
);
|
|
1411
|
+
projected.playbooks = projectSelectedMap(
|
|
1412
|
+
projected.playbooks,
|
|
1413
|
+
playbookIds,
|
|
1414
|
+
'playbooks',
|
|
828
1415
|
);
|
|
1416
|
+
projected.players = projectSelectedMap(
|
|
1417
|
+
projected.players,
|
|
1418
|
+
playerIds,
|
|
1419
|
+
'players',
|
|
1420
|
+
);
|
|
1421
|
+
return projected;
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
function validateSelectedMembers(selectedMembers) {
|
|
1425
|
+
const selected = cloneJson(selectedMembers, 'selectedMembers');
|
|
1426
|
+
const unknownSelectionKeys = Object.keys(selected).filter(
|
|
1427
|
+
(key) => !['playbookIds', 'playerIds'].includes(key),
|
|
1428
|
+
);
|
|
1429
|
+
if (unknownSelectionKeys.length > 0) {
|
|
1430
|
+
throw new Error(
|
|
1431
|
+
`selectedMembers has unknown ${formatKeyList(unknownSelectionKeys)}`,
|
|
1432
|
+
);
|
|
1433
|
+
}
|
|
1434
|
+
return {
|
|
1435
|
+
playbookIds: selectedIdList(
|
|
1436
|
+
selected.playbookIds,
|
|
1437
|
+
'selectedMembers.playbookIds',
|
|
1438
|
+
),
|
|
1439
|
+
playerIds: selectedIdList(
|
|
1440
|
+
selected.playerIds,
|
|
1441
|
+
'selectedMembers.playerIds',
|
|
1442
|
+
),
|
|
1443
|
+
};
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
function projectSelectedLayer(value, selected, path) {
|
|
1447
|
+
if (!isPlainObject(value)) {
|
|
1448
|
+
throw new Error(`${path} must contain only plain JSON objects`);
|
|
1449
|
+
}
|
|
1450
|
+
const descriptors = Object.getOwnPropertyDescriptors(value);
|
|
1451
|
+
const entries = [];
|
|
1452
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
1453
|
+
const descriptor = descriptors[key];
|
|
1454
|
+
if (
|
|
1455
|
+
typeof key === 'symbol' ||
|
|
1456
|
+
descriptor?.get !== undefined ||
|
|
1457
|
+
descriptor?.set !== undefined ||
|
|
1458
|
+
descriptor?.enumerable !== true
|
|
1459
|
+
) {
|
|
1460
|
+
throw new Error(
|
|
1461
|
+
`${path}.${String(key)} must be an enumerable data property`,
|
|
1462
|
+
);
|
|
1463
|
+
}
|
|
1464
|
+
if (key === 'playbooks' || key === 'players') {
|
|
1465
|
+
entries.push([
|
|
1466
|
+
key,
|
|
1467
|
+
projectOptionalSelectedMap(
|
|
1468
|
+
descriptor.value,
|
|
1469
|
+
key === 'playbooks' ? selected.playbookIds : selected.playerIds,
|
|
1470
|
+
`${path}.${key}`,
|
|
1471
|
+
),
|
|
1472
|
+
]);
|
|
1473
|
+
} else {
|
|
1474
|
+
entries.push([key, descriptor.value]);
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
return Object.fromEntries(entries);
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
function projectOptionalSelectedMap(value, ids, path) {
|
|
1481
|
+
if (!isPlainObject(value)) {
|
|
1482
|
+
throw new Error(`${path} must contain only plain JSON objects`);
|
|
1483
|
+
}
|
|
1484
|
+
const entries = [];
|
|
1485
|
+
for (const id of ids) {
|
|
1486
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, id);
|
|
1487
|
+
if (descriptor === undefined) continue;
|
|
1488
|
+
if (
|
|
1489
|
+
descriptor.get !== undefined ||
|
|
1490
|
+
descriptor.set !== undefined ||
|
|
1491
|
+
descriptor.enumerable !== true
|
|
1492
|
+
) {
|
|
1493
|
+
throw new Error(`${path}.${id} must be an enumerable data property`);
|
|
1494
|
+
}
|
|
1495
|
+
entries.push([id, descriptor.value]);
|
|
1496
|
+
}
|
|
1497
|
+
return Object.fromEntries(entries);
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
function projectSelectedMap(value, ids, path) {
|
|
1501
|
+
if (!isPlainObject(value)) {
|
|
1502
|
+
throw new Error(`${path} must contain only plain JSON objects`);
|
|
1503
|
+
}
|
|
1504
|
+
return Object.fromEntries(
|
|
1505
|
+
ids.map((id) => {
|
|
1506
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, id);
|
|
1507
|
+
if (descriptor === undefined) {
|
|
1508
|
+
throw new Error(
|
|
1509
|
+
`selected ${path} member ${JSON.stringify(id)} is missing`,
|
|
1510
|
+
);
|
|
1511
|
+
}
|
|
1512
|
+
if (
|
|
1513
|
+
descriptor.get !== undefined ||
|
|
1514
|
+
descriptor.set !== undefined ||
|
|
1515
|
+
descriptor.enumerable !== true
|
|
1516
|
+
) {
|
|
1517
|
+
throw new Error(`${path}.${id} must be an enumerable data property`);
|
|
1518
|
+
}
|
|
1519
|
+
return [id, descriptor.value];
|
|
1520
|
+
}),
|
|
1521
|
+
);
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
function selectedIdList(value, path) {
|
|
1525
|
+
if (
|
|
1526
|
+
!Array.isArray(value) ||
|
|
1527
|
+
value.some((id) => typeof id !== 'string' || id.trim().length === 0) ||
|
|
1528
|
+
new Set(value).size !== value.length
|
|
1529
|
+
) {
|
|
1530
|
+
throw new Error(`${path} must be a duplicate-free array of nonblank ids`);
|
|
1531
|
+
}
|
|
1532
|
+
return value;
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
function resolveRoleBinding(value, path) {
|
|
1536
|
+
if (typeof value === 'string') {
|
|
1537
|
+
assertPlayerId(value, path);
|
|
1538
|
+
return { playerId: value };
|
|
1539
|
+
}
|
|
1540
|
+
const block = requireObject(value, path);
|
|
1541
|
+
const unknown = Object.keys(block).filter(
|
|
1542
|
+
(key) => !['player', 'model', 'effort'].includes(key),
|
|
1543
|
+
);
|
|
1544
|
+
if (unknown.length > 0) {
|
|
1545
|
+
throw new Error(`${path} has unknown ${formatKeyList(unknown)}`);
|
|
1546
|
+
}
|
|
1547
|
+
assertPlayerId(block.player, `${path}.player`);
|
|
1548
|
+
for (const field of ['model', 'effort']) {
|
|
1549
|
+
if (
|
|
1550
|
+
block[field] !== undefined &&
|
|
1551
|
+
block[field] !== false &&
|
|
1552
|
+
(typeof block[field] !== 'string' || block[field].trim().length === 0)
|
|
1553
|
+
) {
|
|
1554
|
+
throw new Error(
|
|
1555
|
+
`${path}.${field} must be a nonblank string or false for provider-default`,
|
|
1556
|
+
);
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
return {
|
|
1560
|
+
playerId: block.player,
|
|
1561
|
+
...(block.model === undefined ? {} : { model: block.model }),
|
|
1562
|
+
...(block.effort === undefined ? {} : { effort: block.effort }),
|
|
1563
|
+
};
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
function applyTuningOverrides(agent, binding) {
|
|
1567
|
+
const effective = { ...agent };
|
|
1568
|
+
if (binding.model === false) delete effective.model;
|
|
1569
|
+
else if (binding.model !== undefined) effective.model = binding.model;
|
|
1570
|
+
if (binding.effort !== undefined) {
|
|
1571
|
+
delete effective.reasoningEffort;
|
|
1572
|
+
if (binding.effort === false) delete effective.effort;
|
|
1573
|
+
else effective.effort = binding.effort;
|
|
1574
|
+
}
|
|
1575
|
+
return effective;
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
function sessionAgentFromHostAgent(agent, path) {
|
|
1579
|
+
if (!isObject(agent)) {
|
|
1580
|
+
throw new Error('installed cligent omitted a retained agent');
|
|
1581
|
+
}
|
|
1582
|
+
return {
|
|
1583
|
+
adapter: agent.adapter,
|
|
1584
|
+
model: tuningSelection(agent.model, `${path}.model`),
|
|
1585
|
+
effort: tuningSelection(agent.effort, `${path}.effort`),
|
|
1586
|
+
...(agent.instruction === undefined
|
|
1587
|
+
? {}
|
|
1588
|
+
: { instruction: agent.instruction }),
|
|
1589
|
+
...(agent.permissions === undefined
|
|
1590
|
+
? {}
|
|
1591
|
+
: { permissions: agent.permissions }),
|
|
1592
|
+
};
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
// Shared by the interactive and headless host projections. A tagged
|
|
1596
|
+
// provider-default is represented to cligent by omitting that configured
|
|
1597
|
+
// default; the complete tagged selection remains in sessionAgents.
|
|
1598
|
+
export function projectHostAgent(agent, path = 'agent') {
|
|
1599
|
+
const normalized = cloneJson(agent, path);
|
|
1600
|
+
return {
|
|
1601
|
+
adapter: normalized.adapter,
|
|
1602
|
+
...(normalized.model?.kind === 'value'
|
|
1603
|
+
? { model: normalized.model.value }
|
|
1604
|
+
: {}),
|
|
1605
|
+
...(normalized.effort?.kind === 'value'
|
|
1606
|
+
? { effort: normalized.effort.value }
|
|
1607
|
+
: {}),
|
|
1608
|
+
...(normalized.instruction === undefined
|
|
1609
|
+
? {}
|
|
1610
|
+
: { instruction: normalized.instruction }),
|
|
1611
|
+
...(normalized.permissions === undefined
|
|
1612
|
+
? {}
|
|
1613
|
+
: { permissions: normalized.permissions }),
|
|
1614
|
+
};
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
function tuningSelection(value, path) {
|
|
1618
|
+
if (
|
|
1619
|
+
value !== undefined &&
|
|
1620
|
+
(typeof value !== 'string' || value.trim().length === 0)
|
|
1621
|
+
) {
|
|
1622
|
+
throw new Error(`${path} must be a nonblank string`);
|
|
1623
|
+
}
|
|
1624
|
+
return value === undefined
|
|
1625
|
+
? { kind: 'provider-default' }
|
|
1626
|
+
: { kind: 'value', value };
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
function overrideTuningSelection(value) {
|
|
1630
|
+
return value === false
|
|
1631
|
+
? { kind: 'provider-default' }
|
|
1632
|
+
: tuningSelection(value, 'role tuning override');
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
function canonicalizePreparedRegistrySpecifier(value, path) {
|
|
1636
|
+
if (value !== value.trim()) {
|
|
1637
|
+
throw new Error(
|
|
1638
|
+
`${path} preparation must return a canonical trimmed module specifier`,
|
|
1639
|
+
);
|
|
1640
|
+
}
|
|
1641
|
+
if (
|
|
1642
|
+
isAbsolute(value) ||
|
|
1643
|
+
/^(?:\.{1,2}(?:[\\/]|$)|[\\/]|[A-Za-z]:[\\/])/.test(value)
|
|
1644
|
+
) {
|
|
1645
|
+
throw new Error(
|
|
1646
|
+
`${path} preparation must return a canonical module specifier`,
|
|
1647
|
+
);
|
|
1648
|
+
}
|
|
1649
|
+
if (value.startsWith('file:')) {
|
|
1650
|
+
let canonical;
|
|
1651
|
+
try {
|
|
1652
|
+
canonical = pathToFileURL(fileURLToPath(value)).href;
|
|
1653
|
+
} catch {
|
|
1654
|
+
throw new Error(`${path} preparation must return a canonical file URL`);
|
|
1655
|
+
}
|
|
1656
|
+
if (canonical !== value) {
|
|
1657
|
+
throw new Error(`${path} preparation must return a canonical file URL`);
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
return value;
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
function assertPlayerId(value, path) {
|
|
1664
|
+
if (typeof value !== 'string' || !PLAYER_ID_PATTERN.test(value)) {
|
|
1665
|
+
throw new Error(
|
|
1666
|
+
`${path} must name a player matching ${PLAYER_ID_PATTERN.source}`,
|
|
1667
|
+
);
|
|
1668
|
+
}
|
|
1669
|
+
if (value === RESERVED_CAPTAIN_ROLE_ID) {
|
|
1670
|
+
throw new Error(`${path} uses reserved player id "captain"`);
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
function assertRoleId(value, path) {
|
|
1675
|
+
if (typeof value !== 'string' || !ROLE_ID_PATTERN.test(value)) {
|
|
1676
|
+
throw new Error(`${path} must use a canonical lowercase local role id`);
|
|
1677
|
+
}
|
|
1678
|
+
if (value === RESERVED_CAPTAIN_ROLE_ID) {
|
|
1679
|
+
throw new Error(`${path} uses reserved local role id "captain"`);
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
function invalidManifestRoles(value) {
|
|
1684
|
+
if (!Array.isArray(value)) return 'must be an array';
|
|
1685
|
+
if (value.some((role) => typeof role !== 'string')) {
|
|
1686
|
+
return 'must contain only strings';
|
|
1687
|
+
}
|
|
1688
|
+
const canonical = value.map((role) => role.toLowerCase());
|
|
1689
|
+
if (new Set(canonical).size !== canonical.length) {
|
|
1690
|
+
return 'contains roles that collide after canonical lowercase derivation';
|
|
1691
|
+
}
|
|
1692
|
+
const invalid = value.find((role) => !ROLE_ID_PATTERN.test(role));
|
|
1693
|
+
if (invalid !== undefined) {
|
|
1694
|
+
return `contains noncanonical role ${JSON.stringify(invalid)}`;
|
|
1695
|
+
}
|
|
1696
|
+
if (value.includes(RESERVED_CAPTAIN_ROLE_ID)) {
|
|
1697
|
+
return 'contains reserved local role "captain"';
|
|
1698
|
+
}
|
|
1699
|
+
return undefined;
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
function invalidConcurrentRoleSets(value, requiredRoleIds) {
|
|
1703
|
+
if (!Array.isArray(value)) return 'must be an array';
|
|
1704
|
+
const required = new Set(requiredRoleIds);
|
|
1705
|
+
const seen = new Set();
|
|
1706
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
1707
|
+
const set = value[index];
|
|
1708
|
+
if (!Array.isArray(set) || set.length < 2) {
|
|
1709
|
+
return `[${index}] must contain at least two roles`;
|
|
1710
|
+
}
|
|
1711
|
+
if (
|
|
1712
|
+
set.some(
|
|
1713
|
+
(role) =>
|
|
1714
|
+
typeof role !== 'string' ||
|
|
1715
|
+
!ROLE_ID_PATTERN.test(role) ||
|
|
1716
|
+
role === RESERVED_CAPTAIN_ROLE_ID ||
|
|
1717
|
+
!required.has(role),
|
|
1718
|
+
)
|
|
1719
|
+
) {
|
|
1720
|
+
return `[${index}] must contain only required canonical local roles`;
|
|
1721
|
+
}
|
|
1722
|
+
if (new Set(set).size !== set.length) {
|
|
1723
|
+
return `[${index}] must contain pairwise-distinct roles`;
|
|
1724
|
+
}
|
|
1725
|
+
const signature = JSON.stringify(set);
|
|
1726
|
+
if (seen.has(signature)) return `contains duplicate set ${signature}`;
|
|
1727
|
+
seen.add(signature);
|
|
1728
|
+
}
|
|
1729
|
+
return undefined;
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
function assertExactRoleBindings(id, configured, required) {
|
|
1733
|
+
const missing = required.filter((role) => !configured.includes(role));
|
|
1734
|
+
const extra = configured.filter((role) => !required.includes(role));
|
|
1735
|
+
if (missing.length > 0 || extra.length > 0) {
|
|
1736
|
+
throw new Error(
|
|
1737
|
+
`playbooks.${id}.roles must exactly cover requiredRoleIds` +
|
|
1738
|
+
`${missing.length === 0 ? '' : `; missing ${missing.map(JSON.stringify).join(', ')}`}` +
|
|
1739
|
+
`${
|
|
1740
|
+
extra.length === 0
|
|
1741
|
+
? ''
|
|
1742
|
+
: `; extra ${extra.map(JSON.stringify).join(', ')}`
|
|
1743
|
+
}`,
|
|
1744
|
+
);
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
function assertConcurrentPlayers(id, sets, roles) {
|
|
1749
|
+
for (const set of sets) {
|
|
1750
|
+
const playerIds = set.map((role) => roles[role].playerId);
|
|
1751
|
+
if (new Set(playerIds).size !== playerIds.length) {
|
|
1752
|
+
throw new Error(
|
|
1753
|
+
`playbooks.${id}.concurrentRoleSets ${JSON.stringify(set)} must bind ` +
|
|
1754
|
+
'to pairwise-distinct player ids',
|
|
1755
|
+
);
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
function distinct(values) {
|
|
1761
|
+
return [...new Set(values)];
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
function findLegacyPlayersPath(top) {
|
|
1765
|
+
const playbooks = isObject(top?.playbooks) ? top.playbooks : {};
|
|
1766
|
+
for (const [id, block] of Object.entries(playbooks)) {
|
|
1767
|
+
if (isObject(block) && hasOwn(block, 'players')) {
|
|
1768
|
+
return `playbooks.${id}.players`;
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
return undefined;
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
function legacyPlayersError(path, configPath) {
|
|
1775
|
+
const where = configPath ? ` in ${configPath}` : '';
|
|
1776
|
+
const error = new Error(
|
|
1777
|
+
`${path} was removed in the explicit-session-player major release${where}: ` +
|
|
1778
|
+
'define stable ids in top-level players and bind them explicitly under ' +
|
|
1779
|
+
'playbooks.<id>.roles; automatic migration would choose which prior ' +
|
|
1780
|
+
'conversations share a session',
|
|
1781
|
+
);
|
|
1782
|
+
error.code = 'PLAYBOOK_LEGACY_PLAYERS';
|
|
1783
|
+
error.legacyPath = path;
|
|
1784
|
+
return error;
|
|
829
1785
|
}
|
|
830
1786
|
|
|
831
1787
|
function cloneJson(value, path, seen = new Set()) {
|
|
@@ -918,6 +1874,12 @@ function isObject(value) {
|
|
|
918
1874
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
919
1875
|
}
|
|
920
1876
|
|
|
1877
|
+
function isPlainObject(value) {
|
|
1878
|
+
if (!isObject(value)) return false;
|
|
1879
|
+
const prototype = Object.getPrototypeOf(value);
|
|
1880
|
+
return prototype === Object.prototype || prototype === null;
|
|
1881
|
+
}
|
|
1882
|
+
|
|
921
1883
|
function hasOwn(value, key) {
|
|
922
1884
|
return Object.prototype.hasOwnProperty.call(value, key);
|
|
923
1885
|
}
|