@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.
- package/docs/configuration.md +29 -2
- package/package.json +2 -2
- package/reference/sdlc/captain.md +1 -1
- package/reference/sdlc/captain.playbook/captain.fsm.js +1 -1
- package/reference/sdlc/captain.playbook/captain.fsm.ts +1 -1
- package/reference/sdlc/captain.playbook/captain.gears.md +1 -1
- package/reference/sdlc/code.playbook/bin/launch-config.js +55 -13
- package/reference/sdlc/code.playbook/bin/repository-effects.js +26 -2
- package/reference/sdlc/code.playbook/bin/session-store.js +3 -0
- package/reference/sdlc/code.playbook/host-capabilities.d.ts +1 -0
- package/reference/sdlc/code.playbook/playbook-captain.d.ts +2 -1
- package/reference/sdlc/code.playbook/playbook-captain.js +170 -48
- package/reference/sdlc/code.playbook/playbook-captain.ts +227 -63
- package/slc/link.md +25 -3
- package/src/runtime.d.ts +13 -0
- package/src/runtime.ts +14 -0
- package/src/xstate-playbook-runtime.d.ts +4 -3
- package/src/xstate-playbook-runtime.js +163 -19
- package/src/xstate-playbook-runtime.ts +197 -18
- package/src/xstate-runtime.d.ts +7 -5
- package/src/xstate-runtime.js +40 -9
- package/src/xstate-runtime.ts +53 -24
package/docs/configuration.md
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
481
|
+
let overlay = loadOverlayFragment(overlayPath);
|
|
476
482
|
addCurrentPlaybookIds(overlay, currentPlaybookIds);
|
|
477
|
-
|
|
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
|
-
//
|
|
1300
|
-
|
|
1301
|
-
|
|
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
|
-
|
|
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;
|