@sublang/playbook 0.4.2 → 0.5.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 +27 -15
- package/package.json +6 -4
- package/reference/sdlc/code.playbook/bin/playbook-code.js +248 -105
- package/reference/sdlc/code.playbook/code.fsm.d.ts +1 -0
- package/reference/sdlc/code.playbook/code.fsm.js +51 -21
- package/reference/sdlc/code.playbook/code.fsm.ts +58 -21
- package/reference/sdlc/code.playbook/code.gears.md +50 -22
- 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.tmux-play.d.ts +4 -0
- package/reference/sdlc/code.playbook/code.tmux-play.js +75 -12
- package/reference/sdlc/code.playbook/code.tmux-play.ts +88 -12
- package/reference/sdlc/code.playbook/playbook-code.config.template.yaml +41 -25
package/README.md
CHANGED
|
@@ -82,7 +82,10 @@ playbook-code --help
|
|
|
82
82
|
```
|
|
83
83
|
|
|
84
84
|
The seed template runs each agent in cligent's protected auto mode
|
|
85
|
-
(`permissions.mode: auto`), suppressing routine approval prompts.
|
|
85
|
+
(`permissions.mode: auto`), suppressing routine approval prompts. Its
|
|
86
|
+
Codex Coder also grants `permissions.writablePaths: [.git]` so git
|
|
87
|
+
metadata writes stay available under auto mode without switching to
|
|
88
|
+
bypass permissions.
|
|
86
89
|
|
|
87
90
|
### Configure agents
|
|
88
91
|
|
|
@@ -95,20 +98,26 @@ $EDITOR "${XDG_CONFIG_HOME:-$HOME/.config}/playbook/playbook-code.config.yaml"
|
|
|
95
98
|
Both CODE players can use `claude` or `codex`; other adapter ids are
|
|
96
99
|
passed through to `tmux-play` with a warning because `playbook-code`
|
|
97
100
|
does not know how to preflight their auth. The safe tuning points are
|
|
98
|
-
`captain.adapter`, `captain.model`, each
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
101
|
+
`captain.adapter`, `captain.model`, and each role's `adapter` and
|
|
102
|
+
`model` under `players.coder` / `players.reviewer`. The composer owns
|
|
103
|
+
`captain.from` and the `coder` / `reviewer` role keys, so leave those
|
|
104
|
+
keys as-is; the runtime binds to those host-configuration invariants
|
|
105
|
+
per [PBRT-4](specs/user/playbook-runtime.md#pbrt-4) and derives the
|
|
106
|
+
`<coder-llm>` / `<reviewer-llm>` substitution strings from each role's
|
|
107
|
+
`model` when pinned and `adapter` otherwise — so the Committer's
|
|
108
|
+
commit-message trailers can name the concrete model
|
|
109
|
+
(e.g. `claude-opus-4-8`) rather than the adapter family (`claude`).
|
|
110
|
+
|
|
111
|
+
`players.committer` is an optional alias naming which role — `coder`
|
|
112
|
+
or `reviewer` — runs the commit turn; the seeded overlay points it at
|
|
113
|
+
the Reviewer. Absent the alias the Committer falls back to the Coder
|
|
114
|
+
([PBRT-8](specs/user/playbook-runtime.md#pbrt-8)).
|
|
115
|
+
|
|
116
|
+
For example, the seeded overlay runs the Coder on Codex and the
|
|
117
|
+
Reviewer on Claude, with the Committer aliased to the Reviewer:
|
|
108
118
|
|
|
109
119
|
```yaml
|
|
110
120
|
captain:
|
|
111
|
-
from: "@sublang/playbook/code/tmux-play"
|
|
112
121
|
adapter: claude
|
|
113
122
|
model: claude-sonnet-4-6
|
|
114
123
|
reasoningEffort: high
|
|
@@ -116,18 +125,21 @@ captain:
|
|
|
116
125
|
mode: auto
|
|
117
126
|
|
|
118
127
|
players:
|
|
119
|
-
|
|
128
|
+
coder: # role key must stay `coder` — see PBRT-4
|
|
120
129
|
adapter: codex
|
|
121
130
|
model: gpt-5.5
|
|
122
131
|
reasoningEffort: xhigh
|
|
123
132
|
permissions:
|
|
124
133
|
mode: auto
|
|
125
|
-
|
|
134
|
+
writablePaths:
|
|
135
|
+
- .git # allow git metadata writes under Codex auto mode
|
|
136
|
+
reviewer: # role key must stay `reviewer` — see PBRT-4
|
|
126
137
|
adapter: claude
|
|
127
|
-
model: claude-opus-4-
|
|
138
|
+
model: claude-opus-4-8
|
|
128
139
|
reasoningEffort: xhigh
|
|
129
140
|
permissions:
|
|
130
141
|
mode: auto
|
|
142
|
+
committer: reviewer # which role commits — `coder` or `reviewer`
|
|
131
143
|
```
|
|
132
144
|
|
|
133
145
|
Normal `playbook-code` runs use the seeded path above. If you need a
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sublang/playbook",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Reference CODE playbook — XState v5 FSM, host-agnostic runtime, and tmux-play adapter for a coder/reviewer/committer loop driven by GEARS spec items.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"reference/sdlc/code.playbook/tmux-play.config.yaml",
|
|
42
42
|
"reference/sdlc/code.playbook/tmux-play.production.config.yaml",
|
|
43
43
|
"reference/sdlc/code.playbook/playbook-code.config.template.yaml",
|
|
44
|
-
"reference/sdlc/code.playbook/bin"
|
|
44
|
+
"reference/sdlc/code.playbook/bin/playbook-code.js"
|
|
45
45
|
],
|
|
46
46
|
"bin": {
|
|
47
47
|
"playbook-code": "reference/sdlc/code.playbook/bin/playbook-code.js"
|
|
@@ -58,7 +58,8 @@
|
|
|
58
58
|
},
|
|
59
59
|
"scripts": {
|
|
60
60
|
"build": "tsc",
|
|
61
|
-
"test": "vitest run"
|
|
61
|
+
"test": "vitest run",
|
|
62
|
+
"playbook-code": "node reference/sdlc/code.playbook/bin/playbook-code.js"
|
|
62
63
|
},
|
|
63
64
|
"publishConfig": {
|
|
64
65
|
"access": "public",
|
|
@@ -68,7 +69,8 @@
|
|
|
68
69
|
"@anthropic-ai/claude-agent-sdk": "^0.3.143",
|
|
69
70
|
"@openai/codex-sdk": "^0.130.0",
|
|
70
71
|
"@sublang/cligent": "latest",
|
|
71
|
-
"xstate": "^5.19.4"
|
|
72
|
+
"xstate": "^5.19.4",
|
|
73
|
+
"yaml": "^2.9.0"
|
|
72
74
|
},
|
|
73
75
|
"devDependencies": {
|
|
74
76
|
"@types/node": "^22.0.0",
|
|
@@ -8,16 +8,39 @@ 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 { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
|
|
17
25
|
|
|
18
26
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
19
27
|
const templatePath = resolve(here, '..', 'playbook-code.config.template.yaml');
|
|
20
28
|
const READINESS_FAILURE_EXIT_CODE = 2;
|
|
29
|
+
const COMPOSITION_FAILURE_EXIT_CODE = 1;
|
|
30
|
+
|
|
31
|
+
// PBCODE-16: the composer injects `captain.from` (the CODE adapter
|
|
32
|
+
// module) and the `coder` / `reviewer` player ids, so the user-edited
|
|
33
|
+
// overlay carries neither.
|
|
34
|
+
export const CODE_ADAPTER_MODULE = '@sublang/playbook/code/tmux-play';
|
|
35
|
+
const CODE_ROLES = ['coder', 'reviewer'];
|
|
36
|
+
// Accepted `players` keys: the two fixed CODE roles plus the optional
|
|
37
|
+
// `committer` alias (a string naming one of the roles); PBCODE-17.
|
|
38
|
+
const CODE_PLAYER_KEYS = [...CODE_ROLES, 'committer'];
|
|
39
|
+
// Captain-judge fields inherited from a base config when the overlay
|
|
40
|
+
// leaves them unset (PBCODE-16); `adapter` is handled separately
|
|
41
|
+
// because it is required in the composed config.
|
|
42
|
+
const CAPTAIN_INHERITED_FIELDS = ['model', 'reasoningEffort', 'permissions'];
|
|
43
|
+
const PLAYER_FIELDS = ['model', 'reasoningEffort', 'permissions'];
|
|
21
44
|
|
|
22
45
|
export async function runPlaybookCodeCli(options = {}) {
|
|
23
46
|
const argv = [...(options.argv ?? process.argv.slice(2))];
|
|
@@ -27,6 +50,8 @@ export async function runPlaybookCodeCli(options = {}) {
|
|
|
27
50
|
const spawnFn = options.spawn ?? spawn;
|
|
28
51
|
const tmuxPlayBin = options.tmuxPlayBin ?? resolveTmuxPlayBin();
|
|
29
52
|
const home = options.homeDir ?? env.HOME ?? homedir();
|
|
53
|
+
const cwd = options.cwd ?? process.cwd();
|
|
54
|
+
const configHome = resolveConfigHome(env, home);
|
|
30
55
|
const userConfigPath = resolveUserConfigPath(env, home);
|
|
31
56
|
|
|
32
57
|
if (argv.includes('--help') || argv.includes('-h')) {
|
|
@@ -34,92 +59,226 @@ export async function runPlaybookCodeCli(options = {}) {
|
|
|
34
59
|
return { code: 0 };
|
|
35
60
|
}
|
|
36
61
|
|
|
37
|
-
|
|
38
|
-
|
|
62
|
+
// PBCODE-1: explicit `--config <path>` bypasses seeding, readiness,
|
|
63
|
+
// and composition — the path is launched verbatim.
|
|
64
|
+
if (hasExplicitConfig(argv)) {
|
|
65
|
+
return await launchTmuxPlay(spawnFn, [tmuxPlayBin, ...argv], stderr);
|
|
66
|
+
}
|
|
39
67
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
68
|
+
seedUserConfigIfMissing(userConfigPath, stderr);
|
|
69
|
+
|
|
70
|
+
// PBCODE-16/17: compose the launched config from the overlay plus an
|
|
71
|
+
// optional base tmux-play config. Composition failures (missing role,
|
|
72
|
+
// non-CODE role id, missing `captain.adapter`) surface a path-named
|
|
73
|
+
// error and abort before launch.
|
|
74
|
+
let composed;
|
|
75
|
+
try {
|
|
76
|
+
composed = await composeLaunchConfig({
|
|
77
|
+
overlayPath: userConfigPath,
|
|
78
|
+
cwd,
|
|
79
|
+
configHome,
|
|
80
|
+
});
|
|
81
|
+
} catch (error) {
|
|
82
|
+
stderr.write(`playbook-code: ${errorMessage(error)}\n`);
|
|
83
|
+
return { code: COMPOSITION_FAILURE_EXIT_CODE };
|
|
57
84
|
}
|
|
58
85
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
86
|
+
// PBCODE-8: readiness reads the adapters of the composed config —
|
|
87
|
+
// including a captain adapter that may have been inherited from the
|
|
88
|
+
// base — not the raw overlay.
|
|
89
|
+
const readiness = checkReadiness(
|
|
90
|
+
adaptersFromComposedConfig(composed),
|
|
91
|
+
env,
|
|
92
|
+
home,
|
|
93
|
+
);
|
|
94
|
+
for (const adapter of readiness.unknownAdapters) {
|
|
95
|
+
stderr.write(
|
|
96
|
+
`playbook-code: warning: no readiness check for adapter "${adapter}"\n`,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
if (readiness.failingAdapters.length > 0) {
|
|
100
|
+
stderr.write(
|
|
101
|
+
helpText({
|
|
102
|
+
userConfigPath,
|
|
103
|
+
failingAdapters: readiness.failingAdapters,
|
|
104
|
+
}),
|
|
105
|
+
);
|
|
106
|
+
return { code: READINESS_FAILURE_EXIT_CODE };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// PBCODE-16: materialize the composed config to a temp file, launch
|
|
110
|
+
// against it, and remove it before the shim exits — on normal exit,
|
|
111
|
+
// on non-zero child exit, and before re-raising a forwarded signal.
|
|
112
|
+
const { dir: tempDir, path: composedPath } = writeComposedConfig(composed);
|
|
113
|
+
try {
|
|
114
|
+
return await launchTmuxPlay(
|
|
115
|
+
spawnFn,
|
|
116
|
+
[tmuxPlayBin, '--config', composedPath, ...argv],
|
|
117
|
+
stderr,
|
|
118
|
+
);
|
|
119
|
+
} finally {
|
|
120
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function resolveConfigHome(env = process.env, home = homedir()) {
|
|
125
|
+
return env.XDG_CONFIG_HOME || join(home, '.config');
|
|
63
126
|
}
|
|
64
127
|
|
|
65
128
|
export function resolveUserConfigPath(env = process.env, home = homedir()) {
|
|
66
|
-
|
|
67
|
-
return join(configHome, 'playbook', 'playbook-code.config.yaml');
|
|
129
|
+
return join(resolveConfigHome(env, home), 'playbook', 'playbook-code.config.yaml');
|
|
68
130
|
}
|
|
69
131
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
playerChildIndent = undefined;
|
|
91
|
-
continue;
|
|
132
|
+
// PBCODE-17: read the overlay, locate an optional base tmux-play config
|
|
133
|
+
// with cligent's exported `findTmuxPlayConfig` (never the bare
|
|
134
|
+
// `loadTmuxPlayConfig`, which writes a default config when none is
|
|
135
|
+
// found), load the base only when a path is located, and compose.
|
|
136
|
+
async function composeLaunchConfig({ overlayPath, cwd, configHome }) {
|
|
137
|
+
const overlay = parseYaml(readFileSync(overlayPath, 'utf8')) ?? {};
|
|
138
|
+
const basePath = findTmuxPlayConfig(cwd, configHome);
|
|
139
|
+
let base;
|
|
140
|
+
if (basePath) {
|
|
141
|
+
base = (await loadTmuxPlayConfig({ configPath: basePath })).config;
|
|
142
|
+
// cligent's loader normalizes a discovered base to captain / players
|
|
143
|
+
// / theme and drops host fields it does not model yet — notably
|
|
144
|
+
// `layout`. Recover a base `layout` from the raw base YAML so base
|
|
145
|
+
// inheritance (PBCODE-16/17) holds; the `=== undefined` guard makes
|
|
146
|
+
// this inert once a future loader preserves `layout` itself.
|
|
147
|
+
if (base && base.layout === undefined) {
|
|
148
|
+
const rawBase = parseYaml(readFileSync(basePath, 'utf8'));
|
|
149
|
+
if (isObject(rawBase) && rawBase.layout !== undefined) {
|
|
150
|
+
base.layout = rawBase.layout;
|
|
151
|
+
}
|
|
92
152
|
}
|
|
153
|
+
}
|
|
154
|
+
return composeRuntimeConfig(overlay, base);
|
|
155
|
+
}
|
|
93
156
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
157
|
+
/**
|
|
158
|
+
* Compose the launched tmux-play config (PBCODE-16/17) from the CODE
|
|
159
|
+
* overlay and an optional base tmux-play config.
|
|
160
|
+
*
|
|
161
|
+
* @returns {import('@sublang/cligent/tmux-play').TmuxPlayConfig}
|
|
162
|
+
*/
|
|
163
|
+
export function composeRuntimeConfig(
|
|
164
|
+
overlay,
|
|
165
|
+
base,
|
|
166
|
+
adapterModule = CODE_ADAPTER_MODULE,
|
|
167
|
+
) {
|
|
168
|
+
const overlayConfig = requireObject(overlay, 'config');
|
|
169
|
+
const overlayPlayers = requireObject(overlayConfig.players, 'players');
|
|
170
|
+
|
|
171
|
+
// Reject any players key other than the two fixed CODE roles and the
|
|
172
|
+
// optional `committer` alias.
|
|
173
|
+
for (const key of Object.keys(overlayPlayers)) {
|
|
174
|
+
if (!CODE_PLAYER_KEYS.includes(key)) {
|
|
175
|
+
throw new Error(`Unknown config field players.${key}`);
|
|
100
176
|
}
|
|
177
|
+
}
|
|
101
178
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
179
|
+
// PBCODE-17: `players.committer` is an optional string naming `coder`
|
|
180
|
+
// or `reviewer`. The composer resolves it into the composed
|
|
181
|
+
// `captain.options.code.committer` below and emits no extra
|
|
182
|
+
// `players[]` entry, so the roster stays coder + reviewer.
|
|
183
|
+
let committerAlias;
|
|
184
|
+
if (overlayPlayers.committer !== undefined) {
|
|
185
|
+
const value = overlayPlayers.committer;
|
|
186
|
+
if (!CODE_ROLES.includes(value)) {
|
|
187
|
+
throw new Error(
|
|
188
|
+
`players.committer must name one of: ${CODE_ROLES.join(', ')}`,
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
committerAlias = value;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// One composed `players[]` entry per role, with `id` = the role key
|
|
195
|
+
// and that role's required `adapter` plus optional fields.
|
|
196
|
+
const players = CODE_ROLES.map((role) => {
|
|
197
|
+
if (overlayPlayers[role] === undefined) {
|
|
198
|
+
throw new Error(`Missing required field players.${role}`);
|
|
199
|
+
}
|
|
200
|
+
const block = requireObject(overlayPlayers[role], `players.${role}`);
|
|
201
|
+
if (block.adapter === undefined) {
|
|
202
|
+
throw new Error(`Missing required field players.${role}.adapter`);
|
|
203
|
+
}
|
|
204
|
+
const entry = { id: role, adapter: block.adapter };
|
|
205
|
+
for (const field of PLAYER_FIELDS) {
|
|
206
|
+
if (block[field] !== undefined) entry[field] = block[field];
|
|
207
|
+
}
|
|
208
|
+
return entry;
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
const overlayCaptain =
|
|
212
|
+
overlayConfig.captain === undefined
|
|
213
|
+
? {}
|
|
214
|
+
: requireObject(overlayConfig.captain, 'captain');
|
|
215
|
+
const baseCaptain = isObject(base?.captain) ? base.captain : {};
|
|
216
|
+
|
|
217
|
+
// `captain.adapter` comes from the overlay when present, else the
|
|
218
|
+
// base; composition fails with a path-named error when neither
|
|
219
|
+
// supplies it. Role adapters are required in the overlay and are not
|
|
220
|
+
// inherited.
|
|
221
|
+
const captainAdapter = overlayCaptain.adapter ?? baseCaptain.adapter;
|
|
222
|
+
if (captainAdapter === undefined) {
|
|
223
|
+
throw new Error('Missing required field captain.adapter');
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const captain = { from: adapterModule, adapter: captainAdapter };
|
|
227
|
+
for (const field of CAPTAIN_INHERITED_FIELDS) {
|
|
228
|
+
const value = overlayCaptain[field] ?? baseCaptain[field];
|
|
229
|
+
if (value !== undefined) captain[field] = value;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// PBCODE-17: the shim is the sole writer of the composed
|
|
233
|
+
// `captain.options.code.committer`. Read the overlay's
|
|
234
|
+
// `captain.options.code` (carried through unchanged), reject a
|
|
235
|
+
// directly-set `committer` there, then layer the resolved
|
|
236
|
+
// `players.committer` alias on top.
|
|
237
|
+
let overlayCode;
|
|
238
|
+
if (overlayCaptain.options !== undefined) {
|
|
239
|
+
const captainOptions = requireObject(
|
|
240
|
+
overlayCaptain.options,
|
|
241
|
+
'captain.options',
|
|
242
|
+
);
|
|
243
|
+
if (captainOptions.code !== undefined) {
|
|
244
|
+
overlayCode = requireObject(captainOptions.code, 'captain.options.code');
|
|
245
|
+
if (overlayCode.committer !== undefined) {
|
|
246
|
+
throw new Error(
|
|
247
|
+
'captain.options.code.committer is composer-owned; ' +
|
|
248
|
+
'set players.committer instead',
|
|
249
|
+
);
|
|
114
250
|
}
|
|
115
251
|
}
|
|
116
252
|
}
|
|
253
|
+
if (overlayCode !== undefined || committerAlias !== undefined) {
|
|
254
|
+
const code = { ...(overlayCode ?? {}) };
|
|
255
|
+
if (committerAlias !== undefined) code.committer = committerAlias;
|
|
256
|
+
captain.options = { code };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Inherit `theme` and `layout` from the base when the overlay omits
|
|
260
|
+
// them; never the base `players[]` roster.
|
|
261
|
+
const composed = {};
|
|
262
|
+
const theme = overlayConfig.theme ?? base?.theme;
|
|
263
|
+
if (theme !== undefined) composed.theme = theme;
|
|
264
|
+
const layout = overlayConfig.layout ?? base?.layout;
|
|
265
|
+
if (layout !== undefined) composed.layout = layout;
|
|
266
|
+
composed.captain = captain;
|
|
267
|
+
composed.players = players;
|
|
268
|
+
return composed;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export function adaptersFromComposedConfig(config) {
|
|
272
|
+
const adapters = new Set();
|
|
273
|
+
const captainAdapter = config?.captain?.adapter;
|
|
274
|
+
if (captainAdapter) adapters.add(captainAdapter);
|
|
275
|
+
for (const player of config?.players ?? []) {
|
|
276
|
+
if (player?.adapter) adapters.add(player.adapter);
|
|
277
|
+
}
|
|
117
278
|
return [...adapters];
|
|
118
279
|
}
|
|
119
280
|
|
|
120
|
-
export function checkReadiness(
|
|
121
|
-
const config = readFileSync(configPath, 'utf8');
|
|
122
|
-
const adapters = collectAdaptersFromConfig(config);
|
|
281
|
+
export function checkReadiness(adapters, env = process.env, home = homedir()) {
|
|
123
282
|
const failingAdapters = [];
|
|
124
283
|
const unknownAdapters = [];
|
|
125
284
|
|
|
@@ -142,6 +301,15 @@ export function checkReadiness(configPath, env = process.env, home = homedir())
|
|
|
142
301
|
return { failingAdapters, unknownAdapters };
|
|
143
302
|
}
|
|
144
303
|
|
|
304
|
+
function writeComposedConfig(composed) {
|
|
305
|
+
const dir = mkdtempSync(join(tmpdir(), 'playbook-code-'));
|
|
306
|
+
// cligent's loader only accepts a `.yaml` extension; name the temp
|
|
307
|
+
// file accordingly.
|
|
308
|
+
const path = join(dir, 'tmux-play.config.yaml');
|
|
309
|
+
writeFileSync(path, stringifyYaml(composed));
|
|
310
|
+
return { dir, path };
|
|
311
|
+
}
|
|
312
|
+
|
|
145
313
|
function seedUserConfigIfMissing(userConfigPath, stderr) {
|
|
146
314
|
if (existsSync(userConfigPath)) return;
|
|
147
315
|
mkdirSync(dirname(userConfigPath), { recursive: true });
|
|
@@ -175,10 +343,11 @@ function helpText({ userConfigPath, failingAdapters = [] }) {
|
|
|
175
343
|
'',
|
|
176
344
|
'Agent swap recipe:',
|
|
177
345
|
' - 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
|
-
' -
|
|
346
|
+
' - change adapter and model under players.coder and players.reviewer',
|
|
347
|
+
' for the Coder and Reviewer; model when pinned, else adapter, is',
|
|
348
|
+
' substituted into <coder-llm>/<reviewer-llm> player prompts (PBRT-4)',
|
|
349
|
+
' - the composer injects captain.from; the role keys coder and',
|
|
350
|
+
' reviewer are fixed',
|
|
182
351
|
'',
|
|
183
352
|
].join('\n');
|
|
184
353
|
}
|
|
@@ -220,41 +389,15 @@ function resolveTmuxPlayBin() {
|
|
|
220
389
|
return join(dirname(fileURLToPath(tmuxPlayIndexUrl)), 'cli.js');
|
|
221
390
|
}
|
|
222
391
|
|
|
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;
|
|
392
|
+
function isObject(value) {
|
|
393
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
236
394
|
}
|
|
237
395
|
|
|
238
|
-
function
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
(trimmed.startsWith('"') && trimmed.endsWith('"')) ||
|
|
242
|
-
(trimmed.startsWith("'") && trimmed.endsWith("'"))
|
|
243
|
-
) {
|
|
244
|
-
return trimmed.slice(1, -1).trim();
|
|
396
|
+
function requireObject(value, path) {
|
|
397
|
+
if (!isObject(value)) {
|
|
398
|
+
throw new Error(`${path} must be an object`);
|
|
245
399
|
}
|
|
246
|
-
return
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
function collectAdapterValue(trimmedLine, adapters) {
|
|
250
|
-
const match = trimmedLine.match(/^adapter\s*:\s*(.+?)\s*$/);
|
|
251
|
-
if (!match) return;
|
|
252
|
-
const value = unquoteYamlScalar(match[1].trim());
|
|
253
|
-
if (value) adapters.add(value);
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
function leadingSpaceCount(line) {
|
|
257
|
-
return line.length - line.trimStart().length;
|
|
400
|
+
return value;
|
|
258
401
|
}
|
|
259
402
|
|
|
260
403
|
function errorMessage(error) {
|