@sublang/playbook 15.1.0 → 17.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/README.md +2 -0
  2. package/docs/cli.md +36 -18
  3. package/docs/configuration.md +7 -5
  4. package/docs/embedding.md +28 -7
  5. package/package.json +4 -4
  6. package/reference/sdlc/branch.playbook/branch.fsm.d.ts +63 -33
  7. package/reference/sdlc/branch.playbook/branch.fsm.js +190 -108
  8. package/reference/sdlc/branch.playbook/branch.fsm.ts +269 -147
  9. package/reference/sdlc/branch.playbook/branch.gears.md +7 -17
  10. package/reference/sdlc/branch.playbook/branch.playbook.d.ts +12 -16
  11. package/reference/sdlc/branch.playbook/branch.playbook.js +112 -101
  12. package/reference/sdlc/branch.playbook/branch.playbook.ts +128 -171
  13. package/reference/sdlc/branch.playbook/branch.registry.d.ts +3 -2
  14. package/reference/sdlc/branch.playbook/branch.registry.ts +6 -2
  15. package/reference/sdlc/captain.md +19 -10
  16. package/reference/sdlc/captain.playbook/captain.fsm.d.ts +6 -2
  17. package/reference/sdlc/captain.playbook/captain.fsm.js +19 -7
  18. package/reference/sdlc/captain.playbook/captain.fsm.ts +25 -10
  19. package/reference/sdlc/captain.playbook/captain.gears.md +16 -8
  20. package/reference/sdlc/captain.playbook/captain.playbook.d.ts +1 -1
  21. package/reference/sdlc/captain.playbook/captain.playbook.js +3 -2
  22. package/reference/sdlc/captain.playbook/captain.playbook.ts +4 -3
  23. package/reference/sdlc/code.playbook/bin/interactive-session.js +14 -1
  24. package/reference/sdlc/code.playbook/bin/portable-codec.js +3 -0
  25. package/reference/sdlc/code.playbook/bin/repository-effects.js +1 -0
  26. package/reference/sdlc/code.playbook/bin/run.js +128 -85
  27. package/reference/sdlc/code.playbook/bin/session-host.js +40 -8
  28. package/reference/sdlc/code.playbook/bin/session-store.js +162 -34
  29. package/reference/sdlc/code.playbook/code.fsm.d.ts +146 -93
  30. package/reference/sdlc/code.playbook/code.fsm.introspect.d.ts +8 -8
  31. package/reference/sdlc/code.playbook/code.fsm.introspect.ts +13 -13
  32. package/reference/sdlc/code.playbook/code.fsm.js +609 -649
  33. package/reference/sdlc/code.playbook/code.fsm.ts +890 -821
  34. package/reference/sdlc/code.playbook/code.gears.md +39 -29
  35. package/reference/sdlc/code.playbook/code.playbook.d.ts +13 -16
  36. package/reference/sdlc/code.playbook/code.playbook.js +166 -147
  37. package/reference/sdlc/code.playbook/code.playbook.ts +180 -229
  38. package/reference/sdlc/code.playbook/code.registry.d.ts +3 -2
  39. package/reference/sdlc/code.playbook/code.registry.ts +6 -2
  40. package/reference/sdlc/code.playbook/host-capabilities.d.ts +3 -0
  41. package/reference/sdlc/code.playbook/playbook-captain.d.ts +74 -2
  42. package/reference/sdlc/code.playbook/playbook-captain.js +1548 -468
  43. package/reference/sdlc/code.playbook/playbook-captain.ts +1993 -573
  44. package/reference/sdlc/code.playbook/session-host.d.ts +4 -1
  45. package/reference/sdlc/code.playbook/session-store.d.ts +17 -0
  46. package/reference/sdlc/code.playbook/session-store.js +1 -0
  47. package/reference/sdlc/decide.md +1 -0
  48. package/reference/sdlc/decide.playbook/decide.fsm.d.ts +273 -157
  49. package/reference/sdlc/decide.playbook/decide.fsm.js +765 -732
  50. package/reference/sdlc/decide.playbook/decide.fsm.ts +1167 -945
  51. package/reference/sdlc/decide.playbook/decide.gears.md +28 -21
  52. package/reference/sdlc/decide.playbook/decide.playbook.d.ts +11 -59
  53. package/reference/sdlc/decide.playbook/decide.playbook.js +155 -3696
  54. package/reference/sdlc/decide.playbook/decide.playbook.ts +186 -5244
  55. package/reference/sdlc/decide.playbook/decide.registry.d.ts +8 -4
  56. package/reference/sdlc/decide.playbook/decide.registry.js +2 -2
  57. package/reference/sdlc/decide.playbook/decide.registry.ts +13 -6
  58. package/reference/sdlc/dev.playbook/dev.fsm.d.ts +116 -134
  59. package/reference/sdlc/dev.playbook/dev.fsm.js +517 -575
  60. package/reference/sdlc/dev.playbook/dev.fsm.ts +667 -787
  61. package/reference/sdlc/dev.playbook/dev.gears.md +52 -54
  62. package/reference/sdlc/dev.playbook/dev.playbook.d.ts +13 -10
  63. package/reference/sdlc/dev.playbook/dev.playbook.js +128 -120
  64. package/reference/sdlc/dev.playbook/dev.playbook.ts +143 -193
  65. package/reference/sdlc/dev.playbook/dev.registry.d.ts +3 -2
  66. package/reference/sdlc/dev.playbook/dev.registry.ts +6 -2
  67. package/reference/sdlc/pr.playbook/pr.fsm.d.ts +95 -88
  68. package/reference/sdlc/pr.playbook/pr.fsm.js +461 -518
  69. package/reference/sdlc/pr.playbook/pr.fsm.ts +533 -663
  70. package/reference/sdlc/pr.playbook/pr.gears.md +47 -63
  71. package/reference/sdlc/pr.playbook/pr.playbook.d.ts +13 -23
  72. package/reference/sdlc/pr.playbook/pr.playbook.js +118 -112
  73. package/reference/sdlc/pr.playbook/pr.playbook.ts +136 -190
  74. package/reference/sdlc/pr.playbook/pr.registry.d.ts +3 -2
  75. package/reference/sdlc/pr.playbook/pr.registry.ts +4 -2
  76. package/reference/sdlc/review.md +3 -3
  77. package/reference/sdlc/review.playbook/review.fsm.d.ts +150 -116
  78. package/reference/sdlc/review.playbook/review.fsm.js +479 -550
  79. package/reference/sdlc/review.playbook/review.fsm.ts +717 -662
  80. package/reference/sdlc/review.playbook/review.gears.md +32 -29
  81. package/reference/sdlc/review.playbook/review.playbook.d.ts +12 -12
  82. package/reference/sdlc/review.playbook/review.playbook.js +180 -155
  83. package/reference/sdlc/review.playbook/review.playbook.ts +190 -230
  84. package/reference/sdlc/review.playbook/review.registry.d.ts +6 -5
  85. package/reference/sdlc/review.playbook/review.registry.js +3 -3
  86. package/reference/sdlc/review.playbook/review.registry.ts +9 -5
  87. package/slc/gears2fsm.md +77 -8
  88. package/slc/link.md +127 -60
  89. package/slc/materialize-link.mjs +51 -13
  90. package/slc/optimize.md +4 -1
  91. package/slc/prefix-prompts.mjs +29 -28
  92. package/slc/prefix.md +27 -24
  93. package/slc/slc.pin-inputs.json +11 -2
  94. package/slc/text2gears.md +71 -11
  95. package/src/runtime.d.ts +46 -2
  96. package/src/runtime.ts +42 -2
  97. package/src/xstate-playbook-runtime.d.ts +63 -23
  98. package/src/xstate-playbook-runtime.js +1637 -475
  99. package/src/xstate-playbook-runtime.ts +2105 -532
  100. package/src/xstate-runtime.d.ts +3 -4
  101. package/src/xstate-runtime.js +52 -0
  102. package/src/xstate-runtime.ts +59 -4
@@ -1,10 +1,20 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
2
  // SPDX-FileCopyrightText: 2026 SubLang International <https://sublang.ai>
3
- // FSM object artifact compiled from review.gears.md.
4
- // 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.
5
7
  import { assign, fromPromise, setup } from 'xstate';
6
- const NEEDS_BOSS_REPLY_DESCRIPTION = "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>`.";
7
- const SHARED_REVIEW_INSTRUCTION = [
8
+ /** One role-id array per parallel group; REVIEW declares no parallel group. */
9
+ export const concurrentRoleSets = [];
10
+ // ---------------------------------------------------------------------------
11
+ // Prompts and result contracts (verbatim from review.gears.md)
12
+ // ---------------------------------------------------------------------------
13
+ const FIRST_REVIEW_PROMPT = [
14
+ 'A new review begins for the review scope.',
15
+ 'Keep to the original intent and follow what it asks.',
16
+ '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.',
17
+ '',
8
18
  'Understand the full picture and think systematically about the underlying design.',
9
19
  'Continue to identify issues or improvements, if any, without duplication.',
10
20
  'Number the findings consistently across rounds.',
@@ -21,122 +31,293 @@ const SHARED_REVIEW_INSTRUCTION = [
21
31
  '',
22
32
  'Consult @specs/map.md for context if needed; verify it remains accurate.',
23
33
  'Consult @specs/meta.md for spec requirements if needed; verify affected specs follow it.',
34
+ '',
35
+ '> Original request: <caller-input>',
24
36
  ].join('\n');
25
- const INITIAL_REVIEW_PROMPT = [
26
- 'A new review begins for the review scope.',
37
+ const FIX_FINDINGS_PROMPT = [
38
+ 'For each review item, accept or reject it.',
39
+ 'Before deciding, understand the full picture and think systematically about the underlying design.',
27
40
  'Keep to the original intent and follow what it asks.',
28
- '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.',
41
+ 'Reject anything that is not essential or is not worth fixing now.',
42
+ '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.',
43
+ 'If you reject an item, give the reasoning and cite code or test output that supports it.',
44
+ 'Do not re-run tests or builds whose inputs have not changed since any previous reported run.',
29
45
  '',
30
- '> Original request: <caller-input>',
46
+ 'If you accept any item, make minimal changes and add one new review-fix commit; never rewrite any existing commit.',
47
+ 'Follow @specs/packages/git.md.',
48
+ 'Make the commit message explain concisely what changed and why, including relevant verification.',
49
+ 'Identify every new commit you make.',
50
+ 'Credit every AI that contributed to this commit: Coder <coder-llm> and Reviewer <reviewer-llm>, whose findings it answers.',
51
+ '',
52
+ 'If you reject every item, change nothing and make no commit.',
53
+ 'Report every disposition, all relevant run results, and every rebuttal.',
31
54
  '',
32
- SHARED_REVIEW_INSTRUCTION,
55
+ '> Original request: <caller-input>',
56
+ '> Reviewer findings: <reviewer-output>',
33
57
  ].join('\n');
34
- const POST_COMMIT_REVIEW_PROMPT = [
58
+ const REVIEW_AFTER_FIX_PROMPT = [
35
59
  'A new review round begins for the review scope in the cumulative committed state, with particular attention to the latest review-fix commit.',
36
60
  'Keep to the original intent and follow what it asks.',
37
61
  "Read the latest review-fix commit's message and see Coder's feedback below.",
38
62
  '',
63
+ 'Understand the full picture and think systematically about the underlying design.',
64
+ 'Continue to identify issues or improvements, if any, without duplication.',
65
+ 'Number the findings consistently across rounds.',
66
+ 'Flag only what materially affects correctness, behavior, or spec quality — not style, equally valid alternatives, or theoretical threats.',
67
+ 'For specs, flag stale, missing, over-specified, or under-specified ones, if any.',
68
+ '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.',
69
+ '',
70
+ '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.',
71
+ 'For any rebuttal, accept or challenge it.',
72
+ 'Treat as settled, and do not raise again, any finding in this review rejected twice with reasoning.',
73
+ '',
74
+ 'Do not re-run tests or builds whose inputs have not changed since any previous reported run.',
75
+ 'Do not edit files or commit; report findings only.',
76
+ '',
77
+ 'Consult @specs/map.md for context if needed; verify it remains accurate.',
78
+ 'Consult @specs/meta.md for spec requirements if needed; verify affected specs follow it.',
79
+ '',
39
80
  '> Original request: <caller-input>',
40
81
  '> Latest commit: <latest-commit>',
41
82
  '> Coder output: <coder-output>',
42
- '',
43
- SHARED_REVIEW_INSTRUCTION,
44
83
  ].join('\n');
45
- const REBUTTAL_REVIEW_PROMPT = [
84
+ const REVIEW_AFTER_REJECTION_PROMPT = [
46
85
  'No new commit was made because Coder rejected every finding.',
47
86
  "See Coder's feedback below.",
48
87
  '',
49
- '> Original request: <caller-input>',
50
- '> Coder output: <coder-output>',
88
+ 'Understand the full picture and think systematically about the underlying design.',
89
+ 'Continue to identify issues or improvements, if any, without duplication.',
90
+ 'Number the findings consistently across rounds.',
91
+ 'Flag only what materially affects correctness, behavior, or spec quality — not style, equally valid alternatives, or theoretical threats.',
92
+ 'For specs, flag stale, missing, over-specified, or under-specified ones, if any.',
93
+ '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.',
51
94
  '',
52
- SHARED_REVIEW_INSTRUCTION,
53
- ].join('\n');
54
- const CODER_DISPOSITION_PROMPT = [
55
- '> Original request: <caller-input>',
56
- '> Reviewer findings: <reviewer-output>',
95
+ '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.',
96
+ 'For any rebuttal, accept or challenge it.',
97
+ 'Treat as settled, and do not raise again, any finding in this review rejected twice with reasoning.',
57
98
  '',
58
- 'For each review item, accept or reject it.',
59
- 'Before deciding, understand the full picture and think systematically about the underlying design.',
60
- 'Keep to the original intent and follow what it asks.',
61
- 'Reject anything that is not essential or is not worth fixing now.',
62
- '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.',
63
- 'If you reject an item, give the reasoning and cite code or test output that supports it.',
64
99
  'Do not re-run tests or builds whose inputs have not changed since any previous reported run.',
100
+ 'Do not edit files or commit; report findings only.',
65
101
  '',
66
- 'If you accept any item, make minimal changes and add one new review-fix commit; never rewrite any existing commit.',
67
- 'Follow @specs/packages/git.md.',
68
- 'Make the commit message explain concisely what changed and why, including relevant verification.',
69
- 'Identify every new commit you make.',
70
- 'Credit every AI that contributed to this commit: Coder <coder-llm> and Reviewer <reviewer-llm>, whose findings it answers.',
102
+ 'Consult @specs/map.md for context if needed; verify it remains accurate.',
103
+ 'Consult @specs/meta.md for spec requirements if needed; verify affected specs follow it.',
71
104
  '',
72
- 'If you reject every item, change nothing and make no commit.',
73
- 'Report every disposition, all relevant run results, and every rebuttal.',
105
+ '> Original request: <caller-input>',
106
+ '> Coder output: <coder-output>',
74
107
  ].join('\n');
75
- const REVIEW_RESULTS = {
76
- hasFindings: 'Reviewer raised one or more unsettled findings. Output shall include `reviewerOutput: <verbatim final text>`.',
77
- noFindings: '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>`.',
108
+ const NEEDS_BOSS_REPLY_DESCRIPTION = "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>`.";
109
+ const FIRST_REVIEW_RESULT = {
110
+ hasFindings: "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>`.",
111
+ noFindings: "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>`.",
78
112
  needsBossReply: NEEDS_BOSS_REPLY_DESCRIPTION,
79
113
  };
80
- const REBUTTAL_RESULTS = {
81
- hasFindings: 'Reviewer kept one or more unsettled findings. Output shall include `reviewerOutput: <verbatim final text>`.',
82
- noFindings: '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>`.',
114
+ const FIX_FINDINGS_RESULT = {
115
+ committed: "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>`.",
116
+ rejectedAll: "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>`.",
83
117
  needsBossReply: NEEDS_BOSS_REPLY_DESCRIPTION,
84
118
  };
85
- const CODER_RESULTS = {
86
- committed: 'Coder accepted at least one item and added one new review-fix commit. Output shall include `coderOutput: <verbatim final text>` and `latestCommit: <commit identity>`.',
87
- rejectedAll: 'Coder rejected every item and made no commit. Output shall include `coderOutput: <verbatim final text>`.',
119
+ const LATER_REVIEW_RESULT = {
120
+ hasFindings: "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>`.",
121
+ noFindings: "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>`.",
88
122
  needsBossReply: NEEDS_BOSS_REPLY_DESCRIPTION,
89
123
  };
90
- const stateDescriptions = {
91
- ready: 'Waits for a caller to start a scoped committed-work review.',
92
- reviewInitial: "REVIEW-1: Reviewer reviews the caller's review scope against the original intent.",
93
- addressFindings: 'REVIEW-2: Coder accepts or rejects every current review finding.',
94
- reviewAfterCommit: 'REVIEW-3: Reviewer reviews the cumulative committed state after a review-fix commit.',
95
- reviewAfterRebuttal: 'REVIEW-4: Reviewer adjudicates an all-rejected Coder disposition.',
96
- awaitBossReply: 'Waits for Boss to answer the active player question.',
97
- failed: 'Retains a recoverable REVIEW failure for the caller.',
98
- done: 'The requested review is complete: no unsettled findings remain within the review scope.',
124
+ /** REVIEW-3's authored result contract (identical to REVIEW-4's). */
125
+ const REVIEW_AFTER_FIX_RESULT = LATER_REVIEW_RESULT;
126
+ /** REVIEW-4's authored result contract (identical to REVIEW-3's). */
127
+ const REVIEW_AFTER_REJECTION_RESULT = LATER_REVIEW_RESULT;
128
+ const STATE_DESCRIPTIONS = {
129
+ ready: 'Waiting for a caller to start a review.',
130
+ firstReview: 'Reviewer runs the first review round over the review scope.',
131
+ fixFindings: "Coder accepts or rejects each of Reviewer's findings and commits any accepted fixes.",
132
+ reviewAfterFix: 'Reviewer runs the next review round over the cumulative committed state after a review-fix commit.',
133
+ reviewAfterRejection: 'Reviewer runs the next review round after Coder rejected every finding without a commit.',
134
+ awaitBossReply: "Waiting for Boss to answer the acting agent's question.",
135
+ failed: 'REVIEW parked after a failure and waits for Boss to restart or resume it.',
136
+ done: 'REVIEW completed: the review scope was evaluated at the returned revision and no unsettled findings remain.',
137
+ };
138
+ const RESUME_METADATA = {
139
+ firstReview: { sourceItem: 'REVIEW-1', role: 'reviewer' },
140
+ fixFindings: { sourceItem: 'REVIEW-2', role: 'coder' },
141
+ reviewAfterFix: { sourceItem: 'REVIEW-3', role: 'reviewer' },
142
+ reviewAfterRejection: { sourceItem: 'REVIEW-4', role: 'reviewer' },
143
+ };
144
+ // ---------------------------------------------------------------------------
145
+ // Structural helpers
146
+ // ---------------------------------------------------------------------------
147
+ const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
148
+ const isNonEmptyString = (value) => typeof value === 'string' && value.trim().length > 0;
149
+ const doneOutputOf = (event) => isRecord(event) ? event.output : undefined;
150
+ const actorErrorOf = (event) => isRecord(event) ? event.error : undefined;
151
+ /** Narrows a player done event to the declared player output contract. */
152
+ function playerOutputOf(event) {
153
+ const output = doneOutputOf(event);
154
+ if (!isRecord(output))
155
+ return undefined;
156
+ switch (output.guard) {
157
+ case 'hasFindings':
158
+ return isNonEmptyString(output.reviewerOutput)
159
+ ? { guard: 'hasFindings', reviewerOutput: output.reviewerOutput }
160
+ : undefined;
161
+ case 'noFindings':
162
+ return isNonEmptyString(output.evaluatedRevision)
163
+ ? { guard: 'noFindings', evaluatedRevision: output.evaluatedRevision }
164
+ : undefined;
165
+ case 'committed':
166
+ return isNonEmptyString(output.coderOutput) &&
167
+ isNonEmptyString(output.latestCommit)
168
+ ? {
169
+ guard: 'committed',
170
+ coderOutput: output.coderOutput,
171
+ latestCommit: output.latestCommit,
172
+ }
173
+ : undefined;
174
+ case 'rejectedAll':
175
+ return isNonEmptyString(output.coderOutput)
176
+ ? { guard: 'rejectedAll', coderOutput: output.coderOutput }
177
+ : undefined;
178
+ case 'needsBossReply':
179
+ return isNonEmptyString(output.question)
180
+ ? { guard: 'needsBossReply', question: output.question }
181
+ : undefined;
182
+ default:
183
+ return undefined;
184
+ }
185
+ }
186
+ /** Normalizes any thrown or malformed value to a JSON-safe error record. */
187
+ function errorRecord(value) {
188
+ if (value instanceof Error) {
189
+ return {
190
+ name: value.name || 'Error',
191
+ message: value.message,
192
+ ...(typeof value.stack === 'string' ? { stack: value.stack } : {}),
193
+ };
194
+ }
195
+ if (isRecord(value) && typeof value.message === 'string') {
196
+ return {
197
+ name: isNonEmptyString(value.name) ? value.name : 'Error',
198
+ message: value.message,
199
+ };
200
+ }
201
+ return { name: 'Error', message: String(value) };
202
+ }
203
+ const acceptsOutcome = (outcome) => ({ event }) => playerOutputOf(event)?.guard === outcome;
204
+ // The entry guard validates the text the current event supplies, before the
205
+ // entry action copies it into context.
206
+ const validStartReview = ({ event }) => event.type === 'START_REVIEW' && isNonEmptyString(event.callerInput);
207
+ const repliesToPendingQuestion = ({ context, event }) => event.type === 'BOSS_REPLY' &&
208
+ context.pendingBossQuestion !== undefined &&
209
+ (event.questionId === undefined ||
210
+ event.questionId === context.pendingBossQuestion.questionId);
211
+ const emptyBossReply = (args) => repliesToPendingQuestion(args) &&
212
+ args.event.type === 'BOSS_REPLY' &&
213
+ !isNonEmptyString(args.event.answer);
214
+ /** Typed context each jumpable working leaf needs to be entered safely. */
215
+ const INTERRUPT_PRECONDITIONS = {
216
+ firstReview: (context) => isNonEmptyString(context.callerInput),
217
+ fixFindings: (context) => isNonEmptyString(context.callerInput) &&
218
+ isNonEmptyString(context.reviewerOutput),
219
+ reviewAfterFix: (context) => isNonEmptyString(context.callerInput) &&
220
+ context.coderOutcome === 'committed' &&
221
+ isNonEmptyString(context.latestCommit) &&
222
+ isNonEmptyString(context.coderOutput),
223
+ reviewAfterRejection: (context) => isNonEmptyString(context.callerInput) &&
224
+ context.coderOutcome === 'rejectedAll' &&
225
+ isNonEmptyString(context.coderOutput),
226
+ };
227
+ // ---------------------------------------------------------------------------
228
+ // Transition helpers
229
+ // ---------------------------------------------------------------------------
230
+ /** One guarded root transition per jumpable state. */
231
+ function bossInterrupts(ids) {
232
+ return ids.map((id) => ({
233
+ guard: ({ context, event }) => event.type === 'BOSS_INTERRUPT' &&
234
+ event.targetId === id &&
235
+ INTERRUPT_PRECONDITIONS[id](context),
236
+ target: `#${id}`,
237
+ reenter: true,
238
+ actions: 'resetForInterrupt',
239
+ }));
240
+ }
241
+ /** One guarded `BOSS_REPLY` resume transition per suspended working leaf. */
242
+ function resumableStates(ids) {
243
+ return ids.map((id) => ({
244
+ guard: (args) => repliesToPendingQuestion(args) &&
245
+ args.event.type === 'BOSS_REPLY' &&
246
+ isNonEmptyString(args.event.answer) &&
247
+ args.context.pendingBossQuestion?.resumeStateId === id,
248
+ target: `#${id}`,
249
+ reenter: true,
250
+ actions: 'rememberBossReply',
251
+ }));
252
+ }
253
+ const bossReplyFields = (context) => ({
254
+ ...(context.pendingBossQuestion !== undefined
255
+ ? { pendingBossQuestion: context.pendingBossQuestion }
256
+ : {}),
257
+ ...(context.bossReply !== undefined ? { bossReply: context.bossReply } : {}),
258
+ });
259
+ const acceptedOutcome = (source, target, outcome) => ({
260
+ type: 'playbook.acceptedOutcome',
261
+ params: { source, target, acceptedOutcome: outcome },
262
+ });
263
+ /** Reviewer found unsettled findings: Coder answers them next (REVIEW-2). */
264
+ const hasFindingsArm = (source) => ({
265
+ guard: 'acceptHasFindings',
266
+ target: '#fixFindings',
267
+ actions: [
268
+ acceptedOutcome(source, 'fixFindings', 'hasFindings'),
269
+ 'rememberFindings',
270
+ 'clearBossReplyContext',
271
+ ],
272
+ });
273
+ /** Reviewer affirmed a complete review with no unsettled findings. */
274
+ const noFindingsArm = (source) => ({
275
+ guard: 'acceptNoFindings',
276
+ target: '#done',
277
+ actions: [
278
+ acceptedOutcome(source, 'done', 'noFindings'),
279
+ 'rememberEvaluatedRevision',
280
+ 'clearBossReplyContext',
281
+ ],
282
+ });
283
+ const suspendArm = (source) => ({
284
+ guard: 'acceptNeedsBossReply',
285
+ target: '#awaitBossReply',
286
+ actions: [
287
+ acceptedOutcome(source, 'awaitBossReply', 'needsBossReply'),
288
+ { type: 'setPendingBossQuestion', params: { resumeStateId: source } },
289
+ ],
290
+ });
291
+ const malformedPlayerOutputArm = {
292
+ target: '#failed',
293
+ actions: ['rememberMalformedPlayerOutput', 'clearBossReplyContext'],
294
+ };
295
+ const playerOnError = {
296
+ target: '#failed',
297
+ actions: ['rememberActorError', 'clearBossReplyContext'],
99
298
  };
100
- const playbookMeta = (stateId, role) => ({
299
+ const startReviewArm = {
300
+ guard: 'validStartReview',
301
+ target: '#firstReview',
302
+ actions: 'startReview',
303
+ };
304
+ const meta = (stateId, role) => ({
101
305
  playbook: {
102
306
  stateId,
103
- description: stateDescriptions[stateId],
307
+ description: STATE_DESCRIPTIONS[stateId],
104
308
  ...(role === undefined ? {} : { role }),
105
309
  },
106
310
  });
107
- // DR-048: a final state additionally declares whether its outcome means the
108
- // workflow succeeded or failed, so a caller learns that from the machine
109
- // rather than from REVIEW's output fields.
110
- const terminalMeta = (stateId, terminal) => ({
311
+ const finalMeta = (stateId, terminal) => ({
111
312
  playbook: {
112
313
  stateId,
113
- description: stateDescriptions[stateId],
314
+ description: STATE_DESCRIPTIONS[stateId],
114
315
  terminal,
115
316
  },
116
317
  });
117
- const outputOf = (event) => event.output;
118
- const isNonEmptyString = (value) => typeof value === 'string' && value.trim().length > 0;
119
- const stateMetadata = {
120
- reviewInitial: {
121
- sourceItem: 'REVIEW-1',
122
- asker: { kind: 'role', roleId: 'reviewer' },
123
- },
124
- addressFindings: {
125
- sourceItem: 'REVIEW-2',
126
- asker: { kind: 'role', roleId: 'coder' },
127
- },
128
- reviewAfterCommit: {
129
- sourceItem: 'REVIEW-3',
130
- asker: { kind: 'role', roleId: 'reviewer' },
131
- },
132
- reviewAfterRebuttal: {
133
- sourceItem: 'REVIEW-4',
134
- asker: { kind: 'role', roleId: 'reviewer' },
135
- },
136
- };
137
- const playerPlaceholder = fromPromise(async () => {
138
- throw new Error('player actor must be provided by the runner');
139
- });
318
+ // ---------------------------------------------------------------------------
319
+ // Machine
320
+ // ---------------------------------------------------------------------------
140
321
  export const reviewMachine = setup({
141
322
  types: {
142
323
  context: {},
@@ -145,567 +326,315 @@ export const reviewMachine = setup({
145
326
  output: {},
146
327
  },
147
328
  actors: {
148
- player: playerPlaceholder,
149
- },
150
- guards: {
151
- hasFindings: ({ event }) => {
152
- const output = outputOf(event);
153
- return (output.guard === 'hasFindings' &&
154
- isNonEmptyString(output.reviewerOutput));
155
- },
156
- noFindings: ({ event }) => {
157
- const output = outputOf(event);
158
- return (output.guard === 'noFindings' &&
159
- isNonEmptyString(output.evaluatedRevision));
160
- },
161
- committed: ({ event }) => {
162
- const output = outputOf(event);
163
- return (output.guard === 'committed' &&
164
- isNonEmptyString(output.coderOutput) &&
165
- isNonEmptyString(output.latestCommit));
166
- },
167
- rejectedAll: ({ event }) => {
168
- const output = outputOf(event);
169
- return (output.guard === 'rejectedAll' && isNonEmptyString(output.coderOutput));
170
- },
171
- needsBossReply: ({ event }) => {
172
- const output = outputOf(event);
173
- return (output.guard === 'needsBossReply' && isNonEmptyString(output.question));
174
- },
175
- emptyBossReply: ({ event }) => event.type === 'BOSS_REPLY' && event.answer.trim().length === 0,
176
- resumeReviewInitial: ({ context, event }) => event.type === 'BOSS_REPLY' &&
177
- event.answer.trim().length > 0 &&
178
- context.pendingBossQuestion?.resumeStateId === 'reviewInitial' &&
179
- (event.questionId === undefined ||
180
- event.questionId === context.pendingBossQuestion.questionId),
181
- resumeAddressFindings: ({ context, event }) => event.type === 'BOSS_REPLY' &&
182
- event.answer.trim().length > 0 &&
183
- context.pendingBossQuestion?.resumeStateId === 'addressFindings' &&
184
- (event.questionId === undefined ||
185
- event.questionId === context.pendingBossQuestion.questionId),
186
- resumeReviewAfterCommit: ({ context, event }) => event.type === 'BOSS_REPLY' &&
187
- event.answer.trim().length > 0 &&
188
- context.pendingBossQuestion?.resumeStateId === 'reviewAfterCommit' &&
189
- (event.questionId === undefined ||
190
- event.questionId === context.pendingBossQuestion.questionId),
191
- resumeReviewAfterRebuttal: ({ context, event }) => event.type === 'BOSS_REPLY' &&
192
- event.answer.trim().length > 0 &&
193
- context.pendingBossQuestion?.resumeStateId === 'reviewAfterRebuttal' &&
194
- (event.questionId === undefined ||
195
- event.questionId === context.pendingBossQuestion.questionId),
196
- restartInitialReview: ({ event }) => event.type === 'BOSS_INTERRUPT' &&
197
- event.targetId === 'reviewInitial' &&
198
- typeof event.bossIntent === 'string' &&
199
- event.bossIntent.trim().length > 0,
329
+ player: fromPromise(async () => {
330
+ throw new Error('player actor must be provided by the runner');
331
+ }),
200
332
  },
201
333
  actions: {
202
334
  'playbook.acceptedOutcome': (_args, _params) => undefined,
203
- copyStartInput: assign({
204
- callerInput: ({ event }) => event.type === 'START_REVIEW' ? event.callerInput : undefined,
205
- reviewerOutput: undefined,
206
- coderOutput: undefined,
207
- latestCommit: undefined,
208
- evaluatedRevision: undefined,
209
- lastError: undefined,
210
- pendingBossQuestion: undefined,
211
- bossReply: undefined,
212
- }),
213
- copyInterruptedInput: assign({
214
- callerInput: ({ event }) => event.type === 'BOSS_INTERRUPT' ? event.bossIntent : undefined,
215
- reviewerOutput: undefined,
216
- coderOutput: undefined,
217
- latestCommit: undefined,
335
+ startReview: assign(({ event }) => event.type === 'START_REVIEW'
336
+ ? {
337
+ callerInput: event.callerInput,
338
+ reviewerOutput: undefined,
339
+ coderOutput: undefined,
340
+ latestCommit: undefined,
341
+ coderOutcome: undefined,
342
+ evaluatedRevision: undefined,
343
+ lastError: undefined,
344
+ pendingBossQuestion: undefined,
345
+ bossReply: undefined,
346
+ }
347
+ : {}),
348
+ resetForInterrupt: assign({
218
349
  evaluatedRevision: undefined,
219
350
  lastError: undefined,
220
351
  pendingBossQuestion: undefined,
221
352
  bossReply: undefined,
222
353
  }),
223
- rememberReviewerOutput: assign({
224
- reviewerOutput: ({ event }) => {
225
- const output = outputOf(event);
226
- return output.guard === 'hasFindings'
227
- ? output.reviewerOutput
228
- : undefined;
229
- },
230
- lastError: undefined,
231
- pendingBossQuestion: undefined,
232
- bossReply: undefined,
233
- }),
234
- rememberCoderOutput: assign({
235
- coderOutput: ({ event }) => {
236
- const output = outputOf(event);
237
- return output.guard === 'committed' || output.guard === 'rejectedAll'
238
- ? output.coderOutput
239
- : undefined;
240
- },
241
- // The receipt-derived review-fix commit identity replaces the prior one
242
- // only when Coder committed; an all-rejected round leaves the evaluated
243
- // revision unchanged.
244
- latestCommit: ({ context, event }) => {
245
- const output = outputOf(event);
246
- return output.guard === 'committed'
247
- ? output.latestCommit
248
- : context.latestCommit;
249
- },
250
- lastError: undefined,
251
- pendingBossQuestion: undefined,
252
- bossReply: undefined,
354
+ rememberFindings: assign(({ event }) => {
355
+ const output = playerOutputOf(event);
356
+ return output?.guard === 'hasFindings'
357
+ ? { reviewerOutput: output.reviewerOutput, lastError: undefined }
358
+ : {};
253
359
  }),
254
- rememberEvaluatedRevision: assign({
255
- // DR-045: the closing clean round's unchanged receipt observes the
256
- // exact revision the review scope was evaluated at.
257
- evaluatedRevision: ({ context, event }) => {
258
- const output = outputOf(event);
259
- return output.guard === 'noFindings'
260
- ? output.evaluatedRevision
261
- : context.evaluatedRevision;
262
- },
263
- lastError: undefined,
264
- pendingBossQuestion: undefined,
265
- bossReply: undefined,
266
- }),
267
- rememberBossReply: assign({
268
- bossReply: ({ event }) => event.type === 'BOSS_REPLY' ? event.answer : undefined,
269
- }),
270
- rememberActorError: assign({
271
- lastError: ({ event }) => event.error ??
272
- new Error('REVIEW player invocation failed'),
273
- }),
274
- rememberEmptyBossReplyError: assign({
275
- lastError: () => new Error('BOSS_REPLY received an empty answer'),
276
- pendingBossQuestion: undefined,
277
- bossReply: undefined,
278
- }),
279
- setPendingReviewInitial: assign({
280
- pendingBossQuestion: ({ event }) => {
281
- const output = outputOf(event);
360
+ rememberCoderDisposition: assign(({ event }) => {
361
+ const output = playerOutputOf(event);
362
+ if (output?.guard === 'committed') {
282
363
  return {
283
- questionId: 'reviewInitial',
284
- resumeStateId: 'reviewInitial',
285
- ...stateMetadata.reviewInitial,
286
- question: output.guard === 'needsBossReply' ? output.question : '',
364
+ coderOutput: output.coderOutput,
365
+ latestCommit: output.latestCommit,
366
+ coderOutcome: 'committed',
367
+ lastError: undefined,
287
368
  };
288
- },
289
- bossReply: undefined,
290
- }),
291
- setPendingAddressFindings: assign({
292
- pendingBossQuestion: ({ event }) => {
293
- const output = outputOf(event);
369
+ }
370
+ if (output?.guard === 'rejectedAll') {
294
371
  return {
295
- questionId: 'addressFindings',
296
- resumeStateId: 'addressFindings',
297
- ...stateMetadata.addressFindings,
298
- question: output.guard === 'needsBossReply' ? output.question : '',
372
+ coderOutput: output.coderOutput,
373
+ coderOutcome: 'rejectedAll',
374
+ lastError: undefined,
299
375
  };
300
- },
301
- bossReply: undefined,
376
+ }
377
+ return {};
302
378
  }),
303
- setPendingReviewAfterCommit: assign({
304
- pendingBossQuestion: ({ event }) => {
305
- const output = outputOf(event);
306
- return {
307
- questionId: 'reviewAfterCommit',
308
- resumeStateId: 'reviewAfterCommit',
309
- ...stateMetadata.reviewAfterCommit,
310
- question: output.guard === 'needsBossReply' ? output.question : '',
311
- };
312
- },
379
+ rememberEvaluatedRevision: assign(({ event }) => {
380
+ const output = playerOutputOf(event);
381
+ return output?.guard === 'noFindings'
382
+ ? { evaluatedRevision: output.evaluatedRevision, lastError: undefined }
383
+ : {};
384
+ }),
385
+ setPendingBossQuestion: assign(({ event }, params) => {
386
+ const output = playerOutputOf(event);
387
+ if (output?.guard !== 'needsBossReply')
388
+ return {};
389
+ const metadata = RESUME_METADATA[params.resumeStateId];
390
+ const pendingBossQuestion = {
391
+ questionId: params.resumeStateId,
392
+ resumeStateId: params.resumeStateId,
393
+ sourceItem: metadata.sourceItem,
394
+ asker: { kind: 'role', roleId: metadata.role },
395
+ question: output.question,
396
+ };
397
+ return { pendingBossQuestion, bossReply: undefined, lastError: undefined };
398
+ }),
399
+ rememberBossReply: assign(({ event }) => event.type === 'BOSS_REPLY' ? { bossReply: event.answer } : {}),
400
+ clearBossReplyContext: assign({
401
+ pendingBossQuestion: undefined,
313
402
  bossReply: undefined,
314
403
  }),
315
- setPendingReviewAfterRebuttal: assign({
316
- pendingBossQuestion: ({ event }) => {
317
- const output = outputOf(event);
318
- return {
319
- questionId: 'reviewAfterRebuttal',
320
- resumeStateId: 'reviewAfterRebuttal',
321
- ...stateMetadata.reviewAfterRebuttal,
322
- question: output.guard === 'needsBossReply' ? output.question : '',
323
- };
404
+ rememberActorError: assign(({ event }) => ({
405
+ lastError: errorRecord(actorErrorOf(event)),
406
+ })),
407
+ rememberMalformedPlayerOutput: assign(({ event }) => {
408
+ const output = doneOutputOf(event);
409
+ return {
410
+ lastError: {
411
+ name: 'MalformedPlayerOutput',
412
+ message: isRecord(output) && output.guard === 'needsBossReply'
413
+ ? 'Player output declared needsBossReply without a question.'
414
+ : 'Player output did not match an outcome declared for the working state.',
415
+ },
416
+ };
417
+ }),
418
+ rememberMalformedBossReply: assign({
419
+ lastError: {
420
+ name: 'MalformedBossReply',
421
+ message: 'BOSS_REPLY carried an empty answer.',
324
422
  },
325
- bossReply: undefined,
326
423
  }),
327
424
  },
425
+ guards: {
426
+ acceptHasFindings: acceptsOutcome('hasFindings'),
427
+ acceptNoFindings: acceptsOutcome('noFindings'),
428
+ acceptCommitted: acceptsOutcome('committed'),
429
+ acceptRejectedAll: acceptsOutcome('rejectedAll'),
430
+ acceptNeedsBossReply: acceptsOutcome('needsBossReply'),
431
+ validStartReview,
432
+ emptyBossReply,
433
+ },
328
434
  }).createMachine({
329
435
  id: 'review',
330
- context: {},
331
436
  initial: 'ready',
437
+ context: {},
438
+ output: ({ context }) => {
439
+ if (!isNonEmptyString(context.evaluatedRevision)) {
440
+ throw new Error('REVIEW completed without the evaluated repository revision');
441
+ }
442
+ return {
443
+ noUnsettledFindings: true,
444
+ evaluatedRevision: context.evaluatedRevision,
445
+ };
446
+ },
332
447
  on: {
333
- BOSS_INTERRUPT: {
334
- guard: 'restartInitialReview',
335
- target: '#reviewInitial',
336
- reenter: true,
337
- actions: 'copyInterruptedInput',
338
- },
448
+ BOSS_INTERRUPT: bossInterrupts([
449
+ 'firstReview',
450
+ 'fixFindings',
451
+ 'reviewAfterFix',
452
+ 'reviewAfterRejection',
453
+ ]),
339
454
  },
340
455
  states: {
341
456
  ready: {
342
457
  id: 'ready',
343
- description: stateDescriptions.ready,
344
- meta: playbookMeta('ready'),
345
- tags: ['playbook.parked'],
458
+ tags: 'playbook.parked',
459
+ description: STATE_DESCRIPTIONS.ready,
460
+ meta: meta('ready'),
346
461
  on: {
347
- START_REVIEW: {
348
- guard: ({ event }) => event.callerInput.trim().length > 0,
349
- target: '#reviewInitial',
350
- actions: 'copyStartInput',
351
- },
462
+ START_REVIEW: startReviewArm,
352
463
  },
353
464
  },
354
- reviewInitial: {
355
- id: 'reviewInitial',
356
- description: stateDescriptions.reviewInitial,
357
- meta: playbookMeta('reviewInitial', 'reviewer'),
358
- tags: ['playbook.busy'],
465
+ firstReview: {
466
+ id: 'firstReview',
467
+ tags: 'playbook.busy',
468
+ description: STATE_DESCRIPTIONS.firstReview,
469
+ meta: meta('firstReview', 'reviewer'),
359
470
  invoke: {
360
471
  src: 'player',
361
472
  input: ({ context }) => ({
362
- stateId: 'reviewInitial',
363
- sourceItem: 'REVIEW-1',
473
+ stateId: 'firstReview',
364
474
  role: 'reviewer',
365
- prompt: INITIAL_REVIEW_PROMPT,
366
- result: REVIEW_RESULTS,
367
- callerInput: context.callerInput,
368
- pendingBossQuestion: context.pendingBossQuestion,
369
- bossReply: context.bossReply,
475
+ sourceItem: 'REVIEW-1',
476
+ prompt: FIRST_REVIEW_PROMPT,
477
+ result: FIRST_REVIEW_RESULT,
478
+ ...(context.callerInput !== undefined
479
+ ? { callerInput: context.callerInput }
480
+ : {}),
481
+ ...bossReplyFields(context),
370
482
  }),
371
483
  onDone: [
372
- {
373
- guard: 'hasFindings',
374
- target: '#addressFindings',
375
- actions: [
376
- {
377
- type: 'playbook.acceptedOutcome',
378
- params: {
379
- source: 'reviewInitial',
380
- target: 'addressFindings',
381
- acceptedOutcome: 'hasFindings',
382
- },
383
- },
384
- 'rememberReviewerOutput',
385
- ],
386
- },
387
- {
388
- guard: 'noFindings',
389
- target: '#done',
390
- actions: [
391
- {
392
- type: 'playbook.acceptedOutcome',
393
- params: {
394
- source: 'reviewInitial',
395
- target: 'done',
396
- acceptedOutcome: 'noFindings',
397
- },
398
- },
399
- 'rememberEvaluatedRevision',
400
- ],
401
- },
402
- {
403
- guard: 'needsBossReply',
404
- target: '#awaitBossReply',
405
- actions: [
406
- {
407
- type: 'playbook.acceptedOutcome',
408
- params: {
409
- source: 'reviewInitial',
410
- target: 'awaitBossReply',
411
- acceptedOutcome: 'needsBossReply',
412
- },
413
- },
414
- 'setPendingReviewInitial',
415
- ],
416
- },
484
+ hasFindingsArm('firstReview'),
485
+ noFindingsArm('firstReview'),
486
+ suspendArm('firstReview'),
487
+ malformedPlayerOutputArm,
417
488
  ],
418
- onError: {
419
- target: '#failed',
420
- actions: 'rememberActorError',
421
- },
489
+ onError: playerOnError,
422
490
  },
423
491
  },
424
- addressFindings: {
425
- id: 'addressFindings',
426
- description: stateDescriptions.addressFindings,
427
- meta: playbookMeta('addressFindings', 'coder'),
428
- tags: ['playbook.busy'],
492
+ fixFindings: {
493
+ id: 'fixFindings',
494
+ tags: 'playbook.busy',
495
+ description: STATE_DESCRIPTIONS.fixFindings,
496
+ meta: meta('fixFindings', 'coder'),
429
497
  invoke: {
430
498
  src: 'player',
431
499
  input: ({ context }) => ({
432
- stateId: 'addressFindings',
433
- sourceItem: 'REVIEW-2',
500
+ stateId: 'fixFindings',
434
501
  role: 'coder',
435
- prompt: CODER_DISPOSITION_PROMPT,
436
- result: CODER_RESULTS,
437
- callerInput: context.callerInput,
438
- reviewerOutput: context.reviewerOutput,
439
- pendingBossQuestion: context.pendingBossQuestion,
440
- bossReply: context.bossReply,
502
+ sourceItem: 'REVIEW-2',
503
+ prompt: FIX_FINDINGS_PROMPT,
504
+ result: FIX_FINDINGS_RESULT,
505
+ ...(context.callerInput !== undefined
506
+ ? { callerInput: context.callerInput }
507
+ : {}),
508
+ ...(context.reviewerOutput !== undefined
509
+ ? { reviewerOutput: context.reviewerOutput }
510
+ : {}),
511
+ ...bossReplyFields(context),
441
512
  }),
442
513
  onDone: [
443
514
  {
444
- guard: 'committed',
445
- target: '#reviewAfterCommit',
515
+ guard: 'acceptCommitted',
516
+ target: '#reviewAfterFix',
446
517
  actions: [
447
- {
448
- type: 'playbook.acceptedOutcome',
449
- params: {
450
- source: 'addressFindings',
451
- target: 'reviewAfterCommit',
452
- acceptedOutcome: 'committed',
453
- },
454
- },
455
- 'rememberCoderOutput',
518
+ acceptedOutcome('fixFindings', 'reviewAfterFix', 'committed'),
519
+ 'rememberCoderDisposition',
520
+ 'clearBossReplyContext',
456
521
  ],
457
522
  },
458
523
  {
459
- guard: 'rejectedAll',
460
- target: '#reviewAfterRebuttal',
524
+ guard: 'acceptRejectedAll',
525
+ target: '#reviewAfterRejection',
461
526
  actions: [
462
- {
463
- type: 'playbook.acceptedOutcome',
464
- params: {
465
- source: 'addressFindings',
466
- target: 'reviewAfterRebuttal',
467
- acceptedOutcome: 'rejectedAll',
468
- },
469
- },
470
- 'rememberCoderOutput',
471
- ],
472
- },
473
- {
474
- guard: 'needsBossReply',
475
- target: '#awaitBossReply',
476
- actions: [
477
- {
478
- type: 'playbook.acceptedOutcome',
479
- params: {
480
- source: 'addressFindings',
481
- target: 'awaitBossReply',
482
- acceptedOutcome: 'needsBossReply',
483
- },
484
- },
485
- 'setPendingAddressFindings',
527
+ acceptedOutcome('fixFindings', 'reviewAfterRejection', 'rejectedAll'),
528
+ 'rememberCoderDisposition',
529
+ 'clearBossReplyContext',
486
530
  ],
487
531
  },
532
+ suspendArm('fixFindings'),
533
+ malformedPlayerOutputArm,
488
534
  ],
489
- onError: {
490
- target: '#failed',
491
- actions: 'rememberActorError',
492
- },
535
+ onError: playerOnError,
493
536
  },
494
537
  },
495
- reviewAfterCommit: {
496
- id: 'reviewAfterCommit',
497
- description: stateDescriptions.reviewAfterCommit,
498
- meta: playbookMeta('reviewAfterCommit', 'reviewer'),
499
- tags: ['playbook.busy'],
538
+ reviewAfterFix: {
539
+ id: 'reviewAfterFix',
540
+ tags: 'playbook.busy',
541
+ description: STATE_DESCRIPTIONS.reviewAfterFix,
542
+ meta: meta('reviewAfterFix', 'reviewer'),
500
543
  invoke: {
501
544
  src: 'player',
502
545
  input: ({ context }) => ({
503
- stateId: 'reviewAfterCommit',
504
- sourceItem: 'REVIEW-3',
546
+ stateId: 'reviewAfterFix',
505
547
  role: 'reviewer',
506
- prompt: POST_COMMIT_REVIEW_PROMPT,
507
- result: REVIEW_RESULTS,
508
- callerInput: context.callerInput,
509
- latestCommit: context.latestCommit,
510
- coderOutput: context.coderOutput,
511
- pendingBossQuestion: context.pendingBossQuestion,
512
- bossReply: context.bossReply,
548
+ sourceItem: 'REVIEW-3',
549
+ prompt: REVIEW_AFTER_FIX_PROMPT,
550
+ result: REVIEW_AFTER_FIX_RESULT,
551
+ ...(context.callerInput !== undefined
552
+ ? { callerInput: context.callerInput }
553
+ : {}),
554
+ ...(context.latestCommit !== undefined
555
+ ? { latestCommit: context.latestCommit }
556
+ : {}),
557
+ ...(context.coderOutput !== undefined
558
+ ? { coderOutput: context.coderOutput }
559
+ : {}),
560
+ ...bossReplyFields(context),
513
561
  }),
514
562
  onDone: [
515
- {
516
- guard: 'hasFindings',
517
- target: '#addressFindings',
518
- actions: [
519
- {
520
- type: 'playbook.acceptedOutcome',
521
- params: {
522
- source: 'reviewAfterCommit',
523
- target: 'addressFindings',
524
- acceptedOutcome: 'hasFindings',
525
- },
526
- },
527
- 'rememberReviewerOutput',
528
- ],
529
- },
530
- {
531
- guard: 'noFindings',
532
- target: '#done',
533
- actions: [
534
- {
535
- type: 'playbook.acceptedOutcome',
536
- params: {
537
- source: 'reviewAfterCommit',
538
- target: 'done',
539
- acceptedOutcome: 'noFindings',
540
- },
541
- },
542
- 'rememberEvaluatedRevision',
543
- ],
544
- },
545
- {
546
- guard: 'needsBossReply',
547
- target: '#awaitBossReply',
548
- actions: [
549
- {
550
- type: 'playbook.acceptedOutcome',
551
- params: {
552
- source: 'reviewAfterCommit',
553
- target: 'awaitBossReply',
554
- acceptedOutcome: 'needsBossReply',
555
- },
556
- },
557
- 'setPendingReviewAfterCommit',
558
- ],
559
- },
563
+ hasFindingsArm('reviewAfterFix'),
564
+ noFindingsArm('reviewAfterFix'),
565
+ suspendArm('reviewAfterFix'),
566
+ malformedPlayerOutputArm,
560
567
  ],
561
- onError: {
562
- target: '#failed',
563
- actions: 'rememberActorError',
564
- },
568
+ onError: playerOnError,
565
569
  },
566
570
  },
567
- reviewAfterRebuttal: {
568
- id: 'reviewAfterRebuttal',
569
- description: stateDescriptions.reviewAfterRebuttal,
570
- meta: playbookMeta('reviewAfterRebuttal', 'reviewer'),
571
- tags: ['playbook.busy'],
571
+ reviewAfterRejection: {
572
+ id: 'reviewAfterRejection',
573
+ tags: 'playbook.busy',
574
+ description: STATE_DESCRIPTIONS.reviewAfterRejection,
575
+ meta: meta('reviewAfterRejection', 'reviewer'),
572
576
  invoke: {
573
577
  src: 'player',
574
578
  input: ({ context }) => ({
575
- stateId: 'reviewAfterRebuttal',
576
- sourceItem: 'REVIEW-4',
579
+ stateId: 'reviewAfterRejection',
577
580
  role: 'reviewer',
578
- prompt: REBUTTAL_REVIEW_PROMPT,
579
- result: REBUTTAL_RESULTS,
580
- callerInput: context.callerInput,
581
- coderOutput: context.coderOutput,
582
- pendingBossQuestion: context.pendingBossQuestion,
583
- bossReply: context.bossReply,
581
+ sourceItem: 'REVIEW-4',
582
+ prompt: REVIEW_AFTER_REJECTION_PROMPT,
583
+ result: REVIEW_AFTER_REJECTION_RESULT,
584
+ ...(context.callerInput !== undefined
585
+ ? { callerInput: context.callerInput }
586
+ : {}),
587
+ ...(context.coderOutput !== undefined
588
+ ? { coderOutput: context.coderOutput }
589
+ : {}),
590
+ ...bossReplyFields(context),
584
591
  }),
585
592
  onDone: [
586
- {
587
- guard: 'hasFindings',
588
- target: '#addressFindings',
589
- actions: [
590
- {
591
- type: 'playbook.acceptedOutcome',
592
- params: {
593
- source: 'reviewAfterRebuttal',
594
- target: 'addressFindings',
595
- acceptedOutcome: 'hasFindings',
596
- },
597
- },
598
- 'rememberReviewerOutput',
599
- ],
600
- },
601
- {
602
- guard: 'noFindings',
603
- target: '#done',
604
- actions: [
605
- {
606
- type: 'playbook.acceptedOutcome',
607
- params: {
608
- source: 'reviewAfterRebuttal',
609
- target: 'done',
610
- acceptedOutcome: 'noFindings',
611
- },
612
- },
613
- 'rememberEvaluatedRevision',
614
- ],
615
- },
616
- {
617
- guard: 'needsBossReply',
618
- target: '#awaitBossReply',
619
- actions: [
620
- {
621
- type: 'playbook.acceptedOutcome',
622
- params: {
623
- source: 'reviewAfterRebuttal',
624
- target: 'awaitBossReply',
625
- acceptedOutcome: 'needsBossReply',
626
- },
627
- },
628
- 'setPendingReviewAfterRebuttal',
629
- ],
630
- },
593
+ hasFindingsArm('reviewAfterRejection'),
594
+ noFindingsArm('reviewAfterRejection'),
595
+ suspendArm('reviewAfterRejection'),
596
+ malformedPlayerOutputArm,
631
597
  ],
632
- onError: {
633
- target: '#failed',
634
- actions: 'rememberActorError',
635
- },
598
+ onError: playerOnError,
636
599
  },
637
600
  },
638
601
  awaitBossReply: {
639
602
  id: 'awaitBossReply',
640
- description: stateDescriptions.awaitBossReply,
641
- meta: playbookMeta('awaitBossReply'),
642
- tags: ['playbook.parked'],
603
+ tags: 'playbook.parked',
604
+ description: STATE_DESCRIPTIONS.awaitBossReply,
605
+ meta: meta('awaitBossReply'),
643
606
  on: {
644
- START_REVIEW: {
645
- guard: ({ event }) => event.callerInput.trim().length > 0,
646
- target: '#reviewInitial',
647
- actions: 'copyStartInput',
648
- },
649
607
  BOSS_REPLY: [
650
608
  {
651
609
  guard: 'emptyBossReply',
652
- target: '#failed',
653
- actions: 'rememberEmptyBossReplyError',
654
- },
655
- {
656
- guard: 'resumeReviewInitial',
657
- target: '#reviewInitial',
658
- reenter: true,
659
- actions: 'rememberBossReply',
660
- },
661
- {
662
- guard: 'resumeAddressFindings',
663
- target: '#addressFindings',
664
- reenter: true,
665
- actions: 'rememberBossReply',
666
- },
667
- {
668
- guard: 'resumeReviewAfterCommit',
669
- target: '#reviewAfterCommit',
670
- reenter: true,
671
- actions: 'rememberBossReply',
672
- },
673
- {
674
- guard: 'resumeReviewAfterRebuttal',
675
- target: '#reviewAfterRebuttal',
676
- reenter: true,
677
- actions: 'rememberBossReply',
610
+ target: 'failed',
611
+ actions: ['rememberMalformedBossReply', 'clearBossReplyContext'],
678
612
  },
613
+ ...resumableStates([
614
+ 'firstReview',
615
+ 'fixFindings',
616
+ 'reviewAfterFix',
617
+ 'reviewAfterRejection',
618
+ ]),
679
619
  ],
620
+ START_REVIEW: startReviewArm,
680
621
  },
681
622
  },
682
623
  failed: {
683
624
  id: 'failed',
684
- description: stateDescriptions.failed,
685
- meta: playbookMeta('failed'),
686
- tags: ['playbook.parked'],
625
+ tags: 'playbook.parked',
626
+ description: STATE_DESCRIPTIONS.failed,
627
+ meta: meta('failed'),
687
628
  on: {
688
- START_REVIEW: {
689
- guard: ({ event }) => event.callerInput.trim().length > 0,
690
- target: '#reviewInitial',
691
- actions: 'copyStartInput',
692
- },
629
+ START_REVIEW: startReviewArm,
693
630
  },
694
631
  },
695
632
  done: {
696
633
  id: 'done',
697
- description: stateDescriptions.done,
698
- meta: terminalMeta('done', 'success'),
699
634
  type: 'final',
635
+ description: STATE_DESCRIPTIONS.done,
636
+ meta: finalMeta('done', 'success'),
700
637
  },
701
638
  },
702
- output: ({ context }) => {
703
- if (!isNonEmptyString(context.evaluatedRevision)) {
704
- throw new Error('REVIEW reached done without a receipt-observed evaluated revision');
705
- }
706
- return {
707
- noUnsettledFindings: true,
708
- evaluatedRevision: context.evaluatedRevision,
709
- };
710
- },
711
639
  });
640
+ export default reviewMachine;