@sublang/playbook 0.1.2 → 0.2.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 CHANGED
@@ -34,9 +34,12 @@ committer loop end to end.
34
34
 
35
35
  The reference is the canonical worked example —
36
36
  [CODE source](reference/sdlc/code.md) →
37
- [gears](code.gears.md) →
38
- [FSM](code.fsm.ts) → runtime — with the
39
- runtime ported to cligent's `tmux-play` host out of the box.
37
+ [gears](reference/sdlc/code.playbook/code.gears.md) →
38
+ [FSM](reference/sdlc/code.playbook/code.fsm.ts) → runtime — with
39
+ the runtime ported to cligent's `tmux-play` host out of the box.
40
+ The compiled artifacts live under
41
+ [`reference/sdlc/code.playbook/`](reference/sdlc/code.playbook),
42
+ the slc pipeline's `<basename>.<pipeline>/` output directory.
40
43
 
41
44
  ### Install (users)
42
45
 
@@ -53,6 +56,9 @@ Each adapter reads its own auth: the Claude SDK uses your local
53
56
  Claude Code auth (or `ANTHROPIC_API_KEY`); the Codex SDK uses your
54
57
  local codex CLI auth (or `OPENAI_API_KEY`).
55
58
 
59
+ The bundled configs run each agent in cligent's protected auto mode
60
+ (`permissions.mode: auto`), suppressing routine approval prompts.
61
+
56
62
  Then launch the reference playbook in a `tmux-play` session:
57
63
 
58
64
  ```sh
@@ -77,7 +83,7 @@ pnpm build
77
83
  pnpm test
78
84
  ```
79
85
 
80
- `pnpm install` resolves `@sublang/cligent` (0.3.0) from the registry;
86
+ `pnpm install` resolves `@sublang/cligent` (^0.4.0) from the registry;
81
87
  no local link required. To point pnpm at a local `cligent` checkout
82
88
  instead, copy
83
89
  [`pnpm-workspace.yaml.example`](pnpm-workspace.yaml.example)
@@ -85,11 +91,11 @@ into place; the override is gitignored so it never leaks into a
85
91
  production install.
86
92
 
87
93
  Drive a Boss turn against the source tree (uses the developer
88
- [`tmux-play.config.yaml`](tmux-play.config.yaml)
94
+ [`tmux-play.config.yaml`](reference/sdlc/code.playbook/tmux-play.config.yaml)
89
95
  that imports the compiled adapter via relative path):
90
96
 
91
97
  ```sh
92
- pnpm exec tmux-play --config tmux-play.config.yaml
98
+ pnpm exec tmux-play --config reference/sdlc/code.playbook/tmux-play.config.yaml
93
99
  ```
94
100
 
95
101
  `pnpm exec` resolves `tmux-play` from the package's local
@@ -98,16 +104,16 @@ installed globally.
98
104
 
99
105
  ### Running a Boss turn
100
106
 
101
- The Boss pane accepts four slash commands per
102
- [PBRT-1](specs/user/playbook-runtime.md#pbrt-1):
103
-
104
- - `/start <intent>` — begin a single coding turn
105
- - `/continue <#>` pick up an IR task
106
- - `/summarize <#>` turn an IR's commits into spec items
107
- - `/interrupt <stateId> [intent]` preempt the FSM into a named state
107
+ The Boss pane takes plain-language turns; the judge classifies each
108
+ into an FSM event (start a coding turn, continue or summarize an IR,
109
+ interrupt to a named state, or nothing) per
110
+ [PBRT-1](specs/user/playbook-runtime.md#pbrt-1).
111
+ When a player surfaces a clarifying question
112
+ the FSM parks at `awaitBossReply` and the pane shows the question; your
113
+ next turn is normally classified as the reply, or a fresh directive
114
+ abandons it ([PBRT-2](specs/user/playbook-runtime.md#pbrt-2)).
108
115
 
109
- Anything else falls through to an LLM classifier. The Captain pane
110
- streams the state machine progression with a four-glyph vocabulary —
116
+ The Captain pane streams the state machine with a four-glyph vocabulary —
111
117
  `◆ ▸ ⮕ ⤷` per [PBRT-3](specs/user/playbook-runtime.md#pbrt-3) — while
112
118
  player prompts ride their own panes.
113
119
 
@@ -135,14 +141,14 @@ const runtime = createPlaybookRuntime({
135
141
 
136
142
  await runtime.init(ports);
137
143
  await runtime.handleBossInput({
138
- text: '/start fix the bug',
144
+ text: 'Start fixing the bug',
139
145
  signal: new AbortController().signal,
140
146
  });
141
147
  await runtime.dispose();
142
148
  ```
143
149
 
144
150
  See
145
- [`code.playbook.test.ts`](code.playbook.test.ts)
151
+ [`code.playbook.test.ts`](reference/sdlc/code.playbook/code.playbook.test.ts)
146
152
  for the full range of port shapes (classifier, judge, abort, interrupt,
147
153
  status/telemetry) the runtime is contract-tested against.
148
154
 
@@ -175,7 +181,10 @@ package.
175
181
  - Node.js ≥ 20.6.0 (the `playbook-code` shim uses
176
182
  `import.meta.resolve`, unflagged since this release)
177
183
  - pnpm 9 (for the reference package)
178
- - A configured `tmux-play` host (for live Boss turns)
184
+ - A configured `tmux-play` host (for live Boss turns) — requires
185
+ `tmux` and [`glow`](https://github.com/charmbracelet/glow#installation)
186
+ on `PATH`; cligent 0.4+ uses `glow` to render Markdown pane
187
+ output and fails fast without it
179
188
 
180
189
  ## Contributing
181
190
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sublang/playbook",
3
- "version": "0.1.2",
3
+ "version": "0.2.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,34 @@
25
25
  "node": ">=20.6.0"
26
26
  },
27
27
  "files": [
28
- "code.fsm.ts",
29
- "code.fsm.js",
30
- "code.fsm.d.ts",
31
- "code.fsm.introspect.ts",
32
- "code.fsm.introspect.js",
33
- "code.fsm.introspect.d.ts",
34
- "code.playbook.ts",
35
- "code.playbook.js",
36
- "code.playbook.d.ts",
37
- "code.tmux-play.ts",
38
- "code.tmux-play.js",
39
- "code.tmux-play.d.ts",
40
- "code.gears.md",
41
- "tmux-play.config.yaml",
42
- "tmux-play.production.config.yaml",
43
- "bin"
28
+ "reference/sdlc/code.playbook/code.fsm.ts",
29
+ "reference/sdlc/code.playbook/code.fsm.js",
30
+ "reference/sdlc/code.playbook/code.fsm.d.ts",
31
+ "reference/sdlc/code.playbook/code.fsm.introspect.ts",
32
+ "reference/sdlc/code.playbook/code.fsm.introspect.js",
33
+ "reference/sdlc/code.playbook/code.fsm.introspect.d.ts",
34
+ "reference/sdlc/code.playbook/code.playbook.ts",
35
+ "reference/sdlc/code.playbook/code.playbook.js",
36
+ "reference/sdlc/code.playbook/code.playbook.d.ts",
37
+ "reference/sdlc/code.playbook/code.tmux-play.ts",
38
+ "reference/sdlc/code.playbook/code.tmux-play.js",
39
+ "reference/sdlc/code.playbook/code.tmux-play.d.ts",
40
+ "reference/sdlc/code.playbook/code.gears.md",
41
+ "reference/sdlc/code.playbook/tmux-play.config.yaml",
42
+ "reference/sdlc/code.playbook/tmux-play.production.config.yaml",
43
+ "reference/sdlc/code.playbook/bin"
44
44
  ],
45
45
  "bin": {
46
- "playbook-code": "bin/playbook-code.js"
46
+ "playbook-code": "reference/sdlc/code.playbook/bin/playbook-code.js"
47
47
  },
48
48
  "exports": {
49
49
  "./code/playbook": {
50
- "types": "./code.playbook.d.ts",
51
- "default": "./code.playbook.js"
50
+ "types": "./reference/sdlc/code.playbook/code.playbook.d.ts",
51
+ "default": "./reference/sdlc/code.playbook/code.playbook.js"
52
52
  },
53
53
  "./code/tmux-play": {
54
- "types": "./code.tmux-play.d.ts",
55
- "default": "./code.tmux-play.js"
54
+ "types": "./reference/sdlc/code.playbook/code.tmux-play.d.ts",
55
+ "default": "./reference/sdlc/code.playbook/code.tmux-play.js"
56
56
  }
57
57
  },
58
58
  "scripts": {
@@ -66,7 +66,7 @@
66
66
  "dependencies": {
67
67
  "@anthropic-ai/claude-agent-sdk": "^0.3.143",
68
68
  "@openai/codex-sdk": "^0.130.0",
69
- "@sublang/cligent": "^0.3.0",
69
+ "@sublang/cligent": "^0.4.0",
70
70
  "xstate": "^5.19.4"
71
71
  },
72
72
  "devDependencies": {
@@ -4,6 +4,13 @@ type WorkflowKind = 'singleCommit' | 'iteration' | 'specSummary';
4
4
  type ChangeOrigin = 'bossIntent' | 'irTask';
5
5
  type ReviewSubject = 'commit' | 'changes';
6
6
  type AfterReview = 'continueIr' | 'summarizeSpecs' | 'done';
7
+ type ResumableStateId = Exclude<JumpableStateId, 'ready' | 'failed'>;
8
+ type PendingBossQuestion = {
9
+ resumeStateId: ResumableStateId;
10
+ sourceItem: string;
11
+ player: Player;
12
+ question: string;
13
+ };
7
14
  export type CaptainInput = {
8
15
  player: Player;
9
16
  sourceItem: string;
@@ -16,6 +23,8 @@ export type CaptainInput = {
16
23
  challenges?: string;
17
24
  coderPlayer?: string;
18
25
  reviewerPlayer?: string;
26
+ pendingBossQuestion?: PendingBossQuestion;
27
+ bossReply?: string;
19
28
  };
20
29
  export type CaptainOutput = {
21
30
  guard: string;
@@ -24,6 +33,7 @@ export type CaptainOutput = {
24
33
  reviews?: string;
25
34
  challenges?: string;
26
35
  summary?: string;
36
+ question?: string;
27
37
  [k: string]: unknown;
28
38
  };
29
39
  export type CodingInput = {
@@ -43,6 +53,8 @@ export type CodingContext = CodingInput & {
43
53
  challenges?: string;
44
54
  lastResult?: CaptainOutput;
45
55
  lastError?: unknown;
56
+ pendingBossQuestion?: PendingBossQuestion;
57
+ bossReply?: string;
46
58
  };
47
59
  export type CodingEvent = {
48
60
  type: 'START_CODING';
@@ -58,6 +70,9 @@ export type CodingEvent = {
58
70
  targetId: JumpableStateId;
59
71
  intent?: string;
60
72
  irNumber?: string;
73
+ } | {
74
+ type: 'BOSS_REPLY';
75
+ answer: string;
61
76
  };
62
77
  export declare const codingMachine: import("xstate").StateMachine<CodingContext, {
63
78
  type: "START_CODING";
@@ -73,6 +88,9 @@ export declare const codingMachine: import("xstate").StateMachine<CodingContext,
73
88
  targetId: JumpableStateId;
74
89
  intent?: string;
75
90
  irNumber?: string;
91
+ } | {
92
+ type: "BOSS_REPLY";
93
+ answer: string;
76
94
  }, {
77
95
  [x: string]: import("xstate").ActorRefFromLogic<import("xstate").PromiseActorLogic<CaptainOutput, CaptainInput, import("xstate").EventObject>> | undefined;
78
96
  }, {
@@ -9,6 +9,25 @@ export interface CaptainTransition {
9
9
  readonly index: number;
10
10
  readonly target: string;
11
11
  readonly guard: TransitionGuard;
12
+ readonly actions: unknown;
13
+ }
14
+ export interface AwaitBossReplyInfo {
15
+ readonly stateId: string;
16
+ readonly bossReplyTransitions: ReadonlyArray<BossReplyTransition>;
17
+ readonly transitions: ReadonlyArray<AwaitBossReplyTransition>;
18
+ }
19
+ export interface BossReplyTransition {
20
+ readonly index: number;
21
+ readonly target: string;
22
+ readonly guard: TransitionGuard;
23
+ readonly actions: unknown;
24
+ }
25
+ export interface AwaitBossReplyTransition {
26
+ readonly eventType: string;
27
+ readonly index: number;
28
+ readonly target: string;
29
+ readonly guard: TransitionGuard;
30
+ readonly actions: unknown;
12
31
  }
13
32
  export type TransitionGuard = (args: {
14
33
  context: CodingContext;
@@ -25,6 +44,11 @@ export interface RootEventTable {
25
44
  readonly target: string;
26
45
  };
27
46
  readonly bossInterruptTargets: ReadonlyArray<string>;
47
+ readonly bossInterruptTargetDescriptions: ReadonlyArray<{
48
+ readonly stateId: string;
49
+ readonly description: string;
50
+ }>;
28
51
  }
29
52
  export declare function enumerateCaptainStates(machine: typeof codingMachine): readonly CaptainStateInfo[];
30
53
  export declare function enumerateRootEvents(machine: typeof codingMachine): RootEventTable;
54
+ export declare function enumerateAwaitBossReply(machine: typeof codingMachine): AwaitBossReplyInfo;
@@ -15,6 +15,7 @@ export function enumerateCaptainStates(machine) {
15
15
  index,
16
16
  target: stripIdPrefix(String(arm.target ?? '')),
17
17
  guard: arm.guard ?? alwaysTrue,
18
+ actions: arm.actions,
18
19
  }));
19
20
  out.push({
20
21
  stateId,
@@ -29,13 +30,39 @@ export function enumerateRootEvents(machine) {
29
30
  const cfg = getRawConfig(machine);
30
31
  const readyOn = (cfg.states?.ready?.on ?? {});
31
32
  const rootOn = (cfg.on ?? {});
33
+ const bossInterruptTargets = toArmArray(rootOn.BOSS_INTERRUPT).map((arm) => stripIdPrefix(String(arm.target ?? '')));
32
34
  return {
33
35
  startCoding: { target: stripIdPrefix(String(readyOn.START_CODING?.target ?? '')) },
34
36
  continueIr: { target: stripIdPrefix(String(readyOn.CONTINUE_IR?.target ?? '')) },
35
37
  summarizeIr: { target: stripIdPrefix(String(readyOn.SUMMARIZE_IR?.target ?? '')) },
36
- bossInterruptTargets: toArmArray(rootOn.BOSS_INTERRUPT).map((arm) => stripIdPrefix(String(arm.target ?? ''))),
38
+ bossInterruptTargets,
39
+ bossInterruptTargetDescriptions: bossInterruptTargets.map((stateId) => {
40
+ const description = cfg.states?.[stateId]?.description;
41
+ return {
42
+ stateId,
43
+ description: typeof description === 'string' ? description : '',
44
+ };
45
+ }),
37
46
  };
38
47
  }
48
+ export function enumerateAwaitBossReply(machine) {
49
+ const stateId = 'awaitBossReply';
50
+ const awaitOn = getRawConfig(machine).states?.[stateId]?.on ?? {};
51
+ const transitions = Object.entries(awaitOn).flatMap(([eventType, value]) => toArmArray(value).map((arm, index) => ({
52
+ eventType,
53
+ index,
54
+ target: stripIdPrefix(String(arm.target ?? '')),
55
+ guard: arm.guard ?? alwaysTrue,
56
+ actions: arm.actions,
57
+ })));
58
+ const bossReplyTransitions = toArmArray(awaitOn.BOSS_REPLY).map((arm, index) => ({
59
+ index,
60
+ target: stripIdPrefix(String(arm.target ?? '')),
61
+ guard: arm.guard ?? alwaysTrue,
62
+ actions: arm.actions,
63
+ }));
64
+ return { stateId, bossReplyTransitions, transitions };
65
+ }
39
66
  function getRawConfig(machine) {
40
67
  return machine.config;
41
68
  }
@@ -34,6 +34,28 @@ export interface CaptainTransition {
34
34
  readonly index: number;
35
35
  readonly target: string;
36
36
  readonly guard: TransitionGuard;
37
+ readonly actions: unknown;
38
+ }
39
+
40
+ export interface AwaitBossReplyInfo {
41
+ readonly stateId: string;
42
+ readonly bossReplyTransitions: ReadonlyArray<BossReplyTransition>;
43
+ readonly transitions: ReadonlyArray<AwaitBossReplyTransition>;
44
+ }
45
+
46
+ export interface BossReplyTransition {
47
+ readonly index: number;
48
+ readonly target: string;
49
+ readonly guard: TransitionGuard;
50
+ readonly actions: unknown;
51
+ }
52
+
53
+ export interface AwaitBossReplyTransition {
54
+ readonly eventType: string;
55
+ readonly index: number;
56
+ readonly target: string;
57
+ readonly guard: TransitionGuard;
58
+ readonly actions: unknown;
37
59
  }
38
60
 
39
61
  export type TransitionGuard = (args: {
@@ -46,6 +68,10 @@ export interface RootEventTable {
46
68
  readonly continueIr: { readonly target: string };
47
69
  readonly summarizeIr: { readonly target: string };
48
70
  readonly bossInterruptTargets: ReadonlyArray<string>;
71
+ readonly bossInterruptTargetDescriptions: ReadonlyArray<{
72
+ readonly stateId: string;
73
+ readonly description: string;
74
+ }>;
49
75
  }
50
76
 
51
77
  type RawInvoke = {
@@ -54,9 +80,14 @@ type RawInvoke = {
54
80
  onDone?: unknown;
55
81
  };
56
82
 
57
- type RawStateDef = { invoke?: RawInvoke; on?: Record<string, unknown> };
83
+ type RawStateDef = {
84
+ description?: unknown;
85
+ invoke?: RawInvoke;
86
+ on?: Record<string, unknown>;
87
+ };
58
88
 
59
89
  type RawArm = { target?: unknown; guard?: TransitionGuard };
90
+ type RawArmWithActions = RawArm & { actions?: unknown };
60
91
 
61
92
  type RawConfig = {
62
93
  states?: Record<string, RawStateDef>;
@@ -82,6 +113,7 @@ export function enumerateCaptainStates(
82
113
  index,
83
114
  target: stripIdPrefix(String(arm.target ?? '')),
84
115
  guard: arm.guard ?? alwaysTrue,
116
+ actions: arm.actions,
85
117
  }),
86
118
  );
87
119
  out.push({
@@ -103,23 +135,58 @@ export function enumerateRootEvents(
103
135
  { target?: string }
104
136
  >;
105
137
  const rootOn = (cfg.on ?? {}) as Record<string, unknown>;
138
+ const bossInterruptTargets = toArmArray(rootOn.BOSS_INTERRUPT).map((arm) =>
139
+ stripIdPrefix(String(arm.target ?? '')),
140
+ );
106
141
  return {
107
142
  startCoding: { target: stripIdPrefix(String(readyOn.START_CODING?.target ?? '')) },
108
143
  continueIr: { target: stripIdPrefix(String(readyOn.CONTINUE_IR?.target ?? '')) },
109
144
  summarizeIr: { target: stripIdPrefix(String(readyOn.SUMMARIZE_IR?.target ?? '')) },
110
- bossInterruptTargets: toArmArray(rootOn.BOSS_INTERRUPT).map((arm) =>
111
- stripIdPrefix(String(arm.target ?? '')),
112
- ),
145
+ bossInterruptTargets,
146
+ bossInterruptTargetDescriptions: bossInterruptTargets.map((stateId) => {
147
+ const description = cfg.states?.[stateId]?.description;
148
+ return {
149
+ stateId,
150
+ description: typeof description === 'string' ? description : '',
151
+ };
152
+ }),
113
153
  };
114
154
  }
115
155
 
156
+ export function enumerateAwaitBossReply(
157
+ machine: typeof codingMachine,
158
+ ): AwaitBossReplyInfo {
159
+ const stateId = 'awaitBossReply';
160
+ const awaitOn = getRawConfig(machine).states?.[stateId]?.on ?? {};
161
+ const transitions = Object.entries(awaitOn).flatMap(([eventType, value]) =>
162
+ toArmArray(value).map(
163
+ (arm, index): AwaitBossReplyTransition => ({
164
+ eventType,
165
+ index,
166
+ target: stripIdPrefix(String(arm.target ?? '')),
167
+ guard: arm.guard ?? alwaysTrue,
168
+ actions: arm.actions,
169
+ }),
170
+ ),
171
+ );
172
+ const bossReplyTransitions = toArmArray(awaitOn.BOSS_REPLY).map(
173
+ (arm, index): BossReplyTransition => ({
174
+ index,
175
+ target: stripIdPrefix(String(arm.target ?? '')),
176
+ guard: arm.guard ?? alwaysTrue,
177
+ actions: arm.actions,
178
+ }),
179
+ );
180
+ return { stateId, bossReplyTransitions, transitions };
181
+ }
182
+
116
183
  function getRawConfig(machine: typeof codingMachine): RawConfig {
117
184
  return (machine as unknown as { config: RawConfig }).config;
118
185
  }
119
186
 
120
- function toArmArray(value: unknown): RawArm[] {
187
+ function toArmArray(value: unknown): RawArmWithActions[] {
121
188
  if (value === undefined || value === null) return [];
122
- return (Array.isArray(value) ? value : [value]) as RawArm[];
189
+ return (Array.isArray(value) ? value : [value]) as RawArmWithActions[];
123
190
  }
124
191
 
125
192
  function stripIdPrefix(target: string): string {