@sublang/playbook 9.0.0 → 11.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/docs/cli.md +109 -21
- package/docs/configuration.md +89 -40
- package/docs/embedding.md +126 -12
- package/package.json +14 -3
- package/reference/sdlc/captain.md +14 -10
- package/reference/sdlc/captain.playbook/captain.fsm.d.ts +33 -13
- package/reference/sdlc/captain.playbook/captain.fsm.js +80 -9
- package/reference/sdlc/captain.playbook/captain.fsm.ts +137 -18
- package/reference/sdlc/captain.playbook/captain.gears.md +10 -6
- package/reference/sdlc/captain.playbook/captain.playbook.d.ts +5 -1
- package/reference/sdlc/captain.playbook/captain.playbook.js +140 -10
- package/reference/sdlc/captain.playbook/captain.playbook.ts +188 -16
- package/reference/sdlc/code.md +35 -16
- package/reference/sdlc/code.playbook/bin/interactive-session.js +228 -23
- package/reference/sdlc/code.playbook/bin/launch-config.js +611 -221
- package/reference/sdlc/code.playbook/bin/playbook.js +304 -178
- package/reference/sdlc/code.playbook/bin/replay-observer.js +221 -0
- package/reference/sdlc/code.playbook/bin/repository-effects.js +2930 -0
- package/reference/sdlc/code.playbook/bin/run.js +669 -215
- package/reference/sdlc/code.playbook/bin/session-store.js +4546 -502
- package/reference/sdlc/code.playbook/code.fsm.d.ts +7 -0
- package/reference/sdlc/code.playbook/code.fsm.js +74 -25
- package/reference/sdlc/code.playbook/code.fsm.ts +83 -29
- package/reference/sdlc/code.playbook/code.gears.md +0 -2
- package/reference/sdlc/code.playbook/code.playbook.d.ts +5 -2
- package/reference/sdlc/code.playbook/code.playbook.js +54 -2
- package/reference/sdlc/code.playbook/code.playbook.ts +75 -6
- package/reference/sdlc/code.playbook/code.registry.d.ts +10 -3
- package/reference/sdlc/code.playbook/code.registry.js +10 -3
- package/reference/sdlc/code.playbook/code.registry.ts +23 -5
- package/reference/sdlc/code.playbook/playbook-captain.d.ts +103 -8
- package/reference/sdlc/code.playbook/playbook-captain.js +1871 -75
- package/reference/sdlc/code.playbook/playbook-captain.ts +2801 -102
- package/reference/sdlc/code.playbook/playbook.config.template.yaml +14 -10
- package/reference/sdlc/code.playbook/session-store.d.ts +82 -0
- package/reference/sdlc/code.playbook/session-store.js +113 -0
- package/reference/sdlc/decide.md +24 -16
- package/reference/sdlc/decide.playbook/decide.fsm.d.ts +7 -0
- package/reference/sdlc/decide.playbook/decide.fsm.js +80 -29
- package/reference/sdlc/decide.playbook/decide.fsm.ts +89 -31
- package/reference/sdlc/decide.playbook/decide.gears.md +0 -1
- package/reference/sdlc/decide.playbook/decide.playbook.d.ts +13 -5
- package/reference/sdlc/decide.playbook/decide.playbook.js +1712 -91
- package/reference/sdlc/decide.playbook/decide.playbook.ts +2677 -136
- package/reference/sdlc/decide.playbook/decide.registry.d.ts +7 -3
- package/reference/sdlc/decide.playbook/decide.registry.js +10 -3
- package/reference/sdlc/decide.playbook/decide.registry.ts +20 -5
- package/reference/sdlc/review.md +36 -18
- package/reference/sdlc/review.playbook/review.fsm.d.ts +7 -0
- package/reference/sdlc/review.playbook/review.fsm.js +133 -12
- package/reference/sdlc/review.playbook/review.fsm.ts +140 -12
- package/reference/sdlc/review.playbook/review.playbook.d.ts +5 -2
- package/reference/sdlc/review.playbook/review.playbook.js +65 -2
- package/reference/sdlc/review.playbook/review.playbook.ts +83 -6
- package/reference/sdlc/review.playbook/review.registry.d.ts +10 -3
- package/reference/sdlc/review.playbook/review.registry.js +10 -3
- package/reference/sdlc/review.playbook/review.registry.ts +23 -5
- package/slc/gears2fsm.md +6 -5
- package/slc/link.md +544 -41
- package/src/accepted-outcome.d.ts +18 -0
- package/src/accepted-outcome.js +94 -0
- package/src/accepted-outcome.ts +140 -0
- package/src/runtime.d.ts +164 -3
- package/src/runtime.ts +213 -2
- package/src/xstate-playbook-runtime.d.ts +149 -10
- package/src/xstate-playbook-runtime.js +2569 -270
- package/src/xstate-playbook-runtime.ts +4133 -490
- package/src/xstate-runtime.d.ts +59 -1
- package/src/xstate-runtime.js +866 -7
- package/src/xstate-runtime.ts +1397 -7
|
@@ -6,47 +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
|
|
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";
|
|
40
44
|
const PLAYBOOK_TOP_LEVEL_KEYS = new Set([
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
"captain",
|
|
46
|
+
"players",
|
|
47
|
+
"playbooks",
|
|
48
|
+
"layout",
|
|
49
|
+
"notifications",
|
|
50
|
+
"theme",
|
|
51
|
+
"sessions",
|
|
47
52
|
]);
|
|
48
|
-
const RESERVED_CAPTAIN_PLAYBOOK_ID =
|
|
49
|
-
const RESERVED_CAPTAIN_ROLE_ID =
|
|
53
|
+
const RESERVED_CAPTAIN_PLAYBOOK_ID = "captain";
|
|
54
|
+
const RESERVED_CAPTAIN_ROLE_ID = "captain";
|
|
50
55
|
const PLAYER_ID_PATTERN = /^[a-z][a-z0-9_-]*(?:\.[a-z][a-z0-9_-]*)*$/;
|
|
51
56
|
const ROLE_ID_PATTERN = /^[a-z][a-z0-9_-]*$/;
|
|
52
57
|
|
|
@@ -57,16 +62,16 @@ export function extractWithFlags(argv) {
|
|
|
57
62
|
const rest = [];
|
|
58
63
|
for (let i = 0; i < argv.length; i += 1) {
|
|
59
64
|
const arg = argv[i];
|
|
60
|
-
if (arg ===
|
|
65
|
+
if (arg === "--with") {
|
|
61
66
|
const value = argv[i + 1];
|
|
62
|
-
if (value === undefined || value ===
|
|
63
|
-
throw new Error(
|
|
67
|
+
if (value === undefined || value === "") {
|
|
68
|
+
throw new Error("--with needs a value");
|
|
64
69
|
}
|
|
65
70
|
withPaths.push(value);
|
|
66
71
|
i += 1;
|
|
67
|
-
} else if (arg.startsWith(
|
|
68
|
-
const value = arg.slice(
|
|
69
|
-
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");
|
|
70
75
|
withPaths.push(value);
|
|
71
76
|
} else {
|
|
72
77
|
rest.push(arg);
|
|
@@ -80,7 +85,7 @@ export function loadOverlayFragment(overlayPath) {
|
|
|
80
85
|
const resolved = resolve(overlayPath);
|
|
81
86
|
let text;
|
|
82
87
|
try {
|
|
83
|
-
text = readFileSync(resolved,
|
|
88
|
+
text = readFileSync(resolved, "utf8");
|
|
84
89
|
} catch (error) {
|
|
85
90
|
throw new Error(
|
|
86
91
|
`cannot read --with overlay ${overlayPath}: ${errorMessage(error)}`,
|
|
@@ -121,36 +126,113 @@ export function mergeConfigs(base, overlay) {
|
|
|
121
126
|
export function mergeSelectedConfigs(base, overlay, selectedMembers) {
|
|
122
127
|
const selected = validateSelectedMembers(selectedMembers);
|
|
123
128
|
return mergeConfigs(
|
|
124
|
-
projectSelectedLayer(base, selected,
|
|
125
|
-
projectSelectedLayer(overlay, selected,
|
|
129
|
+
projectSelectedLayer(base, selected, "config"),
|
|
130
|
+
projectSelectedLayer(overlay, selected, "overlay"),
|
|
126
131
|
);
|
|
127
132
|
}
|
|
128
133
|
|
|
129
|
-
|
|
130
|
-
|
|
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");
|
|
131
146
|
}
|
|
132
147
|
|
|
133
148
|
export function resolveUserConfigPath(env = process.env, home = homedir()) {
|
|
134
|
-
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);
|
|
135
214
|
}
|
|
136
215
|
|
|
137
216
|
// PBCLI-46: configured filesystem modules are anchored once to the primary
|
|
138
217
|
// config, including paths introduced by overlays. Bare/custom specifiers and
|
|
139
218
|
// already-authored file URLs retain their module semantics.
|
|
140
219
|
export function canonicalizeRegistrySpecifier(from, configPath) {
|
|
141
|
-
if (configPath === undefined || from.startsWith(
|
|
142
|
-
if (
|
|
143
|
-
isAbsolute(from) ||
|
|
144
|
-
from.startsWith('./') ||
|
|
145
|
-
from.startsWith('../') ||
|
|
146
|
-
from.startsWith('.\\') ||
|
|
147
|
-
from.startsWith('..\\')
|
|
148
|
-
) {
|
|
220
|
+
if (configPath === undefined || from.startsWith("file:")) return from;
|
|
221
|
+
if (isAbsolute(from) || isRelativeFilesystemRegistrySpecifier(from)) {
|
|
149
222
|
return pathToFileURL(resolve(dirname(configPath), from)).href;
|
|
150
223
|
}
|
|
151
224
|
return from;
|
|
152
225
|
}
|
|
153
226
|
|
|
227
|
+
function isRelativeFilesystemRegistrySpecifier(from) {
|
|
228
|
+
return (
|
|
229
|
+
from.startsWith("./") ||
|
|
230
|
+
from.startsWith("../") ||
|
|
231
|
+
from.startsWith(".\\") ||
|
|
232
|
+
from.startsWith("..\\")
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
154
236
|
// PBCLI-46: seed, migrate, overlay, validate, and normalize through one path.
|
|
155
237
|
// `prepareRegistryModule` is the single provision-before-import seam used by
|
|
156
238
|
// both front ends, so filesystem registry handling cannot drift by presenter.
|
|
@@ -170,7 +252,7 @@ export async function loadLaunchPlan({
|
|
|
170
252
|
migrateUserConfigIfRetired(userConfigPath, onNotice);
|
|
171
253
|
}
|
|
172
254
|
|
|
173
|
-
let top = parseYaml(readFileSync(userConfigPath,
|
|
255
|
+
let top = parseYaml(readFileSync(userConfigPath, "utf8")) ?? {};
|
|
174
256
|
if (overlayPaths.length > 0 && !isObject(top)) {
|
|
175
257
|
throw new Error(
|
|
176
258
|
`the top-level config at ${userConfigPath} must be a YAML map before --with can overlay it`,
|
|
@@ -207,7 +289,7 @@ export async function loadSelectedLaunchPlanDataOnly({
|
|
|
207
289
|
const selectedMembers = selectedMembersFromStoredStructure(stored);
|
|
208
290
|
seedUserConfigIfMissing(userConfigPath, templatePath, onNotice);
|
|
209
291
|
|
|
210
|
-
let top = parseYaml(readFileSync(userConfigPath,
|
|
292
|
+
let top = parseYaml(readFileSync(userConfigPath, "utf8")) ?? {};
|
|
211
293
|
if (overlayPaths.length > 0 && !isObject(top)) {
|
|
212
294
|
throw new Error(
|
|
213
295
|
`the top-level config at ${userConfigPath} must be a YAML map before --with can overlay it`,
|
|
@@ -216,7 +298,7 @@ export async function loadSelectedLaunchPlanDataOnly({
|
|
|
216
298
|
top = projectSelectedLayer(
|
|
217
299
|
top,
|
|
218
300
|
validateSelectedMembers(selectedMembers),
|
|
219
|
-
|
|
301
|
+
"config",
|
|
220
302
|
);
|
|
221
303
|
for (const overlayPath of overlayPaths) {
|
|
222
304
|
top = mergeSelectedConfigs(
|
|
@@ -245,17 +327,17 @@ export async function normalizeSelectedLaunchPlanDataOnly(
|
|
|
245
327
|
!isDeepStrictEqual(supplied.playerIds, expectedMembers.playerIds)
|
|
246
328
|
) {
|
|
247
329
|
throw new Error(
|
|
248
|
-
|
|
330
|
+
"selected launch members do not match the stored structural projection",
|
|
249
331
|
);
|
|
250
332
|
}
|
|
251
333
|
}
|
|
252
334
|
top = projectSelectedMembers(top, expectedMembers);
|
|
253
|
-
top = cloneJson(top,
|
|
335
|
+
top = cloneJson(top, "config");
|
|
254
336
|
assertNoRetiredProfiles(top, configPath);
|
|
255
|
-
if (hasOwn(top,
|
|
337
|
+
if (hasOwn(top, "run")) {
|
|
256
338
|
throw new Error(
|
|
257
339
|
'top-level "run" was removed: configure the shared Captain under ' +
|
|
258
|
-
|
|
340
|
+
"captain, top-level players, and playbooks.<id>.roles instead",
|
|
259
341
|
);
|
|
260
342
|
}
|
|
261
343
|
const unknownTopLevel = Object.keys(top).filter(
|
|
@@ -267,32 +349,33 @@ export async function normalizeSelectedLaunchPlanDataOnly(
|
|
|
267
349
|
);
|
|
268
350
|
}
|
|
269
351
|
if (top.layout !== undefined && !isObject(top.layout)) {
|
|
270
|
-
throw new Error(
|
|
352
|
+
throw new Error("layout must be a map");
|
|
271
353
|
}
|
|
272
354
|
|
|
273
|
-
const playersCfg = requireObject(top.players,
|
|
355
|
+
const playersCfg = requireObject(top.players, "players");
|
|
274
356
|
const configuredAgents = new Map();
|
|
275
357
|
for (const playerId of expectedMembers.playerIds) {
|
|
276
358
|
assertPlayerId(playerId, `players.${playerId}`);
|
|
277
359
|
const agent = resolveAgent(playersCfg[playerId], `players.${playerId}`, [
|
|
278
|
-
|
|
360
|
+
"id",
|
|
279
361
|
]);
|
|
280
362
|
configuredAgents.set(playerId, agent);
|
|
281
363
|
}
|
|
282
|
-
let captain = resolveAgent(top.captain,
|
|
364
|
+
let captain = resolveAgent(top.captain, "captain", ["from", "options"]);
|
|
283
365
|
|
|
284
|
-
const playbooksCfg = requireObject(top.playbooks,
|
|
366
|
+
const playbooksCfg = requireObject(top.playbooks, "playbooks");
|
|
285
367
|
const tuningChecks = [];
|
|
286
368
|
let tuningCheckIndex = 0;
|
|
287
369
|
const authored = new Map();
|
|
288
370
|
for (const id of expectedMembers.playbookIds) {
|
|
289
371
|
const storedItem = stored.catalog[id];
|
|
290
372
|
const block = requireObject(playbooksCfg[id], `playbooks.${id}`);
|
|
291
|
-
if (hasOwn(block,
|
|
373
|
+
if (hasOwn(block, "players")) {
|
|
292
374
|
throw legacyPlayersError(`playbooks.${id}.players`, configPath);
|
|
293
375
|
}
|
|
376
|
+
rejectConfiguredHostCapabilities(block, `playbooks.${id}`);
|
|
294
377
|
if (
|
|
295
|
-
typeof block.from !==
|
|
378
|
+
typeof block.from !== "string" ||
|
|
296
379
|
block.from.trim().length === 0 ||
|
|
297
380
|
block.from !== block.from.trim()
|
|
298
381
|
) {
|
|
@@ -300,7 +383,10 @@ export async function normalizeSelectedLaunchPlanDataOnly(
|
|
|
300
383
|
`playbooks.${id}.from must be a canonical trimmed module specifier`,
|
|
301
384
|
);
|
|
302
385
|
}
|
|
303
|
-
const configuredFrom = canonicalizeRegistrySpecifier(
|
|
386
|
+
const configuredFrom = canonicalizeRegistrySpecifier(
|
|
387
|
+
block.from,
|
|
388
|
+
configPath,
|
|
389
|
+
);
|
|
304
390
|
if (configuredFrom !== storedItem.from) {
|
|
305
391
|
throw new Error(
|
|
306
392
|
`playbooks.${id}.from changed from the stored structural projection`,
|
|
@@ -330,7 +416,11 @@ export async function normalizeSelectedLaunchPlanDataOnly(
|
|
|
330
416
|
);
|
|
331
417
|
}
|
|
332
418
|
bindings[role] = binding;
|
|
333
|
-
if (
|
|
419
|
+
if (
|
|
420
|
+
binding.model !== undefined ||
|
|
421
|
+
binding.effort !== undefined ||
|
|
422
|
+
binding.fastMode !== undefined
|
|
423
|
+
) {
|
|
334
424
|
let checkId;
|
|
335
425
|
do {
|
|
336
426
|
checkId = `binding-check-${tuningCheckIndex}`;
|
|
@@ -400,9 +490,12 @@ export async function normalizeSelectedLaunchPlanDataOnly(
|
|
|
400
490
|
layout: { ...provisional.layout, initialVisible: [] },
|
|
401
491
|
})
|
|
402
492
|
: normalizedHost;
|
|
403
|
-
const {
|
|
404
|
-
|
|
405
|
-
|
|
493
|
+
const {
|
|
494
|
+
from: _captainFrom,
|
|
495
|
+
options: _captainOptions,
|
|
496
|
+
...normalizedCaptain
|
|
497
|
+
} = normalizedHost.captain;
|
|
498
|
+
captain = sessionAgentFromHostAgent(normalizedCaptain, "captain");
|
|
406
499
|
const hostAgents = new Map(
|
|
407
500
|
normalizedHost.players.map(({ id, ...agent }) => [id, agent]),
|
|
408
501
|
);
|
|
@@ -437,6 +530,11 @@ export async function normalizeSelectedLaunchPlanDataOnly(
|
|
|
437
530
|
binding.effort === undefined
|
|
438
531
|
? agent.effort
|
|
439
532
|
: overrideTuningSelection(binding.effort),
|
|
533
|
+
...(binding.fastMode === undefined
|
|
534
|
+
? agent.fastMode === undefined
|
|
535
|
+
? {}
|
|
536
|
+
: { fastMode: agent.fastMode }
|
|
537
|
+
: { fastMode: binding.fastMode }),
|
|
440
538
|
},
|
|
441
539
|
];
|
|
442
540
|
}),
|
|
@@ -469,7 +567,7 @@ export async function normalizeSelectedLaunchPlanDataOnly(
|
|
|
469
567
|
};
|
|
470
568
|
if (!isDeepStrictEqual(candidateStructure, stored)) {
|
|
471
569
|
throw new Error(
|
|
472
|
-
|
|
570
|
+
"current selected config does not reproduce the stored structural projection",
|
|
473
571
|
);
|
|
474
572
|
}
|
|
475
573
|
|
|
@@ -494,7 +592,7 @@ export async function normalizeSelectedLaunchPlanDataOnly(
|
|
|
494
592
|
: { theme: normalizedPresentationHost.theme }),
|
|
495
593
|
},
|
|
496
594
|
},
|
|
497
|
-
|
|
595
|
+
"selected launch config",
|
|
498
596
|
),
|
|
499
597
|
);
|
|
500
598
|
}
|
|
@@ -502,7 +600,7 @@ export async function normalizeSelectedLaunchPlanDataOnly(
|
|
|
502
600
|
// PBCLI-8 (DR-021): scalar agents are adapter shorthands and full blocks
|
|
503
601
|
// carry their own settings without profile indirection.
|
|
504
602
|
export function resolveAgent(value, path, reservedKeys = []) {
|
|
505
|
-
if (typeof value ===
|
|
603
|
+
if (typeof value === "string") {
|
|
506
604
|
if (value.trim().length === 0) {
|
|
507
605
|
throw new Error(`${path} must name an adapter`);
|
|
508
606
|
}
|
|
@@ -519,6 +617,19 @@ export function resolveAgent(value, path, reservedKeys = []) {
|
|
|
519
617
|
throw new Error(`${path} must be an adapter shorthand or an agent block`);
|
|
520
618
|
}
|
|
521
619
|
|
|
620
|
+
function rejectConfiguredHostCapabilities(value, path) {
|
|
621
|
+
if (
|
|
622
|
+
value !== null &&
|
|
623
|
+
typeof value === "object" &&
|
|
624
|
+
!Array.isArray(value) &&
|
|
625
|
+
hasOwn(value, HOST_CAPABILITIES_OPTION_KEY)
|
|
626
|
+
) {
|
|
627
|
+
throw new Error(
|
|
628
|
+
`${path}.${HOST_CAPABILITIES_OPTION_KEY} is host-owned and cannot be configured`,
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
522
633
|
// PBCLI-46: normalize into a detached, deeply frozen JSON plan. The plan has
|
|
523
634
|
// only execution data and presentation data; imported registry functions are
|
|
524
635
|
// consulted for validation and then discarded.
|
|
@@ -528,12 +639,12 @@ export async function normalizeLaunchPlan(
|
|
|
528
639
|
) {
|
|
529
640
|
const importModule = loadModule ?? ((specifier) => import(specifier));
|
|
530
641
|
top = projectSelectedMembers(top, selectedMembers);
|
|
531
|
-
top = cloneJson(top,
|
|
642
|
+
top = cloneJson(top, "config");
|
|
532
643
|
assertNoRetiredProfiles(top, configPath);
|
|
533
|
-
if (hasOwn(top,
|
|
644
|
+
if (hasOwn(top, "run")) {
|
|
534
645
|
throw new Error(
|
|
535
646
|
'top-level "run" was removed: configure the shared Captain under ' +
|
|
536
|
-
|
|
647
|
+
"captain, top-level players, and playbooks.<id>.roles instead",
|
|
537
648
|
);
|
|
538
649
|
}
|
|
539
650
|
const unknownTopLevel = Object.keys(top).filter(
|
|
@@ -545,19 +656,19 @@ export async function normalizeLaunchPlan(
|
|
|
545
656
|
);
|
|
546
657
|
}
|
|
547
658
|
if (top.layout !== undefined && !isObject(top.layout)) {
|
|
548
|
-
throw new Error(
|
|
659
|
+
throw new Error("layout must be a map");
|
|
549
660
|
}
|
|
550
661
|
|
|
551
|
-
const playersCfg = requireObject(top.players,
|
|
662
|
+
const playersCfg = requireObject(top.players, "players");
|
|
552
663
|
const allPlayerIds = Object.keys(playersCfg);
|
|
553
664
|
const configuredAgents = new Map();
|
|
554
665
|
for (const playerId of allPlayerIds) {
|
|
555
666
|
assertPlayerId(playerId, `players.${playerId}`);
|
|
556
667
|
const agent = resolveAgent(playersCfg[playerId], `players.${playerId}`, [
|
|
557
|
-
|
|
668
|
+
"id",
|
|
558
669
|
]);
|
|
559
670
|
if (
|
|
560
|
-
typeof agent.adapter !==
|
|
671
|
+
typeof agent.adapter !== "string" ||
|
|
561
672
|
agent.adapter.trim().length === 0
|
|
562
673
|
) {
|
|
563
674
|
throw new Error(`players.${playerId} must resolve an adapter`);
|
|
@@ -565,21 +676,21 @@ export async function normalizeLaunchPlan(
|
|
|
565
676
|
configuredAgents.set(playerId, agent);
|
|
566
677
|
}
|
|
567
678
|
|
|
568
|
-
const playbooksCfg = requireObject(top.playbooks,
|
|
679
|
+
const playbooksCfg = requireObject(top.playbooks, "playbooks");
|
|
569
680
|
const ids = Object.keys(playbooksCfg);
|
|
570
681
|
if (ids.length === 0) {
|
|
571
|
-
throw new Error(
|
|
682
|
+
throw new Error("playbooks must enable at least one playbook");
|
|
572
683
|
}
|
|
573
684
|
if (ids.some((id) => id.trim().length === 0 || id !== id.trim())) {
|
|
574
|
-
throw new Error(
|
|
685
|
+
throw new Error("playbooks keys must be canonical trimmed nonblank ids");
|
|
575
686
|
}
|
|
576
687
|
|
|
577
|
-
let captain = resolveAgent(top.captain,
|
|
688
|
+
let captain = resolveAgent(top.captain, "captain", ["from", "options"]);
|
|
578
689
|
if (
|
|
579
|
-
typeof captain.adapter !==
|
|
690
|
+
typeof captain.adapter !== "string" ||
|
|
580
691
|
captain.adapter.trim().length === 0
|
|
581
692
|
) {
|
|
582
|
-
throw new Error(
|
|
693
|
+
throw new Error("captain must resolve an adapter");
|
|
583
694
|
}
|
|
584
695
|
|
|
585
696
|
// Validate and detach every retained config-owned value before provisioning
|
|
@@ -594,12 +705,13 @@ export async function normalizeLaunchPlan(
|
|
|
594
705
|
);
|
|
595
706
|
}
|
|
596
707
|
const block = requireObject(playbooksCfg[id], `playbooks.${id}`);
|
|
597
|
-
if (hasOwn(block,
|
|
708
|
+
if (hasOwn(block, "players")) {
|
|
598
709
|
throw legacyPlayersError(`playbooks.${id}.players`, configPath);
|
|
599
710
|
}
|
|
711
|
+
rejectConfiguredHostCapabilities(block, `playbooks.${id}`);
|
|
600
712
|
const from = block.from;
|
|
601
713
|
if (
|
|
602
|
-
typeof from !==
|
|
714
|
+
typeof from !== "string" ||
|
|
603
715
|
from.trim().length === 0 ||
|
|
604
716
|
from !== from.trim()
|
|
605
717
|
) {
|
|
@@ -609,7 +721,7 @@ export async function normalizeLaunchPlan(
|
|
|
609
721
|
}
|
|
610
722
|
if (
|
|
611
723
|
block.command !== undefined &&
|
|
612
|
-
(typeof block.command !==
|
|
724
|
+
(typeof block.command !== "string" ||
|
|
613
725
|
block.command.trim().length === 0 ||
|
|
614
726
|
block.command !== block.command.trim())
|
|
615
727
|
) {
|
|
@@ -631,7 +743,7 @@ export async function normalizeLaunchPlan(
|
|
|
631
743
|
throw new Error(
|
|
632
744
|
`playbooks.${id}.roles.${RESERVED_CAPTAIN_ROLE_ID} binds local ` +
|
|
633
745
|
`role "${RESERVED_CAPTAIN_ROLE_ID}", which is reserved for the ` +
|
|
634
|
-
|
|
746
|
+
"tmux-play Captain",
|
|
635
747
|
);
|
|
636
748
|
}
|
|
637
749
|
const bindings = Object.create(null);
|
|
@@ -649,7 +761,11 @@ export async function normalizeLaunchPlan(
|
|
|
649
761
|
);
|
|
650
762
|
}
|
|
651
763
|
bindings[role] = binding;
|
|
652
|
-
if (
|
|
764
|
+
if (
|
|
765
|
+
binding.model !== undefined ||
|
|
766
|
+
binding.effort !== undefined ||
|
|
767
|
+
binding.fastMode !== undefined
|
|
768
|
+
) {
|
|
653
769
|
let checkId;
|
|
654
770
|
do {
|
|
655
771
|
checkId = `binding-check-${tuningCheckIndex}`;
|
|
@@ -735,9 +851,12 @@ export async function normalizeLaunchPlan(
|
|
|
735
851
|
layout: { ...provisional.layout, initialVisible: [] },
|
|
736
852
|
})
|
|
737
853
|
: normalizedHost;
|
|
738
|
-
const {
|
|
739
|
-
|
|
740
|
-
|
|
854
|
+
const {
|
|
855
|
+
from: _captainFrom,
|
|
856
|
+
options: _captainOptions,
|
|
857
|
+
...normalizedCaptain
|
|
858
|
+
} = normalizedHost.captain;
|
|
859
|
+
captain = sessionAgentFromHostAgent(normalizedCaptain, "captain");
|
|
741
860
|
const hostAgents = new Map(
|
|
742
861
|
normalizedHost.players.map(({ id, ...agent }) => [id, agent]),
|
|
743
862
|
);
|
|
@@ -770,7 +889,7 @@ export async function normalizeLaunchPlan(
|
|
|
770
889
|
);
|
|
771
890
|
}
|
|
772
891
|
if (
|
|
773
|
-
typeof preparedFrom !==
|
|
892
|
+
typeof preparedFrom !== "string" ||
|
|
774
893
|
preparedFrom.trim().length === 0
|
|
775
894
|
) {
|
|
776
895
|
throw new Error(
|
|
@@ -806,7 +925,7 @@ export async function normalizeLaunchPlan(
|
|
|
806
925
|
`playbooks.${id}.from "${from}" failed to import: ${errorMessage(cause)}`,
|
|
807
926
|
);
|
|
808
927
|
}
|
|
809
|
-
const entry = mod?.default;
|
|
928
|
+
const entry = snapshotRegistryEntry(mod?.default);
|
|
810
929
|
const registryProblem = invalidRegistryEntryReason(entry);
|
|
811
930
|
if (registryProblem !== undefined) {
|
|
812
931
|
throw new Error(
|
|
@@ -852,6 +971,11 @@ export async function normalizeLaunchPlan(
|
|
|
852
971
|
binding.effort === undefined
|
|
853
972
|
? agent.effort
|
|
854
973
|
: overrideTuningSelection(binding.effort),
|
|
974
|
+
...(binding.fastMode === undefined
|
|
975
|
+
? agent.fastMode === undefined
|
|
976
|
+
? {}
|
|
977
|
+
: { fastMode: agent.fastMode }
|
|
978
|
+
: { fastMode: binding.fastMode }),
|
|
855
979
|
},
|
|
856
980
|
];
|
|
857
981
|
}),
|
|
@@ -921,7 +1045,7 @@ export async function normalizeLaunchPlan(
|
|
|
921
1045
|
catalog: Object.fromEntries(catalogEntries),
|
|
922
1046
|
presentation,
|
|
923
1047
|
},
|
|
924
|
-
|
|
1048
|
+
"launch config",
|
|
925
1049
|
),
|
|
926
1050
|
);
|
|
927
1051
|
}
|
|
@@ -930,8 +1054,8 @@ export async function normalizeLaunchPlan(
|
|
|
930
1054
|
// generic launch planning tracks the installed host schema without importing
|
|
931
1055
|
// or duplicating cligent's private validators.
|
|
932
1056
|
export async function normalizeHostConfig(config) {
|
|
933
|
-
const dir = mkdtempSync(join(tmpdir(),
|
|
934
|
-
const path = join(dir,
|
|
1057
|
+
const dir = mkdtempSync(join(tmpdir(), "playbook-host-config-"));
|
|
1058
|
+
const path = join(dir, "tmux-play.config.yaml");
|
|
935
1059
|
try {
|
|
936
1060
|
writeFileSync(path, stringifyYaml(config));
|
|
937
1061
|
return (await loadTmuxPlayConfig({ configPath: path })).config;
|
|
@@ -955,13 +1079,13 @@ export function projectTmuxConfig(plan) {
|
|
|
955
1079
|
]),
|
|
956
1080
|
);
|
|
957
1081
|
const captain = {
|
|
958
|
-
...projectHostAgent(plan.captain,
|
|
1082
|
+
...projectHostAgent(plan.captain, "captain"),
|
|
959
1083
|
from: PLAYBOOK_CAPTAIN_MODULE,
|
|
960
1084
|
};
|
|
961
1085
|
captain.options = {
|
|
962
1086
|
playbooks,
|
|
963
1087
|
sessionAgents: {
|
|
964
|
-
captain: cloneJson(plan.captain,
|
|
1088
|
+
captain: cloneJson(plan.captain, "captain"),
|
|
965
1089
|
players: Object.fromEntries(
|
|
966
1090
|
plan.players.map(({ id, agent }) => [
|
|
967
1091
|
id,
|
|
@@ -969,7 +1093,7 @@ export function projectTmuxConfig(plan) {
|
|
|
969
1093
|
]),
|
|
970
1094
|
),
|
|
971
1095
|
},
|
|
972
|
-
...(typeof captain.adapter ===
|
|
1096
|
+
...(typeof captain.adapter === "string" && captain.adapter.length > 0
|
|
973
1097
|
? { captainAdapter: captain.adapter }
|
|
974
1098
|
: {}),
|
|
975
1099
|
};
|
|
@@ -981,20 +1105,20 @@ export function projectTmuxConfig(plan) {
|
|
|
981
1105
|
})),
|
|
982
1106
|
layout: projectHostLayout(plan.presentation.layout),
|
|
983
1107
|
};
|
|
984
|
-
if (hasOwn(plan.presentation,
|
|
1108
|
+
if (hasOwn(plan.presentation, "notifications")) {
|
|
985
1109
|
config.notifications = cloneJson(
|
|
986
1110
|
plan.presentation.notifications,
|
|
987
|
-
|
|
1111
|
+
"presentation.notifications",
|
|
988
1112
|
);
|
|
989
1113
|
}
|
|
990
|
-
if (hasOwn(plan.presentation,
|
|
991
|
-
config.theme = cloneJson(plan.presentation.theme,
|
|
1114
|
+
if (hasOwn(plan.presentation, "theme")) {
|
|
1115
|
+
config.theme = cloneJson(plan.presentation.theme, "presentation.theme");
|
|
992
1116
|
}
|
|
993
1117
|
return config;
|
|
994
1118
|
}
|
|
995
1119
|
|
|
996
1120
|
function projectHostLayout(layout) {
|
|
997
|
-
const projected = cloneJson(layout,
|
|
1121
|
+
const projected = cloneJson(layout, "presentation.layout");
|
|
998
1122
|
// cligent's normalized runtime shape carries `columnWeights` as the
|
|
999
1123
|
// derived active-shape value alongside both canonical shape fields. The
|
|
1000
1124
|
// authored schema deliberately rejects that alias/canonical combination,
|
|
@@ -1045,14 +1169,14 @@ export function checkReadiness(adapters, env = process.env, home = homedir()) {
|
|
|
1045
1169
|
const failingAdapters = [];
|
|
1046
1170
|
const unknownAdapters = [];
|
|
1047
1171
|
for (const adapter of adapters) {
|
|
1048
|
-
if (adapter ===
|
|
1049
|
-
if (!env.ANTHROPIC_API_KEY && !existsSync(join(home,
|
|
1172
|
+
if (adapter === "claude") {
|
|
1173
|
+
if (!env.ANTHROPIC_API_KEY && !existsSync(join(home, ".claude"))) {
|
|
1050
1174
|
failingAdapters.push(adapter);
|
|
1051
1175
|
}
|
|
1052
1176
|
continue;
|
|
1053
1177
|
}
|
|
1054
|
-
if (adapter ===
|
|
1055
|
-
if (!env.OPENAI_API_KEY && !existsSync(join(home,
|
|
1178
|
+
if (adapter === "codex") {
|
|
1179
|
+
if (!env.OPENAI_API_KEY && !existsSync(join(home, ".codex"))) {
|
|
1056
1180
|
failingAdapters.push(adapter);
|
|
1057
1181
|
}
|
|
1058
1182
|
continue;
|
|
@@ -1071,6 +1195,142 @@ export function deriveLaunchReadiness(
|
|
|
1071
1195
|
return { adapters, ...checkReadiness(adapters, env, home) };
|
|
1072
1196
|
}
|
|
1073
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
|
+
|
|
1074
1334
|
function seedUserConfigIfMissing(userConfigPath, templatePath, onNotice) {
|
|
1075
1335
|
if (existsSync(userConfigPath)) return;
|
|
1076
1336
|
mkdirSync(dirname(userConfigPath), { recursive: true });
|
|
@@ -1082,7 +1342,7 @@ function seedUserConfigIfMissing(userConfigPath, templatePath, onNotice) {
|
|
|
1082
1342
|
function migrateUserConfigIfRetired(userConfigPath, onNotice) {
|
|
1083
1343
|
let text;
|
|
1084
1344
|
try {
|
|
1085
|
-
text = readFileSync(userConfigPath,
|
|
1345
|
+
text = readFileSync(userConfigPath, "utf8");
|
|
1086
1346
|
} catch {
|
|
1087
1347
|
return;
|
|
1088
1348
|
}
|
|
@@ -1090,13 +1350,13 @@ function migrateUserConfigIfRetired(userConfigPath, onNotice) {
|
|
|
1090
1350
|
try {
|
|
1091
1351
|
migrated = migrateRetiredProfiles(text);
|
|
1092
1352
|
} catch (error) {
|
|
1093
|
-
if (error?.code ===
|
|
1353
|
+
if (error?.code === "PLAYBOOK_LEGACY_PLAYERS") {
|
|
1094
1354
|
throw legacyPlayersError(error.legacyPath, userConfigPath);
|
|
1095
1355
|
}
|
|
1096
1356
|
throw new Error(
|
|
1097
1357
|
`cannot migrate the retired profiles config at ${userConfigPath}: ` +
|
|
1098
1358
|
`${errorMessage(error)} — edit it by hand: each agent takes its own ` +
|
|
1099
|
-
|
|
1359
|
+
"adapter, model, effort, fast mode, and permissions",
|
|
1100
1360
|
);
|
|
1101
1361
|
}
|
|
1102
1362
|
if (migrated === undefined) return;
|
|
@@ -1124,33 +1384,33 @@ export function migrateRetiredProfiles(text) {
|
|
|
1124
1384
|
const doc = parseYamlDocument(text);
|
|
1125
1385
|
const contents = doc.contents;
|
|
1126
1386
|
if (!contents || !Array.isArray(contents.items)) return undefined;
|
|
1127
|
-
const playbooks = doc.get(
|
|
1387
|
+
const playbooks = doc.get("playbooks");
|
|
1128
1388
|
if (playbooks && Array.isArray(playbooks.items)) {
|
|
1129
1389
|
for (const entry of playbooks.items) {
|
|
1130
1390
|
const id = String(entry.key);
|
|
1131
|
-
if (doc.getIn([
|
|
1391
|
+
if (doc.getIn(["playbooks", id, "players"]) !== undefined) {
|
|
1132
1392
|
throw legacyPlayersError(`playbooks.${id}.players`);
|
|
1133
1393
|
}
|
|
1134
1394
|
}
|
|
1135
1395
|
}
|
|
1136
|
-
const profiles = doc.get(
|
|
1137
|
-
const agentPaths = [[
|
|
1138
|
-
const players = doc.get(
|
|
1396
|
+
const profiles = doc.get("profiles");
|
|
1397
|
+
const agentPaths = [["captain"]];
|
|
1398
|
+
const players = doc.get("players");
|
|
1139
1399
|
if (players && Array.isArray(players.items)) {
|
|
1140
1400
|
for (const player of players.items) {
|
|
1141
|
-
agentPaths.push([
|
|
1401
|
+
agentPaths.push(["players", String(player.key)]);
|
|
1142
1402
|
}
|
|
1143
1403
|
}
|
|
1144
1404
|
|
|
1145
1405
|
const profileSettings = (name) =>
|
|
1146
|
-
profiles && typeof profiles.get ===
|
|
1406
|
+
profiles && typeof profiles.get === "function"
|
|
1147
1407
|
? profiles.get(name)
|
|
1148
1408
|
: undefined;
|
|
1149
1409
|
|
|
1150
1410
|
let changed = false;
|
|
1151
1411
|
for (const path of agentPaths) {
|
|
1152
1412
|
const node = doc.getIn(path, true);
|
|
1153
|
-
if (node && typeof node.value ===
|
|
1413
|
+
if (node && typeof node.value === "string" && !Array.isArray(node.items)) {
|
|
1154
1414
|
const settings = profileSettings(node.value);
|
|
1155
1415
|
if (settings === undefined) continue;
|
|
1156
1416
|
const inlined = settings.clone();
|
|
@@ -1158,16 +1418,16 @@ export function migrateRetiredProfiles(text) {
|
|
|
1158
1418
|
doc.setIn(path, inlined);
|
|
1159
1419
|
changed = true;
|
|
1160
1420
|
} else if (node && Array.isArray(node.items)) {
|
|
1161
|
-
const named = node.get?.(
|
|
1421
|
+
const named = node.get?.("profile");
|
|
1162
1422
|
if (named === undefined) continue;
|
|
1163
1423
|
const settings = profileSettings(named);
|
|
1164
1424
|
if (settings === undefined) {
|
|
1165
1425
|
throw new Error(
|
|
1166
|
-
`${path.join(
|
|
1167
|
-
|
|
1426
|
+
`${path.join(".")}.profile names "${String(named)}", which no ` +
|
|
1427
|
+
"profiles entry defines",
|
|
1168
1428
|
);
|
|
1169
1429
|
}
|
|
1170
|
-
node.delete(
|
|
1430
|
+
node.delete("profile");
|
|
1171
1431
|
for (const item of settings.items) {
|
|
1172
1432
|
if (node.has(String(item.key))) continue;
|
|
1173
1433
|
node.add(item.clone());
|
|
@@ -1178,11 +1438,11 @@ export function migrateRetiredProfiles(text) {
|
|
|
1178
1438
|
|
|
1179
1439
|
if (profiles !== undefined) {
|
|
1180
1440
|
const index = contents.items.findIndex(
|
|
1181
|
-
(item) => String(item.key) ===
|
|
1441
|
+
(item) => String(item.key) === "profiles",
|
|
1182
1442
|
);
|
|
1183
1443
|
const lead =
|
|
1184
1444
|
index === -1 ? undefined : contents.items[index]?.key?.commentBefore;
|
|
1185
|
-
doc.delete(
|
|
1445
|
+
doc.delete("profiles");
|
|
1186
1446
|
const header = keptHeaderComment(lead);
|
|
1187
1447
|
const next = contents.items[0];
|
|
1188
1448
|
if (header !== undefined && next?.key) {
|
|
@@ -1199,20 +1459,20 @@ export function migrateRetiredProfiles(text) {
|
|
|
1199
1459
|
}
|
|
1200
1460
|
|
|
1201
1461
|
const MIGRATION_NOTE =
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
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.";
|
|
1206
1466
|
|
|
1207
1467
|
function carryScalarComment(node, inlined) {
|
|
1208
1468
|
const parts = [node.commentBefore, node.comment].filter(
|
|
1209
|
-
(part) => typeof part ===
|
|
1469
|
+
(part) => typeof part === "string" && part.trim() !== "",
|
|
1210
1470
|
);
|
|
1211
1471
|
if (parts.length === 0) return;
|
|
1212
1472
|
const first = inlined.items?.[0]?.key;
|
|
1213
1473
|
if (!first) return;
|
|
1214
1474
|
inlined.flow = false;
|
|
1215
|
-
const carried = parts.join(
|
|
1475
|
+
const carried = parts.join("\n");
|
|
1216
1476
|
first.commentBefore =
|
|
1217
1477
|
first.commentBefore === undefined
|
|
1218
1478
|
? carried
|
|
@@ -1220,24 +1480,25 @@ function carryScalarComment(node, inlined) {
|
|
|
1220
1480
|
}
|
|
1221
1481
|
|
|
1222
1482
|
function keptHeaderComment(comment) {
|
|
1223
|
-
if (typeof comment !==
|
|
1224
|
-
const paragraphs = comment.split(
|
|
1225
|
-
const kept = paragraphs.slice(0, -1).join(
|
|
1226
|
-
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;
|
|
1227
1487
|
}
|
|
1228
1488
|
|
|
1229
1489
|
function assertNoRetiredProfiles(top, configPath) {
|
|
1230
|
-
const where = configPath ? ` in ${configPath}` :
|
|
1490
|
+
const where = configPath ? ` in ${configPath}` : "";
|
|
1231
1491
|
if (top.profiles !== undefined) {
|
|
1232
1492
|
throw new Error(
|
|
1233
1493
|
`top-level "profiles" was removed${where}: write each agent's settings ` +
|
|
1234
|
-
|
|
1235
|
-
|
|
1494
|
+
"inline under captain and each top-level players.<player-id> " +
|
|
1495
|
+
"(adapter, model, effort, fast mode, permissions)",
|
|
1236
1496
|
);
|
|
1237
1497
|
}
|
|
1238
1498
|
const legacyPath = findLegacyPlayersPath(top);
|
|
1239
|
-
if (legacyPath !== undefined)
|
|
1240
|
-
|
|
1499
|
+
if (legacyPath !== undefined)
|
|
1500
|
+
throw legacyPlayersError(legacyPath, configPath);
|
|
1501
|
+
const blocks = [["captain", top.captain]];
|
|
1241
1502
|
const playersCfg = isObject(top.players) ? top.players : {};
|
|
1242
1503
|
for (const [playerId, agent] of Object.entries(playersCfg)) {
|
|
1243
1504
|
blocks.push([`players.${playerId}`, agent]);
|
|
@@ -1246,32 +1507,56 @@ function assertNoRetiredProfiles(top, configPath) {
|
|
|
1246
1507
|
if (isObject(block) && block.profile !== undefined) {
|
|
1247
1508
|
throw new Error(
|
|
1248
1509
|
`${path}.profile was removed${where}: write the agent's settings ` +
|
|
1249
|
-
|
|
1510
|
+
"inline in that block (adapter, model, effort, fast mode, permissions)",
|
|
1250
1511
|
);
|
|
1251
1512
|
}
|
|
1252
1513
|
}
|
|
1253
1514
|
}
|
|
1254
1515
|
|
|
1255
|
-
|
|
1256
|
-
|
|
1516
|
+
// Imported manifests remain live JavaScript objects. Capture every member this
|
|
1517
|
+
// host consumes once, then validate and project only the detached record so a
|
|
1518
|
+
// getter or later mutation cannot make those two phases observe different
|
|
1519
|
+
// compatibility declarations or identities.
|
|
1520
|
+
export function snapshotRegistryEntry(value) {
|
|
1521
|
+
if (!isObject(value)) return value;
|
|
1522
|
+
return {
|
|
1523
|
+
id: value.id,
|
|
1524
|
+
command: value.command,
|
|
1525
|
+
intent: value.intent,
|
|
1526
|
+
artifactSchema: value.artifactSchema,
|
|
1527
|
+
runtimeProfile: value.runtimeProfile,
|
|
1528
|
+
requiredRoleIds: value.requiredRoleIds,
|
|
1529
|
+
concurrentRoleSets: value.concurrentRoleSets,
|
|
1530
|
+
validateOptions: value.validateOptions,
|
|
1531
|
+
createRuntime: value.createRuntime,
|
|
1532
|
+
};
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
export function invalidRegistryEntryReason(value) {
|
|
1536
|
+
if (!isObject(value)) return "the default export must be an object";
|
|
1257
1537
|
if (
|
|
1258
|
-
typeof value.id !==
|
|
1538
|
+
typeof value.id !== "string" ||
|
|
1259
1539
|
value.id.trim().length === 0 ||
|
|
1260
1540
|
value.id !== value.id.trim()
|
|
1261
1541
|
) {
|
|
1262
|
-
return
|
|
1542
|
+
return "id must be a canonical trimmed nonblank string";
|
|
1263
1543
|
}
|
|
1264
1544
|
if (
|
|
1265
|
-
typeof value.command !==
|
|
1545
|
+
typeof value.command !== "string" ||
|
|
1266
1546
|
value.command.trim().length === 0 ||
|
|
1267
1547
|
value.command !== value.command.trim()
|
|
1268
1548
|
) {
|
|
1269
|
-
return
|
|
1549
|
+
return "command must be a canonical trimmed nonblank string";
|
|
1270
1550
|
}
|
|
1271
|
-
if (typeof value.intent !==
|
|
1272
|
-
if (value.artifactSchema !==
|
|
1273
|
-
return
|
|
1551
|
+
if (typeof value.intent !== "string") return "intent must be a string";
|
|
1552
|
+
if (value.artifactSchema !== 3) {
|
|
1553
|
+
return "artifactSchema must be 3";
|
|
1274
1554
|
}
|
|
1555
|
+
const runtimeProfileProblem = invalidRuntimeProfileReason(
|
|
1556
|
+
value.runtimeProfile,
|
|
1557
|
+
value.artifactSchema,
|
|
1558
|
+
);
|
|
1559
|
+
if (runtimeProfileProblem !== undefined) return runtimeProfileProblem;
|
|
1275
1560
|
const roleProblem = invalidManifestRoles(value.requiredRoleIds);
|
|
1276
1561
|
if (roleProblem !== undefined) return `requiredRoleIds ${roleProblem}`;
|
|
1277
1562
|
const concurrentProblem = invalidConcurrentRoleSets(
|
|
@@ -1281,33 +1566,114 @@ function invalidRegistryEntryReason(value) {
|
|
|
1281
1566
|
if (concurrentProblem !== undefined) {
|
|
1282
1567
|
return `concurrentRoleSets ${concurrentProblem}`;
|
|
1283
1568
|
}
|
|
1284
|
-
if (typeof value.validateOptions !==
|
|
1285
|
-
return
|
|
1569
|
+
if (typeof value.validateOptions !== "function") {
|
|
1570
|
+
return "validateOptions must be a function";
|
|
1286
1571
|
}
|
|
1287
|
-
if (typeof value.createRuntime !==
|
|
1288
|
-
return
|
|
1572
|
+
if (typeof value.createRuntime !== "function") {
|
|
1573
|
+
return "createRuntime must be a function";
|
|
1289
1574
|
}
|
|
1290
1575
|
return undefined;
|
|
1291
1576
|
}
|
|
1292
1577
|
|
|
1578
|
+
function invalidRuntimeProfileReason(value, advertisedArtifactSchema) {
|
|
1579
|
+
if (!isPlainObject(value)) {
|
|
1580
|
+
return "runtimeProfile must be a plain object";
|
|
1581
|
+
}
|
|
1582
|
+
const kindDescriptor = Object.getOwnPropertyDescriptor(value, "kind");
|
|
1583
|
+
if (
|
|
1584
|
+
kindDescriptor === undefined ||
|
|
1585
|
+
kindDescriptor.get !== undefined ||
|
|
1586
|
+
kindDescriptor.set !== undefined ||
|
|
1587
|
+
kindDescriptor.enumerable !== true
|
|
1588
|
+
) {
|
|
1589
|
+
return "runtimeProfile.kind must be an enumerable data property";
|
|
1590
|
+
}
|
|
1591
|
+
const kind = kindDescriptor.value;
|
|
1592
|
+
if (kind === "shared-factory") {
|
|
1593
|
+
const profile = exactPlainDataRecord(value, ["kind", "compat"]);
|
|
1594
|
+
if (profile === undefined) {
|
|
1595
|
+
return "shared-factory runtimeProfile must contain exactly kind and compat data properties";
|
|
1596
|
+
}
|
|
1597
|
+
const compat = exactPlainDataRecord(profile.compat, [
|
|
1598
|
+
"artifactSchema",
|
|
1599
|
+
"runtimeAbi",
|
|
1600
|
+
]);
|
|
1601
|
+
if (compat === undefined) {
|
|
1602
|
+
return "shared-factory runtimeProfile.compat must contain exactly artifactSchema and runtimeAbi data properties";
|
|
1603
|
+
}
|
|
1604
|
+
if (!Number.isSafeInteger(compat.artifactSchema)) {
|
|
1605
|
+
return "shared-factory runtimeProfile.compat.artifactSchema must be an integer";
|
|
1606
|
+
}
|
|
1607
|
+
if (!Number.isSafeInteger(compat.runtimeAbi)) {
|
|
1608
|
+
return "shared-factory runtimeProfile.compat.runtimeAbi must be an integer";
|
|
1609
|
+
}
|
|
1610
|
+
if (compat.artifactSchema !== advertisedArtifactSchema) {
|
|
1611
|
+
return "artifactSchema must match runtimeProfile.compat.artifactSchema";
|
|
1612
|
+
}
|
|
1613
|
+
return undefined;
|
|
1614
|
+
}
|
|
1615
|
+
if (kind === "bespoke") {
|
|
1616
|
+
const profile = exactPlainDataRecord(value, ["kind", "artifactSchema"]);
|
|
1617
|
+
if (profile === undefined) {
|
|
1618
|
+
return "bespoke runtimeProfile must contain exactly kind and artifactSchema data properties";
|
|
1619
|
+
}
|
|
1620
|
+
if (!Number.isSafeInteger(profile.artifactSchema)) {
|
|
1621
|
+
return "bespoke runtimeProfile.artifactSchema must be an integer";
|
|
1622
|
+
}
|
|
1623
|
+
if (profile.artifactSchema !== advertisedArtifactSchema) {
|
|
1624
|
+
return "artifactSchema must match runtimeProfile.artifactSchema";
|
|
1625
|
+
}
|
|
1626
|
+
return undefined;
|
|
1627
|
+
}
|
|
1628
|
+
return "runtimeProfile.kind must be shared-factory or bespoke";
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
function exactPlainDataRecord(value, expectedKeys) {
|
|
1632
|
+
if (!isPlainObject(value)) return undefined;
|
|
1633
|
+
const keys = Reflect.ownKeys(value);
|
|
1634
|
+
if (
|
|
1635
|
+
keys.length !== expectedKeys.length ||
|
|
1636
|
+
keys.some((key) => typeof key !== "string" || !expectedKeys.includes(key))
|
|
1637
|
+
) {
|
|
1638
|
+
return undefined;
|
|
1639
|
+
}
|
|
1640
|
+
const descriptors = Object.getOwnPropertyDescriptors(value);
|
|
1641
|
+
if (
|
|
1642
|
+
expectedKeys.some((key) => {
|
|
1643
|
+
const descriptor = descriptors[key];
|
|
1644
|
+
return (
|
|
1645
|
+
descriptor === undefined ||
|
|
1646
|
+
descriptor.get !== undefined ||
|
|
1647
|
+
descriptor.set !== undefined ||
|
|
1648
|
+
descriptor.enumerable !== true
|
|
1649
|
+
);
|
|
1650
|
+
})
|
|
1651
|
+
) {
|
|
1652
|
+
return undefined;
|
|
1653
|
+
}
|
|
1654
|
+
return Object.fromEntries(
|
|
1655
|
+
expectedKeys.map((key) => [key, descriptors[key].value]),
|
|
1656
|
+
);
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1293
1659
|
function validateStoredStructuralProjection(value) {
|
|
1294
|
-
const stored = cloneJson(value,
|
|
1660
|
+
const stored = cloneJson(value, "stored structural projection");
|
|
1295
1661
|
if (!isPlainObject(stored) || stored.schemaVersion !== 1) {
|
|
1296
|
-
throw new Error(
|
|
1662
|
+
throw new Error("stored structural projection schema 1 is required");
|
|
1297
1663
|
}
|
|
1298
|
-
const captain = requireObject(stored.captain,
|
|
1299
|
-
if (typeof captain.adapter !==
|
|
1300
|
-
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");
|
|
1301
1667
|
}
|
|
1302
1668
|
if (!Array.isArray(stored.players) || !isPlainObject(stored.catalog)) {
|
|
1303
1669
|
throw new Error(
|
|
1304
|
-
|
|
1670
|
+
"stored structural projection must contain players and catalog",
|
|
1305
1671
|
);
|
|
1306
1672
|
}
|
|
1307
1673
|
const playerIds = stored.players.map((player, index) => {
|
|
1308
1674
|
const record = requireObject(player, `stored structural players.${index}`);
|
|
1309
1675
|
assertPlayerId(record.id, `stored structural players.${index}.id`);
|
|
1310
|
-
if (typeof record.adapter !==
|
|
1676
|
+
if (typeof record.adapter !== "string" || record.adapter.length === 0) {
|
|
1311
1677
|
throw new Error(
|
|
1312
1678
|
`stored structural players.${index} must name an adapter`,
|
|
1313
1679
|
);
|
|
@@ -1315,20 +1681,15 @@ function validateStoredStructuralProjection(value) {
|
|
|
1315
1681
|
return record.id;
|
|
1316
1682
|
});
|
|
1317
1683
|
if (new Set(playerIds).size !== playerIds.length) {
|
|
1318
|
-
throw new Error(
|
|
1684
|
+
throw new Error("stored structural player ids must be unique");
|
|
1319
1685
|
}
|
|
1320
1686
|
for (const [id, itemValue] of Object.entries(stored.catalog)) {
|
|
1321
1687
|
const item = requireObject(itemValue, `stored structural catalog.${id}`);
|
|
1322
1688
|
if (item.id !== id || id === RESERVED_CAPTAIN_PLAYBOOK_ID) {
|
|
1323
1689
|
throw new Error(`stored structural catalog.${id}.id is invalid`);
|
|
1324
1690
|
}
|
|
1325
|
-
for (const field of [
|
|
1326
|
-
|
|
1327
|
-
'manifestCommand',
|
|
1328
|
-
'command',
|
|
1329
|
-
'intent',
|
|
1330
|
-
]) {
|
|
1331
|
-
if (typeof item[field] !== 'string') {
|
|
1691
|
+
for (const field of ["from", "manifestCommand", "command", "intent"]) {
|
|
1692
|
+
if (typeof item[field] !== "string") {
|
|
1332
1693
|
throw new Error(`stored structural catalog.${id}.${field} is invalid`);
|
|
1333
1694
|
}
|
|
1334
1695
|
}
|
|
@@ -1340,6 +1701,15 @@ function validateStoredStructuralProjection(value) {
|
|
|
1340
1701
|
) {
|
|
1341
1702
|
throw new Error(`stored structural catalog.${id} is malformed`);
|
|
1342
1703
|
}
|
|
1704
|
+
if (item.artifactSchema !== 3) {
|
|
1705
|
+
throw new Error(
|
|
1706
|
+
`stored structural catalog.${id}.artifactSchema must be 3`,
|
|
1707
|
+
);
|
|
1708
|
+
}
|
|
1709
|
+
rejectConfiguredHostCapabilities(
|
|
1710
|
+
item.options,
|
|
1711
|
+
`stored structural catalog.${id}.options`,
|
|
1712
|
+
);
|
|
1343
1713
|
if (
|
|
1344
1714
|
JSON.stringify(Object.keys(item.roles)) !==
|
|
1345
1715
|
JSON.stringify(item.requiredRoleIds)
|
|
@@ -1387,7 +1757,7 @@ function projectSelectedMembers(top, selectedMembers) {
|
|
|
1387
1757
|
if (selectedMembers === undefined) return top;
|
|
1388
1758
|
const { playbookIds, playerIds } = validateSelectedMembers(selectedMembers);
|
|
1389
1759
|
if (!isPlainObject(top)) {
|
|
1390
|
-
throw new Error(
|
|
1760
|
+
throw new Error("config must contain only plain JSON objects");
|
|
1391
1761
|
}
|
|
1392
1762
|
|
|
1393
1763
|
const descriptors = Object.getOwnPropertyDescriptors(top);
|
|
@@ -1395,7 +1765,7 @@ function projectSelectedMembers(top, selectedMembers) {
|
|
|
1395
1765
|
for (const key of keys) {
|
|
1396
1766
|
const descriptor = descriptors[key];
|
|
1397
1767
|
if (
|
|
1398
|
-
typeof key ===
|
|
1768
|
+
typeof key === "symbol" ||
|
|
1399
1769
|
descriptor?.get !== undefined ||
|
|
1400
1770
|
descriptor?.set !== undefined ||
|
|
1401
1771
|
descriptor?.enumerable !== true
|
|
@@ -1411,20 +1781,20 @@ function projectSelectedMembers(top, selectedMembers) {
|
|
|
1411
1781
|
projected.playbooks = projectSelectedMap(
|
|
1412
1782
|
projected.playbooks,
|
|
1413
1783
|
playbookIds,
|
|
1414
|
-
|
|
1784
|
+
"playbooks",
|
|
1415
1785
|
);
|
|
1416
1786
|
projected.players = projectSelectedMap(
|
|
1417
1787
|
projected.players,
|
|
1418
1788
|
playerIds,
|
|
1419
|
-
|
|
1789
|
+
"players",
|
|
1420
1790
|
);
|
|
1421
1791
|
return projected;
|
|
1422
1792
|
}
|
|
1423
1793
|
|
|
1424
1794
|
function validateSelectedMembers(selectedMembers) {
|
|
1425
|
-
const selected = cloneJson(selectedMembers,
|
|
1795
|
+
const selected = cloneJson(selectedMembers, "selectedMembers");
|
|
1426
1796
|
const unknownSelectionKeys = Object.keys(selected).filter(
|
|
1427
|
-
(key) => ![
|
|
1797
|
+
(key) => !["playbookIds", "playerIds"].includes(key),
|
|
1428
1798
|
);
|
|
1429
1799
|
if (unknownSelectionKeys.length > 0) {
|
|
1430
1800
|
throw new Error(
|
|
@@ -1434,12 +1804,9 @@ function validateSelectedMembers(selectedMembers) {
|
|
|
1434
1804
|
return {
|
|
1435
1805
|
playbookIds: selectedIdList(
|
|
1436
1806
|
selected.playbookIds,
|
|
1437
|
-
|
|
1438
|
-
),
|
|
1439
|
-
playerIds: selectedIdList(
|
|
1440
|
-
selected.playerIds,
|
|
1441
|
-
'selectedMembers.playerIds',
|
|
1807
|
+
"selectedMembers.playbookIds",
|
|
1442
1808
|
),
|
|
1809
|
+
playerIds: selectedIdList(selected.playerIds, "selectedMembers.playerIds"),
|
|
1443
1810
|
};
|
|
1444
1811
|
}
|
|
1445
1812
|
|
|
@@ -1452,7 +1819,7 @@ function projectSelectedLayer(value, selected, path) {
|
|
|
1452
1819
|
for (const key of Reflect.ownKeys(value)) {
|
|
1453
1820
|
const descriptor = descriptors[key];
|
|
1454
1821
|
if (
|
|
1455
|
-
typeof key ===
|
|
1822
|
+
typeof key === "symbol" ||
|
|
1456
1823
|
descriptor?.get !== undefined ||
|
|
1457
1824
|
descriptor?.set !== undefined ||
|
|
1458
1825
|
descriptor?.enumerable !== true
|
|
@@ -1461,12 +1828,12 @@ function projectSelectedLayer(value, selected, path) {
|
|
|
1461
1828
|
`${path}.${String(key)} must be an enumerable data property`,
|
|
1462
1829
|
);
|
|
1463
1830
|
}
|
|
1464
|
-
if (key ===
|
|
1831
|
+
if (key === "playbooks" || key === "players") {
|
|
1465
1832
|
entries.push([
|
|
1466
1833
|
key,
|
|
1467
1834
|
projectOptionalSelectedMap(
|
|
1468
1835
|
descriptor.value,
|
|
1469
|
-
key ===
|
|
1836
|
+
key === "playbooks" ? selected.playbookIds : selected.playerIds,
|
|
1470
1837
|
`${path}.${key}`,
|
|
1471
1838
|
),
|
|
1472
1839
|
]);
|
|
@@ -1524,7 +1891,7 @@ function projectSelectedMap(value, ids, path) {
|
|
|
1524
1891
|
function selectedIdList(value, path) {
|
|
1525
1892
|
if (
|
|
1526
1893
|
!Array.isArray(value) ||
|
|
1527
|
-
value.some((id) => typeof id !==
|
|
1894
|
+
value.some((id) => typeof id !== "string" || id.trim().length === 0) ||
|
|
1528
1895
|
new Set(value).size !== value.length
|
|
1529
1896
|
) {
|
|
1530
1897
|
throw new Error(`${path} must be a duplicate-free array of nonblank ids`);
|
|
@@ -1533,33 +1900,40 @@ function selectedIdList(value, path) {
|
|
|
1533
1900
|
}
|
|
1534
1901
|
|
|
1535
1902
|
function resolveRoleBinding(value, path) {
|
|
1536
|
-
if (typeof value ===
|
|
1903
|
+
if (typeof value === "string") {
|
|
1537
1904
|
assertPlayerId(value, path);
|
|
1538
1905
|
return { playerId: value };
|
|
1539
1906
|
}
|
|
1540
1907
|
const block = requireObject(value, path);
|
|
1541
1908
|
const unknown = Object.keys(block).filter(
|
|
1542
|
-
(key) => ![
|
|
1909
|
+
(key) => !["player", "model", "effort", "fastMode"].includes(key),
|
|
1543
1910
|
);
|
|
1544
1911
|
if (unknown.length > 0) {
|
|
1545
1912
|
throw new Error(`${path} has unknown ${formatKeyList(unknown)}`);
|
|
1546
1913
|
}
|
|
1547
1914
|
assertPlayerId(block.player, `${path}.player`);
|
|
1548
|
-
for (const field of [
|
|
1915
|
+
for (const field of ["model", "effort"]) {
|
|
1549
1916
|
if (
|
|
1550
1917
|
block[field] !== undefined &&
|
|
1551
1918
|
block[field] !== false &&
|
|
1552
|
-
(typeof block[field] !==
|
|
1919
|
+
(typeof block[field] !== "string" || block[field].trim().length === 0)
|
|
1553
1920
|
) {
|
|
1554
1921
|
throw new Error(
|
|
1555
1922
|
`${path}.${field} must be a nonblank string or false for provider-default`,
|
|
1556
1923
|
);
|
|
1557
1924
|
}
|
|
1558
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
|
+
}
|
|
1559
1932
|
return {
|
|
1560
1933
|
playerId: block.player,
|
|
1561
1934
|
...(block.model === undefined ? {} : { model: block.model }),
|
|
1562
1935
|
...(block.effort === undefined ? {} : { effort: block.effort }),
|
|
1936
|
+
...(block.fastMode === undefined ? {} : { fastMode: block.fastMode }),
|
|
1563
1937
|
};
|
|
1564
1938
|
}
|
|
1565
1939
|
|
|
@@ -1572,17 +1946,21 @@ function applyTuningOverrides(agent, binding) {
|
|
|
1572
1946
|
if (binding.effort === false) delete effective.effort;
|
|
1573
1947
|
else effective.effort = binding.effort;
|
|
1574
1948
|
}
|
|
1949
|
+
if (binding.fastMode !== undefined) effective.fastMode = binding.fastMode;
|
|
1575
1950
|
return effective;
|
|
1576
1951
|
}
|
|
1577
1952
|
|
|
1578
1953
|
function sessionAgentFromHostAgent(agent, path) {
|
|
1579
1954
|
if (!isObject(agent)) {
|
|
1580
|
-
throw new Error(
|
|
1955
|
+
throw new Error("installed cligent omitted a retained agent");
|
|
1581
1956
|
}
|
|
1582
1957
|
return {
|
|
1583
1958
|
adapter: agent.adapter,
|
|
1584
1959
|
model: tuningSelection(agent.model, `${path}.model`),
|
|
1585
1960
|
effort: tuningSelection(agent.effort, `${path}.effort`),
|
|
1961
|
+
...(agent.fastMode === undefined
|
|
1962
|
+
? {}
|
|
1963
|
+
: { fastMode: fastModeSelection(agent.fastMode, `${path}.fastMode`) }),
|
|
1586
1964
|
...(agent.instruction === undefined
|
|
1587
1965
|
? {}
|
|
1588
1966
|
: { instruction: agent.instruction }),
|
|
@@ -1595,16 +1973,19 @@ function sessionAgentFromHostAgent(agent, path) {
|
|
|
1595
1973
|
// Shared by the interactive and headless host projections. A tagged
|
|
1596
1974
|
// provider-default is represented to cligent by omitting that configured
|
|
1597
1975
|
// default; the complete tagged selection remains in sessionAgents.
|
|
1598
|
-
export function projectHostAgent(agent, path =
|
|
1976
|
+
export function projectHostAgent(agent, path = "agent") {
|
|
1599
1977
|
const normalized = cloneJson(agent, path);
|
|
1600
1978
|
return {
|
|
1601
1979
|
adapter: normalized.adapter,
|
|
1602
|
-
...(normalized.model?.kind ===
|
|
1980
|
+
...(normalized.model?.kind === "value"
|
|
1603
1981
|
? { model: normalized.model.value }
|
|
1604
1982
|
: {}),
|
|
1605
|
-
...(normalized.effort?.kind ===
|
|
1983
|
+
...(normalized.effort?.kind === "value"
|
|
1606
1984
|
? { effort: normalized.effort.value }
|
|
1607
1985
|
: {}),
|
|
1986
|
+
...(normalized.fastMode === undefined
|
|
1987
|
+
? {}
|
|
1988
|
+
: { fastMode: normalized.fastMode }),
|
|
1608
1989
|
...(normalized.instruction === undefined
|
|
1609
1990
|
? {}
|
|
1610
1991
|
: { instruction: normalized.instruction }),
|
|
@@ -1614,22 +1995,29 @@ export function projectHostAgent(agent, path = 'agent') {
|
|
|
1614
1995
|
};
|
|
1615
1996
|
}
|
|
1616
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
|
+
|
|
1617
2005
|
function tuningSelection(value, path) {
|
|
1618
2006
|
if (
|
|
1619
2007
|
value !== undefined &&
|
|
1620
|
-
(typeof value !==
|
|
2008
|
+
(typeof value !== "string" || value.trim().length === 0)
|
|
1621
2009
|
) {
|
|
1622
2010
|
throw new Error(`${path} must be a nonblank string`);
|
|
1623
2011
|
}
|
|
1624
2012
|
return value === undefined
|
|
1625
|
-
? { kind:
|
|
1626
|
-
: { kind:
|
|
2013
|
+
? { kind: "provider-default" }
|
|
2014
|
+
: { kind: "value", value };
|
|
1627
2015
|
}
|
|
1628
2016
|
|
|
1629
2017
|
function overrideTuningSelection(value) {
|
|
1630
2018
|
return value === false
|
|
1631
|
-
? { kind:
|
|
1632
|
-
: tuningSelection(value,
|
|
2019
|
+
? { kind: "provider-default" }
|
|
2020
|
+
: tuningSelection(value, "role tuning override");
|
|
1633
2021
|
}
|
|
1634
2022
|
|
|
1635
2023
|
function canonicalizePreparedRegistrySpecifier(value, path) {
|
|
@@ -1646,7 +2034,7 @@ function canonicalizePreparedRegistrySpecifier(value, path) {
|
|
|
1646
2034
|
`${path} preparation must return a canonical module specifier`,
|
|
1647
2035
|
);
|
|
1648
2036
|
}
|
|
1649
|
-
if (value.startsWith(
|
|
2037
|
+
if (value.startsWith("file:")) {
|
|
1650
2038
|
let canonical;
|
|
1651
2039
|
try {
|
|
1652
2040
|
canonical = pathToFileURL(fileURLToPath(value)).href;
|
|
@@ -1661,7 +2049,7 @@ function canonicalizePreparedRegistrySpecifier(value, path) {
|
|
|
1661
2049
|
}
|
|
1662
2050
|
|
|
1663
2051
|
function assertPlayerId(value, path) {
|
|
1664
|
-
if (typeof value !==
|
|
2052
|
+
if (typeof value !== "string" || !PLAYER_ID_PATTERN.test(value)) {
|
|
1665
2053
|
throw new Error(
|
|
1666
2054
|
`${path} must name a player matching ${PLAYER_ID_PATTERN.source}`,
|
|
1667
2055
|
);
|
|
@@ -1672,7 +2060,7 @@ function assertPlayerId(value, path) {
|
|
|
1672
2060
|
}
|
|
1673
2061
|
|
|
1674
2062
|
function assertRoleId(value, path) {
|
|
1675
|
-
if (typeof value !==
|
|
2063
|
+
if (typeof value !== "string" || !ROLE_ID_PATTERN.test(value)) {
|
|
1676
2064
|
throw new Error(`${path} must use a canonical lowercase local role id`);
|
|
1677
2065
|
}
|
|
1678
2066
|
if (value === RESERVED_CAPTAIN_ROLE_ID) {
|
|
@@ -1681,13 +2069,13 @@ function assertRoleId(value, path) {
|
|
|
1681
2069
|
}
|
|
1682
2070
|
|
|
1683
2071
|
function invalidManifestRoles(value) {
|
|
1684
|
-
if (!Array.isArray(value)) return
|
|
1685
|
-
if (value.some((role) => typeof role !==
|
|
1686
|
-
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";
|
|
1687
2075
|
}
|
|
1688
2076
|
const canonical = value.map((role) => role.toLowerCase());
|
|
1689
2077
|
if (new Set(canonical).size !== canonical.length) {
|
|
1690
|
-
return
|
|
2078
|
+
return "contains roles that collide after canonical lowercase derivation";
|
|
1691
2079
|
}
|
|
1692
2080
|
const invalid = value.find((role) => !ROLE_ID_PATTERN.test(role));
|
|
1693
2081
|
if (invalid !== undefined) {
|
|
@@ -1700,7 +2088,7 @@ function invalidManifestRoles(value) {
|
|
|
1700
2088
|
}
|
|
1701
2089
|
|
|
1702
2090
|
function invalidConcurrentRoleSets(value, requiredRoleIds) {
|
|
1703
|
-
if (!Array.isArray(value)) return
|
|
2091
|
+
if (!Array.isArray(value)) return "must be an array";
|
|
1704
2092
|
const required = new Set(requiredRoleIds);
|
|
1705
2093
|
const seen = new Set();
|
|
1706
2094
|
for (let index = 0; index < value.length; index += 1) {
|
|
@@ -1711,7 +2099,7 @@ function invalidConcurrentRoleSets(value, requiredRoleIds) {
|
|
|
1711
2099
|
if (
|
|
1712
2100
|
set.some(
|
|
1713
2101
|
(role) =>
|
|
1714
|
-
typeof role !==
|
|
2102
|
+
typeof role !== "string" ||
|
|
1715
2103
|
!ROLE_ID_PATTERN.test(role) ||
|
|
1716
2104
|
role === RESERVED_CAPTAIN_ROLE_ID ||
|
|
1717
2105
|
!required.has(role),
|
|
@@ -1735,11 +2123,11 @@ function assertExactRoleBindings(id, configured, required) {
|
|
|
1735
2123
|
if (missing.length > 0 || extra.length > 0) {
|
|
1736
2124
|
throw new Error(
|
|
1737
2125
|
`playbooks.${id}.roles must exactly cover requiredRoleIds` +
|
|
1738
|
-
`${missing.length === 0 ?
|
|
2126
|
+
`${missing.length === 0 ? "" : `; missing ${missing.map(JSON.stringify).join(", ")}`}` +
|
|
1739
2127
|
`${
|
|
1740
2128
|
extra.length === 0
|
|
1741
|
-
?
|
|
1742
|
-
: `; extra ${extra.map(JSON.stringify).join(
|
|
2129
|
+
? ""
|
|
2130
|
+
: `; extra ${extra.map(JSON.stringify).join(", ")}`
|
|
1743
2131
|
}`,
|
|
1744
2132
|
);
|
|
1745
2133
|
}
|
|
@@ -1751,7 +2139,7 @@ function assertConcurrentPlayers(id, sets, roles) {
|
|
|
1751
2139
|
if (new Set(playerIds).size !== playerIds.length) {
|
|
1752
2140
|
throw new Error(
|
|
1753
2141
|
`playbooks.${id}.concurrentRoleSets ${JSON.stringify(set)} must bind ` +
|
|
1754
|
-
|
|
2142
|
+
"to pairwise-distinct player ids",
|
|
1755
2143
|
);
|
|
1756
2144
|
}
|
|
1757
2145
|
}
|
|
@@ -1764,7 +2152,7 @@ function distinct(values) {
|
|
|
1764
2152
|
function findLegacyPlayersPath(top) {
|
|
1765
2153
|
const playbooks = isObject(top?.playbooks) ? top.playbooks : {};
|
|
1766
2154
|
for (const [id, block] of Object.entries(playbooks)) {
|
|
1767
|
-
if (isObject(block) && hasOwn(block,
|
|
2155
|
+
if (isObject(block) && hasOwn(block, "players")) {
|
|
1768
2156
|
return `playbooks.${id}.players`;
|
|
1769
2157
|
}
|
|
1770
2158
|
}
|
|
@@ -1772,27 +2160,31 @@ function findLegacyPlayersPath(top) {
|
|
|
1772
2160
|
}
|
|
1773
2161
|
|
|
1774
2162
|
function legacyPlayersError(path, configPath) {
|
|
1775
|
-
const where = configPath ? ` in ${configPath}` :
|
|
2163
|
+
const where = configPath ? ` in ${configPath}` : "";
|
|
1776
2164
|
const error = new Error(
|
|
1777
2165
|
`${path} was removed in the explicit-session-player major release${where}: ` +
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
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",
|
|
1781
2169
|
);
|
|
1782
|
-
error.code =
|
|
2170
|
+
error.code = "PLAYBOOK_LEGACY_PLAYERS";
|
|
1783
2171
|
error.legacyPath = path;
|
|
1784
2172
|
return error;
|
|
1785
2173
|
}
|
|
1786
2174
|
|
|
1787
2175
|
function cloneJson(value, path, seen = new Set()) {
|
|
1788
|
-
if (
|
|
2176
|
+
if (
|
|
2177
|
+
value === null ||
|
|
2178
|
+
typeof value === "string" ||
|
|
2179
|
+
typeof value === "boolean"
|
|
2180
|
+
) {
|
|
1789
2181
|
return value;
|
|
1790
2182
|
}
|
|
1791
|
-
if (typeof value ===
|
|
2183
|
+
if (typeof value === "number") {
|
|
1792
2184
|
if (Number.isFinite(value)) return value;
|
|
1793
2185
|
throw new Error(`${path} must contain only finite JSON numbers`);
|
|
1794
2186
|
}
|
|
1795
|
-
if (typeof value !==
|
|
2187
|
+
if (typeof value !== "object") {
|
|
1796
2188
|
throw new Error(`${path} must contain only JSON values`);
|
|
1797
2189
|
}
|
|
1798
2190
|
if (seen.has(value)) throw new Error(`${path} must not contain a cycle`);
|
|
@@ -1803,8 +2195,8 @@ function cloneJson(value, path, seen = new Set()) {
|
|
|
1803
2195
|
if (
|
|
1804
2196
|
keys.some(
|
|
1805
2197
|
(key) =>
|
|
1806
|
-
typeof key ===
|
|
1807
|
-
(key !==
|
|
2198
|
+
typeof key === "symbol" ||
|
|
2199
|
+
(key !== "length" &&
|
|
1808
2200
|
(!/^(?:0|[1-9]\d*)$/.test(key) || Number(key) >= value.length)),
|
|
1809
2201
|
)
|
|
1810
2202
|
) {
|
|
@@ -1826,9 +2218,7 @@ function cloneJson(value, path, seen = new Set()) {
|
|
|
1826
2218
|
`${path}[${index}] must be an enumerable data property`,
|
|
1827
2219
|
);
|
|
1828
2220
|
}
|
|
1829
|
-
cloned.push(
|
|
1830
|
-
cloneJson(descriptor.value, `${path}[${index}]`, seen),
|
|
1831
|
-
);
|
|
2221
|
+
cloned.push(cloneJson(descriptor.value, `${path}[${index}]`, seen));
|
|
1832
2222
|
}
|
|
1833
2223
|
return cloned;
|
|
1834
2224
|
}
|
|
@@ -1837,7 +2227,7 @@ function cloneJson(value, path, seen = new Set()) {
|
|
|
1837
2227
|
throw new Error(`${path} must contain only plain JSON objects`);
|
|
1838
2228
|
}
|
|
1839
2229
|
const keys = Reflect.ownKeys(value);
|
|
1840
|
-
if (keys.some((key) => typeof key ===
|
|
2230
|
+
if (keys.some((key) => typeof key === "symbol")) {
|
|
1841
2231
|
throw new Error(`${path} must not contain symbol keys`);
|
|
1842
2232
|
}
|
|
1843
2233
|
const descriptors = Object.getOwnPropertyDescriptors(value);
|
|
@@ -1863,7 +2253,7 @@ function cloneJson(value, path, seen = new Set()) {
|
|
|
1863
2253
|
}
|
|
1864
2254
|
|
|
1865
2255
|
function deepFreeze(value) {
|
|
1866
|
-
if (value === null || typeof value !==
|
|
2256
|
+
if (value === null || typeof value !== "object" || Object.isFrozen(value)) {
|
|
1867
2257
|
return value;
|
|
1868
2258
|
}
|
|
1869
2259
|
for (const child of Object.values(value)) deepFreeze(child);
|
|
@@ -1871,7 +2261,7 @@ function deepFreeze(value) {
|
|
|
1871
2261
|
}
|
|
1872
2262
|
|
|
1873
2263
|
function isObject(value) {
|
|
1874
|
-
return typeof value ===
|
|
2264
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1875
2265
|
}
|
|
1876
2266
|
|
|
1877
2267
|
function isPlainObject(value) {
|
|
@@ -1890,9 +2280,9 @@ function requireObject(value, path) {
|
|
|
1890
2280
|
}
|
|
1891
2281
|
|
|
1892
2282
|
function formatKeyList(keys) {
|
|
1893
|
-
return `${keys.length === 1 ?
|
|
2283
|
+
return `${keys.length === 1 ? "key" : "keys"} ${keys
|
|
1894
2284
|
.map((key) => JSON.stringify(key))
|
|
1895
|
-
.join(
|
|
2285
|
+
.join(", ")}`;
|
|
1896
2286
|
}
|
|
1897
2287
|
|
|
1898
2288
|
function errorMessage(error) {
|