@sublang/playbook 11.0.0 → 12.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/docs/cli.md +9 -6
- package/docs/configuration.md +15 -1
- package/package.json +22 -3
- package/reference/sdlc/code.playbook/code.fsm.d.ts +22 -19
- package/reference/sdlc/code.playbook/code.fsm.js +116 -52
- package/reference/sdlc/code.playbook/code.fsm.ts +149 -64
- package/reference/sdlc/code.playbook/code.gears.md +40 -20
- package/reference/sdlc/code.playbook/code.playbook.js +23 -2
- package/reference/sdlc/code.playbook/code.playbook.ts +23 -2
- package/reference/sdlc/code.playbook/playbook.config.template.yaml +18 -2
- package/reference/sdlc/decide.playbook/decide.fsm.d.ts +13 -6
- package/reference/sdlc/decide.playbook/decide.fsm.js +54 -27
- package/reference/sdlc/decide.playbook/decide.fsm.ts +68 -29
- package/reference/sdlc/decide.playbook/decide.gears.md +25 -19
- package/reference/sdlc/decide.playbook/decide.playbook.js +11 -3
- package/reference/sdlc/decide.playbook/decide.playbook.ts +11 -3
- package/reference/sdlc/decide.playbook/decide.registry.js +1 -1
- package/reference/sdlc/decide.playbook/decide.registry.ts +1 -1
- package/reference/sdlc/dev.md +52 -0
- package/reference/sdlc/dev.playbook/dev.fsm.d.ts +261 -0
- package/reference/sdlc/dev.playbook/dev.fsm.js +723 -0
- package/reference/sdlc/dev.playbook/dev.fsm.ts +988 -0
- package/reference/sdlc/dev.playbook/dev.gears.md +91 -0
- package/reference/sdlc/dev.playbook/dev.playbook.d.ts +21 -0
- package/reference/sdlc/dev.playbook/dev.playbook.js +143 -0
- package/reference/sdlc/dev.playbook/dev.playbook.ts +246 -0
- package/reference/sdlc/dev.playbook/dev.registry.d.ts +40 -0
- package/reference/sdlc/dev.playbook/dev.registry.js +64 -0
- package/reference/sdlc/dev.playbook/dev.registry.ts +120 -0
- package/reference/sdlc/review.playbook/review.fsm.d.ts +15 -2
- package/reference/sdlc/review.playbook/review.fsm.js +77 -27
- package/reference/sdlc/review.playbook/review.fsm.ts +96 -30
- package/reference/sdlc/review.playbook/review.gears.md +52 -26
- package/reference/sdlc/review.playbook/review.playbook.js +17 -7
- package/reference/sdlc/review.playbook/review.playbook.ts +17 -7
- package/reference/sdlc/review.playbook/review.registry.js +1 -1
- package/reference/sdlc/review.playbook/review.registry.ts +1 -1
- package/slc/link.md +12 -5
- package/slc/text2gears.md +3 -0
- package/src/xstate-playbook-runtime.js +5 -2
- package/src/xstate-playbook-runtime.ts +5 -2
- package/src/xstate-runtime.js +13 -1
- package/src/xstate-runtime.ts +13 -1
|
@@ -0,0 +1,723 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// SPDX-FileCopyrightText: 2026 SubLang International <https://sublang.ai>
|
|
3
|
+
//
|
|
4
|
+
// Generated by slc/gears2fsm.md from ./dev.gears.md.
|
|
5
|
+
import { assign, fromPromise, setup } from 'xstate';
|
|
6
|
+
const PLAN_ANALYSIS_PROMPT = [
|
|
7
|
+
'> <development-request>',
|
|
8
|
+
'> <discussion-context>',
|
|
9
|
+
'> <run-results>',
|
|
10
|
+
'',
|
|
11
|
+
'Inspect the request and the relevant repository and specs only as needed to determine the smallest sound next step.',
|
|
12
|
+
'Do not change files or commit while planning or discussing the request.',
|
|
13
|
+
'',
|
|
14
|
+
'- If useful analysis or clarification should be discussed before any repository work, give Boss the useful response and ask one material question that advances the decision.',
|
|
15
|
+
'- If the discussion has concluded after a Boss reply and no repository work should follow, choose `discussion complete`.',
|
|
16
|
+
'- If implementation can proceed under the existing decisions, choose `code`.',
|
|
17
|
+
'- If implementation first requires a new or amended durable decision that the existing specs do not settle, choose `decide then code`.',
|
|
18
|
+
'',
|
|
19
|
+
'A question or exploratory discussion is not by itself authorization to create a durable decision or implement changes.',
|
|
20
|
+
'Do not choose `decide then code` merely because the work is large.',
|
|
21
|
+
'Consult @specs/map.md for relevant context and @specs/meta.md for spec requirements, if needed.',
|
|
22
|
+
].join('\n');
|
|
23
|
+
const PLAN_ANALYSIS_RESULTS = {
|
|
24
|
+
discussionComplete: 'Analyst concluded the discussion after a Boss reply, with no repository work to follow.',
|
|
25
|
+
code: 'Analyst determined implementation can proceed under the existing decisions. Output shall include `planningResult: <verbatim final text>`.',
|
|
26
|
+
decideThenCode: 'Analyst determined implementation first requires a new or amended durable decision that the existing specs do not settle. Output shall include `planningResult: <verbatim final text>`.',
|
|
27
|
+
needsBossReply: "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>`.",
|
|
28
|
+
};
|
|
29
|
+
const STATE_DESCRIPTIONS = {
|
|
30
|
+
ready: 'Waiting for a development request.',
|
|
31
|
+
planAnalysis: 'Analyst is analyzing the development request to choose the smallest sound next step.',
|
|
32
|
+
callCode: 'The CODE playbook is implementing the planned development path.',
|
|
33
|
+
callDecide: 'The DECIDE playbook is settling the durable decision the planned path requires.',
|
|
34
|
+
callCodeAfterDecide: 'The CODE playbook is implementing the decided development path.',
|
|
35
|
+
awaitBossReply: 'Waiting for Boss to answer Analyst.',
|
|
36
|
+
failed: 'The development planning workflow failed and is waiting for a new development request.',
|
|
37
|
+
discussionComplete: 'The planning discussion concluded after a Boss reply with no repository work to follow.',
|
|
38
|
+
done: "The selected development path completed with the final child playbook's successful result.",
|
|
39
|
+
reportedChildFailure: "The development workflow relayed a child playbook's authored abort, failure, or insufficient terminal result.",
|
|
40
|
+
};
|
|
41
|
+
function playbookMeta(stateId, role) {
|
|
42
|
+
return {
|
|
43
|
+
playbook: {
|
|
44
|
+
stateId,
|
|
45
|
+
description: STATE_DESCRIPTIONS[stateId],
|
|
46
|
+
...(role === undefined ? {} : { role }),
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function isRecord(value) {
|
|
51
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
const prototype = Object.getPrototypeOf(value);
|
|
55
|
+
return prototype === Object.prototype || prototype === null;
|
|
56
|
+
}
|
|
57
|
+
function isNonEmptyString(value) {
|
|
58
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
59
|
+
}
|
|
60
|
+
function playerOutput(event) {
|
|
61
|
+
if (!isRecord(event))
|
|
62
|
+
return undefined;
|
|
63
|
+
return isRecord(event.output) ? event.output : undefined;
|
|
64
|
+
}
|
|
65
|
+
function outputGuard(event, guard) {
|
|
66
|
+
return playerOutput(event)?.guard === guard;
|
|
67
|
+
}
|
|
68
|
+
function outputString(event, field) {
|
|
69
|
+
const value = playerOutput(event)?.[field];
|
|
70
|
+
return isNonEmptyString(value) ? value : undefined;
|
|
71
|
+
}
|
|
72
|
+
function isCodePath({ event }) {
|
|
73
|
+
return (outputGuard(event, 'code') &&
|
|
74
|
+
outputString(event, 'planningResult') !== undefined);
|
|
75
|
+
}
|
|
76
|
+
function isDecideThenCode({ event }) {
|
|
77
|
+
return (outputGuard(event, 'decideThenCode') &&
|
|
78
|
+
outputString(event, 'planningResult') !== undefined);
|
|
79
|
+
}
|
|
80
|
+
// Discussion complete is available only after a Boss reply: the round that
|
|
81
|
+
// produced it must itself have been resumed with Boss's answer.
|
|
82
|
+
function isDiscussionComplete({ context, event, }) {
|
|
83
|
+
return (outputGuard(event, 'discussionComplete') &&
|
|
84
|
+
isNonEmptyString(context.bossReply));
|
|
85
|
+
}
|
|
86
|
+
function needsBossReply({ event }) {
|
|
87
|
+
return (outputGuard(event, 'needsBossReply') &&
|
|
88
|
+
outputString(event, 'question') !== undefined);
|
|
89
|
+
}
|
|
90
|
+
function childOutputOf(event) {
|
|
91
|
+
return isRecord(event) ? event.output : undefined;
|
|
92
|
+
}
|
|
93
|
+
function isDecideSuccessValue(value) {
|
|
94
|
+
return (isRecord(value) &&
|
|
95
|
+
isNonEmptyString(value.decideCommit) &&
|
|
96
|
+
isNonEmptyString(value.evaluatedRevision) &&
|
|
97
|
+
value.noUnsettledFindings === true);
|
|
98
|
+
}
|
|
99
|
+
function isDecideSuccess({ event }) {
|
|
100
|
+
return isDecideSuccessValue(childOutputOf(event));
|
|
101
|
+
}
|
|
102
|
+
// CODE's canonical successful terminal output discriminates completion with
|
|
103
|
+
// `status: 'complete'`, carries the exact last CODE-owned commit, and
|
|
104
|
+
// affirms that every phase's review passed.
|
|
105
|
+
function isCodeSuccessValue(value) {
|
|
106
|
+
return (isRecord(value) &&
|
|
107
|
+
value.status === 'complete' &&
|
|
108
|
+
isNonEmptyString(value.lastCodeCommit) &&
|
|
109
|
+
value.allReviewsPassed === true);
|
|
110
|
+
}
|
|
111
|
+
function isCodeSuccess({ event }) {
|
|
112
|
+
return isCodeSuccessValue(childOutputOf(event));
|
|
113
|
+
}
|
|
114
|
+
function isStateValue(value, ancestors = new Set()) {
|
|
115
|
+
if (typeof value === 'string')
|
|
116
|
+
return true;
|
|
117
|
+
if (!isRecord(value) || ancestors.has(value))
|
|
118
|
+
return false;
|
|
119
|
+
const next = new Set(ancestors).add(value);
|
|
120
|
+
return Reflect.ownKeys(value).every((key) => typeof key === 'string' &&
|
|
121
|
+
Object.prototype.propertyIsEnumerable.call(value, key) &&
|
|
122
|
+
isStateValue(value[key], next));
|
|
123
|
+
}
|
|
124
|
+
function isStringArray(value) {
|
|
125
|
+
return (Array.isArray(value) &&
|
|
126
|
+
Reflect.ownKeys(value).length === value.length + 1 &&
|
|
127
|
+
Reflect.ownKeys(value).every((key) => {
|
|
128
|
+
if (key === 'length')
|
|
129
|
+
return true;
|
|
130
|
+
return (typeof key === 'string' &&
|
|
131
|
+
/^(0|[1-9][0-9]*)$/.test(key) &&
|
|
132
|
+
Number(key) < value.length);
|
|
133
|
+
}) &&
|
|
134
|
+
value.every((entry) => isNonEmptyString(entry)) &&
|
|
135
|
+
new Set(value).size === value.length);
|
|
136
|
+
}
|
|
137
|
+
function isPlaybookState(value) {
|
|
138
|
+
if (!isRecord(value))
|
|
139
|
+
return false;
|
|
140
|
+
const allowed = new Set([
|
|
141
|
+
'value',
|
|
142
|
+
'activeStateIds',
|
|
143
|
+
'tags',
|
|
144
|
+
'status',
|
|
145
|
+
'quiescent',
|
|
146
|
+
'stateId',
|
|
147
|
+
]);
|
|
148
|
+
if (Reflect.ownKeys(value).some((key) => typeof key !== 'string' || !allowed.has(key)) ||
|
|
149
|
+
!Object.prototype.hasOwnProperty.call(value, 'value') ||
|
|
150
|
+
!Object.prototype.hasOwnProperty.call(value, 'activeStateIds') ||
|
|
151
|
+
!Object.prototype.hasOwnProperty.call(value, 'tags') ||
|
|
152
|
+
!Object.prototype.hasOwnProperty.call(value, 'status') ||
|
|
153
|
+
!Object.prototype.hasOwnProperty.call(value, 'quiescent')) {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
return (isStateValue(value.value) &&
|
|
157
|
+
isStringArray(value.activeStateIds) &&
|
|
158
|
+
isStringArray(value.tags) &&
|
|
159
|
+
(value.status === 'active' ||
|
|
160
|
+
value.status === 'done' ||
|
|
161
|
+
value.status === 'error' ||
|
|
162
|
+
value.status === 'stopped') &&
|
|
163
|
+
typeof value.quiescent === 'boolean' &&
|
|
164
|
+
(!Object.prototype.hasOwnProperty.call(value, 'stateId') ||
|
|
165
|
+
(isNonEmptyString(value.stateId) &&
|
|
166
|
+
value.activeStateIds.length === 1 &&
|
|
167
|
+
value.activeStateIds[0] === value.stateId)));
|
|
168
|
+
}
|
|
169
|
+
function nestedResultFromError(error) {
|
|
170
|
+
if (!(error instanceof Error))
|
|
171
|
+
return undefined;
|
|
172
|
+
const result = error.result;
|
|
173
|
+
return isRecord(result) ? result : undefined;
|
|
174
|
+
}
|
|
175
|
+
function normalizedChildFailure(error, playbookId) {
|
|
176
|
+
const result = nestedResultFromError(error);
|
|
177
|
+
if (result === undefined)
|
|
178
|
+
return undefined;
|
|
179
|
+
const allowed = new Set([
|
|
180
|
+
'status',
|
|
181
|
+
'playbookId',
|
|
182
|
+
'childSessionId',
|
|
183
|
+
'state',
|
|
184
|
+
'error',
|
|
185
|
+
]);
|
|
186
|
+
if (Reflect.ownKeys(result).some((key) => typeof key !== 'string' || !allowed.has(key)) ||
|
|
187
|
+
(result.status !== 'aborted' && result.status !== 'error') ||
|
|
188
|
+
result.playbookId !== playbookId) {
|
|
189
|
+
return undefined;
|
|
190
|
+
}
|
|
191
|
+
if (Object.prototype.hasOwnProperty.call(result, 'childSessionId') &&
|
|
192
|
+
!isNonEmptyString(result.childSessionId)) {
|
|
193
|
+
return undefined;
|
|
194
|
+
}
|
|
195
|
+
if (Object.prototype.hasOwnProperty.call(result, 'state') &&
|
|
196
|
+
!isPlaybookState(result.state)) {
|
|
197
|
+
return undefined;
|
|
198
|
+
}
|
|
199
|
+
let normalizedError;
|
|
200
|
+
if (Object.prototype.hasOwnProperty.call(result, 'error')) {
|
|
201
|
+
if (!isRecord(result.error))
|
|
202
|
+
return undefined;
|
|
203
|
+
const errorKeys = Reflect.ownKeys(result.error);
|
|
204
|
+
if (errorKeys.some((key) => typeof key !== 'string' ||
|
|
205
|
+
(key !== 'name' && key !== 'message' && key !== 'stack')) ||
|
|
206
|
+
!Object.prototype.hasOwnProperty.call(result.error, 'name') ||
|
|
207
|
+
!Object.prototype.hasOwnProperty.call(result.error, 'message') ||
|
|
208
|
+
!isNonEmptyString(result.error.name) ||
|
|
209
|
+
typeof result.error.message !== 'string' ||
|
|
210
|
+
(Object.prototype.hasOwnProperty.call(result.error, 'stack') &&
|
|
211
|
+
typeof result.error.stack !== 'string')) {
|
|
212
|
+
return undefined;
|
|
213
|
+
}
|
|
214
|
+
normalizedError = {
|
|
215
|
+
name: result.error.name,
|
|
216
|
+
message: result.error.message,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
else if (result.status === 'error') {
|
|
220
|
+
return undefined;
|
|
221
|
+
}
|
|
222
|
+
return {
|
|
223
|
+
status: result.status,
|
|
224
|
+
...(normalizedError === undefined ? {} : { error: normalizedError }),
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
function authoredChildFailureGuard(playbookId) {
|
|
228
|
+
return ({ event }) => {
|
|
229
|
+
const error = isRecord(event) ? event.error : undefined;
|
|
230
|
+
return normalizedChildFailure(error, playbookId) !== undefined;
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
function authoredChildError(event, playbookId) {
|
|
234
|
+
const outer = isRecord(event) ? event.error : undefined;
|
|
235
|
+
const failure = normalizedChildFailure(outer, playbookId);
|
|
236
|
+
if (failure?.error !== undefined)
|
|
237
|
+
return failure.error;
|
|
238
|
+
if (failure?.status === 'aborted') {
|
|
239
|
+
return {
|
|
240
|
+
name: 'AbortError',
|
|
241
|
+
message: `${playbookId.toUpperCase()} was aborted.`,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
if (outer instanceof Error) {
|
|
245
|
+
return { name: outer.name || 'Error', message: outer.message };
|
|
246
|
+
}
|
|
247
|
+
return { name: 'Error', message: String(outer) };
|
|
248
|
+
}
|
|
249
|
+
function relayedChildFailure(event, playbookId) {
|
|
250
|
+
const outer = isRecord(event) ? event.error : undefined;
|
|
251
|
+
const failure = normalizedChildFailure(outer, playbookId);
|
|
252
|
+
return {
|
|
253
|
+
playbookId,
|
|
254
|
+
status: failure?.status ?? 'error',
|
|
255
|
+
error: authoredChildError(event, playbookId),
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
function insufficientChildResult(event, playbookId) {
|
|
259
|
+
const output = childOutputOf(event);
|
|
260
|
+
return {
|
|
261
|
+
playbookId,
|
|
262
|
+
status: 'ok',
|
|
263
|
+
...(output === undefined ? {} : { output: output }),
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
/** Consumed planning Q&A rendered for later relayed discussion context. */
|
|
267
|
+
export function renderDiscussionContext(exchanges) {
|
|
268
|
+
return exchanges
|
|
269
|
+
.map((exchange) => `Analyst question: ${exchange.question}\nBoss reply: ${exchange.answer}`)
|
|
270
|
+
.join('\n\n');
|
|
271
|
+
}
|
|
272
|
+
function archivedExchanges(context) {
|
|
273
|
+
if (context.pendingBossQuestion === undefined ||
|
|
274
|
+
!isNonEmptyString(context.bossReply)) {
|
|
275
|
+
return context.discussionExchanges;
|
|
276
|
+
}
|
|
277
|
+
return [
|
|
278
|
+
...context.discussionExchanges,
|
|
279
|
+
{
|
|
280
|
+
question: context.pendingBossQuestion.question,
|
|
281
|
+
answer: context.bossReply,
|
|
282
|
+
},
|
|
283
|
+
];
|
|
284
|
+
}
|
|
285
|
+
function quotedRelay(values) {
|
|
286
|
+
return values
|
|
287
|
+
.filter((value) => value.length > 0)
|
|
288
|
+
.map((value) => value
|
|
289
|
+
.split('\n')
|
|
290
|
+
.map((line) => `> ${line}`)
|
|
291
|
+
.join('\n'))
|
|
292
|
+
.join('\n');
|
|
293
|
+
}
|
|
294
|
+
function planningRelayValues(context) {
|
|
295
|
+
return [
|
|
296
|
+
context.developmentRequest ?? '',
|
|
297
|
+
renderDiscussionContext(context.discussionExchanges),
|
|
298
|
+
context.planningResult ?? '',
|
|
299
|
+
];
|
|
300
|
+
}
|
|
301
|
+
function codeCallText(context) {
|
|
302
|
+
return quotedRelay(planningRelayValues(context));
|
|
303
|
+
}
|
|
304
|
+
function decideCallText(context) {
|
|
305
|
+
return quotedRelay(planningRelayValues(context));
|
|
306
|
+
}
|
|
307
|
+
function codeAfterDecideCallText(context) {
|
|
308
|
+
return quotedRelay([
|
|
309
|
+
...planningRelayValues(context),
|
|
310
|
+
context.decideCommit ?? '',
|
|
311
|
+
context.evaluatedRevision ?? '',
|
|
312
|
+
]);
|
|
313
|
+
}
|
|
314
|
+
const machineSetup = setup({
|
|
315
|
+
types: {},
|
|
316
|
+
actors: {
|
|
317
|
+
player: fromPromise(async () => {
|
|
318
|
+
throw new Error('player actor must be provided by the runner');
|
|
319
|
+
}),
|
|
320
|
+
playbook: fromPromise(async () => {
|
|
321
|
+
throw new Error('playbook actor must be provided by the runner');
|
|
322
|
+
}),
|
|
323
|
+
},
|
|
324
|
+
guards: {
|
|
325
|
+
isDiscussionComplete,
|
|
326
|
+
isCodePath,
|
|
327
|
+
isDecideThenCode,
|
|
328
|
+
needsBossReply,
|
|
329
|
+
isCodeSuccess,
|
|
330
|
+
isDecideSuccess,
|
|
331
|
+
authoredCodeFailure: authoredChildFailureGuard('code'),
|
|
332
|
+
authoredDecideFailure: authoredChildFailureGuard('decide'),
|
|
333
|
+
emptyBossReply: ({ event }) => event.type === 'BOSS_REPLY' && event.answer.trim().length === 0,
|
|
334
|
+
resumesPlanAnalysis: ({ context, event }) => event.type === 'BOSS_REPLY' &&
|
|
335
|
+
event.answer.trim().length > 0 &&
|
|
336
|
+
context.pendingBossQuestion?.resumeStateId === 'planAnalysis' &&
|
|
337
|
+
(event.questionId === undefined || event.questionId === 'planAnalysis'),
|
|
338
|
+
},
|
|
339
|
+
actions: {
|
|
340
|
+
'playbook.acceptedOutcome': (_args, _params) => undefined,
|
|
341
|
+
startDev: assign(({ context, event }) => {
|
|
342
|
+
if (event.type !== 'START_DEV')
|
|
343
|
+
return {};
|
|
344
|
+
return {
|
|
345
|
+
runResults: context.runResults,
|
|
346
|
+
developmentRequest: event.developmentRequest,
|
|
347
|
+
discussionExchanges: [],
|
|
348
|
+
planningResult: undefined,
|
|
349
|
+
decideCommit: undefined,
|
|
350
|
+
evaluatedRevision: undefined,
|
|
351
|
+
completion: undefined,
|
|
352
|
+
childOutput: undefined,
|
|
353
|
+
childFailure: undefined,
|
|
354
|
+
lastError: undefined,
|
|
355
|
+
pendingBossQuestion: undefined,
|
|
356
|
+
bossReply: undefined,
|
|
357
|
+
};
|
|
358
|
+
}),
|
|
359
|
+
completeDiscussion: assign(({ context }) => ({
|
|
360
|
+
discussionExchanges: archivedExchanges(context),
|
|
361
|
+
completion: 'discussion-complete',
|
|
362
|
+
pendingBossQuestion: undefined,
|
|
363
|
+
bossReply: undefined,
|
|
364
|
+
})),
|
|
365
|
+
rememberCodePath: assign(({ context, event }) => ({
|
|
366
|
+
planningResult: outputString(event, 'planningResult'),
|
|
367
|
+
discussionExchanges: archivedExchanges(context),
|
|
368
|
+
pendingBossQuestion: undefined,
|
|
369
|
+
bossReply: undefined,
|
|
370
|
+
})),
|
|
371
|
+
rememberDecidePath: assign(({ context, event }) => ({
|
|
372
|
+
planningResult: outputString(event, 'planningResult'),
|
|
373
|
+
discussionExchanges: archivedExchanges(context),
|
|
374
|
+
pendingBossQuestion: undefined,
|
|
375
|
+
bossReply: undefined,
|
|
376
|
+
})),
|
|
377
|
+
rememberPendingQuestion: assign(({ context, event }) => {
|
|
378
|
+
const question = outputString(event, 'question');
|
|
379
|
+
if (question === undefined || !outputGuard(event, 'needsBossReply')) {
|
|
380
|
+
return { lastError: new Error('Analyst question output was malformed') };
|
|
381
|
+
}
|
|
382
|
+
return {
|
|
383
|
+
discussionExchanges: archivedExchanges(context),
|
|
384
|
+
pendingBossQuestion: {
|
|
385
|
+
questionId: 'planAnalysis',
|
|
386
|
+
resumeStateId: 'planAnalysis',
|
|
387
|
+
sourceItem: 'DEV-1',
|
|
388
|
+
asker: { kind: 'role', roleId: 'analyst' },
|
|
389
|
+
question,
|
|
390
|
+
},
|
|
391
|
+
bossReply: undefined,
|
|
392
|
+
};
|
|
393
|
+
}),
|
|
394
|
+
rememberBossReply: assign(({ event }) => event.type === 'BOSS_REPLY' ? { bossReply: event.answer } : {}),
|
|
395
|
+
rememberEmptyBossReplyError: assign({
|
|
396
|
+
lastError: () => new Error('BOSS_REPLY received an empty answer'),
|
|
397
|
+
pendingBossQuestion: undefined,
|
|
398
|
+
bossReply: undefined,
|
|
399
|
+
}),
|
|
400
|
+
rememberDecideResult: assign(({ event }) => {
|
|
401
|
+
const output = childOutputOf(event);
|
|
402
|
+
if (!isDecideSuccessValue(output))
|
|
403
|
+
return {};
|
|
404
|
+
return {
|
|
405
|
+
decideCommit: output.decideCommit,
|
|
406
|
+
evaluatedRevision: output.evaluatedRevision,
|
|
407
|
+
};
|
|
408
|
+
}),
|
|
409
|
+
completeWithChildSuccess: assign(({ event }) => {
|
|
410
|
+
const output = childOutputOf(event);
|
|
411
|
+
return {
|
|
412
|
+
completion: 'complete',
|
|
413
|
+
...(output === undefined ? {} : { childOutput: output }),
|
|
414
|
+
};
|
|
415
|
+
}),
|
|
416
|
+
completeWithInsufficientCodeResult: assign(({ event }) => ({
|
|
417
|
+
completion: 'child-failed',
|
|
418
|
+
childFailure: insufficientChildResult(event, 'code'),
|
|
419
|
+
})),
|
|
420
|
+
completeWithInsufficientDecideResult: assign(({ event }) => ({
|
|
421
|
+
completion: 'child-failed',
|
|
422
|
+
childFailure: insufficientChildResult(event, 'decide'),
|
|
423
|
+
})),
|
|
424
|
+
completeWithCodeFailure: assign(({ event }) => ({
|
|
425
|
+
completion: 'child-failed',
|
|
426
|
+
childFailure: relayedChildFailure(event, 'code'),
|
|
427
|
+
})),
|
|
428
|
+
completeWithDecideFailure: assign(({ event }) => ({
|
|
429
|
+
completion: 'child-failed',
|
|
430
|
+
childFailure: relayedChildFailure(event, 'decide'),
|
|
431
|
+
})),
|
|
432
|
+
rememberActorError: assign(({ event }) => ({
|
|
433
|
+
lastError: event.error,
|
|
434
|
+
})),
|
|
435
|
+
rememberMalformedPlayerOutput: assign({
|
|
436
|
+
lastError: () => new Error('Analyst result for DEV-1 did not match an available outcome.'),
|
|
437
|
+
}),
|
|
438
|
+
},
|
|
439
|
+
});
|
|
440
|
+
export const devMachine = machineSetup.createMachine({
|
|
441
|
+
id: 'dev',
|
|
442
|
+
initial: 'ready',
|
|
443
|
+
context: ({ input }) => ({
|
|
444
|
+
runResults: typeof input.runResults === 'string' ? input.runResults : '',
|
|
445
|
+
discussionExchanges: [],
|
|
446
|
+
}),
|
|
447
|
+
output: ({ context }) => {
|
|
448
|
+
if (context.completion === 'discussion-complete') {
|
|
449
|
+
return { status: 'discussion-complete' };
|
|
450
|
+
}
|
|
451
|
+
if (context.completion === 'complete') {
|
|
452
|
+
return {
|
|
453
|
+
status: 'complete',
|
|
454
|
+
childPlaybookId: 'code',
|
|
455
|
+
...(context.childOutput === undefined
|
|
456
|
+
? {}
|
|
457
|
+
: { childOutput: context.childOutput }),
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
if (context.completion === 'child-failed') {
|
|
461
|
+
if (context.childFailure === undefined) {
|
|
462
|
+
throw new Error('DEV reached a final state without a child result');
|
|
463
|
+
}
|
|
464
|
+
return { status: 'child-failed', childResult: context.childFailure };
|
|
465
|
+
}
|
|
466
|
+
throw new Error('DEV reached a final state without a recorded completion');
|
|
467
|
+
},
|
|
468
|
+
states: {
|
|
469
|
+
ready: {
|
|
470
|
+
id: 'ready',
|
|
471
|
+
description: STATE_DESCRIPTIONS.ready,
|
|
472
|
+
meta: playbookMeta('ready'),
|
|
473
|
+
tags: ['playbook.parked'],
|
|
474
|
+
on: {
|
|
475
|
+
START_DEV: { target: 'planAnalysis', actions: 'startDev' },
|
|
476
|
+
},
|
|
477
|
+
},
|
|
478
|
+
planAnalysis: {
|
|
479
|
+
id: 'planAnalysis',
|
|
480
|
+
description: STATE_DESCRIPTIONS.planAnalysis,
|
|
481
|
+
meta: playbookMeta('planAnalysis', 'analyst'),
|
|
482
|
+
tags: ['playbook.busy'],
|
|
483
|
+
invoke: {
|
|
484
|
+
src: 'player',
|
|
485
|
+
input: ({ context }) => ({
|
|
486
|
+
stateId: 'planAnalysis',
|
|
487
|
+
role: 'analyst',
|
|
488
|
+
sourceItem: 'DEV-1',
|
|
489
|
+
prompt: PLAN_ANALYSIS_PROMPT,
|
|
490
|
+
result: PLAN_ANALYSIS_RESULTS,
|
|
491
|
+
developmentRequest: context.developmentRequest ?? '',
|
|
492
|
+
discussionContext: renderDiscussionContext(context.discussionExchanges),
|
|
493
|
+
runResults: context.runResults,
|
|
494
|
+
...(context.pendingBossQuestion === undefined
|
|
495
|
+
? {}
|
|
496
|
+
: { pendingBossQuestion: context.pendingBossQuestion }),
|
|
497
|
+
...(context.bossReply === undefined
|
|
498
|
+
? {}
|
|
499
|
+
: { bossReply: context.bossReply }),
|
|
500
|
+
}),
|
|
501
|
+
onDone: [
|
|
502
|
+
{
|
|
503
|
+
guard: 'isDiscussionComplete',
|
|
504
|
+
target: 'discussionComplete',
|
|
505
|
+
actions: [
|
|
506
|
+
{
|
|
507
|
+
type: 'playbook.acceptedOutcome',
|
|
508
|
+
params: {
|
|
509
|
+
source: 'planAnalysis',
|
|
510
|
+
target: 'discussionComplete',
|
|
511
|
+
acceptedOutcome: 'discussionComplete',
|
|
512
|
+
},
|
|
513
|
+
},
|
|
514
|
+
'completeDiscussion',
|
|
515
|
+
],
|
|
516
|
+
},
|
|
517
|
+
{
|
|
518
|
+
guard: 'isCodePath',
|
|
519
|
+
target: 'callCode',
|
|
520
|
+
actions: [
|
|
521
|
+
{
|
|
522
|
+
type: 'playbook.acceptedOutcome',
|
|
523
|
+
params: {
|
|
524
|
+
source: 'planAnalysis',
|
|
525
|
+
target: 'callCode',
|
|
526
|
+
acceptedOutcome: 'code',
|
|
527
|
+
},
|
|
528
|
+
},
|
|
529
|
+
'rememberCodePath',
|
|
530
|
+
],
|
|
531
|
+
},
|
|
532
|
+
{
|
|
533
|
+
guard: 'isDecideThenCode',
|
|
534
|
+
target: 'callDecide',
|
|
535
|
+
actions: [
|
|
536
|
+
{
|
|
537
|
+
type: 'playbook.acceptedOutcome',
|
|
538
|
+
params: {
|
|
539
|
+
source: 'planAnalysis',
|
|
540
|
+
target: 'callDecide',
|
|
541
|
+
acceptedOutcome: 'decideThenCode',
|
|
542
|
+
},
|
|
543
|
+
},
|
|
544
|
+
'rememberDecidePath',
|
|
545
|
+
],
|
|
546
|
+
},
|
|
547
|
+
{
|
|
548
|
+
guard: 'needsBossReply',
|
|
549
|
+
target: 'awaitBossReply',
|
|
550
|
+
actions: [
|
|
551
|
+
{
|
|
552
|
+
type: 'playbook.acceptedOutcome',
|
|
553
|
+
params: {
|
|
554
|
+
source: 'planAnalysis',
|
|
555
|
+
target: 'awaitBossReply',
|
|
556
|
+
acceptedOutcome: 'needsBossReply',
|
|
557
|
+
},
|
|
558
|
+
},
|
|
559
|
+
'rememberPendingQuestion',
|
|
560
|
+
],
|
|
561
|
+
},
|
|
562
|
+
{
|
|
563
|
+
target: 'failed',
|
|
564
|
+
actions: 'rememberMalformedPlayerOutput',
|
|
565
|
+
},
|
|
566
|
+
],
|
|
567
|
+
onError: { target: 'failed', actions: 'rememberActorError' },
|
|
568
|
+
},
|
|
569
|
+
},
|
|
570
|
+
callCode: {
|
|
571
|
+
id: 'callCode',
|
|
572
|
+
description: STATE_DESCRIPTIONS.callCode,
|
|
573
|
+
meta: playbookMeta('callCode'),
|
|
574
|
+
tags: ['playbook.suspended'],
|
|
575
|
+
invoke: {
|
|
576
|
+
src: 'playbook',
|
|
577
|
+
input: ({ context }) => ({
|
|
578
|
+
stateId: 'callCode',
|
|
579
|
+
sourceItem: 'DEV-2',
|
|
580
|
+
playbookId: 'code',
|
|
581
|
+
text: codeCallText(context),
|
|
582
|
+
}),
|
|
583
|
+
onDone: [
|
|
584
|
+
{
|
|
585
|
+
guard: 'isCodeSuccess',
|
|
586
|
+
target: 'done',
|
|
587
|
+
actions: 'completeWithChildSuccess',
|
|
588
|
+
},
|
|
589
|
+
{
|
|
590
|
+
target: 'reportedChildFailure',
|
|
591
|
+
actions: 'completeWithInsufficientCodeResult',
|
|
592
|
+
},
|
|
593
|
+
],
|
|
594
|
+
onError: [
|
|
595
|
+
{
|
|
596
|
+
guard: 'authoredCodeFailure',
|
|
597
|
+
target: 'reportedChildFailure',
|
|
598
|
+
actions: 'completeWithCodeFailure',
|
|
599
|
+
},
|
|
600
|
+
{ target: 'failed', actions: 'rememberActorError' },
|
|
601
|
+
],
|
|
602
|
+
},
|
|
603
|
+
},
|
|
604
|
+
callDecide: {
|
|
605
|
+
id: 'callDecide',
|
|
606
|
+
description: STATE_DESCRIPTIONS.callDecide,
|
|
607
|
+
meta: playbookMeta('callDecide'),
|
|
608
|
+
tags: ['playbook.suspended'],
|
|
609
|
+
invoke: {
|
|
610
|
+
src: 'playbook',
|
|
611
|
+
input: ({ context }) => ({
|
|
612
|
+
stateId: 'callDecide',
|
|
613
|
+
sourceItem: 'DEV-3',
|
|
614
|
+
playbookId: 'decide',
|
|
615
|
+
text: decideCallText(context),
|
|
616
|
+
}),
|
|
617
|
+
onDone: [
|
|
618
|
+
{
|
|
619
|
+
guard: 'isDecideSuccess',
|
|
620
|
+
target: 'callCodeAfterDecide',
|
|
621
|
+
actions: 'rememberDecideResult',
|
|
622
|
+
},
|
|
623
|
+
{
|
|
624
|
+
target: 'reportedChildFailure',
|
|
625
|
+
actions: 'completeWithInsufficientDecideResult',
|
|
626
|
+
},
|
|
627
|
+
],
|
|
628
|
+
onError: [
|
|
629
|
+
{
|
|
630
|
+
guard: 'authoredDecideFailure',
|
|
631
|
+
target: 'reportedChildFailure',
|
|
632
|
+
actions: 'completeWithDecideFailure',
|
|
633
|
+
},
|
|
634
|
+
{ target: 'failed', actions: 'rememberActorError' },
|
|
635
|
+
],
|
|
636
|
+
},
|
|
637
|
+
},
|
|
638
|
+
callCodeAfterDecide: {
|
|
639
|
+
id: 'callCodeAfterDecide',
|
|
640
|
+
description: STATE_DESCRIPTIONS.callCodeAfterDecide,
|
|
641
|
+
meta: playbookMeta('callCodeAfterDecide'),
|
|
642
|
+
tags: ['playbook.suspended'],
|
|
643
|
+
invoke: {
|
|
644
|
+
src: 'playbook',
|
|
645
|
+
input: ({ context }) => ({
|
|
646
|
+
stateId: 'callCodeAfterDecide',
|
|
647
|
+
sourceItem: 'DEV-4',
|
|
648
|
+
playbookId: 'code',
|
|
649
|
+
text: codeAfterDecideCallText(context),
|
|
650
|
+
}),
|
|
651
|
+
onDone: [
|
|
652
|
+
{
|
|
653
|
+
guard: 'isCodeSuccess',
|
|
654
|
+
target: 'done',
|
|
655
|
+
actions: 'completeWithChildSuccess',
|
|
656
|
+
},
|
|
657
|
+
{
|
|
658
|
+
target: 'reportedChildFailure',
|
|
659
|
+
actions: 'completeWithInsufficientCodeResult',
|
|
660
|
+
},
|
|
661
|
+
],
|
|
662
|
+
onError: [
|
|
663
|
+
{
|
|
664
|
+
guard: 'authoredCodeFailure',
|
|
665
|
+
target: 'reportedChildFailure',
|
|
666
|
+
actions: 'completeWithCodeFailure',
|
|
667
|
+
},
|
|
668
|
+
{ target: 'failed', actions: 'rememberActorError' },
|
|
669
|
+
],
|
|
670
|
+
},
|
|
671
|
+
},
|
|
672
|
+
awaitBossReply: {
|
|
673
|
+
id: 'awaitBossReply',
|
|
674
|
+
description: STATE_DESCRIPTIONS.awaitBossReply,
|
|
675
|
+
meta: playbookMeta('awaitBossReply'),
|
|
676
|
+
tags: ['playbook.parked'],
|
|
677
|
+
on: {
|
|
678
|
+
BOSS_REPLY: [
|
|
679
|
+
{
|
|
680
|
+
guard: 'emptyBossReply',
|
|
681
|
+
target: '#failed',
|
|
682
|
+
actions: 'rememberEmptyBossReplyError',
|
|
683
|
+
},
|
|
684
|
+
{
|
|
685
|
+
guard: 'resumesPlanAnalysis',
|
|
686
|
+
target: '#planAnalysis',
|
|
687
|
+
reenter: true,
|
|
688
|
+
actions: 'rememberBossReply',
|
|
689
|
+
},
|
|
690
|
+
],
|
|
691
|
+
START_DEV: { target: 'planAnalysis', actions: 'startDev' },
|
|
692
|
+
},
|
|
693
|
+
},
|
|
694
|
+
failed: {
|
|
695
|
+
id: 'failed',
|
|
696
|
+
description: STATE_DESCRIPTIONS.failed,
|
|
697
|
+
meta: playbookMeta('failed'),
|
|
698
|
+
tags: ['playbook.parked'],
|
|
699
|
+
on: {
|
|
700
|
+
START_DEV: { target: 'planAnalysis', actions: 'startDev' },
|
|
701
|
+
},
|
|
702
|
+
},
|
|
703
|
+
discussionComplete: {
|
|
704
|
+
id: 'discussionComplete',
|
|
705
|
+
description: STATE_DESCRIPTIONS.discussionComplete,
|
|
706
|
+
meta: playbookMeta('discussionComplete'),
|
|
707
|
+
type: 'final',
|
|
708
|
+
},
|
|
709
|
+
done: {
|
|
710
|
+
id: 'done',
|
|
711
|
+
description: STATE_DESCRIPTIONS.done,
|
|
712
|
+
meta: playbookMeta('done'),
|
|
713
|
+
type: 'final',
|
|
714
|
+
},
|
|
715
|
+
reportedChildFailure: {
|
|
716
|
+
id: 'reportedChildFailure',
|
|
717
|
+
description: STATE_DESCRIPTIONS.reportedChildFailure,
|
|
718
|
+
meta: playbookMeta('reportedChildFailure'),
|
|
719
|
+
type: 'final',
|
|
720
|
+
},
|
|
721
|
+
},
|
|
722
|
+
});
|
|
723
|
+
export default devMachine;
|