@sublang/playbook 15.0.0 → 16.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 (81) hide show
  1. package/README.md +1 -1
  2. package/docs/embedding.md +9 -7
  3. package/package.json +3 -1
  4. package/reference/sdlc/branch.playbook/branch.fsm.d.ts +63 -33
  5. package/reference/sdlc/branch.playbook/branch.fsm.js +190 -108
  6. package/reference/sdlc/branch.playbook/branch.fsm.ts +269 -147
  7. package/reference/sdlc/branch.playbook/branch.gears.md +7 -17
  8. package/reference/sdlc/branch.playbook/branch.playbook.d.ts +12 -16
  9. package/reference/sdlc/branch.playbook/branch.playbook.js +112 -101
  10. package/reference/sdlc/branch.playbook/branch.playbook.ts +128 -171
  11. package/reference/sdlc/branch.playbook/branch.registry.d.ts +3 -2
  12. package/reference/sdlc/branch.playbook/branch.registry.ts +6 -2
  13. package/reference/sdlc/code.playbook/bin/repository-effects.js +54 -13
  14. package/reference/sdlc/code.playbook/code.fsm.d.ts +143 -90
  15. package/reference/sdlc/code.playbook/code.fsm.introspect.d.ts +8 -8
  16. package/reference/sdlc/code.playbook/code.fsm.introspect.ts +13 -13
  17. package/reference/sdlc/code.playbook/code.fsm.js +609 -649
  18. package/reference/sdlc/code.playbook/code.fsm.ts +890 -821
  19. package/reference/sdlc/code.playbook/code.gears.md +39 -29
  20. package/reference/sdlc/code.playbook/code.playbook.d.ts +13 -16
  21. package/reference/sdlc/code.playbook/code.playbook.js +166 -147
  22. package/reference/sdlc/code.playbook/code.playbook.ts +180 -229
  23. package/reference/sdlc/code.playbook/code.registry.d.ts +3 -2
  24. package/reference/sdlc/code.playbook/code.registry.ts +6 -2
  25. package/reference/sdlc/code.playbook/playbook-captain.d.ts +42 -0
  26. package/reference/sdlc/code.playbook/playbook-captain.js +829 -194
  27. package/reference/sdlc/code.playbook/playbook-captain.ts +1227 -220
  28. package/reference/sdlc/decide.md +1 -0
  29. package/reference/sdlc/decide.playbook/decide.fsm.d.ts +270 -154
  30. package/reference/sdlc/decide.playbook/decide.fsm.js +765 -732
  31. package/reference/sdlc/decide.playbook/decide.fsm.ts +1167 -945
  32. package/reference/sdlc/decide.playbook/decide.gears.md +28 -21
  33. package/reference/sdlc/decide.playbook/decide.playbook.d.ts +11 -67
  34. package/reference/sdlc/decide.playbook/decide.playbook.js +155 -3570
  35. package/reference/sdlc/decide.playbook/decide.playbook.ts +186 -5061
  36. package/reference/sdlc/decide.playbook/decide.registry.d.ts +8 -4
  37. package/reference/sdlc/decide.playbook/decide.registry.js +2 -2
  38. package/reference/sdlc/decide.playbook/decide.registry.ts +13 -6
  39. package/reference/sdlc/dev.playbook/dev.fsm.d.ts +113 -131
  40. package/reference/sdlc/dev.playbook/dev.fsm.js +517 -575
  41. package/reference/sdlc/dev.playbook/dev.fsm.ts +667 -787
  42. package/reference/sdlc/dev.playbook/dev.gears.md +52 -54
  43. package/reference/sdlc/dev.playbook/dev.playbook.d.ts +13 -10
  44. package/reference/sdlc/dev.playbook/dev.playbook.js +128 -120
  45. package/reference/sdlc/dev.playbook/dev.playbook.ts +143 -193
  46. package/reference/sdlc/dev.playbook/dev.registry.d.ts +3 -2
  47. package/reference/sdlc/dev.playbook/dev.registry.ts +6 -2
  48. package/reference/sdlc/pr.playbook/pr.fsm.d.ts +92 -85
  49. package/reference/sdlc/pr.playbook/pr.fsm.js +461 -518
  50. package/reference/sdlc/pr.playbook/pr.fsm.ts +533 -663
  51. package/reference/sdlc/pr.playbook/pr.gears.md +47 -63
  52. package/reference/sdlc/pr.playbook/pr.playbook.d.ts +13 -23
  53. package/reference/sdlc/pr.playbook/pr.playbook.js +118 -112
  54. package/reference/sdlc/pr.playbook/pr.playbook.ts +136 -190
  55. package/reference/sdlc/pr.playbook/pr.registry.d.ts +3 -2
  56. package/reference/sdlc/pr.playbook/pr.registry.ts +4 -2
  57. package/reference/sdlc/review.md +3 -3
  58. package/reference/sdlc/review.playbook/review.fsm.d.ts +150 -116
  59. package/reference/sdlc/review.playbook/review.fsm.js +479 -550
  60. package/reference/sdlc/review.playbook/review.fsm.ts +717 -662
  61. package/reference/sdlc/review.playbook/review.gears.md +32 -29
  62. package/reference/sdlc/review.playbook/review.playbook.d.ts +12 -12
  63. package/reference/sdlc/review.playbook/review.playbook.js +180 -155
  64. package/reference/sdlc/review.playbook/review.playbook.ts +190 -230
  65. package/reference/sdlc/review.playbook/review.registry.d.ts +6 -5
  66. package/reference/sdlc/review.playbook/review.registry.js +3 -3
  67. package/reference/sdlc/review.playbook/review.registry.ts +9 -5
  68. package/slc/gears2fsm.md +72 -3
  69. package/slc/link.md +50 -27
  70. package/slc/materialize-link.mjs +51 -13
  71. package/slc/optimize.md +4 -1
  72. package/slc/prefix-prompts.mjs +191 -0
  73. package/slc/prefix.md +203 -0
  74. package/slc/slc.pin-inputs.json +11 -1
  75. package/slc/text2gears.md +71 -11
  76. package/src/xstate-playbook-runtime.d.ts +82 -9
  77. package/src/xstate-playbook-runtime.js +1224 -283
  78. package/src/xstate-playbook-runtime.ts +1671 -338
  79. package/src/xstate-runtime.d.ts +3 -1
  80. package/src/xstate-runtime.js +9 -2
  81. package/src/xstate-runtime.ts +12 -2
@@ -1,20 +1,19 @@
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
  import { assign, fromPromise, setup } from 'xstate';
6
- const APPENDED_PHASE_PROMPT = [
7
- 'Keep to the original intent and follow what it asks.',
8
- 'Do not re-run tests or builds whose inputs have not changed since any previous reported run.',
9
- "Make the phase's minimal changes and then one new commit, following @specs/packages/git.md; never amend an existing commit.",
10
- 'Make the commit message explain concisely what changed and why, including relevant verification.',
11
- 'Identify every new commit you make.',
12
- 'Credit every AI that contributed to this commit: Coder <coder-llm>.',
13
- ];
8
+ import { validatePlaybookCallResult } from '@sublang/playbook/xstate-runtime';
9
+ /** One role-id array per parallel group; CODE declares no parallel group. */
10
+ export const concurrentRoleSets = [];
11
+ /** The literal nested-call target shared by both call states and guards. */
12
+ const REVIEW_PLAYBOOK_ID = 'review';
13
+ // ---------------------------------------------------------------------------
14
+ // Prompts and result contracts (verbatim from code.gears.md)
15
+ // ---------------------------------------------------------------------------
14
16
  const FIRST_PHASE_PROMPT = [
15
- '> Original request: <caller-input>',
16
- '> Run results: <run-results>',
17
- '',
18
17
  'First determine whether the coding request starts a new coding intent or continues an existing IR with unfinished work.',
19
18
  'If the request may continue an existing IR but does not identify it unambiguously, ask Boss before changing files.',
20
19
  '',
@@ -30,320 +29,424 @@ const FIRST_PHASE_PROMPT = [
30
29
  '',
31
30
  'Consult @specs/map.md for relevant context and @specs/meta.md for spec requirements, if needed.',
32
31
  '',
33
- ...APPENDED_PHASE_PROMPT,
34
- ].join('\n');
35
- const IR_TASK_PROMPT = [
32
+ 'Keep to the original intent and follow what it asks.',
33
+ 'Do not re-run tests or builds whose inputs have not changed since any previous reported run.',
34
+ "Make the phase's minimal changes and then one new commit, following @specs/packages/git.md; never amend an existing commit.",
35
+ 'Make the commit message explain concisely what changed and why, including relevant verification.',
36
+ 'Identify every new commit you make.',
37
+ 'Credit every AI that contributed to this commit: Coder <coder-llm>.',
38
+ '',
36
39
  '> Original request: <caller-input>',
37
- '> IR number: <ir-number>',
38
40
  '> Run results: <run-results>',
39
- '',
41
+ ].join('\n');
42
+ const IR_TASK_PHASE_PROMPT = [
40
43
  'Read the identified IR and implement exactly its next unfinished task, including corresponding tests or specs if any.',
41
44
  'Do not implement a later task in this phase.',
42
45
  "Mark the IR's progress and deliverables when relevant.",
43
46
  'If the IR will be finished after this phase, double-check that all acceptance criteria are met.',
44
47
  '',
45
- ...APPENDED_PHASE_PROMPT,
48
+ 'Keep to the original intent and follow what it asks.',
49
+ 'Do not re-run tests or builds whose inputs have not changed since any previous reported run.',
50
+ "Make the phase's minimal changes and then one new commit, following @specs/packages/git.md; never amend an existing commit.",
51
+ 'Make the commit message explain concisely what changed and why, including relevant verification.',
52
+ 'Identify every new commit you make.',
53
+ 'Credit every AI that contributed to this commit: Coder <coder-llm>.',
54
+ '',
55
+ '> Original request: <caller-input>',
56
+ '> IR number: <ir-number>',
57
+ '> Run results: <run-results>',
58
+ ].join('\n');
59
+ const REVIEW_NEW_INTENT_PHASE_TEMPLATE = [
60
+ '> Original intent: <caller-input>',
61
+ '> Review scope: the commit <code-commit> from this coding phase and its resulting repository state.',
62
+ '> Coder output: <coder-output>',
46
63
  ].join('\n');
47
- const NEEDS_BOSS_REPLY_RESULT = "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>`.";
48
- const FIRST_PHASE_RESULTS = {
49
- directCommit: 'Coder completed and committed the direct implementation phase. Output shall include `coderOutput: <verbatim final text>` and `latestCommit: <commit identity>`.',
50
- irCommit: '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.',
51
- moreTasks: '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.',
52
- finalTask: '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.',
53
- needsBossReply: NEEDS_BOSS_REPLY_RESULT,
64
+ const REVIEW_IR_TASK_PHASE_TEMPLATE = [
65
+ '> Original intent: <caller-input>',
66
+ '> Review scope: the commit <code-commit> from this coding phase and its resulting repository state.',
67
+ '> Coder output: <coder-output>',
68
+ '',
69
+ '> Current IR task: <ir-task>',
70
+ ].join('\n');
71
+ const NEEDS_BOSS_REPLY_DESCRIPTION = "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>`.";
72
+ const FIRST_PHASE_RESULT = {
73
+ directCommit: "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>`.",
74
+ irCommit: "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.",
75
+ moreTasks: "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.",
76
+ finalTask: "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.",
77
+ needsBossReply: NEEDS_BOSS_REPLY_DESCRIPTION,
54
78
  };
55
- const IR_TASK_RESULTS = {
56
- moreTasks: "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.",
57
- finalTask: "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.",
58
- needsBossReply: NEEDS_BOSS_REPLY_RESULT,
79
+ const IR_TASK_PHASE_RESULT = {
80
+ moreTasks: "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.",
81
+ finalTask: "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.",
82
+ needsBossReply: NEEDS_BOSS_REPLY_DESCRIPTION,
59
83
  };
60
84
  const STATE_DESCRIPTIONS = {
61
- ready: 'Waiting for a coding intent.',
62
- runFirstPhase: 'Coder is running the first coding phase: a direct implementation, a new intent record, or an existing intent-record task.',
63
- reviewFirstCommit: 'The REVIEW playbook is checking the direct or new-IR phase commit.',
64
- runIrTask: 'Coder is implementing exactly one unfinished intent-record task.',
65
- reviewIrTask: 'The REVIEW playbook is checking the latest task commit.',
66
- awaitBossReply: 'Waiting for Boss to answer Coder.',
67
- failed: 'The coding workflow failed and is waiting for a new coding intent.',
68
- reportedReviewFailure: 'The coding workflow reported a REVIEW failure and the last code-owned commit.',
69
- done: "The coding workflow completed after every phase's REVIEW passed with no unsettled findings.",
85
+ ready: 'Waiting for a coding request.',
86
+ firstPhase: 'Coder runs the first coding phase: a direct implementation, a new IR, or the next task of an existing IR.',
87
+ reviewNewIntentPhase: 'REVIEW examines the commit of the direct implementation or new-IR phase.',
88
+ irTaskPhase: "Coder implements the identified IR's next unfinished task.",
89
+ reviewIrTaskPhase: 'REVIEW examines the commit of the IR-task phase.',
90
+ awaitBossReply: "Waiting for Boss to answer the acting agent's question.",
91
+ failed: 'CODE parked after a control-plane failure and waits for Boss to restart or resume it.',
92
+ reviewFailed: 'CODE stopped because review did not pass a phase and reports the failure with its last code-owned commit.',
93
+ done: "CODE completed: every phase's review passed with no unsettled findings.",
70
94
  };
71
- function playbookMeta(stateId, role) {
72
- return {
73
- playbook: {
74
- stateId,
75
- description: STATE_DESCRIPTIONS[stateId],
76
- ...(role === undefined ? {} : { role }),
77
- },
78
- };
79
- }
80
- // DR-048: a final state additionally declares whether its outcome means the
81
- // workflow succeeded or failed, so a caller learns that from the machine
82
- // rather than from CODE's output fields.
83
- function terminalMeta(stateId, terminal) {
84
- return {
85
- playbook: {
86
- stateId,
87
- description: STATE_DESCRIPTIONS[stateId],
88
- terminal,
89
- },
90
- };
91
- }
92
- function isRecord(value) {
93
- if (value === null || typeof value !== 'object' || Array.isArray(value)) {
94
- return false;
95
- }
96
- const prototype = Object.getPrototypeOf(value);
97
- return prototype === Object.prototype || prototype === null;
98
- }
99
- function isNonEmptyString(value) {
100
- return typeof value === 'string' && value.trim().length > 0;
101
- }
102
- function playerOutput(event) {
103
- if (!isRecord(event))
95
+ const RESUME_METADATA = {
96
+ firstPhase: { sourceItem: 'CODE-1' },
97
+ irTaskPhase: { sourceItem: 'CODE-3' },
98
+ };
99
+ const REVIEW_NOT_PASSED = {
100
+ name: 'ReviewNotPassed',
101
+ message: 'review returned a terminal result that does not establish that the supplied scope was evaluated with no unsettled findings.',
102
+ };
103
+ // ---------------------------------------------------------------------------
104
+ // Structural helpers
105
+ // ---------------------------------------------------------------------------
106
+ const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
107
+ const isNonEmptyString = (value) => typeof value === 'string' && value.trim().length > 0;
108
+ const doneOutputOf = (event) => isRecord(event) ? event.output : undefined;
109
+ const actorErrorOf = (event) => isRecord(event) ? event.error : undefined;
110
+ /** Narrows a player done event to the declared player output contract. */
111
+ function playerOutputOf(event) {
112
+ const output = doneOutputOf(event);
113
+ if (!isRecord(output))
104
114
  return undefined;
105
- return isRecord(event.output) ? event.output : undefined;
106
- }
107
- function outputGuard(event, guard) {
108
- return playerOutput(event)?.guard === guard;
109
- }
110
- function outputString(event, field) {
111
- const value = playerOutput(event)?.[field];
112
- return isNonEmptyString(value) ? value : undefined;
113
- }
114
- function hasCommittedOutput(event) {
115
- return (outputString(event, 'coderOutput') !== undefined &&
116
- outputString(event, 'latestCommit') !== undefined);
117
- }
118
- function hasIrTaskOutput(event) {
119
- return (hasCommittedOutput(event) &&
120
- outputString(event, 'irNumber') !== undefined &&
121
- outputString(event, 'irTask') !== undefined);
122
- }
123
- function isDirectCommit({ event }) {
124
- return (outputGuard(event, 'directCommit') &&
125
- hasCommittedOutput(event));
126
- }
127
- function isIrCommit({ event }) {
128
- return (outputGuard(event, 'irCommit') &&
129
- hasCommittedOutput(event) &&
130
- outputString(event, 'irNumber') !== undefined);
131
- }
132
- function isMoreTasks({ event }) {
133
- return (outputGuard(event, 'moreTasks') &&
134
- hasIrTaskOutput(event));
135
- }
136
- function isFinalTask({ event }) {
137
- return (outputGuard(event, 'finalTask') &&
138
- hasIrTaskOutput(event));
139
- }
140
- function needsBossReply({ event }) {
141
- return (outputGuard(event, 'needsBossReply') &&
142
- outputString(event, 'question') !== undefined);
143
- }
144
- function reviewOutput(event) {
145
- return isRecord(event) ? event.output : undefined;
146
- }
147
- function isReviewApprovedValue(value) {
148
- if (!isRecord(value))
149
- return false;
150
- if (Reflect.ownKeys(value).some((key) => typeof key !== 'string' ||
151
- (key !== 'noUnsettledFindings' && key !== 'evaluatedRevision'))) {
152
- return false;
115
+ switch (output.guard) {
116
+ case 'directCommit':
117
+ return isNonEmptyString(output.coderOutput) &&
118
+ isNonEmptyString(output.latestCommit)
119
+ ? {
120
+ guard: 'directCommit',
121
+ coderOutput: output.coderOutput,
122
+ latestCommit: output.latestCommit,
123
+ }
124
+ : undefined;
125
+ case 'irCommit':
126
+ return isNonEmptyString(output.coderOutput) &&
127
+ isNonEmptyString(output.latestCommit) &&
128
+ isNonEmptyString(output.irNumber)
129
+ ? {
130
+ guard: 'irCommit',
131
+ coderOutput: output.coderOutput,
132
+ latestCommit: output.latestCommit,
133
+ irNumber: output.irNumber,
134
+ }
135
+ : undefined;
136
+ case 'moreTasks':
137
+ case 'finalTask':
138
+ return isNonEmptyString(output.coderOutput) &&
139
+ isNonEmptyString(output.latestCommit) &&
140
+ isNonEmptyString(output.irNumber) &&
141
+ isNonEmptyString(output.irTask)
142
+ ? {
143
+ guard: output.guard,
144
+ coderOutput: output.coderOutput,
145
+ latestCommit: output.latestCommit,
146
+ irNumber: output.irNumber,
147
+ irTask: output.irTask,
148
+ }
149
+ : undefined;
150
+ case 'needsBossReply':
151
+ return isNonEmptyString(output.question)
152
+ ? { guard: 'needsBossReply', question: output.question }
153
+ : undefined;
154
+ default:
155
+ return undefined;
153
156
  }
154
- return (value.noUnsettledFindings === true &&
155
- isNonEmptyString(value.evaluatedRevision));
156
- }
157
- function reviewApprovedFor(phase) {
158
- return ({ context, event }) => context.phase === phase && isReviewApprovedValue(reviewOutput(event));
159
157
  }
160
- function isStateValue(value, ancestors = new Set()) {
161
- if (typeof value === 'string')
158
+ /**
159
+ * Strict JSON validation: null, booleans, finite numbers, strings, exact
160
+ * arrays, and plain records of enumerable own data properties. Cycle
161
+ * detection tracks only the active recursion path.
162
+ */
163
+ function isJsonValue(value, path = new Set()) {
164
+ if (value === null)
162
165
  return true;
163
- if (!isRecord(value) || ancestors.has(value))
164
- return false;
165
- const next = new Set(ancestors).add(value);
166
- return Reflect.ownKeys(value).every((key) => typeof key === 'string' &&
167
- Object.prototype.propertyIsEnumerable.call(value, key) &&
168
- isStateValue(value[key], next));
169
- }
170
- function isStringArray(value) {
171
- return (Array.isArray(value) &&
172
- Reflect.ownKeys(value).length === value.length + 1 &&
173
- Reflect.ownKeys(value).every((key) => {
174
- if (key === 'length')
175
- return true;
176
- return (typeof key === 'string' &&
177
- /^(0|[1-9][0-9]*)$/.test(key) &&
178
- Number(key) < value.length);
179
- }) &&
180
- value.every((entry) => isNonEmptyString(entry)) &&
181
- new Set(value).size === value.length);
182
- }
183
- function isPlaybookState(value) {
184
- if (!isRecord(value))
185
- return false;
186
- const allowed = new Set([
187
- 'value',
188
- 'activeStateIds',
189
- 'tags',
190
- 'status',
191
- 'quiescent',
192
- 'stateId',
193
- ]);
194
- if (Reflect.ownKeys(value).some((key) => typeof key !== 'string' || !allowed.has(key)) ||
195
- !Object.prototype.hasOwnProperty.call(value, 'value') ||
196
- !Object.prototype.hasOwnProperty.call(value, 'activeStateIds') ||
197
- !Object.prototype.hasOwnProperty.call(value, 'tags') ||
198
- !Object.prototype.hasOwnProperty.call(value, 'status') ||
199
- !Object.prototype.hasOwnProperty.call(value, 'quiescent')) {
200
- return false;
166
+ switch (typeof value) {
167
+ case 'boolean':
168
+ case 'string':
169
+ return true;
170
+ case 'number':
171
+ return Number.isFinite(value);
172
+ case 'object':
173
+ break;
174
+ default:
175
+ return false;
201
176
  }
202
- return (isStateValue(value.value) &&
203
- isStringArray(value.activeStateIds) &&
204
- isStringArray(value.tags) &&
205
- (value.status === 'active' ||
206
- value.status === 'done' ||
207
- value.status === 'error' ||
208
- value.status === 'stopped') &&
209
- typeof value.quiescent === 'boolean' &&
210
- (!Object.prototype.hasOwnProperty.call(value, 'stateId') ||
211
- (isNonEmptyString(value.stateId) &&
212
- value.activeStateIds.length === 1 &&
213
- value.activeStateIds[0] === value.stateId)));
214
- }
215
- function nestedResultFromError(error) {
216
- if (!(error instanceof Error))
217
- return undefined;
218
- const result = error.result;
219
- return isRecord(result) ? result : undefined;
220
- }
221
- function isFailureTerminalRecord(value) {
222
- if (!isRecord(value))
177
+ const container = value;
178
+ if (path.has(container))
223
179
  return false;
224
- const allowed = new Set(['stateId', 'kind', 'description']);
225
- return (Reflect.ownKeys(value).every((key) => typeof key === 'string' && allowed.has(key)) &&
226
- isNonEmptyString(value.stateId) &&
227
- value.kind === 'failure' &&
228
- (!Object.prototype.hasOwnProperty.call(value, 'description') ||
229
- typeof value.description === 'string'));
230
- }
231
- function normalizedReviewFailure(error) {
232
- const result = nestedResultFromError(error);
233
- if (result === undefined)
234
- return undefined;
235
- const allowed = new Set([
236
- 'status',
237
- 'playbookId',
238
- 'childSessionId',
239
- 'state',
240
- ...(result.status === 'ok' ? ['output', 'terminal'] : ['error']),
241
- ]);
242
- if (Reflect.ownKeys(result).some((key) => typeof key !== 'string' || !allowed.has(key)) ||
243
- (result.status !== 'aborted' &&
244
- result.status !== 'error' &&
245
- result.status !== 'ok') ||
246
- result.playbookId !== 'review') {
247
- return undefined;
248
- }
249
- if (Object.prototype.hasOwnProperty.call(result, 'childSessionId') &&
250
- !isNonEmptyString(result.childSessionId)) {
251
- return undefined;
252
- }
253
- if (Object.prototype.hasOwnProperty.call(result, 'state') &&
254
- !isPlaybookState(result.state)) {
255
- return undefined;
256
- }
257
- if (result.status === 'ok') {
258
- if (!Object.prototype.hasOwnProperty.call(result, 'terminal') ||
259
- !isFailureTerminalRecord(result.terminal) ||
260
- !isNonEmptyString(result.childSessionId)) {
261
- return undefined;
180
+ path.add(container);
181
+ try {
182
+ if (Array.isArray(container)) {
183
+ if (Object.getPrototypeOf(container) !== Array.prototype)
184
+ return false;
185
+ const keys = Reflect.ownKeys(container);
186
+ if (keys.length !== container.length + 1)
187
+ return false;
188
+ const lengthDescriptor = Object.getOwnPropertyDescriptor(container, 'length');
189
+ if (lengthDescriptor === undefined ||
190
+ lengthDescriptor.configurable !== false ||
191
+ lengthDescriptor.enumerable !== false ||
192
+ lengthDescriptor.value !== container.length) {
193
+ return false;
194
+ }
195
+ for (let index = 0; index < container.length; index += 1) {
196
+ const descriptor = Object.getOwnPropertyDescriptor(container, String(index));
197
+ if (descriptor === undefined ||
198
+ !descriptor.enumerable ||
199
+ !('value' in descriptor) ||
200
+ !isJsonValue(descriptor.value, path)) {
201
+ return false;
202
+ }
203
+ }
204
+ return true;
262
205
  }
263
- return {
264
- status: 'ok',
265
- ...(result.output === undefined
266
- ? {}
267
- : { output: result.output }),
268
- };
269
- }
270
- let normalizedError;
271
- if (Object.prototype.hasOwnProperty.call(result, 'error')) {
272
- if (!isRecord(result.error))
273
- return undefined;
274
- const errorKeys = Reflect.ownKeys(result.error);
275
- if (errorKeys.some((key) => typeof key !== 'string' ||
276
- (key !== 'name' && key !== 'message' && key !== 'stack')) ||
277
- !Object.prototype.hasOwnProperty.call(result.error, 'name') ||
278
- !Object.prototype.hasOwnProperty.call(result.error, 'message') ||
279
- !isNonEmptyString(result.error.name) ||
280
- typeof result.error.message !== 'string' ||
281
- (Object.prototype.hasOwnProperty.call(result.error, 'stack') &&
282
- typeof result.error.stack !== 'string')) {
283
- return undefined;
206
+ const prototype = Object.getPrototypeOf(container);
207
+ if (prototype !== Object.prototype && prototype !== null)
208
+ return false;
209
+ for (const key of Reflect.ownKeys(container)) {
210
+ if (typeof key !== 'string')
211
+ return false;
212
+ const descriptor = Object.getOwnPropertyDescriptor(container, key);
213
+ if (descriptor === undefined ||
214
+ !descriptor.enumerable ||
215
+ !('value' in descriptor) ||
216
+ !isJsonValue(descriptor.value, path)) {
217
+ return false;
218
+ }
284
219
  }
285
- normalizedError = {
286
- name: result.error.name,
287
- message: result.error.message,
288
- };
220
+ return true;
289
221
  }
290
- else if (result.status === 'error') {
291
- return undefined;
222
+ finally {
223
+ path.delete(container);
292
224
  }
293
- return {
294
- status: result.status,
295
- ...(normalizedError === undefined ? {} : { error: normalizedError }),
296
- };
297
- }
298
- function authoredReviewFailure({ event }) {
299
- const error = isRecord(event) ? event.error : undefined;
300
- return normalizedReviewFailure(error) !== undefined;
301
225
  }
302
- function compactError(value) {
226
+ /** Normalizes any thrown or malformed value to a JSON-safe error record. */
227
+ function errorRecord(value) {
303
228
  if (value instanceof Error) {
304
229
  return {
305
230
  name: value.name || 'Error',
306
231
  message: value.message,
232
+ ...(typeof value.stack === 'string' ? { stack: value.stack } : {}),
233
+ };
234
+ }
235
+ if (isRecord(value) && typeof value.message === 'string') {
236
+ return {
237
+ name: isNonEmptyString(value.name) ? value.name : 'Error',
238
+ message: value.message,
307
239
  };
308
240
  }
309
241
  return { name: 'Error', message: String(value) };
310
242
  }
311
- function authoredReviewError(event) {
312
- const outer = isRecord(event) ? event.error : undefined;
313
- const failure = normalizedReviewFailure(outer);
314
- if (failure !== undefined && failure.status !== 'ok' && failure.error) {
315
- return failure.error;
243
+ // ---------------------------------------------------------------------------
244
+ // Nested `review` results
245
+ // ---------------------------------------------------------------------------
246
+ /**
247
+ * Source-authored acceptance on `review`'s declared public output: the
248
+ * result returns the exact evaluated repository revision and affirmatively
249
+ * establishes that no unsettled findings remain. The shared bridge correlates
250
+ * the result with the supplied review scope.
251
+ */
252
+ function reviewPassOf(event) {
253
+ const output = doneOutputOf(event);
254
+ if (!isRecord(output))
255
+ return undefined;
256
+ return output.noUnsettledFindings === true &&
257
+ isNonEmptyString(output.evaluatedRevision)
258
+ ? { noUnsettledFindings: true, evaluatedRevision: output.evaluatedRevision }
259
+ : undefined;
260
+ }
261
+ /**
262
+ * Recognizes an authored rejected child result: an aborted or error call, or
263
+ * a child that completed at its own authored failure terminal.
264
+ */
265
+ export function authoredChildResult(error, expectedPlaybookId) {
266
+ if (!(error instanceof Error))
267
+ return undefined;
268
+ try {
269
+ const result = validatePlaybookCallResult(error.result, expectedPlaybookId);
270
+ return result.status !== 'ok' || result.terminal?.kind === 'failure'
271
+ ? result
272
+ : undefined;
316
273
  }
317
- if (failure?.status === 'aborted') {
318
- return { name: 'AbortError', message: 'REVIEW was aborted.' };
274
+ catch {
275
+ return undefined;
319
276
  }
320
- return compactError(outer);
321
277
  }
322
- function quoteRelay(label, value) {
323
- const lines = value.split('\n');
324
- return [`> ${label}: ${lines[0] ?? ''}`, ...lines.slice(1).map((line) => `> ${line}`)].join('\n');
278
+ /** Compact evidence and reported error for an authored review failure. */
279
+ function authoredReviewFailureOf(event) {
280
+ const result = authoredChildResult(actorErrorOf(event), REVIEW_PLAYBOOK_ID);
281
+ if (result === undefined)
282
+ return undefined;
283
+ if (result.status === 'ok') {
284
+ const description = result.terminal?.description;
285
+ return {
286
+ evidence: {
287
+ playbookId: REVIEW_PLAYBOOK_ID,
288
+ status: 'ok',
289
+ ...(result.output === undefined ? {} : { output: result.output }),
290
+ },
291
+ error: {
292
+ name: 'ReviewFailed',
293
+ message: isNonEmptyString(description)
294
+ ? `review ended at its authored failure terminal: ${description}`
295
+ : 'review ended at its authored failure terminal.',
296
+ },
297
+ };
298
+ }
299
+ const error = result.error !== undefined
300
+ ? { name: result.error.name, message: result.error.message }
301
+ : { name: 'AbortError', message: 'The review call was aborted.' };
302
+ return {
303
+ evidence: { playbookId: REVIEW_PLAYBOOK_ID, status: result.status, error },
304
+ error,
305
+ };
325
306
  }
326
- function reviewScopeLine(context) {
327
- return (`> Review scope: the commit ${context.latestCommit ?? ''} ` +
328
- 'from this coding phase and its resulting repository state.');
307
+ // ---------------------------------------------------------------------------
308
+ // Nested call text composition (quoted relays)
309
+ // ---------------------------------------------------------------------------
310
+ const REVIEW_PLACEHOLDER = /<caller-input>|<code-commit>|<coder-output>|<ir-task>/g;
311
+ /**
312
+ * Composes review call text in one callback pass per template line: a quoted
313
+ * relay line whose value is empty contributes no line, a multi-line value is
314
+ * quoted line by line, and an absent value keeps its placeholder literal.
315
+ */
316
+ function composeReviewText(template, values) {
317
+ const lines = [];
318
+ for (const line of template.split('\n')) {
319
+ const quoted = line.startsWith('> ');
320
+ const tokens = line.match(REVIEW_PLACEHOLDER) ?? [];
321
+ if (quoted && tokens.some((token) => values[token] === ''))
322
+ continue;
323
+ lines.push(line.replace(REVIEW_PLACEHOLDER, (token) => {
324
+ const value = values[token];
325
+ if (value === undefined)
326
+ return token;
327
+ return quoted ? value.split('\n').join('\n> ') : value;
328
+ }));
329
+ }
330
+ while (lines.length > 0 && lines[lines.length - 1] === '')
331
+ lines.pop();
332
+ return lines.join('\n');
329
333
  }
330
- function newIntentReviewText(context) {
331
- return [
332
- quoteRelay('Original intent', context.callerInput ?? ''),
333
- reviewScopeLine(context),
334
- quoteRelay('Coder output', context.coderOutput ?? ''),
335
- ].join('\n');
334
+ const acceptsOutcome = (outcome) => ({ event }) => playerOutputOf(event)?.guard === outcome;
335
+ const needsBossReplyWithQuestion = ({ event }) => playerOutputOf(event)?.guard === 'needsBossReply';
336
+ // A review state is entered only through an accepting phase arm, which records
337
+ // the phase outcome and its non-empty code-owned commit.
338
+ const reviewPassedAfter = (outcome) => ({ context, event }) => context.phaseOutcome === outcome &&
339
+ isNonEmptyString(context.codeCommit) &&
340
+ reviewPassOf(event) !== undefined;
341
+ const authoredReviewFailure = ({ event }) => authoredReviewFailureOf(event) !== undefined;
342
+ const validStartCode = ({ event }) => event.type === 'START_CODE' &&
343
+ isNonEmptyString(event.callerInput) &&
344
+ (event.runResults === undefined || typeof event.runResults === 'string');
345
+ const repliesToPendingQuestion = ({ context, event }) => event.type === 'BOSS_REPLY' &&
346
+ context.pendingBossQuestion !== undefined &&
347
+ (event.questionId === undefined ||
348
+ event.questionId === context.pendingBossQuestion.questionId);
349
+ const emptyBossReply = (args) => repliesToPendingQuestion(args) &&
350
+ args.event.type === 'BOSS_REPLY' &&
351
+ !isNonEmptyString(args.event.answer);
352
+ const INTERRUPT_PRECONDITIONS = {
353
+ firstPhase: (context) => isNonEmptyString(context.callerInput),
354
+ irTaskPhase: (context) => isNonEmptyString(context.callerInput) &&
355
+ isNonEmptyString(context.irNumber),
356
+ };
357
+ // ---------------------------------------------------------------------------
358
+ // Transition helpers
359
+ // ---------------------------------------------------------------------------
360
+ /** One guarded root transition per jumpable state. */
361
+ function bossInterrupts(ids) {
362
+ return ids.map((id) => ({
363
+ guard: ({ context, event }) => event.type === 'BOSS_INTERRUPT' &&
364
+ event.targetId === id &&
365
+ INTERRUPT_PRECONDITIONS[id](context),
366
+ target: `#${id}`,
367
+ reenter: true,
368
+ actions: 'resetForInterrupt',
369
+ }));
336
370
  }
337
- function irTaskReviewText(context) {
338
- return [
339
- quoteRelay('Original intent', context.callerInput ?? ''),
340
- reviewScopeLine(context),
341
- quoteRelay('Coder output', context.coderOutput ?? ''),
342
- quoteRelay('Current IR task', context.irTask ?? ''),
343
- ].join('\n');
371
+ /** One guarded `BOSS_REPLY` resume transition per suspended working leaf. */
372
+ function resumableStates(ids) {
373
+ return ids.map((id) => ({
374
+ guard: (args) => repliesToPendingQuestion(args) &&
375
+ args.event.type === 'BOSS_REPLY' &&
376
+ isNonEmptyString(args.event.answer) &&
377
+ args.context.pendingBossQuestion?.resumeStateId === id,
378
+ target: `#${id}`,
379
+ reenter: true,
380
+ actions: 'rememberBossReply',
381
+ }));
344
382
  }
345
- const machineSetup = setup({
346
- types: {},
383
+ const bossReplyFields = (context) => ({
384
+ ...(context.pendingBossQuestion !== undefined
385
+ ? { pendingBossQuestion: context.pendingBossQuestion }
386
+ : {}),
387
+ ...(context.bossReply !== undefined ? { bossReply: context.bossReply } : {}),
388
+ });
389
+ const acceptedOutcome = (source, target, outcome) => ({
390
+ type: 'playbook.acceptedOutcome',
391
+ params: { source, target, acceptedOutcome: outcome },
392
+ });
393
+ const acceptPhaseArm = (source, outcome, guard, target) => ({
394
+ guard,
395
+ target: `#${target}`,
396
+ actions: [
397
+ acceptedOutcome(source, target, outcome),
398
+ 'rememberPhaseCommit',
399
+ 'clearBossReplyContext',
400
+ ],
401
+ });
402
+ const suspendArm = (source) => ({
403
+ guard: 'needsBossReplyWithQuestion',
404
+ target: '#awaitBossReply',
405
+ actions: [
406
+ acceptedOutcome(source, 'awaitBossReply', 'needsBossReply'),
407
+ { type: 'setPendingBossQuestion', params: { resumeStateId: source } },
408
+ ],
409
+ });
410
+ const malformedPlayerOutputArm = {
411
+ target: '#failed',
412
+ actions: ['rememberMalformedPlayerOutput', 'clearBossReplyContext'],
413
+ };
414
+ const playerOnError = {
415
+ target: '#failed',
416
+ actions: ['rememberActorError', 'clearBossReplyContext'],
417
+ };
418
+ const reviewOnError = [
419
+ {
420
+ guard: 'authoredReviewFailure',
421
+ target: '#reviewFailed',
422
+ actions: 'rememberAuthoredReviewFailure',
423
+ },
424
+ { target: '#failed', actions: 'rememberActorError' },
425
+ ];
426
+ const meta = (stateId, role) => ({
427
+ playbook: {
428
+ stateId,
429
+ description: STATE_DESCRIPTIONS[stateId],
430
+ ...(role === undefined ? {} : { role }),
431
+ },
432
+ });
433
+ const finalMeta = (stateId, terminal) => ({
434
+ playbook: {
435
+ stateId,
436
+ description: STATE_DESCRIPTIONS[stateId],
437
+ terminal,
438
+ },
439
+ });
440
+ // ---------------------------------------------------------------------------
441
+ // Machine
442
+ // ---------------------------------------------------------------------------
443
+ export const codeMachine = setup({
444
+ types: {
445
+ context: {},
446
+ events: {},
447
+ input: {},
448
+ output: {},
449
+ },
347
450
  actors: {
348
451
  player: fromPromise(async () => {
349
452
  throw new Error('player actor must be provided by the runner');
@@ -352,488 +455,345 @@ const machineSetup = setup({
352
455
  throw new Error('playbook actor must be provided by the runner');
353
456
  }),
354
457
  },
355
- guards: {
356
- isDirectCommit,
357
- isIrCommit,
358
- isMoreTasks,
359
- isFinalTask,
360
- needsBossReply,
361
- reviewApprovedDirect: reviewApprovedFor('direct'),
362
- reviewApprovedIrCreated: reviewApprovedFor('ir-created'),
363
- reviewApprovedMoreTasks: reviewApprovedFor('ir-task-more'),
364
- reviewApprovedFinalTask: reviewApprovedFor('ir-task-final'),
365
- authoredReviewFailure,
366
- emptyBossReply: ({ event }) => event.type === 'BOSS_REPLY' && event.answer.trim().length === 0,
367
- resumesFirstPhase: ({ context, event }) => event.type === 'BOSS_REPLY' &&
368
- event.answer.trim().length > 0 &&
369
- context.pendingBossQuestion?.resumeStateId === 'runFirstPhase' &&
370
- (event.questionId === undefined || event.questionId === 'runFirstPhase'),
371
- resumesIrTask: ({ context, event }) => event.type === 'BOSS_REPLY' &&
372
- event.answer.trim().length > 0 &&
373
- context.pendingBossQuestion?.resumeStateId === 'runIrTask' &&
374
- (event.questionId === undefined || event.questionId === 'runIrTask'),
375
- },
376
458
  actions: {
377
459
  'playbook.acceptedOutcome': (_args, _params) => undefined,
378
- startCoding: assign(({ context, event }) => {
379
- if (event.type !== 'START_CODE')
380
- return {};
381
- return {
382
- runResults: context.runResults,
460
+ startCode: assign(({ context, event }) => event.type === 'START_CODE'
461
+ ? {
383
462
  callerInput: event.callerInput,
463
+ runResults: event.runResults ?? context.runResults,
464
+ codeCommit: undefined,
384
465
  coderOutput: undefined,
385
- latestCommit: undefined,
386
466
  irNumber: undefined,
387
467
  irTask: undefined,
468
+ phaseOutcome: undefined,
388
469
  evaluatedRevision: undefined,
389
- phase: undefined,
390
- completion: undefined,
391
470
  reviewError: undefined,
471
+ reviewEvidence: undefined,
392
472
  lastError: undefined,
393
473
  pendingBossQuestion: undefined,
394
474
  bossReply: undefined,
395
- };
396
- }),
397
- rememberDirectCommit: assign(({ event }) => ({
398
- coderOutput: outputString(event, 'coderOutput'),
399
- latestCommit: outputString(event, 'latestCommit'),
400
- phase: 'direct',
401
- pendingBossQuestion: undefined,
402
- bossReply: undefined,
403
- })),
404
- rememberIrCommit: assign(({ event }) => ({
405
- coderOutput: outputString(event, 'coderOutput'),
406
- latestCommit: outputString(event, 'latestCommit'),
407
- irNumber: outputString(event, 'irNumber'),
408
- irTask: undefined,
409
- phase: 'ir-created',
410
- pendingBossQuestion: undefined,
411
- bossReply: undefined,
412
- })),
413
- rememberMoreTasks: assign(({ event }) => ({
414
- coderOutput: outputString(event, 'coderOutput'),
415
- latestCommit: outputString(event, 'latestCommit'),
416
- irNumber: outputString(event, 'irNumber'),
417
- irTask: outputString(event, 'irTask'),
418
- phase: 'ir-task-more',
419
- pendingBossQuestion: undefined,
420
- bossReply: undefined,
421
- })),
422
- rememberFinalTask: assign(({ event }) => ({
423
- coderOutput: outputString(event, 'coderOutput'),
424
- latestCommit: outputString(event, 'latestCommit'),
425
- irNumber: outputString(event, 'irNumber'),
426
- irTask: outputString(event, 'irTask'),
427
- phase: 'ir-task-final',
475
+ }
476
+ : {}),
477
+ resetForInterrupt: assign({
478
+ phaseOutcome: undefined,
479
+ evaluatedRevision: undefined,
480
+ reviewError: undefined,
481
+ reviewEvidence: undefined,
482
+ lastError: undefined,
428
483
  pendingBossQuestion: undefined,
429
484
  bossReply: undefined,
430
- })),
431
- rememberPendingQuestion: assign(({ context, event }) => {
432
- const question = outputString(event, 'question');
433
- const output = playerOutput(event);
434
- const stateId = context.irNumber === undefined ? 'runFirstPhase' : 'runIrTask';
435
- if (question === undefined || output?.guard !== 'needsBossReply') {
436
- return { lastError: new Error('Coder question output was malformed') };
437
- }
485
+ }),
486
+ rememberPhaseCommit: assign(({ event }) => {
487
+ const output = playerOutputOf(event);
488
+ if (output === undefined || output.guard === 'needsBossReply')
489
+ return {};
438
490
  return {
439
- pendingBossQuestion: {
440
- questionId: stateId,
441
- resumeStateId: stateId,
442
- sourceItem: stateId === 'runFirstPhase' ? 'CODE-1' : 'CODE-3',
443
- asker: { kind: 'role', roleId: 'coder' },
444
- question,
445
- },
446
- bossReply: undefined,
491
+ codeCommit: output.latestCommit,
492
+ coderOutput: output.coderOutput,
493
+ phaseOutcome: output.guard,
494
+ irNumber: output.guard === 'directCommit' ? undefined : output.irNumber,
495
+ irTask: output.guard === 'moreTasks' || output.guard === 'finalTask'
496
+ ? output.irTask
497
+ : undefined,
498
+ lastError: undefined,
447
499
  };
448
500
  }),
501
+ setPendingBossQuestion: assign(({ event }, params) => {
502
+ const output = playerOutputOf(event);
503
+ if (output?.guard !== 'needsBossReply')
504
+ return {};
505
+ const pendingBossQuestion = {
506
+ questionId: params.resumeStateId,
507
+ resumeStateId: params.resumeStateId,
508
+ sourceItem: RESUME_METADATA[params.resumeStateId].sourceItem,
509
+ asker: { kind: 'role', roleId: 'coder' },
510
+ question: output.question,
511
+ };
512
+ return { pendingBossQuestion, bossReply: undefined, lastError: undefined };
513
+ }),
449
514
  rememberBossReply: assign(({ event }) => event.type === 'BOSS_REPLY' ? { bossReply: event.answer } : {}),
450
- rememberEmptyBossReplyError: assign({
451
- lastError: () => new Error('BOSS_REPLY received an empty answer'),
515
+ clearBossReplyContext: assign({
452
516
  pendingBossQuestion: undefined,
453
517
  bossReply: undefined,
454
518
  }),
455
- completeSuccessfully: assign(({ event }) => {
456
- const output = reviewOutput(event);
457
- const approved = isReviewApprovedValue(output) ? output : undefined;
458
- return {
459
- completion: 'complete',
460
- evaluatedRevision: approved?.evaluatedRevision,
461
- reviewError: undefined,
519
+ rememberReviewPass: assign(({ event }) => {
520
+ const pass = reviewPassOf(event);
521
+ return pass === undefined
522
+ ? {}
523
+ : { evaluatedRevision: pass.evaluatedRevision, lastError: undefined };
524
+ }),
525
+ rememberReviewNotPassed: assign(({ event }) => {
526
+ const output = doneOutputOf(event);
527
+ const reviewEvidence = {
528
+ playbookId: REVIEW_PLAYBOOK_ID,
529
+ status: 'ok',
530
+ ...(isJsonValue(output) ? { output } : {}),
462
531
  };
532
+ return { reviewEvidence, reviewError: REVIEW_NOT_PASSED, lastError: undefined };
533
+ }),
534
+ rememberAuthoredReviewFailure: assign(({ event }) => {
535
+ const failure = authoredReviewFailureOf(event);
536
+ return failure === undefined
537
+ ? {}
538
+ : {
539
+ reviewEvidence: failure.evidence,
540
+ reviewError: failure.error,
541
+ lastError: undefined,
542
+ };
463
543
  }),
464
- completeWithReviewFailure: assign(({ event }) => ({
465
- completion: 'review-failed',
466
- reviewError: authoredReviewError(event),
467
- })),
468
- completeWithInvalidReviewOutput: assign(({ event }) => ({
469
- completion: 'review-failed',
470
- reviewError: {
471
- name: 'ReviewContractError',
472
- message: `REVIEW returned an invalid approval result: ` +
473
- JSON.stringify(reviewOutput(event)),
474
- },
475
- })),
476
544
  rememberActorError: assign(({ event }) => ({
477
- lastError: event.error,
478
- })),
479
- rememberMalformedPlayerOutput: assign(({ context }) => ({
480
- lastError: new Error(`Coder result for ${context.irNumber === undefined ? 'CODE-1' : 'CODE-3'} ` +
481
- 'did not match a declared outcome.'),
545
+ lastError: errorRecord(actorErrorOf(event)),
482
546
  })),
547
+ rememberMalformedPlayerOutput: assign(({ event }) => {
548
+ const output = doneOutputOf(event);
549
+ return {
550
+ lastError: {
551
+ name: 'MalformedPlayerOutput',
552
+ message: isRecord(output) && output.guard === 'needsBossReply'
553
+ ? 'Coder output declared needsBossReply without a question.'
554
+ : 'Coder output did not match an outcome declared for the working state.',
555
+ },
556
+ };
557
+ }),
558
+ rememberMalformedBossReply: assign({
559
+ lastError: {
560
+ name: 'MalformedBossReply',
561
+ message: 'BOSS_REPLY carried an empty answer.',
562
+ },
563
+ }),
483
564
  },
484
- });
485
- export const codingMachine = machineSetup.createMachine({
565
+ guards: {
566
+ needsBossReplyWithQuestion,
567
+ acceptDirectCommit: acceptsOutcome('directCommit'),
568
+ acceptIrCommit: acceptsOutcome('irCommit'),
569
+ acceptMoreTasks: acceptsOutcome('moreTasks'),
570
+ acceptFinalTask: acceptsOutcome('finalTask'),
571
+ reviewPassedDirect: reviewPassedAfter('directCommit'),
572
+ reviewPassedNewIr: reviewPassedAfter('irCommit'),
573
+ reviewPassedNonfinalTask: reviewPassedAfter('moreTasks'),
574
+ reviewPassedFinalTask: reviewPassedAfter('finalTask'),
575
+ authoredReviewFailure,
576
+ validStartCode,
577
+ emptyBossReply,
578
+ },
579
+ }).createMachine({
486
580
  id: 'code',
487
581
  initial: 'ready',
488
582
  context: ({ input }) => ({
489
- runResults: typeof input.runResults === 'string' ? input.runResults : '',
583
+ runResults: typeof input?.runResults === 'string' ? input.runResults : '',
490
584
  }),
491
585
  output: ({ context }) => {
492
- const lastCodeCommit = context.latestCommit;
493
- if (!isNonEmptyString(lastCodeCommit)) {
494
- throw new Error('CODE reached a final state without a commit identity');
586
+ if (!isNonEmptyString(context.codeCommit)) {
587
+ throw new Error('CODE reached a final state without its last code-owned commit');
495
588
  }
496
- if (context.completion === 'review-failed') {
589
+ if (context.reviewError !== undefined) {
497
590
  return {
498
591
  status: 'review-failed',
499
- lastCodeCommit,
500
- error: context.reviewError ?? {
501
- name: 'Error',
502
- message: 'REVIEW failed without error detail.',
503
- },
592
+ lastCodeCommit: context.codeCommit,
593
+ error: context.reviewError,
504
594
  };
505
595
  }
506
- const finalEvaluatedRevision = context.evaluatedRevision;
507
- if (!isNonEmptyString(finalEvaluatedRevision)) {
508
- throw new Error('CODE completed without an evaluated repository revision');
596
+ if (!isNonEmptyString(context.evaluatedRevision)) {
597
+ throw new Error('CODE completed without the final evaluated revision');
509
598
  }
510
599
  return {
511
600
  status: 'complete',
512
- lastCodeCommit,
513
- finalEvaluatedRevision,
601
+ lastCodeCommit: context.codeCommit,
602
+ finalEvaluatedRevision: context.evaluatedRevision,
514
603
  allReviewsPassed: true,
515
604
  };
516
605
  },
606
+ on: {
607
+ BOSS_INTERRUPT: bossInterrupts(['firstPhase', 'irTaskPhase']),
608
+ },
517
609
  states: {
518
610
  ready: {
519
611
  id: 'ready',
612
+ tags: 'playbook.parked',
520
613
  description: STATE_DESCRIPTIONS.ready,
521
- meta: playbookMeta('ready'),
522
- tags: ['playbook.parked'],
614
+ meta: meta('ready'),
523
615
  on: {
524
- START_CODE: { target: 'runFirstPhase', actions: 'startCoding' },
616
+ START_CODE: {
617
+ guard: 'validStartCode',
618
+ target: 'firstPhase',
619
+ actions: 'startCode',
620
+ },
525
621
  },
526
622
  },
527
- runFirstPhase: {
528
- id: 'runFirstPhase',
529
- description: STATE_DESCRIPTIONS.runFirstPhase,
530
- meta: playbookMeta('runFirstPhase', 'coder'),
531
- tags: ['playbook.busy'],
623
+ firstPhase: {
624
+ id: 'firstPhase',
625
+ tags: 'playbook.busy',
626
+ description: STATE_DESCRIPTIONS.firstPhase,
627
+ meta: meta('firstPhase', 'coder'),
532
628
  invoke: {
533
629
  src: 'player',
534
630
  input: ({ context }) => ({
535
- stateId: 'runFirstPhase',
631
+ stateId: 'firstPhase',
536
632
  role: 'coder',
537
633
  sourceItem: 'CODE-1',
538
634
  prompt: FIRST_PHASE_PROMPT,
539
- result: FIRST_PHASE_RESULTS,
540
- callerInput: context.callerInput ?? '',
635
+ result: FIRST_PHASE_RESULT,
636
+ ...(context.callerInput !== undefined
637
+ ? { callerInput: context.callerInput }
638
+ : {}),
541
639
  runResults: context.runResults,
542
- ...(context.pendingBossQuestion === undefined
543
- ? {}
544
- : { pendingBossQuestion: context.pendingBossQuestion }),
545
- ...(context.bossReply === undefined
546
- ? {}
547
- : { bossReply: context.bossReply }),
640
+ ...bossReplyFields(context),
548
641
  }),
549
642
  onDone: [
550
- {
551
- guard: 'isDirectCommit',
552
- target: 'reviewFirstCommit',
553
- actions: [
554
- {
555
- type: 'playbook.acceptedOutcome',
556
- params: {
557
- source: 'runFirstPhase',
558
- target: 'reviewFirstCommit',
559
- acceptedOutcome: 'directCommit',
560
- },
561
- },
562
- 'rememberDirectCommit',
563
- ],
564
- },
565
- {
566
- guard: 'isIrCommit',
567
- target: 'reviewFirstCommit',
568
- actions: [
569
- {
570
- type: 'playbook.acceptedOutcome',
571
- params: {
572
- source: 'runFirstPhase',
573
- target: 'reviewFirstCommit',
574
- acceptedOutcome: 'irCommit',
575
- },
576
- },
577
- 'rememberIrCommit',
578
- ],
579
- },
580
- {
581
- guard: 'isMoreTasks',
582
- target: 'reviewIrTask',
583
- actions: [
584
- {
585
- type: 'playbook.acceptedOutcome',
586
- params: {
587
- source: 'runFirstPhase',
588
- target: 'reviewIrTask',
589
- acceptedOutcome: 'moreTasks',
590
- },
591
- },
592
- 'rememberMoreTasks',
593
- ],
594
- },
595
- {
596
- guard: 'isFinalTask',
597
- target: 'reviewIrTask',
598
- actions: [
599
- {
600
- type: 'playbook.acceptedOutcome',
601
- params: {
602
- source: 'runFirstPhase',
603
- target: 'reviewIrTask',
604
- acceptedOutcome: 'finalTask',
605
- },
606
- },
607
- 'rememberFinalTask',
608
- ],
609
- },
610
- {
611
- guard: 'needsBossReply',
612
- target: 'awaitBossReply',
613
- actions: [
614
- {
615
- type: 'playbook.acceptedOutcome',
616
- params: {
617
- source: 'runFirstPhase',
618
- target: 'awaitBossReply',
619
- acceptedOutcome: 'needsBossReply',
620
- },
621
- },
622
- 'rememberPendingQuestion',
623
- ],
624
- },
625
- {
626
- target: 'failed',
627
- actions: 'rememberMalformedPlayerOutput',
628
- },
643
+ suspendArm('firstPhase'),
644
+ acceptPhaseArm('firstPhase', 'directCommit', 'acceptDirectCommit', 'reviewNewIntentPhase'),
645
+ acceptPhaseArm('firstPhase', 'irCommit', 'acceptIrCommit', 'reviewNewIntentPhase'),
646
+ acceptPhaseArm('firstPhase', 'moreTasks', 'acceptMoreTasks', 'reviewIrTaskPhase'),
647
+ acceptPhaseArm('firstPhase', 'finalTask', 'acceptFinalTask', 'reviewIrTaskPhase'),
648
+ malformedPlayerOutputArm,
629
649
  ],
630
- onError: { target: 'failed', actions: 'rememberActorError' },
650
+ onError: playerOnError,
631
651
  },
632
652
  },
633
- reviewFirstCommit: {
634
- id: 'reviewFirstCommit',
635
- description: STATE_DESCRIPTIONS.reviewFirstCommit,
636
- meta: playbookMeta('reviewFirstCommit'),
637
- tags: ['playbook.suspended'],
653
+ reviewNewIntentPhase: {
654
+ id: 'reviewNewIntentPhase',
655
+ tags: 'playbook.suspended',
656
+ description: STATE_DESCRIPTIONS.reviewNewIntentPhase,
657
+ meta: meta('reviewNewIntentPhase'),
638
658
  invoke: {
639
659
  src: 'playbook',
640
660
  input: ({ context }) => ({
641
- stateId: 'reviewFirstCommit',
661
+ stateId: 'reviewNewIntentPhase',
642
662
  sourceItem: 'CODE-2',
643
- playbookId: 'review',
644
- text: newIntentReviewText(context),
663
+ playbookId: REVIEW_PLAYBOOK_ID,
664
+ text: composeReviewText(REVIEW_NEW_INTENT_PHASE_TEMPLATE, {
665
+ '<caller-input>': context.callerInput,
666
+ '<code-commit>': context.codeCommit,
667
+ '<coder-output>': context.coderOutput,
668
+ }),
645
669
  }),
646
670
  onDone: [
647
671
  {
648
- guard: 'reviewApprovedDirect',
672
+ guard: 'reviewPassedDirect',
649
673
  target: 'done',
650
- actions: 'completeSuccessfully',
674
+ actions: 'rememberReviewPass',
651
675
  },
652
676
  {
653
- guard: 'reviewApprovedIrCreated',
654
- target: 'runIrTask',
677
+ guard: 'reviewPassedNewIr',
678
+ target: 'irTaskPhase',
679
+ actions: 'rememberReviewPass',
655
680
  },
656
- {
657
- target: 'reportedReviewFailure',
658
- actions: 'completeWithInvalidReviewOutput',
659
- },
660
- ],
661
- onError: [
662
- {
663
- guard: 'authoredReviewFailure',
664
- target: 'reportedReviewFailure',
665
- actions: 'completeWithReviewFailure',
666
- },
667
- { target: 'failed', actions: 'rememberActorError' },
681
+ { target: 'reviewFailed', actions: 'rememberReviewNotPassed' },
668
682
  ],
683
+ onError: reviewOnError,
669
684
  },
670
685
  },
671
- runIrTask: {
672
- id: 'runIrTask',
673
- description: STATE_DESCRIPTIONS.runIrTask,
674
- meta: playbookMeta('runIrTask', 'coder'),
675
- tags: ['playbook.busy'],
686
+ irTaskPhase: {
687
+ id: 'irTaskPhase',
688
+ tags: 'playbook.busy',
689
+ description: STATE_DESCRIPTIONS.irTaskPhase,
690
+ meta: meta('irTaskPhase', 'coder'),
676
691
  invoke: {
677
692
  src: 'player',
678
693
  input: ({ context }) => ({
679
- stateId: 'runIrTask',
694
+ stateId: 'irTaskPhase',
680
695
  role: 'coder',
681
696
  sourceItem: 'CODE-3',
682
- prompt: IR_TASK_PROMPT,
683
- result: IR_TASK_RESULTS,
684
- callerInput: context.callerInput ?? '',
697
+ prompt: IR_TASK_PHASE_PROMPT,
698
+ result: IR_TASK_PHASE_RESULT,
699
+ ...(context.callerInput !== undefined
700
+ ? { callerInput: context.callerInput }
701
+ : {}),
702
+ ...(context.irNumber !== undefined
703
+ ? { irNumber: context.irNumber }
704
+ : {}),
685
705
  runResults: context.runResults,
686
- ...(context.irNumber === undefined
687
- ? {}
688
- : { irNumber: context.irNumber }),
689
- ...(context.pendingBossQuestion === undefined
690
- ? {}
691
- : { pendingBossQuestion: context.pendingBossQuestion }),
692
- ...(context.bossReply === undefined
693
- ? {}
694
- : { bossReply: context.bossReply }),
706
+ ...bossReplyFields(context),
695
707
  }),
696
708
  onDone: [
697
- {
698
- guard: 'isMoreTasks',
699
- target: 'reviewIrTask',
700
- actions: [
701
- {
702
- type: 'playbook.acceptedOutcome',
703
- params: {
704
- source: 'runIrTask',
705
- target: 'reviewIrTask',
706
- acceptedOutcome: 'moreTasks',
707
- },
708
- },
709
- 'rememberMoreTasks',
710
- ],
711
- },
712
- {
713
- guard: 'isFinalTask',
714
- target: 'reviewIrTask',
715
- actions: [
716
- {
717
- type: 'playbook.acceptedOutcome',
718
- params: {
719
- source: 'runIrTask',
720
- target: 'reviewIrTask',
721
- acceptedOutcome: 'finalTask',
722
- },
723
- },
724
- 'rememberFinalTask',
725
- ],
726
- },
727
- {
728
- guard: 'needsBossReply',
729
- target: 'awaitBossReply',
730
- actions: [
731
- {
732
- type: 'playbook.acceptedOutcome',
733
- params: {
734
- source: 'runIrTask',
735
- target: 'awaitBossReply',
736
- acceptedOutcome: 'needsBossReply',
737
- },
738
- },
739
- 'rememberPendingQuestion',
740
- ],
741
- },
742
- {
743
- target: 'failed',
744
- actions: 'rememberMalformedPlayerOutput',
745
- },
709
+ suspendArm('irTaskPhase'),
710
+ acceptPhaseArm('irTaskPhase', 'moreTasks', 'acceptMoreTasks', 'reviewIrTaskPhase'),
711
+ acceptPhaseArm('irTaskPhase', 'finalTask', 'acceptFinalTask', 'reviewIrTaskPhase'),
712
+ malformedPlayerOutputArm,
746
713
  ],
747
- onError: { target: 'failed', actions: 'rememberActorError' },
714
+ onError: playerOnError,
748
715
  },
749
716
  },
750
- reviewIrTask: {
751
- id: 'reviewIrTask',
752
- description: STATE_DESCRIPTIONS.reviewIrTask,
753
- meta: playbookMeta('reviewIrTask'),
754
- tags: ['playbook.suspended'],
717
+ reviewIrTaskPhase: {
718
+ id: 'reviewIrTaskPhase',
719
+ tags: 'playbook.suspended',
720
+ description: STATE_DESCRIPTIONS.reviewIrTaskPhase,
721
+ meta: meta('reviewIrTaskPhase'),
755
722
  invoke: {
756
723
  src: 'playbook',
757
724
  input: ({ context }) => ({
758
- stateId: 'reviewIrTask',
725
+ stateId: 'reviewIrTaskPhase',
759
726
  sourceItem: 'CODE-4',
760
- playbookId: 'review',
761
- text: irTaskReviewText(context),
727
+ playbookId: REVIEW_PLAYBOOK_ID,
728
+ text: composeReviewText(REVIEW_IR_TASK_PHASE_TEMPLATE, {
729
+ '<caller-input>': context.callerInput,
730
+ '<code-commit>': context.codeCommit,
731
+ '<coder-output>': context.coderOutput,
732
+ '<ir-task>': context.irTask,
733
+ }),
762
734
  }),
763
735
  onDone: [
764
736
  {
765
- guard: 'reviewApprovedMoreTasks',
766
- target: 'runIrTask',
737
+ guard: 'reviewPassedNonfinalTask',
738
+ target: 'irTaskPhase',
739
+ actions: 'rememberReviewPass',
767
740
  },
768
741
  {
769
- guard: 'reviewApprovedFinalTask',
742
+ guard: 'reviewPassedFinalTask',
770
743
  target: 'done',
771
- actions: 'completeSuccessfully',
744
+ actions: 'rememberReviewPass',
772
745
  },
773
- {
774
- target: 'reportedReviewFailure',
775
- actions: 'completeWithInvalidReviewOutput',
776
- },
777
- ],
778
- onError: [
779
- {
780
- guard: 'authoredReviewFailure',
781
- target: 'reportedReviewFailure',
782
- actions: 'completeWithReviewFailure',
783
- },
784
- { target: 'failed', actions: 'rememberActorError' },
746
+ { target: 'reviewFailed', actions: 'rememberReviewNotPassed' },
785
747
  ],
748
+ onError: reviewOnError,
786
749
  },
787
750
  },
788
751
  awaitBossReply: {
789
752
  id: 'awaitBossReply',
753
+ tags: 'playbook.parked',
790
754
  description: STATE_DESCRIPTIONS.awaitBossReply,
791
- meta: playbookMeta('awaitBossReply'),
792
- tags: ['playbook.parked'],
755
+ meta: meta('awaitBossReply'),
793
756
  on: {
794
757
  BOSS_REPLY: [
795
758
  {
796
759
  guard: 'emptyBossReply',
797
- target: '#failed',
798
- actions: 'rememberEmptyBossReplyError',
799
- },
800
- {
801
- guard: 'resumesFirstPhase',
802
- target: '#runFirstPhase',
803
- reenter: true,
804
- actions: 'rememberBossReply',
805
- },
806
- {
807
- guard: 'resumesIrTask',
808
- target: '#runIrTask',
809
- reenter: true,
810
- actions: 'rememberBossReply',
760
+ target: 'failed',
761
+ actions: ['rememberMalformedBossReply', 'clearBossReplyContext'],
811
762
  },
763
+ ...resumableStates(['firstPhase', 'irTaskPhase']),
812
764
  ],
813
- START_CODE: { target: 'runFirstPhase', actions: 'startCoding' },
765
+ START_CODE: {
766
+ guard: 'validStartCode',
767
+ target: 'firstPhase',
768
+ actions: 'startCode',
769
+ },
814
770
  },
815
771
  },
816
772
  failed: {
817
773
  id: 'failed',
774
+ tags: 'playbook.parked',
818
775
  description: STATE_DESCRIPTIONS.failed,
819
- meta: playbookMeta('failed'),
820
- tags: ['playbook.parked'],
776
+ meta: meta('failed'),
821
777
  on: {
822
- START_CODE: { target: 'runFirstPhase', actions: 'startCoding' },
778
+ START_CODE: {
779
+ guard: 'validStartCode',
780
+ target: 'firstPhase',
781
+ actions: 'startCode',
782
+ },
823
783
  },
824
784
  },
825
- reportedReviewFailure: {
826
- id: 'reportedReviewFailure',
827
- description: STATE_DESCRIPTIONS.reportedReviewFailure,
828
- meta: terminalMeta('reportedReviewFailure', 'failure'),
785
+ reviewFailed: {
786
+ id: 'reviewFailed',
829
787
  type: 'final',
788
+ description: STATE_DESCRIPTIONS.reviewFailed,
789
+ meta: finalMeta('reviewFailed', 'failure'),
830
790
  },
831
791
  done: {
832
792
  id: 'done',
833
- description: STATE_DESCRIPTIONS.done,
834
- meta: terminalMeta('done', 'success'),
835
793
  type: 'final',
794
+ description: STATE_DESCRIPTIONS.done,
795
+ meta: finalMeta('done', 'success'),
836
796
  },
837
797
  },
838
798
  });
839
- export default codingMachine;
799
+ export default codeMachine;