@sublang/playbook 7.0.0 → 9.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 (62) hide show
  1. package/README.md +20 -7
  2. package/docs/cli.md +88 -43
  3. package/docs/configuration.md +221 -119
  4. package/docs/embedding.md +78 -27
  5. package/package.json +4 -3
  6. package/reference/sdlc/captain.playbook/captain.playbook.js +16 -5
  7. package/reference/sdlc/captain.playbook/captain.playbook.ts +20 -6
  8. package/reference/sdlc/code.md +1 -1
  9. package/reference/sdlc/code.playbook/bin/interactive-session.js +816 -0
  10. package/reference/sdlc/code.playbook/bin/launch-config.js +1078 -116
  11. package/reference/sdlc/code.playbook/bin/playbook.js +489 -34
  12. package/reference/sdlc/code.playbook/bin/run.js +283 -298
  13. package/reference/sdlc/code.playbook/bin/session-store.js +818 -26
  14. package/reference/sdlc/code.playbook/code.fsm.d.ts +9 -6
  15. package/reference/sdlc/code.playbook/code.fsm.introspect.js +2 -2
  16. package/reference/sdlc/code.playbook/code.fsm.introspect.ts +2 -2
  17. package/reference/sdlc/code.playbook/code.fsm.js +18 -15
  18. package/reference/sdlc/code.playbook/code.fsm.ts +21 -21
  19. package/reference/sdlc/code.playbook/code.gears.md +1 -1
  20. package/reference/sdlc/code.playbook/code.playbook.d.ts +2 -1
  21. package/reference/sdlc/code.playbook/code.playbook.js +25 -15
  22. package/reference/sdlc/code.playbook/code.playbook.ts +34 -17
  23. package/reference/sdlc/code.playbook/code.registry.d.ts +5 -13
  24. package/reference/sdlc/code.playbook/code.registry.js +3 -10
  25. package/reference/sdlc/code.playbook/code.registry.ts +7 -32
  26. package/reference/sdlc/code.playbook/playbook-captain.d.ts +39 -14
  27. package/reference/sdlc/code.playbook/playbook-captain.js +1014 -299
  28. package/reference/sdlc/code.playbook/playbook-captain.ts +1450 -406
  29. package/reference/sdlc/code.playbook/playbook.config.template.yaml +41 -49
  30. package/reference/sdlc/decide.md +4 -4
  31. package/reference/sdlc/decide.playbook/decide.fsm.d.ts +10 -10
  32. package/reference/sdlc/decide.playbook/decide.fsm.js +21 -14
  33. package/reference/sdlc/decide.playbook/decide.fsm.ts +27 -23
  34. package/reference/sdlc/decide.playbook/decide.gears.md +3 -5
  35. package/reference/sdlc/decide.playbook/decide.playbook.d.ts +11 -13
  36. package/reference/sdlc/decide.playbook/decide.playbook.js +465 -246
  37. package/reference/sdlc/decide.playbook/decide.playbook.ts +623 -283
  38. package/reference/sdlc/decide.playbook/decide.registry.d.ts +5 -13
  39. package/reference/sdlc/decide.playbook/decide.registry.js +3 -9
  40. package/reference/sdlc/decide.playbook/decide.registry.ts +7 -31
  41. package/reference/sdlc/review.md +4 -5
  42. package/reference/sdlc/review.playbook/review.fsm.d.ts +9 -11
  43. package/reference/sdlc/review.playbook/review.fsm.js +30 -24
  44. package/reference/sdlc/review.playbook/review.fsm.ts +39 -35
  45. package/reference/sdlc/review.playbook/review.gears.md +6 -5
  46. package/reference/sdlc/review.playbook/review.playbook.d.ts +2 -1
  47. package/reference/sdlc/review.playbook/review.playbook.js +29 -23
  48. package/reference/sdlc/review.playbook/review.playbook.ts +38 -28
  49. package/reference/sdlc/review.playbook/review.registry.d.ts +5 -13
  50. package/reference/sdlc/review.playbook/review.registry.js +3 -16
  51. package/reference/sdlc/review.playbook/review.registry.ts +7 -38
  52. package/slc/gears2fsm.md +45 -24
  53. package/slc/link.md +297 -135
  54. package/slc/text2gears.md +19 -18
  55. package/src/runtime.d.ts +21 -16
  56. package/src/runtime.ts +20 -23
  57. package/src/xstate-playbook-runtime.d.ts +34 -20
  58. package/src/xstate-playbook-runtime.js +973 -400
  59. package/src/xstate-playbook-runtime.ts +1203 -457
  60. package/src/xstate-runtime.d.ts +17 -7
  61. package/src/xstate-runtime.js +198 -81
  62. package/src/xstate-runtime.ts +339 -112
@@ -1,9 +1,10 @@
1
+ import { type XStatePromptIdentity } from '@sublang/playbook/xstate-runtime';
1
2
  import { type PlayerInput, type ReviewInput } from './review.fsm.js';
2
3
  import type { CaptainCallOptions, CaptainResult, JsonValue, NormalizedError, PlayerCallOptions, PlaybookCallRequest, PlaybookCallResult, PlaybookCallStart, PlaybookControlReceipt, PlaybookControlView, PlaybookPendingCall, PlaybookPorts, PlaybookRunResult, PlaybookRuntime, PlaybookRuntimeFactory, PlaybookRuntimeSnapshot, PlaybookSession, PlaybookState, PlaybookStateValue, PlaybookTraceEvent, PlaybookTraceType, PlayerResult, PlayerSessionStore } from '@sublang/playbook/runtime';
3
4
  export type { CaptainCallOptions, CaptainResult, JsonValue, NormalizedError, PlayerCallOptions, PlaybookCallRequest, PlaybookCallResult, PlaybookCallStart, PlaybookControlReceipt, PlaybookControlView, PlaybookPendingCall, PlaybookPorts, PlaybookRunResult, PlaybookRuntime, PlaybookRuntimeFactory, PlaybookRuntimeSnapshot, PlaybookSession, PlaybookState, PlaybookStateValue, PlaybookTraceEvent, PlaybookTraceType, PlayerResult, PlayerSessionStore, };
4
5
  export type ReviewPlaybookOptions = ReviewInput;
5
6
  /** Keep every line of a relayed runtime value inside its authored quote. */
6
- declare function composePlayerPrompt(input: PlayerInput): string;
7
+ declare function composePlayerPrompt(input: PlayerInput, promptIdentity: XStatePromptIdentity): string;
7
8
  export declare const _internal: {
8
9
  composePlayerPrompt: typeof composePlayerPrompt;
9
10
  VERBATIM_PAYLOAD_FIELDS: ReadonlySet<string>;
@@ -3,15 +3,14 @@
3
3
  //
4
4
  // Generated by slc/link.md (FSM-to-Runtime linker).
5
5
  // Source FSM: ./review.fsm.ts
6
- // Player bind: Coder→coder, Reviewer→reviewer (default lowercased binding)
6
+ // Role metadata: Coder→coder, Reviewer→reviewer (canonical local roles)
7
7
  // Boss event: deterministic START_REVIEW entry; exact Boss text becomes
8
8
  // callerInput; pending player questions retain BOSS_REPLY
9
9
  // Adjudication: LLM judge per state; coderOutput and reviewerOutput are
10
10
  // carried verbatim
11
- // Compat: artifact schema 1 / runtime ABI 1
12
- import { RUNTIME_ABI, createXStatePlaybookRuntime, snapshotJsonValue, } from '@sublang/playbook/xstate-runtime';
11
+ // Compat: artifact schema 2 / runtime ABI 1
12
+ import { createXStatePlaybookRuntime, snapshotJsonValue, } from '@sublang/playbook/xstate-runtime';
13
13
  import { reviewMachine, } from './review.fsm.js';
14
- const OPTION_KEYS = new Set(['coderLlm', 'reviewerLlm']);
15
14
  const PLACEHOLDER = /<(#|[A-Za-z_$][A-Za-z0-9_$-]*)>/g;
16
15
  const CONTINUATION_PREAMBLE = 'You previously paused this task to ask Boss a question; Boss has now replied. Continue the same task using the reply below.';
17
16
  const VERBATIM_PAYLOAD_FIELDS = new Set([
@@ -27,15 +26,7 @@ function snapshotReviewOptions(value) {
27
26
  }
28
27
  const record = captured;
29
28
  for (const key of Object.keys(record)) {
30
- if (!OPTION_KEYS.has(key)) {
31
- throw new TypeError(`REVIEW runtime options.${key} is not declared`);
32
- }
33
- }
34
- for (const key of OPTION_KEYS) {
35
- const option = record[key];
36
- if (typeof option !== 'string' || option.trim().length === 0) {
37
- throw new TypeError(`REVIEW runtime options.${key} must be a non-empty string`);
38
- }
29
+ throw new TypeError(`REVIEW runtime options.${key} is not declared`);
39
30
  }
40
31
  return captured;
41
32
  }
@@ -48,10 +39,14 @@ function quotedContinuation(value) {
48
39
  return value.replaceAll('\n', '\n> ');
49
40
  }
50
41
  /** Keep every line of a relayed runtime value inside its authored quote. */
51
- function composePlayerPrompt(input) {
42
+ function composePlayerPrompt(input, promptIdentity) {
52
43
  const fields = input;
53
44
  const body = input.prompt.replace(PLACEHOLDER, (match, token, offset, source) => {
54
- const value = fields[placeholderField(token)];
45
+ const value = token === 'coder-llm'
46
+ ? promptIdentity('coder')
47
+ : token === 'reviewer-llm'
48
+ ? promptIdentity('reviewer')
49
+ : fields[placeholderField(token)];
55
50
  if (typeof value !== 'string')
56
51
  return match;
57
52
  const lineStart = source.lastIndexOf('\n', offset - 1) + 1;
@@ -76,28 +71,39 @@ export const _internal = {
76
71
  };
77
72
  const runtimeSpec = {
78
73
  label: 'REVIEW',
79
- compat: { artifactSchema: 1, runtimeAbi: RUNTIME_ABI },
74
+ // DR-022 / slc/link.md: the declaration carries the value current at link
75
+ // time — a literal, never the loading engine's RUNTIME_ABI self-report,
76
+ // which would follow whatever engine loads the module and make the
77
+ // factory's skew check compare that engine with itself.
78
+ compat: { artifactSchema: 2, runtimeAbi: 1 },
80
79
  snapshotOptions: snapshotReviewOptions,
81
- entryEvent: { type: 'START_REVIEW', textField: 'callerInput' },
82
- playerStates: {
80
+ entryEvent: {
81
+ type: 'START_REVIEW',
82
+ textField: 'callerInput',
83
+ // `copyStartInput` copies the entry text here, so the failure-state
84
+ // retry reads it back from the persisted machine snapshot and survives
85
+ // a continued session (DR-034).
86
+ contextField: 'callerInput',
87
+ },
88
+ roleStates: {
83
89
  reviewInitial: {
84
- player: 'Reviewer',
90
+ role: 'reviewer',
85
91
  label: 'REVIEW-1: Reviewer examines the latest commit against the caller input.',
86
92
  },
87
93
  addressFindings: {
88
- player: 'Coder',
94
+ role: 'coder',
89
95
  label: 'REVIEW-2: Coder accepts or rejects every current review finding.',
90
96
  },
91
97
  reviewAfterCommit: {
92
- player: 'Reviewer',
98
+ role: 'reviewer',
93
99
  label: 'REVIEW-3: Reviewer examines the new review-fix commit and repository state.',
94
100
  },
95
101
  reviewAfterRebuttal: {
96
- player: 'Reviewer',
102
+ role: 'reviewer',
97
103
  label: 'REVIEW-4: Reviewer adjudicates an all-rejected Coder disposition.',
98
104
  },
99
105
  },
100
- composePlayerPrompt: (input) => composePlayerPrompt(input),
106
+ composePlayerPrompt: (input, promptIdentity) => composePlayerPrompt(input, promptIdentity),
101
107
  verbatimPayloadFields: VERBATIM_PAYLOAD_FIELDS,
102
108
  controlContextFields: [],
103
109
  transitionEventFields: [
@@ -3,18 +3,18 @@
3
3
  //
4
4
  // Generated by slc/link.md (FSM-to-Runtime linker).
5
5
  // Source FSM: ./review.fsm.ts
6
- // Player bind: Coder→coder, Reviewer→reviewer (default lowercased binding)
6
+ // Role metadata: Coder→coder, Reviewer→reviewer (canonical local roles)
7
7
  // Boss event: deterministic START_REVIEW entry; exact Boss text becomes
8
8
  // callerInput; pending player questions retain BOSS_REPLY
9
9
  // Adjudication: LLM judge per state; coderOutput and reviewerOutput are
10
10
  // carried verbatim
11
- // Compat: artifact schema 1 / runtime ABI 1
11
+ // Compat: artifact schema 2 / runtime ABI 1
12
12
 
13
13
  import {
14
- RUNTIME_ABI,
15
14
  createXStatePlaybookRuntime,
16
15
  snapshotJsonValue,
17
16
  type PlaybookPlayerInput,
17
+ type XStatePromptIdentity,
18
18
  type XStatePlaybookRuntimeSpec,
19
19
  } from '@sublang/playbook/xstate-runtime';
20
20
  import {
@@ -76,7 +76,6 @@ export type {
76
76
 
77
77
  export type ReviewPlaybookOptions = ReviewInput;
78
78
 
79
- const OPTION_KEYS = new Set(['coderLlm', 'reviewerLlm']);
80
79
  const PLACEHOLDER = /<(#|[A-Za-z_$][A-Za-z0-9_$-]*)>/g;
81
80
  const CONTINUATION_PREAMBLE =
82
81
  'You previously paused this task to ask Boss a question; Boss has now replied. Continue the same task using the reply below.';
@@ -97,17 +96,7 @@ function snapshotReviewOptions(value: unknown): ReviewPlaybookOptions {
97
96
  }
98
97
  const record = captured as Readonly<Record<string, JsonValue>>;
99
98
  for (const key of Object.keys(record)) {
100
- if (!OPTION_KEYS.has(key)) {
101
- throw new TypeError(`REVIEW runtime options.${key} is not declared`);
102
- }
103
- }
104
- for (const key of OPTION_KEYS) {
105
- const option = record[key];
106
- if (typeof option !== 'string' || option.trim().length === 0) {
107
- throw new TypeError(
108
- `REVIEW runtime options.${key} must be a non-empty string`,
109
- );
110
- }
99
+ throw new TypeError(`REVIEW runtime options.${key} is not declared`);
111
100
  }
112
101
  return captured as unknown as ReviewPlaybookOptions;
113
102
  }
@@ -124,12 +113,20 @@ function quotedContinuation(value: string): string {
124
113
  }
125
114
 
126
115
  /** Keep every line of a relayed runtime value inside its authored quote. */
127
- function composePlayerPrompt(input: PlayerInput): string {
116
+ function composePlayerPrompt(
117
+ input: PlayerInput,
118
+ promptIdentity: XStatePromptIdentity,
119
+ ): string {
128
120
  const fields = input as unknown as Record<string, unknown>;
129
121
  const body = input.prompt.replace(
130
122
  PLACEHOLDER,
131
123
  (match: string, token: string, offset: number, source: string) => {
132
- const value = fields[placeholderField(token)];
124
+ const value =
125
+ token === 'coder-llm'
126
+ ? promptIdentity('coder')
127
+ : token === 'reviewer-llm'
128
+ ? promptIdentity('reviewer')
129
+ : fields[placeholderField(token)];
133
130
  if (typeof value !== 'string') return match;
134
131
  const lineStart = source.lastIndexOf('\n', offset - 1) + 1;
135
132
  return source.slice(lineStart, offset) === '> '
@@ -156,34 +153,47 @@ export const _internal = {
156
153
  VERBATIM_PAYLOAD_FIELDS,
157
154
  };
158
155
 
159
- const runtimeSpec: XStatePlaybookRuntimeSpec<ReviewPlaybookOptions> = {
156
+ const runtimeSpec = {
160
157
  label: 'REVIEW',
161
- compat: { artifactSchema: 1, runtimeAbi: RUNTIME_ABI },
158
+ // DR-022 / slc/link.md: the declaration carries the value current at link
159
+ // time — a literal, never the loading engine's RUNTIME_ABI self-report,
160
+ // which would follow whatever engine loads the module and make the
161
+ // factory's skew check compare that engine with itself.
162
+ compat: { artifactSchema: 2, runtimeAbi: 1 },
162
163
  snapshotOptions: snapshotReviewOptions,
163
- entryEvent: { type: 'START_REVIEW', textField: 'callerInput' },
164
- playerStates: {
164
+ entryEvent: {
165
+ type: 'START_REVIEW',
166
+ textField: 'callerInput',
167
+ // `copyStartInput` copies the entry text here, so the failure-state
168
+ // retry reads it back from the persisted machine snapshot and survives
169
+ // a continued session (DR-034).
170
+ contextField: 'callerInput',
171
+ },
172
+ roleStates: {
165
173
  reviewInitial: {
166
- player: 'Reviewer',
174
+ role: 'reviewer',
167
175
  label:
168
176
  'REVIEW-1: Reviewer examines the latest commit against the caller input.',
169
177
  },
170
178
  addressFindings: {
171
- player: 'Coder',
179
+ role: 'coder',
172
180
  label: 'REVIEW-2: Coder accepts or rejects every current review finding.',
173
181
  },
174
182
  reviewAfterCommit: {
175
- player: 'Reviewer',
183
+ role: 'reviewer',
176
184
  label:
177
185
  'REVIEW-3: Reviewer examines the new review-fix commit and repository state.',
178
186
  },
179
187
  reviewAfterRebuttal: {
180
- player: 'Reviewer',
188
+ role: 'reviewer',
181
189
  label:
182
190
  'REVIEW-4: Reviewer adjudicates an all-rejected Coder disposition.',
183
191
  },
184
192
  },
185
- composePlayerPrompt: (input: PlaybookPlayerInput) =>
186
- composePlayerPrompt(input as PlayerInput),
193
+ composePlayerPrompt: (
194
+ input: PlaybookPlayerInput,
195
+ promptIdentity: XStatePromptIdentity,
196
+ ) => composePlayerPrompt(input as PlayerInput, promptIdentity),
187
197
  verbatimPayloadFields: VERBATIM_PAYLOAD_FIELDS,
188
198
  controlContextFields: [],
189
199
  transitionEventFields: [
@@ -193,7 +203,7 @@ const runtimeSpec: XStatePlaybookRuntimeSpec<ReviewPlaybookOptions> = {
193
203
  'answer',
194
204
  'questionId',
195
205
  ],
196
- };
206
+ } satisfies XStatePlaybookRuntimeSpec<ReviewPlaybookOptions>;
197
207
 
198
208
  const createPlaybookRuntime: PlaybookRuntimeFactory<ReviewPlaybookOptions> =
199
209
  createXStatePlaybookRuntime(reviewMachine, runtimeSpec);
@@ -1,4 +1,4 @@
1
- import { type PlaybookRuntime, type ReviewPlaybookOptions } from './review.playbook.js';
1
+ import { type PlaybookRuntime } from './review.playbook.js';
2
2
  export interface PlaybookSummaryPolicy {
3
3
  stateCountLabels: Readonly<Record<string, string>>;
4
4
  copyPasteGuardNames: readonly string[];
@@ -7,24 +7,17 @@ export interface PlaybookSummaryPolicy {
7
7
  copyPastes: number;
8
8
  }, rounds: number): string;
9
9
  }
10
- export interface RegistryPlayer {
11
- id: string;
12
- adapter?: string;
13
- model?: string;
14
- }
15
- export interface CreateReviewRuntimeOptions {
16
- captainOptions: unknown;
17
- players: readonly RegistryPlayer[];
18
- }
19
10
  export type ReviewOptions = Readonly<Record<string, never>>;
20
11
  export interface ReviewPlaybookRegistryEntry {
21
12
  id: 'review';
22
13
  command: 'review';
23
14
  intent: string;
15
+ artifactSchema: 2;
24
16
  requiredRoleIds: readonly ['coder', 'reviewer'];
17
+ concurrentRoleSets: readonly [];
25
18
  summaryPolicy: PlaybookSummaryPolicy;
26
- validateOptions(captainOptions: unknown): ReviewOptions;
27
- createRuntime(options: CreateReviewRuntimeOptions): PlaybookRuntime;
19
+ validateOptions(optionSlice: unknown): ReviewOptions;
20
+ createRuntime(options: ReviewOptions): PlaybookRuntime;
28
21
  }
29
22
  export declare const reviewStateCountLabels: {
30
23
  readonly reviewInitial: "review round";
@@ -38,6 +31,5 @@ export declare function reviewSavedCountsLine(counts: {
38
31
  }, rounds: number): string;
39
32
  export declare const reviewSummaryPolicy: PlaybookSummaryPolicy;
40
33
  export declare function validateReviewOptions(optionSlice: unknown): ReviewOptions;
41
- export declare function createReviewRuntimeOptions({ captainOptions, players, }: CreateReviewRuntimeOptions): ReviewPlaybookOptions;
42
34
  export declare const reviewPlaybookRegistryEntry: ReviewPlaybookRegistryEntry;
43
35
  export default reviewPlaybookRegistryEntry;
@@ -44,30 +44,17 @@ export function validateReviewOptions(optionSlice) {
44
44
  }
45
45
  return Object.freeze({});
46
46
  }
47
- function playerIdentity(players, id) {
48
- const player = players.find((entry) => entry.id === id);
49
- const identity = player?.model ?? player?.adapter;
50
- if (identity === undefined || identity.trim().length === 0) {
51
- throw new Error(`REVIEW requires configured player ${id}`);
52
- }
53
- return identity;
54
- }
55
- export function createReviewRuntimeOptions({ captainOptions, players, }) {
56
- validateReviewOptions(captainOptions);
57
- return {
58
- coderLlm: playerIdentity(players, 'coder'),
59
- reviewerLlm: playerIdentity(players, 'reviewer'),
60
- };
61
- }
62
47
  export const reviewPlaybookRegistryEntry = {
63
48
  id: 'review',
64
49
  command: 'review',
65
50
  intent: 'review the latest commit until no material correctness or spec findings remain',
51
+ artifactSchema: 2,
66
52
  requiredRoleIds: ['coder', 'reviewer'],
53
+ concurrentRoleSets: [],
67
54
  summaryPolicy: reviewSummaryPolicy,
68
55
  validateOptions: validateReviewOptions,
69
56
  createRuntime(options) {
70
- return createPlaybookRuntime(createReviewRuntimeOptions(options));
57
+ return createPlaybookRuntime(options);
71
58
  },
72
59
  };
73
60
  export default reviewPlaybookRegistryEntry;
@@ -3,7 +3,6 @@
3
3
 
4
4
  import createPlaybookRuntime, {
5
5
  type PlaybookRuntime,
6
- type ReviewPlaybookOptions,
7
6
  } from './review.playbook.js';
8
7
 
9
8
  export interface PlaybookSummaryPolicy {
@@ -15,27 +14,18 @@ export interface PlaybookSummaryPolicy {
15
14
  ): string;
16
15
  }
17
16
 
18
- export interface RegistryPlayer {
19
- id: string;
20
- adapter?: string;
21
- model?: string;
22
- }
23
-
24
- export interface CreateReviewRuntimeOptions {
25
- captainOptions: unknown;
26
- players: readonly RegistryPlayer[];
27
- }
28
-
29
17
  export type ReviewOptions = Readonly<Record<string, never>>;
30
18
 
31
19
  export interface ReviewPlaybookRegistryEntry {
32
20
  id: 'review';
33
21
  command: 'review';
34
22
  intent: string;
23
+ artifactSchema: 2;
35
24
  requiredRoleIds: readonly ['coder', 'reviewer'];
25
+ concurrentRoleSets: readonly [];
36
26
  summaryPolicy: PlaybookSummaryPolicy;
37
- validateOptions(captainOptions: unknown): ReviewOptions;
38
- createRuntime(options: CreateReviewRuntimeOptions): PlaybookRuntime;
27
+ validateOptions(optionSlice: unknown): ReviewOptions;
28
+ createRuntime(options: ReviewOptions): PlaybookRuntime;
39
29
  }
40
30
 
41
31
  export const reviewStateCountLabels = {
@@ -99,39 +89,18 @@ export function validateReviewOptions(optionSlice: unknown): ReviewOptions {
99
89
  return Object.freeze({});
100
90
  }
101
91
 
102
- function playerIdentity(
103
- players: readonly RegistryPlayer[],
104
- id: 'coder' | 'reviewer',
105
- ): string {
106
- const player = players.find((entry) => entry.id === id);
107
- const identity = player?.model ?? player?.adapter;
108
- if (identity === undefined || identity.trim().length === 0) {
109
- throw new Error(`REVIEW requires configured player ${id}`);
110
- }
111
- return identity;
112
- }
113
-
114
- export function createReviewRuntimeOptions({
115
- captainOptions,
116
- players,
117
- }: CreateReviewRuntimeOptions): ReviewPlaybookOptions {
118
- validateReviewOptions(captainOptions);
119
- return {
120
- coderLlm: playerIdentity(players, 'coder'),
121
- reviewerLlm: playerIdentity(players, 'reviewer'),
122
- };
123
- }
124
-
125
92
  export const reviewPlaybookRegistryEntry: ReviewPlaybookRegistryEntry = {
126
93
  id: 'review',
127
94
  command: 'review',
128
95
  intent:
129
96
  'review the latest commit until no material correctness or spec findings remain',
97
+ artifactSchema: 2,
130
98
  requiredRoleIds: ['coder', 'reviewer'],
99
+ concurrentRoleSets: [],
131
100
  summaryPolicy: reviewSummaryPolicy,
132
101
  validateOptions: validateReviewOptions,
133
102
  createRuntime(options) {
134
- return createPlaybookRuntime(createReviewRuntimeOptions(options));
103
+ return createPlaybookRuntime(options);
135
104
  },
136
105
  };
137
106
 
package/slc/gears2fsm.md CHANGED
@@ -36,7 +36,7 @@ map for every actor kind the GEARS artifact uses, using typed actor logic such
36
36
  as `fromPromise<Output, Input>(...)` [[11]]:
37
37
 
38
38
  - `captain` for direct work performed by Captain;
39
- - `player` for work Captain delegates to a named player;
39
+ - `player` for work Captain delegates to a named role;
40
40
  - `playbook` for a nested playbook call; and
41
41
  - `script` for a deterministic shell script an
42
42
  [optimizer-introduced script item](text2gears.md#script-behaviors-optimizer-introduced)
@@ -73,7 +73,7 @@ comment delimiters into a TypeScript target.
73
73
  `PlayerInput` shall be a typed object with at least:
74
74
 
75
75
  - `stateId`: the stable id of the invoking working leaf;
76
- - `player`: the [player](text2gears.md#players) Captain is to invoke;
76
+ - `role`: the canonical lowercase local id derived from the [role](text2gears.md#roles) Captain is to delegate;
77
77
  - `sourceItem`: the GEARS item ID this state realizes;
78
78
  - `prompt`: the source item's full final prompt, verbatim;
79
79
  - `result`: a record whose keys are the valid guard names this invocation may return.
@@ -244,22 +244,25 @@ Each state shall declare:
244
244
  - a stable `id` (for `#id` targeting and Boss interrupts);
245
245
  - an intuitive state key (the property name under `states: { ... }`);
246
246
  - a one-line `description` (for inspector tools and documentation);
247
- - JSON-safe `meta: { playbook: { stateId, description, player? } }` repeating
248
- its stable id and description so linked runtimes can discover active public
247
+ - JSON-safe `meta: { playbook: { stateId, description, role? } }` naming the
248
+ state's public playbook identity per the identity rule below and repeating
249
+ its description so linked runtimes can discover active public
249
250
  identities through `snapshot.getMeta()` without private XState nodes. A
250
- delegated-player state shall also carry the exact source player in
251
- `meta.playbook.player`; every other state shall omit `player`;
251
+ delegated-role state shall also carry the canonical lowercase source role id in
252
+ `meta.playbook.role`; every other state shall omit `role`;
252
253
  - if it invokes the direct `captain` actor: `invoke.input` carrying
253
254
  `sourceItem`, `prompt`, and `result` (per [Setup](#setup));
254
255
  - if it invokes the delegated `player` actor: `invoke.input` additionally
255
- carrying the same source-derived `player` as `meta.playbook.player`;
256
+ carrying the same source-derived `role` as `meta.playbook.role`;
256
257
  - if it invokes the `script` actor: `invoke.input` carrying `stateId`,
257
258
  `sourceItem`, `command`, and `result` (per [Setup](#setup)) — no `prompt`
258
- and no `player`.
259
+ and no `role`.
259
260
 
260
261
  The source item ID shall live in `invoke.input.sourceItem`, not in a comment — this keeps the GEARS-to-state mapping machine-readable.
261
- A delegated state's `invoke.input.player` shall match its source item's named
262
- player. A direct Captain state shall not invent a `Captain` player binding.
262
+
263
+ Outside a parallel group's regions, a state's `meta.playbook.stateId` shall equal its state key — the one identity a factory-backed linked runtime indexes by.
264
+ A delegated state's `invoke.input.role` shall match the canonical lowercase id of its source item's named role.
265
+ A direct Captain state shall not invent a `Captain` role binding.
263
266
 
264
267
  Every invoking working leaf — sequential or parallel, whatever its actor
265
268
  kind — shall carry the tag `playbook.busy`: the shared quiescence helper
@@ -281,7 +284,7 @@ invoke: {
281
284
  src: 'player',
282
285
  input: ({ context }): PlayerInput => ({
283
286
  stateId: '<stable-state-id>',
284
- player: 'Reviewer',
287
+ role: 'reviewer',
285
288
  sourceItem: '<ITEM-A>',
286
289
  prompt: [
287
290
  'Flag any issues or improvements (numbered; no duplication).',
@@ -299,17 +302,17 @@ invoke: {
299
302
 
300
303
  For an item in which Captain acts directly, the corresponding invocation uses
301
304
  `src: 'captain'` and a `CaptainInput` with the same static mapping fields but no
302
- `player` field.
305
+ `role` field.
303
306
 
304
307
  ## Mapping
305
308
 
306
309
  Each Source spec item shall map to exactly one state in Target.
307
310
  A state's `invoke.input.sourceItem` shall be that item's ID, and `invoke.input.prompt` shall carry the item's prompt verbatim.
308
311
 
309
- An item written as direct Captain work shall map to exactly one `captain`
310
- invocation. An item that prompts or relays to a named player shall map to
311
- exactly one `player` invocation. A nested-call item shall map to exactly one
312
- `playbook` invocation. A script item (`Captain shall run:`) shall map to
312
+ An item written as direct Captain work shall map to exactly one `captain` invocation.
313
+ An item that prompts or relays to a named role shall map to exactly one `player` invocation.
314
+ A nested-call item shall map to exactly one `playbook` invocation.
315
+ A script item (`Captain shall run:`) shall map to
313
316
  exactly one `script` invocation whose `input.command` carries the blockquote
314
317
  verbatim and whose `result` preserves the item's two guards in declared order.
315
318
  The compiler shall not infer one actor kind from a
@@ -333,10 +336,14 @@ Each member shall be a delegated-player item; a direct-Captain or nested-call
333
336
  member is malformed because those actor kinds share one Captain control lane or one
334
337
  pending-child slot. Each region shall contain a delegated-player working leaf
335
338
  and a local final state; the working leaf retains the item's stable state id,
336
- `sourceItem`, player, prompt, and result contract.
339
+ `sourceItem`, role, prompt, and result contract.
340
+ The members' canonical role ids shall be pairwise distinct; a repeated role in one group is malformed.
337
341
  The parallel parent shall use `onDone` as the join, which XState takes only
338
342
  after every region reaches final.
339
343
 
344
+ The artifact shall export `concurrentRoleSets` as a deeply readonly array containing one role-id array per parallel group in first-item source order, with each inner array following that group's item order.
345
+ An artifact with no parallel group shall export an empty array.
346
+
340
347
  Each branch shall assign only its own staged result.
341
348
  The join shall promote all staged results atomically before later work begins,
342
349
  so branch completion order cannot change downstream inputs.
@@ -527,8 +534,8 @@ the number of sequential child calls without an arbitrary runtime call limit.
527
534
  Prompts shall pass only the **specific extracted fields** the player needs.
528
535
  The compiler shall not dump `JSON.stringify(lastResult)` or any opaque blob: it leaks internal `guard` strings, wastes tokens, and confuses the LLM.
529
536
 
530
- Player bindings and per-run parameters shall flow in via the machine's `input` and be copied into context at start-up.
531
- The artifact shall not bake in player bindings, model names, or per-run values.
537
+ Player bindings and prompt identities shall enter only through `PlaybookSession.roleBindings` at runtime call, prompt, and trace boundaries.
538
+ The artifact shall not bake them into machine input, options, or context; model names and other host settings shall remain host policy rather than persisted FSM state.
532
539
  Host-owned configuration such as an enabled-playbook catalog shall remain
533
540
  immutable machine input/context for the session. Boss events and actor outputs
534
541
  shall not carry, replace, append to, or otherwise overwrite that catalog.
@@ -585,7 +592,7 @@ Phases may set typed routing fields so terminal outcomes return to the originati
585
592
 
586
593
  ## Boss control
587
594
 
588
- [Boss](text2gears.md#players) input enters the machine through three surfaces: pre-emptive interrupts on active states, typed entry events on idle or recoverable states, and Boss replies to player questions that suspended the FSM in a dedicated wait state.
595
+ [Boss](text2gears.md#roles) input enters the machine through three surfaces: pre-emptive interrupts on active states, typed entry events on idle or recoverable states, and Boss replies to delegated-role questions that suspended the FSM in a dedicated wait state.
589
596
 
590
597
  ### Boss interrupts
591
598
 
@@ -667,12 +674,12 @@ the colon as the JSON field name.
667
674
  The linked runtime composes player prompts per [link.md "Player prompt composition"](link.md#player-prompt-composition), without adding a player-visible Boss-question instruction.
668
675
 
669
676
  The question record shall be
670
- `{ questionId, resumeStateId, sourceItem, player, question }`.
677
+ `{ questionId, resumeStateId, sourceItem, asker, question }`, where `asker` is exactly `{ kind: 'captain' }` or `{ kind: 'role', roleId }`.
671
678
  `questionId` and `resumeStateId` shall both equal the stable working-leaf
672
679
  `stateId`.
673
- `questionId`, `resumeStateId`, and `sourceItem` shall come from the suspended
674
- working leaf's stable invocation metadata. `player` shall come from a delegated
675
- `PlayerInput`, or be the literal `Captain` for a direct-Captain state. Only
680
+ `questionId`, `resumeStateId`, and `sourceItem` shall come from the suspended working leaf's stable invocation metadata.
681
+ A delegated `PlayerInput` shall produce the role asker with its canonical local role id, while a direct-Captain state shall produce the Captain asker without inventing a role.
682
+ Only
676
683
  `question` shall come from adjudicated actor output.
677
684
 
678
685
  A machine with at most one active Captain or player task may use the scalar
@@ -744,6 +751,20 @@ an unhandled runtime error.
744
751
  Every machine shall declare at least one `type: 'final'` state (typically `done`) reachable on completion.
745
752
  A never-terminating machine is a defect: the runner has no completion signal.
746
753
 
754
+ At least one is a floor, not a ceiling. A final state's `description` is the
755
+ machine's published terminal meaning: a host that cannot read the machine's
756
+ output quotes that description to report what the run did. It shall therefore
757
+ be true of every arm that enters the state and of no other terminal outcome.
758
+ Where Source declares more than one terminal outcome — an approval that
759
+ completes the workflow and a failure the workflow reports to its caller
760
+ instead of parking — each outcome shall get its own `type: 'final'` state
761
+ whose description names it. Routing an approval arm and a failure, abort, or
762
+ invalid-result arm into one final state is a defect of the same kind as a
763
+ wrong result field, because the quoting host cannot detect the difference.
764
+ This constrains only published meaning: the declared machine `output` still
765
+ derives its status and fields from typed context, so a caller that does read
766
+ the output is unaffected.
767
+
747
768
  Where Source declares a JSON-safe terminal result, the setup types shall
748
769
  declare that output and the root machine shall derive it from typed context
749
770
  through XState's machine `output` function. A final-state transition alone does