@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,11 +1,17 @@
|
|
|
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 decide.gears.md.
|
|
5
|
-
//
|
|
6
|
-
//
|
|
3
|
+
//
|
|
4
|
+
// FSM object artifact compiled from ./decide.gears.md by gears2fsm.
|
|
5
|
+
// It declares only the machine, its actor contracts, and typed inputs; the
|
|
6
|
+
// linked runtime supplies the `player` and `playbook` actor implementations.
|
|
7
7
|
|
|
8
8
|
import { assign, fromPromise, setup } from 'xstate';
|
|
9
|
+
import { validatePlaybookCallResult } from '@sublang/playbook/xstate-runtime';
|
|
10
|
+
import type { PlaybookCallResult } from '@sublang/playbook/runtime';
|
|
11
|
+
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Shared JSON and error shapes (exact readonly variance of the shared boundary)
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
9
15
|
|
|
10
16
|
export type JsonValue =
|
|
11
17
|
| null
|
|
@@ -15,148 +21,116 @@ export type JsonValue =
|
|
|
15
21
|
| readonly JsonValue[]
|
|
16
22
|
| { readonly [key: string]: JsonValue };
|
|
17
23
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
type JumpableStateId = 'independentProposals';
|
|
23
|
-
|
|
24
|
-
type ResumableStateId =
|
|
25
|
-
| 'askCoderProposal'
|
|
26
|
-
| 'askReviewerProposal'
|
|
27
|
-
| 'commitCoderProposal';
|
|
28
|
-
|
|
29
|
-
export interface PendingBossQuestion {
|
|
30
|
-
questionId: ResumableStateId;
|
|
31
|
-
resumeStateId: ResumableStateId;
|
|
32
|
-
sourceItem: string;
|
|
33
|
-
asker: { kind: 'role'; roleId: Role };
|
|
34
|
-
question: string;
|
|
24
|
+
export interface CompactError {
|
|
25
|
+
readonly name: string;
|
|
26
|
+
readonly message: string;
|
|
35
27
|
}
|
|
36
28
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
type BossReplies = Partial<Record<ResumableStateId, string>>;
|
|
42
|
-
|
|
43
|
-
type PendingBossQuestionParams = Omit<PendingBossQuestion, 'questionId'>;
|
|
44
|
-
|
|
45
|
-
export interface ReviewSuccessOutput {
|
|
46
|
-
evaluatedRevision: string;
|
|
47
|
-
noUnsettledFindings: true;
|
|
29
|
+
export interface ErrorRecord {
|
|
30
|
+
readonly name: string;
|
|
31
|
+
readonly message: string;
|
|
32
|
+
readonly stack?: string;
|
|
48
33
|
}
|
|
49
34
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
noUnsettledFindings: true;
|
|
54
|
-
}
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// Identities
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
55
38
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
noUnsettledFindings: false;
|
|
59
|
-
reviewStatus: 'aborted' | 'error';
|
|
60
|
-
error?: {
|
|
61
|
-
name: string;
|
|
62
|
-
message: string;
|
|
63
|
-
};
|
|
64
|
-
}
|
|
39
|
+
/** Canonical lowercase local ids of the source roles Coder and Reviewer. */
|
|
40
|
+
export type DecideRoleId = 'coder' | 'reviewer';
|
|
65
41
|
|
|
66
|
-
export type
|
|
42
|
+
export type DecideSourceItem = 'DECIDE-1' | 'DECIDE-2' | 'DECIDE-3' | 'DECIDE-4';
|
|
67
43
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
message: string;
|
|
74
|
-
};
|
|
75
|
-
}
|
|
44
|
+
/** Delegated-player working leaves; each is its own Boss-reply resume target. */
|
|
45
|
+
export type ResumableStateId =
|
|
46
|
+
| 'askCoderProposal'
|
|
47
|
+
| 'askReviewerProposal'
|
|
48
|
+
| 'synthesizeCommit';
|
|
76
49
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
reviewResult?: ReviewSuccessOutput;
|
|
84
|
-
reviewFailure?: ChildFailure;
|
|
85
|
-
lastResult?: PlayerOutput;
|
|
86
|
-
lastError?: unknown;
|
|
87
|
-
pendingBossQuestions?: PendingBossQuestions;
|
|
88
|
-
bossReplies?: BossReplies;
|
|
89
|
-
stagedCoderResult?: PlayerOutput;
|
|
90
|
-
stagedReviewerResult?: PlayerOutput;
|
|
91
|
-
}
|
|
50
|
+
/**
|
|
51
|
+
* Root `BOSS_INTERRUPT` targets: the parallel proposal pair as one jumpable
|
|
52
|
+
* unit (DECIDE-1, DECIDE-2), and the synthesis leaf (DECIDE-3). Branch working
|
|
53
|
+
* leaves and waits are never interrupt targets.
|
|
54
|
+
*/
|
|
55
|
+
export type JumpableStateId = 'independentProposals' | 'synthesizeCommit';
|
|
92
56
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
| {
|
|
101
|
-
type: 'BOSS_REPLY';
|
|
102
|
-
questionId?: ResumableStateId;
|
|
103
|
-
answer: string;
|
|
104
|
-
};
|
|
57
|
+
/**
|
|
58
|
+
* One role-id array per parallel group, in first-item source order; the inner
|
|
59
|
+
* array follows the `independent-proposals` item order (DECIDE-1, DECIDE-2).
|
|
60
|
+
*/
|
|
61
|
+
export const concurrentRoleSets: readonly (readonly DecideRoleId[])[] = [
|
|
62
|
+
['coder', 'reviewer'],
|
|
63
|
+
] as const;
|
|
105
64
|
|
|
106
|
-
|
|
65
|
+
/** The literal nested-call target of DECIDE-4. */
|
|
66
|
+
const REVIEW_PLAYBOOK_ID = 'review' as const;
|
|
67
|
+
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
// Boss-reply suspension (keyed form: the machine has parallel player tasks)
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
107
71
|
|
|
108
|
-
export interface
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
callerTopic?: string;
|
|
115
|
-
reviewerProposal?: string;
|
|
116
|
-
pendingBossQuestion?: PendingBossQuestion;
|
|
117
|
-
bossReply?: string;
|
|
72
|
+
export interface PendingBossQuestion {
|
|
73
|
+
readonly questionId: ResumableStateId;
|
|
74
|
+
readonly resumeStateId: ResumableStateId;
|
|
75
|
+
readonly sourceItem: 'DECIDE-1' | 'DECIDE-2' | 'DECIDE-3';
|
|
76
|
+
readonly asker: { readonly kind: 'role'; readonly roleId: DecideRoleId };
|
|
77
|
+
readonly question: string;
|
|
118
78
|
}
|
|
119
79
|
|
|
120
|
-
type
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
coderOutput?: string;
|
|
124
|
-
latestCommit?: string;
|
|
125
|
-
question?: string;
|
|
126
|
-
readonly [key: string]: unknown;
|
|
127
|
-
};
|
|
80
|
+
export type PendingBossQuestions = Partial<
|
|
81
|
+
Record<ResumableStateId, PendingBossQuestion>
|
|
82
|
+
>;
|
|
128
83
|
|
|
129
|
-
export type
|
|
130
|
-
| ({ guard: 'proposed'; coderProposal: string } & AdditionalPlayerFields)
|
|
131
|
-
| ({ guard: 'proposed'; reviewerProposal: string } & AdditionalPlayerFields)
|
|
132
|
-
| ({ guard: 'committed'; coderOutput: string; latestCommit: string } &
|
|
133
|
-
AdditionalPlayerFields)
|
|
134
|
-
| ({ guard: 'needsBossReply'; question: string } & AdditionalPlayerFields);
|
|
84
|
+
export type BossReplies = Partial<Record<ResumableStateId, string>>;
|
|
135
85
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
sourceItem: 'DECIDE-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
86
|
+
const RESUMABLE_STATES = {
|
|
87
|
+
askCoderProposal: { sourceItem: 'DECIDE-1', roleId: 'coder' },
|
|
88
|
+
askReviewerProposal: { sourceItem: 'DECIDE-2', roleId: 'reviewer' },
|
|
89
|
+
synthesizeCommit: { sourceItem: 'DECIDE-3', roleId: 'coder' },
|
|
90
|
+
} as const satisfies Record<
|
|
91
|
+
ResumableStateId,
|
|
92
|
+
{
|
|
93
|
+
readonly sourceItem: PendingBossQuestion['sourceItem'];
|
|
94
|
+
readonly roleId: DecideRoleId;
|
|
95
|
+
}
|
|
96
|
+
>;
|
|
142
97
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
// Prompts (each item's blockquote, verbatim) and local result contracts
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
146
101
|
|
|
147
102
|
const NEEDS_BOSS_REPLY_DESCRIPTION =
|
|
148
103
|
"The acting agent's prose surfaces a clarifying question for Boss that the agent cannot answer alone. Output shall include `question: <verbatim question text from the acting agent's prose>`.";
|
|
149
104
|
|
|
150
|
-
|
|
151
|
-
|
|
105
|
+
/** DECIDE-1 blockquote; `<caller-topic>` is carried as `callerTopic`. */
|
|
106
|
+
const CODER_PROPOSAL_PROMPT = [
|
|
107
|
+
'Assess whether the topic is better expressed as a few spec items under @specs/packages/ or requires one or more DRs under @specs/decisions/.',
|
|
108
|
+
'Propose your design.',
|
|
109
|
+
'Keep your proposal coherent, focused, and concise.',
|
|
110
|
+
'Consult @specs/map.md for relevant context and @specs/meta.md for spec requirements, if needed.',
|
|
111
|
+
'Do not change any files.',
|
|
152
112
|
'',
|
|
113
|
+
'> Original topic: <caller-topic>',
|
|
114
|
+
].join('\n');
|
|
115
|
+
|
|
116
|
+
/** DECIDE-2 blockquote; `<caller-topic>` is carried as `callerTopic`. */
|
|
117
|
+
const REVIEWER_PROPOSAL_PROMPT = [
|
|
153
118
|
'Assess whether the topic is better expressed as a few spec items under @specs/packages/ or requires one or more DRs under @specs/decisions/.',
|
|
154
119
|
'Propose your design.',
|
|
155
120
|
'Keep your proposal coherent, focused, and concise.',
|
|
156
121
|
'Consult @specs/map.md for relevant context and @specs/meta.md for spec requirements, if needed.',
|
|
157
122
|
'Do not change any files.',
|
|
123
|
+
'',
|
|
124
|
+
'> Original topic: <caller-topic>',
|
|
158
125
|
].join('\n');
|
|
159
126
|
|
|
127
|
+
/**
|
|
128
|
+
* DECIDE-3 blockquote. `<caller-topic>` is carried as `callerTopic` and
|
|
129
|
+
* `<reviewer-proposal>` as `reviewerProposal`. `<coder-llm>` and
|
|
130
|
+
* `<reviewer-llm>` are local-role prompt identities of Coder and Reviewer: the
|
|
131
|
+
* linker resolves them through the invocation-scoped `promptIdentity(roleId)`
|
|
132
|
+
* lookup, so they stay literal here and have no machine or actor-input field.
|
|
133
|
+
*/
|
|
160
134
|
const SYNTHESIZE_COMMIT_PROMPT = [
|
|
161
135
|
"Synthesize your independent proposal with Reviewer's proposal below.",
|
|
162
136
|
'Keep to the original topic below and follow what it asks.',
|
|
@@ -174,362 +148,773 @@ const SYNTHESIZE_COMMIT_PROMPT = [
|
|
|
174
148
|
"> Reviewer's independent proposal: <reviewer-proposal>",
|
|
175
149
|
].join('\n');
|
|
176
150
|
|
|
177
|
-
const
|
|
151
|
+
const CODER_PROPOSAL_RESULT = {
|
|
152
|
+
proposed:
|
|
153
|
+
'Coder affirmatively provided a complete design proposal; a progress report, status update, or promise of a later proposal supports no proposal outcome. Output shall include `coderProposal: <verbatim final text>`.',
|
|
154
|
+
needsBossReply: NEEDS_BOSS_REPLY_DESCRIPTION,
|
|
155
|
+
} as const;
|
|
156
|
+
|
|
157
|
+
const REVIEWER_PROPOSAL_RESULT = {
|
|
158
|
+
proposed:
|
|
159
|
+
'Reviewer affirmatively provided a complete design proposal; a progress report, status update, or promise of a later proposal supports no proposal outcome. Output shall include `reviewerProposal: <verbatim final text>`.',
|
|
160
|
+
needsBossReply: NEEDS_BOSS_REPLY_DESCRIPTION,
|
|
161
|
+
} as const;
|
|
162
|
+
|
|
163
|
+
const SYNTHESIZE_COMMIT_RESULT = {
|
|
164
|
+
committed:
|
|
165
|
+
'Coder synthesized the proposals into the necessary DRs and/or spec items and committed the result as one new commit. Output shall include `coderOutput: <verbatim final text>` and `latestCommit: <commit identity>`.',
|
|
166
|
+
needsBossReply: NEEDS_BOSS_REPLY_DESCRIPTION,
|
|
167
|
+
} as const;
|
|
168
|
+
|
|
169
|
+
/** DECIDE-4 blockquote: the `review` call-text template, verbatim. */
|
|
170
|
+
const REVIEW_COMMIT_TEMPLATE = [
|
|
178
171
|
'> Original intent: <caller-topic>',
|
|
179
172
|
'> Review scope: the `decide`-owned commit <decide-commit> and its resulting repository state.',
|
|
173
|
+
"> Coder's independent proposal: <coder-proposal>",
|
|
180
174
|
'> Coder output: <coder-output>',
|
|
181
175
|
].join('\n');
|
|
182
176
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
'<decide-commit>': decideCommit ?? '',
|
|
191
|
-
'<coder-output>': coderOutput ?? '',
|
|
192
|
-
};
|
|
193
|
-
return REVIEW_INPUT_TEMPLATE.replace(
|
|
194
|
-
/<caller-topic>|<decide-commit>|<coder-output>/g,
|
|
195
|
-
(placeholder, offset: number, source: string) => {
|
|
196
|
-
const value = fields[placeholder];
|
|
197
|
-
const lineStart = source.lastIndexOf('\n', offset - 1) + 1;
|
|
198
|
-
return source.slice(lineStart, offset).startsWith('> ')
|
|
199
|
-
? value.replaceAll('\n', '\n> ')
|
|
200
|
-
: value;
|
|
201
|
-
},
|
|
202
|
-
);
|
|
177
|
+
// ---------------------------------------------------------------------------
|
|
178
|
+
// Delegated player actor contract
|
|
179
|
+
// ---------------------------------------------------------------------------
|
|
180
|
+
|
|
181
|
+
interface PlayerInputBase {
|
|
182
|
+
readonly pendingBossQuestion?: PendingBossQuestion;
|
|
183
|
+
readonly bossReply?: string;
|
|
203
184
|
}
|
|
204
185
|
|
|
205
|
-
|
|
206
|
-
|
|
186
|
+
/** DECIDE-1: relays `<caller-topic>` as `callerTopic`. */
|
|
187
|
+
export interface CoderProposalInput extends PlayerInputBase {
|
|
188
|
+
readonly stateId: 'askCoderProposal';
|
|
189
|
+
readonly role: 'coder';
|
|
190
|
+
readonly sourceItem: 'DECIDE-1';
|
|
191
|
+
readonly prompt: string;
|
|
192
|
+
readonly result: typeof CODER_PROPOSAL_RESULT;
|
|
193
|
+
/** `<caller-topic>`: the caller's complete topic. */
|
|
194
|
+
readonly callerTopic: string;
|
|
195
|
+
}
|
|
207
196
|
|
|
208
|
-
|
|
209
|
-
|
|
197
|
+
/** DECIDE-2: relays `<caller-topic>` as `callerTopic`. */
|
|
198
|
+
export interface ReviewerProposalInput extends PlayerInputBase {
|
|
199
|
+
readonly stateId: 'askReviewerProposal';
|
|
200
|
+
readonly role: 'reviewer';
|
|
201
|
+
readonly sourceItem: 'DECIDE-2';
|
|
202
|
+
readonly prompt: string;
|
|
203
|
+
readonly result: typeof REVIEWER_PROPOSAL_RESULT;
|
|
204
|
+
/** `<caller-topic>`: the caller's complete topic. */
|
|
205
|
+
readonly callerTopic: string;
|
|
206
|
+
}
|
|
210
207
|
|
|
211
|
-
|
|
212
|
-
|
|
208
|
+
/**
|
|
209
|
+
* DECIDE-3: relays `<caller-topic>` as `callerTopic` and
|
|
210
|
+
* `<reviewer-proposal>` as `reviewerProposal`.
|
|
211
|
+
*/
|
|
212
|
+
export interface SynthesizeCommitInput extends PlayerInputBase {
|
|
213
|
+
readonly stateId: 'synthesizeCommit';
|
|
214
|
+
readonly role: 'coder';
|
|
215
|
+
readonly sourceItem: 'DECIDE-3';
|
|
216
|
+
readonly prompt: string;
|
|
217
|
+
readonly result: typeof SYNTHESIZE_COMMIT_RESULT;
|
|
218
|
+
/** `<caller-topic>`: the caller's complete topic. */
|
|
219
|
+
readonly callerTopic: string;
|
|
220
|
+
/** `<reviewer-proposal>`: Reviewer's complete independent proposal. */
|
|
221
|
+
readonly reviewerProposal: string;
|
|
222
|
+
}
|
|
213
223
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
224
|
+
export type PlayerInput =
|
|
225
|
+
| CoderProposalInput
|
|
226
|
+
| ReviewerProposalInput
|
|
227
|
+
| SynthesizeCommitInput;
|
|
218
228
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
);
|
|
224
|
-
};
|
|
229
|
+
export interface NeedsBossReplyOutput {
|
|
230
|
+
readonly guard: 'needsBossReply';
|
|
231
|
+
readonly question: string;
|
|
232
|
+
}
|
|
225
233
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
234
|
+
export type CoderProposalOutput =
|
|
235
|
+
| { readonly guard: 'proposed'; readonly coderProposal: string }
|
|
236
|
+
| NeedsBossReplyOutput;
|
|
237
|
+
|
|
238
|
+
export type ReviewerProposalOutput =
|
|
239
|
+
| { readonly guard: 'proposed'; readonly reviewerProposal: string }
|
|
240
|
+
| NeedsBossReplyOutput;
|
|
241
|
+
|
|
242
|
+
export type SynthesizeCommitOutput =
|
|
243
|
+
| {
|
|
244
|
+
readonly guard: 'committed';
|
|
245
|
+
readonly coderOutput: string;
|
|
246
|
+
/** Effect-owned: the runtime fills it from the repository receipt. */
|
|
247
|
+
readonly latestCommit: string;
|
|
248
|
+
}
|
|
249
|
+
| NeedsBossReplyOutput;
|
|
250
|
+
|
|
251
|
+
export type PlayerOutput =
|
|
252
|
+
| CoderProposalOutput
|
|
253
|
+
| ReviewerProposalOutput
|
|
254
|
+
| SynthesizeCommitOutput;
|
|
255
|
+
|
|
256
|
+
// ---------------------------------------------------------------------------
|
|
257
|
+
// Nested playbook actor contract
|
|
258
|
+
// ---------------------------------------------------------------------------
|
|
259
|
+
|
|
260
|
+
/** DECIDE-4: literal call of the builtin `review` playbook. */
|
|
261
|
+
export interface PlaybookInput {
|
|
262
|
+
readonly stateId: 'reviewCommit';
|
|
263
|
+
readonly sourceItem?: 'DECIDE-4';
|
|
264
|
+
readonly playbookId: typeof REVIEW_PLAYBOOK_ID;
|
|
265
|
+
readonly text: string;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** A successful call yields the child's own JSON-safe machine output. */
|
|
269
|
+
export type PlaybookOutput = JsonValue | undefined;
|
|
270
|
+
|
|
271
|
+
/** Public output interface of the packaged builtin `review`. */
|
|
272
|
+
export interface ReviewOutput {
|
|
273
|
+
readonly noUnsettledFindings: true;
|
|
274
|
+
readonly evaluatedRevision: string;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** Sanitized completed-result evidence of the `review` call that ended DECIDE. */
|
|
278
|
+
export type CompletedReviewResult =
|
|
279
|
+
| {
|
|
280
|
+
readonly playbookId: typeof REVIEW_PLAYBOOK_ID;
|
|
281
|
+
readonly status: 'ok';
|
|
282
|
+
readonly output?: JsonValue;
|
|
283
|
+
}
|
|
284
|
+
| {
|
|
285
|
+
readonly playbookId: typeof REVIEW_PLAYBOOK_ID;
|
|
286
|
+
readonly status: 'aborted' | 'error';
|
|
287
|
+
readonly error: CompactError;
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
// ---------------------------------------------------------------------------
|
|
291
|
+
// Machine input, context, events, and output
|
|
292
|
+
// ---------------------------------------------------------------------------
|
|
293
|
+
|
|
294
|
+
/** The machine reads no input: the caller's topic arrives on `START_DECIDE`. */
|
|
295
|
+
export type DecideInput = Readonly<Record<string, never>>;
|
|
296
|
+
|
|
297
|
+
export type ReviewStatus = 'aborted' | 'error';
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Typed run context. A text field holds `''` until its producer runs; every
|
|
301
|
+
* transition that enters a consumer guards the fields it reads as non-empty.
|
|
302
|
+
*/
|
|
303
|
+
export interface DecideContext {
|
|
304
|
+
/** `<caller-topic>`: set by `START_DECIDE` or a restarting interrupt. */
|
|
305
|
+
readonly callerTopic: string;
|
|
306
|
+
/** Branch-staged proposals; the parallel join promotes them atomically. */
|
|
307
|
+
readonly stagedCoderProposal: string;
|
|
308
|
+
readonly stagedReviewerProposal: string;
|
|
309
|
+
/** `<coder-proposal>`: Coder's promoted independent proposal. */
|
|
310
|
+
readonly coderProposal: string;
|
|
311
|
+
/** `<reviewer-proposal>`: Reviewer's promoted independent proposal. */
|
|
312
|
+
readonly reviewerProposal: string;
|
|
313
|
+
/** `<coder-output>`: Coder's verbatim final synthesis text. */
|
|
314
|
+
readonly coderOutput: string;
|
|
315
|
+
/** `<decide-commit>`: DECIDE-3's accepted, receipt-owned `latestCommit`. */
|
|
316
|
+
readonly decideCommit: string;
|
|
317
|
+
/** Revision evaluated by the passing `review`. */
|
|
318
|
+
readonly evaluatedRevision: string;
|
|
319
|
+
/** Reported status when `review` did not pass the commit. */
|
|
320
|
+
readonly reviewStatus: ReviewStatus | null;
|
|
321
|
+
/** Compact failure DECIDE reports when `review` did not pass the commit. */
|
|
322
|
+
readonly reviewError: CompactError | null;
|
|
323
|
+
/** Sanitized evidence of the `review` result that ended DECIDE. */
|
|
324
|
+
readonly reviewEvidence: CompletedReviewResult | null;
|
|
325
|
+
readonly lastError: ErrorRecord | null;
|
|
326
|
+
readonly pendingBossQuestions: PendingBossQuestions;
|
|
327
|
+
readonly bossReplies: BossReplies;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export type DecideEvent =
|
|
331
|
+
| { readonly type: 'START_DECIDE'; readonly callerTopic: string }
|
|
332
|
+
| {
|
|
333
|
+
readonly type: 'BOSS_INTERRUPT';
|
|
334
|
+
readonly targetId: 'independentProposals';
|
|
335
|
+
/** The new topic both proposal players receive. */
|
|
336
|
+
readonly callerTopic: string;
|
|
337
|
+
}
|
|
338
|
+
| {
|
|
339
|
+
readonly type: 'BOSS_INTERRUPT';
|
|
340
|
+
readonly targetId: 'synthesizeCommit';
|
|
341
|
+
}
|
|
342
|
+
| {
|
|
343
|
+
readonly type: 'BOSS_REPLY';
|
|
344
|
+
readonly questionId: string;
|
|
345
|
+
readonly answer: string;
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
/** Exactly the catalog's public `decide` output interface. */
|
|
349
|
+
export type DecideOutput =
|
|
350
|
+
| {
|
|
351
|
+
readonly decideCommit: string;
|
|
352
|
+
readonly evaluatedRevision: string;
|
|
353
|
+
readonly noUnsettledFindings: true;
|
|
354
|
+
}
|
|
355
|
+
| {
|
|
356
|
+
readonly lastDecideCommit: string;
|
|
357
|
+
readonly noUnsettledFindings: false;
|
|
358
|
+
readonly reviewStatus: ReviewStatus;
|
|
359
|
+
readonly error?: CompactError;
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
// ---------------------------------------------------------------------------
|
|
363
|
+
// State descriptions and public metadata
|
|
364
|
+
// ---------------------------------------------------------------------------
|
|
365
|
+
|
|
366
|
+
const STATE_DESCRIPTIONS = {
|
|
367
|
+
ready: 'Waiting for the caller to give a topic to decide.',
|
|
368
|
+
independentProposals:
|
|
369
|
+
'Coder and Reviewer independently propose designs for the topic in parallel.',
|
|
370
|
+
coderProposalRegion: "Coder's independent proposal branch.",
|
|
371
|
+
askCoderProposal: 'Coder is independently proposing a design for the topic.',
|
|
372
|
+
awaitCoderProposalReply:
|
|
373
|
+
"Waiting for Boss to answer Coder's question about its independent proposal.",
|
|
374
|
+
coderProposalStaged:
|
|
375
|
+
"Coder's independent proposal is complete and staged for the join.",
|
|
376
|
+
reviewerProposalRegion: "Reviewer's independent proposal branch.",
|
|
377
|
+
askReviewerProposal:
|
|
378
|
+
'Reviewer is independently proposing a design for the topic.',
|
|
379
|
+
awaitReviewerProposalReply:
|
|
380
|
+
"Waiting for Boss to answer Reviewer's question about its independent proposal.",
|
|
381
|
+
reviewerProposalStaged:
|
|
382
|
+
"Reviewer's independent proposal is complete and staged for the join.",
|
|
383
|
+
synthesizeCommit:
|
|
384
|
+
'Coder synthesizes both proposals into the necessary DRs and/or spec items and commits the result as one new commit.',
|
|
385
|
+
awaitBossReply: "Waiting for Boss to answer the acting agent's question.",
|
|
386
|
+
reviewCommit: 'The review playbook examines the decide-owned commit.',
|
|
387
|
+
failed:
|
|
388
|
+
'DECIDE parked after a control-plane failure and waits for Boss to restart or resume it.',
|
|
389
|
+
reportedReviewFailure:
|
|
390
|
+
"DECIDE reported review's abort, failure, or unestablished result to its caller with the last decide-owned commit.",
|
|
391
|
+
done: 'DECIDE completed: review established no unsettled findings for the decide-owned commit at the reported evaluated revision.',
|
|
392
|
+
} as const;
|
|
393
|
+
|
|
394
|
+
type DescribedStateId = keyof typeof STATE_DESCRIPTIONS;
|
|
395
|
+
|
|
396
|
+
function playbookMeta(stateId: DescribedStateId, role?: DecideRoleId) {
|
|
237
397
|
return {
|
|
238
|
-
|
|
239
|
-
|
|
398
|
+
playbook: {
|
|
399
|
+
stateId,
|
|
400
|
+
description: STATE_DESCRIPTIONS[stateId],
|
|
401
|
+
...(role === undefined ? {} : { role }),
|
|
402
|
+
},
|
|
240
403
|
};
|
|
241
404
|
}
|
|
242
405
|
|
|
243
|
-
|
|
244
|
-
|
|
406
|
+
function terminalMeta(
|
|
407
|
+
stateId: 'reportedReviewFailure' | 'done',
|
|
408
|
+
terminal: 'success' | 'failure',
|
|
409
|
+
) {
|
|
410
|
+
return {
|
|
411
|
+
playbook: {
|
|
412
|
+
stateId,
|
|
413
|
+
description: STATE_DESCRIPTIONS[stateId],
|
|
414
|
+
terminal,
|
|
415
|
+
},
|
|
416
|
+
};
|
|
417
|
+
}
|
|
245
418
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
419
|
+
// ---------------------------------------------------------------------------
|
|
420
|
+
// Structural narrowing helpers
|
|
421
|
+
// ---------------------------------------------------------------------------
|
|
422
|
+
|
|
423
|
+
function isObject(value: unknown): value is Readonly<Record<string, unknown>> {
|
|
424
|
+
return typeof value === 'object' && value !== null;
|
|
425
|
+
}
|
|
250
426
|
|
|
251
|
-
|
|
252
|
-
|
|
427
|
+
function isPlainRecord(
|
|
428
|
+
value: unknown,
|
|
429
|
+
): value is Readonly<Record<string, unknown>> {
|
|
430
|
+
if (!isObject(value) || Array.isArray(value)) return false;
|
|
431
|
+
const prototype = Object.getPrototypeOf(value);
|
|
432
|
+
return prototype === Object.prototype || prototype === null;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function isNonEmptyString(value: unknown): value is string {
|
|
436
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function isResumableStateId(value: unknown): value is ResumableStateId {
|
|
253
440
|
return (
|
|
254
|
-
|
|
441
|
+
value === 'askCoderProposal' ||
|
|
442
|
+
value === 'askReviewerProposal' ||
|
|
443
|
+
value === 'synthesizeCommit'
|
|
255
444
|
);
|
|
256
|
-
}
|
|
445
|
+
}
|
|
257
446
|
|
|
258
|
-
|
|
259
|
-
|
|
447
|
+
/** The actor's `output`, narrowed structurally from an unknown done event. */
|
|
448
|
+
function actorOutput(
|
|
449
|
+
event: unknown,
|
|
450
|
+
): Readonly<Record<string, unknown>> | undefined {
|
|
451
|
+
if (!isObject(event)) return undefined;
|
|
452
|
+
const output = event.output;
|
|
453
|
+
return isPlainRecord(output) ? output : undefined;
|
|
454
|
+
}
|
|
260
455
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
if (event.type !== 'BOSS_REPLY') return undefined;
|
|
266
|
-
if (event.questionId) return event.questionId;
|
|
267
|
-
const ids = pendingQuestionIds(context);
|
|
268
|
-
return ids.length === 1 ? ids[0] : undefined;
|
|
269
|
-
};
|
|
456
|
+
function outputString(event: unknown, field: string): string | undefined {
|
|
457
|
+
const value = actorOutput(event)?.[field];
|
|
458
|
+
return isNonEmptyString(value) ? value : undefined;
|
|
459
|
+
}
|
|
270
460
|
|
|
271
|
-
|
|
272
|
-
(
|
|
273
|
-
|
|
274
|
-
event.type === 'BOSS_REPLY' &&
|
|
275
|
-
resolvedQuestionId(context, event) === stateId &&
|
|
276
|
-
(requireAnswer
|
|
277
|
-
? event.answer.trim().length > 0
|
|
278
|
-
: event.answer.trim().length === 0);
|
|
461
|
+
function outputGuardIs(event: unknown, guard: PlayerOutput['guard']): boolean {
|
|
462
|
+
return actorOutput(event)?.guard === guard;
|
|
463
|
+
}
|
|
279
464
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
465
|
+
/** The actor's `error`, narrowed structurally from an unknown error event. */
|
|
466
|
+
function actorError(event: unknown): unknown {
|
|
467
|
+
return isObject(event) ? event.error : undefined;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/** Normalizes any thrown or malformed value to a JSON-safe error record. */
|
|
471
|
+
function errorRecord(value: unknown): ErrorRecord {
|
|
472
|
+
if (value instanceof Error) {
|
|
473
|
+
return {
|
|
474
|
+
name: isNonEmptyString(value.name) ? value.name : 'Error',
|
|
475
|
+
message: typeof value.message === 'string' ? value.message : '',
|
|
476
|
+
...(typeof value.stack === 'string' ? { stack: value.stack } : {}),
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
if (isObject(value) && typeof value.message === 'string') {
|
|
480
|
+
return {
|
|
481
|
+
name: isNonEmptyString(value.name) ? value.name : 'Error',
|
|
482
|
+
message: value.message,
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
return {
|
|
486
|
+
name: 'Error',
|
|
487
|
+
message:
|
|
488
|
+
typeof value === 'string'
|
|
489
|
+
? value
|
|
490
|
+
: 'The actor failed with a non-Error value.',
|
|
491
|
+
};
|
|
492
|
+
}
|
|
291
493
|
|
|
292
|
-
|
|
293
|
-
record: Partial<Record<ResumableStateId,
|
|
494
|
+
function withoutKey<Value>(
|
|
495
|
+
record: Partial<Record<ResumableStateId, Value>>,
|
|
294
496
|
stateId: ResumableStateId,
|
|
295
|
-
): Partial<Record<ResumableStateId,
|
|
296
|
-
|
|
297
|
-
const
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
497
|
+
): Partial<Record<ResumableStateId, Value>> {
|
|
498
|
+
const next: Partial<Record<ResumableStateId, Value>> = {};
|
|
499
|
+
for (const key of Object.keys(record)) {
|
|
500
|
+
if (!isResumableStateId(key) || key === stateId) continue;
|
|
501
|
+
const value = record[key];
|
|
502
|
+
if (value !== undefined) next[key] = value;
|
|
503
|
+
}
|
|
504
|
+
return next;
|
|
505
|
+
}
|
|
301
506
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
507
|
+
/**
|
|
508
|
+
* Strict JSON validation: null, booleans, finite numbers, strings, exact
|
|
509
|
+
* arrays, and plain records of enumerable own data properties. Cycle
|
|
510
|
+
* detection tracks only the active recursion path.
|
|
511
|
+
*/
|
|
512
|
+
function isJsonValue(
|
|
513
|
+
value: unknown,
|
|
514
|
+
path: Set<object> = new Set(),
|
|
515
|
+
): value is JsonValue {
|
|
516
|
+
if (value === null) return true;
|
|
517
|
+
switch (typeof value) {
|
|
518
|
+
case 'boolean':
|
|
519
|
+
case 'string':
|
|
520
|
+
return true;
|
|
521
|
+
case 'number':
|
|
522
|
+
return Number.isFinite(value);
|
|
523
|
+
case 'object':
|
|
524
|
+
break;
|
|
525
|
+
default:
|
|
526
|
+
return false;
|
|
527
|
+
}
|
|
528
|
+
const container = value as object;
|
|
529
|
+
if (path.has(container)) return false;
|
|
530
|
+
path.add(container);
|
|
531
|
+
try {
|
|
532
|
+
if (Array.isArray(container)) {
|
|
533
|
+
if (Object.getPrototypeOf(container) !== Array.prototype) return false;
|
|
534
|
+
const keys = Reflect.ownKeys(container);
|
|
535
|
+
if (keys.length !== container.length + 1) return false;
|
|
536
|
+
const lengthDescriptor = Object.getOwnPropertyDescriptor(
|
|
537
|
+
container,
|
|
538
|
+
'length',
|
|
539
|
+
);
|
|
540
|
+
if (
|
|
541
|
+
lengthDescriptor === undefined ||
|
|
542
|
+
lengthDescriptor.configurable !== false ||
|
|
543
|
+
lengthDescriptor.enumerable !== false ||
|
|
544
|
+
lengthDescriptor.value !== container.length
|
|
545
|
+
) {
|
|
546
|
+
return false;
|
|
547
|
+
}
|
|
548
|
+
for (let index = 0; index < container.length; index += 1) {
|
|
549
|
+
const descriptor = Object.getOwnPropertyDescriptor(
|
|
550
|
+
container,
|
|
551
|
+
String(index),
|
|
552
|
+
);
|
|
553
|
+
if (
|
|
554
|
+
descriptor === undefined ||
|
|
555
|
+
!descriptor.enumerable ||
|
|
556
|
+
!('value' in descriptor) ||
|
|
557
|
+
!isJsonValue(descriptor.value, path)
|
|
558
|
+
) {
|
|
559
|
+
return false;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
return true;
|
|
563
|
+
}
|
|
564
|
+
const prototype = Object.getPrototypeOf(container);
|
|
565
|
+
if (prototype !== Object.prototype && prototype !== null) return false;
|
|
566
|
+
for (const key of Reflect.ownKeys(container)) {
|
|
567
|
+
if (typeof key !== 'string') return false;
|
|
568
|
+
const descriptor = Object.getOwnPropertyDescriptor(container, key);
|
|
569
|
+
if (
|
|
570
|
+
descriptor === undefined ||
|
|
571
|
+
!descriptor.enumerable ||
|
|
572
|
+
!('value' in descriptor) ||
|
|
573
|
+
!isJsonValue(descriptor.value, path)
|
|
574
|
+
) {
|
|
575
|
+
return false;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
return true;
|
|
579
|
+
} finally {
|
|
580
|
+
path.delete(container);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// ---------------------------------------------------------------------------
|
|
585
|
+
// Actor-output narrowing (declared per-state result contracts)
|
|
586
|
+
// ---------------------------------------------------------------------------
|
|
306
587
|
|
|
307
|
-
|
|
308
|
-
typeof value === 'object' &&
|
|
309
|
-
value !== null &&
|
|
310
|
-
!Array.isArray(value) &&
|
|
311
|
-
(Object.getPrototypeOf(value) === Object.prototype ||
|
|
312
|
-
Object.getPrototypeOf(value) === null);
|
|
313
|
-
|
|
314
|
-
const isExactKeys = (
|
|
315
|
-
value: Record<string, unknown>,
|
|
316
|
-
keys: readonly string[],
|
|
317
|
-
): boolean => {
|
|
318
|
-
const ownKeys = Reflect.ownKeys(value);
|
|
588
|
+
function isCoderProposed(event: unknown): boolean {
|
|
319
589
|
return (
|
|
320
|
-
|
|
321
|
-
|
|
590
|
+
outputGuardIs(event, 'proposed') &&
|
|
591
|
+
outputString(event, 'coderProposal') !== undefined
|
|
322
592
|
);
|
|
323
|
-
}
|
|
593
|
+
}
|
|
324
594
|
|
|
325
|
-
function
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
return Reflect.ownKeys(value).every(
|
|
330
|
-
(key) =>
|
|
331
|
-
typeof key === 'string' &&
|
|
332
|
-
Object.prototype.propertyIsEnumerable.call(value, key) &&
|
|
333
|
-
isStateValue(value[key], next),
|
|
595
|
+
function isReviewerProposed(event: unknown): boolean {
|
|
596
|
+
return (
|
|
597
|
+
outputGuardIs(event, 'proposed') &&
|
|
598
|
+
outputString(event, 'reviewerProposal') !== undefined
|
|
334
599
|
);
|
|
335
600
|
}
|
|
336
601
|
|
|
337
|
-
function
|
|
602
|
+
function isCommitted(event: unknown): boolean {
|
|
338
603
|
return (
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
if (key === 'length') return true;
|
|
343
|
-
return (
|
|
344
|
-
typeof key === 'string' &&
|
|
345
|
-
/^(0|[1-9][0-9]*)$/.test(key) &&
|
|
346
|
-
Number(key) < value.length
|
|
347
|
-
);
|
|
348
|
-
}) &&
|
|
349
|
-
value.every((entry) => isNonEmptyString(entry)) &&
|
|
350
|
-
new Set(value).size === value.length
|
|
604
|
+
outputGuardIs(event, 'committed') &&
|
|
605
|
+
outputString(event, 'coderOutput') !== undefined &&
|
|
606
|
+
outputString(event, 'latestCommit') !== undefined
|
|
351
607
|
);
|
|
352
608
|
}
|
|
353
609
|
|
|
354
|
-
function
|
|
355
|
-
if (!isPlainRecord(value)) return false;
|
|
356
|
-
const allowed = new Set([
|
|
357
|
-
'value',
|
|
358
|
-
'activeStateIds',
|
|
359
|
-
'tags',
|
|
360
|
-
'status',
|
|
361
|
-
'quiescent',
|
|
362
|
-
'stateId',
|
|
363
|
-
]);
|
|
364
|
-
if (
|
|
365
|
-
Reflect.ownKeys(value).some(
|
|
366
|
-
(key) => typeof key !== 'string' || !allowed.has(key),
|
|
367
|
-
) ||
|
|
368
|
-
!Object.prototype.hasOwnProperty.call(value, 'value') ||
|
|
369
|
-
!Object.prototype.hasOwnProperty.call(value, 'activeStateIds') ||
|
|
370
|
-
!Object.prototype.hasOwnProperty.call(value, 'tags') ||
|
|
371
|
-
!Object.prototype.hasOwnProperty.call(value, 'status') ||
|
|
372
|
-
!Object.prototype.hasOwnProperty.call(value, 'quiescent')
|
|
373
|
-
) {
|
|
374
|
-
return false;
|
|
375
|
-
}
|
|
610
|
+
function isNeedsBossReply(event: unknown): boolean {
|
|
376
611
|
return (
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
isStringArray(value.tags) &&
|
|
380
|
-
(value.status === 'active' ||
|
|
381
|
-
value.status === 'done' ||
|
|
382
|
-
value.status === 'error' ||
|
|
383
|
-
value.status === 'stopped') &&
|
|
384
|
-
typeof value.quiescent === 'boolean' &&
|
|
385
|
-
(!Object.prototype.hasOwnProperty.call(value, 'stateId') ||
|
|
386
|
-
(isNonEmptyString(value.stateId) &&
|
|
387
|
-
value.activeStateIds.length === 1 &&
|
|
388
|
-
value.activeStateIds[0] === value.stateId))
|
|
612
|
+
outputGuardIs(event, 'needsBossReply') &&
|
|
613
|
+
outputString(event, 'question') !== undefined
|
|
389
614
|
);
|
|
390
615
|
}
|
|
391
616
|
|
|
392
|
-
//
|
|
393
|
-
//
|
|
394
|
-
//
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
617
|
+
// ---------------------------------------------------------------------------
|
|
618
|
+
// Nested `review` results
|
|
619
|
+
// ---------------------------------------------------------------------------
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* Recognizes an authored rejected child result: a validated public result
|
|
623
|
+
* whose status is `aborted` or `error`, or `ok` at the child's own authored
|
|
624
|
+
* failure terminal. Anything else is a control-plane error.
|
|
625
|
+
*/
|
|
626
|
+
export function authoredChildResult(
|
|
627
|
+
error: unknown,
|
|
628
|
+
expectedPlaybookId: string,
|
|
629
|
+
): PlaybookCallResult | undefined {
|
|
630
|
+
if (!(error instanceof Error)) return undefined;
|
|
631
|
+
try {
|
|
632
|
+
const result = validatePlaybookCallResult(
|
|
633
|
+
(error as Error & { result?: unknown }).result,
|
|
634
|
+
expectedPlaybookId,
|
|
635
|
+
);
|
|
636
|
+
return result.status !== 'ok' || result.terminal?.kind === 'failure'
|
|
637
|
+
? result
|
|
638
|
+
: undefined;
|
|
639
|
+
} catch {
|
|
400
640
|
return undefined;
|
|
401
641
|
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* DECIDE-4's acceptance predicate on the builtin `review` public interface:
|
|
646
|
+
* the result gives the exact evaluated repository revision and affirmatively
|
|
647
|
+
* establishes that no unsettled findings remain. The shared bridge correlates
|
|
648
|
+
* the result with the supplied review scope.
|
|
649
|
+
*/
|
|
650
|
+
function reviewApprovalFrom(event: unknown): ReviewOutput | undefined {
|
|
651
|
+
const output = actorOutput(event);
|
|
652
|
+
if (output === undefined) return undefined;
|
|
402
653
|
if (
|
|
403
|
-
|
|
404
|
-
output.
|
|
654
|
+
output.noUnsettledFindings !== true ||
|
|
655
|
+
!isNonEmptyString(output.evaluatedRevision)
|
|
405
656
|
) {
|
|
406
657
|
return undefined;
|
|
407
658
|
}
|
|
408
659
|
return {
|
|
409
|
-
evaluatedRevision: output.evaluatedRevision,
|
|
410
660
|
noUnsettledFindings: true,
|
|
661
|
+
evaluatedRevision: output.evaluatedRevision,
|
|
411
662
|
};
|
|
412
663
|
}
|
|
413
664
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
if (!isPlainRecord(result)) return undefined;
|
|
421
|
-
const allowed = new Set([
|
|
422
|
-
'status',
|
|
423
|
-
'playbookId',
|
|
424
|
-
'childSessionId',
|
|
425
|
-
'state',
|
|
426
|
-
'error',
|
|
427
|
-
]);
|
|
428
|
-
if (
|
|
429
|
-
Reflect.ownKeys(result).some(
|
|
430
|
-
(key) => typeof key !== 'string' || !allowed.has(key),
|
|
431
|
-
) ||
|
|
432
|
-
(result.status !== 'aborted' && result.status !== 'error') ||
|
|
433
|
-
result.playbookId !== 'review' ||
|
|
434
|
-
Object.prototype.hasOwnProperty.call(result, 'output')
|
|
435
|
-
) {
|
|
436
|
-
return undefined;
|
|
437
|
-
}
|
|
438
|
-
if (
|
|
439
|
-
Object.prototype.hasOwnProperty.call(result, 'childSessionId') &&
|
|
440
|
-
!isNonEmptyString(result.childSessionId)
|
|
441
|
-
) {
|
|
442
|
-
return undefined;
|
|
443
|
-
}
|
|
444
|
-
if (
|
|
445
|
-
Object.prototype.hasOwnProperty.call(result, 'state') &&
|
|
446
|
-
!isPlaybookState(result.state)
|
|
447
|
-
) {
|
|
448
|
-
return undefined;
|
|
449
|
-
}
|
|
450
|
-
let normalizedError: ChildFailure['error'];
|
|
451
|
-
if (Object.prototype.hasOwnProperty.call(result, 'error')) {
|
|
452
|
-
if (!isPlainRecord(result.error)) return undefined;
|
|
453
|
-
const errorKeys = Reflect.ownKeys(result.error);
|
|
454
|
-
if (
|
|
455
|
-
errorKeys.some(
|
|
456
|
-
(key) =>
|
|
457
|
-
typeof key !== 'string' ||
|
|
458
|
-
(key !== 'name' && key !== 'message' && key !== 'stack'),
|
|
459
|
-
) ||
|
|
460
|
-
!Object.prototype.hasOwnProperty.call(result.error, 'name') ||
|
|
461
|
-
!Object.prototype.hasOwnProperty.call(result.error, 'message')
|
|
462
|
-
) {
|
|
463
|
-
return undefined;
|
|
464
|
-
}
|
|
465
|
-
if (
|
|
466
|
-
!isNonEmptyString(result.error.name) ||
|
|
467
|
-
typeof result.error.message !== 'string' ||
|
|
468
|
-
(Object.prototype.hasOwnProperty.call(result.error, 'stack') &&
|
|
469
|
-
typeof result.error.stack !== 'string')
|
|
470
|
-
) {
|
|
471
|
-
return undefined;
|
|
665
|
+
/** Reported status, compact error, and evidence of an authored review failure. */
|
|
666
|
+
function authoredReviewFailureOf(event: unknown):
|
|
667
|
+
| {
|
|
668
|
+
readonly status: ReviewStatus;
|
|
669
|
+
readonly error: CompactError;
|
|
670
|
+
readonly evidence: CompletedReviewResult;
|
|
472
671
|
}
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
672
|
+
| undefined {
|
|
673
|
+
const result = authoredChildResult(actorError(event), REVIEW_PLAYBOOK_ID);
|
|
674
|
+
if (result === undefined) return undefined;
|
|
675
|
+
if (result.status === 'ok') {
|
|
676
|
+
// `ok` at review's own authored failure terminal: a review failure whose
|
|
677
|
+
// own output is relayed as evidence.
|
|
678
|
+
const description = result.terminal?.description;
|
|
679
|
+
return {
|
|
680
|
+
status: 'error',
|
|
681
|
+
error: {
|
|
682
|
+
name: 'ReviewFailed',
|
|
683
|
+
message: isNonEmptyString(description)
|
|
684
|
+
? `review ended at its authored failure terminal: ${description}`
|
|
685
|
+
: 'review ended at its authored failure terminal.',
|
|
686
|
+
},
|
|
687
|
+
evidence: {
|
|
688
|
+
playbookId: REVIEW_PLAYBOOK_ID,
|
|
689
|
+
status: 'ok',
|
|
690
|
+
...(result.output === undefined ? {} : { output: result.output }),
|
|
691
|
+
},
|
|
476
692
|
};
|
|
477
|
-
} else if (result.status === 'error') {
|
|
478
|
-
return undefined;
|
|
479
693
|
}
|
|
694
|
+
const error: CompactError =
|
|
695
|
+
result.error !== undefined
|
|
696
|
+
? { name: result.error.name, message: result.error.message }
|
|
697
|
+
: { name: 'AbortError', message: 'The review call was aborted.' };
|
|
480
698
|
return {
|
|
481
699
|
status: result.status,
|
|
482
|
-
|
|
483
|
-
|
|
700
|
+
error,
|
|
701
|
+
evidence: { playbookId: REVIEW_PLAYBOOK_ID, status: result.status, error },
|
|
484
702
|
};
|
|
485
703
|
}
|
|
486
704
|
|
|
487
|
-
const
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
705
|
+
const REVIEW_NOT_ESTABLISHED: CompactError = {
|
|
706
|
+
name: 'ReviewNotEstablished',
|
|
707
|
+
message:
|
|
708
|
+
'review returned a terminal result that does not establish the exact evaluated repository revision with no unsettled findings.',
|
|
709
|
+
};
|
|
710
|
+
|
|
711
|
+
// ---------------------------------------------------------------------------
|
|
712
|
+
// Nested call text composition (DECIDE-4 quoted relays)
|
|
713
|
+
// ---------------------------------------------------------------------------
|
|
714
|
+
|
|
715
|
+
const REVIEW_PLACEHOLDER =
|
|
716
|
+
/<caller-topic>|<decide-commit>|<coder-proposal>|<coder-output>/g;
|
|
717
|
+
|
|
718
|
+
/**
|
|
719
|
+
* Composes the `review` call text from the verbatim template, one line at a
|
|
720
|
+
* time: a quoted relay line whose value is empty contributes no line, and a
|
|
721
|
+
* multi-line value is quoted line by line. The composer adds no empty quoted
|
|
722
|
+
* line of its own.
|
|
723
|
+
*/
|
|
724
|
+
function reviewCallText(context: DecideContext): string {
|
|
725
|
+
const values: Readonly<Record<string, string>> = {
|
|
726
|
+
'<caller-topic>': context.callerTopic,
|
|
727
|
+
'<decide-commit>': context.decideCommit,
|
|
728
|
+
'<coder-proposal>': context.coderProposal,
|
|
729
|
+
'<coder-output>': context.coderOutput,
|
|
730
|
+
};
|
|
731
|
+
const lines: string[] = [];
|
|
732
|
+
for (const line of REVIEW_COMMIT_TEMPLATE.split('\n')) {
|
|
733
|
+
const tokens = line.match(REVIEW_PLACEHOLDER) ?? [];
|
|
734
|
+
if (tokens.some((token) => values[token] === '')) continue;
|
|
735
|
+
lines.push(
|
|
736
|
+
line.replace(REVIEW_PLACEHOLDER, (token: string) =>
|
|
737
|
+
(values[token] ?? token).split('\n').join('\n> '),
|
|
738
|
+
),
|
|
739
|
+
);
|
|
740
|
+
}
|
|
741
|
+
return lines.join('\n');
|
|
501
742
|
}
|
|
502
743
|
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
744
|
+
// ---------------------------------------------------------------------------
|
|
745
|
+
// Player input mapping (pure reads of typed, entry-validated context)
|
|
746
|
+
// ---------------------------------------------------------------------------
|
|
747
|
+
|
|
748
|
+
function bossReplyFields(
|
|
749
|
+
context: DecideContext,
|
|
750
|
+
stateId: ResumableStateId,
|
|
751
|
+
): PlayerInputBase {
|
|
752
|
+
const pendingBossQuestion = context.pendingBossQuestions[stateId];
|
|
753
|
+
const bossReply = context.bossReplies[stateId];
|
|
754
|
+
return {
|
|
755
|
+
...(pendingBossQuestion === undefined ? {} : { pendingBossQuestion }),
|
|
756
|
+
...(bossReply === undefined ? {} : { bossReply }),
|
|
757
|
+
};
|
|
513
758
|
}
|
|
514
759
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
760
|
+
// ---------------------------------------------------------------------------
|
|
761
|
+
// Context snapshots
|
|
762
|
+
// ---------------------------------------------------------------------------
|
|
763
|
+
|
|
764
|
+
const INITIAL_CONTEXT: DecideContext = {
|
|
765
|
+
callerTopic: '',
|
|
766
|
+
stagedCoderProposal: '',
|
|
767
|
+
stagedReviewerProposal: '',
|
|
768
|
+
coderProposal: '',
|
|
769
|
+
reviewerProposal: '',
|
|
770
|
+
coderOutput: '',
|
|
771
|
+
decideCommit: '',
|
|
772
|
+
evaluatedRevision: '',
|
|
773
|
+
reviewStatus: null,
|
|
774
|
+
reviewError: null,
|
|
775
|
+
reviewEvidence: null,
|
|
776
|
+
lastError: null,
|
|
777
|
+
pendingBossQuestions: {},
|
|
778
|
+
bossReplies: {},
|
|
779
|
+
};
|
|
780
|
+
|
|
781
|
+
/**
|
|
782
|
+
* A fresh run for a topic: clears every staged and promoted proposal, the
|
|
783
|
+
* commit, review evidence, errors, and every pending Boss question and reply.
|
|
784
|
+
*/
|
|
785
|
+
function freshRun(callerTopic: string): DecideContext {
|
|
786
|
+
return { ...INITIAL_CONTEXT, callerTopic };
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
/** Context reset shared by every exit into `failed`. */
|
|
790
|
+
const FAILURE_RESET = {
|
|
791
|
+
pendingBossQuestions: {},
|
|
792
|
+
bossReplies: {},
|
|
793
|
+
stagedCoderProposal: '',
|
|
794
|
+
stagedReviewerProposal: '',
|
|
795
|
+
} as const;
|
|
796
|
+
|
|
797
|
+
/**
|
|
798
|
+
* A synthesis restart keeps the topic and the promoted proposals, and clears
|
|
799
|
+
* the prior commit, review evidence, error, and every pending question.
|
|
800
|
+
*/
|
|
801
|
+
const SYNTHESIS_RESTART = {
|
|
802
|
+
coderOutput: '',
|
|
803
|
+
decideCommit: '',
|
|
804
|
+
evaluatedRevision: '',
|
|
805
|
+
reviewStatus: null,
|
|
806
|
+
reviewError: null,
|
|
807
|
+
reviewEvidence: null,
|
|
808
|
+
lastError: null,
|
|
809
|
+
pendingBossQuestions: {},
|
|
810
|
+
bossReplies: {},
|
|
811
|
+
} as const;
|
|
812
|
+
|
|
813
|
+
// ---------------------------------------------------------------------------
|
|
814
|
+
// Transition helpers
|
|
815
|
+
// ---------------------------------------------------------------------------
|
|
816
|
+
|
|
817
|
+
/**
|
|
818
|
+
* Accepted-outcome marker (artifact schema 3). `target` names the public
|
|
819
|
+
* state the next snapshot shows after the transition's macrostep.
|
|
820
|
+
*/
|
|
821
|
+
function acceptedOutcome(
|
|
822
|
+
source: ResumableStateId,
|
|
823
|
+
target: DescribedStateId,
|
|
824
|
+
outcome: 'proposed' | 'committed' | 'needsBossReply',
|
|
825
|
+
) {
|
|
826
|
+
return {
|
|
827
|
+
type: 'playbook.acceptedOutcome',
|
|
828
|
+
params: { source, target, acceptedOutcome: outcome },
|
|
829
|
+
} as const;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
/** Root `BOSS_INTERRUPT` arms, one per jumpable state. */
|
|
833
|
+
function bossInterrupts<const Ids extends readonly JumpableStateId[]>(
|
|
834
|
+
ids: Ids,
|
|
835
|
+
) {
|
|
836
|
+
return ids.map(
|
|
837
|
+
(targetId) =>
|
|
838
|
+
({
|
|
839
|
+
guard: { type: 'interruptTargets', params: { targetId } },
|
|
840
|
+
target: `#${targetId}`,
|
|
841
|
+
reenter: true,
|
|
842
|
+
actions: 'restartFromInterrupt',
|
|
843
|
+
}) as const,
|
|
844
|
+
);
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
/** Keyed `BOSS_REPLY` arms of the wait that resumes the given working leaves. */
|
|
848
|
+
function resumableStates<const Ids extends readonly ResumableStateId[]>(
|
|
849
|
+
ids: Ids,
|
|
850
|
+
) {
|
|
851
|
+
return ids.flatMap((stateId) => [
|
|
852
|
+
{
|
|
853
|
+
guard: { type: 'bossReplyIsEmpty', params: { stateId } },
|
|
854
|
+
target: '#failed',
|
|
855
|
+
actions: 'rememberEmptyBossReply',
|
|
856
|
+
} as const,
|
|
857
|
+
{
|
|
858
|
+
guard: { type: 'bossReplyResumes', params: { stateId } },
|
|
859
|
+
target: `#${stateId}`,
|
|
860
|
+
reenter: true,
|
|
861
|
+
actions: { type: 'rememberBossReply', params: { stateId } },
|
|
862
|
+
} as const,
|
|
863
|
+
]);
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
/**
|
|
867
|
+
* At the root synthesis wait, where no other task is active, a keyed
|
|
868
|
+
* `BOSS_REPLY` that names no pending question fails the run.
|
|
869
|
+
*/
|
|
870
|
+
const unknownBossReplyArm = {
|
|
871
|
+
guard: 'bossReplyNamesNoPendingQuestion',
|
|
872
|
+
target: '#failed',
|
|
873
|
+
actions: 'rememberUnknownBossReply',
|
|
874
|
+
} as const;
|
|
875
|
+
|
|
876
|
+
const startDecideArm = {
|
|
877
|
+
guard: 'validStartTopic',
|
|
878
|
+
target: '#independentProposals',
|
|
879
|
+
actions: 'startDecide',
|
|
880
|
+
} as const;
|
|
881
|
+
|
|
882
|
+
function suspendArm(
|
|
883
|
+
stateId: ResumableStateId,
|
|
884
|
+
wait: 'awaitCoderProposalReply' | 'awaitReviewerProposalReply' | 'awaitBossReply',
|
|
885
|
+
) {
|
|
886
|
+
return {
|
|
887
|
+
guard: 'needsBossReply',
|
|
888
|
+
target: `#${wait}`,
|
|
889
|
+
actions: [
|
|
890
|
+
acceptedOutcome(stateId, wait, 'needsBossReply'),
|
|
891
|
+
{ type: 'setPendingBossQuestion', params: { stateId } },
|
|
892
|
+
],
|
|
893
|
+
} as const;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
function malformedPlayerOutputArm(sourceItem: DecideSourceItem) {
|
|
897
|
+
return {
|
|
898
|
+
target: '#failed',
|
|
899
|
+
actions: { type: 'rememberMalformedPlayerOutput', params: { sourceItem } },
|
|
900
|
+
} as const;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
const playerOnError = {
|
|
904
|
+
target: '#failed',
|
|
905
|
+
actions: 'rememberActorError',
|
|
906
|
+
} as const;
|
|
907
|
+
|
|
908
|
+
type EventArgs = { readonly event: unknown };
|
|
909
|
+
type ContextEventArgs = {
|
|
910
|
+
readonly context: DecideContext;
|
|
911
|
+
readonly event: unknown;
|
|
531
912
|
};
|
|
532
913
|
|
|
914
|
+
// ---------------------------------------------------------------------------
|
|
915
|
+
// Machine
|
|
916
|
+
// ---------------------------------------------------------------------------
|
|
917
|
+
|
|
533
918
|
export const decideMachine = setup({
|
|
534
919
|
types: {
|
|
535
920
|
context: {} as DecideContext,
|
|
@@ -541,10 +926,75 @@ export const decideMachine = setup({
|
|
|
541
926
|
player: fromPromise<PlayerOutput, PlayerInput>(async () => {
|
|
542
927
|
throw new Error('player actor must be provided by the runner');
|
|
543
928
|
}),
|
|
544
|
-
playbook: fromPromise<
|
|
929
|
+
playbook: fromPromise<PlaybookOutput, PlaybookInput>(async () => {
|
|
545
930
|
throw new Error('playbook actor must be provided by the runner');
|
|
546
931
|
}),
|
|
547
932
|
},
|
|
933
|
+
guards: {
|
|
934
|
+
validStartTopic: ({ event }) =>
|
|
935
|
+
event.type === 'START_DECIDE' && isNonEmptyString(event.callerTopic),
|
|
936
|
+
// Each arm guards its target id and every typed precondition for
|
|
937
|
+
// entering that target safely.
|
|
938
|
+
interruptTargets: (
|
|
939
|
+
{ context, event },
|
|
940
|
+
params: { readonly targetId: JumpableStateId },
|
|
941
|
+
) => {
|
|
942
|
+
if (event.type !== 'BOSS_INTERRUPT') return false;
|
|
943
|
+
if (event.targetId !== params.targetId) return false;
|
|
944
|
+
if (event.targetId === 'independentProposals') {
|
|
945
|
+
return isNonEmptyString(event.callerTopic);
|
|
946
|
+
}
|
|
947
|
+
return (
|
|
948
|
+
isNonEmptyString(context.callerTopic) &&
|
|
949
|
+
isNonEmptyString(context.coderProposal) &&
|
|
950
|
+
isNonEmptyString(context.reviewerProposal)
|
|
951
|
+
);
|
|
952
|
+
},
|
|
953
|
+
// A branch completing after its sibling has staged its proposal also
|
|
954
|
+
// completes the join, so its accepted outcome lands on `synthesizeCommit`.
|
|
955
|
+
coderProposedJoining: ({ context, event }: ContextEventArgs) =>
|
|
956
|
+
isCoderProposed(event) &&
|
|
957
|
+
isNonEmptyString(context.stagedReviewerProposal) &&
|
|
958
|
+
isNonEmptyString(context.callerTopic),
|
|
959
|
+
coderProposed: ({ event }: EventArgs) => isCoderProposed(event),
|
|
960
|
+
reviewerProposedJoining: ({ context, event }: ContextEventArgs) =>
|
|
961
|
+
isReviewerProposed(event) &&
|
|
962
|
+
isNonEmptyString(context.stagedCoderProposal) &&
|
|
963
|
+
isNonEmptyString(context.callerTopic),
|
|
964
|
+
reviewerProposed: ({ event }: EventArgs) => isReviewerProposed(event),
|
|
965
|
+
committed: ({ context, event }: ContextEventArgs) =>
|
|
966
|
+
isCommitted(event) &&
|
|
967
|
+
isNonEmptyString(context.callerTopic) &&
|
|
968
|
+
isNonEmptyString(context.coderProposal),
|
|
969
|
+
needsBossReply: ({ event }: EventArgs) => isNeedsBossReply(event),
|
|
970
|
+
reviewApproved: ({ context, event }: ContextEventArgs) =>
|
|
971
|
+
isNonEmptyString(context.decideCommit) &&
|
|
972
|
+
reviewApprovalFrom(event) !== undefined,
|
|
973
|
+
authoredReviewFailure: ({ context, event }: ContextEventArgs) =>
|
|
974
|
+
isNonEmptyString(context.decideCommit) &&
|
|
975
|
+
authoredReviewFailureOf(event) !== undefined,
|
|
976
|
+
bossReplyNamesNoPendingQuestion: ({ context, event }) =>
|
|
977
|
+
event.type === 'BOSS_REPLY' &&
|
|
978
|
+
(!isResumableStateId(event.questionId) ||
|
|
979
|
+
context.pendingBossQuestions[event.questionId] === undefined),
|
|
980
|
+
bossReplyIsEmpty: (
|
|
981
|
+
{ context, event },
|
|
982
|
+
params: { readonly stateId: ResumableStateId },
|
|
983
|
+
) =>
|
|
984
|
+
event.type === 'BOSS_REPLY' &&
|
|
985
|
+
event.questionId === params.stateId &&
|
|
986
|
+
context.pendingBossQuestions[params.stateId] !== undefined &&
|
|
987
|
+
!isNonEmptyString(event.answer),
|
|
988
|
+
bossReplyResumes: (
|
|
989
|
+
{ context, event },
|
|
990
|
+
params: { readonly stateId: ResumableStateId },
|
|
991
|
+
) =>
|
|
992
|
+
event.type === 'BOSS_REPLY' &&
|
|
993
|
+
event.questionId === params.stateId &&
|
|
994
|
+
context.pendingBossQuestions[params.stateId] !== undefined &&
|
|
995
|
+
isNonEmptyString(event.answer) &&
|
|
996
|
+
isNonEmptyString(context.callerTopic),
|
|
997
|
+
},
|
|
548
998
|
actions: {
|
|
549
999
|
'playbook.acceptedOutcome': (
|
|
550
1000
|
_args,
|
|
@@ -554,702 +1004,474 @@ export const decideMachine = setup({
|
|
|
554
1004
|
readonly acceptedOutcome: string;
|
|
555
1005
|
},
|
|
556
1006
|
) => undefined,
|
|
557
|
-
|
|
558
|
-
callerTopic:
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
lastError: undefined,
|
|
568
|
-
pendingBossQuestions: undefined,
|
|
569
|
-
bossReplies: undefined,
|
|
570
|
-
stagedCoderResult: undefined,
|
|
571
|
-
stagedReviewerResult: undefined,
|
|
572
|
-
}),
|
|
573
|
-
copyInterruptedTopic: assign({
|
|
574
|
-
callerTopic: ({ event }) =>
|
|
575
|
-
event.type === 'BOSS_INTERRUPT' ? event.bossIntent : undefined,
|
|
576
|
-
coderProposal: undefined,
|
|
577
|
-
reviewerProposal: undefined,
|
|
578
|
-
latestCommit: undefined,
|
|
579
|
-
coderOutput: undefined,
|
|
580
|
-
reviewResult: undefined,
|
|
581
|
-
reviewFailure: undefined,
|
|
582
|
-
lastResult: undefined,
|
|
583
|
-
lastError: undefined,
|
|
584
|
-
pendingBossQuestions: undefined,
|
|
585
|
-
bossReplies: undefined,
|
|
586
|
-
stagedCoderResult: undefined,
|
|
587
|
-
stagedReviewerResult: undefined,
|
|
588
|
-
}),
|
|
589
|
-
stageCoderProposal: assign({
|
|
590
|
-
stagedCoderResult: ({ event }) => outputOf(event),
|
|
591
|
-
lastResult: ({ event }) => outputOf(event),
|
|
592
|
-
lastError: undefined,
|
|
593
|
-
}),
|
|
594
|
-
stageReviewerProposal: assign({
|
|
595
|
-
stagedReviewerResult: ({ event }) => outputOf(event),
|
|
596
|
-
lastResult: ({ event }) => outputOf(event),
|
|
597
|
-
lastError: undefined,
|
|
598
|
-
}),
|
|
599
|
-
promoteProposals: assign({
|
|
600
|
-
coderProposal: ({ context }) =>
|
|
601
|
-
context.stagedCoderResult?.coderProposal,
|
|
602
|
-
reviewerProposal: ({ context }) =>
|
|
603
|
-
context.stagedReviewerResult?.reviewerProposal,
|
|
604
|
-
stagedCoderResult: undefined,
|
|
605
|
-
stagedReviewerResult: undefined,
|
|
606
|
-
lastResult: undefined,
|
|
607
|
-
lastError: undefined,
|
|
608
|
-
}),
|
|
609
|
-
rememberCommit: assign({
|
|
610
|
-
latestCommit: ({ event }) => outputOf(event).latestCommit,
|
|
611
|
-
coderOutput: ({ event }) => outputOf(event).coderOutput,
|
|
612
|
-
lastResult: ({ event }) => outputOf(event),
|
|
613
|
-
lastError: undefined,
|
|
1007
|
+
startDecide: assign(({ event }) =>
|
|
1008
|
+
event.type === 'START_DECIDE' ? freshRun(event.callerTopic) : {},
|
|
1009
|
+
),
|
|
1010
|
+
// A new topic restarts the whole proposal pair from a fresh run; a
|
|
1011
|
+
// synthesis restart keeps the topic and the promoted proposals.
|
|
1012
|
+
restartFromInterrupt: assign(({ event }) => {
|
|
1013
|
+
if (event.type !== 'BOSS_INTERRUPT') return {};
|
|
1014
|
+
return event.targetId === 'independentProposals'
|
|
1015
|
+
? freshRun(event.callerTopic)
|
|
1016
|
+
: SYNTHESIS_RESTART;
|
|
614
1017
|
}),
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
})
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
1018
|
+
// Each branch assigns only its own staged result.
|
|
1019
|
+
stageCoderProposal: assign(({ context, event }) => ({
|
|
1020
|
+
stagedCoderProposal: outputString(event, 'coderProposal') ?? '',
|
|
1021
|
+
pendingBossQuestions: withoutKey(
|
|
1022
|
+
context.pendingBossQuestions,
|
|
1023
|
+
'askCoderProposal',
|
|
1024
|
+
),
|
|
1025
|
+
bossReplies: withoutKey(context.bossReplies, 'askCoderProposal'),
|
|
1026
|
+
})),
|
|
1027
|
+
stageReviewerProposal: assign(({ context, event }) => ({
|
|
1028
|
+
stagedReviewerProposal: outputString(event, 'reviewerProposal') ?? '',
|
|
1029
|
+
pendingBossQuestions: withoutKey(
|
|
1030
|
+
context.pendingBossQuestions,
|
|
1031
|
+
'askReviewerProposal',
|
|
1032
|
+
),
|
|
1033
|
+
bossReplies: withoutKey(context.bossReplies, 'askReviewerProposal'),
|
|
1034
|
+
})),
|
|
1035
|
+
// The join promotes both staged proposals atomically.
|
|
1036
|
+
promoteProposals: assign(({ context }) => ({
|
|
1037
|
+
coderProposal: context.stagedCoderProposal,
|
|
1038
|
+
reviewerProposal: context.stagedReviewerProposal,
|
|
1039
|
+
stagedCoderProposal: '',
|
|
1040
|
+
stagedReviewerProposal: '',
|
|
1041
|
+
pendingBossQuestions: {},
|
|
1042
|
+
bossReplies: {},
|
|
1043
|
+
})),
|
|
1044
|
+
rememberCommit: assign(({ context, event }) => ({
|
|
1045
|
+
coderOutput: outputString(event, 'coderOutput') ?? '',
|
|
1046
|
+
decideCommit: outputString(event, 'latestCommit') ?? '',
|
|
1047
|
+
lastError: null,
|
|
1048
|
+
pendingBossQuestions: withoutKey(
|
|
1049
|
+
context.pendingBossQuestions,
|
|
1050
|
+
'synthesizeCommit',
|
|
1051
|
+
),
|
|
1052
|
+
bossReplies: withoutKey(context.bossReplies, 'synthesizeCommit'),
|
|
1053
|
+
})),
|
|
1054
|
+
setPendingBossQuestion: assign(
|
|
1055
|
+
(
|
|
1056
|
+
{ context, event },
|
|
1057
|
+
params: { readonly stateId: ResumableStateId },
|
|
1058
|
+
) => {
|
|
1059
|
+
const question = outputString(event, 'question');
|
|
1060
|
+
if (question === undefined) return {};
|
|
1061
|
+
const { sourceItem, roleId } = RESUMABLE_STATES[params.stateId];
|
|
1062
|
+
const pending: PendingBossQuestion = {
|
|
1063
|
+
questionId: params.stateId,
|
|
1064
|
+
resumeStateId: params.stateId,
|
|
1065
|
+
sourceItem,
|
|
1066
|
+
asker: { kind: 'role', roleId },
|
|
1067
|
+
question,
|
|
1068
|
+
};
|
|
1069
|
+
return {
|
|
1070
|
+
pendingBossQuestions: {
|
|
1071
|
+
...context.pendingBossQuestions,
|
|
1072
|
+
[params.stateId]: pending,
|
|
1073
|
+
},
|
|
1074
|
+
bossReplies: withoutKey(context.bossReplies, params.stateId),
|
|
1075
|
+
};
|
|
634
1076
|
},
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
lastError: ({ event }) =>
|
|
639
|
-
(event as unknown as ActorErrorEvent).error,
|
|
640
|
-
}),
|
|
641
|
-
rememberMalformedActorOutput: assign({
|
|
642
|
-
lastError: ({ event }) =>
|
|
643
|
-
(event as unknown as PlayerDoneEvent | PlaybookDoneEvent).output,
|
|
644
|
-
}),
|
|
645
|
-
rememberMalformedBossReply: assign({
|
|
646
|
-
lastError: ({ event }) => event,
|
|
647
|
-
}),
|
|
648
|
-
setPendingBossQuestion: assign({
|
|
649
|
-
pendingBossQuestions: (
|
|
1077
|
+
),
|
|
1078
|
+
rememberBossReply: assign(
|
|
1079
|
+
(
|
|
650
1080
|
{ context, event },
|
|
651
|
-
params:
|
|
652
|
-
) =>
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
lastError:
|
|
1081
|
+
params: { readonly stateId: ResumableStateId },
|
|
1082
|
+
) =>
|
|
1083
|
+
event.type === 'BOSS_REPLY'
|
|
1084
|
+
? {
|
|
1085
|
+
bossReplies: {
|
|
1086
|
+
...context.bossReplies,
|
|
1087
|
+
[params.stateId]: event.answer,
|
|
1088
|
+
},
|
|
1089
|
+
}
|
|
1090
|
+
: {},
|
|
1091
|
+
),
|
|
1092
|
+
rememberEmptyBossReply: assign({
|
|
1093
|
+
lastError: {
|
|
1094
|
+
name: 'MalformedBossReply',
|
|
1095
|
+
message: 'BOSS_REPLY carried an empty answer.',
|
|
1096
|
+
},
|
|
1097
|
+
...FAILURE_RESET,
|
|
664
1098
|
}),
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
? { ...context.bossReplies, [questionId]: event.answer }
|
|
670
|
-
: context.bossReplies;
|
|
1099
|
+
rememberUnknownBossReply: assign({
|
|
1100
|
+
lastError: {
|
|
1101
|
+
name: 'MalformedBossReply',
|
|
1102
|
+
message: 'BOSS_REPLY names no pending question.',
|
|
671
1103
|
},
|
|
672
|
-
|
|
1104
|
+
...FAILURE_RESET,
|
|
673
1105
|
}),
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
1106
|
+
rememberMalformedPlayerOutput: assign(
|
|
1107
|
+
({ event }, params: { readonly sourceItem: DecideSourceItem }) => {
|
|
1108
|
+
const output = actorOutput(event);
|
|
1109
|
+
return {
|
|
1110
|
+
lastError: {
|
|
1111
|
+
name: 'MalformedPlayerOutput',
|
|
1112
|
+
message:
|
|
1113
|
+
output?.guard === 'needsBossReply'
|
|
1114
|
+
? `Player output for ${params.sourceItem} declared needsBossReply without a question.`
|
|
1115
|
+
: `Player output for ${params.sourceItem} did not match an outcome declared for its state.`,
|
|
1116
|
+
},
|
|
1117
|
+
...FAILURE_RESET,
|
|
1118
|
+
};
|
|
1119
|
+
},
|
|
1120
|
+
),
|
|
1121
|
+
rememberActorError: assign(({ event }) => ({
|
|
1122
|
+
lastError: errorRecord(actorError(event)),
|
|
1123
|
+
...FAILURE_RESET,
|
|
1124
|
+
})),
|
|
1125
|
+
rememberReviewApproval: assign(({ event }) => {
|
|
1126
|
+
const approval = reviewApprovalFrom(event);
|
|
1127
|
+
if (approval === undefined) return {};
|
|
1128
|
+
const output = actorOutput(event);
|
|
1129
|
+
const reviewEvidence: CompletedReviewResult = {
|
|
1130
|
+
playbookId: REVIEW_PLAYBOOK_ID,
|
|
1131
|
+
status: 'ok',
|
|
1132
|
+
...(isJsonValue(output) ? { output } : {}),
|
|
1133
|
+
};
|
|
1134
|
+
return {
|
|
1135
|
+
evaluatedRevision: approval.evaluatedRevision,
|
|
1136
|
+
reviewStatus: null,
|
|
1137
|
+
reviewError: null,
|
|
1138
|
+
reviewEvidence,
|
|
1139
|
+
lastError: null,
|
|
1140
|
+
};
|
|
681
1141
|
}),
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
1142
|
+
rememberUnestablishedReview: assign(({ event }) => {
|
|
1143
|
+
const output = actorOutput(event);
|
|
1144
|
+
const reviewEvidence: CompletedReviewResult = {
|
|
1145
|
+
playbookId: REVIEW_PLAYBOOK_ID,
|
|
1146
|
+
status: 'ok',
|
|
1147
|
+
...(isJsonValue(output) ? { output } : {}),
|
|
1148
|
+
};
|
|
1149
|
+
return {
|
|
1150
|
+
evaluatedRevision: '',
|
|
1151
|
+
reviewStatus: 'error' as const,
|
|
1152
|
+
reviewError: REVIEW_NOT_ESTABLISHED,
|
|
1153
|
+
reviewEvidence,
|
|
1154
|
+
lastError: null,
|
|
1155
|
+
};
|
|
687
1156
|
}),
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
1157
|
+
rememberAuthoredReviewFailure: assign(({ event }) => {
|
|
1158
|
+
const failure = authoredReviewFailureOf(event);
|
|
1159
|
+
return failure === undefined
|
|
1160
|
+
? {}
|
|
1161
|
+
: {
|
|
1162
|
+
evaluatedRevision: '',
|
|
1163
|
+
reviewStatus: failure.status,
|
|
1164
|
+
reviewError: failure.error,
|
|
1165
|
+
reviewEvidence: failure.evidence,
|
|
1166
|
+
lastError: null,
|
|
1167
|
+
};
|
|
691
1168
|
}),
|
|
692
1169
|
},
|
|
693
|
-
guards: {
|
|
694
|
-
coderProposed,
|
|
695
|
-
reviewerProposed,
|
|
696
|
-
committed,
|
|
697
|
-
needsBossReplyWithQuestion,
|
|
698
|
-
needsBossReplyWithoutQuestion,
|
|
699
|
-
validReviewSuccess,
|
|
700
|
-
authoredReviewFailure,
|
|
701
|
-
},
|
|
702
1170
|
}).createMachine({
|
|
703
1171
|
id: 'decide',
|
|
1172
|
+
description:
|
|
1173
|
+
'DECIDE: independent Coder and Reviewer proposals, a synthesized decide-owned commit, and a nested review.',
|
|
704
1174
|
initial: 'ready',
|
|
705
|
-
context: (): DecideContext =>
|
|
1175
|
+
context: (): DecideContext => INITIAL_CONTEXT,
|
|
706
1176
|
output: ({ context }): DecideOutput => {
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
1177
|
+
const commit = context.decideCommit;
|
|
1178
|
+
if (!isNonEmptyString(commit)) {
|
|
1179
|
+
throw new Error(
|
|
1180
|
+
'DECIDE reached a final state without its decide-owned commit',
|
|
1181
|
+
);
|
|
1182
|
+
}
|
|
1183
|
+
if (context.reviewStatus !== null) {
|
|
711
1184
|
return {
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
1185
|
+
lastDecideCommit: commit,
|
|
1186
|
+
noUnsettledFindings: false,
|
|
1187
|
+
reviewStatus: context.reviewStatus,
|
|
1188
|
+
...(context.reviewError === null ? {} : { error: context.reviewError }),
|
|
715
1189
|
};
|
|
716
1190
|
}
|
|
717
|
-
|
|
718
|
-
|
|
1191
|
+
const evaluatedRevision = context.evaluatedRevision;
|
|
1192
|
+
if (!isNonEmptyString(evaluatedRevision)) {
|
|
1193
|
+
throw new Error(
|
|
1194
|
+
'DECIDE completed without the evaluated repository revision',
|
|
1195
|
+
);
|
|
719
1196
|
}
|
|
720
1197
|
return {
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
...(context.reviewFailure.error
|
|
725
|
-
? { error: context.reviewFailure.error }
|
|
726
|
-
: {}),
|
|
1198
|
+
decideCommit: commit,
|
|
1199
|
+
evaluatedRevision,
|
|
1200
|
+
noUnsettledFindings: true,
|
|
727
1201
|
};
|
|
728
1202
|
},
|
|
729
1203
|
on: {
|
|
730
|
-
BOSS_INTERRUPT: bossInterrupts([
|
|
1204
|
+
BOSS_INTERRUPT: bossInterrupts([
|
|
1205
|
+
'independentProposals',
|
|
1206
|
+
'synthesizeCommit',
|
|
1207
|
+
] as const),
|
|
731
1208
|
},
|
|
732
1209
|
states: {
|
|
733
1210
|
ready: {
|
|
734
1211
|
id: 'ready',
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
playbook: {
|
|
739
|
-
stateId: 'ready',
|
|
740
|
-
description: 'Waiting for a topic to decide.',
|
|
741
|
-
},
|
|
742
|
-
},
|
|
1212
|
+
description: STATE_DESCRIPTIONS.ready,
|
|
1213
|
+
meta: playbookMeta('ready'),
|
|
1214
|
+
tags: ['playbook.parked'],
|
|
743
1215
|
on: {
|
|
744
|
-
START_DECIDE:
|
|
745
|
-
guard: ({ event }) => event.callerTopic.trim().length > 0,
|
|
746
|
-
target: 'independentProposals',
|
|
747
|
-
actions: 'copyStartTopic',
|
|
748
|
-
},
|
|
1216
|
+
START_DECIDE: startDecideArm,
|
|
749
1217
|
},
|
|
750
1218
|
},
|
|
751
1219
|
independentProposals: {
|
|
752
1220
|
id: 'independentProposals',
|
|
1221
|
+
description: STATE_DESCRIPTIONS.independentProposals,
|
|
1222
|
+
meta: playbookMeta('independentProposals'),
|
|
753
1223
|
type: 'parallel',
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
stateId: 'independentProposals',
|
|
758
|
-
description: 'Coder and Reviewer prepare independent proposals.',
|
|
759
|
-
},
|
|
760
|
-
},
|
|
1224
|
+
// No group-level BOSS_REPLY handler: a reply is handled only by the
|
|
1225
|
+
// branch wait whose pending question it names, and a reply naming no
|
|
1226
|
+
// pending branch question moves no branch.
|
|
761
1227
|
states: {
|
|
762
|
-
|
|
763
|
-
|
|
1228
|
+
coderProposalRegion: {
|
|
1229
|
+
id: 'coderProposalRegion',
|
|
1230
|
+
description: STATE_DESCRIPTIONS.coderProposalRegion,
|
|
1231
|
+
meta: playbookMeta('coderProposalRegion'),
|
|
1232
|
+
initial: 'askCoderProposal',
|
|
764
1233
|
states: {
|
|
765
|
-
|
|
1234
|
+
askCoderProposal: {
|
|
766
1235
|
id: 'askCoderProposal',
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
playbook: {
|
|
771
|
-
stateId: 'askCoderProposal',
|
|
772
|
-
description: 'Coder independently proposes a spec design.',
|
|
773
|
-
role: 'coder',
|
|
774
|
-
},
|
|
775
|
-
},
|
|
1236
|
+
description: STATE_DESCRIPTIONS.askCoderProposal,
|
|
1237
|
+
meta: playbookMeta('askCoderProposal', 'coder'),
|
|
1238
|
+
tags: ['playbook.busy'],
|
|
776
1239
|
invoke: {
|
|
777
1240
|
src: 'player',
|
|
778
|
-
input: ({ context }):
|
|
1241
|
+
input: ({ context }): CoderProposalInput => ({
|
|
779
1242
|
stateId: 'askCoderProposal',
|
|
780
|
-
sourceItem: 'DECIDE-1',
|
|
781
1243
|
role: 'coder',
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
'Coder affirmatively provided a complete design proposal; a progress report, status update, or promise of a later proposal supports no proposal outcome. Output shall include `coderProposal: <verbatim final text>`.',
|
|
786
|
-
}),
|
|
1244
|
+
sourceItem: 'DECIDE-1',
|
|
1245
|
+
prompt: CODER_PROPOSAL_PROMPT,
|
|
1246
|
+
result: CODER_PROPOSAL_RESULT,
|
|
787
1247
|
callerTopic: context.callerTopic,
|
|
788
1248
|
...bossReplyFields(context, 'askCoderProposal'),
|
|
789
1249
|
}),
|
|
790
1250
|
onDone: [
|
|
1251
|
+
suspendArm('askCoderProposal', 'awaitCoderProposalReply'),
|
|
791
1252
|
{
|
|
792
|
-
guard: '
|
|
793
|
-
target: '
|
|
794
|
-
actions: [
|
|
795
|
-
{
|
|
796
|
-
type: 'playbook.acceptedOutcome',
|
|
797
|
-
params: {
|
|
798
|
-
source: 'askCoderProposal',
|
|
799
|
-
target: 'waitCoderProposalReply',
|
|
800
|
-
acceptedOutcome: 'needsBossReply',
|
|
801
|
-
},
|
|
802
|
-
},
|
|
803
|
-
{
|
|
804
|
-
type: 'setPendingBossQuestion',
|
|
805
|
-
params: {
|
|
806
|
-
...stateMetadata.askCoderProposal,
|
|
807
|
-
resumeStateId: 'askCoderProposal',
|
|
808
|
-
question: '',
|
|
809
|
-
},
|
|
810
|
-
},
|
|
811
|
-
],
|
|
812
|
-
},
|
|
813
|
-
{
|
|
814
|
-
guard: 'needsBossReplyWithoutQuestion',
|
|
815
|
-
target: '#failed',
|
|
1253
|
+
guard: 'coderProposedJoining',
|
|
1254
|
+
target: 'coderProposalStaged',
|
|
816
1255
|
actions: [
|
|
817
|
-
|
|
818
|
-
|
|
1256
|
+
acceptedOutcome(
|
|
1257
|
+
'askCoderProposal',
|
|
1258
|
+
'synthesizeCommit',
|
|
1259
|
+
'proposed',
|
|
1260
|
+
),
|
|
1261
|
+
'stageCoderProposal',
|
|
819
1262
|
],
|
|
820
1263
|
},
|
|
821
1264
|
{
|
|
822
1265
|
guard: 'coderProposed',
|
|
823
|
-
target: '
|
|
1266
|
+
target: 'coderProposalStaged',
|
|
824
1267
|
actions: [
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
context.stagedReviewerResult === undefined
|
|
831
|
-
? 'coderProposalComplete'
|
|
832
|
-
: 'commitCoderProposal',
|
|
833
|
-
acceptedOutcome: 'proposed',
|
|
834
|
-
}),
|
|
835
|
-
},
|
|
1268
|
+
acceptedOutcome(
|
|
1269
|
+
'askCoderProposal',
|
|
1270
|
+
'coderProposalStaged',
|
|
1271
|
+
'proposed',
|
|
1272
|
+
),
|
|
836
1273
|
'stageCoderProposal',
|
|
837
|
-
{
|
|
838
|
-
type: 'clearBranchBossReplyContext',
|
|
839
|
-
params: { stateId: 'askCoderProposal' },
|
|
840
|
-
},
|
|
841
|
-
],
|
|
842
|
-
},
|
|
843
|
-
{
|
|
844
|
-
target: '#failed',
|
|
845
|
-
actions: [
|
|
846
|
-
'rememberMalformedActorOutput',
|
|
847
|
-
'clearProposalRoundContext',
|
|
848
1274
|
],
|
|
849
1275
|
},
|
|
1276
|
+
malformedPlayerOutputArm('DECIDE-1'),
|
|
850
1277
|
],
|
|
851
|
-
onError:
|
|
852
|
-
target: '#failed',
|
|
853
|
-
actions: [
|
|
854
|
-
'rememberActorError',
|
|
855
|
-
'clearProposalRoundContext',
|
|
856
|
-
],
|
|
857
|
-
},
|
|
1278
|
+
onError: playerOnError,
|
|
858
1279
|
},
|
|
859
1280
|
},
|
|
860
|
-
|
|
861
|
-
id: '
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
playbook: {
|
|
866
|
-
stateId: 'waitCoderProposalReply',
|
|
867
|
-
description: 'Coder waits for Boss to answer a question.',
|
|
868
|
-
},
|
|
869
|
-
},
|
|
1281
|
+
awaitCoderProposalReply: {
|
|
1282
|
+
id: 'awaitCoderProposalReply',
|
|
1283
|
+
description: STATE_DESCRIPTIONS.awaitCoderProposalReply,
|
|
1284
|
+
meta: playbookMeta('awaitCoderProposalReply'),
|
|
1285
|
+
tags: ['playbook.parked'],
|
|
870
1286
|
on: {
|
|
871
|
-
BOSS_REPLY: [
|
|
872
|
-
{
|
|
873
|
-
guard: bossReplyTargets('askCoderProposal', false),
|
|
874
|
-
target: '#failed',
|
|
875
|
-
actions: [
|
|
876
|
-
'rememberMalformedBossReply',
|
|
877
|
-
'clearProposalRoundContext',
|
|
878
|
-
],
|
|
879
|
-
},
|
|
880
|
-
{
|
|
881
|
-
guard: bossReplyTargets('askCoderProposal', true),
|
|
882
|
-
target: 'working',
|
|
883
|
-
actions: 'rememberBossReply',
|
|
884
|
-
},
|
|
885
|
-
],
|
|
1287
|
+
BOSS_REPLY: resumableStates(['askCoderProposal'] as const),
|
|
886
1288
|
},
|
|
887
1289
|
},
|
|
888
|
-
|
|
1290
|
+
coderProposalStaged: {
|
|
1291
|
+
id: 'coderProposalStaged',
|
|
1292
|
+
description: STATE_DESCRIPTIONS.coderProposalStaged,
|
|
1293
|
+
meta: playbookMeta('coderProposalStaged'),
|
|
889
1294
|
type: 'final',
|
|
890
|
-
description: 'Coder proposal is staged for the parallel join.',
|
|
891
|
-
meta: {
|
|
892
|
-
playbook: {
|
|
893
|
-
stateId: 'coderProposalComplete',
|
|
894
|
-
description:
|
|
895
|
-
'Coder proposal is staged for the parallel join.',
|
|
896
|
-
},
|
|
897
|
-
},
|
|
898
1295
|
},
|
|
899
1296
|
},
|
|
900
1297
|
},
|
|
901
|
-
|
|
902
|
-
|
|
1298
|
+
reviewerProposalRegion: {
|
|
1299
|
+
id: 'reviewerProposalRegion',
|
|
1300
|
+
description: STATE_DESCRIPTIONS.reviewerProposalRegion,
|
|
1301
|
+
meta: playbookMeta('reviewerProposalRegion'),
|
|
1302
|
+
initial: 'askReviewerProposal',
|
|
903
1303
|
states: {
|
|
904
|
-
|
|
1304
|
+
askReviewerProposal: {
|
|
905
1305
|
id: 'askReviewerProposal',
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
playbook: {
|
|
910
|
-
stateId: 'askReviewerProposal',
|
|
911
|
-
description:
|
|
912
|
-
'Reviewer independently proposes a spec design.',
|
|
913
|
-
role: 'reviewer',
|
|
914
|
-
},
|
|
915
|
-
},
|
|
1306
|
+
description: STATE_DESCRIPTIONS.askReviewerProposal,
|
|
1307
|
+
meta: playbookMeta('askReviewerProposal', 'reviewer'),
|
|
1308
|
+
tags: ['playbook.busy'],
|
|
916
1309
|
invoke: {
|
|
917
1310
|
src: 'player',
|
|
918
|
-
input: ({ context }):
|
|
1311
|
+
input: ({ context }): ReviewerProposalInput => ({
|
|
919
1312
|
stateId: 'askReviewerProposal',
|
|
920
|
-
sourceItem: 'DECIDE-2',
|
|
921
1313
|
role: 'reviewer',
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
'Reviewer affirmatively provided a complete design proposal; a progress report, status update, or promise of a later proposal supports no proposal outcome. Output shall include `reviewerProposal: <verbatim final text>`.',
|
|
926
|
-
}),
|
|
1314
|
+
sourceItem: 'DECIDE-2',
|
|
1315
|
+
prompt: REVIEWER_PROPOSAL_PROMPT,
|
|
1316
|
+
result: REVIEWER_PROPOSAL_RESULT,
|
|
927
1317
|
callerTopic: context.callerTopic,
|
|
928
1318
|
...bossReplyFields(context, 'askReviewerProposal'),
|
|
929
1319
|
}),
|
|
930
1320
|
onDone: [
|
|
1321
|
+
suspendArm('askReviewerProposal', 'awaitReviewerProposalReply'),
|
|
931
1322
|
{
|
|
932
|
-
guard: '
|
|
933
|
-
target: '
|
|
1323
|
+
guard: 'reviewerProposedJoining',
|
|
1324
|
+
target: 'reviewerProposalStaged',
|
|
934
1325
|
actions: [
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
},
|
|
942
|
-
},
|
|
943
|
-
{
|
|
944
|
-
type: 'setPendingBossQuestion',
|
|
945
|
-
params: {
|
|
946
|
-
...stateMetadata.askReviewerProposal,
|
|
947
|
-
resumeStateId: 'askReviewerProposal',
|
|
948
|
-
question: '',
|
|
949
|
-
},
|
|
950
|
-
},
|
|
951
|
-
],
|
|
952
|
-
},
|
|
953
|
-
{
|
|
954
|
-
guard: 'needsBossReplyWithoutQuestion',
|
|
955
|
-
target: '#failed',
|
|
956
|
-
actions: [
|
|
957
|
-
'rememberMalformedActorOutput',
|
|
958
|
-
'clearProposalRoundContext',
|
|
1326
|
+
acceptedOutcome(
|
|
1327
|
+
'askReviewerProposal',
|
|
1328
|
+
'synthesizeCommit',
|
|
1329
|
+
'proposed',
|
|
1330
|
+
),
|
|
1331
|
+
'stageReviewerProposal',
|
|
959
1332
|
],
|
|
960
1333
|
},
|
|
961
1334
|
{
|
|
962
1335
|
guard: 'reviewerProposed',
|
|
963
|
-
target: '
|
|
1336
|
+
target: 'reviewerProposalStaged',
|
|
964
1337
|
actions: [
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
context.stagedCoderResult === undefined
|
|
971
|
-
? 'reviewerProposalComplete'
|
|
972
|
-
: 'commitCoderProposal',
|
|
973
|
-
acceptedOutcome: 'proposed',
|
|
974
|
-
}),
|
|
975
|
-
},
|
|
1338
|
+
acceptedOutcome(
|
|
1339
|
+
'askReviewerProposal',
|
|
1340
|
+
'reviewerProposalStaged',
|
|
1341
|
+
'proposed',
|
|
1342
|
+
),
|
|
976
1343
|
'stageReviewerProposal',
|
|
977
|
-
{
|
|
978
|
-
type: 'clearBranchBossReplyContext',
|
|
979
|
-
params: { stateId: 'askReviewerProposal' },
|
|
980
|
-
},
|
|
981
|
-
],
|
|
982
|
-
},
|
|
983
|
-
{
|
|
984
|
-
target: '#failed',
|
|
985
|
-
actions: [
|
|
986
|
-
'rememberMalformedActorOutput',
|
|
987
|
-
'clearProposalRoundContext',
|
|
988
1344
|
],
|
|
989
1345
|
},
|
|
1346
|
+
malformedPlayerOutputArm('DECIDE-2'),
|
|
990
1347
|
],
|
|
991
|
-
onError:
|
|
992
|
-
target: '#failed',
|
|
993
|
-
actions: [
|
|
994
|
-
'rememberActorError',
|
|
995
|
-
'clearProposalRoundContext',
|
|
996
|
-
],
|
|
997
|
-
},
|
|
1348
|
+
onError: playerOnError,
|
|
998
1349
|
},
|
|
999
1350
|
},
|
|
1000
|
-
|
|
1001
|
-
id: '
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
playbook: {
|
|
1006
|
-
stateId: 'waitReviewerProposalReply',
|
|
1007
|
-
description:
|
|
1008
|
-
'Reviewer waits for Boss to answer a question.',
|
|
1009
|
-
},
|
|
1010
|
-
},
|
|
1351
|
+
awaitReviewerProposalReply: {
|
|
1352
|
+
id: 'awaitReviewerProposalReply',
|
|
1353
|
+
description: STATE_DESCRIPTIONS.awaitReviewerProposalReply,
|
|
1354
|
+
meta: playbookMeta('awaitReviewerProposalReply'),
|
|
1355
|
+
tags: ['playbook.parked'],
|
|
1011
1356
|
on: {
|
|
1012
|
-
BOSS_REPLY: [
|
|
1013
|
-
{
|
|
1014
|
-
guard: bossReplyTargets('askReviewerProposal', false),
|
|
1015
|
-
target: '#failed',
|
|
1016
|
-
actions: [
|
|
1017
|
-
'rememberMalformedBossReply',
|
|
1018
|
-
'clearProposalRoundContext',
|
|
1019
|
-
],
|
|
1020
|
-
},
|
|
1021
|
-
{
|
|
1022
|
-
guard: bossReplyTargets('askReviewerProposal', true),
|
|
1023
|
-
target: 'working',
|
|
1024
|
-
actions: 'rememberBossReply',
|
|
1025
|
-
},
|
|
1026
|
-
],
|
|
1357
|
+
BOSS_REPLY: resumableStates(['askReviewerProposal'] as const),
|
|
1027
1358
|
},
|
|
1028
1359
|
},
|
|
1029
|
-
|
|
1360
|
+
reviewerProposalStaged: {
|
|
1361
|
+
id: 'reviewerProposalStaged',
|
|
1362
|
+
description: STATE_DESCRIPTIONS.reviewerProposalStaged,
|
|
1363
|
+
meta: playbookMeta('reviewerProposalStaged'),
|
|
1030
1364
|
type: 'final',
|
|
1031
|
-
description:
|
|
1032
|
-
'Reviewer proposal is staged for the parallel join.',
|
|
1033
|
-
meta: {
|
|
1034
|
-
playbook: {
|
|
1035
|
-
stateId: 'reviewerProposalComplete',
|
|
1036
|
-
description:
|
|
1037
|
-
'Reviewer proposal is staged for the parallel join.',
|
|
1038
|
-
},
|
|
1039
|
-
},
|
|
1040
1365
|
},
|
|
1041
1366
|
},
|
|
1042
1367
|
},
|
|
1043
1368
|
},
|
|
1369
|
+
// The join: taken only after both regions reach final; it promotes both
|
|
1370
|
+
// staged proposals atomically before DECIDE-3 begins.
|
|
1044
1371
|
onDone: {
|
|
1045
|
-
target: '
|
|
1372
|
+
target: 'synthesizeCommit',
|
|
1046
1373
|
actions: 'promoteProposals',
|
|
1047
1374
|
},
|
|
1048
1375
|
},
|
|
1049
|
-
|
|
1050
|
-
id: '
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
playbook: {
|
|
1055
|
-
stateId: 'commitCoderProposal',
|
|
1056
|
-
description:
|
|
1057
|
-
'Coder synthesizes both proposals and commits the design.',
|
|
1058
|
-
role: 'coder',
|
|
1059
|
-
},
|
|
1060
|
-
},
|
|
1376
|
+
synthesizeCommit: {
|
|
1377
|
+
id: 'synthesizeCommit',
|
|
1378
|
+
description: STATE_DESCRIPTIONS.synthesizeCommit,
|
|
1379
|
+
meta: playbookMeta('synthesizeCommit', 'coder'),
|
|
1380
|
+
tags: ['playbook.busy'],
|
|
1061
1381
|
invoke: {
|
|
1062
1382
|
src: 'player',
|
|
1063
|
-
input: ({ context }):
|
|
1064
|
-
stateId: '
|
|
1065
|
-
sourceItem: 'DECIDE-3',
|
|
1383
|
+
input: ({ context }): SynthesizeCommitInput => ({
|
|
1384
|
+
stateId: 'synthesizeCommit',
|
|
1066
1385
|
role: 'coder',
|
|
1386
|
+
sourceItem: 'DECIDE-3',
|
|
1067
1387
|
prompt: SYNTHESIZE_COMMIT_PROMPT,
|
|
1068
|
-
result:
|
|
1069
|
-
committed:
|
|
1070
|
-
'Coder synthesized both proposals and committed the resulting design as one new commit. Output shall include `coderOutput: <verbatim final text>` and `latestCommit: <commit identity>`.',
|
|
1071
|
-
}),
|
|
1388
|
+
result: SYNTHESIZE_COMMIT_RESULT,
|
|
1072
1389
|
callerTopic: context.callerTopic,
|
|
1073
1390
|
reviewerProposal: context.reviewerProposal,
|
|
1074
|
-
...bossReplyFields(context, '
|
|
1391
|
+
...bossReplyFields(context, 'synthesizeCommit'),
|
|
1075
1392
|
}),
|
|
1076
1393
|
onDone: [
|
|
1077
|
-
|
|
1078
|
-
guard: 'needsBossReplyWithQuestion',
|
|
1079
|
-
target: 'awaitBossReply',
|
|
1080
|
-
actions: [
|
|
1081
|
-
{
|
|
1082
|
-
type: 'playbook.acceptedOutcome',
|
|
1083
|
-
params: {
|
|
1084
|
-
source: 'commitCoderProposal',
|
|
1085
|
-
target: 'awaitBossReply',
|
|
1086
|
-
acceptedOutcome: 'needsBossReply',
|
|
1087
|
-
},
|
|
1088
|
-
},
|
|
1089
|
-
{
|
|
1090
|
-
type: 'setPendingBossQuestion',
|
|
1091
|
-
params: {
|
|
1092
|
-
...stateMetadata.commitCoderProposal,
|
|
1093
|
-
resumeStateId: 'commitCoderProposal',
|
|
1094
|
-
question: '',
|
|
1095
|
-
},
|
|
1096
|
-
},
|
|
1097
|
-
],
|
|
1098
|
-
},
|
|
1099
|
-
{
|
|
1100
|
-
guard: 'needsBossReplyWithoutQuestion',
|
|
1101
|
-
target: 'failed',
|
|
1102
|
-
actions: [
|
|
1103
|
-
'rememberMalformedActorOutput',
|
|
1104
|
-
'clearBossReplyContext',
|
|
1105
|
-
],
|
|
1106
|
-
},
|
|
1394
|
+
suspendArm('synthesizeCommit', 'awaitBossReply'),
|
|
1107
1395
|
{
|
|
1108
1396
|
guard: 'committed',
|
|
1109
1397
|
target: 'reviewCommit',
|
|
1110
1398
|
actions: [
|
|
1111
|
-
|
|
1112
|
-
type: 'playbook.acceptedOutcome',
|
|
1113
|
-
params: {
|
|
1114
|
-
source: 'commitCoderProposal',
|
|
1115
|
-
target: 'reviewCommit',
|
|
1116
|
-
acceptedOutcome: 'committed',
|
|
1117
|
-
},
|
|
1118
|
-
},
|
|
1399
|
+
acceptedOutcome('synthesizeCommit', 'reviewCommit', 'committed'),
|
|
1119
1400
|
'rememberCommit',
|
|
1120
|
-
{
|
|
1121
|
-
type: 'clearBranchBossReplyContext',
|
|
1122
|
-
params: { stateId: 'commitCoderProposal' },
|
|
1123
|
-
},
|
|
1124
|
-
],
|
|
1125
|
-
},
|
|
1126
|
-
{
|
|
1127
|
-
target: 'failed',
|
|
1128
|
-
actions: [
|
|
1129
|
-
'rememberMalformedActorOutput',
|
|
1130
|
-
'clearBossReplyContext',
|
|
1131
1401
|
],
|
|
1132
1402
|
},
|
|
1403
|
+
malformedPlayerOutputArm('DECIDE-3'),
|
|
1133
1404
|
],
|
|
1134
|
-
onError:
|
|
1135
|
-
target: 'failed',
|
|
1136
|
-
actions: ['rememberActorError', 'clearBossReplyContext'],
|
|
1137
|
-
},
|
|
1405
|
+
onError: playerOnError,
|
|
1138
1406
|
},
|
|
1139
1407
|
},
|
|
1140
1408
|
awaitBossReply: {
|
|
1141
1409
|
id: 'awaitBossReply',
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
playbook: {
|
|
1146
|
-
stateId: 'awaitBossReply',
|
|
1147
|
-
description: 'Waiting for Boss to answer Coder’s question.',
|
|
1148
|
-
},
|
|
1149
|
-
},
|
|
1410
|
+
description: STATE_DESCRIPTIONS.awaitBossReply,
|
|
1411
|
+
meta: playbookMeta('awaitBossReply'),
|
|
1412
|
+
tags: ['playbook.parked'],
|
|
1150
1413
|
on: {
|
|
1151
1414
|
BOSS_REPLY: [
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
target: 'failed',
|
|
1155
|
-
actions: [
|
|
1156
|
-
'rememberMalformedBossReply',
|
|
1157
|
-
'clearBossReplyContext',
|
|
1158
|
-
],
|
|
1159
|
-
},
|
|
1160
|
-
...resumableStates(['commitCoderProposal']),
|
|
1415
|
+
unknownBossReplyArm,
|
|
1416
|
+
...resumableStates(['synthesizeCommit'] as const),
|
|
1161
1417
|
],
|
|
1418
|
+
START_DECIDE: startDecideArm,
|
|
1162
1419
|
},
|
|
1163
1420
|
},
|
|
1164
1421
|
reviewCommit: {
|
|
1165
1422
|
id: 'reviewCommit',
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
playbook: {
|
|
1170
|
-
stateId: 'reviewCommit',
|
|
1171
|
-
description: 'REVIEW examines the committed proposal.',
|
|
1172
|
-
},
|
|
1173
|
-
},
|
|
1423
|
+
description: STATE_DESCRIPTIONS.reviewCommit,
|
|
1424
|
+
meta: playbookMeta('reviewCommit'),
|
|
1425
|
+
tags: ['playbook.suspended'],
|
|
1174
1426
|
invoke: {
|
|
1175
1427
|
src: 'playbook',
|
|
1176
1428
|
input: ({ context }): PlaybookInput => ({
|
|
1177
1429
|
stateId: 'reviewCommit',
|
|
1178
1430
|
sourceItem: 'DECIDE-4',
|
|
1179
|
-
playbookId:
|
|
1180
|
-
text:
|
|
1181
|
-
context.callerTopic,
|
|
1182
|
-
context.latestCommit,
|
|
1183
|
-
context.coderOutput,
|
|
1184
|
-
),
|
|
1431
|
+
playbookId: REVIEW_PLAYBOOK_ID,
|
|
1432
|
+
text: reviewCallText(context),
|
|
1185
1433
|
}),
|
|
1186
1434
|
onDone: [
|
|
1187
1435
|
{
|
|
1188
|
-
guard: '
|
|
1436
|
+
guard: 'reviewApproved',
|
|
1189
1437
|
target: 'done',
|
|
1190
|
-
actions: '
|
|
1438
|
+
actions: 'rememberReviewApproval',
|
|
1191
1439
|
},
|
|
1192
1440
|
{
|
|
1193
1441
|
target: 'reportedReviewFailure',
|
|
1194
|
-
actions: '
|
|
1442
|
+
actions: 'rememberUnestablishedReview',
|
|
1195
1443
|
},
|
|
1196
1444
|
],
|
|
1197
1445
|
onError: [
|
|
1198
1446
|
{
|
|
1199
1447
|
guard: 'authoredReviewFailure',
|
|
1200
1448
|
target: 'reportedReviewFailure',
|
|
1201
|
-
actions: '
|
|
1202
|
-
},
|
|
1203
|
-
{
|
|
1204
|
-
target: 'failed',
|
|
1205
|
-
actions: 'rememberActorError',
|
|
1449
|
+
actions: 'rememberAuthoredReviewFailure',
|
|
1206
1450
|
},
|
|
1451
|
+
{ target: 'failed', actions: 'rememberActorError' },
|
|
1207
1452
|
],
|
|
1208
1453
|
},
|
|
1209
1454
|
},
|
|
1210
1455
|
failed: {
|
|
1211
1456
|
id: 'failed',
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
playbook: {
|
|
1216
|
-
stateId: 'failed',
|
|
1217
|
-
description: 'DECIDE failed and is waiting for a new topic.',
|
|
1218
|
-
},
|
|
1219
|
-
},
|
|
1457
|
+
description: STATE_DESCRIPTIONS.failed,
|
|
1458
|
+
meta: playbookMeta('failed'),
|
|
1459
|
+
tags: ['playbook.parked'],
|
|
1220
1460
|
on: {
|
|
1221
|
-
START_DECIDE:
|
|
1222
|
-
guard: ({ event }) => event.callerTopic.trim().length > 0,
|
|
1223
|
-
target: 'independentProposals',
|
|
1224
|
-
actions: 'copyStartTopic',
|
|
1225
|
-
},
|
|
1461
|
+
START_DECIDE: startDecideArm,
|
|
1226
1462
|
},
|
|
1227
1463
|
},
|
|
1228
1464
|
reportedReviewFailure: {
|
|
1229
1465
|
id: 'reportedReviewFailure',
|
|
1466
|
+
description: STATE_DESCRIPTIONS.reportedReviewFailure,
|
|
1467
|
+
meta: terminalMeta('reportedReviewFailure', 'failure'),
|
|
1230
1468
|
type: 'final',
|
|
1231
|
-
description: 'DECIDE reports REVIEW’s failure and its last commit.',
|
|
1232
|
-
meta: {
|
|
1233
|
-
playbook: {
|
|
1234
|
-
stateId: 'reportedReviewFailure',
|
|
1235
|
-
description: 'DECIDE reports REVIEW’s failure and its last commit.',
|
|
1236
|
-
// DR-048: this final state means the workflow failed, so a caller
|
|
1237
|
-
// learns that from the machine rather than from DECIDE's output.
|
|
1238
|
-
terminal: 'failure',
|
|
1239
|
-
},
|
|
1240
|
-
},
|
|
1241
1469
|
},
|
|
1242
1470
|
done: {
|
|
1243
1471
|
id: 'done',
|
|
1472
|
+
description: STATE_DESCRIPTIONS.done,
|
|
1473
|
+
meta: terminalMeta('done', 'success'),
|
|
1244
1474
|
type: 'final',
|
|
1245
|
-
description: 'DECIDE completed with an approved commit.',
|
|
1246
|
-
meta: {
|
|
1247
|
-
playbook: {
|
|
1248
|
-
stateId: 'done',
|
|
1249
|
-
description: 'DECIDE completed with an approved commit.',
|
|
1250
|
-
terminal: 'success',
|
|
1251
|
-
},
|
|
1252
|
-
},
|
|
1253
1475
|
},
|
|
1254
1476
|
},
|
|
1255
1477
|
});
|