@sublang/playbook 0.1.2 → 0.2.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.
@@ -40,6 +40,14 @@ type WorkflowKind = 'singleCommit' | 'iteration' | 'specSummary';
40
40
  type ChangeOrigin = 'bossIntent' | 'irTask';
41
41
  type ReviewSubject = 'commit' | 'changes';
42
42
  type AfterReview = 'continueIr' | 'summarizeSpecs' | 'done';
43
+ type ResumableStateId = Exclude<JumpableStateId, 'ready' | 'failed'>;
44
+
45
+ type PendingBossQuestion = {
46
+ resumeStateId: ResumableStateId;
47
+ sourceItem: string;
48
+ player: Player;
49
+ question: string;
50
+ };
43
51
 
44
52
  export type CaptainInput = {
45
53
  player: Player;
@@ -58,6 +66,8 @@ export type CaptainInput = {
58
66
  challenges?: string;
59
67
  coderPlayer?: string;
60
68
  reviewerPlayer?: string;
69
+ pendingBossQuestion?: PendingBossQuestion;
70
+ bossReply?: string;
61
71
  };
62
72
 
63
73
  export type CaptainOutput = {
@@ -67,6 +77,7 @@ export type CaptainOutput = {
67
77
  reviews?: string;
68
78
  challenges?: string;
69
79
  summary?: string;
80
+ question?: string;
70
81
  [k: string]: unknown;
71
82
  };
72
83
 
@@ -88,6 +99,8 @@ export type CodingContext = CodingInput & {
88
99
  challenges?: string;
89
100
  lastResult?: CaptainOutput;
90
101
  lastError?: unknown;
102
+ pendingBossQuestion?: PendingBossQuestion;
103
+ bossReply?: string;
91
104
  };
92
105
 
93
106
  export type CodingEvent =
@@ -99,7 +112,8 @@ export type CodingEvent =
99
112
  targetId: JumpableStateId;
100
113
  intent?: string;
101
114
  irNumber?: string;
102
- };
115
+ }
116
+ | { type: 'BOSS_REPLY'; answer: string };
103
117
 
104
118
  const jumpableStateIds = [
105
119
  'ready',
@@ -125,6 +139,28 @@ const jumpableStateIds = [
125
139
  'failed',
126
140
  ] as const satisfies readonly JumpableStateId[];
127
141
 
142
+ const resumableStateIds = [
143
+ 'planAndImplement',
144
+ 'respondToReview',
145
+ 'continueIr',
146
+ 'summarizeSpecs',
147
+ 'reviewBossCommitSpecs',
148
+ 'reviewBossCommitCode',
149
+ 'reviewBossCommitMixed',
150
+ 'reviewIrTaskCommitSpecs',
151
+ 'reviewIrTaskCommitCode',
152
+ 'reviewIrTaskCommitMixed',
153
+ 'reviewChangesSpecs',
154
+ 'reviewChangesCode',
155
+ 'reviewChangesMixed',
156
+ 'reviewChangesAndChallengesSpecs',
157
+ 'reviewChangesAndChallengesCode',
158
+ 'reviewChangesAndChallengesMixed',
159
+ 'adjudicateChallenges',
160
+ 'commitCoderInitial',
161
+ 'commitJoint',
162
+ ] as const satisfies readonly ResumableStateId[];
163
+
128
164
  const outputOf = (event: unknown): CaptainOutput | undefined =>
129
165
  (event as { output?: CaptainOutput }).output;
130
166
 
@@ -160,6 +196,10 @@ const rememberCaptainError = assign({
160
196
  lastError: ({ event }: { event: unknown }) => (event as { error?: unknown }).error,
161
197
  });
162
198
 
199
+ const rememberEmptyBossReplyError = assign({
200
+ lastError: () => new Error('BOSS_REPLY received empty answer'),
201
+ });
202
+
163
203
  const rememberBossInput = assign({
164
204
  intent: ({ context, event }: { context: CodingContext; event: unknown }) =>
165
205
  (event as { intent?: string }).intent ?? context.intent,
@@ -167,15 +207,185 @@ const rememberBossInput = assign({
167
207
  (event as { irNumber?: string }).irNumber ?? context.irNumber,
168
208
  });
169
209
 
170
- const bossInterrupts = (ids: readonly JumpableStateId[]) =>
210
+ type CaptainInputFactory = (context: Partial<CodingContext>) => CaptainInput;
211
+
212
+ const needsBossReplyDescription =
213
+ "The player's prose surfaces a clarifying question for Boss that the player cannot answer alone. Output shall include `question: <verbatim question text from the player's prose>`.";
214
+
215
+ const bossReplyInputFields = (context: Partial<CodingContext>) => ({
216
+ pendingBossQuestion: context.pendingBossQuestion,
217
+ bossReply: context.bossReply,
218
+ });
219
+
220
+ const withNeedsBossReply = <T extends Record<string, string>>(result: T) => ({
221
+ ...result,
222
+ needsBossReply: needsBossReplyDescription,
223
+ });
224
+
225
+ const captainStateMetadata = {
226
+ planAndImplement: { sourceItem: 'CODE-1', player: 'Coder' },
227
+ respondToReview: { sourceItem: 'CODE-2', player: 'Coder' },
228
+ continueIr: { sourceItem: 'CODE-3', player: 'Coder' },
229
+ summarizeSpecs: { sourceItem: 'CODE-4', player: 'Coder' },
230
+ reviewBossCommitSpecs: { sourceItem: 'CODE-5', player: 'Reviewer' },
231
+ reviewBossCommitCode: { sourceItem: 'CODE-6', player: 'Reviewer' },
232
+ reviewBossCommitMixed: { sourceItem: 'CODE-7', player: 'Reviewer' },
233
+ reviewIrTaskCommitSpecs: { sourceItem: 'CODE-8', player: 'Reviewer' },
234
+ reviewIrTaskCommitCode: { sourceItem: 'CODE-9', player: 'Reviewer' },
235
+ reviewIrTaskCommitMixed: { sourceItem: 'CODE-10', player: 'Reviewer' },
236
+ reviewChangesSpecs: { sourceItem: 'CODE-11', player: 'Reviewer' },
237
+ reviewChangesCode: { sourceItem: 'CODE-12', player: 'Reviewer' },
238
+ reviewChangesMixed: { sourceItem: 'CODE-13', player: 'Reviewer' },
239
+ reviewChangesAndChallengesSpecs: { sourceItem: 'CODE-15', player: 'Reviewer' },
240
+ reviewChangesAndChallengesCode: { sourceItem: 'CODE-16', player: 'Reviewer' },
241
+ reviewChangesAndChallengesMixed: { sourceItem: 'CODE-17', player: 'Reviewer' },
242
+ adjudicateChallenges: { sourceItem: 'CODE-14', player: 'Reviewer' },
243
+ commitCoderInitial: { sourceItem: 'CODE-18', player: 'Committer' },
244
+ commitJoint: { sourceItem: 'CODE-19', player: 'Committer' },
245
+ } as const satisfies Record<
246
+ ResumableStateId,
247
+ { sourceItem: string; player: Player }
248
+ >;
249
+
250
+ const planAndImplementInput: CaptainInputFactory = (context) => ({
251
+ player: 'Coder',
252
+ sourceItem: 'CODE-1',
253
+ intent: context.intent,
254
+ ...bossReplyInputFields(context),
255
+ prompt: [
256
+ 'Assess whether this can be completed in a single commit, following best practices.',
257
+ 'If yes, implement and test, updating both code and specs; otherwise, decompose into tasks as a new IR under @specs/iterations.',
258
+ 'Consult @specs/map.md for relevant context if needed; ensure it reflects the changes.',
259
+ 'Do not commit.',
260
+ ].join('\n'),
261
+ result: withNeedsBossReply({
262
+ singleCommitReady:
263
+ 'Coder produced uncommitted single-commit changes (Initial Changes).',
264
+ irDrafted:
265
+ 'Coder decomposed the intent into a new IR and drafted it uncommitted (Initial Changes).',
266
+ }),
267
+ });
268
+
269
+ const continueIrInput: CaptainInputFactory = (context) => ({
270
+ player: 'Coder',
271
+ sourceItem: 'CODE-3',
272
+ irNumber: context.irNumber,
273
+ ...bossReplyInputFields(context),
274
+ prompt: [
275
+ 'Continue to implement IR-<#> if not all deliverables and tasks are done.',
276
+ 'Implement one task at a time (including corresponding tests if any).',
277
+ 'Stop after each task for review — do not commit yet.',
278
+ 'If relevant, mark progress in the IR.',
279
+ ].join('\n'),
280
+ result: withNeedsBossReply({
281
+ taskReady:
282
+ 'Coder produced uncommitted changes for the next IR task (Initial Changes). Output shall include `taskDescription: <one-line description of the task just implemented>`.',
283
+ iterationDone: 'All IR deliverables and tasks are done.',
284
+ }),
285
+ });
286
+
287
+ const summarizeSpecsInput: CaptainInputFactory = (context) => ({
288
+ player: 'Coder',
289
+ sourceItem: 'CODE-4',
290
+ irNumber: context.irNumber,
291
+ ...bossReplyInputFields(context),
292
+ prompt: [
293
+ 'Read IR-<#> and corresponding commits.',
294
+ 'According to @specs/meta.md, add or update spec items to fully capture:',
295
+ '',
296
+ '- the user requirements in @specs/user,',
297
+ '- the system behavior in @specs/dev, and',
298
+ '- the integration/system test cases in @specs/test.',
299
+ '',
300
+ 'The spec items should be the *minimal* set needed to reimplement code without the IR.',
301
+ 'The set should be complete and coherent.',
302
+ 'Avoid implementation specifics.',
303
+ 'Avoid redundant spec items.',
304
+ 'Consult @specs/map.md for relevant context and update it to reflect your changes.',
305
+ ].join('\n'),
306
+ result: withNeedsBossReply({
307
+ specsReady: 'Coder produced uncommitted spec updates (Initial Changes).',
308
+ noSpecChanges: 'Existing specs already capture the iteration.',
309
+ }),
310
+ });
311
+
312
+ const setPendingBossQuestion = (resumeStateId: ResumableStateId) =>
313
+ assign({
314
+ pendingBossQuestion: ({
315
+ context,
316
+ event,
317
+ }: {
318
+ context: CodingContext;
319
+ event: unknown;
320
+ }) => {
321
+ const input = captainStateMetadata[resumeStateId];
322
+ return {
323
+ resumeStateId,
324
+ sourceItem: input.sourceItem,
325
+ player: input.player,
326
+ question: outputOf(event)?.question ?? '',
327
+ };
328
+ },
329
+ bossReply: () => undefined,
330
+ });
331
+
332
+ const rememberBossReply = assign({
333
+ bossReply: ({ event }: { event: unknown }) =>
334
+ (event as { answer?: string }).answer ?? '',
335
+ });
336
+
337
+ const clearBossReplyContext = assign({
338
+ pendingBossQuestion: () => undefined,
339
+ bossReply: () => undefined,
340
+ });
341
+
342
+ const actionsArray = (actions: unknown) =>
343
+ actions === undefined ? [] : Array.isArray(actions) ? actions : [actions];
344
+
345
+ const withClearBossReplyContext = <T extends { actions?: unknown }>(transition: T): T => ({
346
+ ...transition,
347
+ actions: [clearBossReplyContext, ...actionsArray(transition.actions)],
348
+ });
349
+
350
+ const bossInterrupts = (
351
+ ids: readonly JumpableStateId[],
352
+ extraActions: readonly unknown[] = [],
353
+ ) =>
171
354
  ids.map((id) => ({
172
355
  target: `#${id}` as const,
173
356
  guard: ({ event }: { event: CodingEvent }) =>
174
357
  event.type === 'BOSS_INTERRUPT' && event.targetId === id,
175
358
  reenter: true,
176
- actions: rememberBossInput,
359
+ actions: [...extraActions, rememberBossInput],
177
360
  }));
178
361
 
362
+ const resumableStates = (ids: readonly ResumableStateId[]) =>
363
+ ids.map((id) => ({
364
+ target: `#${id}` as const,
365
+ guard: ({ context, event }: { context: CodingContext; event: CodingEvent }) =>
366
+ event.type === 'BOSS_REPLY' &&
367
+ context.pendingBossQuestion?.resumeStateId === id,
368
+ reenter: true,
369
+ actions: rememberBossReply,
370
+ }));
371
+
372
+ const bossReplyIsEmpty = ({ event }: { event: CodingEvent }) =>
373
+ event.type === 'BOSS_REPLY' && event.answer.trim() === '';
374
+
375
+ const withNeedsBossReplyTransition = <
376
+ T extends readonly { guard?: unknown; target?: unknown; actions?: unknown }[],
377
+ >(
378
+ resumeStateId: ResumableStateId,
379
+ transitions: T,
380
+ ) => [
381
+ ...transitions.map((transition) => withClearBossReplyContext(transition)),
382
+ {
383
+ guard: guardIs('needsBossReply'),
384
+ target: '#awaitBossReply',
385
+ actions: [rememberCaptainOutput, setPendingBossQuestion(resumeStateId)],
386
+ },
387
+ ];
388
+
179
389
  const captainError = {
180
390
  target: '#failed',
181
391
  actions: rememberCaptainError,
@@ -217,6 +427,21 @@ const readyEvents = {
217
427
  },
218
428
  };
219
429
 
430
+ const awaitBossReplyEvents = {
431
+ START_CODING: withClearBossReplyContext(readyEvents.START_CODING),
432
+ CONTINUE_IR: withClearBossReplyContext(readyEvents.CONTINUE_IR),
433
+ SUMMARIZE_IR: withClearBossReplyContext(readyEvents.SUMMARIZE_IR),
434
+ BOSS_INTERRUPT: bossInterrupts(jumpableStateIds, [clearBossReplyContext]),
435
+ BOSS_REPLY: [
436
+ {
437
+ guard: bossReplyIsEmpty,
438
+ target: '#failed',
439
+ actions: [clearBossReplyContext, rememberEmptyBossReplyError],
440
+ },
441
+ ...resumableStates(resumableStateIds),
442
+ ],
443
+ };
444
+
220
445
  const captainPlaceholder = fromPromise<CaptainOutput, CaptainInput>(async () => {
221
446
  throw new Error('captain actor must be provided by the runner');
222
447
  });
@@ -250,31 +475,20 @@ export const codingMachine = setup({
250
475
  on: readyEvents,
251
476
  },
252
477
 
478
+ awaitBossReply: {
479
+ id: 'awaitBossReply',
480
+ description: 'Waiting for Boss to answer a player question.',
481
+ on: awaitBossReplyEvents,
482
+ },
483
+
253
484
  planAndImplement: {
254
485
  id: 'planAndImplement',
255
486
  description:
256
487
  'CODE-1: Coder assesses a Boss intent and either implements a single-commit change or drafts an IR.',
257
488
  invoke: {
258
489
  src: 'captain',
259
- input: ({ context }): CaptainInput => ({
260
- player: 'Coder',
261
- sourceItem: 'CODE-1',
262
- intent: context.intent,
263
- prompt: [
264
- 'Assess whether this can be completed in a single commit, following best practices.',
265
- 'If yes, implement and test, updating both code and specs; otherwise, decompose into tasks as a new IR under @specs/iterations.',
266
- 'Consult @specs/map.md for relevant context if needed; ensure it reflects the changes.',
267
- 'Do not commit.',
268
- ].join('\n'),
269
- result: {
270
- singleCommitReady:
271
- 'Coder produced uncommitted single-commit changes (Initial Changes).',
272
- irDrafted:
273
- 'Coder decomposed the intent into a new IR and drafted it uncommitted (Initial Changes).',
274
- needsBossInput: 'Progress requires additional Boss input.',
275
- },
276
- }),
277
- onDone: [
490
+ input: ({ context }): CaptainInput => planAndImplementInput(context),
491
+ onDone: withNeedsBossReplyTransition('planAndImplement', [
278
492
  {
279
493
  guard: guardIs('singleCommitReady'),
280
494
  target: '#commitCoderInitial',
@@ -299,8 +513,7 @@ export const codingMachine = setup({
299
513
  }),
300
514
  ],
301
515
  },
302
- { guard: guardIs('needsBossInput'), target: '#ready', actions: rememberCaptainOutput },
303
- ],
516
+ ]),
304
517
  onError: captainError,
305
518
  },
306
519
  },
@@ -313,6 +526,7 @@ export const codingMachine = setup({
313
526
  input: ({ context }): CaptainInput => ({
314
527
  player: 'Coder',
315
528
  sourceItem: 'CODE-2',
529
+ ...bossReplyInputFields(context),
316
530
  reviews: context.reviews,
317
531
  prompt: [
318
532
  'For each review item below for the above changes, challenge or accept it, with strong reasoning, solid evidence, and comprehensive thinking.',
@@ -335,9 +549,10 @@ export const codingMachine = setup({
335
549
  'Coder challenged one or more review items without producing any code edits. Output shall include `challenges: <numbered rebuttals, one per challenged item>`.',
336
550
  accepted:
337
551
  'Coder accepted the review outcome without further edits.',
552
+ needsBossReply: needsBossReplyDescription,
338
553
  },
339
554
  }),
340
- onDone: [
555
+ onDone: withNeedsBossReplyTransition('respondToReview', [
341
556
  {
342
557
  guard: guardIs('changesMadeSpecs'),
343
558
  target: '#reviewChangesSpecs',
@@ -402,7 +617,7 @@ export const codingMachine = setup({
402
617
  target: '#done',
403
618
  actions: rememberCaptainOutput,
404
619
  },
405
- ],
620
+ ]),
406
621
  onError: captainError,
407
622
  },
408
623
  },
@@ -413,24 +628,8 @@ export const codingMachine = setup({
413
628
  'CODE-3: Coder continues an IR after the previous task or IR draft passed review.',
414
629
  invoke: {
415
630
  src: 'captain',
416
- input: ({ context }): CaptainInput => ({
417
- player: 'Coder',
418
- sourceItem: 'CODE-3',
419
- irNumber: context.irNumber,
420
- prompt: [
421
- 'Continue to implement IR-<#> if not all deliverables and tasks are done.',
422
- 'Implement one task at a time (including corresponding tests if any).',
423
- 'Stop after each task for review — do not commit yet.',
424
- 'If relevant, mark progress in the IR.',
425
- ].join('\n'),
426
- result: {
427
- taskReady:
428
- 'Coder produced uncommitted changes for the next IR task (Initial Changes). Output shall include `taskDescription: <one-line description of the task just implemented>`.',
429
- iterationDone: 'All IR deliverables and tasks are done.',
430
- needsBossInput: 'Progress requires additional Boss input.',
431
- },
432
- }),
433
- onDone: [
631
+ input: ({ context }): CaptainInput => continueIrInput(context),
632
+ onDone: withNeedsBossReplyTransition('continueIr', [
434
633
  {
435
634
  guard: guardIs('taskReady'),
436
635
  target: '#commitCoderInitial',
@@ -454,8 +653,7 @@ export const codingMachine = setup({
454
653
  }),
455
654
  ],
456
655
  },
457
- { guard: guardIs('needsBossInput'), target: '#ready', actions: rememberCaptainOutput },
458
- ],
656
+ ]),
459
657
  onError: captainError,
460
658
  },
461
659
  },
@@ -465,31 +663,8 @@ export const codingMachine = setup({
465
663
  description: 'CODE-4: Coder summarizes a completed IR into minimal spec items.',
466
664
  invoke: {
467
665
  src: 'captain',
468
- input: ({ context }): CaptainInput => ({
469
- player: 'Coder',
470
- sourceItem: 'CODE-4',
471
- irNumber: context.irNumber,
472
- prompt: [
473
- 'Read IR-<#> and corresponding commits.',
474
- 'According to @specs/meta.md, add or update spec items to fully capture:',
475
- '',
476
- '- the user requirements in @specs/user,',
477
- '- the system behavior in @specs/dev, and',
478
- '- the integration/system test cases in @specs/test.',
479
- '',
480
- 'The spec items should be the *minimal* set needed to reimplement code without the IR.',
481
- 'The set should be complete and coherent.',
482
- 'Avoid implementation specifics.',
483
- 'Avoid redundant spec items.',
484
- 'Consult @specs/map.md for relevant context and update it to reflect your changes.',
485
- ].join('\n'),
486
- result: {
487
- specsReady: 'Coder produced uncommitted spec updates (Initial Changes).',
488
- noSpecChanges: 'Existing specs already capture the iteration.',
489
- needsBossInput: 'Spec summarization requires additional Boss input.',
490
- },
491
- }),
492
- onDone: [
666
+ input: ({ context }): CaptainInput => summarizeSpecsInput(context),
667
+ onDone: withNeedsBossReplyTransition('summarizeSpecs', [
493
668
  {
494
669
  guard: guardIs('specsReady'),
495
670
  target: '#commitCoderInitial',
@@ -502,9 +677,12 @@ export const codingMachine = setup({
502
677
  }),
503
678
  ],
504
679
  },
505
- { guard: guardIs('noSpecChanges'), target: '#done', actions: rememberCaptainOutput },
506
- { guard: guardIs('needsBossInput'), target: '#ready', actions: rememberCaptainOutput },
507
- ],
680
+ {
681
+ guard: guardIs('noSpecChanges'),
682
+ target: '#done',
683
+ actions: rememberCaptainOutput,
684
+ },
685
+ ]),
508
686
  onError: captainError,
509
687
  },
510
688
  },
@@ -518,6 +696,7 @@ export const codingMachine = setup({
518
696
  input: ({ context }): CaptainInput => ({
519
697
  player: 'Reviewer',
520
698
  sourceItem: 'CODE-5',
699
+ ...bossReplyInputFields(context),
521
700
  intent: context.intent,
522
701
  prompt: [
523
702
  'Review the latest commit.',
@@ -536,9 +715,10 @@ export const codingMachine = setup({
536
715
  noFindings: 'The spec-only commit has no review findings.',
537
716
  hasFindings:
538
717
  'The review produced findings for Coder. Output shall include `reviews: <numbered list of findings, no duplication>`.',
718
+ needsBossReply: needsBossReplyDescription,
539
719
  },
540
720
  }),
541
- onDone: [
721
+ onDone: withNeedsBossReplyTransition('reviewBossCommitSpecs', [
542
722
  { guard: noFindingsAfter('continueIr'), target: '#continueIr', actions: rememberCaptainOutput },
543
723
  { guard: noFindingsAfter('summarizeSpecs'), target: '#summarizeSpecs', actions: rememberCaptainOutput },
544
724
  { guard: noFindingsAfter('done'), target: '#done', actions: rememberCaptainOutput },
@@ -547,7 +727,7 @@ export const codingMachine = setup({
547
727
  target: '#respondToReview',
548
728
  actions: [rememberCaptainOutput, assign({ reviewSubject: () => 'commit' as const })],
549
729
  },
550
- ],
730
+ ]),
551
731
  onError: captainError,
552
732
  },
553
733
  },
@@ -561,6 +741,7 @@ export const codingMachine = setup({
561
741
  input: ({ context }): CaptainInput => ({
562
742
  player: 'Reviewer',
563
743
  sourceItem: 'CODE-6',
744
+ ...bossReplyInputFields(context),
564
745
  intent: context.intent,
565
746
  prompt: [
566
747
  'Review the latest commit.',
@@ -574,9 +755,10 @@ export const codingMachine = setup({
574
755
  noFindings: 'The code-only commit has no review findings.',
575
756
  hasFindings:
576
757
  'The review produced findings for Coder. Output shall include `reviews: <numbered list of findings, no duplication>`.',
758
+ needsBossReply: needsBossReplyDescription,
577
759
  },
578
760
  }),
579
- onDone: [
761
+ onDone: withNeedsBossReplyTransition('reviewBossCommitCode', [
580
762
  { guard: noFindingsAfter('continueIr'), target: '#continueIr', actions: rememberCaptainOutput },
581
763
  { guard: noFindingsAfter('summarizeSpecs'), target: '#summarizeSpecs', actions: rememberCaptainOutput },
582
764
  { guard: noFindingsAfter('done'), target: '#done', actions: rememberCaptainOutput },
@@ -585,7 +767,7 @@ export const codingMachine = setup({
585
767
  target: '#respondToReview',
586
768
  actions: [rememberCaptainOutput, assign({ reviewSubject: () => 'commit' as const })],
587
769
  },
588
- ],
770
+ ]),
589
771
  onError: captainError,
590
772
  },
591
773
  },
@@ -599,6 +781,7 @@ export const codingMachine = setup({
599
781
  input: ({ context }): CaptainInput => ({
600
782
  player: 'Reviewer',
601
783
  sourceItem: 'CODE-7',
784
+ ...bossReplyInputFields(context),
602
785
  intent: context.intent,
603
786
  prompt: [
604
787
  'Review the latest commit.',
@@ -619,9 +802,10 @@ export const codingMachine = setup({
619
802
  noFindings: 'The mixed commit has no review findings.',
620
803
  hasFindings:
621
804
  'The review produced findings for Coder. Output shall include `reviews: <numbered list of findings, no duplication>`.',
805
+ needsBossReply: needsBossReplyDescription,
622
806
  },
623
807
  }),
624
- onDone: [
808
+ onDone: withNeedsBossReplyTransition('reviewBossCommitMixed', [
625
809
  { guard: noFindingsAfter('continueIr'), target: '#continueIr', actions: rememberCaptainOutput },
626
810
  { guard: noFindingsAfter('summarizeSpecs'), target: '#summarizeSpecs', actions: rememberCaptainOutput },
627
811
  { guard: noFindingsAfter('done'), target: '#done', actions: rememberCaptainOutput },
@@ -630,7 +814,7 @@ export const codingMachine = setup({
630
814
  target: '#respondToReview',
631
815
  actions: [rememberCaptainOutput, assign({ reviewSubject: () => 'commit' as const })],
632
816
  },
633
- ],
817
+ ]),
634
818
  onError: captainError,
635
819
  },
636
820
  },
@@ -644,6 +828,7 @@ export const codingMachine = setup({
644
828
  input: ({ context }): CaptainInput => ({
645
829
  player: 'Reviewer',
646
830
  sourceItem: 'CODE-8',
831
+ ...bossReplyInputFields(context),
647
832
  irNumber: context.irNumber,
648
833
  taskDescription: context.taskDescription,
649
834
  prompt: [
@@ -663,9 +848,10 @@ export const codingMachine = setup({
663
848
  noFindings: 'The IR-task spec-only commit has no review findings.',
664
849
  hasFindings:
665
850
  'The review produced findings for Coder. Output shall include `reviews: <numbered list of findings, no duplication>`.',
851
+ needsBossReply: needsBossReplyDescription,
666
852
  },
667
853
  }),
668
- onDone: [
854
+ onDone: withNeedsBossReplyTransition('reviewIrTaskCommitSpecs', [
669
855
  { guard: noFindingsAfter('continueIr'), target: '#continueIr', actions: rememberCaptainOutput },
670
856
  { guard: noFindingsAfter('summarizeSpecs'), target: '#summarizeSpecs', actions: rememberCaptainOutput },
671
857
  { guard: noFindingsAfter('done'), target: '#done', actions: rememberCaptainOutput },
@@ -674,7 +860,7 @@ export const codingMachine = setup({
674
860
  target: '#respondToReview',
675
861
  actions: [rememberCaptainOutput, assign({ reviewSubject: () => 'commit' as const })],
676
862
  },
677
- ],
863
+ ]),
678
864
  onError: captainError,
679
865
  },
680
866
  },
@@ -688,6 +874,7 @@ export const codingMachine = setup({
688
874
  input: ({ context }): CaptainInput => ({
689
875
  player: 'Reviewer',
690
876
  sourceItem: 'CODE-9',
877
+ ...bossReplyInputFields(context),
691
878
  irNumber: context.irNumber,
692
879
  taskDescription: context.taskDescription,
693
880
  prompt: [
@@ -702,9 +889,10 @@ export const codingMachine = setup({
702
889
  noFindings: 'The IR-task code-only commit has no review findings.',
703
890
  hasFindings:
704
891
  'The review produced findings for Coder. Output shall include `reviews: <numbered list of findings, no duplication>`.',
892
+ needsBossReply: needsBossReplyDescription,
705
893
  },
706
894
  }),
707
- onDone: [
895
+ onDone: withNeedsBossReplyTransition('reviewIrTaskCommitCode', [
708
896
  { guard: noFindingsAfter('continueIr'), target: '#continueIr', actions: rememberCaptainOutput },
709
897
  { guard: noFindingsAfter('summarizeSpecs'), target: '#summarizeSpecs', actions: rememberCaptainOutput },
710
898
  { guard: noFindingsAfter('done'), target: '#done', actions: rememberCaptainOutput },
@@ -713,7 +901,7 @@ export const codingMachine = setup({
713
901
  target: '#respondToReview',
714
902
  actions: [rememberCaptainOutput, assign({ reviewSubject: () => 'commit' as const })],
715
903
  },
716
- ],
904
+ ]),
717
905
  onError: captainError,
718
906
  },
719
907
  },
@@ -727,6 +915,7 @@ export const codingMachine = setup({
727
915
  input: ({ context }): CaptainInput => ({
728
916
  player: 'Reviewer',
729
917
  sourceItem: 'CODE-10',
918
+ ...bossReplyInputFields(context),
730
919
  irNumber: context.irNumber,
731
920
  taskDescription: context.taskDescription,
732
921
  prompt: [
@@ -748,9 +937,10 @@ export const codingMachine = setup({
748
937
  noFindings: 'The IR-task mixed commit has no review findings.',
749
938
  hasFindings:
750
939
  'The review produced findings for Coder. Output shall include `reviews: <numbered list of findings, no duplication>`.',
940
+ needsBossReply: needsBossReplyDescription,
751
941
  },
752
942
  }),
753
- onDone: [
943
+ onDone: withNeedsBossReplyTransition('reviewIrTaskCommitMixed', [
754
944
  { guard: noFindingsAfter('continueIr'), target: '#continueIr', actions: rememberCaptainOutput },
755
945
  { guard: noFindingsAfter('summarizeSpecs'), target: '#summarizeSpecs', actions: rememberCaptainOutput },
756
946
  { guard: noFindingsAfter('done'), target: '#done', actions: rememberCaptainOutput },
@@ -759,7 +949,7 @@ export const codingMachine = setup({
759
949
  target: '#respondToReview',
760
950
  actions: [rememberCaptainOutput, assign({ reviewSubject: () => 'commit' as const })],
761
951
  },
762
- ],
952
+ ]),
763
953
  onError: captainError,
764
954
  },
765
955
  },
@@ -770,9 +960,10 @@ export const codingMachine = setup({
770
960
  'CODE-11: Reviewer reviews uncommitted Coder changes that touch only @specs/{user,dev,test}/ with no accompanying rebuttals.',
771
961
  invoke: {
772
962
  src: 'captain',
773
- input: (): CaptainInput => ({
963
+ input: ({ context }): CaptainInput => ({
774
964
  player: 'Reviewer',
775
965
  sourceItem: 'CODE-11',
966
+ ...bossReplyInputFields(context),
776
967
  prompt: [
777
968
  'Review the unstaged/untracked changes.',
778
969
  'Understand the intent.',
@@ -790,9 +981,10 @@ export const codingMachine = setup({
790
981
  noFindings: 'The uncommitted spec-only changes are ready to commit.',
791
982
  hasFindings:
792
983
  'The review produced findings for Coder. Output shall include `reviews: <numbered list of findings, no duplication>`.',
984
+ needsBossReply: needsBossReplyDescription,
793
985
  },
794
986
  }),
795
- onDone: [
987
+ onDone: withNeedsBossReplyTransition('reviewChangesSpecs', [
796
988
  {
797
989
  guard: guardIs('noFindings'),
798
990
  target: '#commitJoint',
@@ -803,7 +995,7 @@ export const codingMachine = setup({
803
995
  target: '#respondToReview',
804
996
  actions: [rememberCaptainOutput, assign({ reviewSubject: () => 'changes' as const })],
805
997
  },
806
- ],
998
+ ]),
807
999
  onError: captainError,
808
1000
  },
809
1001
  },
@@ -814,9 +1006,10 @@ export const codingMachine = setup({
814
1006
  'CODE-12: Reviewer reviews uncommitted Coder changes that touch only files outside @specs/{user,dev,test}/ with no accompanying rebuttals.',
815
1007
  invoke: {
816
1008
  src: 'captain',
817
- input: (): CaptainInput => ({
1009
+ input: ({ context }): CaptainInput => ({
818
1010
  player: 'Reviewer',
819
1011
  sourceItem: 'CODE-12',
1012
+ ...bossReplyInputFields(context),
820
1013
  prompt: [
821
1014
  'Review the unstaged/untracked changes.',
822
1015
  'Understand the intent.',
@@ -829,9 +1022,10 @@ export const codingMachine = setup({
829
1022
  noFindings: 'The uncommitted code-only changes are ready to commit.',
830
1023
  hasFindings:
831
1024
  'The review produced findings for Coder. Output shall include `reviews: <numbered list of findings, no duplication>`.',
1025
+ needsBossReply: needsBossReplyDescription,
832
1026
  },
833
1027
  }),
834
- onDone: [
1028
+ onDone: withNeedsBossReplyTransition('reviewChangesCode', [
835
1029
  {
836
1030
  guard: guardIs('noFindings'),
837
1031
  target: '#commitJoint',
@@ -842,7 +1036,7 @@ export const codingMachine = setup({
842
1036
  target: '#respondToReview',
843
1037
  actions: [rememberCaptainOutput, assign({ reviewSubject: () => 'changes' as const })],
844
1038
  },
845
- ],
1039
+ ]),
846
1040
  onError: captainError,
847
1041
  },
848
1042
  },
@@ -853,9 +1047,10 @@ export const codingMachine = setup({
853
1047
  'CODE-13: Reviewer reviews uncommitted Coder changes that touch both @specs/{user,dev,test}/ and other files with no accompanying rebuttals.',
854
1048
  invoke: {
855
1049
  src: 'captain',
856
- input: (): CaptainInput => ({
1050
+ input: ({ context }): CaptainInput => ({
857
1051
  player: 'Reviewer',
858
1052
  sourceItem: 'CODE-13',
1053
+ ...bossReplyInputFields(context),
859
1054
  prompt: [
860
1055
  'Review the unstaged/untracked changes.',
861
1056
  'Understand the intent.',
@@ -875,9 +1070,10 @@ export const codingMachine = setup({
875
1070
  noFindings: 'The uncommitted mixed changes are ready to commit.',
876
1071
  hasFindings:
877
1072
  'The review produced findings for Coder. Output shall include `reviews: <numbered list of findings, no duplication>`.',
1073
+ needsBossReply: needsBossReplyDescription,
878
1074
  },
879
1075
  }),
880
- onDone: [
1076
+ onDone: withNeedsBossReplyTransition('reviewChangesMixed', [
881
1077
  {
882
1078
  guard: guardIs('noFindings'),
883
1079
  target: '#commitJoint',
@@ -888,7 +1084,7 @@ export const codingMachine = setup({
888
1084
  target: '#respondToReview',
889
1085
  actions: [rememberCaptainOutput, assign({ reviewSubject: () => 'changes' as const })],
890
1086
  },
891
- ],
1087
+ ]),
892
1088
  onError: captainError,
893
1089
  },
894
1090
  },
@@ -902,6 +1098,7 @@ export const codingMachine = setup({
902
1098
  input: ({ context }): CaptainInput => ({
903
1099
  player: 'Reviewer',
904
1100
  sourceItem: 'CODE-15',
1101
+ ...bossReplyInputFields(context),
905
1102
  reviews: context.reviews,
906
1103
  challenges: context.challenges,
907
1104
  prompt: [
@@ -923,9 +1120,10 @@ export const codingMachine = setup({
923
1120
  'The new spec-only changes are ready to commit and every rebuttal was accepted (or no items remained open).',
924
1121
  needsRevision:
925
1122
  'The combined round needs more work: the review produced findings, one or more rebuttals were rejected, or both. Output shall include `reviews: <numbered list of any new findings or rejected rebuttals to address>`.',
1123
+ needsBossReply: needsBossReplyDescription,
926
1124
  },
927
1125
  }),
928
- onDone: [
1126
+ onDone: withNeedsBossReplyTransition('reviewChangesAndChallengesSpecs', [
929
1127
  {
930
1128
  guard: guardIs('approved'),
931
1129
  target: '#commitJoint',
@@ -936,7 +1134,7 @@ export const codingMachine = setup({
936
1134
  target: '#respondToReview',
937
1135
  actions: [rememberCaptainOutput, assign({ reviewSubject: () => 'changes' as const })],
938
1136
  },
939
- ],
1137
+ ]),
940
1138
  onError: captainError,
941
1139
  },
942
1140
  },
@@ -950,6 +1148,7 @@ export const codingMachine = setup({
950
1148
  input: ({ context }): CaptainInput => ({
951
1149
  player: 'Reviewer',
952
1150
  sourceItem: 'CODE-16',
1151
+ ...bossReplyInputFields(context),
953
1152
  reviews: context.reviews,
954
1153
  challenges: context.challenges,
955
1154
  prompt: [
@@ -966,9 +1165,10 @@ export const codingMachine = setup({
966
1165
  'The new code-only changes are ready to commit and every rebuttal was accepted (or no items remained open).',
967
1166
  needsRevision:
968
1167
  'The combined round needs more work: the review produced findings, one or more rebuttals were rejected, or both. Output shall include `reviews: <numbered list of any new findings or rejected rebuttals to address>`.',
1168
+ needsBossReply: needsBossReplyDescription,
969
1169
  },
970
1170
  }),
971
- onDone: [
1171
+ onDone: withNeedsBossReplyTransition('reviewChangesAndChallengesCode', [
972
1172
  {
973
1173
  guard: guardIs('approved'),
974
1174
  target: '#commitJoint',
@@ -979,7 +1179,7 @@ export const codingMachine = setup({
979
1179
  target: '#respondToReview',
980
1180
  actions: [rememberCaptainOutput, assign({ reviewSubject: () => 'changes' as const })],
981
1181
  },
982
- ],
1182
+ ]),
983
1183
  onError: captainError,
984
1184
  },
985
1185
  },
@@ -993,6 +1193,7 @@ export const codingMachine = setup({
993
1193
  input: ({ context }): CaptainInput => ({
994
1194
  player: 'Reviewer',
995
1195
  sourceItem: 'CODE-17',
1196
+ ...bossReplyInputFields(context),
996
1197
  reviews: context.reviews,
997
1198
  challenges: context.challenges,
998
1199
  prompt: [
@@ -1016,9 +1217,10 @@ export const codingMachine = setup({
1016
1217
  'The new mixed changes are ready to commit and every rebuttal was accepted (or no items remained open).',
1017
1218
  needsRevision:
1018
1219
  'The combined round needs more work: the review produced findings, one or more rebuttals were rejected, or both. Output shall include `reviews: <numbered list of any new findings or rejected rebuttals to address>`.',
1220
+ needsBossReply: needsBossReplyDescription,
1019
1221
  },
1020
1222
  }),
1021
- onDone: [
1223
+ onDone: withNeedsBossReplyTransition('reviewChangesAndChallengesMixed', [
1022
1224
  {
1023
1225
  guard: guardIs('approved'),
1024
1226
  target: '#commitJoint',
@@ -1029,7 +1231,7 @@ export const codingMachine = setup({
1029
1231
  target: '#respondToReview',
1030
1232
  actions: [rememberCaptainOutput, assign({ reviewSubject: () => 'changes' as const })],
1031
1233
  },
1032
- ],
1234
+ ]),
1033
1235
  onError: captainError,
1034
1236
  },
1035
1237
  },
@@ -1042,6 +1244,7 @@ export const codingMachine = setup({
1042
1244
  input: ({ context }): CaptainInput => ({
1043
1245
  player: 'Reviewer',
1044
1246
  sourceItem: 'CODE-14',
1247
+ ...bossReplyInputFields(context),
1045
1248
  reviews: context.reviews,
1046
1249
  challenges: context.challenges,
1047
1250
  prompt: [
@@ -1053,9 +1256,10 @@ export const codingMachine = setup({
1053
1256
  challengeRejected:
1054
1257
  'Reviewer rejected the rebuttal — Coder must respond again.',
1055
1258
  noOpenItems: 'No review items remain open.',
1259
+ needsBossReply: needsBossReplyDescription,
1056
1260
  },
1057
1261
  }),
1058
- onDone: [
1262
+ onDone: withNeedsBossReplyTransition('adjudicateChallenges', [
1059
1263
  {
1060
1264
  guard: ({ context, event }) =>
1061
1265
  (outputOf(event)?.guard === 'challengeAccepted' ||
@@ -1096,7 +1300,7 @@ export const codingMachine = setup({
1096
1300
  target: '#respondToReview',
1097
1301
  actions: rememberCaptainOutput,
1098
1302
  },
1099
- ],
1303
+ ]),
1100
1304
  onError: captainError,
1101
1305
  },
1102
1306
  },
@@ -1110,6 +1314,7 @@ export const codingMachine = setup({
1110
1314
  input: ({ context }): CaptainInput => ({
1111
1315
  player: 'Committer',
1112
1316
  sourceItem: 'CODE-18',
1317
+ ...bossReplyInputFields(context),
1113
1318
  coderPlayer: context.coderPlayer,
1114
1319
  prompt: [
1115
1320
  'Make a commit of the changes that belong in the repo, following @specs/dev/git.md (reread if necessary).',
@@ -1121,9 +1326,10 @@ export const codingMachine = setup({
1121
1326
  committedMixed: 'Committed changes that span both @specs/{user,dev,test}/ and other files.',
1122
1327
  noRelevantChanges: 'There are no relevant changes to commit.',
1123
1328
  needsBossInput: 'Committing requires additional Boss input.',
1329
+ needsBossReply: needsBossReplyDescription,
1124
1330
  },
1125
1331
  }),
1126
- onDone: [
1332
+ onDone: withNeedsBossReplyTransition('commitCoderInitial', [
1127
1333
  {
1128
1334
  guard: guardAndOrigin('committedSpecs', 'bossIntent'),
1129
1335
  target: '#reviewBossCommitSpecs',
@@ -1156,7 +1362,7 @@ export const codingMachine = setup({
1156
1362
  },
1157
1363
  { guard: guardIs('noRelevantChanges'), target: '#ready', actions: rememberCaptainOutput },
1158
1364
  { guard: guardIs('needsBossInput'), target: '#ready', actions: rememberCaptainOutput },
1159
- ],
1365
+ ]),
1160
1366
  onError: captainError,
1161
1367
  },
1162
1368
  },
@@ -1170,6 +1376,7 @@ export const codingMachine = setup({
1170
1376
  input: ({ context }): CaptainInput => ({
1171
1377
  player: 'Committer',
1172
1378
  sourceItem: 'CODE-19',
1379
+ ...bossReplyInputFields(context),
1173
1380
  coderPlayer: context.coderPlayer,
1174
1381
  reviewerPlayer: context.reviewerPlayer,
1175
1382
  prompt: [
@@ -1180,9 +1387,10 @@ export const codingMachine = setup({
1180
1387
  committed: 'Relevant changes were committed.',
1181
1388
  noRelevantChanges: 'There are no relevant changes to commit.',
1182
1389
  needsBossInput: 'Committing requires additional Boss input.',
1390
+ needsBossReply: needsBossReplyDescription,
1183
1391
  },
1184
1392
  }),
1185
- onDone: [
1393
+ onDone: withNeedsBossReplyTransition('commitJoint', [
1186
1394
  {
1187
1395
  guard: ({ context, event }) =>
1188
1396
  outputOf(event)?.guard === 'committed' && context.afterReview === 'continueIr',
@@ -1203,7 +1411,7 @@ export const codingMachine = setup({
1203
1411
  },
1204
1412
  { guard: guardIs('noRelevantChanges'), target: '#done', actions: rememberCaptainOutput },
1205
1413
  { guard: guardIs('needsBossInput'), target: '#ready', actions: rememberCaptainOutput },
1206
- ],
1414
+ ]),
1207
1415
  onError: captainError,
1208
1416
  },
1209
1417
  },