@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,1251 @@
|
|
|
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
|
+
|
|
6
|
+
import { assign, fromPromise, setup } from 'xstate';
|
|
7
|
+
|
|
8
|
+
export type JsonValue =
|
|
9
|
+
| null
|
|
10
|
+
| boolean
|
|
11
|
+
| number
|
|
12
|
+
| string
|
|
13
|
+
| readonly JsonValue[]
|
|
14
|
+
| { readonly [key: string]: JsonValue };
|
|
15
|
+
|
|
16
|
+
export type PrStateId =
|
|
17
|
+
| 'openPullRequest'
|
|
18
|
+
| 'waitForChecks'
|
|
19
|
+
| 'fixChecks'
|
|
20
|
+
| 'publishFix'
|
|
21
|
+
| 'waitForChecksAfterFix'
|
|
22
|
+
| 'mergePullRequest'
|
|
23
|
+
| 'updateLocalDefault';
|
|
24
|
+
|
|
25
|
+
export type PrSourceItem =
|
|
26
|
+
| 'PR-1'
|
|
27
|
+
| 'PR-2'
|
|
28
|
+
| 'PR-3'
|
|
29
|
+
| 'PR-4'
|
|
30
|
+
| 'PR-5'
|
|
31
|
+
| 'PR-6'
|
|
32
|
+
| 'PR-7';
|
|
33
|
+
|
|
34
|
+
export type PrScriptStateId =
|
|
35
|
+
| 'waitForChecks'
|
|
36
|
+
| 'publishFix'
|
|
37
|
+
| 'waitForChecksAfterFix'
|
|
38
|
+
| 'mergePullRequest'
|
|
39
|
+
| 'updateLocalDefault';
|
|
40
|
+
|
|
41
|
+
export type PrScriptSourceItem = 'PR-2' | 'PR-4' | 'PR-5' | 'PR-6' | 'PR-7';
|
|
42
|
+
|
|
43
|
+
export type PrChildPlaybookId = 'code';
|
|
44
|
+
|
|
45
|
+
export type PendingBossQuestion = {
|
|
46
|
+
readonly questionId: 'openPullRequest';
|
|
47
|
+
readonly resumeStateId: 'openPullRequest';
|
|
48
|
+
readonly sourceItem: 'PR-1';
|
|
49
|
+
readonly asker: { readonly kind: 'role'; readonly roleId: 'coder' };
|
|
50
|
+
readonly question: string;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export type PlayerInput = {
|
|
54
|
+
readonly stateId: 'openPullRequest';
|
|
55
|
+
readonly role: 'coder';
|
|
56
|
+
readonly sourceItem: 'PR-1';
|
|
57
|
+
readonly prompt: string;
|
|
58
|
+
readonly result: Readonly<Record<string, string>>;
|
|
59
|
+
readonly callerInput: string;
|
|
60
|
+
readonly pendingBossQuestion?: PendingBossQuestion;
|
|
61
|
+
readonly bossReply?: string;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export type PlayerOutput =
|
|
65
|
+
| {
|
|
66
|
+
readonly guard: 'opened';
|
|
67
|
+
readonly pullRequest: string;
|
|
68
|
+
readonly pullRequestUrl: string;
|
|
69
|
+
}
|
|
70
|
+
| { readonly guard: 'notPublished'; readonly coderOutput: string }
|
|
71
|
+
| { readonly guard: 'needsBossReply'; readonly question: string };
|
|
72
|
+
|
|
73
|
+
export type PlaybookInput = {
|
|
74
|
+
readonly stateId: 'fixChecks';
|
|
75
|
+
readonly sourceItem: 'PR-3';
|
|
76
|
+
readonly playbookId: PrChildPlaybookId;
|
|
77
|
+
readonly text: string;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export type ScriptInput = {
|
|
81
|
+
readonly stateId: PrScriptStateId;
|
|
82
|
+
readonly sourceItem: PrScriptSourceItem;
|
|
83
|
+
readonly command: string;
|
|
84
|
+
readonly result: Readonly<Record<string, string>>;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export type ScriptOutput =
|
|
88
|
+
| { readonly guard: 'checksPassed'; readonly exitStatus: number }
|
|
89
|
+
| { readonly guard: 'checksFailed'; readonly exitStatus: number }
|
|
90
|
+
| { readonly guard: 'fixPublished'; readonly exitStatus: number }
|
|
91
|
+
| { readonly guard: 'fixNotPublished'; readonly exitStatus: number }
|
|
92
|
+
| { readonly guard: 'checksStillFailing'; readonly exitStatus: number }
|
|
93
|
+
| { readonly guard: 'merged'; readonly exitStatus: number }
|
|
94
|
+
| { readonly guard: 'mergeRefused'; readonly exitStatus: number }
|
|
95
|
+
| { readonly guard: 'localDefaultUpdated'; readonly exitStatus: number }
|
|
96
|
+
| { readonly guard: 'localDefaultNotUpdated'; readonly exitStatus: number };
|
|
97
|
+
|
|
98
|
+
export type CompactError = {
|
|
99
|
+
readonly name: string;
|
|
100
|
+
readonly message: string;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
/** Sanitized canonical child result relayed as PR's own failure outcome. */
|
|
104
|
+
export type CompletedChildResult =
|
|
105
|
+
| {
|
|
106
|
+
readonly playbookId: PrChildPlaybookId;
|
|
107
|
+
readonly status: 'ok';
|
|
108
|
+
readonly output?: JsonValue;
|
|
109
|
+
}
|
|
110
|
+
| {
|
|
111
|
+
readonly playbookId: PrChildPlaybookId;
|
|
112
|
+
readonly status: 'aborted' | 'error';
|
|
113
|
+
readonly error: CompactError;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
export type PrPlaybookOutput =
|
|
117
|
+
| {
|
|
118
|
+
readonly status: 'merged';
|
|
119
|
+
/** The pull request number, exactly as Coder reported it. */
|
|
120
|
+
readonly pullRequest: string;
|
|
121
|
+
/** The pull request URL, exactly as Coder reported it. */
|
|
122
|
+
readonly pullRequestUrl: string;
|
|
123
|
+
/** Whether the local default branch was fast-forwarded to the merged head. */
|
|
124
|
+
readonly localDefaultUpdated: boolean;
|
|
125
|
+
}
|
|
126
|
+
| {
|
|
127
|
+
readonly status: 'not-merged';
|
|
128
|
+
readonly reason: 'not-published';
|
|
129
|
+
/** Coder's complete result carrying the reason. */
|
|
130
|
+
readonly coderOutput: string;
|
|
131
|
+
}
|
|
132
|
+
| {
|
|
133
|
+
readonly status: 'not-merged';
|
|
134
|
+
readonly reason: 'fix-failed';
|
|
135
|
+
readonly pullRequest: string;
|
|
136
|
+
readonly pullRequestUrl: string;
|
|
137
|
+
/** The relayed canonical `code` result that ended the one fix attempt. */
|
|
138
|
+
readonly childResult: CompletedChildResult;
|
|
139
|
+
}
|
|
140
|
+
| {
|
|
141
|
+
readonly status: 'not-merged';
|
|
142
|
+
readonly reason: 'fix-not-published' | 'checks-failed' | 'merge-refused';
|
|
143
|
+
readonly pullRequest: string;
|
|
144
|
+
readonly pullRequestUrl: string;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
export type PrCompletion =
|
|
148
|
+
| 'merged'
|
|
149
|
+
| 'not-published'
|
|
150
|
+
| 'fix-failed'
|
|
151
|
+
| 'fix-not-published'
|
|
152
|
+
| 'checks-failed'
|
|
153
|
+
| 'merge-refused';
|
|
154
|
+
|
|
155
|
+
/** The machine reads no input: the caller input arrives on `START_PR`. */
|
|
156
|
+
export type PrInput = Readonly<Record<never, never>>;
|
|
157
|
+
|
|
158
|
+
export type PrContext = {
|
|
159
|
+
readonly callerInput?: string;
|
|
160
|
+
readonly pullRequest?: string;
|
|
161
|
+
readonly pullRequestUrl?: string;
|
|
162
|
+
readonly completion?: PrCompletion;
|
|
163
|
+
readonly localDefaultUpdated?: boolean;
|
|
164
|
+
readonly coderOutput?: string;
|
|
165
|
+
readonly childFailure?: CompletedChildResult;
|
|
166
|
+
readonly lastError?: unknown;
|
|
167
|
+
readonly pendingBossQuestion?: PendingBossQuestion;
|
|
168
|
+
readonly bossReply?: string;
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
export type PrEvent =
|
|
172
|
+
| { readonly type: 'START_PR'; readonly callerInput: string }
|
|
173
|
+
| {
|
|
174
|
+
readonly type: 'BOSS_REPLY';
|
|
175
|
+
readonly answer: string;
|
|
176
|
+
readonly questionId?: 'openPullRequest';
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const OPEN_PULL_REQUEST_PROMPT = [
|
|
180
|
+
'> Original request: <caller-input>',
|
|
181
|
+
'',
|
|
182
|
+
'Publish the branch and open its pull request without changing any file or making any commit.',
|
|
183
|
+
'Confirm that the working tree is clean and that the checked-out branch is the branch to deliver, not the repository default branch.',
|
|
184
|
+
"Push the branch to the repository's GitHub remote with its upstream set; never force-push.",
|
|
185
|
+
'If an open pull request for this branch already exists, use it; otherwise open one against the repository default branch with `gh pr create`.',
|
|
186
|
+
'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.',
|
|
187
|
+
'Report the pull request number and URL exactly.',
|
|
188
|
+
'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.',
|
|
189
|
+
].join('\n');
|
|
190
|
+
|
|
191
|
+
const NEEDS_BOSS_REPLY_RESULT =
|
|
192
|
+
"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>`.";
|
|
193
|
+
|
|
194
|
+
const OPEN_PULL_REQUEST_RESULTS = {
|
|
195
|
+
opened:
|
|
196
|
+
'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>`.',
|
|
197
|
+
notPublished:
|
|
198
|
+
'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>`.',
|
|
199
|
+
needsBossReply: NEEDS_BOSS_REPLY_RESULT,
|
|
200
|
+
} as const;
|
|
201
|
+
|
|
202
|
+
// The mechanical steps are static POSIX shell text (slc/optimize.md): no
|
|
203
|
+
// placeholder, no runtime value, and no prose — `gh` infers the pull request
|
|
204
|
+
// from the checked-out branch.
|
|
205
|
+
const WAIT_FOR_CHECKS_COMMAND = [
|
|
206
|
+
'n=0',
|
|
207
|
+
"while gh pr checks 2>&1 | grep -q 'no checks reported'; do",
|
|
208
|
+
'n=$((n + 1))',
|
|
209
|
+
'[ "$n" -ge 6 ] && exit 0',
|
|
210
|
+
'sleep 10',
|
|
211
|
+
'done',
|
|
212
|
+
'gh pr checks --watch --fail-fast >/dev/null 2>&1',
|
|
213
|
+
].join('\n');
|
|
214
|
+
|
|
215
|
+
const PUBLISH_FIX_COMMAND = [
|
|
216
|
+
'git push || exit 1',
|
|
217
|
+
'n=0',
|
|
218
|
+
'until [ "$(gh pr view --json headRefOid --jq .headRefOid 2>/dev/null)" = "$(git rev-parse HEAD)" ]; do',
|
|
219
|
+
'n=$((n + 1))',
|
|
220
|
+
'[ "$n" -ge 12 ] && exit 1',
|
|
221
|
+
'sleep 5',
|
|
222
|
+
'done',
|
|
223
|
+
].join('\n');
|
|
224
|
+
|
|
225
|
+
const MERGE_PULL_REQUEST_COMMAND =
|
|
226
|
+
'gh pr merge --merge --delete-branch --match-head-commit "$(git rev-parse HEAD)"';
|
|
227
|
+
|
|
228
|
+
const UPDATE_LOCAL_DEFAULT_COMMAND = 'git pull --ff-only';
|
|
229
|
+
|
|
230
|
+
const WAIT_FOR_CHECKS_RESULTS = {
|
|
231
|
+
checksPassed:
|
|
232
|
+
"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.",
|
|
233
|
+
checksFailed:
|
|
234
|
+
"The command exited with a nonzero status: the pull request's checks failed.",
|
|
235
|
+
} as const;
|
|
236
|
+
|
|
237
|
+
const PUBLISH_FIX_RESULTS = {
|
|
238
|
+
fixPublished:
|
|
239
|
+
"The command exited with status zero: the fix is pushed and the pull request's head is the pushed commit.",
|
|
240
|
+
fixNotPublished:
|
|
241
|
+
"The command exited with a nonzero status: the push was rejected or the pull request's head did not advance to the pushed commit.",
|
|
242
|
+
} as const;
|
|
243
|
+
|
|
244
|
+
const WAIT_FOR_CHECKS_AFTER_FIX_RESULTS = {
|
|
245
|
+
checksPassed:
|
|
246
|
+
"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.",
|
|
247
|
+
checksStillFailing:
|
|
248
|
+
"The command exited with a nonzero status: the pull request's checks are still failing after the one fix attempt.",
|
|
249
|
+
} as const;
|
|
250
|
+
|
|
251
|
+
const MERGE_PULL_REQUEST_RESULTS = {
|
|
252
|
+
merged:
|
|
253
|
+
'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.',
|
|
254
|
+
mergeRefused:
|
|
255
|
+
'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.',
|
|
256
|
+
} as const;
|
|
257
|
+
|
|
258
|
+
const UPDATE_LOCAL_DEFAULT_RESULTS = {
|
|
259
|
+
localDefaultUpdated:
|
|
260
|
+
'The command exited with status zero: the local default branch is fast-forwarded to the merged head.',
|
|
261
|
+
localDefaultNotUpdated:
|
|
262
|
+
'The command exited with a nonzero status: the local default branch was not fast-forwarded to the merged head.',
|
|
263
|
+
} as const;
|
|
264
|
+
|
|
265
|
+
// PR-3's coding request is static prompt text of the nested call template.
|
|
266
|
+
const FIX_CODING_REQUEST =
|
|
267
|
+
"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.";
|
|
268
|
+
|
|
269
|
+
const STATE_DESCRIPTIONS = {
|
|
270
|
+
ready: 'Waiting for the caller input that names the branch to deliver.',
|
|
271
|
+
openPullRequest:
|
|
272
|
+
'Coder is publishing the branch and opening its pull request.',
|
|
273
|
+
waitForChecks:
|
|
274
|
+
"Waiting for the pull request's checks to settle before any fix attempt.",
|
|
275
|
+
fixChecks:
|
|
276
|
+
'The CODE playbook is fixing the cause of the red checks on the checked-out branch.',
|
|
277
|
+
publishFix:
|
|
278
|
+
"Publishing the fix to the pull request and waiting for the pull request's head to reach the pushed commit.",
|
|
279
|
+
waitForChecksAfterFix:
|
|
280
|
+
"Waiting for the pull request's checks to settle after the one fix attempt.",
|
|
281
|
+
mergePullRequest:
|
|
282
|
+
'Merging the pull request into the repository default branch.',
|
|
283
|
+
updateLocalDefault:
|
|
284
|
+
'Fast-forwarding the local default branch to the merged head.',
|
|
285
|
+
awaitBossReply: 'Waiting for Boss to answer Coder.',
|
|
286
|
+
failed:
|
|
287
|
+
'The pull-request delivery workflow failed and is waiting for new caller input.',
|
|
288
|
+
merged:
|
|
289
|
+
'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.',
|
|
290
|
+
mergedLocalBehind:
|
|
291
|
+
'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.',
|
|
292
|
+
notPublished:
|
|
293
|
+
'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.',
|
|
294
|
+
fixFailed:
|
|
295
|
+
"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.",
|
|
296
|
+
fixNotPublished:
|
|
297
|
+
"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.",
|
|
298
|
+
checksStillFailing:
|
|
299
|
+
"The pull request's checks are still failing after the one fix attempt; the pull request remains open.",
|
|
300
|
+
mergeRefused:
|
|
301
|
+
'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.',
|
|
302
|
+
} as const;
|
|
303
|
+
|
|
304
|
+
function playbookMeta<StateId extends keyof typeof STATE_DESCRIPTIONS>(
|
|
305
|
+
stateId: StateId,
|
|
306
|
+
role?: 'coder',
|
|
307
|
+
) {
|
|
308
|
+
return {
|
|
309
|
+
playbook: {
|
|
310
|
+
stateId,
|
|
311
|
+
description: STATE_DESCRIPTIONS[stateId],
|
|
312
|
+
...(role === undefined ? {} : { role }),
|
|
313
|
+
},
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// DR-048: a final state additionally declares whether its outcome means the
|
|
318
|
+
// workflow succeeded or failed, so PR's own caller learns that from the
|
|
319
|
+
// machine rather than from PR's output fields.
|
|
320
|
+
function terminalMeta<StateId extends keyof typeof STATE_DESCRIPTIONS>(
|
|
321
|
+
stateId: StateId,
|
|
322
|
+
terminal: 'success' | 'failure',
|
|
323
|
+
) {
|
|
324
|
+
return {
|
|
325
|
+
playbook: {
|
|
326
|
+
stateId,
|
|
327
|
+
description: STATE_DESCRIPTIONS[stateId],
|
|
328
|
+
terminal,
|
|
329
|
+
},
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
334
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
335
|
+
return false;
|
|
336
|
+
}
|
|
337
|
+
const prototype = Object.getPrototypeOf(value);
|
|
338
|
+
return prototype === Object.prototype || prototype === null;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function isNonEmptyString(value: unknown): value is string {
|
|
342
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function actorOutput(event: unknown): Record<string, unknown> | undefined {
|
|
346
|
+
if (!isRecord(event)) return undefined;
|
|
347
|
+
return isRecord(event.output) ? event.output : undefined;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function outputGuard(event: unknown, guard: string): boolean {
|
|
351
|
+
return actorOutput(event)?.guard === guard;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function outputString(event: unknown, field: string): string | undefined {
|
|
355
|
+
const value = actorOutput(event)?.[field];
|
|
356
|
+
return isNonEmptyString(value) ? value : undefined;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function isOpened({ event }: { event: unknown }): boolean {
|
|
360
|
+
return (
|
|
361
|
+
outputGuard(event, 'opened') &&
|
|
362
|
+
outputString(event, 'pullRequest') !== undefined &&
|
|
363
|
+
outputString(event, 'pullRequestUrl') !== undefined
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function isNotPublished({ event }: { event: unknown }): boolean {
|
|
368
|
+
return (
|
|
369
|
+
outputGuard(event, 'notPublished') &&
|
|
370
|
+
outputString(event, 'coderOutput') !== undefined
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function needsBossReply({ event }: { event: unknown }): boolean {
|
|
375
|
+
return (
|
|
376
|
+
outputGuard(event, 'needsBossReply') &&
|
|
377
|
+
outputString(event, 'question') !== undefined
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// A script actor resolves mechanically from the exit status: the first
|
|
382
|
+
// declared guard with status zero, the second with the nonzero status.
|
|
383
|
+
function scriptExitStatus(event: unknown): number | undefined {
|
|
384
|
+
const value = actorOutput(event)?.exitStatus;
|
|
385
|
+
return Number.isInteger(value) ? (value as number) : undefined;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function zeroExitGuard(
|
|
389
|
+
guard: ScriptOutput['guard'],
|
|
390
|
+
): (args: { event: unknown }) => boolean {
|
|
391
|
+
return ({ event }) =>
|
|
392
|
+
outputGuard(event, guard) && scriptExitStatus(event) === 0;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function nonzeroExitGuard(
|
|
396
|
+
guard: ScriptOutput['guard'],
|
|
397
|
+
): (args: { event: unknown }) => boolean {
|
|
398
|
+
return ({ event }) => {
|
|
399
|
+
const exitStatus = scriptExitStatus(event);
|
|
400
|
+
return (
|
|
401
|
+
outputGuard(event, guard) && exitStatus !== undefined && exitStatus !== 0
|
|
402
|
+
);
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function childOutputOf(event: unknown): unknown {
|
|
407
|
+
return isRecord(event) ? event.output : undefined;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// CODE's canonical successful terminal output discriminates completion with
|
|
411
|
+
// `status: 'complete'`, carries the exact last CODE-owned commit, and
|
|
412
|
+
// affirms that every phase's review passed.
|
|
413
|
+
function isCodeSuccessValue(value: unknown): boolean {
|
|
414
|
+
return (
|
|
415
|
+
isRecord(value) &&
|
|
416
|
+
value.status === 'complete' &&
|
|
417
|
+
isNonEmptyString(value.lastCodeCommit) &&
|
|
418
|
+
value.allReviewsPassed === true
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function isCodeSuccess({ event }: { event: unknown }): boolean {
|
|
423
|
+
return isCodeSuccessValue(childOutputOf(event));
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function isStateValue(value: unknown, ancestors = new Set<object>()): boolean {
|
|
427
|
+
if (typeof value === 'string') return true;
|
|
428
|
+
if (!isRecord(value) || ancestors.has(value)) return false;
|
|
429
|
+
const next = new Set(ancestors).add(value);
|
|
430
|
+
return Reflect.ownKeys(value).every(
|
|
431
|
+
(key) =>
|
|
432
|
+
typeof key === 'string' &&
|
|
433
|
+
Object.prototype.propertyIsEnumerable.call(value, key) &&
|
|
434
|
+
isStateValue(value[key], next),
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function isStringArray(value: unknown): value is readonly string[] {
|
|
439
|
+
return (
|
|
440
|
+
Array.isArray(value) &&
|
|
441
|
+
Reflect.ownKeys(value).length === value.length + 1 &&
|
|
442
|
+
Reflect.ownKeys(value).every((key) => {
|
|
443
|
+
if (key === 'length') return true;
|
|
444
|
+
return (
|
|
445
|
+
typeof key === 'string' &&
|
|
446
|
+
/^(0|[1-9][0-9]*)$/.test(key) &&
|
|
447
|
+
Number(key) < value.length
|
|
448
|
+
);
|
|
449
|
+
}) &&
|
|
450
|
+
value.every((entry) => isNonEmptyString(entry)) &&
|
|
451
|
+
new Set(value).size === value.length
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function isPlaybookState(value: unknown): boolean {
|
|
456
|
+
if (!isRecord(value)) return false;
|
|
457
|
+
const allowed = new Set([
|
|
458
|
+
'value',
|
|
459
|
+
'activeStateIds',
|
|
460
|
+
'tags',
|
|
461
|
+
'status',
|
|
462
|
+
'quiescent',
|
|
463
|
+
'stateId',
|
|
464
|
+
]);
|
|
465
|
+
if (
|
|
466
|
+
Reflect.ownKeys(value).some(
|
|
467
|
+
(key) => typeof key !== 'string' || !allowed.has(key),
|
|
468
|
+
) ||
|
|
469
|
+
!Object.prototype.hasOwnProperty.call(value, 'value') ||
|
|
470
|
+
!Object.prototype.hasOwnProperty.call(value, 'activeStateIds') ||
|
|
471
|
+
!Object.prototype.hasOwnProperty.call(value, 'tags') ||
|
|
472
|
+
!Object.prototype.hasOwnProperty.call(value, 'status') ||
|
|
473
|
+
!Object.prototype.hasOwnProperty.call(value, 'quiescent')
|
|
474
|
+
) {
|
|
475
|
+
return false;
|
|
476
|
+
}
|
|
477
|
+
return (
|
|
478
|
+
isStateValue(value.value) &&
|
|
479
|
+
isStringArray(value.activeStateIds) &&
|
|
480
|
+
isStringArray(value.tags) &&
|
|
481
|
+
(value.status === 'active' ||
|
|
482
|
+
value.status === 'done' ||
|
|
483
|
+
value.status === 'error' ||
|
|
484
|
+
value.status === 'stopped') &&
|
|
485
|
+
typeof value.quiescent === 'boolean' &&
|
|
486
|
+
(!Object.prototype.hasOwnProperty.call(value, 'stateId') ||
|
|
487
|
+
(isNonEmptyString(value.stateId) &&
|
|
488
|
+
value.activeStateIds.length === 1 &&
|
|
489
|
+
value.activeStateIds[0] === value.stateId))
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function nestedResultFromError(
|
|
494
|
+
error: unknown,
|
|
495
|
+
): Record<string, unknown> | undefined {
|
|
496
|
+
if (!(error instanceof Error)) return undefined;
|
|
497
|
+
const result = (error as Error & { readonly result?: unknown }).result;
|
|
498
|
+
return isRecord(result) ? result : undefined;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
type AuthoredChildFailure =
|
|
502
|
+
| {
|
|
503
|
+
readonly status: 'aborted' | 'error';
|
|
504
|
+
readonly error?: CompactError;
|
|
505
|
+
}
|
|
506
|
+
// DR-048: a child that completed at an authored failure terminal. The
|
|
507
|
+
// bridge rejects it through the same error path, so PR recognizes the
|
|
508
|
+
// failure from the child's own machine and never from its output fields.
|
|
509
|
+
| {
|
|
510
|
+
readonly status: 'ok';
|
|
511
|
+
readonly output?: JsonValue;
|
|
512
|
+
};
|
|
513
|
+
|
|
514
|
+
function isFailureTerminalRecord(value: unknown): boolean {
|
|
515
|
+
if (!isRecord(value)) return false;
|
|
516
|
+
const allowed = new Set(['stateId', 'kind', 'description']);
|
|
517
|
+
return (
|
|
518
|
+
Reflect.ownKeys(value).every(
|
|
519
|
+
(key) => typeof key === 'string' && allowed.has(key),
|
|
520
|
+
) &&
|
|
521
|
+
isNonEmptyString(value.stateId) &&
|
|
522
|
+
value.kind === 'failure' &&
|
|
523
|
+
(!Object.prototype.hasOwnProperty.call(value, 'description') ||
|
|
524
|
+
typeof value.description === 'string')
|
|
525
|
+
);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function normalizedChildFailure(
|
|
529
|
+
error: unknown,
|
|
530
|
+
playbookId: PrChildPlaybookId,
|
|
531
|
+
): AuthoredChildFailure | undefined {
|
|
532
|
+
const result = nestedResultFromError(error);
|
|
533
|
+
if (result === undefined) return undefined;
|
|
534
|
+
const allowed = new Set([
|
|
535
|
+
'status',
|
|
536
|
+
'playbookId',
|
|
537
|
+
'childSessionId',
|
|
538
|
+
'state',
|
|
539
|
+
...(result.status === 'ok' ? ['output', 'terminal'] : ['error']),
|
|
540
|
+
]);
|
|
541
|
+
if (
|
|
542
|
+
Reflect.ownKeys(result).some(
|
|
543
|
+
(key) => typeof key !== 'string' || !allowed.has(key),
|
|
544
|
+
) ||
|
|
545
|
+
(result.status !== 'aborted' &&
|
|
546
|
+
result.status !== 'error' &&
|
|
547
|
+
result.status !== 'ok') ||
|
|
548
|
+
result.playbookId !== playbookId
|
|
549
|
+
) {
|
|
550
|
+
return undefined;
|
|
551
|
+
}
|
|
552
|
+
if (
|
|
553
|
+
Object.prototype.hasOwnProperty.call(result, 'childSessionId') &&
|
|
554
|
+
!isNonEmptyString(result.childSessionId)
|
|
555
|
+
) {
|
|
556
|
+
return undefined;
|
|
557
|
+
}
|
|
558
|
+
if (result.status === 'ok') {
|
|
559
|
+
if (
|
|
560
|
+
!Object.prototype.hasOwnProperty.call(result, 'terminal') ||
|
|
561
|
+
!isFailureTerminalRecord(result.terminal) ||
|
|
562
|
+
!isNonEmptyString(result.childSessionId) ||
|
|
563
|
+
(Object.prototype.hasOwnProperty.call(result, 'state') &&
|
|
564
|
+
!isPlaybookState(result.state))
|
|
565
|
+
) {
|
|
566
|
+
return undefined;
|
|
567
|
+
}
|
|
568
|
+
return {
|
|
569
|
+
status: 'ok',
|
|
570
|
+
...(result.output === undefined
|
|
571
|
+
? {}
|
|
572
|
+
: { output: result.output as JsonValue }),
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
if (
|
|
576
|
+
Object.prototype.hasOwnProperty.call(result, 'state') &&
|
|
577
|
+
!isPlaybookState(result.state)
|
|
578
|
+
) {
|
|
579
|
+
return undefined;
|
|
580
|
+
}
|
|
581
|
+
let normalizedError: CompactError | undefined;
|
|
582
|
+
if (Object.prototype.hasOwnProperty.call(result, 'error')) {
|
|
583
|
+
if (!isRecord(result.error)) return undefined;
|
|
584
|
+
const errorKeys = Reflect.ownKeys(result.error);
|
|
585
|
+
if (
|
|
586
|
+
errorKeys.some(
|
|
587
|
+
(key) =>
|
|
588
|
+
typeof key !== 'string' ||
|
|
589
|
+
(key !== 'name' && key !== 'message' && key !== 'stack'),
|
|
590
|
+
) ||
|
|
591
|
+
!Object.prototype.hasOwnProperty.call(result.error, 'name') ||
|
|
592
|
+
!Object.prototype.hasOwnProperty.call(result.error, 'message') ||
|
|
593
|
+
!isNonEmptyString(result.error.name) ||
|
|
594
|
+
typeof result.error.message !== 'string' ||
|
|
595
|
+
(Object.prototype.hasOwnProperty.call(result.error, 'stack') &&
|
|
596
|
+
typeof result.error.stack !== 'string')
|
|
597
|
+
) {
|
|
598
|
+
return undefined;
|
|
599
|
+
}
|
|
600
|
+
normalizedError = {
|
|
601
|
+
name: result.error.name,
|
|
602
|
+
message: result.error.message,
|
|
603
|
+
};
|
|
604
|
+
} else if (result.status === 'error') {
|
|
605
|
+
return undefined;
|
|
606
|
+
}
|
|
607
|
+
return {
|
|
608
|
+
status: result.status,
|
|
609
|
+
...(normalizedError === undefined ? {} : { error: normalizedError }),
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function authoredChildFailureGuard(
|
|
614
|
+
playbookId: PrChildPlaybookId,
|
|
615
|
+
): (args: { event: unknown }) => boolean {
|
|
616
|
+
return ({ event }) => {
|
|
617
|
+
const error = isRecord(event) ? event.error : undefined;
|
|
618
|
+
return normalizedChildFailure(error, playbookId) !== undefined;
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function authoredChildError(
|
|
623
|
+
event: unknown,
|
|
624
|
+
playbookId: PrChildPlaybookId,
|
|
625
|
+
): CompactError {
|
|
626
|
+
const outer = isRecord(event) ? event.error : undefined;
|
|
627
|
+
const failure = normalizedChildFailure(outer, playbookId);
|
|
628
|
+
if (failure !== undefined && failure.status !== 'ok' && failure.error) {
|
|
629
|
+
return failure.error;
|
|
630
|
+
}
|
|
631
|
+
if (failure?.status === 'aborted') {
|
|
632
|
+
return {
|
|
633
|
+
name: 'AbortError',
|
|
634
|
+
message: `${playbookId.toUpperCase()} was aborted.`,
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
if (outer instanceof Error) {
|
|
638
|
+
return { name: outer.name || 'Error', message: outer.message };
|
|
639
|
+
}
|
|
640
|
+
return { name: 'Error', message: String(outer) };
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function relayedChildFailure(
|
|
644
|
+
event: unknown,
|
|
645
|
+
playbookId: PrChildPlaybookId,
|
|
646
|
+
): CompletedChildResult {
|
|
647
|
+
const outer = isRecord(event) ? event.error : undefined;
|
|
648
|
+
const failure = normalizedChildFailure(outer, playbookId);
|
|
649
|
+
// DR-048: a completed child that reached an authored failure terminal
|
|
650
|
+
// relays its own output, exactly as a terminal result that did not prove
|
|
651
|
+
// success does through the retained onDone field check.
|
|
652
|
+
if (failure?.status === 'ok') {
|
|
653
|
+
return {
|
|
654
|
+
playbookId,
|
|
655
|
+
status: 'ok',
|
|
656
|
+
...(failure.output === undefined ? {} : { output: failure.output }),
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
return {
|
|
660
|
+
playbookId,
|
|
661
|
+
status: failure?.status === 'aborted' ? 'aborted' : 'error',
|
|
662
|
+
error: authoredChildError(event, playbookId),
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
function insufficientChildResult(
|
|
667
|
+
event: unknown,
|
|
668
|
+
playbookId: PrChildPlaybookId,
|
|
669
|
+
): CompletedChildResult {
|
|
670
|
+
const output = childOutputOf(event);
|
|
671
|
+
return {
|
|
672
|
+
playbookId,
|
|
673
|
+
status: 'ok',
|
|
674
|
+
...(output === undefined ? {} : { output: output as JsonValue }),
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
function quotedRelay(values: readonly (readonly [string, string])[]): string {
|
|
679
|
+
return values
|
|
680
|
+
.filter(([, value]) => value.length > 0)
|
|
681
|
+
.map(([label, value]) =>
|
|
682
|
+
`${label}: ${value}`
|
|
683
|
+
.split('\n')
|
|
684
|
+
.map((line) => `> ${line}`)
|
|
685
|
+
.join('\n'),
|
|
686
|
+
)
|
|
687
|
+
.join('\n');
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/** PR-3's composed child input: the caller input, the pull request, and the static coding request. */
|
|
691
|
+
export function fixChecksCallText(context: PrContext): string {
|
|
692
|
+
return quotedRelay([
|
|
693
|
+
['Original request', context.callerInput ?? ''],
|
|
694
|
+
['Pull request', context.pullRequestUrl ?? ''],
|
|
695
|
+
['Coding request', FIX_CODING_REQUEST],
|
|
696
|
+
]);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
function pullRequestIdentity(context: PrContext): {
|
|
700
|
+
readonly pullRequest: string;
|
|
701
|
+
readonly pullRequestUrl: string;
|
|
702
|
+
} {
|
|
703
|
+
if (
|
|
704
|
+
!isNonEmptyString(context.pullRequest) ||
|
|
705
|
+
!isNonEmptyString(context.pullRequestUrl)
|
|
706
|
+
) {
|
|
707
|
+
throw new Error('PR reached a final state without its pull request identity');
|
|
708
|
+
}
|
|
709
|
+
return {
|
|
710
|
+
pullRequest: context.pullRequest,
|
|
711
|
+
pullRequestUrl: context.pullRequestUrl,
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
const machineSetup = setup({
|
|
716
|
+
types: {} as {
|
|
717
|
+
context: PrContext;
|
|
718
|
+
events: PrEvent;
|
|
719
|
+
input: PrInput;
|
|
720
|
+
output: PrPlaybookOutput;
|
|
721
|
+
},
|
|
722
|
+
actors: {
|
|
723
|
+
player: fromPromise<PlayerOutput, PlayerInput>(async () => {
|
|
724
|
+
throw new Error('player actor must be provided by the runner');
|
|
725
|
+
}),
|
|
726
|
+
playbook: fromPromise<JsonValue | undefined, PlaybookInput>(async () => {
|
|
727
|
+
throw new Error('playbook actor must be provided by the runner');
|
|
728
|
+
}),
|
|
729
|
+
script: fromPromise<ScriptOutput, ScriptInput>(async () => {
|
|
730
|
+
throw new Error('script actor must be provided by the runner');
|
|
731
|
+
}),
|
|
732
|
+
},
|
|
733
|
+
guards: {
|
|
734
|
+
isOpened,
|
|
735
|
+
isNotPublished,
|
|
736
|
+
needsBossReply,
|
|
737
|
+
checksPassed: zeroExitGuard('checksPassed'),
|
|
738
|
+
checksFailed: nonzeroExitGuard('checksFailed'),
|
|
739
|
+
isCodeSuccess,
|
|
740
|
+
authoredCodeFailure: authoredChildFailureGuard('code'),
|
|
741
|
+
fixPublished: zeroExitGuard('fixPublished'),
|
|
742
|
+
fixNotPublished: nonzeroExitGuard('fixNotPublished'),
|
|
743
|
+
checksStillFailing: nonzeroExitGuard('checksStillFailing'),
|
|
744
|
+
merged: zeroExitGuard('merged'),
|
|
745
|
+
mergeRefused: nonzeroExitGuard('mergeRefused'),
|
|
746
|
+
localDefaultUpdated: zeroExitGuard('localDefaultUpdated'),
|
|
747
|
+
localDefaultNotUpdated: nonzeroExitGuard('localDefaultNotUpdated'),
|
|
748
|
+
emptyBossReply: ({ event }) =>
|
|
749
|
+
event.type === 'BOSS_REPLY' && event.answer.trim().length === 0,
|
|
750
|
+
resumesOpenPullRequest: ({ context, event }) =>
|
|
751
|
+
event.type === 'BOSS_REPLY' &&
|
|
752
|
+
event.answer.trim().length > 0 &&
|
|
753
|
+
context.pendingBossQuestion?.resumeStateId === 'openPullRequest' &&
|
|
754
|
+
(event.questionId === undefined ||
|
|
755
|
+
event.questionId === 'openPullRequest'),
|
|
756
|
+
},
|
|
757
|
+
actions: {
|
|
758
|
+
'playbook.acceptedOutcome': (
|
|
759
|
+
_args,
|
|
760
|
+
_params: {
|
|
761
|
+
readonly source: string;
|
|
762
|
+
readonly target: string;
|
|
763
|
+
readonly acceptedOutcome: string;
|
|
764
|
+
},
|
|
765
|
+
) => undefined,
|
|
766
|
+
startPr: assign(({ event }) => {
|
|
767
|
+
if (event.type !== 'START_PR') return {};
|
|
768
|
+
return {
|
|
769
|
+
callerInput: event.callerInput,
|
|
770
|
+
pullRequest: undefined,
|
|
771
|
+
pullRequestUrl: undefined,
|
|
772
|
+
completion: undefined,
|
|
773
|
+
localDefaultUpdated: undefined,
|
|
774
|
+
coderOutput: undefined,
|
|
775
|
+
childFailure: undefined,
|
|
776
|
+
lastError: undefined,
|
|
777
|
+
pendingBossQuestion: undefined,
|
|
778
|
+
bossReply: undefined,
|
|
779
|
+
};
|
|
780
|
+
}),
|
|
781
|
+
rememberPullRequest: assign(({ event }) => ({
|
|
782
|
+
pullRequest: outputString(event, 'pullRequest'),
|
|
783
|
+
pullRequestUrl: outputString(event, 'pullRequestUrl'),
|
|
784
|
+
pendingBossQuestion: undefined,
|
|
785
|
+
bossReply: undefined,
|
|
786
|
+
})),
|
|
787
|
+
completeNotPublished: assign(({ event }) => ({
|
|
788
|
+
completion: 'not-published' as const,
|
|
789
|
+
coderOutput: outputString(event, 'coderOutput'),
|
|
790
|
+
pendingBossQuestion: undefined,
|
|
791
|
+
bossReply: undefined,
|
|
792
|
+
})),
|
|
793
|
+
rememberPendingQuestion: assign(({ event }) => {
|
|
794
|
+
const question = outputString(event, 'question');
|
|
795
|
+
if (question === undefined || !outputGuard(event, 'needsBossReply')) {
|
|
796
|
+
return { lastError: new Error('Coder question output was malformed') };
|
|
797
|
+
}
|
|
798
|
+
return {
|
|
799
|
+
pendingBossQuestion: {
|
|
800
|
+
questionId: 'openPullRequest' as const,
|
|
801
|
+
resumeStateId: 'openPullRequest' as const,
|
|
802
|
+
sourceItem: 'PR-1' as const,
|
|
803
|
+
asker: { kind: 'role' as const, roleId: 'coder' as const },
|
|
804
|
+
question,
|
|
805
|
+
},
|
|
806
|
+
bossReply: undefined,
|
|
807
|
+
};
|
|
808
|
+
}),
|
|
809
|
+
rememberBossReply: assign(({ event }) =>
|
|
810
|
+
event.type === 'BOSS_REPLY' ? { bossReply: event.answer } : {},
|
|
811
|
+
),
|
|
812
|
+
rememberEmptyBossReplyError: assign({
|
|
813
|
+
lastError: () => new Error('BOSS_REPLY received an empty answer'),
|
|
814
|
+
pendingBossQuestion: undefined,
|
|
815
|
+
bossReply: undefined,
|
|
816
|
+
}),
|
|
817
|
+
completeWithInsufficientCodeResult: assign(({ event }) => ({
|
|
818
|
+
completion: 'fix-failed' as const,
|
|
819
|
+
childFailure: insufficientChildResult(event, 'code'),
|
|
820
|
+
})),
|
|
821
|
+
completeWithCodeFailure: assign(({ event }) => ({
|
|
822
|
+
completion: 'fix-failed' as const,
|
|
823
|
+
childFailure: relayedChildFailure(event, 'code'),
|
|
824
|
+
})),
|
|
825
|
+
completeFixNotPublished: assign({
|
|
826
|
+
completion: 'fix-not-published' as const,
|
|
827
|
+
}),
|
|
828
|
+
completeChecksStillFailing: assign({
|
|
829
|
+
completion: 'checks-failed' as const,
|
|
830
|
+
}),
|
|
831
|
+
completeMergeRefused: assign({
|
|
832
|
+
completion: 'merge-refused' as const,
|
|
833
|
+
}),
|
|
834
|
+
completeMerged: assign({
|
|
835
|
+
completion: 'merged' as const,
|
|
836
|
+
localDefaultUpdated: true,
|
|
837
|
+
}),
|
|
838
|
+
completeMergedLocalBehind: assign({
|
|
839
|
+
completion: 'merged' as const,
|
|
840
|
+
localDefaultUpdated: false,
|
|
841
|
+
}),
|
|
842
|
+
rememberActorError: assign(({ event }) => ({
|
|
843
|
+
lastError: (event as unknown as { readonly error?: unknown }).error,
|
|
844
|
+
})),
|
|
845
|
+
rememberMalformedPlayerOutput: assign({
|
|
846
|
+
lastError: () =>
|
|
847
|
+
new Error('Coder result for PR-1 did not match an available outcome.'),
|
|
848
|
+
}),
|
|
849
|
+
rememberMalformedScriptOutput: assign(
|
|
850
|
+
(_args, params: { readonly sourceItem: PrScriptSourceItem }) => ({
|
|
851
|
+
lastError: new Error(
|
|
852
|
+
`Script result for ${params.sourceItem} did not match a declared outcome.`,
|
|
853
|
+
),
|
|
854
|
+
}),
|
|
855
|
+
),
|
|
856
|
+
},
|
|
857
|
+
});
|
|
858
|
+
|
|
859
|
+
export const prMachine = machineSetup.createMachine({
|
|
860
|
+
id: 'pr',
|
|
861
|
+
initial: 'ready',
|
|
862
|
+
context: () => ({}),
|
|
863
|
+
output: ({ context }): PrPlaybookOutput => {
|
|
864
|
+
if (context.completion === 'merged') {
|
|
865
|
+
return {
|
|
866
|
+
status: 'merged',
|
|
867
|
+
...pullRequestIdentity(context),
|
|
868
|
+
localDefaultUpdated: context.localDefaultUpdated === true,
|
|
869
|
+
};
|
|
870
|
+
}
|
|
871
|
+
if (context.completion === 'not-published') {
|
|
872
|
+
if (!isNonEmptyString(context.coderOutput)) {
|
|
873
|
+
throw new Error("PR reached notPublished without Coder's result");
|
|
874
|
+
}
|
|
875
|
+
return {
|
|
876
|
+
status: 'not-merged',
|
|
877
|
+
reason: 'not-published',
|
|
878
|
+
coderOutput: context.coderOutput,
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
if (context.completion === 'fix-failed') {
|
|
882
|
+
if (context.childFailure === undefined) {
|
|
883
|
+
throw new Error('PR reached fixFailed without a child result');
|
|
884
|
+
}
|
|
885
|
+
return {
|
|
886
|
+
status: 'not-merged',
|
|
887
|
+
reason: 'fix-failed',
|
|
888
|
+
...pullRequestIdentity(context),
|
|
889
|
+
childResult: context.childFailure,
|
|
890
|
+
};
|
|
891
|
+
}
|
|
892
|
+
if (
|
|
893
|
+
context.completion === 'fix-not-published' ||
|
|
894
|
+
context.completion === 'checks-failed' ||
|
|
895
|
+
context.completion === 'merge-refused'
|
|
896
|
+
) {
|
|
897
|
+
return {
|
|
898
|
+
status: 'not-merged',
|
|
899
|
+
reason: context.completion,
|
|
900
|
+
...pullRequestIdentity(context),
|
|
901
|
+
};
|
|
902
|
+
}
|
|
903
|
+
throw new Error('PR reached a final state without a recorded completion');
|
|
904
|
+
},
|
|
905
|
+
states: {
|
|
906
|
+
ready: {
|
|
907
|
+
id: 'ready',
|
|
908
|
+
description: STATE_DESCRIPTIONS.ready,
|
|
909
|
+
meta: playbookMeta('ready'),
|
|
910
|
+
tags: ['playbook.parked'],
|
|
911
|
+
on: {
|
|
912
|
+
START_PR: { target: 'openPullRequest', actions: 'startPr' },
|
|
913
|
+
},
|
|
914
|
+
},
|
|
915
|
+
openPullRequest: {
|
|
916
|
+
id: 'openPullRequest',
|
|
917
|
+
description: STATE_DESCRIPTIONS.openPullRequest,
|
|
918
|
+
meta: playbookMeta('openPullRequest', 'coder'),
|
|
919
|
+
tags: ['playbook.busy'],
|
|
920
|
+
invoke: {
|
|
921
|
+
src: 'player',
|
|
922
|
+
input: ({ context }): PlayerInput => ({
|
|
923
|
+
stateId: 'openPullRequest',
|
|
924
|
+
role: 'coder',
|
|
925
|
+
sourceItem: 'PR-1',
|
|
926
|
+
prompt: OPEN_PULL_REQUEST_PROMPT,
|
|
927
|
+
result: OPEN_PULL_REQUEST_RESULTS,
|
|
928
|
+
callerInput: context.callerInput ?? '',
|
|
929
|
+
...(context.pendingBossQuestion === undefined
|
|
930
|
+
? {}
|
|
931
|
+
: { pendingBossQuestion: context.pendingBossQuestion }),
|
|
932
|
+
...(context.bossReply === undefined
|
|
933
|
+
? {}
|
|
934
|
+
: { bossReply: context.bossReply }),
|
|
935
|
+
}),
|
|
936
|
+
onDone: [
|
|
937
|
+
{
|
|
938
|
+
guard: 'isOpened',
|
|
939
|
+
target: 'waitForChecks',
|
|
940
|
+
actions: [
|
|
941
|
+
{
|
|
942
|
+
type: 'playbook.acceptedOutcome',
|
|
943
|
+
params: {
|
|
944
|
+
source: 'openPullRequest',
|
|
945
|
+
target: 'waitForChecks',
|
|
946
|
+
acceptedOutcome: 'opened',
|
|
947
|
+
},
|
|
948
|
+
},
|
|
949
|
+
'rememberPullRequest',
|
|
950
|
+
],
|
|
951
|
+
},
|
|
952
|
+
{
|
|
953
|
+
guard: 'isNotPublished',
|
|
954
|
+
target: 'notPublished',
|
|
955
|
+
actions: [
|
|
956
|
+
{
|
|
957
|
+
type: 'playbook.acceptedOutcome',
|
|
958
|
+
params: {
|
|
959
|
+
source: 'openPullRequest',
|
|
960
|
+
target: 'notPublished',
|
|
961
|
+
acceptedOutcome: 'notPublished',
|
|
962
|
+
},
|
|
963
|
+
},
|
|
964
|
+
'completeNotPublished',
|
|
965
|
+
],
|
|
966
|
+
},
|
|
967
|
+
{
|
|
968
|
+
guard: 'needsBossReply',
|
|
969
|
+
target: 'awaitBossReply',
|
|
970
|
+
actions: [
|
|
971
|
+
{
|
|
972
|
+
type: 'playbook.acceptedOutcome',
|
|
973
|
+
params: {
|
|
974
|
+
source: 'openPullRequest',
|
|
975
|
+
target: 'awaitBossReply',
|
|
976
|
+
acceptedOutcome: 'needsBossReply',
|
|
977
|
+
},
|
|
978
|
+
},
|
|
979
|
+
'rememberPendingQuestion',
|
|
980
|
+
],
|
|
981
|
+
},
|
|
982
|
+
{
|
|
983
|
+
target: 'failed',
|
|
984
|
+
actions: 'rememberMalformedPlayerOutput',
|
|
985
|
+
},
|
|
986
|
+
],
|
|
987
|
+
onError: { target: 'failed', actions: 'rememberActorError' },
|
|
988
|
+
},
|
|
989
|
+
},
|
|
990
|
+
waitForChecks: {
|
|
991
|
+
id: 'waitForChecks',
|
|
992
|
+
description: STATE_DESCRIPTIONS.waitForChecks,
|
|
993
|
+
meta: playbookMeta('waitForChecks'),
|
|
994
|
+
tags: ['playbook.busy'],
|
|
995
|
+
invoke: {
|
|
996
|
+
src: 'script',
|
|
997
|
+
input: (): ScriptInput => ({
|
|
998
|
+
stateId: 'waitForChecks',
|
|
999
|
+
sourceItem: 'PR-2',
|
|
1000
|
+
command: WAIT_FOR_CHECKS_COMMAND,
|
|
1001
|
+
result: WAIT_FOR_CHECKS_RESULTS,
|
|
1002
|
+
}),
|
|
1003
|
+
onDone: [
|
|
1004
|
+
{ guard: 'checksPassed', target: 'mergePullRequest' },
|
|
1005
|
+
{ guard: 'checksFailed', target: 'fixChecks' },
|
|
1006
|
+
{
|
|
1007
|
+
target: 'failed',
|
|
1008
|
+
actions: {
|
|
1009
|
+
type: 'rememberMalformedScriptOutput',
|
|
1010
|
+
params: { sourceItem: 'PR-2' },
|
|
1011
|
+
},
|
|
1012
|
+
},
|
|
1013
|
+
],
|
|
1014
|
+
onError: { target: 'failed', actions: 'rememberActorError' },
|
|
1015
|
+
},
|
|
1016
|
+
},
|
|
1017
|
+
fixChecks: {
|
|
1018
|
+
id: 'fixChecks',
|
|
1019
|
+
description: STATE_DESCRIPTIONS.fixChecks,
|
|
1020
|
+
meta: playbookMeta('fixChecks'),
|
|
1021
|
+
tags: ['playbook.suspended'],
|
|
1022
|
+
invoke: {
|
|
1023
|
+
src: 'playbook',
|
|
1024
|
+
input: ({ context }): PlaybookInput => ({
|
|
1025
|
+
stateId: 'fixChecks',
|
|
1026
|
+
sourceItem: 'PR-3',
|
|
1027
|
+
playbookId: 'code',
|
|
1028
|
+
text: fixChecksCallText(context),
|
|
1029
|
+
}),
|
|
1030
|
+
onDone: [
|
|
1031
|
+
{ guard: 'isCodeSuccess', target: 'publishFix' },
|
|
1032
|
+
{
|
|
1033
|
+
target: 'fixFailed',
|
|
1034
|
+
actions: 'completeWithInsufficientCodeResult',
|
|
1035
|
+
},
|
|
1036
|
+
],
|
|
1037
|
+
onError: [
|
|
1038
|
+
{
|
|
1039
|
+
guard: 'authoredCodeFailure',
|
|
1040
|
+
target: 'fixFailed',
|
|
1041
|
+
actions: 'completeWithCodeFailure',
|
|
1042
|
+
},
|
|
1043
|
+
{ target: 'failed', actions: 'rememberActorError' },
|
|
1044
|
+
],
|
|
1045
|
+
},
|
|
1046
|
+
},
|
|
1047
|
+
publishFix: {
|
|
1048
|
+
id: 'publishFix',
|
|
1049
|
+
description: STATE_DESCRIPTIONS.publishFix,
|
|
1050
|
+
meta: playbookMeta('publishFix'),
|
|
1051
|
+
tags: ['playbook.busy'],
|
|
1052
|
+
invoke: {
|
|
1053
|
+
src: 'script',
|
|
1054
|
+
input: (): ScriptInput => ({
|
|
1055
|
+
stateId: 'publishFix',
|
|
1056
|
+
sourceItem: 'PR-4',
|
|
1057
|
+
command: PUBLISH_FIX_COMMAND,
|
|
1058
|
+
result: PUBLISH_FIX_RESULTS,
|
|
1059
|
+
}),
|
|
1060
|
+
onDone: [
|
|
1061
|
+
{ guard: 'fixPublished', target: 'waitForChecksAfterFix' },
|
|
1062
|
+
{
|
|
1063
|
+
guard: 'fixNotPublished',
|
|
1064
|
+
target: 'fixNotPublished',
|
|
1065
|
+
actions: 'completeFixNotPublished',
|
|
1066
|
+
},
|
|
1067
|
+
{
|
|
1068
|
+
target: 'failed',
|
|
1069
|
+
actions: {
|
|
1070
|
+
type: 'rememberMalformedScriptOutput',
|
|
1071
|
+
params: { sourceItem: 'PR-4' },
|
|
1072
|
+
},
|
|
1073
|
+
},
|
|
1074
|
+
],
|
|
1075
|
+
onError: { target: 'failed', actions: 'rememberActorError' },
|
|
1076
|
+
},
|
|
1077
|
+
},
|
|
1078
|
+
waitForChecksAfterFix: {
|
|
1079
|
+
id: 'waitForChecksAfterFix',
|
|
1080
|
+
description: STATE_DESCRIPTIONS.waitForChecksAfterFix,
|
|
1081
|
+
meta: playbookMeta('waitForChecksAfterFix'),
|
|
1082
|
+
tags: ['playbook.busy'],
|
|
1083
|
+
invoke: {
|
|
1084
|
+
src: 'script',
|
|
1085
|
+
input: (): ScriptInput => ({
|
|
1086
|
+
stateId: 'waitForChecksAfterFix',
|
|
1087
|
+
sourceItem: 'PR-5',
|
|
1088
|
+
command: WAIT_FOR_CHECKS_COMMAND,
|
|
1089
|
+
result: WAIT_FOR_CHECKS_AFTER_FIX_RESULTS,
|
|
1090
|
+
}),
|
|
1091
|
+
onDone: [
|
|
1092
|
+
{ guard: 'checksPassed', target: 'mergePullRequest' },
|
|
1093
|
+
{
|
|
1094
|
+
guard: 'checksStillFailing',
|
|
1095
|
+
target: 'checksStillFailing',
|
|
1096
|
+
actions: 'completeChecksStillFailing',
|
|
1097
|
+
},
|
|
1098
|
+
{
|
|
1099
|
+
target: 'failed',
|
|
1100
|
+
actions: {
|
|
1101
|
+
type: 'rememberMalformedScriptOutput',
|
|
1102
|
+
params: { sourceItem: 'PR-5' },
|
|
1103
|
+
},
|
|
1104
|
+
},
|
|
1105
|
+
],
|
|
1106
|
+
onError: { target: 'failed', actions: 'rememberActorError' },
|
|
1107
|
+
},
|
|
1108
|
+
},
|
|
1109
|
+
mergePullRequest: {
|
|
1110
|
+
id: 'mergePullRequest',
|
|
1111
|
+
description: STATE_DESCRIPTIONS.mergePullRequest,
|
|
1112
|
+
meta: playbookMeta('mergePullRequest'),
|
|
1113
|
+
tags: ['playbook.busy'],
|
|
1114
|
+
invoke: {
|
|
1115
|
+
src: 'script',
|
|
1116
|
+
input: (): ScriptInput => ({
|
|
1117
|
+
stateId: 'mergePullRequest',
|
|
1118
|
+
sourceItem: 'PR-6',
|
|
1119
|
+
command: MERGE_PULL_REQUEST_COMMAND,
|
|
1120
|
+
result: MERGE_PULL_REQUEST_RESULTS,
|
|
1121
|
+
}),
|
|
1122
|
+
onDone: [
|
|
1123
|
+
{ guard: 'merged', target: 'updateLocalDefault' },
|
|
1124
|
+
{
|
|
1125
|
+
guard: 'mergeRefused',
|
|
1126
|
+
target: 'mergeRefused',
|
|
1127
|
+
actions: 'completeMergeRefused',
|
|
1128
|
+
},
|
|
1129
|
+
{
|
|
1130
|
+
target: 'failed',
|
|
1131
|
+
actions: {
|
|
1132
|
+
type: 'rememberMalformedScriptOutput',
|
|
1133
|
+
params: { sourceItem: 'PR-6' },
|
|
1134
|
+
},
|
|
1135
|
+
},
|
|
1136
|
+
],
|
|
1137
|
+
onError: { target: 'failed', actions: 'rememberActorError' },
|
|
1138
|
+
},
|
|
1139
|
+
},
|
|
1140
|
+
updateLocalDefault: {
|
|
1141
|
+
id: 'updateLocalDefault',
|
|
1142
|
+
description: STATE_DESCRIPTIONS.updateLocalDefault,
|
|
1143
|
+
meta: playbookMeta('updateLocalDefault'),
|
|
1144
|
+
tags: ['playbook.busy'],
|
|
1145
|
+
invoke: {
|
|
1146
|
+
src: 'script',
|
|
1147
|
+
input: (): ScriptInput => ({
|
|
1148
|
+
stateId: 'updateLocalDefault',
|
|
1149
|
+
sourceItem: 'PR-7',
|
|
1150
|
+
command: UPDATE_LOCAL_DEFAULT_COMMAND,
|
|
1151
|
+
result: UPDATE_LOCAL_DEFAULT_RESULTS,
|
|
1152
|
+
}),
|
|
1153
|
+
onDone: [
|
|
1154
|
+
{
|
|
1155
|
+
guard: 'localDefaultUpdated',
|
|
1156
|
+
target: 'merged',
|
|
1157
|
+
actions: 'completeMerged',
|
|
1158
|
+
},
|
|
1159
|
+
{
|
|
1160
|
+
guard: 'localDefaultNotUpdated',
|
|
1161
|
+
target: 'mergedLocalBehind',
|
|
1162
|
+
actions: 'completeMergedLocalBehind',
|
|
1163
|
+
},
|
|
1164
|
+
{
|
|
1165
|
+
target: 'failed',
|
|
1166
|
+
actions: {
|
|
1167
|
+
type: 'rememberMalformedScriptOutput',
|
|
1168
|
+
params: { sourceItem: 'PR-7' },
|
|
1169
|
+
},
|
|
1170
|
+
},
|
|
1171
|
+
],
|
|
1172
|
+
onError: { target: 'failed', actions: 'rememberActorError' },
|
|
1173
|
+
},
|
|
1174
|
+
},
|
|
1175
|
+
awaitBossReply: {
|
|
1176
|
+
id: 'awaitBossReply',
|
|
1177
|
+
description: STATE_DESCRIPTIONS.awaitBossReply,
|
|
1178
|
+
meta: playbookMeta('awaitBossReply'),
|
|
1179
|
+
tags: ['playbook.parked'],
|
|
1180
|
+
on: {
|
|
1181
|
+
BOSS_REPLY: [
|
|
1182
|
+
{
|
|
1183
|
+
guard: 'emptyBossReply',
|
|
1184
|
+
target: '#failed',
|
|
1185
|
+
actions: 'rememberEmptyBossReplyError',
|
|
1186
|
+
},
|
|
1187
|
+
{
|
|
1188
|
+
guard: 'resumesOpenPullRequest',
|
|
1189
|
+
target: '#openPullRequest',
|
|
1190
|
+
reenter: true,
|
|
1191
|
+
actions: 'rememberBossReply',
|
|
1192
|
+
},
|
|
1193
|
+
],
|
|
1194
|
+
START_PR: { target: 'openPullRequest', actions: 'startPr' },
|
|
1195
|
+
},
|
|
1196
|
+
},
|
|
1197
|
+
failed: {
|
|
1198
|
+
id: 'failed',
|
|
1199
|
+
description: STATE_DESCRIPTIONS.failed,
|
|
1200
|
+
meta: playbookMeta('failed'),
|
|
1201
|
+
tags: ['playbook.parked'],
|
|
1202
|
+
on: {
|
|
1203
|
+
START_PR: { target: 'openPullRequest', actions: 'startPr' },
|
|
1204
|
+
},
|
|
1205
|
+
},
|
|
1206
|
+
merged: {
|
|
1207
|
+
id: 'merged',
|
|
1208
|
+
description: STATE_DESCRIPTIONS.merged,
|
|
1209
|
+
meta: terminalMeta('merged', 'success'),
|
|
1210
|
+
type: 'final',
|
|
1211
|
+
},
|
|
1212
|
+
mergedLocalBehind: {
|
|
1213
|
+
id: 'mergedLocalBehind',
|
|
1214
|
+
description: STATE_DESCRIPTIONS.mergedLocalBehind,
|
|
1215
|
+
meta: terminalMeta('mergedLocalBehind', 'success'),
|
|
1216
|
+
type: 'final',
|
|
1217
|
+
},
|
|
1218
|
+
notPublished: {
|
|
1219
|
+
id: 'notPublished',
|
|
1220
|
+
description: STATE_DESCRIPTIONS.notPublished,
|
|
1221
|
+
meta: terminalMeta('notPublished', 'failure'),
|
|
1222
|
+
type: 'final',
|
|
1223
|
+
},
|
|
1224
|
+
fixFailed: {
|
|
1225
|
+
id: 'fixFailed',
|
|
1226
|
+
description: STATE_DESCRIPTIONS.fixFailed,
|
|
1227
|
+
meta: terminalMeta('fixFailed', 'failure'),
|
|
1228
|
+
type: 'final',
|
|
1229
|
+
},
|
|
1230
|
+
fixNotPublished: {
|
|
1231
|
+
id: 'fixNotPublished',
|
|
1232
|
+
description: STATE_DESCRIPTIONS.fixNotPublished,
|
|
1233
|
+
meta: terminalMeta('fixNotPublished', 'failure'),
|
|
1234
|
+
type: 'final',
|
|
1235
|
+
},
|
|
1236
|
+
checksStillFailing: {
|
|
1237
|
+
id: 'checksStillFailing',
|
|
1238
|
+
description: STATE_DESCRIPTIONS.checksStillFailing,
|
|
1239
|
+
meta: terminalMeta('checksStillFailing', 'failure'),
|
|
1240
|
+
type: 'final',
|
|
1241
|
+
},
|
|
1242
|
+
mergeRefused: {
|
|
1243
|
+
id: 'mergeRefused',
|
|
1244
|
+
description: STATE_DESCRIPTIONS.mergeRefused,
|
|
1245
|
+
meta: terminalMeta('mergeRefused', 'failure'),
|
|
1246
|
+
type: 'final',
|
|
1247
|
+
},
|
|
1248
|
+
},
|
|
1249
|
+
});
|
|
1250
|
+
|
|
1251
|
+
export default prMachine;
|