@sublang/playbook 4.0.0 → 6.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 +69 -122
- package/docs/assets/playbook-venn.svg +13 -0
- package/docs/cli.md +43 -26
- package/docs/configuration.md +63 -18
- package/docs/embedding.md +24 -16
- package/package.json +43 -22
- package/reference/sdlc/captain.md +70 -83
- package/reference/sdlc/captain.playbook/captain.fsm.d.ts +127 -142
- package/reference/sdlc/captain.playbook/captain.fsm.js +349 -470
- package/reference/sdlc/captain.playbook/captain.fsm.ts +535 -598
- package/reference/sdlc/captain.playbook/captain.gears.md +37 -41
- package/reference/sdlc/captain.playbook/captain.playbook.d.ts +90 -15
- package/reference/sdlc/captain.playbook/captain.playbook.js +466 -976
- package/reference/sdlc/captain.playbook/captain.playbook.ts +698 -1001
- package/reference/sdlc/code.md +55 -97
- package/reference/sdlc/code.playbook/code.fsm.d.ts +229 -94
- package/reference/sdlc/code.playbook/code.fsm.introspect.d.ts +26 -44
- package/reference/sdlc/code.playbook/code.fsm.introspect.js +61 -66
- package/reference/sdlc/code.playbook/code.fsm.introspect.ts +100 -149
- package/reference/sdlc/code.playbook/code.fsm.js +587 -1347
- package/reference/sdlc/code.playbook/code.fsm.ts +809 -1650
- package/reference/sdlc/code.playbook/code.gears.md +51 -263
- package/reference/sdlc/code.playbook/code.playbook.d.ts +8 -47
- package/reference/sdlc/code.playbook/code.playbook.js +69 -639
- package/reference/sdlc/code.playbook/code.playbook.ts +90 -850
- package/reference/sdlc/code.playbook/code.registry.d.ts +9 -25
- package/reference/sdlc/code.playbook/code.registry.js +20 -78
- package/reference/sdlc/code.playbook/code.registry.ts +58 -122
- package/reference/sdlc/code.playbook/playbook-captain.d.ts +2 -0
- package/reference/sdlc/code.playbook/playbook-captain.js +1877 -251
- package/reference/sdlc/code.playbook/playbook-captain.ts +2385 -352
- package/reference/sdlc/code.playbook/playbook.config.template.yaml +33 -22
- package/reference/sdlc/decide.md +54 -0
- package/reference/sdlc/decide.playbook/decide.fsm.d.ts +261 -0
- package/reference/sdlc/decide.playbook/decide.fsm.js +894 -0
- package/reference/sdlc/decide.playbook/decide.fsm.ts +1152 -0
- package/reference/sdlc/decide.playbook/decide.gears.md +88 -0
- package/reference/sdlc/decide.playbook/decide.playbook.d.ts +67 -0
- package/reference/sdlc/{discuss.playbook/discuss.playbook.js → decide.playbook/decide.playbook.js} +511 -370
- package/reference/sdlc/{discuss.playbook/discuss.playbook.ts → decide.playbook/decide.playbook.ts} +616 -451
- package/reference/sdlc/decide.playbook/decide.registry.d.ts +41 -0
- package/reference/sdlc/decide.playbook/decide.registry.js +60 -0
- package/reference/sdlc/decide.playbook/decide.registry.ts +125 -0
- package/reference/sdlc/review.md +81 -0
- package/reference/sdlc/review.playbook/review.fsm.d.ts +183 -0
- package/reference/sdlc/review.playbook/review.fsm.js +524 -0
- package/reference/sdlc/review.playbook/review.fsm.ts +652 -0
- package/reference/sdlc/review.playbook/review.gears.md +112 -0
- package/reference/sdlc/review.playbook/review.playbook.d.ts +12 -0
- package/reference/sdlc/review.playbook/review.playbook.js +112 -0
- package/reference/sdlc/review.playbook/review.playbook.ts +201 -0
- package/reference/sdlc/review.playbook/review.registry.d.ts +43 -0
- package/reference/sdlc/review.playbook/review.registry.js +73 -0
- package/reference/sdlc/review.playbook/review.registry.ts +138 -0
- package/slc/gears2fsm.md +67 -6
- package/slc/link.md +339 -25
- package/slc/text2gears.md +22 -2
- package/src/runtime.d.ts +36 -1
- package/src/runtime.ts +59 -0
- package/src/xstate-playbook-runtime.d.ts +96 -7
- package/src/xstate-playbook-runtime.js +1018 -49
- package/src/xstate-playbook-runtime.ts +1283 -59
- package/src/xstate-runtime.js +25 -0
- package/src/xstate-runtime.ts +51 -0
- package/reference/sdlc/discuss.md +0 -93
- package/reference/sdlc/discuss.playbook/discuss.fsm.d.ts +0 -396
- package/reference/sdlc/discuss.playbook/discuss.fsm.js +0 -2067
- package/reference/sdlc/discuss.playbook/discuss.fsm.ts +0 -2465
- package/reference/sdlc/discuss.playbook/discuss.gears.md +0 -258
- package/reference/sdlc/discuss.playbook/discuss.playbook.d.ts +0 -113
- package/reference/sdlc/discuss.playbook/discuss.registry.d.ts +0 -58
- package/reference/sdlc/discuss.playbook/discuss.registry.js +0 -97
- package/reference/sdlc/discuss.playbook/discuss.registry.ts +0 -153
|
@@ -0,0 +1,1152 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// SPDX-FileCopyrightText: 2026 SubLang International <https://sublang.ai>
|
|
3
|
+
|
|
4
|
+
// FSM object artifact compiled from decide.gears.md.
|
|
5
|
+
// This module defines only the machine, actor contracts, and typed inputs.
|
|
6
|
+
// The linked runtime supplies the player and nested-playbook actors.
|
|
7
|
+
|
|
8
|
+
import { assign, fromPromise, setup } from 'xstate';
|
|
9
|
+
|
|
10
|
+
export type JsonValue =
|
|
11
|
+
| null
|
|
12
|
+
| boolean
|
|
13
|
+
| number
|
|
14
|
+
| string
|
|
15
|
+
| readonly JsonValue[]
|
|
16
|
+
| { readonly [key: string]: JsonValue };
|
|
17
|
+
|
|
18
|
+
type Player = 'Coder' | 'Reviewer';
|
|
19
|
+
|
|
20
|
+
type JumpableStateId = 'independentProposals';
|
|
21
|
+
|
|
22
|
+
type ResumableStateId =
|
|
23
|
+
| 'askCoderProposal'
|
|
24
|
+
| 'askReviewerProposal'
|
|
25
|
+
| 'commitCoderProposal';
|
|
26
|
+
|
|
27
|
+
export interface PendingBossQuestion {
|
|
28
|
+
questionId: ResumableStateId;
|
|
29
|
+
resumeStateId: ResumableStateId;
|
|
30
|
+
sourceItem: string;
|
|
31
|
+
player: Player;
|
|
32
|
+
question: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
type PendingBossQuestions = Partial<
|
|
36
|
+
Record<ResumableStateId, PendingBossQuestion>
|
|
37
|
+
>;
|
|
38
|
+
|
|
39
|
+
type BossReplies = Partial<Record<ResumableStateId, string>>;
|
|
40
|
+
|
|
41
|
+
type PendingBossQuestionParams = Omit<PendingBossQuestion, 'questionId'>;
|
|
42
|
+
|
|
43
|
+
export interface ReviewSuccessOutput {
|
|
44
|
+
approvedCommit: 'latest';
|
|
45
|
+
noUnsettledFindings: true;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface DecideFailureOutput {
|
|
49
|
+
lastDecideCommit: string;
|
|
50
|
+
noUnsettledFindings: false;
|
|
51
|
+
reviewStatus: 'aborted' | 'error';
|
|
52
|
+
error?: {
|
|
53
|
+
name: string;
|
|
54
|
+
message: string;
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export type DecideOutput = ReviewSuccessOutput | DecideFailureOutput;
|
|
59
|
+
|
|
60
|
+
interface ChildFailure {
|
|
61
|
+
status: 'aborted' | 'error';
|
|
62
|
+
playbookId: 'review';
|
|
63
|
+
error?: {
|
|
64
|
+
name: string;
|
|
65
|
+
message: string;
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface DecideContext {
|
|
70
|
+
coderLlm: string;
|
|
71
|
+
callerTopic?: string;
|
|
72
|
+
coderProposal?: string;
|
|
73
|
+
reviewerProposal?: string;
|
|
74
|
+
latestCommit?: string;
|
|
75
|
+
reviewResult?: ReviewSuccessOutput;
|
|
76
|
+
reviewFailure?: ChildFailure;
|
|
77
|
+
lastResult?: PlayerOutput;
|
|
78
|
+
lastError?: unknown;
|
|
79
|
+
pendingBossQuestions?: PendingBossQuestions;
|
|
80
|
+
bossReplies?: BossReplies;
|
|
81
|
+
stagedCoderResult?: PlayerOutput;
|
|
82
|
+
stagedReviewerResult?: PlayerOutput;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export type DecideEvent =
|
|
86
|
+
| { type: 'START_DECIDE'; callerTopic: string }
|
|
87
|
+
| {
|
|
88
|
+
type: 'BOSS_INTERRUPT';
|
|
89
|
+
targetId: JumpableStateId;
|
|
90
|
+
bossIntent: string;
|
|
91
|
+
}
|
|
92
|
+
| {
|
|
93
|
+
type: 'BOSS_REPLY';
|
|
94
|
+
questionId?: ResumableStateId;
|
|
95
|
+
answer: string;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export interface DecideInput {
|
|
99
|
+
coderLlm: string;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface PlayerInput {
|
|
103
|
+
stateId: ResumableStateId;
|
|
104
|
+
sourceItem: string;
|
|
105
|
+
prompt: string;
|
|
106
|
+
result: Record<string, string>;
|
|
107
|
+
player: Player;
|
|
108
|
+
callerTopic?: string;
|
|
109
|
+
coderLlm?: string;
|
|
110
|
+
pendingBossQuestion?: PendingBossQuestion;
|
|
111
|
+
bossReply?: string;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
type AdditionalPlayerFields = {
|
|
115
|
+
coderProposal?: string;
|
|
116
|
+
reviewerProposal?: string;
|
|
117
|
+
coderOutput?: string;
|
|
118
|
+
latestCommit?: string;
|
|
119
|
+
question?: string;
|
|
120
|
+
readonly [key: string]: unknown;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
export type PlayerOutput =
|
|
124
|
+
| ({ guard: 'proposed'; coderProposal: string } & AdditionalPlayerFields)
|
|
125
|
+
| ({ guard: 'proposed'; reviewerProposal: string } & AdditionalPlayerFields)
|
|
126
|
+
| ({ guard: 'committed'; coderOutput: string; latestCommit: string } &
|
|
127
|
+
AdditionalPlayerFields)
|
|
128
|
+
| ({ guard: 'needsBossReply'; question: string } & AdditionalPlayerFields);
|
|
129
|
+
|
|
130
|
+
export interface PlaybookInput {
|
|
131
|
+
stateId: 'reviewCommit';
|
|
132
|
+
sourceItem: 'DECIDE-4';
|
|
133
|
+
playbookId: 'review';
|
|
134
|
+
text: string;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
type PlayerDoneEvent = { type: string; output: PlayerOutput };
|
|
138
|
+
type ActorErrorEvent = { type: string; error: unknown };
|
|
139
|
+
type PlaybookDoneEvent = { type: string; output: JsonValue | undefined };
|
|
140
|
+
|
|
141
|
+
const NEEDS_BOSS_REPLY_DESCRIPTION =
|
|
142
|
+
"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>`.";
|
|
143
|
+
|
|
144
|
+
const INDEPENDENT_PROPOSAL_PROMPT = [
|
|
145
|
+
'> <caller-topic>',
|
|
146
|
+
'',
|
|
147
|
+
'Assess whether the topic is better expressed as a few spec items under @specs/packages/ or requires one or more DRs under @specs/decisions/.',
|
|
148
|
+
'Propose your design.',
|
|
149
|
+
'Keep your proposal coherent, focused, and concise.',
|
|
150
|
+
'Consult @specs/map.md for relevant context and @specs/meta.md for spec requirements, if needed.',
|
|
151
|
+
'Do not change any files.',
|
|
152
|
+
].join('\n');
|
|
153
|
+
|
|
154
|
+
const COMMIT_CODER_PROMPT = [
|
|
155
|
+
'Turn your proposal into the necessary spec items or DRs.',
|
|
156
|
+
'Follow @specs/meta.md and update @specs/map.md when needed.',
|
|
157
|
+
"Do not inspect or incorporate Reviewer's proposal before this commit.",
|
|
158
|
+
'Do not change code or implement the proposal.',
|
|
159
|
+
'',
|
|
160
|
+
'Commit the result as one new commit, following @specs/packages/git.md.',
|
|
161
|
+
'Make the commit message explain concisely what changed and why.',
|
|
162
|
+
'Report it as exactly one final-response line beginning `Commit: `, followed only by the exact commit identity.',
|
|
163
|
+
'Coder is <coder-llm>; format the model token in conventional human form.',
|
|
164
|
+
].join('\n');
|
|
165
|
+
|
|
166
|
+
const REVIEW_INPUT_TEMPLATE = [
|
|
167
|
+
'Review the latest commit as a spec-design change against the initial intent.',
|
|
168
|
+
'Compare it with your independent proposal and take the best of both.',
|
|
169
|
+
'Make your suggestions.',
|
|
170
|
+
'',
|
|
171
|
+
'Initial intent: <caller-topic>.',
|
|
172
|
+
"Coder's independent proposal: <coder-proposal>.",
|
|
173
|
+
].join('\n');
|
|
174
|
+
|
|
175
|
+
function composeReviewInput(
|
|
176
|
+
callerTopic: string | undefined,
|
|
177
|
+
coderProposal: string | undefined,
|
|
178
|
+
): string {
|
|
179
|
+
const fields: Readonly<Record<string, string>> = {
|
|
180
|
+
'<caller-topic>': callerTopic ?? '',
|
|
181
|
+
'<coder-proposal>': coderProposal ?? '',
|
|
182
|
+
};
|
|
183
|
+
return REVIEW_INPUT_TEMPLATE.replace(
|
|
184
|
+
/<caller-topic>|<coder-proposal>/g,
|
|
185
|
+
(placeholder) => fields[placeholder],
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const outputOf = (event: unknown): PlayerOutput =>
|
|
190
|
+
(event as PlayerDoneEvent).output;
|
|
191
|
+
|
|
192
|
+
const playbookOutputOf = (event: unknown): JsonValue | undefined =>
|
|
193
|
+
(event as PlaybookDoneEvent).output;
|
|
194
|
+
|
|
195
|
+
const isNonEmptyString = (value: unknown): value is string =>
|
|
196
|
+
typeof value === 'string' && value.trim().length > 0;
|
|
197
|
+
|
|
198
|
+
const coderProposed = ({ event }: { event: unknown }) => {
|
|
199
|
+
const output = outputOf(event);
|
|
200
|
+
return output.guard === 'proposed' && isNonEmptyString(output.coderProposal);
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const reviewerProposed = ({ event }: { event: unknown }) => {
|
|
204
|
+
const output = outputOf(event);
|
|
205
|
+
return (
|
|
206
|
+
output.guard === 'proposed' && isNonEmptyString(output.reviewerProposal)
|
|
207
|
+
);
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
function commitOutput(
|
|
211
|
+
event: unknown,
|
|
212
|
+
): { readonly coderOutput: string; readonly latestCommit: string } | undefined {
|
|
213
|
+
const output = outputOf(event);
|
|
214
|
+
if (
|
|
215
|
+
output.guard !== 'committed' ||
|
|
216
|
+
!isNonEmptyString(output.coderOutput) ||
|
|
217
|
+
!isNonEmptyString(output.latestCommit)
|
|
218
|
+
) {
|
|
219
|
+
return undefined;
|
|
220
|
+
}
|
|
221
|
+
const commitLines = output.coderOutput
|
|
222
|
+
.split('\n')
|
|
223
|
+
.filter((line) => line.startsWith('Commit: '));
|
|
224
|
+
if (
|
|
225
|
+
commitLines.length !== 1 ||
|
|
226
|
+
commitLines[0] !== `Commit: ${output.latestCommit}`
|
|
227
|
+
) {
|
|
228
|
+
return undefined;
|
|
229
|
+
}
|
|
230
|
+
return {
|
|
231
|
+
coderOutput: output.coderOutput,
|
|
232
|
+
latestCommit: output.latestCommit,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const committed = ({ event }: { event: unknown }) =>
|
|
237
|
+
commitOutput(event) !== undefined;
|
|
238
|
+
|
|
239
|
+
const needsBossReplyWithQuestion = ({ event }: { event: unknown }) => {
|
|
240
|
+
const output = outputOf(event);
|
|
241
|
+
return output.guard === 'needsBossReply' && isNonEmptyString(output.question);
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const needsBossReplyWithoutQuestion = ({ event }: { event: unknown }) => {
|
|
245
|
+
const output = outputOf(event);
|
|
246
|
+
return (
|
|
247
|
+
output.guard === 'needsBossReply' && !isNonEmptyString(output.question)
|
|
248
|
+
);
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
const pendingQuestionIds = (context: DecideContext): ResumableStateId[] =>
|
|
252
|
+
Object.keys(context.pendingBossQuestions ?? {}) as ResumableStateId[];
|
|
253
|
+
|
|
254
|
+
const resolvedQuestionId = (
|
|
255
|
+
context: DecideContext,
|
|
256
|
+
event: DecideEvent,
|
|
257
|
+
): ResumableStateId | undefined => {
|
|
258
|
+
if (event.type !== 'BOSS_REPLY') return undefined;
|
|
259
|
+
if (event.questionId) return event.questionId;
|
|
260
|
+
const ids = pendingQuestionIds(context);
|
|
261
|
+
return ids.length === 1 ? ids[0] : undefined;
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
const bossReplyTargets =
|
|
265
|
+
(stateId: ResumableStateId, requireAnswer: boolean) =>
|
|
266
|
+
({ context, event }: { context: DecideContext; event: DecideEvent }) =>
|
|
267
|
+
event.type === 'BOSS_REPLY' &&
|
|
268
|
+
resolvedQuestionId(context, event) === stateId &&
|
|
269
|
+
(requireAnswer
|
|
270
|
+
? event.answer.trim().length > 0
|
|
271
|
+
: event.answer.trim().length === 0);
|
|
272
|
+
|
|
273
|
+
const bossReplyFields = (
|
|
274
|
+
context: DecideContext,
|
|
275
|
+
stateId: ResumableStateId,
|
|
276
|
+
) => ({
|
|
277
|
+
...(context.pendingBossQuestions?.[stateId]
|
|
278
|
+
? { pendingBossQuestion: context.pendingBossQuestions[stateId] }
|
|
279
|
+
: {}),
|
|
280
|
+
...(context.bossReplies?.[stateId]
|
|
281
|
+
? { bossReply: context.bossReplies[stateId] }
|
|
282
|
+
: {}),
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
const withoutKey = <T>(
|
|
286
|
+
record: Partial<Record<ResumableStateId, T>> | undefined,
|
|
287
|
+
stateId: ResumableStateId,
|
|
288
|
+
): Partial<Record<ResumableStateId, T>> | undefined => {
|
|
289
|
+
if (!record || record[stateId] === undefined) return record;
|
|
290
|
+
const next = { ...record };
|
|
291
|
+
delete next[stateId];
|
|
292
|
+
return Object.keys(next).length > 0 ? next : undefined;
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
const withNeedsBossReply = <T extends Record<string, string>>(result: T) => ({
|
|
296
|
+
...result,
|
|
297
|
+
needsBossReply: NEEDS_BOSS_REPLY_DESCRIPTION,
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
const isPlainRecord = (value: unknown): value is Record<string, unknown> =>
|
|
301
|
+
typeof value === 'object' &&
|
|
302
|
+
value !== null &&
|
|
303
|
+
!Array.isArray(value) &&
|
|
304
|
+
(Object.getPrototypeOf(value) === Object.prototype ||
|
|
305
|
+
Object.getPrototypeOf(value) === null);
|
|
306
|
+
|
|
307
|
+
const isExactKeys = (
|
|
308
|
+
value: Record<string, unknown>,
|
|
309
|
+
keys: readonly string[],
|
|
310
|
+
): boolean => {
|
|
311
|
+
const ownKeys = Reflect.ownKeys(value);
|
|
312
|
+
return (
|
|
313
|
+
ownKeys.length === keys.length &&
|
|
314
|
+
ownKeys.every((key) => typeof key === 'string' && keys.includes(key))
|
|
315
|
+
);
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
function isStateValue(value: unknown, ancestors = new Set<object>()): boolean {
|
|
319
|
+
if (typeof value === 'string') return true;
|
|
320
|
+
if (!isPlainRecord(value) || ancestors.has(value)) return false;
|
|
321
|
+
const next = new Set(ancestors).add(value);
|
|
322
|
+
return Reflect.ownKeys(value).every(
|
|
323
|
+
(key) =>
|
|
324
|
+
typeof key === 'string' &&
|
|
325
|
+
Object.prototype.propertyIsEnumerable.call(value, key) &&
|
|
326
|
+
isStateValue(value[key], next),
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function isStringArray(value: unknown): value is readonly string[] {
|
|
331
|
+
return (
|
|
332
|
+
Array.isArray(value) &&
|
|
333
|
+
Reflect.ownKeys(value).length === value.length + 1 &&
|
|
334
|
+
Reflect.ownKeys(value).every((key) => {
|
|
335
|
+
if (key === 'length') return true;
|
|
336
|
+
return (
|
|
337
|
+
typeof key === 'string' &&
|
|
338
|
+
/^(0|[1-9][0-9]*)$/.test(key) &&
|
|
339
|
+
Number(key) < value.length
|
|
340
|
+
);
|
|
341
|
+
}) &&
|
|
342
|
+
value.every((entry) => isNonEmptyString(entry)) &&
|
|
343
|
+
new Set(value).size === value.length
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function isPlaybookState(value: unknown): boolean {
|
|
348
|
+
if (!isPlainRecord(value)) return false;
|
|
349
|
+
const allowed = new Set([
|
|
350
|
+
'value',
|
|
351
|
+
'activeStateIds',
|
|
352
|
+
'tags',
|
|
353
|
+
'status',
|
|
354
|
+
'quiescent',
|
|
355
|
+
'stateId',
|
|
356
|
+
]);
|
|
357
|
+
if (
|
|
358
|
+
Reflect.ownKeys(value).some(
|
|
359
|
+
(key) => typeof key !== 'string' || !allowed.has(key),
|
|
360
|
+
) ||
|
|
361
|
+
!Object.prototype.hasOwnProperty.call(value, 'value') ||
|
|
362
|
+
!Object.prototype.hasOwnProperty.call(value, 'activeStateIds') ||
|
|
363
|
+
!Object.prototype.hasOwnProperty.call(value, 'tags') ||
|
|
364
|
+
!Object.prototype.hasOwnProperty.call(value, 'status') ||
|
|
365
|
+
!Object.prototype.hasOwnProperty.call(value, 'quiescent')
|
|
366
|
+
) {
|
|
367
|
+
return false;
|
|
368
|
+
}
|
|
369
|
+
return (
|
|
370
|
+
isStateValue(value.value) &&
|
|
371
|
+
isStringArray(value.activeStateIds) &&
|
|
372
|
+
isStringArray(value.tags) &&
|
|
373
|
+
(value.status === 'active' ||
|
|
374
|
+
value.status === 'done' ||
|
|
375
|
+
value.status === 'error' ||
|
|
376
|
+
value.status === 'stopped') &&
|
|
377
|
+
typeof value.quiescent === 'boolean' &&
|
|
378
|
+
(!Object.prototype.hasOwnProperty.call(value, 'stateId') ||
|
|
379
|
+
(isNonEmptyString(value.stateId) &&
|
|
380
|
+
value.activeStateIds.length === 1 &&
|
|
381
|
+
value.activeStateIds[0] === value.stateId))
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function reviewSuccessFrom(event: unknown): ReviewSuccessOutput | undefined {
|
|
386
|
+
const output = playbookOutputOf(event);
|
|
387
|
+
if (!isPlainRecord(output)) return undefined;
|
|
388
|
+
if (!isExactKeys(output, ['approvedCommit', 'noUnsettledFindings'])) {
|
|
389
|
+
return undefined;
|
|
390
|
+
}
|
|
391
|
+
if (
|
|
392
|
+
output.approvedCommit !== 'latest' ||
|
|
393
|
+
output.noUnsettledFindings !== true
|
|
394
|
+
) {
|
|
395
|
+
return undefined;
|
|
396
|
+
}
|
|
397
|
+
return {
|
|
398
|
+
approvedCommit: 'latest',
|
|
399
|
+
noUnsettledFindings: true,
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const validReviewSuccess = ({ event }: { event: unknown }) =>
|
|
404
|
+
reviewSuccessFrom(event) !== undefined;
|
|
405
|
+
|
|
406
|
+
function normalizedChildFailure(error: unknown): ChildFailure | undefined {
|
|
407
|
+
if (!(error instanceof Error)) return undefined;
|
|
408
|
+
const result = (error as Error & { result?: unknown }).result;
|
|
409
|
+
if (!isPlainRecord(result)) return undefined;
|
|
410
|
+
const allowed = new Set([
|
|
411
|
+
'status',
|
|
412
|
+
'playbookId',
|
|
413
|
+
'childSessionId',
|
|
414
|
+
'state',
|
|
415
|
+
'error',
|
|
416
|
+
]);
|
|
417
|
+
if (
|
|
418
|
+
Reflect.ownKeys(result).some(
|
|
419
|
+
(key) => typeof key !== 'string' || !allowed.has(key),
|
|
420
|
+
) ||
|
|
421
|
+
(result.status !== 'aborted' && result.status !== 'error') ||
|
|
422
|
+
result.playbookId !== 'review' ||
|
|
423
|
+
Object.prototype.hasOwnProperty.call(result, 'output')
|
|
424
|
+
) {
|
|
425
|
+
return undefined;
|
|
426
|
+
}
|
|
427
|
+
if (
|
|
428
|
+
Object.prototype.hasOwnProperty.call(result, 'childSessionId') &&
|
|
429
|
+
!isNonEmptyString(result.childSessionId)
|
|
430
|
+
) {
|
|
431
|
+
return undefined;
|
|
432
|
+
}
|
|
433
|
+
if (
|
|
434
|
+
Object.prototype.hasOwnProperty.call(result, 'state') &&
|
|
435
|
+
!isPlaybookState(result.state)
|
|
436
|
+
) {
|
|
437
|
+
return undefined;
|
|
438
|
+
}
|
|
439
|
+
let normalizedError: ChildFailure['error'];
|
|
440
|
+
if (Object.prototype.hasOwnProperty.call(result, 'error')) {
|
|
441
|
+
if (!isPlainRecord(result.error)) return undefined;
|
|
442
|
+
const errorKeys = Reflect.ownKeys(result.error);
|
|
443
|
+
if (
|
|
444
|
+
errorKeys.some(
|
|
445
|
+
(key) =>
|
|
446
|
+
typeof key !== 'string' ||
|
|
447
|
+
(key !== 'name' && key !== 'message' && key !== 'stack'),
|
|
448
|
+
) ||
|
|
449
|
+
!Object.prototype.hasOwnProperty.call(result.error, 'name') ||
|
|
450
|
+
!Object.prototype.hasOwnProperty.call(result.error, 'message')
|
|
451
|
+
) {
|
|
452
|
+
return undefined;
|
|
453
|
+
}
|
|
454
|
+
if (
|
|
455
|
+
!isNonEmptyString(result.error.name) ||
|
|
456
|
+
typeof result.error.message !== 'string' ||
|
|
457
|
+
(Object.prototype.hasOwnProperty.call(result.error, 'stack') &&
|
|
458
|
+
typeof result.error.stack !== 'string')
|
|
459
|
+
) {
|
|
460
|
+
return undefined;
|
|
461
|
+
}
|
|
462
|
+
normalizedError = {
|
|
463
|
+
name: result.error.name,
|
|
464
|
+
message: result.error.message,
|
|
465
|
+
};
|
|
466
|
+
} else if (result.status === 'error') {
|
|
467
|
+
return undefined;
|
|
468
|
+
}
|
|
469
|
+
return {
|
|
470
|
+
status: result.status,
|
|
471
|
+
playbookId: 'review',
|
|
472
|
+
...(normalizedError ? { error: normalizedError } : {}),
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const authoredReviewFailure = ({ event }: { event: unknown }) =>
|
|
477
|
+
normalizedChildFailure((event as ActorErrorEvent).error) !== undefined;
|
|
478
|
+
|
|
479
|
+
function bossInterrupts(ids: readonly JumpableStateId[]): any[] {
|
|
480
|
+
return ids.map((id) => ({
|
|
481
|
+
guard: ({ event }: { event: DecideEvent }) =>
|
|
482
|
+
event.type === 'BOSS_INTERRUPT' &&
|
|
483
|
+
event.targetId === id &&
|
|
484
|
+
typeof event.bossIntent === 'string' &&
|
|
485
|
+
event.bossIntent.trim().length > 0,
|
|
486
|
+
target: `#${id}`,
|
|
487
|
+
reenter: true,
|
|
488
|
+
actions: 'copyInterruptedTopic',
|
|
489
|
+
}));
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function resumableStates(ids: readonly ResumableStateId[]): any[] {
|
|
493
|
+
return ids.map((id) => ({
|
|
494
|
+
guard: ({ context, event }: { context: DecideContext; event: DecideEvent }) =>
|
|
495
|
+
event.type === 'BOSS_REPLY' &&
|
|
496
|
+
event.answer.trim().length > 0 &&
|
|
497
|
+
resolvedQuestionId(context, event) === id,
|
|
498
|
+
target: `#${id}`,
|
|
499
|
+
reenter: true,
|
|
500
|
+
actions: 'rememberBossReply',
|
|
501
|
+
}));
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const stateMetadata: Record<
|
|
505
|
+
ResumableStateId,
|
|
506
|
+
{ sourceItem: string; player: Player }
|
|
507
|
+
> = {
|
|
508
|
+
askCoderProposal: { sourceItem: 'DECIDE-1', player: 'Coder' },
|
|
509
|
+
askReviewerProposal: { sourceItem: 'DECIDE-2', player: 'Reviewer' },
|
|
510
|
+
commitCoderProposal: { sourceItem: 'DECIDE-3', player: 'Coder' },
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
export const decideMachine = setup({
|
|
514
|
+
types: {
|
|
515
|
+
context: {} as DecideContext,
|
|
516
|
+
events: {} as DecideEvent,
|
|
517
|
+
input: {} as DecideInput,
|
|
518
|
+
output: {} as DecideOutput,
|
|
519
|
+
},
|
|
520
|
+
actors: {
|
|
521
|
+
player: fromPromise<PlayerOutput, PlayerInput>(async () => {
|
|
522
|
+
throw new Error('player actor must be provided by the runner');
|
|
523
|
+
}),
|
|
524
|
+
playbook: fromPromise<JsonValue | undefined, PlaybookInput>(async () => {
|
|
525
|
+
throw new Error('playbook actor must be provided by the runner');
|
|
526
|
+
}),
|
|
527
|
+
},
|
|
528
|
+
actions: {
|
|
529
|
+
copyStartTopic: assign({
|
|
530
|
+
callerTopic: ({ event }) =>
|
|
531
|
+
event.type === 'START_DECIDE' ? event.callerTopic : undefined,
|
|
532
|
+
coderProposal: undefined,
|
|
533
|
+
reviewerProposal: undefined,
|
|
534
|
+
latestCommit: undefined,
|
|
535
|
+
reviewResult: undefined,
|
|
536
|
+
reviewFailure: undefined,
|
|
537
|
+
lastResult: undefined,
|
|
538
|
+
lastError: undefined,
|
|
539
|
+
pendingBossQuestions: undefined,
|
|
540
|
+
bossReplies: undefined,
|
|
541
|
+
stagedCoderResult: undefined,
|
|
542
|
+
stagedReviewerResult: undefined,
|
|
543
|
+
}),
|
|
544
|
+
copyInterruptedTopic: assign({
|
|
545
|
+
callerTopic: ({ event }) =>
|
|
546
|
+
event.type === 'BOSS_INTERRUPT' ? event.bossIntent : undefined,
|
|
547
|
+
coderProposal: undefined,
|
|
548
|
+
reviewerProposal: undefined,
|
|
549
|
+
latestCommit: undefined,
|
|
550
|
+
reviewResult: undefined,
|
|
551
|
+
reviewFailure: undefined,
|
|
552
|
+
lastResult: undefined,
|
|
553
|
+
lastError: undefined,
|
|
554
|
+
pendingBossQuestions: undefined,
|
|
555
|
+
bossReplies: undefined,
|
|
556
|
+
stagedCoderResult: undefined,
|
|
557
|
+
stagedReviewerResult: undefined,
|
|
558
|
+
}),
|
|
559
|
+
stageCoderProposal: assign({
|
|
560
|
+
stagedCoderResult: ({ event }) => outputOf(event),
|
|
561
|
+
lastResult: ({ event }) => outputOf(event),
|
|
562
|
+
lastError: undefined,
|
|
563
|
+
}),
|
|
564
|
+
stageReviewerProposal: assign({
|
|
565
|
+
stagedReviewerResult: ({ event }) => outputOf(event),
|
|
566
|
+
lastResult: ({ event }) => outputOf(event),
|
|
567
|
+
lastError: undefined,
|
|
568
|
+
}),
|
|
569
|
+
promoteProposals: assign({
|
|
570
|
+
coderProposal: ({ context }) =>
|
|
571
|
+
context.stagedCoderResult?.coderProposal,
|
|
572
|
+
reviewerProposal: ({ context }) =>
|
|
573
|
+
context.stagedReviewerResult?.reviewerProposal,
|
|
574
|
+
stagedCoderResult: undefined,
|
|
575
|
+
stagedReviewerResult: undefined,
|
|
576
|
+
lastResult: undefined,
|
|
577
|
+
lastError: undefined,
|
|
578
|
+
}),
|
|
579
|
+
rememberCommit: assign({
|
|
580
|
+
latestCommit: ({ event }) => outputOf(event).latestCommit,
|
|
581
|
+
lastResult: ({ event }) => outputOf(event),
|
|
582
|
+
lastError: undefined,
|
|
583
|
+
}),
|
|
584
|
+
rememberReviewSuccess: assign({
|
|
585
|
+
reviewResult: ({ event }) => reviewSuccessFrom(event),
|
|
586
|
+
reviewFailure: undefined,
|
|
587
|
+
lastError: undefined,
|
|
588
|
+
}),
|
|
589
|
+
rememberReviewFailure: assign({
|
|
590
|
+
reviewFailure: ({ event }) =>
|
|
591
|
+
normalizedChildFailure((event as unknown as ActorErrorEvent).error),
|
|
592
|
+
lastError: undefined,
|
|
593
|
+
}),
|
|
594
|
+
rememberReviewProtocolFailure: assign({
|
|
595
|
+
reviewFailure: {
|
|
596
|
+
status: 'error',
|
|
597
|
+
playbookId: 'review',
|
|
598
|
+
error: {
|
|
599
|
+
name: 'ReviewProtocolError',
|
|
600
|
+
message:
|
|
601
|
+
'REVIEW returned without an approved latest commit and a no-findings result.',
|
|
602
|
+
},
|
|
603
|
+
},
|
|
604
|
+
lastError: undefined,
|
|
605
|
+
}),
|
|
606
|
+
rememberActorError: assign({
|
|
607
|
+
lastError: ({ event }) =>
|
|
608
|
+
(event as unknown as ActorErrorEvent).error,
|
|
609
|
+
}),
|
|
610
|
+
rememberMalformedActorOutput: assign({
|
|
611
|
+
lastError: ({ event }) =>
|
|
612
|
+
(event as unknown as PlayerDoneEvent | PlaybookDoneEvent).output,
|
|
613
|
+
}),
|
|
614
|
+
rememberMalformedBossReply: assign({
|
|
615
|
+
lastError: ({ event }) => event,
|
|
616
|
+
}),
|
|
617
|
+
setPendingBossQuestion: assign({
|
|
618
|
+
pendingBossQuestions: (
|
|
619
|
+
{ context, event },
|
|
620
|
+
params: PendingBossQuestionParams,
|
|
621
|
+
) => ({
|
|
622
|
+
...context.pendingBossQuestions,
|
|
623
|
+
[params.resumeStateId]: {
|
|
624
|
+
...params,
|
|
625
|
+
questionId: params.resumeStateId,
|
|
626
|
+
question: outputOf(event).question ?? '',
|
|
627
|
+
},
|
|
628
|
+
}),
|
|
629
|
+
bossReplies: ({ context }, params: PendingBossQuestionParams) =>
|
|
630
|
+
withoutKey(context.bossReplies, params.resumeStateId),
|
|
631
|
+
lastResult: ({ event }) => outputOf(event),
|
|
632
|
+
lastError: undefined,
|
|
633
|
+
}),
|
|
634
|
+
rememberBossReply: assign({
|
|
635
|
+
bossReplies: ({ context, event }) => {
|
|
636
|
+
const questionId = resolvedQuestionId(context, event);
|
|
637
|
+
return event.type === 'BOSS_REPLY' && questionId
|
|
638
|
+
? { ...context.bossReplies, [questionId]: event.answer }
|
|
639
|
+
: context.bossReplies;
|
|
640
|
+
},
|
|
641
|
+
lastError: undefined,
|
|
642
|
+
}),
|
|
643
|
+
clearBranchBossReplyContext: assign({
|
|
644
|
+
pendingBossQuestions: (
|
|
645
|
+
{ context },
|
|
646
|
+
params: { stateId: ResumableStateId },
|
|
647
|
+
) => withoutKey(context.pendingBossQuestions, params.stateId),
|
|
648
|
+
bossReplies: ({ context }, params: { stateId: ResumableStateId }) =>
|
|
649
|
+
withoutKey(context.bossReplies, params.stateId),
|
|
650
|
+
}),
|
|
651
|
+
clearProposalRoundContext: assign({
|
|
652
|
+
pendingBossQuestions: undefined,
|
|
653
|
+
bossReplies: undefined,
|
|
654
|
+
stagedCoderResult: undefined,
|
|
655
|
+
stagedReviewerResult: undefined,
|
|
656
|
+
}),
|
|
657
|
+
clearBossReplyContext: assign({
|
|
658
|
+
pendingBossQuestions: undefined,
|
|
659
|
+
bossReplies: undefined,
|
|
660
|
+
}),
|
|
661
|
+
},
|
|
662
|
+
guards: {
|
|
663
|
+
coderProposed,
|
|
664
|
+
reviewerProposed,
|
|
665
|
+
committed,
|
|
666
|
+
needsBossReplyWithQuestion,
|
|
667
|
+
needsBossReplyWithoutQuestion,
|
|
668
|
+
validReviewSuccess,
|
|
669
|
+
authoredReviewFailure,
|
|
670
|
+
},
|
|
671
|
+
}).createMachine({
|
|
672
|
+
id: 'decide',
|
|
673
|
+
initial: 'ready',
|
|
674
|
+
context: ({ input }): DecideContext => ({
|
|
675
|
+
coderLlm: input.coderLlm,
|
|
676
|
+
}),
|
|
677
|
+
output: ({ context }): DecideOutput => {
|
|
678
|
+
if (context.reviewResult) return context.reviewResult;
|
|
679
|
+
if (!context.latestCommit || !context.reviewFailure) {
|
|
680
|
+
throw new Error('DECIDE reached a final state without a result');
|
|
681
|
+
}
|
|
682
|
+
return {
|
|
683
|
+
lastDecideCommit: context.latestCommit,
|
|
684
|
+
noUnsettledFindings: false,
|
|
685
|
+
reviewStatus: context.reviewFailure.status,
|
|
686
|
+
...(context.reviewFailure.error
|
|
687
|
+
? { error: context.reviewFailure.error }
|
|
688
|
+
: {}),
|
|
689
|
+
};
|
|
690
|
+
},
|
|
691
|
+
on: {
|
|
692
|
+
BOSS_INTERRUPT: bossInterrupts(['independentProposals']),
|
|
693
|
+
},
|
|
694
|
+
states: {
|
|
695
|
+
ready: {
|
|
696
|
+
id: 'ready',
|
|
697
|
+
tags: 'playbook.parked',
|
|
698
|
+
description: 'Waiting for a topic to decide.',
|
|
699
|
+
meta: {
|
|
700
|
+
playbook: {
|
|
701
|
+
stateId: 'ready',
|
|
702
|
+
description: 'Waiting for a topic to decide.',
|
|
703
|
+
},
|
|
704
|
+
},
|
|
705
|
+
on: {
|
|
706
|
+
START_DECIDE: {
|
|
707
|
+
guard: ({ event }) => event.callerTopic.trim().length > 0,
|
|
708
|
+
target: 'independentProposals',
|
|
709
|
+
actions: 'copyStartTopic',
|
|
710
|
+
},
|
|
711
|
+
},
|
|
712
|
+
},
|
|
713
|
+
independentProposals: {
|
|
714
|
+
id: 'independentProposals',
|
|
715
|
+
type: 'parallel',
|
|
716
|
+
description: 'Coder and Reviewer prepare independent proposals.',
|
|
717
|
+
meta: {
|
|
718
|
+
playbook: {
|
|
719
|
+
stateId: 'independentProposals',
|
|
720
|
+
description: 'Coder and Reviewer prepare independent proposals.',
|
|
721
|
+
},
|
|
722
|
+
},
|
|
723
|
+
states: {
|
|
724
|
+
coder: {
|
|
725
|
+
initial: 'working',
|
|
726
|
+
states: {
|
|
727
|
+
working: {
|
|
728
|
+
id: 'askCoderProposal',
|
|
729
|
+
tags: 'playbook.busy',
|
|
730
|
+
description: 'Coder independently proposes a spec design.',
|
|
731
|
+
meta: {
|
|
732
|
+
playbook: {
|
|
733
|
+
stateId: 'askCoderProposal',
|
|
734
|
+
description: 'Coder independently proposes a spec design.',
|
|
735
|
+
player: 'Coder',
|
|
736
|
+
},
|
|
737
|
+
},
|
|
738
|
+
invoke: {
|
|
739
|
+
src: 'player',
|
|
740
|
+
input: ({ context }): PlayerInput => ({
|
|
741
|
+
stateId: 'askCoderProposal',
|
|
742
|
+
sourceItem: 'DECIDE-1',
|
|
743
|
+
player: 'Coder',
|
|
744
|
+
prompt: INDEPENDENT_PROPOSAL_PROMPT,
|
|
745
|
+
result: withNeedsBossReply({
|
|
746
|
+
proposed:
|
|
747
|
+
'Coder completed an independent proposal. Output shall include `coderProposal: <verbatim final text>`.',
|
|
748
|
+
}),
|
|
749
|
+
callerTopic: context.callerTopic,
|
|
750
|
+
...bossReplyFields(context, 'askCoderProposal'),
|
|
751
|
+
}),
|
|
752
|
+
onDone: [
|
|
753
|
+
{
|
|
754
|
+
guard: 'needsBossReplyWithQuestion',
|
|
755
|
+
target: 'waiting',
|
|
756
|
+
actions: {
|
|
757
|
+
type: 'setPendingBossQuestion',
|
|
758
|
+
params: {
|
|
759
|
+
...stateMetadata.askCoderProposal,
|
|
760
|
+
resumeStateId: 'askCoderProposal',
|
|
761
|
+
question: '',
|
|
762
|
+
},
|
|
763
|
+
},
|
|
764
|
+
},
|
|
765
|
+
{
|
|
766
|
+
guard: 'needsBossReplyWithoutQuestion',
|
|
767
|
+
target: '#failed',
|
|
768
|
+
actions: [
|
|
769
|
+
'rememberMalformedActorOutput',
|
|
770
|
+
'clearProposalRoundContext',
|
|
771
|
+
],
|
|
772
|
+
},
|
|
773
|
+
{
|
|
774
|
+
guard: 'coderProposed',
|
|
775
|
+
target: 'complete',
|
|
776
|
+
actions: [
|
|
777
|
+
'stageCoderProposal',
|
|
778
|
+
{
|
|
779
|
+
type: 'clearBranchBossReplyContext',
|
|
780
|
+
params: { stateId: 'askCoderProposal' },
|
|
781
|
+
},
|
|
782
|
+
],
|
|
783
|
+
},
|
|
784
|
+
{
|
|
785
|
+
target: '#failed',
|
|
786
|
+
actions: [
|
|
787
|
+
'rememberMalformedActorOutput',
|
|
788
|
+
'clearProposalRoundContext',
|
|
789
|
+
],
|
|
790
|
+
},
|
|
791
|
+
],
|
|
792
|
+
onError: {
|
|
793
|
+
target: '#failed',
|
|
794
|
+
actions: [
|
|
795
|
+
'rememberActorError',
|
|
796
|
+
'clearProposalRoundContext',
|
|
797
|
+
],
|
|
798
|
+
},
|
|
799
|
+
},
|
|
800
|
+
},
|
|
801
|
+
waiting: {
|
|
802
|
+
id: 'waitCoderProposalReply',
|
|
803
|
+
tags: 'playbook.parked',
|
|
804
|
+
description: 'Coder waits for Boss to answer a question.',
|
|
805
|
+
meta: {
|
|
806
|
+
playbook: {
|
|
807
|
+
stateId: 'waitCoderProposalReply',
|
|
808
|
+
description: 'Coder waits for Boss to answer a question.',
|
|
809
|
+
},
|
|
810
|
+
},
|
|
811
|
+
on: {
|
|
812
|
+
BOSS_REPLY: [
|
|
813
|
+
{
|
|
814
|
+
guard: bossReplyTargets('askCoderProposal', false),
|
|
815
|
+
target: '#failed',
|
|
816
|
+
actions: [
|
|
817
|
+
'rememberMalformedBossReply',
|
|
818
|
+
'clearProposalRoundContext',
|
|
819
|
+
],
|
|
820
|
+
},
|
|
821
|
+
{
|
|
822
|
+
guard: bossReplyTargets('askCoderProposal', true),
|
|
823
|
+
target: 'working',
|
|
824
|
+
actions: 'rememberBossReply',
|
|
825
|
+
},
|
|
826
|
+
],
|
|
827
|
+
},
|
|
828
|
+
},
|
|
829
|
+
complete: {
|
|
830
|
+
type: 'final',
|
|
831
|
+
description: 'Coder proposal is staged for the parallel join.',
|
|
832
|
+
meta: {
|
|
833
|
+
playbook: {
|
|
834
|
+
stateId: 'coderProposalComplete',
|
|
835
|
+
description:
|
|
836
|
+
'Coder proposal is staged for the parallel join.',
|
|
837
|
+
},
|
|
838
|
+
},
|
|
839
|
+
},
|
|
840
|
+
},
|
|
841
|
+
},
|
|
842
|
+
reviewer: {
|
|
843
|
+
initial: 'working',
|
|
844
|
+
states: {
|
|
845
|
+
working: {
|
|
846
|
+
id: 'askReviewerProposal',
|
|
847
|
+
tags: 'playbook.busy',
|
|
848
|
+
description: 'Reviewer independently proposes a spec design.',
|
|
849
|
+
meta: {
|
|
850
|
+
playbook: {
|
|
851
|
+
stateId: 'askReviewerProposal',
|
|
852
|
+
description:
|
|
853
|
+
'Reviewer independently proposes a spec design.',
|
|
854
|
+
player: 'Reviewer',
|
|
855
|
+
},
|
|
856
|
+
},
|
|
857
|
+
invoke: {
|
|
858
|
+
src: 'player',
|
|
859
|
+
input: ({ context }): PlayerInput => ({
|
|
860
|
+
stateId: 'askReviewerProposal',
|
|
861
|
+
sourceItem: 'DECIDE-2',
|
|
862
|
+
player: 'Reviewer',
|
|
863
|
+
prompt: INDEPENDENT_PROPOSAL_PROMPT,
|
|
864
|
+
result: withNeedsBossReply({
|
|
865
|
+
proposed:
|
|
866
|
+
'Reviewer completed an independent proposal. Output shall include `reviewerProposal: <verbatim final text>`.',
|
|
867
|
+
}),
|
|
868
|
+
callerTopic: context.callerTopic,
|
|
869
|
+
...bossReplyFields(context, 'askReviewerProposal'),
|
|
870
|
+
}),
|
|
871
|
+
onDone: [
|
|
872
|
+
{
|
|
873
|
+
guard: 'needsBossReplyWithQuestion',
|
|
874
|
+
target: 'waiting',
|
|
875
|
+
actions: {
|
|
876
|
+
type: 'setPendingBossQuestion',
|
|
877
|
+
params: {
|
|
878
|
+
...stateMetadata.askReviewerProposal,
|
|
879
|
+
resumeStateId: 'askReviewerProposal',
|
|
880
|
+
question: '',
|
|
881
|
+
},
|
|
882
|
+
},
|
|
883
|
+
},
|
|
884
|
+
{
|
|
885
|
+
guard: 'needsBossReplyWithoutQuestion',
|
|
886
|
+
target: '#failed',
|
|
887
|
+
actions: [
|
|
888
|
+
'rememberMalformedActorOutput',
|
|
889
|
+
'clearProposalRoundContext',
|
|
890
|
+
],
|
|
891
|
+
},
|
|
892
|
+
{
|
|
893
|
+
guard: 'reviewerProposed',
|
|
894
|
+
target: 'complete',
|
|
895
|
+
actions: [
|
|
896
|
+
'stageReviewerProposal',
|
|
897
|
+
{
|
|
898
|
+
type: 'clearBranchBossReplyContext',
|
|
899
|
+
params: { stateId: 'askReviewerProposal' },
|
|
900
|
+
},
|
|
901
|
+
],
|
|
902
|
+
},
|
|
903
|
+
{
|
|
904
|
+
target: '#failed',
|
|
905
|
+
actions: [
|
|
906
|
+
'rememberMalformedActorOutput',
|
|
907
|
+
'clearProposalRoundContext',
|
|
908
|
+
],
|
|
909
|
+
},
|
|
910
|
+
],
|
|
911
|
+
onError: {
|
|
912
|
+
target: '#failed',
|
|
913
|
+
actions: [
|
|
914
|
+
'rememberActorError',
|
|
915
|
+
'clearProposalRoundContext',
|
|
916
|
+
],
|
|
917
|
+
},
|
|
918
|
+
},
|
|
919
|
+
},
|
|
920
|
+
waiting: {
|
|
921
|
+
id: 'waitReviewerProposalReply',
|
|
922
|
+
tags: 'playbook.parked',
|
|
923
|
+
description: 'Reviewer waits for Boss to answer a question.',
|
|
924
|
+
meta: {
|
|
925
|
+
playbook: {
|
|
926
|
+
stateId: 'waitReviewerProposalReply',
|
|
927
|
+
description:
|
|
928
|
+
'Reviewer waits for Boss to answer a question.',
|
|
929
|
+
},
|
|
930
|
+
},
|
|
931
|
+
on: {
|
|
932
|
+
BOSS_REPLY: [
|
|
933
|
+
{
|
|
934
|
+
guard: bossReplyTargets('askReviewerProposal', false),
|
|
935
|
+
target: '#failed',
|
|
936
|
+
actions: [
|
|
937
|
+
'rememberMalformedBossReply',
|
|
938
|
+
'clearProposalRoundContext',
|
|
939
|
+
],
|
|
940
|
+
},
|
|
941
|
+
{
|
|
942
|
+
guard: bossReplyTargets('askReviewerProposal', true),
|
|
943
|
+
target: 'working',
|
|
944
|
+
actions: 'rememberBossReply',
|
|
945
|
+
},
|
|
946
|
+
],
|
|
947
|
+
},
|
|
948
|
+
},
|
|
949
|
+
complete: {
|
|
950
|
+
type: 'final',
|
|
951
|
+
description:
|
|
952
|
+
'Reviewer proposal is staged for the parallel join.',
|
|
953
|
+
meta: {
|
|
954
|
+
playbook: {
|
|
955
|
+
stateId: 'reviewerProposalComplete',
|
|
956
|
+
description:
|
|
957
|
+
'Reviewer proposal is staged for the parallel join.',
|
|
958
|
+
},
|
|
959
|
+
},
|
|
960
|
+
},
|
|
961
|
+
},
|
|
962
|
+
},
|
|
963
|
+
},
|
|
964
|
+
onDone: {
|
|
965
|
+
target: 'commitCoderProposal',
|
|
966
|
+
actions: 'promoteProposals',
|
|
967
|
+
},
|
|
968
|
+
},
|
|
969
|
+
commitCoderProposal: {
|
|
970
|
+
id: 'commitCoderProposal',
|
|
971
|
+
tags: 'playbook.busy',
|
|
972
|
+
description: 'Coder writes and commits Coder’s independent proposal.',
|
|
973
|
+
meta: {
|
|
974
|
+
playbook: {
|
|
975
|
+
stateId: 'commitCoderProposal',
|
|
976
|
+
description: 'Coder writes and commits Coder’s independent proposal.',
|
|
977
|
+
player: 'Coder',
|
|
978
|
+
},
|
|
979
|
+
},
|
|
980
|
+
invoke: {
|
|
981
|
+
src: 'player',
|
|
982
|
+
input: ({ context }): PlayerInput => ({
|
|
983
|
+
stateId: 'commitCoderProposal',
|
|
984
|
+
sourceItem: 'DECIDE-3',
|
|
985
|
+
player: 'Coder',
|
|
986
|
+
prompt: COMMIT_CODER_PROMPT,
|
|
987
|
+
result: withNeedsBossReply({
|
|
988
|
+
committed:
|
|
989
|
+
"Coder committed Coder's proposal. Output shall include `coderOutput: <verbatim final text>` and `latestCommit: <commit identity>`.",
|
|
990
|
+
}),
|
|
991
|
+
coderLlm: context.coderLlm,
|
|
992
|
+
...bossReplyFields(context, 'commitCoderProposal'),
|
|
993
|
+
}),
|
|
994
|
+
onDone: [
|
|
995
|
+
{
|
|
996
|
+
guard: 'needsBossReplyWithQuestion',
|
|
997
|
+
target: 'awaitBossReply',
|
|
998
|
+
actions: {
|
|
999
|
+
type: 'setPendingBossQuestion',
|
|
1000
|
+
params: {
|
|
1001
|
+
...stateMetadata.commitCoderProposal,
|
|
1002
|
+
resumeStateId: 'commitCoderProposal',
|
|
1003
|
+
question: '',
|
|
1004
|
+
},
|
|
1005
|
+
},
|
|
1006
|
+
},
|
|
1007
|
+
{
|
|
1008
|
+
guard: 'needsBossReplyWithoutQuestion',
|
|
1009
|
+
target: 'failed',
|
|
1010
|
+
actions: [
|
|
1011
|
+
'rememberMalformedActorOutput',
|
|
1012
|
+
'clearBossReplyContext',
|
|
1013
|
+
],
|
|
1014
|
+
},
|
|
1015
|
+
{
|
|
1016
|
+
guard: 'committed',
|
|
1017
|
+
target: 'reviewCommit',
|
|
1018
|
+
actions: [
|
|
1019
|
+
'rememberCommit',
|
|
1020
|
+
{
|
|
1021
|
+
type: 'clearBranchBossReplyContext',
|
|
1022
|
+
params: { stateId: 'commitCoderProposal' },
|
|
1023
|
+
},
|
|
1024
|
+
],
|
|
1025
|
+
},
|
|
1026
|
+
{
|
|
1027
|
+
target: 'failed',
|
|
1028
|
+
actions: [
|
|
1029
|
+
'rememberMalformedActorOutput',
|
|
1030
|
+
'clearBossReplyContext',
|
|
1031
|
+
],
|
|
1032
|
+
},
|
|
1033
|
+
],
|
|
1034
|
+
onError: {
|
|
1035
|
+
target: 'failed',
|
|
1036
|
+
actions: ['rememberActorError', 'clearBossReplyContext'],
|
|
1037
|
+
},
|
|
1038
|
+
},
|
|
1039
|
+
},
|
|
1040
|
+
awaitBossReply: {
|
|
1041
|
+
id: 'awaitBossReply',
|
|
1042
|
+
tags: 'playbook.parked',
|
|
1043
|
+
description: 'Waiting for Boss to answer Coder’s question.',
|
|
1044
|
+
meta: {
|
|
1045
|
+
playbook: {
|
|
1046
|
+
stateId: 'awaitBossReply',
|
|
1047
|
+
description: 'Waiting for Boss to answer Coder’s question.',
|
|
1048
|
+
},
|
|
1049
|
+
},
|
|
1050
|
+
on: {
|
|
1051
|
+
BOSS_REPLY: [
|
|
1052
|
+
{
|
|
1053
|
+
guard: bossReplyTargets('commitCoderProposal', false),
|
|
1054
|
+
target: 'failed',
|
|
1055
|
+
actions: [
|
|
1056
|
+
'rememberMalformedBossReply',
|
|
1057
|
+
'clearBossReplyContext',
|
|
1058
|
+
],
|
|
1059
|
+
},
|
|
1060
|
+
...resumableStates(['commitCoderProposal']),
|
|
1061
|
+
],
|
|
1062
|
+
},
|
|
1063
|
+
},
|
|
1064
|
+
reviewCommit: {
|
|
1065
|
+
id: 'reviewCommit',
|
|
1066
|
+
tags: 'playbook.suspended',
|
|
1067
|
+
description: 'REVIEW examines the committed proposal.',
|
|
1068
|
+
meta: {
|
|
1069
|
+
playbook: {
|
|
1070
|
+
stateId: 'reviewCommit',
|
|
1071
|
+
description: 'REVIEW examines the committed proposal.',
|
|
1072
|
+
},
|
|
1073
|
+
},
|
|
1074
|
+
invoke: {
|
|
1075
|
+
src: 'playbook',
|
|
1076
|
+
input: ({ context }): PlaybookInput => ({
|
|
1077
|
+
stateId: 'reviewCommit',
|
|
1078
|
+
sourceItem: 'DECIDE-4',
|
|
1079
|
+
playbookId: 'review',
|
|
1080
|
+
text: composeReviewInput(
|
|
1081
|
+
context.callerTopic,
|
|
1082
|
+
context.coderProposal,
|
|
1083
|
+
),
|
|
1084
|
+
}),
|
|
1085
|
+
onDone: [
|
|
1086
|
+
{
|
|
1087
|
+
guard: 'validReviewSuccess',
|
|
1088
|
+
target: 'done',
|
|
1089
|
+
actions: 'rememberReviewSuccess',
|
|
1090
|
+
},
|
|
1091
|
+
{
|
|
1092
|
+
target: 'reportedReviewFailure',
|
|
1093
|
+
actions: 'rememberReviewProtocolFailure',
|
|
1094
|
+
},
|
|
1095
|
+
],
|
|
1096
|
+
onError: [
|
|
1097
|
+
{
|
|
1098
|
+
guard: 'authoredReviewFailure',
|
|
1099
|
+
target: 'reportedReviewFailure',
|
|
1100
|
+
actions: 'rememberReviewFailure',
|
|
1101
|
+
},
|
|
1102
|
+
{
|
|
1103
|
+
target: 'failed',
|
|
1104
|
+
actions: 'rememberActorError',
|
|
1105
|
+
},
|
|
1106
|
+
],
|
|
1107
|
+
},
|
|
1108
|
+
},
|
|
1109
|
+
failed: {
|
|
1110
|
+
id: 'failed',
|
|
1111
|
+
tags: 'playbook.parked',
|
|
1112
|
+
description: 'DECIDE failed and is waiting for a new topic.',
|
|
1113
|
+
meta: {
|
|
1114
|
+
playbook: {
|
|
1115
|
+
stateId: 'failed',
|
|
1116
|
+
description: 'DECIDE failed and is waiting for a new topic.',
|
|
1117
|
+
},
|
|
1118
|
+
},
|
|
1119
|
+
on: {
|
|
1120
|
+
START_DECIDE: {
|
|
1121
|
+
guard: ({ event }) => event.callerTopic.trim().length > 0,
|
|
1122
|
+
target: 'independentProposals',
|
|
1123
|
+
actions: 'copyStartTopic',
|
|
1124
|
+
},
|
|
1125
|
+
},
|
|
1126
|
+
},
|
|
1127
|
+
reportedReviewFailure: {
|
|
1128
|
+
id: 'reportedReviewFailure',
|
|
1129
|
+
type: 'final',
|
|
1130
|
+
description: 'DECIDE reports REVIEW’s failure and its last commit.',
|
|
1131
|
+
meta: {
|
|
1132
|
+
playbook: {
|
|
1133
|
+
stateId: 'reportedReviewFailure',
|
|
1134
|
+
description: 'DECIDE reports REVIEW’s failure and its last commit.',
|
|
1135
|
+
},
|
|
1136
|
+
},
|
|
1137
|
+
},
|
|
1138
|
+
done: {
|
|
1139
|
+
id: 'done',
|
|
1140
|
+
type: 'final',
|
|
1141
|
+
description: 'DECIDE completed with an approved commit.',
|
|
1142
|
+
meta: {
|
|
1143
|
+
playbook: {
|
|
1144
|
+
stateId: 'done',
|
|
1145
|
+
description: 'DECIDE completed with an approved commit.',
|
|
1146
|
+
},
|
|
1147
|
+
},
|
|
1148
|
+
},
|
|
1149
|
+
},
|
|
1150
|
+
});
|
|
1151
|
+
|
|
1152
|
+
export default decideMachine;
|