@sublang/playbook 1.0.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -1
- package/package.json +5 -1
- package/reference/sdlc/captain.playbook/captain.fsm.d.ts +1 -1
- package/reference/sdlc/code.playbook/bin/playbook.js +6 -2
- package/reference/sdlc/code.playbook/bin/run.js +110 -4
- package/reference/sdlc/code.playbook/code.playbook.d.ts +4 -16
- package/reference/sdlc/code.playbook/code.playbook.js +88 -1272
- package/reference/sdlc/code.playbook/code.playbook.ts +127 -1547
- package/reference/sdlc/code.playbook/playbook.config.template.yaml +11 -0
- package/reference/sdlc/discuss.playbook/discuss.fsm.d.ts +1 -1
- package/slc/link.md +95 -36
- package/slc/optimize.md +7 -3
- package/slc/text2gears.md +8 -2
- package/src/runtime.d.ts +1 -0
- package/src/runtime.ts +1 -0
- package/src/xstate-playbook-runtime.d.ts +201 -0
- package/src/xstate-playbook-runtime.js +2058 -0
- package/src/xstate-playbook-runtime.ts +2792 -0
- package/src/xstate-runtime.d.ts +1 -0
- package/src/xstate-runtime.js +11 -0
- package/src/xstate-runtime.ts +14 -0
package/README.md
CHANGED
|
@@ -149,7 +149,14 @@ and the captain default to `claude`; bind them with
|
|
|
149
149
|
`claude@high` for the default model at high reasoning effort; the
|
|
150
150
|
model keeps every interior colon (`opencode:ollama/llama3:8b@max`),
|
|
151
151
|
and unsupported efforts are rejected up front naming the adapter's
|
|
152
|
-
supported values.
|
|
152
|
+
supported values. To stop retyping those flags, set durable defaults
|
|
153
|
+
once in the user config's top-level `run:` block — `run.captain`,
|
|
154
|
+
`run.players.<role>`, and a `run.player` catch-all for any other
|
|
155
|
+
required role, each the same agent string; flags keep winning per
|
|
156
|
+
role, and `resume` always keeps the lineup stored with the parked
|
|
157
|
+
session ([PBCLI-28](specs/user/playbook-cli.md#pbcli-28),
|
|
158
|
+
[DR-017](specs/decisions/017-run-defaults-config.md)). Pass a
|
|
159
|
+
playbook option with
|
|
153
160
|
`--option <key>=<value>`, and add `--json` to print one JSON envelope
|
|
154
161
|
(`outcome`, `sessionId`, and the output or pending questions) instead
|
|
155
162
|
of plain text. It exits `0` on a terminal outcome, `2` on failure, `3`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sublang/playbook",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Composable XState v5 playbook runtime with compiled Captain, CODE, and DISCUSS workflows driven by GEARS specs.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -31,6 +31,9 @@
|
|
|
31
31
|
"src/xstate-runtime.ts",
|
|
32
32
|
"src/xstate-runtime.js",
|
|
33
33
|
"src/xstate-runtime.d.ts",
|
|
34
|
+
"src/xstate-playbook-runtime.ts",
|
|
35
|
+
"src/xstate-playbook-runtime.js",
|
|
36
|
+
"src/xstate-playbook-runtime.d.ts",
|
|
34
37
|
"slc/link.md",
|
|
35
38
|
"slc/gears2fsm.md",
|
|
36
39
|
"slc/text2gears.md",
|
|
@@ -124,6 +127,7 @@
|
|
|
124
127
|
"@anthropic-ai/claude-agent-sdk": "^0.3.154",
|
|
125
128
|
"@openai/codex-sdk": "^0.139.0",
|
|
126
129
|
"@sublang/cligent": "^0.15.0",
|
|
130
|
+
"@sublang/spex": "^0.3.0",
|
|
127
131
|
"p-queue": "^9.3.1",
|
|
128
132
|
"xstate": "^5.19.4",
|
|
129
133
|
"yaml": "^2.9.0"
|
|
@@ -198,7 +198,7 @@ export declare const captainMachine: import("xstate").StateMachine<Context, Boss
|
|
|
198
198
|
} | {
|
|
199
199
|
type: "isAuthoredChildError";
|
|
200
200
|
params: unknown;
|
|
201
|
-
}, never, "done" | "failed" | "callPlaybook" | "
|
|
201
|
+
}, never, "done" | "failed" | "callPlaybook" | "awaitBossReply" | "ready" | "routing" | "reassessing", string, CaptainMachineInput, CaptainMachineOutput, import("xstate").EventObject, import("xstate").MetaObject, {
|
|
202
202
|
id: "captain";
|
|
203
203
|
states: {
|
|
204
204
|
readonly ready: {
|
|
@@ -44,6 +44,9 @@ export async function runPlaybookCli(options = {}) {
|
|
|
44
44
|
const stdout = options.stdout ?? process.stdout;
|
|
45
45
|
const stderr = options.stderr ?? process.stderr;
|
|
46
46
|
const loadModule = options.loadModule ?? ((specifier) => import(specifier));
|
|
47
|
+
const home = options.homeDir ?? env.HOME ?? homedir();
|
|
48
|
+
const userConfigPath =
|
|
49
|
+
options.userConfigPath ?? resolveUserConfigPath(env, home);
|
|
47
50
|
|
|
48
51
|
// PBCLI-18: `playbook run ...` is the non-interactive one-shot path; it
|
|
49
52
|
// never seeds, composes, resolves tmux-play, or launches it.
|
|
@@ -53,6 +56,9 @@ export async function runPlaybookCli(options = {}) {
|
|
|
53
56
|
argv: argv.slice(1),
|
|
54
57
|
stdout,
|
|
55
58
|
stderr,
|
|
59
|
+
// PBCLI-29: the run host reads the same user config as the launcher,
|
|
60
|
+
// honoring any injected env, home, or explicit path.
|
|
61
|
+
userConfigPath,
|
|
56
62
|
...(options.loadModule ? { loadModule: options.loadModule } : {}),
|
|
57
63
|
...(options.createAgent ? { createAgent: options.createAgent } : {}),
|
|
58
64
|
...(options.readStdin ? { readStdin: options.readStdin } : {}),
|
|
@@ -62,8 +68,6 @@ export async function runPlaybookCli(options = {}) {
|
|
|
62
68
|
|
|
63
69
|
const spawnFn = options.spawn ?? spawn;
|
|
64
70
|
const tmuxPlayBin = options.tmuxPlayBin ?? resolveTmuxPlayBin();
|
|
65
|
-
const home = options.homeDir ?? env.HOME ?? homedir();
|
|
66
|
-
const userConfigPath = resolveUserConfigPath(env, home);
|
|
67
71
|
|
|
68
72
|
// PBCLI-6: `--help` / `-h` print help and exit 0 without seeding,
|
|
69
73
|
// composing, or launching.
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
isEffortSupported,
|
|
28
28
|
supportedEffortValues,
|
|
29
29
|
} from '@sublang/cligent';
|
|
30
|
+
import { parse as parseYaml } from 'yaml';
|
|
30
31
|
|
|
31
32
|
// PBCLI-19: adapter shorthands the run host can construct.
|
|
32
33
|
const ADAPTER_LOADERS = {
|
|
@@ -61,6 +62,10 @@ export async function runPlaybookRun(options = {}) {
|
|
|
61
62
|
const createAgent = options.createAgent ?? defaultCreateAgent;
|
|
62
63
|
const readStdin = options.readStdin ?? readAllStdin;
|
|
63
64
|
const sessionsDir = options.sessionsDir ?? defaultSessionsDir(process.env);
|
|
65
|
+
// PBCLI-28/29: config defaults come from the same user config file the
|
|
66
|
+
// interactive launcher resolves; tests inject a hermetic path.
|
|
67
|
+
const userConfigPath =
|
|
68
|
+
options.userConfigPath ?? (await defaultUserConfigPath());
|
|
64
69
|
const ctx = {
|
|
65
70
|
stdout,
|
|
66
71
|
stderr,
|
|
@@ -69,6 +74,7 @@ export async function runPlaybookRun(options = {}) {
|
|
|
69
74
|
createAgent,
|
|
70
75
|
readStdin,
|
|
71
76
|
sessionsDir,
|
|
77
|
+
userConfigPath,
|
|
72
78
|
};
|
|
73
79
|
|
|
74
80
|
let args;
|
|
@@ -105,9 +111,28 @@ async function runFirst(args, ctx) {
|
|
|
105
111
|
return { code: EXIT.arg };
|
|
106
112
|
}
|
|
107
113
|
|
|
108
|
-
// PBCLI-
|
|
114
|
+
// PBCLI-28 (DR-017): config-supplied defaults bind only a first run;
|
|
115
|
+
// runResume rebuilds the lineup stored with the session.
|
|
116
|
+
let runDefaults;
|
|
117
|
+
try {
|
|
118
|
+
runDefaults = await loadRunDefaults(ctx.userConfigPath);
|
|
119
|
+
} catch (error) {
|
|
120
|
+
stderr.write(`playbook run: ${message(error)}\n`);
|
|
121
|
+
return { code: EXIT.arg };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// PBCLI-19/28: bind every required role, then the captain — flag over
|
|
125
|
+
// config default over built-in claude. An unrequired run.players role is
|
|
126
|
+
// ignored (the config is global across playbooks); an unrequired
|
|
127
|
+
// --player flag stays an error below.
|
|
109
128
|
const roleSpecs = new Map(
|
|
110
|
-
entry.requiredRoleIds.map((role) => [
|
|
129
|
+
entry.requiredRoleIds.map((role) => [
|
|
130
|
+
role,
|
|
131
|
+
{
|
|
132
|
+
...(runDefaults.players.get(role) ??
|
|
133
|
+
runDefaults.player ?? { adapter: DEFAULT_ADAPTER }),
|
|
134
|
+
},
|
|
135
|
+
]),
|
|
111
136
|
);
|
|
112
137
|
for (const [role, spec] of args.players) {
|
|
113
138
|
if (!roleSpecs.has(role)) {
|
|
@@ -116,7 +141,8 @@ async function runFirst(args, ctx) {
|
|
|
116
141
|
}
|
|
117
142
|
roleSpecs.set(role, spec);
|
|
118
143
|
}
|
|
119
|
-
const captainSpec =
|
|
144
|
+
const captainSpec =
|
|
145
|
+
args.captain ?? runDefaults.captain ?? { adapter: DEFAULT_ADAPTER };
|
|
120
146
|
const specError = specsDiagnostic([...roleSpecs.values(), captainSpec]);
|
|
121
147
|
if (specError !== undefined) {
|
|
122
148
|
stderr.write(`playbook run: ${specError}\n`);
|
|
@@ -586,6 +612,83 @@ function isAgentSpec(spec) {
|
|
|
586
612
|
);
|
|
587
613
|
}
|
|
588
614
|
|
|
615
|
+
// PBCLI-29: the run host reads the same user config file the interactive
|
|
616
|
+
// launcher resolves. The resolver is imported lazily: a static import of
|
|
617
|
+
// ./playbook.js would deadlock the CLI entry — playbook.js is still
|
|
618
|
+
// mid-evaluation of its own top-level await when it dynamically imports
|
|
619
|
+
// this module, and a circular static edge back to it can never settle.
|
|
620
|
+
// The launcher always injects userConfigPath, so this default runs only
|
|
621
|
+
// for direct runPlaybookRun callers, where playbook.js is not evaluating.
|
|
622
|
+
async function defaultUserConfigPath() {
|
|
623
|
+
const { resolveUserConfigPath } = await import('./playbook.js');
|
|
624
|
+
return resolveUserConfigPath(process.env, process.env.HOME ?? homedir());
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// PBCLI-28/29 (DR-017): default agent specs for a first run, read from the
|
|
628
|
+
// user config's top-level `run` map. An absent file or absent map is an
|
|
629
|
+
// empty default set; a malformed file or block fails closed — the run must
|
|
630
|
+
// never silently bind different agents than the user configured. Adapter
|
|
631
|
+
// and effort support of the specs actually bound flow through the shared
|
|
632
|
+
// specsDiagnostic path.
|
|
633
|
+
async function loadRunDefaults(userConfigPath) {
|
|
634
|
+
const defaults = { players: new Map() };
|
|
635
|
+
let text;
|
|
636
|
+
try {
|
|
637
|
+
text = await readFile(userConfigPath, 'utf8');
|
|
638
|
+
} catch (error) {
|
|
639
|
+
if (error?.code === 'ENOENT') return defaults;
|
|
640
|
+
throw new Error(`cannot read config ${userConfigPath}: ${message(error)}`);
|
|
641
|
+
}
|
|
642
|
+
let config;
|
|
643
|
+
try {
|
|
644
|
+
config = parseYaml(text);
|
|
645
|
+
} catch (error) {
|
|
646
|
+
throw new Error(`cannot parse config ${userConfigPath}: ${message(error)}`);
|
|
647
|
+
}
|
|
648
|
+
const run = isPlainMap(config) ? config.run : undefined;
|
|
649
|
+
if (run === undefined || run === null) return defaults;
|
|
650
|
+
if (!isPlainMap(run)) {
|
|
651
|
+
throw new Error(`${userConfigPath}: run must be a map of agent defaults`);
|
|
652
|
+
}
|
|
653
|
+
if (run.captain !== undefined) {
|
|
654
|
+
defaults.captain = parseAgentDefault(run.captain, 'run.captain', userConfigPath);
|
|
655
|
+
}
|
|
656
|
+
if (run.player !== undefined) {
|
|
657
|
+
defaults.player = parseAgentDefault(run.player, 'run.player', userConfigPath);
|
|
658
|
+
}
|
|
659
|
+
if (run.players !== undefined && run.players !== null) {
|
|
660
|
+
if (!isPlainMap(run.players)) {
|
|
661
|
+
throw new Error(
|
|
662
|
+
`${userConfigPath}: run.players must be a map of <role>: <agent>`,
|
|
663
|
+
);
|
|
664
|
+
}
|
|
665
|
+
for (const [role, value] of Object.entries(run.players)) {
|
|
666
|
+
defaults.players.set(
|
|
667
|
+
role,
|
|
668
|
+
parseAgentDefault(value, `run.players.${role}`, userConfigPath),
|
|
669
|
+
);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
return defaults;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
function parseAgentDefault(value, key, userConfigPath) {
|
|
676
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
677
|
+
throw new Error(
|
|
678
|
+
`${userConfigPath}: ${key} must be an <adapter>[:<model>][@<effort>] string`,
|
|
679
|
+
);
|
|
680
|
+
}
|
|
681
|
+
try {
|
|
682
|
+
return parseAgent(value);
|
|
683
|
+
} catch (error) {
|
|
684
|
+
throw new Error(`${userConfigPath}: ${key}: ${message(error)}`);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
function isPlainMap(value) {
|
|
689
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
690
|
+
}
|
|
691
|
+
|
|
589
692
|
// PBCLI-23: the session store honors XDG_STATE_HOME at invocation time.
|
|
590
693
|
export function defaultSessionsDir(env = process.env) {
|
|
591
694
|
const stateHome =
|
|
@@ -877,7 +980,10 @@ function runHelpText() {
|
|
|
877
980
|
' <agent> is <adapter>[:<model>][@<effort>] over the shorthands',
|
|
878
981
|
' claude, codex, gemini, opencode — e.g. codex:gpt-5.5@xhigh, or',
|
|
879
982
|
' claude@high for the default model at high effort. Every role and',
|
|
880
|
-
' the captain default to claude
|
|
983
|
+
' the captain default to claude, unless a top-level run: block in',
|
|
984
|
+
' ${XDG_CONFIG_HOME:-$HOME/.config}/playbook/playbook.config.yaml',
|
|
985
|
+
' supplies defaults — run.captain, run.players.<role>, or the',
|
|
986
|
+
' run.player catch-all for other roles; flags override per role.',
|
|
881
987
|
'',
|
|
882
988
|
' When a playbook needs a Boss reply, the run prints the question,',
|
|
883
989
|
' parks the session under',
|
|
@@ -1,27 +1,14 @@
|
|
|
1
|
+
import { normalizeErrorCompact, normalizeErrorFull, type PlaybookPlayerInput, type RuntimeBoundaryCalls } from '../../../src/xstate-runtime.js';
|
|
1
2
|
import { type PlayerInput, type PlayerOutput, type CodingEvent, type CodingInput } from './code.fsm.js';
|
|
2
3
|
import type { CaptainCallOptions, CaptainResult, JsonValue, NormalizedError, PlayerCallOptions, PlaybookCallRequest, PlaybookCallResult, PlaybookCallStart, PlaybookPendingCall, PlaybookRunResult, PlaybookRuntimeSnapshot, PlaybookSession, PlaybookState, PlaybookStateValue, PlaybookTraceEvent, PlaybookTraceType, PlaybookPorts, PlaybookRuntime, PlaybookRuntimeFactory, PlayerResult } from '@sublang/playbook/runtime';
|
|
3
4
|
export type { CaptainCallOptions, CaptainResult, JsonValue, NormalizedError, PlayerCallOptions, PlaybookCallRequest, PlaybookCallResult, PlaybookCallStart, PlaybookPendingCall, PlaybookRunResult, PlayerResult, PlaybookPorts, PlaybookSession, PlaybookState, PlaybookStateValue, PlaybookTraceEvent, PlaybookTraceType, PlaybookRuntime, PlaybookRuntimeFactory, PlaybookRuntimeSnapshot, };
|
|
4
5
|
export type CodePlaybookOptions = CodingInput;
|
|
5
|
-
declare function normalizeErrorCompact(err: unknown): {
|
|
6
|
-
name: string;
|
|
7
|
-
message: string;
|
|
8
|
-
} | undefined;
|
|
9
|
-
declare function normalizeErrorFull(err: unknown): {
|
|
10
|
-
name: string;
|
|
11
|
-
message: string;
|
|
12
|
-
stack?: string;
|
|
13
|
-
} | undefined;
|
|
14
6
|
declare function normalizeEventForTelemetry(event: unknown): unknown;
|
|
15
7
|
declare function composePlayerPrompt(input: PlayerInput): string;
|
|
16
8
|
declare function resolvePlayerId(input: PlayerInput): string;
|
|
17
|
-
type JudgePurpose = 'boss-input-classification' | 'player-output-adjudication';
|
|
18
|
-
interface RuntimeBoundaryCalls {
|
|
19
|
-
callPlayer(input: PlayerInput, playerId: string, prompt: string, signal: AbortSignal): Promise<PlayerResult>;
|
|
20
|
-
callJudge(purpose: JudgePurpose, stateId: string | undefined, prompt: string, signal: AbortSignal): Promise<string>;
|
|
21
|
-
}
|
|
22
9
|
declare function adjudicate(input: PlayerInput, finalText: string, ports: PlaybookPorts, signal: AbortSignal, boundary?: RuntimeBoundaryCalls): Promise<PlayerOutput>;
|
|
23
10
|
declare function classifyBossText(text: string, ports: PlaybookPorts, signal: AbortSignal, snapshotOrState?: unknown, boundary?: RuntimeBoundaryCalls): Promise<CodingEvent | undefined>;
|
|
24
|
-
declare function captainBridge(ports: PlaybookPorts, getActiveSignal?: () => AbortSignal | undefined, boundary?: RuntimeBoundaryCalls, onControlPlaneError?: (error: unknown) => void): import("xstate").PromiseActorLogic<import("
|
|
11
|
+
declare function captainBridge(ports: PlaybookPorts, getActiveSignal?: () => AbortSignal | undefined, boundary?: RuntimeBoundaryCalls, onControlPlaneError?: (error: unknown) => void): import("xstate").PromiseActorLogic<import("../../../src/xstate-playbook-runtime.js").PlaybookActorOutput, PlaybookPlayerInput>;
|
|
25
12
|
interface StateMetadata {
|
|
26
13
|
player: PlayerInput['player'];
|
|
27
14
|
sourceItem: string;
|
|
@@ -64,4 +51,5 @@ export declare const _internal: {
|
|
|
64
51
|
normalizeEventForTelemetry: typeof normalizeEventForTelemetry;
|
|
65
52
|
VERBATIM_PAYLOAD_FIELDS: ReadonlySet<string>;
|
|
66
53
|
};
|
|
67
|
-
|
|
54
|
+
declare const createPlaybookRuntime: PlaybookRuntimeFactory<CodePlaybookOptions>;
|
|
55
|
+
export default createPlaybookRuntime;
|