@sublang/playbook 15.0.0 → 16.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.
Files changed (81) hide show
  1. package/README.md +1 -1
  2. package/docs/embedding.md +9 -7
  3. package/package.json +3 -1
  4. package/reference/sdlc/branch.playbook/branch.fsm.d.ts +63 -33
  5. package/reference/sdlc/branch.playbook/branch.fsm.js +190 -108
  6. package/reference/sdlc/branch.playbook/branch.fsm.ts +269 -147
  7. package/reference/sdlc/branch.playbook/branch.gears.md +7 -17
  8. package/reference/sdlc/branch.playbook/branch.playbook.d.ts +12 -16
  9. package/reference/sdlc/branch.playbook/branch.playbook.js +112 -101
  10. package/reference/sdlc/branch.playbook/branch.playbook.ts +128 -171
  11. package/reference/sdlc/branch.playbook/branch.registry.d.ts +3 -2
  12. package/reference/sdlc/branch.playbook/branch.registry.ts +6 -2
  13. package/reference/sdlc/code.playbook/bin/repository-effects.js +54 -13
  14. package/reference/sdlc/code.playbook/code.fsm.d.ts +143 -90
  15. package/reference/sdlc/code.playbook/code.fsm.introspect.d.ts +8 -8
  16. package/reference/sdlc/code.playbook/code.fsm.introspect.ts +13 -13
  17. package/reference/sdlc/code.playbook/code.fsm.js +609 -649
  18. package/reference/sdlc/code.playbook/code.fsm.ts +890 -821
  19. package/reference/sdlc/code.playbook/code.gears.md +39 -29
  20. package/reference/sdlc/code.playbook/code.playbook.d.ts +13 -16
  21. package/reference/sdlc/code.playbook/code.playbook.js +166 -147
  22. package/reference/sdlc/code.playbook/code.playbook.ts +180 -229
  23. package/reference/sdlc/code.playbook/code.registry.d.ts +3 -2
  24. package/reference/sdlc/code.playbook/code.registry.ts +6 -2
  25. package/reference/sdlc/code.playbook/playbook-captain.d.ts +42 -0
  26. package/reference/sdlc/code.playbook/playbook-captain.js +829 -194
  27. package/reference/sdlc/code.playbook/playbook-captain.ts +1227 -220
  28. package/reference/sdlc/decide.md +1 -0
  29. package/reference/sdlc/decide.playbook/decide.fsm.d.ts +270 -154
  30. package/reference/sdlc/decide.playbook/decide.fsm.js +765 -732
  31. package/reference/sdlc/decide.playbook/decide.fsm.ts +1167 -945
  32. package/reference/sdlc/decide.playbook/decide.gears.md +28 -21
  33. package/reference/sdlc/decide.playbook/decide.playbook.d.ts +11 -67
  34. package/reference/sdlc/decide.playbook/decide.playbook.js +155 -3570
  35. package/reference/sdlc/decide.playbook/decide.playbook.ts +186 -5061
  36. package/reference/sdlc/decide.playbook/decide.registry.d.ts +8 -4
  37. package/reference/sdlc/decide.playbook/decide.registry.js +2 -2
  38. package/reference/sdlc/decide.playbook/decide.registry.ts +13 -6
  39. package/reference/sdlc/dev.playbook/dev.fsm.d.ts +113 -131
  40. package/reference/sdlc/dev.playbook/dev.fsm.js +517 -575
  41. package/reference/sdlc/dev.playbook/dev.fsm.ts +667 -787
  42. package/reference/sdlc/dev.playbook/dev.gears.md +52 -54
  43. package/reference/sdlc/dev.playbook/dev.playbook.d.ts +13 -10
  44. package/reference/sdlc/dev.playbook/dev.playbook.js +128 -120
  45. package/reference/sdlc/dev.playbook/dev.playbook.ts +143 -193
  46. package/reference/sdlc/dev.playbook/dev.registry.d.ts +3 -2
  47. package/reference/sdlc/dev.playbook/dev.registry.ts +6 -2
  48. package/reference/sdlc/pr.playbook/pr.fsm.d.ts +92 -85
  49. package/reference/sdlc/pr.playbook/pr.fsm.js +461 -518
  50. package/reference/sdlc/pr.playbook/pr.fsm.ts +533 -663
  51. package/reference/sdlc/pr.playbook/pr.gears.md +47 -63
  52. package/reference/sdlc/pr.playbook/pr.playbook.d.ts +13 -23
  53. package/reference/sdlc/pr.playbook/pr.playbook.js +118 -112
  54. package/reference/sdlc/pr.playbook/pr.playbook.ts +136 -190
  55. package/reference/sdlc/pr.playbook/pr.registry.d.ts +3 -2
  56. package/reference/sdlc/pr.playbook/pr.registry.ts +4 -2
  57. package/reference/sdlc/review.md +3 -3
  58. package/reference/sdlc/review.playbook/review.fsm.d.ts +150 -116
  59. package/reference/sdlc/review.playbook/review.fsm.js +479 -550
  60. package/reference/sdlc/review.playbook/review.fsm.ts +717 -662
  61. package/reference/sdlc/review.playbook/review.gears.md +32 -29
  62. package/reference/sdlc/review.playbook/review.playbook.d.ts +12 -12
  63. package/reference/sdlc/review.playbook/review.playbook.js +180 -155
  64. package/reference/sdlc/review.playbook/review.playbook.ts +190 -230
  65. package/reference/sdlc/review.playbook/review.registry.d.ts +6 -5
  66. package/reference/sdlc/review.playbook/review.registry.js +3 -3
  67. package/reference/sdlc/review.playbook/review.registry.ts +9 -5
  68. package/slc/gears2fsm.md +72 -3
  69. package/slc/link.md +50 -27
  70. package/slc/materialize-link.mjs +51 -13
  71. package/slc/optimize.md +4 -1
  72. package/slc/prefix-prompts.mjs +191 -0
  73. package/slc/prefix.md +203 -0
  74. package/slc/slc.pin-inputs.json +11 -1
  75. package/slc/text2gears.md +71 -11
  76. package/src/xstate-playbook-runtime.d.ts +82 -9
  77. package/src/xstate-playbook-runtime.js +1224 -283
  78. package/src/xstate-playbook-runtime.ts +1671 -338
  79. package/src/xstate-runtime.d.ts +3 -1
  80. package/src/xstate-runtime.js +9 -2
  81. package/src/xstate-runtime.ts +12 -2
@@ -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 slc/gears2fsm.md from ./pr.gears.md.
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
- | 'waitForChecksAfterFix'
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
- 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';
36
+ /** Working leaves a Boss reply may resume (scalar Boss-reply form). */
37
+ export type ResumableStateId = 'openPullRequest';
42
38
 
43
- export type PrChildPlaybookId = 'code';
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 PlaybookInput = {
74
- readonly stateId: 'fixChecks';
75
- readonly sourceItem: 'PR-3';
76
- readonly playbookId: PrChildPlaybookId;
77
- readonly text: string;
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: PrScriptStateId;
82
- readonly sourceItem: PrScriptSourceItem;
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
- /** Sanitized canonical child result relayed as PR's own failure outcome. */
104
- export type CompletedChildResult =
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: PrChildPlaybookId;
124
+ readonly playbookId: 'code';
107
125
  readonly status: 'ok';
108
126
  readonly output?: JsonValue;
109
127
  }
110
128
  | {
111
- readonly playbookId: PrChildPlaybookId;
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
- /** The relayed canonical `code` result that ended the one fix attempt. */
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 childFailure?: CompletedChildResult;
176
- readonly lastError?: unknown;
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
- '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>`.',
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
- '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>`.',
209
- needsBossReply: NEEDS_BOSS_REPLY_RESULT,
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
- // The mechanical steps are static POSIX shell text (slc/optimize.md) with no
213
- // prose. `gh` infers the pull request from the checked-out branch, which the
214
- // run does not own, so the two steps that act on the pull request itself relay
215
- // one runtime value — the pull request PR-1 published — through the same
216
- // placeholder relay an acting prompt uses.
217
- // The placeholder occupies a single-quoted shell word, and binding replaces
218
- // that whole word — quotes included — with the value's own quoted literal.
219
- const QUOTED_PULL_REQUEST_URL = "'<pull-request-url>'";
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
- "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.",
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
- "The command exited with a nonzero status: the pull request's checks failed.",
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: the fix is pushed and the pull request's head is the pushed commit.",
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
- "The command exited with a nonzero status: the checkout no longer infers the published pull request, the push was rejected, or the pull request's head did not advance to the pushed commit.",
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 WAIT_FOR_CHECKS_AFTER_FIX_RESULTS = {
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
- "The command exited with a nonzero status: the pull request's checks are still failing after the one fix attempt.",
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 MERGE_PULL_REQUEST_RESULTS = {
290
- merged:
291
- 'The command exited with status zero: the pull request targeted the repository default branch and is merged into it with a merge commit, deletion of the remote and local branch was requested, and the local default branch is checked out.',
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 does not target the repository default branch, GitHub refused the merge, its merged state could not be confirmed, or the local switch to the default branch or the branch deletion failed.',
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 UPDATE_LOCAL_DEFAULT_RESULTS = {
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 the caller input that names the branch to deliver.',
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
- waitForChecks:
312
- "Waiting for the pull request's checks to settle before any fix attempt.",
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
- 'The CODE playbook is fixing the cause of the red checks on the checked-out branch.',
301
+ "The CODE playbook is making the one fix attempt for the pull request's failing checks.",
315
302
  publishFix:
316
- "Publishing the fix to the pull request and waiting for the pull request's head to reach the pushed commit.",
317
- waitForChecksAfterFix:
318
- "Waiting for the pull request's checks to settle after the one fix attempt.",
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 delivery workflow failed and is waiting for new caller input.',
326
- merged:
327
- 'The pull request is merged with a merge commit on the repository default branch, which is checked out and fast-forwarded to the merged head; the merge requested deletion of the remote and local branch.',
328
- mergedLocalBehind:
329
- 'The pull request is merged with a merge commit on the repository default branch, which is checked out but could not be fast-forwarded to the merged head; the merge requested deletion of the remote and local branch.',
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
- '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.',
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 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.",
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
- "The fix could not be published: the checked-out branch no longer carries the published pull request, the push was rejected, or the pull request's head did not advance to the pushed commit; the pull request remains open.",
336
- checksStillFailing:
337
- "The pull request's checks are still failing after the one fix attempt; the pull request remains open.",
338
- mergeRefused:
339
- 'The merge did not complete: the checked-out branch no longer carries the published pull request, the pull request does not target the repository default branch, GitHub refused the merge, or the merge may have landed while its confirmation, the local switch to the default branch, or the branch deletion failed; the pull request is in the state GitHub reports.',
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
- function playbookMeta<StateId extends keyof typeof STATE_DESCRIPTIONS>(
343
- stateId: StateId,
344
- role?: 'coder',
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
- // DR-048: a final state additionally declares whether its outcome means the
356
- // workflow succeeded or failed, so PR's own caller learns that from the
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
- outputGuard(event, 'opened') &&
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
- outputGuard(event, 'notPublished') &&
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
- outputGuard(event, 'needsBossReply') &&
507
+ playerGuard(event, 'needsBossReply') &&
415
508
  outputString(event, 'question') !== undefined
416
509
  );
417
510
  }
418
511
 
419
- // A script actor resolves mechanically from the exit status: the first
420
- // declared guard with status zero, the second with the nonzero status.
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 exitStatus = scriptExitStatus(event);
438
- return (
439
- outputGuard(event, guard) && exitStatus !== undefined && exitStatus !== 0
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
- // CODE's canonical successful terminal output discriminates completion with
449
- // `status: 'complete'`, carries the exact last CODE-owned commit, and
450
- // affirms that every phase's review passed.
451
- function isCodeSuccessValue(value: unknown): boolean {
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
- return isCodeSuccessValue(childOutputOf(event));
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
- Array.isArray(value) &&
479
- Reflect.ownKeys(value).length === value.length + 1 &&
480
- Reflect.ownKeys(value).every((key) => {
481
- if (key === 'length') return true;
482
- return (
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
- function isPlaybookState(value: unknown): boolean {
494
- if (!isRecord(value)) return false;
495
- const allowed = new Set([
496
- 'value',
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 nestedResultFromError(
551
+ export function authoredChildResult(
532
552
  error: unknown,
533
- ): Record<string, unknown> | undefined {
553
+ expectedPlaybookId: string,
554
+ ): PlaybookCallResult | undefined {
534
555
  if (!(error instanceof Error)) return undefined;
535
- const result = (error as Error & { readonly result?: unknown }).result;
536
- return isRecord(result) ? result : undefined;
537
- }
538
-
539
- type AuthoredChildFailure =
540
- | {
541
- readonly status: 'aborted' | 'error';
542
- readonly error?: CompactError;
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 authoredChildFailureGuard(
652
- playbookId: PrChildPlaybookId,
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 authoredChildError(
661
- event: unknown,
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 relayedChildFailure(
682
- event: unknown,
683
- playbookId: PrChildPlaybookId,
684
- ): CompletedChildResult {
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
- ...(failure.output === undefined ? {} : { output: failure.output }),
584
+ ...(result.output === undefined ? {} : { output: result.output }),
695
585
  };
696
586
  }
697
- return {
698
- playbookId,
699
- status: failure?.status === 'aborted' ? 'aborted' : 'error',
700
- error: authoredChildError(event, playbookId),
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 insufficientChildResult(
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 ? {} : { output: output as JsonValue }),
602
+ ...(output === undefined || !isJsonValue(output)
603
+ ? {}
604
+ : { output: output as JsonValue }),
713
605
  };
714
606
  }
715
607
 
716
- function quotedRelay(values: readonly (readonly [string, string])[]): string {
717
- return values
718
- .filter(([, value]) => value.length > 0)
719
- .map(([label, value]) =>
720
- `${label}: ${value}`
721
- .split('\n')
722
- .map((line) => `> ${line}`)
723
- .join('\n'),
724
- )
725
- .join('\n');
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<JsonValue | undefined, PlaybookInput>(async () => {
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
- resumesOpenPullRequest: ({ context, event }) =>
789
- event.type === 'BOSS_REPLY' &&
790
- event.answer.trim().length > 0 &&
791
- context.pendingBossQuestion?.resumeStateId === 'openPullRequest' &&
792
- (event.questionId === undefined ||
793
- event.questionId === 'openPullRequest'),
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
- childFailure: undefined,
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
- rememberPullRequest: assign(({ event }) => ({
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
- pendingBossQuestion: undefined,
829
- bossReply: undefined,
691
+ completion: 'not-published' as const,
830
692
  })),
831
- rememberPendingQuestion: assign(({ event }) => {
693
+ setPendingBossQuestion: assign(({ event }) => {
832
694
  const question = outputString(event, 'question');
833
- if (question === undefined || !outputGuard(event, 'needsBossReply')) {
834
- return { lastError: new Error('Coder question output was malformed') };
835
- }
836
- return {
837
- pendingBossQuestion: {
838
- questionId: 'openPullRequest' as const,
839
- resumeStateId: 'openPullRequest' as const,
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: () => new Error('BOSS_REPLY received an empty answer'),
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
- completeChecksStillFailing: assign({
867
- completion: 'checks-failed' as const,
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
- completeMerged: assign({
737
+ completeMergedLocalUpdated: assign({
873
738
  completion: 'merged' as const,
874
739
  localDefaultUpdated: true,
875
740
  }),
876
- completeMergedLocalBehind: assign({
741
+ completeMergedLocalNotUpdated: assign({
877
742
  completion: 'merged' as const,
878
743
  localDefaultUpdated: false,
879
744
  }),
880
745
  rememberActorError: assign(({ event }) => ({
881
- lastError: (event as unknown as { readonly error?: unknown }).error,
746
+ lastError: normalizeError(eventError(event)),
882
747
  })),
883
748
  rememberMalformedPlayerOutput: assign({
884
- lastError: () =>
885
- new Error('Coder result for PR-1 did not match an available outcome.'),
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
- (_args, params: { readonly sourceItem: PrScriptSourceItem }) => ({
889
- lastError: new Error(
890
- `Script result for ${params.sourceItem} did not match a declared outcome.`,
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
- if (context.completion === 'merged') {
903
- return {
904
- status: 'merged',
905
- ...pullRequestIdentity(context),
906
- localDefaultUpdated: context.localDefaultUpdated === true,
907
- };
908
- }
909
- if (context.completion === 'not-published') {
910
- if (!isNonEmptyString(context.coderOutput)) {
911
- throw new Error("PR reached notPublished without Coder's result");
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
- return {
914
- status: 'not-merged',
915
- reason: 'not-published',
916
- coderOutput: context.coderOutput,
917
- };
918
- }
919
- if (context.completion === 'fix-failed') {
920
- if (context.childFailure === undefined) {
921
- throw new Error('PR reached fixFailed without a child result');
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
- return {
924
- status: 'not-merged',
925
- reason: 'fix-failed',
926
- ...pullRequestIdentity(context),
927
- childResult: context.childFailure,
928
- };
929
- }
930
- if (context.completion === 'merge-unconfirmed') {
931
- return {
932
- status: 'merge-unconfirmed',
933
- ...pullRequestIdentity(context),
934
- };
935
- }
936
- if (
937
- context.completion === 'fix-not-published' ||
938
- context.completion === 'checks-failed'
939
- ) {
940
- return {
941
- status: 'not-merged',
942
- reason: context.completion,
943
- ...pullRequestIdentity(context),
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: { target: 'openPullRequest', actions: 'startPr' },
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
- 'rememberPullRequest',
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
- 'rememberPendingQuestion',
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
- target: 'failed',
1027
- actions: 'rememberMalformedPlayerOutput',
936
+ guard: 'emptyBossReply',
937
+ target: '#failed',
938
+ actions: 'rememberEmptyBossReplyError',
1028
939
  },
940
+ ...resumableStates(['openPullRequest']),
1029
941
  ],
1030
- onError: { target: 'failed', actions: 'rememberActorError' },
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: WAIT_FOR_CHECKS_COMMAND,
959
+ command: CHECK_WAIT_COMMAND,
1044
960
  result: WAIT_FOR_CHECKS_RESULTS,
1045
961
  }),
1046
962
  onDone: [
1047
- { guard: 'checksPassed', target: 'mergePullRequest' },
1048
- { guard: 'checksFailed', target: 'fixChecks' },
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: 'code',
1071
- text: fixChecksCallText(context),
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: 'authoredCodeFailure',
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: scriptCommand(PUBLISH_FIX_COMMAND, context),
1012
+ command: bindPullRequestUrl(PUBLISH_FIX_COMMAND_TEMPLATE, context),
1101
1013
  result: PUBLISH_FIX_RESULTS,
1102
1014
  }),
1103
1015
  onDone: [
1104
- { guard: 'fixPublished', target: 'waitForChecksAfterFix' },
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
- waitForChecksAfterFix: {
1122
- id: 'waitForChecksAfterFix',
1123
- description: STATE_DESCRIPTIONS.waitForChecksAfterFix,
1124
- meta: playbookMeta('waitForChecksAfterFix'),
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: 'waitForChecksAfterFix',
1035
+ stateId: 'waitForFixChecks',
1130
1036
  sourceItem: 'PR-5',
1131
- command: WAIT_FOR_CHECKS_COMMAND,
1132
- result: WAIT_FOR_CHECKS_AFTER_FIX_RESULTS,
1037
+ command: CHECK_WAIT_COMMAND,
1038
+ result: WAIT_FOR_FIX_CHECKS_RESULTS,
1133
1039
  }),
1134
1040
  onDone: [
1135
- { guard: 'checksPassed', target: 'mergePullRequest' },
1041
+ { guard: 'waitForFixChecksPassed', target: 'mergePullRequest' },
1136
1042
  {
1137
1043
  guard: 'checksStillFailing',
1138
- target: 'checksStillFailing',
1139
- actions: 'completeChecksStillFailing',
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: scriptCommand(MERGE_PULL_REQUEST_COMMAND, context),
1163
- result: MERGE_PULL_REQUEST_RESULTS,
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: 'mergeRefused',
1170
- actions: 'completeMergeRefused',
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: UPDATE_LOCAL_DEFAULT_COMMAND,
1194
- result: UPDATE_LOCAL_DEFAULT_RESULTS,
1087
+ command: LOCAL_UPDATE_COMMAND,
1088
+ result: LOCAL_UPDATE_RESULTS,
1195
1089
  }),
1196
1090
  onDone: [
1197
1091
  {
1198
1092
  guard: 'localDefaultUpdated',
1199
- target: 'merged',
1200
- actions: 'completeMerged',
1093
+ target: 'mergedLocalUpdated',
1094
+ actions: 'completeMergedLocalUpdated',
1201
1095
  },
1202
1096
  {
1203
1097
  guard: 'localDefaultNotUpdated',
1204
- target: 'mergedLocalBehind',
1205
- actions: 'completeMergedLocalBehind',
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: { target: 'openPullRequest', actions: 'startPr' },
1112
+ START_PR: {
1113
+ guard: 'validStartPr',
1114
+ target: 'openPullRequest',
1115
+ actions: 'startPr',
1116
+ },
1247
1117
  },
1248
1118
  },
1249
- merged: {
1250
- id: 'merged',
1251
- description: STATE_DESCRIPTIONS.merged,
1252
- meta: terminalMeta('merged', 'success'),
1119
+ mergedLocalUpdated: {
1120
+ id: 'mergedLocalUpdated',
1121
+ description: STATE_DESCRIPTIONS.mergedLocalUpdated,
1122
+ meta: terminalMeta('mergedLocalUpdated', 'success'),
1253
1123
  type: 'final',
1254
1124
  },
1255
- mergedLocalBehind: {
1256
- id: 'mergedLocalBehind',
1257
- description: STATE_DESCRIPTIONS.mergedLocalBehind,
1258
- meta: terminalMeta('mergedLocalBehind', 'success'),
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
- checksStillFailing: {
1280
- id: 'checksStillFailing',
1281
- description: STATE_DESCRIPTIONS.checksStillFailing,
1282
- meta: terminalMeta('checksStillFailing', 'failure'),
1149
+ checksFailed: {
1150
+ id: 'checksFailed',
1151
+ description: STATE_DESCRIPTIONS.checksFailed,
1152
+ meta: terminalMeta('checksFailed', 'failure'),
1283
1153
  type: 'final',
1284
1154
  },
1285
- mergeRefused: {
1286
- id: 'mergeRefused',
1287
- description: STATE_DESCRIPTIONS.mergeRefused,
1288
- meta: terminalMeta('mergeRefused', 'failure'),
1155
+ mergeUnconfirmed: {
1156
+ id: 'mergeUnconfirmed',
1157
+ description: STATE_DESCRIPTIONS.mergeUnconfirmed,
1158
+ meta: terminalMeta('mergeUnconfirmed', 'failure'),
1289
1159
  type: 'final',
1290
1160
  },
1291
1161
  },