@sublang/playbook 5.0.0 → 7.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 (72) hide show
  1. package/README.md +22 -14
  2. package/docs/cli.md +138 -90
  3. package/docs/configuration.md +113 -29
  4. package/docs/embedding.md +24 -16
  5. package/package.json +42 -21
  6. package/reference/sdlc/captain.playbook/captain.playbook.js +2 -0
  7. package/reference/sdlc/captain.playbook/captain.playbook.ts +2 -0
  8. package/reference/sdlc/code.md +55 -97
  9. package/reference/sdlc/code.playbook/bin/launch-config.js +938 -0
  10. package/reference/sdlc/code.playbook/bin/playbook.js +145 -562
  11. package/reference/sdlc/code.playbook/bin/provision.js +84 -38
  12. package/reference/sdlc/code.playbook/bin/run.js +1171 -983
  13. package/reference/sdlc/code.playbook/bin/session-store.js +1169 -0
  14. package/reference/sdlc/code.playbook/code.fsm.d.ts +229 -94
  15. package/reference/sdlc/code.playbook/code.fsm.introspect.d.ts +26 -44
  16. package/reference/sdlc/code.playbook/code.fsm.introspect.js +61 -66
  17. package/reference/sdlc/code.playbook/code.fsm.introspect.ts +100 -149
  18. package/reference/sdlc/code.playbook/code.fsm.js +587 -1347
  19. package/reference/sdlc/code.playbook/code.fsm.ts +809 -1650
  20. package/reference/sdlc/code.playbook/code.gears.md +51 -263
  21. package/reference/sdlc/code.playbook/code.playbook.d.ts +8 -47
  22. package/reference/sdlc/code.playbook/code.playbook.js +69 -656
  23. package/reference/sdlc/code.playbook/code.playbook.ts +90 -867
  24. package/reference/sdlc/code.playbook/code.registry.d.ts +9 -25
  25. package/reference/sdlc/code.playbook/code.registry.js +20 -78
  26. package/reference/sdlc/code.playbook/code.registry.ts +58 -122
  27. package/reference/sdlc/code.playbook/playbook-captain.d.ts +70 -3
  28. package/reference/sdlc/code.playbook/playbook-captain.js +954 -80
  29. package/reference/sdlc/code.playbook/playbook-captain.ts +1408 -80
  30. package/reference/sdlc/code.playbook/playbook.config.template.yaml +37 -36
  31. package/reference/sdlc/decide.md +54 -0
  32. package/reference/sdlc/decide.playbook/decide.fsm.d.ts +261 -0
  33. package/reference/sdlc/decide.playbook/decide.fsm.js +894 -0
  34. package/reference/sdlc/decide.playbook/decide.fsm.ts +1152 -0
  35. package/reference/sdlc/decide.playbook/decide.gears.md +88 -0
  36. package/reference/sdlc/decide.playbook/decide.playbook.d.ts +67 -0
  37. package/reference/sdlc/{discuss.playbook/discuss.playbook.js → decide.playbook/decide.playbook.js} +545 -372
  38. package/reference/sdlc/{discuss.playbook/discuss.playbook.ts → decide.playbook/decide.playbook.ts} +665 -454
  39. package/reference/sdlc/decide.playbook/decide.registry.d.ts +41 -0
  40. package/reference/sdlc/decide.playbook/decide.registry.js +60 -0
  41. package/reference/sdlc/decide.playbook/decide.registry.ts +125 -0
  42. package/reference/sdlc/review.md +81 -0
  43. package/reference/sdlc/review.playbook/review.fsm.d.ts +183 -0
  44. package/reference/sdlc/review.playbook/review.fsm.js +524 -0
  45. package/reference/sdlc/review.playbook/review.fsm.ts +652 -0
  46. package/reference/sdlc/review.playbook/review.gears.md +112 -0
  47. package/reference/sdlc/review.playbook/review.playbook.d.ts +12 -0
  48. package/reference/sdlc/review.playbook/review.playbook.js +112 -0
  49. package/reference/sdlc/review.playbook/review.playbook.ts +201 -0
  50. package/reference/sdlc/review.playbook/review.registry.d.ts +43 -0
  51. package/reference/sdlc/review.playbook/review.registry.js +73 -0
  52. package/reference/sdlc/review.playbook/review.registry.ts +138 -0
  53. package/slc/gears2fsm.md +13 -4
  54. package/slc/link.md +83 -14
  55. package/slc/text2gears.md +22 -2
  56. package/src/runtime.d.ts +21 -2
  57. package/src/runtime.ts +38 -6
  58. package/src/xstate-playbook-runtime.d.ts +9 -2
  59. package/src/xstate-playbook-runtime.js +319 -35
  60. package/src/xstate-playbook-runtime.ts +412 -41
  61. package/src/xstate-runtime.d.ts +19 -2
  62. package/src/xstate-runtime.js +384 -57
  63. package/src/xstate-runtime.ts +542 -71
  64. package/reference/sdlc/discuss.md +0 -93
  65. package/reference/sdlc/discuss.playbook/discuss.fsm.d.ts +0 -396
  66. package/reference/sdlc/discuss.playbook/discuss.fsm.js +0 -2067
  67. package/reference/sdlc/discuss.playbook/discuss.fsm.ts +0 -2465
  68. package/reference/sdlc/discuss.playbook/discuss.gears.md +0 -258
  69. package/reference/sdlc/discuss.playbook/discuss.playbook.d.ts +0 -113
  70. package/reference/sdlc/discuss.playbook/discuss.registry.d.ts +0 -58
  71. package/reference/sdlc/discuss.playbook/discuss.registry.js +0 -97
  72. package/reference/sdlc/discuss.playbook/discuss.registry.ts +0 -153
@@ -0,0 +1,524 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // SPDX-FileCopyrightText: 2026 SubLang International <https://sublang.ai>
3
+ // FSM object artifact compiled from review.gears.md.
4
+ // The linked runtime supplies the delegated-player actor.
5
+ import { assign, fromPromise, setup } from 'xstate';
6
+ 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>`.";
7
+ const SHARED_REVIEW_INSTRUCTION = [
8
+ 'Understand the full picture and think systematically about the underlying design.',
9
+ 'Continue to identify issues or improvements, if any (numbered; no duplication).',
10
+ 'Treat as settled, and do not raise again, any finding in this review rejected twice with reasoning.',
11
+ 'Flag only what materially affects correctness, behavior, or spec quality — not style, equally valid alternatives, or theoretical threats.',
12
+ 'For specs, flag stale, missing, over-specified, or under-specified ones.',
13
+ 'Avoid unnecessary complexity in code or tests, but flag any fundamental design flaw when leaving it would cost more in later patches than fixing it now.',
14
+ '',
15
+ 'Do not re-run tests or builds whose inputs have not changed since any previous reported run.',
16
+ 'Do not edit files or commit; report findings only.',
17
+ '',
18
+ 'Consult @specs/map.md for context if needed; verify it remains accurate.',
19
+ 'Consult @specs/meta.md for spec requirements if needed; verify affected specs follow it.',
20
+ ].join('\n');
21
+ const INITIAL_REVIEW_PROMPT = [
22
+ 'A new review begins on the latest commit.',
23
+ 'Review the latest commit and resulting repository state; read the commit message first for its intent and rationale.',
24
+ '',
25
+ '> <caller-input>',
26
+ '',
27
+ SHARED_REVIEW_INSTRUCTION,
28
+ ].join('\n');
29
+ const POST_COMMIT_REVIEW_PROMPT = [
30
+ 'Review the latest commit and resulting repository state; read the commit message first for its intent and rationale.',
31
+ '',
32
+ '> <coder-output>',
33
+ '',
34
+ SHARED_REVIEW_INSTRUCTION,
35
+ ].join('\n');
36
+ const REBUTTAL_REVIEW_PROMPT = [
37
+ 'No new commit was made because Coder rejected every finding.',
38
+ 'For any rebuttal, accept or challenge it.',
39
+ 'State which findings, if any, remain.',
40
+ '',
41
+ '> <coder-output>',
42
+ '',
43
+ SHARED_REVIEW_INSTRUCTION,
44
+ ].join('\n');
45
+ const CODER_DISPOSITION_PROMPT = [
46
+ '> <reviewer-output>',
47
+ '',
48
+ 'For each review item, accept or reject it.',
49
+ 'Before deciding, understand the full picture and think systematically about the underlying design.',
50
+ 'Reject anything that is not essential or is not worth fixing now.',
51
+ 'If you accept an item, fix its root cause, including any fundamental design flaw - do not patch around it.',
52
+ 'If you reject an item, give the reasoning and cite code or test output that supports it.',
53
+ 'Do not re-run tests or builds whose inputs have not changed since any previous reported run.',
54
+ '',
55
+ 'If you accept any item, make minimal changes and commit them as one new commit; never amend the reviewed commit.',
56
+ 'Follow @specs/packages/git.md.',
57
+ 'Make the commit message explain concisely what changed and why, including relevant verification.',
58
+ 'Coder is <coder-llm>; Reviewer is <reviewer-llm>; format model tokens in conventional human form.',
59
+ '',
60
+ 'If you reject every item, change nothing and make no commit.',
61
+ 'Report every disposition, all relevant run results, and every rebuttal.',
62
+ ].join('\n');
63
+ const REVIEW_RESULTS = {
64
+ hasFindings: 'Reviewer raised one or more unsettled findings. Output shall include `reviewerOutput: <verbatim final text>`.',
65
+ noFindings: 'Reviewer raised no unsettled findings.',
66
+ needsBossReply: NEEDS_BOSS_REPLY_DESCRIPTION,
67
+ };
68
+ const REBUTTAL_RESULTS = {
69
+ hasFindings: 'Reviewer kept one or more unsettled findings. Output shall include `reviewerOutput: <verbatim final text>`.',
70
+ noFindings: 'Reviewer accepted the rebuttals and no unsettled findings remain.',
71
+ needsBossReply: NEEDS_BOSS_REPLY_DESCRIPTION,
72
+ };
73
+ const CODER_RESULTS = {
74
+ committed: 'Coder accepted at least one item and made one new commit. Output shall include `coderOutput: <verbatim final text>`.',
75
+ rejectedAll: 'Coder rejected every item and made no commit. Output shall include `coderOutput: <verbatim final text>`.',
76
+ needsBossReply: NEEDS_BOSS_REPLY_DESCRIPTION,
77
+ };
78
+ const stateDescriptions = {
79
+ ready: 'Waits for a caller to start a committed-work review.',
80
+ reviewInitial: 'REVIEW-1: Reviewer examines the latest commit against the caller input.',
81
+ addressFindings: 'REVIEW-2: Coder accepts or rejects every current review finding.',
82
+ reviewAfterCommit: 'REVIEW-3: Reviewer examines the new review-fix commit and repository state.',
83
+ reviewAfterRebuttal: 'REVIEW-4: Reviewer adjudicates an all-rejected Coder disposition.',
84
+ awaitBossReply: 'Waits for Boss to answer the active player question.',
85
+ failed: 'Retains a recoverable REVIEW failure for the caller.',
86
+ done: 'The latest commit is approved with no unsettled findings.',
87
+ };
88
+ const playbookMeta = (stateId, player) => ({
89
+ playbook: {
90
+ stateId,
91
+ description: stateDescriptions[stateId],
92
+ ...(player === undefined ? {} : { player }),
93
+ },
94
+ });
95
+ const outputOf = (event) => event.output;
96
+ const isNonEmptyString = (value) => typeof value === 'string' && value.trim().length > 0;
97
+ const stateMetadata = {
98
+ reviewInitial: { sourceItem: 'REVIEW-1', player: 'Reviewer' },
99
+ addressFindings: { sourceItem: 'REVIEW-2', player: 'Coder' },
100
+ reviewAfterCommit: { sourceItem: 'REVIEW-3', player: 'Reviewer' },
101
+ reviewAfterRebuttal: { sourceItem: 'REVIEW-4', player: 'Reviewer' },
102
+ };
103
+ const playerPlaceholder = fromPromise(async () => {
104
+ throw new Error('player actor must be provided by the runner');
105
+ });
106
+ export const reviewMachine = setup({
107
+ types: {
108
+ context: {},
109
+ events: {},
110
+ input: {},
111
+ output: {},
112
+ },
113
+ actors: {
114
+ player: playerPlaceholder,
115
+ },
116
+ guards: {
117
+ hasFindings: ({ event }) => {
118
+ const output = outputOf(event);
119
+ return (output.guard === 'hasFindings' &&
120
+ isNonEmptyString(output.reviewerOutput));
121
+ },
122
+ noFindings: ({ event }) => outputOf(event).guard === 'noFindings',
123
+ committed: ({ event }) => {
124
+ const output = outputOf(event);
125
+ return (output.guard === 'committed' && isNonEmptyString(output.coderOutput));
126
+ },
127
+ rejectedAll: ({ event }) => {
128
+ const output = outputOf(event);
129
+ return (output.guard === 'rejectedAll' && isNonEmptyString(output.coderOutput));
130
+ },
131
+ needsBossReply: ({ event }) => {
132
+ const output = outputOf(event);
133
+ return (output.guard === 'needsBossReply' && isNonEmptyString(output.question));
134
+ },
135
+ emptyBossReply: ({ event }) => event.type === 'BOSS_REPLY' && event.answer.trim().length === 0,
136
+ resumeReviewInitial: ({ context, event }) => event.type === 'BOSS_REPLY' &&
137
+ event.answer.trim().length > 0 &&
138
+ context.pendingBossQuestion?.resumeStateId === 'reviewInitial' &&
139
+ (event.questionId === undefined ||
140
+ event.questionId === context.pendingBossQuestion.questionId),
141
+ resumeAddressFindings: ({ context, event }) => event.type === 'BOSS_REPLY' &&
142
+ event.answer.trim().length > 0 &&
143
+ context.pendingBossQuestion?.resumeStateId === 'addressFindings' &&
144
+ (event.questionId === undefined ||
145
+ event.questionId === context.pendingBossQuestion.questionId),
146
+ resumeReviewAfterCommit: ({ context, event }) => event.type === 'BOSS_REPLY' &&
147
+ event.answer.trim().length > 0 &&
148
+ context.pendingBossQuestion?.resumeStateId === 'reviewAfterCommit' &&
149
+ (event.questionId === undefined ||
150
+ event.questionId === context.pendingBossQuestion.questionId),
151
+ resumeReviewAfterRebuttal: ({ context, event }) => event.type === 'BOSS_REPLY' &&
152
+ event.answer.trim().length > 0 &&
153
+ context.pendingBossQuestion?.resumeStateId === 'reviewAfterRebuttal' &&
154
+ (event.questionId === undefined ||
155
+ event.questionId === context.pendingBossQuestion.questionId),
156
+ restartInitialReview: ({ event }) => event.type === 'BOSS_INTERRUPT' &&
157
+ event.targetId === 'reviewInitial' &&
158
+ typeof event.bossIntent === 'string' &&
159
+ event.bossIntent.trim().length > 0,
160
+ },
161
+ actions: {
162
+ copyStartInput: assign({
163
+ callerInput: ({ event }) => event.type === 'START_REVIEW' ? event.callerInput : undefined,
164
+ reviewerOutput: undefined,
165
+ coderOutput: undefined,
166
+ lastError: undefined,
167
+ pendingBossQuestion: undefined,
168
+ bossReply: undefined,
169
+ }),
170
+ copyInterruptedInput: assign({
171
+ callerInput: ({ event }) => event.type === 'BOSS_INTERRUPT' ? event.bossIntent : undefined,
172
+ reviewerOutput: undefined,
173
+ coderOutput: undefined,
174
+ lastError: undefined,
175
+ pendingBossQuestion: undefined,
176
+ bossReply: undefined,
177
+ }),
178
+ rememberReviewerOutput: assign({
179
+ reviewerOutput: ({ event }) => {
180
+ const output = outputOf(event);
181
+ return output.guard === 'hasFindings'
182
+ ? output.reviewerOutput
183
+ : undefined;
184
+ },
185
+ lastError: undefined,
186
+ pendingBossQuestion: undefined,
187
+ bossReply: undefined,
188
+ }),
189
+ rememberCoderOutput: assign({
190
+ coderOutput: ({ event }) => {
191
+ const output = outputOf(event);
192
+ return output.guard === 'committed' || output.guard === 'rejectedAll'
193
+ ? output.coderOutput
194
+ : undefined;
195
+ },
196
+ lastError: undefined,
197
+ pendingBossQuestion: undefined,
198
+ bossReply: undefined,
199
+ }),
200
+ clearBossReplyContext: assign({
201
+ pendingBossQuestion: undefined,
202
+ bossReply: undefined,
203
+ }),
204
+ rememberBossReply: assign({
205
+ bossReply: ({ event }) => event.type === 'BOSS_REPLY' ? event.answer : undefined,
206
+ }),
207
+ rememberActorError: assign({
208
+ lastError: ({ event }) => event.error ??
209
+ new Error('REVIEW player invocation failed'),
210
+ }),
211
+ rememberEmptyBossReplyError: assign({
212
+ lastError: () => new Error('BOSS_REPLY received an empty answer'),
213
+ pendingBossQuestion: undefined,
214
+ bossReply: undefined,
215
+ }),
216
+ setPendingReviewInitial: assign({
217
+ pendingBossQuestion: ({ event }) => {
218
+ const output = outputOf(event);
219
+ return {
220
+ questionId: 'reviewInitial',
221
+ resumeStateId: 'reviewInitial',
222
+ ...stateMetadata.reviewInitial,
223
+ question: output.guard === 'needsBossReply' ? output.question : '',
224
+ };
225
+ },
226
+ bossReply: undefined,
227
+ }),
228
+ setPendingAddressFindings: assign({
229
+ pendingBossQuestion: ({ event }) => {
230
+ const output = outputOf(event);
231
+ return {
232
+ questionId: 'addressFindings',
233
+ resumeStateId: 'addressFindings',
234
+ ...stateMetadata.addressFindings,
235
+ question: output.guard === 'needsBossReply' ? output.question : '',
236
+ };
237
+ },
238
+ bossReply: undefined,
239
+ }),
240
+ setPendingReviewAfterCommit: assign({
241
+ pendingBossQuestion: ({ event }) => {
242
+ const output = outputOf(event);
243
+ return {
244
+ questionId: 'reviewAfterCommit',
245
+ resumeStateId: 'reviewAfterCommit',
246
+ ...stateMetadata.reviewAfterCommit,
247
+ question: output.guard === 'needsBossReply' ? output.question : '',
248
+ };
249
+ },
250
+ bossReply: undefined,
251
+ }),
252
+ setPendingReviewAfterRebuttal: assign({
253
+ pendingBossQuestion: ({ event }) => {
254
+ const output = outputOf(event);
255
+ return {
256
+ questionId: 'reviewAfterRebuttal',
257
+ resumeStateId: 'reviewAfterRebuttal',
258
+ ...stateMetadata.reviewAfterRebuttal,
259
+ question: output.guard === 'needsBossReply' ? output.question : '',
260
+ };
261
+ },
262
+ bossReply: undefined,
263
+ }),
264
+ },
265
+ }).createMachine({
266
+ id: 'review',
267
+ context: ({ input }) => ({
268
+ coderLlm: input.coderLlm,
269
+ reviewerLlm: input.reviewerLlm,
270
+ }),
271
+ initial: 'ready',
272
+ on: {
273
+ BOSS_INTERRUPT: {
274
+ guard: 'restartInitialReview',
275
+ target: '#reviewInitial',
276
+ reenter: true,
277
+ actions: 'copyInterruptedInput',
278
+ },
279
+ },
280
+ states: {
281
+ ready: {
282
+ id: 'ready',
283
+ description: stateDescriptions.ready,
284
+ meta: playbookMeta('ready'),
285
+ tags: ['playbook.parked'],
286
+ on: {
287
+ START_REVIEW: {
288
+ guard: ({ event }) => event.callerInput.trim().length > 0,
289
+ target: '#reviewInitial',
290
+ actions: 'copyStartInput',
291
+ },
292
+ },
293
+ },
294
+ reviewInitial: {
295
+ id: 'reviewInitial',
296
+ description: stateDescriptions.reviewInitial,
297
+ meta: playbookMeta('reviewInitial', 'Reviewer'),
298
+ tags: ['playbook.busy'],
299
+ invoke: {
300
+ src: 'player',
301
+ input: ({ context }) => ({
302
+ stateId: 'reviewInitial',
303
+ sourceItem: 'REVIEW-1',
304
+ player: 'Reviewer',
305
+ prompt: INITIAL_REVIEW_PROMPT,
306
+ result: REVIEW_RESULTS,
307
+ callerInput: context.callerInput,
308
+ pendingBossQuestion: context.pendingBossQuestion,
309
+ bossReply: context.bossReply,
310
+ }),
311
+ onDone: [
312
+ {
313
+ guard: 'hasFindings',
314
+ target: '#addressFindings',
315
+ actions: 'rememberReviewerOutput',
316
+ },
317
+ {
318
+ guard: 'noFindings',
319
+ target: '#done',
320
+ actions: 'clearBossReplyContext',
321
+ },
322
+ {
323
+ guard: 'needsBossReply',
324
+ target: '#awaitBossReply',
325
+ actions: 'setPendingReviewInitial',
326
+ },
327
+ ],
328
+ onError: {
329
+ target: '#failed',
330
+ actions: 'rememberActorError',
331
+ },
332
+ },
333
+ },
334
+ addressFindings: {
335
+ id: 'addressFindings',
336
+ description: stateDescriptions.addressFindings,
337
+ meta: playbookMeta('addressFindings', 'Coder'),
338
+ tags: ['playbook.busy'],
339
+ invoke: {
340
+ src: 'player',
341
+ input: ({ context }) => ({
342
+ stateId: 'addressFindings',
343
+ sourceItem: 'REVIEW-2',
344
+ player: 'Coder',
345
+ prompt: CODER_DISPOSITION_PROMPT,
346
+ result: CODER_RESULTS,
347
+ reviewerOutput: context.reviewerOutput,
348
+ coderLlm: context.coderLlm,
349
+ reviewerLlm: context.reviewerLlm,
350
+ pendingBossQuestion: context.pendingBossQuestion,
351
+ bossReply: context.bossReply,
352
+ }),
353
+ onDone: [
354
+ {
355
+ guard: 'committed',
356
+ target: '#reviewAfterCommit',
357
+ actions: 'rememberCoderOutput',
358
+ },
359
+ {
360
+ guard: 'rejectedAll',
361
+ target: '#reviewAfterRebuttal',
362
+ actions: 'rememberCoderOutput',
363
+ },
364
+ {
365
+ guard: 'needsBossReply',
366
+ target: '#awaitBossReply',
367
+ actions: 'setPendingAddressFindings',
368
+ },
369
+ ],
370
+ onError: {
371
+ target: '#failed',
372
+ actions: 'rememberActorError',
373
+ },
374
+ },
375
+ },
376
+ reviewAfterCommit: {
377
+ id: 'reviewAfterCommit',
378
+ description: stateDescriptions.reviewAfterCommit,
379
+ meta: playbookMeta('reviewAfterCommit', 'Reviewer'),
380
+ tags: ['playbook.busy'],
381
+ invoke: {
382
+ src: 'player',
383
+ input: ({ context }) => ({
384
+ stateId: 'reviewAfterCommit',
385
+ sourceItem: 'REVIEW-3',
386
+ player: 'Reviewer',
387
+ prompt: POST_COMMIT_REVIEW_PROMPT,
388
+ result: REVIEW_RESULTS,
389
+ coderOutput: context.coderOutput,
390
+ pendingBossQuestion: context.pendingBossQuestion,
391
+ bossReply: context.bossReply,
392
+ }),
393
+ onDone: [
394
+ {
395
+ guard: 'hasFindings',
396
+ target: '#addressFindings',
397
+ actions: 'rememberReviewerOutput',
398
+ },
399
+ {
400
+ guard: 'noFindings',
401
+ target: '#done',
402
+ actions: 'clearBossReplyContext',
403
+ },
404
+ {
405
+ guard: 'needsBossReply',
406
+ target: '#awaitBossReply',
407
+ actions: 'setPendingReviewAfterCommit',
408
+ },
409
+ ],
410
+ onError: {
411
+ target: '#failed',
412
+ actions: 'rememberActorError',
413
+ },
414
+ },
415
+ },
416
+ reviewAfterRebuttal: {
417
+ id: 'reviewAfterRebuttal',
418
+ description: stateDescriptions.reviewAfterRebuttal,
419
+ meta: playbookMeta('reviewAfterRebuttal', 'Reviewer'),
420
+ tags: ['playbook.busy'],
421
+ invoke: {
422
+ src: 'player',
423
+ input: ({ context }) => ({
424
+ stateId: 'reviewAfterRebuttal',
425
+ sourceItem: 'REVIEW-4',
426
+ player: 'Reviewer',
427
+ prompt: REBUTTAL_REVIEW_PROMPT,
428
+ result: REBUTTAL_RESULTS,
429
+ coderOutput: context.coderOutput,
430
+ pendingBossQuestion: context.pendingBossQuestion,
431
+ bossReply: context.bossReply,
432
+ }),
433
+ onDone: [
434
+ {
435
+ guard: 'hasFindings',
436
+ target: '#addressFindings',
437
+ actions: 'rememberReviewerOutput',
438
+ },
439
+ {
440
+ guard: 'noFindings',
441
+ target: '#done',
442
+ actions: 'clearBossReplyContext',
443
+ },
444
+ {
445
+ guard: 'needsBossReply',
446
+ target: '#awaitBossReply',
447
+ actions: 'setPendingReviewAfterRebuttal',
448
+ },
449
+ ],
450
+ onError: {
451
+ target: '#failed',
452
+ actions: 'rememberActorError',
453
+ },
454
+ },
455
+ },
456
+ awaitBossReply: {
457
+ id: 'awaitBossReply',
458
+ description: stateDescriptions.awaitBossReply,
459
+ meta: playbookMeta('awaitBossReply'),
460
+ tags: ['playbook.parked'],
461
+ on: {
462
+ START_REVIEW: {
463
+ guard: ({ event }) => event.callerInput.trim().length > 0,
464
+ target: '#reviewInitial',
465
+ actions: 'copyStartInput',
466
+ },
467
+ BOSS_REPLY: [
468
+ {
469
+ guard: 'emptyBossReply',
470
+ target: '#failed',
471
+ actions: 'rememberEmptyBossReplyError',
472
+ },
473
+ {
474
+ guard: 'resumeReviewInitial',
475
+ target: '#reviewInitial',
476
+ reenter: true,
477
+ actions: 'rememberBossReply',
478
+ },
479
+ {
480
+ guard: 'resumeAddressFindings',
481
+ target: '#addressFindings',
482
+ reenter: true,
483
+ actions: 'rememberBossReply',
484
+ },
485
+ {
486
+ guard: 'resumeReviewAfterCommit',
487
+ target: '#reviewAfterCommit',
488
+ reenter: true,
489
+ actions: 'rememberBossReply',
490
+ },
491
+ {
492
+ guard: 'resumeReviewAfterRebuttal',
493
+ target: '#reviewAfterRebuttal',
494
+ reenter: true,
495
+ actions: 'rememberBossReply',
496
+ },
497
+ ],
498
+ },
499
+ },
500
+ failed: {
501
+ id: 'failed',
502
+ description: stateDescriptions.failed,
503
+ meta: playbookMeta('failed'),
504
+ tags: ['playbook.parked'],
505
+ on: {
506
+ START_REVIEW: {
507
+ guard: ({ event }) => event.callerInput.trim().length > 0,
508
+ target: '#reviewInitial',
509
+ actions: 'copyStartInput',
510
+ },
511
+ },
512
+ },
513
+ done: {
514
+ id: 'done',
515
+ description: stateDescriptions.done,
516
+ meta: playbookMeta('done'),
517
+ type: 'final',
518
+ },
519
+ },
520
+ output: () => ({
521
+ approvedCommit: 'latest',
522
+ noUnsettledFindings: true,
523
+ }),
524
+ });