@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
@@ -0,0 +1,120 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // SPDX-FileCopyrightText: 2026 SubLang International <https://sublang.ai>
3
+
4
+ import createPlaybookRuntime, {
5
+ type DevPlaybookHostCapabilities,
6
+ type PlaybookRuntime,
7
+ } from './dev.playbook.js';
8
+
9
+ export interface PlaybookSummaryPolicy {
10
+ stateCountLabels: Readonly<Record<string, string>>;
11
+ copyPasteGuardNames: readonly string[];
12
+ savedCountsLine(
13
+ counts: { interruptions: number; copyPastes: number },
14
+ rounds: number,
15
+ ): string;
16
+ }
17
+
18
+ export type DevOptions = Readonly<Record<string, never>>;
19
+
20
+ export interface DevPlaybookRegistryEntry {
21
+ id: 'dev';
22
+ command: 'dev';
23
+ intent: string;
24
+ artifactSchema: 3;
25
+ runtimeProfile: {
26
+ readonly kind: 'shared-factory';
27
+ readonly compat: {
28
+ readonly artifactSchema: 3;
29
+ readonly runtimeAbi: number;
30
+ };
31
+ };
32
+ requiredRoleIds: readonly ['analyst'];
33
+ concurrentRoleSets: readonly [];
34
+ summaryPolicy: PlaybookSummaryPolicy;
35
+ validateOptions(optionSlice: unknown): DevOptions;
36
+ createRuntime(
37
+ options: DevOptions,
38
+ hostCapabilities: DevPlaybookHostCapabilities,
39
+ ): PlaybookRuntime;
40
+ }
41
+
42
+ // CODE and DECIDE own and label the rounds of the paths DEV starts. DEV's
43
+ // suspended call states only delegate to those children and must not
44
+ // double-count their rounds; only the Analyst planning rounds are DEV's own.
45
+ export const devStateCountLabels = {
46
+ planAnalysis: 'planning round',
47
+ } as const;
48
+
49
+ export const devCopyPasteGuardNames = ['code', 'decideThenCode'] as const;
50
+
51
+ function countNoun(
52
+ count: number,
53
+ singular: string,
54
+ plural = `${singular}s`,
55
+ ): string {
56
+ return `${count} ${count === 1 ? singular : plural}`;
57
+ }
58
+
59
+ export function devSavedCountsLine(
60
+ counts: { interruptions: number; copyPastes: number },
61
+ rounds: number,
62
+ ): string {
63
+ return [
64
+ 'Saved you',
65
+ countNoun(counts.interruptions, 'interruption'),
66
+ 'and',
67
+ countNoun(counts.copyPastes, 'copy-paste'),
68
+ 'across',
69
+ countNoun(rounds, 'round'),
70
+ 'of planning.',
71
+ ].join(' ');
72
+ }
73
+
74
+ export const devSummaryPolicy: PlaybookSummaryPolicy = {
75
+ stateCountLabels: devStateCountLabels,
76
+ copyPasteGuardNames: devCopyPasteGuardNames,
77
+ savedCountsLine: devSavedCountsLine,
78
+ };
79
+
80
+ export function validateDevOptions(optionSlice: unknown): DevOptions {
81
+ if (optionSlice === undefined) return Object.freeze({});
82
+ if (
83
+ optionSlice === null ||
84
+ typeof optionSlice !== 'object' ||
85
+ Array.isArray(optionSlice)
86
+ ) {
87
+ throw new Error('captain.options.playbooks.dev.options must be an object');
88
+ }
89
+ const keys = Object.keys(optionSlice);
90
+ if (keys.length > 0) {
91
+ throw new Error(
92
+ `Unknown config field captain.options.playbooks.dev.options.${keys[0]}`,
93
+ );
94
+ }
95
+ return Object.freeze({});
96
+ }
97
+
98
+ export const devPlaybookRegistryEntry: DevPlaybookRegistryEntry = {
99
+ id: 'dev',
100
+ command: 'dev',
101
+ intent:
102
+ 'analyze a development request that needs planning before choosing direct implementation or a durable decision first',
103
+ artifactSchema: 3,
104
+ runtimeProfile: Object.freeze({
105
+ kind: 'shared-factory',
106
+ compat: createPlaybookRuntime.compat,
107
+ }),
108
+ requiredRoleIds: ['analyst'],
109
+ concurrentRoleSets: [],
110
+ summaryPolicy: devSummaryPolicy,
111
+ validateOptions: validateDevOptions,
112
+ createRuntime(options, hostCapabilities) {
113
+ return createPlaybookRuntime({
114
+ configuredOptions: options,
115
+ hostCapabilities,
116
+ });
117
+ },
118
+ };
119
+
120
+ export default devPlaybookRegistryEntry;
@@ -12,14 +12,24 @@ export interface PendingBossQuestion {
12
12
  question: string;
13
13
  }
14
14
  export interface ReviewOutput {
15
- approvedCommit: 'latest';
16
15
  noUnsettledFindings: true;
16
+ /**
17
+ * Exact receipt-derived repository revision at which the review scope was
18
+ * evaluated: the closing clean round's `unchanged` receipt observes it as
19
+ * HEAD — the last review-fix commit when one landed, or the caller-supplied
20
+ * scope revision when none did (DR-045).
21
+ */
22
+ evaluatedRevision: string;
17
23
  }
18
24
  export type ReviewInput = Readonly<Record<string, never>>;
19
25
  export interface ReviewContext {
20
26
  callerInput?: string;
21
27
  reviewerOutput?: string;
22
28
  coderOutput?: string;
29
+ /** Receipt-derived OID of the latest review-fix commit (never player prose). */
30
+ latestCommit?: string;
31
+ /** Observed HEAD of the closing clean round's unchanged receipt (DR-045). */
32
+ evaluatedRevision?: string;
23
33
  lastError?: unknown;
24
34
  pendingBossQuestion?: PendingBossQuestion;
25
35
  bossReply?: string;
@@ -45,6 +55,7 @@ export interface PlayerInput {
45
55
  callerInput?: string;
46
56
  reviewerOutput?: string;
47
57
  coderOutput?: string;
58
+ latestCommit?: string;
48
59
  pendingBossQuestion?: PendingBossQuestion;
49
60
  bossReply?: string;
50
61
  }
@@ -53,9 +64,11 @@ export type PlayerOutput = {
53
64
  reviewerOutput: string;
54
65
  } | {
55
66
  guard: 'noFindings';
67
+ evaluatedRevision: string;
56
68
  } | {
57
69
  guard: 'committed';
58
70
  coderOutput: string;
71
+ latestCommit: string;
59
72
  } | {
60
73
  guard: 'rejectedAll';
61
74
  coderOutput: string;
@@ -109,7 +122,7 @@ export declare const reviewMachine: import("xstate").StateMachine<ReviewContext,
109
122
  type: "rememberCoderOutput";
110
123
  params: import("xstate").NonReducibleUnknown;
111
124
  } | {
112
- type: "clearBossReplyContext";
125
+ type: "rememberEvaluatedRevision";
113
126
  params: import("xstate").NonReducibleUnknown;
114
127
  } | {
115
128
  type: "setPendingReviewInitial";
@@ -6,13 +6,16 @@ import { assign, fromPromise, setup } from 'xstate';
6
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
7
  const SHARED_REVIEW_INSTRUCTION = [
8
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
- 'For any rebuttal, accept or challenge it.',
11
- 'Treat as settled, and do not raise again, any finding in this review rejected twice with reasoning.',
9
+ 'Continue to identify issues or improvements, if any, without duplication.',
10
+ 'Number the findings consistently across rounds.',
12
11
  'Flag only what materially affects correctness, behavior, or spec quality — not style, equally valid alternatives, or theoretical threats.',
13
- 'For specs, flag stale, missing, over-specified, or under-specified ones.',
12
+ 'For specs, flag stale, missing, over-specified, or under-specified ones, if any.',
14
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.',
15
14
  '',
15
+ 'If an issue represents a class of defect, find every instance within the review scope worth fixing rather than surfacing one or two per round, which drags out the review.',
16
+ 'For any rebuttal, accept or challenge it.',
17
+ 'Treat as settled, and do not raise again, any finding in this review rejected twice with reasoning.',
18
+ '',
16
19
  'Do not re-run tests or builds whose inputs have not changed since any previous reported run.',
17
20
  'Do not edit files or commit; report findings only.',
18
21
  '',
@@ -20,69 +23,79 @@ const SHARED_REVIEW_INSTRUCTION = [
20
23
  'Consult @specs/meta.md for spec requirements if needed; verify affected specs follow it.',
21
24
  ].join('\n');
22
25
  const INITIAL_REVIEW_PROMPT = [
23
- 'A new review begins on the latest commit.',
24
- 'Review the latest commit and resulting repository state; read the commit message first for its intent, scope, and rationale.',
26
+ 'A new review begins for the review scope.',
27
+ 'Keep to the original intent and follow what it asks.',
28
+ 'When the scope names commits, read each commit message for its context and rationale; otherwise use repository history and commit messages wherever they help establish that context.',
25
29
  '',
26
30
  '> <caller-input>',
27
31
  '',
28
32
  SHARED_REVIEW_INSTRUCTION,
29
33
  ].join('\n');
30
34
  const POST_COMMIT_REVIEW_PROMPT = [
31
- 'Review the latest commit and resulting repository state; read the commit message first for its intent, scope, and rationale.',
35
+ 'A new review round begins for the review scope in the cumulative committed state, with particular attention to the latest review-fix commit.',
36
+ 'Keep to the original intent and follow what it asks.',
37
+ "Read the latest review-fix commit's message and see Coder's feedback below.",
32
38
  '',
39
+ '> <caller-input>',
40
+ '> <latest-commit>',
33
41
  '> <coder-output>',
34
42
  '',
35
43
  SHARED_REVIEW_INSTRUCTION,
36
44
  ].join('\n');
37
45
  const REBUTTAL_REVIEW_PROMPT = [
38
46
  'No new commit was made because Coder rejected every finding.',
47
+ "See Coder's feedback below.",
39
48
  '',
49
+ '> <caller-input>',
40
50
  '> <coder-output>',
41
51
  '',
42
52
  SHARED_REVIEW_INSTRUCTION,
43
53
  ].join('\n');
44
54
  const CODER_DISPOSITION_PROMPT = [
55
+ '> <caller-input>',
45
56
  '> <reviewer-output>',
46
57
  '',
47
58
  'For each review item, accept or reject it.',
48
59
  'Before deciding, understand the full picture and think systematically about the underlying design.',
60
+ 'Keep to the original intent and follow what it asks.',
49
61
  'Reject anything that is not essential or is not worth fixing now.',
50
- 'If you accept an item, fix its root cause, including any fundamental design flaw - do not patch around it.',
62
+ 'If you accept an item, fix its root cause, including any fundamental design flaw do not patch around it; if it represents a class of defect, find every instance within the review scope worth fixing rather than addressing one or two per round, which drags out the review.',
51
63
  'If you reject an item, give the reasoning and cite code or test output that supports it.',
52
64
  'Do not re-run tests or builds whose inputs have not changed since any previous reported run.',
53
65
  '',
54
- 'If you accept any item, make minimal changes and commit them as one new commit; never amend the reviewed commit.',
66
+ 'If you accept any item, make minimal changes and add one new review-fix commit; never rewrite any existing commit.',
55
67
  'Follow @specs/packages/git.md.',
56
68
  'Make the commit message explain concisely what changed and why, including relevant verification.',
57
- 'Coder is <coder-llm>; Reviewer is <reviewer-llm>; format model tokens in conventional human form.',
69
+ 'Identify every new commit you make.',
70
+ 'Coder is <coder-llm>; Reviewer is <reviewer-llm>.',
58
71
  '',
59
72
  'If you reject every item, change nothing and make no commit.',
60
73
  'Report every disposition, all relevant run results, and every rebuttal.',
61
74
  ].join('\n');
62
75
  const REVIEW_RESULTS = {
63
76
  hasFindings: 'Reviewer raised one or more unsettled findings. Output shall include `reviewerOutput: <verbatim final text>`.',
64
- noFindings: 'Reviewer raised no unsettled findings.',
77
+ noFindings: 'Reviewer affirmatively reported the requested review complete with no unsettled findings remaining; a progress report, status update, or promise of a later result supports no review outcome. Output shall include `evaluatedRevision: <repository revision>`.',
65
78
  needsBossReply: NEEDS_BOSS_REPLY_DESCRIPTION,
66
79
  };
67
80
  const REBUTTAL_RESULTS = {
68
81
  hasFindings: 'Reviewer kept one or more unsettled findings. Output shall include `reviewerOutput: <verbatim final text>`.',
69
- noFindings: 'Reviewer accepted the rebuttals and no unsettled findings remain.',
82
+ noFindings: 'Reviewer accepted the rebuttals and affirmatively reported the requested review complete with no unsettled findings remaining; a progress report, status update, or promise of a later result supports no review outcome. Output shall include `evaluatedRevision: <repository revision>`.',
70
83
  needsBossReply: NEEDS_BOSS_REPLY_DESCRIPTION,
71
84
  };
72
85
  const CODER_RESULTS = {
73
- committed: 'Coder accepted at least one item and made one new commit. Output shall include `coderOutput: <verbatim final text>`.',
86
+ committed: 'Coder accepted at least one item and added one new review-fix commit. Output shall include `coderOutput: <verbatim final text>` and `latestCommit: <commit identity>`.',
74
87
  rejectedAll: 'Coder rejected every item and made no commit. Output shall include `coderOutput: <verbatim final text>`.',
75
88
  needsBossReply: NEEDS_BOSS_REPLY_DESCRIPTION,
76
89
  };
77
90
  const stateDescriptions = {
78
- ready: 'Waits for a caller to start a committed-work review.',
79
- reviewInitial: 'REVIEW-1: Reviewer examines the latest commit against the caller input.',
91
+ ready: 'Waits for a caller to start a scoped committed-work review.',
92
+ reviewInitial: "REVIEW-1: Reviewer reviews the caller's review scope against the original intent.",
80
93
  addressFindings: 'REVIEW-2: Coder accepts or rejects every current review finding.',
81
- reviewAfterCommit: 'REVIEW-3: Reviewer examines the new review-fix commit and repository state.',
94
+ reviewAfterCommit: 'REVIEW-3: Reviewer reviews the cumulative committed state after a review-fix commit.',
82
95
  reviewAfterRebuttal: 'REVIEW-4: Reviewer adjudicates an all-rejected Coder disposition.',
83
96
  awaitBossReply: 'Waits for Boss to answer the active player question.',
84
97
  failed: 'Retains a recoverable REVIEW failure for the caller.',
85
- done: 'The latest commit is approved with no unsettled findings.',
98
+ done: 'The requested review is complete: no unsettled findings remain within the review scope.',
86
99
  };
87
100
  const playbookMeta = (stateId, role) => ({
88
101
  playbook: {
@@ -130,10 +143,16 @@ export const reviewMachine = setup({
130
143
  return (output.guard === 'hasFindings' &&
131
144
  isNonEmptyString(output.reviewerOutput));
132
145
  },
133
- noFindings: ({ event }) => outputOf(event).guard === 'noFindings',
146
+ noFindings: ({ event }) => {
147
+ const output = outputOf(event);
148
+ return (output.guard === 'noFindings' &&
149
+ isNonEmptyString(output.evaluatedRevision));
150
+ },
134
151
  committed: ({ event }) => {
135
152
  const output = outputOf(event);
136
- return (output.guard === 'committed' && isNonEmptyString(output.coderOutput));
153
+ return (output.guard === 'committed' &&
154
+ isNonEmptyString(output.coderOutput) &&
155
+ isNonEmptyString(output.latestCommit));
137
156
  },
138
157
  rejectedAll: ({ event }) => {
139
158
  const output = outputOf(event);
@@ -175,6 +194,8 @@ export const reviewMachine = setup({
175
194
  callerInput: ({ event }) => event.type === 'START_REVIEW' ? event.callerInput : undefined,
176
195
  reviewerOutput: undefined,
177
196
  coderOutput: undefined,
197
+ latestCommit: undefined,
198
+ evaluatedRevision: undefined,
178
199
  lastError: undefined,
179
200
  pendingBossQuestion: undefined,
180
201
  bossReply: undefined,
@@ -183,6 +204,8 @@ export const reviewMachine = setup({
183
204
  callerInput: ({ event }) => event.type === 'BOSS_INTERRUPT' ? event.bossIntent : undefined,
184
205
  reviewerOutput: undefined,
185
206
  coderOutput: undefined,
207
+ latestCommit: undefined,
208
+ evaluatedRevision: undefined,
186
209
  lastError: undefined,
187
210
  pendingBossQuestion: undefined,
188
211
  bossReply: undefined,
@@ -205,11 +228,29 @@ export const reviewMachine = setup({
205
228
  ? output.coderOutput
206
229
  : undefined;
207
230
  },
231
+ // The receipt-derived review-fix commit identity replaces the prior one
232
+ // only when Coder committed; an all-rejected round leaves the evaluated
233
+ // revision unchanged.
234
+ latestCommit: ({ context, event }) => {
235
+ const output = outputOf(event);
236
+ return output.guard === 'committed'
237
+ ? output.latestCommit
238
+ : context.latestCommit;
239
+ },
208
240
  lastError: undefined,
209
241
  pendingBossQuestion: undefined,
210
242
  bossReply: undefined,
211
243
  }),
212
- clearBossReplyContext: assign({
244
+ rememberEvaluatedRevision: assign({
245
+ // DR-045: the closing clean round's unchanged receipt observes the
246
+ // exact revision the review scope was evaluated at.
247
+ evaluatedRevision: ({ context, event }) => {
248
+ const output = outputOf(event);
249
+ return output.guard === 'noFindings'
250
+ ? output.evaluatedRevision
251
+ : context.evaluatedRevision;
252
+ },
253
+ lastError: undefined,
213
254
  pendingBossQuestion: undefined,
214
255
  bossReply: undefined,
215
256
  }),
@@ -345,7 +386,7 @@ export const reviewMachine = setup({
345
386
  acceptedOutcome: 'noFindings',
346
387
  },
347
388
  },
348
- 'clearBossReplyContext',
389
+ 'rememberEvaluatedRevision',
349
390
  ],
350
391
  },
351
392
  {
@@ -383,6 +424,7 @@ export const reviewMachine = setup({
383
424
  role: 'coder',
384
425
  prompt: CODER_DISPOSITION_PROMPT,
385
426
  result: CODER_RESULTS,
427
+ callerInput: context.callerInput,
386
428
  reviewerOutput: context.reviewerOutput,
387
429
  pendingBossQuestion: context.pendingBossQuestion,
388
430
  bossReply: context.bossReply,
@@ -453,6 +495,8 @@ export const reviewMachine = setup({
453
495
  role: 'reviewer',
454
496
  prompt: POST_COMMIT_REVIEW_PROMPT,
455
497
  result: REVIEW_RESULTS,
498
+ callerInput: context.callerInput,
499
+ latestCommit: context.latestCommit,
456
500
  coderOutput: context.coderOutput,
457
501
  pendingBossQuestion: context.pendingBossQuestion,
458
502
  bossReply: context.bossReply,
@@ -485,7 +529,7 @@ export const reviewMachine = setup({
485
529
  acceptedOutcome: 'noFindings',
486
530
  },
487
531
  },
488
- 'clearBossReplyContext',
532
+ 'rememberEvaluatedRevision',
489
533
  ],
490
534
  },
491
535
  {
@@ -523,6 +567,7 @@ export const reviewMachine = setup({
523
567
  role: 'reviewer',
524
568
  prompt: REBUTTAL_REVIEW_PROMPT,
525
569
  result: REBUTTAL_RESULTS,
570
+ callerInput: context.callerInput,
526
571
  coderOutput: context.coderOutput,
527
572
  pendingBossQuestion: context.pendingBossQuestion,
528
573
  bossReply: context.bossReply,
@@ -555,7 +600,7 @@ export const reviewMachine = setup({
555
600
  acceptedOutcome: 'noFindings',
556
601
  },
557
602
  },
558
- 'clearBossReplyContext',
603
+ 'rememberEvaluatedRevision',
559
604
  ],
560
605
  },
561
606
  {
@@ -644,8 +689,13 @@ export const reviewMachine = setup({
644
689
  type: 'final',
645
690
  },
646
691
  },
647
- output: () => ({
648
- approvedCommit: 'latest',
649
- noUnsettledFindings: true,
650
- }),
692
+ output: ({ context }) => {
693
+ if (!isNonEmptyString(context.evaluatedRevision)) {
694
+ throw new Error('REVIEW reached done without a receipt-observed evaluated revision');
695
+ }
696
+ return {
697
+ noUnsettledFindings: true,
698
+ evaluatedRevision: context.evaluatedRevision,
699
+ };
700
+ },
651
701
  });