@sublang/playbook 17.8.1 → 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.8.1",
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",
@@ -96,7 +96,7 @@ When an acting turn's selection — parse-resolved or model-decided — settles
96
96
  > Do not finish with a bare acknowledgement, a promise to act, or an announcement that the round is complete.
97
97
  > When mentioning progress detail, use only the aggregate counts the report supplies.
98
98
  > Append the supplied saved-counts line verbatim only when one is supplied; when none is supplied, append no saved-counts line.
99
- > Use familiar words and usually one to three short sentences, or a short list of choices. When a step's ending is supplied, give every fact that step reported — paths, record ids, commit hashes, names — as it gave them, before anything you say about them; leave none out and add none. Omit routine acknowledgements, unchanged-file reports, and other commit identifiers unless they explain a failure or a requested result. Never repeat other jargon, even in quotes: explain its meaning instead. Preserve every choice and material constraint without repeating background. Boss must understand and answer from your reply alone, without reading a player pane.
99
+ > Use familiar words and usually one to three short sentences, or a short list of choices. When a step's ending is supplied, give every fact that step reported — paths, record ids, commit hashes, names — as it gave them, before anything you say about them; leave none out and add none. Where the report states a step's commits, name each commit by its hash and the branch it landed on, exactly as the report gives them. Omit routine acknowledgements, unchanged-file reports, and other commit identifiers unless they explain a failure or a requested result. Never repeat other jargon, even in quotes: explain its meaning instead. Preserve every choice and material constraint without repeating background. Boss must understand and answer from your reply alone, without reading a player pane.
100
100
  > Keep the reply to 60 words unless extra words are essential to preserve choices, constraints, or result evidence.
101
101
  > Omit routine status and routing updates. Do not add guarantees or conditions beyond the supplied facts.
102
102
  > Write concise human chat prose with no guard names, result property names, control JSON, hidden control data, internal state ids, session ids, call ids, stack data, or private reasoning.
@@ -53,7 +53,7 @@ const CLOSING_REPLY_PROMPT = [
53
53
  'Do not finish with a bare acknowledgement, a promise to act, or an announcement that the round is complete.',
54
54
  'When mentioning progress detail, use only the aggregate counts the report supplies.',
55
55
  'Append the supplied saved-counts line verbatim only when one is supplied; when none is supplied, append no saved-counts line.',
56
- "Use familiar words and usually one to three short sentences, or a short list of choices. When a step's ending is supplied, give every fact that step reported — paths, record ids, commit hashes, names — as it gave them, before anything you say about them; leave none out and add none. Omit routine acknowledgements, unchanged-file reports, and other commit identifiers unless they explain a failure or a requested result. Never repeat other jargon, even in quotes: explain its meaning instead. Preserve every choice and material constraint without repeating background. Boss must understand and answer from your reply alone, without reading a player pane.",
56
+ "Use familiar words and usually one to three short sentences, or a short list of choices. When a step's ending is supplied, give every fact that step reported — paths, record ids, commit hashes, names — as it gave them, before anything you say about them; leave none out and add none. Where the report states a step's commits, name each commit by its hash and the branch it landed on, exactly as the report gives them. Omit routine acknowledgements, unchanged-file reports, and other commit identifiers unless they explain a failure or a requested result. Never repeat other jargon, even in quotes: explain its meaning instead. Preserve every choice and material constraint without repeating background. Boss must understand and answer from your reply alone, without reading a player pane.",
57
57
  'Keep the reply to 60 words unless extra words are essential to preserve choices, constraints, or result evidence.',
58
58
  'Omit routine status and routing updates. Do not add guarantees or conditions beyond the supplied facts.',
59
59
  'Write concise human chat prose with no guard names, result property names, control JSON, hidden control data, internal state ids, session ids, call ids, stack data, or private reasoning.',
@@ -267,7 +267,7 @@ const CLOSING_REPLY_PROMPT = [
267
267
  'Do not finish with a bare acknowledgement, a promise to act, or an announcement that the round is complete.',
268
268
  'When mentioning progress detail, use only the aggregate counts the report supplies.',
269
269
  'Append the supplied saved-counts line verbatim only when one is supplied; when none is supplied, append no saved-counts line.',
270
- "Use familiar words and usually one to three short sentences, or a short list of choices. When a step's ending is supplied, give every fact that step reported — paths, record ids, commit hashes, names — as it gave them, before anything you say about them; leave none out and add none. Omit routine acknowledgements, unchanged-file reports, and other commit identifiers unless they explain a failure or a requested result. Never repeat other jargon, even in quotes: explain its meaning instead. Preserve every choice and material constraint without repeating background. Boss must understand and answer from your reply alone, without reading a player pane.",
270
+ "Use familiar words and usually one to three short sentences, or a short list of choices. When a step's ending is supplied, give every fact that step reported — paths, record ids, commit hashes, names — as it gave them, before anything you say about them; leave none out and add none. Where the report states a step's commits, name each commit by its hash and the branch it landed on, exactly as the report gives them. Omit routine acknowledgements, unchanged-file reports, and other commit identifiers unless they explain a failure or a requested result. Never repeat other jargon, even in quotes: explain its meaning instead. Preserve every choice and material constraint without repeating background. Boss must understand and answer from your reply alone, without reading a player pane.",
271
271
  'Keep the reply to 60 words unless extra words are essential to preserve choices, constraints, or result evidence.',
272
272
  'Omit routine status and routing updates. Do not add guarantees or conditions beyond the supplied facts.',
273
273
  'Write concise human chat prose with no guard names, result property names, control JSON, hidden control data, internal state ids, session ids, call ids, stack data, or private reasoning.',
@@ -65,7 +65,7 @@ Where this is the default generic session Captain playbook with no players beyon
65
65
  > Do not finish with a bare acknowledgement, a promise to act, or an announcement that the round is complete.
66
66
  > When mentioning progress detail, use only the aggregate counts the report supplies.
67
67
  > Append the supplied saved-counts line verbatim only when one is supplied; when none is supplied, append no saved-counts line.
68
- > Use familiar words and usually one to three short sentences, or a short list of choices. When a step's ending is supplied, give every fact that step reported — paths, record ids, commit hashes, names — as it gave them, before anything you say about them; leave none out and add none. Omit routine acknowledgements, unchanged-file reports, and other commit identifiers unless they explain a failure or a requested result. Never repeat other jargon, even in quotes: explain its meaning instead. Preserve every choice and material constraint without repeating background. Boss must understand and answer from your reply alone, without reading a player pane.
68
+ > Use familiar words and usually one to three short sentences, or a short list of choices. When a step's ending is supplied, give every fact that step reported — paths, record ids, commit hashes, names — as it gave them, before anything you say about them; leave none out and add none. Where the report states a step's commits, name each commit by its hash and the branch it landed on, exactly as the report gives them. Omit routine acknowledgements, unchanged-file reports, and other commit identifiers unless they explain a failure or a requested result. Never repeat other jargon, even in quotes: explain its meaning instead. Preserve every choice and material constraint without repeating background. Boss must understand and answer from your reply alone, without reading a player pane.
69
69
  > Keep the reply to 60 words unless extra words are essential to preserve choices, constraints, or result evidence.
70
70
  > Omit routine status and routing updates. Do not add guarantees or conditions beyond the supplied facts.
71
71
  > Write concise human chat prose with no guard names, result property names, control JSON, hidden control data, internal state ids, session ids, call ids, stack data, or private reasoning.
@@ -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 }
@@ -779,6 +779,23 @@ async function resolveHead(worktree, env) {
779
779
  }
780
780
  }
781
781
 
782
+ // DR-090 §4: the branch HEAD names, or undefined when HEAD is detached:
783
+ // `symbolic-ref --quiet` exits 1 without output for a detached HEAD, and
784
+ // every other failure stays a failure.
785
+ async function resolveBranch(worktree, env) {
786
+ try {
787
+ const branch = await runGitText(
788
+ worktree,
789
+ ['symbolic-ref', '--quiet', '--short', 'HEAD'],
790
+ { env },
791
+ );
792
+ return branch.length === 0 ? undefined : branch;
793
+ } catch (error) {
794
+ if (error?.code === 1 && error.stdout?.length === 0) return undefined;
795
+ throw error;
796
+ }
797
+ }
798
+
782
799
  function gitFailureSays(error, exitCode, text) {
783
800
  return (
784
801
  typeof error === 'object' &&
@@ -862,20 +879,25 @@ export async function resolveCanonicalGitWorktree(cwd) {
862
879
  }
863
880
 
864
881
  async function sampleWorktree(worktree, env) {
865
- const [headResult, visibilityResult, statusResult] = await Promise.allSettled([
882
+ // A directory that is not a repository yet names no branch: its scratch
883
+ // repository's HEAD is not the worktree's.
884
+ const [headResult, branchResult, visibilityResult, statusResult] = await Promise.allSettled([
866
885
  resolveHead(worktree, env),
886
+ env === undefined ? resolveBranch(worktree, env) : Promise.resolve(undefined),
867
887
  rawIndexVisibility(worktree, env),
868
888
  rawRepositoryStatus(worktree, env),
869
889
  ]);
870
890
  if (headResult.status === 'rejected') throw headResult.reason;
891
+ if (branchResult.status === 'rejected') throw branchResult.reason;
871
892
  if (visibilityResult.status === 'rejected') throw visibilityResult.reason;
872
893
  const head = headResult.value;
894
+ const branch = branchResult.value;
873
895
  const indexVisibility = visibilityResult.value;
874
896
  assertIndexVisibility(indexVisibility);
875
897
  if (statusResult.status === 'rejected') throw statusResult.reason;
876
898
  const status = statusResult.value;
877
899
  const projection = await projectionFromStatus(worktree, status);
878
- return { head, indexVisibility, status, projection };
900
+ return { head, branch, indexVisibility, status, projection };
879
901
  }
880
902
 
881
903
  async function observeWorktreeSamples(identity, env, options) {
@@ -885,6 +907,7 @@ async function observeWorktreeSamples(identity, env, options) {
885
907
  const beforeText = projectionText(before.projection);
886
908
  if (
887
909
  before.head !== after.head ||
910
+ before.branch !== after.branch ||
888
911
  !before.indexVisibility.equals(after.indexVisibility) ||
889
912
  !before.status.equals(after.status) ||
890
913
  beforeText !== projectionText(after.projection)
@@ -895,6 +918,7 @@ async function observeWorktreeSamples(identity, env, options) {
895
918
  worktree: identity.worktree,
896
919
  gitDir: identity.gitDir,
897
920
  head: before.head,
921
+ ...(before.branch === undefined ? {} : { branch: before.branch }),
898
922
  projection: before.projection,
899
923
  projectionDigest: `sha256:${sha256(beforeText)}`,
900
924
  });
@@ -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
  );
@@ -29,6 +29,7 @@ export interface RepositoryIdentity {
29
29
  }
30
30
  export interface PlaybookRepositoryObservation extends RepositoryIdentity {
31
31
  readonly head: string;
32
+ readonly branch?: string;
32
33
  readonly projection: Readonly<Record<string, JsonValue>>;
33
34
  readonly projectionDigest: string;
34
35
  }
@@ -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;