@sublang/playbook 17.9.0 → 17.10.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.
@@ -17,7 +17,7 @@ file's directory with it when that leaves it empty, and leaves no compatibility
17
17
  alias, so running an older Spex host afterward could seed a second file at the
18
18
  path it still resolves.
19
19
 
20
- The current guard rejects relocation when a former relative `sessions` value
20
+ The current guard rejects relocation when a former relative skill path, `sessions` value
21
21
  or relative filesystem `playbooks.<id>.from` would resolve differently below
22
22
  the new directory. It leaves the former file unchanged and names every
23
23
  target-preserving absolute replacement. Apply those replacements and retry;
@@ -277,6 +277,33 @@ Claude Code auth or `ANTHROPIC_API_KEY`; `codex` with local Codex CLI
277
277
  auth or `OPENAI_API_KEY`. A known adapter that is not ready blocks the
278
278
  launch and prints the help text.
279
279
 
280
+ ## Skills across agents
281
+
282
+ Set a shared `skills` list to let working agents use the same native skill:
283
+
284
+ ```yaml
285
+ skills:
286
+ - ../skills/project-conventions
287
+ captain:
288
+ adapter: claude
289
+ skills: []
290
+ players:
291
+ dev.coder: codex
292
+ dev.reviewer: claude
293
+ ```
294
+
295
+ Entries name a skill directory or its `SKILL.md`. Relative entries resolve from
296
+ whichever config or `--with` overlay file declares them. An agent's own list
297
+ replaces the shared list; `[]` opts out. Playbook preserves absolute locators
298
+ through interactive and headless launches, and Cligent stages them for the native
299
+ agent without changing its permissions. Role bindings do not override skills.
300
+
301
+ Reopening a settled session uses the current lists for the next turn. Recovery
302
+ of an uncertain turn keeps its attempted lists. Skill sources remain external
303
+ files, so keep their locations available for resumed runs. Ordinary Captain
304
+ routing, judging and replies receive `skills: []`; bounded recovery preparation
305
+ can use the Captain's configured skills.
306
+
280
307
  ## Browser capability
281
308
 
282
309
  A Captain or player agent block may set `browser: true` to make Cligent's managed
@@ -287,7 +314,7 @@ uses that attempt's recorded value. Agent permissions remain fixed and retain
287
314
  their native precedence.
288
315
 
289
316
  Ordinary Captain decisions, replies, question checks and judges always receive
290
- `browser: false` and an empty MCP configuration. Enabling Browser does not turn
317
+ `browser: false`, `skills: []` and an empty MCP configuration. Enabling Browser does not turn
291
318
  them into working actors. A working player, or bounded recovery preparation when
292
319
  needed, can use the capability explicitly configured for its agent. An embedding
293
320
  application should use Cligent's capability and browser-preparation APIs before
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sublang/playbook",
3
- "version": "17.9.0",
3
+ "version": "17.10.0",
4
4
  "type": "module",
5
5
  "description": "Composable XState v5 playbook runtime with compiled Captain, CODE, REVIEW, DECIDE, DEV, BRANCH, PR, and INSPECT workflows driven by GEARS specs.",
6
6
  "license": "Apache-2.0",
@@ -280,7 +280,7 @@
280
280
  "provenance": true
281
281
  },
282
282
  "dependencies": {
283
- "@sublang/cligent": "^0.33.2",
283
+ "@sublang/cligent": "^0.34.0",
284
284
  "@sublang/spex": "^3.0.0",
285
285
  "p-queue": "^9.3.1",
286
286
  "xstate": "^5.19.4",
@@ -48,6 +48,7 @@ const PLAYBOOK_LAUNCHER_KEYS = ["from", "command", "roles"];
48
48
  const HOST_CAPABILITIES_OPTION_KEY = "hostCapabilities";
49
49
  const PLAYBOOK_TOP_LEVEL_KEYS = new Set([
50
50
  "captain",
51
+ "skills",
51
52
  "players",
52
53
  "playbooks",
53
54
  "layout",
@@ -419,9 +420,13 @@ export async function loadLaunchPlan({
419
420
  );
420
421
  }
421
422
  const currentPlaybookIds = addCurrentPlaybookIds(top);
423
+ if (selectedMembers !== undefined) top = projectSelectedLayer(top, validateSelectedMembers(selectedMembers), "config");
424
+ top = resolveSkillLocators(top, userConfigPath);
422
425
  for (const overlayPath of overlayPaths) {
423
- const overlay = loadOverlayFragment(overlayPath);
426
+ let overlay = loadOverlayFragment(overlayPath);
424
427
  addCurrentPlaybookIds(overlay, currentPlaybookIds);
428
+ if (selectedMembers !== undefined) overlay = projectSelectedLayer(overlay, validateSelectedMembers(selectedMembers), "overlay");
429
+ overlay = resolveSkillLocators(overlay, overlayPath);
425
430
  top =
426
431
  selectedMembers === undefined
427
432
  ? mergeConfigs(top, overlay)
@@ -471,10 +476,12 @@ export async function loadSelectedLaunchPlanDataOnly({
471
476
  validateSelectedMembers(selectedMembers),
472
477
  "config",
473
478
  );
479
+ top = resolveSkillLocators(top, userConfigPath);
474
480
  for (const overlayPath of overlayPaths) {
475
- const overlay = loadOverlayFragment(overlayPath);
481
+ let overlay = loadOverlayFragment(overlayPath);
476
482
  addCurrentPlaybookIds(overlay, currentPlaybookIds);
477
- top = mergeSelectedConfigs(top, overlay, selectedMembers);
483
+ overlay = projectSelectedLayer(overlay, validateSelectedMembers(selectedMembers), "overlay");
484
+ top = mergeSelectedConfigs(top, resolveSkillLocators(overlay, overlayPath), selectedMembers);
478
485
  }
479
486
  return await normalizeSelectedLaunchPlanDataOnlyWith(
480
487
  top,
@@ -512,7 +519,7 @@ async function normalizeSelectedLaunchPlanDataOnlyWith(
512
519
  }
513
520
  }
514
521
  top = projectSelectedMembers(top, expectedMembers);
515
- top = cloneJson(top, "config");
522
+ top = resolveSkillLocators(cloneJson(top, "config"), configPath);
516
523
  assertNoRetiredProfiles(top, configPath);
517
524
  if (hasOwn(top, "run")) {
518
525
  throw new Error(
@@ -663,6 +670,7 @@ async function normalizeSelectedLaunchPlanDataOnlyWith(
663
670
  ? expectedMembers.playerIds.slice(0, 1)
664
671
  : initialVisible;
665
672
  const provisional = {
673
+ ...(top.skills === undefined ? {} : { skills: top.skills }),
666
674
  captain: { ...captain, from: PLAYBOOK_CAPTAIN_MODULE, options: {} },
667
675
  players: [
668
676
  ...expectedMembers.playerIds.map((id) => ({
@@ -694,14 +702,14 @@ async function normalizeSelectedLaunchPlanDataOnlyWith(
694
702
  options: _captainOptions,
695
703
  ...normalizedCaptain
696
704
  } = normalizedHost.captain;
697
- captain = sessionAgentFromHostAgent(normalizedCaptain, "captain");
705
+ captain = sessionAgentFromHostAgent(normalizedCaptain, "captain", normalizedHost.skills);
698
706
  const hostAgents = new Map(
699
707
  normalizedHost.players.map(({ id, ...agent }) => [id, agent]),
700
708
  );
701
709
  const normalizedPlayerAgents = new Map(
702
710
  expectedMembers.playerIds.map((id) => [
703
711
  id,
704
- sessionAgentFromHostAgent(hostAgents.get(id), `players.${id}`),
712
+ sessionAgentFromHostAgent(hostAgents.get(id), `players.${id}`, normalizedHost.skills),
705
713
  ]),
706
714
  );
707
715
 
@@ -857,7 +865,7 @@ async function normalizeLaunchPlanWith(
857
865
  ) {
858
866
  const importModule = loadModule ?? ((specifier) => import(specifier));
859
867
  top = projectSelectedMembers(top, selectedMembers);
860
- top = cloneJson(top, "config");
868
+ top = resolveSkillLocators(cloneJson(top, "config"), configPath);
861
869
  assertNoRetiredProfiles(top, configPath);
862
870
  if (hasOwn(top, "run")) {
863
871
  throw new Error(
@@ -1060,6 +1068,7 @@ async function normalizeLaunchPlanWith(
1060
1068
  ? allPlayerIds.slice(0, 1)
1061
1069
  : provisionalVisible;
1062
1070
  const provisional = {
1071
+ ...(top.skills === undefined ? {} : { skills: top.skills }),
1063
1072
  captain: {
1064
1073
  ...captain,
1065
1074
  from: PLAYBOOK_CAPTAIN_MODULE,
@@ -1099,14 +1108,14 @@ async function normalizeLaunchPlanWith(
1099
1108
  options: _captainOptions,
1100
1109
  ...normalizedCaptain
1101
1110
  } = normalizedHost.captain;
1102
- captain = sessionAgentFromHostAgent(normalizedCaptain, "captain");
1111
+ captain = sessionAgentFromHostAgent(normalizedCaptain, "captain", normalizedHost.skills);
1103
1112
  const hostAgents = new Map(
1104
1113
  normalizedHost.players.map(({ id, ...agent }) => [id, agent]),
1105
1114
  );
1106
1115
  const normalizedPlayerAgents = new Map(
1107
1116
  allPlayerIds.map((id) => [
1108
1117
  id,
1109
- sessionAgentFromHostAgent(hostAgents.get(id), `players.${id}`),
1118
+ sessionAgentFromHostAgent(hostAgents.get(id), `players.${id}`, normalizedHost.skills),
1110
1119
  ]),
1111
1120
  );
1112
1121
 
@@ -1296,9 +1305,27 @@ async function normalizeLaunchPlanWith(
1296
1305
  );
1297
1306
  }
1298
1307
 
1299
- // Run cligent's public explicit-config loader over an isolated projection so
1300
- // generic launch planning tracks the installed host schema without importing
1301
- // or duplicating cligent's private validators.
1308
+ // Resolve authored locators before the temporary host config changes their base.
1309
+ function resolveSkillLocators(top, configPath) {
1310
+ if (!isObject(top)) return top;
1311
+ const base = configPath === undefined ? process.cwd() : dirname(resolve(configPath));
1312
+ const list = (value, path) => {
1313
+ if (!Array.isArray(value) || value.some(item => typeof item !== "string" || item.trim().length === 0 || item.includes("\0"))) {
1314
+ throw new Error(`${path} must be an array of nonblank skill paths`);
1315
+ }
1316
+ return value.map(item => resolve(base, item));
1317
+ };
1318
+ const agent = (value, path) => !isObject(value) || value.skills === undefined
1319
+ ? value : { ...value, skills: list(value.skills, `${path}.skills`) };
1320
+ return {
1321
+ ...top,
1322
+ ...(top.skills === undefined ? {} : { skills: list(top.skills, "skills") }),
1323
+ ...(top.captain === undefined ? {} : { captain: agent(top.captain, "captain") }),
1324
+ ...(isObject(top.players) ? { players: Object.fromEntries(Object.entries(top.players).map(([id, value]) => [id, agent(value, `players.${id}`)])) } : {}),
1325
+ };
1326
+ }
1327
+
1328
+ // Validate through the installed public loader before registry imports.
1302
1329
  export async function normalizeHostConfig(config) {
1303
1330
  const dir = mkdtempSync(join(tmpdir(), "playbook-host-config-"));
1304
1331
  const path = join(dir, "tmux-play.config.yaml");
@@ -1590,6 +1617,19 @@ function assertLegacyRelocationLocatorsSafe(
1590
1617
  }
1591
1618
  }
1592
1619
 
1620
+ const skillLists = [["skills", top.skills], ["captain.skills", isObject(top.captain) ? top.captain.skills : undefined]];
1621
+ if (isObject(top.players)) for (const [id, agent] of Object.entries(top.players)) {
1622
+ if (isObject(agent)) skillLists.push([`players.${id}.skills`, agent.skills]);
1623
+ }
1624
+ for (const [path, entries] of skillLists) {
1625
+ if (!Array.isArray(entries)) continue;
1626
+ entries.forEach((entry, index) => {
1627
+ if (typeof entry !== "string" || entry.trim().length === 0 || isAbsolute(entry)) return;
1628
+ const formerTarget = resolve(legacyDir, entry);
1629
+ if (formerTarget !== resolve(canonicalDir, entry)) affected.push({ path: `${path}[${index}]`, replacement: formerTarget });
1630
+ });
1631
+ }
1632
+
1593
1633
  if (affected.length === 0) return;
1594
1634
  const replacements = affected
1595
1635
  .map(({ path, replacement }) => `${path} = ${JSON.stringify(replacement)}`)
@@ -2353,12 +2393,13 @@ function applyTuningOverrides(agent, binding) {
2353
2393
  return effective;
2354
2394
  }
2355
2395
 
2356
- function sessionAgentFromHostAgent(agent, path) {
2396
+ function sessionAgentFromHostAgent(agent, path, defaultSkills) {
2357
2397
  if (!isObject(agent)) {
2358
2398
  throw new Error("installed cligent omitted a retained agent");
2359
2399
  }
2360
2400
  return {
2361
2401
  adapter: agent.adapter,
2402
+ ...((agent.skills ?? defaultSkills) === undefined ? {} : { skills: [...(agent.skills ?? defaultSkills)] }),
2362
2403
  ...(agent.browser === undefined ? {} : { browser: fastModeSelection(agent.browser, `${path}.browser`) }),
2363
2404
  model: tuningSelection(agent.model, `${path}.model`),
2364
2405
  effort: tuningSelection(agent.effort, `${path}.effort`),
@@ -2397,6 +2438,7 @@ export function projectHostAgent(agent, path = "agent") {
2397
2438
  const normalized = cloneJson(agent, path);
2398
2439
  return {
2399
2440
  adapter: normalized.adapter,
2441
+ ...(normalized.skills === undefined ? {} : { skills: normalized.skills }),
2400
2442
  ...(normalized.browser === undefined ? {} : { browser: fastModeSelection(normalized.browser, `${path}.browser`) }),
2401
2443
  ...(normalized.model?.kind === "value"
2402
2444
  ? { model: normalized.model.value }
@@ -5580,6 +5580,7 @@ function validateCaptainSessionProjection(
5580
5580
  'subagentModel',
5581
5581
  'subagentEffort',
5582
5582
  'browser',
5583
+ 'skills',
5583
5584
  ],
5584
5585
  playerPath,
5585
5586
  );
@@ -5751,6 +5752,7 @@ function validateProjectedAgent(value, path, { structural, hasId = false }) {
5751
5752
  if (!structural) {
5752
5753
  validateProjectedFastMode(agent, `${path}.fastMode`);
5753
5754
  if (agent.browser !== undefined && typeof agent.browser !== 'boolean') throw new Error(`${path}.browser must be a boolean`);
5755
+ if (agent.skills !== undefined && (!Array.isArray(agent.skills) || agent.skills.some(item => typeof item !== 'string' || item.includes('\0') || !isRecordedAbsolutePath(item)))) throw new Error(`${path}.skills must be an array of absolute skill paths`);
5754
5756
  validateProjectedSubagentTuning(agent, path);
5755
5757
  }
5756
5758
  if (!hasId) {
@@ -5766,6 +5768,7 @@ function validateProjectedAgent(value, path, { structural, hasId = false }) {
5766
5768
  'subagentModel',
5767
5769
  'subagentEffort',
5768
5770
  'browser',
5771
+ 'skills',
5769
5772
  ],
5770
5773
  path,
5771
5774
  );
@@ -8,6 +8,7 @@ import type { SessionAssetRef } from './session-assets.js';
8
8
  interface SessionAgent {
9
9
  /** Mutable next-turn execution capability; never structural identity. */
10
10
  readonly browser?: boolean;
11
+ readonly skills?: readonly string[];
11
12
  readonly adapter: string;
12
13
  readonly model: TuningSelection;
13
14
  readonly effort: TuningSelection<Effort>;
@@ -44,7 +45,7 @@ interface PlaybookCaptainUnresolvedEffectSettlementInput {
44
45
  readonly rootPlaybookId: string;
45
46
  readonly unresolvedEffects: readonly PlaybookCaptainUnresolvedEffect[];
46
47
  }
47
- type SnapshotAgentEnvelope = DeepReadonly<Omit<SessionAgent, TuningKey | 'browser'>>;
48
+ type SnapshotAgentEnvelope = DeepReadonly<Omit<SessionAgent, TuningKey | 'browser' | 'skills'>>;
48
49
  type PlayerLedgerSnapshotEntry = DeepReadonly<PlayerLedgerEntry>;
49
50
  export interface ProgressChange {
50
51
  snapshot?: PlaybookCaptainShellSnapshot | null;
@@ -1,6 +1,7 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
2
  // SPDX-FileCopyrightText: 2026 SubLang International <https://sublang.ai>
3
3
  import { randomUUID } from 'node:crypto';
4
+ import { posix, win32 } from 'node:path';
4
5
  import { isDeepStrictEqual } from 'node:util';
5
6
  import PQueue from 'p-queue';
6
7
  import { isAgentCallSettingsError, } from '@sublang/cligent/tmux-play';
@@ -2310,6 +2311,12 @@ function snapshotNonblankTuning(value, path) {
2310
2311
  }
2311
2312
  return value;
2312
2313
  }
2314
+ function snapshotSkillLocators(value, path) {
2315
+ if (!Array.isArray(value) || value.some(item => typeof item !== 'string' || item.includes('\0') || !(posix.isAbsolute(item) || win32.isAbsolute(item)))) {
2316
+ throw new Error(`${path} must be an array of absolute skill paths`);
2317
+ }
2318
+ return [...value];
2319
+ }
2313
2320
  function snapshotSessionAgent(value, path) {
2314
2321
  const agent = snapshotRecord(value, path);
2315
2322
  rejectSnapshotKeys(agent, [
@@ -2320,6 +2327,7 @@ function snapshotSessionAgent(value, path) {
2320
2327
  'subagentModel',
2321
2328
  'subagentEffort',
2322
2329
  'browser',
2330
+ 'skills',
2323
2331
  'instruction',
2324
2332
  'permissions',
2325
2333
  ], path);
@@ -2328,7 +2336,8 @@ function snapshotSessionAgent(value, path) {
2328
2336
  key !== 'fastMode' &&
2329
2337
  key !== 'subagentModel' &&
2330
2338
  key !== 'subagentEffort' &&
2331
- key !== 'browser')), path);
2339
+ key !== 'browser' &&
2340
+ key !== 'skills')), path);
2332
2341
  return {
2333
2342
  adapter: fixed.adapter,
2334
2343
  ...(fixed.instruction === undefined
@@ -2337,6 +2346,7 @@ function snapshotSessionAgent(value, path) {
2337
2346
  ...(fixed.permissions === undefined
2338
2347
  ? {}
2339
2348
  : { permissions: livePermissions(fixed.permissions) }),
2349
+ ...(agent.skills === undefined ? {} : { skills: snapshotSkillLocators(agent.skills, `${path}.skills`) }),
2340
2350
  ...(agent.browser === undefined ? {} : { browser: snapshotFastMode(agent.browser, `${path}.browser`) }),
2341
2351
  model: snapshotTuningSelection(agent.model, `${path}.model`),
2342
2352
  effort: snapshotEffortSelection(agent.effort, `${path}.effort`),
@@ -2372,6 +2382,7 @@ function callSettings(agent, tuning = agent) {
2372
2382
  model: tuning.model,
2373
2383
  effort: tuning.effort,
2374
2384
  browser: agent.browser ?? false,
2385
+ skills: agent.skills === undefined ? [] : [...agent.skills],
2375
2386
  ...(tuning.fastMode === undefined ? {} : { fastMode: tuning.fastMode }),
2376
2387
  ...(tuning.subagentModel === undefined
2377
2388
  ? {}
@@ -2384,7 +2395,7 @@ function callSettings(agent, tuning = agent) {
2384
2395
  };
2385
2396
  }
2386
2397
  function controlCallSettings(agent) {
2387
- return { ...callSettings(agent), browser: false, mcpServers: {} };
2398
+ return { ...callSettings(agent), browser: false, skills: [], mcpServers: {} };
2388
2399
  }
2389
2400
  function promptIdentity(binding) {
2390
2401
  return binding.model.kind === 'value'
@@ -2,6 +2,7 @@
2
2
  // SPDX-FileCopyrightText: 2026 SubLang International <https://sublang.ai>
3
3
 
4
4
  import { randomUUID } from 'node:crypto';
5
+ import { posix, win32 } from 'node:path';
5
6
  import { isDeepStrictEqual } from 'node:util';
6
7
  import PQueue from 'p-queue';
7
8
 
@@ -69,6 +70,7 @@ import { QUOTED_EVIDENCE_LIMIT, workerReportExcerpt, workerReportBlock } from '.
69
70
  interface SessionAgent {
70
71
  /** Mutable next-turn execution capability; never structural identity. */
71
72
  readonly browser?: boolean;
73
+ readonly skills?: readonly string[];
72
74
  readonly adapter: string;
73
75
  readonly model: TuningSelection;
74
76
  readonly effort: TuningSelection<Effort>;
@@ -127,7 +129,7 @@ interface PlaybookCaptainUnresolvedEffectSettlementInput {
127
129
  readonly unresolvedEffects: readonly PlaybookCaptainUnresolvedEffect[];
128
130
  }
129
131
 
130
- type SnapshotAgentEnvelope = DeepReadonly<Omit<SessionAgent, TuningKey | 'browser'>>;
132
+ type SnapshotAgentEnvelope = DeepReadonly<Omit<SessionAgent, TuningKey | 'browser' | 'skills'>>;
131
133
 
132
134
  type PlayerLedgerSnapshotEntry = DeepReadonly<PlayerLedgerEntry>;
133
135
 
@@ -4059,6 +4061,13 @@ function snapshotNonblankTuning(
4059
4061
  return value;
4060
4062
  }
4061
4063
 
4064
+ function snapshotSkillLocators(value: JsonValue, path: string): readonly string[] {
4065
+ if (!Array.isArray(value) || value.some(item => typeof item !== 'string' || item.includes('\0') || !(posix.isAbsolute(item) || win32.isAbsolute(item)))) {
4066
+ throw new Error(`${path} must be an array of absolute skill paths`);
4067
+ }
4068
+ return [...value] as string[];
4069
+ }
4070
+
4062
4071
  function snapshotSessionAgent(
4063
4072
  value: JsonValue | undefined,
4064
4073
  path: string,
@@ -4074,6 +4083,7 @@ function snapshotSessionAgent(
4074
4083
  'subagentModel',
4075
4084
  'subagentEffort',
4076
4085
  'browser',
4086
+ 'skills',
4077
4087
  'instruction',
4078
4088
  'permissions',
4079
4089
  ],
@@ -4088,7 +4098,8 @@ function snapshotSessionAgent(
4088
4098
  key !== 'fastMode' &&
4089
4099
  key !== 'subagentModel' &&
4090
4100
  key !== 'subagentEffort' &&
4091
- key !== 'browser',
4101
+ key !== 'browser' &&
4102
+ key !== 'skills',
4092
4103
  ),
4093
4104
  ) as JsonValue,
4094
4105
  path,
@@ -4101,6 +4112,7 @@ function snapshotSessionAgent(
4101
4112
  ...(fixed.permissions === undefined
4102
4113
  ? {}
4103
4114
  : { permissions: livePermissions(fixed.permissions) }),
4115
+ ...(agent.skills === undefined ? {} : { skills: snapshotSkillLocators(agent.skills, `${path}.skills`) }),
4104
4116
  ...(agent.browser === undefined ? {} : { browser: snapshotFastMode(agent.browser, `${path}.browser`) }),
4105
4117
  model: snapshotTuningSelection(agent.model, `${path}.model`),
4106
4118
  effort: snapshotEffortSelection(agent.effort, `${path}.effort`),
@@ -4126,7 +4138,7 @@ function snapshotSessionAgent(
4126
4138
  };
4127
4139
  }
4128
4140
 
4129
- function fixedAgent(agent: SessionAgent): Omit<SessionAgent, TuningKey | 'browser'> {
4141
+ function fixedAgent(agent: SessionAgent): Omit<SessionAgent, TuningKey | 'browser' | 'skills'> {
4130
4142
  return {
4131
4143
  adapter: agent.adapter,
4132
4144
  ...(agent.instruction === undefined ? {} : { instruction: agent.instruction }),
@@ -4147,6 +4159,7 @@ function callSettings(
4147
4159
  model: tuning.model,
4148
4160
  effort: tuning.effort,
4149
4161
  browser: agent.browser ?? false,
4162
+ skills: agent.skills === undefined ? [] : [...agent.skills],
4150
4163
  ...(tuning.fastMode === undefined ? {} : { fastMode: tuning.fastMode }),
4151
4164
  ...(tuning.subagentModel === undefined
4152
4165
  ? {}
@@ -4160,7 +4173,7 @@ function callSettings(
4160
4173
  }
4161
4174
 
4162
4175
  function controlCallSettings(agent: SessionAgent): AgentCallSettings {
4163
- return { ...callSettings(agent), browser: false, mcpServers: {} };
4176
+ return { ...callSettings(agent), browser: false, skills: [], mcpServers: {} };
4164
4177
  }
4165
4178
 
4166
4179
  function promptIdentity(binding: EffectivePlayerBinding): string {
@@ -995,14 +995,20 @@ export function renderGovernedOutcomeContract(guard, description, outcome, shape
995
995
  for (const [field, authority] of Object.entries(outcome?.fields ?? {})) {
996
996
  if (authority === 'semantic') {
997
997
  const entry = authored.get(field);
998
- // DR-089: the value has the shape the compiled machine admits.
998
+ // DR-089: the value has the shape the compiled machine admits; a
999
+ // shape the probe did not reveal is the machine's to judge, so the
1000
+ // example asserts none (DR-091).
999
1001
  const shape = shapes !== undefined && Object.prototype.hasOwnProperty.call(shapes, field)
1000
1002
  ? shapes[field]
1001
- : 'string';
1002
- const words = describePlaybookSemanticValueShape(shape);
1003
+ : undefined;
1004
+ const words = shape === undefined ? undefined : describePlaybookSemanticValueShape(shape);
1003
1005
  replyMembers.push(`${JSON.stringify(field)}: ${entry?.placeholder ??
1004
- (shape === 'string' ? '<string>' : `<${words.replace(/^a /, '')}>`)}`);
1005
- if (shape !== 'string')
1006
+ (shape === undefined
1007
+ ? '<value>'
1008
+ : shape === 'string'
1009
+ ? '<string>'
1010
+ : `<${words.replace(/^a /, '')}>`)}`);
1011
+ if (shape !== undefined && shape !== 'string')
1006
1012
  valueShapes.push(`\`${field}\` is ${words}`);
1007
1013
  semanticAsAuthored.push(entry?.segment ?? `\`${field}\``);
1008
1014
  }
@@ -1874,19 +1874,25 @@ export function renderGovernedOutcomeContract(
1874
1874
  for (const [field, authority] of Object.entries(outcome?.fields ?? {})) {
1875
1875
  if (authority === 'semantic') {
1876
1876
  const entry = authored.get(field);
1877
- // DR-089: the value has the shape the compiled machine admits.
1877
+ // DR-089: the value has the shape the compiled machine admits; a
1878
+ // shape the probe did not reveal is the machine's to judge, so the
1879
+ // example asserts none (DR-091).
1878
1880
  const shape =
1879
1881
  shapes !== undefined && Object.prototype.hasOwnProperty.call(shapes, field)
1880
1882
  ? shapes[field]!
1881
- : 'string';
1882
- const words = describePlaybookSemanticValueShape(shape);
1883
+ : undefined;
1884
+ const words = shape === undefined ? undefined : describePlaybookSemanticValueShape(shape);
1883
1885
  replyMembers.push(
1884
1886
  `${JSON.stringify(field)}: ${
1885
1887
  entry?.placeholder ??
1886
- (shape === 'string' ? '<string>' : `<${words.replace(/^a /, '')}>`)
1888
+ (shape === undefined
1889
+ ? '<value>'
1890
+ : shape === 'string'
1891
+ ? '<string>'
1892
+ : `<${words!.replace(/^a /, '')}>`)
1887
1893
  }`,
1888
1894
  );
1889
- if (shape !== 'string') valueShapes.push(`\`${field}\` is ${words}`);
1895
+ if (shape !== undefined && shape !== 'string') valueShapes.push(`\`${field}\` is ${words}`);
1890
1896
  semanticAsAuthored.push(entry?.segment ?? `\`${field}\``);
1891
1897
  } else {
1892
1898
  runtimeSupplied.push(`\`${field}\` (${authority}-owned)`);