@sublang/playbook 0.9.0 → 1.3.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 +190 -151
- package/package.json +50 -6
- package/reference/sdlc/captain.md +102 -0
- package/reference/sdlc/captain.playbook/captain.fsm.d.ts +227 -0
- package/reference/sdlc/captain.playbook/captain.fsm.js +628 -0
- package/reference/sdlc/captain.playbook/captain.fsm.ts +851 -0
- package/reference/sdlc/captain.playbook/captain.gears.md +60 -0
- package/reference/sdlc/captain.playbook/captain.playbook.d.ts +23 -0
- package/reference/sdlc/captain.playbook/captain.playbook.js +1053 -0
- package/reference/sdlc/captain.playbook/captain.playbook.ts +1144 -0
- package/reference/sdlc/code.playbook/bin/playbook.js +158 -12
- package/reference/sdlc/code.playbook/bin/run.js +999 -0
- package/reference/sdlc/code.playbook/code.fsm.d.ts +11 -4
- package/reference/sdlc/code.playbook/code.fsm.introspect.d.ts +2 -2
- package/reference/sdlc/code.playbook/code.fsm.introspect.js +1 -1
- package/reference/sdlc/code.playbook/code.fsm.introspect.ts +6 -6
- package/reference/sdlc/code.playbook/code.fsm.js +334 -102
- package/reference/sdlc/code.playbook/code.fsm.ts +467 -180
- package/reference/sdlc/code.playbook/code.gears.md +11 -10
- package/reference/sdlc/code.playbook/code.playbook.d.ts +16 -19
- package/reference/sdlc/code.playbook/code.playbook.js +199 -488
- package/reference/sdlc/code.playbook/code.playbook.ts +327 -566
- package/reference/sdlc/code.playbook/code.registry.d.ts +0 -3
- package/reference/sdlc/code.playbook/code.registry.js +0 -3
- package/reference/sdlc/code.playbook/code.registry.ts +0 -6
- package/reference/sdlc/code.playbook/playbook-captain.d.ts +9 -4
- package/reference/sdlc/code.playbook/playbook-captain.js +889 -210
- package/reference/sdlc/code.playbook/playbook-captain.ts +1136 -257
- package/reference/sdlc/code.playbook/playbook.config.template.yaml +21 -0
- package/reference/sdlc/discuss.playbook/discuss.fsm.d.ts +396 -0
- package/reference/sdlc/discuss.playbook/discuss.fsm.js +2066 -0
- package/reference/sdlc/discuss.playbook/discuss.fsm.ts +2464 -0
- package/reference/sdlc/discuss.playbook/discuss.gears.md +251 -0
- package/reference/sdlc/discuss.playbook/discuss.playbook.d.ts +113 -0
- package/reference/sdlc/discuss.playbook/discuss.playbook.js +1514 -0
- package/reference/sdlc/discuss.playbook/discuss.playbook.ts +1926 -0
- package/reference/sdlc/discuss.playbook/discuss.registry.d.ts +58 -0
- package/reference/sdlc/discuss.playbook/discuss.registry.js +97 -0
- package/reference/sdlc/discuss.playbook/discuss.registry.ts +153 -0
- package/slc/gears2fsm.md +557 -57
- package/slc/link.md +1165 -89
- package/slc/optimize.md +92 -0
- package/slc/text2gears.md +255 -7
- package/src/runtime.d.ts +146 -3
- package/src/runtime.ts +201 -2
- package/src/xstate-playbook-runtime.d.ts +201 -0
- package/src/xstate-playbook-runtime.js +2058 -0
- package/src/xstate-playbook-runtime.ts +2792 -0
- package/src/xstate-runtime.d.ts +95 -0
- package/src/xstate-runtime.js +1258 -0
- package/src/xstate-runtime.ts +1816 -0
|
@@ -31,6 +31,10 @@ const ADAPTER_SHORTHANDS = ['claude', 'codex'];
|
|
|
31
31
|
// PBCLI-8: launcher-owned keys inside a `playbooks.<id>` block; every other
|
|
32
32
|
// key belongs to that playbook's option slice.
|
|
33
33
|
const PLAYBOOK_LAUNCHER_KEYS = ['from', 'command', 'players'];
|
|
34
|
+
const RESERVED_CAPTAIN_PLAYBOOK_ID = 'captain';
|
|
35
|
+
// PBCLI-9: the bare `captain` id names the tmux-play host Captain, so no
|
|
36
|
+
// playbook-local role may take it.
|
|
37
|
+
const RESERVED_CAPTAIN_ROLE_ID = 'captain';
|
|
34
38
|
const READINESS_FAILURE_EXIT_CODE = 2;
|
|
35
39
|
const COMPOSITION_FAILURE_EXIT_CODE = 1;
|
|
36
40
|
|
|
@@ -39,11 +43,31 @@ export async function runPlaybookCli(options = {}) {
|
|
|
39
43
|
const env = options.env ?? process.env;
|
|
40
44
|
const stdout = options.stdout ?? process.stdout;
|
|
41
45
|
const stderr = options.stderr ?? process.stderr;
|
|
42
|
-
const spawnFn = options.spawn ?? spawn;
|
|
43
|
-
const tmuxPlayBin = options.tmuxPlayBin ?? resolveTmuxPlayBin();
|
|
44
46
|
const loadModule = options.loadModule ?? ((specifier) => import(specifier));
|
|
45
47
|
const home = options.homeDir ?? env.HOME ?? homedir();
|
|
46
|
-
const userConfigPath =
|
|
48
|
+
const userConfigPath =
|
|
49
|
+
options.userConfigPath ?? resolveUserConfigPath(env, home);
|
|
50
|
+
|
|
51
|
+
// PBCLI-18: `playbook run ...` is the non-interactive one-shot path; it
|
|
52
|
+
// never seeds, composes, resolves tmux-play, or launches it.
|
|
53
|
+
if (argv[0] === 'run') {
|
|
54
|
+
const { runPlaybookRun } = await import('./run.js');
|
|
55
|
+
return await runPlaybookRun({
|
|
56
|
+
argv: argv.slice(1),
|
|
57
|
+
stdout,
|
|
58
|
+
stderr,
|
|
59
|
+
// PBCLI-29: the run host reads the same user config as the launcher,
|
|
60
|
+
// honoring any injected env, home, or explicit path.
|
|
61
|
+
userConfigPath,
|
|
62
|
+
...(options.loadModule ? { loadModule: options.loadModule } : {}),
|
|
63
|
+
...(options.createAgent ? { createAgent: options.createAgent } : {}),
|
|
64
|
+
...(options.readStdin ? { readStdin: options.readStdin } : {}),
|
|
65
|
+
...(options.sessionsDir ? { sessionsDir: options.sessionsDir } : {}),
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const spawnFn = options.spawn ?? spawn;
|
|
70
|
+
const tmuxPlayBin = options.tmuxPlayBin ?? resolveTmuxPlayBin();
|
|
47
71
|
|
|
48
72
|
// PBCLI-6: `--help` / `-h` print help and exit 0 without seeding,
|
|
49
73
|
// composing, or launching.
|
|
@@ -52,6 +76,25 @@ export async function runPlaybookCli(options = {}) {
|
|
|
52
76
|
return { code: 0 };
|
|
53
77
|
}
|
|
54
78
|
|
|
79
|
+
// PBCLI-25/26: `--with <path>` overlays are launcher-owned — consumed
|
|
80
|
+
// here, never forwarded to tmux-play, and incompatible with a raw
|
|
81
|
+
// `--config` launch, which bypasses the composition they target.
|
|
82
|
+
let withPaths;
|
|
83
|
+
let forwardArgv;
|
|
84
|
+
try {
|
|
85
|
+
({ withPaths, rest: forwardArgv } = extractWithFlags(argv));
|
|
86
|
+
} catch (error) {
|
|
87
|
+
stderr.write(`playbook: ${errorMessage(error)}\n`);
|
|
88
|
+
return { code: COMPOSITION_FAILURE_EXIT_CODE };
|
|
89
|
+
}
|
|
90
|
+
if (withPaths.length > 0 && hasExplicitConfig(argv)) {
|
|
91
|
+
stderr.write(
|
|
92
|
+
'playbook: --with overlays the top-level config and cannot combine ' +
|
|
93
|
+
'with a raw --config launch\n',
|
|
94
|
+
);
|
|
95
|
+
return { code: COMPOSITION_FAILURE_EXIT_CODE };
|
|
96
|
+
}
|
|
97
|
+
|
|
55
98
|
// PBCLI-1: explicit `--config <path>` launches that raw tmux-play config
|
|
56
99
|
// directly, bypassing seeding, composition, and the readiness gate.
|
|
57
100
|
if (hasExplicitConfig(argv)) {
|
|
@@ -62,10 +105,16 @@ export async function runPlaybookCli(options = {}) {
|
|
|
62
105
|
|
|
63
106
|
let composed;
|
|
64
107
|
try {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
108
|
+
let top = parseYaml(readFileSync(userConfigPath, 'utf8')) ?? {};
|
|
109
|
+
if (withPaths.length > 0 && !isObject(top)) {
|
|
110
|
+
throw new Error(
|
|
111
|
+
`the top-level config at ${userConfigPath} must be a YAML map before --with can overlay it`,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
for (const overlayPath of withPaths) {
|
|
115
|
+
top = mergeConfigs(top, loadOverlayFragment(overlayPath));
|
|
116
|
+
}
|
|
117
|
+
composed = await composeGenericConfig(top, loadModule);
|
|
69
118
|
} catch (error) {
|
|
70
119
|
stderr.write(`playbook: ${errorMessage(error)}\n`);
|
|
71
120
|
return { code: COMPOSITION_FAILURE_EXIT_CODE };
|
|
@@ -104,7 +153,7 @@ export async function runPlaybookCli(options = {}) {
|
|
|
104
153
|
try {
|
|
105
154
|
return await launchTmuxPlay(
|
|
106
155
|
spawnFn,
|
|
107
|
-
[tmuxPlayBin, '--config', composedPath, ...
|
|
156
|
+
[tmuxPlayBin, '--config', composedPath, ...forwardArgv],
|
|
108
157
|
stderr,
|
|
109
158
|
);
|
|
110
159
|
} finally {
|
|
@@ -112,6 +161,72 @@ export async function runPlaybookCli(options = {}) {
|
|
|
112
161
|
}
|
|
113
162
|
}
|
|
114
163
|
|
|
164
|
+
// PBCLI-26: split `--with <path>` pairs out of the argument vector so
|
|
165
|
+
// they are consumed by the launcher rather than forwarded to tmux-play.
|
|
166
|
+
function extractWithFlags(argv) {
|
|
167
|
+
const withPaths = [];
|
|
168
|
+
const rest = [];
|
|
169
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
170
|
+
const arg = argv[i];
|
|
171
|
+
if (arg === '--with') {
|
|
172
|
+
const value = argv[i + 1];
|
|
173
|
+
if (value === undefined || value === '') {
|
|
174
|
+
throw new Error('--with needs a value');
|
|
175
|
+
}
|
|
176
|
+
withPaths.push(value);
|
|
177
|
+
i += 1;
|
|
178
|
+
} else if (arg.startsWith('--with=')) {
|
|
179
|
+
const value = arg.slice('--with='.length);
|
|
180
|
+
if (!value) throw new Error('--with needs a value');
|
|
181
|
+
withPaths.push(value);
|
|
182
|
+
} else {
|
|
183
|
+
rest.push(arg);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return { withPaths, rest };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// PBCLI-25: an overlay fragment is a top-level-format YAML map.
|
|
190
|
+
function loadOverlayFragment(overlayPath) {
|
|
191
|
+
const resolved = resolve(overlayPath);
|
|
192
|
+
let text;
|
|
193
|
+
try {
|
|
194
|
+
text = readFileSync(resolved, 'utf8');
|
|
195
|
+
} catch (error) {
|
|
196
|
+
throw new Error(
|
|
197
|
+
`cannot read --with overlay ${overlayPath}: ${errorMessage(error)}`,
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
let fragment;
|
|
201
|
+
try {
|
|
202
|
+
fragment = parseYaml(text);
|
|
203
|
+
} catch (error) {
|
|
204
|
+
throw new Error(
|
|
205
|
+
`cannot parse --with overlay ${overlayPath}: ${errorMessage(error)}`,
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
if (!isObject(fragment)) {
|
|
209
|
+
throw new Error(`--with overlay ${overlayPath} must be a YAML map`);
|
|
210
|
+
}
|
|
211
|
+
return fragment;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// PBCLI-25/26: recursive merge for plain maps, replacement for every
|
|
215
|
+
// other value; neither input is mutated. Object.fromEntries defines own
|
|
216
|
+
// data properties, so a hostile fragment key such as __proto__ cannot
|
|
217
|
+
// reach the prototype.
|
|
218
|
+
function mergeConfigs(base, overlay) {
|
|
219
|
+
return Object.fromEntries([
|
|
220
|
+
...Object.entries(base),
|
|
221
|
+
...Object.entries(overlay).map(([key, value]) => [
|
|
222
|
+
key,
|
|
223
|
+
isObject(base[key]) && isObject(value)
|
|
224
|
+
? mergeConfigs(base[key], value)
|
|
225
|
+
: value,
|
|
226
|
+
]),
|
|
227
|
+
]);
|
|
228
|
+
}
|
|
229
|
+
|
|
115
230
|
export function resolveConfigHome(env = process.env, home = homedir()) {
|
|
116
231
|
return env.XDG_CONFIG_HOME || join(home, '.config');
|
|
117
232
|
}
|
|
@@ -152,9 +267,6 @@ function isValidRegistryEntry(value) {
|
|
|
152
267
|
typeof value.command === 'string' &&
|
|
153
268
|
typeof value.intent === 'string' &&
|
|
154
269
|
Array.isArray(value.requiredRoleIds) &&
|
|
155
|
-
typeof value.idleStateId === 'string' &&
|
|
156
|
-
typeof value.finalStateId === 'string' &&
|
|
157
|
-
Array.isArray(value.parkStateIds) &&
|
|
158
270
|
typeof value.validateOptions === 'function' &&
|
|
159
271
|
typeof value.createRuntime === 'function'
|
|
160
272
|
);
|
|
@@ -196,6 +308,11 @@ export async function composeGenericConfig(top, loadModule) {
|
|
|
196
308
|
let firstVisible;
|
|
197
309
|
|
|
198
310
|
for (const id of ids) {
|
|
311
|
+
if (id === RESERVED_CAPTAIN_PLAYBOOK_ID) {
|
|
312
|
+
throw new Error(
|
|
313
|
+
`playbooks.${id} collides with the reserved internal Captain id`,
|
|
314
|
+
);
|
|
315
|
+
}
|
|
199
316
|
const block = requireObject(playbooksCfg[id], `playbooks.${id}`);
|
|
200
317
|
const from = block.from;
|
|
201
318
|
if (typeof from !== 'string' || from.length === 0) {
|
|
@@ -229,13 +346,35 @@ export async function composeGenericConfig(top, loadModule) {
|
|
|
229
346
|
typeof block.command === 'string' && block.command.length > 0
|
|
230
347
|
? block.command
|
|
231
348
|
: entry.command;
|
|
349
|
+
if (command === RESERVED_CAPTAIN_PLAYBOOK_ID) {
|
|
350
|
+
throw new Error(
|
|
351
|
+
`playbooks.${id}.command collides with the reserved internal Captain command`,
|
|
352
|
+
);
|
|
353
|
+
}
|
|
232
354
|
if (seenCommands.has(command)) {
|
|
233
355
|
throw new Error(`duplicate effective command "${command}"`);
|
|
234
356
|
}
|
|
235
357
|
seenCommands.set(command, id);
|
|
236
358
|
|
|
359
|
+
// PBCLI-9: reject the reserved role before the coverage checks below, so
|
|
360
|
+
// an entry requiring `captain` names the real fault rather than a missing
|
|
361
|
+
// players entry.
|
|
362
|
+
if (entry.requiredRoleIds.includes(RESERVED_CAPTAIN_ROLE_ID)) {
|
|
363
|
+
throw new Error(
|
|
364
|
+
`playbooks.${id} requires local role "${RESERVED_CAPTAIN_ROLE_ID}", ` +
|
|
365
|
+
'which is reserved for the tmux-play Captain',
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
|
|
237
369
|
const playersMap = requireObject(block.players, `playbooks.${id}.players`);
|
|
238
370
|
const roles = Object.keys(playersMap);
|
|
371
|
+
if (roles.includes(RESERVED_CAPTAIN_ROLE_ID)) {
|
|
372
|
+
throw new Error(
|
|
373
|
+
`playbooks.${id}.players.${RESERVED_CAPTAIN_ROLE_ID} binds local ` +
|
|
374
|
+
`role "${RESERVED_CAPTAIN_ROLE_ID}", which is reserved for the ` +
|
|
375
|
+
'tmux-play Captain',
|
|
376
|
+
);
|
|
377
|
+
}
|
|
239
378
|
if (roles.length === 0) {
|
|
240
379
|
throw new Error(`playbooks.${id} resolves no visible local role`);
|
|
241
380
|
}
|
|
@@ -348,11 +487,18 @@ function helpText({ userConfigPath, failingAdapters = [] }) {
|
|
|
348
487
|
return [
|
|
349
488
|
...failures,
|
|
350
489
|
'Usage:',
|
|
351
|
-
' playbook [--list] [--config <path>] [tmux-play options]',
|
|
490
|
+
' playbook [--list] [--with <path>]... [--config <path>] [tmux-play options]',
|
|
491
|
+
' playbook run <from> [task] [options] # non-interactive one-shot',
|
|
492
|
+
' playbook run resume <session-id> [reply] # answer a parked run',
|
|
352
493
|
' playbook --help',
|
|
353
494
|
'',
|
|
354
495
|
`Default config: ${userConfigPath}`,
|
|
355
496
|
'',
|
|
497
|
+
' --with <path> overlays a top-level config fragment (same format as',
|
|
498
|
+
' the default config) over the default config for this launch only —',
|
|
499
|
+
' maps merge recursively, other values replace, later files win. The',
|
|
500
|
+
' default config file is never modified.',
|
|
501
|
+
'',
|
|
356
502
|
'Adapter setup:',
|
|
357
503
|
' claude: run Claude Code once or set ANTHROPIC_API_KEY.',
|
|
358
504
|
' codex: run Codex CLI once or set OPENAI_API_KEY.',
|