@sublang/playbook 5.0.0 → 7.0.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 +22 -14
- package/docs/cli.md +138 -90
- package/docs/configuration.md +113 -29
- package/docs/embedding.md +24 -16
- package/package.json +42 -21
- package/reference/sdlc/captain.playbook/captain.playbook.js +2 -0
- package/reference/sdlc/captain.playbook/captain.playbook.ts +2 -0
- package/reference/sdlc/code.md +55 -97
- package/reference/sdlc/code.playbook/bin/launch-config.js +938 -0
- package/reference/sdlc/code.playbook/bin/playbook.js +145 -562
- package/reference/sdlc/code.playbook/bin/provision.js +84 -38
- package/reference/sdlc/code.playbook/bin/run.js +1171 -983
- package/reference/sdlc/code.playbook/bin/session-store.js +1169 -0
- package/reference/sdlc/code.playbook/code.fsm.d.ts +229 -94
- package/reference/sdlc/code.playbook/code.fsm.introspect.d.ts +26 -44
- package/reference/sdlc/code.playbook/code.fsm.introspect.js +61 -66
- package/reference/sdlc/code.playbook/code.fsm.introspect.ts +100 -149
- package/reference/sdlc/code.playbook/code.fsm.js +587 -1347
- package/reference/sdlc/code.playbook/code.fsm.ts +809 -1650
- package/reference/sdlc/code.playbook/code.gears.md +51 -263
- package/reference/sdlc/code.playbook/code.playbook.d.ts +8 -47
- package/reference/sdlc/code.playbook/code.playbook.js +69 -656
- package/reference/sdlc/code.playbook/code.playbook.ts +90 -867
- package/reference/sdlc/code.playbook/code.registry.d.ts +9 -25
- package/reference/sdlc/code.playbook/code.registry.js +20 -78
- package/reference/sdlc/code.playbook/code.registry.ts +58 -122
- package/reference/sdlc/code.playbook/playbook-captain.d.ts +70 -3
- package/reference/sdlc/code.playbook/playbook-captain.js +954 -80
- package/reference/sdlc/code.playbook/playbook-captain.ts +1408 -80
- package/reference/sdlc/code.playbook/playbook.config.template.yaml +37 -36
- package/reference/sdlc/decide.md +54 -0
- package/reference/sdlc/decide.playbook/decide.fsm.d.ts +261 -0
- package/reference/sdlc/decide.playbook/decide.fsm.js +894 -0
- package/reference/sdlc/decide.playbook/decide.fsm.ts +1152 -0
- package/reference/sdlc/decide.playbook/decide.gears.md +88 -0
- package/reference/sdlc/decide.playbook/decide.playbook.d.ts +67 -0
- package/reference/sdlc/{discuss.playbook/discuss.playbook.js → decide.playbook/decide.playbook.js} +545 -372
- package/reference/sdlc/{discuss.playbook/discuss.playbook.ts → decide.playbook/decide.playbook.ts} +665 -454
- package/reference/sdlc/decide.playbook/decide.registry.d.ts +41 -0
- package/reference/sdlc/decide.playbook/decide.registry.js +60 -0
- package/reference/sdlc/decide.playbook/decide.registry.ts +125 -0
- package/reference/sdlc/review.md +81 -0
- package/reference/sdlc/review.playbook/review.fsm.d.ts +183 -0
- package/reference/sdlc/review.playbook/review.fsm.js +524 -0
- package/reference/sdlc/review.playbook/review.fsm.ts +652 -0
- package/reference/sdlc/review.playbook/review.gears.md +112 -0
- package/reference/sdlc/review.playbook/review.playbook.d.ts +12 -0
- package/reference/sdlc/review.playbook/review.playbook.js +112 -0
- package/reference/sdlc/review.playbook/review.playbook.ts +201 -0
- package/reference/sdlc/review.playbook/review.registry.d.ts +43 -0
- package/reference/sdlc/review.playbook/review.registry.js +73 -0
- package/reference/sdlc/review.playbook/review.registry.ts +138 -0
- package/slc/gears2fsm.md +13 -4
- package/slc/link.md +83 -14
- package/slc/text2gears.md +22 -2
- package/src/runtime.d.ts +21 -2
- package/src/runtime.ts +38 -6
- package/src/xstate-playbook-runtime.d.ts +9 -2
- package/src/xstate-playbook-runtime.js +319 -35
- package/src/xstate-playbook-runtime.ts +412 -41
- package/src/xstate-runtime.d.ts +19 -2
- package/src/xstate-runtime.js +384 -57
- package/src/xstate-runtime.ts +542 -71
- package/reference/sdlc/discuss.md +0 -93
- package/reference/sdlc/discuss.playbook/discuss.fsm.d.ts +0 -396
- package/reference/sdlc/discuss.playbook/discuss.fsm.js +0 -2067
- package/reference/sdlc/discuss.playbook/discuss.fsm.ts +0 -2465
- package/reference/sdlc/discuss.playbook/discuss.gears.md +0 -258
- package/reference/sdlc/discuss.playbook/discuss.playbook.d.ts +0 -113
- package/reference/sdlc/discuss.playbook/discuss.registry.d.ts +0 -58
- package/reference/sdlc/discuss.playbook/discuss.registry.js +0 -97
- package/reference/sdlc/discuss.playbook/discuss.registry.ts +0 -153
|
@@ -4,47 +4,53 @@
|
|
|
4
4
|
|
|
5
5
|
import { spawn } from 'node:child_process';
|
|
6
6
|
import {
|
|
7
|
-
constants,
|
|
8
|
-
copyFileSync,
|
|
9
|
-
existsSync,
|
|
10
|
-
mkdirSync,
|
|
11
7
|
mkdtempSync,
|
|
12
|
-
readFileSync,
|
|
13
8
|
realpathSync,
|
|
14
9
|
rmSync,
|
|
15
10
|
writeFileSync,
|
|
16
11
|
} from 'node:fs';
|
|
17
12
|
import { homedir, tmpdir } from 'node:os';
|
|
18
|
-
import { dirname, join
|
|
13
|
+
import { dirname, join } from 'node:path';
|
|
19
14
|
import { fileURLToPath } from 'node:url';
|
|
20
|
-
import {
|
|
21
|
-
parse as parseYaml,
|
|
22
|
-
parseDocument as parseYamlDocument,
|
|
23
|
-
stringify as stringifyYaml,
|
|
24
|
-
} from 'yaml';
|
|
15
|
+
import { stringify as stringifyYaml } from 'yaml';
|
|
25
16
|
import {
|
|
26
17
|
adapterSdkFailureLines,
|
|
27
18
|
checkAdapterSdks,
|
|
28
19
|
mappedSdksFor,
|
|
29
20
|
probeAdapterSdk,
|
|
30
21
|
} from './adapter-sdk.js';
|
|
22
|
+
import {
|
|
23
|
+
adaptersFromLaunchPlan,
|
|
24
|
+
extractWithFlags,
|
|
25
|
+
loadLaunchPlan,
|
|
26
|
+
projectTmuxConfig,
|
|
27
|
+
resolveUserConfigPath,
|
|
28
|
+
checkReadiness,
|
|
29
|
+
} from './launch-config.js';
|
|
30
|
+
import { prepareConfiguredRegistries } from './provision.js';
|
|
31
|
+
|
|
32
|
+
// Preserve the established import surface while the CLI itself delegates to
|
|
33
|
+
// the host-neutral launch-config module.
|
|
34
|
+
export {
|
|
35
|
+
PLAYBOOK_CAPTAIN_MODULE,
|
|
36
|
+
adaptersFromComposedConfig,
|
|
37
|
+
adaptersFromLaunchPlan,
|
|
38
|
+
canonicalizeRegistrySpecifier,
|
|
39
|
+
checkReadiness,
|
|
40
|
+
composeGenericConfig,
|
|
41
|
+
deriveLaunchReadiness,
|
|
42
|
+
extractWithFlags,
|
|
43
|
+
loadLaunchPlan,
|
|
44
|
+
loadOverlayFragment,
|
|
45
|
+
mergeConfigs,
|
|
46
|
+
migrateRetiredProfiles,
|
|
47
|
+
normalizeLaunchPlan,
|
|
48
|
+
projectTmuxConfig,
|
|
49
|
+
resolveAgent,
|
|
50
|
+
resolveConfigHome,
|
|
51
|
+
resolveUserConfigPath,
|
|
52
|
+
} from './launch-config.js';
|
|
31
53
|
|
|
32
|
-
const here = dirname(fileURLToPath(import.meta.url));
|
|
33
|
-
const templatePath = resolve(here, '..', 'playbook.config.template.yaml');
|
|
34
|
-
|
|
35
|
-
// PBCLI-1/8: the launcher composes a tmux-play config whose Captain is the
|
|
36
|
-
// Playbook Captain shell adapter module.
|
|
37
|
-
export const PLAYBOOK_CAPTAIN_MODULE = '@sublang/playbook/playbook-captain';
|
|
38
|
-
// PBCLI-12: known adapter shorthands — the adapters with readiness
|
|
39
|
-
// predicates.
|
|
40
|
-
const ADAPTER_SHORTHANDS = ['claude', 'codex'];
|
|
41
|
-
// PBCLI-8: launcher-owned keys inside a `playbooks.<id>` block; every other
|
|
42
|
-
// key belongs to that playbook's option slice.
|
|
43
|
-
const PLAYBOOK_LAUNCHER_KEYS = ['from', 'command', 'players'];
|
|
44
|
-
const RESERVED_CAPTAIN_PLAYBOOK_ID = 'captain';
|
|
45
|
-
// PBCLI-9: the bare `captain` id names the tmux-play host Captain, so no
|
|
46
|
-
// playbook-local role may take it.
|
|
47
|
-
const RESERVED_CAPTAIN_ROLE_ID = 'captain';
|
|
48
54
|
const READINESS_FAILURE_EXIT_CODE = 2;
|
|
49
55
|
const COMPOSITION_FAILURE_EXIT_CODE = 1;
|
|
50
56
|
|
|
@@ -58,22 +64,52 @@ export async function runPlaybookCli(options = {}) {
|
|
|
58
64
|
const userConfigPath =
|
|
59
65
|
options.userConfigPath ?? resolveUserConfigPath(env, home);
|
|
60
66
|
|
|
61
|
-
// PBCLI-18: `playbook run ...` is the non-interactive
|
|
62
|
-
//
|
|
67
|
+
// PBCLI-18: `playbook run ...` is the non-interactive presentation of the
|
|
68
|
+
// same generic-config Captain session. It never resolves or launches the
|
|
69
|
+
// tmux presenter, but it receives the launch inputs shared with this host.
|
|
63
70
|
if (argv[0] === 'run') {
|
|
64
71
|
const { runPlaybookRun } = await import('./run.js');
|
|
65
72
|
return await runPlaybookRun({
|
|
66
73
|
argv: argv.slice(1),
|
|
67
74
|
stdout,
|
|
68
75
|
stderr,
|
|
69
|
-
|
|
70
|
-
|
|
76
|
+
env,
|
|
77
|
+
homeDir: home,
|
|
71
78
|
userConfigPath,
|
|
79
|
+
...(options.cwd ? { cwd: options.cwd } : {}),
|
|
72
80
|
...(options.loadModule ? { loadModule: options.loadModule } : {}),
|
|
73
|
-
...(options.createAgent ? { createAgent: options.createAgent } : {}),
|
|
74
81
|
...(options.readStdin ? { readStdin: options.readStdin } : {}),
|
|
75
|
-
...(options.sessionsDir ? { sessionsDir: options.sessionsDir } : {}),
|
|
76
82
|
...(options.hostRoots ? { hostRoots: options.hostRoots } : {}),
|
|
83
|
+
...(options.prepareRegistryModule
|
|
84
|
+
? { prepareRegistryModule: options.prepareRegistryModule }
|
|
85
|
+
: {}),
|
|
86
|
+
...(options.adapterImports
|
|
87
|
+
? { adapterImports: options.adapterImports }
|
|
88
|
+
: {}),
|
|
89
|
+
...(options.createCaptainRuntime
|
|
90
|
+
? { createCaptainRuntime: options.createCaptainRuntime }
|
|
91
|
+
: {}),
|
|
92
|
+
...(options.createCaptainSessionId
|
|
93
|
+
? { createCaptainSessionId: options.createCaptainSessionId }
|
|
94
|
+
: {}),
|
|
95
|
+
...(options.createLogicalSessionId
|
|
96
|
+
? { createLogicalSessionId: options.createLogicalSessionId }
|
|
97
|
+
: {}),
|
|
98
|
+
...(options.createHostRuntime
|
|
99
|
+
? { createHostRuntime: options.createHostRuntime }
|
|
100
|
+
: {}),
|
|
101
|
+
...(options.sessionStore
|
|
102
|
+
? { sessionStore: options.sessionStore }
|
|
103
|
+
: {}),
|
|
104
|
+
...(options.sessionsDir ? { sessionsDir: options.sessionsDir } : {}),
|
|
105
|
+
...(options.now ? { now: options.now } : {}),
|
|
106
|
+
...(options.createSessionTempId
|
|
107
|
+
? { createSessionTempId: options.createSessionTempId }
|
|
108
|
+
: {}),
|
|
109
|
+
...(options.createAttemptId
|
|
110
|
+
? { createAttemptId: options.createAttemptId }
|
|
111
|
+
: {}),
|
|
112
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
77
113
|
// PBCLI-39: the run path gates on SDK availability too.
|
|
78
114
|
...(options.probeAdapterSdk
|
|
79
115
|
? { probeAdapterSdk: options.probeAdapterSdk }
|
|
@@ -115,6 +151,15 @@ export async function runPlaybookCli(options = {}) {
|
|
|
115
151
|
);
|
|
116
152
|
return { code: COMPOSITION_FAILURE_EXIT_CODE };
|
|
117
153
|
}
|
|
154
|
+
const noProvision = forwardArgv.includes('--no-provision');
|
|
155
|
+
forwardArgv = forwardArgv.filter((arg) => arg !== '--no-provision');
|
|
156
|
+
if (noProvision && hasExplicitConfig(argv)) {
|
|
157
|
+
stderr.write(
|
|
158
|
+
'playbook: --no-provision applies to configured registry preparation ' +
|
|
159
|
+
'and cannot combine with a raw --config launch\n',
|
|
160
|
+
);
|
|
161
|
+
return { code: COMPOSITION_FAILURE_EXIT_CODE };
|
|
162
|
+
}
|
|
118
163
|
|
|
119
164
|
// PBCLI-1: explicit `--config <path>` launches that raw tmux-play config
|
|
120
165
|
// directly, bypassing seeding, composition, and the readiness gate.
|
|
@@ -122,29 +167,22 @@ export async function runPlaybookCli(options = {}) {
|
|
|
122
167
|
return await launchTmuxPlay(spawnFn, [tmuxPlayBin, ...argv], stderr);
|
|
123
168
|
}
|
|
124
169
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
// DR-021 §3: an existing profiles-based config is rewritten in place once,
|
|
128
|
-
// with the original kept beside it, so the user launches without editing.
|
|
129
|
-
try {
|
|
130
|
-
migrateUserConfigIfRetired(userConfigPath, stderr);
|
|
131
|
-
} catch (error) {
|
|
132
|
-
stderr.write(`playbook: ${errorMessage(error)}\n`);
|
|
133
|
-
return { code: COMPOSITION_FAILURE_EXIT_CODE };
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
let composed;
|
|
170
|
+
let plan;
|
|
137
171
|
try {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
172
|
+
plan = await loadLaunchPlan({
|
|
173
|
+
userConfigPath,
|
|
174
|
+
overlayPaths: withPaths,
|
|
175
|
+
loadModule,
|
|
176
|
+
prepareRegistryModule:
|
|
177
|
+
options.prepareRegistryModule ??
|
|
178
|
+
prepareConfiguredRegistries({
|
|
179
|
+
enabled: !noProvision,
|
|
180
|
+
stderr,
|
|
181
|
+
hostRoots: options.hostRoots,
|
|
182
|
+
commandName: 'playbook',
|
|
183
|
+
}),
|
|
184
|
+
onNotice: (line) => stderr.write(line),
|
|
185
|
+
});
|
|
148
186
|
} catch (error) {
|
|
149
187
|
stderr.write(`playbook: ${errorMessage(error)}\n`);
|
|
150
188
|
return { code: COMPOSITION_FAILURE_EXIT_CODE };
|
|
@@ -153,14 +191,15 @@ export async function runPlaybookCli(options = {}) {
|
|
|
153
191
|
// PBCLI-5: `--list` prints each configured playbook's id, effective
|
|
154
192
|
// command, and intent without launching tmux-play.
|
|
155
193
|
if (argv.includes('--list')) {
|
|
156
|
-
for (const pb of
|
|
194
|
+
for (const pb of Object.values(plan.catalog)) {
|
|
157
195
|
stdout.write(`/${pb.command} ${pb.id} — ${pb.intent}\n`);
|
|
158
196
|
}
|
|
159
197
|
return { code: 0 };
|
|
160
198
|
}
|
|
161
199
|
|
|
162
|
-
// PBCLI-12: readiness
|
|
163
|
-
|
|
200
|
+
// PBCLI-12/46: readiness derives from the same normalized execution plan
|
|
201
|
+
// that both front ends consume, independent of its tmux projection.
|
|
202
|
+
const declaredAdapters = adaptersFromLaunchPlan(plan);
|
|
164
203
|
const readiness = checkReadiness(declaredAdapters, env, home);
|
|
165
204
|
// PBCLI-39/40: SDK availability is an independent check with its own
|
|
166
205
|
// remedy — a credential and an SDK can be missing at once, and reporting
|
|
@@ -196,7 +235,7 @@ export async function runPlaybookCli(options = {}) {
|
|
|
196
235
|
}
|
|
197
236
|
|
|
198
237
|
const { dir: tempDir, path: composedPath } = writeComposedConfig(
|
|
199
|
-
|
|
238
|
+
projectTmuxConfig(plan),
|
|
200
239
|
);
|
|
201
240
|
try {
|
|
202
241
|
return await launchTmuxPlay(
|
|
@@ -209,489 +248,46 @@ export async function runPlaybookCli(options = {}) {
|
|
|
209
248
|
}
|
|
210
249
|
}
|
|
211
250
|
|
|
212
|
-
// PBCLI-
|
|
213
|
-
//
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
const value = argv[i + 1];
|
|
221
|
-
if (value === undefined || value === '') {
|
|
222
|
-
throw new Error('--with needs a value');
|
|
223
|
-
}
|
|
224
|
-
withPaths.push(value);
|
|
225
|
-
i += 1;
|
|
226
|
-
} else if (arg.startsWith('--with=')) {
|
|
227
|
-
const value = arg.slice('--with='.length);
|
|
228
|
-
if (!value) throw new Error('--with needs a value');
|
|
229
|
-
withPaths.push(value);
|
|
230
|
-
} else {
|
|
231
|
-
rest.push(arg);
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
return { withPaths, rest };
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
// PBCLI-25: an overlay fragment is a top-level-format YAML map.
|
|
238
|
-
function loadOverlayFragment(overlayPath) {
|
|
239
|
-
const resolved = resolve(overlayPath);
|
|
240
|
-
let text;
|
|
241
|
-
try {
|
|
242
|
-
text = readFileSync(resolved, 'utf8');
|
|
243
|
-
} catch (error) {
|
|
244
|
-
throw new Error(
|
|
245
|
-
`cannot read --with overlay ${overlayPath}: ${errorMessage(error)}`,
|
|
246
|
-
);
|
|
251
|
+
// PBCLI-23/24: the executable converts termination signals into an abort of
|
|
252
|
+
// the active headless host, lets its uncertain marker and lease cleanup
|
|
253
|
+
// finish, then asks the caller to re-raise the original signal.
|
|
254
|
+
export async function runPlaybookCliEntry(options = {}) {
|
|
255
|
+
const processLike = options.processLike ?? process;
|
|
256
|
+
const entryArgv = options.argv ?? processLike.argv?.slice(2) ?? [];
|
|
257
|
+
if (entryArgv[0] !== 'run') {
|
|
258
|
+
return runPlaybookCli(options);
|
|
247
259
|
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
);
|
|
255
|
-
}
|
|
256
|
-
if (!isObject(fragment)) {
|
|
257
|
-
throw new Error(`--with overlay ${overlayPath} must be a YAML map`);
|
|
258
|
-
}
|
|
259
|
-
return fragment;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
// PBCLI-25/26: recursive merge for plain maps, replacement for every
|
|
263
|
-
// other value; neither input is mutated. Object.fromEntries defines own
|
|
264
|
-
// data properties, so a hostile fragment key such as __proto__ cannot
|
|
265
|
-
// reach the prototype.
|
|
266
|
-
function mergeConfigs(base, overlay) {
|
|
267
|
-
return Object.fromEntries([
|
|
268
|
-
...Object.entries(base),
|
|
269
|
-
...Object.entries(overlay).map(([key, value]) => [
|
|
270
|
-
key,
|
|
271
|
-
isObject(base[key]) && isObject(value)
|
|
272
|
-
? mergeConfigs(base[key], value)
|
|
273
|
-
: value,
|
|
274
|
-
]),
|
|
275
|
-
]);
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
export function resolveConfigHome(env = process.env, home = homedir()) {
|
|
279
|
-
return env.XDG_CONFIG_HOME || join(home, '.config');
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
export function resolveUserConfigPath(env = process.env, home = homedir()) {
|
|
283
|
-
return join(resolveConfigHome(env, home), 'playbook', 'playbook.config.yaml');
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
// PBCLI-8 (DR-021): a scalar `captain` / `players.<role>` value is an
|
|
287
|
-
// adapter shorthand; a full block is a self-contained tmux-play agent block
|
|
288
|
-
// carrying its own adapter/model/effort/permissions. There is no profile
|
|
289
|
-
// indirection, so retuning one agent cannot change another.
|
|
290
|
-
export function resolveAgent(value, path) {
|
|
291
|
-
if (typeof value === 'string') return { adapter: value };
|
|
292
|
-
if (isObject(value)) return { ...value };
|
|
293
|
-
throw new Error(`${path} must be an adapter shorthand or an agent block`);
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
// DR-021 §3: migrate the user's config on disk, once, keeping the original.
|
|
297
|
-
// The backup is written before the rewrite and never overwrites an existing
|
|
298
|
-
// file, so a prior backup — or a user's own .bak — cannot be lost.
|
|
299
|
-
function migrateUserConfigIfRetired(userConfigPath, stderr) {
|
|
300
|
-
let text;
|
|
301
|
-
try {
|
|
302
|
-
text = readFileSync(userConfigPath, 'utf8');
|
|
303
|
-
} catch {
|
|
304
|
-
return;
|
|
305
|
-
}
|
|
306
|
-
let migrated;
|
|
307
|
-
try {
|
|
308
|
-
migrated = migrateRetiredProfiles(text);
|
|
309
|
-
} catch (error) {
|
|
310
|
-
throw new Error(
|
|
311
|
-
`cannot migrate the retired profiles config at ${userConfigPath}: ` +
|
|
312
|
-
`${errorMessage(error)} — edit it by hand: each agent takes its own ` +
|
|
313
|
-
'adapter, model, effort, and permissions',
|
|
314
|
-
);
|
|
315
|
-
}
|
|
316
|
-
if (migrated === undefined) return;
|
|
317
|
-
const backupPath = freeBackupPath(userConfigPath);
|
|
318
|
-
writeFileSync(backupPath, text, { mode: 0o600 });
|
|
319
|
-
writeFileSync(userConfigPath, migrated);
|
|
320
|
-
stderr.write(
|
|
321
|
-
`playbook: migrated ${userConfigPath} to inline agent settings ` +
|
|
322
|
-
`(the top-level "profiles" map was removed in 3.0.0); ` +
|
|
323
|
-
`the original is at ${backupPath}\n`,
|
|
324
|
-
);
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
function freeBackupPath(userConfigPath) {
|
|
328
|
-
const first = `${userConfigPath}.bak`;
|
|
329
|
-
if (!existsSync(first)) return first;
|
|
330
|
-
for (let n = 2; ; n += 1) {
|
|
331
|
-
const candidate = `${userConfigPath}.bak.${n}`;
|
|
332
|
-
if (!existsSync(candidate)) return candidate;
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
// DR-021 §3: rewrite a config written for the retired profiles model in
|
|
337
|
-
// place, inlining each agent's settings and keeping the original beside it.
|
|
338
|
-
// Edits go through the YAML Document API so the user's comments survive;
|
|
339
|
-
// only the profiles block and its own commentary are removed. Returns the
|
|
340
|
-
// migrated text, or undefined when there is nothing to migrate.
|
|
341
|
-
export function migrateRetiredProfiles(text) {
|
|
342
|
-
const doc = parseYamlDocument(text);
|
|
343
|
-
const contents = doc.contents;
|
|
344
|
-
if (!contents || !Array.isArray(contents.items)) return undefined;
|
|
345
|
-
const profiles = doc.get('profiles');
|
|
346
|
-
const agentPaths = [['captain']];
|
|
347
|
-
const playbooks = doc.get('playbooks');
|
|
348
|
-
if (playbooks && Array.isArray(playbooks.items)) {
|
|
349
|
-
for (const entry of playbooks.items) {
|
|
350
|
-
const id = String(entry.key);
|
|
351
|
-
const players = doc.getIn(['playbooks', id, 'players']);
|
|
352
|
-
if (!players || !Array.isArray(players.items)) continue;
|
|
353
|
-
for (const player of players.items) {
|
|
354
|
-
agentPaths.push(['playbooks', id, 'players', String(player.key)]);
|
|
355
|
-
}
|
|
260
|
+
const controller = new AbortController();
|
|
261
|
+
let receivedSignal;
|
|
262
|
+
const handlers = {};
|
|
263
|
+
const removeHandlers = () => {
|
|
264
|
+
for (const [signal, handler] of Object.entries(handlers)) {
|
|
265
|
+
processLike.off(signal, handler);
|
|
356
266
|
}
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
const profileSettings = (name) =>
|
|
360
|
-
profiles && typeof profiles.get === 'function'
|
|
361
|
-
? profiles.get(name)
|
|
362
|
-
: undefined;
|
|
363
|
-
|
|
364
|
-
let changed = false;
|
|
365
|
-
for (const path of agentPaths) {
|
|
366
|
-
const node = doc.getIn(path, true);
|
|
367
|
-
if (node && typeof node.value === 'string' && !Array.isArray(node.items)) {
|
|
368
|
-
// A scalar that named a profile; a bare adapter shorthand stays.
|
|
369
|
-
const settings = profileSettings(node.value);
|
|
370
|
-
if (settings === undefined) continue;
|
|
371
|
-
const inlined = settings.clone();
|
|
372
|
-
// The scalar carried any comment on that line, and replacing the node
|
|
373
|
-
// would drop it. Re-attach it above the block that replaces it.
|
|
374
|
-
carryScalarComment(node, inlined);
|
|
375
|
-
doc.setIn(path, inlined);
|
|
376
|
-
changed = true;
|
|
377
|
-
} else if (node && Array.isArray(node.items)) {
|
|
378
|
-
const named = node.get?.('profile');
|
|
379
|
-
if (named === undefined) continue;
|
|
380
|
-
const settings = profileSettings(named);
|
|
381
|
-
if (settings === undefined) {
|
|
382
|
-
throw new Error(
|
|
383
|
-
`${path.join('.')}.profile names "${String(named)}", which no ` +
|
|
384
|
-
'profiles entry defines',
|
|
385
|
-
);
|
|
386
|
-
}
|
|
387
|
-
// Fill the block from its profile in place — never rebuild it — so
|
|
388
|
-
// the user's own keys, ordering, and comments survive untouched. The
|
|
389
|
-
// block's own fields stay authoritative, so only absent keys are added.
|
|
390
|
-
node.delete('profile');
|
|
391
|
-
for (const item of settings.items) {
|
|
392
|
-
if (node.has(String(item.key))) continue;
|
|
393
|
-
// Append the whole pair, not a rebuilt key/value: a comment above a
|
|
394
|
-
// setting rides on that setting's key node, so stringifying the key
|
|
395
|
-
// would drop it.
|
|
396
|
-
node.add(item.clone());
|
|
397
|
-
}
|
|
398
|
-
changed = true;
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
if (profiles !== undefined) {
|
|
403
|
-
// The comment block above `profiles` usually carries the file's own
|
|
404
|
-
// header, which must outlive the removed section: keep every paragraph
|
|
405
|
-
// except the last, which documents profiles themselves.
|
|
406
|
-
const index = contents.items.findIndex(
|
|
407
|
-
(item) => String(item.key) === 'profiles',
|
|
408
|
-
);
|
|
409
|
-
const lead = index === -1 ? undefined : contents.items[index]?.key
|
|
410
|
-
?.commentBefore;
|
|
411
|
-
doc.delete('profiles');
|
|
412
|
-
const header = keptHeaderComment(lead);
|
|
413
|
-
const next = contents.items[0];
|
|
414
|
-
if (header !== undefined && next?.key) {
|
|
415
|
-
next.key.commentBefore =
|
|
416
|
-
next.key.commentBefore === undefined
|
|
417
|
-
? header
|
|
418
|
-
: `${header}\n\n${next.key.commentBefore}`;
|
|
419
|
-
}
|
|
420
|
-
changed = true;
|
|
421
|
-
}
|
|
422
|
-
if (!changed) return undefined;
|
|
423
|
-
// Say what happened at the top of the file the user will open next:
|
|
424
|
-
// some of their remaining comments describe the retired model.
|
|
425
|
-
doc.commentBefore = MIGRATION_NOTE;
|
|
426
|
-
return doc.toString();
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
const MIGRATION_NOTE =
|
|
430
|
-
' Migrated by playbook 3.0.0: the top-level `profiles` map was removed and\n' +
|
|
431
|
-
' each agent now carries its settings inline. The pre-migration file is\n' +
|
|
432
|
-
' kept beside this one as a .bak. Comments below may still describe the\n' +
|
|
433
|
-
' retired profiles model.';
|
|
434
|
-
|
|
435
|
-
// Move a scalar agent's own comments onto the block that replaces it, so
|
|
436
|
-
// `captain: base # the judge` keeps its note. The pair's key comments are
|
|
437
|
-
// untouched by the replacement and need no carrying.
|
|
438
|
-
function carryScalarComment(node, inlined) {
|
|
439
|
-
const parts = [node.commentBefore, node.comment].filter(
|
|
440
|
-
(part) => typeof part === 'string' && part.trim() !== '',
|
|
441
|
-
);
|
|
442
|
-
if (parts.length === 0) return;
|
|
443
|
-
const first = inlined.items?.[0]?.key;
|
|
444
|
-
if (!first) return;
|
|
445
|
-
// A flow map carrying a comment renders as a multi-line brace block; the
|
|
446
|
-
// ordinary block form is what the rest of the config looks like.
|
|
447
|
-
inlined.flow = false;
|
|
448
|
-
const carried = parts.join('\n');
|
|
449
|
-
first.commentBefore =
|
|
450
|
-
first.commentBefore === undefined
|
|
451
|
-
? carried
|
|
452
|
-
: `${carried}\n${first.commentBefore}`;
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
// Drop the trailing paragraph — the one describing the profiles block —
|
|
456
|
-
// and keep the rest of the leading comment (SPDX header, file overview).
|
|
457
|
-
function keptHeaderComment(comment) {
|
|
458
|
-
if (typeof comment !== 'string' || comment.trim() === '') return undefined;
|
|
459
|
-
const paragraphs = comment.split('\n\n');
|
|
460
|
-
const kept = paragraphs.slice(0, -1).join('\n\n');
|
|
461
|
-
return kept.trim() === '' ? undefined : kept;
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
// A `profile` key that survives migration — introduced by a `--with`
|
|
465
|
-
// overlay rather than the user's own config — is still rejected.
|
|
466
|
-
function assertNoRetiredProfiles(top, configPath) {
|
|
467
|
-
const where = configPath ? ` in ${configPath}` : '';
|
|
468
|
-
if (top.profiles !== undefined) {
|
|
469
|
-
throw new Error(
|
|
470
|
-
`top-level "profiles" was removed${where}: write each agent's settings ` +
|
|
471
|
-
'inline under captain and each playbooks.<id>.players.<role> ' +
|
|
472
|
-
'(adapter, model, effort, permissions)',
|
|
473
|
-
);
|
|
474
|
-
}
|
|
475
|
-
const blocks = [['captain', top.captain]];
|
|
476
|
-
const playbooksCfg = isObject(top.playbooks) ? top.playbooks : {};
|
|
477
|
-
for (const [id, block] of Object.entries(playbooksCfg)) {
|
|
478
|
-
const playersMap = isObject(block) && isObject(block.players)
|
|
479
|
-
? block.players
|
|
480
|
-
: {};
|
|
481
|
-
for (const [role, agent] of Object.entries(playersMap)) {
|
|
482
|
-
blocks.push([`playbooks.${id}.players.${role}`, agent]);
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
for (const [path, block] of blocks) {
|
|
486
|
-
if (isObject(block) && block.profile !== undefined) {
|
|
487
|
-
throw new Error(
|
|
488
|
-
`${path}.profile was removed${where}: write the agent's settings ` +
|
|
489
|
-
'inline in that block (adapter, model, effort, permissions)',
|
|
490
|
-
);
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
function isValidRegistryEntry(value) {
|
|
496
|
-
if (!isObject(value)) return false;
|
|
497
|
-
return (
|
|
498
|
-
typeof value.id === 'string' &&
|
|
499
|
-
typeof value.command === 'string' &&
|
|
500
|
-
typeof value.intent === 'string' &&
|
|
501
|
-
Array.isArray(value.requiredRoleIds) &&
|
|
502
|
-
typeof value.validateOptions === 'function' &&
|
|
503
|
-
typeof value.createRuntime === 'function'
|
|
504
|
-
);
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
// PBCLI-8/9/10: normalize the top-level `playbooks` config into
|
|
508
|
-
// a tmux-play config (Captain = the shell adapter; `captain.options.playbooks`
|
|
509
|
-
// the normalized enablement; a launch-time namespaced `<id>-<role>` roster;
|
|
510
|
-
// launcher-owned `layout.initialVisible`).
|
|
511
|
-
export async function composeGenericConfig(top, loadModule, configPath) {
|
|
512
|
-
assertNoRetiredProfiles(top, configPath);
|
|
513
|
-
|
|
514
|
-
const playbooksCfg = requireObject(top.playbooks, 'playbooks');
|
|
515
|
-
const ids = Object.keys(playbooksCfg);
|
|
516
|
-
if (ids.length === 0) {
|
|
517
|
-
throw new Error('playbooks must enable at least one playbook');
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
const captain = {
|
|
521
|
-
from: PLAYBOOK_CAPTAIN_MODULE,
|
|
522
|
-
...resolveAgent(top.captain, 'captain'),
|
|
523
267
|
};
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
const listing = [];
|
|
531
|
-
const seenCommands = new Map();
|
|
532
|
-
const seenIds = new Set();
|
|
533
|
-
let firstVisible;
|
|
534
|
-
|
|
535
|
-
for (const id of ids) {
|
|
536
|
-
if (id === RESERVED_CAPTAIN_PLAYBOOK_ID) {
|
|
537
|
-
throw new Error(
|
|
538
|
-
`playbooks.${id} collides with the reserved internal Captain id`,
|
|
539
|
-
);
|
|
540
|
-
}
|
|
541
|
-
const block = requireObject(playbooksCfg[id], `playbooks.${id}`);
|
|
542
|
-
const from = block.from;
|
|
543
|
-
if (typeof from !== 'string' || from.length === 0) {
|
|
544
|
-
throw new Error(`playbooks.${id}.from must be a module specifier`);
|
|
545
|
-
}
|
|
546
|
-
let mod;
|
|
547
|
-
try {
|
|
548
|
-
mod = await loadModule(from);
|
|
549
|
-
} catch (cause) {
|
|
550
|
-
throw new Error(
|
|
551
|
-
`playbooks.${id}.from "${from}" failed to import: ${errorMessage(cause)}`,
|
|
552
|
-
);
|
|
553
|
-
}
|
|
554
|
-
const entry = mod?.default;
|
|
555
|
-
if (!isValidRegistryEntry(entry)) {
|
|
556
|
-
throw new Error(
|
|
557
|
-
`playbooks.${id}.from "${from}" exposes no valid registry entry`,
|
|
558
|
-
);
|
|
559
|
-
}
|
|
560
|
-
if (entry.id !== id) {
|
|
561
|
-
throw new Error(
|
|
562
|
-
`playbooks.${id} key must equal the module manifest id "${entry.id}"`,
|
|
563
|
-
);
|
|
564
|
-
}
|
|
565
|
-
if (seenIds.has(entry.id)) {
|
|
566
|
-
throw new Error(`duplicate playbook id "${entry.id}"`);
|
|
567
|
-
}
|
|
568
|
-
seenIds.add(entry.id);
|
|
569
|
-
|
|
570
|
-
const command =
|
|
571
|
-
typeof block.command === 'string' && block.command.length > 0
|
|
572
|
-
? block.command
|
|
573
|
-
: entry.command;
|
|
574
|
-
if (command === RESERVED_CAPTAIN_PLAYBOOK_ID) {
|
|
575
|
-
throw new Error(
|
|
576
|
-
`playbooks.${id}.command collides with the reserved internal Captain command`,
|
|
577
|
-
);
|
|
578
|
-
}
|
|
579
|
-
if (seenCommands.has(command)) {
|
|
580
|
-
throw new Error(`duplicate effective command "${command}"`);
|
|
581
|
-
}
|
|
582
|
-
seenCommands.set(command, id);
|
|
583
|
-
|
|
584
|
-
// PBCLI-9: reject the reserved role before the coverage checks below, so
|
|
585
|
-
// an entry requiring `captain` names the real fault rather than a missing
|
|
586
|
-
// players entry.
|
|
587
|
-
if (entry.requiredRoleIds.includes(RESERVED_CAPTAIN_ROLE_ID)) {
|
|
588
|
-
throw new Error(
|
|
589
|
-
`playbooks.${id} requires local role "${RESERVED_CAPTAIN_ROLE_ID}", ` +
|
|
590
|
-
'which is reserved for the tmux-play Captain',
|
|
591
|
-
);
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
const playersMap = requireObject(block.players, `playbooks.${id}.players`);
|
|
595
|
-
const roles = Object.keys(playersMap);
|
|
596
|
-
if (roles.includes(RESERVED_CAPTAIN_ROLE_ID)) {
|
|
597
|
-
throw new Error(
|
|
598
|
-
`playbooks.${id}.players.${RESERVED_CAPTAIN_ROLE_ID} binds local ` +
|
|
599
|
-
`role "${RESERVED_CAPTAIN_ROLE_ID}", which is reserved for the ` +
|
|
600
|
-
'tmux-play Captain',
|
|
601
|
-
);
|
|
602
|
-
}
|
|
603
|
-
if (roles.length === 0) {
|
|
604
|
-
throw new Error(`playbooks.${id} resolves no visible local role`);
|
|
605
|
-
}
|
|
606
|
-
for (const required of entry.requiredRoleIds) {
|
|
607
|
-
if (!roles.includes(required)) {
|
|
608
|
-
throw new Error(
|
|
609
|
-
`playbooks.${id} required role "${required}" has no players entry`,
|
|
610
|
-
);
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
const generated = [];
|
|
614
|
-
for (const role of roles) {
|
|
615
|
-
const agent = resolveAgent(
|
|
616
|
-
playersMap[role],
|
|
617
|
-
`playbooks.${id}.players.${role}`,
|
|
618
|
-
);
|
|
619
|
-
if (agent.adapter === undefined) {
|
|
620
|
-
throw new Error(
|
|
621
|
-
`playbooks.${id}.players.${role} must resolve an adapter`,
|
|
622
|
-
);
|
|
623
|
-
}
|
|
624
|
-
const hostId = `${id}-${role}`;
|
|
625
|
-
roster.push({ id: hostId, ...agent });
|
|
626
|
-
generated.push(hostId);
|
|
627
|
-
}
|
|
628
|
-
if (firstVisible === undefined) firstVisible = generated;
|
|
629
|
-
|
|
630
|
-
const optionSlice = {};
|
|
631
|
-
for (const key of Object.keys(block)) {
|
|
632
|
-
if (!PLAYBOOK_LAUNCHER_KEYS.includes(key)) {
|
|
633
|
-
optionSlice[key] = block[key];
|
|
268
|
+
for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP']) {
|
|
269
|
+
handlers[signal] = () => {
|
|
270
|
+
if (receivedSignal !== undefined) {
|
|
271
|
+
removeHandlers();
|
|
272
|
+
processLike.kill(processLike.pid, signal);
|
|
273
|
+
return;
|
|
634
274
|
}
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
from,
|
|
638
|
-
...(typeof block.command === 'string' && block.command.length > 0
|
|
639
|
-
? { command: block.command }
|
|
640
|
-
: {}),
|
|
641
|
-
options: optionSlice,
|
|
275
|
+
receivedSignal = signal;
|
|
276
|
+
controller.abort(new Error(`received ${signal}`));
|
|
642
277
|
};
|
|
643
|
-
listing.push({ id, command, intent: entry.intent });
|
|
644
278
|
}
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
// tmux-play CaptainContext, so the launcher — which resolved it — passes it
|
|
648
|
-
// through. The shell needs it to decide whether an explicit empty tool
|
|
649
|
-
// allowlist can be enforced or must degrade to prompt-level restriction.
|
|
650
|
-
captain.options = {
|
|
651
|
-
playbooks: optionsPlaybooks,
|
|
652
|
-
...(typeof captain.adapter === 'string' && captain.adapter.length > 0
|
|
653
|
-
? { captainAdapter: captain.adapter }
|
|
654
|
-
: {}),
|
|
655
|
-
};
|
|
656
|
-
const config = { captain, players: roster };
|
|
657
|
-
// PBCLI-10: carry the user's tmux-play layout window/weight fields through;
|
|
658
|
-
// the launcher owns `layout.initialVisible` (first enabled playbook).
|
|
659
|
-
const layout = isObject(top.layout) ? { ...top.layout } : {};
|
|
660
|
-
layout.initialVisible = firstVisible;
|
|
661
|
-
config.layout = layout;
|
|
662
|
-
if (top.notifications !== undefined) config.notifications = top.notifications;
|
|
663
|
-
if (top.theme !== undefined) config.theme = top.theme;
|
|
664
|
-
return { config, playbooks: listing };
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
export function adaptersFromComposedConfig(config) {
|
|
668
|
-
const adapters = new Set();
|
|
669
|
-
if (config?.captain?.adapter) adapters.add(config.captain.adapter);
|
|
670
|
-
for (const player of config?.players ?? []) {
|
|
671
|
-
if (player?.adapter) adapters.add(player.adapter);
|
|
279
|
+
for (const [signal, handler] of Object.entries(handlers)) {
|
|
280
|
+
processLike.on(signal, handler);
|
|
672
281
|
}
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
if (!env.ANTHROPIC_API_KEY && !existsSync(join(home, '.claude'))) {
|
|
682
|
-
failingAdapters.push(adapter);
|
|
683
|
-
}
|
|
684
|
-
continue;
|
|
685
|
-
}
|
|
686
|
-
if (adapter === 'codex') {
|
|
687
|
-
if (!env.OPENAI_API_KEY && !existsSync(join(home, '.codex'))) {
|
|
688
|
-
failingAdapters.push(adapter);
|
|
689
|
-
}
|
|
690
|
-
continue;
|
|
691
|
-
}
|
|
692
|
-
unknownAdapters.push(adapter);
|
|
282
|
+
try {
|
|
283
|
+
const result = await runPlaybookCli({
|
|
284
|
+
...options,
|
|
285
|
+
signal: controller.signal,
|
|
286
|
+
});
|
|
287
|
+
return receivedSignal === undefined ? result : { signal: receivedSignal };
|
|
288
|
+
} finally {
|
|
289
|
+
removeHandlers();
|
|
693
290
|
}
|
|
694
|
-
return { failingAdapters, unknownAdapters };
|
|
695
291
|
}
|
|
696
292
|
|
|
697
293
|
function writeComposedConfig(composed) {
|
|
@@ -701,13 +297,6 @@ function writeComposedConfig(composed) {
|
|
|
701
297
|
return { dir, path };
|
|
702
298
|
}
|
|
703
299
|
|
|
704
|
-
function seedUserConfigIfMissing(userConfigPath, stderr) {
|
|
705
|
-
if (existsSync(userConfigPath)) return;
|
|
706
|
-
mkdirSync(dirname(userConfigPath), { recursive: true });
|
|
707
|
-
copyFileSync(templatePath, userConfigPath, constants.COPYFILE_EXCL);
|
|
708
|
-
stderr.write(`playbook: created config at ${userConfigPath}\n`);
|
|
709
|
-
}
|
|
710
|
-
|
|
711
300
|
function hasExplicitConfig(argv) {
|
|
712
301
|
return argv.some((arg) => arg === '--config' || arg.startsWith('--config='));
|
|
713
302
|
}
|
|
@@ -727,17 +316,25 @@ function helpText({
|
|
|
727
316
|
...sdkFailureLines,
|
|
728
317
|
...failures,
|
|
729
318
|
'Usage:',
|
|
730
|
-
' playbook [--list] [--with <path>]... [--
|
|
731
|
-
'
|
|
732
|
-
' playbook run
|
|
319
|
+
' playbook [--list] [--with <path>]... [--no-provision]',
|
|
320
|
+
' [--config <path>] [tmux-play options]',
|
|
321
|
+
' playbook run [--with <path>]... [--no-provision] [--json]',
|
|
322
|
+
' [--verbose] [--] [input]',
|
|
323
|
+
' playbook run (--continue | --session <id>) [reply]',
|
|
324
|
+
' playbook run --session <id> --retry-uncertain',
|
|
325
|
+
' playbook run --session <id> --discard-uncertain',
|
|
733
326
|
' playbook --help',
|
|
734
327
|
'',
|
|
735
328
|
`Default config: ${userConfigPath}`,
|
|
736
329
|
'',
|
|
737
330
|
' --with <path> overlays a top-level config fragment (same format as',
|
|
738
|
-
' the default config) over the default config for
|
|
331
|
+
' the default config) over the default config for a fresh launch only —',
|
|
739
332
|
' maps merge recursively, other values replace, later files win. The',
|
|
740
333
|
' default config file is never modified.',
|
|
334
|
+
' --no-provision keeps configured filesystem registries read-only;',
|
|
335
|
+
' any missing engine links remain a launch error.',
|
|
336
|
+
' `playbook run --verbose` prints Captain telemetry topics to stderr.',
|
|
337
|
+
' `playbook run --help` prints complete continuation and recovery usage.',
|
|
741
338
|
'',
|
|
742
339
|
'Adapter setup:',
|
|
743
340
|
' claude: npm install -g @anthropic-ai/claude-agent-sdk, then run',
|
|
@@ -793,21 +390,6 @@ function resolveTmuxPlayBin() {
|
|
|
793
390
|
return join(dirname(fileURLToPath(tmuxPlayIndexUrl)), 'cli.js');
|
|
794
391
|
}
|
|
795
392
|
|
|
796
|
-
function isObject(value) {
|
|
797
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
function hasOwn(value, key) {
|
|
801
|
-
return Object.prototype.hasOwnProperty.call(value, key);
|
|
802
|
-
}
|
|
803
|
-
|
|
804
|
-
function requireObject(value, path) {
|
|
805
|
-
if (!isObject(value)) {
|
|
806
|
-
throw new Error(`${path} must be an object`);
|
|
807
|
-
}
|
|
808
|
-
return value;
|
|
809
|
-
}
|
|
810
|
-
|
|
811
393
|
function errorMessage(error) {
|
|
812
394
|
return error instanceof Error ? error.message : String(error);
|
|
813
395
|
}
|
|
@@ -822,7 +404,8 @@ function isCliEntry(argv1 = process.argv[1], moduleUrl = import.meta.url) {
|
|
|
822
404
|
}
|
|
823
405
|
|
|
824
406
|
if (isCliEntry()) {
|
|
825
|
-
const result = await
|
|
407
|
+
const result = await runPlaybookCliEntry();
|
|
826
408
|
if (result.signal) process.kill(process.pid, result.signal);
|
|
827
|
-
|
|
409
|
+
// Let Node drain a long piped Captain reply or diagnostic naturally.
|
|
410
|
+
else process.exitCode = result.code ?? 0;
|
|
828
411
|
}
|