@sublang/playbook 0.4.2 → 0.6.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/README.md +42 -19
- package/package.json +18 -6
- package/reference/sdlc/code.playbook/bin/playbook-code.js +277 -104
- package/reference/sdlc/code.playbook/code.fsm.d.ts +1 -0
- package/reference/sdlc/code.playbook/code.fsm.js +67 -29
- package/reference/sdlc/code.playbook/code.fsm.ts +74 -29
- package/reference/sdlc/code.playbook/code.gears.md +66 -30
- package/reference/sdlc/code.playbook/code.playbook.d.ts +19 -3
- package/reference/sdlc/code.playbook/code.playbook.js +292 -34
- package/reference/sdlc/code.playbook/code.playbook.ts +290 -35
- package/reference/sdlc/code.playbook/code.registry.d.ts +43 -0
- package/reference/sdlc/code.playbook/code.registry.js +109 -0
- package/reference/sdlc/code.playbook/code.registry.ts +159 -0
- package/reference/sdlc/code.playbook/code.tmux-play.d.ts +2 -0
- package/reference/sdlc/code.playbook/code.tmux-play.js +7 -93
- package/reference/sdlc/code.playbook/code.tmux-play.ts +20 -118
- package/reference/sdlc/code.playbook/playbook-captain.d.ts +21 -0
- package/reference/sdlc/code.playbook/playbook-captain.js +491 -0
- package/reference/sdlc/code.playbook/playbook-captain.ts +701 -0
- package/reference/sdlc/code.playbook/playbook-code.config.template.yaml +50 -25
- package/reference/sdlc/code.playbook/tmux-play.config.yaml +13 -8
- package/reference/sdlc/code.playbook/tmux-play.production.config.yaml +8 -4
package/README.md
CHANGED
|
@@ -36,7 +36,8 @@ The reference is the canonical worked example —
|
|
|
36
36
|
[CODE source](reference/sdlc/code.md) →
|
|
37
37
|
[gears](reference/sdlc/code.playbook/code.gears.md) →
|
|
38
38
|
[FSM](reference/sdlc/code.playbook/code.fsm.ts) → runtime — with
|
|
39
|
-
the runtime
|
|
39
|
+
the runtime registered behind the built-in Playbook Captain shell for
|
|
40
|
+
cligent's `tmux-play` host out of the box.
|
|
40
41
|
The compiled artifacts live under
|
|
41
42
|
[`reference/sdlc/code.playbook/`](reference/sdlc/code.playbook),
|
|
42
43
|
the slc pipeline's `<basename>.<pipeline>/` output directory.
|
|
@@ -82,7 +83,10 @@ playbook-code --help
|
|
|
82
83
|
```
|
|
83
84
|
|
|
84
85
|
The seed template runs each agent in cligent's protected auto mode
|
|
85
|
-
(`permissions.mode: auto`), suppressing routine approval prompts.
|
|
86
|
+
(`permissions.mode: auto`), suppressing routine approval prompts. Its
|
|
87
|
+
Codex Coder also grants `permissions.writablePaths: [.git]` so git
|
|
88
|
+
metadata writes stay available under auto mode without switching to
|
|
89
|
+
bypass permissions.
|
|
86
90
|
|
|
87
91
|
### Configure agents
|
|
88
92
|
|
|
@@ -95,20 +99,27 @@ $EDITOR "${XDG_CONFIG_HOME:-$HOME/.config}/playbook/playbook-code.config.yaml"
|
|
|
95
99
|
Both CODE players can use `claude` or `codex`; other adapter ids are
|
|
96
100
|
passed through to `tmux-play` with a warning because `playbook-code`
|
|
97
101
|
does not know how to preflight their auth. The safe tuning points are
|
|
98
|
-
`captain.adapter`, `captain.model`, each
|
|
99
|
-
|
|
100
|
-
|
|
102
|
+
`captain.adapter`, `captain.model`, and each role's `adapter` and
|
|
103
|
+
`model` under `players.coder` / `players.reviewer`. The composer owns
|
|
104
|
+
`captain.from` and points it at the Playbook Captain shell adapter, and
|
|
105
|
+
it owns the `coder` / `reviewer` role keys, so leave those keys as-is;
|
|
106
|
+
the runtime binds to those host-configuration invariants per
|
|
101
107
|
[PBRT-4](specs/user/playbook-runtime.md#pbrt-4) and derives the
|
|
102
|
-
`<coder-llm>` / `<reviewer-llm>` substitution strings from each
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
(e.g. `claude-opus-4-
|
|
108
|
+
`<coder-llm>` / `<reviewer-llm>` substitution strings from each role's
|
|
109
|
+
`model` when pinned and `adapter` otherwise — so the Committer's
|
|
110
|
+
commit-message trailers can name the concrete model
|
|
111
|
+
(e.g. `claude-opus-4-8`) rather than the adapter family (`claude`).
|
|
106
112
|
|
|
107
|
-
|
|
113
|
+
`players.committer` is an optional alias naming which role — `coder`
|
|
114
|
+
or `reviewer` — runs the commit turn; the seeded overlay points it at
|
|
115
|
+
the Reviewer. Absent the alias the Committer falls back to the Coder
|
|
116
|
+
([PBRT-8](specs/user/playbook-runtime.md#pbrt-8)).
|
|
117
|
+
|
|
118
|
+
For example, the seeded overlay runs the Coder on Codex and the
|
|
119
|
+
Reviewer on Claude, with the Committer aliased to the Reviewer:
|
|
108
120
|
|
|
109
121
|
```yaml
|
|
110
122
|
captain:
|
|
111
|
-
from: "@sublang/playbook/code/tmux-play"
|
|
112
123
|
adapter: claude
|
|
113
124
|
model: claude-sonnet-4-6
|
|
114
125
|
reasoningEffort: high
|
|
@@ -116,18 +127,21 @@ captain:
|
|
|
116
127
|
mode: auto
|
|
117
128
|
|
|
118
129
|
players:
|
|
119
|
-
|
|
130
|
+
coder: # role key must stay `coder` — see PBRT-4
|
|
120
131
|
adapter: codex
|
|
121
132
|
model: gpt-5.5
|
|
122
133
|
reasoningEffort: xhigh
|
|
123
134
|
permissions:
|
|
124
135
|
mode: auto
|
|
125
|
-
|
|
136
|
+
writablePaths:
|
|
137
|
+
- .git # allow git metadata writes under Codex auto mode
|
|
138
|
+
reviewer: # role key must stay `reviewer` — see PBRT-4
|
|
126
139
|
adapter: claude
|
|
127
|
-
model: claude-opus-4-
|
|
140
|
+
model: claude-opus-4-8
|
|
128
141
|
reasoningEffort: xhigh
|
|
129
142
|
permissions:
|
|
130
143
|
mode: auto
|
|
144
|
+
committer: reviewer # which role commits — `coder` or `reviewer`
|
|
131
145
|
```
|
|
132
146
|
|
|
133
147
|
Normal `playbook-code` runs use the seeded path above. If you need a
|
|
@@ -174,7 +188,7 @@ production install.
|
|
|
174
188
|
|
|
175
189
|
Drive a Boss turn against the source tree (uses the developer
|
|
176
190
|
[`tmux-play.config.yaml`](reference/sdlc/code.playbook/tmux-play.config.yaml)
|
|
177
|
-
that imports the compiled
|
|
191
|
+
that imports the compiled Playbook Captain shell via relative path):
|
|
178
192
|
|
|
179
193
|
```sh
|
|
180
194
|
pnpm exec tmux-play --config reference/sdlc/code.playbook/tmux-play.config.yaml
|
|
@@ -186,7 +200,9 @@ installed globally.
|
|
|
186
200
|
|
|
187
201
|
### Running a Boss turn
|
|
188
202
|
|
|
189
|
-
The Boss pane
|
|
203
|
+
The Boss pane starts at the Playbook Captain shell. Use `/code <task>`
|
|
204
|
+
to explicitly select the CODE playbook, or use ordinary text and let the
|
|
205
|
+
shell route it. Once a turn reaches CODE, the CODE judge classifies it
|
|
190
206
|
into an FSM event (start a coding turn, continue or summarize an IR,
|
|
191
207
|
interrupt to a named state, or nothing) per
|
|
192
208
|
[PBRT-1](specs/user/playbook-runtime.md#pbrt-1).
|
|
@@ -195,9 +211,16 @@ the FSM parks at `awaitBossReply` and the pane shows the question; your
|
|
|
195
211
|
next turn is normally classified as the reply, or a fresh directive
|
|
196
212
|
abandons it ([PBRT-2](specs/user/playbook-runtime.md#pbrt-2)).
|
|
197
213
|
|
|
198
|
-
The Captain pane
|
|
199
|
-
`◆ ▸ ⮕ ⤷` per
|
|
200
|
-
player prompts
|
|
214
|
+
The Captain pane shows `/code` start/stop/finished status with `◇` lines
|
|
215
|
+
and streams the CODE state machine with the four-glyph vocabulary `◆ ▸ ⮕ ⤷` per
|
|
216
|
+
[PBRT-3](specs/user/playbook-runtime.md#pbrt-3), while player prompts
|
|
217
|
+
ride their own panes.
|
|
218
|
+
|
|
219
|
+
Published configs should import the shell adapter from
|
|
220
|
+
`@sublang/playbook/playbook-captain`. Existing explicit configs that
|
|
221
|
+
still import `@sublang/playbook/code/tmux-play` keep resolving through
|
|
222
|
+
a compatibility shim that delegates to the same shell with CODE
|
|
223
|
+
registered.
|
|
201
224
|
|
|
202
225
|
### Embedding the runtime in your own host
|
|
203
226
|
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sublang/playbook",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "Reference CODE playbook — XState v5 FSM, host-agnostic runtime, and
|
|
5
|
+
"description": "Reference CODE playbook — XState v5 FSM, host-agnostic runtime, and Playbook Captain shell for a coder/reviewer/committer loop driven by GEARS spec items.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
7
|
"homepage": "https://github.com/sublang-ai/playbook#readme",
|
|
8
8
|
"repository": {
|
|
@@ -34,6 +34,12 @@
|
|
|
34
34
|
"reference/sdlc/code.playbook/code.playbook.ts",
|
|
35
35
|
"reference/sdlc/code.playbook/code.playbook.js",
|
|
36
36
|
"reference/sdlc/code.playbook/code.playbook.d.ts",
|
|
37
|
+
"reference/sdlc/code.playbook/code.registry.ts",
|
|
38
|
+
"reference/sdlc/code.playbook/code.registry.js",
|
|
39
|
+
"reference/sdlc/code.playbook/code.registry.d.ts",
|
|
40
|
+
"reference/sdlc/code.playbook/playbook-captain.ts",
|
|
41
|
+
"reference/sdlc/code.playbook/playbook-captain.js",
|
|
42
|
+
"reference/sdlc/code.playbook/playbook-captain.d.ts",
|
|
37
43
|
"reference/sdlc/code.playbook/code.tmux-play.ts",
|
|
38
44
|
"reference/sdlc/code.playbook/code.tmux-play.js",
|
|
39
45
|
"reference/sdlc/code.playbook/code.tmux-play.d.ts",
|
|
@@ -41,7 +47,7 @@
|
|
|
41
47
|
"reference/sdlc/code.playbook/tmux-play.config.yaml",
|
|
42
48
|
"reference/sdlc/code.playbook/tmux-play.production.config.yaml",
|
|
43
49
|
"reference/sdlc/code.playbook/playbook-code.config.template.yaml",
|
|
44
|
-
"reference/sdlc/code.playbook/bin"
|
|
50
|
+
"reference/sdlc/code.playbook/bin/playbook-code.js"
|
|
45
51
|
],
|
|
46
52
|
"bin": {
|
|
47
53
|
"playbook-code": "reference/sdlc/code.playbook/bin/playbook-code.js"
|
|
@@ -51,6 +57,10 @@
|
|
|
51
57
|
"types": "./reference/sdlc/code.playbook/code.playbook.d.ts",
|
|
52
58
|
"default": "./reference/sdlc/code.playbook/code.playbook.js"
|
|
53
59
|
},
|
|
60
|
+
"./playbook-captain": {
|
|
61
|
+
"types": "./reference/sdlc/code.playbook/playbook-captain.d.ts",
|
|
62
|
+
"default": "./reference/sdlc/code.playbook/playbook-captain.js"
|
|
63
|
+
},
|
|
54
64
|
"./code/tmux-play": {
|
|
55
65
|
"types": "./reference/sdlc/code.playbook/code.tmux-play.d.ts",
|
|
56
66
|
"default": "./reference/sdlc/code.playbook/code.tmux-play.js"
|
|
@@ -58,7 +68,8 @@
|
|
|
58
68
|
},
|
|
59
69
|
"scripts": {
|
|
60
70
|
"build": "tsc",
|
|
61
|
-
"test": "vitest run"
|
|
71
|
+
"test": "vitest run",
|
|
72
|
+
"playbook-code": "node reference/sdlc/code.playbook/bin/playbook-code.js"
|
|
62
73
|
},
|
|
63
74
|
"publishConfig": {
|
|
64
75
|
"access": "public",
|
|
@@ -66,9 +77,10 @@
|
|
|
66
77
|
},
|
|
67
78
|
"dependencies": {
|
|
68
79
|
"@anthropic-ai/claude-agent-sdk": "^0.3.143",
|
|
69
|
-
"@openai/codex-sdk": "^0.
|
|
80
|
+
"@openai/codex-sdk": "^0.139.0",
|
|
70
81
|
"@sublang/cligent": "latest",
|
|
71
|
-
"xstate": "^5.19.4"
|
|
82
|
+
"xstate": "^5.19.4",
|
|
83
|
+
"yaml": "^2.9.0"
|
|
72
84
|
},
|
|
73
85
|
"devDependencies": {
|
|
74
86
|
"@types/node": "^22.0.0",
|
|
@@ -8,16 +8,52 @@ import {
|
|
|
8
8
|
copyFileSync,
|
|
9
9
|
existsSync,
|
|
10
10
|
mkdirSync,
|
|
11
|
+
mkdtempSync,
|
|
11
12
|
readFileSync,
|
|
12
13
|
realpathSync,
|
|
14
|
+
rmSync,
|
|
15
|
+
writeFileSync,
|
|
13
16
|
} from 'node:fs';
|
|
14
|
-
import { homedir } from 'node:os';
|
|
17
|
+
import { homedir, tmpdir } from 'node:os';
|
|
15
18
|
import { dirname, join, resolve } from 'node:path';
|
|
16
19
|
import { fileURLToPath } from 'node:url';
|
|
20
|
+
import {
|
|
21
|
+
findTmuxPlayConfig,
|
|
22
|
+
loadTmuxPlayConfig,
|
|
23
|
+
} from '@sublang/cligent/tmux-play';
|
|
24
|
+
import {
|
|
25
|
+
parse as parseYaml,
|
|
26
|
+
parseDocument as parseYamlDocument,
|
|
27
|
+
stringify as stringifyYaml,
|
|
28
|
+
} from 'yaml';
|
|
17
29
|
|
|
18
30
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
19
31
|
const templatePath = resolve(here, '..', 'playbook-code.config.template.yaml');
|
|
20
32
|
const READINESS_FAILURE_EXIT_CODE = 2;
|
|
33
|
+
const COMPOSITION_FAILURE_EXIT_CODE = 1;
|
|
34
|
+
const DEFAULT_NOTIFICATION_BLOCK = [
|
|
35
|
+
'',
|
|
36
|
+
'# tmux-play host notifications. Omitted turn_aborted resolves to off.',
|
|
37
|
+
'notifications:',
|
|
38
|
+
' player_finished: bell',
|
|
39
|
+
' turn_finished: desktop',
|
|
40
|
+
'',
|
|
41
|
+
].join('\n');
|
|
42
|
+
|
|
43
|
+
// PBCODE-16: the composer injects `captain.from` (the Playbook
|
|
44
|
+
// Captain shell adapter module) and the `coder` / `reviewer` player
|
|
45
|
+
// ids, so the user-edited overlay carries neither.
|
|
46
|
+
export const PLAYBOOK_CAPTAIN_MODULE = '@sublang/playbook/playbook-captain';
|
|
47
|
+
export const CODE_ADAPTER_MODULE = PLAYBOOK_CAPTAIN_MODULE;
|
|
48
|
+
const CODE_ROLES = ['coder', 'reviewer'];
|
|
49
|
+
// Accepted `players` keys: the two fixed CODE roles plus the optional
|
|
50
|
+
// `committer` alias (a string naming one of the roles); PBCODE-17.
|
|
51
|
+
const CODE_PLAYER_KEYS = [...CODE_ROLES, 'committer'];
|
|
52
|
+
// Captain-judge fields inherited from a base config when the overlay
|
|
53
|
+
// leaves them unset (PBCODE-16); `adapter` is handled separately
|
|
54
|
+
// because it is required in the composed config.
|
|
55
|
+
const CAPTAIN_INHERITED_FIELDS = ['model', 'reasoningEffort', 'permissions'];
|
|
56
|
+
const PLAYER_FIELDS = ['model', 'reasoningEffort', 'permissions'];
|
|
21
57
|
|
|
22
58
|
export async function runPlaybookCodeCli(options = {}) {
|
|
23
59
|
const argv = [...(options.argv ?? process.argv.slice(2))];
|
|
@@ -27,6 +63,8 @@ export async function runPlaybookCodeCli(options = {}) {
|
|
|
27
63
|
const spawnFn = options.spawn ?? spawn;
|
|
28
64
|
const tmuxPlayBin = options.tmuxPlayBin ?? resolveTmuxPlayBin();
|
|
29
65
|
const home = options.homeDir ?? env.HOME ?? homedir();
|
|
66
|
+
const cwd = options.cwd ?? process.cwd();
|
|
67
|
+
const configHome = resolveConfigHome(env, home);
|
|
30
68
|
const userConfigPath = resolveUserConfigPath(env, home);
|
|
31
69
|
|
|
32
70
|
if (argv.includes('--help') || argv.includes('-h')) {
|
|
@@ -34,92 +72,218 @@ export async function runPlaybookCodeCli(options = {}) {
|
|
|
34
72
|
return { code: 0 };
|
|
35
73
|
}
|
|
36
74
|
|
|
37
|
-
|
|
38
|
-
|
|
75
|
+
// PBCODE-1: explicit `--config <path>` bypasses seeding, readiness,
|
|
76
|
+
// and composition — the path is launched verbatim.
|
|
77
|
+
if (hasExplicitConfig(argv)) {
|
|
78
|
+
return await launchTmuxPlay(spawnFn, [tmuxPlayBin, ...argv], stderr);
|
|
79
|
+
}
|
|
39
80
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
81
|
+
seedUserConfigIfMissing(userConfigPath, stderr);
|
|
82
|
+
migrateUserConfigNotificationsIfMissing(userConfigPath, stderr);
|
|
83
|
+
|
|
84
|
+
// PBCODE-16/17: compose the launched config from the overlay plus an
|
|
85
|
+
// optional base tmux-play config. Composition failures (missing role,
|
|
86
|
+
// non-CODE role id, missing `captain.adapter`) surface a path-named
|
|
87
|
+
// error and abort before launch.
|
|
88
|
+
let composed;
|
|
89
|
+
try {
|
|
90
|
+
composed = await composeLaunchConfig({
|
|
91
|
+
overlayPath: userConfigPath,
|
|
92
|
+
cwd,
|
|
93
|
+
configHome,
|
|
94
|
+
});
|
|
95
|
+
} catch (error) {
|
|
96
|
+
stderr.write(`playbook-code: ${errorMessage(error)}\n`);
|
|
97
|
+
return { code: COMPOSITION_FAILURE_EXIT_CODE };
|
|
57
98
|
}
|
|
58
99
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
100
|
+
// PBCODE-8: readiness reads the adapters of the composed config —
|
|
101
|
+
// including a captain adapter that may have been inherited from the
|
|
102
|
+
// base — not the raw overlay.
|
|
103
|
+
const readiness = checkReadiness(
|
|
104
|
+
adaptersFromComposedConfig(composed),
|
|
105
|
+
env,
|
|
106
|
+
home,
|
|
107
|
+
);
|
|
108
|
+
for (const adapter of readiness.unknownAdapters) {
|
|
109
|
+
stderr.write(
|
|
110
|
+
`playbook-code: warning: no readiness check for adapter "${adapter}"\n`,
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
if (readiness.failingAdapters.length > 0) {
|
|
114
|
+
stderr.write(
|
|
115
|
+
helpText({
|
|
116
|
+
userConfigPath,
|
|
117
|
+
failingAdapters: readiness.failingAdapters,
|
|
118
|
+
}),
|
|
119
|
+
);
|
|
120
|
+
return { code: READINESS_FAILURE_EXIT_CODE };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// PBCODE-16: materialize the composed config to a temp file, launch
|
|
124
|
+
// against it, and remove it before the shim exits — on normal exit,
|
|
125
|
+
// on non-zero child exit, and before re-raising a forwarded signal.
|
|
126
|
+
const { dir: tempDir, path: composedPath } = writeComposedConfig(composed);
|
|
127
|
+
try {
|
|
128
|
+
return await launchTmuxPlay(
|
|
129
|
+
spawnFn,
|
|
130
|
+
[tmuxPlayBin, '--config', composedPath, ...argv],
|
|
131
|
+
stderr,
|
|
132
|
+
);
|
|
133
|
+
} finally {
|
|
134
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function resolveConfigHome(env = process.env, home = homedir()) {
|
|
139
|
+
return env.XDG_CONFIG_HOME || join(home, '.config');
|
|
63
140
|
}
|
|
64
141
|
|
|
65
142
|
export function resolveUserConfigPath(env = process.env, home = homedir()) {
|
|
66
|
-
|
|
67
|
-
return join(configHome, 'playbook', 'playbook-code.config.yaml');
|
|
143
|
+
return join(resolveConfigHome(env, home), 'playbook', 'playbook-code.config.yaml');
|
|
68
144
|
}
|
|
69
145
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
146
|
+
// PBCODE-17: read the overlay, locate an optional base tmux-play config
|
|
147
|
+
// with cligent's exported `findTmuxPlayConfig` (never the bare
|
|
148
|
+
// `loadTmuxPlayConfig`, which writes a default config when none is
|
|
149
|
+
// found), load the base only when a path is located, and compose.
|
|
150
|
+
async function composeLaunchConfig({ overlayPath, cwd, configHome }) {
|
|
151
|
+
const overlay = parseYaml(readFileSync(overlayPath, 'utf8')) ?? {};
|
|
152
|
+
const basePath = findTmuxPlayConfig(cwd, configHome);
|
|
153
|
+
let base;
|
|
154
|
+
if (basePath) {
|
|
155
|
+
base = (await loadTmuxPlayConfig({ configPath: basePath })).config;
|
|
156
|
+
}
|
|
157
|
+
return composeRuntimeConfig(overlay, base);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Compose the launched tmux-play config (PBCODE-16/17) from the CODE
|
|
162
|
+
* overlay and an optional base tmux-play config.
|
|
163
|
+
*
|
|
164
|
+
* @returns {import('@sublang/cligent/tmux-play').TmuxPlayConfig}
|
|
165
|
+
*/
|
|
166
|
+
export function composeRuntimeConfig(
|
|
167
|
+
overlay,
|
|
168
|
+
base,
|
|
169
|
+
adapterModule = PLAYBOOK_CAPTAIN_MODULE,
|
|
170
|
+
) {
|
|
171
|
+
const overlayConfig = requireObject(overlay, 'config');
|
|
172
|
+
const overlayPlayers = requireObject(overlayConfig.players, 'players');
|
|
173
|
+
|
|
174
|
+
// Reject any players key other than the two fixed CODE roles and the
|
|
175
|
+
// optional `committer` alias.
|
|
176
|
+
for (const key of Object.keys(overlayPlayers)) {
|
|
177
|
+
if (!CODE_PLAYER_KEYS.includes(key)) {
|
|
178
|
+
throw new Error(`Unknown config field players.${key}`);
|
|
92
179
|
}
|
|
180
|
+
}
|
|
93
181
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
182
|
+
// PBCODE-17: `players.committer` is an optional string naming `coder`
|
|
183
|
+
// or `reviewer`. The composer resolves it into the composed
|
|
184
|
+
// `captain.options.code.committer` below and emits no extra
|
|
185
|
+
// `players[]` entry, so the roster stays coder + reviewer.
|
|
186
|
+
let committerAlias;
|
|
187
|
+
if (overlayPlayers.committer !== undefined) {
|
|
188
|
+
const value = overlayPlayers.committer;
|
|
189
|
+
if (!CODE_ROLES.includes(value)) {
|
|
190
|
+
throw new Error(
|
|
191
|
+
`players.committer must name one of: ${CODE_ROLES.join(', ')}`,
|
|
192
|
+
);
|
|
100
193
|
}
|
|
194
|
+
committerAlias = value;
|
|
195
|
+
}
|
|
101
196
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
197
|
+
// One composed `players[]` entry per role, with `id` = the role key
|
|
198
|
+
// and that role's required `adapter` plus optional fields.
|
|
199
|
+
const players = CODE_ROLES.map((role) => {
|
|
200
|
+
if (overlayPlayers[role] === undefined) {
|
|
201
|
+
throw new Error(`Missing required field players.${role}`);
|
|
202
|
+
}
|
|
203
|
+
const block = requireObject(overlayPlayers[role], `players.${role}`);
|
|
204
|
+
if (block.adapter === undefined) {
|
|
205
|
+
throw new Error(`Missing required field players.${role}.adapter`);
|
|
206
|
+
}
|
|
207
|
+
const entry = { id: role, adapter: block.adapter };
|
|
208
|
+
for (const field of PLAYER_FIELDS) {
|
|
209
|
+
if (block[field] !== undefined) entry[field] = block[field];
|
|
210
|
+
}
|
|
211
|
+
return entry;
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
const overlayCaptain =
|
|
215
|
+
overlayConfig.captain === undefined
|
|
216
|
+
? {}
|
|
217
|
+
: requireObject(overlayConfig.captain, 'captain');
|
|
218
|
+
const baseCaptain = isObject(base?.captain) ? base.captain : {};
|
|
219
|
+
|
|
220
|
+
// `captain.adapter` comes from the overlay when present, else the
|
|
221
|
+
// base; composition fails with a path-named error when neither
|
|
222
|
+
// supplies it. Role adapters are required in the overlay and are not
|
|
223
|
+
// inherited.
|
|
224
|
+
const captainAdapter = overlayCaptain.adapter ?? baseCaptain.adapter;
|
|
225
|
+
if (captainAdapter === undefined) {
|
|
226
|
+
throw new Error('Missing required field captain.adapter');
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const captain = { from: adapterModule, adapter: captainAdapter };
|
|
230
|
+
for (const field of CAPTAIN_INHERITED_FIELDS) {
|
|
231
|
+
const value = overlayCaptain[field] ?? baseCaptain[field];
|
|
232
|
+
if (value !== undefined) captain[field] = value;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// PBCODE-17: the shim is the sole writer of the composed
|
|
236
|
+
// `captain.options.code.committer`. Read the overlay's
|
|
237
|
+
// `captain.options.code` (carried through unchanged), reject a
|
|
238
|
+
// directly-set `committer` there, then layer the resolved
|
|
239
|
+
// `players.committer` alias on top.
|
|
240
|
+
let overlayCode;
|
|
241
|
+
if (overlayCaptain.options !== undefined) {
|
|
242
|
+
const captainOptions = requireObject(
|
|
243
|
+
overlayCaptain.options,
|
|
244
|
+
'captain.options',
|
|
245
|
+
);
|
|
246
|
+
if (captainOptions.code !== undefined) {
|
|
247
|
+
overlayCode = requireObject(captainOptions.code, 'captain.options.code');
|
|
248
|
+
if (overlayCode.committer !== undefined) {
|
|
249
|
+
throw new Error(
|
|
250
|
+
'captain.options.code.committer is composer-owned; ' +
|
|
251
|
+
'set players.committer instead',
|
|
252
|
+
);
|
|
114
253
|
}
|
|
115
254
|
}
|
|
116
255
|
}
|
|
256
|
+
if (overlayCode !== undefined || committerAlias !== undefined) {
|
|
257
|
+
const code = { ...(overlayCode ?? {}) };
|
|
258
|
+
if (committerAlias !== undefined) code.committer = committerAlias;
|
|
259
|
+
captain.options = { code };
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Inherit host-owned top-level fields from the base when the overlay
|
|
263
|
+
// omits them; never the base `players[]` roster.
|
|
264
|
+
const composed = {};
|
|
265
|
+
const theme = overlayConfig.theme ?? base?.theme;
|
|
266
|
+
if (theme !== undefined) composed.theme = theme;
|
|
267
|
+
const layout = overlayConfig.layout ?? base?.layout;
|
|
268
|
+
if (layout !== undefined) composed.layout = layout;
|
|
269
|
+
const notifications = overlayConfig.notifications ?? base?.notifications;
|
|
270
|
+
if (notifications !== undefined) composed.notifications = notifications;
|
|
271
|
+
composed.captain = captain;
|
|
272
|
+
composed.players = players;
|
|
273
|
+
return composed;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export function adaptersFromComposedConfig(config) {
|
|
277
|
+
const adapters = new Set();
|
|
278
|
+
const captainAdapter = config?.captain?.adapter;
|
|
279
|
+
if (captainAdapter) adapters.add(captainAdapter);
|
|
280
|
+
for (const player of config?.players ?? []) {
|
|
281
|
+
if (player?.adapter) adapters.add(player.adapter);
|
|
282
|
+
}
|
|
117
283
|
return [...adapters];
|
|
118
284
|
}
|
|
119
285
|
|
|
120
|
-
export function checkReadiness(
|
|
121
|
-
const config = readFileSync(configPath, 'utf8');
|
|
122
|
-
const adapters = collectAdaptersFromConfig(config);
|
|
286
|
+
export function checkReadiness(adapters, env = process.env, home = homedir()) {
|
|
123
287
|
const failingAdapters = [];
|
|
124
288
|
const unknownAdapters = [];
|
|
125
289
|
|
|
@@ -142,6 +306,15 @@ export function checkReadiness(configPath, env = process.env, home = homedir())
|
|
|
142
306
|
return { failingAdapters, unknownAdapters };
|
|
143
307
|
}
|
|
144
308
|
|
|
309
|
+
function writeComposedConfig(composed) {
|
|
310
|
+
const dir = mkdtempSync(join(tmpdir(), 'playbook-code-'));
|
|
311
|
+
// cligent's loader only accepts a `.yaml` extension; name the temp
|
|
312
|
+
// file accordingly.
|
|
313
|
+
const path = join(dir, 'tmux-play.config.yaml');
|
|
314
|
+
writeFileSync(path, stringifyYaml(composed));
|
|
315
|
+
return { dir, path };
|
|
316
|
+
}
|
|
317
|
+
|
|
145
318
|
function seedUserConfigIfMissing(userConfigPath, stderr) {
|
|
146
319
|
if (existsSync(userConfigPath)) return;
|
|
147
320
|
mkdirSync(dirname(userConfigPath), { recursive: true });
|
|
@@ -149,6 +322,27 @@ function seedUserConfigIfMissing(userConfigPath, stderr) {
|
|
|
149
322
|
stderr.write(`playbook-code: created config at ${userConfigPath}\n`);
|
|
150
323
|
}
|
|
151
324
|
|
|
325
|
+
function migrateUserConfigNotificationsIfMissing(userConfigPath, stderr) {
|
|
326
|
+
const source = readFileSync(userConfigPath, 'utf8');
|
|
327
|
+
let parsed;
|
|
328
|
+
try {
|
|
329
|
+
const document = parseYamlDocument(source);
|
|
330
|
+
if (document.errors.length > 0) return;
|
|
331
|
+
parsed = document.contents === null ? {} : document.toJS();
|
|
332
|
+
} catch {
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
if (!isObject(parsed) || hasOwn(parsed, 'notifications')) return;
|
|
336
|
+
const separator = source.endsWith('\n') ? '' : '\n';
|
|
337
|
+
writeFileSync(
|
|
338
|
+
userConfigPath,
|
|
339
|
+
`${source}${separator}${DEFAULT_NOTIFICATION_BLOCK}`,
|
|
340
|
+
);
|
|
341
|
+
stderr.write(
|
|
342
|
+
`playbook-code: added notifications defaults to config at ${userConfigPath}\n`,
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
|
|
152
346
|
function hasExplicitConfig(argv) {
|
|
153
347
|
return argv.some((arg) => arg === '--config' || arg.startsWith('--config='));
|
|
154
348
|
}
|
|
@@ -175,10 +369,11 @@ function helpText({ userConfigPath, failingAdapters = [] }) {
|
|
|
175
369
|
'',
|
|
176
370
|
'Agent swap recipe:',
|
|
177
371
|
' - change captain.adapter and captain.model for the Captain/Judge',
|
|
178
|
-
' - change
|
|
179
|
-
' and Reviewer; model when pinned, else adapter, is
|
|
180
|
-
' into <coder-llm>/<reviewer-llm> player prompts (PBRT-4)',
|
|
181
|
-
' -
|
|
372
|
+
' - change adapter and model under players.coder and players.reviewer',
|
|
373
|
+
' for the Coder and Reviewer; model when pinned, else adapter, is',
|
|
374
|
+
' substituted into <coder-llm>/<reviewer-llm> player prompts (PBRT-4)',
|
|
375
|
+
' - the composer injects captain.from; the role keys coder and',
|
|
376
|
+
' reviewer are fixed',
|
|
182
377
|
'',
|
|
183
378
|
].join('\n');
|
|
184
379
|
}
|
|
@@ -220,41 +415,19 @@ function resolveTmuxPlayBin() {
|
|
|
220
415
|
return join(dirname(fileURLToPath(tmuxPlayIndexUrl)), 'cli.js');
|
|
221
416
|
}
|
|
222
417
|
|
|
223
|
-
function
|
|
224
|
-
|
|
225
|
-
for (let index = 0; index < line.length; index += 1) {
|
|
226
|
-
const char = line[index];
|
|
227
|
-
if ((char === '"' || char === "'") && line[index - 1] !== '\\') {
|
|
228
|
-
quote = quote === char ? '' : quote || char;
|
|
229
|
-
continue;
|
|
230
|
-
}
|
|
231
|
-
if (char === '#' && !quote) {
|
|
232
|
-
return line.slice(0, index);
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
return line;
|
|
418
|
+
function isObject(value) {
|
|
419
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
236
420
|
}
|
|
237
421
|
|
|
238
|
-
function
|
|
239
|
-
|
|
240
|
-
if (
|
|
241
|
-
(trimmed.startsWith('"') && trimmed.endsWith('"')) ||
|
|
242
|
-
(trimmed.startsWith("'") && trimmed.endsWith("'"))
|
|
243
|
-
) {
|
|
244
|
-
return trimmed.slice(1, -1).trim();
|
|
245
|
-
}
|
|
246
|
-
return trimmed;
|
|
422
|
+
function hasOwn(value, key) {
|
|
423
|
+
return Object.prototype.hasOwnProperty.call(value, key);
|
|
247
424
|
}
|
|
248
425
|
|
|
249
|
-
function
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
function leadingSpaceCount(line) {
|
|
257
|
-
return line.length - line.trimStart().length;
|
|
426
|
+
function requireObject(value, path) {
|
|
427
|
+
if (!isObject(value)) {
|
|
428
|
+
throw new Error(`${path} must be an object`);
|
|
429
|
+
}
|
|
430
|
+
return value;
|
|
258
431
|
}
|
|
259
432
|
|
|
260
433
|
function errorMessage(error) {
|