@sublang/playbook 15.1.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 (76) hide show
  1. package/docs/embedding.md +2 -2
  2. package/package.json +1 -1
  3. package/reference/sdlc/branch.playbook/branch.fsm.d.ts +63 -33
  4. package/reference/sdlc/branch.playbook/branch.fsm.js +190 -108
  5. package/reference/sdlc/branch.playbook/branch.fsm.ts +269 -147
  6. package/reference/sdlc/branch.playbook/branch.gears.md +7 -17
  7. package/reference/sdlc/branch.playbook/branch.playbook.d.ts +12 -16
  8. package/reference/sdlc/branch.playbook/branch.playbook.js +112 -101
  9. package/reference/sdlc/branch.playbook/branch.playbook.ts +128 -171
  10. package/reference/sdlc/branch.playbook/branch.registry.d.ts +3 -2
  11. package/reference/sdlc/branch.playbook/branch.registry.ts +6 -2
  12. package/reference/sdlc/code.playbook/code.fsm.d.ts +143 -90
  13. package/reference/sdlc/code.playbook/code.fsm.introspect.d.ts +8 -8
  14. package/reference/sdlc/code.playbook/code.fsm.introspect.ts +13 -13
  15. package/reference/sdlc/code.playbook/code.fsm.js +609 -649
  16. package/reference/sdlc/code.playbook/code.fsm.ts +890 -821
  17. package/reference/sdlc/code.playbook/code.gears.md +39 -29
  18. package/reference/sdlc/code.playbook/code.playbook.d.ts +13 -16
  19. package/reference/sdlc/code.playbook/code.playbook.js +166 -147
  20. package/reference/sdlc/code.playbook/code.playbook.ts +180 -229
  21. package/reference/sdlc/code.playbook/code.registry.d.ts +3 -2
  22. package/reference/sdlc/code.playbook/code.registry.ts +6 -2
  23. package/reference/sdlc/code.playbook/playbook-captain.d.ts +42 -0
  24. package/reference/sdlc/code.playbook/playbook-captain.js +829 -194
  25. package/reference/sdlc/code.playbook/playbook-captain.ts +1227 -220
  26. package/reference/sdlc/decide.md +1 -0
  27. package/reference/sdlc/decide.playbook/decide.fsm.d.ts +270 -154
  28. package/reference/sdlc/decide.playbook/decide.fsm.js +765 -732
  29. package/reference/sdlc/decide.playbook/decide.fsm.ts +1167 -945
  30. package/reference/sdlc/decide.playbook/decide.gears.md +28 -21
  31. package/reference/sdlc/decide.playbook/decide.playbook.d.ts +11 -59
  32. package/reference/sdlc/decide.playbook/decide.playbook.js +155 -3696
  33. package/reference/sdlc/decide.playbook/decide.playbook.ts +186 -5244
  34. package/reference/sdlc/decide.playbook/decide.registry.d.ts +8 -4
  35. package/reference/sdlc/decide.playbook/decide.registry.js +2 -2
  36. package/reference/sdlc/decide.playbook/decide.registry.ts +13 -6
  37. package/reference/sdlc/dev.playbook/dev.fsm.d.ts +113 -131
  38. package/reference/sdlc/dev.playbook/dev.fsm.js +517 -575
  39. package/reference/sdlc/dev.playbook/dev.fsm.ts +667 -787
  40. package/reference/sdlc/dev.playbook/dev.gears.md +52 -54
  41. package/reference/sdlc/dev.playbook/dev.playbook.d.ts +13 -10
  42. package/reference/sdlc/dev.playbook/dev.playbook.js +128 -120
  43. package/reference/sdlc/dev.playbook/dev.playbook.ts +143 -193
  44. package/reference/sdlc/dev.playbook/dev.registry.d.ts +3 -2
  45. package/reference/sdlc/dev.playbook/dev.registry.ts +6 -2
  46. package/reference/sdlc/pr.playbook/pr.fsm.d.ts +92 -85
  47. package/reference/sdlc/pr.playbook/pr.fsm.js +461 -518
  48. package/reference/sdlc/pr.playbook/pr.fsm.ts +533 -663
  49. package/reference/sdlc/pr.playbook/pr.gears.md +47 -63
  50. package/reference/sdlc/pr.playbook/pr.playbook.d.ts +13 -23
  51. package/reference/sdlc/pr.playbook/pr.playbook.js +118 -112
  52. package/reference/sdlc/pr.playbook/pr.playbook.ts +136 -190
  53. package/reference/sdlc/pr.playbook/pr.registry.d.ts +3 -2
  54. package/reference/sdlc/pr.playbook/pr.registry.ts +4 -2
  55. package/reference/sdlc/review.md +3 -3
  56. package/reference/sdlc/review.playbook/review.fsm.d.ts +150 -116
  57. package/reference/sdlc/review.playbook/review.fsm.js +479 -550
  58. package/reference/sdlc/review.playbook/review.fsm.ts +717 -662
  59. package/reference/sdlc/review.playbook/review.gears.md +32 -29
  60. package/reference/sdlc/review.playbook/review.playbook.d.ts +12 -12
  61. package/reference/sdlc/review.playbook/review.playbook.js +180 -155
  62. package/reference/sdlc/review.playbook/review.playbook.ts +190 -230
  63. package/reference/sdlc/review.playbook/review.registry.d.ts +6 -5
  64. package/reference/sdlc/review.playbook/review.registry.js +3 -3
  65. package/reference/sdlc/review.playbook/review.registry.ts +9 -5
  66. package/slc/gears2fsm.md +72 -3
  67. package/slc/link.md +50 -27
  68. package/slc/materialize-link.mjs +51 -13
  69. package/slc/optimize.md +4 -1
  70. package/slc/prefix-prompts.mjs +29 -28
  71. package/slc/prefix.md +27 -24
  72. package/slc/slc.pin-inputs.json +11 -2
  73. package/slc/text2gears.md +71 -11
  74. package/src/xstate-playbook-runtime.d.ts +54 -15
  75. package/src/xstate-playbook-runtime.js +1154 -248
  76. package/src/xstate-playbook-runtime.ts +1603 -317
@@ -1,13 +1,15 @@
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 ./dev.gears.md.
4
+ // FSM object artifact compiled from ./dev.gears.md by gears2fsm.
5
+ // It defines only the machine, actor contracts, and typed inputs; the linked
6
+ // runtime supplies the `player` and `playbook` actor implementations.
5
7
  import { assign, fromPromise, setup } from 'xstate';
8
+ import { validatePlaybookCallResult } from '@sublang/playbook/xstate-runtime';
9
+ /** DEV declares no parallel group. */
10
+ export const concurrentRoleSets = [];
11
+ // DEV-1: the item's blockquote, verbatim, with the outer GEARS marker removed.
6
12
  const PLAN_ANALYSIS_PROMPT = [
7
- '> Original request: <development-request>',
8
- '> Prior discussion: <discussion-context>',
9
- '> Run results: <run-results>',
10
- '',
11
13
  'Plan which playbooks run for this request; the playbooks do the work.',
12
14
  'Read the request, then the specs and the repository only as far as choosing the path requires.',
13
15
  'Do not change files or commit while planning or discussing the request.',
@@ -26,50 +28,79 @@ const PLAN_ANALYSIS_PROMPT = [
26
28
  'A question or exploratory discussion is not by itself authorization to create a durable decision or implement changes.',
27
29
  'Do not choose `decide then code` merely because the work is large, nor `code` merely because it is small.',
28
30
  'Consult @specs/map.md for relevant context and @specs/meta.md for spec requirements, if needed.',
31
+ '',
32
+ '> Original request: <development-request>',
33
+ '> Prior discussion: <discussion-context>',
34
+ '> Run results: <run-results>',
29
35
  ].join('\n');
30
- const PLAN_ANALYSIS_RESULTS = {
31
- discussionComplete: 'Analyst concluded the discussion after a Boss reply, with no repository work to follow.',
32
- code: 'Analyst chose `code`: the existing decisions and spec items settle how the work is done. Output shall include `planningResult: <verbatim final text>`.',
33
- decideThenCode: 'Analyst chose `decide then code`: the work turns on a point the specs leave open or contradict. Output shall include `planningResult: <verbatim final text>`.',
34
- codeViaPullRequest: 'Analyst chose `code via pull request`: the existing decisions settle the work, and the request names a GitHub issue or explicitly asks for pull-request delivery. Output shall include `planningResult: <verbatim final text>`.',
35
- decideThenCodeViaPullRequest: 'Analyst chose `decide then code via pull request`: the work turns on a point the specs leave open or contradict, and the request names a GitHub issue or explicitly asks for pull-request delivery. Output shall include `planningResult: <verbatim final text>`.',
36
- needsBossReply: "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>`.",
36
+ 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>`.";
37
+ // DEV-1 local result contract: the authored `Results:` bullets in declared
38
+ // order, plus the universal Boss-reply suspension.
39
+ const PLAN_ANALYSIS_RESULT = {
40
+ discussionComplete: 'After a Boss reply that settles that no repository work should follow, Analyst concluded the discussion; dev completes without a child call or repository change.',
41
+ code: 'Analyst chose the code path because the existing decisions and spec items settle how the work is done. Output shall include `planningResult: <verbatim final text>`.',
42
+ decideThenCode: 'Analyst chose the decide then code path because the work turns on a rule, concept, term, shape, or trade-off the specs leave open or contradict. Output shall include `planningResult: <verbatim final text>`.',
43
+ codeViaPullRequest: 'Analyst chose the code via pull request path because the existing decisions and spec items settle how the work is done and the request names a GitHub issue or explicitly asks for pull-request delivery. Output shall include `planningResult: <verbatim final text>`.',
44
+ decideThenCodeViaPullRequest: 'Analyst chose the decide then code via pull request path because the work turns on a rule, concept, term, shape, or trade-off the specs leave open or contradict and the request names a GitHub issue or explicitly asks for pull-request delivery. Output shall include `planningResult: <verbatim final text>`.',
45
+ needsBossReply: NEEDS_BOSS_REPLY_DESCRIPTION,
37
46
  };
47
+ // DEV-2 and DEV-3 and DEV-5: identical composed blockquotes.
48
+ const PLANNED_CALL_TEMPLATE = [
49
+ '> Original request: <development-request>',
50
+ '> Prior discussion: <discussion-context>',
51
+ '> Planning result: <planning-result>',
52
+ ];
53
+ // DEV-4.
54
+ const CODE_AFTER_DECIDE_CALL_TEMPLATE = [
55
+ '> Original request: <development-request>',
56
+ '> Prior discussion: <discussion-context>',
57
+ '> Planning result: <planning-result>',
58
+ '> DECIDE commit: <decide-commit>',
59
+ '> Evaluated revision: <evaluated-revision>',
60
+ ];
61
+ // DEV-6.
62
+ const PULL_REQUEST_CALL_TEMPLATE = [
63
+ '> Original request: <development-request>',
64
+ '> Issue summary: <issue-summary>',
65
+ '> Branch: <branch>',
66
+ '> Base revision: <base-revision>',
67
+ '> CODE commit: <last-code-commit>',
68
+ '> Evaluated revision: <final-evaluated-revision>',
69
+ ];
38
70
  const STATE_DESCRIPTIONS = {
39
71
  ready: 'Waiting for a development request.',
40
- planAnalysis: 'Analyst is choosing the development path.',
41
- createBranch: 'The BRANCH playbook is preparing the pull-request branch for the planned development path.',
42
- callCode: 'The CODE playbook is implementing the planned development path.',
43
- callDecide: 'The DECIDE playbook is settling the durable decision the planned path requires.',
44
- callCodeAfterDecide: 'The CODE playbook is implementing the decided development path.',
45
- openPullRequest: 'The PR playbook is delivering the implemented branch through a pull request.',
46
- awaitBossReply: 'Waiting for Boss to answer Analyst.',
47
- failed: 'The development planning workflow failed and is waiting for a new development request.',
48
- discussionComplete: 'The planning discussion concluded after a Boss reply with no repository work to follow.',
49
- done: "The selected development path completed with the final child playbook's successful result.",
50
- reportedChildFailure: "The development workflow relayed a child playbook's authored abort, failure, or insufficient terminal result.",
72
+ planAnalysis: 'Analyst chooses the development path in a short planning note.',
73
+ awaitBossReply: "Waiting for Boss to answer the acting agent's question.",
74
+ createBranch: 'The BRANCH playbook prepares the branch for the pull-request delivery path.',
75
+ callCode: 'The CODE playbook implements the planned development path.',
76
+ callDecide: 'The DECIDE playbook settles the decision the planned development path requires.',
77
+ callCodeAfterDecide: 'The CODE playbook implements the development path DECIDE settled.',
78
+ openPullRequest: 'The PR playbook delivers the implemented branch through a pull request.',
79
+ failed: 'DEV failed outside a child result contract and waits for a new development request.',
80
+ discussionComplete: 'After a Boss reply, the planning discussion concluded with no child call or repository change.',
81
+ done: "The selected development path completed with its final child playbook's successful result.",
82
+ reportedChildFailure: "DEV ended by relaying a child playbook's authored abort, failure, or result that did not prove the required success.",
51
83
  };
52
- function playbookMeta(stateId, role) {
84
+ function playbookMeta(stateId) {
53
85
  return {
54
- playbook: {
55
- stateId,
56
- description: STATE_DESCRIPTIONS[stateId],
57
- ...(role === undefined ? {} : { role }),
58
- },
86
+ playbook: { stateId, description: STATE_DESCRIPTIONS[stateId] },
87
+ };
88
+ }
89
+ function roleMeta(stateId, role) {
90
+ return {
91
+ playbook: { stateId, description: STATE_DESCRIPTIONS[stateId], role },
59
92
  };
60
93
  }
61
- // DR-048: a final state additionally declares whether its outcome means the
62
- // workflow succeeded or failed, so DEV's own caller learns that from the
63
- // machine rather than from DEV's output fields.
64
94
  function terminalMeta(stateId, terminal) {
65
95
  return {
66
- playbook: {
67
- stateId,
68
- description: STATE_DESCRIPTIONS[stateId],
69
- terminal,
70
- },
96
+ playbook: { stateId, description: STATE_DESCRIPTIONS[stateId], terminal },
71
97
  };
72
98
  }
99
+ const PLAN_ANALYSIS_QUESTION = {
100
+ resumeStateId: 'planAnalysis',
101
+ sourceItem: 'DEV-1',
102
+ asker: { kind: 'role', roleId: 'analyst' },
103
+ };
73
104
  function isRecord(value) {
74
105
  if (value === null || typeof value !== 'object' || Array.isArray(value)) {
75
106
  return false;
@@ -80,364 +111,247 @@ function isRecord(value) {
80
111
  function isNonEmptyString(value) {
81
112
  return typeof value === 'string' && value.trim().length > 0;
82
113
  }
83
- function playerOutput(event) {
114
+ // ---------------------------------------------------------------------------
115
+ // Player output (DEV-1), narrowed structurally from an unknown done event.
116
+ function playerOutputOf(event) {
84
117
  if (!isRecord(event))
85
118
  return undefined;
86
119
  return isRecord(event.output) ? event.output : undefined;
87
120
  }
88
- function outputGuard(event, guard) {
89
- return playerOutput(event)?.guard === guard;
121
+ function hasGuard(event, guard) {
122
+ return playerOutputOf(event)?.guard === guard;
90
123
  }
91
- function outputString(event, field) {
92
- const value = playerOutput(event)?.[field];
124
+ function outputText(event, field) {
125
+ const value = playerOutputOf(event)?.[field];
93
126
  return isNonEmptyString(value) ? value : undefined;
94
127
  }
95
- function isCodePath({ event }) {
96
- return (outputGuard(event, 'code') &&
97
- outputString(event, 'planningResult') !== undefined);
98
- }
99
- function isDecideThenCode({ event }) {
100
- return (outputGuard(event, 'decideThenCode') &&
101
- outputString(event, 'planningResult') !== undefined);
128
+ function plannedPath(guard) {
129
+ return ({ event }) => hasGuard(event, guard) && outputText(event, 'planningResult') !== undefined;
102
130
  }
103
- // DR-050: the two pull-request outcomes select the same development paths
104
- // as `code` and `decide then code`, entered through `branch` first.
105
- function isCodeViaPullRequest({ event }) {
106
- return (outputGuard(event, 'codeViaPullRequest') &&
107
- outputString(event, 'planningResult') !== undefined);
108
- }
109
- function isDecideThenCodeViaPullRequest({ event, }) {
110
- return (outputGuard(event, 'decideThenCodeViaPullRequest') &&
111
- outputString(event, 'planningResult') !== undefined);
112
- }
113
- // Discussion complete is available only after a Boss reply: the round that
114
- // produced it must itself have been resumed with Boss's answer.
115
- function isDiscussionComplete({ context, event, }) {
116
- return (outputGuard(event, 'discussionComplete') &&
131
+ // Discussion complete is available only after a Boss reply: the Analyst
132
+ // invocation that returns it must have been resumed with Boss's answer.
133
+ function discussionCompleteAfterBossReply({ context, event, }) {
134
+ return (hasGuard(event, 'discussionComplete') &&
135
+ context.pendingBossQuestion !== undefined &&
117
136
  isNonEmptyString(context.bossReply));
118
137
  }
119
- function needsBossReply({ event }) {
120
- return (outputGuard(event, 'needsBossReply') &&
121
- outputString(event, 'question') !== undefined);
138
+ function needsBossReplyWithQuestion({ event }) {
139
+ return (hasGuard(event, 'needsBossReply') && outputText(event, 'question') !== undefined);
140
+ }
141
+ function needsBossReplyWithoutQuestion({ event }) {
142
+ return (hasGuard(event, 'needsBossReply') && outputText(event, 'question') === undefined);
122
143
  }
144
+ // ---------------------------------------------------------------------------
145
+ // Child outputs, read only from each child's canonical structured result and
146
+ // checked against the packaged public output interfaces.
123
147
  function childOutputOf(event) {
124
148
  return isRecord(event) ? event.output : undefined;
125
149
  }
126
- function isDecideSuccessValue(value) {
127
- return (isRecord(value) &&
128
- isNonEmptyString(value.decideCommit) &&
129
- isNonEmptyString(value.evaluatedRevision) &&
130
- value.noUnsettledFindings === true);
131
- }
132
- function isDecideSuccess({ event }) {
133
- return isDecideSuccessValue(childOutputOf(event));
134
- }
135
- // BRANCH's canonical successful terminal output carries the exact name of
136
- // the new checked-out branch, the exact receipt-owned base revision, and the
137
- // issue summary; DEV consumes exactly those three by name and never defaults
138
- // a missing one.
139
- function isBranchSuccessValue(value) {
140
- return (isRecord(value) &&
141
- isNonEmptyString(value.branch) &&
142
- isNonEmptyString(value.baseRevision) &&
143
- isNonEmptyString(value.issueSummary));
144
- }
145
- function isBranchSuccessForCode({ context, event, }) {
146
- return (context.pullRequestPath === 'code' &&
147
- isBranchSuccessValue(childOutputOf(event)));
148
- }
149
- function isBranchSuccessForDecide({ context, event, }) {
150
- return (context.pullRequestPath === 'decide-then-code' &&
151
- isBranchSuccessValue(childOutputOf(event)));
152
- }
153
- // CODE's canonical successful terminal output discriminates completion with
154
- // `status: 'complete'`, carries the exact last CODE-owned commit, and
155
- // affirms that every phase's review passed.
156
- function isCodeSuccessValue(value) {
157
- return (isRecord(value) &&
158
- value.status === 'complete' &&
159
- isNonEmptyString(value.lastCodeCommit) &&
160
- value.allReviewsPassed === true);
161
- }
162
- // A plain path completes DEV on that proof alone, exactly as before DR-050.
163
- function isPlainCodeSuccess({ context, event, }) {
164
- return (context.deliveryViaPullRequest !== true &&
165
- isCodeSuccessValue(childOutputOf(event)));
166
- }
167
- // A pull-request path additionally consumes the exact final evaluated
168
- // repository revision for the `pr` call, so a CODE result lacking it does not
169
- // prove the success that path requires.
170
- function isCodeSuccessValueForPullRequest(value) {
171
- return (isCodeSuccessValue(value) &&
172
- isRecord(value) &&
173
- isNonEmptyString(value.finalEvaluatedRevision));
174
- }
175
- function isCodeSuccessViaPullRequest({ context, event, }) {
176
- return (context.deliveryViaPullRequest === true &&
177
- isCodeSuccessValueForPullRequest(childOutputOf(event)));
178
- }
179
- function isStateValue(value, ancestors = new Set()) {
180
- if (typeof value === 'string')
181
- return true;
182
- if (!isRecord(value) || ancestors.has(value))
183
- return false;
184
- const next = new Set(ancestors).add(value);
185
- return Reflect.ownKeys(value).every((key) => typeof key === 'string' &&
186
- Object.prototype.propertyIsEnumerable.call(value, key) &&
187
- isStateValue(value[key], next));
188
- }
189
- function isStringArray(value) {
190
- return (Array.isArray(value) &&
191
- Reflect.ownKeys(value).length === value.length + 1 &&
192
- Reflect.ownKeys(value).every((key) => {
193
- if (key === 'length')
194
- return true;
195
- return (typeof key === 'string' &&
196
- /^(0|[1-9][0-9]*)$/.test(key) &&
197
- Number(key) < value.length);
198
- }) &&
199
- value.every((entry) => isNonEmptyString(entry)) &&
200
- new Set(value).size === value.length);
201
- }
202
- function isPlaybookState(value) {
203
- if (!isRecord(value))
204
- return false;
205
- const allowed = new Set([
206
- 'value',
207
- 'activeStateIds',
208
- 'tags',
209
- 'status',
210
- 'quiescent',
211
- 'stateId',
212
- ]);
213
- if (Reflect.ownKeys(value).some((key) => typeof key !== 'string' || !allowed.has(key)) ||
214
- !Object.prototype.hasOwnProperty.call(value, 'value') ||
215
- !Object.prototype.hasOwnProperty.call(value, 'activeStateIds') ||
216
- !Object.prototype.hasOwnProperty.call(value, 'tags') ||
217
- !Object.prototype.hasOwnProperty.call(value, 'status') ||
218
- !Object.prototype.hasOwnProperty.call(value, 'quiescent')) {
219
- return false;
220
- }
221
- return (isStateValue(value.value) &&
222
- isStringArray(value.activeStateIds) &&
223
- isStringArray(value.tags) &&
224
- (value.status === 'active' ||
225
- value.status === 'done' ||
226
- value.status === 'error' ||
227
- value.status === 'stopped') &&
228
- typeof value.quiescent === 'boolean' &&
229
- (!Object.prototype.hasOwnProperty.call(value, 'stateId') ||
230
- (isNonEmptyString(value.stateId) &&
231
- value.activeStateIds.length === 1 &&
232
- value.activeStateIds[0] === value.stateId)));
233
- }
234
- function nestedResultFromError(error) {
235
- if (!(error instanceof Error))
236
- return undefined;
237
- const result = error.result;
238
- return isRecord(result) ? result : undefined;
150
+ function childErrorOf(event) {
151
+ return isRecord(event) ? event.error : undefined;
239
152
  }
240
- function isFailureTerminalRecord(value) {
241
- if (!isRecord(value))
242
- return false;
243
- const allowed = new Set(['stateId', 'kind', 'description']);
244
- return (Reflect.ownKeys(value).every((key) => typeof key === 'string' && allowed.has(key)) &&
245
- isNonEmptyString(value.stateId) &&
246
- value.kind === 'failure' &&
247
- (!Object.prototype.hasOwnProperty.call(value, 'description') ||
248
- typeof value.description === 'string'));
249
- }
250
- function normalizedChildFailure(error, playbookId) {
251
- const result = nestedResultFromError(error);
252
- if (result === undefined)
253
- return undefined;
254
- const allowed = new Set([
255
- 'status',
256
- 'playbookId',
257
- 'childSessionId',
258
- 'state',
259
- ...(result.status === 'ok' ? ['output', 'terminal'] : ['error']),
260
- ]);
261
- if (Reflect.ownKeys(result).some((key) => typeof key !== 'string' || !allowed.has(key)) ||
262
- (result.status !== 'aborted' &&
263
- result.status !== 'error' &&
264
- result.status !== 'ok') ||
265
- result.playbookId !== playbookId) {
153
+ function branchSuccessOf(value) {
154
+ if (!isRecord(value) ||
155
+ value.status !== 'branched' ||
156
+ !isNonEmptyString(value.branch) ||
157
+ !isNonEmptyString(value.baseRevision) ||
158
+ !isNonEmptyString(value.issueSummary)) {
266
159
  return undefined;
267
160
  }
268
- if (Object.prototype.hasOwnProperty.call(result, 'childSessionId') &&
269
- !isNonEmptyString(result.childSessionId)) {
270
- return undefined;
271
- }
272
- if (result.status === 'ok') {
273
- if (!Object.prototype.hasOwnProperty.call(result, 'terminal') ||
274
- !isFailureTerminalRecord(result.terminal) ||
275
- !isNonEmptyString(result.childSessionId) ||
276
- (Object.prototype.hasOwnProperty.call(result, 'state') &&
277
- !isPlaybookState(result.state))) {
278
- return undefined;
279
- }
280
- return {
281
- status: 'ok',
282
- ...(result.output === undefined
283
- ? {}
284
- : { output: result.output }),
285
- };
286
- }
287
- if (Object.prototype.hasOwnProperty.call(result, 'state') &&
288
- !isPlaybookState(result.state)) {
161
+ return {
162
+ branch: value.branch,
163
+ baseRevision: value.baseRevision,
164
+ issueSummary: value.issueSummary,
165
+ };
166
+ }
167
+ function decideSuccessOf(value) {
168
+ if (!isRecord(value) ||
169
+ value.noUnsettledFindings !== true ||
170
+ !isNonEmptyString(value.decideCommit) ||
171
+ !isNonEmptyString(value.evaluatedRevision)) {
289
172
  return undefined;
290
173
  }
291
- let normalizedError;
292
- if (Object.prototype.hasOwnProperty.call(result, 'error')) {
293
- if (!isRecord(result.error))
294
- return undefined;
295
- const errorKeys = Reflect.ownKeys(result.error);
296
- if (errorKeys.some((key) => typeof key !== 'string' ||
297
- (key !== 'name' && key !== 'message' && key !== 'stack')) ||
298
- !Object.prototype.hasOwnProperty.call(result.error, 'name') ||
299
- !Object.prototype.hasOwnProperty.call(result.error, 'message') ||
300
- !isNonEmptyString(result.error.name) ||
301
- typeof result.error.message !== 'string' ||
302
- (Object.prototype.hasOwnProperty.call(result.error, 'stack') &&
303
- typeof result.error.stack !== 'string')) {
304
- return undefined;
305
- }
306
- normalizedError = {
307
- name: result.error.name,
308
- message: result.error.message,
309
- };
310
- }
311
- else if (result.status === 'error') {
174
+ return {
175
+ decideCommit: value.decideCommit,
176
+ evaluatedRevision: value.evaluatedRevision,
177
+ };
178
+ }
179
+ function codeSuccessOf(value) {
180
+ if (!isRecord(value) ||
181
+ value.status !== 'complete' ||
182
+ value.allReviewsPassed !== true ||
183
+ !isNonEmptyString(value.lastCodeCommit) ||
184
+ !isNonEmptyString(value.finalEvaluatedRevision)) {
312
185
  return undefined;
313
186
  }
314
187
  return {
315
- status: result.status,
316
- ...(normalizedError === undefined ? {} : { error: normalizedError }),
188
+ lastCodeCommit: value.lastCodeCommit,
189
+ finalEvaluatedRevision: value.finalEvaluatedRevision,
317
190
  };
318
191
  }
319
- function authoredChildFailureGuard(playbookId) {
320
- return ({ event }) => {
321
- const error = isRecord(event) ? event.error : undefined;
322
- return normalizedChildFailure(error, playbookId) !== undefined;
323
- };
192
+ function branchSucceededFor(path) {
193
+ return ({ context, event }) => context.pullRequestPath === path &&
194
+ branchSuccessOf(childOutputOf(event)) !== undefined;
324
195
  }
325
- function authoredChildError(event, playbookId) {
326
- const outer = isRecord(event) ? event.error : undefined;
327
- const failure = normalizedChildFailure(outer, playbookId);
328
- if (failure !== undefined && failure.status !== 'ok' && failure.error) {
329
- return failure.error;
330
- }
331
- if (failure?.status === 'aborted') {
332
- return {
333
- name: 'AbortError',
334
- message: `${playbookId.toUpperCase()} was aborted.`,
335
- };
196
+ function decideSucceeded({ event }) {
197
+ return decideSuccessOf(childOutputOf(event)) !== undefined;
198
+ }
199
+ function codeSucceededOnPullRequestPath({ context, event, }) {
200
+ return (context.pullRequestPath !== undefined &&
201
+ codeSuccessOf(childOutputOf(event)) !== undefined);
202
+ }
203
+ function codeSucceededOnPlainPath({ context, event, }) {
204
+ return (context.pullRequestPath === undefined &&
205
+ codeSuccessOf(childOutputOf(event)) !== undefined);
206
+ }
207
+ // ---------------------------------------------------------------------------
208
+ // Authored rejected child results.
209
+ export function authoredChildResult(error, expectedPlaybookId) {
210
+ if (!(error instanceof Error))
211
+ return undefined;
212
+ try {
213
+ const result = validatePlaybookCallResult(error.result, expectedPlaybookId);
214
+ return result.status !== 'ok' || result.terminal?.kind === 'failure'
215
+ ? result
216
+ : undefined;
336
217
  }
337
- if (outer instanceof Error) {
338
- return { name: outer.name || 'Error', message: outer.message };
218
+ catch {
219
+ return undefined;
339
220
  }
340
- return { name: 'Error', message: String(outer) };
341
- }
342
- function relayedChildFailure(event, playbookId) {
343
- const outer = isRecord(event) ? event.error : undefined;
344
- const failure = normalizedChildFailure(outer, playbookId);
345
- // DR-048: a completed child that reached an authored failure terminal
346
- // relays its own output, exactly as a terminal result that did not prove
347
- // success did before it was routed through the error path.
348
- if (failure?.status === 'ok') {
221
+ }
222
+ function authoredResultFrom(playbookId) {
223
+ return ({ event }) => authoredChildResult(childErrorOf(event), playbookId) !== undefined;
224
+ }
225
+ function relayedChildResult(event, playbookId) {
226
+ const result = authoredChildResult(childErrorOf(event), playbookId);
227
+ if (result === undefined)
228
+ return undefined;
229
+ if (result.status === 'ok') {
230
+ // A child that completed at its own authored failure terminal relays its
231
+ // own output; an absent output is omitted rather than stored.
349
232
  return {
350
233
  playbookId,
351
234
  status: 'ok',
352
- ...(failure.output === undefined ? {} : { output: failure.output }),
235
+ ...(result.output === undefined ? {} : { output: result.output }),
353
236
  };
354
237
  }
355
- return {
356
- playbookId,
357
- status: failure?.status === 'aborted' ? 'aborted' : 'error',
358
- error: authoredChildError(event, playbookId),
359
- };
238
+ const error = result.error === undefined
239
+ ? {
240
+ name: 'AbortError',
241
+ message: `The ${playbookId} playbook call was aborted.`,
242
+ }
243
+ : { name: result.error.name, message: result.error.message };
244
+ return { playbookId, status: result.status, error };
360
245
  }
361
246
  function insufficientChildResult(event, playbookId) {
362
247
  const output = childOutputOf(event);
363
248
  return {
364
249
  playbookId,
365
250
  status: 'ok',
366
- ...(output === undefined ? {} : { output: output }),
251
+ ...(output === undefined ? {} : { output }),
367
252
  };
368
253
  }
369
- /** Consumed planning Q&A rendered for later relayed discussion context. */
370
- export function renderDiscussionContext(exchanges) {
371
- return exchanges
372
- .map((exchange) => `Analyst question: ${exchange.question}\nBoss reply: ${exchange.answer}`)
373
- .join('\n\n');
254
+ function normalizedControlError(error) {
255
+ if (error instanceof Error) {
256
+ return {
257
+ name: error.name.length > 0 ? error.name : 'Error',
258
+ message: error.message,
259
+ ...(typeof error.stack === 'string' ? { stack: error.stack } : {}),
260
+ };
261
+ }
262
+ return {
263
+ name: 'Error',
264
+ message: typeof error === 'string' ? error : 'Actor failed with a non-Error value.',
265
+ };
266
+ }
267
+ // ---------------------------------------------------------------------------
268
+ // Discussion history and quoted-relay composition.
269
+ /** One consumed Analyst question and Boss reply, as relayed discussion. */
270
+ export function renderDiscussionExchange(question, answer) {
271
+ return `Analyst question: ${question}\nBoss reply: ${answer}`;
374
272
  }
375
- function archivedExchanges(context) {
273
+ // Persists the answered pending Q&A pair into the typed discussion context
274
+ // before the pending fields are replaced or cleared, so later calls relay it.
275
+ function archivedDiscussion(context) {
376
276
  if (context.pendingBossQuestion === undefined ||
377
277
  !isNonEmptyString(context.bossReply)) {
378
- return context.discussionExchanges;
278
+ return context.discussionContext;
379
279
  }
380
- return [
381
- ...context.discussionExchanges,
382
- {
383
- question: context.pendingBossQuestion.question,
384
- answer: context.bossReply,
385
- },
386
- ];
387
- }
388
- function quotedRelay(values) {
389
- return values
390
- .filter(([, value]) => value.length > 0)
391
- .map(([label, value]) => `${label}: ${value}`
392
- .split('\n')
393
- .map((line) => `> ${line}`)
394
- .join('\n'))
395
- .join('\n');
396
- }
397
- function planningRelayValues(context) {
398
- return [
399
- ['Original request', context.developmentRequest ?? ''],
400
- ['Prior discussion', renderDiscussionContext(context.discussionExchanges)],
401
- ['Planning result', context.planningResult ?? ''],
402
- ];
403
- }
404
- function codeCallText(context) {
405
- return quotedRelay(planningRelayValues(context));
406
- }
407
- function decideCallText(context) {
408
- return quotedRelay(planningRelayValues(context));
280
+ const exchange = renderDiscussionExchange(context.pendingBossQuestion.question, context.bossReply);
281
+ return context.discussionContext.length > 0
282
+ ? `${context.discussionContext}\n\n${exchange}`
283
+ : exchange;
284
+ }
285
+ const RELAY_TOKEN = /<[a-z][a-z0-9-]*>/g;
286
+ // Quoted-relay composition: an empty value contributes no line, every
287
+ // continuation line of a multi-line value is quoted with `> ` (keeping LF/CRLF
288
+ // separators), and no quoted line is inserted. Substitution is single-pass and
289
+ // literal; a missing value keeps its placeholder literal.
290
+ function composeRelay(template, values) {
291
+ const lines = [];
292
+ for (const line of template) {
293
+ const tokens = line.match(RELAY_TOKEN) ?? [];
294
+ if (tokens.some((token) => values[token] === ''))
295
+ continue;
296
+ const composed = line.replace(RELAY_TOKEN, (token) => {
297
+ const value = values[token];
298
+ return value === undefined
299
+ ? token
300
+ : value.replace(/\r?\n/g, (separator) => `${separator}> `);
301
+ });
302
+ lines.push(composed);
303
+ }
304
+ return lines.join('\n');
409
305
  }
410
- function codeAfterDecideCallText(context) {
411
- return quotedRelay([
412
- ...planningRelayValues(context),
413
- ['DECIDE commit', context.decideCommit ?? ''],
414
- ['Evaluated revision', context.evaluatedRevision ?? ''],
415
- ]);
306
+ function plannedCallValues(context) {
307
+ return {
308
+ '<development-request>': context.developmentRequest,
309
+ '<discussion-context>': context.discussionContext,
310
+ '<planning-result>': context.planningResult,
311
+ };
416
312
  }
417
- function branchCallText(context) {
418
- return quotedRelay(planningRelayValues(context));
313
+ function plannedCallText(context) {
314
+ return composeRelay(PLANNED_CALL_TEMPLATE, plannedCallValues(context));
315
+ }
316
+ function codeAfterDecideCallText(context) {
317
+ return composeRelay(CODE_AFTER_DECIDE_CALL_TEMPLATE, {
318
+ ...plannedCallValues(context),
319
+ '<decide-commit>': context.decideCommit,
320
+ '<evaluated-revision>': context.evaluatedRevision,
321
+ });
419
322
  }
420
- // DEV-6: every placeholder is substituted from typed context that only a
421
- // child's canonical structured result populated — never from player prose.
422
323
  function pullRequestCallText(context) {
423
- return quotedRelay([
424
- ['Original request', context.developmentRequest ?? ''],
425
- ['Issue summary', context.issueSummary ?? ''],
426
- ['Branch', context.branch ?? ''],
427
- ['Base revision', context.baseRevision ?? ''],
428
- ['CODE commit', context.lastCodeCommit ?? ''],
429
- ['Evaluated revision', context.finalEvaluatedRevision ?? ''],
430
- ]);
431
- }
432
- function acceptedPlanningPath(context, event, pullRequestPath) {
433
- return {
434
- planningResult: outputString(event, 'planningResult'),
435
- discussionExchanges: archivedExchanges(context),
436
- deliveryViaPullRequest: pullRequestPath !== undefined,
437
- pullRequestPath,
438
- pendingBossQuestion: undefined,
439
- bossReply: undefined,
440
- };
324
+ return composeRelay(PULL_REQUEST_CALL_TEMPLATE, {
325
+ '<development-request>': context.developmentRequest,
326
+ '<issue-summary>': context.issueSummary,
327
+ '<branch>': context.branch,
328
+ '<base-revision>': context.baseRevision,
329
+ '<last-code-commit>': context.lastCodeCommit,
330
+ '<final-evaluated-revision>': context.finalEvaluatedRevision,
331
+ });
332
+ }
333
+ // ---------------------------------------------------------------------------
334
+ // Boss entry and reply surfaces.
335
+ function startsDev({ event }) {
336
+ return (event.type === 'START_DEV' &&
337
+ typeof event.developmentRequest === 'string' &&
338
+ event.developmentRequest.trim().length > 0);
339
+ }
340
+ function emptyBossReply({ event }) {
341
+ return (event.type === 'BOSS_REPLY' &&
342
+ (typeof event.answer !== 'string' || event.answer.trim().length === 0));
343
+ }
344
+ function resumableStates(ids) {
345
+ return ids.map((id) => ({
346
+ guard: ({ context, event }) => event.type === 'BOSS_REPLY' &&
347
+ typeof event.answer === 'string' &&
348
+ event.answer.trim().length > 0 &&
349
+ context.pendingBossQuestion?.resumeStateId === id &&
350
+ (event.questionId === undefined || event.questionId === id),
351
+ target: `#${id}`,
352
+ reenter: true,
353
+ actions: 'rememberBossReply',
354
+ }));
441
355
  }
442
356
  const machineSetup = setup({
443
357
  types: {},
@@ -450,188 +364,159 @@ const machineSetup = setup({
450
364
  }),
451
365
  },
452
366
  guards: {
453
- isDiscussionComplete,
454
- isCodePath,
455
- isDecideThenCode,
456
- isCodeViaPullRequest,
457
- isDecideThenCodeViaPullRequest,
458
- needsBossReply,
459
- isBranchSuccessForCode,
460
- isBranchSuccessForDecide,
461
- isCodeSuccessViaPullRequest,
462
- isPlainCodeSuccess,
463
- isDecideSuccess,
464
- authoredBranchFailure: authoredChildFailureGuard('branch'),
465
- authoredCodeFailure: authoredChildFailureGuard('code'),
466
- authoredDecideFailure: authoredChildFailureGuard('decide'),
467
- authoredPrFailure: authoredChildFailureGuard('pr'),
468
- emptyBossReply: ({ event }) => event.type === 'BOSS_REPLY' && event.answer.trim().length === 0,
469
- resumesPlanAnalysis: ({ context, event }) => event.type === 'BOSS_REPLY' &&
470
- event.answer.trim().length > 0 &&
471
- context.pendingBossQuestion?.resumeStateId === 'planAnalysis' &&
472
- (event.questionId === undefined || event.questionId === 'planAnalysis'),
367
+ startsDev,
368
+ emptyBossReply,
369
+ discussionCompleteAfterBossReply,
370
+ codePlanned: plannedPath('code'),
371
+ decideThenCodePlanned: plannedPath('decideThenCode'),
372
+ codeViaPullRequestPlanned: plannedPath('codeViaPullRequest'),
373
+ decideThenCodeViaPullRequestPlanned: plannedPath('decideThenCodeViaPullRequest'),
374
+ needsBossReplyWithQuestion,
375
+ needsBossReplyWithoutQuestion,
376
+ branchSucceededForCode: branchSucceededFor('code'),
377
+ branchSucceededForDecide: branchSucceededFor('decide-then-code'),
378
+ decideSucceeded,
379
+ codeSucceededOnPullRequestPath,
380
+ codeSucceededOnPlainPath,
381
+ authoredBranchResult: authoredResultFrom('branch'),
382
+ authoredDecideResult: authoredResultFrom('decide'),
383
+ authoredCodeResult: authoredResultFrom('code'),
384
+ authoredPrResult: authoredResultFrom('pr'),
473
385
  },
474
386
  actions: {
475
387
  'playbook.acceptedOutcome': (_args, _params) => undefined,
476
- startDev: assign(({ context, event }) => {
477
- if (event.type !== 'START_DEV')
478
- return {};
479
- return {
480
- runResults: context.runResults,
481
- developmentRequest: event.developmentRequest,
482
- discussionExchanges: [],
483
- planningResult: undefined,
484
- decideCommit: undefined,
485
- evaluatedRevision: undefined,
486
- deliveryViaPullRequest: false,
487
- pullRequestPath: undefined,
488
- branch: undefined,
489
- baseRevision: undefined,
490
- issueSummary: undefined,
491
- lastCodeCommit: undefined,
492
- finalEvaluatedRevision: undefined,
493
- completion: undefined,
494
- childOutput: undefined,
495
- childFailure: undefined,
496
- lastError: undefined,
497
- pendingBossQuestion: undefined,
498
- bossReply: undefined,
499
- };
500
- }),
388
+ startDev: assign(({ event }) => ({
389
+ developmentRequest: event.type === 'START_DEV' ? event.developmentRequest : undefined,
390
+ discussionContext: '',
391
+ planningResult: undefined,
392
+ pullRequestPath: undefined,
393
+ decideCommit: undefined,
394
+ evaluatedRevision: undefined,
395
+ branch: undefined,
396
+ baseRevision: undefined,
397
+ issueSummary: undefined,
398
+ lastCodeCommit: undefined,
399
+ finalEvaluatedRevision: undefined,
400
+ completion: undefined,
401
+ lastError: undefined,
402
+ pendingBossQuestion: undefined,
403
+ bossReply: undefined,
404
+ })),
501
405
  completeDiscussion: assign(({ context }) => ({
502
- discussionExchanges: archivedExchanges(context),
503
- completion: 'discussion-complete',
406
+ discussionContext: archivedDiscussion(context),
407
+ completion: { kind: 'discussion-complete' },
504
408
  pendingBossQuestion: undefined,
505
409
  bossReply: undefined,
506
410
  })),
507
- rememberCodePath: assign(({ context, event }) => acceptedPlanningPath(context, event)),
508
- rememberDecidePath: assign(({ context, event }) => acceptedPlanningPath(context, event)),
509
- rememberCodeViaPullRequestPath: assign(({ context, event }) => acceptedPlanningPath(context, event, 'code')),
510
- rememberDecideThenCodeViaPullRequestPath: assign(({ context, event }) => acceptedPlanningPath(context, event, 'decide-then-code')),
511
- rememberPendingQuestion: assign(({ context, event }) => {
512
- const question = outputString(event, 'question');
513
- if (question === undefined || !outputGuard(event, 'needsBossReply')) {
514
- return { lastError: new Error('Analyst question output was malformed') };
515
- }
516
- return {
517
- discussionExchanges: archivedExchanges(context),
518
- pendingBossQuestion: {
519
- questionId: 'planAnalysis',
520
- resumeStateId: 'planAnalysis',
521
- sourceItem: 'DEV-1',
522
- asker: { kind: 'role', roleId: 'analyst' },
523
- question,
524
- },
525
- bossReply: undefined,
526
- };
527
- }),
411
+ acceptPlainPlan: assign(({ context, event }) => ({
412
+ planningResult: outputText(event, 'planningResult'),
413
+ discussionContext: archivedDiscussion(context),
414
+ pendingBossQuestion: undefined,
415
+ bossReply: undefined,
416
+ })),
417
+ acceptPullRequestPlan: assign(({ context, event }, params) => ({
418
+ planningResult: outputText(event, 'planningResult'),
419
+ pullRequestPath: params.path,
420
+ discussionContext: archivedDiscussion(context),
421
+ pendingBossQuestion: undefined,
422
+ bossReply: undefined,
423
+ })),
424
+ setPendingBossQuestion: assign(({ context, event }, params) => ({
425
+ discussionContext: archivedDiscussion(context),
426
+ pendingBossQuestion: {
427
+ ...params,
428
+ questionId: params.resumeStateId,
429
+ question: outputText(event, 'question') ?? '',
430
+ },
431
+ bossReply: undefined,
432
+ })),
528
433
  rememberBossReply: assign(({ event }) => event.type === 'BOSS_REPLY' ? { bossReply: event.answer } : {}),
529
- rememberEmptyBossReplyError: assign({
530
- lastError: () => new Error('BOSS_REPLY received an empty answer'),
434
+ clearBossReplyContext: assign({
531
435
  pendingBossQuestion: undefined,
532
436
  bossReply: undefined,
533
437
  }),
534
438
  rememberBranchResult: assign(({ event }) => {
535
- const output = childOutputOf(event);
536
- if (!isBranchSuccessValue(output))
537
- return {};
538
- return {
539
- branch: output.branch,
540
- baseRevision: output.baseRevision,
541
- issueSummary: output.issueSummary,
542
- };
439
+ const success = branchSuccessOf(childOutputOf(event));
440
+ return success === undefined ? {} : { ...success };
543
441
  }),
544
442
  rememberDecideResult: assign(({ event }) => {
545
- const output = childOutputOf(event);
546
- if (!isDecideSuccessValue(output))
547
- return {};
548
- return {
549
- decideCommit: output.decideCommit,
550
- evaluatedRevision: output.evaluatedRevision,
551
- };
443
+ const success = decideSuccessOf(childOutputOf(event));
444
+ return success === undefined ? {} : { ...success };
552
445
  }),
553
446
  rememberCodeResult: assign(({ event }) => {
554
- const output = childOutputOf(event);
555
- if (!isCodeSuccessValueForPullRequest(output))
556
- return {};
557
- return {
558
- lastCodeCommit: output.lastCodeCommit,
559
- finalEvaluatedRevision: output.finalEvaluatedRevision,
560
- };
447
+ const success = codeSuccessOf(childOutputOf(event));
448
+ return success === undefined ? {} : { ...success };
561
449
  }),
562
- completeWithChildSuccess: assign(({ event }) => {
450
+ completeWithChildSuccess: assign(({ event }, params) => {
563
451
  const output = childOutputOf(event);
564
452
  return {
565
- completion: 'complete',
566
- ...(output === undefined ? {} : { childOutput: output }),
453
+ completion: {
454
+ kind: 'complete',
455
+ childPlaybookId: params.childPlaybookId,
456
+ ...(output === undefined ? {} : { childOutput: output }),
457
+ },
567
458
  };
568
459
  }),
569
- completeWithInsufficientBranchResult: assign(({ event }) => ({
570
- completion: 'child-failed',
571
- childFailure: insufficientChildResult(event, 'branch'),
572
- })),
573
- completeWithInsufficientCodeResult: assign(({ event }) => ({
574
- completion: 'child-failed',
575
- childFailure: insufficientChildResult(event, 'code'),
576
- })),
577
- completeWithInsufficientDecideResult: assign(({ event }) => ({
578
- completion: 'child-failed',
579
- childFailure: insufficientChildResult(event, 'decide'),
580
- })),
581
- completeWithBranchFailure: assign(({ event }) => ({
582
- completion: 'child-failed',
583
- childFailure: relayedChildFailure(event, 'branch'),
584
- })),
585
- completeWithCodeFailure: assign(({ event }) => ({
586
- completion: 'child-failed',
587
- childFailure: relayedChildFailure(event, 'code'),
588
- })),
589
- completeWithDecideFailure: assign(({ event }) => ({
590
- completion: 'child-failed',
591
- childFailure: relayedChildFailure(event, 'decide'),
592
- })),
593
- completeWithPrFailure: assign(({ event }) => ({
594
- completion: 'child-failed',
595
- childFailure: relayedChildFailure(event, 'pr'),
460
+ completeWithInsufficientResult: assign(({ event }, params) => ({
461
+ completion: {
462
+ kind: 'child-failed',
463
+ childResult: insufficientChildResult(event, params.playbookId),
464
+ },
596
465
  })),
466
+ completeWithAuthoredChildResult: assign(({ event }, params) => {
467
+ const childResult = relayedChildResult(event, params.playbookId);
468
+ return childResult === undefined
469
+ ? {}
470
+ : { completion: { kind: 'child-failed', childResult } };
471
+ }),
597
472
  rememberActorError: assign(({ event }) => ({
598
- lastError: event.error,
473
+ lastError: normalizedControlError(childErrorOf(event)),
599
474
  })),
600
475
  rememberMalformedPlayerOutput: assign({
601
- lastError: () => new Error('Analyst result for DEV-1 did not match an available outcome.'),
476
+ lastError: {
477
+ name: 'MalformedActorOutput',
478
+ message: 'Analyst output for DEV-1 did not match an available declared result.',
479
+ },
480
+ pendingBossQuestion: undefined,
481
+ bossReply: undefined,
482
+ }),
483
+ rememberMalformedBossReply: assign({
484
+ lastError: {
485
+ name: 'MalformedBossReply',
486
+ message: 'BOSS_REPLY carried an empty or whitespace-only answer.',
487
+ },
488
+ pendingBossQuestion: undefined,
489
+ bossReply: undefined,
602
490
  }),
603
491
  },
604
492
  });
605
493
  export const devMachine = machineSetup.createMachine({
606
494
  id: 'dev',
495
+ description: 'Plans the development path for a request and runs the child playbooks it selects.',
607
496
  initial: 'ready',
608
497
  context: ({ input }) => ({
609
- runResults: typeof input.runResults === 'string' ? input.runResults : '',
610
- discussionExchanges: [],
611
- deliveryViaPullRequest: false,
498
+ runResults: typeof input?.runResults === 'string' ? input.runResults : '',
499
+ discussionContext: '',
612
500
  }),
613
501
  output: ({ context }) => {
614
- if (context.completion === 'discussion-complete') {
615
- return { status: 'discussion-complete' };
616
- }
617
- if (context.completion === 'complete') {
618
- return {
619
- status: 'complete',
620
- // `done` is entered by `pr` on a pull-request path and by `code` on a
621
- // plain path; no other arm completes the workflow.
622
- childPlaybookId: context.deliveryViaPullRequest ? 'pr' : 'code',
623
- ...(context.childOutput === undefined
624
- ? {}
625
- : { childOutput: context.childOutput }),
626
- };
502
+ const completion = context.completion;
503
+ if (completion === undefined) {
504
+ throw new Error('DEV reached a final state without a recorded completion');
627
505
  }
628
- if (context.completion === 'child-failed') {
629
- if (context.childFailure === undefined) {
630
- throw new Error('DEV reached a final state without a child result');
631
- }
632
- return { status: 'child-failed', childResult: context.childFailure };
506
+ switch (completion.kind) {
507
+ case 'discussion-complete':
508
+ return { status: 'discussion-complete' };
509
+ case 'complete':
510
+ return {
511
+ status: 'complete',
512
+ childPlaybookId: completion.childPlaybookId,
513
+ ...(completion.childOutput === undefined
514
+ ? {}
515
+ : { childOutput: completion.childOutput }),
516
+ };
517
+ case 'child-failed':
518
+ return { status: 'child-failed', childResult: completion.childResult };
633
519
  }
634
- throw new Error('DEV reached a final state without a recorded completion');
635
520
  },
636
521
  states: {
637
522
  ready: {
@@ -640,13 +525,17 @@ export const devMachine = machineSetup.createMachine({
640
525
  meta: playbookMeta('ready'),
641
526
  tags: ['playbook.parked'],
642
527
  on: {
643
- START_DEV: { target: 'planAnalysis', actions: 'startDev' },
528
+ START_DEV: {
529
+ guard: 'startsDev',
530
+ target: 'planAnalysis',
531
+ actions: 'startDev',
532
+ },
644
533
  },
645
534
  },
646
535
  planAnalysis: {
647
536
  id: 'planAnalysis',
648
537
  description: STATE_DESCRIPTIONS.planAnalysis,
649
- meta: playbookMeta('planAnalysis', 'analyst'),
538
+ meta: roleMeta('planAnalysis', 'analyst'),
650
539
  tags: ['playbook.busy'],
651
540
  invoke: {
652
541
  src: 'player',
@@ -655,9 +544,9 @@ export const devMachine = machineSetup.createMachine({
655
544
  role: 'analyst',
656
545
  sourceItem: 'DEV-1',
657
546
  prompt: PLAN_ANALYSIS_PROMPT,
658
- result: PLAN_ANALYSIS_RESULTS,
547
+ result: PLAN_ANALYSIS_RESULT,
659
548
  developmentRequest: context.developmentRequest ?? '',
660
- discussionContext: renderDiscussionContext(context.discussionExchanges),
549
+ discussionContext: context.discussionContext,
661
550
  runResults: context.runResults,
662
551
  ...(context.pendingBossQuestion === undefined
663
552
  ? {}
@@ -668,7 +557,27 @@ export const devMachine = machineSetup.createMachine({
668
557
  }),
669
558
  onDone: [
670
559
  {
671
- guard: 'isDiscussionComplete',
560
+ guard: 'needsBossReplyWithQuestion',
561
+ target: 'awaitBossReply',
562
+ actions: [
563
+ {
564
+ type: 'playbook.acceptedOutcome',
565
+ params: {
566
+ source: 'planAnalysis',
567
+ target: 'awaitBossReply',
568
+ acceptedOutcome: 'needsBossReply',
569
+ },
570
+ },
571
+ { type: 'setPendingBossQuestion', params: PLAN_ANALYSIS_QUESTION },
572
+ ],
573
+ },
574
+ {
575
+ guard: 'needsBossReplyWithoutQuestion',
576
+ target: 'failed',
577
+ actions: 'rememberMalformedPlayerOutput',
578
+ },
579
+ {
580
+ guard: 'discussionCompleteAfterBossReply',
672
581
  target: 'discussionComplete',
673
582
  actions: [
674
583
  {
@@ -683,7 +592,7 @@ export const devMachine = machineSetup.createMachine({
683
592
  ],
684
593
  },
685
594
  {
686
- guard: 'isCodePath',
595
+ guard: 'codePlanned',
687
596
  target: 'callCode',
688
597
  actions: [
689
598
  {
@@ -694,11 +603,11 @@ export const devMachine = machineSetup.createMachine({
694
603
  acceptedOutcome: 'code',
695
604
  },
696
605
  },
697
- 'rememberCodePath',
606
+ 'acceptPlainPlan',
698
607
  ],
699
608
  },
700
609
  {
701
- guard: 'isDecideThenCode',
610
+ guard: 'decideThenCodePlanned',
702
611
  target: 'callDecide',
703
612
  actions: [
704
613
  {
@@ -709,11 +618,11 @@ export const devMachine = machineSetup.createMachine({
709
618
  acceptedOutcome: 'decideThenCode',
710
619
  },
711
620
  },
712
- 'rememberDecidePath',
621
+ 'acceptPlainPlan',
713
622
  ],
714
623
  },
715
624
  {
716
- guard: 'isCodeViaPullRequest',
625
+ guard: 'codeViaPullRequestPlanned',
717
626
  target: 'createBranch',
718
627
  actions: [
719
628
  {
@@ -724,11 +633,11 @@ export const devMachine = machineSetup.createMachine({
724
633
  acceptedOutcome: 'codeViaPullRequest',
725
634
  },
726
635
  },
727
- 'rememberCodeViaPullRequestPath',
636
+ { type: 'acceptPullRequestPlan', params: { path: 'code' } },
728
637
  ],
729
638
  },
730
639
  {
731
- guard: 'isDecideThenCodeViaPullRequest',
640
+ guard: 'decideThenCodeViaPullRequestPlanned',
732
641
  target: 'createBranch',
733
642
  actions: [
734
643
  {
@@ -739,22 +648,10 @@ export const devMachine = machineSetup.createMachine({
739
648
  acceptedOutcome: 'decideThenCodeViaPullRequest',
740
649
  },
741
650
  },
742
- 'rememberDecideThenCodeViaPullRequestPath',
743
- ],
744
- },
745
- {
746
- guard: 'needsBossReply',
747
- target: 'awaitBossReply',
748
- actions: [
749
651
  {
750
- type: 'playbook.acceptedOutcome',
751
- params: {
752
- source: 'planAnalysis',
753
- target: 'awaitBossReply',
754
- acceptedOutcome: 'needsBossReply',
755
- },
652
+ type: 'acceptPullRequestPlan',
653
+ params: { path: 'decide-then-code' },
756
654
  },
757
- 'rememberPendingQuestion',
758
655
  ],
759
656
  },
760
657
  {
@@ -762,7 +659,31 @@ export const devMachine = machineSetup.createMachine({
762
659
  actions: 'rememberMalformedPlayerOutput',
763
660
  },
764
661
  ],
765
- onError: { target: 'failed', actions: 'rememberActorError' },
662
+ onError: {
663
+ target: 'failed',
664
+ actions: ['rememberActorError', 'clearBossReplyContext'],
665
+ },
666
+ },
667
+ },
668
+ awaitBossReply: {
669
+ id: 'awaitBossReply',
670
+ description: STATE_DESCRIPTIONS.awaitBossReply,
671
+ meta: playbookMeta('awaitBossReply'),
672
+ tags: ['playbook.parked'],
673
+ on: {
674
+ BOSS_REPLY: [
675
+ {
676
+ guard: 'emptyBossReply',
677
+ target: 'failed',
678
+ actions: 'rememberMalformedBossReply',
679
+ },
680
+ ...resumableStates(['planAnalysis']),
681
+ ],
682
+ START_DEV: {
683
+ guard: 'startsDev',
684
+ target: 'planAnalysis',
685
+ actions: 'startDev',
686
+ },
766
687
  },
767
688
  },
768
689
  createBranch: {
@@ -776,29 +697,35 @@ export const devMachine = machineSetup.createMachine({
776
697
  stateId: 'createBranch',
777
698
  sourceItem: 'DEV-5',
778
699
  playbookId: 'branch',
779
- text: branchCallText(context),
700
+ text: plannedCallText(context),
780
701
  }),
781
702
  onDone: [
782
703
  {
783
- guard: 'isBranchSuccessForCode',
704
+ guard: 'branchSucceededForCode',
784
705
  target: 'callCode',
785
706
  actions: 'rememberBranchResult',
786
707
  },
787
708
  {
788
- guard: 'isBranchSuccessForDecide',
709
+ guard: 'branchSucceededForDecide',
789
710
  target: 'callDecide',
790
711
  actions: 'rememberBranchResult',
791
712
  },
792
713
  {
793
714
  target: 'reportedChildFailure',
794
- actions: 'completeWithInsufficientBranchResult',
715
+ actions: {
716
+ type: 'completeWithInsufficientResult',
717
+ params: { playbookId: 'branch' },
718
+ },
795
719
  },
796
720
  ],
797
721
  onError: [
798
722
  {
799
- guard: 'authoredBranchFailure',
723
+ guard: 'authoredBranchResult',
800
724
  target: 'reportedChildFailure',
801
- actions: 'completeWithBranchFailure',
725
+ actions: {
726
+ type: 'completeWithAuthoredChildResult',
727
+ params: { playbookId: 'branch' },
728
+ },
802
729
  },
803
730
  { target: 'failed', actions: 'rememberActorError' },
804
731
  ],
@@ -815,29 +742,38 @@ export const devMachine = machineSetup.createMachine({
815
742
  stateId: 'callCode',
816
743
  sourceItem: 'DEV-2',
817
744
  playbookId: 'code',
818
- text: codeCallText(context),
745
+ text: plannedCallText(context),
819
746
  }),
820
747
  onDone: [
821
748
  {
822
- guard: 'isCodeSuccessViaPullRequest',
749
+ guard: 'codeSucceededOnPullRequestPath',
823
750
  target: 'openPullRequest',
824
751
  actions: 'rememberCodeResult',
825
752
  },
826
753
  {
827
- guard: 'isPlainCodeSuccess',
754
+ guard: 'codeSucceededOnPlainPath',
828
755
  target: 'done',
829
- actions: 'completeWithChildSuccess',
756
+ actions: {
757
+ type: 'completeWithChildSuccess',
758
+ params: { childPlaybookId: 'code' },
759
+ },
830
760
  },
831
761
  {
832
762
  target: 'reportedChildFailure',
833
- actions: 'completeWithInsufficientCodeResult',
763
+ actions: {
764
+ type: 'completeWithInsufficientResult',
765
+ params: { playbookId: 'code' },
766
+ },
834
767
  },
835
768
  ],
836
769
  onError: [
837
770
  {
838
- guard: 'authoredCodeFailure',
771
+ guard: 'authoredCodeResult',
839
772
  target: 'reportedChildFailure',
840
- actions: 'completeWithCodeFailure',
773
+ actions: {
774
+ type: 'completeWithAuthoredChildResult',
775
+ params: { playbookId: 'code' },
776
+ },
841
777
  },
842
778
  { target: 'failed', actions: 'rememberActorError' },
843
779
  ],
@@ -854,24 +790,30 @@ export const devMachine = machineSetup.createMachine({
854
790
  stateId: 'callDecide',
855
791
  sourceItem: 'DEV-3',
856
792
  playbookId: 'decide',
857
- text: decideCallText(context),
793
+ text: plannedCallText(context),
858
794
  }),
859
795
  onDone: [
860
796
  {
861
- guard: 'isDecideSuccess',
797
+ guard: 'decideSucceeded',
862
798
  target: 'callCodeAfterDecide',
863
799
  actions: 'rememberDecideResult',
864
800
  },
865
801
  {
866
802
  target: 'reportedChildFailure',
867
- actions: 'completeWithInsufficientDecideResult',
803
+ actions: {
804
+ type: 'completeWithInsufficientResult',
805
+ params: { playbookId: 'decide' },
806
+ },
868
807
  },
869
808
  ],
870
809
  onError: [
871
810
  {
872
- guard: 'authoredDecideFailure',
811
+ guard: 'authoredDecideResult',
873
812
  target: 'reportedChildFailure',
874
- actions: 'completeWithDecideFailure',
813
+ actions: {
814
+ type: 'completeWithAuthoredChildResult',
815
+ params: { playbookId: 'decide' },
816
+ },
875
817
  },
876
818
  { target: 'failed', actions: 'rememberActorError' },
877
819
  ],
@@ -892,25 +834,34 @@ export const devMachine = machineSetup.createMachine({
892
834
  }),
893
835
  onDone: [
894
836
  {
895
- guard: 'isCodeSuccessViaPullRequest',
837
+ guard: 'codeSucceededOnPullRequestPath',
896
838
  target: 'openPullRequest',
897
839
  actions: 'rememberCodeResult',
898
840
  },
899
841
  {
900
- guard: 'isPlainCodeSuccess',
842
+ guard: 'codeSucceededOnPlainPath',
901
843
  target: 'done',
902
- actions: 'completeWithChildSuccess',
844
+ actions: {
845
+ type: 'completeWithChildSuccess',
846
+ params: { childPlaybookId: 'code' },
847
+ },
903
848
  },
904
849
  {
905
850
  target: 'reportedChildFailure',
906
- actions: 'completeWithInsufficientCodeResult',
851
+ actions: {
852
+ type: 'completeWithInsufficientResult',
853
+ params: { playbookId: 'code' },
854
+ },
907
855
  },
908
856
  ],
909
857
  onError: [
910
858
  {
911
- guard: 'authoredCodeFailure',
859
+ guard: 'authoredCodeResult',
912
860
  target: 'reportedChildFailure',
913
- actions: 'completeWithCodeFailure',
861
+ actions: {
862
+ type: 'completeWithAuthoredChildResult',
863
+ params: { playbookId: 'code' },
864
+ },
914
865
  },
915
866
  { target: 'failed', actions: 'rememberActorError' },
916
867
  ],
@@ -929,67 +880,58 @@ export const devMachine = machineSetup.createMachine({
929
880
  playbookId: 'pr',
930
881
  text: pullRequestCallText(context),
931
882
  }),
932
- // DR-048: `pr` declares its terminal kinds, so `onDone` alone proves
933
- // its success; DEV consumes none of its fields by name.
934
- onDone: { target: 'done', actions: 'completeWithChildSuccess' },
883
+ // `pr` success completes DEV with that result; a `pr` failure
884
+ // terminal arrives through `onError` and is relayed below.
885
+ onDone: {
886
+ target: 'done',
887
+ actions: {
888
+ type: 'completeWithChildSuccess',
889
+ params: { childPlaybookId: 'pr' },
890
+ },
891
+ },
935
892
  onError: [
936
893
  {
937
- guard: 'authoredPrFailure',
894
+ guard: 'authoredPrResult',
938
895
  target: 'reportedChildFailure',
939
- actions: 'completeWithPrFailure',
896
+ actions: {
897
+ type: 'completeWithAuthoredChildResult',
898
+ params: { playbookId: 'pr' },
899
+ },
940
900
  },
941
901
  { target: 'failed', actions: 'rememberActorError' },
942
902
  ],
943
903
  },
944
904
  },
945
- awaitBossReply: {
946
- id: 'awaitBossReply',
947
- description: STATE_DESCRIPTIONS.awaitBossReply,
948
- meta: playbookMeta('awaitBossReply'),
949
- tags: ['playbook.parked'],
950
- on: {
951
- BOSS_REPLY: [
952
- {
953
- guard: 'emptyBossReply',
954
- target: '#failed',
955
- actions: 'rememberEmptyBossReplyError',
956
- },
957
- {
958
- guard: 'resumesPlanAnalysis',
959
- target: '#planAnalysis',
960
- reenter: true,
961
- actions: 'rememberBossReply',
962
- },
963
- ],
964
- START_DEV: { target: 'planAnalysis', actions: 'startDev' },
965
- },
966
- },
967
905
  failed: {
968
906
  id: 'failed',
969
907
  description: STATE_DESCRIPTIONS.failed,
970
908
  meta: playbookMeta('failed'),
971
909
  tags: ['playbook.parked'],
972
910
  on: {
973
- START_DEV: { target: 'planAnalysis', actions: 'startDev' },
911
+ START_DEV: {
912
+ guard: 'startsDev',
913
+ target: 'planAnalysis',
914
+ actions: 'startDev',
915
+ },
974
916
  },
975
917
  },
976
918
  discussionComplete: {
977
919
  id: 'discussionComplete',
920
+ type: 'final',
978
921
  description: STATE_DESCRIPTIONS.discussionComplete,
979
922
  meta: terminalMeta('discussionComplete', 'success'),
980
- type: 'final',
981
923
  },
982
924
  done: {
983
925
  id: 'done',
926
+ type: 'final',
984
927
  description: STATE_DESCRIPTIONS.done,
985
928
  meta: terminalMeta('done', 'success'),
986
- type: 'final',
987
929
  },
988
930
  reportedChildFailure: {
989
931
  id: 'reportedChildFailure',
932
+ type: 'final',
990
933
  description: STATE_DESCRIPTIONS.reportedChildFailure,
991
934
  meta: terminalMeta('reportedChildFailure', 'failure'),
992
- type: 'final',
993
935
  },
994
936
  },
995
937
  });