@sublang/playbook 15.1.0 → 17.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 (102) hide show
  1. package/README.md +2 -0
  2. package/docs/cli.md +36 -18
  3. package/docs/configuration.md +7 -5
  4. package/docs/embedding.md +28 -7
  5. package/package.json +4 -4
  6. package/reference/sdlc/branch.playbook/branch.fsm.d.ts +63 -33
  7. package/reference/sdlc/branch.playbook/branch.fsm.js +190 -108
  8. package/reference/sdlc/branch.playbook/branch.fsm.ts +269 -147
  9. package/reference/sdlc/branch.playbook/branch.gears.md +7 -17
  10. package/reference/sdlc/branch.playbook/branch.playbook.d.ts +12 -16
  11. package/reference/sdlc/branch.playbook/branch.playbook.js +112 -101
  12. package/reference/sdlc/branch.playbook/branch.playbook.ts +128 -171
  13. package/reference/sdlc/branch.playbook/branch.registry.d.ts +3 -2
  14. package/reference/sdlc/branch.playbook/branch.registry.ts +6 -2
  15. package/reference/sdlc/captain.md +19 -10
  16. package/reference/sdlc/captain.playbook/captain.fsm.d.ts +6 -2
  17. package/reference/sdlc/captain.playbook/captain.fsm.js +19 -7
  18. package/reference/sdlc/captain.playbook/captain.fsm.ts +25 -10
  19. package/reference/sdlc/captain.playbook/captain.gears.md +16 -8
  20. package/reference/sdlc/captain.playbook/captain.playbook.d.ts +1 -1
  21. package/reference/sdlc/captain.playbook/captain.playbook.js +3 -2
  22. package/reference/sdlc/captain.playbook/captain.playbook.ts +4 -3
  23. package/reference/sdlc/code.playbook/bin/interactive-session.js +14 -1
  24. package/reference/sdlc/code.playbook/bin/portable-codec.js +3 -0
  25. package/reference/sdlc/code.playbook/bin/repository-effects.js +1 -0
  26. package/reference/sdlc/code.playbook/bin/run.js +128 -85
  27. package/reference/sdlc/code.playbook/bin/session-host.js +40 -8
  28. package/reference/sdlc/code.playbook/bin/session-store.js +162 -34
  29. package/reference/sdlc/code.playbook/code.fsm.d.ts +146 -93
  30. package/reference/sdlc/code.playbook/code.fsm.introspect.d.ts +8 -8
  31. package/reference/sdlc/code.playbook/code.fsm.introspect.ts +13 -13
  32. package/reference/sdlc/code.playbook/code.fsm.js +609 -649
  33. package/reference/sdlc/code.playbook/code.fsm.ts +890 -821
  34. package/reference/sdlc/code.playbook/code.gears.md +39 -29
  35. package/reference/sdlc/code.playbook/code.playbook.d.ts +13 -16
  36. package/reference/sdlc/code.playbook/code.playbook.js +166 -147
  37. package/reference/sdlc/code.playbook/code.playbook.ts +180 -229
  38. package/reference/sdlc/code.playbook/code.registry.d.ts +3 -2
  39. package/reference/sdlc/code.playbook/code.registry.ts +6 -2
  40. package/reference/sdlc/code.playbook/host-capabilities.d.ts +3 -0
  41. package/reference/sdlc/code.playbook/playbook-captain.d.ts +74 -2
  42. package/reference/sdlc/code.playbook/playbook-captain.js +1548 -468
  43. package/reference/sdlc/code.playbook/playbook-captain.ts +1993 -573
  44. package/reference/sdlc/code.playbook/session-host.d.ts +4 -1
  45. package/reference/sdlc/code.playbook/session-store.d.ts +17 -0
  46. package/reference/sdlc/code.playbook/session-store.js +1 -0
  47. package/reference/sdlc/decide.md +1 -0
  48. package/reference/sdlc/decide.playbook/decide.fsm.d.ts +273 -157
  49. package/reference/sdlc/decide.playbook/decide.fsm.js +765 -732
  50. package/reference/sdlc/decide.playbook/decide.fsm.ts +1167 -945
  51. package/reference/sdlc/decide.playbook/decide.gears.md +28 -21
  52. package/reference/sdlc/decide.playbook/decide.playbook.d.ts +11 -59
  53. package/reference/sdlc/decide.playbook/decide.playbook.js +155 -3696
  54. package/reference/sdlc/decide.playbook/decide.playbook.ts +186 -5244
  55. package/reference/sdlc/decide.playbook/decide.registry.d.ts +8 -4
  56. package/reference/sdlc/decide.playbook/decide.registry.js +2 -2
  57. package/reference/sdlc/decide.playbook/decide.registry.ts +13 -6
  58. package/reference/sdlc/dev.playbook/dev.fsm.d.ts +116 -134
  59. package/reference/sdlc/dev.playbook/dev.fsm.js +517 -575
  60. package/reference/sdlc/dev.playbook/dev.fsm.ts +667 -787
  61. package/reference/sdlc/dev.playbook/dev.gears.md +52 -54
  62. package/reference/sdlc/dev.playbook/dev.playbook.d.ts +13 -10
  63. package/reference/sdlc/dev.playbook/dev.playbook.js +128 -120
  64. package/reference/sdlc/dev.playbook/dev.playbook.ts +143 -193
  65. package/reference/sdlc/dev.playbook/dev.registry.d.ts +3 -2
  66. package/reference/sdlc/dev.playbook/dev.registry.ts +6 -2
  67. package/reference/sdlc/pr.playbook/pr.fsm.d.ts +95 -88
  68. package/reference/sdlc/pr.playbook/pr.fsm.js +461 -518
  69. package/reference/sdlc/pr.playbook/pr.fsm.ts +533 -663
  70. package/reference/sdlc/pr.playbook/pr.gears.md +47 -63
  71. package/reference/sdlc/pr.playbook/pr.playbook.d.ts +13 -23
  72. package/reference/sdlc/pr.playbook/pr.playbook.js +118 -112
  73. package/reference/sdlc/pr.playbook/pr.playbook.ts +136 -190
  74. package/reference/sdlc/pr.playbook/pr.registry.d.ts +3 -2
  75. package/reference/sdlc/pr.playbook/pr.registry.ts +4 -2
  76. package/reference/sdlc/review.md +3 -3
  77. package/reference/sdlc/review.playbook/review.fsm.d.ts +150 -116
  78. package/reference/sdlc/review.playbook/review.fsm.js +479 -550
  79. package/reference/sdlc/review.playbook/review.fsm.ts +717 -662
  80. package/reference/sdlc/review.playbook/review.gears.md +32 -29
  81. package/reference/sdlc/review.playbook/review.playbook.d.ts +12 -12
  82. package/reference/sdlc/review.playbook/review.playbook.js +180 -155
  83. package/reference/sdlc/review.playbook/review.playbook.ts +190 -230
  84. package/reference/sdlc/review.playbook/review.registry.d.ts +6 -5
  85. package/reference/sdlc/review.playbook/review.registry.js +3 -3
  86. package/reference/sdlc/review.playbook/review.registry.ts +9 -5
  87. package/slc/gears2fsm.md +77 -8
  88. package/slc/link.md +127 -60
  89. package/slc/materialize-link.mjs +51 -13
  90. package/slc/optimize.md +4 -1
  91. package/slc/prefix-prompts.mjs +29 -28
  92. package/slc/prefix.md +27 -24
  93. package/slc/slc.pin-inputs.json +11 -2
  94. package/slc/text2gears.md +71 -11
  95. package/src/runtime.d.ts +46 -2
  96. package/src/runtime.ts +42 -2
  97. package/src/xstate-playbook-runtime.d.ts +63 -23
  98. package/src/xstate-playbook-runtime.js +1637 -475
  99. package/src/xstate-playbook-runtime.ts +2105 -532
  100. package/src/xstate-runtime.d.ts +3 -4
  101. package/src/xstate-runtime.js +52 -0
  102. package/src/xstate-runtime.ts +59 -4
@@ -1,9 +1,17 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
2
  // SPDX-FileCopyrightText: 2026 SubLang International <https://sublang.ai>
3
3
  //
4
- // Generated by slc/gears2fsm.md from ./code.gears.md.
4
+ // FSM object artifact compiled from ./code.gears.md by gears2fsm.
5
+ // It declares only the machine, its actor contracts, and typed inputs; the
6
+ // linked runtime supplies the `player` and `playbook` actor implementations.
5
7
 
6
8
  import { assign, fromPromise, setup } from 'xstate';
9
+ import { validatePlaybookCallResult } from '@sublang/playbook/xstate-runtime';
10
+ import type { PlaybookCallResult } from '@sublang/playbook/runtime';
11
+
12
+ // ---------------------------------------------------------------------------
13
+ // Shared JSON value type (exact readonly variance of the shared boundary)
14
+ // ---------------------------------------------------------------------------
7
15
 
8
16
  export type JsonValue =
9
17
  | null
@@ -13,40 +21,83 @@ export type JsonValue =
13
21
  | readonly JsonValue[]
14
22
  | { readonly [key: string]: JsonValue };
15
23
 
16
- export type CodeStateId =
17
- | 'runFirstPhase'
18
- | 'reviewFirstCommit'
19
- | 'runIrTask'
20
- | 'reviewIrTask';
24
+ // ---------------------------------------------------------------------------
25
+ // Identities
26
+ // ---------------------------------------------------------------------------
27
+
28
+ /** Canonical lowercase local id of the source role Coder. */
29
+ export type CodeRole = 'coder';
30
+
31
+ /** One role-id array per parallel group; CODE declares no parallel group. */
32
+ export const concurrentRoleSets: readonly (readonly CodeRole[])[] = [];
33
+
34
+ /** Delegated-player working leaves (CODE-1, CODE-3). */
35
+ export type WorkingStateId = 'firstPhase' | 'irTaskPhase';
36
+
37
+ /** Working leaves that may suspend for, and resume from, a Boss reply. */
38
+ export type ResumableStateId = WorkingStateId;
39
+
40
+ /** Root `BOSS_INTERRUPT` targets. */
41
+ export type JumpableStateId = WorkingStateId;
42
+
43
+ /** Nested `review` call states (CODE-2, CODE-4). */
44
+ export type ReviewStateId = 'reviewNewIntentPhase' | 'reviewIrTaskPhase';
21
45
 
22
46
  export type CodeSourceItem = 'CODE-1' | 'CODE-2' | 'CODE-3' | 'CODE-4';
23
47
 
24
- export type CodePhase =
25
- | 'direct'
26
- | 'ir-created'
27
- | 'ir-task-more'
28
- | 'ir-task-final';
48
+ /** The accepted phase outcome whose `review` call decides the next step. */
49
+ export type PhaseOutcome =
50
+ | 'directCommit'
51
+ | 'irCommit'
52
+ | 'moreTasks'
53
+ | 'finalTask';
54
+
55
+ /** The literal nested-call target shared by both call states and guards. */
56
+ const REVIEW_PLAYBOOK_ID = 'review';
29
57
 
30
- export type PendingBossQuestion = {
31
- readonly questionId: 'runFirstPhase' | 'runIrTask';
32
- readonly resumeStateId: 'runFirstPhase' | 'runIrTask';
58
+ // ---------------------------------------------------------------------------
59
+ // Boss-reply suspension (scalar form: one active player task at a time)
60
+ // ---------------------------------------------------------------------------
61
+
62
+ export interface PendingBossQuestion {
63
+ readonly questionId: ResumableStateId;
64
+ readonly resumeStateId: ResumableStateId;
33
65
  readonly sourceItem: 'CODE-1' | 'CODE-3';
34
- readonly asker: { readonly kind: 'role'; readonly roleId: 'coder' };
66
+ readonly asker: { readonly kind: 'role'; readonly roleId: CodeRole };
35
67
  readonly question: string;
36
- };
68
+ }
37
69
 
38
- export type PlayerInput = {
39
- readonly stateId: 'runFirstPhase' | 'runIrTask';
40
- readonly role: 'coder';
41
- readonly sourceItem: 'CODE-1' | 'CODE-3';
70
+ // ---------------------------------------------------------------------------
71
+ // Delegated player actor contract
72
+ // ---------------------------------------------------------------------------
73
+
74
+ interface PlayerInputBase {
75
+ readonly role: CodeRole;
76
+ /** The source item's full final prompt, verbatim. */
42
77
  readonly prompt: string;
78
+ /** This state's local result contract: guard name → description. */
43
79
  readonly result: Readonly<Record<string, string>>;
44
- readonly callerInput: string;
80
+ /** `<caller-input>`: the caller's complete coding request. */
81
+ readonly callerInput?: string;
82
+ /** `<run-results>`: relevant run results; empty when none were supplied. */
45
83
  readonly runResults: string;
46
- readonly irNumber?: string;
47
84
  readonly pendingBossQuestion?: PendingBossQuestion;
48
85
  readonly bossReply?: string;
49
- };
86
+ }
87
+
88
+ export interface FirstPhasePlayerInput extends PlayerInputBase {
89
+ readonly stateId: 'firstPhase';
90
+ readonly sourceItem: 'CODE-1';
91
+ }
92
+
93
+ export interface IrTaskPhasePlayerInput extends PlayerInputBase {
94
+ readonly stateId: 'irTaskPhase';
95
+ readonly sourceItem: 'CODE-3';
96
+ /** `<ir-number>`: the IR whose next unfinished task this phase implements. */
97
+ readonly irNumber?: string;
98
+ }
99
+
100
+ export type PlayerInput = FirstPhasePlayerInput | IrTaskPhasePlayerInput;
50
101
 
51
102
  export type PlayerOutput =
52
103
  | {
@@ -74,91 +125,119 @@ export type PlayerOutput =
74
125
  readonly irNumber: string;
75
126
  readonly irTask: string;
76
127
  }
77
- | {
78
- readonly guard: 'needsBossReply';
79
- readonly question: string;
80
- };
128
+ | { readonly guard: 'needsBossReply'; readonly question: string };
81
129
 
82
- export type PlaybookInput = {
83
- readonly stateId: 'reviewFirstCommit' | 'reviewIrTask';
130
+ // ---------------------------------------------------------------------------
131
+ // Nested playbook actor contract
132
+ // ---------------------------------------------------------------------------
133
+
134
+ export interface PlaybookInput {
135
+ readonly stateId: ReviewStateId;
84
136
  readonly sourceItem: 'CODE-2' | 'CODE-4';
85
- readonly playbookId: 'review';
137
+ readonly playbookId: typeof REVIEW_PLAYBOOK_ID;
86
138
  readonly text: string;
87
- };
139
+ }
88
140
 
89
- export type ReviewOutput = {
141
+ /** A successful call yields the child's own JSON-safe machine output. */
142
+ export type PlaybookOutput = JsonValue | undefined;
143
+
144
+ /** Public output interface of the packaged builtin `review`. */
145
+ export interface ReviewPassOutput {
90
146
  readonly noUnsettledFindings: true;
91
- /**
92
- * Exact repository revision at which the review scope was evaluated:
93
- * the last review-fix commit when one landed, otherwise the clean
94
- * round's receipt-observed HEAD (DR-045). Always present.
95
- */
96
147
  readonly evaluatedRevision: string;
97
- };
148
+ }
98
149
 
99
- export type CompactError = {
150
+ export interface CompactError {
100
151
  readonly name: string;
101
152
  readonly message: string;
102
- };
153
+ }
154
+
155
+ export interface ErrorRecord {
156
+ readonly name: string;
157
+ readonly message: string;
158
+ readonly stack?: string;
159
+ }
103
160
 
104
- export type CodePlaybookOutput =
161
+ /** Sanitized completed-result evidence of the `review` call that ended CODE. */
162
+ export type CompletedReviewResult =
163
+ | {
164
+ readonly playbookId: typeof REVIEW_PLAYBOOK_ID;
165
+ readonly status: 'ok';
166
+ readonly output?: JsonValue;
167
+ }
168
+ | {
169
+ readonly playbookId: typeof REVIEW_PLAYBOOK_ID;
170
+ readonly status: 'aborted' | 'error';
171
+ readonly error: CompactError;
172
+ };
173
+
174
+ // ---------------------------------------------------------------------------
175
+ // Machine input, context, events, and output
176
+ // ---------------------------------------------------------------------------
177
+
178
+ /** Public output interface of the packaged builtin `code`. */
179
+ export type CodeOutput =
105
180
  | {
106
181
  readonly status: 'complete';
107
- /** Exact identity of the last CODE-owned commit. */
108
182
  readonly lastCodeCommit: string;
109
- /** Exact repository revision the final passing review evaluated. */
110
183
  readonly finalEvaluatedRevision: string;
111
- /** Every phase's review passed with no unsettled findings. */
112
184
  readonly allReviewsPassed: true;
113
185
  }
114
186
  | {
115
187
  readonly status: 'review-failed';
116
- /** Exact identity of the last CODE-owned commit. */
117
188
  readonly lastCodeCommit: string;
118
189
  readonly error: CompactError;
119
190
  };
120
191
 
121
- export type CodingInput = {
192
+ export interface CodeInput {
193
+ /** Optional seed for `<run-results>`; `START_CODE` may supply its own. */
122
194
  readonly runResults?: string;
123
- };
195
+ }
124
196
 
125
- export type CodingContext = {
197
+ export interface CodeContext {
198
+ /** `<run-results>`: relevant run results relayed to every phase. */
126
199
  readonly runResults: string;
200
+ /** `<caller-input>`: the caller's coding request and relevant context. */
127
201
  readonly callerInput?: string;
202
+ /** `<code-commit>`: the accepted `latestCommit` of the latest phase. */
203
+ readonly codeCommit?: string;
204
+ /** `<coder-output>`: Coder's verbatim final text for the latest phase. */
128
205
  readonly coderOutput?: string;
129
- readonly latestCommit?: string;
206
+ /** `<ir-number>`: the created or continued IR. */
130
207
  readonly irNumber?: string;
208
+ /** `<ir-task>`: the IR task the latest IR-task phase implemented. */
131
209
  readonly irTask?: string;
210
+ /** The accepted outcome of the phase under review. */
211
+ readonly phaseOutcome?: PhaseOutcome;
212
+ /** Revision evaluated by the latest passing review. */
132
213
  readonly evaluatedRevision?: string;
133
- readonly phase?: CodePhase;
134
- readonly completion?: 'complete' | 'review-failed';
214
+ /** Compact failure CODE reports when review did not pass a phase. */
135
215
  readonly reviewError?: CompactError;
136
- readonly lastError?: unknown;
216
+ /** Sanitized evidence of the review result that ended CODE. */
217
+ readonly reviewEvidence?: CompletedReviewResult;
218
+ readonly lastError?: ErrorRecord;
137
219
  readonly pendingBossQuestion?: PendingBossQuestion;
138
220
  readonly bossReply?: string;
139
- };
221
+ }
140
222
 
141
- export type CodingEvent =
142
- | { readonly type: 'START_CODE'; readonly callerInput: string }
223
+ export type CodeEvent =
224
+ | {
225
+ readonly type: 'START_CODE';
226
+ readonly callerInput: string;
227
+ readonly runResults?: string;
228
+ }
229
+ | { readonly type: 'BOSS_INTERRUPT'; readonly targetId: JumpableStateId }
143
230
  | {
144
231
  readonly type: 'BOSS_REPLY';
145
232
  readonly answer: string;
146
- readonly questionId?: 'runFirstPhase' | 'runIrTask';
233
+ readonly questionId?: ResumableStateId;
147
234
  };
148
235
 
149
- const APPENDED_PHASE_PROMPT = [
150
- 'Keep to the original intent and follow what it asks.',
151
- 'Do not re-run tests or builds whose inputs have not changed since any previous reported run.',
152
- "Make the phase's minimal changes and then one new commit, following @specs/packages/git.md; never amend an existing commit.",
153
- 'Make the commit message explain concisely what changed and why, including relevant verification.',
154
- 'Identify every new commit you make.',
155
- 'Credit every AI that contributed to this commit: Coder <coder-llm>.',
156
- ];
236
+ // ---------------------------------------------------------------------------
237
+ // Prompts and result contracts (verbatim from code.gears.md)
238
+ // ---------------------------------------------------------------------------
157
239
 
158
240
  const FIRST_PHASE_PROMPT = [
159
- '> Original request: <caller-input>',
160
- '> Run results: <run-results>',
161
- '',
162
241
  'First determine whether the coding request starts a new coding intent or continues an existing IR with unfinished work.',
163
242
  'If the request may continue an existing IR but does not identify it unambiguously, ask Boss before changing files.',
164
243
  '',
@@ -174,473 +253,556 @@ const FIRST_PHASE_PROMPT = [
174
253
  '',
175
254
  'Consult @specs/map.md for relevant context and @specs/meta.md for spec requirements, if needed.',
176
255
  '',
177
- ...APPENDED_PHASE_PROMPT,
178
- ].join('\n');
179
-
180
- const IR_TASK_PROMPT = [
256
+ 'Keep to the original intent and follow what it asks.',
257
+ 'Do not re-run tests or builds whose inputs have not changed since any previous reported run.',
258
+ "Make the phase's minimal changes and then one new commit, following @specs/packages/git.md; never amend an existing commit.",
259
+ 'Make the commit message explain concisely what changed and why, including relevant verification.',
260
+ 'Identify every new commit you make.',
261
+ 'Credit every AI that contributed to this commit: Coder <coder-llm>.',
262
+ '',
181
263
  '> Original request: <caller-input>',
182
- '> IR number: <ir-number>',
183
264
  '> Run results: <run-results>',
184
- '',
265
+ ].join('\n');
266
+
267
+ const IR_TASK_PHASE_PROMPT = [
185
268
  'Read the identified IR and implement exactly its next unfinished task, including corresponding tests or specs if any.',
186
269
  'Do not implement a later task in this phase.',
187
270
  "Mark the IR's progress and deliverables when relevant.",
188
271
  'If the IR will be finished after this phase, double-check that all acceptance criteria are met.',
189
272
  '',
190
- ...APPENDED_PHASE_PROMPT,
273
+ 'Keep to the original intent and follow what it asks.',
274
+ 'Do not re-run tests or builds whose inputs have not changed since any previous reported run.',
275
+ "Make the phase's minimal changes and then one new commit, following @specs/packages/git.md; never amend an existing commit.",
276
+ 'Make the commit message explain concisely what changed and why, including relevant verification.',
277
+ 'Identify every new commit you make.',
278
+ 'Credit every AI that contributed to this commit: Coder <coder-llm>.',
279
+ '',
280
+ '> Original request: <caller-input>',
281
+ '> IR number: <ir-number>',
282
+ '> Run results: <run-results>',
283
+ ].join('\n');
284
+
285
+ const REVIEW_NEW_INTENT_PHASE_TEMPLATE = [
286
+ '> Original intent: <caller-input>',
287
+ '> Review scope: the commit <code-commit> from this coding phase and its resulting repository state.',
288
+ '> Coder output: <coder-output>',
289
+ ].join('\n');
290
+
291
+ const REVIEW_IR_TASK_PHASE_TEMPLATE = [
292
+ '> Original intent: <caller-input>',
293
+ '> Review scope: the commit <code-commit> from this coding phase and its resulting repository state.',
294
+ '> Coder output: <coder-output>',
295
+ '',
296
+ '> Current IR task: <ir-task>',
191
297
  ].join('\n');
192
298
 
193
- const NEEDS_BOSS_REPLY_RESULT =
299
+ const NEEDS_BOSS_REPLY_DESCRIPTION =
194
300
  "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>`.";
195
301
 
196
- const FIRST_PHASE_RESULTS = {
302
+ const FIRST_PHASE_RESULT = {
197
303
  directCommit:
198
- 'Coder completed and committed the direct implementation phase. Output shall include `coderOutput: <verbatim final text>` and `latestCommit: <commit identity>`.',
304
+ "Coder completed the new coding intent as one direct implementation phase and made its one new commit; Coder's result affirmatively supports this outcome, and no fixed presentation format of the reply is required. Output shall include `coderOutput: <verbatim final text>` and `latestCommit: <commit identity>`.",
199
305
  irCommit:
200
- 'Coder created and committed a new IR without implementing an IR task. Output shall include `coderOutput: <verbatim final text>`, `latestCommit: <commit identity>`, and `irNumber` identifying the created IR.',
306
+ "Coder decomposed the new coding intent into a new IR, implemented no IR task, and made its one new commit; Coder's result affirmatively supports this outcome, and no fixed presentation format of the reply is required. Output shall include `coderOutput: <verbatim final text>`, `latestCommit: <commit identity>`, and `irNumber` identifying the created IR.",
201
307
  moreTasks:
202
- 'Coder continued an existing IR, completed and committed exactly its next unfinished task, and at least one task remains. Output shall include `coderOutput: <verbatim final text>`, `latestCommit: <commit identity>`, `irNumber` identifying the continued IR, and `irTask` naming the implemented task.',
308
+ "Coder continued an existing IR, implemented exactly its next unfinished task, made its one new commit, and at least one IR task remains unfinished; Coder's result affirmatively supports this outcome, and no fixed presentation format of the reply is required. Output shall include `coderOutput: <verbatim final text>`, `latestCommit: <commit identity>`, `irNumber` identifying the continued IR, and `irTask` naming the implemented task.",
203
309
  finalTask:
204
- 'Coder continued an existing IR and completed and committed its final task. Output shall include `coderOutput: <verbatim final text>`, `latestCommit: <commit identity>`, `irNumber` identifying the continued IR, and `irTask` naming the implemented task.',
205
- needsBossReply: NEEDS_BOSS_REPLY_RESULT,
310
+ "Coder continued an existing IR, implemented its final unfinished task, and made its one new commit; Coder's result affirmatively supports this outcome, and no fixed presentation format of the reply is required. Output shall include `coderOutput: <verbatim final text>`, `latestCommit: <commit identity>`, `irNumber` identifying the continued IR, and `irTask` naming the implemented task.",
311
+ needsBossReply: NEEDS_BOSS_REPLY_DESCRIPTION,
206
312
  } as const;
207
313
 
208
- const IR_TASK_RESULTS = {
314
+ const IR_TASK_PHASE_RESULT = {
209
315
  moreTasks:
210
- "Coder completed and committed exactly the IR's next unfinished task and at least one task remains. Output shall include `coderOutput: <verbatim final text>`, `latestCommit: <commit identity>`, `irNumber` identifying the continued IR, and `irTask` naming the implemented task.",
316
+ "Coder implemented exactly the IR's next unfinished task, made its one new commit, and at least one IR task remains unfinished; Coder's result affirmatively supports this outcome, and no fixed presentation format of the reply is required. Output shall include `coderOutput: <verbatim final text>`, `latestCommit: <commit identity>`, `irNumber` identifying the IR, and `irTask` naming the implemented task.",
211
317
  finalTask:
212
- "Coder completed and committed the IR's final task. Output shall include `coderOutput: <verbatim final text>`, `latestCommit: <commit identity>`, `irNumber` identifying the continued IR, and `irTask` naming the implemented task.",
213
- needsBossReply: NEEDS_BOSS_REPLY_RESULT,
318
+ "Coder implemented the IR's final unfinished task and made its one new commit; Coder's result affirmatively supports this outcome, and no fixed presentation format of the reply is required. Output shall include `coderOutput: <verbatim final text>`, `latestCommit: <commit identity>`, `irNumber` identifying the IR, and `irTask` naming the implemented task.",
319
+ needsBossReply: NEEDS_BOSS_REPLY_DESCRIPTION,
214
320
  } as const;
215
321
 
216
322
  const STATE_DESCRIPTIONS = {
217
- ready: 'Waiting for a coding intent.',
218
- runFirstPhase:
219
- 'Coder is running the first coding phase: a direct implementation, a new intent record, or an existing intent-record task.',
220
- reviewFirstCommit:
221
- 'The REVIEW playbook is checking the direct or new-IR phase commit.',
222
- runIrTask: 'Coder is implementing exactly one unfinished intent-record task.',
223
- reviewIrTask: 'The REVIEW playbook is checking the latest task commit.',
224
- awaitBossReply: 'Waiting for Boss to answer Coder.',
225
- failed: 'The coding workflow failed and is waiting for a new coding intent.',
226
- reportedReviewFailure:
227
- 'The coding workflow reported a REVIEW failure and the last code-owned commit.',
228
- done: "The coding workflow completed after every phase's REVIEW passed with no unsettled findings.",
323
+ ready: 'Waiting for a coding request.',
324
+ firstPhase:
325
+ 'Coder runs the first coding phase: a direct implementation, a new IR, or the next task of an existing IR.',
326
+ reviewNewIntentPhase:
327
+ 'REVIEW examines the commit of the direct implementation or new-IR phase.',
328
+ irTaskPhase: "Coder implements the identified IR's next unfinished task.",
329
+ reviewIrTaskPhase: 'REVIEW examines the commit of the IR-task phase.',
330
+ awaitBossReply: "Waiting for Boss to answer the acting agent's question.",
331
+ failed:
332
+ 'CODE parked after a control-plane failure and waits for Boss to restart or resume it.',
333
+ reviewFailed:
334
+ 'CODE stopped because review did not pass a phase and reports the failure with its last code-owned commit.',
335
+ done: "CODE completed: every phase's review passed with no unsettled findings.",
229
336
  } as const;
230
337
 
231
- function playbookMeta<StateId extends keyof typeof STATE_DESCRIPTIONS>(
232
- stateId: StateId,
233
- role?: 'coder',
234
- ) {
235
- return {
236
- playbook: {
237
- stateId,
238
- description: STATE_DESCRIPTIONS[stateId],
239
- ...(role === undefined ? {} : { role }),
240
- },
241
- };
242
- }
338
+ type StateKey = keyof typeof STATE_DESCRIPTIONS;
243
339
 
244
- // DR-048: a final state additionally declares whether its outcome means the
245
- // workflow succeeded or failed, so a caller learns that from the machine
246
- // rather than from CODE's output fields.
247
- function terminalMeta<StateId extends keyof typeof STATE_DESCRIPTIONS>(
248
- stateId: StateId,
249
- terminal: 'success' | 'failure',
250
- ) {
251
- return {
252
- playbook: {
253
- stateId,
254
- description: STATE_DESCRIPTIONS[stateId],
255
- terminal,
256
- },
257
- };
258
- }
259
-
260
- function isRecord(value: unknown): value is Record<string, unknown> {
261
- if (value === null || typeof value !== 'object' || Array.isArray(value)) {
262
- return false;
263
- }
264
- const prototype = Object.getPrototypeOf(value);
265
- return prototype === Object.prototype || prototype === null;
266
- }
340
+ const RESUME_METADATA = {
341
+ firstPhase: { sourceItem: 'CODE-1' },
342
+ irTaskPhase: { sourceItem: 'CODE-3' },
343
+ } as const satisfies Record<
344
+ ResumableStateId,
345
+ { readonly sourceItem: PendingBossQuestion['sourceItem'] }
346
+ >;
267
347
 
268
- function isNonEmptyString(value: unknown): value is string {
269
- return typeof value === 'string' && value.trim().length > 0;
270
- }
271
-
272
- function playerOutput(event: unknown): Record<string, unknown> | undefined {
273
- if (!isRecord(event)) return undefined;
274
- return isRecord(event.output) ? event.output : undefined;
275
- }
276
-
277
- function outputGuard(event: unknown, guard: PlayerOutput['guard']): boolean {
278
- return playerOutput(event)?.guard === guard;
279
- }
280
-
281
- function outputString(event: unknown, field: string): string | undefined {
282
- const value = playerOutput(event)?.[field];
283
- return isNonEmptyString(value) ? value : undefined;
284
- }
285
-
286
- function hasCommittedOutput(event: unknown): boolean {
287
- return (
288
- outputString(event, 'coderOutput') !== undefined &&
289
- outputString(event, 'latestCommit') !== undefined
290
- );
291
- }
292
-
293
- function hasIrTaskOutput(event: unknown): boolean {
294
- return (
295
- hasCommittedOutput(event) &&
296
- outputString(event, 'irNumber') !== undefined &&
297
- outputString(event, 'irTask') !== undefined
298
- );
299
- }
300
-
301
- function isDirectCommit({ event }: { event: unknown }): boolean {
302
- return (
303
- outputGuard(event, 'directCommit') &&
304
- hasCommittedOutput(event)
305
- );
306
- }
348
+ const REVIEW_NOT_PASSED: CompactError = {
349
+ name: 'ReviewNotPassed',
350
+ message:
351
+ 'review returned a terminal result that does not establish that the supplied scope was evaluated with no unsettled findings.',
352
+ };
307
353
 
308
- function isIrCommit({ event }: { event: unknown }): boolean {
309
- return (
310
- outputGuard(event, 'irCommit') &&
311
- hasCommittedOutput(event) &&
312
- outputString(event, 'irNumber') !== undefined
313
- );
314
- }
354
+ // ---------------------------------------------------------------------------
355
+ // Structural helpers
356
+ // ---------------------------------------------------------------------------
315
357
 
316
- function isMoreTasks({ event }: { event: unknown }): boolean {
317
- return (
318
- outputGuard(event, 'moreTasks') &&
319
- hasIrTaskOutput(event)
320
- );
321
- }
358
+ const isRecord = (
359
+ value: unknown,
360
+ ): value is Readonly<Record<string, unknown>> =>
361
+ typeof value === 'object' && value !== null && !Array.isArray(value);
322
362
 
323
- function isFinalTask({ event }: { event: unknown }): boolean {
324
- return (
325
- outputGuard(event, 'finalTask') &&
326
- hasIrTaskOutput(event)
327
- );
328
- }
363
+ const isNonEmptyString = (value: unknown): value is string =>
364
+ typeof value === 'string' && value.trim().length > 0;
329
365
 
330
- function needsBossReply({ event }: { event: unknown }): boolean {
331
- return (
332
- outputGuard(event, 'needsBossReply') &&
333
- outputString(event, 'question') !== undefined
334
- );
335
- }
366
+ const doneOutputOf = (event: unknown): unknown =>
367
+ isRecord(event) ? event.output : undefined;
336
368
 
337
- function reviewOutput(event: unknown): unknown {
338
- return isRecord(event) ? event.output : undefined;
339
- }
369
+ const actorErrorOf = (event: unknown): unknown =>
370
+ isRecord(event) ? event.error : undefined;
340
371
 
341
- function isReviewApprovedValue(value: unknown): value is ReviewOutput {
342
- if (!isRecord(value)) return false;
343
- if (
344
- Reflect.ownKeys(value).some(
345
- (key) =>
346
- typeof key !== 'string' ||
347
- (key !== 'noUnsettledFindings' && key !== 'evaluatedRevision'),
348
- )
349
- ) {
350
- return false;
372
+ /** Narrows a player done event to the declared player output contract. */
373
+ function playerOutputOf(event: unknown): PlayerOutput | undefined {
374
+ const output = doneOutputOf(event);
375
+ if (!isRecord(output)) return undefined;
376
+ switch (output.guard) {
377
+ case 'directCommit':
378
+ return isNonEmptyString(output.coderOutput) &&
379
+ isNonEmptyString(output.latestCommit)
380
+ ? {
381
+ guard: 'directCommit',
382
+ coderOutput: output.coderOutput,
383
+ latestCommit: output.latestCommit,
384
+ }
385
+ : undefined;
386
+ case 'irCommit':
387
+ return isNonEmptyString(output.coderOutput) &&
388
+ isNonEmptyString(output.latestCommit) &&
389
+ isNonEmptyString(output.irNumber)
390
+ ? {
391
+ guard: 'irCommit',
392
+ coderOutput: output.coderOutput,
393
+ latestCommit: output.latestCommit,
394
+ irNumber: output.irNumber,
395
+ }
396
+ : undefined;
397
+ case 'moreTasks':
398
+ case 'finalTask':
399
+ return isNonEmptyString(output.coderOutput) &&
400
+ isNonEmptyString(output.latestCommit) &&
401
+ isNonEmptyString(output.irNumber) &&
402
+ isNonEmptyString(output.irTask)
403
+ ? {
404
+ guard: output.guard,
405
+ coderOutput: output.coderOutput,
406
+ latestCommit: output.latestCommit,
407
+ irNumber: output.irNumber,
408
+ irTask: output.irTask,
409
+ }
410
+ : undefined;
411
+ case 'needsBossReply':
412
+ return isNonEmptyString(output.question)
413
+ ? { guard: 'needsBossReply', question: output.question }
414
+ : undefined;
415
+ default:
416
+ return undefined;
351
417
  }
352
- return (
353
- value.noUnsettledFindings === true &&
354
- isNonEmptyString(value.evaluatedRevision)
355
- );
356
- }
357
-
358
- function reviewApprovedFor(
359
- phase: CodePhase,
360
- ): (args: { context: CodingContext; event: unknown }) => boolean {
361
- return ({ context, event }) =>
362
- context.phase === phase && isReviewApprovedValue(reviewOutput(event));
363
- }
364
-
365
- function isStateValue(value: unknown, ancestors = new Set<object>()): boolean {
366
- if (typeof value === 'string') return true;
367
- if (!isRecord(value) || ancestors.has(value)) return false;
368
- const next = new Set(ancestors).add(value);
369
- return Reflect.ownKeys(value).every(
370
- (key) =>
371
- typeof key === 'string' &&
372
- Object.prototype.propertyIsEnumerable.call(value, key) &&
373
- isStateValue(value[key], next),
374
- );
375
418
  }
376
419
 
377
- function isStringArray(value: unknown): value is readonly string[] {
378
- return (
379
- Array.isArray(value) &&
380
- Reflect.ownKeys(value).length === value.length + 1 &&
381
- Reflect.ownKeys(value).every((key) => {
382
- if (key === 'length') return true;
383
- return (
384
- typeof key === 'string' &&
385
- /^(0|[1-9][0-9]*)$/.test(key) &&
386
- Number(key) < value.length
387
- );
388
- }) &&
389
- value.every((entry) => isNonEmptyString(entry)) &&
390
- new Set(value).size === value.length
391
- );
392
- }
393
-
394
- function isPlaybookState(value: unknown): boolean {
395
- if (!isRecord(value)) return false;
396
- const allowed = new Set([
397
- 'value',
398
- 'activeStateIds',
399
- 'tags',
400
- 'status',
401
- 'quiescent',
402
- 'stateId',
403
- ]);
404
- if (
405
- Reflect.ownKeys(value).some(
406
- (key) => typeof key !== 'string' || !allowed.has(key),
407
- ) ||
408
- !Object.prototype.hasOwnProperty.call(value, 'value') ||
409
- !Object.prototype.hasOwnProperty.call(value, 'activeStateIds') ||
410
- !Object.prototype.hasOwnProperty.call(value, 'tags') ||
411
- !Object.prototype.hasOwnProperty.call(value, 'status') ||
412
- !Object.prototype.hasOwnProperty.call(value, 'quiescent')
413
- ) {
414
- return false;
420
+ /**
421
+ * Strict JSON validation: null, booleans, finite numbers, strings, exact
422
+ * arrays, and plain records of enumerable own data properties. Cycle
423
+ * detection tracks only the active recursion path.
424
+ */
425
+ function isJsonValue(value: unknown, path: Set<object> = new Set()): value is JsonValue {
426
+ if (value === null) return true;
427
+ switch (typeof value) {
428
+ case 'boolean':
429
+ case 'string':
430
+ return true;
431
+ case 'number':
432
+ return Number.isFinite(value);
433
+ case 'object':
434
+ break;
435
+ default:
436
+ return false;
437
+ }
438
+ const container = value as object;
439
+ if (path.has(container)) return false;
440
+ path.add(container);
441
+ try {
442
+ if (Array.isArray(container)) {
443
+ if (Object.getPrototypeOf(container) !== Array.prototype) return false;
444
+ const keys = Reflect.ownKeys(container);
445
+ if (keys.length !== container.length + 1) return false;
446
+ const lengthDescriptor = Object.getOwnPropertyDescriptor(container, 'length');
447
+ if (
448
+ lengthDescriptor === undefined ||
449
+ lengthDescriptor.configurable !== false ||
450
+ lengthDescriptor.enumerable !== false ||
451
+ lengthDescriptor.value !== container.length
452
+ ) {
453
+ return false;
454
+ }
455
+ for (let index = 0; index < container.length; index += 1) {
456
+ const descriptor = Object.getOwnPropertyDescriptor(container, String(index));
457
+ if (
458
+ descriptor === undefined ||
459
+ !descriptor.enumerable ||
460
+ !('value' in descriptor) ||
461
+ !isJsonValue(descriptor.value, path)
462
+ ) {
463
+ return false;
464
+ }
465
+ }
466
+ return true;
467
+ }
468
+ const prototype = Object.getPrototypeOf(container);
469
+ if (prototype !== Object.prototype && prototype !== null) return false;
470
+ for (const key of Reflect.ownKeys(container)) {
471
+ if (typeof key !== 'string') return false;
472
+ const descriptor = Object.getOwnPropertyDescriptor(container, key);
473
+ if (
474
+ descriptor === undefined ||
475
+ !descriptor.enumerable ||
476
+ !('value' in descriptor) ||
477
+ !isJsonValue(descriptor.value, path)
478
+ ) {
479
+ return false;
480
+ }
481
+ }
482
+ return true;
483
+ } finally {
484
+ path.delete(container);
415
485
  }
416
- return (
417
- isStateValue(value.value) &&
418
- isStringArray(value.activeStateIds) &&
419
- isStringArray(value.tags) &&
420
- (value.status === 'active' ||
421
- value.status === 'done' ||
422
- value.status === 'error' ||
423
- value.status === 'stopped') &&
424
- typeof value.quiescent === 'boolean' &&
425
- (!Object.prototype.hasOwnProperty.call(value, 'stateId') ||
426
- (isNonEmptyString(value.stateId) &&
427
- value.activeStateIds.length === 1 &&
428
- value.activeStateIds[0] === value.stateId))
429
- );
430
486
  }
431
487
 
432
- function nestedResultFromError(error: unknown): Record<string, unknown> | undefined {
433
- if (!(error instanceof Error)) return undefined;
434
- const result = (error as Error & { readonly result?: unknown }).result;
435
- return isRecord(result) ? result : undefined;
488
+ /** Normalizes any thrown or malformed value to a JSON-safe error record. */
489
+ function errorRecord(value: unknown): ErrorRecord {
490
+ if (value instanceof Error) {
491
+ return {
492
+ name: value.name || 'Error',
493
+ message: value.message,
494
+ ...(typeof value.stack === 'string' ? { stack: value.stack } : {}),
495
+ };
496
+ }
497
+ if (isRecord(value) && typeof value.message === 'string') {
498
+ return {
499
+ name: isNonEmptyString(value.name) ? value.name : 'Error',
500
+ message: value.message,
501
+ };
502
+ }
503
+ return { name: 'Error', message: String(value) };
436
504
  }
437
505
 
438
- type AuthoredReviewFailure =
439
- | {
440
- readonly status: 'aborted' | 'error';
441
- readonly error?: CompactError;
442
- }
443
- // DR-048: a child that completed at an authored failure terminal. The
444
- // bridge rejects it through the same error path, so CODE recognizes the
445
- // failure from REVIEW's own machine and never from its output fields.
446
- | {
447
- readonly status: 'ok';
448
- readonly output?: JsonValue;
449
- };
506
+ // ---------------------------------------------------------------------------
507
+ // Nested `review` results
508
+ // ---------------------------------------------------------------------------
450
509
 
451
- function isFailureTerminalRecord(value: unknown): boolean {
452
- if (!isRecord(value)) return false;
453
- const allowed = new Set(['stateId', 'kind', 'description']);
454
- return (
455
- Reflect.ownKeys(value).every(
456
- (key) => typeof key === 'string' && allowed.has(key),
457
- ) &&
458
- isNonEmptyString(value.stateId) &&
459
- value.kind === 'failure' &&
460
- (!Object.prototype.hasOwnProperty.call(value, 'description') ||
461
- typeof value.description === 'string')
462
- );
510
+ /**
511
+ * Source-authored acceptance on `review`'s declared public output: the
512
+ * result returns the exact evaluated repository revision and affirmatively
513
+ * establishes that no unsettled findings remain. The shared bridge correlates
514
+ * the result with the supplied review scope.
515
+ */
516
+ function reviewPassOf(event: unknown): ReviewPassOutput | undefined {
517
+ const output = doneOutputOf(event);
518
+ if (!isRecord(output)) return undefined;
519
+ return output.noUnsettledFindings === true &&
520
+ isNonEmptyString(output.evaluatedRevision)
521
+ ? { noUnsettledFindings: true, evaluatedRevision: output.evaluatedRevision }
522
+ : undefined;
463
523
  }
464
524
 
465
- function normalizedReviewFailure(
525
+ /**
526
+ * Recognizes an authored rejected child result: an aborted or error call, or
527
+ * a child that completed at its own authored failure terminal.
528
+ */
529
+ export function authoredChildResult(
466
530
  error: unknown,
467
- ): AuthoredReviewFailure | undefined {
468
- const result = nestedResultFromError(error);
469
- if (result === undefined) return undefined;
470
- const allowed = new Set([
471
- 'status',
472
- 'playbookId',
473
- 'childSessionId',
474
- 'state',
475
- ...(result.status === 'ok' ? ['output', 'terminal'] : ['error']),
476
- ]);
477
- if (
478
- Reflect.ownKeys(result).some(
479
- (key) => typeof key !== 'string' || !allowed.has(key),
480
- ) ||
481
- (result.status !== 'aborted' &&
482
- result.status !== 'error' &&
483
- result.status !== 'ok') ||
484
- result.playbookId !== 'review'
485
- ) {
486
- return undefined;
487
- }
488
- if (
489
- Object.prototype.hasOwnProperty.call(result, 'childSessionId') &&
490
- !isNonEmptyString(result.childSessionId)
491
- ) {
492
- return undefined;
493
- }
494
- if (
495
- Object.prototype.hasOwnProperty.call(result, 'state') &&
496
- !isPlaybookState(result.state)
497
- ) {
531
+ expectedPlaybookId: string,
532
+ ): PlaybookCallResult | undefined {
533
+ if (!(error instanceof Error)) return undefined;
534
+ try {
535
+ const result = validatePlaybookCallResult(
536
+ (error as Error & { result?: unknown }).result,
537
+ expectedPlaybookId,
538
+ );
539
+ return result.status !== 'ok' || result.terminal?.kind === 'failure'
540
+ ? result
541
+ : undefined;
542
+ } catch {
498
543
  return undefined;
499
544
  }
545
+ }
546
+
547
+ /** Compact evidence and reported error for an authored review failure. */
548
+ function authoredReviewFailureOf(
549
+ event: unknown,
550
+ ):
551
+ | { readonly evidence: CompletedReviewResult; readonly error: CompactError }
552
+ | undefined {
553
+ const result = authoredChildResult(actorErrorOf(event), REVIEW_PLAYBOOK_ID);
554
+ if (result === undefined) return undefined;
500
555
  if (result.status === 'ok') {
501
- if (
502
- !Object.prototype.hasOwnProperty.call(result, 'terminal') ||
503
- !isFailureTerminalRecord(result.terminal) ||
504
- !isNonEmptyString(result.childSessionId)
505
- ) {
506
- return undefined;
507
- }
556
+ const description = result.terminal?.description;
508
557
  return {
509
- status: 'ok',
510
- ...(result.output === undefined
511
- ? {}
512
- : { output: result.output as JsonValue }),
513
- };
514
- }
515
- let normalizedError: CompactError | undefined;
516
- if (Object.prototype.hasOwnProperty.call(result, 'error')) {
517
- if (!isRecord(result.error)) return undefined;
518
- const errorKeys = Reflect.ownKeys(result.error);
519
- if (
520
- errorKeys.some(
521
- (key) =>
522
- typeof key !== 'string' ||
523
- (key !== 'name' && key !== 'message' && key !== 'stack'),
524
- ) ||
525
- !Object.prototype.hasOwnProperty.call(result.error, 'name') ||
526
- !Object.prototype.hasOwnProperty.call(result.error, 'message') ||
527
- !isNonEmptyString(result.error.name) ||
528
- typeof result.error.message !== 'string' ||
529
- (Object.prototype.hasOwnProperty.call(result.error, 'stack') &&
530
- typeof result.error.stack !== 'string')
531
- ) {
532
- return undefined;
533
- }
534
- normalizedError = {
535
- name: result.error.name,
536
- message: result.error.message,
558
+ evidence: {
559
+ playbookId: REVIEW_PLAYBOOK_ID,
560
+ status: 'ok',
561
+ ...(result.output === undefined ? {} : { output: result.output }),
562
+ },
563
+ error: {
564
+ name: 'ReviewFailed',
565
+ message: isNonEmptyString(description)
566
+ ? `review ended at its authored failure terminal: ${description}`
567
+ : 'review ended at its authored failure terminal.',
568
+ },
537
569
  };
538
- } else if (result.status === 'error') {
539
- return undefined;
540
570
  }
571
+ const error: CompactError =
572
+ result.error !== undefined
573
+ ? { name: result.error.name, message: result.error.message }
574
+ : { name: 'AbortError', message: 'The review call was aborted.' };
541
575
  return {
542
- status: result.status,
543
- ...(normalizedError === undefined ? {} : { error: normalizedError }),
576
+ evidence: { playbookId: REVIEW_PLAYBOOK_ID, status: result.status, error },
577
+ error,
544
578
  };
545
579
  }
546
580
 
547
- function authoredReviewFailure({ event }: { event: unknown }): boolean {
548
- const error = isRecord(event) ? event.error : undefined;
549
- return normalizedReviewFailure(error) !== undefined;
550
- }
581
+ // ---------------------------------------------------------------------------
582
+ // Nested call text composition (quoted relays)
583
+ // ---------------------------------------------------------------------------
551
584
 
552
- function compactError(value: unknown): CompactError {
553
- if (value instanceof Error) {
554
- return {
555
- name: value.name || 'Error',
556
- message: value.message,
557
- };
558
- }
559
- return { name: 'Error', message: String(value) };
560
- }
585
+ const REVIEW_PLACEHOLDER =
586
+ /<caller-input>|<code-commit>|<coder-output>|<ir-task>/g;
561
587
 
562
- function authoredReviewError(event: unknown): CompactError {
563
- const outer = isRecord(event) ? event.error : undefined;
564
- const failure = normalizedReviewFailure(outer);
565
- if (failure !== undefined && failure.status !== 'ok' && failure.error) {
566
- return failure.error;
567
- }
568
- if (failure?.status === 'aborted') {
569
- return { name: 'AbortError', message: 'REVIEW was aborted.' };
588
+ /**
589
+ * Composes review call text in one callback pass per template line: a quoted
590
+ * relay line whose value is empty contributes no line, a multi-line value is
591
+ * quoted line by line, and an absent value keeps its placeholder literal.
592
+ */
593
+ function composeReviewText(
594
+ template: string,
595
+ values: Readonly<Record<string, string | undefined>>,
596
+ ): string {
597
+ const lines: string[] = [];
598
+ for (const line of template.split('\n')) {
599
+ const quoted = line.startsWith('> ');
600
+ const tokens = line.match(REVIEW_PLACEHOLDER) ?? [];
601
+ if (quoted && tokens.some((token) => values[token] === '')) continue;
602
+ lines.push(
603
+ line.replace(REVIEW_PLACEHOLDER, (token: string) => {
604
+ const value = values[token];
605
+ if (value === undefined) return token;
606
+ return quoted ? value.split('\n').join('\n> ') : value;
607
+ }),
608
+ );
570
609
  }
571
- return compactError(outer);
610
+ while (lines.length > 0 && lines[lines.length - 1] === '') lines.pop();
611
+ return lines.join('\n');
572
612
  }
573
613
 
574
- function quoteRelay(label: string, value: string): string {
575
- const lines = value.split('\n');
576
- return [`> ${label}: ${lines[0] ?? ''}`, ...lines.slice(1).map((line) => `> ${line}`)].join(
577
- '\n',
578
- );
579
- }
614
+ // ---------------------------------------------------------------------------
615
+ // Guards
616
+ // ---------------------------------------------------------------------------
580
617
 
581
- function reviewScopeLine(context: CodingContext): string {
582
- return (
583
- `> Review scope: the commit ${context.latestCommit ?? ''} ` +
584
- 'from this coding phase and its resulting repository state.'
585
- );
586
- }
618
+ type EventArgs = { readonly event: unknown };
619
+ type ContextEventArgs = {
620
+ readonly context: CodeContext;
621
+ readonly event: unknown;
622
+ };
623
+ type MachineArgs = {
624
+ readonly context: CodeContext;
625
+ readonly event: CodeEvent;
626
+ };
627
+
628
+ const acceptsOutcome =
629
+ (outcome: PhaseOutcome) =>
630
+ ({ event }: EventArgs): boolean =>
631
+ playerOutputOf(event)?.guard === outcome;
632
+
633
+ const needsBossReplyWithQuestion = ({ event }: EventArgs): boolean =>
634
+ playerOutputOf(event)?.guard === 'needsBossReply';
635
+
636
+ // A review state is entered only through an accepting phase arm, which records
637
+ // the phase outcome and its non-empty code-owned commit.
638
+ const reviewPassedAfter =
639
+ (outcome: PhaseOutcome) =>
640
+ ({ context, event }: ContextEventArgs): boolean =>
641
+ context.phaseOutcome === outcome &&
642
+ isNonEmptyString(context.codeCommit) &&
643
+ reviewPassOf(event) !== undefined;
644
+
645
+ const authoredReviewFailure = ({ event }: EventArgs): boolean =>
646
+ authoredReviewFailureOf(event) !== undefined;
647
+
648
+ const validStartCode = ({ event }: MachineArgs): boolean =>
649
+ event.type === 'START_CODE' &&
650
+ isNonEmptyString(event.callerInput) &&
651
+ (event.runResults === undefined || typeof event.runResults === 'string');
652
+
653
+ const repliesToPendingQuestion = ({ context, event }: MachineArgs): boolean =>
654
+ event.type === 'BOSS_REPLY' &&
655
+ context.pendingBossQuestion !== undefined &&
656
+ (event.questionId === undefined ||
657
+ event.questionId === context.pendingBossQuestion.questionId);
658
+
659
+ const emptyBossReply = (args: MachineArgs): boolean =>
660
+ repliesToPendingQuestion(args) &&
661
+ args.event.type === 'BOSS_REPLY' &&
662
+ !isNonEmptyString(args.event.answer);
587
663
 
588
- function newIntentReviewText(context: CodingContext): string {
589
- return [
590
- quoteRelay('Original intent', context.callerInput ?? ''),
591
- reviewScopeLine(context),
592
- quoteRelay('Coder output', context.coderOutput ?? ''),
593
- ].join('\n');
664
+ const INTERRUPT_PRECONDITIONS = {
665
+ firstPhase: (context: CodeContext) => isNonEmptyString(context.callerInput),
666
+ irTaskPhase: (context: CodeContext) =>
667
+ isNonEmptyString(context.callerInput) &&
668
+ isNonEmptyString(context.irNumber),
669
+ } as const satisfies Record<JumpableStateId, (context: CodeContext) => boolean>;
670
+
671
+ // ---------------------------------------------------------------------------
672
+ // Transition helpers
673
+ // ---------------------------------------------------------------------------
674
+
675
+ /** One guarded root transition per jumpable state. */
676
+ function bossInterrupts<const Ids extends readonly JumpableStateId[]>(
677
+ ids: Ids,
678
+ ) {
679
+ return ids.map((id) => ({
680
+ guard: ({ context, event }: MachineArgs) =>
681
+ event.type === 'BOSS_INTERRUPT' &&
682
+ event.targetId === id &&
683
+ INTERRUPT_PRECONDITIONS[id](context),
684
+ target: `#${id}` as const,
685
+ reenter: true,
686
+ actions: 'resetForInterrupt' as const,
687
+ }));
594
688
  }
595
689
 
596
- function irTaskReviewText(context: CodingContext): string {
597
- return [
598
- quoteRelay('Original intent', context.callerInput ?? ''),
599
- reviewScopeLine(context),
600
- quoteRelay('Coder output', context.coderOutput ?? ''),
601
- quoteRelay('Current IR task', context.irTask ?? ''),
602
- ].join('\n');
690
+ /** One guarded `BOSS_REPLY` resume transition per suspended working leaf. */
691
+ function resumableStates<const Ids extends readonly ResumableStateId[]>(
692
+ ids: Ids,
693
+ ) {
694
+ return ids.map((id) => ({
695
+ guard: (args: MachineArgs) =>
696
+ repliesToPendingQuestion(args) &&
697
+ args.event.type === 'BOSS_REPLY' &&
698
+ isNonEmptyString(args.event.answer) &&
699
+ args.context.pendingBossQuestion?.resumeStateId === id,
700
+ target: `#${id}` as const,
701
+ reenter: true,
702
+ actions: 'rememberBossReply' as const,
703
+ }));
603
704
  }
604
705
 
605
- const machineSetup = setup({
606
- types: {} as {
607
- context: CodingContext;
608
- events: CodingEvent;
609
- input: CodingInput;
610
- output: CodePlaybookOutput;
706
+ const bossReplyFields = (context: CodeContext) => ({
707
+ ...(context.pendingBossQuestion !== undefined
708
+ ? { pendingBossQuestion: context.pendingBossQuestion }
709
+ : {}),
710
+ ...(context.bossReply !== undefined ? { bossReply: context.bossReply } : {}),
711
+ });
712
+
713
+ const acceptedOutcome = (
714
+ source: WorkingStateId,
715
+ target: string,
716
+ outcome: PhaseOutcome | 'needsBossReply',
717
+ ) =>
718
+ ({
719
+ type: 'playbook.acceptedOutcome',
720
+ params: { source, target, acceptedOutcome: outcome },
721
+ }) as const;
722
+
723
+ const acceptPhaseArm = (
724
+ source: WorkingStateId,
725
+ outcome: PhaseOutcome,
726
+ guard: 'acceptDirectCommit' | 'acceptIrCommit' | 'acceptMoreTasks' | 'acceptFinalTask',
727
+ target: ReviewStateId,
728
+ ) =>
729
+ ({
730
+ guard,
731
+ target: `#${target}`,
732
+ actions: [
733
+ acceptedOutcome(source, target, outcome),
734
+ 'rememberPhaseCommit',
735
+ 'clearBossReplyContext',
736
+ ],
737
+ }) as const;
738
+
739
+ const suspendArm = (source: WorkingStateId) =>
740
+ ({
741
+ guard: 'needsBossReplyWithQuestion',
742
+ target: '#awaitBossReply',
743
+ actions: [
744
+ acceptedOutcome(source, 'awaitBossReply', 'needsBossReply'),
745
+ { type: 'setPendingBossQuestion', params: { resumeStateId: source } },
746
+ ],
747
+ }) as const;
748
+
749
+ const malformedPlayerOutputArm = {
750
+ target: '#failed',
751
+ actions: ['rememberMalformedPlayerOutput', 'clearBossReplyContext'],
752
+ } as const;
753
+
754
+ const playerOnError = {
755
+ target: '#failed',
756
+ actions: ['rememberActorError', 'clearBossReplyContext'],
757
+ } as const;
758
+
759
+ const reviewOnError = [
760
+ {
761
+ guard: 'authoredReviewFailure',
762
+ target: '#reviewFailed',
763
+ actions: 'rememberAuthoredReviewFailure',
764
+ },
765
+ { target: '#failed', actions: 'rememberActorError' },
766
+ ] as const;
767
+
768
+ const meta = (stateId: StateKey, role?: CodeRole) => ({
769
+ playbook: {
770
+ stateId,
771
+ description: STATE_DESCRIPTIONS[stateId],
772
+ ...(role === undefined ? {} : { role }),
773
+ },
774
+ });
775
+
776
+ const finalMeta = (
777
+ stateId: 'reviewFailed' | 'done',
778
+ terminal: 'success' | 'failure',
779
+ ) => ({
780
+ playbook: {
781
+ stateId,
782
+ description: STATE_DESCRIPTIONS[stateId],
783
+ terminal,
784
+ },
785
+ });
786
+
787
+ // ---------------------------------------------------------------------------
788
+ // Machine
789
+ // ---------------------------------------------------------------------------
790
+
791
+ export const codeMachine = setup({
792
+ types: {
793
+ context: {} as CodeContext,
794
+ events: {} as CodeEvent,
795
+ input: {} as CodeInput,
796
+ output: {} as CodeOutput,
611
797
  },
612
798
  actors: {
613
799
  player: fromPromise<PlayerOutput, PlayerInput>(async () => {
614
800
  throw new Error('player actor must be provided by the runner');
615
801
  }),
616
- playbook: fromPromise<JsonValue | undefined, PlaybookInput>(async () => {
802
+ playbook: fromPromise<PlaybookOutput, PlaybookInput>(async () => {
617
803
  throw new Error('playbook actor must be provided by the runner');
618
804
  }),
619
805
  },
620
- guards: {
621
- isDirectCommit,
622
- isIrCommit,
623
- isMoreTasks,
624
- isFinalTask,
625
- needsBossReply,
626
- reviewApprovedDirect: reviewApprovedFor('direct'),
627
- reviewApprovedIrCreated: reviewApprovedFor('ir-created'),
628
- reviewApprovedMoreTasks: reviewApprovedFor('ir-task-more'),
629
- reviewApprovedFinalTask: reviewApprovedFor('ir-task-final'),
630
- authoredReviewFailure,
631
- emptyBossReply: ({ event }) =>
632
- event.type === 'BOSS_REPLY' && event.answer.trim().length === 0,
633
- resumesFirstPhase: ({ context, event }) =>
634
- event.type === 'BOSS_REPLY' &&
635
- event.answer.trim().length > 0 &&
636
- context.pendingBossQuestion?.resumeStateId === 'runFirstPhase' &&
637
- (event.questionId === undefined || event.questionId === 'runFirstPhase'),
638
- resumesIrTask: ({ context, event }) =>
639
- event.type === 'BOSS_REPLY' &&
640
- event.answer.trim().length > 0 &&
641
- context.pendingBossQuestion?.resumeStateId === 'runIrTask' &&
642
- (event.questionId === undefined || event.questionId === 'runIrTask'),
643
- },
644
806
  actions: {
645
807
  'playbook.acceptedOutcome': (
646
808
  _args,
@@ -650,475 +812,382 @@ const machineSetup = setup({
650
812
  readonly acceptedOutcome: string;
651
813
  },
652
814
  ) => undefined,
653
- startCoding: assign(({ context, event }) => {
654
- if (event.type !== 'START_CODE') return {};
655
- return {
656
- runResults: context.runResults,
657
- callerInput: event.callerInput,
658
- coderOutput: undefined,
659
- latestCommit: undefined,
660
- irNumber: undefined,
661
- irTask: undefined,
662
- evaluatedRevision: undefined,
663
- phase: undefined,
664
- completion: undefined,
665
- reviewError: undefined,
666
- lastError: undefined,
667
- pendingBossQuestion: undefined,
668
- bossReply: undefined,
669
- };
670
- }),
671
- rememberDirectCommit: assign(({ event }) => ({
672
- coderOutput: outputString(event, 'coderOutput'),
673
- latestCommit: outputString(event, 'latestCommit'),
674
- phase: 'direct' as const,
675
- pendingBossQuestion: undefined,
676
- bossReply: undefined,
677
- })),
678
- rememberIrCommit: assign(({ event }) => ({
679
- coderOutput: outputString(event, 'coderOutput'),
680
- latestCommit: outputString(event, 'latestCommit'),
681
- irNumber: outputString(event, 'irNumber'),
682
- irTask: undefined,
683
- phase: 'ir-created' as const,
684
- pendingBossQuestion: undefined,
685
- bossReply: undefined,
686
- })),
687
- rememberMoreTasks: assign(({ event }) => ({
688
- coderOutput: outputString(event, 'coderOutput'),
689
- latestCommit: outputString(event, 'latestCommit'),
690
- irNumber: outputString(event, 'irNumber'),
691
- irTask: outputString(event, 'irTask'),
692
- phase: 'ir-task-more' as const,
693
- pendingBossQuestion: undefined,
694
- bossReply: undefined,
695
- })),
696
- rememberFinalTask: assign(({ event }) => ({
697
- coderOutput: outputString(event, 'coderOutput'),
698
- latestCommit: outputString(event, 'latestCommit'),
699
- irNumber: outputString(event, 'irNumber'),
700
- irTask: outputString(event, 'irTask'),
701
- phase: 'ir-task-final' as const,
815
+ startCode: assign(({ context, event }) =>
816
+ event.type === 'START_CODE'
817
+ ? {
818
+ callerInput: event.callerInput,
819
+ runResults: event.runResults ?? context.runResults,
820
+ codeCommit: undefined,
821
+ coderOutput: undefined,
822
+ irNumber: undefined,
823
+ irTask: undefined,
824
+ phaseOutcome: undefined,
825
+ evaluatedRevision: undefined,
826
+ reviewError: undefined,
827
+ reviewEvidence: undefined,
828
+ lastError: undefined,
829
+ pendingBossQuestion: undefined,
830
+ bossReply: undefined,
831
+ }
832
+ : {},
833
+ ),
834
+ resetForInterrupt: assign({
835
+ phaseOutcome: undefined,
836
+ evaluatedRevision: undefined,
837
+ reviewError: undefined,
838
+ reviewEvidence: undefined,
839
+ lastError: undefined,
702
840
  pendingBossQuestion: undefined,
703
841
  bossReply: undefined,
704
- })),
705
- rememberPendingQuestion: assign(({ context, event }) => {
706
- const question = outputString(event, 'question');
707
- const output = playerOutput(event);
708
- const stateId =
709
- context.irNumber === undefined ? 'runFirstPhase' : 'runIrTask';
710
- if (question === undefined || output?.guard !== 'needsBossReply') {
711
- return { lastError: new Error('Coder question output was malformed') };
712
- }
842
+ }),
843
+ rememberPhaseCommit: assign(({ event }) => {
844
+ const output = playerOutputOf(event);
845
+ if (output === undefined || output.guard === 'needsBossReply') return {};
713
846
  return {
714
- pendingBossQuestion: {
715
- questionId: stateId,
716
- resumeStateId: stateId,
717
- sourceItem: stateId === 'runFirstPhase' ? 'CODE-1' : 'CODE-3',
718
- asker: { kind: 'role', roleId: 'coder' },
719
- question,
720
- },
721
- bossReply: undefined,
847
+ codeCommit: output.latestCommit,
848
+ coderOutput: output.coderOutput,
849
+ phaseOutcome: output.guard,
850
+ irNumber: output.guard === 'directCommit' ? undefined : output.irNumber,
851
+ irTask:
852
+ output.guard === 'moreTasks' || output.guard === 'finalTask'
853
+ ? output.irTask
854
+ : undefined,
855
+ lastError: undefined,
722
856
  };
723
857
  }),
858
+ setPendingBossQuestion: assign(
859
+ ({ event }, params: { readonly resumeStateId: ResumableStateId }) => {
860
+ const output = playerOutputOf(event);
861
+ if (output?.guard !== 'needsBossReply') return {};
862
+ const pendingBossQuestion: PendingBossQuestion = {
863
+ questionId: params.resumeStateId,
864
+ resumeStateId: params.resumeStateId,
865
+ sourceItem: RESUME_METADATA[params.resumeStateId].sourceItem,
866
+ asker: { kind: 'role', roleId: 'coder' },
867
+ question: output.question,
868
+ };
869
+ return { pendingBossQuestion, bossReply: undefined, lastError: undefined };
870
+ },
871
+ ),
724
872
  rememberBossReply: assign(({ event }) =>
725
873
  event.type === 'BOSS_REPLY' ? { bossReply: event.answer } : {},
726
874
  ),
727
- rememberEmptyBossReplyError: assign({
728
- lastError: () => new Error('BOSS_REPLY received an empty answer'),
875
+ clearBossReplyContext: assign({
729
876
  pendingBossQuestion: undefined,
730
877
  bossReply: undefined,
731
878
  }),
732
- completeSuccessfully: assign(({ event }) => {
733
- const output = reviewOutput(event);
734
- const approved = isReviewApprovedValue(output) ? output : undefined;
735
- return {
736
- completion: 'complete' as const,
737
- evaluatedRevision: approved?.evaluatedRevision,
738
- reviewError: undefined,
879
+ rememberReviewPass: assign(({ event }) => {
880
+ const pass = reviewPassOf(event);
881
+ return pass === undefined
882
+ ? {}
883
+ : { evaluatedRevision: pass.evaluatedRevision, lastError: undefined };
884
+ }),
885
+ rememberReviewNotPassed: assign(({ event }) => {
886
+ const output = doneOutputOf(event);
887
+ const reviewEvidence: CompletedReviewResult = {
888
+ playbookId: REVIEW_PLAYBOOK_ID,
889
+ status: 'ok',
890
+ ...(isJsonValue(output) ? { output } : {}),
739
891
  };
892
+ return { reviewEvidence, reviewError: REVIEW_NOT_PASSED, lastError: undefined };
893
+ }),
894
+ rememberAuthoredReviewFailure: assign(({ event }) => {
895
+ const failure = authoredReviewFailureOf(event);
896
+ return failure === undefined
897
+ ? {}
898
+ : {
899
+ reviewEvidence: failure.evidence,
900
+ reviewError: failure.error,
901
+ lastError: undefined,
902
+ };
740
903
  }),
741
- completeWithReviewFailure: assign(({ event }) => ({
742
- completion: 'review-failed' as const,
743
- reviewError: authoredReviewError(event),
744
- })),
745
- completeWithInvalidReviewOutput: assign(({ event }) => ({
746
- completion: 'review-failed' as const,
747
- reviewError: {
748
- name: 'ReviewContractError',
749
- message:
750
- `REVIEW returned an invalid approval result: ` +
751
- JSON.stringify(reviewOutput(event)),
752
- },
753
- })),
754
904
  rememberActorError: assign(({ event }) => ({
755
- lastError: (event as unknown as { readonly error?: unknown }).error,
756
- })),
757
- rememberMalformedPlayerOutput: assign(({ context }) => ({
758
- lastError: new Error(
759
- `Coder result for ${context.irNumber === undefined ? 'CODE-1' : 'CODE-3'} ` +
760
- 'did not match a declared outcome.',
761
- ),
905
+ lastError: errorRecord(actorErrorOf(event)),
762
906
  })),
907
+ rememberMalformedPlayerOutput: assign(({ event }) => {
908
+ const output = doneOutputOf(event);
909
+ return {
910
+ lastError: {
911
+ name: 'MalformedPlayerOutput',
912
+ message:
913
+ isRecord(output) && output.guard === 'needsBossReply'
914
+ ? 'Coder output declared needsBossReply without a question.'
915
+ : 'Coder output did not match an outcome declared for the working state.',
916
+ },
917
+ };
918
+ }),
919
+ rememberMalformedBossReply: assign({
920
+ lastError: {
921
+ name: 'MalformedBossReply',
922
+ message: 'BOSS_REPLY carried an empty answer.',
923
+ },
924
+ }),
763
925
  },
764
- });
765
-
766
- export const codingMachine = machineSetup.createMachine({
926
+ guards: {
927
+ needsBossReplyWithQuestion,
928
+ acceptDirectCommit: acceptsOutcome('directCommit'),
929
+ acceptIrCommit: acceptsOutcome('irCommit'),
930
+ acceptMoreTasks: acceptsOutcome('moreTasks'),
931
+ acceptFinalTask: acceptsOutcome('finalTask'),
932
+ reviewPassedDirect: reviewPassedAfter('directCommit'),
933
+ reviewPassedNewIr: reviewPassedAfter('irCommit'),
934
+ reviewPassedNonfinalTask: reviewPassedAfter('moreTasks'),
935
+ reviewPassedFinalTask: reviewPassedAfter('finalTask'),
936
+ authoredReviewFailure,
937
+ validStartCode,
938
+ emptyBossReply,
939
+ },
940
+ }).createMachine({
767
941
  id: 'code',
768
942
  initial: 'ready',
769
- context: ({ input }) => ({
770
- runResults: typeof input.runResults === 'string' ? input.runResults : '',
943
+ context: ({ input }): CodeContext => ({
944
+ runResults: typeof input?.runResults === 'string' ? input.runResults : '',
771
945
  }),
772
- output: ({ context }): CodePlaybookOutput => {
773
- const lastCodeCommit = context.latestCommit;
774
- if (!isNonEmptyString(lastCodeCommit)) {
775
- throw new Error('CODE reached a final state without a commit identity');
946
+ output: ({ context }): CodeOutput => {
947
+ if (!isNonEmptyString(context.codeCommit)) {
948
+ throw new Error(
949
+ 'CODE reached a final state without its last code-owned commit',
950
+ );
776
951
  }
777
- if (context.completion === 'review-failed') {
952
+ if (context.reviewError !== undefined) {
778
953
  return {
779
954
  status: 'review-failed',
780
- lastCodeCommit,
781
- error:
782
- context.reviewError ?? {
783
- name: 'Error',
784
- message: 'REVIEW failed without error detail.',
785
- },
955
+ lastCodeCommit: context.codeCommit,
956
+ error: context.reviewError,
786
957
  };
787
958
  }
788
- const finalEvaluatedRevision = context.evaluatedRevision;
789
- if (!isNonEmptyString(finalEvaluatedRevision)) {
790
- throw new Error(
791
- 'CODE completed without an evaluated repository revision',
792
- );
959
+ if (!isNonEmptyString(context.evaluatedRevision)) {
960
+ throw new Error('CODE completed without the final evaluated revision');
793
961
  }
794
962
  return {
795
963
  status: 'complete',
796
- lastCodeCommit,
797
- finalEvaluatedRevision,
964
+ lastCodeCommit: context.codeCommit,
965
+ finalEvaluatedRevision: context.evaluatedRevision,
798
966
  allReviewsPassed: true,
799
967
  };
800
968
  },
969
+ on: {
970
+ BOSS_INTERRUPT: bossInterrupts(['firstPhase', 'irTaskPhase'] as const),
971
+ },
801
972
  states: {
802
973
  ready: {
803
974
  id: 'ready',
975
+ tags: 'playbook.parked',
804
976
  description: STATE_DESCRIPTIONS.ready,
805
- meta: playbookMeta('ready'),
806
- tags: ['playbook.parked'],
977
+ meta: meta('ready'),
807
978
  on: {
808
- START_CODE: { target: 'runFirstPhase', actions: 'startCoding' },
979
+ START_CODE: {
980
+ guard: 'validStartCode',
981
+ target: 'firstPhase',
982
+ actions: 'startCode',
983
+ },
809
984
  },
810
985
  },
811
- runFirstPhase: {
812
- id: 'runFirstPhase',
813
- description: STATE_DESCRIPTIONS.runFirstPhase,
814
- meta: playbookMeta('runFirstPhase', 'coder'),
815
- tags: ['playbook.busy'],
986
+ firstPhase: {
987
+ id: 'firstPhase',
988
+ tags: 'playbook.busy',
989
+ description: STATE_DESCRIPTIONS.firstPhase,
990
+ meta: meta('firstPhase', 'coder'),
816
991
  invoke: {
817
992
  src: 'player',
818
- input: ({ context }): PlayerInput => ({
819
- stateId: 'runFirstPhase',
993
+ input: ({ context }): FirstPhasePlayerInput => ({
994
+ stateId: 'firstPhase',
820
995
  role: 'coder',
821
996
  sourceItem: 'CODE-1',
822
997
  prompt: FIRST_PHASE_PROMPT,
823
- result: FIRST_PHASE_RESULTS,
824
- callerInput: context.callerInput ?? '',
998
+ result: FIRST_PHASE_RESULT,
999
+ ...(context.callerInput !== undefined
1000
+ ? { callerInput: context.callerInput }
1001
+ : {}),
825
1002
  runResults: context.runResults,
826
- ...(context.pendingBossQuestion === undefined
827
- ? {}
828
- : { pendingBossQuestion: context.pendingBossQuestion }),
829
- ...(context.bossReply === undefined
830
- ? {}
831
- : { bossReply: context.bossReply }),
1003
+ ...bossReplyFields(context),
832
1004
  }),
833
1005
  onDone: [
834
- {
835
- guard: 'isDirectCommit',
836
- target: 'reviewFirstCommit',
837
- actions: [
838
- {
839
- type: 'playbook.acceptedOutcome',
840
- params: {
841
- source: 'runFirstPhase',
842
- target: 'reviewFirstCommit',
843
- acceptedOutcome: 'directCommit',
844
- },
845
- },
846
- 'rememberDirectCommit',
847
- ],
848
- },
849
- {
850
- guard: 'isIrCommit',
851
- target: 'reviewFirstCommit',
852
- actions: [
853
- {
854
- type: 'playbook.acceptedOutcome',
855
- params: {
856
- source: 'runFirstPhase',
857
- target: 'reviewFirstCommit',
858
- acceptedOutcome: 'irCommit',
859
- },
860
- },
861
- 'rememberIrCommit',
862
- ],
863
- },
864
- {
865
- guard: 'isMoreTasks',
866
- target: 'reviewIrTask',
867
- actions: [
868
- {
869
- type: 'playbook.acceptedOutcome',
870
- params: {
871
- source: 'runFirstPhase',
872
- target: 'reviewIrTask',
873
- acceptedOutcome: 'moreTasks',
874
- },
875
- },
876
- 'rememberMoreTasks',
877
- ],
878
- },
879
- {
880
- guard: 'isFinalTask',
881
- target: 'reviewIrTask',
882
- actions: [
883
- {
884
- type: 'playbook.acceptedOutcome',
885
- params: {
886
- source: 'runFirstPhase',
887
- target: 'reviewIrTask',
888
- acceptedOutcome: 'finalTask',
889
- },
890
- },
891
- 'rememberFinalTask',
892
- ],
893
- },
894
- {
895
- guard: 'needsBossReply',
896
- target: 'awaitBossReply',
897
- actions: [
898
- {
899
- type: 'playbook.acceptedOutcome',
900
- params: {
901
- source: 'runFirstPhase',
902
- target: 'awaitBossReply',
903
- acceptedOutcome: 'needsBossReply',
904
- },
905
- },
906
- 'rememberPendingQuestion',
907
- ],
908
- },
909
- {
910
- target: 'failed',
911
- actions: 'rememberMalformedPlayerOutput',
912
- },
1006
+ suspendArm('firstPhase'),
1007
+ acceptPhaseArm(
1008
+ 'firstPhase',
1009
+ 'directCommit',
1010
+ 'acceptDirectCommit',
1011
+ 'reviewNewIntentPhase',
1012
+ ),
1013
+ acceptPhaseArm(
1014
+ 'firstPhase',
1015
+ 'irCommit',
1016
+ 'acceptIrCommit',
1017
+ 'reviewNewIntentPhase',
1018
+ ),
1019
+ acceptPhaseArm(
1020
+ 'firstPhase',
1021
+ 'moreTasks',
1022
+ 'acceptMoreTasks',
1023
+ 'reviewIrTaskPhase',
1024
+ ),
1025
+ acceptPhaseArm(
1026
+ 'firstPhase',
1027
+ 'finalTask',
1028
+ 'acceptFinalTask',
1029
+ 'reviewIrTaskPhase',
1030
+ ),
1031
+ malformedPlayerOutputArm,
913
1032
  ],
914
- onError: { target: 'failed', actions: 'rememberActorError' },
1033
+ onError: playerOnError,
915
1034
  },
916
1035
  },
917
- reviewFirstCommit: {
918
- id: 'reviewFirstCommit',
919
- description: STATE_DESCRIPTIONS.reviewFirstCommit,
920
- meta: playbookMeta('reviewFirstCommit'),
921
- tags: ['playbook.suspended'],
1036
+ reviewNewIntentPhase: {
1037
+ id: 'reviewNewIntentPhase',
1038
+ tags: 'playbook.suspended',
1039
+ description: STATE_DESCRIPTIONS.reviewNewIntentPhase,
1040
+ meta: meta('reviewNewIntentPhase'),
922
1041
  invoke: {
923
1042
  src: 'playbook',
924
1043
  input: ({ context }): PlaybookInput => ({
925
- stateId: 'reviewFirstCommit',
1044
+ stateId: 'reviewNewIntentPhase',
926
1045
  sourceItem: 'CODE-2',
927
- playbookId: 'review',
928
- text: newIntentReviewText(context),
1046
+ playbookId: REVIEW_PLAYBOOK_ID,
1047
+ text: composeReviewText(REVIEW_NEW_INTENT_PHASE_TEMPLATE, {
1048
+ '<caller-input>': context.callerInput,
1049
+ '<code-commit>': context.codeCommit,
1050
+ '<coder-output>': context.coderOutput,
1051
+ }),
929
1052
  }),
930
1053
  onDone: [
931
1054
  {
932
- guard: 'reviewApprovedDirect',
1055
+ guard: 'reviewPassedDirect',
933
1056
  target: 'done',
934
- actions: 'completeSuccessfully',
935
- },
936
- {
937
- guard: 'reviewApprovedIrCreated',
938
- target: 'runIrTask',
939
- },
940
- {
941
- target: 'reportedReviewFailure',
942
- actions: 'completeWithInvalidReviewOutput',
1057
+ actions: 'rememberReviewPass',
943
1058
  },
944
- ],
945
- onError: [
946
1059
  {
947
- guard: 'authoredReviewFailure',
948
- target: 'reportedReviewFailure',
949
- actions: 'completeWithReviewFailure',
1060
+ guard: 'reviewPassedNewIr',
1061
+ target: 'irTaskPhase',
1062
+ actions: 'rememberReviewPass',
950
1063
  },
951
- { target: 'failed', actions: 'rememberActorError' },
1064
+ { target: 'reviewFailed', actions: 'rememberReviewNotPassed' },
952
1065
  ],
1066
+ onError: reviewOnError,
953
1067
  },
954
1068
  },
955
- runIrTask: {
956
- id: 'runIrTask',
957
- description: STATE_DESCRIPTIONS.runIrTask,
958
- meta: playbookMeta('runIrTask', 'coder'),
959
- tags: ['playbook.busy'],
1069
+ irTaskPhase: {
1070
+ id: 'irTaskPhase',
1071
+ tags: 'playbook.busy',
1072
+ description: STATE_DESCRIPTIONS.irTaskPhase,
1073
+ meta: meta('irTaskPhase', 'coder'),
960
1074
  invoke: {
961
1075
  src: 'player',
962
- input: ({ context }): PlayerInput => ({
963
- stateId: 'runIrTask',
1076
+ input: ({ context }): IrTaskPhasePlayerInput => ({
1077
+ stateId: 'irTaskPhase',
964
1078
  role: 'coder',
965
1079
  sourceItem: 'CODE-3',
966
- prompt: IR_TASK_PROMPT,
967
- result: IR_TASK_RESULTS,
968
- callerInput: context.callerInput ?? '',
1080
+ prompt: IR_TASK_PHASE_PROMPT,
1081
+ result: IR_TASK_PHASE_RESULT,
1082
+ ...(context.callerInput !== undefined
1083
+ ? { callerInput: context.callerInput }
1084
+ : {}),
1085
+ ...(context.irNumber !== undefined
1086
+ ? { irNumber: context.irNumber }
1087
+ : {}),
969
1088
  runResults: context.runResults,
970
- ...(context.irNumber === undefined
971
- ? {}
972
- : { irNumber: context.irNumber }),
973
- ...(context.pendingBossQuestion === undefined
974
- ? {}
975
- : { pendingBossQuestion: context.pendingBossQuestion }),
976
- ...(context.bossReply === undefined
977
- ? {}
978
- : { bossReply: context.bossReply }),
1089
+ ...bossReplyFields(context),
979
1090
  }),
980
1091
  onDone: [
981
- {
982
- guard: 'isMoreTasks',
983
- target: 'reviewIrTask',
984
- actions: [
985
- {
986
- type: 'playbook.acceptedOutcome',
987
- params: {
988
- source: 'runIrTask',
989
- target: 'reviewIrTask',
990
- acceptedOutcome: 'moreTasks',
991
- },
992
- },
993
- 'rememberMoreTasks',
994
- ],
995
- },
996
- {
997
- guard: 'isFinalTask',
998
- target: 'reviewIrTask',
999
- actions: [
1000
- {
1001
- type: 'playbook.acceptedOutcome',
1002
- params: {
1003
- source: 'runIrTask',
1004
- target: 'reviewIrTask',
1005
- acceptedOutcome: 'finalTask',
1006
- },
1007
- },
1008
- 'rememberFinalTask',
1009
- ],
1010
- },
1011
- {
1012
- guard: 'needsBossReply',
1013
- target: 'awaitBossReply',
1014
- actions: [
1015
- {
1016
- type: 'playbook.acceptedOutcome',
1017
- params: {
1018
- source: 'runIrTask',
1019
- target: 'awaitBossReply',
1020
- acceptedOutcome: 'needsBossReply',
1021
- },
1022
- },
1023
- 'rememberPendingQuestion',
1024
- ],
1025
- },
1026
- {
1027
- target: 'failed',
1028
- actions: 'rememberMalformedPlayerOutput',
1029
- },
1092
+ suspendArm('irTaskPhase'),
1093
+ acceptPhaseArm(
1094
+ 'irTaskPhase',
1095
+ 'moreTasks',
1096
+ 'acceptMoreTasks',
1097
+ 'reviewIrTaskPhase',
1098
+ ),
1099
+ acceptPhaseArm(
1100
+ 'irTaskPhase',
1101
+ 'finalTask',
1102
+ 'acceptFinalTask',
1103
+ 'reviewIrTaskPhase',
1104
+ ),
1105
+ malformedPlayerOutputArm,
1030
1106
  ],
1031
- onError: { target: 'failed', actions: 'rememberActorError' },
1107
+ onError: playerOnError,
1032
1108
  },
1033
1109
  },
1034
- reviewIrTask: {
1035
- id: 'reviewIrTask',
1036
- description: STATE_DESCRIPTIONS.reviewIrTask,
1037
- meta: playbookMeta('reviewIrTask'),
1038
- tags: ['playbook.suspended'],
1110
+ reviewIrTaskPhase: {
1111
+ id: 'reviewIrTaskPhase',
1112
+ tags: 'playbook.suspended',
1113
+ description: STATE_DESCRIPTIONS.reviewIrTaskPhase,
1114
+ meta: meta('reviewIrTaskPhase'),
1039
1115
  invoke: {
1040
1116
  src: 'playbook',
1041
1117
  input: ({ context }): PlaybookInput => ({
1042
- stateId: 'reviewIrTask',
1118
+ stateId: 'reviewIrTaskPhase',
1043
1119
  sourceItem: 'CODE-4',
1044
- playbookId: 'review',
1045
- text: irTaskReviewText(context),
1120
+ playbookId: REVIEW_PLAYBOOK_ID,
1121
+ text: composeReviewText(REVIEW_IR_TASK_PHASE_TEMPLATE, {
1122
+ '<caller-input>': context.callerInput,
1123
+ '<code-commit>': context.codeCommit,
1124
+ '<coder-output>': context.coderOutput,
1125
+ '<ir-task>': context.irTask,
1126
+ }),
1046
1127
  }),
1047
1128
  onDone: [
1048
1129
  {
1049
- guard: 'reviewApprovedMoreTasks',
1050
- target: 'runIrTask',
1130
+ guard: 'reviewPassedNonfinalTask',
1131
+ target: 'irTaskPhase',
1132
+ actions: 'rememberReviewPass',
1051
1133
  },
1052
1134
  {
1053
- guard: 'reviewApprovedFinalTask',
1135
+ guard: 'reviewPassedFinalTask',
1054
1136
  target: 'done',
1055
- actions: 'completeSuccessfully',
1056
- },
1057
- {
1058
- target: 'reportedReviewFailure',
1059
- actions: 'completeWithInvalidReviewOutput',
1060
- },
1061
- ],
1062
- onError: [
1063
- {
1064
- guard: 'authoredReviewFailure',
1065
- target: 'reportedReviewFailure',
1066
- actions: 'completeWithReviewFailure',
1137
+ actions: 'rememberReviewPass',
1067
1138
  },
1068
- { target: 'failed', actions: 'rememberActorError' },
1139
+ { target: 'reviewFailed', actions: 'rememberReviewNotPassed' },
1069
1140
  ],
1141
+ onError: reviewOnError,
1070
1142
  },
1071
1143
  },
1072
1144
  awaitBossReply: {
1073
1145
  id: 'awaitBossReply',
1146
+ tags: 'playbook.parked',
1074
1147
  description: STATE_DESCRIPTIONS.awaitBossReply,
1075
- meta: playbookMeta('awaitBossReply'),
1076
- tags: ['playbook.parked'],
1148
+ meta: meta('awaitBossReply'),
1077
1149
  on: {
1078
1150
  BOSS_REPLY: [
1079
1151
  {
1080
1152
  guard: 'emptyBossReply',
1081
- target: '#failed',
1082
- actions: 'rememberEmptyBossReplyError',
1083
- },
1084
- {
1085
- guard: 'resumesFirstPhase',
1086
- target: '#runFirstPhase',
1087
- reenter: true,
1088
- actions: 'rememberBossReply',
1089
- },
1090
- {
1091
- guard: 'resumesIrTask',
1092
- target: '#runIrTask',
1093
- reenter: true,
1094
- actions: 'rememberBossReply',
1153
+ target: 'failed',
1154
+ actions: ['rememberMalformedBossReply', 'clearBossReplyContext'],
1095
1155
  },
1156
+ ...resumableStates(['firstPhase', 'irTaskPhase'] as const),
1096
1157
  ],
1097
- START_CODE: { target: 'runFirstPhase', actions: 'startCoding' },
1158
+ START_CODE: {
1159
+ guard: 'validStartCode',
1160
+ target: 'firstPhase',
1161
+ actions: 'startCode',
1162
+ },
1098
1163
  },
1099
1164
  },
1100
1165
  failed: {
1101
1166
  id: 'failed',
1167
+ tags: 'playbook.parked',
1102
1168
  description: STATE_DESCRIPTIONS.failed,
1103
- meta: playbookMeta('failed'),
1104
- tags: ['playbook.parked'],
1169
+ meta: meta('failed'),
1105
1170
  on: {
1106
- START_CODE: { target: 'runFirstPhase', actions: 'startCoding' },
1171
+ START_CODE: {
1172
+ guard: 'validStartCode',
1173
+ target: 'firstPhase',
1174
+ actions: 'startCode',
1175
+ },
1107
1176
  },
1108
1177
  },
1109
- reportedReviewFailure: {
1110
- id: 'reportedReviewFailure',
1111
- description: STATE_DESCRIPTIONS.reportedReviewFailure,
1112
- meta: terminalMeta('reportedReviewFailure', 'failure'),
1178
+ reviewFailed: {
1179
+ id: 'reviewFailed',
1113
1180
  type: 'final',
1181
+ description: STATE_DESCRIPTIONS.reviewFailed,
1182
+ meta: finalMeta('reviewFailed', 'failure'),
1114
1183
  },
1115
1184
  done: {
1116
1185
  id: 'done',
1117
- description: STATE_DESCRIPTIONS.done,
1118
- meta: terminalMeta('done', 'success'),
1119
1186
  type: 'final',
1187
+ description: STATE_DESCRIPTIONS.done,
1188
+ meta: finalMeta('done', 'success'),
1120
1189
  },
1121
1190
  },
1122
1191
  });
1123
1192
 
1124
- export default codingMachine;
1193
+ export default codeMachine;