@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.
- package/docs/configuration.md +29 -2
- package/package.json +2 -2
- package/reference/sdlc/code.playbook/bin/launch-config.js +55 -13
- package/reference/sdlc/code.playbook/bin/session-store.js +3 -0
- package/reference/sdlc/code.playbook/playbook-captain.d.ts +2 -1
- package/reference/sdlc/code.playbook/playbook-captain.js +13 -2
- package/reference/sdlc/code.playbook/playbook-captain.ts +17 -4
- package/src/xstate-playbook-runtime.js +11 -5
- package/src/xstate-playbook-runtime.ts +11 -5
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",
|
|
@@ -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 }
|
|
@@ -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'
|
|
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
|
-
:
|
|
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 ===
|
|
1005
|
-
|
|
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
|
-
:
|
|
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 ===
|
|
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)`);
|