@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.
- package/README.md +11 -7
- package/docs/cli.md +38 -35
- package/docs/configuration.md +58 -15
- package/docs/embedding.md +24 -16
- package/package.json +40 -21
- package/reference/sdlc/captain.playbook/captain.playbook.js +2 -0
- package/reference/sdlc/captain.playbook/captain.playbook.ts +2 -0
- package/reference/sdlc/code.md +55 -97
- package/reference/sdlc/code.playbook/code.fsm.d.ts +229 -94
- package/reference/sdlc/code.playbook/code.fsm.introspect.d.ts +26 -44
- package/reference/sdlc/code.playbook/code.fsm.introspect.js +61 -66
- package/reference/sdlc/code.playbook/code.fsm.introspect.ts +100 -149
- package/reference/sdlc/code.playbook/code.fsm.js +587 -1347
- package/reference/sdlc/code.playbook/code.fsm.ts +809 -1650
- package/reference/sdlc/code.playbook/code.gears.md +51 -263
- package/reference/sdlc/code.playbook/code.playbook.d.ts +8 -47
- package/reference/sdlc/code.playbook/code.playbook.js +69 -656
- package/reference/sdlc/code.playbook/code.playbook.ts +90 -867
- package/reference/sdlc/code.playbook/code.registry.d.ts +9 -25
- package/reference/sdlc/code.playbook/code.registry.js +20 -78
- package/reference/sdlc/code.playbook/code.registry.ts +58 -122
- package/reference/sdlc/code.playbook/playbook-captain.js +93 -15
- package/reference/sdlc/code.playbook/playbook-captain.ts +115 -19
- package/reference/sdlc/code.playbook/playbook.config.template.yaml +33 -22
- package/reference/sdlc/decide.md +54 -0
- package/reference/sdlc/decide.playbook/decide.fsm.d.ts +261 -0
- package/reference/sdlc/decide.playbook/decide.fsm.js +894 -0
- package/reference/sdlc/decide.playbook/decide.fsm.ts +1152 -0
- package/reference/sdlc/decide.playbook/decide.gears.md +88 -0
- package/reference/sdlc/decide.playbook/decide.playbook.d.ts +67 -0
- package/reference/sdlc/{discuss.playbook/discuss.playbook.js → decide.playbook/decide.playbook.js} +471 -362
- package/reference/sdlc/{discuss.playbook/discuss.playbook.ts → decide.playbook/decide.playbook.ts} +575 -443
- package/reference/sdlc/decide.playbook/decide.registry.d.ts +41 -0
- package/reference/sdlc/decide.playbook/decide.registry.js +60 -0
- package/reference/sdlc/decide.playbook/decide.registry.ts +125 -0
- package/reference/sdlc/review.md +81 -0
- package/reference/sdlc/review.playbook/review.fsm.d.ts +183 -0
- package/reference/sdlc/review.playbook/review.fsm.js +524 -0
- package/reference/sdlc/review.playbook/review.fsm.ts +652 -0
- package/reference/sdlc/review.playbook/review.gears.md +112 -0
- package/reference/sdlc/review.playbook/review.playbook.d.ts +12 -0
- package/reference/sdlc/review.playbook/review.playbook.js +112 -0
- package/reference/sdlc/review.playbook/review.playbook.ts +201 -0
- package/reference/sdlc/review.playbook/review.registry.d.ts +43 -0
- package/reference/sdlc/review.playbook/review.registry.js +73 -0
- package/reference/sdlc/review.playbook/review.registry.ts +138 -0
- package/slc/gears2fsm.md +13 -4
- package/slc/link.md +48 -2
- package/slc/text2gears.md +22 -2
- package/src/runtime.d.ts +7 -0
- package/src/runtime.ts +12 -0
- package/src/xstate-playbook-runtime.d.ts +9 -2
- package/src/xstate-playbook-runtime.js +255 -21
- package/src/xstate-playbook-runtime.ts +333 -28
- package/src/xstate-runtime.js +25 -0
- package/src/xstate-runtime.ts +51 -0
- package/reference/sdlc/discuss.md +0 -93
- package/reference/sdlc/discuss.playbook/discuss.fsm.d.ts +0 -396
- package/reference/sdlc/discuss.playbook/discuss.fsm.js +0 -2067
- package/reference/sdlc/discuss.playbook/discuss.fsm.ts +0 -2465
- package/reference/sdlc/discuss.playbook/discuss.gears.md +0 -258
- package/reference/sdlc/discuss.playbook/discuss.playbook.d.ts +0 -113
- package/reference/sdlc/discuss.playbook/discuss.registry.d.ts +0 -58
- package/reference/sdlc/discuss.playbook/discuss.registry.js +0 -97
- package/reference/sdlc/discuss.playbook/discuss.registry.ts +0 -153
|
@@ -3,28 +3,21 @@
|
|
|
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
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
|
|
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.
|
|
24
|
-
import { createPlayerBridge, createXStatePlaybookRuntime, adjudicatePlayerOutput, normalizeError, normalizeErrorCompact, normalizeErrorFull, parseJudgeJson, snapshotJsonValue, } from '../../../src/xstate-runtime.js';
|
|
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
|
|
12
|
+
import { RUNTIME_ABI, createXStatePlaybookRuntime, snapshotJsonValue, } from '@sublang/playbook/xstate-runtime';
|
|
25
13
|
import { codingMachine, } from './code.fsm.js';
|
|
26
|
-
|
|
27
|
-
|
|
14
|
+
const OPTION_KEYS = new Set(['coderPlayer', 'runResults']);
|
|
15
|
+
const PLACEHOLDER = /<(#|[A-Za-z_$][A-Za-z0-9_$-]*)>/g;
|
|
16
|
+
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
|
+
const VERBATIM_PAYLOAD_FIELDS = new Set([
|
|
18
|
+
'coderOutput',
|
|
19
|
+
]);
|
|
20
|
+
function snapshotCodeOptions(value) {
|
|
28
21
|
const captured = snapshotJsonValue(value, 'CODE runtime options');
|
|
29
22
|
if (captured === null ||
|
|
30
23
|
typeof captured !== 'object' ||
|
|
@@ -32,15 +25,8 @@ function snapshotCodePlaybookOptions(value) {
|
|
|
32
25
|
throw new TypeError('CODE runtime options must be an object');
|
|
33
26
|
}
|
|
34
27
|
const record = captured;
|
|
35
|
-
const allowed = new Set([
|
|
36
|
-
'intent',
|
|
37
|
-
'irNumber',
|
|
38
|
-
'coderPlayer',
|
|
39
|
-
'reviewerPlayer',
|
|
40
|
-
'committerPlayer',
|
|
41
|
-
]);
|
|
42
28
|
for (const [key, option] of Object.entries(record)) {
|
|
43
|
-
if (!
|
|
29
|
+
if (!OPTION_KEYS.has(key)) {
|
|
44
30
|
throw new TypeError(`CODE runtime options.${key} is not declared`);
|
|
45
31
|
}
|
|
46
32
|
if (typeof option !== 'string') {
|
|
@@ -49,643 +35,70 @@ function snapshotCodePlaybookOptions(value) {
|
|
|
49
35
|
}
|
|
50
36
|
return captured;
|
|
51
37
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
68
|
-
function normalizeEventValue(value, path, ancestors) {
|
|
69
|
-
if (Array.isArray(value))
|
|
70
|
-
return snapshotJsonValue(value, path);
|
|
71
|
-
if (value === null || typeof value !== 'object') {
|
|
72
|
-
return snapshotJsonValue(value, path);
|
|
73
|
-
}
|
|
74
|
-
if (ancestors.has(value)) {
|
|
75
|
-
throw new TypeError(`${path} must not contain a JSON cycle`);
|
|
76
|
-
}
|
|
77
|
-
const prototype = Object.getPrototypeOf(value);
|
|
78
|
-
if (prototype !== Object.prototype && prototype !== null) {
|
|
79
|
-
return snapshotJsonValue(value, path);
|
|
80
|
-
}
|
|
81
|
-
if (Object.getOwnPropertySymbols(value).length > 0) {
|
|
82
|
-
return snapshotJsonValue(value, path);
|
|
83
|
-
}
|
|
84
|
-
const nextAncestors = new Set(ancestors).add(value);
|
|
85
|
-
const normalized = {};
|
|
86
|
-
for (const [key, descriptor] of Object.entries(Object.getOwnPropertyDescriptors(value))) {
|
|
87
|
-
if (!descriptor.enumerable) {
|
|
88
|
-
throw new TypeError(`${path}.${key} must be an enumerable JSON property`);
|
|
89
|
-
}
|
|
90
|
-
if (!Object.prototype.hasOwnProperty.call(descriptor, 'value')) {
|
|
91
|
-
throw new TypeError(`${path}.${key} must be a JSON data property`);
|
|
92
|
-
}
|
|
93
|
-
if (descriptor.value === undefined)
|
|
94
|
-
continue;
|
|
95
|
-
normalized[key] =
|
|
96
|
-
key === 'error'
|
|
97
|
-
? snapshotJsonValue(normalizeError(descriptor.value), `${path}.error`)
|
|
98
|
-
: normalizeEventValue(descriptor.value, `${path}.${key}`, nextAncestors);
|
|
99
|
-
}
|
|
100
|
-
return snapshotJsonValue(normalized, path);
|
|
101
|
-
}
|
|
102
|
-
// Player-prompt composer — DR-004 §6.
|
|
103
|
-
// Substitutes the three placeholder tokens in `input.prompt` (literal
|
|
104
|
-
// string replace, no escaping) and arranges labelled blocks around
|
|
105
|
-
// the prompt body. Context blocks the body refers to as prior
|
|
106
|
-
// material (`Boss intent:`, `Task description:`) are prepended;
|
|
107
|
-
// action blocks the body refers to as material below
|
|
108
|
-
// (`Review items:`, `Rebuttals:`) are appended after the body so the
|
|
109
|
-
// CODE-N prompts' "review item below" / "rebuttal below" phrasing
|
|
110
|
-
// matches the rendered layout. When a state resumes from a Boss
|
|
111
|
-
// reply, the continuation preamble and Q/A blocks precede every
|
|
112
|
-
// ordinary block. The FSM's prompt body is never re-flowed.
|
|
38
|
+
function placeholderField(token) {
|
|
39
|
+
if (token === '#')
|
|
40
|
+
return 'irNumber';
|
|
41
|
+
if (token === 'coder-llm')
|
|
42
|
+
return 'coderPlayer';
|
|
43
|
+
return token.replace(/-([A-Za-z0-9_$])/g, (_match, next) => next.toUpperCase());
|
|
44
|
+
}
|
|
45
|
+
function quoteContinuation(value) {
|
|
46
|
+
return value.replaceAll('\n', '\n> ');
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Preserve authored Markdown quote markers around every line of relayed
|
|
50
|
+
* runtime text. The generic composer preserves the marker itself; CODE's
|
|
51
|
+
* override additionally keeps a multiline value inside that quote.
|
|
52
|
+
*/
|
|
113
53
|
function composePlayerPrompt(input) {
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
body
|
|
136
|
-
|
|
137
|
-
blocks.push(body);
|
|
138
|
-
if (input.reviews !== undefined) {
|
|
139
|
-
blocks.push(`Review items:\n${input.reviews}`);
|
|
140
|
-
}
|
|
141
|
-
if (input.challenges !== undefined) {
|
|
142
|
-
blocks.push(`Rebuttals:\n${input.challenges}`);
|
|
143
|
-
}
|
|
144
|
-
return blocks.join('\n\n');
|
|
145
|
-
}
|
|
146
|
-
// Player-id resolver — DR-004 §2 / PBRT-8.
|
|
147
|
-
// Non-composite: Coder→'coder', Reviewer→'reviewer'. The composite
|
|
148
|
-
// Committer (= Coder | Reviewer per code.gears.md) resolves to the
|
|
149
|
-
// configured alias when present: `input.committerPlayer`, the
|
|
150
|
-
// validated `captain.options.playbooks.code.options.committer`
|
|
151
|
-
// (PBRT-8 / PBRT-30), already a baked player id ('coder' / 'reviewer').
|
|
152
|
-
// Absent a
|
|
153
|
-
// configured alias it falls back to the DR-004 §2 baked binding by
|
|
154
|
-
// populated <playerName>Player field: prefer `coderPlayer` (CODE-18
|
|
155
|
-
// wires only coderPlayer; CODE-19 wires both so coderPlayer still wins
|
|
156
|
-
// as the alias's first alternative), fall back to `reviewerPlayer`
|
|
157
|
-
// only if a future gear ships a reviewer-only Committer item, and
|
|
158
|
-
// finally to the alias's first alternative (Coder) when neither is
|
|
159
|
-
// set. The alias selects only the host pane; it is not a PBRT-4
|
|
160
|
-
// identity string, so it leaves <coder-llm> / <reviewer-llm>
|
|
161
|
-
// untouched and `input.player` stays `Committer` (PLAYBOOK-3).
|
|
162
|
-
function resolvePlayerId(input) {
|
|
163
|
-
switch (input.player) {
|
|
164
|
-
case 'Coder':
|
|
165
|
-
return 'coder';
|
|
166
|
-
case 'Reviewer':
|
|
167
|
-
return 'reviewer';
|
|
168
|
-
case 'Committer':
|
|
169
|
-
if (input.committerPlayer !== undefined)
|
|
170
|
-
return input.committerPlayer;
|
|
171
|
-
if (input.coderPlayer !== undefined)
|
|
172
|
-
return 'coder';
|
|
173
|
-
if (input.reviewerPlayer !== undefined)
|
|
174
|
-
return 'reviewer';
|
|
175
|
-
return 'coder';
|
|
176
|
-
default: {
|
|
177
|
-
const exhaustive = input.player;
|
|
178
|
-
throw new Error(`resolvePlayerId: unknown player ${String(exhaustive)}`);
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
function extractRequiredFields(description) {
|
|
183
|
-
const fields = [];
|
|
184
|
-
const re = /Output shall include `([A-Za-z_][A-Za-z0-9_]*):/g;
|
|
185
|
-
for (const m of description.matchAll(re)) {
|
|
186
|
-
fields.push(m[1]);
|
|
187
|
-
}
|
|
188
|
-
return fields;
|
|
189
|
-
}
|
|
190
|
-
function buildJudgePrompt(input, finalText) {
|
|
191
|
-
const lines = [];
|
|
192
|
-
lines.push('This is hidden control work. Do not call tools, inspect files, or ' +
|
|
193
|
-
'seek external evidence. Decide only from the supplied player output ' +
|
|
194
|
-
'and outcome descriptions. Reply with exactly one JSON object and no prose.');
|
|
195
|
-
lines.push('');
|
|
196
|
-
lines.push(`The ${input.player} just produced this output:`);
|
|
197
|
-
lines.push('');
|
|
198
|
-
lines.push('```');
|
|
199
|
-
lines.push(finalText);
|
|
200
|
-
lines.push('```');
|
|
201
|
-
lines.push('');
|
|
202
|
-
lines.push('Pick exactly one outcome by `guard` and return JSON ' +
|
|
203
|
-
'`{ guard, …payloadFields }`. Required payload fields are ' +
|
|
204
|
-
'named in the outcome description. Do not copy long-form ' +
|
|
205
|
-
'verbatim fields (`reviews`, `challenges`) — the runtime ' +
|
|
206
|
-
"carries the player's output into those fields verbatim, " +
|
|
207
|
-
'so any value you supply for them will be overwritten.');
|
|
208
|
-
lines.push('');
|
|
209
|
-
for (const [key, description] of Object.entries(input.result)) {
|
|
210
|
-
lines.push(`- \`${key}\` — ${description}`);
|
|
211
|
-
}
|
|
212
|
-
return lines.join('\n');
|
|
213
|
-
}
|
|
214
|
-
// CODE-specific adjudication strategy: the CODE judge prompt above, the
|
|
215
|
-
// DR-004 `Output shall include` required-field extraction, and the
|
|
216
|
-
// verbatim long-form payload fields.
|
|
217
|
-
const CODE_ADJUDICATION = {
|
|
218
|
-
buildJudgePrompt: (input, finalText) => buildJudgePrompt(input, finalText),
|
|
219
|
-
extractRequiredFields,
|
|
220
|
-
verbatimPayloadFields: VERBATIM_PAYLOAD_FIELDS,
|
|
221
|
-
};
|
|
222
|
-
// LLM judge — DR-004 §4. Delegates to the shared adjudicator with the
|
|
223
|
-
// CODE strategy: it lists each declared outcome verbatim, asks
|
|
224
|
-
// ports.callJudge for a JSON `{ guard, …payloadFields }` response, and
|
|
225
|
-
// returns the parsed object once the chosen guard is one of the
|
|
226
|
-
// input.result keys. Adjudicator failures (malformed JSON,
|
|
227
|
-
// missing/unknown guard) are control-plane errors and propagate via
|
|
228
|
-
// throw per slc/link.md.
|
|
229
|
-
async function adjudicate(input, finalText, ports, signal, boundary) {
|
|
230
|
-
return (await adjudicatePlayerOutput(CODE_ADJUDICATION, input, finalText, ports, signal, boundary));
|
|
231
|
-
}
|
|
232
|
-
// Boss-event classifier — DR-004 §3.
|
|
233
|
-
// Every non-empty Boss turn goes through ports.callJudge. Slash-prefixed
|
|
234
|
-
// text is ordinary content once a host has routed the turn to this
|
|
235
|
-
// playbook; `/command` selection belongs outside handleBossInput. The
|
|
236
|
-
// classifier is state-aware so awaitBossReply can distinguish a direct
|
|
237
|
-
// answer (BOSS_REPLY) from a fresh directive that abandons the pending
|
|
238
|
-
// question through the FSM's existing transitions.
|
|
239
|
-
async function classifyBossText(text, ports, signal, snapshotOrState, boundary) {
|
|
240
|
-
const trimmed = text.trim();
|
|
241
|
-
if (trimmed === '')
|
|
242
|
-
return undefined;
|
|
243
|
-
return classifyWithLlm(text, ports, signal, snapshotOrState, boundary);
|
|
244
|
-
}
|
|
245
|
-
const rootEvents = enumerateRootEvents(codingMachine);
|
|
246
|
-
const bossInterruptTargets = rootEvents.bossInterruptTargetDescriptions;
|
|
247
|
-
const bossInterruptTargetIds = new Set(bossInterruptTargets.map((target) => target.stateId));
|
|
248
|
-
async function classifyWithLlm(text, ports, signal, snapshotOrState, boundary) {
|
|
249
|
-
const state = classifierState(snapshotOrState);
|
|
250
|
-
const prompt = buildClassifierPrompt(text, state);
|
|
251
|
-
const stateId = typeof state.value === 'string' ? state.value : undefined;
|
|
252
|
-
const raw = boundary
|
|
253
|
-
? await boundary.callJudge('boss-input-classification', stateId, prompt, signal)
|
|
254
|
-
: await ports.callJudge(prompt, signal);
|
|
255
|
-
let parsed;
|
|
256
|
-
try {
|
|
257
|
-
parsed = parseJudgeJson(raw);
|
|
258
|
-
}
|
|
259
|
-
catch {
|
|
260
|
-
// No JSON value could be recovered. Classification failures are
|
|
261
|
-
// non-fatal — unlike adjudication, which throws to the failure
|
|
262
|
-
// state (PBRT-10) — so surface one status and take no FSM action,
|
|
263
|
-
// consistent with the other invalid-reply paths below (PBRT-7).
|
|
264
|
-
await ports.emitStatus('Classifier reply was not recoverable JSON');
|
|
265
|
-
return undefined;
|
|
266
|
-
}
|
|
267
|
-
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
268
|
-
await ports.emitStatus('Classifier returned a non-object JSON response');
|
|
269
|
-
return undefined;
|
|
270
|
-
}
|
|
271
|
-
const obj = parsed;
|
|
272
|
-
const eventType = obj.event;
|
|
273
|
-
if (typeof eventType !== 'string') {
|
|
274
|
-
await ports.emitStatus('Classifier did not name an event type');
|
|
275
|
-
return undefined;
|
|
276
|
-
}
|
|
277
|
-
const payload = typeof obj.payload === 'object' &&
|
|
278
|
-
obj.payload !== null &&
|
|
279
|
-
!Array.isArray(obj.payload)
|
|
280
|
-
? obj.payload
|
|
281
|
-
: {};
|
|
282
|
-
switch (eventType) {
|
|
283
|
-
case 'NO_ACTION':
|
|
284
|
-
case 'NO_FSM_ACTION':
|
|
285
|
-
case 'NONE':
|
|
286
|
-
return undefined;
|
|
287
|
-
case 'START_CODING': {
|
|
288
|
-
if (typeof payload.intent !== 'string') {
|
|
289
|
-
await ports.emitStatus('Classifier omitted intent for START_CODING');
|
|
290
|
-
return undefined;
|
|
291
|
-
}
|
|
292
|
-
return { type: 'START_CODING', intent: payload.intent };
|
|
293
|
-
}
|
|
294
|
-
case 'CONTINUE_IR': {
|
|
295
|
-
if (typeof payload.irNumber !== 'string') {
|
|
296
|
-
await ports.emitStatus('Classifier omitted irNumber for CONTINUE_IR');
|
|
297
|
-
return undefined;
|
|
298
|
-
}
|
|
299
|
-
return { type: 'CONTINUE_IR', irNumber: payload.irNumber };
|
|
300
|
-
}
|
|
301
|
-
case 'SUMMARIZE_IR': {
|
|
302
|
-
if (typeof payload.irNumber !== 'string') {
|
|
303
|
-
await ports.emitStatus('Classifier omitted irNumber for SUMMARIZE_IR');
|
|
304
|
-
return undefined;
|
|
305
|
-
}
|
|
306
|
-
return { type: 'SUMMARIZE_IR', irNumber: payload.irNumber };
|
|
307
|
-
}
|
|
308
|
-
case 'BOSS_INTERRUPT': {
|
|
309
|
-
if (typeof payload.targetId !== 'string') {
|
|
310
|
-
await ports.emitStatus('Classifier omitted targetId for BOSS_INTERRUPT');
|
|
311
|
-
return undefined;
|
|
312
|
-
}
|
|
313
|
-
if (!bossInterruptTargetIds.has(payload.targetId)) {
|
|
314
|
-
await ports.emitStatus(`Classifier supplied invalid targetId for BOSS_INTERRUPT: ${payload.targetId}`);
|
|
315
|
-
return undefined;
|
|
316
|
-
}
|
|
317
|
-
return {
|
|
318
|
-
type: 'BOSS_INTERRUPT',
|
|
319
|
-
targetId: payload.targetId,
|
|
320
|
-
...(typeof payload.intent === 'string'
|
|
321
|
-
? { intent: payload.intent }
|
|
322
|
-
: {}),
|
|
323
|
-
...(typeof payload.irNumber === 'string'
|
|
324
|
-
? { irNumber: payload.irNumber }
|
|
325
|
-
: {}),
|
|
326
|
-
};
|
|
327
|
-
}
|
|
328
|
-
case 'BOSS_REPLY': {
|
|
329
|
-
if (state.value !== 'awaitBossReply') {
|
|
330
|
-
await ports.emitStatus('Classifier returned BOSS_REPLY outside awaitBossReply');
|
|
331
|
-
return undefined;
|
|
332
|
-
}
|
|
333
|
-
if (typeof payload.answer !== 'string') {
|
|
334
|
-
await ports.emitStatus('Classifier omitted answer for BOSS_REPLY');
|
|
335
|
-
return undefined;
|
|
336
|
-
}
|
|
337
|
-
const pending = pendingBossQuestionFromContext(state.context);
|
|
338
|
-
if (!pending) {
|
|
339
|
-
await ports.emitStatus('Classifier returned BOSS_REPLY without a pending question');
|
|
340
|
-
return undefined;
|
|
341
|
-
}
|
|
342
|
-
if (payload.questionId !== undefined &&
|
|
343
|
-
typeof payload.questionId !== 'string') {
|
|
344
|
-
await ports.emitStatus('Classifier supplied a non-string questionId for BOSS_REPLY');
|
|
345
|
-
return undefined;
|
|
346
|
-
}
|
|
347
|
-
if (typeof payload.questionId === 'string' &&
|
|
348
|
-
payload.questionId !== pending.questionId) {
|
|
349
|
-
await ports.emitStatus(`Classifier supplied unknown questionId for BOSS_REPLY: ${payload.questionId}`);
|
|
350
|
-
return undefined;
|
|
351
|
-
}
|
|
352
|
-
return {
|
|
353
|
-
type: 'BOSS_REPLY',
|
|
354
|
-
answer: payload.answer,
|
|
355
|
-
questionId: pending.questionId,
|
|
356
|
-
};
|
|
357
|
-
}
|
|
358
|
-
default:
|
|
359
|
-
await ports.emitStatus(`Classifier returned unknown event type: ${eventType}`);
|
|
360
|
-
return undefined;
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
function classifierState(snapshotOrState) {
|
|
364
|
-
if (snapshotOrState !== null &&
|
|
365
|
-
typeof snapshotOrState === 'object' &&
|
|
366
|
-
'value' in snapshotOrState) {
|
|
367
|
-
const candidate = snapshotOrState;
|
|
368
|
-
return {
|
|
369
|
-
value: candidate.value,
|
|
370
|
-
context: candidate.context !== null &&
|
|
371
|
-
typeof candidate.context === 'object' &&
|
|
372
|
-
!Array.isArray(candidate.context)
|
|
373
|
-
? candidate.context
|
|
374
|
-
: {},
|
|
375
|
-
};
|
|
376
|
-
}
|
|
377
|
-
return { value: snapshotOrState, context: {} };
|
|
378
|
-
}
|
|
379
|
-
function buildClassifierPrompt(text, state) {
|
|
380
|
-
const currentState = typeof state.value === 'string'
|
|
381
|
-
? state.value
|
|
382
|
-
: JSON.stringify(state.value ?? null);
|
|
383
|
-
const pendingBossQuestion = pendingBossQuestionFromContext(state.context);
|
|
384
|
-
const lines = [
|
|
385
|
-
'Classify the following Boss message into exactly one of these events.',
|
|
386
|
-
'Respond with JSON: { "event": "<TYPE>", "payload": { ...fields } }.',
|
|
387
|
-
'Use { "event": "NO_ACTION", "payload": {} } when no FSM action should be taken.',
|
|
388
|
-
'',
|
|
389
|
-
`Current state: ${currentState}`,
|
|
390
|
-
];
|
|
391
|
-
if (pendingBossQuestion !== undefined) {
|
|
392
|
-
lines.push(`Pending question id: ${pendingBossQuestion.questionId}`, `Pending asking player: ${pendingBossQuestion.player}`, `Pending Boss question: ${pendingBossQuestion.question}`);
|
|
393
|
-
}
|
|
394
|
-
lines.push('', 'Events:', '- START_CODING: payload { intent: "<free-form goal>" }', '- CONTINUE_IR: payload { irNumber: "<number>" }', '- SUMMARIZE_IR: payload { irNumber: "<number>" }', '- BOSS_INTERRUPT: payload { targetId: "<stateId>", intent?: "<free-form goal>", irNumber?: "<number>" }', ' targetId must be one of these jumpable states:');
|
|
395
|
-
for (const target of bossInterruptTargets) {
|
|
396
|
-
lines.push(` - ${target.stateId}: ${target.description}`);
|
|
397
|
-
}
|
|
398
|
-
if (currentState === 'awaitBossReply') {
|
|
399
|
-
lines.push('- BOSS_REPLY: payload { answer: "<verbatim Boss answer>", questionId?: "<pending question id>" }');
|
|
400
|
-
}
|
|
401
|
-
else {
|
|
402
|
-
lines.push('- BOSS_REPLY: valid only when Current state is awaitBossReply');
|
|
403
|
-
}
|
|
404
|
-
lines.push('', 'Boss message:', '```', text, '```');
|
|
405
|
-
return lines.join('\n');
|
|
406
|
-
}
|
|
407
|
-
// Delegated-player actor bridge — DR-004 §7. One PromiseActorLogic that the
|
|
408
|
-
// codingMachine invokes from every player-invoking state, built by the
|
|
409
|
-
// shared createPlayerBridge with the CODE binding, composer, and
|
|
410
|
-
// adjudication strategy. Per turn: resolve playerId, compose the player
|
|
411
|
-
// prompt, await ports.callPlayer, adjudicate the finalText. PlayerResult
|
|
412
|
-
// status of 'aborted' or 'error' throws so XState routes via onError →
|
|
413
|
-
// #failed (the single fail-stop sink for both Captain errors and player
|
|
414
|
-
// failures). Captain remains the orchestrator and adjudicator; it is not
|
|
415
|
-
// encoded as the delegated FSM actor.
|
|
416
|
-
//
|
|
417
|
-
// `getActiveSignal` is the runtime's hook for flowing the Boss's
|
|
418
|
-
// `handleBossInput.signal` into the host port calls — fromPromise
|
|
419
|
-
// hands the bridge XState's actor-scoped signal, which only fires
|
|
420
|
-
// on actor.stop(), not on Boss abort. When omitted (e.g. direct
|
|
421
|
-
// captainBridge tests), the bridge falls back to XState's signal.
|
|
422
|
-
function captainBridge(ports, getActiveSignal, boundary, onControlPlaneError) {
|
|
423
|
-
return createPlayerBridge({
|
|
424
|
-
resolvePlayerId: (input) => resolvePlayerId(input),
|
|
425
|
-
composePlayerPrompt: (input) => composePlayerPrompt(input),
|
|
426
|
-
adjudication: CODE_ADJUDICATION,
|
|
427
|
-
resumableStateIds: registeredResumableStateIds,
|
|
428
|
-
}, ports, getActiveSignal, boundary, onControlPlaneError);
|
|
429
|
-
}
|
|
430
|
-
// Captain pane display — PBRT-3 / PBRT-14.
|
|
431
|
-
// The Captain pane is a stream of three glyphs plus one bare
|
|
432
|
-
// captain-speech act, designed so a reader can parse each line at a
|
|
433
|
-
// glance:
|
|
434
|
-
// (no glyph) bare FSM event type — host renders as captain speech
|
|
435
|
-
// (e.g., `captain> START_CODING`)
|
|
436
|
-
// ⤷ player-invoking state entry: `<Player>: <label>`
|
|
437
|
-
// → transition guard outcome (`· field=N` tallies
|
|
438
|
-
// appended); the host presenter owns any visual
|
|
439
|
-
// nesting under the preceding ⤷ entry
|
|
440
|
-
// ◆ failure state (with `lastError` data) and the
|
|
441
|
-
// `awaitBossReply` suspension state (custom payload)
|
|
442
|
-
// The runtime emits no status line on entry to the idle state
|
|
443
|
-
// (`ready`) or the terminal state (`done`); the next `boss>` prompt
|
|
444
|
-
// is the implicit "turn over" signal. Prompts and full player output
|
|
445
|
-
// ride the player panes; the Captain pane keeps to the state-machine
|
|
446
|
-
// shape.
|
|
447
|
-
const STATE_LABELS = {
|
|
448
|
-
planAndImplement: 'plan & implement',
|
|
449
|
-
respondToReview: 'respond to review',
|
|
450
|
-
continueIr: 'continue IR task',
|
|
451
|
-
summarizeSpecs: 'summarize IR into specs',
|
|
452
|
-
reviewBossCommitSpecs: 'review Boss-intent commit (specs only)',
|
|
453
|
-
reviewBossCommitCode: 'review Boss-intent commit (code only)',
|
|
454
|
-
reviewBossCommitMixed: 'review Boss-intent commit (specs + code)',
|
|
455
|
-
reviewIrTaskCommitSpecs: 'review IR-task commit (specs only)',
|
|
456
|
-
reviewIrTaskCommitCode: 'review IR-task commit (code only)',
|
|
457
|
-
reviewIrTaskCommitMixed: 'review IR-task commit (specs + code)',
|
|
458
|
-
reviewChangesSpecs: 'review uncommitted edits (specs only)',
|
|
459
|
-
reviewChangesCode: 'review uncommitted edits (code only)',
|
|
460
|
-
reviewChangesMixed: 'review uncommitted edits (specs + code)',
|
|
461
|
-
reviewChangesAndChallengesSpecs: 'review uncommitted edits + rebuttals (specs only)',
|
|
462
|
-
reviewChangesAndChallengesCode: 'review uncommitted edits + rebuttals (code only)',
|
|
463
|
-
reviewChangesAndChallengesMixed: 'review uncommitted edits + rebuttals (specs + code)',
|
|
464
|
-
adjudicateChallenges: 'adjudicate rebuttals',
|
|
465
|
-
commitCoderInitial: "commit Coder's initial changes",
|
|
466
|
-
commitJoint: 'commit reviewed changes',
|
|
467
|
-
};
|
|
468
|
-
const stateMetadata = (() => {
|
|
469
|
-
const m = new Map();
|
|
470
|
-
for (const s of enumerateCaptainStates(codingMachine)) {
|
|
471
|
-
const label = STATE_LABELS[s.stateId];
|
|
472
|
-
if (!label) {
|
|
473
|
-
throw new Error(`code.playbook.ts: STATE_LABELS missing entry for player-invoking state '${s.stateId}'`);
|
|
474
|
-
}
|
|
475
|
-
const input = s.getInput({});
|
|
476
|
-
m.set(s.stateId, { player: input.player, sourceItem: s.sourceItem, label });
|
|
477
|
-
}
|
|
478
|
-
return m;
|
|
479
|
-
})();
|
|
480
|
-
const registeredResumableStateIds = new Set(enumerateAwaitBossReply(codingMachine).bossReplyTransitions.map((transition) => transition.target));
|
|
481
|
-
const QUIESCENT_STATES = new Set([
|
|
482
|
-
'ready',
|
|
483
|
-
'awaitBossReply',
|
|
484
|
-
'done',
|
|
485
|
-
'failed',
|
|
486
|
-
]);
|
|
487
|
-
// States whose entry the runtime does not surface on the Captain
|
|
488
|
-
// pane per PBRT-3: the readline returning to its `boss>` prompt is
|
|
489
|
-
// the implicit "turn over" signal, so a `◆ ready` / `◆ done`
|
|
490
|
-
// tombstone is redundant.
|
|
491
|
-
const SUPPRESSED_ENTRY_STATES = new Set(['ready', 'done']);
|
|
492
|
-
// Captain-pane surface (PBRT-3): every player-invoking state plus
|
|
493
|
-
// the quiescent states whose entry still carries information
|
|
494
|
-
// (failure with `lastError`, awaitBossReply with the pending
|
|
495
|
-
// question). `ready` and `done` flow through the inspect handler
|
|
496
|
-
// but their entries are dropped before emitStatus per
|
|
497
|
-
// SUPPRESSED_ENTRY_STATES above.
|
|
498
|
-
const CAPTAIN_PANE_STATES = new Set([
|
|
499
|
-
...stateMetadata.keys(),
|
|
500
|
-
...QUIESCENT_STATES,
|
|
501
|
-
]);
|
|
502
|
-
function pendingBossQuestionFromContext(context) {
|
|
503
|
-
const pending = context.pendingBossQuestion;
|
|
504
|
-
if (pending === undefined || typeof pending !== 'object') {
|
|
505
|
-
return undefined;
|
|
506
|
-
}
|
|
507
|
-
const candidate = pending;
|
|
508
|
-
if (typeof candidate.resumeStateId !== 'string' ||
|
|
509
|
-
typeof candidate.questionId !== 'string' ||
|
|
510
|
-
typeof candidate.sourceItem !== 'string' ||
|
|
511
|
-
typeof candidate.player !== 'string' ||
|
|
512
|
-
typeof candidate.question !== 'string') {
|
|
513
|
-
return undefined;
|
|
514
|
-
}
|
|
515
|
-
return {
|
|
516
|
-
questionId: candidate.questionId,
|
|
517
|
-
resumeStateId: candidate.resumeStateId,
|
|
518
|
-
sourceItem: candidate.sourceItem,
|
|
519
|
-
player: candidate.player,
|
|
520
|
-
question: candidate.question,
|
|
521
|
-
};
|
|
522
|
-
}
|
|
523
|
-
// PBRT-3 / PBRT-14: on entry to `awaitBossReply` the runtime surfaces
|
|
524
|
-
// the pending player question as a captain-speech act attributed to the
|
|
525
|
-
// asking player (`<player> asks: <full question>`), emitted with no
|
|
526
|
-
// glyph so the host renders it as captain speech. The question is
|
|
527
|
-
// carried verbatim and in full — the judge JSON that produced it rides
|
|
528
|
-
// a hidden callCaptain (PBRT-15), so this line is the Boss's only
|
|
529
|
-
// legible view of what was asked.
|
|
530
|
-
function formatAwaitBossReplyQuestion(context) {
|
|
531
|
-
const pending = pendingBossQuestionFromContext(context);
|
|
532
|
-
const player = pending?.player ?? 'unknown';
|
|
533
|
-
const question = pending?.question ?? '';
|
|
534
|
-
return `${player} asks: ${question}`;
|
|
535
|
-
}
|
|
536
|
-
// The rider-less routing marker emitted right after the question line.
|
|
537
|
-
// It carries only the resume target, asking player, and source item;
|
|
538
|
-
// the former `q="<first 80 chars>"` excerpt rider is dropped now that
|
|
539
|
-
// the full question rides the captain-speech line above.
|
|
540
|
-
function formatAwaitBossReplyMarker(context) {
|
|
541
|
-
const pending = pendingBossQuestionFromContext(context);
|
|
542
|
-
const resumeStateId = pending?.resumeStateId ?? 'unknown';
|
|
543
|
-
const player = pending?.player ?? 'unknown';
|
|
544
|
-
const sourceItem = pending?.sourceItem ?? 'unknown';
|
|
545
|
-
return `◆ awaiting Boss reply · ${resumeStateId} · ${player} · ${sourceItem}`;
|
|
546
|
-
}
|
|
547
|
-
function formatStateEntry(stateId) {
|
|
548
|
-
if (SUPPRESSED_ENTRY_STATES.has(stateId))
|
|
549
|
-
return undefined;
|
|
550
|
-
if (stateId === 'failed')
|
|
551
|
-
return '◆ failed';
|
|
552
|
-
const meta = stateMetadata.get(stateId);
|
|
553
|
-
if (!meta)
|
|
554
|
-
return `⤷ ${stateId}`;
|
|
555
|
-
return `⤷ ${meta.player}: ${meta.label}`;
|
|
556
|
-
}
|
|
557
|
-
function formatTransition(event) {
|
|
558
|
-
const output = event?.output;
|
|
559
|
-
if (!output || typeof output.guard !== 'string')
|
|
560
|
-
return undefined;
|
|
561
|
-
const tallies = [];
|
|
562
|
-
for (const field of ['reviews', 'challenges']) {
|
|
563
|
-
const value = output[field];
|
|
564
|
-
if (typeof value === 'string' && value.trim().length > 0) {
|
|
565
|
-
const items = value.match(/^\s*\d+\.\s/gm);
|
|
566
|
-
tallies.push(`${field}=${items ? items.length : 1}`);
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
const suffix = tallies.length > 0 ? ` · ${tallies.join(' · ')}` : '';
|
|
570
|
-
// No leading whitespace: visual nesting under the preceding ⤷
|
|
571
|
-
// entry is the host presenter's concern (cligent's writeStatusLine
|
|
572
|
-
// emits status messages verbatim, with its own chrome but no
|
|
573
|
-
// continuation indent; layout is its job, not ours).
|
|
574
|
-
return `→ ${output.guard}${suffix}`;
|
|
575
|
-
}
|
|
576
|
-
function formatClassification(eventType) {
|
|
577
|
-
return eventType;
|
|
578
|
-
}
|
|
579
|
-
function stateTelemetryPayload(from, to, event, context) {
|
|
580
|
-
const payload = {
|
|
581
|
-
from: from ?? null,
|
|
582
|
-
to,
|
|
583
|
-
event: normalizeEventForTelemetry(event) ?? null,
|
|
584
|
-
};
|
|
585
|
-
if (to === 'awaitBossReply') {
|
|
586
|
-
const pendingBossQuestion = pendingBossQuestionFromContext(context);
|
|
587
|
-
if (pendingBossQuestion !== undefined) {
|
|
588
|
-
payload.pendingBossQuestion = pendingBossQuestion;
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
if (to === 'failed') {
|
|
592
|
-
const lastError = normalizeErrorFull(context.lastError);
|
|
593
|
-
if (lastError !== undefined) {
|
|
594
|
-
payload.lastError = lastError;
|
|
595
|
-
}
|
|
596
|
-
}
|
|
597
|
-
return payload;
|
|
598
|
-
}
|
|
599
|
-
// Captain-pane status lines for a root transition (PBRT-3 / PBRT-14):
|
|
600
|
-
// the `→ guard` outcome line for the settling transition, then either
|
|
601
|
-
// the awaitBossReply question + rider-less marker pair or the state's
|
|
602
|
-
// entry line (with `lastError` data on `failed`).
|
|
603
|
-
function statusesForState(state, context, event) {
|
|
604
|
-
const to = state.stateId;
|
|
605
|
-
if (to === undefined || !CAPTAIN_PANE_STATES.has(to))
|
|
606
|
-
return [];
|
|
607
|
-
const statuses = [];
|
|
608
|
-
const transitionLine = formatTransition(event);
|
|
609
|
-
if (transitionLine !== undefined) {
|
|
610
|
-
statuses.push({ message: transitionLine });
|
|
611
|
-
}
|
|
612
|
-
if (to === 'awaitBossReply') {
|
|
613
|
-
statuses.push({ message: formatAwaitBossReplyQuestion(context) }, { message: formatAwaitBossReplyMarker(context) });
|
|
614
|
-
}
|
|
615
|
-
else {
|
|
616
|
-
const entryLine = formatStateEntry(to);
|
|
617
|
-
if (entryLine !== undefined) {
|
|
618
|
-
const lastError = to === 'failed' ? normalizeErrorCompact(context.lastError) : undefined;
|
|
619
|
-
statuses.push({
|
|
620
|
-
message: entryLine,
|
|
621
|
-
...(lastError === undefined
|
|
622
|
-
? {}
|
|
623
|
-
: {
|
|
624
|
-
data: snapshotJsonValue({ lastError }, 'failed status data'),
|
|
625
|
-
}),
|
|
626
|
-
});
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
return statuses;
|
|
54
|
+
const fields = input;
|
|
55
|
+
const template = input.prompt
|
|
56
|
+
.split('\n')
|
|
57
|
+
.filter((line) => !(line === '> <run-results>' && input.runResults.length === 0))
|
|
58
|
+
.join('\n');
|
|
59
|
+
const body = template.replace(PLACEHOLDER, (match, token, offset, source) => {
|
|
60
|
+
const value = fields[placeholderField(token)];
|
|
61
|
+
if (typeof value !== 'string')
|
|
62
|
+
return match;
|
|
63
|
+
const lineStart = source.lastIndexOf('\n', offset - 1) + 1;
|
|
64
|
+
const literal = source.slice(lineStart, offset);
|
|
65
|
+
return literal === '> ' ? quoteContinuation(value) : value;
|
|
66
|
+
});
|
|
67
|
+
if (input.pendingBossQuestion === undefined ||
|
|
68
|
+
input.bossReply === undefined) {
|
|
69
|
+
return body;
|
|
70
|
+
}
|
|
71
|
+
return [
|
|
72
|
+
CONTINUATION_PREAMBLE,
|
|
73
|
+
`Boss question:\n${input.pendingBossQuestion.question}`,
|
|
74
|
+
`Boss reply:\n${input.bossReply}`,
|
|
75
|
+
body,
|
|
76
|
+
].join('\n\n');
|
|
630
77
|
}
|
|
631
|
-
// Internal export surface for tests. Not part of the stable public API;
|
|
632
|
-
// the leading underscore signals "subject to change."
|
|
633
78
|
export const _internal = {
|
|
634
79
|
composePlayerPrompt,
|
|
635
|
-
resolvePlayerId,
|
|
636
|
-
adjudicate,
|
|
637
|
-
classifyBossText,
|
|
638
|
-
captainBridge,
|
|
639
|
-
STATE_LABELS,
|
|
640
|
-
stateMetadata,
|
|
641
|
-
pendingBossQuestionFromContext,
|
|
642
|
-
formatAwaitBossReplyQuestion,
|
|
643
|
-
formatAwaitBossReplyMarker,
|
|
644
|
-
formatStateEntry,
|
|
645
|
-
formatTransition,
|
|
646
|
-
formatClassification,
|
|
647
|
-
stateTelemetryPayload,
|
|
648
|
-
normalizeErrorCompact,
|
|
649
|
-
normalizeErrorFull,
|
|
650
|
-
normalizeEventForTelemetry,
|
|
651
80
|
VERBATIM_PAYLOAD_FIELDS,
|
|
652
81
|
};
|
|
653
|
-
// The CODE-specific spec handed to the shared runtime factory
|
|
654
|
-
// (slc/link.md §Output, DR-019). The generic machinery — actor wiring,
|
|
655
|
-
// boundary tracing, Boss-turn lifecycle, nested-playbook bridge, and the
|
|
656
|
-
// DR-014 parked-session snapshot capability — lives in
|
|
657
|
-
// @sublang/playbook/xstate-runtime; this spec carries only what is
|
|
658
|
-
// CODE-specific.
|
|
659
82
|
const runtimeSpec = {
|
|
660
83
|
label: 'CODE',
|
|
661
|
-
|
|
662
|
-
|
|
84
|
+
compat: { artifactSchema: 1, runtimeAbi: RUNTIME_ABI },
|
|
85
|
+
snapshotOptions: snapshotCodeOptions,
|
|
86
|
+
entryEvent: { type: 'START_CODE', textField: 'callerInput' },
|
|
87
|
+
playerStates: {
|
|
88
|
+
runFirstPhase: {
|
|
89
|
+
player: 'Coder',
|
|
90
|
+
label: 'Coder is implementing one direct phase or committing a new intent record.',
|
|
91
|
+
},
|
|
92
|
+
runIrTask: {
|
|
93
|
+
player: 'Coder',
|
|
94
|
+
label: 'Coder is implementing exactly one unfinished intent-record task.',
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
placeholderFields: { 'coder-llm': 'coderPlayer' },
|
|
663
98
|
composePlayerPrompt: (input) => composePlayerPrompt(input),
|
|
664
|
-
buildJudgePrompt: CODE_ADJUDICATION.buildJudgePrompt,
|
|
665
|
-
extractRequiredFields,
|
|
666
99
|
verbatimPayloadFields: VERBATIM_PAYLOAD_FIELDS,
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
// closed-vocabulary classification members the FSM assigns itself, which
|
|
670
|
-
// are what a controller needs to see to describe or steer a CODE
|
|
671
|
-
// engagement. Everything else in `CodingContext` is deliberately absent:
|
|
672
|
-
// `coderPlayer` / `reviewerPlayer` / `committerPlayer` are the resolved
|
|
673
|
-
// host player roster, `intent` is an option value, and `irNumber`,
|
|
674
|
-
// `taskDescription`, `reviews`, `challenges`, and `lastResult` all carry
|
|
675
|
-
// player-authored text — all of which a session-Captain prompt must
|
|
676
|
-
// exclude, or may carry only as fenced quotes
|
|
677
|
-
// (CAPTAIN-9). A member added to `CodingContext`
|
|
678
|
-
// later stays private until it is named here.
|
|
679
|
-
controlContextFields: [
|
|
680
|
-
'workflow',
|
|
681
|
-
'changeOrigin',
|
|
682
|
-
'reviewSubject',
|
|
683
|
-
'afterReview',
|
|
684
|
-
],
|
|
685
|
-
classifyBossText: (text, ports, signal, snapshotOrState, boundary) => classifyBossText(text, ports, signal, snapshotOrState, boundary),
|
|
686
|
-
classificationStatus: (event) => formatClassification(event.type),
|
|
687
|
-
statusesForState,
|
|
688
|
-
normalizeTransitionEvent: (event) => normalizeEventForTelemetry(event),
|
|
100
|
+
controlContextFields: ['phase'],
|
|
101
|
+
transitionEventFields: ['callerInput', 'answer', 'questionId'],
|
|
689
102
|
};
|
|
690
103
|
const createPlaybookRuntime = createXStatePlaybookRuntime(codingMachine, runtimeSpec);
|
|
691
104
|
export default createPlaybookRuntime;
|