@sublang/playbook 13.0.0 → 13.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 +7 -5
- package/docs/cli.md +28 -9
- package/docs/configuration.md +21 -5
- package/docs/embedding.md +1 -1
- package/package.json +41 -3
- package/reference/sdlc/branch.md +37 -0
- package/reference/sdlc/branch.playbook/branch.fsm.d.ts +169 -0
- package/reference/sdlc/branch.playbook/branch.fsm.js +325 -0
- package/reference/sdlc/branch.playbook/branch.fsm.ts +458 -0
- package/reference/sdlc/branch.playbook/branch.gears.md +44 -0
- package/reference/sdlc/branch.playbook/branch.playbook.d.ts +20 -0
- package/reference/sdlc/branch.playbook/branch.playbook.js +121 -0
- package/reference/sdlc/branch.playbook/branch.playbook.ts +217 -0
- package/reference/sdlc/branch.playbook/branch.registry.d.ts +40 -0
- package/reference/sdlc/branch.playbook/branch.registry.js +63 -0
- package/reference/sdlc/branch.playbook/branch.registry.ts +121 -0
- package/reference/sdlc/code.md +7 -0
- package/reference/sdlc/code.playbook/code.fsm.js +5 -5
- package/reference/sdlc/code.playbook/code.fsm.ts +5 -5
- package/reference/sdlc/code.playbook/code.gears.md +5 -5
- package/reference/sdlc/code.playbook/code.playbook.d.ts +1 -1
- package/reference/sdlc/code.playbook/code.playbook.js +6 -16
- package/reference/sdlc/code.playbook/code.playbook.ts +7 -17
- package/reference/sdlc/code.playbook/playbook-captain.js +4 -1
- package/reference/sdlc/code.playbook/playbook-captain.ts +4 -1
- package/reference/sdlc/code.playbook/playbook.config.template.yaml +16 -2
- package/reference/sdlc/decide.md +2 -0
- package/reference/sdlc/decide.playbook/decide.fsm.js +1 -1
- package/reference/sdlc/decide.playbook/decide.fsm.ts +1 -1
- package/reference/sdlc/decide.playbook/decide.gears.md +2 -2
- package/reference/sdlc/decide.playbook/decide.playbook.d.ts +1 -2
- package/reference/sdlc/decide.playbook/decide.playbook.js +8 -22
- package/reference/sdlc/decide.playbook/decide.playbook.ts +8 -25
- package/reference/sdlc/dev.md +45 -4
- package/reference/sdlc/dev.playbook/dev.fsm.d.ts +100 -9
- package/reference/sdlc/dev.playbook/dev.fsm.js +245 -28
- package/reference/sdlc/dev.playbook/dev.fsm.ts +381 -37
- package/reference/sdlc/dev.playbook/dev.gears.md +60 -22
- package/reference/sdlc/dev.playbook/dev.playbook.d.ts +1 -1
- package/reference/sdlc/dev.playbook/dev.playbook.js +20 -18
- package/reference/sdlc/dev.playbook/dev.playbook.ts +21 -21
- package/reference/sdlc/dev.playbook/dev.registry.d.ts +1 -1
- package/reference/sdlc/dev.playbook/dev.registry.js +7 -2
- package/reference/sdlc/dev.playbook/dev.registry.ts +7 -2
- package/reference/sdlc/pr.md +108 -0
- package/reference/sdlc/pr.playbook/pr.fsm.d.ts +352 -0
- package/reference/sdlc/pr.playbook/pr.fsm.js +902 -0
- package/reference/sdlc/pr.playbook/pr.fsm.ts +1251 -0
- package/reference/sdlc/pr.playbook/pr.gears.md +155 -0
- package/reference/sdlc/pr.playbook/pr.playbook.d.ts +27 -0
- package/reference/sdlc/pr.playbook/pr.playbook.js +133 -0
- package/reference/sdlc/pr.playbook/pr.playbook.ts +235 -0
- package/reference/sdlc/pr.playbook/pr.registry.d.ts +40 -0
- package/reference/sdlc/pr.playbook/pr.registry.js +68 -0
- package/reference/sdlc/pr.playbook/pr.registry.ts +124 -0
- package/reference/sdlc/review.md +18 -0
- package/reference/sdlc/review.playbook/review.fsm.js +8 -8
- package/reference/sdlc/review.playbook/review.fsm.ts +8 -8
- package/reference/sdlc/review.playbook/review.gears.md +8 -8
- package/reference/sdlc/review.playbook/review.playbook.d.ts +1 -1
- package/reference/sdlc/review.playbook/review.playbook.js +5 -15
- package/reference/sdlc/review.playbook/review.playbook.ts +6 -16
- package/slc/link.md +12 -24
- package/src/runtime.d.ts +2 -0
- package/src/runtime.ts +2 -0
- package/src/xstate-playbook-runtime.d.ts +4 -2
- package/src/xstate-playbook-runtime.js +18 -13
- package/src/xstate-playbook-runtime.ts +26 -14
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// SPDX-FileCopyrightText: 2026 SubLang International <https://sublang.ai>
|
|
3
|
+
//
|
|
4
|
+
// Generated by slc/gears2fsm.md from ./branch.gears.md.
|
|
5
|
+
import { assign, fromPromise, setup } from 'xstate';
|
|
6
|
+
const CREATE_BRANCH_PROMPT = [
|
|
7
|
+
'> Original request: <caller-input>',
|
|
8
|
+
'',
|
|
9
|
+
'Prepare a new branch for this work without changing any file or making any commit.',
|
|
10
|
+
'Identify the GitHub issue the request names, if any, and read it with its comments (`gh issue view --comments` with the issue number).',
|
|
11
|
+
'If the request could refer to more than one issue, or does not say which work to branch for, ask Boss before creating anything.',
|
|
12
|
+
"Confirm that the working tree is clean and that `gh` is authenticated for the repository's GitHub remote.",
|
|
13
|
+
'Name the branch `issue-N-short-kebab-slug` for issue number N, otherwise a short kebab-case slug of the request.',
|
|
14
|
+
'Create the branch from the current commit and check it out; do not pull, reset, stash, or move HEAD to another commit.',
|
|
15
|
+
'Report the exact branch name, the commit it was created from, and a concise summary of the issue and its comments, or of the request when no issue is named.',
|
|
16
|
+
'If the working tree is not clean, `gh` is not authenticated, the named issue does not exist or cannot be read, or a branch with that name already exists locally or on the remote, create nothing and report the failure with its reason.',
|
|
17
|
+
].join('\n');
|
|
18
|
+
const CREATE_BRANCH_RESULTS = {
|
|
19
|
+
branched: 'Coder created the new branch from the current commit and checked it out, reporting its exact name and a concise summary of the issue and its comments, or of the request when no issue is named; the absence of a reported obstacle is not support. Output shall include `branch: <exact branch name>`, `baseRevision: <repository revision>`, and `issueSummary: <concise summary>`.',
|
|
20
|
+
refused: 'Coder created nothing and reported the failure with its reason: the working tree is not clean, `gh` is not authenticated, the named issue does not exist or cannot be read, or a branch with that name already exists locally or on the remote. Output shall include `coderOutput: <verbatim final text>`.',
|
|
21
|
+
needsBossReply: "The acting agent's prose surfaces a clarifying question for Boss that the agent cannot answer alone. Output shall include `question: <verbatim question text from the acting agent's prose>`.",
|
|
22
|
+
};
|
|
23
|
+
const STATE_DESCRIPTIONS = {
|
|
24
|
+
ready: 'Waiting for a development request to branch for.',
|
|
25
|
+
createBranch: 'Coder is reading the named issue and creating and checking out a new branch at the current commit.',
|
|
26
|
+
awaitBossReply: 'Waiting for Boss to answer Coder.',
|
|
27
|
+
failed: 'The branch workflow failed and is waiting for a new development request.',
|
|
28
|
+
branched: "A new branch for the request is checked out at the caller's current commit; no file changed and no commit was made.",
|
|
29
|
+
refused: 'No branch was created: Coder reported the obstacle with its reason — a dirty working tree, an unauthenticated `gh`, an unreadable issue, or a branch-name collision.',
|
|
30
|
+
};
|
|
31
|
+
function playbookMeta(stateId, role) {
|
|
32
|
+
return {
|
|
33
|
+
playbook: {
|
|
34
|
+
stateId,
|
|
35
|
+
description: STATE_DESCRIPTIONS[stateId],
|
|
36
|
+
...(role === undefined ? {} : { role }),
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
// DR-048: a final state additionally declares whether its outcome means the
|
|
41
|
+
// workflow succeeded or failed, so BRANCH's own caller learns that from the
|
|
42
|
+
// machine rather than from BRANCH's output fields.
|
|
43
|
+
function terminalMeta(stateId, terminal) {
|
|
44
|
+
return {
|
|
45
|
+
playbook: {
|
|
46
|
+
stateId,
|
|
47
|
+
description: STATE_DESCRIPTIONS[stateId],
|
|
48
|
+
terminal,
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function isRecord(value) {
|
|
53
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
const prototype = Object.getPrototypeOf(value);
|
|
57
|
+
return prototype === Object.prototype || prototype === null;
|
|
58
|
+
}
|
|
59
|
+
function isNonEmptyString(value) {
|
|
60
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
61
|
+
}
|
|
62
|
+
function playerOutput(event) {
|
|
63
|
+
if (!isRecord(event))
|
|
64
|
+
return undefined;
|
|
65
|
+
return isRecord(event.output) ? event.output : undefined;
|
|
66
|
+
}
|
|
67
|
+
function outputGuard(event, guard) {
|
|
68
|
+
return playerOutput(event)?.guard === guard;
|
|
69
|
+
}
|
|
70
|
+
function outputString(event, field) {
|
|
71
|
+
const value = playerOutput(event)?.[field];
|
|
72
|
+
return isNonEmptyString(value) ? value : undefined;
|
|
73
|
+
}
|
|
74
|
+
// DR-045: the branched outcome is accepted only with the receipt-observed
|
|
75
|
+
// base revision the linked runtime injects; Coder's prose never supplies it.
|
|
76
|
+
function isBranched({ event }) {
|
|
77
|
+
return (outputGuard(event, 'branched') &&
|
|
78
|
+
outputString(event, 'branch') !== undefined &&
|
|
79
|
+
outputString(event, 'baseRevision') !== undefined &&
|
|
80
|
+
outputString(event, 'issueSummary') !== undefined);
|
|
81
|
+
}
|
|
82
|
+
function isRefused({ event }) {
|
|
83
|
+
return (outputGuard(event, 'refused') &&
|
|
84
|
+
outputString(event, 'coderOutput') !== undefined);
|
|
85
|
+
}
|
|
86
|
+
function needsBossReply({ event }) {
|
|
87
|
+
return (outputGuard(event, 'needsBossReply') &&
|
|
88
|
+
outputString(event, 'question') !== undefined);
|
|
89
|
+
}
|
|
90
|
+
const machineSetup = setup({
|
|
91
|
+
types: {},
|
|
92
|
+
actors: {
|
|
93
|
+
player: fromPromise(async () => {
|
|
94
|
+
throw new Error('player actor must be provided by the runner');
|
|
95
|
+
}),
|
|
96
|
+
},
|
|
97
|
+
guards: {
|
|
98
|
+
isBranched,
|
|
99
|
+
isRefused,
|
|
100
|
+
needsBossReply,
|
|
101
|
+
emptyBossReply: ({ event }) => event.type === 'BOSS_REPLY' && event.answer.trim().length === 0,
|
|
102
|
+
resumesCreateBranch: ({ context, event }) => event.type === 'BOSS_REPLY' &&
|
|
103
|
+
event.answer.trim().length > 0 &&
|
|
104
|
+
context.pendingBossQuestion?.resumeStateId === 'createBranch' &&
|
|
105
|
+
(event.questionId === undefined || event.questionId === 'createBranch'),
|
|
106
|
+
},
|
|
107
|
+
actions: {
|
|
108
|
+
'playbook.acceptedOutcome': (_args, _params) => undefined,
|
|
109
|
+
startBranch: assign(({ event }) => {
|
|
110
|
+
if (event.type !== 'START_BRANCH')
|
|
111
|
+
return {};
|
|
112
|
+
return {
|
|
113
|
+
callerInput: event.callerInput,
|
|
114
|
+
branch: undefined,
|
|
115
|
+
baseRevision: undefined,
|
|
116
|
+
issueSummary: undefined,
|
|
117
|
+
coderOutput: undefined,
|
|
118
|
+
completion: undefined,
|
|
119
|
+
lastError: undefined,
|
|
120
|
+
pendingBossQuestion: undefined,
|
|
121
|
+
bossReply: undefined,
|
|
122
|
+
};
|
|
123
|
+
}),
|
|
124
|
+
rememberBranched: assign(({ event }) => ({
|
|
125
|
+
branch: outputString(event, 'branch'),
|
|
126
|
+
// DR-045: the receipt-observed HEAD the branch was created from.
|
|
127
|
+
baseRevision: outputString(event, 'baseRevision'),
|
|
128
|
+
issueSummary: outputString(event, 'issueSummary'),
|
|
129
|
+
completion: 'branched',
|
|
130
|
+
pendingBossQuestion: undefined,
|
|
131
|
+
bossReply: undefined,
|
|
132
|
+
})),
|
|
133
|
+
rememberRefused: assign(({ event }) => ({
|
|
134
|
+
coderOutput: outputString(event, 'coderOutput'),
|
|
135
|
+
completion: 'refused',
|
|
136
|
+
pendingBossQuestion: undefined,
|
|
137
|
+
bossReply: undefined,
|
|
138
|
+
})),
|
|
139
|
+
rememberPendingQuestion: assign(({ event }) => {
|
|
140
|
+
const question = outputString(event, 'question');
|
|
141
|
+
if (question === undefined || !outputGuard(event, 'needsBossReply')) {
|
|
142
|
+
return { lastError: new Error('Coder question output was malformed') };
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
pendingBossQuestion: {
|
|
146
|
+
questionId: 'createBranch',
|
|
147
|
+
resumeStateId: 'createBranch',
|
|
148
|
+
sourceItem: 'BRANCH-1',
|
|
149
|
+
asker: { kind: 'role', roleId: 'coder' },
|
|
150
|
+
question,
|
|
151
|
+
},
|
|
152
|
+
bossReply: undefined,
|
|
153
|
+
};
|
|
154
|
+
}),
|
|
155
|
+
rememberBossReply: assign(({ event }) => event.type === 'BOSS_REPLY' ? { bossReply: event.answer } : {}),
|
|
156
|
+
rememberEmptyBossReplyError: assign({
|
|
157
|
+
lastError: () => new Error('BOSS_REPLY received an empty answer'),
|
|
158
|
+
pendingBossQuestion: undefined,
|
|
159
|
+
bossReply: undefined,
|
|
160
|
+
}),
|
|
161
|
+
rememberActorError: assign(({ event }) => ({
|
|
162
|
+
lastError: event.error,
|
|
163
|
+
})),
|
|
164
|
+
rememberMalformedPlayerOutput: assign({
|
|
165
|
+
lastError: () => new Error('Coder result for BRANCH-1 did not match an available outcome.'),
|
|
166
|
+
}),
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
export const branchMachine = machineSetup.createMachine({
|
|
170
|
+
id: 'branch',
|
|
171
|
+
initial: 'ready',
|
|
172
|
+
context: {},
|
|
173
|
+
output: ({ context }) => {
|
|
174
|
+
if (context.completion === 'branched') {
|
|
175
|
+
if (!isNonEmptyString(context.branch) ||
|
|
176
|
+
!isNonEmptyString(context.baseRevision) ||
|
|
177
|
+
!isNonEmptyString(context.issueSummary)) {
|
|
178
|
+
throw new Error('BRANCH reached branched without the exact branch name, a receipt-observed base revision, and an issue summary');
|
|
179
|
+
}
|
|
180
|
+
return {
|
|
181
|
+
status: 'branched',
|
|
182
|
+
branch: context.branch,
|
|
183
|
+
baseRevision: context.baseRevision,
|
|
184
|
+
issueSummary: context.issueSummary,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
if (context.completion === 'refused') {
|
|
188
|
+
if (!isNonEmptyString(context.coderOutput)) {
|
|
189
|
+
throw new Error("BRANCH reached refused without Coder's report");
|
|
190
|
+
}
|
|
191
|
+
return { status: 'refused', coderOutput: context.coderOutput };
|
|
192
|
+
}
|
|
193
|
+
throw new Error('BRANCH reached a final state without a recorded completion');
|
|
194
|
+
},
|
|
195
|
+
states: {
|
|
196
|
+
ready: {
|
|
197
|
+
id: 'ready',
|
|
198
|
+
description: STATE_DESCRIPTIONS.ready,
|
|
199
|
+
meta: playbookMeta('ready'),
|
|
200
|
+
tags: ['playbook.parked'],
|
|
201
|
+
on: {
|
|
202
|
+
START_BRANCH: { target: 'createBranch', actions: 'startBranch' },
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
createBranch: {
|
|
206
|
+
id: 'createBranch',
|
|
207
|
+
description: STATE_DESCRIPTIONS.createBranch,
|
|
208
|
+
meta: playbookMeta('createBranch', 'coder'),
|
|
209
|
+
tags: ['playbook.busy'],
|
|
210
|
+
invoke: {
|
|
211
|
+
src: 'player',
|
|
212
|
+
input: ({ context }) => ({
|
|
213
|
+
stateId: 'createBranch',
|
|
214
|
+
role: 'coder',
|
|
215
|
+
sourceItem: 'BRANCH-1',
|
|
216
|
+
prompt: CREATE_BRANCH_PROMPT,
|
|
217
|
+
result: CREATE_BRANCH_RESULTS,
|
|
218
|
+
callerInput: context.callerInput ?? '',
|
|
219
|
+
...(context.pendingBossQuestion === undefined
|
|
220
|
+
? {}
|
|
221
|
+
: { pendingBossQuestion: context.pendingBossQuestion }),
|
|
222
|
+
...(context.bossReply === undefined
|
|
223
|
+
? {}
|
|
224
|
+
: { bossReply: context.bossReply }),
|
|
225
|
+
}),
|
|
226
|
+
onDone: [
|
|
227
|
+
{
|
|
228
|
+
guard: 'isBranched',
|
|
229
|
+
target: 'branched',
|
|
230
|
+
actions: [
|
|
231
|
+
{
|
|
232
|
+
type: 'playbook.acceptedOutcome',
|
|
233
|
+
params: {
|
|
234
|
+
source: 'createBranch',
|
|
235
|
+
target: 'branched',
|
|
236
|
+
acceptedOutcome: 'branched',
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
'rememberBranched',
|
|
240
|
+
],
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
guard: 'isRefused',
|
|
244
|
+
target: 'refused',
|
|
245
|
+
actions: [
|
|
246
|
+
{
|
|
247
|
+
type: 'playbook.acceptedOutcome',
|
|
248
|
+
params: {
|
|
249
|
+
source: 'createBranch',
|
|
250
|
+
target: 'refused',
|
|
251
|
+
acceptedOutcome: 'refused',
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
'rememberRefused',
|
|
255
|
+
],
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
guard: 'needsBossReply',
|
|
259
|
+
target: 'awaitBossReply',
|
|
260
|
+
actions: [
|
|
261
|
+
{
|
|
262
|
+
type: 'playbook.acceptedOutcome',
|
|
263
|
+
params: {
|
|
264
|
+
source: 'createBranch',
|
|
265
|
+
target: 'awaitBossReply',
|
|
266
|
+
acceptedOutcome: 'needsBossReply',
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
'rememberPendingQuestion',
|
|
270
|
+
],
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
target: 'failed',
|
|
274
|
+
actions: 'rememberMalformedPlayerOutput',
|
|
275
|
+
},
|
|
276
|
+
],
|
|
277
|
+
onError: { target: 'failed', actions: 'rememberActorError' },
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
awaitBossReply: {
|
|
281
|
+
id: 'awaitBossReply',
|
|
282
|
+
description: STATE_DESCRIPTIONS.awaitBossReply,
|
|
283
|
+
meta: playbookMeta('awaitBossReply'),
|
|
284
|
+
tags: ['playbook.parked'],
|
|
285
|
+
on: {
|
|
286
|
+
BOSS_REPLY: [
|
|
287
|
+
{
|
|
288
|
+
guard: 'emptyBossReply',
|
|
289
|
+
target: '#failed',
|
|
290
|
+
actions: 'rememberEmptyBossReplyError',
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
guard: 'resumesCreateBranch',
|
|
294
|
+
target: '#createBranch',
|
|
295
|
+
reenter: true,
|
|
296
|
+
actions: 'rememberBossReply',
|
|
297
|
+
},
|
|
298
|
+
],
|
|
299
|
+
START_BRANCH: { target: 'createBranch', actions: 'startBranch' },
|
|
300
|
+
},
|
|
301
|
+
},
|
|
302
|
+
failed: {
|
|
303
|
+
id: 'failed',
|
|
304
|
+
description: STATE_DESCRIPTIONS.failed,
|
|
305
|
+
meta: playbookMeta('failed'),
|
|
306
|
+
tags: ['playbook.parked'],
|
|
307
|
+
on: {
|
|
308
|
+
START_BRANCH: { target: 'createBranch', actions: 'startBranch' },
|
|
309
|
+
},
|
|
310
|
+
},
|
|
311
|
+
branched: {
|
|
312
|
+
id: 'branched',
|
|
313
|
+
description: STATE_DESCRIPTIONS.branched,
|
|
314
|
+
meta: terminalMeta('branched', 'success'),
|
|
315
|
+
type: 'final',
|
|
316
|
+
},
|
|
317
|
+
refused: {
|
|
318
|
+
id: 'refused',
|
|
319
|
+
description: STATE_DESCRIPTIONS.refused,
|
|
320
|
+
meta: terminalMeta('refused', 'failure'),
|
|
321
|
+
type: 'final',
|
|
322
|
+
},
|
|
323
|
+
},
|
|
324
|
+
});
|
|
325
|
+
export default branchMachine;
|