@sublang/playbook 0.1.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/LICENSE +201 -0
- package/README.md +181 -0
- package/bin/playbook-code.js +40 -0
- package/code.fsm.d.ts +86 -0
- package/code.fsm.introspect.d.ts +30 -0
- package/code.fsm.introspect.js +50 -0
- package/code.fsm.introspect.ts +129 -0
- package/code.fsm.js +1022 -0
- package/code.fsm.ts +1224 -0
- package/code.gears.md +250 -0
- package/code.playbook.d.ts +53 -0
- package/code.playbook.js +598 -0
- package/code.playbook.ts +743 -0
- package/code.tmux-play.d.ts +2 -0
- package/code.tmux-play.js +77 -0
- package/code.tmux-play.ts +110 -0
- package/package.json +77 -0
- package/tmux-play.config.yaml +33 -0
- package/tmux-play.production.config.yaml +21 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// SPDX-FileCopyrightText: 2026 SubLang International <https://sublang.ai>
|
|
3
|
+
|
|
4
|
+
// FSM introspection helpers — IR-005 Task 2.
|
|
5
|
+
// Static walkers over `codingMachine.config` consumed by the
|
|
6
|
+
// conformance, coverage, and prompt-contract tests landing in
|
|
7
|
+
// Tasks 3–5. Reaching into `machine.config` is internal but stable
|
|
8
|
+
// in xstate v5: it preserves the literal `createMachine` argument,
|
|
9
|
+
// so `invoke.input` is still the original `({ context }) =>
|
|
10
|
+
// CaptainInput` function and `invoke.onDone` is still the per-arm
|
|
11
|
+
// array with `guard` / `target` / `actions` keys.
|
|
12
|
+
|
|
13
|
+
import type {
|
|
14
|
+
CaptainInput,
|
|
15
|
+
CodingContext,
|
|
16
|
+
codingMachine,
|
|
17
|
+
} from './code.fsm.js';
|
|
18
|
+
|
|
19
|
+
export interface CaptainStateInfo {
|
|
20
|
+
readonly stateId: string;
|
|
21
|
+
readonly sourceItem: string;
|
|
22
|
+
readonly getInput: (context: Partial<CodingContext>) => CaptainInput;
|
|
23
|
+
readonly transitions: ReadonlyArray<CaptainTransition>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface CaptainTransition {
|
|
27
|
+
// Position in the state's `invoke.onDone` array. Stable per FSM
|
|
28
|
+
// source — the coverage test's fixture table keys
|
|
29
|
+
// `(stateId, index)` to address each arm uniquely, since
|
|
30
|
+
// `(stateId, target)` collides for arms that share a target
|
|
31
|
+
// (e.g., `commitReviewerCleared` and `commitJoint` each route
|
|
32
|
+
// both `committed && afterReview === 'done'` and
|
|
33
|
+
// `noRelevantChanges` to `done`).
|
|
34
|
+
readonly index: number;
|
|
35
|
+
readonly target: string;
|
|
36
|
+
readonly guard: TransitionGuard;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type TransitionGuard = (args: {
|
|
40
|
+
context: CodingContext;
|
|
41
|
+
event: unknown;
|
|
42
|
+
}) => boolean;
|
|
43
|
+
|
|
44
|
+
export interface RootEventTable {
|
|
45
|
+
readonly startCoding: { readonly target: string };
|
|
46
|
+
readonly continueIr: { readonly target: string };
|
|
47
|
+
readonly summarizeIr: { readonly target: string };
|
|
48
|
+
readonly bossInterruptTargets: ReadonlyArray<string>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
type RawInvoke = {
|
|
52
|
+
src?: unknown;
|
|
53
|
+
input?: (args: { context: Partial<CodingContext> }) => CaptainInput;
|
|
54
|
+
onDone?: unknown;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
type RawStateDef = { invoke?: RawInvoke; on?: Record<string, unknown> };
|
|
58
|
+
|
|
59
|
+
type RawArm = { target?: unknown; guard?: TransitionGuard };
|
|
60
|
+
|
|
61
|
+
type RawConfig = {
|
|
62
|
+
states?: Record<string, RawStateDef>;
|
|
63
|
+
on?: Record<string, unknown>;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export function enumerateCaptainStates(
|
|
67
|
+
machine: typeof codingMachine,
|
|
68
|
+
): readonly CaptainStateInfo[] {
|
|
69
|
+
const states = getRawConfig(machine).states ?? {};
|
|
70
|
+
const out: CaptainStateInfo[] = [];
|
|
71
|
+
for (const [stateId, def] of Object.entries(states)) {
|
|
72
|
+
const invoke = def.invoke;
|
|
73
|
+
if (!invoke || invoke.src !== 'captain' || typeof invoke.input !== 'function') {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
const inputFn = invoke.input;
|
|
77
|
+
const getInput = (context: Partial<CodingContext>): CaptainInput =>
|
|
78
|
+
inputFn({ context });
|
|
79
|
+
const probe = getInput({});
|
|
80
|
+
const transitions = toArmArray(invoke.onDone).map(
|
|
81
|
+
(arm, index): CaptainTransition => ({
|
|
82
|
+
index,
|
|
83
|
+
target: stripIdPrefix(String(arm.target ?? '')),
|
|
84
|
+
guard: arm.guard ?? alwaysTrue,
|
|
85
|
+
}),
|
|
86
|
+
);
|
|
87
|
+
out.push({
|
|
88
|
+
stateId,
|
|
89
|
+
sourceItem: probe.sourceItem,
|
|
90
|
+
getInput,
|
|
91
|
+
transitions,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
return out;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function enumerateRootEvents(
|
|
98
|
+
machine: typeof codingMachine,
|
|
99
|
+
): RootEventTable {
|
|
100
|
+
const cfg = getRawConfig(machine);
|
|
101
|
+
const readyOn = (cfg.states?.ready?.on ?? {}) as Record<
|
|
102
|
+
string,
|
|
103
|
+
{ target?: string }
|
|
104
|
+
>;
|
|
105
|
+
const rootOn = (cfg.on ?? {}) as Record<string, unknown>;
|
|
106
|
+
return {
|
|
107
|
+
startCoding: { target: stripIdPrefix(String(readyOn.START_CODING?.target ?? '')) },
|
|
108
|
+
continueIr: { target: stripIdPrefix(String(readyOn.CONTINUE_IR?.target ?? '')) },
|
|
109
|
+
summarizeIr: { target: stripIdPrefix(String(readyOn.SUMMARIZE_IR?.target ?? '')) },
|
|
110
|
+
bossInterruptTargets: toArmArray(rootOn.BOSS_INTERRUPT).map((arm) =>
|
|
111
|
+
stripIdPrefix(String(arm.target ?? '')),
|
|
112
|
+
),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function getRawConfig(machine: typeof codingMachine): RawConfig {
|
|
117
|
+
return (machine as unknown as { config: RawConfig }).config;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function toArmArray(value: unknown): RawArm[] {
|
|
121
|
+
if (value === undefined || value === null) return [];
|
|
122
|
+
return (Array.isArray(value) ? value : [value]) as RawArm[];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function stripIdPrefix(target: string): string {
|
|
126
|
+
return target.startsWith('#') ? target.slice(1) : target;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const alwaysTrue: TransitionGuard = () => true;
|