@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,95 +1,190 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
2
  // SPDX-FileCopyrightText: 2026 SubLang International <https://sublang.ai>
3
-
4
- // FSM object artifact compiled from review.gears.md.
5
- // The linked runtime supplies the delegated-player actor.
3
+ //
4
+ // FSM object artifact compiled from ./review.gears.md by gears2fsm.
5
+ // It declares only the machine, its actor contracts, and typed inputs; the
6
+ // linked runtime supplies the `player` actor implementation.
6
7
 
7
8
  import { assign, fromPromise, setup } from 'xstate';
8
9
 
9
- type Role = 'coder' | 'reviewer';
10
+ // ---------------------------------------------------------------------------
11
+ // Identities
12
+ // ---------------------------------------------------------------------------
13
+
14
+ /** Canonical lowercase local ids of the source roles Coder and Reviewer. */
15
+ export type ReviewRole = 'coder' | 'reviewer';
16
+
17
+ /** One role-id array per parallel group; REVIEW declares no parallel group. */
18
+ export const concurrentRoleSets: readonly (readonly ReviewRole[])[] = [];
19
+
20
+ /** Delegated-player working leaves (REVIEW-1 through REVIEW-4). */
21
+ export type WorkingStateId =
22
+ | 'firstReview'
23
+ | 'fixFindings'
24
+ | 'reviewAfterFix'
25
+ | 'reviewAfterRejection';
26
+
27
+ /** Working leaves that may suspend for, and resume from, a Boss reply. */
28
+ export type ResumableStateId = WorkingStateId;
29
+
30
+ /** Root `BOSS_INTERRUPT` targets. */
31
+ export type JumpableStateId = WorkingStateId;
10
32
 
11
- type JumpableStateId = 'reviewInitial';
33
+ export type ReviewSourceItem = 'REVIEW-1' | 'REVIEW-2' | 'REVIEW-3' | 'REVIEW-4';
12
34
 
13
- export type ResumableStateId =
14
- | 'reviewInitial'
15
- | 'addressFindings'
16
- | 'reviewAfterCommit'
17
- | 'reviewAfterRebuttal';
35
+ /** Coder's accepted disposition of the latest findings. */
36
+ export type CoderOutcome = 'committed' | 'rejectedAll';
37
+
38
+ // ---------------------------------------------------------------------------
39
+ // Boss-reply suspension (scalar form: one active player task at a time)
40
+ // ---------------------------------------------------------------------------
18
41
 
19
42
  export interface PendingBossQuestion {
20
- questionId: ResumableStateId;
21
- resumeStateId: ResumableStateId;
22
- sourceItem: `REVIEW-${1 | 2 | 3 | 4}`;
23
- asker: { readonly kind: 'role'; readonly roleId: Role };
24
- question: string;
43
+ readonly questionId: ResumableStateId;
44
+ readonly resumeStateId: ResumableStateId;
45
+ readonly sourceItem: ReviewSourceItem;
46
+ readonly asker: { readonly kind: 'role'; readonly roleId: ReviewRole };
47
+ readonly question: string;
25
48
  }
26
49
 
27
- export interface ReviewOutput {
28
- noUnsettledFindings: true;
29
- /**
30
- * Exact receipt-derived repository revision at which the review scope was
31
- * evaluated: the closing clean round's `unchanged` receipt observes it as
32
- * HEAD — the last review-fix commit when one landed, or the caller-supplied
33
- * scope revision when none did (DR-045).
34
- */
35
- evaluatedRevision: string;
50
+ // ---------------------------------------------------------------------------
51
+ // Delegated player actor contract
52
+ // ---------------------------------------------------------------------------
53
+
54
+ interface PlayerInputBase {
55
+ /** The source item's full final prompt, verbatim. */
56
+ readonly prompt: string;
57
+ /** This state's local result contract: guard name → description. */
58
+ readonly result: Readonly<Record<string, string>>;
59
+ /** `<caller-input>`: the caller's complete review request. */
60
+ readonly callerInput?: string;
61
+ readonly pendingBossQuestion?: PendingBossQuestion;
62
+ readonly bossReply?: string;
36
63
  }
37
64
 
38
- export type ReviewInput = Readonly<Record<string, never>>;
65
+ export interface FirstReviewPlayerInput extends PlayerInputBase {
66
+ readonly stateId: 'firstReview';
67
+ readonly role: 'reviewer';
68
+ readonly sourceItem: 'REVIEW-1';
69
+ }
39
70
 
40
- export interface ReviewContext {
41
- callerInput?: string;
42
- reviewerOutput?: string;
43
- coderOutput?: string;
44
- /** Receipt-derived OID of the latest review-fix commit (never player prose). */
45
- latestCommit?: string;
46
- /** Observed HEAD of the closing clean round's unchanged receipt (DR-045). */
47
- evaluatedRevision?: string;
48
- lastError?: unknown;
49
- pendingBossQuestion?: PendingBossQuestion;
50
- bossReply?: string;
71
+ export interface FixFindingsPlayerInput extends PlayerInputBase {
72
+ readonly stateId: 'fixFindings';
73
+ readonly role: 'coder';
74
+ readonly sourceItem: 'REVIEW-2';
75
+ /** `<reviewer-output>`: Reviewer's verbatim final text of the latest round. */
76
+ readonly reviewerOutput?: string;
51
77
  }
52
78
 
53
- export type ReviewEvent =
54
- | { type: 'START_REVIEW'; callerInput: string }
79
+ export interface ReviewAfterFixPlayerInput extends PlayerInputBase {
80
+ readonly stateId: 'reviewAfterFix';
81
+ readonly role: 'reviewer';
82
+ readonly sourceItem: 'REVIEW-3';
83
+ /** `<latest-commit>`: the receipt-owned latest review-fix commit. */
84
+ readonly latestCommit?: string;
85
+ /** `<coder-output>`: Coder's verbatim final text of the latest fix round. */
86
+ readonly coderOutput?: string;
87
+ }
88
+
89
+ export interface ReviewAfterRejectionPlayerInput extends PlayerInputBase {
90
+ readonly stateId: 'reviewAfterRejection';
91
+ readonly role: 'reviewer';
92
+ readonly sourceItem: 'REVIEW-4';
93
+ /** `<coder-output>`: Coder's verbatim final text of the latest fix round. */
94
+ readonly coderOutput?: string;
95
+ }
96
+
97
+ export type PlayerInput =
98
+ | FirstReviewPlayerInput
99
+ | FixFindingsPlayerInput
100
+ | ReviewAfterFixPlayerInput
101
+ | ReviewAfterRejectionPlayerInput;
102
+
103
+ export type NeedsBossReplyOutput = {
104
+ readonly guard: 'needsBossReply';
105
+ readonly question: string;
106
+ };
107
+
108
+ /** Reviewer outcomes (REVIEW-1, REVIEW-3, REVIEW-4). */
109
+ export type ReviewerOutput =
110
+ | { readonly guard: 'hasFindings'; readonly reviewerOutput: string }
55
111
  | {
56
- type: 'BOSS_INTERRUPT';
57
- targetId: JumpableStateId;
58
- bossIntent: string;
112
+ readonly guard: 'noFindings';
113
+ /** Effect-owned: the exact revision taken from repository authority. */
114
+ readonly evaluatedRevision: string;
59
115
  }
116
+ | NeedsBossReplyOutput;
117
+
118
+ /** Coder outcomes (REVIEW-2). */
119
+ export type CoderOutput =
60
120
  | {
61
- type: 'BOSS_REPLY';
62
- questionId?: ResumableStateId;
63
- answer: string;
64
- };
121
+ readonly guard: 'committed';
122
+ readonly coderOutput: string;
123
+ /** Effect-owned: the runtime fills it from the repository receipt. */
124
+ readonly latestCommit: string;
125
+ }
126
+ | { readonly guard: 'rejectedAll'; readonly coderOutput: string }
127
+ | NeedsBossReplyOutput;
65
128
 
66
- export interface PlayerInput {
67
- stateId: ResumableStateId;
68
- sourceItem: `REVIEW-${1 | 2 | 3 | 4}`;
69
- role: Role;
70
- prompt: string;
71
- result: Readonly<Record<string, string>>;
72
- callerInput?: string;
73
- reviewerOutput?: string;
74
- coderOutput?: string;
75
- latestCommit?: string;
76
- pendingBossQuestion?: PendingBossQuestion;
77
- bossReply?: string;
129
+ export type PlayerOutput = ReviewerOutput | CoderOutput;
130
+
131
+ type PlayerGuard = PlayerOutput['guard'];
132
+
133
+ export interface ErrorRecord {
134
+ readonly name: string;
135
+ readonly message: string;
136
+ readonly stack?: string;
78
137
  }
79
138
 
80
- export type PlayerOutput =
81
- | { guard: 'hasFindings'; reviewerOutput: string }
82
- | { guard: 'noFindings'; evaluatedRevision: string }
83
- | { guard: 'committed'; coderOutput: string; latestCommit: string }
84
- | { guard: 'rejectedAll'; coderOutput: string }
85
- | { guard: 'needsBossReply'; question: string };
139
+ // ---------------------------------------------------------------------------
140
+ // Machine input, context, events, and output
141
+ // ---------------------------------------------------------------------------
86
142
 
87
- type PlayerDoneEvent = { type: string; output: PlayerOutput };
143
+ /** Public output interface of the packaged builtin `review`. */
144
+ export interface ReviewOutput {
145
+ readonly noUnsettledFindings: true;
146
+ readonly evaluatedRevision: string;
147
+ }
88
148
 
89
- const NEEDS_BOSS_REPLY_DESCRIPTION =
90
- "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>`.";
149
+ /** The machine reads no input: the caller input arrives on `START_REVIEW`. */
150
+ export type ReviewInput = Readonly<Record<never, never>>;
151
+
152
+ export interface ReviewContext {
153
+ /** `<caller-input>`: the caller's intent, review scope, and context. */
154
+ readonly callerInput?: string;
155
+ /** `<reviewer-output>`: Reviewer's verbatim final text with findings. */
156
+ readonly reviewerOutput?: string;
157
+ /** `<coder-output>`: Coder's verbatim final text of the latest fix round. */
158
+ readonly coderOutput?: string;
159
+ /** `<latest-commit>`: the accepted, receipt-owned latest review-fix commit. */
160
+ readonly latestCommit?: string;
161
+ /** Coder's accepted disposition of the latest findings. */
162
+ readonly coderOutcome?: CoderOutcome;
163
+ /** Revision evaluated by the clean review round that ended REVIEW. */
164
+ readonly evaluatedRevision?: string;
165
+ readonly lastError?: ErrorRecord;
166
+ readonly pendingBossQuestion?: PendingBossQuestion;
167
+ readonly bossReply?: string;
168
+ }
169
+
170
+ export type ReviewEvent =
171
+ | { readonly type: 'START_REVIEW'; readonly callerInput: string }
172
+ | { readonly type: 'BOSS_INTERRUPT'; readonly targetId: JumpableStateId }
173
+ | {
174
+ readonly type: 'BOSS_REPLY';
175
+ readonly answer: string;
176
+ readonly questionId?: ResumableStateId;
177
+ };
91
178
 
92
- const SHARED_REVIEW_INSTRUCTION = [
179
+ // ---------------------------------------------------------------------------
180
+ // Prompts and result contracts (verbatim from review.gears.md)
181
+ // ---------------------------------------------------------------------------
182
+
183
+ const FIRST_REVIEW_PROMPT = [
184
+ 'A new review begins for the review scope.',
185
+ 'Keep to the original intent and follow what it asks.',
186
+ 'When the scope names commits, read each commit message for its context and rationale; otherwise use repository history and commit messages wherever they help establish that context.',
187
+ '',
93
188
  'Understand the full picture and think systematically about the underlying design.',
94
189
  'Continue to identify issues or improvements, if any, without duplication.',
95
190
  'Number the findings consistently across rounds.',
@@ -106,160 +201,387 @@ const SHARED_REVIEW_INSTRUCTION = [
106
201
  '',
107
202
  'Consult @specs/map.md for context if needed; verify it remains accurate.',
108
203
  'Consult @specs/meta.md for spec requirements if needed; verify affected specs follow it.',
204
+ '',
205
+ '> Original request: <caller-input>',
109
206
  ].join('\n');
110
207
 
111
- const INITIAL_REVIEW_PROMPT = [
112
- 'A new review begins for the review scope.',
208
+ const FIX_FINDINGS_PROMPT = [
209
+ 'For each review item, accept or reject it.',
210
+ 'Before deciding, understand the full picture and think systematically about the underlying design.',
113
211
  'Keep to the original intent and follow what it asks.',
114
- 'When the scope names commits, read each commit message for its context and rationale; otherwise use repository history and commit messages wherever they help establish that context.',
212
+ 'Reject anything that is not essential or is not worth fixing now.',
213
+ 'If you accept an item, fix its root cause, including any fundamental design flaw — do not patch around it; if it represents a class of defect, find every instance within the review scope worth fixing rather than addressing one or two per round, which drags out the review.',
214
+ 'If you reject an item, give the reasoning and cite code or test output that supports it.',
215
+ 'Do not re-run tests or builds whose inputs have not changed since any previous reported run.',
115
216
  '',
116
- '> Original request: <caller-input>',
217
+ 'If you accept any item, make minimal changes and add one new review-fix commit; never rewrite any existing commit.',
218
+ 'Follow @specs/packages/git.md.',
219
+ 'Make the commit message explain concisely what changed and why, including relevant verification.',
220
+ 'Identify every new commit you make.',
221
+ 'Credit every AI that contributed to this commit: Coder <coder-llm> and Reviewer <reviewer-llm>, whose findings it answers.',
222
+ '',
223
+ 'If you reject every item, change nothing and make no commit.',
224
+ 'Report every disposition, all relevant run results, and every rebuttal.',
117
225
  '',
118
- SHARED_REVIEW_INSTRUCTION,
226
+ '> Original request: <caller-input>',
227
+ '> Reviewer findings: <reviewer-output>',
119
228
  ].join('\n');
120
229
 
121
- const POST_COMMIT_REVIEW_PROMPT = [
230
+ const REVIEW_AFTER_FIX_PROMPT = [
122
231
  'A new review round begins for the review scope in the cumulative committed state, with particular attention to the latest review-fix commit.',
123
232
  'Keep to the original intent and follow what it asks.',
124
233
  "Read the latest review-fix commit's message and see Coder's feedback below.",
125
234
  '',
235
+ 'Understand the full picture and think systematically about the underlying design.',
236
+ 'Continue to identify issues or improvements, if any, without duplication.',
237
+ 'Number the findings consistently across rounds.',
238
+ 'Flag only what materially affects correctness, behavior, or spec quality — not style, equally valid alternatives, or theoretical threats.',
239
+ 'For specs, flag stale, missing, over-specified, or under-specified ones, if any.',
240
+ 'Avoid unnecessary complexity in code or tests, but flag any fundamental design flaw when leaving it would cost more in later patches than fixing it now.',
241
+ '',
242
+ 'If an issue represents a class of defect, find every instance within the review scope worth fixing rather than surfacing one or two per round, which drags out the review.',
243
+ 'For any rebuttal, accept or challenge it.',
244
+ 'Treat as settled, and do not raise again, any finding in this review rejected twice with reasoning.',
245
+ '',
246
+ 'Do not re-run tests or builds whose inputs have not changed since any previous reported run.',
247
+ 'Do not edit files or commit; report findings only.',
248
+ '',
249
+ 'Consult @specs/map.md for context if needed; verify it remains accurate.',
250
+ 'Consult @specs/meta.md for spec requirements if needed; verify affected specs follow it.',
251
+ '',
126
252
  '> Original request: <caller-input>',
127
253
  '> Latest commit: <latest-commit>',
128
254
  '> Coder output: <coder-output>',
129
- '',
130
- SHARED_REVIEW_INSTRUCTION,
131
255
  ].join('\n');
132
256
 
133
- const REBUTTAL_REVIEW_PROMPT = [
257
+ const REVIEW_AFTER_REJECTION_PROMPT = [
134
258
  'No new commit was made because Coder rejected every finding.',
135
259
  "See Coder's feedback below.",
136
260
  '',
137
- '> Original request: <caller-input>',
138
- '> Coder output: <coder-output>',
261
+ 'Understand the full picture and think systematically about the underlying design.',
262
+ 'Continue to identify issues or improvements, if any, without duplication.',
263
+ 'Number the findings consistently across rounds.',
264
+ 'Flag only what materially affects correctness, behavior, or spec quality — not style, equally valid alternatives, or theoretical threats.',
265
+ 'For specs, flag stale, missing, over-specified, or under-specified ones, if any.',
266
+ 'Avoid unnecessary complexity in code or tests, but flag any fundamental design flaw when leaving it would cost more in later patches than fixing it now.',
139
267
  '',
140
- SHARED_REVIEW_INSTRUCTION,
141
- ].join('\n');
142
-
143
- const CODER_DISPOSITION_PROMPT = [
144
- '> Original request: <caller-input>',
145
- '> Reviewer findings: <reviewer-output>',
268
+ 'If an issue represents a class of defect, find every instance within the review scope worth fixing rather than surfacing one or two per round, which drags out the review.',
269
+ 'For any rebuttal, accept or challenge it.',
270
+ 'Treat as settled, and do not raise again, any finding in this review rejected twice with reasoning.',
146
271
  '',
147
- 'For each review item, accept or reject it.',
148
- 'Before deciding, understand the full picture and think systematically about the underlying design.',
149
- 'Keep to the original intent and follow what it asks.',
150
- 'Reject anything that is not essential or is not worth fixing now.',
151
- 'If you accept an item, fix its root cause, including any fundamental design flaw — do not patch around it; if it represents a class of defect, find every instance within the review scope worth fixing rather than addressing one or two per round, which drags out the review.',
152
- 'If you reject an item, give the reasoning and cite code or test output that supports it.',
153
272
  'Do not re-run tests or builds whose inputs have not changed since any previous reported run.',
273
+ 'Do not edit files or commit; report findings only.',
154
274
  '',
155
- 'If you accept any item, make minimal changes and add one new review-fix commit; never rewrite any existing commit.',
156
- 'Follow @specs/packages/git.md.',
157
- 'Make the commit message explain concisely what changed and why, including relevant verification.',
158
- 'Identify every new commit you make.',
159
- 'Credit every AI that contributed to this commit: Coder <coder-llm> and Reviewer <reviewer-llm>, whose findings it answers.',
275
+ 'Consult @specs/map.md for context if needed; verify it remains accurate.',
276
+ 'Consult @specs/meta.md for spec requirements if needed; verify affected specs follow it.',
160
277
  '',
161
- 'If you reject every item, change nothing and make no commit.',
162
- 'Report every disposition, all relevant run results, and every rebuttal.',
278
+ '> Original request: <caller-input>',
279
+ '> Coder output: <coder-output>',
163
280
  ].join('\n');
164
281
 
165
- const REVIEW_RESULTS = {
282
+ const NEEDS_BOSS_REPLY_DESCRIPTION =
283
+ "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>`.";
284
+
285
+ const FIRST_REVIEW_RESULT = {
166
286
  hasFindings:
167
- 'Reviewer raised one or more unsettled findings. Output shall include `reviewerOutput: <verbatim final text>`.',
287
+ "Reviewer raised one or more unsettled findings; a progress report, status update, or promise of a later result supports no review outcome, and the outcome does not depend on finding numbering or any fixed presentation format of Reviewer's reply. Output shall include `reviewerOutput: <verbatim final text>`.",
168
288
  noFindings:
169
- 'Reviewer affirmatively reported the requested review complete with no unsettled findings remaining; a progress report, status update, or promise of a later result supports no review outcome. Output shall include `evaluatedRevision: <repository revision>`.',
289
+ "Reviewer affirmatively reported that the requested review is complete and no unsettled findings remain; a progress report, status update, or promise of a later result supports no review outcome, and the outcome does not depend on finding numbering or any fixed presentation format of Reviewer's reply. On this outcome the workflow returns the exact repository revision at which the review scope was evaluated, taken from repository authority and not from either player's prose, as evaluatedRevision, and the fact that no unsettled findings remain within that scope, as noUnsettledFindings true. Output shall include `evaluatedRevision: <repository revision>`.",
290
+ needsBossReply: NEEDS_BOSS_REPLY_DESCRIPTION,
291
+ } as const;
292
+
293
+ const FIX_FINDINGS_RESULT = {
294
+ committed:
295
+ "Coder made one new review-fix commit; the outcome does not depend on finding numbering or any fixed presentation format of Coder's reply. Output shall include `coderOutput: <verbatim final text>` and `latestCommit: <commit identity>`.",
296
+ rejectedAll:
297
+ "Coder rejected every finding, changed nothing, and made no commit; the outcome does not depend on finding numbering or any fixed presentation format of Coder's reply. Output shall include `coderOutput: <verbatim final text>`.",
170
298
  needsBossReply: NEEDS_BOSS_REPLY_DESCRIPTION,
171
299
  } as const;
172
300
 
173
- const REBUTTAL_RESULTS = {
301
+ const LATER_REVIEW_RESULT = {
174
302
  hasFindings:
175
- 'Reviewer kept one or more unsettled findings. Output shall include `reviewerOutput: <verbatim final text>`.',
303
+ "Reviewer raised or kept one or more unsettled findings; a progress report, status update, or promise of a later result supports no review outcome, and the outcome does not depend on finding numbering or any fixed presentation format of Reviewer's reply. Output shall include `reviewerOutput: <verbatim final text>`.",
176
304
  noFindings:
177
- 'Reviewer accepted the rebuttals and affirmatively reported the requested review complete with no unsettled findings remaining; a progress report, status update, or promise of a later result supports no review outcome. Output shall include `evaluatedRevision: <repository revision>`.',
305
+ "Reviewer affirmatively reported that the requested review is complete and no unsettled findings remain; a progress report, status update, or promise of a later result supports no review outcome, and the outcome does not depend on finding numbering or any fixed presentation format of Reviewer's reply. On this outcome the workflow returns the exact repository revision at which the review scope was evaluated, taken from repository authority and not from either player's prose, as evaluatedRevision, and the fact that no unsettled findings remain within that scope, as noUnsettledFindings true. Output shall include `evaluatedRevision: <repository revision>`.",
178
306
  needsBossReply: NEEDS_BOSS_REPLY_DESCRIPTION,
179
307
  } as const;
180
308
 
181
- const CODER_RESULTS = {
182
- committed:
183
- 'Coder accepted at least one item and added one new review-fix commit. Output shall include `coderOutput: <verbatim final text>` and `latestCommit: <commit identity>`.',
184
- rejectedAll:
185
- 'Coder rejected every item and made no commit. Output shall include `coderOutput: <verbatim final text>`.',
186
- needsBossReply: NEEDS_BOSS_REPLY_DESCRIPTION,
309
+ /** REVIEW-3's authored result contract (identical to REVIEW-4's). */
310
+ const REVIEW_AFTER_FIX_RESULT = LATER_REVIEW_RESULT;
311
+
312
+ /** REVIEW-4's authored result contract (identical to REVIEW-3's). */
313
+ const REVIEW_AFTER_REJECTION_RESULT = LATER_REVIEW_RESULT;
314
+
315
+ const STATE_DESCRIPTIONS = {
316
+ ready: 'Waiting for a caller to start a review.',
317
+ firstReview: 'Reviewer runs the first review round over the review scope.',
318
+ fixFindings:
319
+ "Coder accepts or rejects each of Reviewer's findings and commits any accepted fixes.",
320
+ reviewAfterFix:
321
+ 'Reviewer runs the next review round over the cumulative committed state after a review-fix commit.',
322
+ reviewAfterRejection:
323
+ 'Reviewer runs the next review round after Coder rejected every finding without a commit.',
324
+ awaitBossReply: "Waiting for Boss to answer the acting agent's question.",
325
+ failed:
326
+ 'REVIEW parked after a failure and waits for Boss to restart or resume it.',
327
+ done: 'REVIEW completed: the review scope was evaluated at the returned revision and no unsettled findings remain.',
328
+ } as const;
329
+
330
+ type StateKey = keyof typeof STATE_DESCRIPTIONS;
331
+
332
+ const RESUME_METADATA = {
333
+ firstReview: { sourceItem: 'REVIEW-1', role: 'reviewer' },
334
+ fixFindings: { sourceItem: 'REVIEW-2', role: 'coder' },
335
+ reviewAfterFix: { sourceItem: 'REVIEW-3', role: 'reviewer' },
336
+ reviewAfterRejection: { sourceItem: 'REVIEW-4', role: 'reviewer' },
337
+ } as const satisfies Record<
338
+ ResumableStateId,
339
+ { readonly sourceItem: ReviewSourceItem; readonly role: ReviewRole }
340
+ >;
341
+
342
+ // ---------------------------------------------------------------------------
343
+ // Structural helpers
344
+ // ---------------------------------------------------------------------------
345
+
346
+ const isRecord = (
347
+ value: unknown,
348
+ ): value is Readonly<Record<string, unknown>> =>
349
+ typeof value === 'object' && value !== null && !Array.isArray(value);
350
+
351
+ const isNonEmptyString = (value: unknown): value is string =>
352
+ typeof value === 'string' && value.trim().length > 0;
353
+
354
+ const doneOutputOf = (event: unknown): unknown =>
355
+ isRecord(event) ? event.output : undefined;
356
+
357
+ const actorErrorOf = (event: unknown): unknown =>
358
+ isRecord(event) ? event.error : undefined;
359
+
360
+ /** Narrows a player done event to the declared player output contract. */
361
+ function playerOutputOf(event: unknown): PlayerOutput | undefined {
362
+ const output = doneOutputOf(event);
363
+ if (!isRecord(output)) return undefined;
364
+ switch (output.guard) {
365
+ case 'hasFindings':
366
+ return isNonEmptyString(output.reviewerOutput)
367
+ ? { guard: 'hasFindings', reviewerOutput: output.reviewerOutput }
368
+ : undefined;
369
+ case 'noFindings':
370
+ return isNonEmptyString(output.evaluatedRevision)
371
+ ? { guard: 'noFindings', evaluatedRevision: output.evaluatedRevision }
372
+ : undefined;
373
+ case 'committed':
374
+ return isNonEmptyString(output.coderOutput) &&
375
+ isNonEmptyString(output.latestCommit)
376
+ ? {
377
+ guard: 'committed',
378
+ coderOutput: output.coderOutput,
379
+ latestCommit: output.latestCommit,
380
+ }
381
+ : undefined;
382
+ case 'rejectedAll':
383
+ return isNonEmptyString(output.coderOutput)
384
+ ? { guard: 'rejectedAll', coderOutput: output.coderOutput }
385
+ : undefined;
386
+ case 'needsBossReply':
387
+ return isNonEmptyString(output.question)
388
+ ? { guard: 'needsBossReply', question: output.question }
389
+ : undefined;
390
+ default:
391
+ return undefined;
392
+ }
393
+ }
394
+
395
+ /** Normalizes any thrown or malformed value to a JSON-safe error record. */
396
+ function errorRecord(value: unknown): ErrorRecord {
397
+ if (value instanceof Error) {
398
+ return {
399
+ name: value.name || 'Error',
400
+ message: value.message,
401
+ ...(typeof value.stack === 'string' ? { stack: value.stack } : {}),
402
+ };
403
+ }
404
+ if (isRecord(value) && typeof value.message === 'string') {
405
+ return {
406
+ name: isNonEmptyString(value.name) ? value.name : 'Error',
407
+ message: value.message,
408
+ };
409
+ }
410
+ return { name: 'Error', message: String(value) };
411
+ }
412
+
413
+ // ---------------------------------------------------------------------------
414
+ // Guards
415
+ // ---------------------------------------------------------------------------
416
+
417
+ type EventArgs = { readonly event: unknown };
418
+ type MachineArgs = {
419
+ readonly context: ReviewContext;
420
+ readonly event: ReviewEvent;
421
+ };
422
+
423
+ const acceptsOutcome =
424
+ (outcome: PlayerGuard) =>
425
+ ({ event }: EventArgs): boolean =>
426
+ playerOutputOf(event)?.guard === outcome;
427
+
428
+ // The entry guard validates the text the current event supplies, before the
429
+ // entry action copies it into context.
430
+ const validStartReview = ({ event }: MachineArgs): boolean =>
431
+ event.type === 'START_REVIEW' && isNonEmptyString(event.callerInput);
432
+
433
+ const repliesToPendingQuestion = ({ context, event }: MachineArgs): boolean =>
434
+ event.type === 'BOSS_REPLY' &&
435
+ context.pendingBossQuestion !== undefined &&
436
+ (event.questionId === undefined ||
437
+ event.questionId === context.pendingBossQuestion.questionId);
438
+
439
+ const emptyBossReply = (args: MachineArgs): boolean =>
440
+ repliesToPendingQuestion(args) &&
441
+ args.event.type === 'BOSS_REPLY' &&
442
+ !isNonEmptyString(args.event.answer);
443
+
444
+ /** Typed context each jumpable working leaf needs to be entered safely. */
445
+ const INTERRUPT_PRECONDITIONS = {
446
+ firstReview: (context: ReviewContext) => isNonEmptyString(context.callerInput),
447
+ fixFindings: (context: ReviewContext) =>
448
+ isNonEmptyString(context.callerInput) &&
449
+ isNonEmptyString(context.reviewerOutput),
450
+ reviewAfterFix: (context: ReviewContext) =>
451
+ isNonEmptyString(context.callerInput) &&
452
+ context.coderOutcome === 'committed' &&
453
+ isNonEmptyString(context.latestCommit) &&
454
+ isNonEmptyString(context.coderOutput),
455
+ reviewAfterRejection: (context: ReviewContext) =>
456
+ isNonEmptyString(context.callerInput) &&
457
+ context.coderOutcome === 'rejectedAll' &&
458
+ isNonEmptyString(context.coderOutput),
459
+ } as const satisfies Record<
460
+ JumpableStateId,
461
+ (context: ReviewContext) => boolean
462
+ >;
463
+
464
+ // ---------------------------------------------------------------------------
465
+ // Transition helpers
466
+ // ---------------------------------------------------------------------------
467
+
468
+ /** One guarded root transition per jumpable state. */
469
+ function bossInterrupts<const Ids extends readonly JumpableStateId[]>(
470
+ ids: Ids,
471
+ ) {
472
+ return ids.map((id) => ({
473
+ guard: ({ context, event }: MachineArgs) =>
474
+ event.type === 'BOSS_INTERRUPT' &&
475
+ event.targetId === id &&
476
+ INTERRUPT_PRECONDITIONS[id](context),
477
+ target: `#${id}` as const,
478
+ reenter: true,
479
+ actions: 'resetForInterrupt' as const,
480
+ }));
481
+ }
482
+
483
+ /** One guarded `BOSS_REPLY` resume transition per suspended working leaf. */
484
+ function resumableStates<const Ids extends readonly ResumableStateId[]>(
485
+ ids: Ids,
486
+ ) {
487
+ return ids.map((id) => ({
488
+ guard: (args: MachineArgs) =>
489
+ repliesToPendingQuestion(args) &&
490
+ args.event.type === 'BOSS_REPLY' &&
491
+ isNonEmptyString(args.event.answer) &&
492
+ args.context.pendingBossQuestion?.resumeStateId === id,
493
+ target: `#${id}` as const,
494
+ reenter: true,
495
+ actions: 'rememberBossReply' as const,
496
+ }));
497
+ }
498
+
499
+ const bossReplyFields = (context: ReviewContext) => ({
500
+ ...(context.pendingBossQuestion !== undefined
501
+ ? { pendingBossQuestion: context.pendingBossQuestion }
502
+ : {}),
503
+ ...(context.bossReply !== undefined ? { bossReply: context.bossReply } : {}),
504
+ });
505
+
506
+ const acceptedOutcome = (
507
+ source: WorkingStateId,
508
+ target: StateKey,
509
+ outcome: PlayerGuard,
510
+ ) =>
511
+ ({
512
+ type: 'playbook.acceptedOutcome',
513
+ params: { source, target, acceptedOutcome: outcome },
514
+ }) as const;
515
+
516
+ /** Reviewer found unsettled findings: Coder answers them next (REVIEW-2). */
517
+ const hasFindingsArm = (source: 'firstReview' | 'reviewAfterFix' | 'reviewAfterRejection') =>
518
+ ({
519
+ guard: 'acceptHasFindings',
520
+ target: '#fixFindings',
521
+ actions: [
522
+ acceptedOutcome(source, 'fixFindings', 'hasFindings'),
523
+ 'rememberFindings',
524
+ 'clearBossReplyContext',
525
+ ],
526
+ }) as const;
527
+
528
+ /** Reviewer affirmed a complete review with no unsettled findings. */
529
+ const noFindingsArm = (source: 'firstReview' | 'reviewAfterFix' | 'reviewAfterRejection') =>
530
+ ({
531
+ guard: 'acceptNoFindings',
532
+ target: '#done',
533
+ actions: [
534
+ acceptedOutcome(source, 'done', 'noFindings'),
535
+ 'rememberEvaluatedRevision',
536
+ 'clearBossReplyContext',
537
+ ],
538
+ }) as const;
539
+
540
+ const suspendArm = (source: WorkingStateId) =>
541
+ ({
542
+ guard: 'acceptNeedsBossReply',
543
+ target: '#awaitBossReply',
544
+ actions: [
545
+ acceptedOutcome(source, 'awaitBossReply', 'needsBossReply'),
546
+ { type: 'setPendingBossQuestion', params: { resumeStateId: source } },
547
+ ],
548
+ }) as const;
549
+
550
+ const malformedPlayerOutputArm = {
551
+ target: '#failed',
552
+ actions: ['rememberMalformedPlayerOutput', 'clearBossReplyContext'],
187
553
  } as const;
188
554
 
189
- const stateDescriptions = {
190
- ready: 'Waits for a caller to start a scoped committed-work review.',
191
- reviewInitial:
192
- "REVIEW-1: Reviewer reviews the caller's review scope against the original intent.",
193
- addressFindings:
194
- 'REVIEW-2: Coder accepts or rejects every current review finding.',
195
- reviewAfterCommit:
196
- 'REVIEW-3: Reviewer reviews the cumulative committed state after a review-fix commit.',
197
- reviewAfterRebuttal:
198
- 'REVIEW-4: Reviewer adjudicates an all-rejected Coder disposition.',
199
- awaitBossReply: 'Waits for Boss to answer the active player question.',
200
- failed: 'Retains a recoverable REVIEW failure for the caller.',
201
- done: 'The requested review is complete: no unsettled findings remain within the review scope.',
555
+ const playerOnError = {
556
+ target: '#failed',
557
+ actions: ['rememberActorError', 'clearBossReplyContext'],
202
558
  } as const;
203
559
 
204
- const playbookMeta = <StateId extends keyof typeof stateDescriptions>(
205
- stateId: StateId,
206
- role?: Role,
207
- ) => ({
560
+ const startReviewArm = {
561
+ guard: 'validStartReview',
562
+ target: '#firstReview',
563
+ actions: 'startReview',
564
+ } as const;
565
+
566
+ const meta = (stateId: Exclude<StateKey, 'done'>, role?: ReviewRole) => ({
208
567
  playbook: {
209
568
  stateId,
210
- description: stateDescriptions[stateId],
569
+ description: STATE_DESCRIPTIONS[stateId],
211
570
  ...(role === undefined ? {} : { role }),
212
571
  },
213
572
  });
214
573
 
215
- // DR-048: a final state additionally declares whether its outcome means the
216
- // workflow succeeded or failed, so a caller learns that from the machine
217
- // rather than from REVIEW's output fields.
218
- const terminalMeta = <StateId extends keyof typeof stateDescriptions>(
219
- stateId: StateId,
220
- terminal: 'success' | 'failure',
221
- ) => ({
574
+ const finalMeta = (stateId: 'done', terminal: 'success' | 'failure') => ({
222
575
  playbook: {
223
576
  stateId,
224
- description: stateDescriptions[stateId],
577
+ description: STATE_DESCRIPTIONS[stateId],
225
578
  terminal,
226
579
  },
227
580
  });
228
581
 
229
- const outputOf = (event: unknown): PlayerOutput =>
230
- (event as PlayerDoneEvent).output;
231
-
232
- const isNonEmptyString = (value: unknown): value is string =>
233
- typeof value === 'string' && value.trim().length > 0;
234
-
235
- const stateMetadata = {
236
- reviewInitial: {
237
- sourceItem: 'REVIEW-1',
238
- asker: { kind: 'role', roleId: 'reviewer' },
239
- },
240
- addressFindings: {
241
- sourceItem: 'REVIEW-2',
242
- asker: { kind: 'role', roleId: 'coder' },
243
- },
244
- reviewAfterCommit: {
245
- sourceItem: 'REVIEW-3',
246
- asker: { kind: 'role', roleId: 'reviewer' },
247
- },
248
- reviewAfterRebuttal: {
249
- sourceItem: 'REVIEW-4',
250
- asker: { kind: 'role', roleId: 'reviewer' },
251
- },
252
- } as const satisfies Record<
253
- ResumableStateId,
254
- {
255
- sourceItem: `REVIEW-${1 | 2 | 3 | 4}`;
256
- asker: { readonly kind: 'role'; readonly roleId: Role };
257
- }
258
- >;
259
-
260
- const playerPlaceholder = fromPromise<PlayerOutput, PlayerInput>(async () => {
261
- throw new Error('player actor must be provided by the runner');
262
- });
582
+ // ---------------------------------------------------------------------------
583
+ // Machine
584
+ // ---------------------------------------------------------------------------
263
585
 
264
586
  export const reviewMachine = setup({
265
587
  types: {
@@ -269,74 +591,9 @@ export const reviewMachine = setup({
269
591
  output: {} as ReviewOutput,
270
592
  },
271
593
  actors: {
272
- player: playerPlaceholder,
273
- },
274
- guards: {
275
- hasFindings: ({ event }) => {
276
- const output = outputOf(event);
277
- return (
278
- output.guard === 'hasFindings' &&
279
- isNonEmptyString(output.reviewerOutput)
280
- );
281
- },
282
- noFindings: ({ event }) => {
283
- const output = outputOf(event);
284
- return (
285
- output.guard === 'noFindings' &&
286
- isNonEmptyString(output.evaluatedRevision)
287
- );
288
- },
289
- committed: ({ event }) => {
290
- const output = outputOf(event);
291
- return (
292
- output.guard === 'committed' &&
293
- isNonEmptyString(output.coderOutput) &&
294
- isNonEmptyString(output.latestCommit)
295
- );
296
- },
297
- rejectedAll: ({ event }) => {
298
- const output = outputOf(event);
299
- return (
300
- output.guard === 'rejectedAll' && isNonEmptyString(output.coderOutput)
301
- );
302
- },
303
- needsBossReply: ({ event }) => {
304
- const output = outputOf(event);
305
- return (
306
- output.guard === 'needsBossReply' && isNonEmptyString(output.question)
307
- );
308
- },
309
- emptyBossReply: ({ event }) =>
310
- event.type === 'BOSS_REPLY' && event.answer.trim().length === 0,
311
- resumeReviewInitial: ({ context, event }) =>
312
- event.type === 'BOSS_REPLY' &&
313
- event.answer.trim().length > 0 &&
314
- context.pendingBossQuestion?.resumeStateId === 'reviewInitial' &&
315
- (event.questionId === undefined ||
316
- event.questionId === context.pendingBossQuestion.questionId),
317
- resumeAddressFindings: ({ context, event }) =>
318
- event.type === 'BOSS_REPLY' &&
319
- event.answer.trim().length > 0 &&
320
- context.pendingBossQuestion?.resumeStateId === 'addressFindings' &&
321
- (event.questionId === undefined ||
322
- event.questionId === context.pendingBossQuestion.questionId),
323
- resumeReviewAfterCommit: ({ context, event }) =>
324
- event.type === 'BOSS_REPLY' &&
325
- event.answer.trim().length > 0 &&
326
- context.pendingBossQuestion?.resumeStateId === 'reviewAfterCommit' &&
327
- (event.questionId === undefined ||
328
- event.questionId === context.pendingBossQuestion.questionId),
329
- resumeReviewAfterRebuttal: ({ context, event }) =>
330
- event.type === 'BOSS_REPLY' &&
331
- event.answer.trim().length > 0 &&
332
- context.pendingBossQuestion?.resumeStateId === 'reviewAfterRebuttal' &&
333
- (event.questionId === undefined ||
334
- event.questionId === context.pendingBossQuestion.questionId),
335
- restartInitialReview: ({ event }) =>
336
- event.type === 'BOSS_INTERRUPT' &&
337
- event.targetId === 'reviewInitial' &&
338
- typeof event.bossIntent === 'string' &&
339
- event.bossIntent.trim().length > 0,
594
+ player: fromPromise<PlayerOutput, PlayerInput>(async () => {
595
+ throw new Error('player actor must be provided by the runner');
596
+ }),
340
597
  },
341
598
  actions: {
342
599
  'playbook.acceptedOutcome': (
@@ -347,518 +604,316 @@ export const reviewMachine = setup({
347
604
  readonly acceptedOutcome: string;
348
605
  },
349
606
  ) => undefined,
350
- copyStartInput: assign({
351
- callerInput: ({ event }) =>
352
- event.type === 'START_REVIEW' ? event.callerInput : undefined,
353
- reviewerOutput: undefined,
354
- coderOutput: undefined,
355
- latestCommit: undefined,
607
+ startReview: assign(({ event }) =>
608
+ event.type === 'START_REVIEW'
609
+ ? {
610
+ callerInput: event.callerInput,
611
+ reviewerOutput: undefined,
612
+ coderOutput: undefined,
613
+ latestCommit: undefined,
614
+ coderOutcome: undefined,
615
+ evaluatedRevision: undefined,
616
+ lastError: undefined,
617
+ pendingBossQuestion: undefined,
618
+ bossReply: undefined,
619
+ }
620
+ : {},
621
+ ),
622
+ resetForInterrupt: assign({
356
623
  evaluatedRevision: undefined,
357
624
  lastError: undefined,
358
625
  pendingBossQuestion: undefined,
359
626
  bossReply: undefined,
360
627
  }),
361
- copyInterruptedInput: assign({
362
- callerInput: ({ event }) =>
363
- event.type === 'BOSS_INTERRUPT' ? event.bossIntent : undefined,
364
- reviewerOutput: undefined,
365
- coderOutput: undefined,
366
- latestCommit: undefined,
367
- evaluatedRevision: undefined,
368
- lastError: undefined,
369
- pendingBossQuestion: undefined,
370
- bossReply: undefined,
628
+ rememberFindings: assign(({ event }) => {
629
+ const output = playerOutputOf(event);
630
+ return output?.guard === 'hasFindings'
631
+ ? { reviewerOutput: output.reviewerOutput, lastError: undefined }
632
+ : {};
371
633
  }),
372
- rememberReviewerOutput: assign({
373
- reviewerOutput: ({ event }) => {
374
- const output = outputOf(event);
375
- return output.guard === 'hasFindings'
376
- ? output.reviewerOutput
377
- : undefined;
378
- },
379
- lastError: undefined,
380
- pendingBossQuestion: undefined,
381
- bossReply: undefined,
382
- }),
383
- rememberCoderOutput: assign({
384
- coderOutput: ({ event }) => {
385
- const output = outputOf(event);
386
- return output.guard === 'committed' || output.guard === 'rejectedAll'
387
- ? output.coderOutput
388
- : undefined;
389
- },
390
- // The receipt-derived review-fix commit identity replaces the prior one
391
- // only when Coder committed; an all-rejected round leaves the evaluated
392
- // revision unchanged.
393
- latestCommit: ({ context, event }) => {
394
- const output = outputOf(event);
395
- return output.guard === 'committed'
396
- ? output.latestCommit
397
- : context.latestCommit;
398
- },
399
- lastError: undefined,
400
- pendingBossQuestion: undefined,
401
- bossReply: undefined,
402
- }),
403
- rememberEvaluatedRevision: assign({
404
- // DR-045: the closing clean round's unchanged receipt observes the
405
- // exact revision the review scope was evaluated at.
406
- evaluatedRevision: ({ context, event }) => {
407
- const output = outputOf(event);
408
- return output.guard === 'noFindings'
409
- ? output.evaluatedRevision
410
- : context.evaluatedRevision;
411
- },
412
- lastError: undefined,
413
- pendingBossQuestion: undefined,
414
- bossReply: undefined,
415
- }),
416
- rememberBossReply: assign({
417
- bossReply: ({ event }) =>
418
- event.type === 'BOSS_REPLY' ? event.answer : undefined,
419
- }),
420
- rememberActorError: assign({
421
- lastError: ({ event }) =>
422
- (event as { error?: unknown }).error ??
423
- new Error('REVIEW player invocation failed'),
424
- }),
425
- rememberEmptyBossReplyError: assign({
426
- lastError: () => new Error('BOSS_REPLY received an empty answer'),
427
- pendingBossQuestion: undefined,
428
- bossReply: undefined,
429
- }),
430
- setPendingReviewInitial: assign({
431
- pendingBossQuestion: ({ event }) => {
432
- const output = outputOf(event);
634
+ rememberCoderDisposition: assign(({ event }) => {
635
+ const output = playerOutputOf(event);
636
+ if (output?.guard === 'committed') {
433
637
  return {
434
- questionId: 'reviewInitial',
435
- resumeStateId: 'reviewInitial',
436
- ...stateMetadata.reviewInitial,
437
- question: output.guard === 'needsBossReply' ? output.question : '',
638
+ coderOutput: output.coderOutput,
639
+ latestCommit: output.latestCommit,
640
+ coderOutcome: 'committed' as const,
641
+ lastError: undefined,
438
642
  };
439
- },
440
- bossReply: undefined,
441
- }),
442
- setPendingAddressFindings: assign({
443
- pendingBossQuestion: ({ event }) => {
444
- const output = outputOf(event);
643
+ }
644
+ if (output?.guard === 'rejectedAll') {
445
645
  return {
446
- questionId: 'addressFindings',
447
- resumeStateId: 'addressFindings',
448
- ...stateMetadata.addressFindings,
449
- question: output.guard === 'needsBossReply' ? output.question : '',
646
+ coderOutput: output.coderOutput,
647
+ coderOutcome: 'rejectedAll' as const,
648
+ lastError: undefined,
450
649
  };
451
- },
452
- bossReply: undefined,
650
+ }
651
+ return {};
453
652
  }),
454
- setPendingReviewAfterCommit: assign({
455
- pendingBossQuestion: ({ event }) => {
456
- const output = outputOf(event);
457
- return {
458
- questionId: 'reviewAfterCommit',
459
- resumeStateId: 'reviewAfterCommit',
460
- ...stateMetadata.reviewAfterCommit,
461
- question: output.guard === 'needsBossReply' ? output.question : '',
653
+ rememberEvaluatedRevision: assign(({ event }) => {
654
+ const output = playerOutputOf(event);
655
+ return output?.guard === 'noFindings'
656
+ ? { evaluatedRevision: output.evaluatedRevision, lastError: undefined }
657
+ : {};
658
+ }),
659
+ setPendingBossQuestion: assign(
660
+ ({ event }, params: { readonly resumeStateId: ResumableStateId }) => {
661
+ const output = playerOutputOf(event);
662
+ if (output?.guard !== 'needsBossReply') return {};
663
+ const metadata = RESUME_METADATA[params.resumeStateId];
664
+ const pendingBossQuestion: PendingBossQuestion = {
665
+ questionId: params.resumeStateId,
666
+ resumeStateId: params.resumeStateId,
667
+ sourceItem: metadata.sourceItem,
668
+ asker: { kind: 'role', roleId: metadata.role },
669
+ question: output.question,
462
670
  };
671
+ return { pendingBossQuestion, bossReply: undefined, lastError: undefined };
463
672
  },
673
+ ),
674
+ rememberBossReply: assign(({ event }) =>
675
+ event.type === 'BOSS_REPLY' ? { bossReply: event.answer } : {},
676
+ ),
677
+ clearBossReplyContext: assign({
678
+ pendingBossQuestion: undefined,
464
679
  bossReply: undefined,
465
680
  }),
466
- setPendingReviewAfterRebuttal: assign({
467
- pendingBossQuestion: ({ event }) => {
468
- const output = outputOf(event);
469
- return {
470
- questionId: 'reviewAfterRebuttal',
471
- resumeStateId: 'reviewAfterRebuttal',
472
- ...stateMetadata.reviewAfterRebuttal,
473
- question: output.guard === 'needsBossReply' ? output.question : '',
474
- };
681
+ rememberActorError: assign(({ event }) => ({
682
+ lastError: errorRecord(actorErrorOf(event)),
683
+ })),
684
+ rememberMalformedPlayerOutput: assign(({ event }) => {
685
+ const output = doneOutputOf(event);
686
+ return {
687
+ lastError: {
688
+ name: 'MalformedPlayerOutput',
689
+ message:
690
+ isRecord(output) && output.guard === 'needsBossReply'
691
+ ? 'Player output declared needsBossReply without a question.'
692
+ : 'Player output did not match an outcome declared for the working state.',
693
+ },
694
+ };
695
+ }),
696
+ rememberMalformedBossReply: assign({
697
+ lastError: {
698
+ name: 'MalformedBossReply',
699
+ message: 'BOSS_REPLY carried an empty answer.',
475
700
  },
476
- bossReply: undefined,
477
701
  }),
478
702
  },
703
+ guards: {
704
+ acceptHasFindings: acceptsOutcome('hasFindings'),
705
+ acceptNoFindings: acceptsOutcome('noFindings'),
706
+ acceptCommitted: acceptsOutcome('committed'),
707
+ acceptRejectedAll: acceptsOutcome('rejectedAll'),
708
+ acceptNeedsBossReply: acceptsOutcome('needsBossReply'),
709
+ validStartReview,
710
+ emptyBossReply,
711
+ },
479
712
  }).createMachine({
480
713
  id: 'review',
481
- context: {},
482
714
  initial: 'ready',
715
+ context: {},
716
+ output: ({ context }): ReviewOutput => {
717
+ if (!isNonEmptyString(context.evaluatedRevision)) {
718
+ throw new Error('REVIEW completed without the evaluated repository revision');
719
+ }
720
+ return {
721
+ noUnsettledFindings: true,
722
+ evaluatedRevision: context.evaluatedRevision,
723
+ };
724
+ },
483
725
  on: {
484
- BOSS_INTERRUPT: {
485
- guard: 'restartInitialReview',
486
- target: '#reviewInitial',
487
- reenter: true,
488
- actions: 'copyInterruptedInput',
489
- },
726
+ BOSS_INTERRUPT: bossInterrupts([
727
+ 'firstReview',
728
+ 'fixFindings',
729
+ 'reviewAfterFix',
730
+ 'reviewAfterRejection',
731
+ ] as const),
490
732
  },
491
733
  states: {
492
734
  ready: {
493
735
  id: 'ready',
494
- description: stateDescriptions.ready,
495
- meta: playbookMeta('ready'),
496
- tags: ['playbook.parked'],
736
+ tags: 'playbook.parked',
737
+ description: STATE_DESCRIPTIONS.ready,
738
+ meta: meta('ready'),
497
739
  on: {
498
- START_REVIEW: {
499
- guard: ({ event }) => event.callerInput.trim().length > 0,
500
- target: '#reviewInitial',
501
- actions: 'copyStartInput',
502
- },
740
+ START_REVIEW: startReviewArm,
503
741
  },
504
742
  },
505
- reviewInitial: {
506
- id: 'reviewInitial',
507
- description: stateDescriptions.reviewInitial,
508
- meta: playbookMeta('reviewInitial', 'reviewer'),
509
- tags: ['playbook.busy'],
743
+ firstReview: {
744
+ id: 'firstReview',
745
+ tags: 'playbook.busy',
746
+ description: STATE_DESCRIPTIONS.firstReview,
747
+ meta: meta('firstReview', 'reviewer'),
510
748
  invoke: {
511
749
  src: 'player',
512
- input: ({ context }): PlayerInput => ({
513
- stateId: 'reviewInitial',
514
- sourceItem: 'REVIEW-1',
750
+ input: ({ context }): FirstReviewPlayerInput => ({
751
+ stateId: 'firstReview',
515
752
  role: 'reviewer',
516
- prompt: INITIAL_REVIEW_PROMPT,
517
- result: REVIEW_RESULTS,
518
- callerInput: context.callerInput,
519
- pendingBossQuestion: context.pendingBossQuestion,
520
- bossReply: context.bossReply,
753
+ sourceItem: 'REVIEW-1',
754
+ prompt: FIRST_REVIEW_PROMPT,
755
+ result: FIRST_REVIEW_RESULT,
756
+ ...(context.callerInput !== undefined
757
+ ? { callerInput: context.callerInput }
758
+ : {}),
759
+ ...bossReplyFields(context),
521
760
  }),
522
761
  onDone: [
523
- {
524
- guard: 'hasFindings',
525
- target: '#addressFindings',
526
- actions: [
527
- {
528
- type: 'playbook.acceptedOutcome',
529
- params: {
530
- source: 'reviewInitial',
531
- target: 'addressFindings',
532
- acceptedOutcome: 'hasFindings',
533
- },
534
- },
535
- 'rememberReviewerOutput',
536
- ],
537
- },
538
- {
539
- guard: 'noFindings',
540
- target: '#done',
541
- actions: [
542
- {
543
- type: 'playbook.acceptedOutcome',
544
- params: {
545
- source: 'reviewInitial',
546
- target: 'done',
547
- acceptedOutcome: 'noFindings',
548
- },
549
- },
550
- 'rememberEvaluatedRevision',
551
- ],
552
- },
553
- {
554
- guard: 'needsBossReply',
555
- target: '#awaitBossReply',
556
- actions: [
557
- {
558
- type: 'playbook.acceptedOutcome',
559
- params: {
560
- source: 'reviewInitial',
561
- target: 'awaitBossReply',
562
- acceptedOutcome: 'needsBossReply',
563
- },
564
- },
565
- 'setPendingReviewInitial',
566
- ],
567
- },
762
+ hasFindingsArm('firstReview'),
763
+ noFindingsArm('firstReview'),
764
+ suspendArm('firstReview'),
765
+ malformedPlayerOutputArm,
568
766
  ],
569
- onError: {
570
- target: '#failed',
571
- actions: 'rememberActorError',
572
- },
767
+ onError: playerOnError,
573
768
  },
574
769
  },
575
- addressFindings: {
576
- id: 'addressFindings',
577
- description: stateDescriptions.addressFindings,
578
- meta: playbookMeta('addressFindings', 'coder'),
579
- tags: ['playbook.busy'],
770
+ fixFindings: {
771
+ id: 'fixFindings',
772
+ tags: 'playbook.busy',
773
+ description: STATE_DESCRIPTIONS.fixFindings,
774
+ meta: meta('fixFindings', 'coder'),
580
775
  invoke: {
581
776
  src: 'player',
582
- input: ({ context }): PlayerInput => ({
583
- stateId: 'addressFindings',
584
- sourceItem: 'REVIEW-2',
777
+ input: ({ context }): FixFindingsPlayerInput => ({
778
+ stateId: 'fixFindings',
585
779
  role: 'coder',
586
- prompt: CODER_DISPOSITION_PROMPT,
587
- result: CODER_RESULTS,
588
- callerInput: context.callerInput,
589
- reviewerOutput: context.reviewerOutput,
590
- pendingBossQuestion: context.pendingBossQuestion,
591
- bossReply: context.bossReply,
780
+ sourceItem: 'REVIEW-2',
781
+ prompt: FIX_FINDINGS_PROMPT,
782
+ result: FIX_FINDINGS_RESULT,
783
+ ...(context.callerInput !== undefined
784
+ ? { callerInput: context.callerInput }
785
+ : {}),
786
+ ...(context.reviewerOutput !== undefined
787
+ ? { reviewerOutput: context.reviewerOutput }
788
+ : {}),
789
+ ...bossReplyFields(context),
592
790
  }),
593
791
  onDone: [
594
792
  {
595
- guard: 'committed',
596
- target: '#reviewAfterCommit',
597
- actions: [
598
- {
599
- type: 'playbook.acceptedOutcome',
600
- params: {
601
- source: 'addressFindings',
602
- target: 'reviewAfterCommit',
603
- acceptedOutcome: 'committed',
604
- },
605
- },
606
- 'rememberCoderOutput',
607
- ],
608
- },
609
- {
610
- guard: 'rejectedAll',
611
- target: '#reviewAfterRebuttal',
793
+ guard: 'acceptCommitted',
794
+ target: '#reviewAfterFix',
612
795
  actions: [
613
- {
614
- type: 'playbook.acceptedOutcome',
615
- params: {
616
- source: 'addressFindings',
617
- target: 'reviewAfterRebuttal',
618
- acceptedOutcome: 'rejectedAll',
619
- },
620
- },
621
- 'rememberCoderOutput',
796
+ acceptedOutcome('fixFindings', 'reviewAfterFix', 'committed'),
797
+ 'rememberCoderDisposition',
798
+ 'clearBossReplyContext',
622
799
  ],
623
800
  },
624
801
  {
625
- guard: 'needsBossReply',
626
- target: '#awaitBossReply',
802
+ guard: 'acceptRejectedAll',
803
+ target: '#reviewAfterRejection',
627
804
  actions: [
628
- {
629
- type: 'playbook.acceptedOutcome',
630
- params: {
631
- source: 'addressFindings',
632
- target: 'awaitBossReply',
633
- acceptedOutcome: 'needsBossReply',
634
- },
635
- },
636
- 'setPendingAddressFindings',
805
+ acceptedOutcome('fixFindings', 'reviewAfterRejection', 'rejectedAll'),
806
+ 'rememberCoderDisposition',
807
+ 'clearBossReplyContext',
637
808
  ],
638
809
  },
810
+ suspendArm('fixFindings'),
811
+ malformedPlayerOutputArm,
639
812
  ],
640
- onError: {
641
- target: '#failed',
642
- actions: 'rememberActorError',
643
- },
813
+ onError: playerOnError,
644
814
  },
645
815
  },
646
- reviewAfterCommit: {
647
- id: 'reviewAfterCommit',
648
- description: stateDescriptions.reviewAfterCommit,
649
- meta: playbookMeta('reviewAfterCommit', 'reviewer'),
650
- tags: ['playbook.busy'],
816
+ reviewAfterFix: {
817
+ id: 'reviewAfterFix',
818
+ tags: 'playbook.busy',
819
+ description: STATE_DESCRIPTIONS.reviewAfterFix,
820
+ meta: meta('reviewAfterFix', 'reviewer'),
651
821
  invoke: {
652
822
  src: 'player',
653
- input: ({ context }): PlayerInput => ({
654
- stateId: 'reviewAfterCommit',
655
- sourceItem: 'REVIEW-3',
823
+ input: ({ context }): ReviewAfterFixPlayerInput => ({
824
+ stateId: 'reviewAfterFix',
656
825
  role: 'reviewer',
657
- prompt: POST_COMMIT_REVIEW_PROMPT,
658
- result: REVIEW_RESULTS,
659
- callerInput: context.callerInput,
660
- latestCommit: context.latestCommit,
661
- coderOutput: context.coderOutput,
662
- pendingBossQuestion: context.pendingBossQuestion,
663
- bossReply: context.bossReply,
826
+ sourceItem: 'REVIEW-3',
827
+ prompt: REVIEW_AFTER_FIX_PROMPT,
828
+ result: REVIEW_AFTER_FIX_RESULT,
829
+ ...(context.callerInput !== undefined
830
+ ? { callerInput: context.callerInput }
831
+ : {}),
832
+ ...(context.latestCommit !== undefined
833
+ ? { latestCommit: context.latestCommit }
834
+ : {}),
835
+ ...(context.coderOutput !== undefined
836
+ ? { coderOutput: context.coderOutput }
837
+ : {}),
838
+ ...bossReplyFields(context),
664
839
  }),
665
840
  onDone: [
666
- {
667
- guard: 'hasFindings',
668
- target: '#addressFindings',
669
- actions: [
670
- {
671
- type: 'playbook.acceptedOutcome',
672
- params: {
673
- source: 'reviewAfterCommit',
674
- target: 'addressFindings',
675
- acceptedOutcome: 'hasFindings',
676
- },
677
- },
678
- 'rememberReviewerOutput',
679
- ],
680
- },
681
- {
682
- guard: 'noFindings',
683
- target: '#done',
684
- actions: [
685
- {
686
- type: 'playbook.acceptedOutcome',
687
- params: {
688
- source: 'reviewAfterCommit',
689
- target: 'done',
690
- acceptedOutcome: 'noFindings',
691
- },
692
- },
693
- 'rememberEvaluatedRevision',
694
- ],
695
- },
696
- {
697
- guard: 'needsBossReply',
698
- target: '#awaitBossReply',
699
- actions: [
700
- {
701
- type: 'playbook.acceptedOutcome',
702
- params: {
703
- source: 'reviewAfterCommit',
704
- target: 'awaitBossReply',
705
- acceptedOutcome: 'needsBossReply',
706
- },
707
- },
708
- 'setPendingReviewAfterCommit',
709
- ],
710
- },
841
+ hasFindingsArm('reviewAfterFix'),
842
+ noFindingsArm('reviewAfterFix'),
843
+ suspendArm('reviewAfterFix'),
844
+ malformedPlayerOutputArm,
711
845
  ],
712
- onError: {
713
- target: '#failed',
714
- actions: 'rememberActorError',
715
- },
846
+ onError: playerOnError,
716
847
  },
717
848
  },
718
- reviewAfterRebuttal: {
719
- id: 'reviewAfterRebuttal',
720
- description: stateDescriptions.reviewAfterRebuttal,
721
- meta: playbookMeta('reviewAfterRebuttal', 'reviewer'),
722
- tags: ['playbook.busy'],
849
+ reviewAfterRejection: {
850
+ id: 'reviewAfterRejection',
851
+ tags: 'playbook.busy',
852
+ description: STATE_DESCRIPTIONS.reviewAfterRejection,
853
+ meta: meta('reviewAfterRejection', 'reviewer'),
723
854
  invoke: {
724
855
  src: 'player',
725
- input: ({ context }): PlayerInput => ({
726
- stateId: 'reviewAfterRebuttal',
727
- sourceItem: 'REVIEW-4',
856
+ input: ({ context }): ReviewAfterRejectionPlayerInput => ({
857
+ stateId: 'reviewAfterRejection',
728
858
  role: 'reviewer',
729
- prompt: REBUTTAL_REVIEW_PROMPT,
730
- result: REBUTTAL_RESULTS,
731
- callerInput: context.callerInput,
732
- coderOutput: context.coderOutput,
733
- pendingBossQuestion: context.pendingBossQuestion,
734
- bossReply: context.bossReply,
859
+ sourceItem: 'REVIEW-4',
860
+ prompt: REVIEW_AFTER_REJECTION_PROMPT,
861
+ result: REVIEW_AFTER_REJECTION_RESULT,
862
+ ...(context.callerInput !== undefined
863
+ ? { callerInput: context.callerInput }
864
+ : {}),
865
+ ...(context.coderOutput !== undefined
866
+ ? { coderOutput: context.coderOutput }
867
+ : {}),
868
+ ...bossReplyFields(context),
735
869
  }),
736
870
  onDone: [
737
- {
738
- guard: 'hasFindings',
739
- target: '#addressFindings',
740
- actions: [
741
- {
742
- type: 'playbook.acceptedOutcome',
743
- params: {
744
- source: 'reviewAfterRebuttal',
745
- target: 'addressFindings',
746
- acceptedOutcome: 'hasFindings',
747
- },
748
- },
749
- 'rememberReviewerOutput',
750
- ],
751
- },
752
- {
753
- guard: 'noFindings',
754
- target: '#done',
755
- actions: [
756
- {
757
- type: 'playbook.acceptedOutcome',
758
- params: {
759
- source: 'reviewAfterRebuttal',
760
- target: 'done',
761
- acceptedOutcome: 'noFindings',
762
- },
763
- },
764
- 'rememberEvaluatedRevision',
765
- ],
766
- },
767
- {
768
- guard: 'needsBossReply',
769
- target: '#awaitBossReply',
770
- actions: [
771
- {
772
- type: 'playbook.acceptedOutcome',
773
- params: {
774
- source: 'reviewAfterRebuttal',
775
- target: 'awaitBossReply',
776
- acceptedOutcome: 'needsBossReply',
777
- },
778
- },
779
- 'setPendingReviewAfterRebuttal',
780
- ],
781
- },
871
+ hasFindingsArm('reviewAfterRejection'),
872
+ noFindingsArm('reviewAfterRejection'),
873
+ suspendArm('reviewAfterRejection'),
874
+ malformedPlayerOutputArm,
782
875
  ],
783
- onError: {
784
- target: '#failed',
785
- actions: 'rememberActorError',
786
- },
876
+ onError: playerOnError,
787
877
  },
788
878
  },
789
879
  awaitBossReply: {
790
880
  id: 'awaitBossReply',
791
- description: stateDescriptions.awaitBossReply,
792
- meta: playbookMeta('awaitBossReply'),
793
- tags: ['playbook.parked'],
881
+ tags: 'playbook.parked',
882
+ description: STATE_DESCRIPTIONS.awaitBossReply,
883
+ meta: meta('awaitBossReply'),
794
884
  on: {
795
- START_REVIEW: {
796
- guard: ({ event }) => event.callerInput.trim().length > 0,
797
- target: '#reviewInitial',
798
- actions: 'copyStartInput',
799
- },
800
885
  BOSS_REPLY: [
801
886
  {
802
887
  guard: 'emptyBossReply',
803
- target: '#failed',
804
- actions: 'rememberEmptyBossReplyError',
805
- },
806
- {
807
- guard: 'resumeReviewInitial',
808
- target: '#reviewInitial',
809
- reenter: true,
810
- actions: 'rememberBossReply',
811
- },
812
- {
813
- guard: 'resumeAddressFindings',
814
- target: '#addressFindings',
815
- reenter: true,
816
- actions: 'rememberBossReply',
817
- },
818
- {
819
- guard: 'resumeReviewAfterCommit',
820
- target: '#reviewAfterCommit',
821
- reenter: true,
822
- actions: 'rememberBossReply',
823
- },
824
- {
825
- guard: 'resumeReviewAfterRebuttal',
826
- target: '#reviewAfterRebuttal',
827
- reenter: true,
828
- actions: 'rememberBossReply',
888
+ target: 'failed',
889
+ actions: ['rememberMalformedBossReply', 'clearBossReplyContext'],
829
890
  },
891
+ ...resumableStates([
892
+ 'firstReview',
893
+ 'fixFindings',
894
+ 'reviewAfterFix',
895
+ 'reviewAfterRejection',
896
+ ] as const),
830
897
  ],
898
+ START_REVIEW: startReviewArm,
831
899
  },
832
900
  },
833
901
  failed: {
834
902
  id: 'failed',
835
- description: stateDescriptions.failed,
836
- meta: playbookMeta('failed'),
837
- tags: ['playbook.parked'],
903
+ tags: 'playbook.parked',
904
+ description: STATE_DESCRIPTIONS.failed,
905
+ meta: meta('failed'),
838
906
  on: {
839
- START_REVIEW: {
840
- guard: ({ event }) => event.callerInput.trim().length > 0,
841
- target: '#reviewInitial',
842
- actions: 'copyStartInput',
843
- },
907
+ START_REVIEW: startReviewArm,
844
908
  },
845
909
  },
846
910
  done: {
847
911
  id: 'done',
848
- description: stateDescriptions.done,
849
- meta: terminalMeta('done', 'success'),
850
912
  type: 'final',
913
+ description: STATE_DESCRIPTIONS.done,
914
+ meta: finalMeta('done', 'success'),
851
915
  },
852
916
  },
853
- output: ({ context }): ReviewOutput => {
854
- if (!isNonEmptyString(context.evaluatedRevision)) {
855
- throw new Error(
856
- 'REVIEW reached done without a receipt-observed evaluated revision',
857
- );
858
- }
859
- return {
860
- noUnsettledFindings: true,
861
- evaluatedRevision: context.evaluatedRevision,
862
- };
863
- },
864
917
  });
918
+
919
+ export default reviewMachine;