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