@sublang/playbook 5.0.0 → 6.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 (65) hide show
  1. package/README.md +11 -7
  2. package/docs/cli.md +38 -35
  3. package/docs/configuration.md +58 -15
  4. package/docs/embedding.md +24 -16
  5. package/package.json +40 -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/code.fsm.d.ts +229 -94
  10. package/reference/sdlc/code.playbook/code.fsm.introspect.d.ts +26 -44
  11. package/reference/sdlc/code.playbook/code.fsm.introspect.js +61 -66
  12. package/reference/sdlc/code.playbook/code.fsm.introspect.ts +100 -149
  13. package/reference/sdlc/code.playbook/code.fsm.js +587 -1347
  14. package/reference/sdlc/code.playbook/code.fsm.ts +809 -1650
  15. package/reference/sdlc/code.playbook/code.gears.md +51 -263
  16. package/reference/sdlc/code.playbook/code.playbook.d.ts +8 -47
  17. package/reference/sdlc/code.playbook/code.playbook.js +69 -656
  18. package/reference/sdlc/code.playbook/code.playbook.ts +90 -867
  19. package/reference/sdlc/code.playbook/code.registry.d.ts +9 -25
  20. package/reference/sdlc/code.playbook/code.registry.js +20 -78
  21. package/reference/sdlc/code.playbook/code.registry.ts +58 -122
  22. package/reference/sdlc/code.playbook/playbook-captain.js +93 -15
  23. package/reference/sdlc/code.playbook/playbook-captain.ts +115 -19
  24. package/reference/sdlc/code.playbook/playbook.config.template.yaml +33 -22
  25. package/reference/sdlc/decide.md +54 -0
  26. package/reference/sdlc/decide.playbook/decide.fsm.d.ts +261 -0
  27. package/reference/sdlc/decide.playbook/decide.fsm.js +894 -0
  28. package/reference/sdlc/decide.playbook/decide.fsm.ts +1152 -0
  29. package/reference/sdlc/decide.playbook/decide.gears.md +88 -0
  30. package/reference/sdlc/decide.playbook/decide.playbook.d.ts +67 -0
  31. package/reference/sdlc/{discuss.playbook/discuss.playbook.js → decide.playbook/decide.playbook.js} +471 -362
  32. package/reference/sdlc/{discuss.playbook/discuss.playbook.ts → decide.playbook/decide.playbook.ts} +575 -443
  33. package/reference/sdlc/decide.playbook/decide.registry.d.ts +41 -0
  34. package/reference/sdlc/decide.playbook/decide.registry.js +60 -0
  35. package/reference/sdlc/decide.playbook/decide.registry.ts +125 -0
  36. package/reference/sdlc/review.md +81 -0
  37. package/reference/sdlc/review.playbook/review.fsm.d.ts +183 -0
  38. package/reference/sdlc/review.playbook/review.fsm.js +524 -0
  39. package/reference/sdlc/review.playbook/review.fsm.ts +652 -0
  40. package/reference/sdlc/review.playbook/review.gears.md +112 -0
  41. package/reference/sdlc/review.playbook/review.playbook.d.ts +12 -0
  42. package/reference/sdlc/review.playbook/review.playbook.js +112 -0
  43. package/reference/sdlc/review.playbook/review.playbook.ts +201 -0
  44. package/reference/sdlc/review.playbook/review.registry.d.ts +43 -0
  45. package/reference/sdlc/review.playbook/review.registry.js +73 -0
  46. package/reference/sdlc/review.playbook/review.registry.ts +138 -0
  47. package/slc/gears2fsm.md +13 -4
  48. package/slc/link.md +48 -2
  49. package/slc/text2gears.md +22 -2
  50. package/src/runtime.d.ts +7 -0
  51. package/src/runtime.ts +12 -0
  52. package/src/xstate-playbook-runtime.d.ts +9 -2
  53. package/src/xstate-playbook-runtime.js +255 -21
  54. package/src/xstate-playbook-runtime.ts +333 -28
  55. package/src/xstate-runtime.js +25 -0
  56. package/src/xstate-runtime.ts +51 -0
  57. package/reference/sdlc/discuss.md +0 -93
  58. package/reference/sdlc/discuss.playbook/discuss.fsm.d.ts +0 -396
  59. package/reference/sdlc/discuss.playbook/discuss.fsm.js +0 -2067
  60. package/reference/sdlc/discuss.playbook/discuss.fsm.ts +0 -2465
  61. package/reference/sdlc/discuss.playbook/discuss.gears.md +0 -258
  62. package/reference/sdlc/discuss.playbook/discuss.playbook.d.ts +0 -113
  63. package/reference/sdlc/discuss.playbook/discuss.registry.d.ts +0 -58
  64. package/reference/sdlc/discuss.playbook/discuss.registry.js +0 -97
  65. package/reference/sdlc/discuss.playbook/discuss.registry.ts +0 -153
@@ -3,51 +3,25 @@
3
3
  //
4
4
  // Generated by slc/link.md (FSM-to-Runtime linker).
5
5
  // Source FSM: ./code.fsm.ts
6
- // Player bind: Coder→coder, Reviewer→reviewer,
7
- // Committer→ configured committerPlayer alias when set
8
- // (per-run remapping via options PBRT-8); else the
9
- // baked CODE-18/19 fallback (coder-first: CODE-19 wires
10
- // both coderPlayer and reviewerPlayer, Coder wins as the
11
- // alias's first alternative)
12
- // Boss event: free-text judge classification
13
- // Adjudication: LLM-judge per state
14
- // Contract: PlayerResult / PlaybookPorts / PlaybookSession /
15
- // PlaybookRuntime imported and re-exported from
16
- // @sublang/playbook/runtime
17
- // (slc/link.md §Output, DR-004 Addendum A4)
18
- // Runtime: the shared createXStatePlaybookRuntime factory from
19
- // @sublang/playbook/xstate-runtime interprets the FSM
20
- // (slc/link.md §Output, DR-019); this module carries only
21
- // the CODE-specific spec — options validation, player
22
- // binding, prompt composition, Boss-event classification,
23
- // and Captain-pane status formatting.
6
+ // Player bind: Coder→coder (default lowercased binding)
7
+ // Boss event: deterministic START_CODE entry; exact Boss text becomes
8
+ // callerInput; pending Coder questions retain BOSS_REPLY
9
+ // Adjudication: LLM judge per player state; coderOutput is carried verbatim
10
+ // Nested call: literal review target through the shared bridge
11
+ // Compat: artifact schema 1 / runtime ABI 1
24
12
 
25
13
  import {
26
- createPlayerBridge,
14
+ RUNTIME_ABI,
27
15
  createXStatePlaybookRuntime,
28
- adjudicatePlayerOutput,
29
- normalizeError,
30
- normalizeErrorCompact,
31
- normalizeErrorFull,
32
- parseJudgeJson,
33
16
  snapshotJsonValue,
34
17
  type PlaybookPlayerInput,
35
- type RuntimeBoundaryCalls,
36
- type ScheduledStatus,
37
18
  type XStatePlaybookRuntimeSpec,
38
- } from '../../../src/xstate-runtime.js';
19
+ } from '@sublang/playbook/xstate-runtime';
39
20
  import {
40
21
  codingMachine,
41
- type PlayerInput,
42
- type PlayerOutput,
43
- type CodingEvent,
44
22
  type CodingInput,
23
+ type PlayerInput,
45
24
  } from './code.fsm.js';
46
- import {
47
- enumerateAwaitBossReply,
48
- enumerateCaptainStates,
49
- enumerateRootEvents,
50
- } from './code.fsm.introspect.js';
51
25
  import type {
52
26
  CaptainCallOptions,
53
27
  CaptainResult,
@@ -57,27 +31,23 @@ import type {
57
31
  PlaybookCallRequest,
58
32
  PlaybookCallResult,
59
33
  PlaybookCallStart,
34
+ PlaybookControlReceipt,
35
+ PlaybookControlView,
60
36
  PlaybookPendingCall,
37
+ PlaybookPorts,
61
38
  PlaybookRunResult,
39
+ PlaybookRuntime,
40
+ PlaybookRuntimeFactory,
62
41
  PlaybookRuntimeSnapshot,
63
42
  PlaybookSession,
64
43
  PlaybookState,
65
44
  PlaybookStateValue,
66
45
  PlaybookTraceEvent,
67
46
  PlaybookTraceType,
68
- PlaybookPorts,
69
- PlaybookRuntime,
70
- PlaybookRuntimeFactory,
71
47
  PlayerResult,
48
+ PlayerSessionStore,
72
49
  } from '@sublang/playbook/runtime';
73
50
 
74
- // Public contract. `PlayerResult`, `PlaybookPorts`, `PlaybookSession`, and
75
- // `PlaybookRuntime` are re-exported from the shared runtime module
76
- // (slc/link.md §Output, DR-004 Addendum A4) so this playbook and any
77
- // future one resolve one contract definition rather than redefining it.
78
- // `CodePlaybookOptions` and the default `createPlaybookRuntime` factory
79
- // (typed `PlaybookRuntimeFactory<CodePlaybookOptions>`) stay
80
- // CODE-specific.
81
51
  export type {
82
52
  CaptainCallOptions,
83
53
  CaptainResult,
@@ -87,23 +57,35 @@ export type {
87
57
  PlaybookCallRequest,
88
58
  PlaybookCallResult,
89
59
  PlaybookCallStart,
60
+ PlaybookControlReceipt,
61
+ PlaybookControlView,
90
62
  PlaybookPendingCall,
91
- PlaybookRunResult,
92
- PlayerResult,
93
63
  PlaybookPorts,
64
+ PlaybookRunResult,
65
+ PlaybookRuntime,
66
+ PlaybookRuntimeFactory,
67
+ PlaybookRuntimeSnapshot,
94
68
  PlaybookSession,
95
69
  PlaybookState,
96
70
  PlaybookStateValue,
97
71
  PlaybookTraceEvent,
98
72
  PlaybookTraceType,
99
- PlaybookRuntime,
100
- PlaybookRuntimeFactory,
101
- PlaybookRuntimeSnapshot,
73
+ PlayerResult,
74
+ PlayerSessionStore,
102
75
  };
103
76
 
104
77
  export type CodePlaybookOptions = CodingInput;
105
78
 
106
- function snapshotCodePlaybookOptions(value: unknown): CodePlaybookOptions {
79
+ const OPTION_KEYS = new Set(['coderPlayer', 'runResults']);
80
+ const PLACEHOLDER = /<(#|[A-Za-z_$][A-Za-z0-9_$-]*)>/g;
81
+ const CONTINUATION_PREAMBLE =
82
+ 'You previously paused this task to ask Boss a question; Boss has now replied. Continue the same task using the reply below.';
83
+
84
+ const VERBATIM_PAYLOAD_FIELDS: ReadonlySet<string> = new Set([
85
+ 'coderOutput',
86
+ ]);
87
+
88
+ function snapshotCodeOptions(value: unknown): CodePlaybookOptions {
107
89
  const captured = snapshotJsonValue(value, 'CODE runtime options');
108
90
  if (
109
91
  captured === null ||
@@ -113,15 +95,8 @@ function snapshotCodePlaybookOptions(value: unknown): CodePlaybookOptions {
113
95
  throw new TypeError('CODE runtime options must be an object');
114
96
  }
115
97
  const record = captured as Readonly<Record<string, JsonValue>>;
116
- const allowed = new Set([
117
- 'intent',
118
- 'irNumber',
119
- 'coderPlayer',
120
- 'reviewerPlayer',
121
- 'committerPlayer',
122
- ]);
123
98
  for (const [key, option] of Object.entries(record)) {
124
- if (!allowed.has(key)) {
99
+ if (!OPTION_KEYS.has(key)) {
125
100
  throw new TypeError(`CODE runtime options.${key} is not declared`);
126
101
  }
127
102
  if (typeof option !== 'string') {
@@ -131,834 +106,82 @@ function snapshotCodePlaybookOptions(value: unknown): CodePlaybookOptions {
131
106
  return captured as unknown as CodePlaybookOptions;
132
107
  }
133
108
 
134
- // Required-payload fields whose value is the player's verbatim long-form
135
- // prose. The runtime carries `finalText.trim()` into these fields rather
136
- // than asking the judge to round-trip the text through JSON. Short
137
- // extracted fields (`question`, `taskDescription`, `irNumber`, …) stay
138
- // judge-extracted — they are not in this set.
139
- const VERBATIM_PAYLOAD_FIELDS: ReadonlySet<string> = new Set([
140
- 'reviews',
141
- 'challenges',
142
- ]);
143
-
144
- // Normalize any `error` field inside a telemetry event so failed
145
- // transitions don't leak raw Error instances through the channel.
146
- function normalizeEventForTelemetry(event: unknown): unknown {
147
- if (event === undefined) return undefined;
148
- return normalizeEventValue(event, 'FSM event', new Set());
149
- }
150
-
151
- function normalizeEventValue(
152
- value: unknown,
153
- path: string,
154
- ancestors: ReadonlySet<object>,
155
- ): JsonValue {
156
- if (Array.isArray(value)) return snapshotJsonValue(value, path);
157
- if (value === null || typeof value !== 'object') {
158
- return snapshotJsonValue(value, path);
159
- }
160
- if (ancestors.has(value)) {
161
- throw new TypeError(`${path} must not contain a JSON cycle`);
162
- }
163
- const prototype = Object.getPrototypeOf(value) as unknown;
164
- if (prototype !== Object.prototype && prototype !== null) {
165
- return snapshotJsonValue(value, path);
166
- }
167
- if (Object.getOwnPropertySymbols(value).length > 0) {
168
- return snapshotJsonValue(value, path);
169
- }
170
- const nextAncestors = new Set(ancestors).add(value);
171
- const normalized: Record<string, JsonValue> = {};
172
- for (const [key, descriptor] of Object.entries(
173
- Object.getOwnPropertyDescriptors(value),
174
- )) {
175
- if (!descriptor.enumerable) {
176
- throw new TypeError(`${path}.${key} must be an enumerable JSON property`);
177
- }
178
- if (!Object.prototype.hasOwnProperty.call(descriptor, 'value')) {
179
- throw new TypeError(`${path}.${key} must be a JSON data property`);
180
- }
181
- if (descriptor.value === undefined) continue;
182
- normalized[key] =
183
- key === 'error'
184
- ? snapshotJsonValue(normalizeError(descriptor.value), `${path}.error`)
185
- : normalizeEventValue(
186
- descriptor.value,
187
- `${path}.${key}`,
188
- nextAncestors,
189
- );
190
- }
191
- return snapshotJsonValue(normalized, path);
192
- }
193
-
194
- // Player-prompt composer — DR-004 §6.
195
- // Substitutes the three placeholder tokens in `input.prompt` (literal
196
- // string replace, no escaping) and arranges labelled blocks around
197
- // the prompt body. Context blocks the body refers to as prior
198
- // material (`Boss intent:`, `Task description:`) are prepended;
199
- // action blocks the body refers to as material below
200
- // (`Review items:`, `Rebuttals:`) are appended after the body so the
201
- // CODE-N prompts' "review item below" / "rebuttal below" phrasing
202
- // matches the rendered layout. When a state resumes from a Boss
203
- // reply, the continuation preamble and Q/A blocks precede every
204
- // ordinary block. The FSM's prompt body is never re-flowed.
205
-
206
- function composePlayerPrompt(input: PlayerInput): string {
207
- const blocks: string[] = [];
208
- if (
209
- input.pendingBossQuestion !== undefined &&
210
- input.bossReply !== undefined
211
- ) {
212
- blocks.push(
213
- 'You previously paused this task to ask Boss a question; Boss has now replied. Continue the same task using the reply below.',
214
- );
215
- blocks.push(`Boss question:\n${input.pendingBossQuestion.question}`);
216
- blocks.push(`Boss reply:\n${input.bossReply}`);
217
- }
218
- if (input.intent !== undefined) {
219
- blocks.push(`Boss intent:\n${input.intent}`);
220
- }
221
- if (input.taskDescription !== undefined) {
222
- blocks.push(`Task description:\n${input.taskDescription}`);
223
- }
224
-
225
- let body = input.prompt;
226
- if (input.irNumber !== undefined) {
227
- body = body.replaceAll('<#>', input.irNumber);
228
- }
229
- if (input.coderPlayer !== undefined) {
230
- body = body.replaceAll('<coder-llm>', input.coderPlayer);
231
- }
232
- if (input.reviewerPlayer !== undefined) {
233
- body = body.replaceAll('<reviewer-llm>', input.reviewerPlayer);
234
- }
235
-
236
- blocks.push(body);
237
-
238
- if (input.reviews !== undefined) {
239
- blocks.push(`Review items:\n${input.reviews}`);
240
- }
241
- if (input.challenges !== undefined) {
242
- blocks.push(`Rebuttals:\n${input.challenges}`);
243
- }
244
-
245
- return blocks.join('\n\n');
246
- }
247
-
248
- // Player-id resolver — DR-004 §2 / PBRT-8.
249
- // Non-composite: Coder→'coder', Reviewer→'reviewer'. The composite
250
- // Committer (= Coder | Reviewer per code.gears.md) resolves to the
251
- // configured alias when present: `input.committerPlayer`, the
252
- // validated `captain.options.playbooks.code.options.committer`
253
- // (PBRT-8 / PBRT-30), already a baked player id ('coder' / 'reviewer').
254
- // Absent a
255
- // configured alias it falls back to the DR-004 §2 baked binding by
256
- // populated <playerName>Player field: prefer `coderPlayer` (CODE-18
257
- // wires only coderPlayer; CODE-19 wires both so coderPlayer still wins
258
- // as the alias's first alternative), fall back to `reviewerPlayer`
259
- // only if a future gear ships a reviewer-only Committer item, and
260
- // finally to the alias's first alternative (Coder) when neither is
261
- // set. The alias selects only the host pane; it is not a PBRT-4
262
- // identity string, so it leaves <coder-llm> / <reviewer-llm>
263
- // untouched and `input.player` stays `Committer` (PLAYBOOK-3).
264
- function resolvePlayerId(input: PlayerInput): string {
265
- switch (input.player) {
266
- case 'Coder':
267
- return 'coder';
268
- case 'Reviewer':
269
- return 'reviewer';
270
- case 'Committer':
271
- if (input.committerPlayer !== undefined) return input.committerPlayer;
272
- if (input.coderPlayer !== undefined) return 'coder';
273
- if (input.reviewerPlayer !== undefined) return 'reviewer';
274
- return 'coder';
275
- default: {
276
- const exhaustive: never = input.player;
277
- throw new Error(`resolvePlayerId: unknown player ${String(exhaustive)}`);
278
- }
279
- }
280
- }
281
-
282
- function extractRequiredFields(description: string): string[] {
283
- const fields: string[] = [];
284
- const re = /Output shall include `([A-Za-z_][A-Za-z0-9_]*):/g;
285
- for (const m of description.matchAll(re)) {
286
- fields.push(m[1]);
287
- }
288
- return fields;
289
- }
290
-
291
- function buildJudgePrompt(input: PlayerInput, finalText: string): string {
292
- const lines: string[] = [];
293
- lines.push(
294
- 'This is hidden control work. Do not call tools, inspect files, or ' +
295
- 'seek external evidence. Decide only from the supplied player output ' +
296
- 'and outcome descriptions. Reply with exactly one JSON object and no prose.',
109
+ function placeholderField(token: string): string {
110
+ if (token === '#') return 'irNumber';
111
+ if (token === 'coder-llm') return 'coderPlayer';
112
+ return token.replace(/-([A-Za-z0-9_$])/g, (_match, next: string) =>
113
+ next.toUpperCase(),
297
114
  );
298
- lines.push('');
299
- lines.push(`The ${input.player} just produced this output:`);
300
- lines.push('');
301
- lines.push('```');
302
- lines.push(finalText);
303
- lines.push('```');
304
- lines.push('');
305
- lines.push(
306
- 'Pick exactly one outcome by `guard` and return JSON ' +
307
- '`{ guard, …payloadFields }`. Required payload fields are ' +
308
- 'named in the outcome description. Do not copy long-form ' +
309
- 'verbatim fields (`reviews`, `challenges`) — the runtime ' +
310
- "carries the player's output into those fields verbatim, " +
311
- 'so any value you supply for them will be overwritten.',
312
- );
313
- lines.push('');
314
- for (const [key, description] of Object.entries(input.result)) {
315
- lines.push(`- \`${key}\` — ${description}`);
316
- }
317
- return lines.join('\n');
318
115
  }
319
116
 
320
- // CODE-specific adjudication strategy: the CODE judge prompt above, the
321
- // DR-004 `Output shall include` required-field extraction, and the
322
- // verbatim long-form payload fields.
323
- const CODE_ADJUDICATION = {
324
- buildJudgePrompt: (input: PlaybookPlayerInput, finalText: string) =>
325
- buildJudgePrompt(input as unknown as PlayerInput, finalText),
326
- extractRequiredFields,
327
- verbatimPayloadFields: VERBATIM_PAYLOAD_FIELDS,
328
- };
329
-
330
- // LLM judge — DR-004 §4. Delegates to the shared adjudicator with the
331
- // CODE strategy: it lists each declared outcome verbatim, asks
332
- // ports.callJudge for a JSON `{ guard, …payloadFields }` response, and
333
- // returns the parsed object once the chosen guard is one of the
334
- // input.result keys. Adjudicator failures (malformed JSON,
335
- // missing/unknown guard) are control-plane errors and propagate via
336
- // throw per slc/link.md.
337
- async function adjudicate(
338
- input: PlayerInput,
339
- finalText: string,
340
- ports: PlaybookPorts,
341
- signal: AbortSignal,
342
- boundary?: RuntimeBoundaryCalls,
343
- ): Promise<PlayerOutput> {
344
- return (await adjudicatePlayerOutput(
345
- CODE_ADJUDICATION,
346
- input,
347
- finalText,
348
- ports,
349
- signal,
350
- boundary,
351
- )) as unknown as PlayerOutput;
117
+ function quoteContinuation(value: string): string {
118
+ return value.replaceAll('\n', '\n> ');
352
119
  }
353
120
 
354
- // Boss-event classifier — DR-004 §3.
355
- // Every non-empty Boss turn goes through ports.callJudge. Slash-prefixed
356
- // text is ordinary content once a host has routed the turn to this
357
- // playbook; `/command` selection belongs outside handleBossInput. The
358
- // classifier is state-aware so awaitBossReply can distinguish a direct
359
- // answer (BOSS_REPLY) from a fresh directive that abandons the pending
360
- // question through the FSM's existing transitions.
361
- async function classifyBossText(
362
- text: string,
363
- ports: PlaybookPorts,
364
- signal: AbortSignal,
365
- snapshotOrState?: unknown,
366
- boundary?: RuntimeBoundaryCalls,
367
- ): Promise<CodingEvent | undefined> {
368
- const trimmed = text.trim();
369
- if (trimmed === '') return undefined;
370
-
371
- return classifyWithLlm(text, ports, signal, snapshotOrState, boundary);
372
- }
373
-
374
- // JumpableStateId is internal to code.fsm.ts (not exported), so
375
- // recover it from the CodingEvent union for typed casts here.
376
- type JumpableStateId = Extract<
377
- CodingEvent,
378
- { type: 'BOSS_INTERRUPT' }
379
- >['targetId'];
380
-
381
- const rootEvents = enumerateRootEvents(codingMachine);
382
- const bossInterruptTargets = rootEvents.bossInterruptTargetDescriptions;
383
- const bossInterruptTargetIds: ReadonlySet<string> = new Set(
384
- bossInterruptTargets.map((target) => target.stateId),
385
- );
386
-
387
- async function classifyWithLlm(
388
- text: string,
389
- ports: PlaybookPorts,
390
- signal: AbortSignal,
391
- snapshotOrState?: unknown,
392
- boundary?: RuntimeBoundaryCalls,
393
- ): Promise<CodingEvent | undefined> {
394
- const state = classifierState(snapshotOrState);
395
- const prompt = buildClassifierPrompt(text, state);
396
- const stateId = typeof state.value === 'string' ? state.value : undefined;
397
- const raw = boundary
398
- ? await boundary.callJudge(
399
- 'boss-input-classification',
400
- stateId,
401
- prompt,
402
- signal,
403
- )
404
- : await ports.callJudge(prompt, signal);
405
- let parsed: unknown;
406
- try {
407
- parsed = parseJudgeJson(raw);
408
- } catch {
409
- // No JSON value could be recovered. Classification failures are
410
- // non-fatal — unlike adjudication, which throws to the failure
411
- // state (PBRT-10) — so surface one status and take no FSM action,
412
- // consistent with the other invalid-reply paths below (PBRT-7).
413
- await ports.emitStatus('Classifier reply was not recoverable JSON');
414
- return undefined;
415
- }
416
- if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
417
- await ports.emitStatus('Classifier returned a non-object JSON response');
418
- return undefined;
419
- }
420
- const obj = parsed as Record<string, unknown>;
421
- const eventType = obj.event;
422
- if (typeof eventType !== 'string') {
423
- await ports.emitStatus('Classifier did not name an event type');
424
- return undefined;
425
- }
426
- const payload =
427
- typeof obj.payload === 'object' &&
428
- obj.payload !== null &&
429
- !Array.isArray(obj.payload)
430
- ? (obj.payload as Record<string, unknown>)
431
- : {};
432
-
433
- switch (eventType) {
434
- case 'NO_ACTION':
435
- case 'NO_FSM_ACTION':
436
- case 'NONE':
437
- return undefined;
438
- case 'START_CODING': {
439
- if (typeof payload.intent !== 'string') {
440
- await ports.emitStatus('Classifier omitted intent for START_CODING');
441
- return undefined;
442
- }
443
- return { type: 'START_CODING', intent: payload.intent };
444
- }
445
- case 'CONTINUE_IR': {
446
- if (typeof payload.irNumber !== 'string') {
447
- await ports.emitStatus('Classifier omitted irNumber for CONTINUE_IR');
448
- return undefined;
449
- }
450
- return { type: 'CONTINUE_IR', irNumber: payload.irNumber };
451
- }
452
- case 'SUMMARIZE_IR': {
453
- if (typeof payload.irNumber !== 'string') {
454
- await ports.emitStatus('Classifier omitted irNumber for SUMMARIZE_IR');
455
- return undefined;
456
- }
457
- return { type: 'SUMMARIZE_IR', irNumber: payload.irNumber };
458
- }
459
- case 'BOSS_INTERRUPT': {
460
- if (typeof payload.targetId !== 'string') {
461
- await ports.emitStatus(
462
- 'Classifier omitted targetId for BOSS_INTERRUPT',
463
- );
464
- return undefined;
465
- }
466
- if (!bossInterruptTargetIds.has(payload.targetId)) {
467
- await ports.emitStatus(
468
- `Classifier supplied invalid targetId for BOSS_INTERRUPT: ${payload.targetId}`,
469
- );
470
- return undefined;
471
- }
472
- return {
473
- type: 'BOSS_INTERRUPT',
474
- targetId: payload.targetId as JumpableStateId,
475
- ...(typeof payload.intent === 'string'
476
- ? { intent: payload.intent }
477
- : {}),
478
- ...(typeof payload.irNumber === 'string'
479
- ? { irNumber: payload.irNumber }
480
- : {}),
481
- };
482
- }
483
- case 'BOSS_REPLY': {
484
- if (state.value !== 'awaitBossReply') {
485
- await ports.emitStatus(
486
- 'Classifier returned BOSS_REPLY outside awaitBossReply',
487
- );
488
- return undefined;
489
- }
490
- if (typeof payload.answer !== 'string') {
491
- await ports.emitStatus('Classifier omitted answer for BOSS_REPLY');
492
- return undefined;
493
- }
494
- const pending = pendingBossQuestionFromContext(state.context);
495
- if (!pending) {
496
- await ports.emitStatus(
497
- 'Classifier returned BOSS_REPLY without a pending question',
498
- );
499
- return undefined;
500
- }
501
- if (
502
- payload.questionId !== undefined &&
503
- typeof payload.questionId !== 'string'
504
- ) {
505
- await ports.emitStatus(
506
- 'Classifier supplied a non-string questionId for BOSS_REPLY',
507
- );
508
- return undefined;
509
- }
510
- if (
511
- typeof payload.questionId === 'string' &&
512
- payload.questionId !== pending.questionId
513
- ) {
514
- await ports.emitStatus(
515
- `Classifier supplied unknown questionId for BOSS_REPLY: ${payload.questionId}`,
516
- );
517
- return undefined;
518
- }
519
- return {
520
- type: 'BOSS_REPLY',
521
- answer: payload.answer,
522
- questionId: pending.questionId,
523
- };
524
- }
525
- default:
526
- await ports.emitStatus(
527
- `Classifier returned unknown event type: ${eventType}`,
528
- );
529
- return undefined;
530
- }
531
- }
532
-
533
- interface ClassifierState {
534
- value: unknown;
535
- context: Record<string, unknown>;
536
- }
537
-
538
- function classifierState(snapshotOrState: unknown): ClassifierState {
539
- if (
540
- snapshotOrState !== null &&
541
- typeof snapshotOrState === 'object' &&
542
- 'value' in snapshotOrState
543
- ) {
544
- const candidate = snapshotOrState as {
545
- value?: unknown;
546
- context?: unknown;
547
- };
548
- return {
549
- value: candidate.value,
550
- context:
551
- candidate.context !== null &&
552
- typeof candidate.context === 'object' &&
553
- !Array.isArray(candidate.context)
554
- ? (candidate.context as Record<string, unknown>)
555
- : {},
556
- };
557
- }
558
- return { value: snapshotOrState, context: {} };
559
- }
560
-
561
- function buildClassifierPrompt(text: string, state: ClassifierState): string {
562
- const currentState =
563
- typeof state.value === 'string'
564
- ? state.value
565
- : JSON.stringify(state.value ?? null);
566
- const pendingBossQuestion = pendingBossQuestionFromContext(state.context);
567
- const lines = [
568
- 'Classify the following Boss message into exactly one of these events.',
569
- 'Respond with JSON: { "event": "<TYPE>", "payload": { ...fields } }.',
570
- 'Use { "event": "NO_ACTION", "payload": {} } when no FSM action should be taken.',
571
- '',
572
- `Current state: ${currentState}`,
573
- ];
574
- if (pendingBossQuestion !== undefined) {
575
- lines.push(
576
- `Pending question id: ${pendingBossQuestion.questionId}`,
577
- `Pending asking player: ${pendingBossQuestion.player}`,
578
- `Pending Boss question: ${pendingBossQuestion.question}`,
579
- );
580
- }
581
- lines.push(
582
- '',
583
- 'Events:',
584
- '- START_CODING: payload { intent: "<free-form goal>" }',
585
- '- CONTINUE_IR: payload { irNumber: "<number>" }',
586
- '- SUMMARIZE_IR: payload { irNumber: "<number>" }',
587
- '- BOSS_INTERRUPT: payload { targetId: "<stateId>", intent?: "<free-form goal>", irNumber?: "<number>" }',
588
- ' targetId must be one of these jumpable states:',
589
- );
590
- for (const target of bossInterruptTargets) {
591
- lines.push(` - ${target.stateId}: ${target.description}`);
592
- }
593
- if (currentState === 'awaitBossReply') {
594
- lines.push(
595
- '- BOSS_REPLY: payload { answer: "<verbatim Boss answer>", questionId?: "<pending question id>" }',
596
- );
597
- } else {
598
- lines.push('- BOSS_REPLY: valid only when Current state is awaitBossReply');
599
- }
600
- lines.push('', 'Boss message:', '```', text, '```');
601
- return lines.join('\n');
602
- }
603
-
604
- // Delegated-player actor bridge — DR-004 §7. One PromiseActorLogic that the
605
- // codingMachine invokes from every player-invoking state, built by the
606
- // shared createPlayerBridge with the CODE binding, composer, and
607
- // adjudication strategy. Per turn: resolve playerId, compose the player
608
- // prompt, await ports.callPlayer, adjudicate the finalText. PlayerResult
609
- // status of 'aborted' or 'error' throws so XState routes via onError →
610
- // #failed (the single fail-stop sink for both Captain errors and player
611
- // failures). Captain remains the orchestrator and adjudicator; it is not
612
- // encoded as the delegated FSM actor.
613
- //
614
- // `getActiveSignal` is the runtime's hook for flowing the Boss's
615
- // `handleBossInput.signal` into the host port calls — fromPromise
616
- // hands the bridge XState's actor-scoped signal, which only fires
617
- // on actor.stop(), not on Boss abort. When omitted (e.g. direct
618
- // captainBridge tests), the bridge falls back to XState's signal.
619
- function captainBridge(
620
- ports: PlaybookPorts,
621
- getActiveSignal?: () => AbortSignal | undefined,
622
- boundary?: RuntimeBoundaryCalls,
623
- onControlPlaneError?: (error: unknown) => void,
624
- ) {
625
- return createPlayerBridge(
626
- {
627
- resolvePlayerId: (input) =>
628
- resolvePlayerId(input as unknown as PlayerInput),
629
- composePlayerPrompt: (input) =>
630
- composePlayerPrompt(input as unknown as PlayerInput),
631
- adjudication: CODE_ADJUDICATION,
632
- resumableStateIds: registeredResumableStateIds,
121
+ /**
122
+ * Preserve authored Markdown quote markers around every line of relayed
123
+ * runtime text. The generic composer preserves the marker itself; CODE's
124
+ * override additionally keeps a multiline value inside that quote.
125
+ */
126
+ function composePlayerPrompt(input: PlayerInput): string {
127
+ const fields = input as unknown as Record<string, unknown>;
128
+ const template = input.prompt
129
+ .split('\n')
130
+ .filter(
131
+ (line) => !(line === '> <run-results>' && input.runResults.length === 0),
132
+ )
133
+ .join('\n');
134
+ const body = template.replace(
135
+ PLACEHOLDER,
136
+ (match: string, token: string, offset: number, source: string) => {
137
+ const value = fields[placeholderField(token)];
138
+ if (typeof value !== 'string') return match;
139
+ const lineStart = source.lastIndexOf('\n', offset - 1) + 1;
140
+ const literal = source.slice(lineStart, offset);
141
+ return literal === '> ' ? quoteContinuation(value) : value;
633
142
  },
634
- ports,
635
- getActiveSignal,
636
- boundary,
637
- onControlPlaneError,
638
143
  );
639
- }
640
-
641
- // Captain pane display — PBRT-3 / PBRT-14.
642
- // The Captain pane is a stream of three glyphs plus one bare
643
- // captain-speech act, designed so a reader can parse each line at a
644
- // glance:
645
- // (no glyph) bare FSM event type — host renders as captain speech
646
- // (e.g., `captain> START_CODING`)
647
- // ⤷ player-invoking state entry: `<Player>: <label>`
648
- // → transition guard outcome (`· field=N` tallies
649
- // appended); the host presenter owns any visual
650
- // nesting under the preceding ⤷ entry
651
-
652
- // ◆ failure state (with `lastError` data) and the
653
- // `awaitBossReply` suspension state (custom payload)
654
- // The runtime emits no status line on entry to the idle state
655
- // (`ready`) or the terminal state (`done`); the next `boss>` prompt
656
- // is the implicit "turn over" signal. Prompts and full player output
657
- // ride the player panes; the Captain pane keeps to the state-machine
658
- // shape.
659
-
660
- const STATE_LABELS: Readonly<Record<string, string>> = {
661
- planAndImplement: 'plan & implement',
662
- respondToReview: 'respond to review',
663
- continueIr: 'continue IR task',
664
- summarizeSpecs: 'summarize IR into specs',
665
- reviewBossCommitSpecs: 'review Boss-intent commit (specs only)',
666
- reviewBossCommitCode: 'review Boss-intent commit (code only)',
667
- reviewBossCommitMixed: 'review Boss-intent commit (specs + code)',
668
- reviewIrTaskCommitSpecs: 'review IR-task commit (specs only)',
669
- reviewIrTaskCommitCode: 'review IR-task commit (code only)',
670
- reviewIrTaskCommitMixed: 'review IR-task commit (specs + code)',
671
- reviewChangesSpecs: 'review uncommitted edits (specs only)',
672
- reviewChangesCode: 'review uncommitted edits (code only)',
673
- reviewChangesMixed: 'review uncommitted edits (specs + code)',
674
- reviewChangesAndChallengesSpecs:
675
- 'review uncommitted edits + rebuttals (specs only)',
676
- reviewChangesAndChallengesCode:
677
- 'review uncommitted edits + rebuttals (code only)',
678
- reviewChangesAndChallengesMixed:
679
- 'review uncommitted edits + rebuttals (specs + code)',
680
- adjudicateChallenges: 'adjudicate rebuttals',
681
- commitCoderInitial: "commit Coder's initial changes",
682
- commitJoint: 'commit reviewed changes',
683
- };
684
-
685
- interface StateMetadata {
686
- player: PlayerInput['player'];
687
- sourceItem: string;
688
- label: string;
689
- }
690
-
691
- const stateMetadata: ReadonlyMap<string, StateMetadata> = (() => {
692
- const m = new Map<string, StateMetadata>();
693
- for (const s of enumerateCaptainStates(codingMachine)) {
694
- const label = STATE_LABELS[s.stateId];
695
- if (!label) {
696
- throw new Error(
697
- `code.playbook.ts: STATE_LABELS missing entry for player-invoking state '${s.stateId}'`,
698
- );
699
- }
700
- const input = s.getInput({});
701
- m.set(s.stateId, { player: input.player, sourceItem: s.sourceItem, label });
702
- }
703
- return m;
704
- })();
705
-
706
- const registeredResumableStateIds: ReadonlySet<string> = new Set(
707
- enumerateAwaitBossReply(codingMachine).bossReplyTransitions.map(
708
- (transition) => transition.target,
709
- ),
710
- );
711
-
712
- const QUIESCENT_STATES: ReadonlySet<string> = new Set([
713
- 'ready',
714
- 'awaitBossReply',
715
- 'done',
716
- 'failed',
717
- ]);
718
-
719
- // States whose entry the runtime does not surface on the Captain
720
- // pane per PBRT-3: the readline returning to its `boss>` prompt is
721
- // the implicit "turn over" signal, so a `◆ ready` / `◆ done`
722
- // tombstone is redundant.
723
- const SUPPRESSED_ENTRY_STATES: ReadonlySet<string> = new Set(['ready', 'done']);
724
-
725
- // Captain-pane surface (PBRT-3): every player-invoking state plus
726
- // the quiescent states whose entry still carries information
727
- // (failure with `lastError`, awaitBossReply with the pending
728
- // question). `ready` and `done` flow through the inspect handler
729
- // but their entries are dropped before emitStatus per
730
- // SUPPRESSED_ENTRY_STATES above.
731
- const CAPTAIN_PANE_STATES: ReadonlySet<string> = new Set([
732
- ...stateMetadata.keys(),
733
- ...QUIESCENT_STATES,
734
- ]);
735
-
736
- type BossReplyQuestionId = NonNullable<
737
- Extract<CodingEvent, { type: 'BOSS_REPLY' }>['questionId']
738
- >;
739
-
740
- interface PendingBossQuestionForStatus {
741
- questionId: BossReplyQuestionId;
742
- resumeStateId: string;
743
- sourceItem: string;
744
- player: string;
745
- question: string;
746
- }
747
-
748
- function pendingBossQuestionFromContext(
749
- context: Record<string, unknown>,
750
- ): PendingBossQuestionForStatus | undefined {
751
- const pending = context.pendingBossQuestion;
752
- if (pending === undefined || typeof pending !== 'object') {
753
- return undefined;
754
- }
755
- const candidate = pending as Partial<
756
- Record<keyof PendingBossQuestionForStatus, unknown>
757
- >;
758
144
  if (
759
- typeof candidate.resumeStateId !== 'string' ||
760
- typeof candidate.questionId !== 'string' ||
761
- typeof candidate.sourceItem !== 'string' ||
762
- typeof candidate.player !== 'string' ||
763
- typeof candidate.question !== 'string'
145
+ input.pendingBossQuestion === undefined ||
146
+ input.bossReply === undefined
764
147
  ) {
765
- return undefined;
766
- }
767
- return {
768
- questionId: candidate.questionId as BossReplyQuestionId,
769
- resumeStateId: candidate.resumeStateId,
770
- sourceItem: candidate.sourceItem,
771
- player: candidate.player,
772
- question: candidate.question,
773
- };
774
- }
775
-
776
- // PBRT-3 / PBRT-14: on entry to `awaitBossReply` the runtime surfaces
777
- // the pending player question as a captain-speech act attributed to the
778
- // asking player (`<player> asks: <full question>`), emitted with no
779
- // glyph so the host renders it as captain speech. The question is
780
- // carried verbatim and in full — the judge JSON that produced it rides
781
- // a hidden callCaptain (PBRT-15), so this line is the Boss's only
782
- // legible view of what was asked.
783
- function formatAwaitBossReplyQuestion(
784
- context: Record<string, unknown>,
785
- ): string {
786
- const pending = pendingBossQuestionFromContext(context);
787
- const player = pending?.player ?? 'unknown';
788
- const question = pending?.question ?? '';
789
- return `${player} asks: ${question}`;
790
- }
791
-
792
- // The rider-less routing marker emitted right after the question line.
793
- // It carries only the resume target, asking player, and source item;
794
- // the former `q="<first 80 chars>"` excerpt rider is dropped now that
795
- // the full question rides the captain-speech line above.
796
- function formatAwaitBossReplyMarker(context: Record<string, unknown>): string {
797
- const pending = pendingBossQuestionFromContext(context);
798
- const resumeStateId = pending?.resumeStateId ?? 'unknown';
799
- const player = pending?.player ?? 'unknown';
800
- const sourceItem = pending?.sourceItem ?? 'unknown';
801
- return `◆ awaiting Boss reply · ${resumeStateId} · ${player} · ${sourceItem}`;
802
- }
803
-
804
- function formatStateEntry(stateId: string): string | undefined {
805
- if (SUPPRESSED_ENTRY_STATES.has(stateId)) return undefined;
806
- if (stateId === 'failed') return '◆ failed';
807
- const meta = stateMetadata.get(stateId);
808
- if (!meta) return `⤷ ${stateId}`;
809
- return `⤷ ${meta.player}: ${meta.label}`;
810
- }
811
-
812
- function formatTransition(event: unknown): string | undefined {
813
- const output = (event as { output?: Record<string, unknown> })?.output;
814
- if (!output || typeof output.guard !== 'string') return undefined;
815
- const tallies: string[] = [];
816
- for (const field of ['reviews', 'challenges'] as const) {
817
- const value = output[field];
818
- if (typeof value === 'string' && value.trim().length > 0) {
819
- const items = value.match(/^\s*\d+\.\s/gm);
820
- tallies.push(`${field}=${items ? items.length : 1}`);
821
- }
822
- }
823
- const suffix = tallies.length > 0 ? ` · ${tallies.join(' · ')}` : '';
824
- // No leading whitespace: visual nesting under the preceding ⤷
825
- // entry is the host presenter's concern (cligent's writeStatusLine
826
- // emits status messages verbatim, with its own chrome but no
827
- // continuation indent; layout is its job, not ours).
828
- return `→ ${output.guard}${suffix}`;
829
- }
830
-
831
- function formatClassification(eventType: string): string {
832
- return eventType;
833
- }
834
-
835
- function stateTelemetryPayload(
836
- from: unknown,
837
- to: string,
838
- event: unknown,
839
- context: Record<string, unknown>,
840
- ): Record<string, unknown> {
841
- const payload: Record<string, unknown> = {
842
- from: from ?? null,
843
- to,
844
- event: normalizeEventForTelemetry(event) ?? null,
845
- };
846
- if (to === 'awaitBossReply') {
847
- const pendingBossQuestion = pendingBossQuestionFromContext(context);
848
- if (pendingBossQuestion !== undefined) {
849
- payload.pendingBossQuestion = pendingBossQuestion;
850
- }
148
+ return body;
851
149
  }
852
- if (to === 'failed') {
853
- const lastError = normalizeErrorFull(context.lastError);
854
- if (lastError !== undefined) {
855
- payload.lastError = lastError;
856
- }
857
- }
858
- return payload;
150
+ return [
151
+ CONTINUATION_PREAMBLE,
152
+ `Boss question:\n${input.pendingBossQuestion.question}`,
153
+ `Boss reply:\n${input.bossReply}`,
154
+ body,
155
+ ].join('\n\n');
859
156
  }
860
157
 
861
- // Captain-pane status lines for a root transition (PBRT-3 / PBRT-14):
862
- // the `→ guard` outcome line for the settling transition, then either
863
- // the awaitBossReply question + rider-less marker pair or the state's
864
- // entry line (with `lastError` data on `failed`).
865
- function statusesForState(
866
- state: PlaybookState,
867
- context: Record<string, unknown>,
868
- event: unknown,
869
- ): ScheduledStatus[] {
870
- const to = state.stateId;
871
- if (to === undefined || !CAPTAIN_PANE_STATES.has(to)) return [];
872
- const statuses: ScheduledStatus[] = [];
873
- const transitionLine = formatTransition(event);
874
- if (transitionLine !== undefined) {
875
- statuses.push({ message: transitionLine });
876
- }
877
- if (to === 'awaitBossReply') {
878
- statuses.push(
879
- { message: formatAwaitBossReplyQuestion(context) },
880
- { message: formatAwaitBossReplyMarker(context) },
881
- );
882
- } else {
883
- const entryLine = formatStateEntry(to);
884
- if (entryLine !== undefined) {
885
- const lastError =
886
- to === 'failed' ? normalizeErrorCompact(context.lastError) : undefined;
887
- statuses.push({
888
- message: entryLine,
889
- ...(lastError === undefined
890
- ? {}
891
- : {
892
- data: snapshotJsonValue({ lastError }, 'failed status data'),
893
- }),
894
- });
895
- }
896
- }
897
- return statuses;
898
- }
899
-
900
- // Internal export surface for tests. Not part of the stable public API;
901
- // the leading underscore signals "subject to change."
902
158
  export const _internal = {
903
159
  composePlayerPrompt,
904
- resolvePlayerId,
905
- adjudicate,
906
- classifyBossText,
907
- captainBridge,
908
- STATE_LABELS,
909
- stateMetadata,
910
- pendingBossQuestionFromContext,
911
- formatAwaitBossReplyQuestion,
912
- formatAwaitBossReplyMarker,
913
- formatStateEntry,
914
- formatTransition,
915
- formatClassification,
916
- stateTelemetryPayload,
917
- normalizeErrorCompact,
918
- normalizeErrorFull,
919
- normalizeEventForTelemetry,
920
160
  VERBATIM_PAYLOAD_FIELDS,
921
161
  };
922
162
 
923
- // The CODE-specific spec handed to the shared runtime factory
924
- // (slc/link.md §Output, DR-019). The generic machinery — actor wiring,
925
- // boundary tracing, Boss-turn lifecycle, nested-playbook bridge, and the
926
- // DR-014 parked-session snapshot capability — lives in
927
- // @sublang/playbook/xstate-runtime; this spec carries only what is
928
- // CODE-specific.
929
163
  const runtimeSpec: XStatePlaybookRuntimeSpec<CodePlaybookOptions> = {
930
164
  label: 'CODE',
931
- snapshotOptions: snapshotCodePlaybookOptions,
932
- resolvePlayerId: (input) => resolvePlayerId(input as unknown as PlayerInput),
933
- composePlayerPrompt: (input) =>
934
- composePlayerPrompt(input as unknown as PlayerInput),
935
- buildJudgePrompt: CODE_ADJUDICATION.buildJudgePrompt,
936
- extractRequiredFields,
165
+ compat: { artifactSchema: 1, runtimeAbi: RUNTIME_ABI },
166
+ snapshotOptions: snapshotCodeOptions,
167
+ entryEvent: { type: 'START_CODE', textField: 'callerInput' },
168
+ playerStates: {
169
+ runFirstPhase: {
170
+ player: 'Coder',
171
+ label:
172
+ 'Coder is implementing one direct phase or committing a new intent record.',
173
+ },
174
+ runIrTask: {
175
+ player: 'Coder',
176
+ label: 'Coder is implementing exactly one unfinished intent-record task.',
177
+ },
178
+ },
179
+ placeholderFields: { 'coder-llm': 'coderPlayer' },
180
+ composePlayerPrompt: (input: PlaybookPlayerInput) =>
181
+ composePlayerPrompt(input as PlayerInput),
937
182
  verbatimPayloadFields: VERBATIM_PAYLOAD_FIELDS,
938
- resumableStateIds: registeredResumableStateIds,
939
- // PBRT-52: CODE's own ControlView context projection. Exactly the four
940
- // closed-vocabulary classification members the FSM assigns itself, which
941
- // are what a controller needs to see to describe or steer a CODE
942
- // engagement. Everything else in `CodingContext` is deliberately absent:
943
- // `coderPlayer` / `reviewerPlayer` / `committerPlayer` are the resolved
944
- // host player roster, `intent` is an option value, and `irNumber`,
945
- // `taskDescription`, `reviews`, `challenges`, and `lastResult` all carry
946
- // player-authored text — all of which a session-Captain prompt must
947
- // exclude, or may carry only as fenced quotes
948
- // (CAPTAIN-9). A member added to `CodingContext`
949
- // later stays private until it is named here.
950
- controlContextFields: [
951
- 'workflow',
952
- 'changeOrigin',
953
- 'reviewSubject',
954
- 'afterReview',
955
- ],
956
- classifyBossText: (text, ports, signal, snapshotOrState, boundary) =>
957
- classifyBossText(text, ports, signal, snapshotOrState, boundary),
958
- classificationStatus: (event) => formatClassification(event.type),
959
- statusesForState,
960
- normalizeTransitionEvent: (event) =>
961
- normalizeEventForTelemetry(event) as JsonValue | undefined,
183
+ controlContextFields: ['phase'],
184
+ transitionEventFields: ['callerInput', 'answer', 'questionId'],
962
185
  };
963
186
 
964
187
  const createPlaybookRuntime: PlaybookRuntimeFactory<CodePlaybookOptions> =