@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.
- package/README.md +2 -0
- package/docs/cli.md +36 -18
- package/docs/configuration.md +7 -5
- package/docs/embedding.md +28 -7
- package/package.json +4 -4
- 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/captain.md +19 -10
- package/reference/sdlc/captain.playbook/captain.fsm.d.ts +6 -2
- package/reference/sdlc/captain.playbook/captain.fsm.js +19 -7
- package/reference/sdlc/captain.playbook/captain.fsm.ts +25 -10
- package/reference/sdlc/captain.playbook/captain.gears.md +16 -8
- package/reference/sdlc/captain.playbook/captain.playbook.d.ts +1 -1
- package/reference/sdlc/captain.playbook/captain.playbook.js +3 -2
- package/reference/sdlc/captain.playbook/captain.playbook.ts +4 -3
- package/reference/sdlc/code.playbook/bin/interactive-session.js +14 -1
- package/reference/sdlc/code.playbook/bin/portable-codec.js +3 -0
- package/reference/sdlc/code.playbook/bin/repository-effects.js +1 -0
- package/reference/sdlc/code.playbook/bin/run.js +128 -85
- package/reference/sdlc/code.playbook/bin/session-host.js +40 -8
- package/reference/sdlc/code.playbook/bin/session-store.js +162 -34
- package/reference/sdlc/code.playbook/code.fsm.d.ts +146 -93
- 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/host-capabilities.d.ts +3 -0
- package/reference/sdlc/code.playbook/playbook-captain.d.ts +74 -2
- package/reference/sdlc/code.playbook/playbook-captain.js +1548 -468
- package/reference/sdlc/code.playbook/playbook-captain.ts +1993 -573
- package/reference/sdlc/code.playbook/session-host.d.ts +4 -1
- package/reference/sdlc/code.playbook/session-store.d.ts +17 -0
- package/reference/sdlc/code.playbook/session-store.js +1 -0
- package/reference/sdlc/decide.md +1 -0
- package/reference/sdlc/decide.playbook/decide.fsm.d.ts +273 -157
- 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 -59
- package/reference/sdlc/decide.playbook/decide.playbook.js +155 -3696
- package/reference/sdlc/decide.playbook/decide.playbook.ts +186 -5244
- 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 +116 -134
- 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 +95 -88
- 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 +77 -8
- package/slc/link.md +127 -60
- package/slc/materialize-link.mjs +51 -13
- package/slc/optimize.md +4 -1
- package/slc/prefix-prompts.mjs +29 -28
- package/slc/prefix.md +27 -24
- package/slc/slc.pin-inputs.json +11 -2
- package/slc/text2gears.md +71 -11
- package/src/runtime.d.ts +46 -2
- package/src/runtime.ts +42 -2
- package/src/xstate-playbook-runtime.d.ts +63 -23
- package/src/xstate-playbook-runtime.js +1637 -475
- package/src/xstate-playbook-runtime.ts +2105 -532
- package/src/xstate-runtime.d.ts +3 -4
- package/src/xstate-runtime.js +52 -0
- 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
|
-
//
|
|
4
|
-
//
|
|
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
|
-
|
|
7
|
-
const
|
|
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
|
|
26
|
-
'
|
|
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
|
-
'
|
|
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
|
-
'
|
|
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
|
-
|
|
55
|
+
'> Original request: <caller-input>',
|
|
56
|
+
'> Reviewer findings: <reviewer-output>',
|
|
33
57
|
].join('\n');
|
|
34
|
-
const
|
|
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
|
|
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
|
-
'
|
|
50
|
-
'
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
'
|
|
67
|
-
'
|
|
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
|
-
'
|
|
73
|
-
'
|
|
105
|
+
'> Original request: <caller-input>',
|
|
106
|
+
'> Coder output: <coder-output>',
|
|
74
107
|
].join('\n');
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
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
|
|
81
|
-
|
|
82
|
-
|
|
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
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
|
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:
|
|
307
|
+
description: STATE_DESCRIPTIONS[stateId],
|
|
104
308
|
...(role === undefined ? {} : { role }),
|
|
105
309
|
},
|
|
106
310
|
});
|
|
107
|
-
|
|
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:
|
|
314
|
+
description: STATE_DESCRIPTIONS[stateId],
|
|
114
315
|
terminal,
|
|
115
316
|
},
|
|
116
317
|
});
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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:
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
364
|
+
coderOutput: output.coderOutput,
|
|
365
|
+
latestCommit: output.latestCommit,
|
|
366
|
+
coderOutcome: 'committed',
|
|
367
|
+
lastError: undefined,
|
|
287
368
|
};
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
}),
|
|
291
|
-
setPendingAddressFindings: assign({
|
|
292
|
-
pendingBossQuestion: ({ event }) => {
|
|
293
|
-
const output = outputOf(event);
|
|
369
|
+
}
|
|
370
|
+
if (output?.guard === 'rejectedAll') {
|
|
294
371
|
return {
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
question: output.guard === 'needsBossReply' ? output.question : '',
|
|
372
|
+
coderOutput: output.coderOutput,
|
|
373
|
+
coderOutcome: 'rejectedAll',
|
|
374
|
+
lastError: undefined,
|
|
299
375
|
};
|
|
300
|
-
}
|
|
301
|
-
|
|
376
|
+
}
|
|
377
|
+
return {};
|
|
302
378
|
}),
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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
|
-
|
|
344
|
-
|
|
345
|
-
|
|
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
|
-
|
|
355
|
-
id: '
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
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: '
|
|
363
|
-
sourceItem: 'REVIEW-1',
|
|
473
|
+
stateId: 'firstReview',
|
|
364
474
|
role: 'reviewer',
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
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
|
-
|
|
374
|
-
|
|
375
|
-
|
|
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
|
-
|
|
425
|
-
id: '
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
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: '
|
|
433
|
-
sourceItem: 'REVIEW-2',
|
|
500
|
+
stateId: 'fixFindings',
|
|
434
501
|
role: 'coder',
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
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: '
|
|
445
|
-
target: '#
|
|
515
|
+
guard: 'acceptCommitted',
|
|
516
|
+
target: '#reviewAfterFix',
|
|
446
517
|
actions: [
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
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: '
|
|
460
|
-
target: '#
|
|
524
|
+
guard: 'acceptRejectedAll',
|
|
525
|
+
target: '#reviewAfterRejection',
|
|
461
526
|
actions: [
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
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
|
-
|
|
496
|
-
id: '
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
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: '
|
|
504
|
-
sourceItem: 'REVIEW-3',
|
|
546
|
+
stateId: 'reviewAfterFix',
|
|
505
547
|
role: 'reviewer',
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
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
|
-
|
|
517
|
-
|
|
518
|
-
|
|
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
|
-
|
|
568
|
-
id: '
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
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: '
|
|
576
|
-
sourceItem: 'REVIEW-4',
|
|
579
|
+
stateId: 'reviewAfterRejection',
|
|
577
580
|
role: 'reviewer',
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
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
|
-
|
|
588
|
-
|
|
589
|
-
|
|
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
|
-
|
|
641
|
-
|
|
642
|
-
|
|
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: '
|
|
653
|
-
actions: '
|
|
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
|
-
|
|
685
|
-
|
|
686
|
-
|
|
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;
|