@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.
Files changed (43) hide show
  1. package/docs/cli.md +9 -6
  2. package/docs/configuration.md +15 -1
  3. package/package.json +22 -3
  4. package/reference/sdlc/code.playbook/code.fsm.d.ts +22 -19
  5. package/reference/sdlc/code.playbook/code.fsm.js +116 -52
  6. package/reference/sdlc/code.playbook/code.fsm.ts +149 -64
  7. package/reference/sdlc/code.playbook/code.gears.md +40 -20
  8. package/reference/sdlc/code.playbook/code.playbook.js +23 -2
  9. package/reference/sdlc/code.playbook/code.playbook.ts +23 -2
  10. package/reference/sdlc/code.playbook/playbook.config.template.yaml +18 -2
  11. package/reference/sdlc/decide.playbook/decide.fsm.d.ts +13 -6
  12. package/reference/sdlc/decide.playbook/decide.fsm.js +54 -27
  13. package/reference/sdlc/decide.playbook/decide.fsm.ts +68 -29
  14. package/reference/sdlc/decide.playbook/decide.gears.md +25 -19
  15. package/reference/sdlc/decide.playbook/decide.playbook.js +11 -3
  16. package/reference/sdlc/decide.playbook/decide.playbook.ts +11 -3
  17. package/reference/sdlc/decide.playbook/decide.registry.js +1 -1
  18. package/reference/sdlc/decide.playbook/decide.registry.ts +1 -1
  19. package/reference/sdlc/dev.md +52 -0
  20. package/reference/sdlc/dev.playbook/dev.fsm.d.ts +261 -0
  21. package/reference/sdlc/dev.playbook/dev.fsm.js +723 -0
  22. package/reference/sdlc/dev.playbook/dev.fsm.ts +988 -0
  23. package/reference/sdlc/dev.playbook/dev.gears.md +91 -0
  24. package/reference/sdlc/dev.playbook/dev.playbook.d.ts +21 -0
  25. package/reference/sdlc/dev.playbook/dev.playbook.js +143 -0
  26. package/reference/sdlc/dev.playbook/dev.playbook.ts +246 -0
  27. package/reference/sdlc/dev.playbook/dev.registry.d.ts +40 -0
  28. package/reference/sdlc/dev.playbook/dev.registry.js +64 -0
  29. package/reference/sdlc/dev.playbook/dev.registry.ts +120 -0
  30. package/reference/sdlc/review.playbook/review.fsm.d.ts +15 -2
  31. package/reference/sdlc/review.playbook/review.fsm.js +77 -27
  32. package/reference/sdlc/review.playbook/review.fsm.ts +96 -30
  33. package/reference/sdlc/review.playbook/review.gears.md +52 -26
  34. package/reference/sdlc/review.playbook/review.playbook.js +17 -7
  35. package/reference/sdlc/review.playbook/review.playbook.ts +17 -7
  36. package/reference/sdlc/review.playbook/review.registry.js +1 -1
  37. package/reference/sdlc/review.playbook/review.registry.ts +1 -1
  38. package/slc/link.md +12 -5
  39. package/slc/text2gears.md +3 -0
  40. package/src/xstate-playbook-runtime.js +5 -2
  41. package/src/xstate-playbook-runtime.ts +5 -2
  42. package/src/xstate-runtime.js +13 -1
  43. package/src/xstate-runtime.ts +13 -1
@@ -0,0 +1,988 @@
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
+
6
+ import { assign, fromPromise, setup } from 'xstate';
7
+
8
+ export type JsonValue =
9
+ | null
10
+ | boolean
11
+ | number
12
+ | string
13
+ | readonly JsonValue[]
14
+ | { readonly [key: string]: JsonValue };
15
+
16
+ export type DevStateId =
17
+ | 'planAnalysis'
18
+ | 'callCode'
19
+ | 'callDecide'
20
+ | 'callCodeAfterDecide';
21
+
22
+ export type DevSourceItem = 'DEV-1' | 'DEV-2' | 'DEV-3' | 'DEV-4';
23
+
24
+ export type DevChildPlaybookId = 'code' | 'decide';
25
+
26
+ export type PendingBossQuestion = {
27
+ readonly questionId: 'planAnalysis';
28
+ readonly resumeStateId: 'planAnalysis';
29
+ readonly sourceItem: 'DEV-1';
30
+ readonly asker: { readonly kind: 'role'; readonly roleId: 'analyst' };
31
+ readonly question: string;
32
+ };
33
+
34
+ export type DiscussionExchange = {
35
+ readonly question: string;
36
+ readonly answer: string;
37
+ };
38
+
39
+ export type PlayerInput = {
40
+ readonly stateId: 'planAnalysis';
41
+ readonly role: 'analyst';
42
+ readonly sourceItem: 'DEV-1';
43
+ readonly prompt: string;
44
+ readonly result: Readonly<Record<string, string>>;
45
+ readonly developmentRequest: string;
46
+ readonly discussionContext: string;
47
+ readonly runResults: string;
48
+ readonly pendingBossQuestion?: PendingBossQuestion;
49
+ readonly bossReply?: string;
50
+ };
51
+
52
+ export type PlayerOutput =
53
+ | { readonly guard: 'discussionComplete' }
54
+ | { readonly guard: 'code'; readonly planningResult: string }
55
+ | { readonly guard: 'decideThenCode'; readonly planningResult: string }
56
+ | { readonly guard: 'needsBossReply'; readonly question: string };
57
+
58
+ export type PlaybookInput = {
59
+ readonly stateId: 'callCode' | 'callDecide' | 'callCodeAfterDecide';
60
+ readonly sourceItem: 'DEV-2' | 'DEV-3' | 'DEV-4';
61
+ readonly playbookId: DevChildPlaybookId;
62
+ readonly text: string;
63
+ };
64
+
65
+ /**
66
+ * The affirmative success proof DEV requires from `decide`'s canonical
67
+ * structured terminal output before starting the dependent `code` call:
68
+ * the `decide`-owned commit, the exact evaluated repository revision, and
69
+ * the affirmative no-unsettled-findings fact. Additional members belong to
70
+ * DECIDE's own contract and do not disprove success.
71
+ */
72
+ export type DecideSuccessOutput = {
73
+ readonly decideCommit: string;
74
+ readonly evaluatedRevision: string;
75
+ readonly noUnsettledFindings: true;
76
+ };
77
+
78
+ export type CompactError = {
79
+ readonly name: string;
80
+ readonly message: string;
81
+ };
82
+
83
+ /** Sanitized canonical child result relayed as DEV's own failure outcome. */
84
+ export type CompletedChildResult =
85
+ | {
86
+ readonly playbookId: DevChildPlaybookId;
87
+ readonly status: 'ok';
88
+ readonly output?: JsonValue;
89
+ }
90
+ | {
91
+ readonly playbookId: DevChildPlaybookId;
92
+ readonly status: 'aborted' | 'error';
93
+ readonly error: CompactError;
94
+ };
95
+
96
+ export type DevPlaybookOutput =
97
+ | {
98
+ readonly status: 'discussion-complete';
99
+ }
100
+ | {
101
+ readonly status: 'complete';
102
+ readonly childPlaybookId: 'code';
103
+ /** The successful result of DEV's final child call, when it has one. */
104
+ readonly childOutput?: JsonValue;
105
+ }
106
+ | {
107
+ readonly status: 'child-failed';
108
+ /** The relayed canonical child result that ended the selected path. */
109
+ readonly childResult: CompletedChildResult;
110
+ };
111
+
112
+ export type DevInput = {
113
+ readonly runResults?: string;
114
+ };
115
+
116
+ export type DevContext = {
117
+ readonly runResults: string;
118
+ readonly developmentRequest?: string;
119
+ readonly discussionExchanges: readonly DiscussionExchange[];
120
+ readonly planningResult?: string;
121
+ readonly decideCommit?: string;
122
+ readonly evaluatedRevision?: string;
123
+ readonly completion?: 'discussion-complete' | 'complete' | 'child-failed';
124
+ readonly childOutput?: JsonValue;
125
+ readonly childFailure?: CompletedChildResult;
126
+ readonly lastError?: unknown;
127
+ readonly pendingBossQuestion?: PendingBossQuestion;
128
+ readonly bossReply?: string;
129
+ };
130
+
131
+ export type DevEvent =
132
+ | { readonly type: 'START_DEV'; readonly developmentRequest: string }
133
+ | {
134
+ readonly type: 'BOSS_REPLY';
135
+ readonly answer: string;
136
+ readonly questionId?: 'planAnalysis';
137
+ };
138
+
139
+ const PLAN_ANALYSIS_PROMPT = [
140
+ '> <development-request>',
141
+ '> <discussion-context>',
142
+ '> <run-results>',
143
+ '',
144
+ 'Inspect the request and the relevant repository and specs only as needed to determine the smallest sound next step.',
145
+ 'Do not change files or commit while planning or discussing the request.',
146
+ '',
147
+ '- 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.',
148
+ '- If the discussion has concluded after a Boss reply and no repository work should follow, choose `discussion complete`.',
149
+ '- If implementation can proceed under the existing decisions, choose `code`.',
150
+ '- If implementation first requires a new or amended durable decision that the existing specs do not settle, choose `decide then code`.',
151
+ '',
152
+ 'A question or exploratory discussion is not by itself authorization to create a durable decision or implement changes.',
153
+ 'Do not choose `decide then code` merely because the work is large.',
154
+ 'Consult @specs/map.md for relevant context and @specs/meta.md for spec requirements, if needed.',
155
+ ].join('\n');
156
+
157
+ const PLAN_ANALYSIS_RESULTS = {
158
+ discussionComplete:
159
+ 'Analyst concluded the discussion after a Boss reply, with no repository work to follow.',
160
+ code:
161
+ 'Analyst determined implementation can proceed under the existing decisions. Output shall include `planningResult: <verbatim final text>`.',
162
+ decideThenCode:
163
+ '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>`.',
164
+ needsBossReply:
165
+ "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>`.",
166
+ } as const;
167
+
168
+ const STATE_DESCRIPTIONS = {
169
+ ready: 'Waiting for a development request.',
170
+ planAnalysis:
171
+ 'Analyst is analyzing the development request to choose the smallest sound next step.',
172
+ callCode: 'The CODE playbook is implementing the planned development path.',
173
+ callDecide:
174
+ 'The DECIDE playbook is settling the durable decision the planned path requires.',
175
+ callCodeAfterDecide:
176
+ 'The CODE playbook is implementing the decided development path.',
177
+ awaitBossReply: 'Waiting for Boss to answer Analyst.',
178
+ failed:
179
+ 'The development planning workflow failed and is waiting for a new development request.',
180
+ discussionComplete:
181
+ 'The planning discussion concluded after a Boss reply with no repository work to follow.',
182
+ done: "The selected development path completed with the final child playbook's successful result.",
183
+ reportedChildFailure:
184
+ "The development workflow relayed a child playbook's authored abort, failure, or insufficient terminal result.",
185
+ } as const;
186
+
187
+ function playbookMeta<StateId extends keyof typeof STATE_DESCRIPTIONS>(
188
+ stateId: StateId,
189
+ role?: 'analyst',
190
+ ) {
191
+ return {
192
+ playbook: {
193
+ stateId,
194
+ description: STATE_DESCRIPTIONS[stateId],
195
+ ...(role === undefined ? {} : { role }),
196
+ },
197
+ };
198
+ }
199
+
200
+ function isRecord(value: unknown): value is Record<string, unknown> {
201
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
202
+ return false;
203
+ }
204
+ const prototype = Object.getPrototypeOf(value);
205
+ return prototype === Object.prototype || prototype === null;
206
+ }
207
+
208
+ function isNonEmptyString(value: unknown): value is string {
209
+ return typeof value === 'string' && value.trim().length > 0;
210
+ }
211
+
212
+ function playerOutput(event: unknown): Record<string, unknown> | undefined {
213
+ if (!isRecord(event)) return undefined;
214
+ return isRecord(event.output) ? event.output : undefined;
215
+ }
216
+
217
+ function outputGuard(event: unknown, guard: PlayerOutput['guard']): boolean {
218
+ return playerOutput(event)?.guard === guard;
219
+ }
220
+
221
+ function outputString(event: unknown, field: string): string | undefined {
222
+ const value = playerOutput(event)?.[field];
223
+ return isNonEmptyString(value) ? value : undefined;
224
+ }
225
+
226
+ function isCodePath({ event }: { event: unknown }): boolean {
227
+ return (
228
+ outputGuard(event, 'code') &&
229
+ outputString(event, 'planningResult') !== undefined
230
+ );
231
+ }
232
+
233
+ function isDecideThenCode({ event }: { event: unknown }): boolean {
234
+ return (
235
+ outputGuard(event, 'decideThenCode') &&
236
+ outputString(event, 'planningResult') !== undefined
237
+ );
238
+ }
239
+
240
+ // Discussion complete is available only after a Boss reply: the round that
241
+ // produced it must itself have been resumed with Boss's answer.
242
+ function isDiscussionComplete({
243
+ context,
244
+ event,
245
+ }: {
246
+ context: DevContext;
247
+ event: unknown;
248
+ }): boolean {
249
+ return (
250
+ outputGuard(event, 'discussionComplete') &&
251
+ isNonEmptyString(context.bossReply)
252
+ );
253
+ }
254
+
255
+ function needsBossReply({ event }: { event: unknown }): boolean {
256
+ return (
257
+ outputGuard(event, 'needsBossReply') &&
258
+ outputString(event, 'question') !== undefined
259
+ );
260
+ }
261
+
262
+ function childOutputOf(event: unknown): unknown {
263
+ return isRecord(event) ? event.output : undefined;
264
+ }
265
+
266
+ function isDecideSuccessValue(value: unknown): value is DecideSuccessOutput {
267
+ return (
268
+ isRecord(value) &&
269
+ isNonEmptyString(value.decideCommit) &&
270
+ isNonEmptyString(value.evaluatedRevision) &&
271
+ value.noUnsettledFindings === true
272
+ );
273
+ }
274
+
275
+ function isDecideSuccess({ event }: { event: unknown }): boolean {
276
+ return isDecideSuccessValue(childOutputOf(event));
277
+ }
278
+
279
+ // CODE's canonical successful terminal output discriminates completion with
280
+ // `status: 'complete'`, carries the exact last CODE-owned commit, and
281
+ // affirms that every phase's review passed.
282
+ function isCodeSuccessValue(value: unknown): boolean {
283
+ return (
284
+ isRecord(value) &&
285
+ value.status === 'complete' &&
286
+ isNonEmptyString(value.lastCodeCommit) &&
287
+ value.allReviewsPassed === true
288
+ );
289
+ }
290
+
291
+ function isCodeSuccess({ event }: { event: unknown }): boolean {
292
+ return isCodeSuccessValue(childOutputOf(event));
293
+ }
294
+
295
+ function isStateValue(value: unknown, ancestors = new Set<object>()): boolean {
296
+ if (typeof value === 'string') return true;
297
+ if (!isRecord(value) || ancestors.has(value)) return false;
298
+ const next = new Set(ancestors).add(value);
299
+ return Reflect.ownKeys(value).every(
300
+ (key) =>
301
+ typeof key === 'string' &&
302
+ Object.prototype.propertyIsEnumerable.call(value, key) &&
303
+ isStateValue(value[key], next),
304
+ );
305
+ }
306
+
307
+ function isStringArray(value: unknown): value is readonly string[] {
308
+ return (
309
+ Array.isArray(value) &&
310
+ Reflect.ownKeys(value).length === value.length + 1 &&
311
+ Reflect.ownKeys(value).every((key) => {
312
+ if (key === 'length') return true;
313
+ return (
314
+ typeof key === 'string' &&
315
+ /^(0|[1-9][0-9]*)$/.test(key) &&
316
+ Number(key) < value.length
317
+ );
318
+ }) &&
319
+ value.every((entry) => isNonEmptyString(entry)) &&
320
+ new Set(value).size === value.length
321
+ );
322
+ }
323
+
324
+ function isPlaybookState(value: unknown): boolean {
325
+ if (!isRecord(value)) return false;
326
+ const allowed = new Set([
327
+ 'value',
328
+ 'activeStateIds',
329
+ 'tags',
330
+ 'status',
331
+ 'quiescent',
332
+ 'stateId',
333
+ ]);
334
+ if (
335
+ Reflect.ownKeys(value).some(
336
+ (key) => typeof key !== 'string' || !allowed.has(key),
337
+ ) ||
338
+ !Object.prototype.hasOwnProperty.call(value, 'value') ||
339
+ !Object.prototype.hasOwnProperty.call(value, 'activeStateIds') ||
340
+ !Object.prototype.hasOwnProperty.call(value, 'tags') ||
341
+ !Object.prototype.hasOwnProperty.call(value, 'status') ||
342
+ !Object.prototype.hasOwnProperty.call(value, 'quiescent')
343
+ ) {
344
+ return false;
345
+ }
346
+ return (
347
+ isStateValue(value.value) &&
348
+ isStringArray(value.activeStateIds) &&
349
+ isStringArray(value.tags) &&
350
+ (value.status === 'active' ||
351
+ value.status === 'done' ||
352
+ value.status === 'error' ||
353
+ value.status === 'stopped') &&
354
+ typeof value.quiescent === 'boolean' &&
355
+ (!Object.prototype.hasOwnProperty.call(value, 'stateId') ||
356
+ (isNonEmptyString(value.stateId) &&
357
+ value.activeStateIds.length === 1 &&
358
+ value.activeStateIds[0] === value.stateId))
359
+ );
360
+ }
361
+
362
+ function nestedResultFromError(
363
+ error: unknown,
364
+ ): Record<string, unknown> | undefined {
365
+ if (!(error instanceof Error)) return undefined;
366
+ const result = (error as Error & { readonly result?: unknown }).result;
367
+ return isRecord(result) ? result : undefined;
368
+ }
369
+
370
+ type AuthoredChildFailure = {
371
+ readonly status: 'aborted' | 'error';
372
+ readonly error?: CompactError;
373
+ };
374
+
375
+ function normalizedChildFailure(
376
+ error: unknown,
377
+ playbookId: DevChildPlaybookId,
378
+ ): AuthoredChildFailure | undefined {
379
+ const result = nestedResultFromError(error);
380
+ if (result === undefined) return undefined;
381
+ const allowed = new Set([
382
+ 'status',
383
+ 'playbookId',
384
+ 'childSessionId',
385
+ 'state',
386
+ 'error',
387
+ ]);
388
+ if (
389
+ Reflect.ownKeys(result).some(
390
+ (key) => typeof key !== 'string' || !allowed.has(key),
391
+ ) ||
392
+ (result.status !== 'aborted' && result.status !== 'error') ||
393
+ result.playbookId !== playbookId
394
+ ) {
395
+ return undefined;
396
+ }
397
+ if (
398
+ Object.prototype.hasOwnProperty.call(result, 'childSessionId') &&
399
+ !isNonEmptyString(result.childSessionId)
400
+ ) {
401
+ return undefined;
402
+ }
403
+ if (
404
+ Object.prototype.hasOwnProperty.call(result, 'state') &&
405
+ !isPlaybookState(result.state)
406
+ ) {
407
+ return undefined;
408
+ }
409
+ let normalizedError: CompactError | undefined;
410
+ if (Object.prototype.hasOwnProperty.call(result, 'error')) {
411
+ if (!isRecord(result.error)) return undefined;
412
+ const errorKeys = Reflect.ownKeys(result.error);
413
+ if (
414
+ errorKeys.some(
415
+ (key) =>
416
+ typeof key !== 'string' ||
417
+ (key !== 'name' && key !== 'message' && key !== 'stack'),
418
+ ) ||
419
+ !Object.prototype.hasOwnProperty.call(result.error, 'name') ||
420
+ !Object.prototype.hasOwnProperty.call(result.error, 'message') ||
421
+ !isNonEmptyString(result.error.name) ||
422
+ typeof result.error.message !== 'string' ||
423
+ (Object.prototype.hasOwnProperty.call(result.error, 'stack') &&
424
+ typeof result.error.stack !== 'string')
425
+ ) {
426
+ return undefined;
427
+ }
428
+ normalizedError = {
429
+ name: result.error.name,
430
+ message: result.error.message,
431
+ };
432
+ } else if (result.status === 'error') {
433
+ return undefined;
434
+ }
435
+ return {
436
+ status: result.status,
437
+ ...(normalizedError === undefined ? {} : { error: normalizedError }),
438
+ };
439
+ }
440
+
441
+ function authoredChildFailureGuard(
442
+ playbookId: DevChildPlaybookId,
443
+ ): (args: { event: unknown }) => boolean {
444
+ return ({ event }) => {
445
+ const error = isRecord(event) ? event.error : undefined;
446
+ return normalizedChildFailure(error, playbookId) !== undefined;
447
+ };
448
+ }
449
+
450
+ function authoredChildError(
451
+ event: unknown,
452
+ playbookId: DevChildPlaybookId,
453
+ ): CompactError {
454
+ const outer = isRecord(event) ? event.error : undefined;
455
+ const failure = normalizedChildFailure(outer, playbookId);
456
+ if (failure?.error !== undefined) return failure.error;
457
+ if (failure?.status === 'aborted') {
458
+ return {
459
+ name: 'AbortError',
460
+ message: `${playbookId.toUpperCase()} was aborted.`,
461
+ };
462
+ }
463
+ if (outer instanceof Error) {
464
+ return { name: outer.name || 'Error', message: outer.message };
465
+ }
466
+ return { name: 'Error', message: String(outer) };
467
+ }
468
+
469
+ function relayedChildFailure(
470
+ event: unknown,
471
+ playbookId: DevChildPlaybookId,
472
+ ): CompletedChildResult {
473
+ const outer = isRecord(event) ? event.error : undefined;
474
+ const failure = normalizedChildFailure(outer, playbookId);
475
+ return {
476
+ playbookId,
477
+ status: failure?.status ?? 'error',
478
+ error: authoredChildError(event, playbookId),
479
+ };
480
+ }
481
+
482
+ function insufficientChildResult(
483
+ event: unknown,
484
+ playbookId: DevChildPlaybookId,
485
+ ): CompletedChildResult {
486
+ const output = childOutputOf(event);
487
+ return {
488
+ playbookId,
489
+ status: 'ok',
490
+ ...(output === undefined ? {} : { output: output as JsonValue }),
491
+ };
492
+ }
493
+
494
+ /** Consumed planning Q&A rendered for later relayed discussion context. */
495
+ export function renderDiscussionContext(
496
+ exchanges: readonly DiscussionExchange[],
497
+ ): string {
498
+ return exchanges
499
+ .map(
500
+ (exchange) =>
501
+ `Analyst question: ${exchange.question}\nBoss reply: ${exchange.answer}`,
502
+ )
503
+ .join('\n\n');
504
+ }
505
+
506
+ function archivedExchanges(
507
+ context: DevContext,
508
+ ): readonly DiscussionExchange[] {
509
+ if (
510
+ context.pendingBossQuestion === undefined ||
511
+ !isNonEmptyString(context.bossReply)
512
+ ) {
513
+ return context.discussionExchanges;
514
+ }
515
+ return [
516
+ ...context.discussionExchanges,
517
+ {
518
+ question: context.pendingBossQuestion.question,
519
+ answer: context.bossReply,
520
+ },
521
+ ];
522
+ }
523
+
524
+ function quotedRelay(values: readonly string[]): string {
525
+ return values
526
+ .filter((value) => value.length > 0)
527
+ .map((value) =>
528
+ value
529
+ .split('\n')
530
+ .map((line) => `> ${line}`)
531
+ .join('\n'),
532
+ )
533
+ .join('\n');
534
+ }
535
+
536
+ function planningRelayValues(context: DevContext): readonly string[] {
537
+ return [
538
+ context.developmentRequest ?? '',
539
+ renderDiscussionContext(context.discussionExchanges),
540
+ context.planningResult ?? '',
541
+ ];
542
+ }
543
+
544
+ function codeCallText(context: DevContext): string {
545
+ return quotedRelay(planningRelayValues(context));
546
+ }
547
+
548
+ function decideCallText(context: DevContext): string {
549
+ return quotedRelay(planningRelayValues(context));
550
+ }
551
+
552
+ function codeAfterDecideCallText(context: DevContext): string {
553
+ return quotedRelay([
554
+ ...planningRelayValues(context),
555
+ context.decideCommit ?? '',
556
+ context.evaluatedRevision ?? '',
557
+ ]);
558
+ }
559
+
560
+ const machineSetup = setup({
561
+ types: {} as {
562
+ context: DevContext;
563
+ events: DevEvent;
564
+ input: DevInput;
565
+ output: DevPlaybookOutput;
566
+ },
567
+ actors: {
568
+ player: fromPromise<PlayerOutput, PlayerInput>(async () => {
569
+ throw new Error('player actor must be provided by the runner');
570
+ }),
571
+ playbook: fromPromise<JsonValue | undefined, PlaybookInput>(async () => {
572
+ throw new Error('playbook actor must be provided by the runner');
573
+ }),
574
+ },
575
+ guards: {
576
+ isDiscussionComplete,
577
+ isCodePath,
578
+ isDecideThenCode,
579
+ needsBossReply,
580
+ isCodeSuccess,
581
+ isDecideSuccess,
582
+ authoredCodeFailure: authoredChildFailureGuard('code'),
583
+ authoredDecideFailure: authoredChildFailureGuard('decide'),
584
+ emptyBossReply: ({ event }) =>
585
+ event.type === 'BOSS_REPLY' && event.answer.trim().length === 0,
586
+ resumesPlanAnalysis: ({ context, event }) =>
587
+ event.type === 'BOSS_REPLY' &&
588
+ event.answer.trim().length > 0 &&
589
+ context.pendingBossQuestion?.resumeStateId === 'planAnalysis' &&
590
+ (event.questionId === undefined || event.questionId === 'planAnalysis'),
591
+ },
592
+ actions: {
593
+ 'playbook.acceptedOutcome': (
594
+ _args,
595
+ _params: {
596
+ readonly source: string;
597
+ readonly target: string;
598
+ readonly acceptedOutcome: string;
599
+ },
600
+ ) => undefined,
601
+ startDev: assign(({ context, event }) => {
602
+ if (event.type !== 'START_DEV') return {};
603
+ return {
604
+ runResults: context.runResults,
605
+ developmentRequest: event.developmentRequest,
606
+ discussionExchanges: [] as readonly DiscussionExchange[],
607
+ planningResult: undefined,
608
+ decideCommit: undefined,
609
+ evaluatedRevision: undefined,
610
+ completion: undefined,
611
+ childOutput: undefined,
612
+ childFailure: undefined,
613
+ lastError: undefined,
614
+ pendingBossQuestion: undefined,
615
+ bossReply: undefined,
616
+ };
617
+ }),
618
+ completeDiscussion: assign(({ context }) => ({
619
+ discussionExchanges: archivedExchanges(context),
620
+ completion: 'discussion-complete' as const,
621
+ pendingBossQuestion: undefined,
622
+ bossReply: undefined,
623
+ })),
624
+ rememberCodePath: assign(({ context, event }) => ({
625
+ planningResult: outputString(event, 'planningResult'),
626
+ discussionExchanges: archivedExchanges(context),
627
+ pendingBossQuestion: undefined,
628
+ bossReply: undefined,
629
+ })),
630
+ rememberDecidePath: assign(({ context, event }) => ({
631
+ planningResult: outputString(event, 'planningResult'),
632
+ discussionExchanges: archivedExchanges(context),
633
+ pendingBossQuestion: undefined,
634
+ bossReply: undefined,
635
+ })),
636
+ rememberPendingQuestion: assign(({ context, event }) => {
637
+ const question = outputString(event, 'question');
638
+ if (question === undefined || !outputGuard(event, 'needsBossReply')) {
639
+ return { lastError: new Error('Analyst question output was malformed') };
640
+ }
641
+ return {
642
+ discussionExchanges: archivedExchanges(context),
643
+ pendingBossQuestion: {
644
+ questionId: 'planAnalysis' as const,
645
+ resumeStateId: 'planAnalysis' as const,
646
+ sourceItem: 'DEV-1' as const,
647
+ asker: { kind: 'role' as const, roleId: 'analyst' as const },
648
+ question,
649
+ },
650
+ bossReply: undefined,
651
+ };
652
+ }),
653
+ rememberBossReply: assign(({ event }) =>
654
+ event.type === 'BOSS_REPLY' ? { bossReply: event.answer } : {},
655
+ ),
656
+ rememberEmptyBossReplyError: assign({
657
+ lastError: () => new Error('BOSS_REPLY received an empty answer'),
658
+ pendingBossQuestion: undefined,
659
+ bossReply: undefined,
660
+ }),
661
+ rememberDecideResult: assign(({ event }) => {
662
+ const output = childOutputOf(event);
663
+ if (!isDecideSuccessValue(output)) return {};
664
+ return {
665
+ decideCommit: output.decideCommit,
666
+ evaluatedRevision: output.evaluatedRevision,
667
+ };
668
+ }),
669
+ completeWithChildSuccess: assign(({ event }) => {
670
+ const output = childOutputOf(event);
671
+ return {
672
+ completion: 'complete' as const,
673
+ ...(output === undefined ? {} : { childOutput: output as JsonValue }),
674
+ };
675
+ }),
676
+ completeWithInsufficientCodeResult: assign(({ event }) => ({
677
+ completion: 'child-failed' as const,
678
+ childFailure: insufficientChildResult(event, 'code'),
679
+ })),
680
+ completeWithInsufficientDecideResult: assign(({ event }) => ({
681
+ completion: 'child-failed' as const,
682
+ childFailure: insufficientChildResult(event, 'decide'),
683
+ })),
684
+ completeWithCodeFailure: assign(({ event }) => ({
685
+ completion: 'child-failed' as const,
686
+ childFailure: relayedChildFailure(event, 'code'),
687
+ })),
688
+ completeWithDecideFailure: assign(({ event }) => ({
689
+ completion: 'child-failed' as const,
690
+ childFailure: relayedChildFailure(event, 'decide'),
691
+ })),
692
+ rememberActorError: assign(({ event }) => ({
693
+ lastError: (event as unknown as { readonly error?: unknown }).error,
694
+ })),
695
+ rememberMalformedPlayerOutput: assign({
696
+ lastError: () =>
697
+ new Error('Analyst result for DEV-1 did not match an available outcome.'),
698
+ }),
699
+ },
700
+ });
701
+
702
+ export const devMachine = machineSetup.createMachine({
703
+ id: 'dev',
704
+ initial: 'ready',
705
+ context: ({ input }) => ({
706
+ runResults: typeof input.runResults === 'string' ? input.runResults : '',
707
+ discussionExchanges: [],
708
+ }),
709
+ output: ({ context }): DevPlaybookOutput => {
710
+ if (context.completion === 'discussion-complete') {
711
+ return { status: 'discussion-complete' };
712
+ }
713
+ if (context.completion === 'complete') {
714
+ return {
715
+ status: 'complete',
716
+ childPlaybookId: 'code',
717
+ ...(context.childOutput === undefined
718
+ ? {}
719
+ : { childOutput: context.childOutput }),
720
+ };
721
+ }
722
+ if (context.completion === 'child-failed') {
723
+ if (context.childFailure === undefined) {
724
+ throw new Error('DEV reached a final state without a child result');
725
+ }
726
+ return { status: 'child-failed', childResult: context.childFailure };
727
+ }
728
+ throw new Error('DEV reached a final state without a recorded completion');
729
+ },
730
+ states: {
731
+ ready: {
732
+ id: 'ready',
733
+ description: STATE_DESCRIPTIONS.ready,
734
+ meta: playbookMeta('ready'),
735
+ tags: ['playbook.parked'],
736
+ on: {
737
+ START_DEV: { target: 'planAnalysis', actions: 'startDev' },
738
+ },
739
+ },
740
+ planAnalysis: {
741
+ id: 'planAnalysis',
742
+ description: STATE_DESCRIPTIONS.planAnalysis,
743
+ meta: playbookMeta('planAnalysis', 'analyst'),
744
+ tags: ['playbook.busy'],
745
+ invoke: {
746
+ src: 'player',
747
+ input: ({ context }): PlayerInput => ({
748
+ stateId: 'planAnalysis',
749
+ role: 'analyst',
750
+ sourceItem: 'DEV-1',
751
+ prompt: PLAN_ANALYSIS_PROMPT,
752
+ result: PLAN_ANALYSIS_RESULTS,
753
+ developmentRequest: context.developmentRequest ?? '',
754
+ discussionContext: renderDiscussionContext(
755
+ context.discussionExchanges,
756
+ ),
757
+ runResults: context.runResults,
758
+ ...(context.pendingBossQuestion === undefined
759
+ ? {}
760
+ : { pendingBossQuestion: context.pendingBossQuestion }),
761
+ ...(context.bossReply === undefined
762
+ ? {}
763
+ : { bossReply: context.bossReply }),
764
+ }),
765
+ onDone: [
766
+ {
767
+ guard: 'isDiscussionComplete',
768
+ target: 'discussionComplete',
769
+ actions: [
770
+ {
771
+ type: 'playbook.acceptedOutcome',
772
+ params: {
773
+ source: 'planAnalysis',
774
+ target: 'discussionComplete',
775
+ acceptedOutcome: 'discussionComplete',
776
+ },
777
+ },
778
+ 'completeDiscussion',
779
+ ],
780
+ },
781
+ {
782
+ guard: 'isCodePath',
783
+ target: 'callCode',
784
+ actions: [
785
+ {
786
+ type: 'playbook.acceptedOutcome',
787
+ params: {
788
+ source: 'planAnalysis',
789
+ target: 'callCode',
790
+ acceptedOutcome: 'code',
791
+ },
792
+ },
793
+ 'rememberCodePath',
794
+ ],
795
+ },
796
+ {
797
+ guard: 'isDecideThenCode',
798
+ target: 'callDecide',
799
+ actions: [
800
+ {
801
+ type: 'playbook.acceptedOutcome',
802
+ params: {
803
+ source: 'planAnalysis',
804
+ target: 'callDecide',
805
+ acceptedOutcome: 'decideThenCode',
806
+ },
807
+ },
808
+ 'rememberDecidePath',
809
+ ],
810
+ },
811
+ {
812
+ guard: 'needsBossReply',
813
+ target: 'awaitBossReply',
814
+ actions: [
815
+ {
816
+ type: 'playbook.acceptedOutcome',
817
+ params: {
818
+ source: 'planAnalysis',
819
+ target: 'awaitBossReply',
820
+ acceptedOutcome: 'needsBossReply',
821
+ },
822
+ },
823
+ 'rememberPendingQuestion',
824
+ ],
825
+ },
826
+ {
827
+ target: 'failed',
828
+ actions: 'rememberMalformedPlayerOutput',
829
+ },
830
+ ],
831
+ onError: { target: 'failed', actions: 'rememberActorError' },
832
+ },
833
+ },
834
+ callCode: {
835
+ id: 'callCode',
836
+ description: STATE_DESCRIPTIONS.callCode,
837
+ meta: playbookMeta('callCode'),
838
+ tags: ['playbook.suspended'],
839
+ invoke: {
840
+ src: 'playbook',
841
+ input: ({ context }): PlaybookInput => ({
842
+ stateId: 'callCode',
843
+ sourceItem: 'DEV-2',
844
+ playbookId: 'code',
845
+ text: codeCallText(context),
846
+ }),
847
+ onDone: [
848
+ {
849
+ guard: 'isCodeSuccess',
850
+ target: 'done',
851
+ actions: 'completeWithChildSuccess',
852
+ },
853
+ {
854
+ target: 'reportedChildFailure',
855
+ actions: 'completeWithInsufficientCodeResult',
856
+ },
857
+ ],
858
+ onError: [
859
+ {
860
+ guard: 'authoredCodeFailure',
861
+ target: 'reportedChildFailure',
862
+ actions: 'completeWithCodeFailure',
863
+ },
864
+ { target: 'failed', actions: 'rememberActorError' },
865
+ ],
866
+ },
867
+ },
868
+ callDecide: {
869
+ id: 'callDecide',
870
+ description: STATE_DESCRIPTIONS.callDecide,
871
+ meta: playbookMeta('callDecide'),
872
+ tags: ['playbook.suspended'],
873
+ invoke: {
874
+ src: 'playbook',
875
+ input: ({ context }): PlaybookInput => ({
876
+ stateId: 'callDecide',
877
+ sourceItem: 'DEV-3',
878
+ playbookId: 'decide',
879
+ text: decideCallText(context),
880
+ }),
881
+ onDone: [
882
+ {
883
+ guard: 'isDecideSuccess',
884
+ target: 'callCodeAfterDecide',
885
+ actions: 'rememberDecideResult',
886
+ },
887
+ {
888
+ target: 'reportedChildFailure',
889
+ actions: 'completeWithInsufficientDecideResult',
890
+ },
891
+ ],
892
+ onError: [
893
+ {
894
+ guard: 'authoredDecideFailure',
895
+ target: 'reportedChildFailure',
896
+ actions: 'completeWithDecideFailure',
897
+ },
898
+ { target: 'failed', actions: 'rememberActorError' },
899
+ ],
900
+ },
901
+ },
902
+ callCodeAfterDecide: {
903
+ id: 'callCodeAfterDecide',
904
+ description: STATE_DESCRIPTIONS.callCodeAfterDecide,
905
+ meta: playbookMeta('callCodeAfterDecide'),
906
+ tags: ['playbook.suspended'],
907
+ invoke: {
908
+ src: 'playbook',
909
+ input: ({ context }): PlaybookInput => ({
910
+ stateId: 'callCodeAfterDecide',
911
+ sourceItem: 'DEV-4',
912
+ playbookId: 'code',
913
+ text: codeAfterDecideCallText(context),
914
+ }),
915
+ onDone: [
916
+ {
917
+ guard: 'isCodeSuccess',
918
+ target: 'done',
919
+ actions: 'completeWithChildSuccess',
920
+ },
921
+ {
922
+ target: 'reportedChildFailure',
923
+ actions: 'completeWithInsufficientCodeResult',
924
+ },
925
+ ],
926
+ onError: [
927
+ {
928
+ guard: 'authoredCodeFailure',
929
+ target: 'reportedChildFailure',
930
+ actions: 'completeWithCodeFailure',
931
+ },
932
+ { target: 'failed', actions: 'rememberActorError' },
933
+ ],
934
+ },
935
+ },
936
+ awaitBossReply: {
937
+ id: 'awaitBossReply',
938
+ description: STATE_DESCRIPTIONS.awaitBossReply,
939
+ meta: playbookMeta('awaitBossReply'),
940
+ tags: ['playbook.parked'],
941
+ on: {
942
+ BOSS_REPLY: [
943
+ {
944
+ guard: 'emptyBossReply',
945
+ target: '#failed',
946
+ actions: 'rememberEmptyBossReplyError',
947
+ },
948
+ {
949
+ guard: 'resumesPlanAnalysis',
950
+ target: '#planAnalysis',
951
+ reenter: true,
952
+ actions: 'rememberBossReply',
953
+ },
954
+ ],
955
+ START_DEV: { target: 'planAnalysis', actions: 'startDev' },
956
+ },
957
+ },
958
+ failed: {
959
+ id: 'failed',
960
+ description: STATE_DESCRIPTIONS.failed,
961
+ meta: playbookMeta('failed'),
962
+ tags: ['playbook.parked'],
963
+ on: {
964
+ START_DEV: { target: 'planAnalysis', actions: 'startDev' },
965
+ },
966
+ },
967
+ discussionComplete: {
968
+ id: 'discussionComplete',
969
+ description: STATE_DESCRIPTIONS.discussionComplete,
970
+ meta: playbookMeta('discussionComplete'),
971
+ type: 'final',
972
+ },
973
+ done: {
974
+ id: 'done',
975
+ description: STATE_DESCRIPTIONS.done,
976
+ meta: playbookMeta('done'),
977
+ type: 'final',
978
+ },
979
+ reportedChildFailure: {
980
+ id: 'reportedChildFailure',
981
+ description: STATE_DESCRIPTIONS.reportedChildFailure,
982
+ meta: playbookMeta('reportedChildFailure'),
983
+ type: 'final',
984
+ },
985
+ },
986
+ });
987
+
988
+ export default devMachine;