@sublang/playbook 0.6.0 → 0.8.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 +49 -24
- package/package.json +14 -3
- package/reference/sdlc/code.playbook/bin/playbook-code.js +36 -2
- package/reference/sdlc/code.playbook/code.playbook.d.ts +2 -23
- package/reference/sdlc/code.playbook/code.playbook.js +3 -0
- package/reference/sdlc/code.playbook/code.playbook.ts +17 -27
- package/reference/sdlc/code.playbook/playbook-code.config.template.yaml +9 -9
- package/reference/sdlc/code.playbook/tmux-play.config.yaml +7 -2
- package/reference/sdlc/code.playbook/tmux-play.production.config.yaml +7 -2
- package/slc/gears2fsm.md +209 -0
- package/slc/link.md +341 -0
- package/slc/text2gears.md +85 -0
- package/src/runtime.d.ts +23 -0
- package/src/runtime.js +10 -0
- package/src/runtime.ts +36 -0
package/README.md
CHANGED
|
@@ -84,7 +84,7 @@ playbook-code --help
|
|
|
84
84
|
|
|
85
85
|
The seed template runs each agent in cligent's protected auto mode
|
|
86
86
|
(`permissions.mode: auto`), suppressing routine approval prompts. Its
|
|
87
|
-
Codex
|
|
87
|
+
Codex Reviewer also grants `permissions.writablePaths: [.git]` so git
|
|
88
88
|
metadata writes stay available under auto mode without switching to
|
|
89
89
|
bypass permissions.
|
|
90
90
|
|
|
@@ -108,40 +108,41 @@ the runtime binds to those host-configuration invariants per
|
|
|
108
108
|
`<coder-llm>` / `<reviewer-llm>` substitution strings from each role's
|
|
109
109
|
`model` when pinned and `adapter` otherwise — so the Committer's
|
|
110
110
|
commit-message trailers can name the concrete model
|
|
111
|
-
(e.g. `claude-opus-4-8`) rather than the adapter family
|
|
111
|
+
(e.g. `claude-opus-4-8[1m]`) rather than the adapter family
|
|
112
|
+
(`claude`).
|
|
112
113
|
|
|
113
114
|
`players.committer` is an optional alias naming which role — `coder`
|
|
114
115
|
or `reviewer` — runs the commit turn; the seeded overlay points it at
|
|
115
|
-
the
|
|
116
|
+
the Coder. Absent the alias the Committer falls back to the Coder
|
|
116
117
|
([PBRT-8](specs/user/playbook-runtime.md#pbrt-8)).
|
|
117
118
|
|
|
118
|
-
For example, the seeded overlay runs the Coder on
|
|
119
|
-
Reviewer on
|
|
119
|
+
For example, the seeded overlay runs the Coder on Claude Opus 4.8 1m
|
|
120
|
+
and the Reviewer on GPT-5.5, with the Committer aliased to the Coder:
|
|
120
121
|
|
|
121
122
|
```yaml
|
|
122
123
|
captain:
|
|
123
124
|
adapter: claude
|
|
124
|
-
model: claude-
|
|
125
|
+
model: claude-opus-4-8
|
|
125
126
|
reasoningEffort: high
|
|
126
127
|
permissions:
|
|
127
128
|
mode: auto
|
|
128
129
|
|
|
129
130
|
players:
|
|
130
131
|
coder: # role key must stay `coder` — see PBRT-4
|
|
131
|
-
adapter:
|
|
132
|
-
model:
|
|
132
|
+
adapter: claude
|
|
133
|
+
model: claude-opus-4-8[1m]
|
|
133
134
|
reasoningEffort: xhigh
|
|
134
135
|
permissions:
|
|
135
136
|
mode: auto
|
|
136
|
-
writablePaths:
|
|
137
|
-
- .git # allow git metadata writes under Codex auto mode
|
|
138
137
|
reviewer: # role key must stay `reviewer` — see PBRT-4
|
|
139
|
-
adapter:
|
|
140
|
-
model:
|
|
138
|
+
adapter: codex
|
|
139
|
+
model: gpt-5.5
|
|
141
140
|
reasoningEffort: xhigh
|
|
142
141
|
permissions:
|
|
143
142
|
mode: auto
|
|
144
|
-
|
|
143
|
+
writablePaths:
|
|
144
|
+
- .git # allow git metadata writes under Codex auto mode
|
|
145
|
+
committer: coder # which role commits — `coder` or `reviewer`
|
|
145
146
|
```
|
|
146
147
|
|
|
147
148
|
Normal `playbook-code` runs use the seeded path above. If you need a
|
|
@@ -170,15 +171,13 @@ pnpm test
|
|
|
170
171
|
in the checked-in `pnpm-lock.yaml` — the same version CI installs
|
|
171
172
|
via `--frozen-lockfile`, so contributor checkouts and CI agree.
|
|
172
173
|
The published `package.json` declares `@sublang/cligent` as
|
|
173
|
-
|
|
174
|
-
install -g @sublang/playbook`)
|
|
175
|
-
release
|
|
176
|
-
|
|
177
|
-
contributor pin to today's `latest`, run
|
|
174
|
+
`^0.13.0`, so an end-user install with no lockfile (e.g., `npm
|
|
175
|
+
install -g @sublang/playbook`) resolves a compatible cligent 0.13.x
|
|
176
|
+
release (see [RELEASE-14](specs/dev/release.md#release-14)). To
|
|
177
|
+
refresh the contributor pin within that range, run
|
|
178
178
|
`pnpm update @sublang/cligent` and commit the resulting
|
|
179
|
-
`pnpm-lock.yaml` change
|
|
180
|
-
|
|
181
|
-
already matching `package.json` and skips re-resolving the tag.
|
|
179
|
+
`pnpm-lock.yaml` change. To adopt a later cligent minor, update the
|
|
180
|
+
`package.json` specifier and lockfile together.
|
|
182
181
|
No local link required for any of this. To point pnpm at a local
|
|
183
182
|
`cligent` checkout
|
|
184
183
|
instead, copy
|
|
@@ -225,12 +224,19 @@ registered.
|
|
|
225
224
|
### Embedding the runtime in your own host
|
|
226
225
|
|
|
227
226
|
The runtime is host-agnostic; the `tmux-play` adapter is one host.
|
|
227
|
+
The port and runtime contracts live in the type-only module
|
|
228
|
+
[`@sublang/playbook/runtime`](src/runtime.ts) — a public, semver-stable
|
|
229
|
+
surface (`PlayerResult`, `PlaybookPorts`, `PlaybookRuntime`,
|
|
230
|
+
`PlaybookRuntimeFactory`) that imports no CODE or FSM types, so a host
|
|
231
|
+
satisfies it once and inherits every playbook. The CODE runtime
|
|
232
|
+
re-exports `PlayerResult`, `PlaybookPorts`, and `PlaybookRuntime` from
|
|
233
|
+
`@sublang/playbook/code/playbook`; `PlaybookRuntimeFactory` is available
|
|
234
|
+
from `@sublang/playbook/runtime`.
|
|
228
235
|
Construct the runtime against your own ports:
|
|
229
236
|
|
|
230
237
|
```ts
|
|
231
|
-
import createPlaybookRuntime
|
|
232
|
-
|
|
233
|
-
} from '@sublang/playbook/code/playbook';
|
|
238
|
+
import createPlaybookRuntime from '@sublang/playbook/code/playbook';
|
|
239
|
+
import type { PlaybookPorts } from '@sublang/playbook/runtime';
|
|
234
240
|
|
|
235
241
|
const ports: PlaybookPorts = {
|
|
236
242
|
callPlayer: async (playerId, prompt, signal) => { /* … */ },
|
|
@@ -257,6 +263,25 @@ See
|
|
|
257
263
|
for the full range of port shapes (classifier, judge, abort, interrupt,
|
|
258
264
|
status/telemetry) the runtime is contract-tested against.
|
|
259
265
|
|
|
266
|
+
### Reading the published spec contracts
|
|
267
|
+
|
|
268
|
+
The authored compiler-phase specs ship in the package and are exposed
|
|
269
|
+
as a public, semver-stable surface under `@sublang/playbook/slc/*`.
|
|
270
|
+
Resolve and read one with `import.meta.resolve` plus `fs`:
|
|
271
|
+
|
|
272
|
+
```ts
|
|
273
|
+
import { readFile } from 'node:fs/promises';
|
|
274
|
+
import { fileURLToPath } from 'node:url';
|
|
275
|
+
|
|
276
|
+
const url = import.meta.resolve('@sublang/playbook/slc/link.md');
|
|
277
|
+
const link = await readFile(fileURLToPath(url), 'utf8');
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
The three specs are [`slc/text2gears.md`](slc/text2gears.md),
|
|
281
|
+
[`slc/gears2fsm.md`](slc/gears2fsm.md), and [`slc/link.md`](slc/link.md)
|
|
282
|
+
— the FSM-to-runtime contract that `@sublang/playbook/runtime` projects
|
|
283
|
+
into TypeScript.
|
|
284
|
+
|
|
260
285
|
## Workflow
|
|
261
286
|
|
|
262
287
|
playbook is itself spec-driven: the compiler phases are specs in `slc/`,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sublang/playbook",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Reference CODE playbook — XState v5 FSM, host-agnostic runtime, and Playbook Captain shell for a coder/reviewer/committer loop driven by GEARS spec items.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -25,6 +25,12 @@
|
|
|
25
25
|
"node": ">=20.6.0"
|
|
26
26
|
},
|
|
27
27
|
"files": [
|
|
28
|
+
"src/runtime.ts",
|
|
29
|
+
"src/runtime.js",
|
|
30
|
+
"src/runtime.d.ts",
|
|
31
|
+
"slc/link.md",
|
|
32
|
+
"slc/gears2fsm.md",
|
|
33
|
+
"slc/text2gears.md",
|
|
28
34
|
"reference/sdlc/code.playbook/code.fsm.ts",
|
|
29
35
|
"reference/sdlc/code.playbook/code.fsm.js",
|
|
30
36
|
"reference/sdlc/code.playbook/code.fsm.d.ts",
|
|
@@ -53,6 +59,10 @@
|
|
|
53
59
|
"playbook-code": "reference/sdlc/code.playbook/bin/playbook-code.js"
|
|
54
60
|
},
|
|
55
61
|
"exports": {
|
|
62
|
+
"./runtime": {
|
|
63
|
+
"types": "./src/runtime.d.ts",
|
|
64
|
+
"default": "./src/runtime.js"
|
|
65
|
+
},
|
|
56
66
|
"./code/playbook": {
|
|
57
67
|
"types": "./reference/sdlc/code.playbook/code.playbook.d.ts",
|
|
58
68
|
"default": "./reference/sdlc/code.playbook/code.playbook.js"
|
|
@@ -64,7 +74,8 @@
|
|
|
64
74
|
"./code/tmux-play": {
|
|
65
75
|
"types": "./reference/sdlc/code.playbook/code.tmux-play.d.ts",
|
|
66
76
|
"default": "./reference/sdlc/code.playbook/code.tmux-play.js"
|
|
67
|
-
}
|
|
77
|
+
},
|
|
78
|
+
"./slc/*": "./slc/*"
|
|
68
79
|
},
|
|
69
80
|
"scripts": {
|
|
70
81
|
"build": "tsc",
|
|
@@ -78,7 +89,7 @@
|
|
|
78
89
|
"dependencies": {
|
|
79
90
|
"@anthropic-ai/claude-agent-sdk": "^0.3.143",
|
|
80
91
|
"@openai/codex-sdk": "^0.139.0",
|
|
81
|
-
"@sublang/cligent": "
|
|
92
|
+
"@sublang/cligent": "^0.13.0",
|
|
82
93
|
"xstate": "^5.19.4",
|
|
83
94
|
"yaml": "^2.9.0"
|
|
84
95
|
},
|
|
@@ -264,8 +264,15 @@ export function composeRuntimeConfig(
|
|
|
264
264
|
const composed = {};
|
|
265
265
|
const theme = overlayConfig.theme ?? base?.theme;
|
|
266
266
|
if (theme !== undefined) composed.theme = theme;
|
|
267
|
-
|
|
268
|
-
|
|
267
|
+
// PBCODE-16/17: an overlay `layout` is the user's own tmux-play layout
|
|
268
|
+
// and is carried through verbatim; a base-inherited `layout` is
|
|
269
|
+
// reconciled so it round-trips onto the composed coder + reviewer roster
|
|
270
|
+
// (cligent normalizes the base layout to the base config's own roster).
|
|
271
|
+
if (overlayConfig.layout !== undefined) {
|
|
272
|
+
composed.layout = overlayConfig.layout;
|
|
273
|
+
} else if (base?.layout !== undefined) {
|
|
274
|
+
composed.layout = composeInheritedLayout(base.layout);
|
|
275
|
+
}
|
|
269
276
|
const notifications = overlayConfig.notifications ?? base?.notifications;
|
|
270
277
|
if (notifications !== undefined) composed.notifications = notifications;
|
|
271
278
|
composed.captain = captain;
|
|
@@ -273,6 +280,33 @@ export function composeRuntimeConfig(
|
|
|
273
280
|
return composed;
|
|
274
281
|
}
|
|
275
282
|
|
|
283
|
+
// PBCODE-17: reconcile a base-inherited `layout` so it round-trips onto
|
|
284
|
+
// the composed `coder` + `reviewer` roster. cligent's loaded base `layout`
|
|
285
|
+
// is normalized to the base config's own roster and shape: `initialVisible`
|
|
286
|
+
// names the base config's players (cligent rejects ids outside the composed
|
|
287
|
+
// roster) and the normalized output carries both the `columnWeights` alias
|
|
288
|
+
// and the canonical `singlePlayerColumnWeights` / `multiPlayerColumnWeights`
|
|
289
|
+
// (cligent emits the pair on load but rejects it on reload). Drop
|
|
290
|
+
// `initialVisible` — CODE shows its full composed roster, which cligent
|
|
291
|
+
// restores when the field is omitted — and drop the `columnWeights` alias
|
|
292
|
+
// whenever the canonical field for the alias's shape is present, carrying
|
|
293
|
+
// window and weights through otherwise. An overlay `layout` is the user's
|
|
294
|
+
// own and is not reconciled.
|
|
295
|
+
function composeInheritedLayout(layout) {
|
|
296
|
+
if (!isObject(layout)) return layout;
|
|
297
|
+
const result = { ...layout };
|
|
298
|
+
delete result.initialVisible;
|
|
299
|
+
const alias = result.columnWeights;
|
|
300
|
+
if (Array.isArray(alias)) {
|
|
301
|
+
const conflictsSingle =
|
|
302
|
+
alias.length === 2 && result.singlePlayerColumnWeights !== undefined;
|
|
303
|
+
const conflictsMulti =
|
|
304
|
+
alias.length === 3 && result.multiPlayerColumnWeights !== undefined;
|
|
305
|
+
if (conflictsSingle || conflictsMulti) delete result.columnWeights;
|
|
306
|
+
}
|
|
307
|
+
return result;
|
|
308
|
+
}
|
|
309
|
+
|
|
276
310
|
export function adaptersFromComposedConfig(config) {
|
|
277
311
|
const adapters = new Set();
|
|
278
312
|
const captainAdapter = config?.captain?.adapter;
|
|
@@ -1,26 +1,6 @@
|
|
|
1
1
|
import { type CaptainInput, type CaptainOutput, type CodingEvent, type CodingInput } from './code.fsm.js';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
finalText?: string;
|
|
5
|
-
error?: string;
|
|
6
|
-
}
|
|
7
|
-
export interface PlaybookPorts {
|
|
8
|
-
callPlayer(playerId: string, prompt: string, signal: AbortSignal): Promise<PlayerResult>;
|
|
9
|
-
callJudge(prompt: string, signal: AbortSignal): Promise<string>;
|
|
10
|
-
emitStatus(message: string, data?: unknown): Promise<void>;
|
|
11
|
-
emitTelemetry(event: {
|
|
12
|
-
topic: string;
|
|
13
|
-
payload: unknown;
|
|
14
|
-
}): Promise<void>;
|
|
15
|
-
}
|
|
16
|
-
export interface PlaybookRuntime {
|
|
17
|
-
init(ports: PlaybookPorts): Promise<void>;
|
|
18
|
-
handleBossInput(turn: {
|
|
19
|
-
text: string;
|
|
20
|
-
signal: AbortSignal;
|
|
21
|
-
}): Promise<void>;
|
|
22
|
-
dispose(): Promise<void>;
|
|
23
|
-
}
|
|
2
|
+
import type { PlaybookPorts, PlaybookRuntime, PlayerResult } from '@sublang/playbook/runtime';
|
|
3
|
+
export type { PlayerResult, PlaybookPorts, PlaybookRuntime };
|
|
24
4
|
export type CodePlaybookOptions = CodingInput;
|
|
25
5
|
declare function normalizeErrorCompact(err: unknown): {
|
|
26
6
|
name: string;
|
|
@@ -76,4 +56,3 @@ export declare const _internal: {
|
|
|
76
56
|
VERBATIM_PAYLOAD_FIELDS: ReadonlySet<string>;
|
|
77
57
|
};
|
|
78
58
|
export default function createPlaybookRuntime(options: CodePlaybookOptions): PlaybookRuntime;
|
|
79
|
-
export {};
|
|
@@ -11,6 +11,9 @@
|
|
|
11
11
|
// alias's first alternative)
|
|
12
12
|
// Boss event: free-text judge classification
|
|
13
13
|
// Adjudication: LLM-judge per state
|
|
14
|
+
// Contract: PlayerResult / PlaybookPorts / PlaybookRuntime imported
|
|
15
|
+
// and re-exported from @sublang/playbook/runtime
|
|
16
|
+
// (slc/link.md §Output, DR-004 Addendum A4)
|
|
14
17
|
import { createActor, fromPromise } from 'xstate';
|
|
15
18
|
import { codingMachine, } from './code.fsm.js';
|
|
16
19
|
import { enumerateAwaitBossReply, enumerateCaptainStates, enumerateRootEvents, } from './code.fsm.introspect.js';
|
|
@@ -11,6 +11,9 @@
|
|
|
11
11
|
// alias's first alternative)
|
|
12
12
|
// Boss event: free-text judge classification
|
|
13
13
|
// Adjudication: LLM-judge per state
|
|
14
|
+
// Contract: PlayerResult / PlaybookPorts / PlaybookRuntime imported
|
|
15
|
+
// and re-exported from @sublang/playbook/runtime
|
|
16
|
+
// (slc/link.md §Output, DR-004 Addendum A4)
|
|
14
17
|
|
|
15
18
|
import { createActor, fromPromise } from 'xstate';
|
|
16
19
|
import {
|
|
@@ -25,33 +28,20 @@ import {
|
|
|
25
28
|
enumerateCaptainStates,
|
|
26
29
|
enumerateRootEvents,
|
|
27
30
|
} from './code.fsm.introspect.js';
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
prompt: string,
|
|
43
|
-
signal: AbortSignal,
|
|
44
|
-
): Promise<PlayerResult>;
|
|
45
|
-
callJudge(prompt: string, signal: AbortSignal): Promise<string>;
|
|
46
|
-
emitStatus(message: string, data?: unknown): Promise<void>;
|
|
47
|
-
emitTelemetry(event: { topic: string; payload: unknown }): Promise<void>;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export interface PlaybookRuntime {
|
|
51
|
-
init(ports: PlaybookPorts): Promise<void>;
|
|
52
|
-
handleBossInput(turn: { text: string; signal: AbortSignal }): Promise<void>;
|
|
53
|
-
dispose(): Promise<void>;
|
|
54
|
-
}
|
|
31
|
+
import type {
|
|
32
|
+
PlaybookPorts,
|
|
33
|
+
PlaybookRuntime,
|
|
34
|
+
PlayerResult,
|
|
35
|
+
} from '@sublang/playbook/runtime';
|
|
36
|
+
|
|
37
|
+
// Public contract. `PlayerResult`, `PlaybookPorts`, and `PlaybookRuntime`
|
|
38
|
+
// are re-exported from the shared `@sublang/playbook/runtime` module
|
|
39
|
+
// (slc/link.md §Output, DR-004 Addendum A4) so this playbook and any
|
|
40
|
+
// future one resolve one contract definition rather than redefining it.
|
|
41
|
+
// `CodePlaybookOptions` and the default `createPlaybookRuntime` factory
|
|
42
|
+
// (typed `PlaybookRuntimeFactory<CodePlaybookOptions>`) stay
|
|
43
|
+
// CODE-specific.
|
|
44
|
+
export type { PlayerResult, PlaybookPorts, PlaybookRuntime };
|
|
55
45
|
|
|
56
46
|
export type CodePlaybookOptions = CodingInput;
|
|
57
47
|
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
# instead of requiring bypass permissions.
|
|
28
28
|
# - players.committer optionally aliases the Committer to a role
|
|
29
29
|
# (coder or reviewer); that role's pane runs the commit turn. The
|
|
30
|
-
# seeded value is
|
|
30
|
+
# seeded value is coder; omitting it uses the same Coder fallback.
|
|
31
31
|
# - layout sizes the tmux window (layout.window: columns × rows) and
|
|
32
32
|
# sets the relative column widths (layout.columnWeights) for the
|
|
33
33
|
# Boss/Captain, Coder, and Reviewer columns.
|
|
@@ -49,24 +49,24 @@ notifications:
|
|
|
49
49
|
|
|
50
50
|
captain:
|
|
51
51
|
adapter: claude
|
|
52
|
-
model: claude-
|
|
52
|
+
model: claude-opus-4-8
|
|
53
53
|
reasoningEffort: high
|
|
54
54
|
permissions:
|
|
55
55
|
mode: auto
|
|
56
56
|
|
|
57
57
|
players:
|
|
58
58
|
coder:
|
|
59
|
-
adapter:
|
|
60
|
-
model:
|
|
59
|
+
adapter: claude
|
|
60
|
+
model: claude-opus-4-8[1m]
|
|
61
61
|
reasoningEffort: xhigh
|
|
62
62
|
permissions:
|
|
63
63
|
mode: auto
|
|
64
|
-
writablePaths:
|
|
65
|
-
- .git
|
|
66
64
|
reviewer:
|
|
67
|
-
adapter:
|
|
68
|
-
model:
|
|
65
|
+
adapter: codex
|
|
66
|
+
model: gpt-5.5
|
|
69
67
|
reasoningEffort: xhigh
|
|
70
68
|
permissions:
|
|
71
69
|
mode: auto
|
|
72
|
-
|
|
70
|
+
writablePaths:
|
|
71
|
+
- .git
|
|
72
|
+
committer: coder
|
|
@@ -19,12 +19,15 @@ captain:
|
|
|
19
19
|
# shim that delegates to the same shell with CODE registered.
|
|
20
20
|
from: ./playbook-captain.js
|
|
21
21
|
adapter: claude
|
|
22
|
-
model: claude-
|
|
22
|
+
model: claude-opus-4-8
|
|
23
23
|
reasoningEffort: high
|
|
24
24
|
# Agents run in cligent's classifier/reviewer-protected auto mode
|
|
25
25
|
# (cligent DR-005): claude → permissionMode auto, codex → auto_review.
|
|
26
26
|
permissions:
|
|
27
27
|
mode: auto
|
|
28
|
+
options:
|
|
29
|
+
code:
|
|
30
|
+
committer: coder
|
|
28
31
|
|
|
29
32
|
# `players[].id` shall match the baked playerId strings the adapter
|
|
30
33
|
# routes to. The CODE playbook bakes Coder→'coder' and
|
|
@@ -38,7 +41,7 @@ captain:
|
|
|
38
41
|
players:
|
|
39
42
|
- id: coder
|
|
40
43
|
adapter: claude
|
|
41
|
-
model: claude-opus-4-
|
|
44
|
+
model: claude-opus-4-8[1m]
|
|
42
45
|
reasoningEffort: xhigh
|
|
43
46
|
permissions:
|
|
44
47
|
mode: auto
|
|
@@ -48,3 +51,5 @@ players:
|
|
|
48
51
|
reasoningEffort: xhigh
|
|
49
52
|
permissions:
|
|
50
53
|
mode: auto
|
|
54
|
+
writablePaths:
|
|
55
|
+
- .git
|
|
@@ -13,15 +13,18 @@ notifications:
|
|
|
13
13
|
captain:
|
|
14
14
|
from: "@sublang/playbook/playbook-captain"
|
|
15
15
|
adapter: claude
|
|
16
|
-
model: claude-
|
|
16
|
+
model: claude-opus-4-8
|
|
17
17
|
reasoningEffort: high
|
|
18
18
|
permissions:
|
|
19
19
|
mode: auto
|
|
20
|
+
options:
|
|
21
|
+
code:
|
|
22
|
+
committer: coder
|
|
20
23
|
|
|
21
24
|
players:
|
|
22
25
|
- id: coder
|
|
23
26
|
adapter: claude
|
|
24
|
-
model: claude-opus-4-
|
|
27
|
+
model: claude-opus-4-8[1m]
|
|
25
28
|
reasoningEffort: xhigh
|
|
26
29
|
permissions:
|
|
27
30
|
mode: auto
|
|
@@ -31,3 +34,5 @@ players:
|
|
|
31
34
|
reasoningEffort: xhigh
|
|
32
35
|
permissions:
|
|
33
36
|
mode: auto
|
|
37
|
+
writablePaths:
|
|
38
|
+
- .git
|
package/slc/gears2fsm.md
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
<!-- SPDX-License-Identifier: Apache-2.0 -->
|
|
2
|
+
<!-- SPDX-FileCopyrightText: 2026 SubLang International <https://sublang.ai> -->
|
|
3
|
+
|
|
4
|
+
# GEARS-to-Finite-State-Machine Transformation
|
|
5
|
+
|
|
6
|
+
Second phase of a playbook (a state-machine agent orchestrating other agents).
|
|
7
|
+
Transforms normative GEARS spec items into an XState v5 finite state machine.
|
|
8
|
+
|
|
9
|
+
- Source: GEARS spec items produced by the first phase.
|
|
10
|
+
- Target: an XState v5 machine object artifact [[1]].
|
|
11
|
+
|
|
12
|
+
Target is an object artifact only: it defines the machine, actor contracts, and typed inputs, but shall not bind a runner or supply concrete runtime implementations.
|
|
13
|
+
|
|
14
|
+
## Formats
|
|
15
|
+
|
|
16
|
+
| Role | Format | Extension |
|
|
17
|
+
| --- | --- | --- |
|
|
18
|
+
| source | gears | .md |
|
|
19
|
+
| target | fsm | .ts |
|
|
20
|
+
|
|
21
|
+
## Setup
|
|
22
|
+
|
|
23
|
+
The artifact shall use XState v5's `setup(...)` then `.createMachine(...)` [[10]].
|
|
24
|
+
The `types` block shall declare `context`, `events`, machine `input`, and a typed `Captain` actor contract [[11]].
|
|
25
|
+
The artifact shall not import a runner or bake in a concrete Captain implementation; any actor placeholder shall fail explicitly (e.g., throw `'captain actor must be provided by the runner'`).
|
|
26
|
+
|
|
27
|
+
`CaptainInput` shall be a typed object with at least:
|
|
28
|
+
|
|
29
|
+
- `player`: the [player](text2gears.md#players) Captain is to invoke;
|
|
30
|
+
- `sourceItem`: the GEARS item ID this state realizes;
|
|
31
|
+
- `prompt`: the source item's full final prompt, verbatim;
|
|
32
|
+
- `result`: a record whose keys are the valid guard names this invocation may return.
|
|
33
|
+
|
|
34
|
+
`CaptainOutput` shall be a discriminated object with `guard: string` and any extracted fields downstream states need.
|
|
35
|
+
|
|
36
|
+
Guard names shall be specified and interpreted **per state**, not as a global union.
|
|
37
|
+
A global union encourages name reuse with divergent semantics and couples unrelated states.
|
|
38
|
+
Shared helpers may accept `string`, but each state's `invoke.input.result` is the authoritative local contract.
|
|
39
|
+
|
|
40
|
+
## States
|
|
41
|
+
|
|
42
|
+
Each state shall declare:
|
|
43
|
+
|
|
44
|
+
- a stable `id` (for `#id` targeting and Boss interrupts);
|
|
45
|
+
- an intuitive state key (the property name under `states: { ... }`);
|
|
46
|
+
- a one-line `description` (for inspector tools and documentation);
|
|
47
|
+
- if it invokes Captain: `invoke.input` carrying `player`, `sourceItem`, `prompt`, `result` (per [Setup](#setup)).
|
|
48
|
+
|
|
49
|
+
The source item ID shall live in `invoke.input.sourceItem`, not in a comment — this keeps the GEARS-to-state mapping machine-readable.
|
|
50
|
+
A state's `invoke.input.player` shall match its source item's player.
|
|
51
|
+
|
|
52
|
+
Captain returns a discriminated result with `guard` set to one of `input.result`'s keys [[4]].
|
|
53
|
+
Guards [[5]] on `onDone` transitions inspect `event.output.guard` to route.
|
|
54
|
+
|
|
55
|
+
Example:
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
invoke: {
|
|
59
|
+
src: 'captain',
|
|
60
|
+
input: ({ context }): CaptainInput => ({
|
|
61
|
+
player: 'Reviewer',
|
|
62
|
+
sourceItem: '<ITEM-A>',
|
|
63
|
+
prompt: [
|
|
64
|
+
'Flag any issues or improvements (numbered; no duplication).',
|
|
65
|
+
"Think thoroughly — don't just approve or reject.",
|
|
66
|
+
].join('\n'),
|
|
67
|
+
result: {
|
|
68
|
+
hasFindings: 'Reviewer raised issues or suggestions.',
|
|
69
|
+
noFindings: 'Reviewer has no findings.',
|
|
70
|
+
},
|
|
71
|
+
}),
|
|
72
|
+
onDone: [...],
|
|
73
|
+
onError: { target: 'failed', actions: 'rememberCaptainError' },
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Mapping
|
|
78
|
+
|
|
79
|
+
Each Source spec item shall map to exactly one state in Target.
|
|
80
|
+
A state's `invoke.input.sourceItem` shall be that item's ID, and `invoke.input.prompt` shall carry the item's prompt verbatim.
|
|
81
|
+
|
|
82
|
+
Per text2gears [composition](text2gears.md#composition), each spec item already carries the full final prompt for one state behavior, with no duplicate lines.
|
|
83
|
+
The FSM compiler shall not concatenate prompts across items, re-compose them, or silently dedupe.
|
|
84
|
+
A spec item that still contains duplicate prompt lines is malformed; the compiler shall reject or flag it rather than silently propagate the duplication into `invoke.input.prompt`.
|
|
85
|
+
|
|
86
|
+
## Context and prompts
|
|
87
|
+
|
|
88
|
+
Context fields used to drive guards or compose prompts shall be **typed and named**.
|
|
89
|
+
The compiler shall not branch on untyped properties of `lastResult`; persistent routing decisions belong in typed context fields. (`lastResult` is for inspection only.)
|
|
90
|
+
|
|
91
|
+
Prompts shall pass only the **specific extracted fields** the player needs.
|
|
92
|
+
The compiler shall not dump `JSON.stringify(lastResult)` or any opaque blob: it leaks internal `guard` strings, wastes tokens, and confuses the LLM.
|
|
93
|
+
|
|
94
|
+
Player bindings and per-run parameters shall flow in via the machine's `input` and be copied into context at start-up.
|
|
95
|
+
The artifact shall not bake in player bindings, model names, or per-run values.
|
|
96
|
+
|
|
97
|
+
## Transitions
|
|
98
|
+
|
|
99
|
+
A transition fires on an event — typically `onDone` (actor completed) [[4]].
|
|
100
|
+
When multiple are possible, a synchronous guard [[5]] picks the path.
|
|
101
|
+
Transitions shall persist relevant typed fields from `event.output` to context via `assign` [[6]] so downstream prompts can read them.
|
|
102
|
+
Transitions shall be self-driving when source items define the next obligation.
|
|
103
|
+
Routing to an idle hub is for recovery, unrecoverable Boss input, or one-shot entry events — not the happy path.
|
|
104
|
+
|
|
105
|
+
### Auto-advance on approval
|
|
106
|
+
|
|
107
|
+
A review/approval state's success outcome shall **target the next workflow step**, not idle back to a hub.
|
|
108
|
+
Returning to Boss on success is a defect: it forces manual stepping.
|
|
109
|
+
|
|
110
|
+
### Don't re-validate what already passed
|
|
111
|
+
|
|
112
|
+
A state following an approval shall not enter a fresh approval of the same content — that adds latency and risks ping-pong loops.
|
|
113
|
+
A state may route through approval once when its input came from an unreviewed branch (e.g., re-do without an intervening review).
|
|
114
|
+
|
|
115
|
+
### One feedback cycle across phases
|
|
116
|
+
|
|
117
|
+
When the source has a feedback cycle, all phases that need feedback shall reuse it, not duplicate it per phase.
|
|
118
|
+
Phases may set typed routing fields so terminal outcomes return to the originating branch.
|
|
119
|
+
|
|
120
|
+
## Boss control
|
|
121
|
+
|
|
122
|
+
[Boss](text2gears.md#players) input enters the machine through three surfaces: pre-emptive interrupts on active states, typed entry events on idle or recoverable states, and Boss replies to player questions that suspended the FSM in a dedicated wait state.
|
|
123
|
+
|
|
124
|
+
### Boss interrupts
|
|
125
|
+
|
|
126
|
+
Boss may interrupt any active state at any time. Every jumpable state shall have a stable `id` [[9]].
|
|
127
|
+
The runtime sends `{ type: 'BOSS_INTERRUPT', targetId: '<id>' }`; the root machine handles it with one guarded transition per jumpable state targeting `#<id>` with `reenter: true` [[7]][[8]][[9]], so invoked actors restart cleanly.
|
|
128
|
+
The compiler shall emit a `bossInterrupts(ids)` helper rather than hand-writing one transition per state.
|
|
129
|
+
XState automatically stops the current state's invoked actor on transition [[2]].
|
|
130
|
+
|
|
131
|
+
### Boss entry events vs. BOSS_INTERRUPT
|
|
132
|
+
|
|
133
|
+
`BOSS_INTERRUPT` jumps into an **active** machine, pre-empting whichever state is running.
|
|
134
|
+
**Boss entry events** start or resume from idle or recoverable states when Boss-supplied parameters can't be inferred from machine state alone.
|
|
135
|
+
Entry events shall be typed alongside `BOSS_INTERRUPT` and populate context via a dedicated action.
|
|
136
|
+
The two surfaces shall not be collapsed: `BOSS_INTERRUPT` cannot carry payload, and a parameterless entry event may collapse to interrupt-style routing only when state-jump semantics are identical.
|
|
137
|
+
Entry events shall not be root-level transitions from every active state unless the workflow supports pre-emption; they belong on idle and recoverable states (e.g., `failed`).
|
|
138
|
+
|
|
139
|
+
### Boss-reply suspension
|
|
140
|
+
|
|
141
|
+
When a captain-invoking state needs a Boss decision the player cannot supply alone, the machine shall suspend in a dedicated quiescent state and resume the same state with the Q+A in the next prompt.
|
|
142
|
+
This is a third Boss surface alongside `BOSS_INTERRUPT` and Boss entry events.
|
|
143
|
+
|
|
144
|
+
Every captain-invoking state supports this path.
|
|
145
|
+
There is no source-level opt-in annotation and no `needsBossReply` result metadata in GEARS output.
|
|
146
|
+
The FSM compiler shall preserve the GEARS blockquote as the state's domain `prompt` body and shall not inject any Boss-question instruction into `invoke.input.prompt`.
|
|
147
|
+
|
|
148
|
+
For every captain-invoking state, the compiler shall add `needsBossReply` to the state's `invoke.input.result` map.
|
|
149
|
+
The description shall be the standard adjudicator-facing text:
|
|
150
|
+
|
|
151
|
+
```text
|
|
152
|
+
The player's prose surfaces a clarifying question for Boss that the player cannot answer alone. Output shall include `question: <verbatim question text from the player's prose>`.
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
It shall include the load-bearing substring ``Output shall include `question:`` so the runtime's adjudicator requires `question` in the JSON reply.
|
|
156
|
+
The linked runtime composes player prompts per [link.md "Player prompt composition"](link.md#player-prompt-composition), without adding a player-visible Boss-question instruction.
|
|
157
|
+
|
|
158
|
+
The machine shall declare:
|
|
159
|
+
|
|
160
|
+
- An `awaitBossReply` state with stable `id: 'awaitBossReply'` and `description: 'Waiting for Boss to answer a player question.'`.
|
|
161
|
+
- A `BOSS_REPLY` event carrying `{ answer: string }`.
|
|
162
|
+
- Context fields `pendingBossQuestion?: { resumeStateId, sourceItem, player, question }` and `bossReply?: string`.
|
|
163
|
+
Field provenance is normative: `resumeStateId`, `sourceItem`, and `player` shall come from the suspended state's invocation metadata; only `question` shall come from adjudicated player output.
|
|
164
|
+
|
|
165
|
+
The compiler shall emit three helpers:
|
|
166
|
+
|
|
167
|
+
- `resumableStates(ids)` — emits one `BOSS_REPLY` arm per registered state on `awaitBossReply.on.BOSS_REPLY`, each guarded on `context.pendingBossQuestion?.resumeStateId === '<id>'` and targeting `'#<id>'` with `reenter: true`.
|
|
168
|
+
The compiler shall register every captain-invoking state id with this helper.
|
|
169
|
+
The helper is analogous to `bossInterrupts(ids)`.
|
|
170
|
+
- `setPendingBossQuestion` — `assign({ pendingBossQuestion: <new>, bossReply: undefined })`.
|
|
171
|
+
Used on every `needsBossReply` arm; clearing `bossReply` here prevents a follow-up question from inheriting the prior answer.
|
|
172
|
+
- `clearBossReplyContext` — `assign({ pendingBossQuestion: undefined, bossReply: undefined })`.
|
|
173
|
+
Used on every transition out of `awaitBossReply` other than the resume arm, and on every non-`needsBossReply` outcome of a captain-invoking state.
|
|
174
|
+
|
|
175
|
+
`awaitBossReply` is a quiescent state for the runtime's drive loop.
|
|
176
|
+
It shall declare the standard `bossInterrupts(ids)` handler with `actions: clearBossReplyContext`, so a Boss interrupt event abandons a pending question.
|
|
177
|
+
The machine's root-level Boss entry events shall be re-declared on `awaitBossReply` with `actions: clearBossReplyContext`, so a fresh Boss directive while waiting starts a fresh turn and clears stale context.
|
|
178
|
+
|
|
179
|
+
A captain-invoking state's `invoke.input` function shall carry `pendingBossQuestion` and `bossReply` fields when present so the linked runtime can compose the continuation prompt.
|
|
180
|
+
When both fields are present, the linked runtime shall compose the continuation preamble and labelled Q&A blocks per [link.md "Player prompt composition"](link.md#player-prompt-composition).
|
|
181
|
+
The FSM artifact shall not bake the continuation preamble into the GEARS-derived `prompt` body.
|
|
182
|
+
|
|
183
|
+
The following malformed states shall route to `failed` per [Errors and termination](#errors-and-termination):
|
|
184
|
+
|
|
185
|
+
- Captain output has `guard: 'needsBossReply'` but no `question` field.
|
|
186
|
+
- Captain output declares `needsBossReply` from a state not registered with `resumableStates(ids)`.
|
|
187
|
+
- `BOSS_REPLY` fired with empty or whitespace-only `answer`.
|
|
188
|
+
|
|
189
|
+
## Errors and termination
|
|
190
|
+
|
|
191
|
+
Every `invoke` shall declare an `onError` handler routing to a dedicated `failed` state, with the error captured in `context.lastError` for inspection.
|
|
192
|
+
`failed` is not `final`: Boss may interrupt out of it to recover.
|
|
193
|
+
|
|
194
|
+
Every machine shall declare at least one `type: 'final'` state (typically `done`) reachable on completion.
|
|
195
|
+
A never-terminating machine is a defect: the runner has no completion signal.
|
|
196
|
+
|
|
197
|
+
## References
|
|
198
|
+
|
|
199
|
+
[1]: https://stately.ai/docs/xstate "XState Official Documentation"
|
|
200
|
+
[2]: https://stately.ai/docs/invoke "Invoke — invoking actors from states"
|
|
201
|
+
[3]: https://stately.ai/docs/input "Input — passing data to invoked actors"
|
|
202
|
+
[4]: https://stately.ai/docs/output "Output — receiving actor results via onDone"
|
|
203
|
+
[5]: https://stately.ai/docs/guards "Guards — synchronous transition conditions"
|
|
204
|
+
[6]: https://stately.ai/docs/context "Context — persistent state and assign"
|
|
205
|
+
[7]: https://stately.ai/docs/transitions "Transitions — reenter, root-level routing"
|
|
206
|
+
[8]: https://stately.ai/docs/parent-states "Parent states — root-level event handling"
|
|
207
|
+
[9]: https://stately.ai/docs/finite-states "Finite states — state IDs"
|
|
208
|
+
[10]: https://stately.ai/docs/setup "Setup — typed machine setup"
|
|
209
|
+
[11]: https://stately.ai/docs/actors "Actors — typed actor contracts"
|
package/slc/link.md
ADDED
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
<!-- SPDX-License-Identifier: Apache-2.0 -->
|
|
2
|
+
<!-- SPDX-FileCopyrightText: 2026 SubLang International <https://sublang.ai> -->
|
|
3
|
+
|
|
4
|
+
# FSM-to-Runtime Linking
|
|
5
|
+
|
|
6
|
+
Third phase of a playbook (a state-machine agent orchestrating other agents).
|
|
7
|
+
Compiles the [gears2fsm](gears2fsm.md) artifact into a **`PlaybookRuntime`**: a host-agnostic runner that:
|
|
8
|
+
|
|
9
|
+
- Drives the FSM.
|
|
10
|
+
- Classifies Boss input into typed events.
|
|
11
|
+
- Runs the Captain-actor against the playbook's players.
|
|
12
|
+
- Adjudicates player output into FSM guards.
|
|
13
|
+
- Surfaces transitions as status/telemetry.
|
|
14
|
+
|
|
15
|
+
The runtime is invoked through the stable `PlaybookPorts` contract.
|
|
16
|
+
Presentation layers (tmux-play, web, CLI, tests) implement the four ports once and inherit every playbook.
|
|
17
|
+
|
|
18
|
+
- Source: an XState v5 machine artifact (`.fsm.ts`) produced by gears2fsm.
|
|
19
|
+
- Target: a `PlaybookRuntime` factory module — TypeScript, host-agnostic.
|
|
20
|
+
|
|
21
|
+
Hosts are out of scope for this phase.
|
|
22
|
+
Each host has a small adapter (~30 lines) that loads a `PlaybookRuntime` module and supplies the host's primitives as `PlaybookPorts`.
|
|
23
|
+
The adapter shall speak only `PlaybookPorts` to the runtime and shall not leak host types back into it.
|
|
24
|
+
|
|
25
|
+
The link compiler shall not modify the FSM artifact and shall not re-derive Captain prompts, result keys, or guard semantics — those are fixed by the FSM.
|
|
26
|
+
|
|
27
|
+
## Formats
|
|
28
|
+
|
|
29
|
+
| Role | Format | Extension |
|
|
30
|
+
| --- | --- | --- |
|
|
31
|
+
| source | fsm | .ts |
|
|
32
|
+
| target | playbook | .ts |
|
|
33
|
+
|
|
34
|
+
## PlaybookRuntime contract
|
|
35
|
+
|
|
36
|
+
The emitted module shall default-export a factory of the following shape:
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
interface PlaybookRuntime {
|
|
40
|
+
init(ports: PlaybookPorts): Promise<void>;
|
|
41
|
+
handleBossInput(turn: { text: string; signal: AbortSignal }): Promise<void>;
|
|
42
|
+
dispose(): Promise<void>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
type PlaybookRuntimeFactory<Options = unknown> = (
|
|
46
|
+
options: Options,
|
|
47
|
+
) => PlaybookRuntime;
|
|
48
|
+
|
|
49
|
+
export default function createPlaybookRuntime(
|
|
50
|
+
options: PlaybookRuntimeOptions,
|
|
51
|
+
): PlaybookRuntime;
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The default export conforms to `PlaybookRuntimeFactory<PlaybookRuntimeOptions>`, the generic factory type the shared contract module exposes (§Output).
|
|
55
|
+
|
|
56
|
+
`init` receives the host's ports, constructs the XState actor with FSM `input` derived from `options`, and starts the actor.
|
|
57
|
+
The runtime owns the actor for its lifetime; `handleBossInput` runs one turn, and `dispose` stops the actor and drains pending port emissions.
|
|
58
|
+
|
|
59
|
+
`PlaybookRuntimeOptions` is host-agnostic and carries only *per-run* knobs such as identity strings (e.g., model names a playbook substitutes into prompt placeholders) and strategy overrides the linker exposes.
|
|
60
|
+
The link compiler emits a typed options interface per playbook based on the FSM's `CodingInput` (or equivalent).
|
|
61
|
+
|
|
62
|
+
Player binding is a *linker-time* input baked into the emitted runtime by default.
|
|
63
|
+
A linker may also expose it via `PlaybookRuntimeOptions` for per-run remapping; the contract requires only that the runtime ship with a deterministic binding it applies at every `callPlayer` site.
|
|
64
|
+
|
|
65
|
+
## PlaybookPorts contract
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
interface PlaybookPorts {
|
|
69
|
+
callPlayer(playerId: string, prompt: string, signal: AbortSignal):
|
|
70
|
+
Promise<PlayerResult>;
|
|
71
|
+
callJudge(prompt: string, signal: AbortSignal):
|
|
72
|
+
Promise<string>;
|
|
73
|
+
emitStatus(message: string, data?: unknown): Promise<void>;
|
|
74
|
+
emitTelemetry(event: { topic: string; payload: unknown }): Promise<void>;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
interface PlayerResult {
|
|
78
|
+
status: 'ok' | 'aborted' | 'error';
|
|
79
|
+
finalText?: string;
|
|
80
|
+
error?: string;
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`PlayerResult` mirrors cligent's `PlayerRunResult` shape ([TMUX-033](https://github.com/sublang-ai/cligent/blob/main/specs/user/tmux-play.md#tmux-033)), so a tmux-play port adapter is direct assignment.
|
|
85
|
+
The runtime treats `status !== 'ok'` as a player failure and routes it through the FSM's error path (§Abort).
|
|
86
|
+
|
|
87
|
+
`callJudge` returns free-form text.
|
|
88
|
+
The runtime parses it per the state's adjudication strategy (§Captain adjudication).
|
|
89
|
+
One port serves both classifier and adjudicator — they vary only in prompt.
|
|
90
|
+
|
|
91
|
+
`emitStatus` is human-readable; `emitTelemetry` is structured.
|
|
92
|
+
Both are async and shall be ordered, awaited, and never-dropped; the runtime awaits each emission before issuing the next.
|
|
93
|
+
|
|
94
|
+
The runtime never speaks to LLMs directly and never touches host types beyond `PlaybookPorts`.
|
|
95
|
+
|
|
96
|
+
## Linker inputs
|
|
97
|
+
|
|
98
|
+
The link compiler shall accept:
|
|
99
|
+
|
|
100
|
+
- The FSM artifact (path to a `.fsm.ts`).
|
|
101
|
+
- A **player binding** mapping GEARS players (declared in the
|
|
102
|
+
[text2gears](text2gears.md#players) source) to opaque player-identifier
|
|
103
|
+
strings.
|
|
104
|
+
- An **adjudication strategy** (default: LLM-judge per state) and a
|
|
105
|
+
**Boss-event mapping** (default: free-text judge classification).
|
|
106
|
+
Both strategies are host-agnostic.
|
|
107
|
+
|
|
108
|
+
The host's identity does not enter compilation; the linked module runs unchanged under any host that implements `PlaybookPorts`.
|
|
109
|
+
|
|
110
|
+
## Player binding
|
|
111
|
+
|
|
112
|
+
Each GEARS state names exactly one player (`invoke.input.player`).
|
|
113
|
+
The linker shall map every named player to a `playerId` string used in `PlaybookPorts.callPlayer(playerId, …)`.
|
|
114
|
+
The host adapter routes that opaque string to its concrete primitive.
|
|
115
|
+
|
|
116
|
+
For composite players declared with aliases (e.g., `Committer = Coder | Reviewer`), the linker shall resolve the alias **per source item**.
|
|
117
|
+
Resolution inspects the `CaptainInput` fields populated at that state:
|
|
118
|
+
|
|
119
|
+
- If only one `<playerName>Player` field is present, bind to that player.
|
|
120
|
+
- If multiple are present, prefer the first-listed alternative in the alias declaration order.
|
|
121
|
+
- If none are present, fall back to the alias's first alternative.
|
|
122
|
+
|
|
123
|
+
Resolution shall be deterministic and recorded in the emitted module so future maintainers can audit it without re-running the linker.
|
|
124
|
+
|
|
125
|
+
The linker shall not invent player identifiers and shall not silently collapse aliases at the FSM level — composite players keep their `player: 'Committer'` value on `CaptainInput`; resolution decides only the `callPlayer` invocation.
|
|
126
|
+
|
|
127
|
+
## Player prompt composition
|
|
128
|
+
|
|
129
|
+
The runtime shall compose the actual player prompt from the state's `CaptainInput`.
|
|
130
|
+
`input.prompt` is the GEARS-derived domain prompt body and shall not be mutated, re-flowed, or treated as a place to store framework control instructions.
|
|
131
|
+
|
|
132
|
+
The composer may prepend structured labelled blocks from typed `CaptainInput` fields the FSM exposes (for example `Boss intent:`, `Review items:`, `Rebuttals:`, or `Task description:`).
|
|
133
|
+
Those blocks are outside the domain prompt body.
|
|
134
|
+
|
|
135
|
+
The composer shall not inject a player-visible Boss-question instruction.
|
|
136
|
+
Boss-question detection is adjudicator-facing: it comes from the state's `needsBossReply` result description, not from extra prompt text.
|
|
137
|
+
|
|
138
|
+
When `CaptainInput` carries both `pendingBossQuestion` and `bossReply`, the composer shall prepend the continuation preamble and labelled Q&A blocks before ordinary structured blocks and before the domain prompt body:
|
|
139
|
+
|
|
140
|
+
```text
|
|
141
|
+
You previously paused this task to ask Boss a question; Boss has now replied. Continue the same task using the reply below.
|
|
142
|
+
|
|
143
|
+
Boss question:
|
|
144
|
+
<pendingBossQuestion.question>
|
|
145
|
+
|
|
146
|
+
Boss reply:
|
|
147
|
+
<bossReply>
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
The continuation preamble is framework text supplied by the runtime.
|
|
151
|
+
It is not part of the GEARS blockquote and shall not appear in `invoke.input.prompt`.
|
|
152
|
+
|
|
153
|
+
## Boss-event mapping
|
|
154
|
+
|
|
155
|
+
The FSM's `events` union enumerates every Boss-originated event.
|
|
156
|
+
The runtime receives Boss input as a free-form string (`handleBossInput.text`) and shall classify each non-empty turn into one of the FSM's events plus its payload, or no FSM action, by invoking `callJudge`.
|
|
157
|
+
Empty or whitespace-only text produces no event and no port call.
|
|
158
|
+
|
|
159
|
+
The classifier prompt shall demand JSON against the FSM's typed event union and any state-specific Boss input contract, including the payload fields required for each event.
|
|
160
|
+
When the FSM supports a Boss-reply suspension state, the prompt shall include the current state and the pending Boss question so the judge can distinguish a reply from a fresh directive.
|
|
161
|
+
|
|
162
|
+
A playbook runtime shall not define slash-prefix commands for states or features inside that playbook.
|
|
163
|
+
The `/command` namespace is reserved for host-level or playbook-selection UX before a turn reaches `handleBossInput`.
|
|
164
|
+
If a host forwards text beginning with `/` to `handleBossInput`, the runtime treats it as ordinary Boss text and classifies it through `callJudge`.
|
|
165
|
+
|
|
166
|
+
Hosts that receive structured control input shall resolve host-level concerns before choosing a playbook runtime.
|
|
167
|
+
Once they call `handleBossInput`, they shall pass the Boss content as text and shall not pre-classify in-playbook FSM events or rely on slash forms as a runtime protocol.
|
|
168
|
+
|
|
169
|
+
`BOSS_INTERRUPT` (or the FSM's equivalent explicit-state-jump event) is reached only by the judge choosing it and supplying its required target payload.
|
|
170
|
+
It is *not* an abort surface; aborts go through the abort signal and the strategies in §Abort.
|
|
171
|
+
Hosts where the abort signal is terminal (e.g., SIGINT runs shutdown) shall not route abort to `BOSS_INTERRUPT`.
|
|
172
|
+
|
|
173
|
+
## Captain adjudication
|
|
174
|
+
|
|
175
|
+
After a player call returns, the runtime shall coerce `result.finalText` into one of the **per-state** `invoke.input.result` keys.
|
|
176
|
+
It shall also extract any payload fields the state's `result` description names as required.
|
|
177
|
+
|
|
178
|
+
Two default adjudication strategies, in selection order:
|
|
179
|
+
|
|
180
|
+
- **LLM-judge** (default): construct a fresh prompt for `callJudge` that
|
|
181
|
+
names the source item's player, includes the player's verbatim output,
|
|
182
|
+
lists the `result` keys with their descriptions, and demands a JSON
|
|
183
|
+
`{ guard, …payloadFields }` answer keyed to exactly one of the
|
|
184
|
+
declared guards. The judge prompt shall not interpret the player's
|
|
185
|
+
output, paraphrase it, or alter the FSM's `result` text — it carries
|
|
186
|
+
the description verbatim.
|
|
187
|
+
- **Marker-parse** (alternative): a deterministic parser that scans the
|
|
188
|
+
player output for a terminal control line such as
|
|
189
|
+
`FSM-RESULT: { "guard": "...", ... }`. Useful when player adapters can
|
|
190
|
+
be steered to emit structured trailers and the operator wants to avoid
|
|
191
|
+
the extra LLM call.
|
|
192
|
+
|
|
193
|
+
The linker may select different strategies per state; the default is **LLM-judge for every state**.
|
|
194
|
+
|
|
195
|
+
The adjudicator shall fail loudly on:
|
|
196
|
+
|
|
197
|
+
- A guard the state does not declare,
|
|
198
|
+
- A missing payload field the state's `result` description requires,
|
|
199
|
+
- An empty / malformed response.
|
|
200
|
+
|
|
201
|
+
Adjudicator failures are control-plane errors.
|
|
202
|
+
The runtime shall propagate them by throwing out of `handleBossInput` after attempting cleanup.
|
|
203
|
+
The host adapter surfaces the throw on its control-plane channel (cligent surfaces such throws as `runtime_error` per [TMUX-025](https://github.com/sublang-ai/cligent/blob/main/specs/user/tmux-play.md#tmux-025)).
|
|
204
|
+
The host's player-result channels (`player_finished` and equivalents) are reserved for failures the player itself produced; the host emits them when `callPlayer` resolves with `status !== 'ok'`.
|
|
205
|
+
|
|
206
|
+
## Session lifecycle
|
|
207
|
+
|
|
208
|
+
The `PlaybookRuntime` shall:
|
|
209
|
+
|
|
210
|
+
- In `init`, construct the XState actor with FSM `input` derived from
|
|
211
|
+
`options`. The actor is session-scoped, not turn-scoped. Subscribe to
|
|
212
|
+
actor snapshots so each transition can be surfaced via `emitStatus`
|
|
213
|
+
and `emitTelemetry` before the next event fires. Start the actor.
|
|
214
|
+
- Per `handleBossInput`:
|
|
215
|
+
1. Classify `turn.text` through the Boss-event mapping.
|
|
216
|
+
If it produces no event, return after draining any port emissions.
|
|
217
|
+
2. If the actor is in a `final` state, dispose and reconstruct it —
|
|
218
|
+
`final` is terminal and cannot accept new events.
|
|
219
|
+
3. Send the classified event to the actor.
|
|
220
|
+
4. **Drive to quiescence**: each time the actor invokes its `captain`
|
|
221
|
+
actor, await the invoke's input, build a player prompt, call
|
|
222
|
+
`callPlayer`, adjudicate, and resolve the invoke. Repeat until the
|
|
223
|
+
actor's snapshot value is a state that takes a Boss event
|
|
224
|
+
(typically `ready` or `failed`) or a `final` state.
|
|
225
|
+
- In `dispose`, stop the actor and drain pending port emissions.
|
|
226
|
+
|
|
227
|
+
The actor's `lastError` field shall be surfaced via `emitStatus` when the machine enters its `failed` state.
|
|
228
|
+
|
|
229
|
+
## Abort
|
|
230
|
+
|
|
231
|
+
`handleBossInput.signal` is the abort surface.
|
|
232
|
+
The runtime shall honor it at every `callPlayer`/`callJudge` and at every poll between transitions.
|
|
233
|
+
On abort, the runtime shall drive the actor to a quiescent state before returning from the turn.
|
|
234
|
+
Three strategies are permitted; the linker selects per FSM:
|
|
235
|
+
|
|
236
|
+
- **Natural rejection** — the runtime's Captain actor (e.g.,
|
|
237
|
+
`fromPromise`) ends the invocation by rejecting, and the FSM routes
|
|
238
|
+
the rejection through `onError` to a quiescent sink. The cancelled
|
|
239
|
+
port call may *itself* reject, or it may resolve with
|
|
240
|
+
`PlayerResult { status: 'aborted' | 'error' }` that the runtime
|
|
241
|
+
inspects and converts into a Captain-actor rejection. Either shape
|
|
242
|
+
is permitted — the contract is on the Captain-actor boundary, not on
|
|
243
|
+
the port's promise behavior. Preferred when every Captain-invoking
|
|
244
|
+
state's `onError` lands somewhere quiescent; the FSM's own error
|
|
245
|
+
wiring is the abort path.
|
|
246
|
+
- **Synthetic pre-emption to a quiescent target** — send the FSM's
|
|
247
|
+
pre-emption event (e.g., `BOSS_INTERRUPT { targetId: <state> }`) with
|
|
248
|
+
a target that is itself quiescent (typically `ready` or `failed`).
|
|
249
|
+
The runtime shall not pick the active state as the target:
|
|
250
|
+
`gears2fsm.md` prescribes `reenter: true` for `bossInterrupts`, so
|
|
251
|
+
re-entering the active state restarts its `invoke` and spawns a
|
|
252
|
+
fresh player call.
|
|
253
|
+
- **Programmatic stop** — `actor.stop()` and report the turn as aborted
|
|
254
|
+
via `emitStatus`. Reserved for FSMs with neither `onError` wiring nor
|
|
255
|
+
a pre-emption event.
|
|
256
|
+
|
|
257
|
+
Whether the host's outer abort (e.g., SIGINT) is recoverable or terminal is the host's concern.
|
|
258
|
+
The runtime exits `handleBossInput` cleanly in either case; the host decides whether to call `dispose` afterward.
|
|
259
|
+
|
|
260
|
+
## Status and telemetry
|
|
261
|
+
|
|
262
|
+
The runtime shall emit, at minimum:
|
|
263
|
+
|
|
264
|
+
- One `emitStatus` per Boss-relevant transition (entering a state whose
|
|
265
|
+
semantics matter to Boss — e.g., `respondToReview`, `failed`). The
|
|
266
|
+
default is to emit on every transition and let the host filter; hosts
|
|
267
|
+
may bind a stricter rule.
|
|
268
|
+
- One `emitTelemetry` per state transition under a namespaced topic
|
|
269
|
+
(recommended `playbook.fsm.state`), with payload `{ from, to, event }`.
|
|
270
|
+
Observers consume telemetry; the runtime never interprets the topic.
|
|
271
|
+
|
|
272
|
+
Player prompts and adjudicator JSON ride the host's own record channels when the host has them (cligent's `captain_*` / `player_*`).
|
|
273
|
+
The runtime shall not duplicate them into `emitTelemetry`.
|
|
274
|
+
|
|
275
|
+
## Output
|
|
276
|
+
|
|
277
|
+
The link compiler emits **one** TypeScript module that:
|
|
278
|
+
|
|
279
|
+
- Imports the FSM artifact by relative path.
|
|
280
|
+
- Imports XState's actor primitives (`createActor`, `fromPromise`,
|
|
281
|
+
`setup`'s `.provide`).
|
|
282
|
+
- Exports `createPlaybookRuntime` and the typed `PlaybookRuntimeOptions`
|
|
283
|
+
interface for that playbook.
|
|
284
|
+
- Holds no host-specific types and no host primitive calls. The runtime
|
|
285
|
+
speaks only `PlaybookPorts`.
|
|
286
|
+
- Records the linker inputs (FSM path, player binding, strategies) in a
|
|
287
|
+
top-of-file header comment so the file is reproducible from the same
|
|
288
|
+
inputs.
|
|
289
|
+
- Sources the contract types (`PlayerResult`, `PlaybookPorts`,
|
|
290
|
+
`PlaybookRuntime`, `PlaybookRuntimeFactory`) from a single shared
|
|
291
|
+
type-only module instead of redefining them, and re-exports the names
|
|
292
|
+
its consumers import, so every linked playbook shares one contract
|
|
293
|
+
definition. The shared module imports no FSM or host types, so the
|
|
294
|
+
dependency runs one way — from each linked module to the shared
|
|
295
|
+
contract, never the reverse.
|
|
296
|
+
|
|
297
|
+
## Host adaptation (informative, not normative)
|
|
298
|
+
|
|
299
|
+
A host integrates with playbooks via a small adapter that:
|
|
300
|
+
|
|
301
|
+
1. Accepts a path to a `PlaybookRuntime` module (either as a direct
|
|
302
|
+
import in a playbook-specific adapter, or via the host's config
|
|
303
|
+
surface in a generic adapter).
|
|
304
|
+
2. Imports the module and constructs the runtime with options forwarded
|
|
305
|
+
verbatim from the host config.
|
|
306
|
+
3. Implements `PlaybookPorts` by wrapping the host's own primitives —
|
|
307
|
+
for cligent/tmux-play this is `callPlayer ← context.callPlayer`,
|
|
308
|
+
`callJudge ← context.callCaptain`, `emitStatus`/`emitTelemetry` ←
|
|
309
|
+
`session.emitStatus`/`session.emitTelemetry`.
|
|
310
|
+
4. Calls `runtime.init(ports)` once at session start, forwards each
|
|
311
|
+
Boss turn to `runtime.handleBossInput`, and calls
|
|
312
|
+
`runtime.dispose()` at session end.
|
|
313
|
+
|
|
314
|
+
The adapter is ~30 lines regardless of which playbook is loaded.
|
|
315
|
+
Its location is a project-organization choice:
|
|
316
|
+
|
|
317
|
+
- **Playbook repo** — simplest when the playbook author owns the integration; keeps host primitives a lower-layer dependency.
|
|
318
|
+
- **Host repo** — when the host author wants to ship an opt-in playbook Captain.
|
|
319
|
+
- **Third package** — otherwise.
|
|
320
|
+
|
|
321
|
+
This spec is silent on the choice; the contract is the same in any location.
|
|
322
|
+
|
|
323
|
+
## Out of scope
|
|
324
|
+
|
|
325
|
+
- Defining player prompts, result keys, or guard semantics — those
|
|
326
|
+
belong in the GEARS source and the FSM artifact.
|
|
327
|
+
- Host adapter implementations, host configuration, presentation
|
|
328
|
+
layouts — where these live is a per-project decision (see
|
|
329
|
+
§Host adaptation); this spec only constrains the `PlaybookPorts`
|
|
330
|
+
contract they satisfy.
|
|
331
|
+
- Persisting FSM context across sessions, multi-Boss orchestration, or
|
|
332
|
+
visualizer rendering — separate hosts/observers may add them without
|
|
333
|
+
changing this spec.
|
|
334
|
+
|
|
335
|
+
New behavior in any of these areas requires a separate slc spec.
|
|
336
|
+
|
|
337
|
+
## References
|
|
338
|
+
|
|
339
|
+
[1]: [text2gears](text2gears.md) "First phase: text → GEARS spec items."
|
|
340
|
+
[2]: [gears2fsm](gears2fsm.md) "Second phase: GEARS items → FSM artifact."
|
|
341
|
+
[3]: https://stately.ai/docs/actors "XState actors — `createActor`, snapshots, abort signal handling."
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
<!-- SPDX-License-Identifier: Apache-2.0 -->
|
|
2
|
+
<!-- SPDX-FileCopyrightText: 2026 SubLang International <https://sublang.ai> -->
|
|
3
|
+
|
|
4
|
+
# Text-to-GEARS Transformation
|
|
5
|
+
|
|
6
|
+
First phase of a playbook (a state-machine agent orchestrating other agents).
|
|
7
|
+
Transforms a user's procedure description into normative GEARS [[1]] spec items.
|
|
8
|
+
|
|
9
|
+
- Source: free-form natural-language description.
|
|
10
|
+
- Target: a package of GEARS spec items.
|
|
11
|
+
|
|
12
|
+
The second phase (spec items → state machine) is out of scope.
|
|
13
|
+
|
|
14
|
+
## Formats
|
|
15
|
+
|
|
16
|
+
| Role | Format | Extension |
|
|
17
|
+
| --- | --- | --- |
|
|
18
|
+
| source | text | .md |
|
|
19
|
+
| target | gears | .md |
|
|
20
|
+
|
|
21
|
+
## Players
|
|
22
|
+
|
|
23
|
+
Players name AI agents and the user.
|
|
24
|
+
|
|
25
|
+
Two default players:
|
|
26
|
+
|
|
27
|
+
- Boss: the human user
|
|
28
|
+
- Captain: the coordinating agent
|
|
29
|
+
|
|
30
|
+
Source may declare additional players in an opening `Players:` section.
|
|
31
|
+
A player may alias other players with `=` and `|`; Boss picks one at runtime.
|
|
32
|
+
E.g.:
|
|
33
|
+
|
|
34
|
+
- Coder
|
|
35
|
+
- Reviewer
|
|
36
|
+
- Committer = Coder | Reviewer
|
|
37
|
+
|
|
38
|
+
Capitalize English player names (e.g., `Writer`); quote non-English names (e.g., `作者`) when needed to distinguish from prose.
|
|
39
|
+
|
|
40
|
+
## Behaviors
|
|
41
|
+
|
|
42
|
+
Each spec item names a condition, the player to prompt, and the prompt itself.
|
|
43
|
+
Prompts shall be blockquoted, one point per line.
|
|
44
|
+
|
|
45
|
+
E.g.:
|
|
46
|
+
|
|
47
|
+
```markdown
|
|
48
|
+
### CODE-10
|
|
49
|
+
|
|
50
|
+
When Reviewer is about to review any change, Captain shall prompt Reviewer:
|
|
51
|
+
> Flag any issues or improvements (numbered; no duplication).
|
|
52
|
+
> Think thoroughly — don't just approve or reject.
|
|
53
|
+
> If the change is ready to commit or push, don't raise nitpicks.
|
|
54
|
+
> Do not edit files or commit; report findings only.
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Target should be written in the same language as Source.
|
|
58
|
+
|
|
59
|
+
## Composition
|
|
60
|
+
|
|
61
|
+
Source snippets may overlap or duplicate.
|
|
62
|
+
When composing them into a spec item, text2gears shall deduplicate identical prompt lines.
|
|
63
|
+
|
|
64
|
+
Each spec item addresses one state behavior and carries its full final prompt (the static part).
|
|
65
|
+
Cross-item duplication is acceptable: spec items are compiled artifacts; Source is what users maintain.
|
|
66
|
+
|
|
67
|
+
Test: a human shall be able to simulate a run by copying any single item's prompt verbatim — no cross-item composition needed.
|
|
68
|
+
|
|
69
|
+
### Placeholders vs literals
|
|
70
|
+
|
|
71
|
+
Use `<placeholder>` for dynamic values in blockquoted prompts.
|
|
72
|
+
Everything else inside a blockquote is static text, not an example; examples belong in surrounding prose.
|
|
73
|
+
|
|
74
|
+
### Split by content discriminator
|
|
75
|
+
|
|
76
|
+
Partition items by every variable that determines prompt content — including accumulated state when the trigger alone doesn't.
|
|
77
|
+
|
|
78
|
+
### Prune dead disjuncts
|
|
79
|
+
|
|
80
|
+
Drop disjunctive branches incompatible with the rest of an item's condition or prompt.
|
|
81
|
+
Dead branches mislead readers and downstream phases.
|
|
82
|
+
|
|
83
|
+
## References
|
|
84
|
+
|
|
85
|
+
[1]: [GEARS syntax](/specs/meta.md#item-syntax)
|
package/src/runtime.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface PlayerResult {
|
|
2
|
+
status: 'ok' | 'aborted' | 'error';
|
|
3
|
+
finalText?: string;
|
|
4
|
+
error?: string;
|
|
5
|
+
}
|
|
6
|
+
export interface PlaybookPorts {
|
|
7
|
+
callPlayer(playerId: string, prompt: string, signal: AbortSignal): Promise<PlayerResult>;
|
|
8
|
+
callJudge(prompt: string, signal: AbortSignal): Promise<string>;
|
|
9
|
+
emitStatus(message: string, data?: unknown): Promise<void>;
|
|
10
|
+
emitTelemetry(event: {
|
|
11
|
+
topic: string;
|
|
12
|
+
payload: unknown;
|
|
13
|
+
}): Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
export interface PlaybookRuntime {
|
|
16
|
+
init(ports: PlaybookPorts): Promise<void>;
|
|
17
|
+
handleBossInput(turn: {
|
|
18
|
+
text: string;
|
|
19
|
+
signal: AbortSignal;
|
|
20
|
+
}): Promise<void>;
|
|
21
|
+
dispose(): Promise<void>;
|
|
22
|
+
}
|
|
23
|
+
export type PlaybookRuntimeFactory<Options = unknown> = (options: Options) => PlaybookRuntime;
|
package/src/runtime.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// SPDX-FileCopyrightText: 2026 SubLang International <https://sublang.ai>
|
|
3
|
+
//
|
|
4
|
+
// Public runtime contract for @sublang/playbook — the type-only single
|
|
5
|
+
// source for the PlaybookPorts / PlaybookRuntime contract authored in
|
|
6
|
+
// slc/link.md. It imports no CODE or FSM types, so the dependency runs
|
|
7
|
+
// one way: linked playbook runtimes (e.g. code.playbook.ts) import and
|
|
8
|
+
// re-export these names rather than redefining them
|
|
9
|
+
// (PBRT-5, PBRT-34, DR-004 Addendum A4).
|
|
10
|
+
export {};
|
package/src/runtime.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// SPDX-FileCopyrightText: 2026 SubLang International <https://sublang.ai>
|
|
3
|
+
//
|
|
4
|
+
// Public runtime contract for @sublang/playbook — the type-only single
|
|
5
|
+
// source for the PlaybookPorts / PlaybookRuntime contract authored in
|
|
6
|
+
// slc/link.md. It imports no CODE or FSM types, so the dependency runs
|
|
7
|
+
// one way: linked playbook runtimes (e.g. code.playbook.ts) import and
|
|
8
|
+
// re-export these names rather than redefining them
|
|
9
|
+
// (PBRT-5, PBRT-34, DR-004 Addendum A4).
|
|
10
|
+
|
|
11
|
+
export interface PlayerResult {
|
|
12
|
+
status: 'ok' | 'aborted' | 'error';
|
|
13
|
+
finalText?: string;
|
|
14
|
+
error?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface PlaybookPorts {
|
|
18
|
+
callPlayer(
|
|
19
|
+
playerId: string,
|
|
20
|
+
prompt: string,
|
|
21
|
+
signal: AbortSignal,
|
|
22
|
+
): Promise<PlayerResult>;
|
|
23
|
+
callJudge(prompt: string, signal: AbortSignal): Promise<string>;
|
|
24
|
+
emitStatus(message: string, data?: unknown): Promise<void>;
|
|
25
|
+
emitTelemetry(event: { topic: string; payload: unknown }): Promise<void>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface PlaybookRuntime {
|
|
29
|
+
init(ports: PlaybookPorts): Promise<void>;
|
|
30
|
+
handleBossInput(turn: { text: string; signal: AbortSignal }): Promise<void>;
|
|
31
|
+
dispose(): Promise<void>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type PlaybookRuntimeFactory<Options = unknown> = (
|
|
35
|
+
options: Options,
|
|
36
|
+
) => PlaybookRuntime;
|