@sublang/playbook 0.1.3 → 0.4.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.
Files changed (20) hide show
  1. package/README.md +98 -30
  2. package/package.json +24 -23
  3. package/reference/sdlc/code.playbook/bin/playbook-code.js +278 -0
  4. package/{code.fsm.d.ts → reference/sdlc/code.playbook/code.fsm.d.ts} +18 -0
  5. package/{code.fsm.introspect.d.ts → reference/sdlc/code.playbook/code.fsm.introspect.d.ts} +24 -0
  6. package/{code.fsm.introspect.js → reference/sdlc/code.playbook/code.fsm.introspect.js} +28 -1
  7. package/{code.fsm.introspect.ts → reference/sdlc/code.playbook/code.fsm.introspect.ts} +73 -6
  8. package/{code.fsm.js → reference/sdlc/code.playbook/code.fsm.js} +264 -115
  9. package/{code.fsm.ts → reference/sdlc/code.playbook/code.fsm.ts} +327 -119
  10. package/{code.gears.md → reference/sdlc/code.playbook/code.gears.md} +17 -16
  11. package/{code.playbook.d.ts → reference/sdlc/code.playbook/code.playbook.d.ts} +16 -6
  12. package/{code.playbook.js → reference/sdlc/code.playbook/code.playbook.js} +228 -129
  13. package/{code.playbook.ts → reference/sdlc/code.playbook/code.playbook.ts} +316 -122
  14. package/{code.tmux-play.js → reference/sdlc/code.playbook/code.tmux-play.js} +22 -10
  15. package/{code.tmux-play.ts → reference/sdlc/code.playbook/code.tmux-play.ts} +26 -13
  16. package/reference/sdlc/code.playbook/playbook-code.config.template.yaml +38 -0
  17. package/{tmux-play.config.yaml → reference/sdlc/code.playbook/tmux-play.config.yaml} +14 -10
  18. package/{tmux-play.production.config.yaml → reference/sdlc/code.playbook/tmux-play.production.config.yaml} +7 -4
  19. package/bin/playbook-code.js +0 -43
  20. /package/{code.tmux-play.d.ts → reference/sdlc/code.playbook/code.tmux-play.d.ts} +0 -0
package/README.md CHANGED
@@ -34,36 +34,104 @@ committer loop end to end.
34
34
 
35
35
  The reference is the canonical worked example —
36
36
  [CODE source](reference/sdlc/code.md) →
37
- [gears](code.gears.md) →
38
- [FSM](code.fsm.ts) → runtime — with the
39
- runtime ported to cligent's `tmux-play` host out of the box.
37
+ [gears](reference/sdlc/code.playbook/code.gears.md) →
38
+ [FSM](reference/sdlc/code.playbook/code.fsm.ts) → runtime — with
39
+ the runtime ported to cligent's `tmux-play` host out of the box.
40
+ The compiled artifacts live under
41
+ [`reference/sdlc/code.playbook/`](reference/sdlc/code.playbook),
42
+ the slc pipeline's `<basename>.<pipeline>/` output directory.
40
43
 
41
44
  ### Install (users)
42
45
 
43
- Install the package globally. The bundled production config wires
44
- Anthropic (Captain + Coder) and OpenAI Codex (Reviewer); `@sublang/
45
- cligent`, the two adapter SDKs, and `xstate` are all direct
46
- dependencies, so a single install pulls in everything:
46
+ Install the package globally. `@sublang/cligent`, the Claude and
47
+ Codex adapter SDKs, and `xstate` are direct dependencies, so a single
48
+ install pulls in the reference CODE playbook and its host:
47
49
 
48
50
  ```sh
49
51
  npm install -g @sublang/playbook
50
52
  ```
51
53
 
52
- Each adapter reads its own auth: the Claude SDK uses your local
53
- Claude Code auth (or `ANTHROPIC_API_KEY`); the Codex SDK uses your
54
- local codex CLI auth (or `OPENAI_API_KEY`).
55
-
56
54
  Then launch the reference playbook in a `tmux-play` session:
57
55
 
58
56
  ```sh
59
57
  playbook-code
60
58
  ```
61
59
 
62
- The `playbook-code` bin resolves the bundled
63
- `tmux-play.production.config.yaml` and execs the bundled `tmux-play`
64
- CLI (from playbook's own `node_modules/@sublang/cligent`) against it;
65
- any extra flags pass through (`playbook-code --help` lists
66
- `tmux-play`'s).
60
+ For a one-shot run without a global install, use the same command
61
+ through npx:
62
+
63
+ ```sh
64
+ npx playbook-code
65
+ ```
66
+
67
+ On first run, `playbook-code` creates a commented user config at
68
+ `${XDG_CONFIG_HOME:-$HOME/.config}/playbook/playbook-code.config.yaml`
69
+ from the bundled template, prints that path to stderr, then checks the
70
+ declared adapters before launching. Later runs reuse that file and do
71
+ not overwrite it.
72
+
73
+ Known adapter readiness is intentionally light: `claude` is ready when
74
+ local Claude Code auth exists or `ANTHROPIC_API_KEY` is set; `codex` is
75
+ ready when local Codex CLI auth exists or `OPENAI_API_KEY` is set. If a
76
+ known adapter is not ready, `playbook-code` prints its own help text
77
+ with the config path, auth pointers, and agent-swap recipe, then exits
78
+ without launching. You can view the same recovery text at any time:
79
+
80
+ ```sh
81
+ playbook-code --help
82
+ ```
83
+
84
+ The seed template runs each agent in cligent's protected auto mode
85
+ (`permissions.mode: auto`), suppressing routine approval prompts.
86
+
87
+ ### Configure agents
88
+
89
+ Edit the seeded user config when you want different coding agents:
90
+
91
+ ```sh
92
+ $EDITOR "${XDG_CONFIG_HOME:-$HOME/.config}/playbook/playbook-code.config.yaml"
93
+ ```
94
+
95
+ Both CODE players can use `claude` or `codex`; other adapter ids are
96
+ passed through to `tmux-play` with a warning because `playbook-code`
97
+ does not know how to preflight their auth. The safe tuning points are
98
+ `captain.adapter`, `captain.model`, and each player's `adapter`. Keep
99
+ `captain.from` and the `players[].id` values fixed; the runtime binds
100
+ to those host-configuration invariants per
101
+ [PBRT-4](specs/user/playbook-runtime.md#pbrt-4) and derives the
102
+ `<coder-llm>` / `<reviewer-llm>` substitution strings from each
103
+ player entry's `adapter`.
104
+
105
+ For example, this swaps the Coder to Codex and the Reviewer to Claude:
106
+
107
+ ```yaml
108
+ captain:
109
+ from: "@sublang/playbook/code/tmux-play"
110
+ adapter: claude
111
+ model: claude-opus-4-7
112
+ permissions:
113
+ mode: auto
114
+
115
+ players:
116
+ - id: coder # must stay `coder` — see PBRT-4
117
+ adapter: codex
118
+ permissions:
119
+ mode: auto
120
+ - id: reviewer # must stay `reviewer` — see PBRT-4
121
+ adapter: claude
122
+ permissions:
123
+ mode: auto
124
+ ```
125
+
126
+ Normal `playbook-code` runs use the seeded path above. If you need a
127
+ separate config file for a one-off run, pass it explicitly; this bypasses
128
+ the seed and readiness gate and forwards the arguments to `tmux-play`
129
+ verbatim, as pinned in
130
+ [PBCODE-1](specs/user/playbook-code.md#pbcode-1):
131
+
132
+ ```sh
133
+ playbook-code --config ./playbook-code.config.yaml
134
+ ```
67
135
 
68
136
  ### Install (contributors / from source)
69
137
 
@@ -77,7 +145,7 @@ pnpm build
77
145
  pnpm test
78
146
  ```
79
147
 
80
- `pnpm install` resolves `@sublang/cligent` (0.3.0) from the registry;
148
+ `pnpm install` resolves `@sublang/cligent` (^0.6.0) from the registry;
81
149
  no local link required. To point pnpm at a local `cligent` checkout
82
150
  instead, copy
83
151
  [`pnpm-workspace.yaml.example`](pnpm-workspace.yaml.example)
@@ -85,11 +153,11 @@ into place; the override is gitignored so it never leaks into a
85
153
  production install.
86
154
 
87
155
  Drive a Boss turn against the source tree (uses the developer
88
- [`tmux-play.config.yaml`](tmux-play.config.yaml)
156
+ [`tmux-play.config.yaml`](reference/sdlc/code.playbook/tmux-play.config.yaml)
89
157
  that imports the compiled adapter via relative path):
90
158
 
91
159
  ```sh
92
- pnpm exec tmux-play --config tmux-play.config.yaml
160
+ pnpm exec tmux-play --config reference/sdlc/code.playbook/tmux-play.config.yaml
93
161
  ```
94
162
 
95
163
  `pnpm exec` resolves `tmux-play` from the package's local
@@ -98,16 +166,16 @@ installed globally.
98
166
 
99
167
  ### Running a Boss turn
100
168
 
101
- The Boss pane accepts four slash commands per
102
- [PBRT-1](specs/user/playbook-runtime.md#pbrt-1):
103
-
104
- - `/start <intent>` — begin a single coding turn
105
- - `/continue <#>` pick up an IR task
106
- - `/summarize <#>` turn an IR's commits into spec items
107
- - `/interrupt <stateId> [intent]` preempt the FSM into a named state
169
+ The Boss pane takes plain-language turns; the judge classifies each
170
+ into an FSM event (start a coding turn, continue or summarize an IR,
171
+ interrupt to a named state, or nothing) per
172
+ [PBRT-1](specs/user/playbook-runtime.md#pbrt-1).
173
+ When a player surfaces a clarifying question
174
+ the FSM parks at `awaitBossReply` and the pane shows the question; your
175
+ next turn is normally classified as the reply, or a fresh directive
176
+ abandons it ([PBRT-2](specs/user/playbook-runtime.md#pbrt-2)).
108
177
 
109
- Anything else falls through to an LLM classifier. The Captain pane
110
- streams the state machine progression with a four-glyph vocabulary —
178
+ The Captain pane streams the state machine with a four-glyph vocabulary —
111
179
  `◆ ▸ ⮕ ⤷` per [PBRT-3](specs/user/playbook-runtime.md#pbrt-3) — while
112
180
  player prompts ride their own panes.
113
181
 
@@ -135,14 +203,14 @@ const runtime = createPlaybookRuntime({
135
203
 
136
204
  await runtime.init(ports);
137
205
  await runtime.handleBossInput({
138
- text: '/start fix the bug',
206
+ text: 'Start fixing the bug',
139
207
  signal: new AbortController().signal,
140
208
  });
141
209
  await runtime.dispose();
142
210
  ```
143
211
 
144
212
  See
145
- [`code.playbook.test.ts`](code.playbook.test.ts)
213
+ [`code.playbook.test.ts`](reference/sdlc/code.playbook/code.playbook.test.ts)
146
214
  for the full range of port shapes (classifier, judge, abort, interrupt,
147
215
  status/telemetry) the runtime is contract-tested against.
148
216
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sublang/playbook",
3
- "version": "0.1.3",
3
+ "version": "0.4.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",
@@ -25,34 +25,35 @@
25
25
  "node": ">=20.6.0"
26
26
  },
27
27
  "files": [
28
- "code.fsm.ts",
29
- "code.fsm.js",
30
- "code.fsm.d.ts",
31
- "code.fsm.introspect.ts",
32
- "code.fsm.introspect.js",
33
- "code.fsm.introspect.d.ts",
34
- "code.playbook.ts",
35
- "code.playbook.js",
36
- "code.playbook.d.ts",
37
- "code.tmux-play.ts",
38
- "code.tmux-play.js",
39
- "code.tmux-play.d.ts",
40
- "code.gears.md",
41
- "tmux-play.config.yaml",
42
- "tmux-play.production.config.yaml",
43
- "bin"
28
+ "reference/sdlc/code.playbook/code.fsm.ts",
29
+ "reference/sdlc/code.playbook/code.fsm.js",
30
+ "reference/sdlc/code.playbook/code.fsm.d.ts",
31
+ "reference/sdlc/code.playbook/code.fsm.introspect.ts",
32
+ "reference/sdlc/code.playbook/code.fsm.introspect.js",
33
+ "reference/sdlc/code.playbook/code.fsm.introspect.d.ts",
34
+ "reference/sdlc/code.playbook/code.playbook.ts",
35
+ "reference/sdlc/code.playbook/code.playbook.js",
36
+ "reference/sdlc/code.playbook/code.playbook.d.ts",
37
+ "reference/sdlc/code.playbook/code.tmux-play.ts",
38
+ "reference/sdlc/code.playbook/code.tmux-play.js",
39
+ "reference/sdlc/code.playbook/code.tmux-play.d.ts",
40
+ "reference/sdlc/code.playbook/code.gears.md",
41
+ "reference/sdlc/code.playbook/tmux-play.config.yaml",
42
+ "reference/sdlc/code.playbook/tmux-play.production.config.yaml",
43
+ "reference/sdlc/code.playbook/playbook-code.config.template.yaml",
44
+ "reference/sdlc/code.playbook/bin"
44
45
  ],
45
46
  "bin": {
46
- "playbook-code": "bin/playbook-code.js"
47
+ "playbook-code": "reference/sdlc/code.playbook/bin/playbook-code.js"
47
48
  },
48
49
  "exports": {
49
50
  "./code/playbook": {
50
- "types": "./code.playbook.d.ts",
51
- "default": "./code.playbook.js"
51
+ "types": "./reference/sdlc/code.playbook/code.playbook.d.ts",
52
+ "default": "./reference/sdlc/code.playbook/code.playbook.js"
52
53
  },
53
54
  "./code/tmux-play": {
54
- "types": "./code.tmux-play.d.ts",
55
- "default": "./code.tmux-play.js"
55
+ "types": "./reference/sdlc/code.playbook/code.tmux-play.d.ts",
56
+ "default": "./reference/sdlc/code.playbook/code.tmux-play.js"
56
57
  }
57
58
  },
58
59
  "scripts": {
@@ -66,7 +67,7 @@
66
67
  "dependencies": {
67
68
  "@anthropic-ai/claude-agent-sdk": "^0.3.143",
68
69
  "@openai/codex-sdk": "^0.130.0",
69
- "@sublang/cligent": "^0.4.0",
70
+ "@sublang/cligent": "^0.6.0",
70
71
  "xstate": "^5.19.4"
71
72
  },
72
73
  "devDependencies": {
@@ -0,0 +1,278 @@
1
+ #!/usr/bin/env node
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ // SPDX-FileCopyrightText: 2026 SubLang International <https://sublang.ai>
4
+
5
+ import { spawn } from 'node:child_process';
6
+ import {
7
+ constants,
8
+ copyFileSync,
9
+ existsSync,
10
+ mkdirSync,
11
+ readFileSync,
12
+ realpathSync,
13
+ } from 'node:fs';
14
+ import { homedir } from 'node:os';
15
+ import { dirname, join, resolve } from 'node:path';
16
+ import { fileURLToPath } from 'node:url';
17
+
18
+ const here = dirname(fileURLToPath(import.meta.url));
19
+ const templatePath = resolve(here, '..', 'playbook-code.config.template.yaml');
20
+ const READINESS_FAILURE_EXIT_CODE = 2;
21
+
22
+ export async function runPlaybookCodeCli(options = {}) {
23
+ const argv = [...(options.argv ?? process.argv.slice(2))];
24
+ const env = options.env ?? process.env;
25
+ const stdout = options.stdout ?? process.stdout;
26
+ const stderr = options.stderr ?? process.stderr;
27
+ const spawnFn = options.spawn ?? spawn;
28
+ const tmuxPlayBin = options.tmuxPlayBin ?? resolveTmuxPlayBin();
29
+ const home = options.homeDir ?? env.HOME ?? homedir();
30
+ const userConfigPath = resolveUserConfigPath(env, home);
31
+
32
+ if (argv.includes('--help') || argv.includes('-h')) {
33
+ stdout.write(helpText({ userConfigPath }));
34
+ return { code: 0 };
35
+ }
36
+
37
+ const explicitConfig = hasExplicitConfig(argv);
38
+ const configPath = explicitConfig ? undefined : userConfigPath;
39
+
40
+ if (!explicitConfig) {
41
+ seedUserConfigIfMissing(userConfigPath, stderr);
42
+ const readiness = checkReadiness(userConfigPath, env, home);
43
+ for (const adapter of readiness.unknownAdapters) {
44
+ stderr.write(
45
+ `playbook-code: warning: no readiness check for adapter "${adapter}"\n`,
46
+ );
47
+ }
48
+ if (readiness.failingAdapters.length > 0) {
49
+ stderr.write(
50
+ helpText({
51
+ userConfigPath,
52
+ failingAdapters: readiness.failingAdapters,
53
+ }),
54
+ );
55
+ return { code: READINESS_FAILURE_EXIT_CODE };
56
+ }
57
+ }
58
+
59
+ const childArgs = explicitConfig
60
+ ? [tmuxPlayBin, ...argv]
61
+ : [tmuxPlayBin, '--config', configPath, ...argv];
62
+ return await launchTmuxPlay(spawnFn, childArgs, stderr);
63
+ }
64
+
65
+ export function resolveUserConfigPath(env = process.env, home = homedir()) {
66
+ const configHome = env.XDG_CONFIG_HOME || join(home, '.config');
67
+ return join(configHome, 'playbook', 'playbook-code.config.yaml');
68
+ }
69
+
70
+ export function collectAdaptersFromConfig(source) {
71
+ const adapters = new Set();
72
+ let section = '';
73
+ let captainChildIndent;
74
+ let playerItemIndent;
75
+ let playerChildIndent;
76
+
77
+ for (const line of source.split(/\r?\n/)) {
78
+ const uncommented = stripYamlComment(line);
79
+ if (uncommented.trim() === '') continue;
80
+
81
+ const indent = leadingSpaceCount(uncommented);
82
+ const trimmed = uncommented.trim();
83
+
84
+ if (indent === 0) {
85
+ if (trimmed === 'captain:') section = 'captain';
86
+ else if (trimmed === 'players:') section = 'players';
87
+ else section = '';
88
+ captainChildIndent = undefined;
89
+ playerItemIndent = undefined;
90
+ playerChildIndent = undefined;
91
+ continue;
92
+ }
93
+
94
+ if (section === 'captain') {
95
+ captainChildIndent ??= indent;
96
+ if (indent === captainChildIndent) {
97
+ collectAdapterValue(trimmed, adapters);
98
+ }
99
+ continue;
100
+ }
101
+
102
+ if (section === 'players') {
103
+ if (trimmed.startsWith('- ')) {
104
+ playerItemIndent = indent;
105
+ playerChildIndent = undefined;
106
+ collectAdapterValue(trimmed.slice(2).trim(), adapters);
107
+ continue;
108
+ }
109
+ if (playerItemIndent !== undefined && indent > playerItemIndent) {
110
+ playerChildIndent ??= indent;
111
+ if (indent === playerChildIndent) {
112
+ collectAdapterValue(trimmed, adapters);
113
+ }
114
+ }
115
+ }
116
+ }
117
+ return [...adapters];
118
+ }
119
+
120
+ export function checkReadiness(configPath, env = process.env, home = homedir()) {
121
+ const config = readFileSync(configPath, 'utf8');
122
+ const adapters = collectAdaptersFromConfig(config);
123
+ const failingAdapters = [];
124
+ const unknownAdapters = [];
125
+
126
+ for (const adapter of adapters) {
127
+ if (adapter === 'claude') {
128
+ if (!env.ANTHROPIC_API_KEY && !existsSync(join(home, '.claude'))) {
129
+ failingAdapters.push(adapter);
130
+ }
131
+ continue;
132
+ }
133
+ if (adapter === 'codex') {
134
+ if (!env.OPENAI_API_KEY && !existsSync(join(home, '.codex'))) {
135
+ failingAdapters.push(adapter);
136
+ }
137
+ continue;
138
+ }
139
+ unknownAdapters.push(adapter);
140
+ }
141
+
142
+ return { failingAdapters, unknownAdapters };
143
+ }
144
+
145
+ function seedUserConfigIfMissing(userConfigPath, stderr) {
146
+ if (existsSync(userConfigPath)) return;
147
+ mkdirSync(dirname(userConfigPath), { recursive: true });
148
+ copyFileSync(templatePath, userConfigPath, constants.COPYFILE_EXCL);
149
+ stderr.write(`playbook-code: created config at ${userConfigPath}\n`);
150
+ }
151
+
152
+ function hasExplicitConfig(argv) {
153
+ return argv.some((arg) => arg === '--config' || arg.startsWith('--config='));
154
+ }
155
+
156
+ function helpText({ userConfigPath, failingAdapters = [] }) {
157
+ const failures =
158
+ failingAdapters.length > 0
159
+ ? [
160
+ `Adapters not ready: ${failingAdapters.join(', ')}`,
161
+ '',
162
+ ]
163
+ : [];
164
+ return [
165
+ ...failures,
166
+ 'Usage:',
167
+ ' playbook-code [--config <path>] [tmux-play options]',
168
+ ' playbook-code --help',
169
+ '',
170
+ `Default config: ${userConfigPath}`,
171
+ '',
172
+ 'Adapter setup:',
173
+ ' claude: run Claude Code once or set ANTHROPIC_API_KEY.',
174
+ ' codex: run Codex CLI once or set OPENAI_API_KEY.',
175
+ '',
176
+ 'Agent swap recipe:',
177
+ ' - change captain.adapter and captain.model for the Captain/Judge',
178
+ ' - change each player adapter for the Coder and Reviewer',
179
+ ' - keep captain.from and players[].id fixed',
180
+ '',
181
+ ].join('\n');
182
+ }
183
+
184
+ async function launchTmuxPlay(spawnFn, childArgs, stderr) {
185
+ return await new Promise((resolveResult) => {
186
+ let child;
187
+ try {
188
+ child = spawnFn(process.execPath, childArgs, { stdio: 'inherit' });
189
+ } catch (error) {
190
+ stderr.write(`playbook-code: failed to launch tmux-play: ${errorMessage(error)}\n`);
191
+ resolveResult({ code: 127 });
192
+ return;
193
+ }
194
+
195
+ let settled = false;
196
+ const settle = (result) => {
197
+ if (settled) return;
198
+ settled = true;
199
+ resolveResult(result);
200
+ };
201
+
202
+ child.on('error', (err) => {
203
+ stderr.write(
204
+ `playbook-code: failed to launch tmux-play: ${errorMessage(err)}\n`,
205
+ );
206
+ settle({ code: 127 });
207
+ });
208
+
209
+ child.on('exit', (code, signal) => {
210
+ if (signal) settle({ signal });
211
+ else settle({ code: code ?? 0 });
212
+ });
213
+ });
214
+ }
215
+
216
+ function resolveTmuxPlayBin() {
217
+ const tmuxPlayIndexUrl = import.meta.resolve('@sublang/cligent/tmux-play');
218
+ return join(dirname(fileURLToPath(tmuxPlayIndexUrl)), 'cli.js');
219
+ }
220
+
221
+ function stripYamlComment(line) {
222
+ let quote = '';
223
+ for (let index = 0; index < line.length; index += 1) {
224
+ const char = line[index];
225
+ if ((char === '"' || char === "'") && line[index - 1] !== '\\') {
226
+ quote = quote === char ? '' : quote || char;
227
+ continue;
228
+ }
229
+ if (char === '#' && !quote) {
230
+ return line.slice(0, index);
231
+ }
232
+ }
233
+ return line;
234
+ }
235
+
236
+ function unquoteYamlScalar(value) {
237
+ const trimmed = value.trim();
238
+ if (
239
+ (trimmed.startsWith('"') && trimmed.endsWith('"')) ||
240
+ (trimmed.startsWith("'") && trimmed.endsWith("'"))
241
+ ) {
242
+ return trimmed.slice(1, -1).trim();
243
+ }
244
+ return trimmed;
245
+ }
246
+
247
+ function collectAdapterValue(trimmedLine, adapters) {
248
+ const match = trimmedLine.match(/^adapter\s*:\s*(.+?)\s*$/);
249
+ if (!match) return;
250
+ const value = unquoteYamlScalar(match[1].trim());
251
+ if (value) adapters.add(value);
252
+ }
253
+
254
+ function leadingSpaceCount(line) {
255
+ return line.length - line.trimStart().length;
256
+ }
257
+
258
+ function errorMessage(error) {
259
+ return error instanceof Error ? error.message : String(error);
260
+ }
261
+
262
+ function isCliEntry(
263
+ argv1 = process.argv[1],
264
+ moduleUrl = import.meta.url,
265
+ ) {
266
+ if (!argv1) return false;
267
+ try {
268
+ return realpathSync(argv1) === realpathSync(fileURLToPath(moduleUrl));
269
+ } catch {
270
+ return false;
271
+ }
272
+ }
273
+
274
+ if (isCliEntry()) {
275
+ const result = await runPlaybookCodeCli();
276
+ if (result.signal) process.kill(process.pid, result.signal);
277
+ else process.exit(result.code ?? 0);
278
+ }
@@ -4,6 +4,13 @@ type WorkflowKind = 'singleCommit' | 'iteration' | 'specSummary';
4
4
  type ChangeOrigin = 'bossIntent' | 'irTask';
5
5
  type ReviewSubject = 'commit' | 'changes';
6
6
  type AfterReview = 'continueIr' | 'summarizeSpecs' | 'done';
7
+ type ResumableStateId = Exclude<JumpableStateId, 'ready' | 'failed'>;
8
+ type PendingBossQuestion = {
9
+ resumeStateId: ResumableStateId;
10
+ sourceItem: string;
11
+ player: Player;
12
+ question: string;
13
+ };
7
14
  export type CaptainInput = {
8
15
  player: Player;
9
16
  sourceItem: string;
@@ -16,6 +23,8 @@ export type CaptainInput = {
16
23
  challenges?: string;
17
24
  coderPlayer?: string;
18
25
  reviewerPlayer?: string;
26
+ pendingBossQuestion?: PendingBossQuestion;
27
+ bossReply?: string;
19
28
  };
20
29
  export type CaptainOutput = {
21
30
  guard: string;
@@ -24,6 +33,7 @@ export type CaptainOutput = {
24
33
  reviews?: string;
25
34
  challenges?: string;
26
35
  summary?: string;
36
+ question?: string;
27
37
  [k: string]: unknown;
28
38
  };
29
39
  export type CodingInput = {
@@ -43,6 +53,8 @@ export type CodingContext = CodingInput & {
43
53
  challenges?: string;
44
54
  lastResult?: CaptainOutput;
45
55
  lastError?: unknown;
56
+ pendingBossQuestion?: PendingBossQuestion;
57
+ bossReply?: string;
46
58
  };
47
59
  export type CodingEvent = {
48
60
  type: 'START_CODING';
@@ -58,6 +70,9 @@ export type CodingEvent = {
58
70
  targetId: JumpableStateId;
59
71
  intent?: string;
60
72
  irNumber?: string;
73
+ } | {
74
+ type: 'BOSS_REPLY';
75
+ answer: string;
61
76
  };
62
77
  export declare const codingMachine: import("xstate").StateMachine<CodingContext, {
63
78
  type: "START_CODING";
@@ -73,6 +88,9 @@ export declare const codingMachine: import("xstate").StateMachine<CodingContext,
73
88
  targetId: JumpableStateId;
74
89
  intent?: string;
75
90
  irNumber?: string;
91
+ } | {
92
+ type: "BOSS_REPLY";
93
+ answer: string;
76
94
  }, {
77
95
  [x: string]: import("xstate").ActorRefFromLogic<import("xstate").PromiseActorLogic<CaptainOutput, CaptainInput, import("xstate").EventObject>> | undefined;
78
96
  }, {
@@ -9,6 +9,25 @@ export interface CaptainTransition {
9
9
  readonly index: number;
10
10
  readonly target: string;
11
11
  readonly guard: TransitionGuard;
12
+ readonly actions: unknown;
13
+ }
14
+ export interface AwaitBossReplyInfo {
15
+ readonly stateId: string;
16
+ readonly bossReplyTransitions: ReadonlyArray<BossReplyTransition>;
17
+ readonly transitions: ReadonlyArray<AwaitBossReplyTransition>;
18
+ }
19
+ export interface BossReplyTransition {
20
+ readonly index: number;
21
+ readonly target: string;
22
+ readonly guard: TransitionGuard;
23
+ readonly actions: unknown;
24
+ }
25
+ export interface AwaitBossReplyTransition {
26
+ readonly eventType: string;
27
+ readonly index: number;
28
+ readonly target: string;
29
+ readonly guard: TransitionGuard;
30
+ readonly actions: unknown;
12
31
  }
13
32
  export type TransitionGuard = (args: {
14
33
  context: CodingContext;
@@ -25,6 +44,11 @@ export interface RootEventTable {
25
44
  readonly target: string;
26
45
  };
27
46
  readonly bossInterruptTargets: ReadonlyArray<string>;
47
+ readonly bossInterruptTargetDescriptions: ReadonlyArray<{
48
+ readonly stateId: string;
49
+ readonly description: string;
50
+ }>;
28
51
  }
29
52
  export declare function enumerateCaptainStates(machine: typeof codingMachine): readonly CaptainStateInfo[];
30
53
  export declare function enumerateRootEvents(machine: typeof codingMachine): RootEventTable;
54
+ export declare function enumerateAwaitBossReply(machine: typeof codingMachine): AwaitBossReplyInfo;
@@ -15,6 +15,7 @@ export function enumerateCaptainStates(machine) {
15
15
  index,
16
16
  target: stripIdPrefix(String(arm.target ?? '')),
17
17
  guard: arm.guard ?? alwaysTrue,
18
+ actions: arm.actions,
18
19
  }));
19
20
  out.push({
20
21
  stateId,
@@ -29,13 +30,39 @@ export function enumerateRootEvents(machine) {
29
30
  const cfg = getRawConfig(machine);
30
31
  const readyOn = (cfg.states?.ready?.on ?? {});
31
32
  const rootOn = (cfg.on ?? {});
33
+ const bossInterruptTargets = toArmArray(rootOn.BOSS_INTERRUPT).map((arm) => stripIdPrefix(String(arm.target ?? '')));
32
34
  return {
33
35
  startCoding: { target: stripIdPrefix(String(readyOn.START_CODING?.target ?? '')) },
34
36
  continueIr: { target: stripIdPrefix(String(readyOn.CONTINUE_IR?.target ?? '')) },
35
37
  summarizeIr: { target: stripIdPrefix(String(readyOn.SUMMARIZE_IR?.target ?? '')) },
36
- bossInterruptTargets: toArmArray(rootOn.BOSS_INTERRUPT).map((arm) => stripIdPrefix(String(arm.target ?? ''))),
38
+ bossInterruptTargets,
39
+ bossInterruptTargetDescriptions: bossInterruptTargets.map((stateId) => {
40
+ const description = cfg.states?.[stateId]?.description;
41
+ return {
42
+ stateId,
43
+ description: typeof description === 'string' ? description : '',
44
+ };
45
+ }),
37
46
  };
38
47
  }
48
+ export function enumerateAwaitBossReply(machine) {
49
+ const stateId = 'awaitBossReply';
50
+ const awaitOn = getRawConfig(machine).states?.[stateId]?.on ?? {};
51
+ const transitions = Object.entries(awaitOn).flatMap(([eventType, value]) => toArmArray(value).map((arm, index) => ({
52
+ eventType,
53
+ index,
54
+ target: stripIdPrefix(String(arm.target ?? '')),
55
+ guard: arm.guard ?? alwaysTrue,
56
+ actions: arm.actions,
57
+ })));
58
+ const bossReplyTransitions = toArmArray(awaitOn.BOSS_REPLY).map((arm, index) => ({
59
+ index,
60
+ target: stripIdPrefix(String(arm.target ?? '')),
61
+ guard: arm.guard ?? alwaysTrue,
62
+ actions: arm.actions,
63
+ }));
64
+ return { stateId, bossReplyTransitions, transitions };
65
+ }
39
66
  function getRawConfig(machine) {
40
67
  return machine.config;
41
68
  }