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