@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.
- package/README.md +1 -1
- package/docs/embedding.md +9 -7
- package/package.json +3 -1
- package/reference/sdlc/branch.playbook/branch.fsm.d.ts +63 -33
- package/reference/sdlc/branch.playbook/branch.fsm.js +190 -108
- package/reference/sdlc/branch.playbook/branch.fsm.ts +269 -147
- package/reference/sdlc/branch.playbook/branch.gears.md +7 -17
- package/reference/sdlc/branch.playbook/branch.playbook.d.ts +12 -16
- package/reference/sdlc/branch.playbook/branch.playbook.js +112 -101
- package/reference/sdlc/branch.playbook/branch.playbook.ts +128 -171
- package/reference/sdlc/branch.playbook/branch.registry.d.ts +3 -2
- package/reference/sdlc/branch.playbook/branch.registry.ts +6 -2
- package/reference/sdlc/code.playbook/bin/repository-effects.js +54 -13
- package/reference/sdlc/code.playbook/code.fsm.d.ts +143 -90
- package/reference/sdlc/code.playbook/code.fsm.introspect.d.ts +8 -8
- package/reference/sdlc/code.playbook/code.fsm.introspect.ts +13 -13
- package/reference/sdlc/code.playbook/code.fsm.js +609 -649
- package/reference/sdlc/code.playbook/code.fsm.ts +890 -821
- package/reference/sdlc/code.playbook/code.gears.md +39 -29
- package/reference/sdlc/code.playbook/code.playbook.d.ts +13 -16
- package/reference/sdlc/code.playbook/code.playbook.js +166 -147
- package/reference/sdlc/code.playbook/code.playbook.ts +180 -229
- package/reference/sdlc/code.playbook/code.registry.d.ts +3 -2
- package/reference/sdlc/code.playbook/code.registry.ts +6 -2
- package/reference/sdlc/code.playbook/playbook-captain.d.ts +42 -0
- package/reference/sdlc/code.playbook/playbook-captain.js +829 -194
- package/reference/sdlc/code.playbook/playbook-captain.ts +1227 -220
- package/reference/sdlc/decide.md +1 -0
- package/reference/sdlc/decide.playbook/decide.fsm.d.ts +270 -154
- package/reference/sdlc/decide.playbook/decide.fsm.js +765 -732
- package/reference/sdlc/decide.playbook/decide.fsm.ts +1167 -945
- package/reference/sdlc/decide.playbook/decide.gears.md +28 -21
- package/reference/sdlc/decide.playbook/decide.playbook.d.ts +11 -67
- package/reference/sdlc/decide.playbook/decide.playbook.js +155 -3570
- package/reference/sdlc/decide.playbook/decide.playbook.ts +186 -5061
- package/reference/sdlc/decide.playbook/decide.registry.d.ts +8 -4
- package/reference/sdlc/decide.playbook/decide.registry.js +2 -2
- package/reference/sdlc/decide.playbook/decide.registry.ts +13 -6
- package/reference/sdlc/dev.playbook/dev.fsm.d.ts +113 -131
- package/reference/sdlc/dev.playbook/dev.fsm.js +517 -575
- package/reference/sdlc/dev.playbook/dev.fsm.ts +667 -787
- package/reference/sdlc/dev.playbook/dev.gears.md +52 -54
- package/reference/sdlc/dev.playbook/dev.playbook.d.ts +13 -10
- package/reference/sdlc/dev.playbook/dev.playbook.js +128 -120
- package/reference/sdlc/dev.playbook/dev.playbook.ts +143 -193
- package/reference/sdlc/dev.playbook/dev.registry.d.ts +3 -2
- package/reference/sdlc/dev.playbook/dev.registry.ts +6 -2
- package/reference/sdlc/pr.playbook/pr.fsm.d.ts +92 -85
- package/reference/sdlc/pr.playbook/pr.fsm.js +461 -518
- package/reference/sdlc/pr.playbook/pr.fsm.ts +533 -663
- package/reference/sdlc/pr.playbook/pr.gears.md +47 -63
- package/reference/sdlc/pr.playbook/pr.playbook.d.ts +13 -23
- package/reference/sdlc/pr.playbook/pr.playbook.js +118 -112
- package/reference/sdlc/pr.playbook/pr.playbook.ts +136 -190
- package/reference/sdlc/pr.playbook/pr.registry.d.ts +3 -2
- package/reference/sdlc/pr.playbook/pr.registry.ts +4 -2
- package/reference/sdlc/review.md +3 -3
- package/reference/sdlc/review.playbook/review.fsm.d.ts +150 -116
- package/reference/sdlc/review.playbook/review.fsm.js +479 -550
- package/reference/sdlc/review.playbook/review.fsm.ts +717 -662
- package/reference/sdlc/review.playbook/review.gears.md +32 -29
- package/reference/sdlc/review.playbook/review.playbook.d.ts +12 -12
- package/reference/sdlc/review.playbook/review.playbook.js +180 -155
- package/reference/sdlc/review.playbook/review.playbook.ts +190 -230
- package/reference/sdlc/review.playbook/review.registry.d.ts +6 -5
- package/reference/sdlc/review.playbook/review.registry.js +3 -3
- package/reference/sdlc/review.playbook/review.registry.ts +9 -5
- package/slc/gears2fsm.md +72 -3
- package/slc/link.md +50 -27
- package/slc/materialize-link.mjs +51 -13
- package/slc/optimize.md +4 -1
- package/slc/prefix-prompts.mjs +191 -0
- package/slc/prefix.md +203 -0
- package/slc/slc.pin-inputs.json +11 -1
- package/slc/text2gears.md +71 -11
- package/src/xstate-playbook-runtime.d.ts +82 -9
- package/src/xstate-playbook-runtime.js +1224 -283
- package/src/xstate-playbook-runtime.ts +1671 -338
- package/src/xstate-runtime.d.ts +3 -1
- package/src/xstate-runtime.js +9 -2
- 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
|
-
//
|
|
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
|
-
|
|
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
|
|
33
|
+
export type ReviewSourceItem = 'REVIEW-1' | 'REVIEW-2' | 'REVIEW-3' | 'REVIEW-4';
|
|
12
34
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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:
|
|
23
|
-
asker: { readonly kind: 'role'; readonly roleId:
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
|
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
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
|
|
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
|
|
54
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
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
|
-
|
|
90
|
-
|
|
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
|
-
|
|
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
|
|
112
|
-
'
|
|
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
|
-
'
|
|
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
|
-
'
|
|
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
|
-
|
|
226
|
+
'> Original request: <caller-input>',
|
|
227
|
+
'> Reviewer findings: <reviewer-output>',
|
|
119
228
|
].join('\n');
|
|
120
229
|
|
|
121
|
-
const
|
|
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
|
|
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
|
-
'
|
|
138
|
-
'
|
|
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
|
-
|
|
141
|
-
|
|
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
|
-
'
|
|
156
|
-
'
|
|
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
|
-
'
|
|
162
|
-
'
|
|
278
|
+
'> Original request: <caller-input>',
|
|
279
|
+
'> Coder output: <coder-output>',
|
|
163
280
|
].join('\n');
|
|
164
281
|
|
|
165
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
301
|
+
const LATER_REVIEW_RESULT = {
|
|
174
302
|
hasFindings:
|
|
175
|
-
|
|
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
|
-
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
|
190
|
-
|
|
191
|
-
|
|
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
|
|
205
|
-
|
|
206
|
-
|
|
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:
|
|
569
|
+
description: STATE_DESCRIPTIONS[stateId],
|
|
211
570
|
...(role === undefined ? {} : { role }),
|
|
212
571
|
},
|
|
213
572
|
});
|
|
214
573
|
|
|
215
|
-
|
|
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:
|
|
577
|
+
description: STATE_DESCRIPTIONS[stateId],
|
|
225
578
|
terminal,
|
|
226
579
|
},
|
|
227
580
|
});
|
|
228
581
|
|
|
229
|
-
|
|
230
|
-
|
|
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:
|
|
273
|
-
|
|
274
|
-
|
|
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
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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
|
-
|
|
373
|
-
|
|
374
|
-
|
|
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
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
638
|
+
coderOutput: output.coderOutput,
|
|
639
|
+
latestCommit: output.latestCommit,
|
|
640
|
+
coderOutcome: 'committed' as const,
|
|
641
|
+
lastError: undefined,
|
|
438
642
|
};
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
}),
|
|
442
|
-
setPendingAddressFindings: assign({
|
|
443
|
-
pendingBossQuestion: ({ event }) => {
|
|
444
|
-
const output = outputOf(event);
|
|
643
|
+
}
|
|
644
|
+
if (output?.guard === 'rejectedAll') {
|
|
445
645
|
return {
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
question: output.guard === 'needsBossReply' ? output.question : '',
|
|
646
|
+
coderOutput: output.coderOutput,
|
|
647
|
+
coderOutcome: 'rejectedAll' as const,
|
|
648
|
+
lastError: undefined,
|
|
450
649
|
};
|
|
451
|
-
}
|
|
452
|
-
|
|
650
|
+
}
|
|
651
|
+
return {};
|
|
453
652
|
}),
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
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
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
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
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
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
|
-
|
|
495
|
-
|
|
496
|
-
|
|
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
|
-
|
|
506
|
-
id: '
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
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 }):
|
|
513
|
-
stateId: '
|
|
514
|
-
sourceItem: 'REVIEW-1',
|
|
750
|
+
input: ({ context }): FirstReviewPlayerInput => ({
|
|
751
|
+
stateId: 'firstReview',
|
|
515
752
|
role: 'reviewer',
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
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
|
-
|
|
525
|
-
|
|
526
|
-
|
|
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
|
-
|
|
576
|
-
id: '
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
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 }):
|
|
583
|
-
stateId: '
|
|
584
|
-
sourceItem: 'REVIEW-2',
|
|
777
|
+
input: ({ context }): FixFindingsPlayerInput => ({
|
|
778
|
+
stateId: 'fixFindings',
|
|
585
779
|
role: 'coder',
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
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: '
|
|
596
|
-
target: '#
|
|
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
|
-
|
|
615
|
-
|
|
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: '
|
|
626
|
-
target: '#
|
|
802
|
+
guard: 'acceptRejectedAll',
|
|
803
|
+
target: '#reviewAfterRejection',
|
|
627
804
|
actions: [
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
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
|
-
|
|
647
|
-
id: '
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
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 }):
|
|
654
|
-
stateId: '
|
|
655
|
-
sourceItem: 'REVIEW-3',
|
|
823
|
+
input: ({ context }): ReviewAfterFixPlayerInput => ({
|
|
824
|
+
stateId: 'reviewAfterFix',
|
|
656
825
|
role: 'reviewer',
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
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
|
-
|
|
668
|
-
|
|
669
|
-
|
|
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
|
-
|
|
719
|
-
id: '
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
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 }):
|
|
726
|
-
stateId: '
|
|
727
|
-
sourceItem: 'REVIEW-4',
|
|
856
|
+
input: ({ context }): ReviewAfterRejectionPlayerInput => ({
|
|
857
|
+
stateId: 'reviewAfterRejection',
|
|
728
858
|
role: 'reviewer',
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
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
|
-
|
|
739
|
-
|
|
740
|
-
|
|
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
|
-
|
|
792
|
-
|
|
793
|
-
|
|
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: '
|
|
804
|
-
actions: '
|
|
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
|
-
|
|
836
|
-
|
|
837
|
-
|
|
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;
|