@sublang/playbook 13.1.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 +40 -2
- 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.playbook/playbook.config.template.yaml +16 -2
- package/reference/sdlc/dev.md +24 -3
- package/reference/sdlc/dev.playbook/dev.fsm.d.ts +100 -9
- package/reference/sdlc/dev.playbook/dev.fsm.js +235 -18
- package/reference/sdlc/dev.playbook/dev.fsm.ts +368 -24
- package/reference/sdlc/dev.playbook/dev.gears.md +45 -7
- package/reference/sdlc/dev.playbook/dev.playbook.js +13 -1
- package/reference/sdlc/dev.playbook/dev.playbook.ts +13 -1
- 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
|
@@ -0,0 +1,458 @@
|
|
|
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
|
+
|
|
6
|
+
import { assign, fromPromise, setup } from 'xstate';
|
|
7
|
+
|
|
8
|
+
export type BranchStateId = 'createBranch';
|
|
9
|
+
|
|
10
|
+
export type BranchSourceItem = 'BRANCH-1';
|
|
11
|
+
|
|
12
|
+
export type PendingBossQuestion = {
|
|
13
|
+
readonly questionId: 'createBranch';
|
|
14
|
+
readonly resumeStateId: 'createBranch';
|
|
15
|
+
readonly sourceItem: 'BRANCH-1';
|
|
16
|
+
readonly asker: { readonly kind: 'role'; readonly roleId: 'coder' };
|
|
17
|
+
readonly question: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type PlayerInput = {
|
|
21
|
+
readonly stateId: 'createBranch';
|
|
22
|
+
readonly role: 'coder';
|
|
23
|
+
readonly sourceItem: 'BRANCH-1';
|
|
24
|
+
readonly prompt: string;
|
|
25
|
+
readonly result: Readonly<Record<string, string>>;
|
|
26
|
+
readonly callerInput: string;
|
|
27
|
+
readonly pendingBossQuestion?: PendingBossQuestion;
|
|
28
|
+
readonly bossReply?: string;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type PlayerOutput =
|
|
32
|
+
| {
|
|
33
|
+
readonly guard: 'branched';
|
|
34
|
+
readonly branch: string;
|
|
35
|
+
/**
|
|
36
|
+
* Receipt-owned effect evidence (DR-045): the branching call's
|
|
37
|
+
* `unchanged` receipt observes the commit the branch was created from
|
|
38
|
+
* as HEAD, so the linked runtime injects it and never takes it from
|
|
39
|
+
* Coder's prose.
|
|
40
|
+
*/
|
|
41
|
+
readonly baseRevision: string;
|
|
42
|
+
readonly issueSummary: string;
|
|
43
|
+
}
|
|
44
|
+
| { readonly guard: 'refused'; readonly coderOutput: string }
|
|
45
|
+
| { readonly guard: 'needsBossReply'; readonly question: string };
|
|
46
|
+
|
|
47
|
+
export type BranchPlaybookOutput =
|
|
48
|
+
| {
|
|
49
|
+
readonly status: 'branched';
|
|
50
|
+
/** Exact name of the new branch now checked out. */
|
|
51
|
+
readonly branch: string;
|
|
52
|
+
/** Exact receipt-observed revision the branch was created from. */
|
|
53
|
+
readonly baseRevision: string;
|
|
54
|
+
/** Coder's concise summary of the issue and its comments, or of the request. */
|
|
55
|
+
readonly issueSummary: string;
|
|
56
|
+
}
|
|
57
|
+
| {
|
|
58
|
+
readonly status: 'refused';
|
|
59
|
+
/** Coder's complete report carrying the reason no branch was created. */
|
|
60
|
+
readonly coderOutput: string;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export type BranchInput = Readonly<Record<string, never>>;
|
|
64
|
+
|
|
65
|
+
export type BranchContext = {
|
|
66
|
+
readonly callerInput?: string;
|
|
67
|
+
readonly branch?: string;
|
|
68
|
+
/** Observed HEAD of the branching call's unchanged receipt (DR-045). */
|
|
69
|
+
readonly baseRevision?: string;
|
|
70
|
+
readonly issueSummary?: string;
|
|
71
|
+
readonly coderOutput?: string;
|
|
72
|
+
readonly completion?: 'branched' | 'refused';
|
|
73
|
+
readonly lastError?: unknown;
|
|
74
|
+
readonly pendingBossQuestion?: PendingBossQuestion;
|
|
75
|
+
readonly bossReply?: string;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export type BranchEvent =
|
|
79
|
+
| { readonly type: 'START_BRANCH'; readonly callerInput: string }
|
|
80
|
+
| {
|
|
81
|
+
readonly type: 'BOSS_REPLY';
|
|
82
|
+
readonly answer: string;
|
|
83
|
+
readonly questionId?: 'createBranch';
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const CREATE_BRANCH_PROMPT = [
|
|
87
|
+
'> Original request: <caller-input>',
|
|
88
|
+
'',
|
|
89
|
+
'Prepare a new branch for this work without changing any file or making any commit.',
|
|
90
|
+
'Identify the GitHub issue the request names, if any, and read it with its comments (`gh issue view --comments` with the issue number).',
|
|
91
|
+
'If the request could refer to more than one issue, or does not say which work to branch for, ask Boss before creating anything.',
|
|
92
|
+
"Confirm that the working tree is clean and that `gh` is authenticated for the repository's GitHub remote.",
|
|
93
|
+
'Name the branch `issue-N-short-kebab-slug` for issue number N, otherwise a short kebab-case slug of the request.',
|
|
94
|
+
'Create the branch from the current commit and check it out; do not pull, reset, stash, or move HEAD to another commit.',
|
|
95
|
+
'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.',
|
|
96
|
+
'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.',
|
|
97
|
+
].join('\n');
|
|
98
|
+
|
|
99
|
+
const CREATE_BRANCH_RESULTS = {
|
|
100
|
+
branched:
|
|
101
|
+
'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>`.',
|
|
102
|
+
refused:
|
|
103
|
+
'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>`.',
|
|
104
|
+
needsBossReply:
|
|
105
|
+
"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>`.",
|
|
106
|
+
} as const;
|
|
107
|
+
|
|
108
|
+
const STATE_DESCRIPTIONS = {
|
|
109
|
+
ready: 'Waiting for a development request to branch for.',
|
|
110
|
+
createBranch:
|
|
111
|
+
'Coder is reading the named issue and creating and checking out a new branch at the current commit.',
|
|
112
|
+
awaitBossReply: 'Waiting for Boss to answer Coder.',
|
|
113
|
+
failed:
|
|
114
|
+
'The branch workflow failed and is waiting for a new development request.',
|
|
115
|
+
branched:
|
|
116
|
+
"A new branch for the request is checked out at the caller's current commit; no file changed and no commit was made.",
|
|
117
|
+
refused:
|
|
118
|
+
'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.',
|
|
119
|
+
} as const;
|
|
120
|
+
|
|
121
|
+
function playbookMeta<StateId extends keyof typeof STATE_DESCRIPTIONS>(
|
|
122
|
+
stateId: StateId,
|
|
123
|
+
role?: 'coder',
|
|
124
|
+
) {
|
|
125
|
+
return {
|
|
126
|
+
playbook: {
|
|
127
|
+
stateId,
|
|
128
|
+
description: STATE_DESCRIPTIONS[stateId],
|
|
129
|
+
...(role === undefined ? {} : { role }),
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// DR-048: a final state additionally declares whether its outcome means the
|
|
135
|
+
// workflow succeeded or failed, so BRANCH's own caller learns that from the
|
|
136
|
+
// machine rather than from BRANCH's output fields.
|
|
137
|
+
function terminalMeta<StateId extends keyof typeof STATE_DESCRIPTIONS>(
|
|
138
|
+
stateId: StateId,
|
|
139
|
+
terminal: 'success' | 'failure',
|
|
140
|
+
) {
|
|
141
|
+
return {
|
|
142
|
+
playbook: {
|
|
143
|
+
stateId,
|
|
144
|
+
description: STATE_DESCRIPTIONS[stateId],
|
|
145
|
+
terminal,
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
151
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
const prototype = Object.getPrototypeOf(value);
|
|
155
|
+
return prototype === Object.prototype || prototype === null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function isNonEmptyString(value: unknown): value is string {
|
|
159
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function playerOutput(event: unknown): Record<string, unknown> | undefined {
|
|
163
|
+
if (!isRecord(event)) return undefined;
|
|
164
|
+
return isRecord(event.output) ? event.output : undefined;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function outputGuard(event: unknown, guard: PlayerOutput['guard']): boolean {
|
|
168
|
+
return playerOutput(event)?.guard === guard;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function outputString(event: unknown, field: string): string | undefined {
|
|
172
|
+
const value = playerOutput(event)?.[field];
|
|
173
|
+
return isNonEmptyString(value) ? value : undefined;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// DR-045: the branched outcome is accepted only with the receipt-observed
|
|
177
|
+
// base revision the linked runtime injects; Coder's prose never supplies it.
|
|
178
|
+
function isBranched({ event }: { event: unknown }): boolean {
|
|
179
|
+
return (
|
|
180
|
+
outputGuard(event, 'branched') &&
|
|
181
|
+
outputString(event, 'branch') !== undefined &&
|
|
182
|
+
outputString(event, 'baseRevision') !== undefined &&
|
|
183
|
+
outputString(event, 'issueSummary') !== undefined
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function isRefused({ event }: { event: unknown }): boolean {
|
|
188
|
+
return (
|
|
189
|
+
outputGuard(event, 'refused') &&
|
|
190
|
+
outputString(event, 'coderOutput') !== undefined
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function needsBossReply({ event }: { event: unknown }): boolean {
|
|
195
|
+
return (
|
|
196
|
+
outputGuard(event, 'needsBossReply') &&
|
|
197
|
+
outputString(event, 'question') !== undefined
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const machineSetup = setup({
|
|
202
|
+
types: {} as {
|
|
203
|
+
context: BranchContext;
|
|
204
|
+
events: BranchEvent;
|
|
205
|
+
input: BranchInput;
|
|
206
|
+
output: BranchPlaybookOutput;
|
|
207
|
+
},
|
|
208
|
+
actors: {
|
|
209
|
+
player: fromPromise<PlayerOutput, PlayerInput>(async () => {
|
|
210
|
+
throw new Error('player actor must be provided by the runner');
|
|
211
|
+
}),
|
|
212
|
+
},
|
|
213
|
+
guards: {
|
|
214
|
+
isBranched,
|
|
215
|
+
isRefused,
|
|
216
|
+
needsBossReply,
|
|
217
|
+
emptyBossReply: ({ event }) =>
|
|
218
|
+
event.type === 'BOSS_REPLY' && event.answer.trim().length === 0,
|
|
219
|
+
resumesCreateBranch: ({ context, event }) =>
|
|
220
|
+
event.type === 'BOSS_REPLY' &&
|
|
221
|
+
event.answer.trim().length > 0 &&
|
|
222
|
+
context.pendingBossQuestion?.resumeStateId === 'createBranch' &&
|
|
223
|
+
(event.questionId === undefined || event.questionId === 'createBranch'),
|
|
224
|
+
},
|
|
225
|
+
actions: {
|
|
226
|
+
'playbook.acceptedOutcome': (
|
|
227
|
+
_args,
|
|
228
|
+
_params: {
|
|
229
|
+
readonly source: string;
|
|
230
|
+
readonly target: string;
|
|
231
|
+
readonly acceptedOutcome: string;
|
|
232
|
+
},
|
|
233
|
+
) => undefined,
|
|
234
|
+
startBranch: assign(({ event }) => {
|
|
235
|
+
if (event.type !== 'START_BRANCH') return {};
|
|
236
|
+
return {
|
|
237
|
+
callerInput: event.callerInput,
|
|
238
|
+
branch: undefined,
|
|
239
|
+
baseRevision: undefined,
|
|
240
|
+
issueSummary: undefined,
|
|
241
|
+
coderOutput: undefined,
|
|
242
|
+
completion: undefined,
|
|
243
|
+
lastError: undefined,
|
|
244
|
+
pendingBossQuestion: undefined,
|
|
245
|
+
bossReply: undefined,
|
|
246
|
+
};
|
|
247
|
+
}),
|
|
248
|
+
rememberBranched: assign(({ event }) => ({
|
|
249
|
+
branch: outputString(event, 'branch'),
|
|
250
|
+
// DR-045: the receipt-observed HEAD the branch was created from.
|
|
251
|
+
baseRevision: outputString(event, 'baseRevision'),
|
|
252
|
+
issueSummary: outputString(event, 'issueSummary'),
|
|
253
|
+
completion: 'branched' as const,
|
|
254
|
+
pendingBossQuestion: undefined,
|
|
255
|
+
bossReply: undefined,
|
|
256
|
+
})),
|
|
257
|
+
rememberRefused: assign(({ event }) => ({
|
|
258
|
+
coderOutput: outputString(event, 'coderOutput'),
|
|
259
|
+
completion: 'refused' as const,
|
|
260
|
+
pendingBossQuestion: undefined,
|
|
261
|
+
bossReply: undefined,
|
|
262
|
+
})),
|
|
263
|
+
rememberPendingQuestion: assign(({ event }) => {
|
|
264
|
+
const question = outputString(event, 'question');
|
|
265
|
+
if (question === undefined || !outputGuard(event, 'needsBossReply')) {
|
|
266
|
+
return { lastError: new Error('Coder question output was malformed') };
|
|
267
|
+
}
|
|
268
|
+
return {
|
|
269
|
+
pendingBossQuestion: {
|
|
270
|
+
questionId: 'createBranch' as const,
|
|
271
|
+
resumeStateId: 'createBranch' as const,
|
|
272
|
+
sourceItem: 'BRANCH-1' as const,
|
|
273
|
+
asker: { kind: 'role' as const, roleId: 'coder' as const },
|
|
274
|
+
question,
|
|
275
|
+
},
|
|
276
|
+
bossReply: undefined,
|
|
277
|
+
};
|
|
278
|
+
}),
|
|
279
|
+
rememberBossReply: assign(({ event }) =>
|
|
280
|
+
event.type === 'BOSS_REPLY' ? { bossReply: event.answer } : {},
|
|
281
|
+
),
|
|
282
|
+
rememberEmptyBossReplyError: assign({
|
|
283
|
+
lastError: () => new Error('BOSS_REPLY received an empty answer'),
|
|
284
|
+
pendingBossQuestion: undefined,
|
|
285
|
+
bossReply: undefined,
|
|
286
|
+
}),
|
|
287
|
+
rememberActorError: assign(({ event }) => ({
|
|
288
|
+
lastError: (event as unknown as { readonly error?: unknown }).error,
|
|
289
|
+
})),
|
|
290
|
+
rememberMalformedPlayerOutput: assign({
|
|
291
|
+
lastError: () =>
|
|
292
|
+
new Error('Coder result for BRANCH-1 did not match an available outcome.'),
|
|
293
|
+
}),
|
|
294
|
+
},
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
export const branchMachine = machineSetup.createMachine({
|
|
298
|
+
id: 'branch',
|
|
299
|
+
initial: 'ready',
|
|
300
|
+
context: {},
|
|
301
|
+
output: ({ context }): BranchPlaybookOutput => {
|
|
302
|
+
if (context.completion === 'branched') {
|
|
303
|
+
if (
|
|
304
|
+
!isNonEmptyString(context.branch) ||
|
|
305
|
+
!isNonEmptyString(context.baseRevision) ||
|
|
306
|
+
!isNonEmptyString(context.issueSummary)
|
|
307
|
+
) {
|
|
308
|
+
throw new Error(
|
|
309
|
+
'BRANCH reached branched without the exact branch name, a receipt-observed base revision, and an issue summary',
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
return {
|
|
313
|
+
status: 'branched',
|
|
314
|
+
branch: context.branch,
|
|
315
|
+
baseRevision: context.baseRevision,
|
|
316
|
+
issueSummary: context.issueSummary,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
if (context.completion === 'refused') {
|
|
320
|
+
if (!isNonEmptyString(context.coderOutput)) {
|
|
321
|
+
throw new Error("BRANCH reached refused without Coder's report");
|
|
322
|
+
}
|
|
323
|
+
return { status: 'refused', coderOutput: context.coderOutput };
|
|
324
|
+
}
|
|
325
|
+
throw new Error('BRANCH reached a final state without a recorded completion');
|
|
326
|
+
},
|
|
327
|
+
states: {
|
|
328
|
+
ready: {
|
|
329
|
+
id: 'ready',
|
|
330
|
+
description: STATE_DESCRIPTIONS.ready,
|
|
331
|
+
meta: playbookMeta('ready'),
|
|
332
|
+
tags: ['playbook.parked'],
|
|
333
|
+
on: {
|
|
334
|
+
START_BRANCH: { target: 'createBranch', actions: 'startBranch' },
|
|
335
|
+
},
|
|
336
|
+
},
|
|
337
|
+
createBranch: {
|
|
338
|
+
id: 'createBranch',
|
|
339
|
+
description: STATE_DESCRIPTIONS.createBranch,
|
|
340
|
+
meta: playbookMeta('createBranch', 'coder'),
|
|
341
|
+
tags: ['playbook.busy'],
|
|
342
|
+
invoke: {
|
|
343
|
+
src: 'player',
|
|
344
|
+
input: ({ context }): PlayerInput => ({
|
|
345
|
+
stateId: 'createBranch',
|
|
346
|
+
role: 'coder',
|
|
347
|
+
sourceItem: 'BRANCH-1',
|
|
348
|
+
prompt: CREATE_BRANCH_PROMPT,
|
|
349
|
+
result: CREATE_BRANCH_RESULTS,
|
|
350
|
+
callerInput: context.callerInput ?? '',
|
|
351
|
+
...(context.pendingBossQuestion === undefined
|
|
352
|
+
? {}
|
|
353
|
+
: { pendingBossQuestion: context.pendingBossQuestion }),
|
|
354
|
+
...(context.bossReply === undefined
|
|
355
|
+
? {}
|
|
356
|
+
: { bossReply: context.bossReply }),
|
|
357
|
+
}),
|
|
358
|
+
onDone: [
|
|
359
|
+
{
|
|
360
|
+
guard: 'isBranched',
|
|
361
|
+
target: 'branched',
|
|
362
|
+
actions: [
|
|
363
|
+
{
|
|
364
|
+
type: 'playbook.acceptedOutcome',
|
|
365
|
+
params: {
|
|
366
|
+
source: 'createBranch',
|
|
367
|
+
target: 'branched',
|
|
368
|
+
acceptedOutcome: 'branched',
|
|
369
|
+
},
|
|
370
|
+
},
|
|
371
|
+
'rememberBranched',
|
|
372
|
+
],
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
guard: 'isRefused',
|
|
376
|
+
target: 'refused',
|
|
377
|
+
actions: [
|
|
378
|
+
{
|
|
379
|
+
type: 'playbook.acceptedOutcome',
|
|
380
|
+
params: {
|
|
381
|
+
source: 'createBranch',
|
|
382
|
+
target: 'refused',
|
|
383
|
+
acceptedOutcome: 'refused',
|
|
384
|
+
},
|
|
385
|
+
},
|
|
386
|
+
'rememberRefused',
|
|
387
|
+
],
|
|
388
|
+
},
|
|
389
|
+
{
|
|
390
|
+
guard: 'needsBossReply',
|
|
391
|
+
target: 'awaitBossReply',
|
|
392
|
+
actions: [
|
|
393
|
+
{
|
|
394
|
+
type: 'playbook.acceptedOutcome',
|
|
395
|
+
params: {
|
|
396
|
+
source: 'createBranch',
|
|
397
|
+
target: 'awaitBossReply',
|
|
398
|
+
acceptedOutcome: 'needsBossReply',
|
|
399
|
+
},
|
|
400
|
+
},
|
|
401
|
+
'rememberPendingQuestion',
|
|
402
|
+
],
|
|
403
|
+
},
|
|
404
|
+
{
|
|
405
|
+
target: 'failed',
|
|
406
|
+
actions: 'rememberMalformedPlayerOutput',
|
|
407
|
+
},
|
|
408
|
+
],
|
|
409
|
+
onError: { target: 'failed', actions: 'rememberActorError' },
|
|
410
|
+
},
|
|
411
|
+
},
|
|
412
|
+
awaitBossReply: {
|
|
413
|
+
id: 'awaitBossReply',
|
|
414
|
+
description: STATE_DESCRIPTIONS.awaitBossReply,
|
|
415
|
+
meta: playbookMeta('awaitBossReply'),
|
|
416
|
+
tags: ['playbook.parked'],
|
|
417
|
+
on: {
|
|
418
|
+
BOSS_REPLY: [
|
|
419
|
+
{
|
|
420
|
+
guard: 'emptyBossReply',
|
|
421
|
+
target: '#failed',
|
|
422
|
+
actions: 'rememberEmptyBossReplyError',
|
|
423
|
+
},
|
|
424
|
+
{
|
|
425
|
+
guard: 'resumesCreateBranch',
|
|
426
|
+
target: '#createBranch',
|
|
427
|
+
reenter: true,
|
|
428
|
+
actions: 'rememberBossReply',
|
|
429
|
+
},
|
|
430
|
+
],
|
|
431
|
+
START_BRANCH: { target: 'createBranch', actions: 'startBranch' },
|
|
432
|
+
},
|
|
433
|
+
},
|
|
434
|
+
failed: {
|
|
435
|
+
id: 'failed',
|
|
436
|
+
description: STATE_DESCRIPTIONS.failed,
|
|
437
|
+
meta: playbookMeta('failed'),
|
|
438
|
+
tags: ['playbook.parked'],
|
|
439
|
+
on: {
|
|
440
|
+
START_BRANCH: { target: 'createBranch', actions: 'startBranch' },
|
|
441
|
+
},
|
|
442
|
+
},
|
|
443
|
+
branched: {
|
|
444
|
+
id: 'branched',
|
|
445
|
+
description: STATE_DESCRIPTIONS.branched,
|
|
446
|
+
meta: terminalMeta('branched', 'success'),
|
|
447
|
+
type: 'final',
|
|
448
|
+
},
|
|
449
|
+
refused: {
|
|
450
|
+
id: 'refused',
|
|
451
|
+
description: STATE_DESCRIPTIONS.refused,
|
|
452
|
+
meta: terminalMeta('refused', 'failure'),
|
|
453
|
+
type: 'final',
|
|
454
|
+
},
|
|
455
|
+
},
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
export default branchMachine;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<!-- SPDX-License-Identifier: Apache-2.0 -->
|
|
2
|
+
<!-- SPDX-FileCopyrightText: 2026 SubLang International <https://sublang.ai> -->
|
|
3
|
+
|
|
4
|
+
# BRANCH: Pull-Request Branch Preparation Workflow
|
|
5
|
+
|
|
6
|
+
Roles:
|
|
7
|
+
|
|
8
|
+
- Coder
|
|
9
|
+
|
|
10
|
+
The caller supplies a development request that names a GitHub issue (number or URL) or, when no issue is named, describes the work, together with any relevant context.
|
|
11
|
+
|
|
12
|
+
`branch` prepares pull-request delivery of one development request: it creates and checks out a new branch at the current commit.
|
|
13
|
+
It changes no files and owns no repository commit.
|
|
14
|
+
Captain takes the base revision from repository authority, not from Coder's prose: the `unchanged` repository-effect receipt of the branching call proves the observed HEAD the branch was created from.
|
|
15
|
+
|
|
16
|
+
## Coder
|
|
17
|
+
|
|
18
|
+
### BRANCH-1
|
|
19
|
+
|
|
20
|
+
When the caller gives the request, Captain shall relay the complete caller input in quotes (`>`) to Coder, along with the branching instruction:
|
|
21
|
+
|
|
22
|
+
> > Original request: <caller-input>
|
|
23
|
+
>
|
|
24
|
+
> Prepare a new branch for this work without changing any file or making any commit.
|
|
25
|
+
> Identify the GitHub issue the request names, if any, and read it with its comments (`gh issue view --comments` with the issue number).
|
|
26
|
+
> If the request could refer to more than one issue, or does not say which work to branch for, ask Boss before creating anything.
|
|
27
|
+
> Confirm that the working tree is clean and that `gh` is authenticated for the repository's GitHub remote.
|
|
28
|
+
> Name the branch `issue-N-short-kebab-slug` for issue number N, otherwise a short kebab-case slug of the request.
|
|
29
|
+
> Create the branch from the current commit and check it out; do not pull, reset, stash, or move HEAD to another commit.
|
|
30
|
+
> 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.
|
|
31
|
+
> 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.
|
|
32
|
+
|
|
33
|
+
Results:
|
|
34
|
+
- `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>`.
|
|
35
|
+
- `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>`.
|
|
36
|
+
|
|
37
|
+
Workflow outcomes:
|
|
38
|
+
- The result has two semantic outcomes: branched and refused, plus the Boss question when the request is ambiguous about the issue or work it means.
|
|
39
|
+
- Each outcome requires affirmative support in Coder's result; the absence of a reported obstacle is not support for branched, and no outcome depends on a fixed presentation format of Coder's reply.
|
|
40
|
+
- Every outcome keeps the repository exact: a new branch at the current commit changes neither HEAD's commit nor the working tree.
|
|
41
|
+
- Captain takes the base revision from repository authority, not from Coder's prose.
|
|
42
|
+
- The Boss question uses the standard Boss-question suspension with Coder's complete response; after Boss replies, `branch` resumes with the answer in the same Coder conversation, and the previous question is included only when that conversation must start fresh.
|
|
43
|
+
- For branched, `branch` is complete and returns the exact branch name, the exact base revision, and the issue summary to its caller.
|
|
44
|
+
- For refused, `branch` fails and reports Coder's complete result with its reason to its caller; no branch was created.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type XStatePlaybookRuntimeFactory, type XStatePlaybookRuntimeConstruction } from '@sublang/playbook/xstate-runtime';
|
|
2
|
+
import { type BranchInput, type PlayerInput } from './branch.fsm.js';
|
|
3
|
+
import type { PlaybookHostConstructionCapabilities } from '../code.playbook/playbook-captain.js';
|
|
4
|
+
import type { CaptainCallOptions, CaptainResult, JsonValue, NormalizedError, PlayerCallOptions, PlaybookCallRequest, PlaybookCallResult, PlaybookCallStart, PlaybookControlReceipt, PlaybookControlView, PlaybookPendingCall, PlaybookPorts, PlaybookRunResult, PlaybookRuntime, PlaybookRuntimeFactory, PlaybookRuntimeSnapshot, PlaybookSession, PlaybookState, PlaybookStateValue, PlaybookTraceEvent, PlaybookTraceType, PlayerResult, PlayerSessionStore } from '@sublang/playbook/runtime';
|
|
5
|
+
export type { CaptainCallOptions, CaptainResult, JsonValue, NormalizedError, PlayerCallOptions, PlaybookCallRequest, PlaybookCallResult, PlaybookCallStart, PlaybookControlReceipt, PlaybookControlView, PlaybookPendingCall, PlaybookPorts, PlaybookRunResult, PlaybookRuntime, PlaybookRuntimeFactory, PlaybookRuntimeSnapshot, PlaybookSession, PlaybookState, PlaybookStateValue, PlaybookTraceEvent, PlaybookTraceType, PlayerResult, PlayerSessionStore, };
|
|
6
|
+
export type BranchPlaybookOptions = BranchInput;
|
|
7
|
+
export type BranchPlaybookHostCapabilities = PlaybookHostConstructionCapabilities & XStatePlaybookRuntimeConstruction<BranchPlaybookOptions, object>['hostCapabilities'];
|
|
8
|
+
/**
|
|
9
|
+
* Preserve authored Markdown quote markers around every line of relayed
|
|
10
|
+
* runtime text. The generic composer preserves the marker itself; BRANCH's
|
|
11
|
+
* override additionally keeps a multiline caller input inside that quote.
|
|
12
|
+
*/
|
|
13
|
+
declare function composePlayerPrompt(input: PlayerInput, resuming?: boolean): string;
|
|
14
|
+
export declare const _internal: {
|
|
15
|
+
composePlayerPrompt: typeof composePlayerPrompt;
|
|
16
|
+
VERBATIM_PAYLOAD_FIELDS: ReadonlySet<string>;
|
|
17
|
+
UNFINISHED_FINAL_STATE_IDS: ReadonlySet<string>;
|
|
18
|
+
};
|
|
19
|
+
declare const createPlaybookRuntime: XStatePlaybookRuntimeFactory<XStatePlaybookRuntimeConstruction<BranchPlaybookOptions, BranchPlaybookHostCapabilities>, 3>;
|
|
20
|
+
export default createPlaybookRuntime;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// SPDX-FileCopyrightText: 2026 SubLang International <https://sublang.ai>
|
|
3
|
+
//
|
|
4
|
+
// Generated by slc/link.md (FSM-to-Runtime linker).
|
|
5
|
+
// Source FSM: ./branch.fsm.ts
|
|
6
|
+
// Role metadata: Coder→coder (canonical local role)
|
|
7
|
+
// Boss event: deterministic START_BRANCH entry; exact Boss text becomes
|
|
8
|
+
// callerInput; pending Coder questions retain BOSS_REPLY
|
|
9
|
+
// Adjudication: LLM judge for the one Coder state; coderOutput is carried
|
|
10
|
+
// verbatim; baseRevision is receipt-owned effect evidence —
|
|
11
|
+
// the unchanged receipt's observed HEAD — never taken from
|
|
12
|
+
// Coder's prose
|
|
13
|
+
// Compat: artifact schema 3 / runtime ABI 1
|
|
14
|
+
import { createXStatePlaybookRuntime, composePlayerContinuation, snapshotJsonValue, } from '@sublang/playbook/xstate-runtime';
|
|
15
|
+
import { branchMachine, } from './branch.fsm.js';
|
|
16
|
+
const PLACEHOLDER = /<(#|[A-Za-z_$][A-Za-z0-9_$-]*)>/g;
|
|
17
|
+
const VERBATIM_PAYLOAD_FIELDS = new Set([
|
|
18
|
+
'coderOutput',
|
|
19
|
+
]);
|
|
20
|
+
const UNFINISHED_FINAL_STATE_IDS = new Set(['refused']);
|
|
21
|
+
function snapshotBranchOptions(value) {
|
|
22
|
+
const captured = snapshotJsonValue(value, 'BRANCH runtime options');
|
|
23
|
+
if (captured === null ||
|
|
24
|
+
typeof captured !== 'object' ||
|
|
25
|
+
Array.isArray(captured)) {
|
|
26
|
+
throw new TypeError('BRANCH runtime options must be an object');
|
|
27
|
+
}
|
|
28
|
+
const record = captured;
|
|
29
|
+
for (const key of Object.keys(record)) {
|
|
30
|
+
throw new TypeError(`BRANCH runtime options.${key} is not declared`);
|
|
31
|
+
}
|
|
32
|
+
return captured;
|
|
33
|
+
}
|
|
34
|
+
function placeholderField(token) {
|
|
35
|
+
if (token === '#')
|
|
36
|
+
return 'irNumber';
|
|
37
|
+
return token.replace(/-([A-Za-z0-9_$])/g, (_match, next) => next.toUpperCase());
|
|
38
|
+
}
|
|
39
|
+
function quotedContinuation(value) {
|
|
40
|
+
return value.replaceAll('\n', '\n> ');
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Preserve authored Markdown quote markers around every line of relayed
|
|
44
|
+
* runtime text. The generic composer preserves the marker itself; BRANCH's
|
|
45
|
+
* override additionally keeps a multiline caller input inside that quote.
|
|
46
|
+
*/
|
|
47
|
+
function composePlayerPrompt(input, resuming = false) {
|
|
48
|
+
const fields = input;
|
|
49
|
+
const body = input.prompt.replace(PLACEHOLDER, (match, token, offset, source) => {
|
|
50
|
+
const value = fields[placeholderField(token)];
|
|
51
|
+
if (typeof value !== 'string')
|
|
52
|
+
return match;
|
|
53
|
+
const lineStart = source.lastIndexOf('\n', offset - 1) + 1;
|
|
54
|
+
const literal = source.slice(lineStart, offset);
|
|
55
|
+
return literal.startsWith('> ') ? quotedContinuation(value) : value;
|
|
56
|
+
});
|
|
57
|
+
return composePlayerContinuation(input, body, resuming);
|
|
58
|
+
}
|
|
59
|
+
export const _internal = {
|
|
60
|
+
composePlayerPrompt,
|
|
61
|
+
VERBATIM_PAYLOAD_FIELDS,
|
|
62
|
+
UNFINISHED_FINAL_STATE_IDS,
|
|
63
|
+
};
|
|
64
|
+
const runtimeSpec = {
|
|
65
|
+
label: 'BRANCH',
|
|
66
|
+
// DR-022 / slc/link.md: the declaration carries the value current at link
|
|
67
|
+
// time — a literal, never the loading engine's RUNTIME_ABI self-report,
|
|
68
|
+
// which would follow whatever engine loads the module and make the
|
|
69
|
+
// factory's skew check compare that engine with itself.
|
|
70
|
+
compat: { artifactSchema: 3, runtimeAbi: 1 },
|
|
71
|
+
snapshotOptions: snapshotBranchOptions,
|
|
72
|
+
entryEvent: {
|
|
73
|
+
type: 'START_BRANCH',
|
|
74
|
+
textField: 'callerInput',
|
|
75
|
+
// `startBranch` copies the entry text here, so the failure-state retry
|
|
76
|
+
// reads it back from the persisted machine snapshot and survives a
|
|
77
|
+
// continued session (DR-034).
|
|
78
|
+
contextField: 'callerInput',
|
|
79
|
+
},
|
|
80
|
+
roleStates: {
|
|
81
|
+
createBranch: {
|
|
82
|
+
role: 'coder',
|
|
83
|
+
label: 'Coder is reading the named issue and creating and checking out a new branch at the current commit.',
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
// DR-050: BRANCH owns no repository commit — every Coder outcome, including
|
|
87
|
+
// the Boss question for an ambiguous request, is governed with the
|
|
88
|
+
// `unchanged` repository disposition, because a new branch at the current
|
|
89
|
+
// commit changes neither HEAD's OID nor the worktree projection.
|
|
90
|
+
outcomeAuthority: {
|
|
91
|
+
governedPlayerStates: {
|
|
92
|
+
createBranch: {
|
|
93
|
+
branched: {
|
|
94
|
+
fields: {
|
|
95
|
+
branch: 'semantic',
|
|
96
|
+
// DR-045: the unchanged receipt observes the commit the branch
|
|
97
|
+
// was created from as HEAD; the reconciler injects it here.
|
|
98
|
+
baseRevision: 'effect',
|
|
99
|
+
issueSummary: 'semantic',
|
|
100
|
+
},
|
|
101
|
+
repositoryDisposition: 'unchanged',
|
|
102
|
+
},
|
|
103
|
+
refused: {
|
|
104
|
+
fields: { coderOutput: 'presentation' },
|
|
105
|
+
repositoryDisposition: 'unchanged',
|
|
106
|
+
},
|
|
107
|
+
needsBossReply: {
|
|
108
|
+
fields: { question: 'presentation' },
|
|
109
|
+
repositoryDisposition: 'unchanged',
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
composePlayerPrompt: (input, _identity, resuming) => composePlayerPrompt(input, resuming),
|
|
115
|
+
verbatimPayloadFields: VERBATIM_PAYLOAD_FIELDS,
|
|
116
|
+
controlContextFields: [],
|
|
117
|
+
unfinishedFinalStateIds: UNFINISHED_FINAL_STATE_IDS,
|
|
118
|
+
transitionEventFields: ['callerInput', 'answer', 'questionId'],
|
|
119
|
+
};
|
|
120
|
+
const createPlaybookRuntime = createXStatePlaybookRuntime(branchMachine, runtimeSpec);
|
|
121
|
+
export default createPlaybookRuntime;
|