@sublang/playbook 5.0.0 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/README.md +11 -7
  2. package/docs/cli.md +38 -35
  3. package/docs/configuration.md +58 -15
  4. package/docs/embedding.md +24 -16
  5. package/package.json +40 -21
  6. package/reference/sdlc/captain.playbook/captain.playbook.js +2 -0
  7. package/reference/sdlc/captain.playbook/captain.playbook.ts +2 -0
  8. package/reference/sdlc/code.md +55 -97
  9. package/reference/sdlc/code.playbook/code.fsm.d.ts +229 -94
  10. package/reference/sdlc/code.playbook/code.fsm.introspect.d.ts +26 -44
  11. package/reference/sdlc/code.playbook/code.fsm.introspect.js +61 -66
  12. package/reference/sdlc/code.playbook/code.fsm.introspect.ts +100 -149
  13. package/reference/sdlc/code.playbook/code.fsm.js +587 -1347
  14. package/reference/sdlc/code.playbook/code.fsm.ts +809 -1650
  15. package/reference/sdlc/code.playbook/code.gears.md +51 -263
  16. package/reference/sdlc/code.playbook/code.playbook.d.ts +8 -47
  17. package/reference/sdlc/code.playbook/code.playbook.js +69 -656
  18. package/reference/sdlc/code.playbook/code.playbook.ts +90 -867
  19. package/reference/sdlc/code.playbook/code.registry.d.ts +9 -25
  20. package/reference/sdlc/code.playbook/code.registry.js +20 -78
  21. package/reference/sdlc/code.playbook/code.registry.ts +58 -122
  22. package/reference/sdlc/code.playbook/playbook-captain.js +93 -15
  23. package/reference/sdlc/code.playbook/playbook-captain.ts +115 -19
  24. package/reference/sdlc/code.playbook/playbook.config.template.yaml +33 -22
  25. package/reference/sdlc/decide.md +54 -0
  26. package/reference/sdlc/decide.playbook/decide.fsm.d.ts +261 -0
  27. package/reference/sdlc/decide.playbook/decide.fsm.js +894 -0
  28. package/reference/sdlc/decide.playbook/decide.fsm.ts +1152 -0
  29. package/reference/sdlc/decide.playbook/decide.gears.md +88 -0
  30. package/reference/sdlc/decide.playbook/decide.playbook.d.ts +67 -0
  31. package/reference/sdlc/{discuss.playbook/discuss.playbook.js → decide.playbook/decide.playbook.js} +471 -362
  32. package/reference/sdlc/{discuss.playbook/discuss.playbook.ts → decide.playbook/decide.playbook.ts} +575 -443
  33. package/reference/sdlc/decide.playbook/decide.registry.d.ts +41 -0
  34. package/reference/sdlc/decide.playbook/decide.registry.js +60 -0
  35. package/reference/sdlc/decide.playbook/decide.registry.ts +125 -0
  36. package/reference/sdlc/review.md +81 -0
  37. package/reference/sdlc/review.playbook/review.fsm.d.ts +183 -0
  38. package/reference/sdlc/review.playbook/review.fsm.js +524 -0
  39. package/reference/sdlc/review.playbook/review.fsm.ts +652 -0
  40. package/reference/sdlc/review.playbook/review.gears.md +112 -0
  41. package/reference/sdlc/review.playbook/review.playbook.d.ts +12 -0
  42. package/reference/sdlc/review.playbook/review.playbook.js +112 -0
  43. package/reference/sdlc/review.playbook/review.playbook.ts +201 -0
  44. package/reference/sdlc/review.playbook/review.registry.d.ts +43 -0
  45. package/reference/sdlc/review.playbook/review.registry.js +73 -0
  46. package/reference/sdlc/review.playbook/review.registry.ts +138 -0
  47. package/slc/gears2fsm.md +13 -4
  48. package/slc/link.md +48 -2
  49. package/slc/text2gears.md +22 -2
  50. package/src/runtime.d.ts +7 -0
  51. package/src/runtime.ts +12 -0
  52. package/src/xstate-playbook-runtime.d.ts +9 -2
  53. package/src/xstate-playbook-runtime.js +255 -21
  54. package/src/xstate-playbook-runtime.ts +333 -28
  55. package/src/xstate-runtime.js +25 -0
  56. package/src/xstate-runtime.ts +51 -0
  57. package/reference/sdlc/discuss.md +0 -93
  58. package/reference/sdlc/discuss.playbook/discuss.fsm.d.ts +0 -396
  59. package/reference/sdlc/discuss.playbook/discuss.fsm.js +0 -2067
  60. package/reference/sdlc/discuss.playbook/discuss.fsm.ts +0 -2465
  61. package/reference/sdlc/discuss.playbook/discuss.gears.md +0 -258
  62. package/reference/sdlc/discuss.playbook/discuss.playbook.d.ts +0 -113
  63. package/reference/sdlc/discuss.playbook/discuss.registry.d.ts +0 -58
  64. package/reference/sdlc/discuss.playbook/discuss.registry.js +0 -97
  65. package/reference/sdlc/discuss.playbook/discuss.registry.ts +0 -153
@@ -1,1769 +1,928 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
2
  // SPDX-FileCopyrightText: 2026 SubLang International <https://sublang.ai>
3
-
4
- // XState v5's generics do not flow through helpers declared outside the
5
- // machine (e.g., shared guard/action factories, the `readyEvents` block),
6
- // so strict checking emits structural-mismatch noise without surfacing real
7
- // bugs. The runner overrides `player` via `.provide({ actors: { ... } })`
8
- // and re-asserts the public surface (`CodingContext`, `CodingEvent`,
9
- // `PlayerInput`, `PlayerOutput`) at the boundary.
10
- // @ts-nocheck
3
+ //
4
+ // Generated by slc/gears2fsm.md from ./code.gears.md.
11
5
 
12
6
  import { assign, fromPromise, setup } from 'xstate';
13
7
 
14
- type Player = 'Coder' | 'Reviewer' | 'Committer';
15
-
16
- type JumpableStateId =
17
- | 'ready'
18
- | 'planAndImplement'
19
- | 'respondToReview'
20
- | 'continueIr'
21
- | 'summarizeSpecs'
22
- | 'reviewBossCommitSpecs'
23
- | 'reviewBossCommitCode'
24
- | 'reviewBossCommitMixed'
25
- | 'reviewIrTaskCommitSpecs'
26
- | 'reviewIrTaskCommitCode'
27
- | 'reviewIrTaskCommitMixed'
28
- | 'reviewChangesSpecs'
29
- | 'reviewChangesCode'
30
- | 'reviewChangesMixed'
31
- | 'reviewChangesAndChallengesSpecs'
32
- | 'reviewChangesAndChallengesCode'
33
- | 'reviewChangesAndChallengesMixed'
34
- | 'adjudicateChallenges'
35
- | 'commitCoderInitial'
36
- | 'commitJoint'
37
- | 'failed';
38
-
39
- type WorkflowKind = 'singleCommit' | 'iteration' | 'specSummary';
40
- type ChangeOrigin = 'bossIntent' | 'irTask';
41
- type ReviewSubject = 'commit' | 'changes';
42
- type AfterReview = 'continueIr' | 'summarizeSpecs' | 'done';
43
- type ResumableStateId = Exclude<JumpableStateId, 'ready' | 'failed'>;
44
-
45
- type PendingBossQuestion = {
46
- questionId: ResumableStateId;
47
- resumeStateId: ResumableStateId;
48
- sourceItem: string;
49
- player: Player;
50
- question: string;
8
+ export type JsonValue =
9
+ | null
10
+ | boolean
11
+ | number
12
+ | string
13
+ | readonly JsonValue[]
14
+ | { readonly [key: string]: JsonValue };
15
+
16
+ export type CodeStateId =
17
+ | 'runFirstPhase'
18
+ | 'reviewFirstCommit'
19
+ | 'runIrTask'
20
+ | 'reviewIrTask';
21
+
22
+ export type CodeSourceItem = 'CODE-1' | 'CODE-2' | 'CODE-3' | 'CODE-4';
23
+
24
+ export type CodePhase =
25
+ | 'direct'
26
+ | 'ir-created'
27
+ | 'ir-task-more'
28
+ | 'ir-task-final';
29
+
30
+ export type PendingBossQuestion = {
31
+ readonly questionId: 'runFirstPhase' | 'runIrTask';
32
+ readonly resumeStateId: 'runFirstPhase' | 'runIrTask';
33
+ readonly sourceItem: 'CODE-1' | 'CODE-3';
34
+ readonly player: 'Coder';
35
+ readonly question: string;
51
36
  };
52
37
 
53
- export type CaptainInput = {
54
- stateId: ResumableStateId;
55
- sourceItem: string;
56
- prompt: string;
57
- result: Record<string, string>;
58
- // Optional structured fields the source item references but does not
59
- // expose as <placeholder> tokens in the verbatim prompt. Each state
60
- // populates only the fields its source item depends on; the Captain
61
- // resolves placeholder tokens (e.g. IR-<#>, <coder-llm>) and grounds
62
- // deictic phrasing ("below") from these.
63
- intent?: string;
64
- irNumber?: string;
65
- taskDescription?: string;
66
- reviews?: string;
67
- challenges?: string;
68
- coderPlayer?: string;
69
- reviewerPlayer?: string;
70
- // Routing-only: the configured Committer-alias player id (`coder` /
71
- // `reviewer`) threaded from
72
- // `captain.options.playbooks.code.options.committer` (PBRT-8 / PBRT-30).
73
- // It selects which host pane runs a `Committer`
74
- // commit; it is not a prompt-placeholder source, so it never affects
75
- // <coder-llm> / <reviewer-llm> substitution or any labelled block,
76
- // and `input.player` stays `Committer` (PLAYBOOK-3).
77
- committerPlayer?: string;
78
- pendingBossQuestion?: PendingBossQuestion;
79
- bossReply?: string;
38
+ export type PlayerInput = {
39
+ readonly stateId: 'runFirstPhase' | 'runIrTask';
40
+ readonly player: 'Coder';
41
+ readonly sourceItem: 'CODE-1' | 'CODE-3';
42
+ readonly prompt: string;
43
+ readonly result: Readonly<Record<string, string>>;
44
+ readonly callerInput: string;
45
+ readonly runResults: string;
46
+ readonly coderPlayer: string;
47
+ readonly irNumber?: string;
48
+ readonly irTask?: string;
49
+ readonly pendingBossQuestion?: PendingBossQuestion;
50
+ readonly bossReply?: string;
80
51
  };
81
52
 
82
- export type PlayerInput = CaptainInput & {
83
- player: Player;
84
- };
53
+ export type PlayerOutput =
54
+ | {
55
+ readonly guard: 'directCommit';
56
+ readonly coderOutput: string;
57
+ readonly latestCommit: string;
58
+ }
59
+ | {
60
+ readonly guard: 'irCommit';
61
+ readonly coderOutput: string;
62
+ readonly latestCommit: string;
63
+ readonly irNumber: string;
64
+ readonly irTask: string;
65
+ }
66
+ | {
67
+ readonly guard: 'moreTasks';
68
+ readonly coderOutput: string;
69
+ readonly latestCommit: string;
70
+ readonly irTask: string;
71
+ }
72
+ | {
73
+ readonly guard: 'finalTask';
74
+ readonly coderOutput: string;
75
+ readonly latestCommit: string;
76
+ }
77
+ | {
78
+ readonly guard: 'needsBossReply';
79
+ readonly question: string;
80
+ };
85
81
 
86
- export type CaptainOutput = {
87
- guard: string;
88
- irNumber?: string;
89
- taskDescription?: string;
90
- reviews?: string;
91
- challenges?: string;
92
- summary?: string;
93
- question?: string;
94
- [k: string]: unknown;
82
+ export type PlaybookInput = {
83
+ readonly stateId: 'reviewFirstCommit' | 'reviewIrTask';
84
+ readonly sourceItem: 'CODE-2' | 'CODE-4';
85
+ readonly playbookId: 'review';
86
+ readonly text: string;
95
87
  };
96
88
 
97
- export type PlayerOutput = CaptainOutput;
98
-
99
- export type CodingInput = {
100
- intent?: string;
101
- irNumber?: string;
102
- coderPlayer?: string;
103
- reviewerPlayer?: string;
104
- committerPlayer?: string;
89
+ export type ReviewOutput = {
90
+ readonly approvedCommit: 'latest';
91
+ readonly noUnsettledFindings: true;
105
92
  };
106
93
 
107
- export type CodingContext = CodingInput & {
108
- workflow?: WorkflowKind;
109
- changeOrigin?: ChangeOrigin;
110
- reviewSubject?: ReviewSubject;
111
- afterReview?: AfterReview;
112
- taskDescription?: string;
113
- reviews?: string;
114
- challenges?: string;
115
- lastResult?: CaptainOutput;
116
- lastError?: unknown;
117
- pendingBossQuestion?: PendingBossQuestion;
118
- bossReply?: string;
94
+ export type CompactError = {
95
+ readonly name: string;
96
+ readonly message: string;
119
97
  };
120
98
 
121
- export type CodingEvent =
122
- | { type: 'START_CODING'; intent: string }
123
- | { type: 'CONTINUE_IR'; irNumber: string }
124
- | { type: 'SUMMARIZE_IR'; irNumber: string }
99
+ export type CodePlaybookOutput =
125
100
  | {
126
- type: 'BOSS_INTERRUPT';
127
- targetId: JumpableStateId;
128
- intent?: string;
129
- irNumber?: string;
101
+ readonly status: 'complete';
102
+ /** Exact identity of the latest CODE-owned commit. */
103
+ readonly lastCodeCommit: string;
104
+ /** Exact Coder final text produced after that commit. */
105
+ readonly lastCodeOutput: string;
130
106
  }
131
107
  | {
132
- type: 'BOSS_REPLY';
133
- answer: string;
134
- questionId?: ResumableStateId;
108
+ readonly status: 'review-failed';
109
+ /** Exact identity of the latest CODE-owned commit. */
110
+ readonly lastCodeCommit: string;
111
+ /** Exact Coder final text produced after that commit. */
112
+ readonly lastCodeOutput: string;
113
+ readonly error: CompactError;
135
114
  };
136
115
 
137
- const jumpableStateIds = [
138
- 'ready',
139
- 'planAndImplement',
140
- 'respondToReview',
141
- 'continueIr',
142
- 'summarizeSpecs',
143
- 'reviewBossCommitSpecs',
144
- 'reviewBossCommitCode',
145
- 'reviewBossCommitMixed',
146
- 'reviewIrTaskCommitSpecs',
147
- 'reviewIrTaskCommitCode',
148
- 'reviewIrTaskCommitMixed',
149
- 'reviewChangesSpecs',
150
- 'reviewChangesCode',
151
- 'reviewChangesMixed',
152
- 'reviewChangesAndChallengesSpecs',
153
- 'reviewChangesAndChallengesCode',
154
- 'reviewChangesAndChallengesMixed',
155
- 'adjudicateChallenges',
156
- 'commitCoderInitial',
157
- 'commitJoint',
158
- 'failed',
159
- ] as const satisfies readonly JumpableStateId[];
160
-
161
- const resumableStateIds = [
162
- 'planAndImplement',
163
- 'respondToReview',
164
- 'continueIr',
165
- 'summarizeSpecs',
166
- 'reviewBossCommitSpecs',
167
- 'reviewBossCommitCode',
168
- 'reviewBossCommitMixed',
169
- 'reviewIrTaskCommitSpecs',
170
- 'reviewIrTaskCommitCode',
171
- 'reviewIrTaskCommitMixed',
172
- 'reviewChangesSpecs',
173
- 'reviewChangesCode',
174
- 'reviewChangesMixed',
175
- 'reviewChangesAndChallengesSpecs',
176
- 'reviewChangesAndChallengesCode',
177
- 'reviewChangesAndChallengesMixed',
178
- 'adjudicateChallenges',
179
- 'commitCoderInitial',
180
- 'commitJoint',
181
- ] as const satisfies readonly ResumableStateId[];
182
-
183
- const stateDescriptions = {
184
- ready: 'Idle hub: waits for Boss to start or resume a coding sub-procedure.',
185
- awaitBossReply: 'Waiting for Boss to answer a player question.',
186
- planAndImplement:
187
- 'CODE-1: Coder assesses a Boss intent and either implements a single-commit change or drafts an IR.',
188
- respondToReview: 'CODE-2: Coder addresses or challenges Reviewer findings.',
189
- continueIr:
190
- 'CODE-3: Coder continues an IR after the previous task or IR draft passed review.',
191
- summarizeSpecs:
192
- 'CODE-4: Coder summarizes a completed IR into minimal spec items.',
193
- reviewBossCommitSpecs:
194
- 'CODE-5: Reviewer reviews a Boss-intent commit whose changes are only in spec item files.',
195
- reviewBossCommitCode:
196
- 'CODE-6: Reviewer reviews a Boss-intent commit whose changes are only outside spec item files.',
197
- reviewBossCommitMixed:
198
- 'CODE-7: Reviewer reviews a Boss-intent commit whose changes span both spec item files and other files.',
199
- reviewIrTaskCommitSpecs:
200
- 'CODE-8: Reviewer reviews an IR-task commit whose changes are only in spec item files.',
201
- reviewIrTaskCommitCode:
202
- 'CODE-9: Reviewer reviews an IR-task commit whose changes are only outside spec item files.',
203
- reviewIrTaskCommitMixed:
204
- 'CODE-10: Reviewer reviews an IR-task commit whose changes span both spec item files and other files.',
205
- reviewChangesSpecs:
206
- 'CODE-11: Reviewer reviews uncommitted Coder changes that touch only spec item files with no accompanying rebuttals.',
207
- reviewChangesCode:
208
- 'CODE-12: Reviewer reviews uncommitted Coder changes that touch only files outside spec item files with no accompanying rebuttals.',
209
- reviewChangesMixed:
210
- 'CODE-13: Reviewer reviews uncommitted Coder changes that touch both spec item files and other files with no accompanying rebuttals.',
211
- reviewChangesAndChallengesSpecs:
212
- 'CODE-15: Reviewer reviews uncommitted Coder changes that touch only spec item files and adjudicates accompanying rebuttals in one round.',
213
- reviewChangesAndChallengesCode:
214
- 'CODE-16: Reviewer reviews uncommitted Coder changes that touch only files outside spec item files and adjudicates accompanying rebuttals in one round.',
215
- reviewChangesAndChallengesMixed:
216
- 'CODE-17: Reviewer reviews uncommitted Coder changes that touch both spec item files and other files and adjudicates accompanying rebuttals in one round.',
217
- adjudicateChallenges:
218
- 'CODE-14: Reviewer adjudicates Coder rebuttals against the prior review when Coder produced no code edits this round.',
219
- commitCoderInitial:
220
- 'CODE-18: Committer commits Coder Initial Changes when Reviewer has not played since the last commit.',
221
- commitJoint:
222
- 'CODE-19: Committer commits changes when both Coder and Reviewer have played since the last commit.',
223
- failed:
224
- 'Captures the last Captain error so the runner can report it. Boss may resume from here.',
225
- done: 'The selected coding workflow has completed.',
226
- } as const satisfies Record<
227
- JumpableStateId | 'awaitBossReply' | 'done',
228
- string
229
- >;
230
-
231
- const playbookMeta = <StateId extends keyof typeof stateDescriptions>(
232
- stateId: StateId,
233
- ) => ({
234
- playbook: {
235
- stateId,
236
- description: stateDescriptions[stateId],
237
- },
238
- });
239
-
240
- const outputOf = (event: unknown): CaptainOutput | undefined =>
241
- (event as { output?: CaptainOutput }).output;
242
-
243
- const guardIs =
244
- (guard: string) =>
245
- ({ event }: { event: unknown }) =>
246
- outputOf(event)?.guard === guard;
247
-
248
- const guardAndOrigin =
249
- (guard: string, origin: ChangeOrigin) =>
250
- ({ context, event }: { context: CodingContext; event: unknown }) =>
251
- outputOf(event)?.guard === guard && context.changeOrigin === origin;
252
-
253
- const acceptedAfter =
254
- (subject: ReviewSubject) =>
255
- ({ context, event }: { context: CodingContext; event: unknown }) =>
256
- outputOf(event)?.guard === 'accepted' && context.reviewSubject === subject;
257
-
258
- const noFindingsAfter =
259
- (afterReview: AfterReview) =>
260
- ({ context, event }: { context: CodingContext; event: unknown }) =>
261
- outputOf(event)?.guard === 'noFindings' &&
262
- context.afterReview === afterReview;
263
-
264
- const rememberCaptainOutput = assign({
265
- lastResult: ({ event }: { event: unknown }) => outputOf(event),
266
- lastError: () => undefined,
267
- irNumber: ({ context, event }: { context: CodingContext; event: unknown }) =>
268
- outputOf(event)?.irNumber ?? context.irNumber,
269
- taskDescription: ({
270
- context,
271
- event,
272
- }: {
273
- context: CodingContext;
274
- event: unknown;
275
- }) => outputOf(event)?.taskDescription ?? context.taskDescription,
276
- reviews: ({ context, event }: { context: CodingContext; event: unknown }) =>
277
- outputOf(event)?.reviews ?? context.reviews,
278
- challenges: ({
279
- context,
280
- event,
281
- }: {
282
- context: CodingContext;
283
- event: unknown;
284
- }) => outputOf(event)?.challenges ?? context.challenges,
285
- });
286
-
287
- const rememberCaptainError = assign({
288
- lastError: ({ event }: { event: unknown }) =>
289
- (event as { error?: unknown }).error,
290
- });
291
-
292
- const rememberEmptyBossReplyError = assign({
293
- lastError: () => new Error('BOSS_REPLY received empty answer'),
294
- });
295
-
296
- const rememberBossInput = assign({
297
- intent: ({ context, event }: { context: CodingContext; event: unknown }) =>
298
- (event as { intent?: string }).intent ?? context.intent,
299
- irNumber: ({ context, event }: { context: CodingContext; event: unknown }) =>
300
- (event as { irNumber?: string }).irNumber ?? context.irNumber,
301
- });
302
-
303
- type PlayerInputFactory = (context: Partial<CodingContext>) => PlayerInput;
304
-
305
- const needsBossReplyDescription =
306
- "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>`.";
307
-
308
- const bossReplyInputFields = (context: Partial<CodingContext>) => ({
309
- pendingBossQuestion: context.pendingBossQuestion,
310
- bossReply: context.bossReply,
311
- });
312
-
313
- const withNeedsBossReply = <T extends Record<string, string>>(result: T) => ({
314
- ...result,
315
- needsBossReply: needsBossReplyDescription,
316
- });
317
-
318
- const captainStateMetadata = {
319
- planAndImplement: { sourceItem: 'CODE-1', player: 'Coder' },
320
- respondToReview: { sourceItem: 'CODE-2', player: 'Coder' },
321
- continueIr: { sourceItem: 'CODE-3', player: 'Coder' },
322
- summarizeSpecs: { sourceItem: 'CODE-4', player: 'Coder' },
323
- reviewBossCommitSpecs: { sourceItem: 'CODE-5', player: 'Reviewer' },
324
- reviewBossCommitCode: { sourceItem: 'CODE-6', player: 'Reviewer' },
325
- reviewBossCommitMixed: { sourceItem: 'CODE-7', player: 'Reviewer' },
326
- reviewIrTaskCommitSpecs: { sourceItem: 'CODE-8', player: 'Reviewer' },
327
- reviewIrTaskCommitCode: { sourceItem: 'CODE-9', player: 'Reviewer' },
328
- reviewIrTaskCommitMixed: { sourceItem: 'CODE-10', player: 'Reviewer' },
329
- reviewChangesSpecs: { sourceItem: 'CODE-11', player: 'Reviewer' },
330
- reviewChangesCode: { sourceItem: 'CODE-12', player: 'Reviewer' },
331
- reviewChangesMixed: { sourceItem: 'CODE-13', player: 'Reviewer' },
332
- reviewChangesAndChallengesSpecs: {
333
- sourceItem: 'CODE-15',
334
- player: 'Reviewer',
335
- },
336
- reviewChangesAndChallengesCode: { sourceItem: 'CODE-16', player: 'Reviewer' },
337
- reviewChangesAndChallengesMixed: {
338
- sourceItem: 'CODE-17',
339
- player: 'Reviewer',
340
- },
341
- adjudicateChallenges: { sourceItem: 'CODE-14', player: 'Reviewer' },
342
- commitCoderInitial: { sourceItem: 'CODE-18', player: 'Committer' },
343
- commitJoint: { sourceItem: 'CODE-19', player: 'Committer' },
344
- } as const satisfies Record<
345
- ResumableStateId,
346
- { sourceItem: string; player: Player }
347
- >;
348
-
349
- const planAndImplementInput: PlayerInputFactory = (context) => ({
350
- stateId: 'planAndImplement',
351
- player: 'Coder',
352
- sourceItem: 'CODE-1',
353
- intent: context.intent,
354
- ...bossReplyInputFields(context),
355
- prompt: [
356
- 'Assess whether this can be completed in a single commit, following best practices.',
357
- 'If yes, implement and test, updating both code and specs; otherwise, decompose into tasks as a new IR under @specs/intents (or @specs/iterations in older scaffolds) and stop without implementing any IR task.',
358
- 'For context discovery, @specs/map.md indexes all spec files and @specs/meta.md describes the spec format.',
359
- 'Ensure @specs/map.md reflects the changes.',
360
- 'Do not commit.',
361
- ].join('\n'),
362
- result: withNeedsBossReply({
363
- singleCommitReady:
364
- 'Coder produced uncommitted single-commit changes (Initial Changes).',
365
- irDrafted:
366
- 'Coder decomposed the intent into a new IR and drafted it uncommitted (Initial Changes).',
367
- }),
368
- });
369
-
370
- const continueIrInput: PlayerInputFactory = (context) => ({
371
- stateId: 'continueIr',
372
- player: 'Coder',
373
- sourceItem: 'CODE-3',
374
- irNumber: context.irNumber,
375
- ...bossReplyInputFields(context),
376
- prompt: [
377
- 'Continue to implement IR-<#> if not all deliverables and tasks are done.',
378
- 'Implement one task at a time (including corresponding tests if any).',
379
- 'Stop after each task for review — do not commit yet.',
380
- 'If relevant, mark progress in the IR.',
381
- ].join('\n'),
382
- result: withNeedsBossReply({
383
- taskReady:
384
- 'Coder produced uncommitted changes for the next IR task (Initial Changes). Output shall include `taskDescription: <one-line description of the task just implemented>`.',
385
- iterationDone: 'All IR deliverables and tasks are done.',
386
- }),
387
- });
388
-
389
- const summarizeSpecsInput: PlayerInputFactory = (context) => ({
390
- stateId: 'summarizeSpecs',
391
- player: 'Coder',
392
- sourceItem: 'CODE-4',
393
- irNumber: context.irNumber,
394
- ...bossReplyInputFields(context),
395
- prompt: [
396
- 'Read IR-<#> and corresponding commits.',
397
- 'According to @specs/meta.md, add or update spec items to fully capture:',
398
- '',
399
- '- the external behavior users rely on,',
400
- '- the internal system behavior, and',
401
- '- the integration/system test cases.',
402
- '',
403
- 'The spec items should be the *minimal* set needed to reimplement code without the IR.',
404
- 'The set should be complete and coherent.',
405
- 'Avoid implementation specifics.',
406
- 'Avoid redundant spec items.',
407
- 'Ensure @specs/map.md reflects the changes.',
408
- ].join('\n'),
409
- result: withNeedsBossReply({
410
- specsReady: 'Coder produced uncommitted spec updates (Initial Changes).',
411
- noSpecChanges: 'Existing specs already capture the realized intent.',
412
- }),
413
- });
414
-
415
- const setPendingBossQuestion = (resumeStateId: ResumableStateId) =>
416
- assign({
417
- pendingBossQuestion: ({
418
- context,
419
- event,
420
- }: {
421
- context: CodingContext;
422
- event: unknown;
423
- }) => {
424
- const input = captainStateMetadata[resumeStateId];
425
- return {
426
- questionId: resumeStateId,
427
- resumeStateId,
428
- sourceItem: input.sourceItem,
429
- player: input.player,
430
- question: outputOf(event)?.question ?? '',
431
- };
432
- },
433
- bossReply: () => undefined,
434
- });
435
-
436
- const rememberBossReply = assign({
437
- bossReply: ({ event }: { event: unknown }) =>
438
- (event as { answer?: string }).answer ?? '',
439
- });
440
-
441
- const clearBossReplyContext = assign({
442
- pendingBossQuestion: () => undefined,
443
- bossReply: () => undefined,
444
- });
445
-
446
- const actionsArray = (actions: unknown) =>
447
- actions === undefined ? [] : Array.isArray(actions) ? actions : [actions];
448
-
449
- const withClearBossReplyContext = <T extends { actions?: unknown }>(
450
- transition: T,
451
- ): T => ({
452
- ...transition,
453
- actions: [clearBossReplyContext, ...actionsArray(transition.actions)],
454
- });
455
-
456
- const bossInterrupts = (
457
- ids: readonly JumpableStateId[],
458
- extraActions: readonly unknown[] = [],
459
- ) =>
460
- ids.map((id) => ({
461
- target: `#${id}` as const,
462
- guard: ({ event }: { event: CodingEvent }) =>
463
- event.type === 'BOSS_INTERRUPT' && event.targetId === id,
464
- reenter: true,
465
- actions: [...extraActions, rememberBossInput],
466
- }));
467
-
468
- const resumableStates = (ids: readonly ResumableStateId[]) =>
469
- ids.map((id) => ({
470
- target: `#${id}` as const,
471
- guard: ({
472
- context,
473
- event,
474
- }: {
475
- context: CodingContext;
476
- event: CodingEvent;
477
- }) =>
478
- event.type === 'BOSS_REPLY' &&
479
- context.pendingBossQuestion?.resumeStateId === id &&
480
- (event.questionId === undefined ||
481
- event.questionId === context.pendingBossQuestion.questionId),
482
- reenter: true,
483
- actions: rememberBossReply,
484
- }));
485
-
486
- const bossReplyIsEmpty = ({ event }: { event: CodingEvent }) =>
487
- event.type === 'BOSS_REPLY' && event.answer.trim() === '';
488
-
489
- const withNeedsBossReplyTransition = <
490
- T extends readonly { guard?: unknown; target?: unknown; actions?: unknown }[],
491
- >(
492
- resumeStateId: ResumableStateId,
493
- transitions: T,
494
- ) => [
495
- ...transitions.map((transition) => withClearBossReplyContext(transition)),
496
- {
497
- guard: guardIs('needsBossReply'),
498
- target: '#awaitBossReply',
499
- actions: [rememberCaptainOutput, setPendingBossQuestion(resumeStateId)],
500
- },
501
- ];
502
-
503
- const captainError = {
504
- target: '#failed',
505
- actions: rememberCaptainError,
116
+ export type CodingInput = {
117
+ readonly coderPlayer?: string;
118
+ readonly runResults?: string;
506
119
  };
507
120
 
508
- const readyEvents = {
509
- START_CODING: {
510
- target: '#planAndImplement',
511
- actions: [
512
- rememberBossInput,
513
- assign({
514
- workflow: () => 'singleCommit' as const,
515
- changeOrigin: () => 'bossIntent' as const,
516
- afterReview: () => 'done' as const,
517
- }),
518
- ],
519
- },
520
- CONTINUE_IR: {
521
- target: '#continueIr',
522
- actions: [
523
- rememberBossInput,
524
- assign({
525
- workflow: () => 'iteration' as const,
526
- changeOrigin: () => 'irTask' as const,
527
- afterReview: () => 'continueIr' as const,
528
- }),
529
- ],
530
- },
531
- SUMMARIZE_IR: {
532
- target: '#summarizeSpecs',
533
- actions: [
534
- rememberBossInput,
535
- assign({
536
- workflow: () => 'specSummary' as const,
537
- changeOrigin: () => 'irTask' as const,
538
- afterReview: () => 'done' as const,
539
- }),
540
- ],
541
- },
121
+ export type CodingContext = {
122
+ readonly coderPlayer: string;
123
+ readonly runResults: string;
124
+ readonly callerInput?: string;
125
+ readonly coderOutput?: string;
126
+ readonly latestCommit?: string;
127
+ readonly irNumber?: string;
128
+ readonly irTask?: string;
129
+ readonly nextIrTask?: string;
130
+ readonly phase?: CodePhase;
131
+ readonly completion?: 'complete' | 'review-failed';
132
+ readonly reviewError?: CompactError;
133
+ readonly lastError?: unknown;
134
+ readonly pendingBossQuestion?: PendingBossQuestion;
135
+ readonly bossReply?: string;
542
136
  };
543
137
 
544
- const awaitBossReplyEvents = {
545
- START_CODING: withClearBossReplyContext(readyEvents.START_CODING),
546
- CONTINUE_IR: withClearBossReplyContext(readyEvents.CONTINUE_IR),
547
- SUMMARIZE_IR: withClearBossReplyContext(readyEvents.SUMMARIZE_IR),
548
- BOSS_INTERRUPT: bossInterrupts(jumpableStateIds, [clearBossReplyContext]),
549
- BOSS_REPLY: [
550
- {
551
- guard: bossReplyIsEmpty,
552
- target: '#failed',
553
- actions: [clearBossReplyContext, rememberEmptyBossReplyError],
138
+ export type CodingEvent =
139
+ | { readonly type: 'START_CODE'; readonly callerInput: string }
140
+ | {
141
+ readonly type: 'BOSS_REPLY';
142
+ readonly answer: string;
143
+ readonly questionId?: 'runFirstPhase' | 'runIrTask';
144
+ };
145
+
146
+ const FIRST_PHASE_PROMPT = [
147
+ '> <caller-input>',
148
+ '> <run-results>',
149
+ '',
150
+ 'Assess whether the coding intent can be completed well in one commit.',
151
+ 'If yes, implement and test it, update the affected specs, and ensure @specs/map.md remains accurate.',
152
+ 'Otherwise, decompose it into tasks sized to exactly one commit each, add a new IR under @specs/intents, and do not implement any IR task in this phase.',
153
+ 'Plan affected spec updates before, with, or after their corresponding code changes, either as standalone IR tasks or as explicit work within related tasks.',
154
+ '',
155
+ 'Consult @specs/map.md for relevant context and @specs/meta.md for spec requirements, if needed.',
156
+ '',
157
+ 'Do not re-run tests or builds whose inputs have not changed since any previous reported run.',
158
+ "Make the phase's minimal changes and then one new commit, following @specs/packages/git.md; never amend an existing commit.",
159
+ 'Make the commit message explain concisely what changed and why, including relevant verification.',
160
+ 'Report it as exactly one final-response line beginning `Commit: `, followed only by the exact commit identity.',
161
+ 'Coder is <coder-llm>; format the model token in conventional human form.',
162
+ ].join('\n');
163
+
164
+ const IR_TASK_PROMPT = [
165
+ '> <ir-task>',
166
+ '> <run-results>',
167
+ '',
168
+ 'Read IR-<#> and implement exactly the next unfinished task, including corresponding tests or specs if any.',
169
+ 'Do not implement a later task in this phase.',
170
+ "Mark the IR's progress and deliverables when relevant.",
171
+ 'If the IR will be finished after this phase, double-check that all acceptance criteria are met.',
172
+ '',
173
+ 'Do not re-run tests or builds whose inputs have not changed since any previous reported run.',
174
+ "Make the phase's minimal changes and then one new commit, following @specs/packages/git.md; never amend an existing commit.",
175
+ 'Make the commit message explain concisely what changed and why, including relevant verification.',
176
+ 'Report it as exactly one final-response line beginning `Commit: `, followed only by the exact commit identity.',
177
+ 'Coder is <coder-llm>; format the model token in conventional human form.',
178
+ ].join('\n');
179
+
180
+ const FIRST_PHASE_RESULTS = {
181
+ directCommit:
182
+ 'Coder completed and committed the direct implementation phase. Output shall include `coderOutput: <verbatim final text>` and `latestCommit: <commit identity>`.',
183
+ irCommit:
184
+ 'Coder created and committed a new IR without implementing an IR task. Output shall include `coderOutput: <verbatim final text>`, `latestCommit: <commit identity>`, `irNumber`, and `irTask` naming the exact next unfinished task.',
185
+ needsBossReply:
186
+ "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>`.",
187
+ } as const;
188
+
189
+ const IR_TASK_RESULTS = {
190
+ moreTasks:
191
+ 'Coder completed and committed the current IR task and at least one task remains. Output shall include `coderOutput: <verbatim final text>`, `latestCommit: <commit identity>`, and `irTask` naming the exact next unfinished task.',
192
+ finalTask:
193
+ 'Coder completed and committed the final IR task. Output shall include `coderOutput: <verbatim final text>` and `latestCommit: <commit identity>`.',
194
+ needsBossReply:
195
+ "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>`.",
196
+ } as const;
197
+
198
+ const STATE_DESCRIPTIONS = {
199
+ ready: 'Waiting for a coding intent.',
200
+ runFirstPhase:
201
+ 'Coder is implementing one direct phase or committing a new intent record.',
202
+ reviewFirstCommit:
203
+ 'The REVIEW playbook is checking the first phase commit.',
204
+ runIrTask: 'Coder is implementing exactly one unfinished intent-record task.',
205
+ reviewIrTask: 'The REVIEW playbook is checking the latest task commit.',
206
+ awaitBossReply: 'Waiting for Boss to answer Coder.',
207
+ failed: 'The coding workflow failed and is waiting for a new coding intent.',
208
+ done: 'The coding workflow completed after REVIEW found no unsettled findings.',
209
+ } as const;
210
+
211
+ function playbookMeta<StateId extends keyof typeof STATE_DESCRIPTIONS>(
212
+ stateId: StateId,
213
+ player?: 'Coder',
214
+ ) {
215
+ return {
216
+ playbook: {
217
+ stateId,
218
+ description: STATE_DESCRIPTIONS[stateId],
219
+ ...(player === undefined ? {} : { player }),
554
220
  },
555
- ...resumableStates(resumableStateIds),
556
- ],
221
+ };
222
+ }
223
+
224
+ function isRecord(value: unknown): value is Record<string, unknown> {
225
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
226
+ return false;
227
+ }
228
+ const prototype = Object.getPrototypeOf(value);
229
+ return prototype === Object.prototype || prototype === null;
230
+ }
231
+
232
+ function isNonEmptyString(value: unknown): value is string {
233
+ return typeof value === 'string' && value.trim().length > 0;
234
+ }
235
+
236
+ function playerOutput(event: unknown): Record<string, unknown> | undefined {
237
+ if (!isRecord(event)) return undefined;
238
+ return isRecord(event.output) ? event.output : undefined;
239
+ }
240
+
241
+ function outputGuard(event: unknown, guard: PlayerOutput['guard']): boolean {
242
+ return playerOutput(event)?.guard === guard;
243
+ }
244
+
245
+ function outputString(event: unknown, field: string): string | undefined {
246
+ const value = playerOutput(event)?.[field];
247
+ return isNonEmptyString(value) ? value : undefined;
248
+ }
249
+
250
+ function commitOutput(
251
+ event: unknown,
252
+ ): { readonly coderOutput: string; readonly latestCommit: string } | undefined {
253
+ const coderOutput = outputString(event, 'coderOutput');
254
+ const latestCommit = outputString(event, 'latestCommit');
255
+ const commitLines = coderOutput
256
+ ?.split('\n')
257
+ .filter((line) => line.startsWith('Commit: '));
258
+ if (
259
+ coderOutput === undefined ||
260
+ latestCommit === undefined ||
261
+ commitLines?.length !== 1 ||
262
+ commitLines[0] !== `Commit: ${latestCommit}`
263
+ ) {
264
+ return undefined;
265
+ }
266
+ return { coderOutput, latestCommit };
267
+ }
268
+
269
+ function isDirectCommit({ event }: { event: unknown }): boolean {
270
+ return (
271
+ outputGuard(event, 'directCommit') &&
272
+ commitOutput(event) !== undefined
273
+ );
274
+ }
275
+
276
+ function isIrCommit({ event }: { event: unknown }): boolean {
277
+ return (
278
+ outputGuard(event, 'irCommit') &&
279
+ commitOutput(event) !== undefined &&
280
+ outputString(event, 'irNumber') !== undefined &&
281
+ outputString(event, 'irTask') !== undefined
282
+ );
283
+ }
284
+
285
+ function isMoreTasks({ event }: { event: unknown }): boolean {
286
+ return (
287
+ outputGuard(event, 'moreTasks') &&
288
+ commitOutput(event) !== undefined &&
289
+ outputString(event, 'irTask') !== undefined
290
+ );
291
+ }
292
+
293
+ function isFinalTask({ event }: { event: unknown }): boolean {
294
+ return (
295
+ outputGuard(event, 'finalTask') &&
296
+ commitOutput(event) !== undefined
297
+ );
298
+ }
299
+
300
+ function needsBossReply({ event }: { event: unknown }): boolean {
301
+ return (
302
+ outputGuard(event, 'needsBossReply') &&
303
+ outputString(event, 'question') !== undefined
304
+ );
305
+ }
306
+
307
+ function reviewOutput(event: unknown): unknown {
308
+ return isRecord(event) ? event.output : undefined;
309
+ }
310
+
311
+ function isReviewApprovedValue(value: unknown): value is ReviewOutput {
312
+ return (
313
+ isRecord(value) &&
314
+ Object.keys(value).sort().join('\0') ===
315
+ ['approvedCommit', 'noUnsettledFindings'].sort().join('\0') &&
316
+ value.approvedCommit === 'latest' &&
317
+ value.noUnsettledFindings === true
318
+ );
319
+ }
320
+
321
+ function reviewApprovedFor(
322
+ phase: CodePhase,
323
+ ): (args: { context: CodingContext; event: unknown }) => boolean {
324
+ return ({ context, event }) =>
325
+ context.phase === phase && isReviewApprovedValue(reviewOutput(event));
326
+ }
327
+
328
+ function isStateValue(value: unknown, ancestors = new Set<object>()): boolean {
329
+ if (typeof value === 'string') return true;
330
+ if (!isRecord(value) || ancestors.has(value)) return false;
331
+ const next = new Set(ancestors).add(value);
332
+ return Reflect.ownKeys(value).every(
333
+ (key) =>
334
+ typeof key === 'string' &&
335
+ Object.prototype.propertyIsEnumerable.call(value, key) &&
336
+ isStateValue(value[key], next),
337
+ );
338
+ }
339
+
340
+ function isStringArray(value: unknown): value is readonly string[] {
341
+ return (
342
+ Array.isArray(value) &&
343
+ Reflect.ownKeys(value).length === value.length + 1 &&
344
+ Reflect.ownKeys(value).every((key) => {
345
+ if (key === 'length') return true;
346
+ return (
347
+ typeof key === 'string' &&
348
+ /^(0|[1-9][0-9]*)$/.test(key) &&
349
+ Number(key) < value.length
350
+ );
351
+ }) &&
352
+ value.every((entry) => isNonEmptyString(entry)) &&
353
+ new Set(value).size === value.length
354
+ );
355
+ }
356
+
357
+ function isPlaybookState(value: unknown): boolean {
358
+ if (!isRecord(value)) return false;
359
+ const allowed = new Set([
360
+ 'value',
361
+ 'activeStateIds',
362
+ 'tags',
363
+ 'status',
364
+ 'quiescent',
365
+ 'stateId',
366
+ ]);
367
+ if (
368
+ Reflect.ownKeys(value).some(
369
+ (key) => typeof key !== 'string' || !allowed.has(key),
370
+ ) ||
371
+ !Object.prototype.hasOwnProperty.call(value, 'value') ||
372
+ !Object.prototype.hasOwnProperty.call(value, 'activeStateIds') ||
373
+ !Object.prototype.hasOwnProperty.call(value, 'tags') ||
374
+ !Object.prototype.hasOwnProperty.call(value, 'status') ||
375
+ !Object.prototype.hasOwnProperty.call(value, 'quiescent')
376
+ ) {
377
+ return false;
378
+ }
379
+ return (
380
+ isStateValue(value.value) &&
381
+ isStringArray(value.activeStateIds) &&
382
+ isStringArray(value.tags) &&
383
+ (value.status === 'active' ||
384
+ value.status === 'done' ||
385
+ value.status === 'error' ||
386
+ value.status === 'stopped') &&
387
+ typeof value.quiescent === 'boolean' &&
388
+ (!Object.prototype.hasOwnProperty.call(value, 'stateId') ||
389
+ (isNonEmptyString(value.stateId) &&
390
+ value.activeStateIds.length === 1 &&
391
+ value.activeStateIds[0] === value.stateId))
392
+ );
393
+ }
394
+
395
+ function nestedResultFromError(error: unknown): Record<string, unknown> | undefined {
396
+ if (!(error instanceof Error)) return undefined;
397
+ const result = (error as Error & { readonly result?: unknown }).result;
398
+ return isRecord(result) ? result : undefined;
399
+ }
400
+
401
+ type AuthoredReviewFailure = {
402
+ readonly status: 'aborted' | 'error';
403
+ readonly error?: CompactError;
557
404
  };
558
405
 
559
- const playerPlaceholder = fromPromise<PlayerOutput, PlayerInput>(async () => {
560
- throw new Error('player actor must be provided by the runner');
561
- });
562
-
563
- export const codingMachine = setup({
406
+ function normalizedReviewFailure(
407
+ error: unknown,
408
+ ): AuthoredReviewFailure | undefined {
409
+ const result = nestedResultFromError(error);
410
+ if (result === undefined) return undefined;
411
+ const allowed = new Set([
412
+ 'status',
413
+ 'playbookId',
414
+ 'childSessionId',
415
+ 'state',
416
+ 'error',
417
+ ]);
418
+ if (
419
+ Reflect.ownKeys(result).some(
420
+ (key) => typeof key !== 'string' || !allowed.has(key),
421
+ ) ||
422
+ (result.status !== 'aborted' && result.status !== 'error') ||
423
+ result.playbookId !== 'review'
424
+ ) {
425
+ return undefined;
426
+ }
427
+ if (
428
+ Object.prototype.hasOwnProperty.call(result, 'childSessionId') &&
429
+ !isNonEmptyString(result.childSessionId)
430
+ ) {
431
+ return undefined;
432
+ }
433
+ if (
434
+ Object.prototype.hasOwnProperty.call(result, 'state') &&
435
+ !isPlaybookState(result.state)
436
+ ) {
437
+ return undefined;
438
+ }
439
+ let normalizedError: CompactError | undefined;
440
+ if (Object.prototype.hasOwnProperty.call(result, 'error')) {
441
+ if (!isRecord(result.error)) return undefined;
442
+ const errorKeys = Reflect.ownKeys(result.error);
443
+ if (
444
+ errorKeys.some(
445
+ (key) =>
446
+ typeof key !== 'string' ||
447
+ (key !== 'name' && key !== 'message' && key !== 'stack'),
448
+ ) ||
449
+ !Object.prototype.hasOwnProperty.call(result.error, 'name') ||
450
+ !Object.prototype.hasOwnProperty.call(result.error, 'message') ||
451
+ !isNonEmptyString(result.error.name) ||
452
+ typeof result.error.message !== 'string' ||
453
+ (Object.prototype.hasOwnProperty.call(result.error, 'stack') &&
454
+ typeof result.error.stack !== 'string')
455
+ ) {
456
+ return undefined;
457
+ }
458
+ normalizedError = {
459
+ name: result.error.name,
460
+ message: result.error.message,
461
+ };
462
+ } else if (result.status === 'error') {
463
+ return undefined;
464
+ }
465
+ return {
466
+ status: result.status,
467
+ ...(normalizedError === undefined ? {} : { error: normalizedError }),
468
+ };
469
+ }
470
+
471
+ function authoredReviewFailure({ event }: { event: unknown }): boolean {
472
+ const error = isRecord(event) ? event.error : undefined;
473
+ return normalizedReviewFailure(error) !== undefined;
474
+ }
475
+
476
+ function compactError(value: unknown): CompactError {
477
+ if (value instanceof Error) {
478
+ return {
479
+ name: value.name || 'Error',
480
+ message: value.message,
481
+ };
482
+ }
483
+ return { name: 'Error', message: String(value) };
484
+ }
485
+
486
+ function authoredReviewError(event: unknown): CompactError {
487
+ const outer = isRecord(event) ? event.error : undefined;
488
+ const failure = normalizedReviewFailure(outer);
489
+ if (failure?.error !== undefined) return failure.error;
490
+ if (failure?.status === 'aborted') {
491
+ return { name: 'AbortError', message: 'REVIEW was aborted.' };
492
+ }
493
+ return compactError(outer);
494
+ }
495
+
496
+ function quoteRelay(label: string, value: string): string {
497
+ const lines = value.split('\n');
498
+ return [`> ${label}: ${lines[0] ?? ''}`, ...lines.slice(1).map((line) => `> ${line}`)].join(
499
+ '\n',
500
+ );
501
+ }
502
+
503
+ function initialReviewText(context: CodingContext): string {
504
+ return [
505
+ quoteRelay('Initial intent', context.callerInput ?? ''),
506
+ quoteRelay('Coder output', context.coderOutput ?? ''),
507
+ ].join('\n');
508
+ }
509
+
510
+ function irTaskReviewText(context: CodingContext): string {
511
+ return [
512
+ quoteRelay('IR task', context.irTask ?? ''),
513
+ quoteRelay('Coder output', context.coderOutput ?? ''),
514
+ ].join('\n');
515
+ }
516
+
517
+ const machineSetup = setup({
564
518
  types: {} as {
565
519
  context: CodingContext;
566
520
  events: CodingEvent;
567
- input: CodingInput | undefined;
521
+ input: CodingInput;
522
+ output: CodePlaybookOutput;
568
523
  },
569
524
  actors: {
570
- player: playerPlaceholder,
525
+ player: fromPromise<PlayerOutput, PlayerInput>(async () => {
526
+ throw new Error('player actor must be provided by the runner');
527
+ }),
528
+ playbook: fromPromise<JsonValue | undefined, PlaybookInput>(async () => {
529
+ throw new Error('playbook actor must be provided by the runner');
530
+ }),
571
531
  },
572
- }).createMachine({
573
- id: 'coding',
532
+ guards: {
533
+ isDirectCommit,
534
+ isIrCommit,
535
+ isMoreTasks,
536
+ isFinalTask,
537
+ needsBossReply,
538
+ reviewApprovedDirect: reviewApprovedFor('direct'),
539
+ reviewApprovedIrCreated: reviewApprovedFor('ir-created'),
540
+ reviewApprovedMoreTasks: reviewApprovedFor('ir-task-more'),
541
+ reviewApprovedFinalTask: reviewApprovedFor('ir-task-final'),
542
+ authoredReviewFailure,
543
+ emptyBossReply: ({ event }) =>
544
+ event.type === 'BOSS_REPLY' && event.answer.trim().length === 0,
545
+ resumesFirstPhase: ({ context, event }) =>
546
+ event.type === 'BOSS_REPLY' &&
547
+ event.answer.trim().length > 0 &&
548
+ context.pendingBossQuestion?.resumeStateId === 'runFirstPhase' &&
549
+ (event.questionId === undefined || event.questionId === 'runFirstPhase'),
550
+ resumesIrTask: ({ context, event }) =>
551
+ event.type === 'BOSS_REPLY' &&
552
+ event.answer.trim().length > 0 &&
553
+ context.pendingBossQuestion?.resumeStateId === 'runIrTask' &&
554
+ (event.questionId === undefined || event.questionId === 'runIrTask'),
555
+ },
556
+ actions: {
557
+ startCoding: assign(({ context, event }) => {
558
+ if (event.type !== 'START_CODE') return {};
559
+ return {
560
+ coderPlayer: context.coderPlayer,
561
+ runResults: context.runResults,
562
+ callerInput: event.callerInput,
563
+ coderOutput: undefined,
564
+ latestCommit: undefined,
565
+ irNumber: undefined,
566
+ irTask: undefined,
567
+ nextIrTask: undefined,
568
+ phase: undefined,
569
+ completion: undefined,
570
+ reviewError: undefined,
571
+ lastError: undefined,
572
+ pendingBossQuestion: undefined,
573
+ bossReply: undefined,
574
+ };
575
+ }),
576
+ rememberDirectCommit: assign(({ event }) => ({
577
+ coderOutput: outputString(event, 'coderOutput'),
578
+ latestCommit: outputString(event, 'latestCommit'),
579
+ phase: 'direct' as const,
580
+ pendingBossQuestion: undefined,
581
+ bossReply: undefined,
582
+ })),
583
+ rememberIrCommit: assign(({ event }) => ({
584
+ coderOutput: outputString(event, 'coderOutput'),
585
+ latestCommit: outputString(event, 'latestCommit'),
586
+ irNumber: outputString(event, 'irNumber'),
587
+ irTask: outputString(event, 'irTask'),
588
+ nextIrTask: undefined,
589
+ phase: 'ir-created' as const,
590
+ pendingBossQuestion: undefined,
591
+ bossReply: undefined,
592
+ })),
593
+ rememberMoreTasks: assign(({ event }) => ({
594
+ coderOutput: outputString(event, 'coderOutput'),
595
+ latestCommit: outputString(event, 'latestCommit'),
596
+ nextIrTask: outputString(event, 'irTask'),
597
+ phase: 'ir-task-more' as const,
598
+ pendingBossQuestion: undefined,
599
+ bossReply: undefined,
600
+ })),
601
+ rememberFinalTask: assign(({ event }) => ({
602
+ coderOutput: outputString(event, 'coderOutput'),
603
+ latestCommit: outputString(event, 'latestCommit'),
604
+ nextIrTask: undefined,
605
+ phase: 'ir-task-final' as const,
606
+ pendingBossQuestion: undefined,
607
+ bossReply: undefined,
608
+ })),
609
+ advanceToNextIrTask: assign(({ context }) => ({
610
+ irTask: context.nextIrTask,
611
+ nextIrTask: undefined,
612
+ })),
613
+ rememberPendingQuestion: assign(({ context, event }) => {
614
+ const question = outputString(event, 'question');
615
+ const output = playerOutput(event);
616
+ const stateId =
617
+ context.irNumber === undefined ? 'runFirstPhase' : 'runIrTask';
618
+ if (question === undefined || output?.guard !== 'needsBossReply') {
619
+ return { lastError: new Error('Coder question output was malformed') };
620
+ }
621
+ return {
622
+ pendingBossQuestion: {
623
+ questionId: stateId,
624
+ resumeStateId: stateId,
625
+ sourceItem: stateId === 'runFirstPhase' ? 'CODE-1' : 'CODE-3',
626
+ player: 'Coder',
627
+ question,
628
+ },
629
+ bossReply: undefined,
630
+ };
631
+ }),
632
+ rememberBossReply: assign(({ event }) =>
633
+ event.type === 'BOSS_REPLY' ? { bossReply: event.answer } : {},
634
+ ),
635
+ rememberEmptyBossReplyError: assign({
636
+ lastError: () => new Error('BOSS_REPLY received an empty answer'),
637
+ pendingBossQuestion: undefined,
638
+ bossReply: undefined,
639
+ }),
640
+ completeSuccessfully: assign({
641
+ completion: 'complete',
642
+ reviewError: undefined,
643
+ }),
644
+ completeWithReviewFailure: assign(({ event }) => ({
645
+ completion: 'review-failed' as const,
646
+ reviewError: authoredReviewError(event),
647
+ })),
648
+ completeWithInvalidReviewOutput: assign(({ event }) => ({
649
+ completion: 'review-failed' as const,
650
+ reviewError: {
651
+ name: 'ReviewContractError',
652
+ message:
653
+ `REVIEW returned an invalid approval result: ` +
654
+ JSON.stringify(reviewOutput(event)),
655
+ },
656
+ })),
657
+ rememberActorError: assign(({ event }) => ({
658
+ lastError: (event as unknown as { readonly error?: unknown }).error,
659
+ })),
660
+ rememberMalformedPlayerOutput: assign(({ context }) => ({
661
+ lastError: new Error(
662
+ `Coder result for ${context.irNumber === undefined ? 'CODE-1' : 'CODE-3'} ` +
663
+ 'did not match a declared outcome.',
664
+ ),
665
+ })),
666
+ },
667
+ });
668
+
669
+ export const codingMachine = machineSetup.createMachine({
670
+ id: 'code',
671
+ initial: 'ready',
574
672
  context: ({ input }) => ({
575
- intent: input?.intent,
576
- irNumber: input?.irNumber,
577
- coderPlayer: input?.coderPlayer,
578
- reviewerPlayer: input?.reviewerPlayer,
579
- committerPlayer: input?.committerPlayer,
673
+ coderPlayer:
674
+ isNonEmptyString(input.coderPlayer) ? input.coderPlayer : 'Coder',
675
+ runResults: typeof input.runResults === 'string' ? input.runResults : '',
580
676
  }),
581
- initial: 'ready',
582
- on: {
583
- BOSS_INTERRUPT: bossInterrupts(jumpableStateIds),
677
+ output: ({ context }): CodePlaybookOutput => {
678
+ const lastCodeCommit = context.latestCommit;
679
+ if (!isNonEmptyString(lastCodeCommit)) {
680
+ throw new Error('CODE reached a final state without a commit identity');
681
+ }
682
+ const lastCodeOutput = context.coderOutput ?? '';
683
+ if (context.completion === 'review-failed') {
684
+ return {
685
+ status: 'review-failed',
686
+ lastCodeCommit,
687
+ lastCodeOutput,
688
+ error:
689
+ context.reviewError ?? {
690
+ name: 'Error',
691
+ message: 'REVIEW failed without error detail.',
692
+ },
693
+ };
694
+ }
695
+ return { status: 'complete', lastCodeCommit, lastCodeOutput };
584
696
  },
585
697
  states: {
586
698
  ready: {
587
699
  id: 'ready',
588
- description: stateDescriptions.ready,
700
+ description: STATE_DESCRIPTIONS.ready,
589
701
  meta: playbookMeta('ready'),
590
702
  tags: ['playbook.parked'],
591
- on: readyEvents,
592
- },
593
-
594
- awaitBossReply: {
595
- id: 'awaitBossReply',
596
- description: stateDescriptions.awaitBossReply,
597
- meta: playbookMeta('awaitBossReply'),
598
- tags: ['playbook.parked'],
599
- on: awaitBossReplyEvents,
600
- },
601
-
602
- planAndImplement: {
603
- id: 'planAndImplement',
604
- description: stateDescriptions.planAndImplement,
605
- meta: playbookMeta('planAndImplement'),
606
- tags: ['playbook.busy'],
607
- invoke: {
608
- src: 'player',
609
- input: ({ context }): PlayerInput => planAndImplementInput(context),
610
- onDone: withNeedsBossReplyTransition('planAndImplement', [
611
- {
612
- guard: guardIs('singleCommitReady'),
613
- target: '#commitCoderInitial',
614
- actions: [
615
- rememberCaptainOutput,
616
- assign({
617
- workflow: () => 'singleCommit' as const,
618
- changeOrigin: () => 'bossIntent' as const,
619
- afterReview: () => 'done' as const,
620
- }),
621
- ],
622
- },
623
- {
624
- guard: guardIs('irDrafted'),
625
- target: '#commitCoderInitial',
626
- actions: [
627
- rememberCaptainOutput,
628
- assign({
629
- workflow: () => 'iteration' as const,
630
- changeOrigin: () => 'bossIntent' as const,
631
- afterReview: () => 'continueIr' as const,
632
- }),
633
- ],
634
- },
635
- ]),
636
- onError: captainError,
703
+ on: {
704
+ START_CODE: { target: 'runFirstPhase', actions: 'startCoding' },
637
705
  },
638
706
  },
639
-
640
- respondToReview: {
641
- id: 'respondToReview',
642
- description: stateDescriptions.respondToReview,
643
- meta: playbookMeta('respondToReview'),
707
+ runFirstPhase: {
708
+ id: 'runFirstPhase',
709
+ description: STATE_DESCRIPTIONS.runFirstPhase,
710
+ meta: playbookMeta('runFirstPhase', 'Coder'),
644
711
  tags: ['playbook.busy'],
645
712
  invoke: {
646
713
  src: 'player',
647
714
  input: ({ context }): PlayerInput => ({
648
- stateId: 'respondToReview',
715
+ stateId: 'runFirstPhase',
649
716
  player: 'Coder',
650
- sourceItem: 'CODE-2',
651
- ...bossReplyInputFields(context),
652
- reviews: context.reviews,
653
- prompt: [
654
- 'For each review item below for the above changes, challenge or accept it, with strong reasoning, solid evidence, and comprehensive thinking.',
655
- 'Stage all current changes that belong in the repo before making any edits, and leave your edits unstaged/untracked.',
656
- ].join('\n'),
657
- result: {
658
- changesMadeSpecs:
659
- 'Coder accepted items and produced unstaged/untracked edits only in spec item files (@specs/packages/, @specs/compositions/, or legacy @specs/{user,dev,test}/), without raising any rebuttals.',
660
- changesMadeCode:
661
- 'Coder accepted items and produced unstaged/untracked edits only outside spec item files (any other files, including @specs/ decision, intent, or legacy iteration records, @specs/map.md, and @specs/meta.md), without raising any rebuttals.',
662
- changesMadeMixed:
663
- 'Coder accepted items and produced unstaged/untracked edits spanning both spec item files and other files, without raising any rebuttals.',
664
- changesMadeSpecsAndChallenged:
665
- 'Coder produced unstaged/untracked edits only in spec item files (@specs/packages/, @specs/compositions/, or legacy @specs/{user,dev,test}/) AND challenged one or more review items. Output shall include `challenges: <numbered rebuttals, one per challenged item>`.',
666
- changesMadeCodeAndChallenged:
667
- 'Coder produced unstaged/untracked edits only outside spec item files AND challenged one or more review items. Output shall include `challenges: <numbered rebuttals, one per challenged item>`.',
668
- changesMadeMixedAndChallenged:
669
- 'Coder produced unstaged/untracked edits spanning both spec item files and other files AND challenged one or more review items. Output shall include `challenges: <numbered rebuttals, one per challenged item>`.',
670
- challengesRaised:
671
- 'Coder challenged one or more review items without producing any code edits. Output shall include `challenges: <numbered rebuttals, one per challenged item>`.',
672
- accepted:
673
- 'Coder accepted the review outcome without further edits.',
674
- needsBossReply: needsBossReplyDescription,
675
- },
676
- }),
677
- onDone: withNeedsBossReplyTransition('respondToReview', [
678
- {
679
- guard: guardIs('changesMadeSpecs'),
680
- target: '#reviewChangesSpecs',
681
- actions: rememberCaptainOutput,
682
- },
683
- {
684
- guard: guardIs('changesMadeCode'),
685
- target: '#reviewChangesCode',
686
- actions: rememberCaptainOutput,
687
- },
688
- {
689
- guard: guardIs('changesMadeMixed'),
690
- target: '#reviewChangesMixed',
691
- actions: rememberCaptainOutput,
692
- },
693
- {
694
- guard: guardIs('changesMadeSpecsAndChallenged'),
695
- target: '#reviewChangesAndChallengesSpecs',
696
- actions: rememberCaptainOutput,
697
- },
698
- {
699
- guard: guardIs('changesMadeCodeAndChallenged'),
700
- target: '#reviewChangesAndChallengesCode',
701
- actions: rememberCaptainOutput,
702
- },
703
- {
704
- guard: guardIs('changesMadeMixedAndChallenged'),
705
- target: '#reviewChangesAndChallengesMixed',
706
- actions: rememberCaptainOutput,
707
- },
708
- {
709
- guard: guardIs('challengesRaised'),
710
- target: '#adjudicateChallenges',
711
- actions: rememberCaptainOutput,
712
- },
713
- {
714
- guard: acceptedAfter('changes'),
715
- target: '#commitJoint',
716
- actions: rememberCaptainOutput,
717
- },
718
- {
719
- guard: ({ context, event }) =>
720
- outputOf(event)?.guard === 'accepted' &&
721
- context.reviewSubject === 'commit' &&
722
- context.afterReview === 'continueIr',
723
- target: '#continueIr',
724
- actions: rememberCaptainOutput,
725
- },
726
- {
727
- guard: ({ context, event }) =>
728
- outputOf(event)?.guard === 'accepted' &&
729
- context.reviewSubject === 'commit' &&
730
- context.afterReview === 'summarizeSpecs',
731
- target: '#summarizeSpecs',
732
- actions: rememberCaptainOutput,
733
- },
734
- {
735
- guard: ({ context, event }) =>
736
- outputOf(event)?.guard === 'accepted' &&
737
- context.reviewSubject === 'commit' &&
738
- context.afterReview === 'done',
739
- target: '#done',
740
- actions: rememberCaptainOutput,
741
- },
742
- ]),
743
- onError: captainError,
744
- },
745
- },
746
-
747
- continueIr: {
748
- id: 'continueIr',
749
- description: stateDescriptions.continueIr,
750
- meta: playbookMeta('continueIr'),
751
- tags: ['playbook.busy'],
752
- invoke: {
753
- src: 'player',
754
- input: ({ context }): PlayerInput => continueIrInput(context),
755
- onDone: withNeedsBossReplyTransition('continueIr', [
756
- {
757
- guard: guardIs('taskReady'),
758
- target: '#commitCoderInitial',
759
- actions: [
760
- rememberCaptainOutput,
761
- assign({
762
- workflow: () => 'iteration' as const,
763
- changeOrigin: () => 'irTask' as const,
764
- afterReview: () => 'continueIr' as const,
765
- }),
766
- ],
767
- },
768
- {
769
- guard: guardIs('iterationDone'),
770
- target: '#summarizeSpecs',
771
- actions: [
772
- rememberCaptainOutput,
773
- assign({
774
- workflow: () => 'specSummary' as const,
775
- afterReview: () => 'done' as const,
776
- }),
777
- ],
778
- },
779
- ]),
780
- onError: captainError,
781
- },
782
- },
783
-
784
- summarizeSpecs: {
785
- id: 'summarizeSpecs',
786
- description: stateDescriptions.summarizeSpecs,
787
- meta: playbookMeta('summarizeSpecs'),
788
- tags: ['playbook.busy'],
789
- invoke: {
790
- src: 'player',
791
- input: ({ context }): PlayerInput => summarizeSpecsInput(context),
792
- onDone: withNeedsBossReplyTransition('summarizeSpecs', [
793
- {
794
- guard: guardIs('specsReady'),
795
- target: '#commitCoderInitial',
796
- actions: [
797
- rememberCaptainOutput,
798
- assign({
799
- workflow: () => 'specSummary' as const,
800
- changeOrigin: () => 'irTask' as const,
801
- afterReview: () => 'done' as const,
802
- }),
803
- ],
804
- },
805
- {
806
- guard: guardIs('noSpecChanges'),
807
- target: '#done',
808
- actions: rememberCaptainOutput,
809
- },
810
- ]),
811
- onError: captainError,
812
- },
813
- },
814
-
815
- reviewBossCommitSpecs: {
816
- id: 'reviewBossCommitSpecs',
817
- description: stateDescriptions.reviewBossCommitSpecs,
818
- meta: playbookMeta('reviewBossCommitSpecs'),
819
- tags: ['playbook.busy'],
820
- invoke: {
821
- src: 'player',
822
- input: ({ context }): PlayerInput => ({
823
- stateId: 'reviewBossCommitSpecs',
824
- player: 'Reviewer',
825
- sourceItem: 'CODE-5',
826
- ...bossReplyInputFields(context),
827
- intent: context.intent,
828
- prompt: [
829
- 'Review the latest commit.',
830
- 'Refer to the commit message.',
831
- 'Verify any affected spec items are:',
832
- '',
833
- '- Complete & coherent: sufficient for you to reimplement code.',
834
- '- Right level: external behavior users rely on or internal system behavior (organized per @specs/meta.md), not implementation specifics; integration/system testing, not unit testing.',
835
- '- Minimal: essential and concise; every item earns its place; also check with other items.',
836
- '- Well organized: spec packages are finely scoped, with high cohesion and low coupling.',
837
- '',
838
- 'Flag anything missing, redundant, over-specified, or under-specified.',
839
- 'For context discovery, @specs/map.md indexes all spec files and @specs/meta.md describes the spec format.',
840
- 'Verify @specs/map.md reflects the changes.',
841
- "If the change is ready to commit or push, don't raise nitpicks.",
842
- 'Do not edit files or commit; report findings only.',
843
- ].join('\n'),
844
- result: {
845
- noFindings: 'The spec-only commit has no review findings.',
846
- hasFindings:
847
- 'The review produced findings for Coder. Output shall include `reviews: <numbered list of findings, no duplication>`.',
848
- needsBossReply: needsBossReplyDescription,
849
- },
850
- }),
851
- onDone: withNeedsBossReplyTransition('reviewBossCommitSpecs', [
852
- {
853
- guard: noFindingsAfter('continueIr'),
854
- target: '#continueIr',
855
- actions: rememberCaptainOutput,
856
- },
857
- {
858
- guard: noFindingsAfter('summarizeSpecs'),
859
- target: '#summarizeSpecs',
860
- actions: rememberCaptainOutput,
861
- },
862
- {
863
- guard: noFindingsAfter('done'),
864
- target: '#done',
865
- actions: rememberCaptainOutput,
866
- },
867
- {
868
- guard: guardIs('hasFindings'),
869
- target: '#respondToReview',
870
- actions: [
871
- rememberCaptainOutput,
872
- assign({ reviewSubject: () => 'commit' as const }),
873
- ],
874
- },
875
- ]),
876
- onError: captainError,
877
- },
878
- },
879
-
880
- reviewBossCommitCode: {
881
- id: 'reviewBossCommitCode',
882
- description: stateDescriptions.reviewBossCommitCode,
883
- meta: playbookMeta('reviewBossCommitCode'),
884
- tags: ['playbook.busy'],
885
- invoke: {
886
- src: 'player',
887
- input: ({ context }): PlayerInput => ({
888
- stateId: 'reviewBossCommitCode',
889
- player: 'Reviewer',
890
- sourceItem: 'CODE-6',
891
- ...bossReplyInputFields(context),
892
- intent: context.intent,
893
- prompt: [
894
- 'Review the latest commit.',
895
- 'Refer to the commit message.',
896
- 'Flag any issues or improvements (numbered; no duplication).',
897
- "Think thoroughly — don't just approve or reject.",
898
- 'For context discovery, @specs/map.md indexes all spec files and @specs/meta.md describes the spec format.',
899
- 'Verify @specs/map.md reflects the changes.',
900
- "If the change is ready to commit or push, don't raise nitpicks.",
901
- 'Do not edit files or commit; report findings only.',
902
- ].join('\n'),
903
- result: {
904
- noFindings: 'The code-only commit has no review findings.',
905
- hasFindings:
906
- 'The review produced findings for Coder. Output shall include `reviews: <numbered list of findings, no duplication>`.',
907
- needsBossReply: needsBossReplyDescription,
908
- },
909
- }),
910
- onDone: withNeedsBossReplyTransition('reviewBossCommitCode', [
911
- {
912
- guard: noFindingsAfter('continueIr'),
913
- target: '#continueIr',
914
- actions: rememberCaptainOutput,
915
- },
916
- {
917
- guard: noFindingsAfter('summarizeSpecs'),
918
- target: '#summarizeSpecs',
919
- actions: rememberCaptainOutput,
920
- },
921
- {
922
- guard: noFindingsAfter('done'),
923
- target: '#done',
924
- actions: rememberCaptainOutput,
925
- },
926
- {
927
- guard: guardIs('hasFindings'),
928
- target: '#respondToReview',
929
- actions: [
930
- rememberCaptainOutput,
931
- assign({ reviewSubject: () => 'commit' as const }),
932
- ],
933
- },
934
- ]),
935
- onError: captainError,
936
- },
937
- },
938
-
939
- reviewBossCommitMixed: {
940
- id: 'reviewBossCommitMixed',
941
- description: stateDescriptions.reviewBossCommitMixed,
942
- meta: playbookMeta('reviewBossCommitMixed'),
943
- tags: ['playbook.busy'],
944
- invoke: {
945
- src: 'player',
946
- input: ({ context }): PlayerInput => ({
947
- stateId: 'reviewBossCommitMixed',
948
- player: 'Reviewer',
949
- sourceItem: 'CODE-7',
950
- ...bossReplyInputFields(context),
951
- intent: context.intent,
952
- prompt: [
953
- 'Review the latest commit.',
954
- 'Refer to the commit message.',
955
- 'Flag any issues or improvements (numbered; no duplication).',
956
- "Think thoroughly — don't just approve or reject.",
957
- 'Verify any affected spec items are:',
958
- '',
959
- '- Complete & coherent: sufficient for you to reimplement code.',
960
- '- Right level: external behavior users rely on or internal system behavior (organized per @specs/meta.md), not implementation specifics; integration/system testing, not unit testing.',
961
- '- Minimal: essential and concise; every item earns its place; also check with other items.',
962
- '- Well organized: spec packages are finely scoped, with high cohesion and low coupling.',
963
- '',
964
- 'Flag anything missing, redundant, over-specified, or under-specified.',
965
- 'For context discovery, @specs/map.md indexes all spec files and @specs/meta.md describes the spec format.',
966
- 'Verify @specs/map.md reflects the changes.',
967
- "If the change is ready to commit or push, don't raise nitpicks.",
968
- 'Do not edit files or commit; report findings only.',
969
- ].join('\n'),
970
- result: {
971
- noFindings: 'The mixed commit has no review findings.',
972
- hasFindings:
973
- 'The review produced findings for Coder. Output shall include `reviews: <numbered list of findings, no duplication>`.',
974
- needsBossReply: needsBossReplyDescription,
975
- },
976
- }),
977
- onDone: withNeedsBossReplyTransition('reviewBossCommitMixed', [
978
- {
979
- guard: noFindingsAfter('continueIr'),
980
- target: '#continueIr',
981
- actions: rememberCaptainOutput,
982
- },
983
- {
984
- guard: noFindingsAfter('summarizeSpecs'),
985
- target: '#summarizeSpecs',
986
- actions: rememberCaptainOutput,
987
- },
988
- {
989
- guard: noFindingsAfter('done'),
990
- target: '#done',
991
- actions: rememberCaptainOutput,
992
- },
993
- {
994
- guard: guardIs('hasFindings'),
995
- target: '#respondToReview',
996
- actions: [
997
- rememberCaptainOutput,
998
- assign({ reviewSubject: () => 'commit' as const }),
999
- ],
1000
- },
1001
- ]),
1002
- onError: captainError,
1003
- },
1004
- },
1005
-
1006
- reviewIrTaskCommitSpecs: {
1007
- id: 'reviewIrTaskCommitSpecs',
1008
- description: stateDescriptions.reviewIrTaskCommitSpecs,
1009
- meta: playbookMeta('reviewIrTaskCommitSpecs'),
1010
- tags: ['playbook.busy'],
1011
- invoke: {
1012
- src: 'player',
1013
- input: ({ context }): PlayerInput => ({
1014
- stateId: 'reviewIrTaskCommitSpecs',
1015
- player: 'Reviewer',
1016
- sourceItem: 'CODE-8',
1017
- ...bossReplyInputFields(context),
1018
- irNumber: context.irNumber,
1019
- taskDescription: context.taskDescription,
1020
- prompt: [
1021
- 'Review the latest commit.',
1022
- 'Refer to the commit message.',
1023
- 'Verify any affected spec items are:',
1024
- '',
1025
- '- Complete & coherent: sufficient for you to reimplement code.',
1026
- '- Right level: external behavior users rely on or internal system behavior (organized per @specs/meta.md), not implementation specifics; integration/system testing, not unit testing.',
1027
- '- Minimal: essential and concise; every item earns its place; also check with other items.',
1028
- '- Well organized: spec packages are finely scoped, with high cohesion and low coupling.',
1029
- '',
1030
- 'Flag anything missing, redundant, over-specified, or under-specified.',
1031
- 'For context discovery, @specs/map.md indexes all spec files and @specs/meta.md describes the spec format.',
1032
- 'Verify @specs/map.md reflects the changes.',
1033
- "If the change is ready to commit or push, don't raise nitpicks.",
1034
- 'Do not edit files or commit; report findings only.',
1035
- ].join('\n'),
1036
- result: {
1037
- noFindings: 'The IR-task spec-only commit has no review findings.',
1038
- hasFindings:
1039
- 'The review produced findings for Coder. Output shall include `reviews: <numbered list of findings, no duplication>`.',
1040
- needsBossReply: needsBossReplyDescription,
1041
- },
1042
- }),
1043
- onDone: withNeedsBossReplyTransition('reviewIrTaskCommitSpecs', [
1044
- {
1045
- guard: noFindingsAfter('continueIr'),
1046
- target: '#continueIr',
1047
- actions: rememberCaptainOutput,
1048
- },
1049
- {
1050
- guard: noFindingsAfter('summarizeSpecs'),
1051
- target: '#summarizeSpecs',
1052
- actions: rememberCaptainOutput,
1053
- },
1054
- {
1055
- guard: noFindingsAfter('done'),
1056
- target: '#done',
1057
- actions: rememberCaptainOutput,
1058
- },
1059
- {
1060
- guard: guardIs('hasFindings'),
1061
- target: '#respondToReview',
1062
- actions: [
1063
- rememberCaptainOutput,
1064
- assign({ reviewSubject: () => 'commit' as const }),
1065
- ],
1066
- },
1067
- ]),
1068
- onError: captainError,
1069
- },
1070
- },
1071
-
1072
- reviewIrTaskCommitCode: {
1073
- id: 'reviewIrTaskCommitCode',
1074
- description: stateDescriptions.reviewIrTaskCommitCode,
1075
- meta: playbookMeta('reviewIrTaskCommitCode'),
1076
- tags: ['playbook.busy'],
1077
- invoke: {
1078
- src: 'player',
1079
- input: ({ context }): PlayerInput => ({
1080
- stateId: 'reviewIrTaskCommitCode',
1081
- player: 'Reviewer',
1082
- sourceItem: 'CODE-9',
1083
- ...bossReplyInputFields(context),
1084
- irNumber: context.irNumber,
1085
- taskDescription: context.taskDescription,
1086
- prompt: [
1087
- 'Review the latest commit.',
1088
- 'Refer to the commit message.',
1089
- 'Flag any issues or improvements (numbered; no duplication).',
1090
- "Think thoroughly — don't just approve or reject.",
1091
- 'For context discovery, @specs/map.md indexes all spec files and @specs/meta.md describes the spec format.',
1092
- 'Verify @specs/map.md reflects the changes.',
1093
- "If the change is ready to commit or push, don't raise nitpicks.",
1094
- 'Do not edit files or commit; report findings only.',
1095
- ].join('\n'),
1096
- result: {
1097
- noFindings: 'The IR-task code-only commit has no review findings.',
1098
- hasFindings:
1099
- 'The review produced findings for Coder. Output shall include `reviews: <numbered list of findings, no duplication>`.',
1100
- needsBossReply: needsBossReplyDescription,
1101
- },
1102
- }),
1103
- onDone: withNeedsBossReplyTransition('reviewIrTaskCommitCode', [
1104
- {
1105
- guard: noFindingsAfter('continueIr'),
1106
- target: '#continueIr',
1107
- actions: rememberCaptainOutput,
1108
- },
1109
- {
1110
- guard: noFindingsAfter('summarizeSpecs'),
1111
- target: '#summarizeSpecs',
1112
- actions: rememberCaptainOutput,
1113
- },
1114
- {
1115
- guard: noFindingsAfter('done'),
1116
- target: '#done',
1117
- actions: rememberCaptainOutput,
1118
- },
1119
- {
1120
- guard: guardIs('hasFindings'),
1121
- target: '#respondToReview',
1122
- actions: [
1123
- rememberCaptainOutput,
1124
- assign({ reviewSubject: () => 'commit' as const }),
1125
- ],
1126
- },
1127
- ]),
1128
- onError: captainError,
1129
- },
1130
- },
1131
-
1132
- reviewIrTaskCommitMixed: {
1133
- id: 'reviewIrTaskCommitMixed',
1134
- description: stateDescriptions.reviewIrTaskCommitMixed,
1135
- meta: playbookMeta('reviewIrTaskCommitMixed'),
1136
- tags: ['playbook.busy'],
1137
- invoke: {
1138
- src: 'player',
1139
- input: ({ context }): PlayerInput => ({
1140
- stateId: 'reviewIrTaskCommitMixed',
1141
- player: 'Reviewer',
1142
- sourceItem: 'CODE-10',
1143
- ...bossReplyInputFields(context),
1144
- irNumber: context.irNumber,
1145
- taskDescription: context.taskDescription,
1146
- prompt: [
1147
- 'Review the latest commit.',
1148
- 'Refer to the commit message.',
1149
- 'Flag any issues or improvements (numbered; no duplication).',
1150
- "Think thoroughly — don't just approve or reject.",
1151
- 'Verify any affected spec items are:',
1152
- '',
1153
- '- Complete & coherent: sufficient for you to reimplement code.',
1154
- '- Right level: external behavior users rely on or internal system behavior (organized per @specs/meta.md), not implementation specifics; integration/system testing, not unit testing.',
1155
- '- Minimal: essential and concise; every item earns its place; also check with other items.',
1156
- '- Well organized: spec packages are finely scoped, with high cohesion and low coupling.',
1157
- '',
1158
- 'Flag anything missing, redundant, over-specified, or under-specified.',
1159
- 'For context discovery, @specs/map.md indexes all spec files and @specs/meta.md describes the spec format.',
1160
- 'Verify @specs/map.md reflects the changes.',
1161
- "If the change is ready to commit or push, don't raise nitpicks.",
1162
- 'Do not edit files or commit; report findings only.',
1163
- ].join('\n'),
1164
- result: {
1165
- noFindings: 'The IR-task mixed commit has no review findings.',
1166
- hasFindings:
1167
- 'The review produced findings for Coder. Output shall include `reviews: <numbered list of findings, no duplication>`.',
1168
- needsBossReply: needsBossReplyDescription,
1169
- },
1170
- }),
1171
- onDone: withNeedsBossReplyTransition('reviewIrTaskCommitMixed', [
1172
- {
1173
- guard: noFindingsAfter('continueIr'),
1174
- target: '#continueIr',
1175
- actions: rememberCaptainOutput,
1176
- },
1177
- {
1178
- guard: noFindingsAfter('summarizeSpecs'),
1179
- target: '#summarizeSpecs',
1180
- actions: rememberCaptainOutput,
1181
- },
1182
- {
1183
- guard: noFindingsAfter('done'),
1184
- target: '#done',
1185
- actions: rememberCaptainOutput,
1186
- },
1187
- {
1188
- guard: guardIs('hasFindings'),
1189
- target: '#respondToReview',
1190
- actions: [
1191
- rememberCaptainOutput,
1192
- assign({ reviewSubject: () => 'commit' as const }),
1193
- ],
1194
- },
1195
- ]),
1196
- onError: captainError,
1197
- },
1198
- },
1199
-
1200
- reviewChangesSpecs: {
1201
- id: 'reviewChangesSpecs',
1202
- description: stateDescriptions.reviewChangesSpecs,
1203
- meta: playbookMeta('reviewChangesSpecs'),
1204
- tags: ['playbook.busy'],
1205
- invoke: {
1206
- src: 'player',
1207
- input: ({ context }): PlayerInput => ({
1208
- stateId: 'reviewChangesSpecs',
1209
- player: 'Reviewer',
1210
- sourceItem: 'CODE-11',
1211
- ...bossReplyInputFields(context),
1212
- prompt: [
1213
- 'Review the unstaged and untracked changes in the context of the staged changes.',
1214
- 'Understand the intent.',
1215
- 'Verify any affected spec items are:',
1216
- '',
1217
- '- Complete & coherent: sufficient for you to reimplement code.',
1218
- '- Right level: external behavior users rely on or internal system behavior (organized per @specs/meta.md), not implementation specifics; integration/system testing, not unit testing.',
1219
- '- Minimal: essential and concise; every item earns its place; also check with other items.',
1220
- '- Well organized: spec packages are finely scoped, with high cohesion and low coupling.',
1221
- '',
1222
- 'Flag anything missing, redundant, over-specified, or under-specified.',
1223
- 'For context discovery, @specs/map.md indexes all spec files and @specs/meta.md describes the spec format.',
1224
- 'Verify @specs/map.md reflects the changes.',
1225
- "If the change is ready to commit or push, don't raise nitpicks.",
1226
- 'Do not edit files or commit; report findings only.',
1227
- ].join('\n'),
1228
- result: {
1229
- noFindings:
1230
- 'The uncommitted spec-only changes are ready to commit.',
1231
- hasFindings:
1232
- 'The review produced findings for Coder. Output shall include `reviews: <numbered list of findings, no duplication>`.',
1233
- needsBossReply: needsBossReplyDescription,
1234
- },
717
+ sourceItem: 'CODE-1',
718
+ prompt: FIRST_PHASE_PROMPT,
719
+ result: FIRST_PHASE_RESULTS,
720
+ callerInput: context.callerInput ?? '',
721
+ runResults: context.runResults,
722
+ coderPlayer: context.coderPlayer,
723
+ ...(context.pendingBossQuestion === undefined
724
+ ? {}
725
+ : { pendingBossQuestion: context.pendingBossQuestion }),
726
+ ...(context.bossReply === undefined
727
+ ? {}
728
+ : { bossReply: context.bossReply }),
1235
729
  }),
1236
- onDone: withNeedsBossReplyTransition('reviewChangesSpecs', [
730
+ onDone: [
1237
731
  {
1238
- guard: guardIs('noFindings'),
1239
- target: '#commitJoint',
1240
- actions: rememberCaptainOutput,
732
+ guard: 'isDirectCommit',
733
+ target: 'reviewFirstCommit',
734
+ actions: 'rememberDirectCommit',
1241
735
  },
1242
736
  {
1243
- guard: guardIs('hasFindings'),
1244
- target: '#respondToReview',
1245
- actions: [
1246
- rememberCaptainOutput,
1247
- assign({ reviewSubject: () => 'changes' as const }),
1248
- ],
737
+ guard: 'isIrCommit',
738
+ target: 'reviewFirstCommit',
739
+ actions: 'rememberIrCommit',
1249
740
  },
1250
- ]),
1251
- onError: captainError,
1252
- },
1253
- },
1254
-
1255
- reviewChangesCode: {
1256
- id: 'reviewChangesCode',
1257
- description: stateDescriptions.reviewChangesCode,
1258
- meta: playbookMeta('reviewChangesCode'),
1259
- tags: ['playbook.busy'],
1260
- invoke: {
1261
- src: 'player',
1262
- input: ({ context }): PlayerInput => ({
1263
- stateId: 'reviewChangesCode',
1264
- player: 'Reviewer',
1265
- sourceItem: 'CODE-12',
1266
- ...bossReplyInputFields(context),
1267
- prompt: [
1268
- 'Review the unstaged and untracked changes in the context of the staged changes.',
1269
- 'Understand the intent.',
1270
- 'Flag any issues or improvements (numbered; no duplication).',
1271
- "Think thoroughly — don't just approve or reject.",
1272
- 'For context discovery, @specs/map.md indexes all spec files and @specs/meta.md describes the spec format.',
1273
- 'Verify @specs/map.md reflects the changes.',
1274
- "If the change is ready to commit or push, don't raise nitpicks.",
1275
- 'Do not edit files or commit; report findings only.',
1276
- ].join('\n'),
1277
- result: {
1278
- noFindings:
1279
- 'The uncommitted code-only changes are ready to commit.',
1280
- hasFindings:
1281
- 'The review produced findings for Coder. Output shall include `reviews: <numbered list of findings, no duplication>`.',
1282
- needsBossReply: needsBossReplyDescription,
1283
- },
1284
- }),
1285
- onDone: withNeedsBossReplyTransition('reviewChangesCode', [
1286
741
  {
1287
- guard: guardIs('noFindings'),
1288
- target: '#commitJoint',
1289
- actions: rememberCaptainOutput,
742
+ guard: 'needsBossReply',
743
+ target: 'awaitBossReply',
744
+ actions: 'rememberPendingQuestion',
1290
745
  },
1291
746
  {
1292
- guard: guardIs('hasFindings'),
1293
- target: '#respondToReview',
1294
- actions: [
1295
- rememberCaptainOutput,
1296
- assign({ reviewSubject: () => 'changes' as const }),
1297
- ],
747
+ target: 'failed',
748
+ actions: 'rememberMalformedPlayerOutput',
1298
749
  },
1299
- ]),
1300
- onError: captainError,
750
+ ],
751
+ onError: { target: 'failed', actions: 'rememberActorError' },
1301
752
  },
1302
753
  },
1303
-
1304
- reviewChangesMixed: {
1305
- id: 'reviewChangesMixed',
1306
- description: stateDescriptions.reviewChangesMixed,
1307
- meta: playbookMeta('reviewChangesMixed'),
1308
- tags: ['playbook.busy'],
754
+ reviewFirstCommit: {
755
+ id: 'reviewFirstCommit',
756
+ description: STATE_DESCRIPTIONS.reviewFirstCommit,
757
+ meta: playbookMeta('reviewFirstCommit'),
758
+ tags: ['playbook.suspended'],
1309
759
  invoke: {
1310
- src: 'player',
1311
- input: ({ context }): PlayerInput => ({
1312
- stateId: 'reviewChangesMixed',
1313
- player: 'Reviewer',
1314
- sourceItem: 'CODE-13',
1315
- ...bossReplyInputFields(context),
1316
- prompt: [
1317
- 'Review the unstaged and untracked changes in the context of the staged changes.',
1318
- 'Understand the intent.',
1319
- 'Flag any issues or improvements (numbered; no duplication).',
1320
- "Think thoroughly — don't just approve or reject.",
1321
- 'Verify any affected spec items are:',
1322
- '',
1323
- '- Complete & coherent: sufficient for you to reimplement code.',
1324
- '- Right level: external behavior users rely on or internal system behavior (organized per @specs/meta.md), not implementation specifics; integration/system testing, not unit testing.',
1325
- '- Minimal: essential and concise; every item earns its place; also check with other items.',
1326
- '- Well organized: spec packages are finely scoped, with high cohesion and low coupling.',
1327
- '',
1328
- 'Flag anything missing, redundant, over-specified, or under-specified.',
1329
- 'For context discovery, @specs/map.md indexes all spec files and @specs/meta.md describes the spec format.',
1330
- 'Verify @specs/map.md reflects the changes.',
1331
- "If the change is ready to commit or push, don't raise nitpicks.",
1332
- 'Do not edit files or commit; report findings only.',
1333
- ].join('\n'),
1334
- result: {
1335
- noFindings: 'The uncommitted mixed changes are ready to commit.',
1336
- hasFindings:
1337
- 'The review produced findings for Coder. Output shall include `reviews: <numbered list of findings, no duplication>`.',
1338
- needsBossReply: needsBossReplyDescription,
1339
- },
760
+ src: 'playbook',
761
+ input: ({ context }): PlaybookInput => ({
762
+ stateId: 'reviewFirstCommit',
763
+ sourceItem: 'CODE-2',
764
+ playbookId: 'review',
765
+ text: initialReviewText(context),
1340
766
  }),
1341
- onDone: withNeedsBossReplyTransition('reviewChangesMixed', [
767
+ onDone: [
1342
768
  {
1343
- guard: guardIs('noFindings'),
1344
- target: '#commitJoint',
1345
- actions: rememberCaptainOutput,
769
+ guard: 'reviewApprovedDirect',
770
+ target: 'done',
771
+ actions: 'completeSuccessfully',
1346
772
  },
1347
773
  {
1348
- guard: guardIs('hasFindings'),
1349
- target: '#respondToReview',
1350
- actions: [
1351
- rememberCaptainOutput,
1352
- assign({ reviewSubject: () => 'changes' as const }),
1353
- ],
774
+ guard: 'reviewApprovedIrCreated',
775
+ target: 'runIrTask',
1354
776
  },
1355
- ]),
1356
- onError: captainError,
1357
- },
1358
- },
1359
-
1360
- reviewChangesAndChallengesSpecs: {
1361
- id: 'reviewChangesAndChallengesSpecs',
1362
- description: stateDescriptions.reviewChangesAndChallengesSpecs,
1363
- meta: playbookMeta('reviewChangesAndChallengesSpecs'),
1364
- tags: ['playbook.busy'],
1365
- invoke: {
1366
- src: 'player',
1367
- input: ({ context }): PlayerInput => ({
1368
- stateId: 'reviewChangesAndChallengesSpecs',
1369
- player: 'Reviewer',
1370
- sourceItem: 'CODE-15',
1371
- ...bossReplyInputFields(context),
1372
- reviews: context.reviews,
1373
- challenges: context.challenges,
1374
- prompt: [
1375
- 'Review the unstaged and untracked changes in the context of the staged changes.',
1376
- 'Understand the intent.',
1377
- 'Verify any affected spec items are:',
1378
- '',
1379
- '- Complete & coherent: sufficient for you to reimplement code.',
1380
- '- Right level: external behavior users rely on or internal system behavior (organized per @specs/meta.md), not implementation specifics; integration/system testing, not unit testing.',
1381
- '- Minimal: essential and concise; every item earns its place; also check with other items.',
1382
- '- Well organized: spec packages are finely scoped, with high cohesion and low coupling.',
1383
- '',
1384
- 'Flag anything missing, redundant, over-specified, or under-specified.',
1385
- 'For context discovery, @specs/map.md indexes all spec files and @specs/meta.md describes the spec format.',
1386
- 'Verify @specs/map.md reflects the changes.',
1387
- "If the change is ready to commit or push, don't raise nitpicks.",
1388
- 'Do not edit files or commit; report findings only.',
1389
- 'For each rebuttal below, challenge or accept it, with strong reasoning, solid evidence, and comprehensive thinking.',
1390
- ].join('\n'),
1391
- result: {
1392
- approved:
1393
- 'The new spec-only changes are ready to commit and every rebuttal was accepted (or no items remained open).',
1394
- needsRevision:
1395
- '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>`.',
1396
- needsBossReply: needsBossReplyDescription,
1397
- },
1398
- }),
1399
- onDone: withNeedsBossReplyTransition(
1400
- 'reviewChangesAndChallengesSpecs',
1401
- [
1402
- {
1403
- guard: guardIs('approved'),
1404
- target: '#commitJoint',
1405
- actions: rememberCaptainOutput,
1406
- },
1407
- {
1408
- guard: guardIs('needsRevision'),
1409
- target: '#respondToReview',
1410
- actions: [
1411
- rememberCaptainOutput,
1412
- assign({ reviewSubject: () => 'changes' as const }),
1413
- ],
1414
- },
1415
- ],
1416
- ),
1417
- onError: captainError,
1418
- },
1419
- },
1420
-
1421
- reviewChangesAndChallengesCode: {
1422
- id: 'reviewChangesAndChallengesCode',
1423
- description: stateDescriptions.reviewChangesAndChallengesCode,
1424
- meta: playbookMeta('reviewChangesAndChallengesCode'),
1425
- tags: ['playbook.busy'],
1426
- invoke: {
1427
- src: 'player',
1428
- input: ({ context }): PlayerInput => ({
1429
- stateId: 'reviewChangesAndChallengesCode',
1430
- player: 'Reviewer',
1431
- sourceItem: 'CODE-16',
1432
- ...bossReplyInputFields(context),
1433
- reviews: context.reviews,
1434
- challenges: context.challenges,
1435
- prompt: [
1436
- 'Review the unstaged and untracked changes in the context of the staged changes.',
1437
- 'Understand the intent.',
1438
- 'Flag any issues or improvements (numbered; no duplication).',
1439
- "Think thoroughly — don't just approve or reject.",
1440
- 'For context discovery, @specs/map.md indexes all spec files and @specs/meta.md describes the spec format.',
1441
- 'Verify @specs/map.md reflects the changes.',
1442
- "If the change is ready to commit or push, don't raise nitpicks.",
1443
- 'Do not edit files or commit; report findings only.',
1444
- 'For each rebuttal below, challenge or accept it, with strong reasoning, solid evidence, and comprehensive thinking.',
1445
- ].join('\n'),
1446
- result: {
1447
- approved:
1448
- 'The new code-only changes are ready to commit and every rebuttal was accepted (or no items remained open).',
1449
- needsRevision:
1450
- '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>`.',
1451
- needsBossReply: needsBossReplyDescription,
1452
- },
1453
- }),
1454
- onDone: withNeedsBossReplyTransition('reviewChangesAndChallengesCode', [
1455
777
  {
1456
- guard: guardIs('approved'),
1457
- target: '#commitJoint',
1458
- actions: rememberCaptainOutput,
778
+ target: 'done',
779
+ actions: 'completeWithInvalidReviewOutput',
1459
780
  },
781
+ ],
782
+ onError: [
1460
783
  {
1461
- guard: guardIs('needsRevision'),
1462
- target: '#respondToReview',
1463
- actions: [
1464
- rememberCaptainOutput,
1465
- assign({ reviewSubject: () => 'changes' as const }),
1466
- ],
1467
- },
1468
- ]),
1469
- onError: captainError,
1470
- },
1471
- },
1472
-
1473
- reviewChangesAndChallengesMixed: {
1474
- id: 'reviewChangesAndChallengesMixed',
1475
- description: stateDescriptions.reviewChangesAndChallengesMixed,
1476
- meta: playbookMeta('reviewChangesAndChallengesMixed'),
1477
- tags: ['playbook.busy'],
1478
- invoke: {
1479
- src: 'player',
1480
- input: ({ context }): PlayerInput => ({
1481
- stateId: 'reviewChangesAndChallengesMixed',
1482
- player: 'Reviewer',
1483
- sourceItem: 'CODE-17',
1484
- ...bossReplyInputFields(context),
1485
- reviews: context.reviews,
1486
- challenges: context.challenges,
1487
- prompt: [
1488
- 'Review the unstaged and untracked changes in the context of the staged changes.',
1489
- 'Understand the intent.',
1490
- 'Flag any issues or improvements (numbered; no duplication).',
1491
- "Think thoroughly — don't just approve or reject.",
1492
- 'Verify any affected spec items are:',
1493
- '',
1494
- '- Complete & coherent: sufficient for you to reimplement code.',
1495
- '- Right level: external behavior users rely on or internal system behavior (organized per @specs/meta.md), not implementation specifics; integration/system testing, not unit testing.',
1496
- '- Minimal: essential and concise; every item earns its place; also check with other items.',
1497
- '- Well organized: spec packages are finely scoped, with high cohesion and low coupling.',
1498
- '',
1499
- 'Flag anything missing, redundant, over-specified, or under-specified.',
1500
- 'For context discovery, @specs/map.md indexes all spec files and @specs/meta.md describes the spec format.',
1501
- 'Verify @specs/map.md reflects the changes.',
1502
- "If the change is ready to commit or push, don't raise nitpicks.",
1503
- 'Do not edit files or commit; report findings only.',
1504
- 'For each rebuttal below, challenge or accept it, with strong reasoning, solid evidence, and comprehensive thinking.',
1505
- ].join('\n'),
1506
- result: {
1507
- approved:
1508
- 'The new mixed changes are ready to commit and every rebuttal was accepted (or no items remained open).',
1509
- needsRevision:
1510
- '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>`.',
1511
- needsBossReply: needsBossReplyDescription,
784
+ guard: 'authoredReviewFailure',
785
+ target: 'done',
786
+ actions: 'completeWithReviewFailure',
1512
787
  },
1513
- }),
1514
- onDone: withNeedsBossReplyTransition(
1515
- 'reviewChangesAndChallengesMixed',
1516
- [
1517
- {
1518
- guard: guardIs('approved'),
1519
- target: '#commitJoint',
1520
- actions: rememberCaptainOutput,
1521
- },
1522
- {
1523
- guard: guardIs('needsRevision'),
1524
- target: '#respondToReview',
1525
- actions: [
1526
- rememberCaptainOutput,
1527
- assign({ reviewSubject: () => 'changes' as const }),
1528
- ],
1529
- },
1530
- ],
1531
- ),
1532
- onError: captainError,
788
+ { target: 'failed', actions: 'rememberActorError' },
789
+ ],
1533
790
  },
1534
791
  },
1535
-
1536
- adjudicateChallenges: {
1537
- id: 'adjudicateChallenges',
1538
- description: stateDescriptions.adjudicateChallenges,
1539
- meta: playbookMeta('adjudicateChallenges'),
792
+ runIrTask: {
793
+ id: 'runIrTask',
794
+ description: STATE_DESCRIPTIONS.runIrTask,
795
+ meta: playbookMeta('runIrTask', 'Coder'),
1540
796
  tags: ['playbook.busy'],
1541
797
  invoke: {
1542
798
  src: 'player',
1543
799
  input: ({ context }): PlayerInput => ({
1544
- stateId: 'adjudicateChallenges',
1545
- player: 'Reviewer',
1546
- sourceItem: 'CODE-14',
1547
- ...bossReplyInputFields(context),
1548
- reviews: context.reviews,
1549
- challenges: context.challenges,
1550
- prompt: [
1551
- 'For each rebuttal below, challenge or accept it, with strong reasoning, solid evidence, and comprehensive thinking.',
1552
- 'Do not edit files or commit; report findings only.',
1553
- ].join('\n'),
1554
- result: {
1555
- challengeAccepted:
1556
- 'Reviewer accepted the rebuttal — no further review edits are required.',
1557
- challengeRejected:
1558
- 'Reviewer rejected the rebuttal — Coder must respond again.',
1559
- noOpenItems: 'No review items remain open.',
1560
- needsBossReply: needsBossReplyDescription,
1561
- },
800
+ stateId: 'runIrTask',
801
+ player: 'Coder',
802
+ sourceItem: 'CODE-3',
803
+ prompt: IR_TASK_PROMPT,
804
+ result: IR_TASK_RESULTS,
805
+ callerInput: context.callerInput ?? '',
806
+ runResults: context.runResults,
807
+ coderPlayer: context.coderPlayer,
808
+ ...(context.irNumber === undefined
809
+ ? {}
810
+ : { irNumber: context.irNumber }),
811
+ ...(context.irTask === undefined ? {} : { irTask: context.irTask }),
812
+ ...(context.pendingBossQuestion === undefined
813
+ ? {}
814
+ : { pendingBossQuestion: context.pendingBossQuestion }),
815
+ ...(context.bossReply === undefined
816
+ ? {}
817
+ : { bossReply: context.bossReply }),
1562
818
  }),
1563
- onDone: withNeedsBossReplyTransition('adjudicateChallenges', [
819
+ onDone: [
1564
820
  {
1565
- guard: ({ context, event }) =>
1566
- (outputOf(event)?.guard === 'challengeAccepted' ||
1567
- outputOf(event)?.guard === 'noOpenItems') &&
1568
- context.reviewSubject === 'changes',
1569
- target: '#commitJoint',
1570
- actions: rememberCaptainOutput,
821
+ guard: 'isMoreTasks',
822
+ target: 'reviewIrTask',
823
+ actions: 'rememberMoreTasks',
1571
824
  },
1572
825
  {
1573
- guard: ({ context, event }) =>
1574
- (outputOf(event)?.guard === 'challengeAccepted' ||
1575
- outputOf(event)?.guard === 'noOpenItems') &&
1576
- context.reviewSubject === 'commit' &&
1577
- context.afterReview === 'continueIr',
1578
- target: '#continueIr',
1579
- actions: rememberCaptainOutput,
826
+ guard: 'isFinalTask',
827
+ target: 'reviewIrTask',
828
+ actions: 'rememberFinalTask',
1580
829
  },
1581
830
  {
1582
- guard: ({ context, event }) =>
1583
- (outputOf(event)?.guard === 'challengeAccepted' ||
1584
- outputOf(event)?.guard === 'noOpenItems') &&
1585
- context.reviewSubject === 'commit' &&
1586
- context.afterReview === 'summarizeSpecs',
1587
- target: '#summarizeSpecs',
1588
- actions: rememberCaptainOutput,
831
+ guard: 'needsBossReply',
832
+ target: 'awaitBossReply',
833
+ actions: 'rememberPendingQuestion',
1589
834
  },
1590
835
  {
1591
- guard: ({ context, event }) =>
1592
- (outputOf(event)?.guard === 'challengeAccepted' ||
1593
- outputOf(event)?.guard === 'noOpenItems') &&
1594
- context.reviewSubject === 'commit' &&
1595
- context.afterReview === 'done',
1596
- target: '#done',
1597
- actions: rememberCaptainOutput,
836
+ target: 'failed',
837
+ actions: 'rememberMalformedPlayerOutput',
1598
838
  },
1599
- {
1600
- guard: guardIs('challengeRejected'),
1601
- target: '#respondToReview',
1602
- actions: rememberCaptainOutput,
1603
- },
1604
- ]),
1605
- onError: captainError,
839
+ ],
840
+ onError: { target: 'failed', actions: 'rememberActorError' },
1606
841
  },
1607
842
  },
1608
-
1609
- commitCoderInitial: {
1610
- id: 'commitCoderInitial',
1611
- description: stateDescriptions.commitCoderInitial,
1612
- meta: playbookMeta('commitCoderInitial'),
1613
- tags: ['playbook.busy'],
843
+ reviewIrTask: {
844
+ id: 'reviewIrTask',
845
+ description: STATE_DESCRIPTIONS.reviewIrTask,
846
+ meta: playbookMeta('reviewIrTask'),
847
+ tags: ['playbook.suspended'],
1614
848
  invoke: {
1615
- src: 'player',
1616
- input: ({ context }): PlayerInput => ({
1617
- stateId: 'commitCoderInitial',
1618
- player: 'Committer',
1619
- sourceItem: 'CODE-18',
1620
- ...bossReplyInputFields(context),
1621
- coderPlayer: context.coderPlayer,
1622
- committerPlayer: context.committerPlayer,
1623
- prompt: [
1624
- 'Make a commit of the changes that belong in the repo, following @specs/packages/git.md (reread if necessary).',
1625
- "If that spec is absent, follow the legacy @specs/dev/git.md; if neither exists, follow the repository's existing commit conventions and do not search elsewhere.",
1626
- 'Write the commit message concisely.',
1627
- 'Coder is <coder-llm>.',
1628
- 'Format the `Co-authored-by` `<model>` token as the conventional human form of the substituted id (e.g., `claude-opus-4-7` → `Claude-Opus-4.7`, `gpt-5.5` → `GPT-5.5`).',
1629
- ].join('\n'),
1630
- result: {
1631
- committedSpecs:
1632
- 'Committed changes that touch only spec item files (@specs/packages/, @specs/compositions/, or legacy @specs/{user,dev,test}/).',
1633
- committedCode:
1634
- 'Committed changes that touch only files that are not spec item files (any other files, including @specs/ decision, intent, or legacy iteration records, @specs/map.md, and @specs/meta.md).',
1635
- committedMixed:
1636
- 'Committed changes that span both spec item files and other files.',
1637
- noRelevantChanges: 'There are no relevant changes to commit.',
1638
- needsBossInput: 'Committing requires additional Boss input.',
1639
- needsBossReply: needsBossReplyDescription,
1640
- },
849
+ src: 'playbook',
850
+ input: ({ context }): PlaybookInput => ({
851
+ stateId: 'reviewIrTask',
852
+ sourceItem: 'CODE-4',
853
+ playbookId: 'review',
854
+ text: irTaskReviewText(context),
1641
855
  }),
1642
- onDone: withNeedsBossReplyTransition('commitCoderInitial', [
1643
- {
1644
- guard: guardAndOrigin('committedSpecs', 'bossIntent'),
1645
- target: '#reviewBossCommitSpecs',
1646
- actions: rememberCaptainOutput,
1647
- },
1648
- {
1649
- guard: guardAndOrigin('committedCode', 'bossIntent'),
1650
- target: '#reviewBossCommitCode',
1651
- actions: rememberCaptainOutput,
1652
- },
1653
- {
1654
- guard: guardAndOrigin('committedMixed', 'bossIntent'),
1655
- target: '#reviewBossCommitMixed',
1656
- actions: rememberCaptainOutput,
1657
- },
856
+ onDone: [
1658
857
  {
1659
- guard: guardAndOrigin('committedSpecs', 'irTask'),
1660
- target: '#reviewIrTaskCommitSpecs',
1661
- actions: rememberCaptainOutput,
858
+ guard: 'reviewApprovedMoreTasks',
859
+ target: 'runIrTask',
860
+ actions: 'advanceToNextIrTask',
1662
861
  },
1663
862
  {
1664
- guard: guardAndOrigin('committedCode', 'irTask'),
1665
- target: '#reviewIrTaskCommitCode',
1666
- actions: rememberCaptainOutput,
863
+ guard: 'reviewApprovedFinalTask',
864
+ target: 'done',
865
+ actions: 'completeSuccessfully',
1667
866
  },
1668
867
  {
1669
- guard: guardAndOrigin('committedMixed', 'irTask'),
1670
- target: '#reviewIrTaskCommitMixed',
1671
- actions: rememberCaptainOutput,
868
+ target: 'done',
869
+ actions: 'completeWithInvalidReviewOutput',
1672
870
  },
871
+ ],
872
+ onError: [
1673
873
  {
1674
- guard: guardIs('noRelevantChanges'),
1675
- target: '#ready',
1676
- actions: rememberCaptainOutput,
874
+ guard: 'authoredReviewFailure',
875
+ target: 'done',
876
+ actions: 'completeWithReviewFailure',
1677
877
  },
1678
- {
1679
- guard: guardIs('needsBossInput'),
1680
- target: '#ready',
1681
- actions: rememberCaptainOutput,
1682
- },
1683
- ]),
1684
- onError: captainError,
878
+ { target: 'failed', actions: 'rememberActorError' },
879
+ ],
1685
880
  },
1686
881
  },
1687
-
1688
- commitJoint: {
1689
- id: 'commitJoint',
1690
- description: stateDescriptions.commitJoint,
1691
- meta: playbookMeta('commitJoint'),
1692
- tags: ['playbook.busy'],
1693
- invoke: {
1694
- src: 'player',
1695
- input: ({ context }): PlayerInput => ({
1696
- stateId: 'commitJoint',
1697
- player: 'Committer',
1698
- sourceItem: 'CODE-19',
1699
- ...bossReplyInputFields(context),
1700
- coderPlayer: context.coderPlayer,
1701
- reviewerPlayer: context.reviewerPlayer,
1702
- committerPlayer: context.committerPlayer,
1703
- prompt: [
1704
- 'Make a commit of the changes that belong in the repo, following @specs/packages/git.md (reread if necessary).',
1705
- "If that spec is absent, follow the legacy @specs/dev/git.md; if neither exists, follow the repository's existing commit conventions and do not search elsewhere.",
1706
- 'Write the commit message concisely.',
1707
- 'Coder is <coder-llm>; Reviewer is <reviewer-llm>.',
1708
- 'Format the `Co-authored-by` `<model>` token as the conventional human form of the substituted id (e.g., `claude-opus-4-7` → `Claude-Opus-4.7`, `gpt-5.5` → `GPT-5.5`).',
1709
- ].join('\n'),
1710
- result: {
1711
- committed: 'Relevant changes were committed.',
1712
- noRelevantChanges: 'There are no relevant changes to commit.',
1713
- needsBossInput: 'Committing requires additional Boss input.',
1714
- needsBossReply: needsBossReplyDescription,
1715
- },
1716
- }),
1717
- onDone: withNeedsBossReplyTransition('commitJoint', [
1718
- {
1719
- guard: ({ context, event }) =>
1720
- outputOf(event)?.guard === 'committed' &&
1721
- context.afterReview === 'continueIr',
1722
- target: '#continueIr',
1723
- actions: rememberCaptainOutput,
1724
- },
1725
- {
1726
- guard: ({ context, event }) =>
1727
- outputOf(event)?.guard === 'committed' &&
1728
- context.afterReview === 'summarizeSpecs',
1729
- target: '#summarizeSpecs',
1730
- actions: rememberCaptainOutput,
1731
- },
882
+ awaitBossReply: {
883
+ id: 'awaitBossReply',
884
+ description: STATE_DESCRIPTIONS.awaitBossReply,
885
+ meta: playbookMeta('awaitBossReply'),
886
+ tags: ['playbook.parked'],
887
+ on: {
888
+ BOSS_REPLY: [
1732
889
  {
1733
- guard: ({ context, event }) =>
1734
- outputOf(event)?.guard === 'committed' &&
1735
- context.afterReview === 'done',
1736
- target: '#done',
1737
- actions: rememberCaptainOutput,
890
+ guard: 'emptyBossReply',
891
+ target: '#failed',
892
+ actions: 'rememberEmptyBossReplyError',
1738
893
  },
1739
894
  {
1740
- guard: guardIs('noRelevantChanges'),
1741
- target: '#done',
1742
- actions: rememberCaptainOutput,
895
+ guard: 'resumesFirstPhase',
896
+ target: '#runFirstPhase',
897
+ reenter: true,
898
+ actions: 'rememberBossReply',
1743
899
  },
1744
900
  {
1745
- guard: guardIs('needsBossInput'),
1746
- target: '#ready',
1747
- actions: rememberCaptainOutput,
901
+ guard: 'resumesIrTask',
902
+ target: '#runIrTask',
903
+ reenter: true,
904
+ actions: 'rememberBossReply',
1748
905
  },
1749
- ]),
1750
- onError: captainError,
906
+ ],
907
+ START_CODE: { target: 'runFirstPhase', actions: 'startCoding' },
1751
908
  },
1752
909
  },
1753
-
1754
910
  failed: {
1755
911
  id: 'failed',
1756
- description: stateDescriptions.failed,
912
+ description: STATE_DESCRIPTIONS.failed,
1757
913
  meta: playbookMeta('failed'),
1758
914
  tags: ['playbook.parked'],
1759
- on: readyEvents,
915
+ on: {
916
+ START_CODE: { target: 'runFirstPhase', actions: 'startCoding' },
917
+ },
1760
918
  },
1761
-
1762
919
  done: {
1763
920
  id: 'done',
1764
- description: stateDescriptions.done,
921
+ description: STATE_DESCRIPTIONS.done,
1765
922
  meta: playbookMeta('done'),
1766
923
  type: 'final',
1767
924
  },
1768
925
  },
1769
926
  });
927
+
928
+ export default codingMachine;