@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.
- package/README.md +98 -30
- package/package.json +24 -23
- package/reference/sdlc/code.playbook/bin/playbook-code.js +278 -0
- package/{code.fsm.d.ts → reference/sdlc/code.playbook/code.fsm.d.ts} +18 -0
- package/{code.fsm.introspect.d.ts → reference/sdlc/code.playbook/code.fsm.introspect.d.ts} +24 -0
- package/{code.fsm.introspect.js → reference/sdlc/code.playbook/code.fsm.introspect.js} +28 -1
- package/{code.fsm.introspect.ts → reference/sdlc/code.playbook/code.fsm.introspect.ts} +73 -6
- package/{code.fsm.js → reference/sdlc/code.playbook/code.fsm.js} +264 -115
- package/{code.fsm.ts → reference/sdlc/code.playbook/code.fsm.ts} +327 -119
- package/{code.gears.md → reference/sdlc/code.playbook/code.gears.md} +17 -16
- package/{code.playbook.d.ts → reference/sdlc/code.playbook/code.playbook.d.ts} +16 -6
- package/{code.playbook.js → reference/sdlc/code.playbook/code.playbook.js} +228 -129
- package/{code.playbook.ts → reference/sdlc/code.playbook/code.playbook.ts} +316 -122
- package/{code.tmux-play.js → reference/sdlc/code.playbook/code.tmux-play.js} +22 -10
- package/{code.tmux-play.ts → reference/sdlc/code.playbook/code.tmux-play.ts} +26 -13
- package/reference/sdlc/code.playbook/playbook-code.config.template.yaml +38 -0
- package/{tmux-play.config.yaml → reference/sdlc/code.playbook/tmux-play.config.yaml} +14 -10
- package/{tmux-play.production.config.yaml → reference/sdlc/code.playbook/tmux-play.production.config.yaml} +7 -4
- package/bin/playbook-code.js +0 -43
- /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
|
|
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.
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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` (
|
|
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
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
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: '
|
|
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.
|
|
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
|
-
"
|
|
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.
|
|
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
|
|
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
|
}
|