@sublang/playbook 11.0.0 → 12.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 (43) hide show
  1. package/docs/cli.md +9 -6
  2. package/docs/configuration.md +15 -1
  3. package/package.json +22 -3
  4. package/reference/sdlc/code.playbook/code.fsm.d.ts +22 -19
  5. package/reference/sdlc/code.playbook/code.fsm.js +116 -52
  6. package/reference/sdlc/code.playbook/code.fsm.ts +149 -64
  7. package/reference/sdlc/code.playbook/code.gears.md +40 -20
  8. package/reference/sdlc/code.playbook/code.playbook.js +23 -2
  9. package/reference/sdlc/code.playbook/code.playbook.ts +23 -2
  10. package/reference/sdlc/code.playbook/playbook.config.template.yaml +18 -2
  11. package/reference/sdlc/decide.playbook/decide.fsm.d.ts +13 -6
  12. package/reference/sdlc/decide.playbook/decide.fsm.js +54 -27
  13. package/reference/sdlc/decide.playbook/decide.fsm.ts +68 -29
  14. package/reference/sdlc/decide.playbook/decide.gears.md +25 -19
  15. package/reference/sdlc/decide.playbook/decide.playbook.js +11 -3
  16. package/reference/sdlc/decide.playbook/decide.playbook.ts +11 -3
  17. package/reference/sdlc/decide.playbook/decide.registry.js +1 -1
  18. package/reference/sdlc/decide.playbook/decide.registry.ts +1 -1
  19. package/reference/sdlc/dev.md +52 -0
  20. package/reference/sdlc/dev.playbook/dev.fsm.d.ts +261 -0
  21. package/reference/sdlc/dev.playbook/dev.fsm.js +723 -0
  22. package/reference/sdlc/dev.playbook/dev.fsm.ts +988 -0
  23. package/reference/sdlc/dev.playbook/dev.gears.md +91 -0
  24. package/reference/sdlc/dev.playbook/dev.playbook.d.ts +21 -0
  25. package/reference/sdlc/dev.playbook/dev.playbook.js +143 -0
  26. package/reference/sdlc/dev.playbook/dev.playbook.ts +246 -0
  27. package/reference/sdlc/dev.playbook/dev.registry.d.ts +40 -0
  28. package/reference/sdlc/dev.playbook/dev.registry.js +64 -0
  29. package/reference/sdlc/dev.playbook/dev.registry.ts +120 -0
  30. package/reference/sdlc/review.playbook/review.fsm.d.ts +15 -2
  31. package/reference/sdlc/review.playbook/review.fsm.js +77 -27
  32. package/reference/sdlc/review.playbook/review.fsm.ts +96 -30
  33. package/reference/sdlc/review.playbook/review.gears.md +52 -26
  34. package/reference/sdlc/review.playbook/review.playbook.js +17 -7
  35. package/reference/sdlc/review.playbook/review.playbook.ts +17 -7
  36. package/reference/sdlc/review.playbook/review.registry.js +1 -1
  37. package/reference/sdlc/review.playbook/review.registry.ts +1 -1
  38. package/slc/link.md +12 -5
  39. package/slc/text2gears.md +3 -0
  40. package/src/xstate-playbook-runtime.js +5 -2
  41. package/src/xstate-playbook-runtime.ts +5 -2
  42. package/src/xstate-runtime.js +13 -1
  43. package/src/xstate-runtime.ts +13 -1
@@ -6,7 +6,8 @@
6
6
  // Role metadata: Coder→coder (canonical local role)
7
7
  // Boss event: deterministic START_CODE entry; exact Boss text becomes
8
8
  // callerInput; pending Coder questions retain BOSS_REPLY
9
- // Adjudication: LLM judge per player state; coderOutput is carried verbatim
9
+ // Adjudication: LLM judge per player state; coderOutput is carried verbatim;
10
+ // the first phase decides among four semantic outcomes
10
11
  // Nested call: literal review target through the shared bridge
11
12
  // Compat: artifact schema 3 / runtime ABI 1
12
13
 
@@ -195,7 +196,7 @@ const runtimeSpec = {
195
196
  runFirstPhase: {
196
197
  role: 'coder',
197
198
  label:
198
- 'Coder is implementing one direct phase or committing a new intent record.',
199
+ 'Coder is running the first coding phase: a direct implementation, a new intent record, or an existing intent-record task.',
199
200
  },
200
201
  runIrTask: {
201
202
  role: 'coder',
@@ -213,6 +214,23 @@ const runtimeSpec = {
213
214
  repositoryDisposition: 'one-descendant-commit',
214
215
  },
215
216
  irCommit: {
217
+ fields: {
218
+ coderOutput: 'presentation',
219
+ latestCommit: 'effect',
220
+ irNumber: 'semantic',
221
+ },
222
+ repositoryDisposition: 'one-descendant-commit',
223
+ },
224
+ moreTasks: {
225
+ fields: {
226
+ coderOutput: 'presentation',
227
+ latestCommit: 'effect',
228
+ irNumber: 'semantic',
229
+ irTask: 'semantic',
230
+ },
231
+ repositoryDisposition: 'one-descendant-commit',
232
+ },
233
+ finalTask: {
216
234
  fields: {
217
235
  coderOutput: 'presentation',
218
236
  latestCommit: 'effect',
@@ -231,6 +249,7 @@ const runtimeSpec = {
231
249
  fields: {
232
250
  coderOutput: 'presentation',
233
251
  latestCommit: 'effect',
252
+ irNumber: 'semantic',
234
253
  irTask: 'semantic',
235
254
  },
236
255
  repositoryDisposition: 'one-descendant-commit',
@@ -239,6 +258,8 @@ const runtimeSpec = {
239
258
  fields: {
240
259
  coderOutput: 'presentation',
241
260
  latestCommit: 'effect',
261
+ irNumber: 'semantic',
262
+ irTask: 'semantic',
242
263
  },
243
264
  repositoryDisposition: 'one-descendant-commit',
244
265
  },
@@ -63,13 +63,24 @@ players:
63
63
  permissions:
64
64
  mode: auto
65
65
 
66
+ # DEV's planner stays a distinct player: equal ids deliberately share one
67
+ # provider conversation, and planning context must not bleed into review
68
+ # conversations (DR-044).
69
+ dev.analyst:
70
+ adapter: claude
71
+ model: claude-opus-5
72
+ effort: xhigh
73
+ permissions:
74
+ mode: auto
75
+
66
76
  # Enabled playbooks. Each is loaded from its explicit `from` module; the
67
77
  # `<id>` key must equal that module's manifest id. `from`, `command`, and
68
78
  # `roles` are launcher-owned; every other key is that playbook's option
69
79
  # slice.
70
80
  #
71
- # Every manifest role is bound explicitly. These three playbooks intentionally
72
- # reuse the same two stable players; change an id to isolate its conversation.
81
+ # Every manifest role is bound explicitly. CODE, REVIEW, and DECIDE
82
+ # intentionally reuse the same two stable players, while DEV keeps its own
83
+ # Analyst; change an id to isolate its conversation.
73
84
  playbooks:
74
85
  code:
75
86
  from: "@sublang/playbook/code/registry"
@@ -87,3 +98,8 @@ playbooks:
87
98
  roles:
88
99
  coder: dev.coder
89
100
  reviewer: dev.reviewer
101
+
102
+ dev:
103
+ from: "@sublang/playbook/dev/registry"
104
+ roles:
105
+ analyst: dev.analyst
@@ -19,7 +19,12 @@ type PendingBossQuestions = Partial<Record<ResumableStateId, PendingBossQuestion
19
19
  type BossReplies = Partial<Record<ResumableStateId, string>>;
20
20
  type PendingBossQuestionParams = Omit<PendingBossQuestion, 'questionId'>;
21
21
  export interface ReviewSuccessOutput {
22
- approvedCommit: 'latest';
22
+ evaluatedRevision: string;
23
+ noUnsettledFindings: true;
24
+ }
25
+ export interface DecideSuccessOutput {
26
+ decideCommit: string;
27
+ evaluatedRevision: string;
23
28
  noUnsettledFindings: true;
24
29
  }
25
30
  export interface DecideFailureOutput {
@@ -31,7 +36,7 @@ export interface DecideFailureOutput {
31
36
  message: string;
32
37
  };
33
38
  }
34
- export type DecideOutput = ReviewSuccessOutput | DecideFailureOutput;
39
+ export type DecideOutput = DecideSuccessOutput | DecideFailureOutput;
35
40
  interface ChildFailure {
36
41
  status: 'aborted' | 'error';
37
42
  playbookId: 'review';
@@ -45,6 +50,7 @@ export interface DecideContext {
45
50
  coderProposal?: string;
46
51
  reviewerProposal?: string;
47
52
  latestCommit?: string;
53
+ coderOutput?: string;
48
54
  reviewResult?: ReviewSuccessOutput;
49
55
  reviewFailure?: ChildFailure;
50
56
  lastResult?: PlayerOutput;
@@ -74,6 +80,7 @@ export interface PlayerInput {
74
80
  result: Record<string, string>;
75
81
  role: Role;
76
82
  callerTopic?: string;
83
+ reviewerProposal?: string;
77
84
  pendingBossQuestion?: PendingBossQuestion;
78
85
  bossReply?: string;
79
86
  }
@@ -139,9 +146,6 @@ export declare const decideMachine: import("xstate").StateMachine<DecideContext,
139
146
  } | {
140
147
  type: "rememberBossReply";
141
148
  params: import("xstate").NonReducibleUnknown;
142
- } | {
143
- type: "clearBossReplyContext";
144
- params: import("xstate").NonReducibleUnknown;
145
149
  } | {
146
150
  type: "copyInterruptedTopic";
147
151
  params: import("xstate").NonReducibleUnknown;
@@ -186,6 +190,9 @@ export declare const decideMachine: import("xstate").StateMachine<DecideContext,
186
190
  } | {
187
191
  type: "clearProposalRoundContext";
188
192
  params: import("xstate").NonReducibleUnknown;
193
+ } | {
194
+ type: "clearBossReplyContext";
195
+ params: import("xstate").NonReducibleUnknown;
189
196
  }, {
190
197
  type: "authoredReviewFailure";
191
198
  params: unknown;
@@ -212,7 +219,7 @@ export declare const decideMachine: import("xstate").StateMachine<DecideContext,
212
219
  coder: "complete" | "working" | "waiting";
213
220
  reviewer: "complete" | "working" | "waiting";
214
221
  };
215
- }, string, Readonly<Record<string, never>>, ReviewSuccessOutput | DecideFailureOutput, import("xstate").EventObject, import("xstate").MetaObject, {
222
+ }, string, Readonly<Record<string, never>>, DecideSuccessOutput | DecideFailureOutput, import("xstate").EventObject, import("xstate").MetaObject, {
216
223
  id: "decide";
217
224
  states: {
218
225
  readonly ready: {
@@ -15,30 +15,40 @@ const INDEPENDENT_PROPOSAL_PROMPT = [
15
15
  'Consult @specs/map.md for relevant context and @specs/meta.md for spec requirements, if needed.',
16
16
  'Do not change any files.',
17
17
  ].join('\n');
18
- const COMMIT_CODER_PROMPT = [
19
- 'Turn your proposal into the necessary spec items or DRs.',
18
+ const SYNTHESIZE_COMMIT_PROMPT = [
19
+ "Synthesize your independent proposal with Reviewer's proposal below.",
20
+ 'Keep to the original topic below and follow what it asks.',
21
+ 'Keep the best, essential parts of either proposal and reject any point that is unsound, unnecessary, or outside the topic.',
22
+ 'Turn the resulting design into the necessary DRs and/or spec items.',
20
23
  'Follow @specs/meta.md and update @specs/map.md when needed.',
21
- "Do not inspect or incorporate Reviewer's proposal before this commit.",
22
- 'Do not change code or implement the proposal.',
24
+ 'Do not change code or implement the design.',
23
25
  '',
24
26
  'Commit the result as one new commit, following @specs/packages/git.md.',
25
27
  'Make the commit message explain concisely what changed and why.',
26
- 'Coder is <coder-llm>; format the model token in conventional human form.',
28
+ 'Identify every new commit you make.',
29
+ 'Coder is <coder-llm> and Reviewer is <reviewer-llm>.',
30
+ '',
31
+ '> Original topic: <caller-topic>',
32
+ "> Reviewer's independent proposal: <reviewer-proposal>",
27
33
  ].join('\n');
28
34
  const REVIEW_INPUT_TEMPLATE = [
29
- 'Review the latest commit as a spec-design change against the initial intent.',
30
- 'Compare it with your independent proposal and take the best of both.',
31
- 'Make your suggestions.',
32
- '',
33
- 'Initial intent: <caller-topic>.',
34
- "Coder's independent proposal: <coder-proposal>.",
35
+ '> Original intent: <caller-topic>',
36
+ '> Review scope: the `decide`-owned commit <decide-commit> and its resulting repository state.',
37
+ '> Coder output: <coder-output>',
35
38
  ].join('\n');
36
- function composeReviewInput(callerTopic, coderProposal) {
39
+ function composeReviewInput(callerTopic, decideCommit, coderOutput) {
37
40
  const fields = {
38
41
  '<caller-topic>': callerTopic ?? '',
39
- '<coder-proposal>': coderProposal ?? '',
42
+ '<decide-commit>': decideCommit ?? '',
43
+ '<coder-output>': coderOutput ?? '',
40
44
  };
41
- return REVIEW_INPUT_TEMPLATE.replace(/<caller-topic>|<coder-proposal>/g, (placeholder) => fields[placeholder]);
45
+ return REVIEW_INPUT_TEMPLATE.replace(/<caller-topic>|<decide-commit>|<coder-output>/g, (placeholder, offset, source) => {
46
+ const value = fields[placeholder];
47
+ const lineStart = source.lastIndexOf('\n', offset - 1) + 1;
48
+ return source.slice(lineStart, offset).startsWith('> ')
49
+ ? value.replaceAll('\n', '\n> ')
50
+ : value;
51
+ });
42
52
  }
43
53
  const outputOf = (event) => event.output;
44
54
  const playbookOutputOf = (event) => event.output;
@@ -170,19 +180,23 @@ function isPlaybookState(value) {
170
180
  value.activeStateIds.length === 1 &&
171
181
  value.activeStateIds[0] === value.stateId)));
172
182
  }
183
+ // DECIDE supplied `review` a scope rooted at the `decide`-owned commit, so a
184
+ // result applies to that scope only when it is the exact declared terminal
185
+ // contract: the evaluated repository revision plus the affirmative
186
+ // no-unsettled-findings fact. Any other shape does not establish those facts.
173
187
  function reviewSuccessFrom(event) {
174
188
  const output = playbookOutputOf(event);
175
189
  if (!isPlainRecord(output))
176
190
  return undefined;
177
- if (!isExactKeys(output, ['approvedCommit', 'noUnsettledFindings'])) {
191
+ if (!isExactKeys(output, ['evaluatedRevision', 'noUnsettledFindings'])) {
178
192
  return undefined;
179
193
  }
180
- if (output.approvedCommit !== 'latest' ||
194
+ if (!isNonEmptyString(output.evaluatedRevision) ||
181
195
  output.noUnsettledFindings !== true) {
182
196
  return undefined;
183
197
  }
184
198
  return {
185
- approvedCommit: 'latest',
199
+ evaluatedRevision: output.evaluatedRevision,
186
200
  noUnsettledFindings: true,
187
201
  };
188
202
  }
@@ -303,6 +317,7 @@ export const decideMachine = setup({
303
317
  coderProposal: undefined,
304
318
  reviewerProposal: undefined,
305
319
  latestCommit: undefined,
320
+ coderOutput: undefined,
306
321
  reviewResult: undefined,
307
322
  reviewFailure: undefined,
308
323
  lastResult: undefined,
@@ -317,6 +332,7 @@ export const decideMachine = setup({
317
332
  coderProposal: undefined,
318
333
  reviewerProposal: undefined,
319
334
  latestCommit: undefined,
335
+ coderOutput: undefined,
320
336
  reviewResult: undefined,
321
337
  reviewFailure: undefined,
322
338
  lastResult: undefined,
@@ -346,6 +362,7 @@ export const decideMachine = setup({
346
362
  }),
347
363
  rememberCommit: assign({
348
364
  latestCommit: ({ event }) => outputOf(event).latestCommit,
365
+ coderOutput: ({ event }) => outputOf(event).coderOutput,
349
366
  lastResult: ({ event }) => outputOf(event),
350
367
  lastError: undefined,
351
368
  }),
@@ -364,7 +381,7 @@ export const decideMachine = setup({
364
381
  playbookId: 'review',
365
382
  error: {
366
383
  name: 'ReviewProtocolError',
367
- message: 'REVIEW returned without an approved latest commit and a no-findings result.',
384
+ message: 'REVIEW returned without the evaluated repository revision and a no-unsettled-findings result for the supplied scope.',
368
385
  },
369
386
  },
370
387
  lastError: undefined,
@@ -429,8 +446,16 @@ export const decideMachine = setup({
429
446
  initial: 'ready',
430
447
  context: () => ({}),
431
448
  output: ({ context }) => {
432
- if (context.reviewResult)
433
- return context.reviewResult;
449
+ if (context.reviewResult) {
450
+ if (!context.latestCommit) {
451
+ throw new Error('DECIDE completed without its decide-owned commit');
452
+ }
453
+ return {
454
+ decideCommit: context.latestCommit,
455
+ evaluatedRevision: context.reviewResult.evaluatedRevision,
456
+ noUnsettledFindings: true,
457
+ };
458
+ }
434
459
  if (!context.latestCommit || !context.reviewFailure) {
435
460
  throw new Error('DECIDE reached a final state without a result');
436
461
  }
@@ -498,7 +523,7 @@ export const decideMachine = setup({
498
523
  role: 'coder',
499
524
  prompt: INDEPENDENT_PROPOSAL_PROMPT,
500
525
  result: withNeedsBossReply({
501
- proposed: 'Coder completed an independent proposal. Output shall include `coderProposal: <verbatim final text>`.',
526
+ proposed: 'Coder affirmatively provided a complete design proposal; a progress report, status update, or promise of a later proposal supports no proposal outcome. Output shall include `coderProposal: <verbatim final text>`.',
502
527
  }),
503
528
  callerTopic: context.callerTopic,
504
529
  ...bossReplyFields(context, 'askCoderProposal'),
@@ -634,7 +659,7 @@ export const decideMachine = setup({
634
659
  role: 'reviewer',
635
660
  prompt: INDEPENDENT_PROPOSAL_PROMPT,
636
661
  result: withNeedsBossReply({
637
- proposed: 'Reviewer completed an independent proposal. Output shall include `reviewerProposal: <verbatim final text>`.',
662
+ proposed: 'Reviewer affirmatively provided a complete design proposal; a progress report, status update, or promise of a later proposal supports no proposal outcome. Output shall include `reviewerProposal: <verbatim final text>`.',
638
663
  }),
639
664
  callerTopic: context.callerTopic,
640
665
  ...bossReplyFields(context, 'askReviewerProposal'),
@@ -757,11 +782,11 @@ export const decideMachine = setup({
757
782
  commitCoderProposal: {
758
783
  id: 'commitCoderProposal',
759
784
  tags: 'playbook.busy',
760
- description: 'Coder writes and commits Coder’s independent proposal.',
785
+ description: 'Coder synthesizes both proposals and commits the design.',
761
786
  meta: {
762
787
  playbook: {
763
788
  stateId: 'commitCoderProposal',
764
- description: 'Coder writes and commits Coder’s independent proposal.',
789
+ description: 'Coder synthesizes both proposals and commits the design.',
765
790
  role: 'coder',
766
791
  },
767
792
  },
@@ -771,10 +796,12 @@ export const decideMachine = setup({
771
796
  stateId: 'commitCoderProposal',
772
797
  sourceItem: 'DECIDE-3',
773
798
  role: 'coder',
774
- prompt: COMMIT_CODER_PROMPT,
799
+ prompt: SYNTHESIZE_COMMIT_PROMPT,
775
800
  result: withNeedsBossReply({
776
- committed: "Coder committed Coder's proposal. Output shall include `coderOutput: <verbatim final text>` and `latestCommit: <commit identity>`.",
801
+ committed: 'Coder synthesized both proposals and committed the resulting design as one new commit. Output shall include `coderOutput: <verbatim final text>` and `latestCommit: <commit identity>`.',
777
802
  }),
803
+ callerTopic: context.callerTopic,
804
+ reviewerProposal: context.reviewerProposal,
778
805
  ...bossReplyFields(context, 'commitCoderProposal'),
779
806
  }),
780
807
  onDone: [
@@ -881,7 +908,7 @@ export const decideMachine = setup({
881
908
  stateId: 'reviewCommit',
882
909
  sourceItem: 'DECIDE-4',
883
910
  playbookId: 'review',
884
- text: composeReviewInput(context.callerTopic, context.coderProposal),
911
+ text: composeReviewInput(context.callerTopic, context.latestCommit, context.coderOutput),
885
912
  }),
886
913
  onDone: [
887
914
  {
@@ -43,7 +43,13 @@ type BossReplies = Partial<Record<ResumableStateId, string>>;
43
43
  type PendingBossQuestionParams = Omit<PendingBossQuestion, 'questionId'>;
44
44
 
45
45
  export interface ReviewSuccessOutput {
46
- approvedCommit: 'latest';
46
+ evaluatedRevision: string;
47
+ noUnsettledFindings: true;
48
+ }
49
+
50
+ export interface DecideSuccessOutput {
51
+ decideCommit: string;
52
+ evaluatedRevision: string;
47
53
  noUnsettledFindings: true;
48
54
  }
49
55
 
@@ -57,7 +63,7 @@ export interface DecideFailureOutput {
57
63
  };
58
64
  }
59
65
 
60
- export type DecideOutput = ReviewSuccessOutput | DecideFailureOutput;
66
+ export type DecideOutput = DecideSuccessOutput | DecideFailureOutput;
61
67
 
62
68
  interface ChildFailure {
63
69
  status: 'aborted' | 'error';
@@ -73,6 +79,7 @@ export interface DecideContext {
73
79
  coderProposal?: string;
74
80
  reviewerProposal?: string;
75
81
  latestCommit?: string;
82
+ coderOutput?: string;
76
83
  reviewResult?: ReviewSuccessOutput;
77
84
  reviewFailure?: ChildFailure;
78
85
  lastResult?: PlayerOutput;
@@ -105,6 +112,7 @@ export interface PlayerInput {
105
112
  result: Record<string, string>;
106
113
  role: Role;
107
114
  callerTopic?: string;
115
+ reviewerProposal?: string;
108
116
  pendingBossQuestion?: PendingBossQuestion;
109
117
  bossReply?: string;
110
118
  }
@@ -149,37 +157,48 @@ const INDEPENDENT_PROPOSAL_PROMPT = [
149
157
  'Do not change any files.',
150
158
  ].join('\n');
151
159
 
152
- const COMMIT_CODER_PROMPT = [
153
- 'Turn your proposal into the necessary spec items or DRs.',
160
+ const SYNTHESIZE_COMMIT_PROMPT = [
161
+ "Synthesize your independent proposal with Reviewer's proposal below.",
162
+ 'Keep to the original topic below and follow what it asks.',
163
+ 'Keep the best, essential parts of either proposal and reject any point that is unsound, unnecessary, or outside the topic.',
164
+ 'Turn the resulting design into the necessary DRs and/or spec items.',
154
165
  'Follow @specs/meta.md and update @specs/map.md when needed.',
155
- "Do not inspect or incorporate Reviewer's proposal before this commit.",
156
- 'Do not change code or implement the proposal.',
166
+ 'Do not change code or implement the design.',
157
167
  '',
158
168
  'Commit the result as one new commit, following @specs/packages/git.md.',
159
169
  'Make the commit message explain concisely what changed and why.',
160
- 'Coder is <coder-llm>; format the model token in conventional human form.',
170
+ 'Identify every new commit you make.',
171
+ 'Coder is <coder-llm> and Reviewer is <reviewer-llm>.',
172
+ '',
173
+ '> Original topic: <caller-topic>',
174
+ "> Reviewer's independent proposal: <reviewer-proposal>",
161
175
  ].join('\n');
162
176
 
163
177
  const REVIEW_INPUT_TEMPLATE = [
164
- 'Review the latest commit as a spec-design change against the initial intent.',
165
- 'Compare it with your independent proposal and take the best of both.',
166
- 'Make your suggestions.',
167
- '',
168
- 'Initial intent: <caller-topic>.',
169
- "Coder's independent proposal: <coder-proposal>.",
178
+ '> Original intent: <caller-topic>',
179
+ '> Review scope: the `decide`-owned commit <decide-commit> and its resulting repository state.',
180
+ '> Coder output: <coder-output>',
170
181
  ].join('\n');
171
182
 
172
183
  function composeReviewInput(
173
184
  callerTopic: string | undefined,
174
- coderProposal: string | undefined,
185
+ decideCommit: string | undefined,
186
+ coderOutput: string | undefined,
175
187
  ): string {
176
188
  const fields: Readonly<Record<string, string>> = {
177
189
  '<caller-topic>': callerTopic ?? '',
178
- '<coder-proposal>': coderProposal ?? '',
190
+ '<decide-commit>': decideCommit ?? '',
191
+ '<coder-output>': coderOutput ?? '',
179
192
  };
180
193
  return REVIEW_INPUT_TEMPLATE.replace(
181
- /<caller-topic>|<coder-proposal>/g,
182
- (placeholder) => fields[placeholder],
194
+ /<caller-topic>|<decide-commit>|<coder-output>/g,
195
+ (placeholder, offset: number, source: string) => {
196
+ const value = fields[placeholder];
197
+ const lineStart = source.lastIndexOf('\n', offset - 1) + 1;
198
+ return source.slice(lineStart, offset).startsWith('> ')
199
+ ? value.replaceAll('\n', '\n> ')
200
+ : value;
201
+ },
183
202
  );
184
203
  }
185
204
 
@@ -370,20 +389,24 @@ function isPlaybookState(value: unknown): boolean {
370
389
  );
371
390
  }
372
391
 
392
+ // DECIDE supplied `review` a scope rooted at the `decide`-owned commit, so a
393
+ // result applies to that scope only when it is the exact declared terminal
394
+ // contract: the evaluated repository revision plus the affirmative
395
+ // no-unsettled-findings fact. Any other shape does not establish those facts.
373
396
  function reviewSuccessFrom(event: unknown): ReviewSuccessOutput | undefined {
374
397
  const output = playbookOutputOf(event);
375
398
  if (!isPlainRecord(output)) return undefined;
376
- if (!isExactKeys(output, ['approvedCommit', 'noUnsettledFindings'])) {
399
+ if (!isExactKeys(output, ['evaluatedRevision', 'noUnsettledFindings'])) {
377
400
  return undefined;
378
401
  }
379
402
  if (
380
- output.approvedCommit !== 'latest' ||
403
+ !isNonEmptyString(output.evaluatedRevision) ||
381
404
  output.noUnsettledFindings !== true
382
405
  ) {
383
406
  return undefined;
384
407
  }
385
408
  return {
386
- approvedCommit: 'latest',
409
+ evaluatedRevision: output.evaluatedRevision,
387
410
  noUnsettledFindings: true,
388
411
  };
389
412
  }
@@ -537,6 +560,7 @@ export const decideMachine = setup({
537
560
  coderProposal: undefined,
538
561
  reviewerProposal: undefined,
539
562
  latestCommit: undefined,
563
+ coderOutput: undefined,
540
564
  reviewResult: undefined,
541
565
  reviewFailure: undefined,
542
566
  lastResult: undefined,
@@ -552,6 +576,7 @@ export const decideMachine = setup({
552
576
  coderProposal: undefined,
553
577
  reviewerProposal: undefined,
554
578
  latestCommit: undefined,
579
+ coderOutput: undefined,
555
580
  reviewResult: undefined,
556
581
  reviewFailure: undefined,
557
582
  lastResult: undefined,
@@ -583,6 +608,7 @@ export const decideMachine = setup({
583
608
  }),
584
609
  rememberCommit: assign({
585
610
  latestCommit: ({ event }) => outputOf(event).latestCommit,
611
+ coderOutput: ({ event }) => outputOf(event).coderOutput,
586
612
  lastResult: ({ event }) => outputOf(event),
587
613
  lastError: undefined,
588
614
  }),
@@ -603,7 +629,7 @@ export const decideMachine = setup({
603
629
  error: {
604
630
  name: 'ReviewProtocolError',
605
631
  message:
606
- 'REVIEW returned without an approved latest commit and a no-findings result.',
632
+ 'REVIEW returned without the evaluated repository revision and a no-unsettled-findings result for the supplied scope.',
607
633
  },
608
634
  },
609
635
  lastError: undefined,
@@ -678,7 +704,16 @@ export const decideMachine = setup({
678
704
  initial: 'ready',
679
705
  context: (): DecideContext => ({}),
680
706
  output: ({ context }): DecideOutput => {
681
- if (context.reviewResult) return context.reviewResult;
707
+ if (context.reviewResult) {
708
+ if (!context.latestCommit) {
709
+ throw new Error('DECIDE completed without its decide-owned commit');
710
+ }
711
+ return {
712
+ decideCommit: context.latestCommit,
713
+ evaluatedRevision: context.reviewResult.evaluatedRevision,
714
+ noUnsettledFindings: true,
715
+ };
716
+ }
682
717
  if (!context.latestCommit || !context.reviewFailure) {
683
718
  throw new Error('DECIDE reached a final state without a result');
684
719
  }
@@ -747,7 +782,7 @@ export const decideMachine = setup({
747
782
  prompt: INDEPENDENT_PROPOSAL_PROMPT,
748
783
  result: withNeedsBossReply({
749
784
  proposed:
750
- 'Coder completed an independent proposal. Output shall include `coderProposal: <verbatim final text>`.',
785
+ 'Coder affirmatively provided a complete design proposal; a progress report, status update, or promise of a later proposal supports no proposal outcome. Output shall include `coderProposal: <verbatim final text>`.',
751
786
  }),
752
787
  callerTopic: context.callerTopic,
753
788
  ...bossReplyFields(context, 'askCoderProposal'),
@@ -887,7 +922,7 @@ export const decideMachine = setup({
887
922
  prompt: INDEPENDENT_PROPOSAL_PROMPT,
888
923
  result: withNeedsBossReply({
889
924
  proposed:
890
- 'Reviewer completed an independent proposal. Output shall include `reviewerProposal: <verbatim final text>`.',
925
+ 'Reviewer affirmatively provided a complete design proposal; a progress report, status update, or promise of a later proposal supports no proposal outcome. Output shall include `reviewerProposal: <verbatim final text>`.',
891
926
  }),
892
927
  callerTopic: context.callerTopic,
893
928
  ...bossReplyFields(context, 'askReviewerProposal'),
@@ -1014,11 +1049,12 @@ export const decideMachine = setup({
1014
1049
  commitCoderProposal: {
1015
1050
  id: 'commitCoderProposal',
1016
1051
  tags: 'playbook.busy',
1017
- description: 'Coder writes and commits Coder’s independent proposal.',
1052
+ description: 'Coder synthesizes both proposals and commits the design.',
1018
1053
  meta: {
1019
1054
  playbook: {
1020
1055
  stateId: 'commitCoderProposal',
1021
- description: 'Coder writes and commits Coder’s independent proposal.',
1056
+ description:
1057
+ 'Coder synthesizes both proposals and commits the design.',
1022
1058
  role: 'coder',
1023
1059
  },
1024
1060
  },
@@ -1028,11 +1064,13 @@ export const decideMachine = setup({
1028
1064
  stateId: 'commitCoderProposal',
1029
1065
  sourceItem: 'DECIDE-3',
1030
1066
  role: 'coder',
1031
- prompt: COMMIT_CODER_PROMPT,
1067
+ prompt: SYNTHESIZE_COMMIT_PROMPT,
1032
1068
  result: withNeedsBossReply({
1033
1069
  committed:
1034
- "Coder committed Coder's proposal. Output shall include `coderOutput: <verbatim final text>` and `latestCommit: <commit identity>`.",
1070
+ 'Coder synthesized both proposals and committed the resulting design as one new commit. Output shall include `coderOutput: <verbatim final text>` and `latestCommit: <commit identity>`.',
1035
1071
  }),
1072
+ callerTopic: context.callerTopic,
1073
+ reviewerProposal: context.reviewerProposal,
1036
1074
  ...bossReplyFields(context, 'commitCoderProposal'),
1037
1075
  }),
1038
1076
  onDone: [
@@ -1141,7 +1179,8 @@ export const decideMachine = setup({
1141
1179
  playbookId: 'review',
1142
1180
  text: composeReviewInput(
1143
1181
  context.callerTopic,
1144
- context.coderProposal,
1182
+ context.latestCommit,
1183
+ context.coderOutput,
1145
1184
  ),
1146
1185
  }),
1147
1186
  onDone: [