@sublang/playbook 10.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.
@@ -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 'node:fs';
18
- import { homedir, tmpdir } from 'node:os';
19
- import { dirname, isAbsolute, join, resolve } from 'node:path';
20
- import { fileURLToPath, pathToFileURL } from 'node:url';
21
- import { isDeepStrictEqual } from 'node:util';
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 'yaml';
27
- import { loadTmuxPlayConfig } from '@sublang/cligent/tmux-play';
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
- 'playbook.config.template.yaml',
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
- '@sublang/playbook/playbook-captain';
39
- const PLAYBOOK_LAUNCHER_KEYS = ['from', 'command', 'roles'];
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
- 'captain',
43
- 'players',
44
- 'playbooks',
45
- 'layout',
46
- 'notifications',
47
- 'theme',
45
+ "captain",
46
+ "players",
47
+ "playbooks",
48
+ "layout",
49
+ "notifications",
50
+ "theme",
51
+ "sessions",
48
52
  ]);
49
- const RESERVED_CAPTAIN_PLAYBOOK_ID = 'captain';
50
- const RESERVED_CAPTAIN_ROLE_ID = 'captain';
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 === '--with') {
65
+ if (arg === "--with") {
62
66
  const value = argv[i + 1];
63
- if (value === undefined || value === '') {
64
- throw new Error('--with needs a value');
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('--with=')) {
69
- const value = arg.slice('--with='.length);
70
- if (!value) throw new Error('--with needs a value');
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, 'utf8');
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, 'config'),
126
- projectSelectedLayer(overlay, selected, 'overlay'),
129
+ projectSelectedLayer(base, selected, "config"),
130
+ projectSelectedLayer(overlay, selected, "overlay"),
127
131
  );
128
132
  }
129
133
 
130
- export function resolveConfigHome(env = process.env, home = homedir()) {
131
- return env.XDG_CONFIG_HOME || join(home, '.config');
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(resolveConfigHome(env, home), 'playbook', 'playbook.config.yaml');
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('file:')) return from;
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, 'utf8')) ?? {};
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, 'utf8')) ?? {};
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
- 'config',
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
- 'selected launch members do not match the stored structural projection',
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, 'config');
335
+ top = cloneJson(top, "config");
255
336
  assertNoRetiredProfiles(top, configPath);
256
- if (hasOwn(top, 'run')) {
337
+ if (hasOwn(top, "run")) {
257
338
  throw new Error(
258
339
  'top-level "run" was removed: configure the shared Captain under ' +
259
- 'captain, top-level players, and playbooks.<id>.roles instead',
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('layout must be a map');
352
+ throw new Error("layout must be a map");
272
353
  }
273
354
 
274
- const playersCfg = requireObject(top.players, '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
- 'id',
360
+ "id",
280
361
  ]);
281
362
  configuredAgents.set(playerId, agent);
282
363
  }
283
- let captain = resolveAgent(top.captain, 'captain', ['from', 'options']);
364
+ let captain = resolveAgent(top.captain, "captain", ["from", "options"]);
284
365
 
285
- const playbooksCfg = requireObject(top.playbooks, '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, 'players')) {
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 !== 'string' ||
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(block.from, configPath);
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 (binding.model !== undefined || binding.effort !== undefined) {
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 { from: _captainFrom, options: _captainOptions, ...normalizedCaptain } =
406
- normalizedHost.captain;
407
- captain = sessionAgentFromHostAgent(normalizedCaptain, 'captain');
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
- 'current selected config does not reproduce the stored structural projection',
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
- 'selected launch config',
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 === 'string') {
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 === 'object' &&
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, 'config');
642
+ top = cloneJson(top, "config");
547
643
  assertNoRetiredProfiles(top, configPath);
548
- if (hasOwn(top, 'run')) {
644
+ if (hasOwn(top, "run")) {
549
645
  throw new Error(
550
646
  'top-level "run" was removed: configure the shared Captain under ' +
551
- 'captain, top-level players, and playbooks.<id>.roles instead',
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('layout must be a map');
659
+ throw new Error("layout must be a map");
564
660
  }
565
661
 
566
- const playersCfg = requireObject(top.players, '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
- 'id',
668
+ "id",
573
669
  ]);
574
670
  if (
575
- typeof agent.adapter !== 'string' ||
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, '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('playbooks must enable at least one playbook');
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('playbooks keys must be canonical trimmed nonblank ids');
685
+ throw new Error("playbooks keys must be canonical trimmed nonblank ids");
590
686
  }
591
687
 
592
- let captain = resolveAgent(top.captain, 'captain', ['from', 'options']);
688
+ let captain = resolveAgent(top.captain, "captain", ["from", "options"]);
593
689
  if (
594
- typeof captain.adapter !== 'string' ||
690
+ typeof captain.adapter !== "string" ||
595
691
  captain.adapter.trim().length === 0
596
692
  ) {
597
- throw new Error('captain must resolve an adapter');
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, 'players')) {
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 !== 'string' ||
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 !== 'string' ||
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
- 'tmux-play Captain',
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 (binding.model !== undefined || binding.effort !== undefined) {
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 { from: _captainFrom, options: _captainOptions, ...normalizedCaptain } =
755
- normalizedHost.captain;
756
- captain = sessionAgentFromHostAgent(normalizedCaptain, 'captain');
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 !== 'string' ||
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
- 'launch config',
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(), 'playbook-host-config-'));
950
- const path = join(dir, 'tmux-play.config.yaml');
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, '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, '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 === 'string' && captain.adapter.length > 0
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, 'notifications')) {
1108
+ if (hasOwn(plan.presentation, "notifications")) {
1001
1109
  config.notifications = cloneJson(
1002
1110
  plan.presentation.notifications,
1003
- 'presentation.notifications',
1111
+ "presentation.notifications",
1004
1112
  );
1005
1113
  }
1006
- if (hasOwn(plan.presentation, 'theme')) {
1007
- config.theme = cloneJson(plan.presentation.theme, '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, 'presentation.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 === 'claude') {
1065
- if (!env.ANTHROPIC_API_KEY && !existsSync(join(home, '.claude'))) {
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 === 'codex') {
1071
- if (!env.OPENAI_API_KEY && !existsSync(join(home, '.codex'))) {
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, 'utf8');
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 === 'PLAYBOOK_LEGACY_PLAYERS') {
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
- 'adapter, model, effort, and permissions',
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('playbooks');
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(['playbooks', id, 'players']) !== undefined) {
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('profiles');
1153
- const agentPaths = [['captain']];
1154
- const players = doc.get('players');
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(['players', String(player.key)]);
1401
+ agentPaths.push(["players", String(player.key)]);
1158
1402
  }
1159
1403
  }
1160
1404
 
1161
1405
  const profileSettings = (name) =>
1162
- profiles && typeof profiles.get === 'function'
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 === 'string' && !Array.isArray(node.items)) {
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?.('profile');
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('.')}.profile names "${String(named)}", which no ` +
1183
- 'profiles entry defines',
1426
+ `${path.join(".")}.profile names "${String(named)}", which no ` +
1427
+ "profiles entry defines",
1184
1428
  );
1185
1429
  }
1186
- node.delete('profile');
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) === 'profiles',
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('profiles');
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
- ' Migrated by playbook 3.0.0: the top-level `profiles` map was removed and\n' +
1219
- ' each agent now carries its settings inline. The pre-migration file is\n' +
1220
- ' kept beside this one as a .bak. Comments below may still describe the\n' +
1221
- ' retired profiles model.';
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 === 'string' && part.trim() !== '',
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('\n');
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 !== 'string' || comment.trim() === '') return undefined;
1240
- const paragraphs = comment.split('\n\n');
1241
- const kept = paragraphs.slice(0, -1).join('\n\n');
1242
- return kept.trim() === '' ? undefined : kept;
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
- 'inline under captain and each top-level players.<player-id> ' +
1251
- '(adapter, model, effort, permissions)',
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) throw legacyPlayersError(legacyPath, configPath);
1256
- const blocks = [['captain', top.captain]];
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
- 'inline in that block (adapter, model, effort, permissions)',
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 'the default export must be an object';
1536
+ if (!isObject(value)) return "the default export must be an object";
1292
1537
  if (
1293
- typeof value.id !== 'string' ||
1538
+ typeof value.id !== "string" ||
1294
1539
  value.id.trim().length === 0 ||
1295
1540
  value.id !== value.id.trim()
1296
1541
  ) {
1297
- return 'id must be a canonical trimmed nonblank string';
1542
+ return "id must be a canonical trimmed nonblank string";
1298
1543
  }
1299
1544
  if (
1300
- typeof value.command !== 'string' ||
1545
+ typeof value.command !== "string" ||
1301
1546
  value.command.trim().length === 0 ||
1302
1547
  value.command !== value.command.trim()
1303
1548
  ) {
1304
- return 'command must be a canonical trimmed nonblank string';
1549
+ return "command must be a canonical trimmed nonblank string";
1305
1550
  }
1306
- if (typeof value.intent !== 'string') return 'intent must be a string';
1551
+ if (typeof value.intent !== "string") return "intent must be a string";
1307
1552
  if (value.artifactSchema !== 3) {
1308
- return 'artifactSchema must be 3';
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 !== 'function') {
1325
- return 'validateOptions must be a function';
1569
+ if (typeof value.validateOptions !== "function") {
1570
+ return "validateOptions must be a function";
1326
1571
  }
1327
- if (typeof value.createRuntime !== 'function') {
1328
- return 'createRuntime must be a function';
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 'runtimeProfile must be a plain object';
1580
+ return "runtimeProfile must be a plain object";
1336
1581
  }
1337
- const kindDescriptor = Object.getOwnPropertyDescriptor(value, 'kind');
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 'runtimeProfile.kind must be an enumerable data property';
1589
+ return "runtimeProfile.kind must be an enumerable data property";
1345
1590
  }
1346
1591
  const kind = kindDescriptor.value;
1347
- if (kind === 'shared-factory') {
1348
- const profile = exactPlainDataRecord(value, ['kind', 'compat']);
1592
+ if (kind === "shared-factory") {
1593
+ const profile = exactPlainDataRecord(value, ["kind", "compat"]);
1349
1594
  if (profile === undefined) {
1350
- return 'shared-factory runtimeProfile must contain exactly kind and compat data properties';
1595
+ return "shared-factory runtimeProfile must contain exactly kind and compat data properties";
1351
1596
  }
1352
1597
  const compat = exactPlainDataRecord(profile.compat, [
1353
- 'artifactSchema',
1354
- 'runtimeAbi',
1598
+ "artifactSchema",
1599
+ "runtimeAbi",
1355
1600
  ]);
1356
1601
  if (compat === undefined) {
1357
- return 'shared-factory runtimeProfile.compat must contain exactly artifactSchema and runtimeAbi data properties';
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 'shared-factory runtimeProfile.compat.artifactSchema must be an integer';
1605
+ return "shared-factory runtimeProfile.compat.artifactSchema must be an integer";
1361
1606
  }
1362
1607
  if (!Number.isSafeInteger(compat.runtimeAbi)) {
1363
- return 'shared-factory runtimeProfile.compat.runtimeAbi must be an integer';
1608
+ return "shared-factory runtimeProfile.compat.runtimeAbi must be an integer";
1364
1609
  }
1365
1610
  if (compat.artifactSchema !== advertisedArtifactSchema) {
1366
- return 'artifactSchema must match runtimeProfile.compat.artifactSchema';
1611
+ return "artifactSchema must match runtimeProfile.compat.artifactSchema";
1367
1612
  }
1368
1613
  return undefined;
1369
1614
  }
1370
- if (kind === 'bespoke') {
1371
- const profile = exactPlainDataRecord(value, ['kind', 'artifactSchema']);
1615
+ if (kind === "bespoke") {
1616
+ const profile = exactPlainDataRecord(value, ["kind", "artifactSchema"]);
1372
1617
  if (profile === undefined) {
1373
- return 'bespoke runtimeProfile must contain exactly kind and artifactSchema data properties';
1618
+ return "bespoke runtimeProfile must contain exactly kind and artifactSchema data properties";
1374
1619
  }
1375
1620
  if (!Number.isSafeInteger(profile.artifactSchema)) {
1376
- return 'bespoke runtimeProfile.artifactSchema must be an integer';
1621
+ return "bespoke runtimeProfile.artifactSchema must be an integer";
1377
1622
  }
1378
1623
  if (profile.artifactSchema !== advertisedArtifactSchema) {
1379
- return 'artifactSchema must match runtimeProfile.artifactSchema';
1624
+ return "artifactSchema must match runtimeProfile.artifactSchema";
1380
1625
  }
1381
1626
  return undefined;
1382
1627
  }
1383
- return 'runtimeProfile.kind must be shared-factory or bespoke';
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, 'stored structural projection');
1660
+ const stored = cloneJson(value, "stored structural projection");
1418
1661
  if (!isPlainObject(stored) || stored.schemaVersion !== 1) {
1419
- throw new Error('stored structural projection schema 1 is required');
1662
+ throw new Error("stored structural projection schema 1 is required");
1420
1663
  }
1421
- const captain = requireObject(stored.captain, 'stored structural captain');
1422
- if (typeof captain.adapter !== 'string' || captain.adapter.length === 0) {
1423
- throw new Error('stored structural captain must name an adapter');
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
- 'stored structural projection must contain players and catalog',
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 !== 'string' || record.adapter.length === 0) {
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('stored structural player ids must be unique');
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
- 'from',
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('config must contain only plain JSON objects');
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 === 'symbol' ||
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
- 'playbooks',
1784
+ "playbooks",
1547
1785
  );
1548
1786
  projected.players = projectSelectedMap(
1549
1787
  projected.players,
1550
1788
  playerIds,
1551
- 'players',
1789
+ "players",
1552
1790
  );
1553
1791
  return projected;
1554
1792
  }
1555
1793
 
1556
1794
  function validateSelectedMembers(selectedMembers) {
1557
- const selected = cloneJson(selectedMembers, 'selectedMembers');
1795
+ const selected = cloneJson(selectedMembers, "selectedMembers");
1558
1796
  const unknownSelectionKeys = Object.keys(selected).filter(
1559
- (key) => !['playbookIds', 'playerIds'].includes(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
- 'selectedMembers.playbookIds',
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 === 'symbol' ||
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 === 'playbooks' || key === 'players') {
1831
+ if (key === "playbooks" || key === "players") {
1597
1832
  entries.push([
1598
1833
  key,
1599
1834
  projectOptionalSelectedMap(
1600
1835
  descriptor.value,
1601
- key === 'playbooks' ? selected.playbookIds : selected.playerIds,
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 !== 'string' || id.trim().length === 0) ||
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 === 'string') {
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) => !['player', 'model', 'effort'].includes(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 ['model', 'effort']) {
1915
+ for (const field of ["model", "effort"]) {
1681
1916
  if (
1682
1917
  block[field] !== undefined &&
1683
1918
  block[field] !== false &&
1684
- (typeof block[field] !== 'string' || block[field].trim().length === 0)
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('installed cligent omitted a retained agent');
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 = 'agent') {
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 === 'value'
1980
+ ...(normalized.model?.kind === "value"
1735
1981
  ? { model: normalized.model.value }
1736
1982
  : {}),
1737
- ...(normalized.effort?.kind === 'value'
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 !== 'string' || value.trim().length === 0)
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: 'provider-default' }
1758
- : { kind: 'value', value };
2013
+ ? { kind: "provider-default" }
2014
+ : { kind: "value", value };
1759
2015
  }
1760
2016
 
1761
2017
  function overrideTuningSelection(value) {
1762
2018
  return value === false
1763
- ? { kind: 'provider-default' }
1764
- : tuningSelection(value, 'role tuning override');
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('file:')) {
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 !== 'string' || !PLAYER_ID_PATTERN.test(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 !== 'string' || !ROLE_ID_PATTERN.test(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 'must be an array';
1817
- if (value.some((role) => typeof role !== 'string')) {
1818
- return 'must contain only strings';
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 'contains roles that collide after canonical lowercase derivation';
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 'must be an array';
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 !== 'string' ||
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 ? '' : `; missing ${missing.map(JSON.stringify).join(', ')}`}` +
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
- 'to pairwise-distinct player ids',
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, 'players')) {
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
- 'define stable ids in top-level players and bind them explicitly under ' +
1911
- 'playbooks.<id>.roles; automatic migration would choose which prior ' +
1912
- 'conversations share a session',
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 = 'PLAYBOOK_LEGACY_PLAYERS';
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 (value === null || typeof value === 'string' || typeof value === 'boolean') {
2176
+ if (
2177
+ value === null ||
2178
+ typeof value === "string" ||
2179
+ typeof value === "boolean"
2180
+ ) {
1921
2181
  return value;
1922
2182
  }
1923
- if (typeof value === 'number') {
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 !== 'object') {
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 === 'symbol' ||
1939
- (key !== 'length' &&
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 === 'symbol')) {
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 !== 'object' || Object.isFrozen(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 === 'object' && value !== null && !Array.isArray(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 ? 'key' : 'keys'} ${keys
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) {