@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,9 +1,13 @@
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
 
6
8
  import { assign, fromPromise, setup } from 'xstate';
9
+ import { validatePlaybookCallResult } from '@sublang/playbook/xstate-runtime';
10
+ import type { PlaybookCallResult } from '@sublang/playbook/runtime';
7
11
 
8
12
  export type JsonValue =
9
13
  | null
@@ -13,56 +17,59 @@ export type JsonValue =
13
17
  | readonly JsonValue[]
14
18
  | { readonly [key: string]: JsonValue };
15
19
 
16
- export type DevStateId =
17
- | 'planAnalysis'
18
- | 'createBranch'
20
+ /** DEV declares no parallel group. */
21
+ export const concurrentRoleSets: readonly (readonly string[])[] = [];
22
+
23
+ export type DevRole = 'analyst';
24
+
25
+ export type DevChildPlaybookId = 'code' | 'decide' | 'branch' | 'pr';
26
+
27
+ export type DevCallStateId =
19
28
  | 'callCode'
20
29
  | 'callDecide'
21
30
  | 'callCodeAfterDecide'
31
+ | 'createBranch'
22
32
  | 'openPullRequest';
23
33
 
24
- export type DevSourceItem =
25
- | 'DEV-1'
26
- | 'DEV-2'
27
- | 'DEV-3'
28
- | 'DEV-4'
29
- | 'DEV-5'
30
- | 'DEV-6';
34
+ export type DevCallSourceItem = 'DEV-2' | 'DEV-3' | 'DEV-4' | 'DEV-5' | 'DEV-6';
31
35
 
32
- export type DevChildPlaybookId = 'code' | 'decide' | 'branch' | 'pr';
36
+ type ResumableStateId = 'planAnalysis';
33
37
 
34
38
  /**
35
- * DR-050: the development path a pull-request planning outcome selected. It
36
- * routes the `branch` success into `callCode` or `callDecide`, so the plain
37
- * `code` and `decide then code` paths keep their items, prompts, and edges.
39
+ * The development path a pull-request planning outcome selected. It routes
40
+ * the `branch` success into the `code` call (DEV-2) or the `decide` call
41
+ * (DEV-3); its absence means a plain path that calls neither `branch` nor `pr`.
38
42
  */
39
43
  export type DevPullRequestPath = 'code' | 'decide-then-code';
40
44
 
41
- export type PendingBossQuestion = {
42
- readonly questionId: 'planAnalysis';
43
- readonly resumeStateId: 'planAnalysis';
45
+ export interface PendingBossQuestion {
46
+ readonly questionId: ResumableStateId;
47
+ readonly resumeStateId: ResumableStateId;
44
48
  readonly sourceItem: 'DEV-1';
45
- readonly asker: { readonly kind: 'role'; readonly roleId: 'analyst' };
49
+ readonly asker: { readonly kind: 'role'; readonly roleId: DevRole };
46
50
  readonly question: string;
47
- };
51
+ }
48
52
 
49
- export type DiscussionExchange = {
50
- readonly question: string;
51
- readonly answer: string;
52
- };
53
+ type PendingBossQuestionParams = Omit<
54
+ PendingBossQuestion,
55
+ 'questionId' | 'question'
56
+ >;
53
57
 
54
- export type PlayerInput = {
55
- readonly stateId: 'planAnalysis';
56
- readonly role: 'analyst';
58
+ export interface PlayerInput {
59
+ readonly stateId: ResumableStateId;
60
+ readonly role: DevRole;
57
61
  readonly sourceItem: 'DEV-1';
58
62
  readonly prompt: string;
59
63
  readonly result: Readonly<Record<string, string>>;
64
+ /** Substitutes `<development-request>`. */
60
65
  readonly developmentRequest: string;
66
+ /** Substitutes `<discussion-context>`. */
61
67
  readonly discussionContext: string;
68
+ /** Substitutes `<run-results>`. */
62
69
  readonly runResults: string;
63
70
  readonly pendingBossQuestion?: PendingBossQuestion;
64
71
  readonly bossReply?: string;
65
- };
72
+ }
66
73
 
67
74
  export type PlayerOutput =
68
75
  | { readonly guard: 'discussionComplete' }
@@ -75,60 +82,29 @@ export type PlayerOutput =
75
82
  }
76
83
  | { readonly guard: 'needsBossReply'; readonly question: string };
77
84
 
78
- export type PlaybookInput = {
79
- readonly stateId:
80
- | 'createBranch'
81
- | 'callCode'
82
- | 'callDecide'
83
- | 'callCodeAfterDecide'
84
- | 'openPullRequest';
85
- readonly sourceItem: 'DEV-2' | 'DEV-3' | 'DEV-4' | 'DEV-5' | 'DEV-6';
85
+ export interface PlaybookInput {
86
+ readonly stateId: DevCallStateId;
87
+ readonly sourceItem: DevCallSourceItem;
86
88
  readonly playbookId: DevChildPlaybookId;
87
89
  readonly text: string;
88
- };
90
+ }
89
91
 
90
- /**
91
- * The affirmative success proof DEV requires from `decide`'s canonical
92
- * structured terminal output before starting the dependent `code` call:
93
- * the `decide`-owned commit, the exact evaluated repository revision, and
94
- * the affirmative no-unsettled-findings fact. Additional members belong to
95
- * DECIDE's own contract and do not disprove success.
96
- */
97
- export type DecideSuccessOutput = {
98
- readonly decideCommit: string;
99
- readonly evaluatedRevision: string;
100
- readonly noUnsettledFindings: true;
101
- };
92
+ /** The child's JSON-safe machine output itself, delivered on `onDone`. */
93
+ export type PlaybookOutput = JsonValue | undefined;
102
94
 
103
- /**
104
- * The fields DEV consumes from `branch`'s canonical structured terminal
105
- * output before continuing a pull-request path: the exact branch name, the
106
- * exact base revision, and the issue summary. Additional members belong to
107
- * BRANCH's own contract and do not disprove success; a missing member is an
108
- * insufficient result, never an empty default.
109
- */
110
- export type BranchSuccessOutput = {
111
- readonly branch: string;
112
- readonly baseRevision: string;
113
- readonly issueSummary: string;
114
- };
115
-
116
- /**
117
- * The fields DEV consumes from `code`'s canonical structured terminal output
118
- * before starting the dependent `pr` call: the exact last `code`-owned commit
119
- * and the exact final evaluated repository revision.
120
- */
121
- export type CodeSuccessOutput = {
122
- readonly lastCodeCommit: string;
123
- readonly finalEvaluatedRevision: string;
124
- };
95
+ export interface CompactError {
96
+ readonly name: string;
97
+ readonly message: string;
98
+ }
125
99
 
126
- export type CompactError = {
100
+ /** A control-plane error retained for inspection while DEV is parked. */
101
+ export interface ControlError {
127
102
  readonly name: string;
128
103
  readonly message: string;
129
- };
104
+ readonly stack?: string;
105
+ }
130
106
 
131
- /** Sanitized canonical child result relayed as DEV's own failure outcome. */
107
+ /** Sanitized canonical child result that DEV relays as its own outcome. */
132
108
  export type CompletedChildResult =
133
109
  | {
134
110
  readonly playbookId: DevChildPlaybookId;
@@ -142,14 +118,12 @@ export type CompletedChildResult =
142
118
  };
143
119
 
144
120
  export type DevPlaybookOutput =
145
- | {
146
- readonly status: 'discussion-complete';
147
- }
121
+ | { readonly status: 'discussion-complete' }
148
122
  | {
149
123
  readonly status: 'complete';
150
124
  /** The final child of the selected path: `pr` on a pull-request path. */
151
125
  readonly childPlaybookId: 'code' | 'pr';
152
- /** The successful result of DEV's final child call, when it has one. */
126
+ /** The successful result of that final child call, when it has one. */
153
127
  readonly childOutput?: JsonValue;
154
128
  }
155
129
  | {
@@ -158,46 +132,50 @@ export type DevPlaybookOutput =
158
132
  readonly childResult: CompletedChildResult;
159
133
  };
160
134
 
161
- export type DevInput = {
135
+ type DevCompletion =
136
+ | { readonly kind: 'discussion-complete' }
137
+ | {
138
+ readonly kind: 'complete';
139
+ readonly childPlaybookId: 'code' | 'pr';
140
+ readonly childOutput?: JsonValue;
141
+ }
142
+ | { readonly kind: 'child-failed'; readonly childResult: CompletedChildResult };
143
+
144
+ export interface DevInput {
145
+ /** Optional seed for `<run-results>`; absent means no relevant run results. */
162
146
  readonly runResults?: string;
163
- };
147
+ }
164
148
 
165
- export type DevContext = {
149
+ export interface DevContext {
166
150
  readonly runResults: string;
167
151
  readonly developmentRequest?: string;
168
- readonly discussionExchanges: readonly DiscussionExchange[];
152
+ /** Consumed Analyst Q&A for `<discussion-context>`; empty before any reply. */
153
+ readonly discussionContext: string;
169
154
  readonly planningResult?: string;
155
+ readonly pullRequestPath?: DevPullRequestPath;
170
156
  readonly decideCommit?: string;
171
157
  readonly evaluatedRevision?: string;
172
- /** DR-050: whether the accepted planning outcome selected a pull-request path. */
173
- readonly deliveryViaPullRequest: boolean;
174
- readonly pullRequestPath?: DevPullRequestPath;
175
158
  readonly branch?: string;
176
159
  readonly baseRevision?: string;
177
160
  readonly issueSummary?: string;
178
161
  readonly lastCodeCommit?: string;
179
162
  readonly finalEvaluatedRevision?: string;
180
- readonly completion?: 'discussion-complete' | 'complete' | 'child-failed';
181
- readonly childOutput?: JsonValue;
182
- readonly childFailure?: CompletedChildResult;
183
- readonly lastError?: unknown;
163
+ readonly completion?: DevCompletion;
164
+ readonly lastError?: ControlError;
184
165
  readonly pendingBossQuestion?: PendingBossQuestion;
185
166
  readonly bossReply?: string;
186
- };
167
+ }
187
168
 
188
169
  export type DevEvent =
189
170
  | { readonly type: 'START_DEV'; readonly developmentRequest: string }
190
171
  | {
191
172
  readonly type: 'BOSS_REPLY';
192
173
  readonly answer: string;
193
- readonly questionId?: 'planAnalysis';
174
+ readonly questionId?: string;
194
175
  };
195
176
 
177
+ // DEV-1: the item's blockquote, verbatim, with the outer GEARS marker removed.
196
178
  const PLAN_ANALYSIS_PROMPT = [
197
- '> Original request: <development-request>',
198
- '> Prior discussion: <discussion-context>',
199
- '> Run results: <run-results>',
200
- '',
201
179
  'Plan which playbooks run for this request; the playbooks do the work.',
202
180
  'Read the request, then the specs and the repository only as far as choosing the path requires.',
203
181
  'Do not change files or commit while planning or discussing the request.',
@@ -216,79 +194,112 @@ const PLAN_ANALYSIS_PROMPT = [
216
194
  'A question or exploratory discussion is not by itself authorization to create a durable decision or implement changes.',
217
195
  'Do not choose `decide then code` merely because the work is large, nor `code` merely because it is small.',
218
196
  'Consult @specs/map.md for relevant context and @specs/meta.md for spec requirements, if needed.',
197
+ '',
198
+ '> Original request: <development-request>',
199
+ '> Prior discussion: <discussion-context>',
200
+ '> Run results: <run-results>',
219
201
  ].join('\n');
220
202
 
221
- const PLAN_ANALYSIS_RESULTS = {
203
+ const NEEDS_BOSS_REPLY_DESCRIPTION =
204
+ "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>`.";
205
+
206
+ // DEV-1 local result contract: the authored `Results:` bullets in declared
207
+ // order, plus the universal Boss-reply suspension.
208
+ const PLAN_ANALYSIS_RESULT = {
222
209
  discussionComplete:
223
- 'Analyst concluded the discussion after a Boss reply, with no repository work to follow.',
224
- code:
225
- 'Analyst chose `code`: the existing decisions and spec items settle how the work is done. Output shall include `planningResult: <verbatim final text>`.',
210
+ '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.',
211
+ 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>`.',
226
212
  decideThenCode:
227
- 'Analyst chose `decide then code`: the work turns on a point the specs leave open or contradict. Output shall include `planningResult: <verbatim final text>`.',
213
+ '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>`.',
228
214
  codeViaPullRequest:
229
- '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>`.',
215
+ '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>`.',
230
216
  decideThenCodeViaPullRequest:
231
- '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>`.',
232
- needsBossReply:
233
- "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>`.",
217
+ '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>`.',
218
+ needsBossReply: NEEDS_BOSS_REPLY_DESCRIPTION,
234
219
  } as const;
235
220
 
221
+ // DEV-2 and DEV-3 and DEV-5: identical composed blockquotes.
222
+ const PLANNED_CALL_TEMPLATE = [
223
+ '> Original request: <development-request>',
224
+ '> Prior discussion: <discussion-context>',
225
+ '> Planning result: <planning-result>',
226
+ ] as const;
227
+
228
+ // DEV-4.
229
+ const CODE_AFTER_DECIDE_CALL_TEMPLATE = [
230
+ '> Original request: <development-request>',
231
+ '> Prior discussion: <discussion-context>',
232
+ '> Planning result: <planning-result>',
233
+ '> DECIDE commit: <decide-commit>',
234
+ '> Evaluated revision: <evaluated-revision>',
235
+ ] as const;
236
+
237
+ // DEV-6.
238
+ const PULL_REQUEST_CALL_TEMPLATE = [
239
+ '> Original request: <development-request>',
240
+ '> Issue summary: <issue-summary>',
241
+ '> Branch: <branch>',
242
+ '> Base revision: <base-revision>',
243
+ '> CODE commit: <last-code-commit>',
244
+ '> Evaluated revision: <final-evaluated-revision>',
245
+ ] as const;
246
+
236
247
  const STATE_DESCRIPTIONS = {
237
248
  ready: 'Waiting for a development request.',
238
- planAnalysis: 'Analyst is choosing the development path.',
249
+ planAnalysis: 'Analyst chooses the development path in a short planning note.',
250
+ awaitBossReply: "Waiting for Boss to answer the acting agent's question.",
239
251
  createBranch:
240
- 'The BRANCH playbook is preparing the pull-request branch for the planned development path.',
241
- callCode: 'The CODE playbook is implementing the planned development path.',
252
+ 'The BRANCH playbook prepares the branch for the pull-request delivery path.',
253
+ callCode: 'The CODE playbook implements the planned development path.',
242
254
  callDecide:
243
- 'The DECIDE playbook is settling the durable decision the planned path requires.',
255
+ 'The DECIDE playbook settles the decision the planned development path requires.',
244
256
  callCodeAfterDecide:
245
- 'The CODE playbook is implementing the decided development path.',
257
+ 'The CODE playbook implements the development path DECIDE settled.',
246
258
  openPullRequest:
247
- 'The PR playbook is delivering the implemented branch through a pull request.',
248
- awaitBossReply: 'Waiting for Boss to answer Analyst.',
259
+ 'The PR playbook delivers the implemented branch through a pull request.',
249
260
  failed:
250
- 'The development planning workflow failed and is waiting for a new development request.',
261
+ 'DEV failed outside a child result contract and waits for a new development request.',
251
262
  discussionComplete:
252
- 'The planning discussion concluded after a Boss reply with no repository work to follow.',
253
- done: "The selected development path completed with the final child playbook's successful result.",
263
+ 'After a Boss reply, the planning discussion concluded with no child call or repository change.',
264
+ done: "The selected development path completed with its final child playbook's successful result.",
254
265
  reportedChildFailure:
255
- "The development workflow relayed a child playbook's authored abort, failure, or insufficient terminal result.",
266
+ "DEV ended by relaying a child playbook's authored abort, failure, or result that did not prove the required success.",
256
267
  } as const;
257
268
 
258
- function playbookMeta<StateId extends keyof typeof STATE_DESCRIPTIONS>(
259
- stateId: StateId,
260
- role?: 'analyst',
261
- ) {
269
+ type DevStateKey = keyof typeof STATE_DESCRIPTIONS;
270
+
271
+ function playbookMeta<const Key extends DevStateKey>(stateId: Key) {
262
272
  return {
263
- playbook: {
264
- stateId,
265
- description: STATE_DESCRIPTIONS[stateId],
266
- ...(role === undefined ? {} : { role }),
267
- },
268
- };
273
+ playbook: { stateId, description: STATE_DESCRIPTIONS[stateId] },
274
+ } as const;
275
+ }
276
+
277
+ function roleMeta<const Key extends DevStateKey>(stateId: Key, role: DevRole) {
278
+ return {
279
+ playbook: { stateId, description: STATE_DESCRIPTIONS[stateId], role },
280
+ } as const;
269
281
  }
270
282
 
271
- // DR-048: a final state additionally declares whether its outcome means the
272
- // workflow succeeded or failed, so DEV's own caller learns that from the
273
- // machine rather than from DEV's output fields.
274
- function terminalMeta<StateId extends keyof typeof STATE_DESCRIPTIONS>(
275
- stateId: StateId,
283
+ function terminalMeta<const Key extends DevStateKey>(
284
+ stateId: Key,
276
285
  terminal: 'success' | 'failure',
277
286
  ) {
278
287
  return {
279
- playbook: {
280
- stateId,
281
- description: STATE_DESCRIPTIONS[stateId],
282
- terminal,
283
- },
284
- };
288
+ playbook: { stateId, description: STATE_DESCRIPTIONS[stateId], terminal },
289
+ } as const;
285
290
  }
286
291
 
292
+ const PLAN_ANALYSIS_QUESTION: PendingBossQuestionParams = {
293
+ resumeStateId: 'planAnalysis',
294
+ sourceItem: 'DEV-1',
295
+ asker: { kind: 'role', roleId: 'analyst' },
296
+ };
297
+
287
298
  function isRecord(value: unknown): value is Record<string, unknown> {
288
299
  if (value === null || typeof value !== 'object' || Array.isArray(value)) {
289
300
  return false;
290
301
  }
291
- const prototype = Object.getPrototypeOf(value);
302
+ const prototype: unknown = Object.getPrototypeOf(value);
292
303
  return prototype === Object.prototype || prototype === null;
293
304
  }
294
305
 
@@ -296,146 +307,147 @@ function isNonEmptyString(value: unknown): value is string {
296
307
  return typeof value === 'string' && value.trim().length > 0;
297
308
  }
298
309
 
299
- function playerOutput(event: unknown): Record<string, unknown> | undefined {
310
+ // ---------------------------------------------------------------------------
311
+ // Player output (DEV-1), narrowed structurally from an unknown done event.
312
+
313
+ function playerOutputOf(event: unknown): Record<string, unknown> | undefined {
300
314
  if (!isRecord(event)) return undefined;
301
315
  return isRecord(event.output) ? event.output : undefined;
302
316
  }
303
317
 
304
- function outputGuard(event: unknown, guard: PlayerOutput['guard']): boolean {
305
- return playerOutput(event)?.guard === guard;
318
+ function hasGuard(event: unknown, guard: PlayerOutput['guard']): boolean {
319
+ return playerOutputOf(event)?.guard === guard;
306
320
  }
307
321
 
308
- function outputString(event: unknown, field: string): string | undefined {
309
- const value = playerOutput(event)?.[field];
322
+ function outputText(event: unknown, field: string): string | undefined {
323
+ const value = playerOutputOf(event)?.[field];
310
324
  return isNonEmptyString(value) ? value : undefined;
311
325
  }
312
326
 
313
- function isCodePath({ event }: { event: unknown }): boolean {
314
- return (
315
- outputGuard(event, 'code') &&
316
- outputString(event, 'planningResult') !== undefined
317
- );
318
- }
319
-
320
- function isDecideThenCode({ event }: { event: unknown }): boolean {
321
- return (
322
- outputGuard(event, 'decideThenCode') &&
323
- outputString(event, 'planningResult') !== undefined
324
- );
325
- }
326
-
327
- // DR-050: the two pull-request outcomes select the same development paths
328
- // as `code` and `decide then code`, entered through `branch` first.
329
- function isCodeViaPullRequest({ event }: { event: unknown }): boolean {
330
- return (
331
- outputGuard(event, 'codeViaPullRequest') &&
332
- outputString(event, 'planningResult') !== undefined
333
- );
327
+ function plannedPath(
328
+ guard: 'code' | 'decideThenCode' | 'codeViaPullRequest' | 'decideThenCodeViaPullRequest',
329
+ ): (args: { event: unknown }) => boolean {
330
+ return ({ event }) =>
331
+ hasGuard(event, guard) && outputText(event, 'planningResult') !== undefined;
334
332
  }
335
333
 
336
- function isDecideThenCodeViaPullRequest({
334
+ // Discussion complete is available only after a Boss reply: the Analyst
335
+ // invocation that returns it must have been resumed with Boss's answer.
336
+ function discussionCompleteAfterBossReply({
337
+ context,
337
338
  event,
338
339
  }: {
340
+ context: DevContext;
339
341
  event: unknown;
340
342
  }): boolean {
341
343
  return (
342
- outputGuard(event, 'decideThenCodeViaPullRequest') &&
343
- outputString(event, 'planningResult') !== undefined
344
+ hasGuard(event, 'discussionComplete') &&
345
+ context.pendingBossQuestion !== undefined &&
346
+ isNonEmptyString(context.bossReply)
344
347
  );
345
348
  }
346
349
 
347
- // Discussion complete is available only after a Boss reply: the round that
348
- // produced it must itself have been resumed with Boss's answer.
349
- function isDiscussionComplete({
350
- context,
351
- event,
352
- }: {
353
- context: DevContext;
354
- event: unknown;
355
- }): boolean {
350
+ function needsBossReplyWithQuestion({ event }: { event: unknown }): boolean {
356
351
  return (
357
- outputGuard(event, 'discussionComplete') &&
358
- isNonEmptyString(context.bossReply)
352
+ hasGuard(event, 'needsBossReply') && outputText(event, 'question') !== undefined
359
353
  );
360
354
  }
361
355
 
362
- function needsBossReply({ event }: { event: unknown }): boolean {
356
+ function needsBossReplyWithoutQuestion({ event }: { event: unknown }): boolean {
363
357
  return (
364
- outputGuard(event, 'needsBossReply') &&
365
- outputString(event, 'question') !== undefined
358
+ hasGuard(event, 'needsBossReply') && outputText(event, 'question') === undefined
366
359
  );
367
360
  }
368
361
 
369
- function childOutputOf(event: unknown): unknown {
370
- return isRecord(event) ? event.output : undefined;
362
+ // ---------------------------------------------------------------------------
363
+ // Child outputs, read only from each child's canonical structured result and
364
+ // checked against the packaged public output interfaces.
365
+
366
+ function childOutputOf(event: unknown): PlaybookOutput {
367
+ return isRecord(event) ? (event.output as PlaybookOutput) : undefined;
371
368
  }
372
369
 
373
- function isDecideSuccessValue(value: unknown): value is DecideSuccessOutput {
374
- return (
375
- isRecord(value) &&
376
- isNonEmptyString(value.decideCommit) &&
377
- isNonEmptyString(value.evaluatedRevision) &&
378
- value.noUnsettledFindings === true
379
- );
370
+ function childErrorOf(event: unknown): unknown {
371
+ return isRecord(event) ? event.error : undefined;
380
372
  }
381
373
 
382
- function isDecideSuccess({ event }: { event: unknown }): boolean {
383
- return isDecideSuccessValue(childOutputOf(event));
374
+ interface BranchSuccess {
375
+ readonly branch: string;
376
+ readonly baseRevision: string;
377
+ readonly issueSummary: string;
384
378
  }
385
379
 
386
- // BRANCH's canonical successful terminal output carries the exact name of
387
- // the new checked-out branch, the exact receipt-owned base revision, and the
388
- // issue summary; DEV consumes exactly those three by name and never defaults
389
- // a missing one.
390
- function isBranchSuccessValue(value: unknown): value is BranchSuccessOutput {
391
- return (
392
- isRecord(value) &&
393
- isNonEmptyString(value.branch) &&
394
- isNonEmptyString(value.baseRevision) &&
395
- isNonEmptyString(value.issueSummary)
396
- );
380
+ function branchSuccessOf(value: unknown): BranchSuccess | undefined {
381
+ if (
382
+ !isRecord(value) ||
383
+ value.status !== 'branched' ||
384
+ !isNonEmptyString(value.branch) ||
385
+ !isNonEmptyString(value.baseRevision) ||
386
+ !isNonEmptyString(value.issueSummary)
387
+ ) {
388
+ return undefined;
389
+ }
390
+ return {
391
+ branch: value.branch,
392
+ baseRevision: value.baseRevision,
393
+ issueSummary: value.issueSummary,
394
+ };
397
395
  }
398
396
 
399
- function isBranchSuccessForCode({
400
- context,
401
- event,
402
- }: {
403
- context: DevContext;
404
- event: unknown;
405
- }): boolean {
406
- return (
407
- context.pullRequestPath === 'code' &&
408
- isBranchSuccessValue(childOutputOf(event))
409
- );
397
+ interface DecideSuccess {
398
+ readonly decideCommit: string;
399
+ readonly evaluatedRevision: string;
410
400
  }
411
401
 
412
- function isBranchSuccessForDecide({
413
- context,
414
- event,
415
- }: {
416
- context: DevContext;
417
- event: unknown;
418
- }): boolean {
419
- return (
420
- context.pullRequestPath === 'decide-then-code' &&
421
- isBranchSuccessValue(childOutputOf(event))
422
- );
402
+ function decideSuccessOf(value: unknown): DecideSuccess | undefined {
403
+ if (
404
+ !isRecord(value) ||
405
+ value.noUnsettledFindings !== true ||
406
+ !isNonEmptyString(value.decideCommit) ||
407
+ !isNonEmptyString(value.evaluatedRevision)
408
+ ) {
409
+ return undefined;
410
+ }
411
+ return {
412
+ decideCommit: value.decideCommit,
413
+ evaluatedRevision: value.evaluatedRevision,
414
+ };
423
415
  }
424
416
 
425
- // CODE's canonical successful terminal output discriminates completion with
426
- // `status: 'complete'`, carries the exact last CODE-owned commit, and
427
- // affirms that every phase's review passed.
428
- function isCodeSuccessValue(value: unknown): boolean {
429
- return (
430
- isRecord(value) &&
431
- value.status === 'complete' &&
432
- isNonEmptyString(value.lastCodeCommit) &&
433
- value.allReviewsPassed === true
434
- );
417
+ interface CodeSuccess {
418
+ readonly lastCodeCommit: string;
419
+ readonly finalEvaluatedRevision: string;
420
+ }
421
+
422
+ function codeSuccessOf(value: unknown): CodeSuccess | undefined {
423
+ if (
424
+ !isRecord(value) ||
425
+ value.status !== 'complete' ||
426
+ value.allReviewsPassed !== true ||
427
+ !isNonEmptyString(value.lastCodeCommit) ||
428
+ !isNonEmptyString(value.finalEvaluatedRevision)
429
+ ) {
430
+ return undefined;
431
+ }
432
+ return {
433
+ lastCodeCommit: value.lastCodeCommit,
434
+ finalEvaluatedRevision: value.finalEvaluatedRevision,
435
+ };
436
+ }
437
+
438
+ function branchSucceededFor(
439
+ path: DevPullRequestPath,
440
+ ): (args: { context: DevContext; event: unknown }) => boolean {
441
+ return ({ context, event }) =>
442
+ context.pullRequestPath === path &&
443
+ branchSuccessOf(childOutputOf(event)) !== undefined;
435
444
  }
436
445
 
437
- // A plain path completes DEV on that proof alone, exactly as before DR-050.
438
- function isPlainCodeSuccess({
446
+ function decideSucceeded({ event }: { event: unknown }): boolean {
447
+ return decideSuccessOf(childOutputOf(event)) !== undefined;
448
+ }
449
+
450
+ function codeSucceededOnPullRequestPath({
439
451
  context,
440
452
  event,
441
453
  }: {
@@ -443,25 +455,12 @@ function isPlainCodeSuccess({
443
455
  event: unknown;
444
456
  }): boolean {
445
457
  return (
446
- context.deliveryViaPullRequest !== true &&
447
- isCodeSuccessValue(childOutputOf(event))
448
- );
449
- }
450
-
451
- // A pull-request path additionally consumes the exact final evaluated
452
- // repository revision for the `pr` call, so a CODE result lacking it does not
453
- // prove the success that path requires.
454
- function isCodeSuccessValueForPullRequest(
455
- value: unknown,
456
- ): value is CodeSuccessOutput & Record<string, unknown> {
457
- return (
458
- isCodeSuccessValue(value) &&
459
- isRecord(value) &&
460
- isNonEmptyString(value.finalEvaluatedRevision)
458
+ context.pullRequestPath !== undefined &&
459
+ codeSuccessOf(childOutputOf(event)) !== undefined
461
460
  );
462
461
  }
463
462
 
464
- function isCodeSuccessViaPullRequest({
463
+ function codeSucceededOnPlainPath({
465
464
  context,
466
465
  event,
467
466
  }: {
@@ -469,249 +468,62 @@ function isCodeSuccessViaPullRequest({
469
468
  event: unknown;
470
469
  }): boolean {
471
470
  return (
472
- context.deliveryViaPullRequest === true &&
473
- isCodeSuccessValueForPullRequest(childOutputOf(event))
474
- );
475
- }
476
-
477
- function isStateValue(value: unknown, ancestors = new Set<object>()): boolean {
478
- if (typeof value === 'string') return true;
479
- if (!isRecord(value) || ancestors.has(value)) return false;
480
- const next = new Set(ancestors).add(value);
481
- return Reflect.ownKeys(value).every(
482
- (key) =>
483
- typeof key === 'string' &&
484
- Object.prototype.propertyIsEnumerable.call(value, key) &&
485
- isStateValue(value[key], next),
486
- );
487
- }
488
-
489
- function isStringArray(value: unknown): value is readonly string[] {
490
- return (
491
- Array.isArray(value) &&
492
- Reflect.ownKeys(value).length === value.length + 1 &&
493
- Reflect.ownKeys(value).every((key) => {
494
- if (key === 'length') return true;
495
- return (
496
- typeof key === 'string' &&
497
- /^(0|[1-9][0-9]*)$/.test(key) &&
498
- Number(key) < value.length
499
- );
500
- }) &&
501
- value.every((entry) => isNonEmptyString(entry)) &&
502
- new Set(value).size === value.length
471
+ context.pullRequestPath === undefined &&
472
+ codeSuccessOf(childOutputOf(event)) !== undefined
503
473
  );
504
474
  }
505
475
 
506
- function isPlaybookState(value: unknown): boolean {
507
- if (!isRecord(value)) return false;
508
- const allowed = new Set([
509
- 'value',
510
- 'activeStateIds',
511
- 'tags',
512
- 'status',
513
- 'quiescent',
514
- 'stateId',
515
- ]);
516
- if (
517
- Reflect.ownKeys(value).some(
518
- (key) => typeof key !== 'string' || !allowed.has(key),
519
- ) ||
520
- !Object.prototype.hasOwnProperty.call(value, 'value') ||
521
- !Object.prototype.hasOwnProperty.call(value, 'activeStateIds') ||
522
- !Object.prototype.hasOwnProperty.call(value, 'tags') ||
523
- !Object.prototype.hasOwnProperty.call(value, 'status') ||
524
- !Object.prototype.hasOwnProperty.call(value, 'quiescent')
525
- ) {
526
- return false;
527
- }
528
- return (
529
- isStateValue(value.value) &&
530
- isStringArray(value.activeStateIds) &&
531
- isStringArray(value.tags) &&
532
- (value.status === 'active' ||
533
- value.status === 'done' ||
534
- value.status === 'error' ||
535
- value.status === 'stopped') &&
536
- typeof value.quiescent === 'boolean' &&
537
- (!Object.prototype.hasOwnProperty.call(value, 'stateId') ||
538
- (isNonEmptyString(value.stateId) &&
539
- value.activeStateIds.length === 1 &&
540
- value.activeStateIds[0] === value.stateId))
541
- );
542
- }
476
+ // ---------------------------------------------------------------------------
477
+ // Authored rejected child results.
543
478
 
544
- function nestedResultFromError(
479
+ export function authoredChildResult(
545
480
  error: unknown,
546
- ): Record<string, unknown> | undefined {
481
+ expectedPlaybookId: string,
482
+ ): PlaybookCallResult | undefined {
547
483
  if (!(error instanceof Error)) return undefined;
548
- const result = (error as Error & { readonly result?: unknown }).result;
549
- return isRecord(result) ? result : undefined;
550
- }
551
-
552
- type AuthoredChildFailure =
553
- | {
554
- readonly status: 'aborted' | 'error';
555
- readonly error?: CompactError;
556
- }
557
- // DR-048: a child that completed at an authored failure terminal. The
558
- // bridge rejects it through the same error path, so DEV recognizes the
559
- // failure from the child's own machine and never from its output fields.
560
- | {
561
- readonly status: 'ok';
562
- readonly output?: JsonValue;
563
- };
564
-
565
- function isFailureTerminalRecord(value: unknown): boolean {
566
- if (!isRecord(value)) return false;
567
- const allowed = new Set(['stateId', 'kind', 'description']);
568
- return (
569
- Reflect.ownKeys(value).every(
570
- (key) => typeof key === 'string' && allowed.has(key),
571
- ) &&
572
- isNonEmptyString(value.stateId) &&
573
- value.kind === 'failure' &&
574
- (!Object.prototype.hasOwnProperty.call(value, 'description') ||
575
- typeof value.description === 'string')
576
- );
577
- }
578
-
579
- function normalizedChildFailure(
580
- error: unknown,
581
- playbookId: DevChildPlaybookId,
582
- ): AuthoredChildFailure | undefined {
583
- const result = nestedResultFromError(error);
584
- if (result === undefined) return undefined;
585
- const allowed = new Set([
586
- 'status',
587
- 'playbookId',
588
- 'childSessionId',
589
- 'state',
590
- ...(result.status === 'ok' ? ['output', 'terminal'] : ['error']),
591
- ]);
592
- if (
593
- Reflect.ownKeys(result).some(
594
- (key) => typeof key !== 'string' || !allowed.has(key),
595
- ) ||
596
- (result.status !== 'aborted' &&
597
- result.status !== 'error' &&
598
- result.status !== 'ok') ||
599
- result.playbookId !== playbookId
600
- ) {
601
- return undefined;
602
- }
603
- if (
604
- Object.prototype.hasOwnProperty.call(result, 'childSessionId') &&
605
- !isNonEmptyString(result.childSessionId)
606
- ) {
607
- return undefined;
608
- }
609
- if (result.status === 'ok') {
610
- if (
611
- !Object.prototype.hasOwnProperty.call(result, 'terminal') ||
612
- !isFailureTerminalRecord(result.terminal) ||
613
- !isNonEmptyString(result.childSessionId) ||
614
- (Object.prototype.hasOwnProperty.call(result, 'state') &&
615
- !isPlaybookState(result.state))
616
- ) {
617
- return undefined;
618
- }
619
- return {
620
- status: 'ok',
621
- ...(result.output === undefined
622
- ? {}
623
- : { output: result.output as JsonValue }),
624
- };
625
- }
626
- if (
627
- Object.prototype.hasOwnProperty.call(result, 'state') &&
628
- !isPlaybookState(result.state)
629
- ) {
630
- return undefined;
631
- }
632
- let normalizedError: CompactError | undefined;
633
- if (Object.prototype.hasOwnProperty.call(result, 'error')) {
634
- if (!isRecord(result.error)) return undefined;
635
- const errorKeys = Reflect.ownKeys(result.error);
636
- if (
637
- errorKeys.some(
638
- (key) =>
639
- typeof key !== 'string' ||
640
- (key !== 'name' && key !== 'message' && key !== 'stack'),
641
- ) ||
642
- !Object.prototype.hasOwnProperty.call(result.error, 'name') ||
643
- !Object.prototype.hasOwnProperty.call(result.error, 'message') ||
644
- !isNonEmptyString(result.error.name) ||
645
- typeof result.error.message !== 'string' ||
646
- (Object.prototype.hasOwnProperty.call(result.error, 'stack') &&
647
- typeof result.error.stack !== 'string')
648
- ) {
649
- return undefined;
650
- }
651
- normalizedError = {
652
- name: result.error.name,
653
- message: result.error.message,
654
- };
655
- } else if (result.status === 'error') {
484
+ try {
485
+ const result = validatePlaybookCallResult(
486
+ (error as Error & { result?: unknown }).result,
487
+ expectedPlaybookId,
488
+ );
489
+ return result.status !== 'ok' || result.terminal?.kind === 'failure'
490
+ ? result
491
+ : undefined;
492
+ } catch {
656
493
  return undefined;
657
494
  }
658
- return {
659
- status: result.status,
660
- ...(normalizedError === undefined ? {} : { error: normalizedError }),
661
- };
662
495
  }
663
496
 
664
- function authoredChildFailureGuard(
497
+ function authoredResultFrom(
665
498
  playbookId: DevChildPlaybookId,
666
499
  ): (args: { event: unknown }) => boolean {
667
- return ({ event }) => {
668
- const error = isRecord(event) ? event.error : undefined;
669
- return normalizedChildFailure(error, playbookId) !== undefined;
670
- };
671
- }
672
-
673
- function authoredChildError(
674
- event: unknown,
675
- playbookId: DevChildPlaybookId,
676
- ): CompactError {
677
- const outer = isRecord(event) ? event.error : undefined;
678
- const failure = normalizedChildFailure(outer, playbookId);
679
- if (failure !== undefined && failure.status !== 'ok' && failure.error) {
680
- return failure.error;
681
- }
682
- if (failure?.status === 'aborted') {
683
- return {
684
- name: 'AbortError',
685
- message: `${playbookId.toUpperCase()} was aborted.`,
686
- };
687
- }
688
- if (outer instanceof Error) {
689
- return { name: outer.name || 'Error', message: outer.message };
690
- }
691
- return { name: 'Error', message: String(outer) };
500
+ return ({ event }) =>
501
+ authoredChildResult(childErrorOf(event), playbookId) !== undefined;
692
502
  }
693
503
 
694
- function relayedChildFailure(
504
+ function relayedChildResult(
695
505
  event: unknown,
696
506
  playbookId: DevChildPlaybookId,
697
- ): CompletedChildResult {
698
- const outer = isRecord(event) ? event.error : undefined;
699
- const failure = normalizedChildFailure(outer, playbookId);
700
- // DR-048: a completed child that reached an authored failure terminal
701
- // relays its own output, exactly as a terminal result that did not prove
702
- // success did before it was routed through the error path.
703
- if (failure?.status === 'ok') {
507
+ ): CompletedChildResult | undefined {
508
+ const result = authoredChildResult(childErrorOf(event), playbookId);
509
+ if (result === undefined) return undefined;
510
+ if (result.status === 'ok') {
511
+ // A child that completed at its own authored failure terminal relays its
512
+ // own output; an absent output is omitted rather than stored.
704
513
  return {
705
514
  playbookId,
706
515
  status: 'ok',
707
- ...(failure.output === undefined ? {} : { output: failure.output }),
516
+ ...(result.output === undefined ? {} : { output: result.output }),
708
517
  };
709
518
  }
710
- return {
711
- playbookId,
712
- status: failure?.status === 'aborted' ? 'aborted' : 'error',
713
- error: authoredChildError(event, playbookId),
714
- };
519
+ const error: CompactError =
520
+ result.error === undefined
521
+ ? {
522
+ name: 'AbortError',
523
+ message: `The ${playbookId} playbook call was aborted.`,
524
+ }
525
+ : { name: result.error.name, message: result.error.message };
526
+ return { playbookId, status: result.status, error };
715
527
  }
716
528
 
717
529
  function insufficientChildResult(
@@ -722,106 +534,140 @@ function insufficientChildResult(
722
534
  return {
723
535
  playbookId,
724
536
  status: 'ok',
725
- ...(output === undefined ? {} : { output: output as JsonValue }),
537
+ ...(output === undefined ? {} : { output }),
726
538
  };
727
539
  }
728
540
 
729
- /** Consumed planning Q&A rendered for later relayed discussion context. */
730
- export function renderDiscussionContext(
731
- exchanges: readonly DiscussionExchange[],
732
- ): string {
733
- return exchanges
734
- .map(
735
- (exchange) =>
736
- `Analyst question: ${exchange.question}\nBoss reply: ${exchange.answer}`,
737
- )
738
- .join('\n\n');
541
+ function normalizedControlError(error: unknown): ControlError {
542
+ if (error instanceof Error) {
543
+ return {
544
+ name: error.name.length > 0 ? error.name : 'Error',
545
+ message: error.message,
546
+ ...(typeof error.stack === 'string' ? { stack: error.stack } : {}),
547
+ };
548
+ }
549
+ return {
550
+ name: 'Error',
551
+ message: typeof error === 'string' ? error : 'Actor failed with a non-Error value.',
552
+ };
739
553
  }
740
554
 
741
- function archivedExchanges(
742
- context: DevContext,
743
- ): readonly DiscussionExchange[] {
555
+ // ---------------------------------------------------------------------------
556
+ // Discussion history and quoted-relay composition.
557
+
558
+ /** One consumed Analyst question and Boss reply, as relayed discussion. */
559
+ export function renderDiscussionExchange(question: string, answer: string): string {
560
+ return `Analyst question: ${question}\nBoss reply: ${answer}`;
561
+ }
562
+
563
+ // Persists the answered pending Q&A pair into the typed discussion context
564
+ // before the pending fields are replaced or cleared, so later calls relay it.
565
+ function archivedDiscussion(context: DevContext): string {
744
566
  if (
745
567
  context.pendingBossQuestion === undefined ||
746
568
  !isNonEmptyString(context.bossReply)
747
569
  ) {
748
- return context.discussionExchanges;
570
+ return context.discussionContext;
749
571
  }
750
- return [
751
- ...context.discussionExchanges,
752
- {
753
- question: context.pendingBossQuestion.question,
754
- answer: context.bossReply,
755
- },
756
- ];
572
+ const exchange = renderDiscussionExchange(
573
+ context.pendingBossQuestion.question,
574
+ context.bossReply,
575
+ );
576
+ return context.discussionContext.length > 0
577
+ ? `${context.discussionContext}\n\n${exchange}`
578
+ : exchange;
757
579
  }
758
580
 
759
- function quotedRelay(values: readonly (readonly [string, string])[]): string {
760
- return values
761
- .filter(([, value]) => value.length > 0)
762
- .map(([label, value]) =>
763
- `${label}: ${value}`
764
- .split('\n')
765
- .map((line) => `> ${line}`)
766
- .join('\n'),
767
- )
768
- .join('\n');
769
- }
581
+ const RELAY_TOKEN = /<[a-z][a-z0-9-]*>/g;
770
582
 
771
- function planningRelayValues(context: DevContext): readonly (readonly [string, string])[] {
772
- return [
773
- ['Original request', context.developmentRequest ?? ''],
774
- ['Prior discussion', renderDiscussionContext(context.discussionExchanges)],
775
- ['Planning result', context.planningResult ?? ''],
776
- ];
583
+ // Quoted-relay composition: an empty value contributes no line, every
584
+ // continuation line of a multi-line value is quoted with `> ` (keeping LF/CRLF
585
+ // separators), and no quoted line is inserted. Substitution is single-pass and
586
+ // literal; a missing value keeps its placeholder literal.
587
+ function composeRelay(
588
+ template: readonly string[],
589
+ values: Readonly<Record<string, string | undefined>>,
590
+ ): string {
591
+ const lines: string[] = [];
592
+ for (const line of template) {
593
+ const tokens = line.match(RELAY_TOKEN) ?? [];
594
+ if (tokens.some((token) => values[token] === '')) continue;
595
+ const composed = line.replace(RELAY_TOKEN, (token) => {
596
+ const value = values[token];
597
+ return value === undefined
598
+ ? token
599
+ : value.replace(/\r?\n/g, (separator) => `${separator}> `);
600
+ });
601
+ lines.push(composed);
602
+ }
603
+ return lines.join('\n');
777
604
  }
778
605
 
779
- function codeCallText(context: DevContext): string {
780
- return quotedRelay(planningRelayValues(context));
606
+ function plannedCallValues(
607
+ context: DevContext,
608
+ ): Readonly<Record<string, string | undefined>> {
609
+ return {
610
+ '<development-request>': context.developmentRequest,
611
+ '<discussion-context>': context.discussionContext,
612
+ '<planning-result>': context.planningResult,
613
+ };
781
614
  }
782
615
 
783
- function decideCallText(context: DevContext): string {
784
- return quotedRelay(planningRelayValues(context));
616
+ function plannedCallText(context: DevContext): string {
617
+ return composeRelay(PLANNED_CALL_TEMPLATE, plannedCallValues(context));
785
618
  }
786
619
 
787
620
  function codeAfterDecideCallText(context: DevContext): string {
788
- return quotedRelay([
789
- ...planningRelayValues(context),
790
- ['DECIDE commit', context.decideCommit ?? ''],
791
- ['Evaluated revision', context.evaluatedRevision ?? ''],
792
- ]);
621
+ return composeRelay(CODE_AFTER_DECIDE_CALL_TEMPLATE, {
622
+ ...plannedCallValues(context),
623
+ '<decide-commit>': context.decideCommit,
624
+ '<evaluated-revision>': context.evaluatedRevision,
625
+ });
793
626
  }
794
627
 
795
- function branchCallText(context: DevContext): string {
796
- return quotedRelay(planningRelayValues(context));
628
+ function pullRequestCallText(context: DevContext): string {
629
+ return composeRelay(PULL_REQUEST_CALL_TEMPLATE, {
630
+ '<development-request>': context.developmentRequest,
631
+ '<issue-summary>': context.issueSummary,
632
+ '<branch>': context.branch,
633
+ '<base-revision>': context.baseRevision,
634
+ '<last-code-commit>': context.lastCodeCommit,
635
+ '<final-evaluated-revision>': context.finalEvaluatedRevision,
636
+ });
797
637
  }
798
638
 
799
- // DEV-6: every placeholder is substituted from typed context that only a
800
- // child's canonical structured result populated — never from player prose.
801
- function pullRequestCallText(context: DevContext): string {
802
- return quotedRelay([
803
- ['Original request', context.developmentRequest ?? ''],
804
- ['Issue summary', context.issueSummary ?? ''],
805
- ['Branch', context.branch ?? ''],
806
- ['Base revision', context.baseRevision ?? ''],
807
- ['CODE commit', context.lastCodeCommit ?? ''],
808
- ['Evaluated revision', context.finalEvaluatedRevision ?? ''],
809
- ]);
639
+ // ---------------------------------------------------------------------------
640
+ // Boss entry and reply surfaces.
641
+
642
+ function startsDev({ event }: { event: DevEvent }): boolean {
643
+ return (
644
+ event.type === 'START_DEV' &&
645
+ typeof event.developmentRequest === 'string' &&
646
+ event.developmentRequest.trim().length > 0
647
+ );
810
648
  }
811
649
 
812
- function acceptedPlanningPath(
813
- context: DevContext,
814
- event: unknown,
815
- pullRequestPath?: DevPullRequestPath,
650
+ function emptyBossReply({ event }: { event: DevEvent }): boolean {
651
+ return (
652
+ event.type === 'BOSS_REPLY' &&
653
+ (typeof event.answer !== 'string' || event.answer.trim().length === 0)
654
+ );
655
+ }
656
+
657
+ function resumableStates<const Id extends ResumableStateId>(
658
+ ids: readonly Id[],
816
659
  ) {
817
- return {
818
- planningResult: outputString(event, 'planningResult'),
819
- discussionExchanges: archivedExchanges(context),
820
- deliveryViaPullRequest: pullRequestPath !== undefined,
821
- pullRequestPath,
822
- pendingBossQuestion: undefined,
823
- bossReply: undefined,
824
- };
660
+ return ids.map((id) => ({
661
+ guard: ({ context, event }: { context: DevContext; event: DevEvent }) =>
662
+ event.type === 'BOSS_REPLY' &&
663
+ typeof event.answer === 'string' &&
664
+ event.answer.trim().length > 0 &&
665
+ context.pendingBossQuestion?.resumeStateId === id &&
666
+ (event.questionId === undefined || event.questionId === id),
667
+ target: `#${id}` as const,
668
+ reenter: true as const,
669
+ actions: 'rememberBossReply' as const,
670
+ }));
825
671
  }
826
672
 
827
673
  const machineSetup = setup({
@@ -835,33 +681,31 @@ const machineSetup = setup({
835
681
  player: fromPromise<PlayerOutput, PlayerInput>(async () => {
836
682
  throw new Error('player actor must be provided by the runner');
837
683
  }),
838
- playbook: fromPromise<JsonValue | undefined, PlaybookInput>(async () => {
684
+ playbook: fromPromise<PlaybookOutput, PlaybookInput>(async () => {
839
685
  throw new Error('playbook actor must be provided by the runner');
840
686
  }),
841
687
  },
842
688
  guards: {
843
- isDiscussionComplete,
844
- isCodePath,
845
- isDecideThenCode,
846
- isCodeViaPullRequest,
847
- isDecideThenCodeViaPullRequest,
848
- needsBossReply,
849
- isBranchSuccessForCode,
850
- isBranchSuccessForDecide,
851
- isCodeSuccessViaPullRequest,
852
- isPlainCodeSuccess,
853
- isDecideSuccess,
854
- authoredBranchFailure: authoredChildFailureGuard('branch'),
855
- authoredCodeFailure: authoredChildFailureGuard('code'),
856
- authoredDecideFailure: authoredChildFailureGuard('decide'),
857
- authoredPrFailure: authoredChildFailureGuard('pr'),
858
- emptyBossReply: ({ event }) =>
859
- event.type === 'BOSS_REPLY' && event.answer.trim().length === 0,
860
- resumesPlanAnalysis: ({ context, event }) =>
861
- event.type === 'BOSS_REPLY' &&
862
- event.answer.trim().length > 0 &&
863
- context.pendingBossQuestion?.resumeStateId === 'planAnalysis' &&
864
- (event.questionId === undefined || event.questionId === 'planAnalysis'),
689
+ startsDev,
690
+ emptyBossReply,
691
+ discussionCompleteAfterBossReply,
692
+ codePlanned: plannedPath('code'),
693
+ decideThenCodePlanned: plannedPath('decideThenCode'),
694
+ codeViaPullRequestPlanned: plannedPath('codeViaPullRequest'),
695
+ decideThenCodeViaPullRequestPlanned: plannedPath(
696
+ 'decideThenCodeViaPullRequest',
697
+ ),
698
+ needsBossReplyWithQuestion,
699
+ needsBossReplyWithoutQuestion,
700
+ branchSucceededForCode: branchSucceededFor('code'),
701
+ branchSucceededForDecide: branchSucceededFor('decide-then-code'),
702
+ decideSucceeded,
703
+ codeSucceededOnPullRequestPath,
704
+ codeSucceededOnPlainPath,
705
+ authoredBranchResult: authoredResultFrom('branch'),
706
+ authoredDecideResult: authoredResultFrom('decide'),
707
+ authoredCodeResult: authoredResultFrom('code'),
708
+ authoredPrResult: authoredResultFrom('pr'),
865
709
  },
866
710
  actions: {
867
711
  'playbook.acceptedOutcome': (
@@ -872,173 +716,154 @@ const machineSetup = setup({
872
716
  readonly acceptedOutcome: string;
873
717
  },
874
718
  ) => undefined,
875
- startDev: assign(({ context, event }) => {
876
- if (event.type !== 'START_DEV') return {};
877
- return {
878
- runResults: context.runResults,
879
- developmentRequest: event.developmentRequest,
880
- discussionExchanges: [] as readonly DiscussionExchange[],
881
- planningResult: undefined,
882
- decideCommit: undefined,
883
- evaluatedRevision: undefined,
884
- deliveryViaPullRequest: false,
885
- pullRequestPath: undefined,
886
- branch: undefined,
887
- baseRevision: undefined,
888
- issueSummary: undefined,
889
- lastCodeCommit: undefined,
890
- finalEvaluatedRevision: undefined,
891
- completion: undefined,
892
- childOutput: undefined,
893
- childFailure: undefined,
894
- lastError: undefined,
895
- pendingBossQuestion: undefined,
896
- bossReply: undefined,
897
- };
898
- }),
719
+ startDev: assign(({ event }) => ({
720
+ developmentRequest:
721
+ event.type === 'START_DEV' ? event.developmentRequest : undefined,
722
+ discussionContext: '',
723
+ planningResult: undefined,
724
+ pullRequestPath: undefined,
725
+ decideCommit: undefined,
726
+ evaluatedRevision: undefined,
727
+ branch: undefined,
728
+ baseRevision: undefined,
729
+ issueSummary: undefined,
730
+ lastCodeCommit: undefined,
731
+ finalEvaluatedRevision: undefined,
732
+ completion: undefined,
733
+ lastError: undefined,
734
+ pendingBossQuestion: undefined,
735
+ bossReply: undefined,
736
+ })),
899
737
  completeDiscussion: assign(({ context }) => ({
900
- discussionExchanges: archivedExchanges(context),
901
- completion: 'discussion-complete' as const,
738
+ discussionContext: archivedDiscussion(context),
739
+ completion: { kind: 'discussion-complete' } as const,
902
740
  pendingBossQuestion: undefined,
903
741
  bossReply: undefined,
904
742
  })),
905
- rememberCodePath: assign(({ context, event }) =>
906
- acceptedPlanningPath(context, event),
907
- ),
908
- rememberDecidePath: assign(({ context, event }) =>
909
- acceptedPlanningPath(context, event),
910
- ),
911
- rememberCodeViaPullRequestPath: assign(({ context, event }) =>
912
- acceptedPlanningPath(context, event, 'code'),
913
- ),
914
- rememberDecideThenCodeViaPullRequestPath: assign(({ context, event }) =>
915
- acceptedPlanningPath(context, event, 'decide-then-code'),
743
+ acceptPlainPlan: assign(({ context, event }) => ({
744
+ planningResult: outputText(event, 'planningResult'),
745
+ discussionContext: archivedDiscussion(context),
746
+ pendingBossQuestion: undefined,
747
+ bossReply: undefined,
748
+ })),
749
+ acceptPullRequestPlan: assign(
750
+ ({ context, event }, params: { readonly path: DevPullRequestPath }) => ({
751
+ planningResult: outputText(event, 'planningResult'),
752
+ pullRequestPath: params.path,
753
+ discussionContext: archivedDiscussion(context),
754
+ pendingBossQuestion: undefined,
755
+ bossReply: undefined,
756
+ }),
916
757
  ),
917
- rememberPendingQuestion: assign(({ context, event }) => {
918
- const question = outputString(event, 'question');
919
- if (question === undefined || !outputGuard(event, 'needsBossReply')) {
920
- return { lastError: new Error('Analyst question output was malformed') };
921
- }
922
- return {
923
- discussionExchanges: archivedExchanges(context),
758
+ setPendingBossQuestion: assign(
759
+ ({ context, event }, params: PendingBossQuestionParams) => ({
760
+ discussionContext: archivedDiscussion(context),
924
761
  pendingBossQuestion: {
925
- questionId: 'planAnalysis' as const,
926
- resumeStateId: 'planAnalysis' as const,
927
- sourceItem: 'DEV-1' as const,
928
- asker: { kind: 'role' as const, roleId: 'analyst' as const },
929
- question,
762
+ ...params,
763
+ questionId: params.resumeStateId,
764
+ question: outputText(event, 'question') ?? '',
930
765
  },
931
766
  bossReply: undefined,
932
- };
933
- }),
767
+ }),
768
+ ),
934
769
  rememberBossReply: assign(({ event }) =>
935
770
  event.type === 'BOSS_REPLY' ? { bossReply: event.answer } : {},
936
771
  ),
937
- rememberEmptyBossReplyError: assign({
938
- lastError: () => new Error('BOSS_REPLY received an empty answer'),
772
+ clearBossReplyContext: assign({
939
773
  pendingBossQuestion: undefined,
940
774
  bossReply: undefined,
941
775
  }),
942
776
  rememberBranchResult: assign(({ event }) => {
943
- const output = childOutputOf(event);
944
- if (!isBranchSuccessValue(output)) return {};
945
- return {
946
- branch: output.branch,
947
- baseRevision: output.baseRevision,
948
- issueSummary: output.issueSummary,
949
- };
777
+ const success = branchSuccessOf(childOutputOf(event));
778
+ return success === undefined ? {} : { ...success };
950
779
  }),
951
780
  rememberDecideResult: assign(({ event }) => {
952
- const output = childOutputOf(event);
953
- if (!isDecideSuccessValue(output)) return {};
954
- return {
955
- decideCommit: output.decideCommit,
956
- evaluatedRevision: output.evaluatedRevision,
957
- };
781
+ const success = decideSuccessOf(childOutputOf(event));
782
+ return success === undefined ? {} : { ...success };
958
783
  }),
959
784
  rememberCodeResult: assign(({ event }) => {
960
- const output = childOutputOf(event);
961
- if (!isCodeSuccessValueForPullRequest(output)) return {};
962
- return {
963
- lastCodeCommit: output.lastCodeCommit,
964
- finalEvaluatedRevision: output.finalEvaluatedRevision,
965
- };
785
+ const success = codeSuccessOf(childOutputOf(event));
786
+ return success === undefined ? {} : { ...success };
966
787
  }),
967
- completeWithChildSuccess: assign(({ event }) => {
968
- const output = childOutputOf(event);
969
- return {
970
- completion: 'complete' as const,
971
- ...(output === undefined ? {} : { childOutput: output as JsonValue }),
972
- };
973
- }),
974
- completeWithInsufficientBranchResult: assign(({ event }) => ({
975
- completion: 'child-failed' as const,
976
- childFailure: insufficientChildResult(event, 'branch'),
977
- })),
978
- completeWithInsufficientCodeResult: assign(({ event }) => ({
979
- completion: 'child-failed' as const,
980
- childFailure: insufficientChildResult(event, 'code'),
981
- })),
982
- completeWithInsufficientDecideResult: assign(({ event }) => ({
983
- completion: 'child-failed' as const,
984
- childFailure: insufficientChildResult(event, 'decide'),
985
- })),
986
- completeWithBranchFailure: assign(({ event }) => ({
987
- completion: 'child-failed' as const,
988
- childFailure: relayedChildFailure(event, 'branch'),
989
- })),
990
- completeWithCodeFailure: assign(({ event }) => ({
991
- completion: 'child-failed' as const,
992
- childFailure: relayedChildFailure(event, 'code'),
993
- })),
994
- completeWithDecideFailure: assign(({ event }) => ({
995
- completion: 'child-failed' as const,
996
- childFailure: relayedChildFailure(event, 'decide'),
997
- })),
998
- completeWithPrFailure: assign(({ event }) => ({
999
- completion: 'child-failed' as const,
1000
- childFailure: relayedChildFailure(event, 'pr'),
1001
- })),
788
+ completeWithChildSuccess: assign(
789
+ ({ event }, params: { readonly childPlaybookId: 'code' | 'pr' }) => {
790
+ const output = childOutputOf(event);
791
+ return {
792
+ completion: {
793
+ kind: 'complete',
794
+ childPlaybookId: params.childPlaybookId,
795
+ ...(output === undefined ? {} : { childOutput: output }),
796
+ } as const,
797
+ };
798
+ },
799
+ ),
800
+ completeWithInsufficientResult: assign(
801
+ ({ event }, params: { readonly playbookId: DevChildPlaybookId }) => ({
802
+ completion: {
803
+ kind: 'child-failed',
804
+ childResult: insufficientChildResult(event, params.playbookId),
805
+ } as const,
806
+ }),
807
+ ),
808
+ completeWithAuthoredChildResult: assign(
809
+ ({ event }, params: { readonly playbookId: DevChildPlaybookId }) => {
810
+ const childResult = relayedChildResult(event, params.playbookId);
811
+ return childResult === undefined
812
+ ? {}
813
+ : { completion: { kind: 'child-failed', childResult } as const };
814
+ },
815
+ ),
1002
816
  rememberActorError: assign(({ event }) => ({
1003
- lastError: (event as unknown as { readonly error?: unknown }).error,
817
+ lastError: normalizedControlError(childErrorOf(event)),
1004
818
  })),
1005
819
  rememberMalformedPlayerOutput: assign({
1006
- lastError: () =>
1007
- new Error('Analyst result for DEV-1 did not match an available outcome.'),
820
+ lastError: {
821
+ name: 'MalformedActorOutput',
822
+ message:
823
+ 'Analyst output for DEV-1 did not match an available declared result.',
824
+ },
825
+ pendingBossQuestion: undefined,
826
+ bossReply: undefined,
827
+ }),
828
+ rememberMalformedBossReply: assign({
829
+ lastError: {
830
+ name: 'MalformedBossReply',
831
+ message: 'BOSS_REPLY carried an empty or whitespace-only answer.',
832
+ },
833
+ pendingBossQuestion: undefined,
834
+ bossReply: undefined,
1008
835
  }),
1009
836
  },
1010
837
  });
1011
838
 
1012
839
  export const devMachine = machineSetup.createMachine({
1013
840
  id: 'dev',
841
+ description:
842
+ 'Plans the development path for a request and runs the child playbooks it selects.',
1014
843
  initial: 'ready',
1015
- context: ({ input }) => ({
1016
- runResults: typeof input.runResults === 'string' ? input.runResults : '',
1017
- discussionExchanges: [],
1018
- deliveryViaPullRequest: false,
844
+ context: ({ input }): DevContext => ({
845
+ runResults: typeof input?.runResults === 'string' ? input.runResults : '',
846
+ discussionContext: '',
1019
847
  }),
1020
848
  output: ({ context }): DevPlaybookOutput => {
1021
- if (context.completion === 'discussion-complete') {
1022
- return { status: 'discussion-complete' };
849
+ const completion = context.completion;
850
+ if (completion === undefined) {
851
+ throw new Error('DEV reached a final state without a recorded completion');
1023
852
  }
1024
- if (context.completion === 'complete') {
1025
- return {
1026
- status: 'complete',
1027
- // `done` is entered by `pr` on a pull-request path and by `code` on a
1028
- // plain path; no other arm completes the workflow.
1029
- childPlaybookId: context.deliveryViaPullRequest ? 'pr' : 'code',
1030
- ...(context.childOutput === undefined
1031
- ? {}
1032
- : { childOutput: context.childOutput }),
1033
- };
1034
- }
1035
- if (context.completion === 'child-failed') {
1036
- if (context.childFailure === undefined) {
1037
- throw new Error('DEV reached a final state without a child result');
1038
- }
1039
- return { status: 'child-failed', childResult: context.childFailure };
853
+ switch (completion.kind) {
854
+ case 'discussion-complete':
855
+ return { status: 'discussion-complete' };
856
+ case 'complete':
857
+ return {
858
+ status: 'complete',
859
+ childPlaybookId: completion.childPlaybookId,
860
+ ...(completion.childOutput === undefined
861
+ ? {}
862
+ : { childOutput: completion.childOutput }),
863
+ };
864
+ case 'child-failed':
865
+ return { status: 'child-failed', childResult: completion.childResult };
1040
866
  }
1041
- throw new Error('DEV reached a final state without a recorded completion');
1042
867
  },
1043
868
  states: {
1044
869
  ready: {
@@ -1047,13 +872,17 @@ export const devMachine = machineSetup.createMachine({
1047
872
  meta: playbookMeta('ready'),
1048
873
  tags: ['playbook.parked'],
1049
874
  on: {
1050
- START_DEV: { target: 'planAnalysis', actions: 'startDev' },
875
+ START_DEV: {
876
+ guard: 'startsDev',
877
+ target: 'planAnalysis',
878
+ actions: 'startDev',
879
+ },
1051
880
  },
1052
881
  },
1053
882
  planAnalysis: {
1054
883
  id: 'planAnalysis',
1055
884
  description: STATE_DESCRIPTIONS.planAnalysis,
1056
- meta: playbookMeta('planAnalysis', 'analyst'),
885
+ meta: roleMeta('planAnalysis', 'analyst'),
1057
886
  tags: ['playbook.busy'],
1058
887
  invoke: {
1059
888
  src: 'player',
@@ -1062,11 +891,9 @@ export const devMachine = machineSetup.createMachine({
1062
891
  role: 'analyst',
1063
892
  sourceItem: 'DEV-1',
1064
893
  prompt: PLAN_ANALYSIS_PROMPT,
1065
- result: PLAN_ANALYSIS_RESULTS,
894
+ result: PLAN_ANALYSIS_RESULT,
1066
895
  developmentRequest: context.developmentRequest ?? '',
1067
- discussionContext: renderDiscussionContext(
1068
- context.discussionExchanges,
1069
- ),
896
+ discussionContext: context.discussionContext,
1070
897
  runResults: context.runResults,
1071
898
  ...(context.pendingBossQuestion === undefined
1072
899
  ? {}
@@ -1077,7 +904,27 @@ export const devMachine = machineSetup.createMachine({
1077
904
  }),
1078
905
  onDone: [
1079
906
  {
1080
- guard: 'isDiscussionComplete',
907
+ guard: 'needsBossReplyWithQuestion',
908
+ target: 'awaitBossReply',
909
+ actions: [
910
+ {
911
+ type: 'playbook.acceptedOutcome',
912
+ params: {
913
+ source: 'planAnalysis',
914
+ target: 'awaitBossReply',
915
+ acceptedOutcome: 'needsBossReply',
916
+ },
917
+ },
918
+ { type: 'setPendingBossQuestion', params: PLAN_ANALYSIS_QUESTION },
919
+ ],
920
+ },
921
+ {
922
+ guard: 'needsBossReplyWithoutQuestion',
923
+ target: 'failed',
924
+ actions: 'rememberMalformedPlayerOutput',
925
+ },
926
+ {
927
+ guard: 'discussionCompleteAfterBossReply',
1081
928
  target: 'discussionComplete',
1082
929
  actions: [
1083
930
  {
@@ -1092,7 +939,7 @@ export const devMachine = machineSetup.createMachine({
1092
939
  ],
1093
940
  },
1094
941
  {
1095
- guard: 'isCodePath',
942
+ guard: 'codePlanned',
1096
943
  target: 'callCode',
1097
944
  actions: [
1098
945
  {
@@ -1103,11 +950,11 @@ export const devMachine = machineSetup.createMachine({
1103
950
  acceptedOutcome: 'code',
1104
951
  },
1105
952
  },
1106
- 'rememberCodePath',
953
+ 'acceptPlainPlan',
1107
954
  ],
1108
955
  },
1109
956
  {
1110
- guard: 'isDecideThenCode',
957
+ guard: 'decideThenCodePlanned',
1111
958
  target: 'callDecide',
1112
959
  actions: [
1113
960
  {
@@ -1118,11 +965,11 @@ export const devMachine = machineSetup.createMachine({
1118
965
  acceptedOutcome: 'decideThenCode',
1119
966
  },
1120
967
  },
1121
- 'rememberDecidePath',
968
+ 'acceptPlainPlan',
1122
969
  ],
1123
970
  },
1124
971
  {
1125
- guard: 'isCodeViaPullRequest',
972
+ guard: 'codeViaPullRequestPlanned',
1126
973
  target: 'createBranch',
1127
974
  actions: [
1128
975
  {
@@ -1133,11 +980,11 @@ export const devMachine = machineSetup.createMachine({
1133
980
  acceptedOutcome: 'codeViaPullRequest',
1134
981
  },
1135
982
  },
1136
- 'rememberCodeViaPullRequestPath',
983
+ { type: 'acceptPullRequestPlan', params: { path: 'code' } },
1137
984
  ],
1138
985
  },
1139
986
  {
1140
- guard: 'isDecideThenCodeViaPullRequest',
987
+ guard: 'decideThenCodeViaPullRequestPlanned',
1141
988
  target: 'createBranch',
1142
989
  actions: [
1143
990
  {
@@ -1148,22 +995,10 @@ export const devMachine = machineSetup.createMachine({
1148
995
  acceptedOutcome: 'decideThenCodeViaPullRequest',
1149
996
  },
1150
997
  },
1151
- 'rememberDecideThenCodeViaPullRequestPath',
1152
- ],
1153
- },
1154
- {
1155
- guard: 'needsBossReply',
1156
- target: 'awaitBossReply',
1157
- actions: [
1158
998
  {
1159
- type: 'playbook.acceptedOutcome',
1160
- params: {
1161
- source: 'planAnalysis',
1162
- target: 'awaitBossReply',
1163
- acceptedOutcome: 'needsBossReply',
1164
- },
999
+ type: 'acceptPullRequestPlan',
1000
+ params: { path: 'decide-then-code' },
1165
1001
  },
1166
- 'rememberPendingQuestion',
1167
1002
  ],
1168
1003
  },
1169
1004
  {
@@ -1171,7 +1006,31 @@ export const devMachine = machineSetup.createMachine({
1171
1006
  actions: 'rememberMalformedPlayerOutput',
1172
1007
  },
1173
1008
  ],
1174
- onError: { target: 'failed', actions: 'rememberActorError' },
1009
+ onError: {
1010
+ target: 'failed',
1011
+ actions: ['rememberActorError', 'clearBossReplyContext'],
1012
+ },
1013
+ },
1014
+ },
1015
+ awaitBossReply: {
1016
+ id: 'awaitBossReply',
1017
+ description: STATE_DESCRIPTIONS.awaitBossReply,
1018
+ meta: playbookMeta('awaitBossReply'),
1019
+ tags: ['playbook.parked'],
1020
+ on: {
1021
+ BOSS_REPLY: [
1022
+ {
1023
+ guard: 'emptyBossReply',
1024
+ target: 'failed',
1025
+ actions: 'rememberMalformedBossReply',
1026
+ },
1027
+ ...resumableStates(['planAnalysis']),
1028
+ ],
1029
+ START_DEV: {
1030
+ guard: 'startsDev',
1031
+ target: 'planAnalysis',
1032
+ actions: 'startDev',
1033
+ },
1175
1034
  },
1176
1035
  },
1177
1036
  createBranch: {
@@ -1185,29 +1044,35 @@ export const devMachine = machineSetup.createMachine({
1185
1044
  stateId: 'createBranch',
1186
1045
  sourceItem: 'DEV-5',
1187
1046
  playbookId: 'branch',
1188
- text: branchCallText(context),
1047
+ text: plannedCallText(context),
1189
1048
  }),
1190
1049
  onDone: [
1191
1050
  {
1192
- guard: 'isBranchSuccessForCode',
1051
+ guard: 'branchSucceededForCode',
1193
1052
  target: 'callCode',
1194
1053
  actions: 'rememberBranchResult',
1195
1054
  },
1196
1055
  {
1197
- guard: 'isBranchSuccessForDecide',
1056
+ guard: 'branchSucceededForDecide',
1198
1057
  target: 'callDecide',
1199
1058
  actions: 'rememberBranchResult',
1200
1059
  },
1201
1060
  {
1202
1061
  target: 'reportedChildFailure',
1203
- actions: 'completeWithInsufficientBranchResult',
1062
+ actions: {
1063
+ type: 'completeWithInsufficientResult',
1064
+ params: { playbookId: 'branch' },
1065
+ },
1204
1066
  },
1205
1067
  ],
1206
1068
  onError: [
1207
1069
  {
1208
- guard: 'authoredBranchFailure',
1070
+ guard: 'authoredBranchResult',
1209
1071
  target: 'reportedChildFailure',
1210
- actions: 'completeWithBranchFailure',
1072
+ actions: {
1073
+ type: 'completeWithAuthoredChildResult',
1074
+ params: { playbookId: 'branch' },
1075
+ },
1211
1076
  },
1212
1077
  { target: 'failed', actions: 'rememberActorError' },
1213
1078
  ],
@@ -1224,29 +1089,38 @@ export const devMachine = machineSetup.createMachine({
1224
1089
  stateId: 'callCode',
1225
1090
  sourceItem: 'DEV-2',
1226
1091
  playbookId: 'code',
1227
- text: codeCallText(context),
1092
+ text: plannedCallText(context),
1228
1093
  }),
1229
1094
  onDone: [
1230
1095
  {
1231
- guard: 'isCodeSuccessViaPullRequest',
1096
+ guard: 'codeSucceededOnPullRequestPath',
1232
1097
  target: 'openPullRequest',
1233
1098
  actions: 'rememberCodeResult',
1234
1099
  },
1235
1100
  {
1236
- guard: 'isPlainCodeSuccess',
1101
+ guard: 'codeSucceededOnPlainPath',
1237
1102
  target: 'done',
1238
- actions: 'completeWithChildSuccess',
1103
+ actions: {
1104
+ type: 'completeWithChildSuccess',
1105
+ params: { childPlaybookId: 'code' },
1106
+ },
1239
1107
  },
1240
1108
  {
1241
1109
  target: 'reportedChildFailure',
1242
- actions: 'completeWithInsufficientCodeResult',
1110
+ actions: {
1111
+ type: 'completeWithInsufficientResult',
1112
+ params: { playbookId: 'code' },
1113
+ },
1243
1114
  },
1244
1115
  ],
1245
1116
  onError: [
1246
1117
  {
1247
- guard: 'authoredCodeFailure',
1118
+ guard: 'authoredCodeResult',
1248
1119
  target: 'reportedChildFailure',
1249
- actions: 'completeWithCodeFailure',
1120
+ actions: {
1121
+ type: 'completeWithAuthoredChildResult',
1122
+ params: { playbookId: 'code' },
1123
+ },
1250
1124
  },
1251
1125
  { target: 'failed', actions: 'rememberActorError' },
1252
1126
  ],
@@ -1263,24 +1137,30 @@ export const devMachine = machineSetup.createMachine({
1263
1137
  stateId: 'callDecide',
1264
1138
  sourceItem: 'DEV-3',
1265
1139
  playbookId: 'decide',
1266
- text: decideCallText(context),
1140
+ text: plannedCallText(context),
1267
1141
  }),
1268
1142
  onDone: [
1269
1143
  {
1270
- guard: 'isDecideSuccess',
1144
+ guard: 'decideSucceeded',
1271
1145
  target: 'callCodeAfterDecide',
1272
1146
  actions: 'rememberDecideResult',
1273
1147
  },
1274
1148
  {
1275
1149
  target: 'reportedChildFailure',
1276
- actions: 'completeWithInsufficientDecideResult',
1150
+ actions: {
1151
+ type: 'completeWithInsufficientResult',
1152
+ params: { playbookId: 'decide' },
1153
+ },
1277
1154
  },
1278
1155
  ],
1279
1156
  onError: [
1280
1157
  {
1281
- guard: 'authoredDecideFailure',
1158
+ guard: 'authoredDecideResult',
1282
1159
  target: 'reportedChildFailure',
1283
- actions: 'completeWithDecideFailure',
1160
+ actions: {
1161
+ type: 'completeWithAuthoredChildResult',
1162
+ params: { playbookId: 'decide' },
1163
+ },
1284
1164
  },
1285
1165
  { target: 'failed', actions: 'rememberActorError' },
1286
1166
  ],
@@ -1301,25 +1181,34 @@ export const devMachine = machineSetup.createMachine({
1301
1181
  }),
1302
1182
  onDone: [
1303
1183
  {
1304
- guard: 'isCodeSuccessViaPullRequest',
1184
+ guard: 'codeSucceededOnPullRequestPath',
1305
1185
  target: 'openPullRequest',
1306
1186
  actions: 'rememberCodeResult',
1307
1187
  },
1308
1188
  {
1309
- guard: 'isPlainCodeSuccess',
1189
+ guard: 'codeSucceededOnPlainPath',
1310
1190
  target: 'done',
1311
- actions: 'completeWithChildSuccess',
1191
+ actions: {
1192
+ type: 'completeWithChildSuccess',
1193
+ params: { childPlaybookId: 'code' },
1194
+ },
1312
1195
  },
1313
1196
  {
1314
1197
  target: 'reportedChildFailure',
1315
- actions: 'completeWithInsufficientCodeResult',
1198
+ actions: {
1199
+ type: 'completeWithInsufficientResult',
1200
+ params: { playbookId: 'code' },
1201
+ },
1316
1202
  },
1317
1203
  ],
1318
1204
  onError: [
1319
1205
  {
1320
- guard: 'authoredCodeFailure',
1206
+ guard: 'authoredCodeResult',
1321
1207
  target: 'reportedChildFailure',
1322
- actions: 'completeWithCodeFailure',
1208
+ actions: {
1209
+ type: 'completeWithAuthoredChildResult',
1210
+ params: { playbookId: 'code' },
1211
+ },
1323
1212
  },
1324
1213
  { target: 'failed', actions: 'rememberActorError' },
1325
1214
  ],
@@ -1338,67 +1227,58 @@ export const devMachine = machineSetup.createMachine({
1338
1227
  playbookId: 'pr',
1339
1228
  text: pullRequestCallText(context),
1340
1229
  }),
1341
- // DR-048: `pr` declares its terminal kinds, so `onDone` alone proves
1342
- // its success; DEV consumes none of its fields by name.
1343
- onDone: { target: 'done', actions: 'completeWithChildSuccess' },
1230
+ // `pr` success completes DEV with that result; a `pr` failure
1231
+ // terminal arrives through `onError` and is relayed below.
1232
+ onDone: {
1233
+ target: 'done',
1234
+ actions: {
1235
+ type: 'completeWithChildSuccess',
1236
+ params: { childPlaybookId: 'pr' },
1237
+ },
1238
+ },
1344
1239
  onError: [
1345
1240
  {
1346
- guard: 'authoredPrFailure',
1241
+ guard: 'authoredPrResult',
1347
1242
  target: 'reportedChildFailure',
1348
- actions: 'completeWithPrFailure',
1243
+ actions: {
1244
+ type: 'completeWithAuthoredChildResult',
1245
+ params: { playbookId: 'pr' },
1246
+ },
1349
1247
  },
1350
1248
  { target: 'failed', actions: 'rememberActorError' },
1351
1249
  ],
1352
1250
  },
1353
1251
  },
1354
- awaitBossReply: {
1355
- id: 'awaitBossReply',
1356
- description: STATE_DESCRIPTIONS.awaitBossReply,
1357
- meta: playbookMeta('awaitBossReply'),
1358
- tags: ['playbook.parked'],
1359
- on: {
1360
- BOSS_REPLY: [
1361
- {
1362
- guard: 'emptyBossReply',
1363
- target: '#failed',
1364
- actions: 'rememberEmptyBossReplyError',
1365
- },
1366
- {
1367
- guard: 'resumesPlanAnalysis',
1368
- target: '#planAnalysis',
1369
- reenter: true,
1370
- actions: 'rememberBossReply',
1371
- },
1372
- ],
1373
- START_DEV: { target: 'planAnalysis', actions: 'startDev' },
1374
- },
1375
- },
1376
1252
  failed: {
1377
1253
  id: 'failed',
1378
1254
  description: STATE_DESCRIPTIONS.failed,
1379
1255
  meta: playbookMeta('failed'),
1380
1256
  tags: ['playbook.parked'],
1381
1257
  on: {
1382
- START_DEV: { target: 'planAnalysis', actions: 'startDev' },
1258
+ START_DEV: {
1259
+ guard: 'startsDev',
1260
+ target: 'planAnalysis',
1261
+ actions: 'startDev',
1262
+ },
1383
1263
  },
1384
1264
  },
1385
1265
  discussionComplete: {
1386
1266
  id: 'discussionComplete',
1267
+ type: 'final',
1387
1268
  description: STATE_DESCRIPTIONS.discussionComplete,
1388
1269
  meta: terminalMeta('discussionComplete', 'success'),
1389
- type: 'final',
1390
1270
  },
1391
1271
  done: {
1392
1272
  id: 'done',
1273
+ type: 'final',
1393
1274
  description: STATE_DESCRIPTIONS.done,
1394
1275
  meta: terminalMeta('done', 'success'),
1395
- type: 'final',
1396
1276
  },
1397
1277
  reportedChildFailure: {
1398
1278
  id: 'reportedChildFailure',
1279
+ type: 'final',
1399
1280
  description: STATE_DESCRIPTIONS.reportedChildFailure,
1400
1281
  meta: terminalMeta('reportedChildFailure', 'failure'),
1401
- type: 'final',
1402
1282
  },
1403
1283
  },
1404
1284
  });