@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.
Files changed (38) hide show
  1. package/README.md +7 -5
  2. package/docs/cli.md +28 -9
  3. package/docs/configuration.md +21 -5
  4. package/docs/embedding.md +1 -1
  5. package/package.json +40 -2
  6. package/reference/sdlc/branch.md +37 -0
  7. package/reference/sdlc/branch.playbook/branch.fsm.d.ts +169 -0
  8. package/reference/sdlc/branch.playbook/branch.fsm.js +325 -0
  9. package/reference/sdlc/branch.playbook/branch.fsm.ts +458 -0
  10. package/reference/sdlc/branch.playbook/branch.gears.md +44 -0
  11. package/reference/sdlc/branch.playbook/branch.playbook.d.ts +20 -0
  12. package/reference/sdlc/branch.playbook/branch.playbook.js +121 -0
  13. package/reference/sdlc/branch.playbook/branch.playbook.ts +217 -0
  14. package/reference/sdlc/branch.playbook/branch.registry.d.ts +40 -0
  15. package/reference/sdlc/branch.playbook/branch.registry.js +63 -0
  16. package/reference/sdlc/branch.playbook/branch.registry.ts +121 -0
  17. package/reference/sdlc/code.playbook/playbook.config.template.yaml +16 -2
  18. package/reference/sdlc/dev.md +24 -3
  19. package/reference/sdlc/dev.playbook/dev.fsm.d.ts +100 -9
  20. package/reference/sdlc/dev.playbook/dev.fsm.js +235 -18
  21. package/reference/sdlc/dev.playbook/dev.fsm.ts +368 -24
  22. package/reference/sdlc/dev.playbook/dev.gears.md +45 -7
  23. package/reference/sdlc/dev.playbook/dev.playbook.js +13 -1
  24. package/reference/sdlc/dev.playbook/dev.playbook.ts +13 -1
  25. package/reference/sdlc/dev.playbook/dev.registry.d.ts +1 -1
  26. package/reference/sdlc/dev.playbook/dev.registry.js +7 -2
  27. package/reference/sdlc/dev.playbook/dev.registry.ts +7 -2
  28. package/reference/sdlc/pr.md +108 -0
  29. package/reference/sdlc/pr.playbook/pr.fsm.d.ts +352 -0
  30. package/reference/sdlc/pr.playbook/pr.fsm.js +902 -0
  31. package/reference/sdlc/pr.playbook/pr.fsm.ts +1251 -0
  32. package/reference/sdlc/pr.playbook/pr.gears.md +155 -0
  33. package/reference/sdlc/pr.playbook/pr.playbook.d.ts +27 -0
  34. package/reference/sdlc/pr.playbook/pr.playbook.js +133 -0
  35. package/reference/sdlc/pr.playbook/pr.playbook.ts +235 -0
  36. package/reference/sdlc/pr.playbook/pr.registry.d.ts +40 -0
  37. package/reference/sdlc/pr.playbook/pr.registry.js +68 -0
  38. package/reference/sdlc/pr.playbook/pr.registry.ts +124 -0
@@ -0,0 +1,902 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // SPDX-FileCopyrightText: 2026 SubLang International <https://sublang.ai>
3
+ //
4
+ // Generated by slc/gears2fsm.md from ./pr.gears.md.
5
+ import { assign, fromPromise, setup } from 'xstate';
6
+ const OPEN_PULL_REQUEST_PROMPT = [
7
+ '> Original request: <caller-input>',
8
+ '',
9
+ 'Publish the branch and open its pull request without changing any file or making any commit.',
10
+ 'Confirm that the working tree is clean and that the checked-out branch is the branch to deliver, not the repository default branch.',
11
+ "Push the branch to the repository's GitHub remote with its upstream set; never force-push.",
12
+ 'If an open pull request for this branch already exists, use it; otherwise open one against the repository default branch with `gh pr create`.',
13
+ 'Give the pull request a title naming the change and a body with a summary of what changed and why from the base revision to the last commit, the verification the commits report, and a `Closes #N` line for issue number N when the request names one.',
14
+ 'Report the pull request number and URL exactly.',
15
+ 'If the working tree is not clean, the checked-out branch is wrong, the push is rejected, or the pull request cannot be opened, open nothing further and report the failure with its reason.',
16
+ ].join('\n');
17
+ const NEEDS_BOSS_REPLY_RESULT = "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>`.";
18
+ const OPEN_PULL_REQUEST_RESULTS = {
19
+ opened: 'Coder pushed the branch with its upstream set and reported the open pull request for it. Output shall include `pullRequest: <pull request number>` and `pullRequestUrl: <pull request URL>`.',
20
+ notPublished: 'Coder reported that the branch could not be published or its pull request could not be opened, with the reason. Output shall include `coderOutput: <verbatim final text>`.',
21
+ needsBossReply: NEEDS_BOSS_REPLY_RESULT,
22
+ };
23
+ // The mechanical steps are static POSIX shell text (slc/optimize.md): no
24
+ // placeholder, no runtime value, and no prose — `gh` infers the pull request
25
+ // from the checked-out branch.
26
+ const WAIT_FOR_CHECKS_COMMAND = [
27
+ 'n=0',
28
+ "while gh pr checks 2>&1 | grep -q 'no checks reported'; do",
29
+ 'n=$((n + 1))',
30
+ '[ "$n" -ge 6 ] && exit 0',
31
+ 'sleep 10',
32
+ 'done',
33
+ 'gh pr checks --watch --fail-fast >/dev/null 2>&1',
34
+ ].join('\n');
35
+ const PUBLISH_FIX_COMMAND = [
36
+ 'git push || exit 1',
37
+ 'n=0',
38
+ 'until [ "$(gh pr view --json headRefOid --jq .headRefOid 2>/dev/null)" = "$(git rev-parse HEAD)" ]; do',
39
+ 'n=$((n + 1))',
40
+ '[ "$n" -ge 12 ] && exit 1',
41
+ 'sleep 5',
42
+ 'done',
43
+ ].join('\n');
44
+ const MERGE_PULL_REQUEST_COMMAND = 'gh pr merge --merge --delete-branch --match-head-commit "$(git rev-parse HEAD)"';
45
+ const UPDATE_LOCAL_DEFAULT_COMMAND = 'git pull --ff-only';
46
+ const WAIT_FOR_CHECKS_RESULTS = {
47
+ checksPassed: "The command exited with status zero: the pull request's checks passed, or the repository still reported no checks after a brief wait for them to register.",
48
+ checksFailed: "The command exited with a nonzero status: the pull request's checks failed.",
49
+ };
50
+ const PUBLISH_FIX_RESULTS = {
51
+ fixPublished: "The command exited with status zero: the fix is pushed and the pull request's head is the pushed commit.",
52
+ fixNotPublished: "The command exited with a nonzero status: the push was rejected or the pull request's head did not advance to the pushed commit.",
53
+ };
54
+ const WAIT_FOR_CHECKS_AFTER_FIX_RESULTS = {
55
+ checksPassed: "The command exited with status zero: the pull request's checks passed after the fix, or the repository still reported no checks after a brief wait for them to register.",
56
+ checksStillFailing: "The command exited with a nonzero status: the pull request's checks are still failing after the one fix attempt.",
57
+ };
58
+ const MERGE_PULL_REQUEST_RESULTS = {
59
+ merged: 'The command exited with status zero: the pull request is merged with a merge commit on the repository default branch, the remote and local branch are deleted, and the local default branch is checked out.',
60
+ mergeRefused: 'The command exited with a nonzero status: GitHub refused the merge, or the merge landed but the local switch to the default branch or the branch deletion failed.',
61
+ };
62
+ const UPDATE_LOCAL_DEFAULT_RESULTS = {
63
+ localDefaultUpdated: 'The command exited with status zero: the local default branch is fast-forwarded to the merged head.',
64
+ localDefaultNotUpdated: 'The command exited with a nonzero status: the local default branch was not fast-forwarded to the merged head.',
65
+ };
66
+ // PR-3's coding request is static prompt text of the nested call template.
67
+ const FIX_CODING_REQUEST = "The pull request's checks are red on the checked-out branch. Inspect the failing checks with `gh pr checks` and `gh run view --log-failed`, fix their cause on this branch with a minimal change, and make the checks pass.";
68
+ const STATE_DESCRIPTIONS = {
69
+ ready: 'Waiting for the caller input that names the branch to deliver.',
70
+ openPullRequest: 'Coder is publishing the branch and opening its pull request.',
71
+ waitForChecks: "Waiting for the pull request's checks to settle before any fix attempt.",
72
+ fixChecks: 'The CODE playbook is fixing the cause of the red checks on the checked-out branch.',
73
+ publishFix: "Publishing the fix to the pull request and waiting for the pull request's head to reach the pushed commit.",
74
+ waitForChecksAfterFix: "Waiting for the pull request's checks to settle after the one fix attempt.",
75
+ mergePullRequest: 'Merging the pull request into the repository default branch.',
76
+ updateLocalDefault: 'Fast-forwarding the local default branch to the merged head.',
77
+ awaitBossReply: 'Waiting for Boss to answer Coder.',
78
+ failed: 'The pull-request delivery workflow failed and is waiting for new caller input.',
79
+ merged: 'The pull request is merged with a merge commit on the repository default branch, both branches are deleted, and the local default branch is fast-forwarded to the merged head.',
80
+ mergedLocalBehind: 'The pull request is merged with a merge commit on the repository default branch and both branches are deleted, but the local default branch could not be fast-forwarded to the merged head.',
81
+ notPublished: 'The branch was not published or its pull request could not be opened; Coder reported the reason and no pull request is guaranteed to exist.',
82
+ fixFailed: "The pull request's checks were red and the single CODE fix attempt returned an authored abort, failure, or insufficient terminal result; the pull request remains open.",
83
+ fixNotPublished: "The fix could not be published: the push was rejected or the pull request's head did not advance to the pushed commit; the pull request remains open.",
84
+ checksStillFailing: "The pull request's checks are still failing after the one fix attempt; the pull request remains open.",
85
+ mergeRefused: 'GitHub refused the merge, or the merge landed but the local switch to the default branch or the branch deletion failed; the pull request is in the state GitHub reports.',
86
+ };
87
+ function playbookMeta(stateId, role) {
88
+ return {
89
+ playbook: {
90
+ stateId,
91
+ description: STATE_DESCRIPTIONS[stateId],
92
+ ...(role === undefined ? {} : { role }),
93
+ },
94
+ };
95
+ }
96
+ // DR-048: a final state additionally declares whether its outcome means the
97
+ // workflow succeeded or failed, so PR's own caller learns that from the
98
+ // machine rather than from PR's output fields.
99
+ function terminalMeta(stateId, terminal) {
100
+ return {
101
+ playbook: {
102
+ stateId,
103
+ description: STATE_DESCRIPTIONS[stateId],
104
+ terminal,
105
+ },
106
+ };
107
+ }
108
+ function isRecord(value) {
109
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
110
+ return false;
111
+ }
112
+ const prototype = Object.getPrototypeOf(value);
113
+ return prototype === Object.prototype || prototype === null;
114
+ }
115
+ function isNonEmptyString(value) {
116
+ return typeof value === 'string' && value.trim().length > 0;
117
+ }
118
+ function actorOutput(event) {
119
+ if (!isRecord(event))
120
+ return undefined;
121
+ return isRecord(event.output) ? event.output : undefined;
122
+ }
123
+ function outputGuard(event, guard) {
124
+ return actorOutput(event)?.guard === guard;
125
+ }
126
+ function outputString(event, field) {
127
+ const value = actorOutput(event)?.[field];
128
+ return isNonEmptyString(value) ? value : undefined;
129
+ }
130
+ function isOpened({ event }) {
131
+ return (outputGuard(event, 'opened') &&
132
+ outputString(event, 'pullRequest') !== undefined &&
133
+ outputString(event, 'pullRequestUrl') !== undefined);
134
+ }
135
+ function isNotPublished({ event }) {
136
+ return (outputGuard(event, 'notPublished') &&
137
+ outputString(event, 'coderOutput') !== undefined);
138
+ }
139
+ function needsBossReply({ event }) {
140
+ return (outputGuard(event, 'needsBossReply') &&
141
+ outputString(event, 'question') !== undefined);
142
+ }
143
+ // A script actor resolves mechanically from the exit status: the first
144
+ // declared guard with status zero, the second with the nonzero status.
145
+ function scriptExitStatus(event) {
146
+ const value = actorOutput(event)?.exitStatus;
147
+ return Number.isInteger(value) ? value : undefined;
148
+ }
149
+ function zeroExitGuard(guard) {
150
+ return ({ event }) => outputGuard(event, guard) && scriptExitStatus(event) === 0;
151
+ }
152
+ function nonzeroExitGuard(guard) {
153
+ return ({ event }) => {
154
+ const exitStatus = scriptExitStatus(event);
155
+ return (outputGuard(event, guard) && exitStatus !== undefined && exitStatus !== 0);
156
+ };
157
+ }
158
+ function childOutputOf(event) {
159
+ return isRecord(event) ? event.output : undefined;
160
+ }
161
+ // CODE's canonical successful terminal output discriminates completion with
162
+ // `status: 'complete'`, carries the exact last CODE-owned commit, and
163
+ // affirms that every phase's review passed.
164
+ function isCodeSuccessValue(value) {
165
+ return (isRecord(value) &&
166
+ value.status === 'complete' &&
167
+ isNonEmptyString(value.lastCodeCommit) &&
168
+ value.allReviewsPassed === true);
169
+ }
170
+ function isCodeSuccess({ event }) {
171
+ return isCodeSuccessValue(childOutputOf(event));
172
+ }
173
+ function isStateValue(value, ancestors = new Set()) {
174
+ if (typeof value === 'string')
175
+ return true;
176
+ if (!isRecord(value) || ancestors.has(value))
177
+ return false;
178
+ const next = new Set(ancestors).add(value);
179
+ return Reflect.ownKeys(value).every((key) => typeof key === 'string' &&
180
+ Object.prototype.propertyIsEnumerable.call(value, key) &&
181
+ isStateValue(value[key], next));
182
+ }
183
+ function isStringArray(value) {
184
+ return (Array.isArray(value) &&
185
+ Reflect.ownKeys(value).length === value.length + 1 &&
186
+ Reflect.ownKeys(value).every((key) => {
187
+ if (key === 'length')
188
+ return true;
189
+ return (typeof key === 'string' &&
190
+ /^(0|[1-9][0-9]*)$/.test(key) &&
191
+ Number(key) < value.length);
192
+ }) &&
193
+ value.every((entry) => isNonEmptyString(entry)) &&
194
+ new Set(value).size === value.length);
195
+ }
196
+ function isPlaybookState(value) {
197
+ if (!isRecord(value))
198
+ return false;
199
+ const allowed = new Set([
200
+ 'value',
201
+ 'activeStateIds',
202
+ 'tags',
203
+ 'status',
204
+ 'quiescent',
205
+ 'stateId',
206
+ ]);
207
+ if (Reflect.ownKeys(value).some((key) => typeof key !== 'string' || !allowed.has(key)) ||
208
+ !Object.prototype.hasOwnProperty.call(value, 'value') ||
209
+ !Object.prototype.hasOwnProperty.call(value, 'activeStateIds') ||
210
+ !Object.prototype.hasOwnProperty.call(value, 'tags') ||
211
+ !Object.prototype.hasOwnProperty.call(value, 'status') ||
212
+ !Object.prototype.hasOwnProperty.call(value, 'quiescent')) {
213
+ return false;
214
+ }
215
+ return (isStateValue(value.value) &&
216
+ isStringArray(value.activeStateIds) &&
217
+ isStringArray(value.tags) &&
218
+ (value.status === 'active' ||
219
+ value.status === 'done' ||
220
+ value.status === 'error' ||
221
+ value.status === 'stopped') &&
222
+ typeof value.quiescent === 'boolean' &&
223
+ (!Object.prototype.hasOwnProperty.call(value, 'stateId') ||
224
+ (isNonEmptyString(value.stateId) &&
225
+ value.activeStateIds.length === 1 &&
226
+ value.activeStateIds[0] === value.stateId)));
227
+ }
228
+ function nestedResultFromError(error) {
229
+ if (!(error instanceof Error))
230
+ return undefined;
231
+ const result = error.result;
232
+ return isRecord(result) ? result : undefined;
233
+ }
234
+ function isFailureTerminalRecord(value) {
235
+ if (!isRecord(value))
236
+ return false;
237
+ const allowed = new Set(['stateId', 'kind', 'description']);
238
+ return (Reflect.ownKeys(value).every((key) => typeof key === 'string' && allowed.has(key)) &&
239
+ isNonEmptyString(value.stateId) &&
240
+ value.kind === 'failure' &&
241
+ (!Object.prototype.hasOwnProperty.call(value, 'description') ||
242
+ typeof value.description === 'string'));
243
+ }
244
+ function normalizedChildFailure(error, playbookId) {
245
+ const result = nestedResultFromError(error);
246
+ if (result === undefined)
247
+ return undefined;
248
+ const allowed = new Set([
249
+ 'status',
250
+ 'playbookId',
251
+ 'childSessionId',
252
+ 'state',
253
+ ...(result.status === 'ok' ? ['output', 'terminal'] : ['error']),
254
+ ]);
255
+ if (Reflect.ownKeys(result).some((key) => typeof key !== 'string' || !allowed.has(key)) ||
256
+ (result.status !== 'aborted' &&
257
+ result.status !== 'error' &&
258
+ result.status !== 'ok') ||
259
+ result.playbookId !== playbookId) {
260
+ return undefined;
261
+ }
262
+ if (Object.prototype.hasOwnProperty.call(result, 'childSessionId') &&
263
+ !isNonEmptyString(result.childSessionId)) {
264
+ return undefined;
265
+ }
266
+ if (result.status === 'ok') {
267
+ if (!Object.prototype.hasOwnProperty.call(result, 'terminal') ||
268
+ !isFailureTerminalRecord(result.terminal) ||
269
+ !isNonEmptyString(result.childSessionId) ||
270
+ (Object.prototype.hasOwnProperty.call(result, 'state') &&
271
+ !isPlaybookState(result.state))) {
272
+ return undefined;
273
+ }
274
+ return {
275
+ status: 'ok',
276
+ ...(result.output === undefined
277
+ ? {}
278
+ : { output: result.output }),
279
+ };
280
+ }
281
+ if (Object.prototype.hasOwnProperty.call(result, 'state') &&
282
+ !isPlaybookState(result.state)) {
283
+ return undefined;
284
+ }
285
+ let normalizedError;
286
+ if (Object.prototype.hasOwnProperty.call(result, 'error')) {
287
+ if (!isRecord(result.error))
288
+ return undefined;
289
+ const errorKeys = Reflect.ownKeys(result.error);
290
+ if (errorKeys.some((key) => typeof key !== 'string' ||
291
+ (key !== 'name' && key !== 'message' && key !== 'stack')) ||
292
+ !Object.prototype.hasOwnProperty.call(result.error, 'name') ||
293
+ !Object.prototype.hasOwnProperty.call(result.error, 'message') ||
294
+ !isNonEmptyString(result.error.name) ||
295
+ typeof result.error.message !== 'string' ||
296
+ (Object.prototype.hasOwnProperty.call(result.error, 'stack') &&
297
+ typeof result.error.stack !== 'string')) {
298
+ return undefined;
299
+ }
300
+ normalizedError = {
301
+ name: result.error.name,
302
+ message: result.error.message,
303
+ };
304
+ }
305
+ else if (result.status === 'error') {
306
+ return undefined;
307
+ }
308
+ return {
309
+ status: result.status,
310
+ ...(normalizedError === undefined ? {} : { error: normalizedError }),
311
+ };
312
+ }
313
+ function authoredChildFailureGuard(playbookId) {
314
+ return ({ event }) => {
315
+ const error = isRecord(event) ? event.error : undefined;
316
+ return normalizedChildFailure(error, playbookId) !== undefined;
317
+ };
318
+ }
319
+ function authoredChildError(event, playbookId) {
320
+ const outer = isRecord(event) ? event.error : undefined;
321
+ const failure = normalizedChildFailure(outer, playbookId);
322
+ if (failure !== undefined && failure.status !== 'ok' && failure.error) {
323
+ return failure.error;
324
+ }
325
+ if (failure?.status === 'aborted') {
326
+ return {
327
+ name: 'AbortError',
328
+ message: `${playbookId.toUpperCase()} was aborted.`,
329
+ };
330
+ }
331
+ if (outer instanceof Error) {
332
+ return { name: outer.name || 'Error', message: outer.message };
333
+ }
334
+ return { name: 'Error', message: String(outer) };
335
+ }
336
+ function relayedChildFailure(event, playbookId) {
337
+ const outer = isRecord(event) ? event.error : undefined;
338
+ const failure = normalizedChildFailure(outer, playbookId);
339
+ // DR-048: a completed child that reached an authored failure terminal
340
+ // relays its own output, exactly as a terminal result that did not prove
341
+ // success does through the retained onDone field check.
342
+ if (failure?.status === 'ok') {
343
+ return {
344
+ playbookId,
345
+ status: 'ok',
346
+ ...(failure.output === undefined ? {} : { output: failure.output }),
347
+ };
348
+ }
349
+ return {
350
+ playbookId,
351
+ status: failure?.status === 'aborted' ? 'aborted' : 'error',
352
+ error: authoredChildError(event, playbookId),
353
+ };
354
+ }
355
+ function insufficientChildResult(event, playbookId) {
356
+ const output = childOutputOf(event);
357
+ return {
358
+ playbookId,
359
+ status: 'ok',
360
+ ...(output === undefined ? {} : { output: output }),
361
+ };
362
+ }
363
+ function quotedRelay(values) {
364
+ return values
365
+ .filter(([, value]) => value.length > 0)
366
+ .map(([label, value]) => `${label}: ${value}`
367
+ .split('\n')
368
+ .map((line) => `> ${line}`)
369
+ .join('\n'))
370
+ .join('\n');
371
+ }
372
+ /** PR-3's composed child input: the caller input, the pull request, and the static coding request. */
373
+ export function fixChecksCallText(context) {
374
+ return quotedRelay([
375
+ ['Original request', context.callerInput ?? ''],
376
+ ['Pull request', context.pullRequestUrl ?? ''],
377
+ ['Coding request', FIX_CODING_REQUEST],
378
+ ]);
379
+ }
380
+ function pullRequestIdentity(context) {
381
+ if (!isNonEmptyString(context.pullRequest) ||
382
+ !isNonEmptyString(context.pullRequestUrl)) {
383
+ throw new Error('PR reached a final state without its pull request identity');
384
+ }
385
+ return {
386
+ pullRequest: context.pullRequest,
387
+ pullRequestUrl: context.pullRequestUrl,
388
+ };
389
+ }
390
+ const machineSetup = setup({
391
+ types: {},
392
+ actors: {
393
+ player: fromPromise(async () => {
394
+ throw new Error('player actor must be provided by the runner');
395
+ }),
396
+ playbook: fromPromise(async () => {
397
+ throw new Error('playbook actor must be provided by the runner');
398
+ }),
399
+ script: fromPromise(async () => {
400
+ throw new Error('script actor must be provided by the runner');
401
+ }),
402
+ },
403
+ guards: {
404
+ isOpened,
405
+ isNotPublished,
406
+ needsBossReply,
407
+ checksPassed: zeroExitGuard('checksPassed'),
408
+ checksFailed: nonzeroExitGuard('checksFailed'),
409
+ isCodeSuccess,
410
+ authoredCodeFailure: authoredChildFailureGuard('code'),
411
+ fixPublished: zeroExitGuard('fixPublished'),
412
+ fixNotPublished: nonzeroExitGuard('fixNotPublished'),
413
+ checksStillFailing: nonzeroExitGuard('checksStillFailing'),
414
+ merged: zeroExitGuard('merged'),
415
+ mergeRefused: nonzeroExitGuard('mergeRefused'),
416
+ localDefaultUpdated: zeroExitGuard('localDefaultUpdated'),
417
+ localDefaultNotUpdated: nonzeroExitGuard('localDefaultNotUpdated'),
418
+ emptyBossReply: ({ event }) => event.type === 'BOSS_REPLY' && event.answer.trim().length === 0,
419
+ resumesOpenPullRequest: ({ context, event }) => event.type === 'BOSS_REPLY' &&
420
+ event.answer.trim().length > 0 &&
421
+ context.pendingBossQuestion?.resumeStateId === 'openPullRequest' &&
422
+ (event.questionId === undefined ||
423
+ event.questionId === 'openPullRequest'),
424
+ },
425
+ actions: {
426
+ 'playbook.acceptedOutcome': (_args, _params) => undefined,
427
+ startPr: assign(({ event }) => {
428
+ if (event.type !== 'START_PR')
429
+ return {};
430
+ return {
431
+ callerInput: event.callerInput,
432
+ pullRequest: undefined,
433
+ pullRequestUrl: undefined,
434
+ completion: undefined,
435
+ localDefaultUpdated: undefined,
436
+ coderOutput: undefined,
437
+ childFailure: undefined,
438
+ lastError: undefined,
439
+ pendingBossQuestion: undefined,
440
+ bossReply: undefined,
441
+ };
442
+ }),
443
+ rememberPullRequest: assign(({ event }) => ({
444
+ pullRequest: outputString(event, 'pullRequest'),
445
+ pullRequestUrl: outputString(event, 'pullRequestUrl'),
446
+ pendingBossQuestion: undefined,
447
+ bossReply: undefined,
448
+ })),
449
+ completeNotPublished: assign(({ event }) => ({
450
+ completion: 'not-published',
451
+ coderOutput: outputString(event, 'coderOutput'),
452
+ pendingBossQuestion: undefined,
453
+ bossReply: undefined,
454
+ })),
455
+ rememberPendingQuestion: assign(({ event }) => {
456
+ const question = outputString(event, 'question');
457
+ if (question === undefined || !outputGuard(event, 'needsBossReply')) {
458
+ return { lastError: new Error('Coder question output was malformed') };
459
+ }
460
+ return {
461
+ pendingBossQuestion: {
462
+ questionId: 'openPullRequest',
463
+ resumeStateId: 'openPullRequest',
464
+ sourceItem: 'PR-1',
465
+ asker: { kind: 'role', roleId: 'coder' },
466
+ question,
467
+ },
468
+ bossReply: undefined,
469
+ };
470
+ }),
471
+ rememberBossReply: assign(({ event }) => event.type === 'BOSS_REPLY' ? { bossReply: event.answer } : {}),
472
+ rememberEmptyBossReplyError: assign({
473
+ lastError: () => new Error('BOSS_REPLY received an empty answer'),
474
+ pendingBossQuestion: undefined,
475
+ bossReply: undefined,
476
+ }),
477
+ completeWithInsufficientCodeResult: assign(({ event }) => ({
478
+ completion: 'fix-failed',
479
+ childFailure: insufficientChildResult(event, 'code'),
480
+ })),
481
+ completeWithCodeFailure: assign(({ event }) => ({
482
+ completion: 'fix-failed',
483
+ childFailure: relayedChildFailure(event, 'code'),
484
+ })),
485
+ completeFixNotPublished: assign({
486
+ completion: 'fix-not-published',
487
+ }),
488
+ completeChecksStillFailing: assign({
489
+ completion: 'checks-failed',
490
+ }),
491
+ completeMergeRefused: assign({
492
+ completion: 'merge-refused',
493
+ }),
494
+ completeMerged: assign({
495
+ completion: 'merged',
496
+ localDefaultUpdated: true,
497
+ }),
498
+ completeMergedLocalBehind: assign({
499
+ completion: 'merged',
500
+ localDefaultUpdated: false,
501
+ }),
502
+ rememberActorError: assign(({ event }) => ({
503
+ lastError: event.error,
504
+ })),
505
+ rememberMalformedPlayerOutput: assign({
506
+ lastError: () => new Error('Coder result for PR-1 did not match an available outcome.'),
507
+ }),
508
+ rememberMalformedScriptOutput: assign((_args, params) => ({
509
+ lastError: new Error(`Script result for ${params.sourceItem} did not match a declared outcome.`),
510
+ })),
511
+ },
512
+ });
513
+ export const prMachine = machineSetup.createMachine({
514
+ id: 'pr',
515
+ initial: 'ready',
516
+ context: () => ({}),
517
+ output: ({ context }) => {
518
+ if (context.completion === 'merged') {
519
+ return {
520
+ status: 'merged',
521
+ ...pullRequestIdentity(context),
522
+ localDefaultUpdated: context.localDefaultUpdated === true,
523
+ };
524
+ }
525
+ if (context.completion === 'not-published') {
526
+ if (!isNonEmptyString(context.coderOutput)) {
527
+ throw new Error("PR reached notPublished without Coder's result");
528
+ }
529
+ return {
530
+ status: 'not-merged',
531
+ reason: 'not-published',
532
+ coderOutput: context.coderOutput,
533
+ };
534
+ }
535
+ if (context.completion === 'fix-failed') {
536
+ if (context.childFailure === undefined) {
537
+ throw new Error('PR reached fixFailed without a child result');
538
+ }
539
+ return {
540
+ status: 'not-merged',
541
+ reason: 'fix-failed',
542
+ ...pullRequestIdentity(context),
543
+ childResult: context.childFailure,
544
+ };
545
+ }
546
+ if (context.completion === 'fix-not-published' ||
547
+ context.completion === 'checks-failed' ||
548
+ context.completion === 'merge-refused') {
549
+ return {
550
+ status: 'not-merged',
551
+ reason: context.completion,
552
+ ...pullRequestIdentity(context),
553
+ };
554
+ }
555
+ throw new Error('PR reached a final state without a recorded completion');
556
+ },
557
+ states: {
558
+ ready: {
559
+ id: 'ready',
560
+ description: STATE_DESCRIPTIONS.ready,
561
+ meta: playbookMeta('ready'),
562
+ tags: ['playbook.parked'],
563
+ on: {
564
+ START_PR: { target: 'openPullRequest', actions: 'startPr' },
565
+ },
566
+ },
567
+ openPullRequest: {
568
+ id: 'openPullRequest',
569
+ description: STATE_DESCRIPTIONS.openPullRequest,
570
+ meta: playbookMeta('openPullRequest', 'coder'),
571
+ tags: ['playbook.busy'],
572
+ invoke: {
573
+ src: 'player',
574
+ input: ({ context }) => ({
575
+ stateId: 'openPullRequest',
576
+ role: 'coder',
577
+ sourceItem: 'PR-1',
578
+ prompt: OPEN_PULL_REQUEST_PROMPT,
579
+ result: OPEN_PULL_REQUEST_RESULTS,
580
+ callerInput: context.callerInput ?? '',
581
+ ...(context.pendingBossQuestion === undefined
582
+ ? {}
583
+ : { pendingBossQuestion: context.pendingBossQuestion }),
584
+ ...(context.bossReply === undefined
585
+ ? {}
586
+ : { bossReply: context.bossReply }),
587
+ }),
588
+ onDone: [
589
+ {
590
+ guard: 'isOpened',
591
+ target: 'waitForChecks',
592
+ actions: [
593
+ {
594
+ type: 'playbook.acceptedOutcome',
595
+ params: {
596
+ source: 'openPullRequest',
597
+ target: 'waitForChecks',
598
+ acceptedOutcome: 'opened',
599
+ },
600
+ },
601
+ 'rememberPullRequest',
602
+ ],
603
+ },
604
+ {
605
+ guard: 'isNotPublished',
606
+ target: 'notPublished',
607
+ actions: [
608
+ {
609
+ type: 'playbook.acceptedOutcome',
610
+ params: {
611
+ source: 'openPullRequest',
612
+ target: 'notPublished',
613
+ acceptedOutcome: 'notPublished',
614
+ },
615
+ },
616
+ 'completeNotPublished',
617
+ ],
618
+ },
619
+ {
620
+ guard: 'needsBossReply',
621
+ target: 'awaitBossReply',
622
+ actions: [
623
+ {
624
+ type: 'playbook.acceptedOutcome',
625
+ params: {
626
+ source: 'openPullRequest',
627
+ target: 'awaitBossReply',
628
+ acceptedOutcome: 'needsBossReply',
629
+ },
630
+ },
631
+ 'rememberPendingQuestion',
632
+ ],
633
+ },
634
+ {
635
+ target: 'failed',
636
+ actions: 'rememberMalformedPlayerOutput',
637
+ },
638
+ ],
639
+ onError: { target: 'failed', actions: 'rememberActorError' },
640
+ },
641
+ },
642
+ waitForChecks: {
643
+ id: 'waitForChecks',
644
+ description: STATE_DESCRIPTIONS.waitForChecks,
645
+ meta: playbookMeta('waitForChecks'),
646
+ tags: ['playbook.busy'],
647
+ invoke: {
648
+ src: 'script',
649
+ input: () => ({
650
+ stateId: 'waitForChecks',
651
+ sourceItem: 'PR-2',
652
+ command: WAIT_FOR_CHECKS_COMMAND,
653
+ result: WAIT_FOR_CHECKS_RESULTS,
654
+ }),
655
+ onDone: [
656
+ { guard: 'checksPassed', target: 'mergePullRequest' },
657
+ { guard: 'checksFailed', target: 'fixChecks' },
658
+ {
659
+ target: 'failed',
660
+ actions: {
661
+ type: 'rememberMalformedScriptOutput',
662
+ params: { sourceItem: 'PR-2' },
663
+ },
664
+ },
665
+ ],
666
+ onError: { target: 'failed', actions: 'rememberActorError' },
667
+ },
668
+ },
669
+ fixChecks: {
670
+ id: 'fixChecks',
671
+ description: STATE_DESCRIPTIONS.fixChecks,
672
+ meta: playbookMeta('fixChecks'),
673
+ tags: ['playbook.suspended'],
674
+ invoke: {
675
+ src: 'playbook',
676
+ input: ({ context }) => ({
677
+ stateId: 'fixChecks',
678
+ sourceItem: 'PR-3',
679
+ playbookId: 'code',
680
+ text: fixChecksCallText(context),
681
+ }),
682
+ onDone: [
683
+ { guard: 'isCodeSuccess', target: 'publishFix' },
684
+ {
685
+ target: 'fixFailed',
686
+ actions: 'completeWithInsufficientCodeResult',
687
+ },
688
+ ],
689
+ onError: [
690
+ {
691
+ guard: 'authoredCodeFailure',
692
+ target: 'fixFailed',
693
+ actions: 'completeWithCodeFailure',
694
+ },
695
+ { target: 'failed', actions: 'rememberActorError' },
696
+ ],
697
+ },
698
+ },
699
+ publishFix: {
700
+ id: 'publishFix',
701
+ description: STATE_DESCRIPTIONS.publishFix,
702
+ meta: playbookMeta('publishFix'),
703
+ tags: ['playbook.busy'],
704
+ invoke: {
705
+ src: 'script',
706
+ input: () => ({
707
+ stateId: 'publishFix',
708
+ sourceItem: 'PR-4',
709
+ command: PUBLISH_FIX_COMMAND,
710
+ result: PUBLISH_FIX_RESULTS,
711
+ }),
712
+ onDone: [
713
+ { guard: 'fixPublished', target: 'waitForChecksAfterFix' },
714
+ {
715
+ guard: 'fixNotPublished',
716
+ target: 'fixNotPublished',
717
+ actions: 'completeFixNotPublished',
718
+ },
719
+ {
720
+ target: 'failed',
721
+ actions: {
722
+ type: 'rememberMalformedScriptOutput',
723
+ params: { sourceItem: 'PR-4' },
724
+ },
725
+ },
726
+ ],
727
+ onError: { target: 'failed', actions: 'rememberActorError' },
728
+ },
729
+ },
730
+ waitForChecksAfterFix: {
731
+ id: 'waitForChecksAfterFix',
732
+ description: STATE_DESCRIPTIONS.waitForChecksAfterFix,
733
+ meta: playbookMeta('waitForChecksAfterFix'),
734
+ tags: ['playbook.busy'],
735
+ invoke: {
736
+ src: 'script',
737
+ input: () => ({
738
+ stateId: 'waitForChecksAfterFix',
739
+ sourceItem: 'PR-5',
740
+ command: WAIT_FOR_CHECKS_COMMAND,
741
+ result: WAIT_FOR_CHECKS_AFTER_FIX_RESULTS,
742
+ }),
743
+ onDone: [
744
+ { guard: 'checksPassed', target: 'mergePullRequest' },
745
+ {
746
+ guard: 'checksStillFailing',
747
+ target: 'checksStillFailing',
748
+ actions: 'completeChecksStillFailing',
749
+ },
750
+ {
751
+ target: 'failed',
752
+ actions: {
753
+ type: 'rememberMalformedScriptOutput',
754
+ params: { sourceItem: 'PR-5' },
755
+ },
756
+ },
757
+ ],
758
+ onError: { target: 'failed', actions: 'rememberActorError' },
759
+ },
760
+ },
761
+ mergePullRequest: {
762
+ id: 'mergePullRequest',
763
+ description: STATE_DESCRIPTIONS.mergePullRequest,
764
+ meta: playbookMeta('mergePullRequest'),
765
+ tags: ['playbook.busy'],
766
+ invoke: {
767
+ src: 'script',
768
+ input: () => ({
769
+ stateId: 'mergePullRequest',
770
+ sourceItem: 'PR-6',
771
+ command: MERGE_PULL_REQUEST_COMMAND,
772
+ result: MERGE_PULL_REQUEST_RESULTS,
773
+ }),
774
+ onDone: [
775
+ { guard: 'merged', target: 'updateLocalDefault' },
776
+ {
777
+ guard: 'mergeRefused',
778
+ target: 'mergeRefused',
779
+ actions: 'completeMergeRefused',
780
+ },
781
+ {
782
+ target: 'failed',
783
+ actions: {
784
+ type: 'rememberMalformedScriptOutput',
785
+ params: { sourceItem: 'PR-6' },
786
+ },
787
+ },
788
+ ],
789
+ onError: { target: 'failed', actions: 'rememberActorError' },
790
+ },
791
+ },
792
+ updateLocalDefault: {
793
+ id: 'updateLocalDefault',
794
+ description: STATE_DESCRIPTIONS.updateLocalDefault,
795
+ meta: playbookMeta('updateLocalDefault'),
796
+ tags: ['playbook.busy'],
797
+ invoke: {
798
+ src: 'script',
799
+ input: () => ({
800
+ stateId: 'updateLocalDefault',
801
+ sourceItem: 'PR-7',
802
+ command: UPDATE_LOCAL_DEFAULT_COMMAND,
803
+ result: UPDATE_LOCAL_DEFAULT_RESULTS,
804
+ }),
805
+ onDone: [
806
+ {
807
+ guard: 'localDefaultUpdated',
808
+ target: 'merged',
809
+ actions: 'completeMerged',
810
+ },
811
+ {
812
+ guard: 'localDefaultNotUpdated',
813
+ target: 'mergedLocalBehind',
814
+ actions: 'completeMergedLocalBehind',
815
+ },
816
+ {
817
+ target: 'failed',
818
+ actions: {
819
+ type: 'rememberMalformedScriptOutput',
820
+ params: { sourceItem: 'PR-7' },
821
+ },
822
+ },
823
+ ],
824
+ onError: { target: 'failed', actions: 'rememberActorError' },
825
+ },
826
+ },
827
+ awaitBossReply: {
828
+ id: 'awaitBossReply',
829
+ description: STATE_DESCRIPTIONS.awaitBossReply,
830
+ meta: playbookMeta('awaitBossReply'),
831
+ tags: ['playbook.parked'],
832
+ on: {
833
+ BOSS_REPLY: [
834
+ {
835
+ guard: 'emptyBossReply',
836
+ target: '#failed',
837
+ actions: 'rememberEmptyBossReplyError',
838
+ },
839
+ {
840
+ guard: 'resumesOpenPullRequest',
841
+ target: '#openPullRequest',
842
+ reenter: true,
843
+ actions: 'rememberBossReply',
844
+ },
845
+ ],
846
+ START_PR: { target: 'openPullRequest', actions: 'startPr' },
847
+ },
848
+ },
849
+ failed: {
850
+ id: 'failed',
851
+ description: STATE_DESCRIPTIONS.failed,
852
+ meta: playbookMeta('failed'),
853
+ tags: ['playbook.parked'],
854
+ on: {
855
+ START_PR: { target: 'openPullRequest', actions: 'startPr' },
856
+ },
857
+ },
858
+ merged: {
859
+ id: 'merged',
860
+ description: STATE_DESCRIPTIONS.merged,
861
+ meta: terminalMeta('merged', 'success'),
862
+ type: 'final',
863
+ },
864
+ mergedLocalBehind: {
865
+ id: 'mergedLocalBehind',
866
+ description: STATE_DESCRIPTIONS.mergedLocalBehind,
867
+ meta: terminalMeta('mergedLocalBehind', 'success'),
868
+ type: 'final',
869
+ },
870
+ notPublished: {
871
+ id: 'notPublished',
872
+ description: STATE_DESCRIPTIONS.notPublished,
873
+ meta: terminalMeta('notPublished', 'failure'),
874
+ type: 'final',
875
+ },
876
+ fixFailed: {
877
+ id: 'fixFailed',
878
+ description: STATE_DESCRIPTIONS.fixFailed,
879
+ meta: terminalMeta('fixFailed', 'failure'),
880
+ type: 'final',
881
+ },
882
+ fixNotPublished: {
883
+ id: 'fixNotPublished',
884
+ description: STATE_DESCRIPTIONS.fixNotPublished,
885
+ meta: terminalMeta('fixNotPublished', 'failure'),
886
+ type: 'final',
887
+ },
888
+ checksStillFailing: {
889
+ id: 'checksStillFailing',
890
+ description: STATE_DESCRIPTIONS.checksStillFailing,
891
+ meta: terminalMeta('checksStillFailing', 'failure'),
892
+ type: 'final',
893
+ },
894
+ mergeRefused: {
895
+ id: 'mergeRefused',
896
+ description: STATE_DESCRIPTIONS.mergeRefused,
897
+ meta: terminalMeta('mergeRefused', 'failure'),
898
+ type: 'final',
899
+ },
900
+ },
901
+ });
902
+ export default prMachine;