@sublang/playbook 10.0.0 → 12.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/docs/cli.md +67 -19
- package/docs/configuration.md +103 -40
- package/docs/embedding.md +88 -0
- package/package.json +30 -4
- package/reference/sdlc/code.md +35 -15
- package/reference/sdlc/code.playbook/bin/interactive-session.js +58 -6
- package/reference/sdlc/code.playbook/bin/launch-config.js +499 -241
- package/reference/sdlc/code.playbook/bin/playbook.js +236 -187
- package/reference/sdlc/code.playbook/bin/replay-observer.js +221 -0
- package/reference/sdlc/code.playbook/bin/run.js +355 -203
- package/reference/sdlc/code.playbook/bin/session-store.js +1512 -136
- package/reference/sdlc/code.playbook/code.fsm.d.ts +22 -19
- package/reference/sdlc/code.playbook/code.fsm.js +116 -52
- package/reference/sdlc/code.playbook/code.fsm.ts +149 -64
- package/reference/sdlc/code.playbook/code.gears.md +40 -20
- package/reference/sdlc/code.playbook/code.playbook.js +23 -2
- package/reference/sdlc/code.playbook/code.playbook.ts +23 -2
- package/reference/sdlc/code.playbook/playbook-captain.d.ts +4 -1
- package/reference/sdlc/code.playbook/playbook-captain.js +21 -3
- package/reference/sdlc/code.playbook/playbook-captain.ts +42 -6
- package/reference/sdlc/code.playbook/playbook.config.template.yaml +31 -11
- package/reference/sdlc/code.playbook/session-store.d.ts +82 -0
- package/reference/sdlc/code.playbook/session-store.js +113 -0
- package/reference/sdlc/decide.md +24 -15
- package/reference/sdlc/decide.playbook/decide.fsm.d.ts +13 -6
- package/reference/sdlc/decide.playbook/decide.fsm.js +54 -27
- package/reference/sdlc/decide.playbook/decide.fsm.ts +68 -29
- package/reference/sdlc/decide.playbook/decide.gears.md +25 -19
- package/reference/sdlc/decide.playbook/decide.playbook.js +11 -3
- package/reference/sdlc/decide.playbook/decide.playbook.ts +11 -3
- package/reference/sdlc/decide.playbook/decide.registry.js +1 -1
- package/reference/sdlc/decide.playbook/decide.registry.ts +1 -1
- package/reference/sdlc/dev.md +52 -0
- package/reference/sdlc/dev.playbook/dev.fsm.d.ts +261 -0
- package/reference/sdlc/dev.playbook/dev.fsm.js +723 -0
- package/reference/sdlc/dev.playbook/dev.fsm.ts +988 -0
- package/reference/sdlc/dev.playbook/dev.gears.md +91 -0
- package/reference/sdlc/dev.playbook/dev.playbook.d.ts +21 -0
- package/reference/sdlc/dev.playbook/dev.playbook.js +143 -0
- package/reference/sdlc/dev.playbook/dev.playbook.ts +246 -0
- package/reference/sdlc/dev.playbook/dev.registry.d.ts +40 -0
- package/reference/sdlc/dev.playbook/dev.registry.js +64 -0
- package/reference/sdlc/dev.playbook/dev.registry.ts +120 -0
- package/reference/sdlc/review.md +36 -18
- package/reference/sdlc/review.playbook/review.fsm.d.ts +15 -2
- package/reference/sdlc/review.playbook/review.fsm.js +77 -27
- package/reference/sdlc/review.playbook/review.fsm.ts +96 -30
- package/reference/sdlc/review.playbook/review.gears.md +52 -26
- package/reference/sdlc/review.playbook/review.playbook.js +17 -7
- package/reference/sdlc/review.playbook/review.playbook.ts +17 -7
- package/reference/sdlc/review.playbook/review.registry.js +1 -1
- package/reference/sdlc/review.playbook/review.registry.ts +1 -1
- package/slc/link.md +12 -5
- package/slc/text2gears.md +3 -0
- package/src/xstate-playbook-runtime.js +5 -2
- package/src/xstate-playbook-runtime.ts +5 -2
- package/src/xstate-runtime.js +13 -1
- package/src/xstate-runtime.ts +13 -1
|
@@ -6,48 +6,52 @@
|
|
|
6
6
|
// control and stores no imported registry functions in the normalized plan.
|
|
7
7
|
|
|
8
8
|
import {
|
|
9
|
+
chmodSync,
|
|
9
10
|
constants,
|
|
10
11
|
copyFileSync,
|
|
11
12
|
existsSync,
|
|
13
|
+
linkSync,
|
|
14
|
+
lstatSync,
|
|
12
15
|
mkdirSync,
|
|
13
16
|
mkdtempSync,
|
|
14
17
|
readFileSync,
|
|
15
18
|
rmSync,
|
|
16
19
|
writeFileSync,
|
|
17
|
-
} from
|
|
18
|
-
import { homedir, tmpdir } from
|
|
19
|
-
import { dirname, isAbsolute, join, resolve } from
|
|
20
|
-
import { fileURLToPath, pathToFileURL } from
|
|
21
|
-
import { isDeepStrictEqual } from
|
|
20
|
+
} from "node:fs";
|
|
21
|
+
import { homedir, tmpdir } from "node:os";
|
|
22
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
23
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
24
|
+
import { isDeepStrictEqual } from "node:util";
|
|
22
25
|
import {
|
|
23
26
|
parse as parseYaml,
|
|
24
27
|
parseDocument as parseYamlDocument,
|
|
25
28
|
stringify as stringifyYaml,
|
|
26
|
-
} from
|
|
27
|
-
import { loadTmuxPlayConfig } from
|
|
29
|
+
} from "yaml";
|
|
30
|
+
import { loadTmuxPlayConfig } from "@sublang/cligent/tmux-play";
|
|
31
|
+
import { defaultCaptainSessionsDir } from "./session-store.js";
|
|
28
32
|
|
|
29
33
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
30
34
|
const DEFAULT_TEMPLATE_PATH = resolve(
|
|
31
35
|
here,
|
|
32
|
-
|
|
33
|
-
|
|
36
|
+
"..",
|
|
37
|
+
"playbook.config.template.yaml",
|
|
34
38
|
);
|
|
35
39
|
|
|
36
40
|
// PBCLI-1/8: the tmux projection uses the Playbook Captain shell adapter.
|
|
37
|
-
export const PLAYBOOK_CAPTAIN_MODULE =
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
const HOST_CAPABILITIES_OPTION_KEY = 'hostCapabilities';
|
|
41
|
+
export const PLAYBOOK_CAPTAIN_MODULE = "@sublang/playbook/playbook-captain";
|
|
42
|
+
const PLAYBOOK_LAUNCHER_KEYS = ["from", "command", "roles"];
|
|
43
|
+
const HOST_CAPABILITIES_OPTION_KEY = "hostCapabilities";
|
|
41
44
|
const PLAYBOOK_TOP_LEVEL_KEYS = new Set([
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
"captain",
|
|
46
|
+
"players",
|
|
47
|
+
"playbooks",
|
|
48
|
+
"layout",
|
|
49
|
+
"notifications",
|
|
50
|
+
"theme",
|
|
51
|
+
"sessions",
|
|
48
52
|
]);
|
|
49
|
-
const RESERVED_CAPTAIN_PLAYBOOK_ID =
|
|
50
|
-
const RESERVED_CAPTAIN_ROLE_ID =
|
|
53
|
+
const RESERVED_CAPTAIN_PLAYBOOK_ID = "captain";
|
|
54
|
+
const RESERVED_CAPTAIN_ROLE_ID = "captain";
|
|
51
55
|
const PLAYER_ID_PATTERN = /^[a-z][a-z0-9_-]*(?:\.[a-z][a-z0-9_-]*)*$/;
|
|
52
56
|
const ROLE_ID_PATTERN = /^[a-z][a-z0-9_-]*$/;
|
|
53
57
|
|
|
@@ -58,16 +62,16 @@ export function extractWithFlags(argv) {
|
|
|
58
62
|
const rest = [];
|
|
59
63
|
for (let i = 0; i < argv.length; i += 1) {
|
|
60
64
|
const arg = argv[i];
|
|
61
|
-
if (arg ===
|
|
65
|
+
if (arg === "--with") {
|
|
62
66
|
const value = argv[i + 1];
|
|
63
|
-
if (value === undefined || value ===
|
|
64
|
-
throw new Error(
|
|
67
|
+
if (value === undefined || value === "") {
|
|
68
|
+
throw new Error("--with needs a value");
|
|
65
69
|
}
|
|
66
70
|
withPaths.push(value);
|
|
67
71
|
i += 1;
|
|
68
|
-
} else if (arg.startsWith(
|
|
69
|
-
const value = arg.slice(
|
|
70
|
-
if (!value) throw new Error(
|
|
72
|
+
} else if (arg.startsWith("--with=")) {
|
|
73
|
+
const value = arg.slice("--with=".length);
|
|
74
|
+
if (!value) throw new Error("--with needs a value");
|
|
71
75
|
withPaths.push(value);
|
|
72
76
|
} else {
|
|
73
77
|
rest.push(arg);
|
|
@@ -81,7 +85,7 @@ export function loadOverlayFragment(overlayPath) {
|
|
|
81
85
|
const resolved = resolve(overlayPath);
|
|
82
86
|
let text;
|
|
83
87
|
try {
|
|
84
|
-
text = readFileSync(resolved,
|
|
88
|
+
text = readFileSync(resolved, "utf8");
|
|
85
89
|
} catch (error) {
|
|
86
90
|
throw new Error(
|
|
87
91
|
`cannot read --with overlay ${overlayPath}: ${errorMessage(error)}`,
|
|
@@ -122,36 +126,113 @@ export function mergeConfigs(base, overlay) {
|
|
|
122
126
|
export function mergeSelectedConfigs(base, overlay, selectedMembers) {
|
|
123
127
|
const selected = validateSelectedMembers(selectedMembers);
|
|
124
128
|
return mergeConfigs(
|
|
125
|
-
projectSelectedLayer(base, selected,
|
|
126
|
-
projectSelectedLayer(overlay, selected,
|
|
129
|
+
projectSelectedLayer(base, selected, "config"),
|
|
130
|
+
projectSelectedLayer(overlay, selected, "overlay"),
|
|
127
131
|
);
|
|
128
132
|
}
|
|
129
133
|
|
|
130
|
-
|
|
131
|
-
|
|
134
|
+
// DR-043: the config lives under the shared Spex root, resolved exactly as
|
|
135
|
+
// Spex's own shells resolve it so both hosts open one file. The singular
|
|
136
|
+
// `playbook/` namespace is ours; Spex owns the plural `playbooks/` library.
|
|
137
|
+
export function resolveSpexHome(env = process.env, home = homedir()) {
|
|
138
|
+
const explicit = env.SPEX_HOME;
|
|
139
|
+
if (typeof explicit === "string" && explicit.trim().length > 0) {
|
|
140
|
+
return explicit;
|
|
141
|
+
}
|
|
142
|
+
const fromEnv = env.HOME;
|
|
143
|
+
const base =
|
|
144
|
+
typeof fromEnv === "string" && fromEnv.trim().length > 0 ? fromEnv : home;
|
|
145
|
+
return join(base, ".spex");
|
|
132
146
|
}
|
|
133
147
|
|
|
134
148
|
export function resolveUserConfigPath(env = process.env, home = homedir()) {
|
|
135
|
-
return join(
|
|
149
|
+
return join(resolveSpexHome(env, home), "playbook", "playbook.config.yaml");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// The pre-DR-043 location, kept only to relocate a config written there.
|
|
153
|
+
export function resolveLegacyUserConfigPath(
|
|
154
|
+
env = process.env,
|
|
155
|
+
home = homedir(),
|
|
156
|
+
) {
|
|
157
|
+
const configHome = env.XDG_CONFIG_HOME || join(home, ".config");
|
|
158
|
+
return join(configHome, "playbook", "playbook.config.yaml");
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// PBCLI-46/78: session selection needs this one root locator before the
|
|
162
|
+
// complete launch plan can be selected and normalized. Read only that scalar
|
|
163
|
+
// from each layer so an ordinary reopen does not inspect unrelated current
|
|
164
|
+
// catalog members before its durable projection is known.
|
|
165
|
+
export function resolveLaunchSessionsDir({
|
|
166
|
+
userConfigPath,
|
|
167
|
+
overlayPaths = [],
|
|
168
|
+
env = process.env,
|
|
169
|
+
homeDir = env.HOME ?? homedir(),
|
|
170
|
+
sessionsDir,
|
|
171
|
+
preparePrimary = false,
|
|
172
|
+
templatePath = DEFAULT_TEMPLATE_PATH,
|
|
173
|
+
onNotice = () => {},
|
|
174
|
+
}) {
|
|
175
|
+
// The private injection used by tests and managed launch plumbing remains
|
|
176
|
+
// authoritative over configuration, just like an injected store.
|
|
177
|
+
if (sessionsDir !== undefined) return sessionsDir;
|
|
178
|
+
|
|
179
|
+
if (preparePrimary) {
|
|
180
|
+
seedUserConfigIfMissing(userConfigPath, templatePath, onNotice);
|
|
181
|
+
migrateUserConfigIfRetired(userConfigPath, onNotice);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
let locator;
|
|
185
|
+
if (existsSync(userConfigPath)) {
|
|
186
|
+
const primary = parseYaml(readFileSync(userConfigPath, "utf8")) ?? {};
|
|
187
|
+
if (!isObject(primary)) {
|
|
188
|
+
throw new Error(
|
|
189
|
+
`the top-level config at ${userConfigPath} must be a YAML map`,
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
if (hasOwn(primary, "sessions")) locator = primary.sessions;
|
|
193
|
+
}
|
|
194
|
+
for (const overlayPath of overlayPaths) {
|
|
195
|
+
const overlay = loadOverlayFragment(overlayPath);
|
|
196
|
+
if (hasOwn(overlay, "sessions")) locator = overlay.sessions;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (locator === undefined) {
|
|
200
|
+
return defaultCaptainSessionsDir(env, homeDir);
|
|
201
|
+
}
|
|
202
|
+
if (typeof locator !== "string" || locator.length === 0) {
|
|
203
|
+
throw new Error("sessions must be a nonempty filesystem path");
|
|
204
|
+
}
|
|
205
|
+
if (locator === "~") return homeDir;
|
|
206
|
+
if (locator.startsWith("~/")) {
|
|
207
|
+
return join(homeDir, locator.slice(2));
|
|
208
|
+
}
|
|
209
|
+
if (locator.startsWith("~")) {
|
|
210
|
+
throw new Error("sessions does not support named-user tilde expansion");
|
|
211
|
+
}
|
|
212
|
+
if (isAbsolute(locator)) return locator;
|
|
213
|
+
return resolve(dirname(userConfigPath), locator);
|
|
136
214
|
}
|
|
137
215
|
|
|
138
216
|
// PBCLI-46: configured filesystem modules are anchored once to the primary
|
|
139
217
|
// config, including paths introduced by overlays. Bare/custom specifiers and
|
|
140
218
|
// already-authored file URLs retain their module semantics.
|
|
141
219
|
export function canonicalizeRegistrySpecifier(from, configPath) {
|
|
142
|
-
if (configPath === undefined || from.startsWith(
|
|
143
|
-
if (
|
|
144
|
-
isAbsolute(from) ||
|
|
145
|
-
from.startsWith('./') ||
|
|
146
|
-
from.startsWith('../') ||
|
|
147
|
-
from.startsWith('.\\') ||
|
|
148
|
-
from.startsWith('..\\')
|
|
149
|
-
) {
|
|
220
|
+
if (configPath === undefined || from.startsWith("file:")) return from;
|
|
221
|
+
if (isAbsolute(from) || isRelativeFilesystemRegistrySpecifier(from)) {
|
|
150
222
|
return pathToFileURL(resolve(dirname(configPath), from)).href;
|
|
151
223
|
}
|
|
152
224
|
return from;
|
|
153
225
|
}
|
|
154
226
|
|
|
227
|
+
function isRelativeFilesystemRegistrySpecifier(from) {
|
|
228
|
+
return (
|
|
229
|
+
from.startsWith("./") ||
|
|
230
|
+
from.startsWith("../") ||
|
|
231
|
+
from.startsWith(".\\") ||
|
|
232
|
+
from.startsWith("..\\")
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
155
236
|
// PBCLI-46: seed, migrate, overlay, validate, and normalize through one path.
|
|
156
237
|
// `prepareRegistryModule` is the single provision-before-import seam used by
|
|
157
238
|
// both front ends, so filesystem registry handling cannot drift by presenter.
|
|
@@ -171,7 +252,7 @@ export async function loadLaunchPlan({
|
|
|
171
252
|
migrateUserConfigIfRetired(userConfigPath, onNotice);
|
|
172
253
|
}
|
|
173
254
|
|
|
174
|
-
let top = parseYaml(readFileSync(userConfigPath,
|
|
255
|
+
let top = parseYaml(readFileSync(userConfigPath, "utf8")) ?? {};
|
|
175
256
|
if (overlayPaths.length > 0 && !isObject(top)) {
|
|
176
257
|
throw new Error(
|
|
177
258
|
`the top-level config at ${userConfigPath} must be a YAML map before --with can overlay it`,
|
|
@@ -208,7 +289,7 @@ export async function loadSelectedLaunchPlanDataOnly({
|
|
|
208
289
|
const selectedMembers = selectedMembersFromStoredStructure(stored);
|
|
209
290
|
seedUserConfigIfMissing(userConfigPath, templatePath, onNotice);
|
|
210
291
|
|
|
211
|
-
let top = parseYaml(readFileSync(userConfigPath,
|
|
292
|
+
let top = parseYaml(readFileSync(userConfigPath, "utf8")) ?? {};
|
|
212
293
|
if (overlayPaths.length > 0 && !isObject(top)) {
|
|
213
294
|
throw new Error(
|
|
214
295
|
`the top-level config at ${userConfigPath} must be a YAML map before --with can overlay it`,
|
|
@@ -217,7 +298,7 @@ export async function loadSelectedLaunchPlanDataOnly({
|
|
|
217
298
|
top = projectSelectedLayer(
|
|
218
299
|
top,
|
|
219
300
|
validateSelectedMembers(selectedMembers),
|
|
220
|
-
|
|
301
|
+
"config",
|
|
221
302
|
);
|
|
222
303
|
for (const overlayPath of overlayPaths) {
|
|
223
304
|
top = mergeSelectedConfigs(
|
|
@@ -246,17 +327,17 @@ export async function normalizeSelectedLaunchPlanDataOnly(
|
|
|
246
327
|
!isDeepStrictEqual(supplied.playerIds, expectedMembers.playerIds)
|
|
247
328
|
) {
|
|
248
329
|
throw new Error(
|
|
249
|
-
|
|
330
|
+
"selected launch members do not match the stored structural projection",
|
|
250
331
|
);
|
|
251
332
|
}
|
|
252
333
|
}
|
|
253
334
|
top = projectSelectedMembers(top, expectedMembers);
|
|
254
|
-
top = cloneJson(top,
|
|
335
|
+
top = cloneJson(top, "config");
|
|
255
336
|
assertNoRetiredProfiles(top, configPath);
|
|
256
|
-
if (hasOwn(top,
|
|
337
|
+
if (hasOwn(top, "run")) {
|
|
257
338
|
throw new Error(
|
|
258
339
|
'top-level "run" was removed: configure the shared Captain under ' +
|
|
259
|
-
|
|
340
|
+
"captain, top-level players, and playbooks.<id>.roles instead",
|
|
260
341
|
);
|
|
261
342
|
}
|
|
262
343
|
const unknownTopLevel = Object.keys(top).filter(
|
|
@@ -268,33 +349,33 @@ export async function normalizeSelectedLaunchPlanDataOnly(
|
|
|
268
349
|
);
|
|
269
350
|
}
|
|
270
351
|
if (top.layout !== undefined && !isObject(top.layout)) {
|
|
271
|
-
throw new Error(
|
|
352
|
+
throw new Error("layout must be a map");
|
|
272
353
|
}
|
|
273
354
|
|
|
274
|
-
const playersCfg = requireObject(top.players,
|
|
355
|
+
const playersCfg = requireObject(top.players, "players");
|
|
275
356
|
const configuredAgents = new Map();
|
|
276
357
|
for (const playerId of expectedMembers.playerIds) {
|
|
277
358
|
assertPlayerId(playerId, `players.${playerId}`);
|
|
278
359
|
const agent = resolveAgent(playersCfg[playerId], `players.${playerId}`, [
|
|
279
|
-
|
|
360
|
+
"id",
|
|
280
361
|
]);
|
|
281
362
|
configuredAgents.set(playerId, agent);
|
|
282
363
|
}
|
|
283
|
-
let captain = resolveAgent(top.captain,
|
|
364
|
+
let captain = resolveAgent(top.captain, "captain", ["from", "options"]);
|
|
284
365
|
|
|
285
|
-
const playbooksCfg = requireObject(top.playbooks,
|
|
366
|
+
const playbooksCfg = requireObject(top.playbooks, "playbooks");
|
|
286
367
|
const tuningChecks = [];
|
|
287
368
|
let tuningCheckIndex = 0;
|
|
288
369
|
const authored = new Map();
|
|
289
370
|
for (const id of expectedMembers.playbookIds) {
|
|
290
371
|
const storedItem = stored.catalog[id];
|
|
291
372
|
const block = requireObject(playbooksCfg[id], `playbooks.${id}`);
|
|
292
|
-
if (hasOwn(block,
|
|
373
|
+
if (hasOwn(block, "players")) {
|
|
293
374
|
throw legacyPlayersError(`playbooks.${id}.players`, configPath);
|
|
294
375
|
}
|
|
295
376
|
rejectConfiguredHostCapabilities(block, `playbooks.${id}`);
|
|
296
377
|
if (
|
|
297
|
-
typeof block.from !==
|
|
378
|
+
typeof block.from !== "string" ||
|
|
298
379
|
block.from.trim().length === 0 ||
|
|
299
380
|
block.from !== block.from.trim()
|
|
300
381
|
) {
|
|
@@ -302,7 +383,10 @@ export async function normalizeSelectedLaunchPlanDataOnly(
|
|
|
302
383
|
`playbooks.${id}.from must be a canonical trimmed module specifier`,
|
|
303
384
|
);
|
|
304
385
|
}
|
|
305
|
-
const configuredFrom = canonicalizeRegistrySpecifier(
|
|
386
|
+
const configuredFrom = canonicalizeRegistrySpecifier(
|
|
387
|
+
block.from,
|
|
388
|
+
configPath,
|
|
389
|
+
);
|
|
306
390
|
if (configuredFrom !== storedItem.from) {
|
|
307
391
|
throw new Error(
|
|
308
392
|
`playbooks.${id}.from changed from the stored structural projection`,
|
|
@@ -332,7 +416,11 @@ export async function normalizeSelectedLaunchPlanDataOnly(
|
|
|
332
416
|
);
|
|
333
417
|
}
|
|
334
418
|
bindings[role] = binding;
|
|
335
|
-
if (
|
|
419
|
+
if (
|
|
420
|
+
binding.model !== undefined ||
|
|
421
|
+
binding.effort !== undefined ||
|
|
422
|
+
binding.fastMode !== undefined
|
|
423
|
+
) {
|
|
336
424
|
let checkId;
|
|
337
425
|
do {
|
|
338
426
|
checkId = `binding-check-${tuningCheckIndex}`;
|
|
@@ -402,9 +490,12 @@ export async function normalizeSelectedLaunchPlanDataOnly(
|
|
|
402
490
|
layout: { ...provisional.layout, initialVisible: [] },
|
|
403
491
|
})
|
|
404
492
|
: normalizedHost;
|
|
405
|
-
const {
|
|
406
|
-
|
|
407
|
-
|
|
493
|
+
const {
|
|
494
|
+
from: _captainFrom,
|
|
495
|
+
options: _captainOptions,
|
|
496
|
+
...normalizedCaptain
|
|
497
|
+
} = normalizedHost.captain;
|
|
498
|
+
captain = sessionAgentFromHostAgent(normalizedCaptain, "captain");
|
|
408
499
|
const hostAgents = new Map(
|
|
409
500
|
normalizedHost.players.map(({ id, ...agent }) => [id, agent]),
|
|
410
501
|
);
|
|
@@ -439,6 +530,11 @@ export async function normalizeSelectedLaunchPlanDataOnly(
|
|
|
439
530
|
binding.effort === undefined
|
|
440
531
|
? agent.effort
|
|
441
532
|
: overrideTuningSelection(binding.effort),
|
|
533
|
+
...(binding.fastMode === undefined
|
|
534
|
+
? agent.fastMode === undefined
|
|
535
|
+
? {}
|
|
536
|
+
: { fastMode: agent.fastMode }
|
|
537
|
+
: { fastMode: binding.fastMode }),
|
|
442
538
|
},
|
|
443
539
|
];
|
|
444
540
|
}),
|
|
@@ -471,7 +567,7 @@ export async function normalizeSelectedLaunchPlanDataOnly(
|
|
|
471
567
|
};
|
|
472
568
|
if (!isDeepStrictEqual(candidateStructure, stored)) {
|
|
473
569
|
throw new Error(
|
|
474
|
-
|
|
570
|
+
"current selected config does not reproduce the stored structural projection",
|
|
475
571
|
);
|
|
476
572
|
}
|
|
477
573
|
|
|
@@ -496,7 +592,7 @@ export async function normalizeSelectedLaunchPlanDataOnly(
|
|
|
496
592
|
: { theme: normalizedPresentationHost.theme }),
|
|
497
593
|
},
|
|
498
594
|
},
|
|
499
|
-
|
|
595
|
+
"selected launch config",
|
|
500
596
|
),
|
|
501
597
|
);
|
|
502
598
|
}
|
|
@@ -504,7 +600,7 @@ export async function normalizeSelectedLaunchPlanDataOnly(
|
|
|
504
600
|
// PBCLI-8 (DR-021): scalar agents are adapter shorthands and full blocks
|
|
505
601
|
// carry their own settings without profile indirection.
|
|
506
602
|
export function resolveAgent(value, path, reservedKeys = []) {
|
|
507
|
-
if (typeof value ===
|
|
603
|
+
if (typeof value === "string") {
|
|
508
604
|
if (value.trim().length === 0) {
|
|
509
605
|
throw new Error(`${path} must name an adapter`);
|
|
510
606
|
}
|
|
@@ -524,7 +620,7 @@ export function resolveAgent(value, path, reservedKeys = []) {
|
|
|
524
620
|
function rejectConfiguredHostCapabilities(value, path) {
|
|
525
621
|
if (
|
|
526
622
|
value !== null &&
|
|
527
|
-
typeof value ===
|
|
623
|
+
typeof value === "object" &&
|
|
528
624
|
!Array.isArray(value) &&
|
|
529
625
|
hasOwn(value, HOST_CAPABILITIES_OPTION_KEY)
|
|
530
626
|
) {
|
|
@@ -543,12 +639,12 @@ export async function normalizeLaunchPlan(
|
|
|
543
639
|
) {
|
|
544
640
|
const importModule = loadModule ?? ((specifier) => import(specifier));
|
|
545
641
|
top = projectSelectedMembers(top, selectedMembers);
|
|
546
|
-
top = cloneJson(top,
|
|
642
|
+
top = cloneJson(top, "config");
|
|
547
643
|
assertNoRetiredProfiles(top, configPath);
|
|
548
|
-
if (hasOwn(top,
|
|
644
|
+
if (hasOwn(top, "run")) {
|
|
549
645
|
throw new Error(
|
|
550
646
|
'top-level "run" was removed: configure the shared Captain under ' +
|
|
551
|
-
|
|
647
|
+
"captain, top-level players, and playbooks.<id>.roles instead",
|
|
552
648
|
);
|
|
553
649
|
}
|
|
554
650
|
const unknownTopLevel = Object.keys(top).filter(
|
|
@@ -560,19 +656,19 @@ export async function normalizeLaunchPlan(
|
|
|
560
656
|
);
|
|
561
657
|
}
|
|
562
658
|
if (top.layout !== undefined && !isObject(top.layout)) {
|
|
563
|
-
throw new Error(
|
|
659
|
+
throw new Error("layout must be a map");
|
|
564
660
|
}
|
|
565
661
|
|
|
566
|
-
const playersCfg = requireObject(top.players,
|
|
662
|
+
const playersCfg = requireObject(top.players, "players");
|
|
567
663
|
const allPlayerIds = Object.keys(playersCfg);
|
|
568
664
|
const configuredAgents = new Map();
|
|
569
665
|
for (const playerId of allPlayerIds) {
|
|
570
666
|
assertPlayerId(playerId, `players.${playerId}`);
|
|
571
667
|
const agent = resolveAgent(playersCfg[playerId], `players.${playerId}`, [
|
|
572
|
-
|
|
668
|
+
"id",
|
|
573
669
|
]);
|
|
574
670
|
if (
|
|
575
|
-
typeof agent.adapter !==
|
|
671
|
+
typeof agent.adapter !== "string" ||
|
|
576
672
|
agent.adapter.trim().length === 0
|
|
577
673
|
) {
|
|
578
674
|
throw new Error(`players.${playerId} must resolve an adapter`);
|
|
@@ -580,21 +676,21 @@ export async function normalizeLaunchPlan(
|
|
|
580
676
|
configuredAgents.set(playerId, agent);
|
|
581
677
|
}
|
|
582
678
|
|
|
583
|
-
const playbooksCfg = requireObject(top.playbooks,
|
|
679
|
+
const playbooksCfg = requireObject(top.playbooks, "playbooks");
|
|
584
680
|
const ids = Object.keys(playbooksCfg);
|
|
585
681
|
if (ids.length === 0) {
|
|
586
|
-
throw new Error(
|
|
682
|
+
throw new Error("playbooks must enable at least one playbook");
|
|
587
683
|
}
|
|
588
684
|
if (ids.some((id) => id.trim().length === 0 || id !== id.trim())) {
|
|
589
|
-
throw new Error(
|
|
685
|
+
throw new Error("playbooks keys must be canonical trimmed nonblank ids");
|
|
590
686
|
}
|
|
591
687
|
|
|
592
|
-
let captain = resolveAgent(top.captain,
|
|
688
|
+
let captain = resolveAgent(top.captain, "captain", ["from", "options"]);
|
|
593
689
|
if (
|
|
594
|
-
typeof captain.adapter !==
|
|
690
|
+
typeof captain.adapter !== "string" ||
|
|
595
691
|
captain.adapter.trim().length === 0
|
|
596
692
|
) {
|
|
597
|
-
throw new Error(
|
|
693
|
+
throw new Error("captain must resolve an adapter");
|
|
598
694
|
}
|
|
599
695
|
|
|
600
696
|
// Validate and detach every retained config-owned value before provisioning
|
|
@@ -609,13 +705,13 @@ export async function normalizeLaunchPlan(
|
|
|
609
705
|
);
|
|
610
706
|
}
|
|
611
707
|
const block = requireObject(playbooksCfg[id], `playbooks.${id}`);
|
|
612
|
-
if (hasOwn(block,
|
|
708
|
+
if (hasOwn(block, "players")) {
|
|
613
709
|
throw legacyPlayersError(`playbooks.${id}.players`, configPath);
|
|
614
710
|
}
|
|
615
711
|
rejectConfiguredHostCapabilities(block, `playbooks.${id}`);
|
|
616
712
|
const from = block.from;
|
|
617
713
|
if (
|
|
618
|
-
typeof from !==
|
|
714
|
+
typeof from !== "string" ||
|
|
619
715
|
from.trim().length === 0 ||
|
|
620
716
|
from !== from.trim()
|
|
621
717
|
) {
|
|
@@ -625,7 +721,7 @@ export async function normalizeLaunchPlan(
|
|
|
625
721
|
}
|
|
626
722
|
if (
|
|
627
723
|
block.command !== undefined &&
|
|
628
|
-
(typeof block.command !==
|
|
724
|
+
(typeof block.command !== "string" ||
|
|
629
725
|
block.command.trim().length === 0 ||
|
|
630
726
|
block.command !== block.command.trim())
|
|
631
727
|
) {
|
|
@@ -647,7 +743,7 @@ export async function normalizeLaunchPlan(
|
|
|
647
743
|
throw new Error(
|
|
648
744
|
`playbooks.${id}.roles.${RESERVED_CAPTAIN_ROLE_ID} binds local ` +
|
|
649
745
|
`role "${RESERVED_CAPTAIN_ROLE_ID}", which is reserved for the ` +
|
|
650
|
-
|
|
746
|
+
"tmux-play Captain",
|
|
651
747
|
);
|
|
652
748
|
}
|
|
653
749
|
const bindings = Object.create(null);
|
|
@@ -665,7 +761,11 @@ export async function normalizeLaunchPlan(
|
|
|
665
761
|
);
|
|
666
762
|
}
|
|
667
763
|
bindings[role] = binding;
|
|
668
|
-
if (
|
|
764
|
+
if (
|
|
765
|
+
binding.model !== undefined ||
|
|
766
|
+
binding.effort !== undefined ||
|
|
767
|
+
binding.fastMode !== undefined
|
|
768
|
+
) {
|
|
669
769
|
let checkId;
|
|
670
770
|
do {
|
|
671
771
|
checkId = `binding-check-${tuningCheckIndex}`;
|
|
@@ -751,9 +851,12 @@ export async function normalizeLaunchPlan(
|
|
|
751
851
|
layout: { ...provisional.layout, initialVisible: [] },
|
|
752
852
|
})
|
|
753
853
|
: normalizedHost;
|
|
754
|
-
const {
|
|
755
|
-
|
|
756
|
-
|
|
854
|
+
const {
|
|
855
|
+
from: _captainFrom,
|
|
856
|
+
options: _captainOptions,
|
|
857
|
+
...normalizedCaptain
|
|
858
|
+
} = normalizedHost.captain;
|
|
859
|
+
captain = sessionAgentFromHostAgent(normalizedCaptain, "captain");
|
|
757
860
|
const hostAgents = new Map(
|
|
758
861
|
normalizedHost.players.map(({ id, ...agent }) => [id, agent]),
|
|
759
862
|
);
|
|
@@ -786,7 +889,7 @@ export async function normalizeLaunchPlan(
|
|
|
786
889
|
);
|
|
787
890
|
}
|
|
788
891
|
if (
|
|
789
|
-
typeof preparedFrom !==
|
|
892
|
+
typeof preparedFrom !== "string" ||
|
|
790
893
|
preparedFrom.trim().length === 0
|
|
791
894
|
) {
|
|
792
895
|
throw new Error(
|
|
@@ -868,6 +971,11 @@ export async function normalizeLaunchPlan(
|
|
|
868
971
|
binding.effort === undefined
|
|
869
972
|
? agent.effort
|
|
870
973
|
: overrideTuningSelection(binding.effort),
|
|
974
|
+
...(binding.fastMode === undefined
|
|
975
|
+
? agent.fastMode === undefined
|
|
976
|
+
? {}
|
|
977
|
+
: { fastMode: agent.fastMode }
|
|
978
|
+
: { fastMode: binding.fastMode }),
|
|
871
979
|
},
|
|
872
980
|
];
|
|
873
981
|
}),
|
|
@@ -937,7 +1045,7 @@ export async function normalizeLaunchPlan(
|
|
|
937
1045
|
catalog: Object.fromEntries(catalogEntries),
|
|
938
1046
|
presentation,
|
|
939
1047
|
},
|
|
940
|
-
|
|
1048
|
+
"launch config",
|
|
941
1049
|
),
|
|
942
1050
|
);
|
|
943
1051
|
}
|
|
@@ -946,8 +1054,8 @@ export async function normalizeLaunchPlan(
|
|
|
946
1054
|
// generic launch planning tracks the installed host schema without importing
|
|
947
1055
|
// or duplicating cligent's private validators.
|
|
948
1056
|
export async function normalizeHostConfig(config) {
|
|
949
|
-
const dir = mkdtempSync(join(tmpdir(),
|
|
950
|
-
const path = join(dir,
|
|
1057
|
+
const dir = mkdtempSync(join(tmpdir(), "playbook-host-config-"));
|
|
1058
|
+
const path = join(dir, "tmux-play.config.yaml");
|
|
951
1059
|
try {
|
|
952
1060
|
writeFileSync(path, stringifyYaml(config));
|
|
953
1061
|
return (await loadTmuxPlayConfig({ configPath: path })).config;
|
|
@@ -971,13 +1079,13 @@ export function projectTmuxConfig(plan) {
|
|
|
971
1079
|
]),
|
|
972
1080
|
);
|
|
973
1081
|
const captain = {
|
|
974
|
-
...projectHostAgent(plan.captain,
|
|
1082
|
+
...projectHostAgent(plan.captain, "captain"),
|
|
975
1083
|
from: PLAYBOOK_CAPTAIN_MODULE,
|
|
976
1084
|
};
|
|
977
1085
|
captain.options = {
|
|
978
1086
|
playbooks,
|
|
979
1087
|
sessionAgents: {
|
|
980
|
-
captain: cloneJson(plan.captain,
|
|
1088
|
+
captain: cloneJson(plan.captain, "captain"),
|
|
981
1089
|
players: Object.fromEntries(
|
|
982
1090
|
plan.players.map(({ id, agent }) => [
|
|
983
1091
|
id,
|
|
@@ -985,7 +1093,7 @@ export function projectTmuxConfig(plan) {
|
|
|
985
1093
|
]),
|
|
986
1094
|
),
|
|
987
1095
|
},
|
|
988
|
-
...(typeof captain.adapter ===
|
|
1096
|
+
...(typeof captain.adapter === "string" && captain.adapter.length > 0
|
|
989
1097
|
? { captainAdapter: captain.adapter }
|
|
990
1098
|
: {}),
|
|
991
1099
|
};
|
|
@@ -997,20 +1105,20 @@ export function projectTmuxConfig(plan) {
|
|
|
997
1105
|
})),
|
|
998
1106
|
layout: projectHostLayout(plan.presentation.layout),
|
|
999
1107
|
};
|
|
1000
|
-
if (hasOwn(plan.presentation,
|
|
1108
|
+
if (hasOwn(plan.presentation, "notifications")) {
|
|
1001
1109
|
config.notifications = cloneJson(
|
|
1002
1110
|
plan.presentation.notifications,
|
|
1003
|
-
|
|
1111
|
+
"presentation.notifications",
|
|
1004
1112
|
);
|
|
1005
1113
|
}
|
|
1006
|
-
if (hasOwn(plan.presentation,
|
|
1007
|
-
config.theme = cloneJson(plan.presentation.theme,
|
|
1114
|
+
if (hasOwn(plan.presentation, "theme")) {
|
|
1115
|
+
config.theme = cloneJson(plan.presentation.theme, "presentation.theme");
|
|
1008
1116
|
}
|
|
1009
1117
|
return config;
|
|
1010
1118
|
}
|
|
1011
1119
|
|
|
1012
1120
|
function projectHostLayout(layout) {
|
|
1013
|
-
const projected = cloneJson(layout,
|
|
1121
|
+
const projected = cloneJson(layout, "presentation.layout");
|
|
1014
1122
|
// cligent's normalized runtime shape carries `columnWeights` as the
|
|
1015
1123
|
// derived active-shape value alongside both canonical shape fields. The
|
|
1016
1124
|
// authored schema deliberately rejects that alias/canonical combination,
|
|
@@ -1061,14 +1169,14 @@ export function checkReadiness(adapters, env = process.env, home = homedir()) {
|
|
|
1061
1169
|
const failingAdapters = [];
|
|
1062
1170
|
const unknownAdapters = [];
|
|
1063
1171
|
for (const adapter of adapters) {
|
|
1064
|
-
if (adapter ===
|
|
1065
|
-
if (!env.ANTHROPIC_API_KEY && !existsSync(join(home,
|
|
1172
|
+
if (adapter === "claude") {
|
|
1173
|
+
if (!env.ANTHROPIC_API_KEY && !existsSync(join(home, ".claude"))) {
|
|
1066
1174
|
failingAdapters.push(adapter);
|
|
1067
1175
|
}
|
|
1068
1176
|
continue;
|
|
1069
1177
|
}
|
|
1070
|
-
if (adapter ===
|
|
1071
|
-
if (!env.OPENAI_API_KEY && !existsSync(join(home,
|
|
1178
|
+
if (adapter === "codex") {
|
|
1179
|
+
if (!env.OPENAI_API_KEY && !existsSync(join(home, ".codex"))) {
|
|
1072
1180
|
failingAdapters.push(adapter);
|
|
1073
1181
|
}
|
|
1074
1182
|
continue;
|
|
@@ -1087,6 +1195,142 @@ export function deriveLaunchReadiness(
|
|
|
1087
1195
|
return { adapters, ...checkReadiness(adapters, env, home) };
|
|
1088
1196
|
}
|
|
1089
1197
|
|
|
1198
|
+
// DR-043: a user-authored config cannot be regenerated, so the one-time move
|
|
1199
|
+
// to the canonical path is the deliberate exception to this project's
|
|
1200
|
+
// reject-don't-migrate posture. It runs before seeding, never clobbers a
|
|
1201
|
+
// canonical file, and is a no-op once the legacy file is gone.
|
|
1202
|
+
export function relocateLegacyUserConfig(
|
|
1203
|
+
userConfigPath,
|
|
1204
|
+
legacyUserConfigPath,
|
|
1205
|
+
onNotice,
|
|
1206
|
+
) {
|
|
1207
|
+
if (legacyUserConfigPath === undefined) return;
|
|
1208
|
+
// Any canonical filesystem entry wins, including a dangling symlink. The
|
|
1209
|
+
// relocation is considered only when that exact pathname is absent; this
|
|
1210
|
+
// keeps an obsolete or malformed legacy entry from blocking a valid config.
|
|
1211
|
+
try {
|
|
1212
|
+
lstatSync(userConfigPath);
|
|
1213
|
+
return;
|
|
1214
|
+
} catch (error) {
|
|
1215
|
+
if (error?.code !== "ENOENT") throw error;
|
|
1216
|
+
}
|
|
1217
|
+
let source;
|
|
1218
|
+
try {
|
|
1219
|
+
source = lstatSync(legacyUserConfigPath);
|
|
1220
|
+
} catch (error) {
|
|
1221
|
+
if (error?.code === "ENOENT") return;
|
|
1222
|
+
throw error;
|
|
1223
|
+
}
|
|
1224
|
+
if (!source.isFile()) {
|
|
1225
|
+
throw new Error(
|
|
1226
|
+
`cannot relocate legacy config at ${legacyUserConfigPath}: ` +
|
|
1227
|
+
"the path is not a regular file",
|
|
1228
|
+
);
|
|
1229
|
+
}
|
|
1230
|
+
mkdirSync(dirname(userConfigPath), { recursive: true });
|
|
1231
|
+
|
|
1232
|
+
// Stage complete bytes and the exact source permission bits beside the
|
|
1233
|
+
// destination, then publish with an exclusive hard link. Unlike rename,
|
|
1234
|
+
// link can never replace a canonical entry that appears after inspection.
|
|
1235
|
+
const stagingDir = mkdtempSync(
|
|
1236
|
+
join(dirname(userConfigPath), ".playbook-config-relocation-"),
|
|
1237
|
+
);
|
|
1238
|
+
const stagedPath = join(stagingDir, "playbook.config.yaml");
|
|
1239
|
+
let published = false;
|
|
1240
|
+
try {
|
|
1241
|
+
copyFileSync(
|
|
1242
|
+
legacyUserConfigPath,
|
|
1243
|
+
stagedPath,
|
|
1244
|
+
constants.COPYFILE_EXCL,
|
|
1245
|
+
);
|
|
1246
|
+
chmodSync(stagedPath, source.mode & 0o7777);
|
|
1247
|
+
assertLegacyRelocationLocatorsSafe(
|
|
1248
|
+
readFileSync(stagedPath, "utf8"),
|
|
1249
|
+
userConfigPath,
|
|
1250
|
+
legacyUserConfigPath,
|
|
1251
|
+
);
|
|
1252
|
+
try {
|
|
1253
|
+
linkSync(stagedPath, userConfigPath);
|
|
1254
|
+
published = true;
|
|
1255
|
+
} catch (error) {
|
|
1256
|
+
// A pre-existing or concurrently published canonical config wins.
|
|
1257
|
+
// Keep both it and the still-authoritative legacy source unchanged.
|
|
1258
|
+
if (error?.code !== "EEXIST") throw error;
|
|
1259
|
+
}
|
|
1260
|
+
} finally {
|
|
1261
|
+
rmSync(stagingDir, { recursive: true, force: true });
|
|
1262
|
+
}
|
|
1263
|
+
if (!published) return;
|
|
1264
|
+
|
|
1265
|
+
rmSync(legacyUserConfigPath, { force: true });
|
|
1266
|
+
onNotice(
|
|
1267
|
+
`playbook: moved config from ${legacyUserConfigPath} to ${userConfigPath}\n`,
|
|
1268
|
+
);
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
function assertLegacyRelocationLocatorsSafe(
|
|
1272
|
+
source,
|
|
1273
|
+
userConfigPath,
|
|
1274
|
+
legacyUserConfigPath,
|
|
1275
|
+
) {
|
|
1276
|
+
// PBCLI-85: byte-preserving relocation is safe only when it also preserves
|
|
1277
|
+
// the absolute targets that primary-directory-relative locators denote.
|
|
1278
|
+
let top;
|
|
1279
|
+
try {
|
|
1280
|
+
top = parseYaml(source) ?? {};
|
|
1281
|
+
} catch {
|
|
1282
|
+
// Invalid YAML cannot be consumed at either location. Preserve the
|
|
1283
|
+
// relocation's existing byte-for-byte behavior and let config loading
|
|
1284
|
+
// report the parse fault from the canonical pathname.
|
|
1285
|
+
return;
|
|
1286
|
+
}
|
|
1287
|
+
if (!isObject(top)) return;
|
|
1288
|
+
|
|
1289
|
+
const affected = [];
|
|
1290
|
+
const legacyDir = dirname(legacyUserConfigPath);
|
|
1291
|
+
const canonicalDir = dirname(userConfigPath);
|
|
1292
|
+
if (
|
|
1293
|
+
typeof top.sessions === "string" &&
|
|
1294
|
+
top.sessions.length > 0 &&
|
|
1295
|
+
!top.sessions.startsWith("~") &&
|
|
1296
|
+
!isAbsolute(top.sessions)
|
|
1297
|
+
) {
|
|
1298
|
+
const formerTarget = resolve(legacyDir, top.sessions);
|
|
1299
|
+
if (formerTarget !== resolve(canonicalDir, top.sessions)) {
|
|
1300
|
+
affected.push({ path: "sessions", replacement: formerTarget });
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
if (isObject(top.playbooks)) {
|
|
1305
|
+
for (const [id, block] of Object.entries(top.playbooks)) {
|
|
1306
|
+
const from = isObject(block) ? block.from : undefined;
|
|
1307
|
+
if (
|
|
1308
|
+
typeof from !== "string" ||
|
|
1309
|
+
!isRelativeFilesystemRegistrySpecifier(from)
|
|
1310
|
+
) {
|
|
1311
|
+
continue;
|
|
1312
|
+
}
|
|
1313
|
+
const formerTarget = resolve(legacyDir, from);
|
|
1314
|
+
if (formerTarget !== resolve(canonicalDir, from)) {
|
|
1315
|
+
affected.push({
|
|
1316
|
+
path: `playbooks.${id}.from`,
|
|
1317
|
+
replacement: pathToFileURL(formerTarget).href,
|
|
1318
|
+
});
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
if (affected.length === 0) return;
|
|
1324
|
+
const replacements = affected
|
|
1325
|
+
.map(({ path, replacement }) => `${path} = ${JSON.stringify(replacement)}`)
|
|
1326
|
+
.join("; ");
|
|
1327
|
+
throw new Error(
|
|
1328
|
+
`cannot relocate legacy config at ${legacyUserConfigPath}: relative ` +
|
|
1329
|
+
`locators would change targets under ${userConfigPath}; replace them ` +
|
|
1330
|
+
`with target-preserving absolute values before retrying: ${replacements}`,
|
|
1331
|
+
);
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1090
1334
|
function seedUserConfigIfMissing(userConfigPath, templatePath, onNotice) {
|
|
1091
1335
|
if (existsSync(userConfigPath)) return;
|
|
1092
1336
|
mkdirSync(dirname(userConfigPath), { recursive: true });
|
|
@@ -1098,7 +1342,7 @@ function seedUserConfigIfMissing(userConfigPath, templatePath, onNotice) {
|
|
|
1098
1342
|
function migrateUserConfigIfRetired(userConfigPath, onNotice) {
|
|
1099
1343
|
let text;
|
|
1100
1344
|
try {
|
|
1101
|
-
text = readFileSync(userConfigPath,
|
|
1345
|
+
text = readFileSync(userConfigPath, "utf8");
|
|
1102
1346
|
} catch {
|
|
1103
1347
|
return;
|
|
1104
1348
|
}
|
|
@@ -1106,13 +1350,13 @@ function migrateUserConfigIfRetired(userConfigPath, onNotice) {
|
|
|
1106
1350
|
try {
|
|
1107
1351
|
migrated = migrateRetiredProfiles(text);
|
|
1108
1352
|
} catch (error) {
|
|
1109
|
-
if (error?.code ===
|
|
1353
|
+
if (error?.code === "PLAYBOOK_LEGACY_PLAYERS") {
|
|
1110
1354
|
throw legacyPlayersError(error.legacyPath, userConfigPath);
|
|
1111
1355
|
}
|
|
1112
1356
|
throw new Error(
|
|
1113
1357
|
`cannot migrate the retired profiles config at ${userConfigPath}: ` +
|
|
1114
1358
|
`${errorMessage(error)} — edit it by hand: each agent takes its own ` +
|
|
1115
|
-
|
|
1359
|
+
"adapter, model, effort, fast mode, and permissions",
|
|
1116
1360
|
);
|
|
1117
1361
|
}
|
|
1118
1362
|
if (migrated === undefined) return;
|
|
@@ -1140,33 +1384,33 @@ export function migrateRetiredProfiles(text) {
|
|
|
1140
1384
|
const doc = parseYamlDocument(text);
|
|
1141
1385
|
const contents = doc.contents;
|
|
1142
1386
|
if (!contents || !Array.isArray(contents.items)) return undefined;
|
|
1143
|
-
const playbooks = doc.get(
|
|
1387
|
+
const playbooks = doc.get("playbooks");
|
|
1144
1388
|
if (playbooks && Array.isArray(playbooks.items)) {
|
|
1145
1389
|
for (const entry of playbooks.items) {
|
|
1146
1390
|
const id = String(entry.key);
|
|
1147
|
-
if (doc.getIn([
|
|
1391
|
+
if (doc.getIn(["playbooks", id, "players"]) !== undefined) {
|
|
1148
1392
|
throw legacyPlayersError(`playbooks.${id}.players`);
|
|
1149
1393
|
}
|
|
1150
1394
|
}
|
|
1151
1395
|
}
|
|
1152
|
-
const profiles = doc.get(
|
|
1153
|
-
const agentPaths = [[
|
|
1154
|
-
const players = doc.get(
|
|
1396
|
+
const profiles = doc.get("profiles");
|
|
1397
|
+
const agentPaths = [["captain"]];
|
|
1398
|
+
const players = doc.get("players");
|
|
1155
1399
|
if (players && Array.isArray(players.items)) {
|
|
1156
1400
|
for (const player of players.items) {
|
|
1157
|
-
agentPaths.push([
|
|
1401
|
+
agentPaths.push(["players", String(player.key)]);
|
|
1158
1402
|
}
|
|
1159
1403
|
}
|
|
1160
1404
|
|
|
1161
1405
|
const profileSettings = (name) =>
|
|
1162
|
-
profiles && typeof profiles.get ===
|
|
1406
|
+
profiles && typeof profiles.get === "function"
|
|
1163
1407
|
? profiles.get(name)
|
|
1164
1408
|
: undefined;
|
|
1165
1409
|
|
|
1166
1410
|
let changed = false;
|
|
1167
1411
|
for (const path of agentPaths) {
|
|
1168
1412
|
const node = doc.getIn(path, true);
|
|
1169
|
-
if (node && typeof node.value ===
|
|
1413
|
+
if (node && typeof node.value === "string" && !Array.isArray(node.items)) {
|
|
1170
1414
|
const settings = profileSettings(node.value);
|
|
1171
1415
|
if (settings === undefined) continue;
|
|
1172
1416
|
const inlined = settings.clone();
|
|
@@ -1174,16 +1418,16 @@ export function migrateRetiredProfiles(text) {
|
|
|
1174
1418
|
doc.setIn(path, inlined);
|
|
1175
1419
|
changed = true;
|
|
1176
1420
|
} else if (node && Array.isArray(node.items)) {
|
|
1177
|
-
const named = node.get?.(
|
|
1421
|
+
const named = node.get?.("profile");
|
|
1178
1422
|
if (named === undefined) continue;
|
|
1179
1423
|
const settings = profileSettings(named);
|
|
1180
1424
|
if (settings === undefined) {
|
|
1181
1425
|
throw new Error(
|
|
1182
|
-
`${path.join(
|
|
1183
|
-
|
|
1426
|
+
`${path.join(".")}.profile names "${String(named)}", which no ` +
|
|
1427
|
+
"profiles entry defines",
|
|
1184
1428
|
);
|
|
1185
1429
|
}
|
|
1186
|
-
node.delete(
|
|
1430
|
+
node.delete("profile");
|
|
1187
1431
|
for (const item of settings.items) {
|
|
1188
1432
|
if (node.has(String(item.key))) continue;
|
|
1189
1433
|
node.add(item.clone());
|
|
@@ -1194,11 +1438,11 @@ export function migrateRetiredProfiles(text) {
|
|
|
1194
1438
|
|
|
1195
1439
|
if (profiles !== undefined) {
|
|
1196
1440
|
const index = contents.items.findIndex(
|
|
1197
|
-
(item) => String(item.key) ===
|
|
1441
|
+
(item) => String(item.key) === "profiles",
|
|
1198
1442
|
);
|
|
1199
1443
|
const lead =
|
|
1200
1444
|
index === -1 ? undefined : contents.items[index]?.key?.commentBefore;
|
|
1201
|
-
doc.delete(
|
|
1445
|
+
doc.delete("profiles");
|
|
1202
1446
|
const header = keptHeaderComment(lead);
|
|
1203
1447
|
const next = contents.items[0];
|
|
1204
1448
|
if (header !== undefined && next?.key) {
|
|
@@ -1215,20 +1459,20 @@ export function migrateRetiredProfiles(text) {
|
|
|
1215
1459
|
}
|
|
1216
1460
|
|
|
1217
1461
|
const MIGRATION_NOTE =
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1462
|
+
" Migrated by playbook 3.0.0: the top-level `profiles` map was removed and\n" +
|
|
1463
|
+
" each agent now carries its settings inline. The pre-migration file is\n" +
|
|
1464
|
+
" kept beside this one as a .bak. Comments below may still describe the\n" +
|
|
1465
|
+
" retired profiles model.";
|
|
1222
1466
|
|
|
1223
1467
|
function carryScalarComment(node, inlined) {
|
|
1224
1468
|
const parts = [node.commentBefore, node.comment].filter(
|
|
1225
|
-
(part) => typeof part ===
|
|
1469
|
+
(part) => typeof part === "string" && part.trim() !== "",
|
|
1226
1470
|
);
|
|
1227
1471
|
if (parts.length === 0) return;
|
|
1228
1472
|
const first = inlined.items?.[0]?.key;
|
|
1229
1473
|
if (!first) return;
|
|
1230
1474
|
inlined.flow = false;
|
|
1231
|
-
const carried = parts.join(
|
|
1475
|
+
const carried = parts.join("\n");
|
|
1232
1476
|
first.commentBefore =
|
|
1233
1477
|
first.commentBefore === undefined
|
|
1234
1478
|
? carried
|
|
@@ -1236,24 +1480,25 @@ function carryScalarComment(node, inlined) {
|
|
|
1236
1480
|
}
|
|
1237
1481
|
|
|
1238
1482
|
function keptHeaderComment(comment) {
|
|
1239
|
-
if (typeof comment !==
|
|
1240
|
-
const paragraphs = comment.split(
|
|
1241
|
-
const kept = paragraphs.slice(0, -1).join(
|
|
1242
|
-
return kept.trim() ===
|
|
1483
|
+
if (typeof comment !== "string" || comment.trim() === "") return undefined;
|
|
1484
|
+
const paragraphs = comment.split("\n\n");
|
|
1485
|
+
const kept = paragraphs.slice(0, -1).join("\n\n");
|
|
1486
|
+
return kept.trim() === "" ? undefined : kept;
|
|
1243
1487
|
}
|
|
1244
1488
|
|
|
1245
1489
|
function assertNoRetiredProfiles(top, configPath) {
|
|
1246
|
-
const where = configPath ? ` in ${configPath}` :
|
|
1490
|
+
const where = configPath ? ` in ${configPath}` : "";
|
|
1247
1491
|
if (top.profiles !== undefined) {
|
|
1248
1492
|
throw new Error(
|
|
1249
1493
|
`top-level "profiles" was removed${where}: write each agent's settings ` +
|
|
1250
|
-
|
|
1251
|
-
|
|
1494
|
+
"inline under captain and each top-level players.<player-id> " +
|
|
1495
|
+
"(adapter, model, effort, fast mode, permissions)",
|
|
1252
1496
|
);
|
|
1253
1497
|
}
|
|
1254
1498
|
const legacyPath = findLegacyPlayersPath(top);
|
|
1255
|
-
if (legacyPath !== undefined)
|
|
1256
|
-
|
|
1499
|
+
if (legacyPath !== undefined)
|
|
1500
|
+
throw legacyPlayersError(legacyPath, configPath);
|
|
1501
|
+
const blocks = [["captain", top.captain]];
|
|
1257
1502
|
const playersCfg = isObject(top.players) ? top.players : {};
|
|
1258
1503
|
for (const [playerId, agent] of Object.entries(playersCfg)) {
|
|
1259
1504
|
blocks.push([`players.${playerId}`, agent]);
|
|
@@ -1262,7 +1507,7 @@ function assertNoRetiredProfiles(top, configPath) {
|
|
|
1262
1507
|
if (isObject(block) && block.profile !== undefined) {
|
|
1263
1508
|
throw new Error(
|
|
1264
1509
|
`${path}.profile was removed${where}: write the agent's settings ` +
|
|
1265
|
-
|
|
1510
|
+
"inline in that block (adapter, model, effort, fast mode, permissions)",
|
|
1266
1511
|
);
|
|
1267
1512
|
}
|
|
1268
1513
|
}
|
|
@@ -1288,24 +1533,24 @@ export function snapshotRegistryEntry(value) {
|
|
|
1288
1533
|
}
|
|
1289
1534
|
|
|
1290
1535
|
export function invalidRegistryEntryReason(value) {
|
|
1291
|
-
if (!isObject(value)) return
|
|
1536
|
+
if (!isObject(value)) return "the default export must be an object";
|
|
1292
1537
|
if (
|
|
1293
|
-
typeof value.id !==
|
|
1538
|
+
typeof value.id !== "string" ||
|
|
1294
1539
|
value.id.trim().length === 0 ||
|
|
1295
1540
|
value.id !== value.id.trim()
|
|
1296
1541
|
) {
|
|
1297
|
-
return
|
|
1542
|
+
return "id must be a canonical trimmed nonblank string";
|
|
1298
1543
|
}
|
|
1299
1544
|
if (
|
|
1300
|
-
typeof value.command !==
|
|
1545
|
+
typeof value.command !== "string" ||
|
|
1301
1546
|
value.command.trim().length === 0 ||
|
|
1302
1547
|
value.command !== value.command.trim()
|
|
1303
1548
|
) {
|
|
1304
|
-
return
|
|
1549
|
+
return "command must be a canonical trimmed nonblank string";
|
|
1305
1550
|
}
|
|
1306
|
-
if (typeof value.intent !==
|
|
1551
|
+
if (typeof value.intent !== "string") return "intent must be a string";
|
|
1307
1552
|
if (value.artifactSchema !== 3) {
|
|
1308
|
-
return
|
|
1553
|
+
return "artifactSchema must be 3";
|
|
1309
1554
|
}
|
|
1310
1555
|
const runtimeProfileProblem = invalidRuntimeProfileReason(
|
|
1311
1556
|
value.runtimeProfile,
|
|
@@ -1321,66 +1566,66 @@ export function invalidRegistryEntryReason(value) {
|
|
|
1321
1566
|
if (concurrentProblem !== undefined) {
|
|
1322
1567
|
return `concurrentRoleSets ${concurrentProblem}`;
|
|
1323
1568
|
}
|
|
1324
|
-
if (typeof value.validateOptions !==
|
|
1325
|
-
return
|
|
1569
|
+
if (typeof value.validateOptions !== "function") {
|
|
1570
|
+
return "validateOptions must be a function";
|
|
1326
1571
|
}
|
|
1327
|
-
if (typeof value.createRuntime !==
|
|
1328
|
-
return
|
|
1572
|
+
if (typeof value.createRuntime !== "function") {
|
|
1573
|
+
return "createRuntime must be a function";
|
|
1329
1574
|
}
|
|
1330
1575
|
return undefined;
|
|
1331
1576
|
}
|
|
1332
1577
|
|
|
1333
1578
|
function invalidRuntimeProfileReason(value, advertisedArtifactSchema) {
|
|
1334
1579
|
if (!isPlainObject(value)) {
|
|
1335
|
-
return
|
|
1580
|
+
return "runtimeProfile must be a plain object";
|
|
1336
1581
|
}
|
|
1337
|
-
const kindDescriptor = Object.getOwnPropertyDescriptor(value,
|
|
1582
|
+
const kindDescriptor = Object.getOwnPropertyDescriptor(value, "kind");
|
|
1338
1583
|
if (
|
|
1339
1584
|
kindDescriptor === undefined ||
|
|
1340
1585
|
kindDescriptor.get !== undefined ||
|
|
1341
1586
|
kindDescriptor.set !== undefined ||
|
|
1342
1587
|
kindDescriptor.enumerable !== true
|
|
1343
1588
|
) {
|
|
1344
|
-
return
|
|
1589
|
+
return "runtimeProfile.kind must be an enumerable data property";
|
|
1345
1590
|
}
|
|
1346
1591
|
const kind = kindDescriptor.value;
|
|
1347
|
-
if (kind ===
|
|
1348
|
-
const profile = exactPlainDataRecord(value, [
|
|
1592
|
+
if (kind === "shared-factory") {
|
|
1593
|
+
const profile = exactPlainDataRecord(value, ["kind", "compat"]);
|
|
1349
1594
|
if (profile === undefined) {
|
|
1350
|
-
return
|
|
1595
|
+
return "shared-factory runtimeProfile must contain exactly kind and compat data properties";
|
|
1351
1596
|
}
|
|
1352
1597
|
const compat = exactPlainDataRecord(profile.compat, [
|
|
1353
|
-
|
|
1354
|
-
|
|
1598
|
+
"artifactSchema",
|
|
1599
|
+
"runtimeAbi",
|
|
1355
1600
|
]);
|
|
1356
1601
|
if (compat === undefined) {
|
|
1357
|
-
return
|
|
1602
|
+
return "shared-factory runtimeProfile.compat must contain exactly artifactSchema and runtimeAbi data properties";
|
|
1358
1603
|
}
|
|
1359
1604
|
if (!Number.isSafeInteger(compat.artifactSchema)) {
|
|
1360
|
-
return
|
|
1605
|
+
return "shared-factory runtimeProfile.compat.artifactSchema must be an integer";
|
|
1361
1606
|
}
|
|
1362
1607
|
if (!Number.isSafeInteger(compat.runtimeAbi)) {
|
|
1363
|
-
return
|
|
1608
|
+
return "shared-factory runtimeProfile.compat.runtimeAbi must be an integer";
|
|
1364
1609
|
}
|
|
1365
1610
|
if (compat.artifactSchema !== advertisedArtifactSchema) {
|
|
1366
|
-
return
|
|
1611
|
+
return "artifactSchema must match runtimeProfile.compat.artifactSchema";
|
|
1367
1612
|
}
|
|
1368
1613
|
return undefined;
|
|
1369
1614
|
}
|
|
1370
|
-
if (kind ===
|
|
1371
|
-
const profile = exactPlainDataRecord(value, [
|
|
1615
|
+
if (kind === "bespoke") {
|
|
1616
|
+
const profile = exactPlainDataRecord(value, ["kind", "artifactSchema"]);
|
|
1372
1617
|
if (profile === undefined) {
|
|
1373
|
-
return
|
|
1618
|
+
return "bespoke runtimeProfile must contain exactly kind and artifactSchema data properties";
|
|
1374
1619
|
}
|
|
1375
1620
|
if (!Number.isSafeInteger(profile.artifactSchema)) {
|
|
1376
|
-
return
|
|
1621
|
+
return "bespoke runtimeProfile.artifactSchema must be an integer";
|
|
1377
1622
|
}
|
|
1378
1623
|
if (profile.artifactSchema !== advertisedArtifactSchema) {
|
|
1379
|
-
return
|
|
1624
|
+
return "artifactSchema must match runtimeProfile.artifactSchema";
|
|
1380
1625
|
}
|
|
1381
1626
|
return undefined;
|
|
1382
1627
|
}
|
|
1383
|
-
return
|
|
1628
|
+
return "runtimeProfile.kind must be shared-factory or bespoke";
|
|
1384
1629
|
}
|
|
1385
1630
|
|
|
1386
1631
|
function exactPlainDataRecord(value, expectedKeys) {
|
|
@@ -1388,9 +1633,7 @@ function exactPlainDataRecord(value, expectedKeys) {
|
|
|
1388
1633
|
const keys = Reflect.ownKeys(value);
|
|
1389
1634
|
if (
|
|
1390
1635
|
keys.length !== expectedKeys.length ||
|
|
1391
|
-
keys.some(
|
|
1392
|
-
(key) => typeof key !== 'string' || !expectedKeys.includes(key),
|
|
1393
|
-
)
|
|
1636
|
+
keys.some((key) => typeof key !== "string" || !expectedKeys.includes(key))
|
|
1394
1637
|
) {
|
|
1395
1638
|
return undefined;
|
|
1396
1639
|
}
|
|
@@ -1414,23 +1657,23 @@ function exactPlainDataRecord(value, expectedKeys) {
|
|
|
1414
1657
|
}
|
|
1415
1658
|
|
|
1416
1659
|
function validateStoredStructuralProjection(value) {
|
|
1417
|
-
const stored = cloneJson(value,
|
|
1660
|
+
const stored = cloneJson(value, "stored structural projection");
|
|
1418
1661
|
if (!isPlainObject(stored) || stored.schemaVersion !== 1) {
|
|
1419
|
-
throw new Error(
|
|
1662
|
+
throw new Error("stored structural projection schema 1 is required");
|
|
1420
1663
|
}
|
|
1421
|
-
const captain = requireObject(stored.captain,
|
|
1422
|
-
if (typeof captain.adapter !==
|
|
1423
|
-
throw new Error(
|
|
1664
|
+
const captain = requireObject(stored.captain, "stored structural captain");
|
|
1665
|
+
if (typeof captain.adapter !== "string" || captain.adapter.length === 0) {
|
|
1666
|
+
throw new Error("stored structural captain must name an adapter");
|
|
1424
1667
|
}
|
|
1425
1668
|
if (!Array.isArray(stored.players) || !isPlainObject(stored.catalog)) {
|
|
1426
1669
|
throw new Error(
|
|
1427
|
-
|
|
1670
|
+
"stored structural projection must contain players and catalog",
|
|
1428
1671
|
);
|
|
1429
1672
|
}
|
|
1430
1673
|
const playerIds = stored.players.map((player, index) => {
|
|
1431
1674
|
const record = requireObject(player, `stored structural players.${index}`);
|
|
1432
1675
|
assertPlayerId(record.id, `stored structural players.${index}.id`);
|
|
1433
|
-
if (typeof record.adapter !==
|
|
1676
|
+
if (typeof record.adapter !== "string" || record.adapter.length === 0) {
|
|
1434
1677
|
throw new Error(
|
|
1435
1678
|
`stored structural players.${index} must name an adapter`,
|
|
1436
1679
|
);
|
|
@@ -1438,20 +1681,15 @@ function validateStoredStructuralProjection(value) {
|
|
|
1438
1681
|
return record.id;
|
|
1439
1682
|
});
|
|
1440
1683
|
if (new Set(playerIds).size !== playerIds.length) {
|
|
1441
|
-
throw new Error(
|
|
1684
|
+
throw new Error("stored structural player ids must be unique");
|
|
1442
1685
|
}
|
|
1443
1686
|
for (const [id, itemValue] of Object.entries(stored.catalog)) {
|
|
1444
1687
|
const item = requireObject(itemValue, `stored structural catalog.${id}`);
|
|
1445
1688
|
if (item.id !== id || id === RESERVED_CAPTAIN_PLAYBOOK_ID) {
|
|
1446
1689
|
throw new Error(`stored structural catalog.${id}.id is invalid`);
|
|
1447
1690
|
}
|
|
1448
|
-
for (const field of [
|
|
1449
|
-
|
|
1450
|
-
'manifestCommand',
|
|
1451
|
-
'command',
|
|
1452
|
-
'intent',
|
|
1453
|
-
]) {
|
|
1454
|
-
if (typeof item[field] !== 'string') {
|
|
1691
|
+
for (const field of ["from", "manifestCommand", "command", "intent"]) {
|
|
1692
|
+
if (typeof item[field] !== "string") {
|
|
1455
1693
|
throw new Error(`stored structural catalog.${id}.${field} is invalid`);
|
|
1456
1694
|
}
|
|
1457
1695
|
}
|
|
@@ -1519,7 +1757,7 @@ function projectSelectedMembers(top, selectedMembers) {
|
|
|
1519
1757
|
if (selectedMembers === undefined) return top;
|
|
1520
1758
|
const { playbookIds, playerIds } = validateSelectedMembers(selectedMembers);
|
|
1521
1759
|
if (!isPlainObject(top)) {
|
|
1522
|
-
throw new Error(
|
|
1760
|
+
throw new Error("config must contain only plain JSON objects");
|
|
1523
1761
|
}
|
|
1524
1762
|
|
|
1525
1763
|
const descriptors = Object.getOwnPropertyDescriptors(top);
|
|
@@ -1527,7 +1765,7 @@ function projectSelectedMembers(top, selectedMembers) {
|
|
|
1527
1765
|
for (const key of keys) {
|
|
1528
1766
|
const descriptor = descriptors[key];
|
|
1529
1767
|
if (
|
|
1530
|
-
typeof key ===
|
|
1768
|
+
typeof key === "symbol" ||
|
|
1531
1769
|
descriptor?.get !== undefined ||
|
|
1532
1770
|
descriptor?.set !== undefined ||
|
|
1533
1771
|
descriptor?.enumerable !== true
|
|
@@ -1543,20 +1781,20 @@ function projectSelectedMembers(top, selectedMembers) {
|
|
|
1543
1781
|
projected.playbooks = projectSelectedMap(
|
|
1544
1782
|
projected.playbooks,
|
|
1545
1783
|
playbookIds,
|
|
1546
|
-
|
|
1784
|
+
"playbooks",
|
|
1547
1785
|
);
|
|
1548
1786
|
projected.players = projectSelectedMap(
|
|
1549
1787
|
projected.players,
|
|
1550
1788
|
playerIds,
|
|
1551
|
-
|
|
1789
|
+
"players",
|
|
1552
1790
|
);
|
|
1553
1791
|
return projected;
|
|
1554
1792
|
}
|
|
1555
1793
|
|
|
1556
1794
|
function validateSelectedMembers(selectedMembers) {
|
|
1557
|
-
const selected = cloneJson(selectedMembers,
|
|
1795
|
+
const selected = cloneJson(selectedMembers, "selectedMembers");
|
|
1558
1796
|
const unknownSelectionKeys = Object.keys(selected).filter(
|
|
1559
|
-
(key) => ![
|
|
1797
|
+
(key) => !["playbookIds", "playerIds"].includes(key),
|
|
1560
1798
|
);
|
|
1561
1799
|
if (unknownSelectionKeys.length > 0) {
|
|
1562
1800
|
throw new Error(
|
|
@@ -1566,12 +1804,9 @@ function validateSelectedMembers(selectedMembers) {
|
|
|
1566
1804
|
return {
|
|
1567
1805
|
playbookIds: selectedIdList(
|
|
1568
1806
|
selected.playbookIds,
|
|
1569
|
-
|
|
1570
|
-
),
|
|
1571
|
-
playerIds: selectedIdList(
|
|
1572
|
-
selected.playerIds,
|
|
1573
|
-
'selectedMembers.playerIds',
|
|
1807
|
+
"selectedMembers.playbookIds",
|
|
1574
1808
|
),
|
|
1809
|
+
playerIds: selectedIdList(selected.playerIds, "selectedMembers.playerIds"),
|
|
1575
1810
|
};
|
|
1576
1811
|
}
|
|
1577
1812
|
|
|
@@ -1584,7 +1819,7 @@ function projectSelectedLayer(value, selected, path) {
|
|
|
1584
1819
|
for (const key of Reflect.ownKeys(value)) {
|
|
1585
1820
|
const descriptor = descriptors[key];
|
|
1586
1821
|
if (
|
|
1587
|
-
typeof key ===
|
|
1822
|
+
typeof key === "symbol" ||
|
|
1588
1823
|
descriptor?.get !== undefined ||
|
|
1589
1824
|
descriptor?.set !== undefined ||
|
|
1590
1825
|
descriptor?.enumerable !== true
|
|
@@ -1593,12 +1828,12 @@ function projectSelectedLayer(value, selected, path) {
|
|
|
1593
1828
|
`${path}.${String(key)} must be an enumerable data property`,
|
|
1594
1829
|
);
|
|
1595
1830
|
}
|
|
1596
|
-
if (key ===
|
|
1831
|
+
if (key === "playbooks" || key === "players") {
|
|
1597
1832
|
entries.push([
|
|
1598
1833
|
key,
|
|
1599
1834
|
projectOptionalSelectedMap(
|
|
1600
1835
|
descriptor.value,
|
|
1601
|
-
key ===
|
|
1836
|
+
key === "playbooks" ? selected.playbookIds : selected.playerIds,
|
|
1602
1837
|
`${path}.${key}`,
|
|
1603
1838
|
),
|
|
1604
1839
|
]);
|
|
@@ -1656,7 +1891,7 @@ function projectSelectedMap(value, ids, path) {
|
|
|
1656
1891
|
function selectedIdList(value, path) {
|
|
1657
1892
|
if (
|
|
1658
1893
|
!Array.isArray(value) ||
|
|
1659
|
-
value.some((id) => typeof id !==
|
|
1894
|
+
value.some((id) => typeof id !== "string" || id.trim().length === 0) ||
|
|
1660
1895
|
new Set(value).size !== value.length
|
|
1661
1896
|
) {
|
|
1662
1897
|
throw new Error(`${path} must be a duplicate-free array of nonblank ids`);
|
|
@@ -1665,33 +1900,40 @@ function selectedIdList(value, path) {
|
|
|
1665
1900
|
}
|
|
1666
1901
|
|
|
1667
1902
|
function resolveRoleBinding(value, path) {
|
|
1668
|
-
if (typeof value ===
|
|
1903
|
+
if (typeof value === "string") {
|
|
1669
1904
|
assertPlayerId(value, path);
|
|
1670
1905
|
return { playerId: value };
|
|
1671
1906
|
}
|
|
1672
1907
|
const block = requireObject(value, path);
|
|
1673
1908
|
const unknown = Object.keys(block).filter(
|
|
1674
|
-
(key) => ![
|
|
1909
|
+
(key) => !["player", "model", "effort", "fastMode"].includes(key),
|
|
1675
1910
|
);
|
|
1676
1911
|
if (unknown.length > 0) {
|
|
1677
1912
|
throw new Error(`${path} has unknown ${formatKeyList(unknown)}`);
|
|
1678
1913
|
}
|
|
1679
1914
|
assertPlayerId(block.player, `${path}.player`);
|
|
1680
|
-
for (const field of [
|
|
1915
|
+
for (const field of ["model", "effort"]) {
|
|
1681
1916
|
if (
|
|
1682
1917
|
block[field] !== undefined &&
|
|
1683
1918
|
block[field] !== false &&
|
|
1684
|
-
(typeof block[field] !==
|
|
1919
|
+
(typeof block[field] !== "string" || block[field].trim().length === 0)
|
|
1685
1920
|
) {
|
|
1686
1921
|
throw new Error(
|
|
1687
1922
|
`${path}.${field} must be a nonblank string or false for provider-default`,
|
|
1688
1923
|
);
|
|
1689
1924
|
}
|
|
1690
1925
|
}
|
|
1926
|
+
// Fast mode carries no provider-default sentinel: omission inherits the
|
|
1927
|
+
// player's value and `false` is a literal request, so it is a plain boolean
|
|
1928
|
+
// rather than the string-or-false tuning shape above.
|
|
1929
|
+
if (block.fastMode !== undefined && typeof block.fastMode !== "boolean") {
|
|
1930
|
+
throw new Error(`${path}.fastMode must be a boolean`);
|
|
1931
|
+
}
|
|
1691
1932
|
return {
|
|
1692
1933
|
playerId: block.player,
|
|
1693
1934
|
...(block.model === undefined ? {} : { model: block.model }),
|
|
1694
1935
|
...(block.effort === undefined ? {} : { effort: block.effort }),
|
|
1936
|
+
...(block.fastMode === undefined ? {} : { fastMode: block.fastMode }),
|
|
1695
1937
|
};
|
|
1696
1938
|
}
|
|
1697
1939
|
|
|
@@ -1704,17 +1946,21 @@ function applyTuningOverrides(agent, binding) {
|
|
|
1704
1946
|
if (binding.effort === false) delete effective.effort;
|
|
1705
1947
|
else effective.effort = binding.effort;
|
|
1706
1948
|
}
|
|
1949
|
+
if (binding.fastMode !== undefined) effective.fastMode = binding.fastMode;
|
|
1707
1950
|
return effective;
|
|
1708
1951
|
}
|
|
1709
1952
|
|
|
1710
1953
|
function sessionAgentFromHostAgent(agent, path) {
|
|
1711
1954
|
if (!isObject(agent)) {
|
|
1712
|
-
throw new Error(
|
|
1955
|
+
throw new Error("installed cligent omitted a retained agent");
|
|
1713
1956
|
}
|
|
1714
1957
|
return {
|
|
1715
1958
|
adapter: agent.adapter,
|
|
1716
1959
|
model: tuningSelection(agent.model, `${path}.model`),
|
|
1717
1960
|
effort: tuningSelection(agent.effort, `${path}.effort`),
|
|
1961
|
+
...(agent.fastMode === undefined
|
|
1962
|
+
? {}
|
|
1963
|
+
: { fastMode: fastModeSelection(agent.fastMode, `${path}.fastMode`) }),
|
|
1718
1964
|
...(agent.instruction === undefined
|
|
1719
1965
|
? {}
|
|
1720
1966
|
: { instruction: agent.instruction }),
|
|
@@ -1727,16 +1973,19 @@ function sessionAgentFromHostAgent(agent, path) {
|
|
|
1727
1973
|
// Shared by the interactive and headless host projections. A tagged
|
|
1728
1974
|
// provider-default is represented to cligent by omitting that configured
|
|
1729
1975
|
// default; the complete tagged selection remains in sessionAgents.
|
|
1730
|
-
export function projectHostAgent(agent, path =
|
|
1976
|
+
export function projectHostAgent(agent, path = "agent") {
|
|
1731
1977
|
const normalized = cloneJson(agent, path);
|
|
1732
1978
|
return {
|
|
1733
1979
|
adapter: normalized.adapter,
|
|
1734
|
-
...(normalized.model?.kind ===
|
|
1980
|
+
...(normalized.model?.kind === "value"
|
|
1735
1981
|
? { model: normalized.model.value }
|
|
1736
1982
|
: {}),
|
|
1737
|
-
...(normalized.effort?.kind ===
|
|
1983
|
+
...(normalized.effort?.kind === "value"
|
|
1738
1984
|
? { effort: normalized.effort.value }
|
|
1739
1985
|
: {}),
|
|
1986
|
+
...(normalized.fastMode === undefined
|
|
1987
|
+
? {}
|
|
1988
|
+
: { fastMode: normalized.fastMode }),
|
|
1740
1989
|
...(normalized.instruction === undefined
|
|
1741
1990
|
? {}
|
|
1742
1991
|
: { instruction: normalized.instruction }),
|
|
@@ -1746,22 +1995,29 @@ export function projectHostAgent(agent, path = 'agent') {
|
|
|
1746
1995
|
};
|
|
1747
1996
|
}
|
|
1748
1997
|
|
|
1998
|
+
function fastModeSelection(value, path) {
|
|
1999
|
+
if (typeof value !== "boolean") {
|
|
2000
|
+
throw new Error(`${path} must be a boolean`);
|
|
2001
|
+
}
|
|
2002
|
+
return value;
|
|
2003
|
+
}
|
|
2004
|
+
|
|
1749
2005
|
function tuningSelection(value, path) {
|
|
1750
2006
|
if (
|
|
1751
2007
|
value !== undefined &&
|
|
1752
|
-
(typeof value !==
|
|
2008
|
+
(typeof value !== "string" || value.trim().length === 0)
|
|
1753
2009
|
) {
|
|
1754
2010
|
throw new Error(`${path} must be a nonblank string`);
|
|
1755
2011
|
}
|
|
1756
2012
|
return value === undefined
|
|
1757
|
-
? { kind:
|
|
1758
|
-
: { kind:
|
|
2013
|
+
? { kind: "provider-default" }
|
|
2014
|
+
: { kind: "value", value };
|
|
1759
2015
|
}
|
|
1760
2016
|
|
|
1761
2017
|
function overrideTuningSelection(value) {
|
|
1762
2018
|
return value === false
|
|
1763
|
-
? { kind:
|
|
1764
|
-
: tuningSelection(value,
|
|
2019
|
+
? { kind: "provider-default" }
|
|
2020
|
+
: tuningSelection(value, "role tuning override");
|
|
1765
2021
|
}
|
|
1766
2022
|
|
|
1767
2023
|
function canonicalizePreparedRegistrySpecifier(value, path) {
|
|
@@ -1778,7 +2034,7 @@ function canonicalizePreparedRegistrySpecifier(value, path) {
|
|
|
1778
2034
|
`${path} preparation must return a canonical module specifier`,
|
|
1779
2035
|
);
|
|
1780
2036
|
}
|
|
1781
|
-
if (value.startsWith(
|
|
2037
|
+
if (value.startsWith("file:")) {
|
|
1782
2038
|
let canonical;
|
|
1783
2039
|
try {
|
|
1784
2040
|
canonical = pathToFileURL(fileURLToPath(value)).href;
|
|
@@ -1793,7 +2049,7 @@ function canonicalizePreparedRegistrySpecifier(value, path) {
|
|
|
1793
2049
|
}
|
|
1794
2050
|
|
|
1795
2051
|
function assertPlayerId(value, path) {
|
|
1796
|
-
if (typeof value !==
|
|
2052
|
+
if (typeof value !== "string" || !PLAYER_ID_PATTERN.test(value)) {
|
|
1797
2053
|
throw new Error(
|
|
1798
2054
|
`${path} must name a player matching ${PLAYER_ID_PATTERN.source}`,
|
|
1799
2055
|
);
|
|
@@ -1804,7 +2060,7 @@ function assertPlayerId(value, path) {
|
|
|
1804
2060
|
}
|
|
1805
2061
|
|
|
1806
2062
|
function assertRoleId(value, path) {
|
|
1807
|
-
if (typeof value !==
|
|
2063
|
+
if (typeof value !== "string" || !ROLE_ID_PATTERN.test(value)) {
|
|
1808
2064
|
throw new Error(`${path} must use a canonical lowercase local role id`);
|
|
1809
2065
|
}
|
|
1810
2066
|
if (value === RESERVED_CAPTAIN_ROLE_ID) {
|
|
@@ -1813,13 +2069,13 @@ function assertRoleId(value, path) {
|
|
|
1813
2069
|
}
|
|
1814
2070
|
|
|
1815
2071
|
function invalidManifestRoles(value) {
|
|
1816
|
-
if (!Array.isArray(value)) return
|
|
1817
|
-
if (value.some((role) => typeof role !==
|
|
1818
|
-
return
|
|
2072
|
+
if (!Array.isArray(value)) return "must be an array";
|
|
2073
|
+
if (value.some((role) => typeof role !== "string")) {
|
|
2074
|
+
return "must contain only strings";
|
|
1819
2075
|
}
|
|
1820
2076
|
const canonical = value.map((role) => role.toLowerCase());
|
|
1821
2077
|
if (new Set(canonical).size !== canonical.length) {
|
|
1822
|
-
return
|
|
2078
|
+
return "contains roles that collide after canonical lowercase derivation";
|
|
1823
2079
|
}
|
|
1824
2080
|
const invalid = value.find((role) => !ROLE_ID_PATTERN.test(role));
|
|
1825
2081
|
if (invalid !== undefined) {
|
|
@@ -1832,7 +2088,7 @@ function invalidManifestRoles(value) {
|
|
|
1832
2088
|
}
|
|
1833
2089
|
|
|
1834
2090
|
function invalidConcurrentRoleSets(value, requiredRoleIds) {
|
|
1835
|
-
if (!Array.isArray(value)) return
|
|
2091
|
+
if (!Array.isArray(value)) return "must be an array";
|
|
1836
2092
|
const required = new Set(requiredRoleIds);
|
|
1837
2093
|
const seen = new Set();
|
|
1838
2094
|
for (let index = 0; index < value.length; index += 1) {
|
|
@@ -1843,7 +2099,7 @@ function invalidConcurrentRoleSets(value, requiredRoleIds) {
|
|
|
1843
2099
|
if (
|
|
1844
2100
|
set.some(
|
|
1845
2101
|
(role) =>
|
|
1846
|
-
typeof role !==
|
|
2102
|
+
typeof role !== "string" ||
|
|
1847
2103
|
!ROLE_ID_PATTERN.test(role) ||
|
|
1848
2104
|
role === RESERVED_CAPTAIN_ROLE_ID ||
|
|
1849
2105
|
!required.has(role),
|
|
@@ -1867,11 +2123,11 @@ function assertExactRoleBindings(id, configured, required) {
|
|
|
1867
2123
|
if (missing.length > 0 || extra.length > 0) {
|
|
1868
2124
|
throw new Error(
|
|
1869
2125
|
`playbooks.${id}.roles must exactly cover requiredRoleIds` +
|
|
1870
|
-
`${missing.length === 0 ?
|
|
2126
|
+
`${missing.length === 0 ? "" : `; missing ${missing.map(JSON.stringify).join(", ")}`}` +
|
|
1871
2127
|
`${
|
|
1872
2128
|
extra.length === 0
|
|
1873
|
-
?
|
|
1874
|
-
: `; extra ${extra.map(JSON.stringify).join(
|
|
2129
|
+
? ""
|
|
2130
|
+
: `; extra ${extra.map(JSON.stringify).join(", ")}`
|
|
1875
2131
|
}`,
|
|
1876
2132
|
);
|
|
1877
2133
|
}
|
|
@@ -1883,7 +2139,7 @@ function assertConcurrentPlayers(id, sets, roles) {
|
|
|
1883
2139
|
if (new Set(playerIds).size !== playerIds.length) {
|
|
1884
2140
|
throw new Error(
|
|
1885
2141
|
`playbooks.${id}.concurrentRoleSets ${JSON.stringify(set)} must bind ` +
|
|
1886
|
-
|
|
2142
|
+
"to pairwise-distinct player ids",
|
|
1887
2143
|
);
|
|
1888
2144
|
}
|
|
1889
2145
|
}
|
|
@@ -1896,7 +2152,7 @@ function distinct(values) {
|
|
|
1896
2152
|
function findLegacyPlayersPath(top) {
|
|
1897
2153
|
const playbooks = isObject(top?.playbooks) ? top.playbooks : {};
|
|
1898
2154
|
for (const [id, block] of Object.entries(playbooks)) {
|
|
1899
|
-
if (isObject(block) && hasOwn(block,
|
|
2155
|
+
if (isObject(block) && hasOwn(block, "players")) {
|
|
1900
2156
|
return `playbooks.${id}.players`;
|
|
1901
2157
|
}
|
|
1902
2158
|
}
|
|
@@ -1904,27 +2160,31 @@ function findLegacyPlayersPath(top) {
|
|
|
1904
2160
|
}
|
|
1905
2161
|
|
|
1906
2162
|
function legacyPlayersError(path, configPath) {
|
|
1907
|
-
const where = configPath ? ` in ${configPath}` :
|
|
2163
|
+
const where = configPath ? ` in ${configPath}` : "";
|
|
1908
2164
|
const error = new Error(
|
|
1909
2165
|
`${path} was removed in the explicit-session-player major release${where}: ` +
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
2166
|
+
"define stable ids in top-level players and bind them explicitly under " +
|
|
2167
|
+
"playbooks.<id>.roles; automatic migration would choose which prior " +
|
|
2168
|
+
"conversations share a session",
|
|
1913
2169
|
);
|
|
1914
|
-
error.code =
|
|
2170
|
+
error.code = "PLAYBOOK_LEGACY_PLAYERS";
|
|
1915
2171
|
error.legacyPath = path;
|
|
1916
2172
|
return error;
|
|
1917
2173
|
}
|
|
1918
2174
|
|
|
1919
2175
|
function cloneJson(value, path, seen = new Set()) {
|
|
1920
|
-
if (
|
|
2176
|
+
if (
|
|
2177
|
+
value === null ||
|
|
2178
|
+
typeof value === "string" ||
|
|
2179
|
+
typeof value === "boolean"
|
|
2180
|
+
) {
|
|
1921
2181
|
return value;
|
|
1922
2182
|
}
|
|
1923
|
-
if (typeof value ===
|
|
2183
|
+
if (typeof value === "number") {
|
|
1924
2184
|
if (Number.isFinite(value)) return value;
|
|
1925
2185
|
throw new Error(`${path} must contain only finite JSON numbers`);
|
|
1926
2186
|
}
|
|
1927
|
-
if (typeof value !==
|
|
2187
|
+
if (typeof value !== "object") {
|
|
1928
2188
|
throw new Error(`${path} must contain only JSON values`);
|
|
1929
2189
|
}
|
|
1930
2190
|
if (seen.has(value)) throw new Error(`${path} must not contain a cycle`);
|
|
@@ -1935,8 +2195,8 @@ function cloneJson(value, path, seen = new Set()) {
|
|
|
1935
2195
|
if (
|
|
1936
2196
|
keys.some(
|
|
1937
2197
|
(key) =>
|
|
1938
|
-
typeof key ===
|
|
1939
|
-
(key !==
|
|
2198
|
+
typeof key === "symbol" ||
|
|
2199
|
+
(key !== "length" &&
|
|
1940
2200
|
(!/^(?:0|[1-9]\d*)$/.test(key) || Number(key) >= value.length)),
|
|
1941
2201
|
)
|
|
1942
2202
|
) {
|
|
@@ -1958,9 +2218,7 @@ function cloneJson(value, path, seen = new Set()) {
|
|
|
1958
2218
|
`${path}[${index}] must be an enumerable data property`,
|
|
1959
2219
|
);
|
|
1960
2220
|
}
|
|
1961
|
-
cloned.push(
|
|
1962
|
-
cloneJson(descriptor.value, `${path}[${index}]`, seen),
|
|
1963
|
-
);
|
|
2221
|
+
cloned.push(cloneJson(descriptor.value, `${path}[${index}]`, seen));
|
|
1964
2222
|
}
|
|
1965
2223
|
return cloned;
|
|
1966
2224
|
}
|
|
@@ -1969,7 +2227,7 @@ function cloneJson(value, path, seen = new Set()) {
|
|
|
1969
2227
|
throw new Error(`${path} must contain only plain JSON objects`);
|
|
1970
2228
|
}
|
|
1971
2229
|
const keys = Reflect.ownKeys(value);
|
|
1972
|
-
if (keys.some((key) => typeof key ===
|
|
2230
|
+
if (keys.some((key) => typeof key === "symbol")) {
|
|
1973
2231
|
throw new Error(`${path} must not contain symbol keys`);
|
|
1974
2232
|
}
|
|
1975
2233
|
const descriptors = Object.getOwnPropertyDescriptors(value);
|
|
@@ -1995,7 +2253,7 @@ function cloneJson(value, path, seen = new Set()) {
|
|
|
1995
2253
|
}
|
|
1996
2254
|
|
|
1997
2255
|
function deepFreeze(value) {
|
|
1998
|
-
if (value === null || typeof value !==
|
|
2256
|
+
if (value === null || typeof value !== "object" || Object.isFrozen(value)) {
|
|
1999
2257
|
return value;
|
|
2000
2258
|
}
|
|
2001
2259
|
for (const child of Object.values(value)) deepFreeze(child);
|
|
@@ -2003,7 +2261,7 @@ function deepFreeze(value) {
|
|
|
2003
2261
|
}
|
|
2004
2262
|
|
|
2005
2263
|
function isObject(value) {
|
|
2006
|
-
return typeof value ===
|
|
2264
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2007
2265
|
}
|
|
2008
2266
|
|
|
2009
2267
|
function isPlainObject(value) {
|
|
@@ -2022,9 +2280,9 @@ function requireObject(value, path) {
|
|
|
2022
2280
|
}
|
|
2023
2281
|
|
|
2024
2282
|
function formatKeyList(keys) {
|
|
2025
|
-
return `${keys.length === 1 ?
|
|
2283
|
+
return `${keys.length === 1 ? "key" : "keys"} ${keys
|
|
2026
2284
|
.map((key) => JSON.stringify(key))
|
|
2027
|
-
.join(
|
|
2285
|
+
.join(", ")}`;
|
|
2028
2286
|
}
|
|
2029
2287
|
|
|
2030
2288
|
function errorMessage(error) {
|