@sublang/playbook 0.4.2 → 0.6.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 +42 -19
- package/package.json +18 -6
- package/reference/sdlc/code.playbook/bin/playbook-code.js +277 -104
- package/reference/sdlc/code.playbook/code.fsm.d.ts +1 -0
- package/reference/sdlc/code.playbook/code.fsm.js +67 -29
- package/reference/sdlc/code.playbook/code.fsm.ts +74 -29
- package/reference/sdlc/code.playbook/code.gears.md +66 -30
- package/reference/sdlc/code.playbook/code.playbook.d.ts +19 -3
- package/reference/sdlc/code.playbook/code.playbook.js +292 -34
- package/reference/sdlc/code.playbook/code.playbook.ts +290 -35
- package/reference/sdlc/code.playbook/code.registry.d.ts +43 -0
- package/reference/sdlc/code.playbook/code.registry.js +109 -0
- package/reference/sdlc/code.playbook/code.registry.ts +159 -0
- package/reference/sdlc/code.playbook/code.tmux-play.d.ts +2 -0
- package/reference/sdlc/code.playbook/code.tmux-play.js +7 -93
- package/reference/sdlc/code.playbook/code.tmux-play.ts +20 -118
- package/reference/sdlc/code.playbook/playbook-captain.d.ts +21 -0
- package/reference/sdlc/code.playbook/playbook-captain.js +491 -0
- package/reference/sdlc/code.playbook/playbook-captain.ts +701 -0
- package/reference/sdlc/code.playbook/playbook-code.config.template.yaml +50 -25
- package/reference/sdlc/code.playbook/tmux-play.config.yaml +13 -8
- package/reference/sdlc/code.playbook/tmux-play.production.config.yaml +8 -4
|
@@ -4,8 +4,10 @@
|
|
|
4
4
|
// Generated by slc/link.md (FSM-to-Runtime linker).
|
|
5
5
|
// Source FSM: ./code.fsm.ts
|
|
6
6
|
// Player bind: Coder→coder, Reviewer→reviewer,
|
|
7
|
-
// Committer→
|
|
8
|
-
//
|
|
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
|
|
9
11
|
// alias's first alternative)
|
|
10
12
|
// Boss event: free-text judge classification
|
|
11
13
|
// Adjudication: LLM-judge per state
|
|
@@ -59,6 +61,70 @@ const BOSS_REPLY_ERRORS = {
|
|
|
59
61
|
`state ${stateId} declared needsBossReply but is not registered as resumable`,
|
|
60
62
|
} as const;
|
|
61
63
|
|
|
64
|
+
// Required-payload fields whose value is the player's verbatim long-form
|
|
65
|
+
// prose. The runtime carries `finalText.trim()` into these fields rather
|
|
66
|
+
// than asking the judge to round-trip the text through JSON. Short
|
|
67
|
+
// extracted fields (`question`, `taskDescription`, `irNumber`, …) stay
|
|
68
|
+
// judge-extracted — they are not in this set.
|
|
69
|
+
const VERBATIM_PAYLOAD_FIELDS: ReadonlySet<string> = new Set([
|
|
70
|
+
'reviews',
|
|
71
|
+
'challenges',
|
|
72
|
+
]);
|
|
73
|
+
|
|
74
|
+
// Normalize an unknown error value to the compact `{ name, message }`
|
|
75
|
+
// shape used by Captain-pane / status emissions. Returns `undefined`
|
|
76
|
+
// for nullish input so callers can omit absent errors.
|
|
77
|
+
function normalizeErrorCompact(
|
|
78
|
+
err: unknown,
|
|
79
|
+
): { name: string; message: string } | undefined {
|
|
80
|
+
if (err === undefined || err === null) return undefined;
|
|
81
|
+
if (err instanceof Error) {
|
|
82
|
+
return { name: err.name, message: err.message };
|
|
83
|
+
}
|
|
84
|
+
if (typeof err === 'object') {
|
|
85
|
+
const o = err as Record<string, unknown>;
|
|
86
|
+
if (typeof o.message === 'string') {
|
|
87
|
+
return {
|
|
88
|
+
name: typeof o.name === 'string' ? o.name : 'Error',
|
|
89
|
+
message: o.message,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return { name: 'Error', message: String(err) };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Normalize an unknown error value to the full `{ name, message, stack }`
|
|
97
|
+
// shape used by telemetry emissions. Returns `undefined` for nullish
|
|
98
|
+
// input. `stack` is omitted when not available on the source value.
|
|
99
|
+
function normalizeErrorFull(
|
|
100
|
+
err: unknown,
|
|
101
|
+
): { name: string; message: string; stack?: string } | undefined {
|
|
102
|
+
const compact = normalizeErrorCompact(err);
|
|
103
|
+
if (compact === undefined) return undefined;
|
|
104
|
+
if (err instanceof Error) {
|
|
105
|
+
return err.stack !== undefined ? { ...compact, stack: err.stack } : compact;
|
|
106
|
+
}
|
|
107
|
+
if (typeof err === 'object' && err !== null) {
|
|
108
|
+
const stack = (err as Record<string, unknown>).stack;
|
|
109
|
+
if (typeof stack === 'string') {
|
|
110
|
+
return { ...compact, stack };
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return compact;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Normalize any `error` field inside a telemetry event so failed
|
|
117
|
+
// transitions don't leak raw Error instances through the channel.
|
|
118
|
+
function normalizeEventForTelemetry(event: unknown): unknown {
|
|
119
|
+
if (event === null || typeof event !== 'object' || Array.isArray(event)) {
|
|
120
|
+
return event;
|
|
121
|
+
}
|
|
122
|
+
const e = event as Record<string, unknown>;
|
|
123
|
+
if (!('error' in e)) return event;
|
|
124
|
+
const normalized = normalizeErrorFull(e.error);
|
|
125
|
+
return { ...e, error: normalized };
|
|
126
|
+
}
|
|
127
|
+
|
|
62
128
|
// Internal capabilities (DR-004 §10). Each ships with its final
|
|
63
129
|
// signature; behavior lands in the per-capability task noted by the
|
|
64
130
|
// TODO marker.
|
|
@@ -117,15 +183,21 @@ function composePlayerPrompt(input: CaptainInput): string {
|
|
|
117
183
|
return blocks.join('\n\n');
|
|
118
184
|
}
|
|
119
185
|
|
|
120
|
-
// Player-id resolver — DR-004 §2.
|
|
186
|
+
// Player-id resolver — DR-004 §2 / PBRT-8.
|
|
121
187
|
// Non-composite: Coder→'coder', Reviewer→'reviewer'. The composite
|
|
122
|
-
// Committer (= Coder | Reviewer per code.gears.md) resolves
|
|
123
|
-
//
|
|
124
|
-
// `
|
|
125
|
-
//
|
|
126
|
-
// back to
|
|
127
|
-
//
|
|
128
|
-
//
|
|
188
|
+
// Committer (= Coder | Reviewer per code.gears.md) resolves to the
|
|
189
|
+
// configured alias when present: `input.committerPlayer`, the
|
|
190
|
+
// validated `captain.options.code.committer` (PBRT-8 / PBRT-30),
|
|
191
|
+
// already a baked player id ('coder' / 'reviewer'). Absent a
|
|
192
|
+
// configured alias it falls back to the DR-004 §2 baked binding by
|
|
193
|
+
// populated <playerName>Player field: prefer `coderPlayer` (CODE-18
|
|
194
|
+
// wires only coderPlayer; CODE-19 wires both so coderPlayer still wins
|
|
195
|
+
// as the alias's first alternative), fall back to `reviewerPlayer`
|
|
196
|
+
// only if a future gear ships a reviewer-only Committer item, and
|
|
197
|
+
// finally to the alias's first alternative (Coder) when neither is
|
|
198
|
+
// set. The alias selects only the host pane; it is not a PBRT-4
|
|
199
|
+
// identity string, so it leaves <coder-llm> / <reviewer-llm>
|
|
200
|
+
// untouched and `input.player` stays `Committer` (PLAYBOOK-3).
|
|
129
201
|
function resolvePlayerId(input: CaptainInput): string {
|
|
130
202
|
switch (input.player) {
|
|
131
203
|
case 'Coder':
|
|
@@ -133,6 +205,7 @@ function resolvePlayerId(input: CaptainInput): string {
|
|
|
133
205
|
case 'Reviewer':
|
|
134
206
|
return 'reviewer';
|
|
135
207
|
case 'Committer':
|
|
208
|
+
if (input.committerPlayer !== undefined) return input.committerPlayer;
|
|
136
209
|
if (input.coderPlayer !== undefined) return 'coder';
|
|
137
210
|
if (input.reviewerPlayer !== undefined) return 'reviewer';
|
|
138
211
|
return 'coder';
|
|
@@ -180,8 +253,17 @@ async function adjudicate(
|
|
|
180
253
|
// required fields with the literal phrase
|
|
181
254
|
// Output shall include `<fieldName>: <...>`
|
|
182
255
|
// so we extract those tokens and require each to be a string in
|
|
183
|
-
// the judge response
|
|
256
|
+
// the judge response — except for VERBATIM_PAYLOAD_FIELDS
|
|
257
|
+
// (`reviews`, `challenges`), where the runtime substitutes
|
|
258
|
+
// `finalText.trim()` so the long-form prose is not round-tripped
|
|
259
|
+
// through judge JSON. Short extracted fields like `question` and
|
|
260
|
+
// `taskDescription` keep the existing extract-and-validate path.
|
|
261
|
+
const verbatim = finalText.trim();
|
|
184
262
|
for (const field of extractRequiredFields(input.result[guard])) {
|
|
263
|
+
if (VERBATIM_PAYLOAD_FIELDS.has(field)) {
|
|
264
|
+
obj[field] = verbatim;
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
185
267
|
if (typeof obj[field] !== 'string') {
|
|
186
268
|
if (guard === 'needsBossReply' && field === 'question') {
|
|
187
269
|
throw new Error(BOSS_REPLY_ERRORS.missingQuestion);
|
|
@@ -214,7 +296,10 @@ function buildJudgePrompt(input: CaptainInput, finalText: string): string {
|
|
|
214
296
|
lines.push(
|
|
215
297
|
'Pick exactly one outcome by `guard` and return JSON ' +
|
|
216
298
|
'`{ guard, …payloadFields }`. Required payload fields are ' +
|
|
217
|
-
'named in the outcome description.'
|
|
299
|
+
'named in the outcome description. Do not copy long-form ' +
|
|
300
|
+
'verbatim fields (`reviews`, `challenges`) — the runtime ' +
|
|
301
|
+
"carries the player's output into those fields verbatim, " +
|
|
302
|
+
'so any value you supply for them will be overwritten.',
|
|
218
303
|
);
|
|
219
304
|
lines.push('');
|
|
220
305
|
for (const [key, description] of Object.entries(input.result)) {
|
|
@@ -223,17 +308,137 @@ function buildJudgePrompt(input: CaptainInput, finalText: string): string {
|
|
|
223
308
|
return lines.join('\n');
|
|
224
309
|
}
|
|
225
310
|
|
|
311
|
+
// Judge replies are meant to be a single JSON object, but LLMs
|
|
312
|
+
// routinely wrap them in prose ("Here is the result: …"), Markdown
|
|
313
|
+
// code fences, or trailing commentary, and occasionally emit a
|
|
314
|
+
// trailing comma or truncate the tail (a dropped closing brace, an
|
|
315
|
+
// unterminated string). parseJudgeJson is deliberately lenient: it
|
|
316
|
+
// first tries a strict parse of the (optionally fenced) body, then
|
|
317
|
+
// scans every `{`/`[` as a possible start in document order and
|
|
318
|
+
// returns the first recoverable object. At each start it prefers a
|
|
319
|
+
// strict balanced span and falls back to a repaired (trailing-comma /
|
|
320
|
+
// truncation) span, so a damaged object earlier in the prose is not
|
|
321
|
+
// overridden by a cleaner one later. Scanning every start (not just
|
|
322
|
+
// the first bracket) keeps a bracketed fragment in surrounding prose —
|
|
323
|
+
// e.g. an aside like `see [1]` or `{n/a}` before the real object —
|
|
324
|
+
// from masking a later, genuinely valid object. Both callers
|
|
325
|
+
// (classification and adjudication) expect an object, so plain objects
|
|
326
|
+
// win over arrays/scalars; the first value of any shape is remembered
|
|
327
|
+
// so a legitimately array/scalar reply still surfaces to the caller's
|
|
328
|
+
// own object check. Only a reply from which no JSON value can be
|
|
329
|
+
// recovered is treated as malformed and throws, preserving the
|
|
330
|
+
// control-plane error contract (PBRT-7, PBRT-10).
|
|
226
331
|
function parseJudgeJson(raw: string): unknown {
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
if (fence) text = fence[1].trim();
|
|
332
|
+
const fenced = stripCodeFence(raw.trim());
|
|
333
|
+
// Fast path: a well-formed (optionally fenced) JSON body.
|
|
230
334
|
try {
|
|
231
|
-
return JSON.parse(
|
|
232
|
-
} catch
|
|
233
|
-
|
|
234
|
-
`adjudicate: judge response is not valid JSON: ${(e as Error).message}`,
|
|
235
|
-
);
|
|
335
|
+
return JSON.parse(fenced);
|
|
336
|
+
} catch {
|
|
337
|
+
// Fall through to lenient extraction + repair.
|
|
236
338
|
}
|
|
339
|
+
const starts: number[] = [];
|
|
340
|
+
for (let i = 0; i < fenced.length; i++) {
|
|
341
|
+
const ch = fenced[i];
|
|
342
|
+
if (ch === '{' || ch === '[') starts.push(i);
|
|
343
|
+
}
|
|
344
|
+
// Walk starts in document order. At each start prefer a strict
|
|
345
|
+
// balanced span (most trustworthy) and fall back to a repaired one
|
|
346
|
+
// for a trailing-comma / truncated tail, so the earliest intended
|
|
347
|
+
// object wins even when it needs repair. Return the first plain
|
|
348
|
+
// object; remember the first value of any shape so a legitimately
|
|
349
|
+
// array/scalar reply still surfaces to the caller's own object check.
|
|
350
|
+
let firstValue: { value: unknown } | undefined;
|
|
351
|
+
for (const start of starts) {
|
|
352
|
+
let parsedHere: { value: unknown } | undefined;
|
|
353
|
+
for (const repair of [false, true]) {
|
|
354
|
+
const candidate = extractJsonValue(fenced, start, repair);
|
|
355
|
+
if (candidate === undefined) continue;
|
|
356
|
+
try {
|
|
357
|
+
parsedHere = { value: JSON.parse(candidate) };
|
|
358
|
+
} catch {
|
|
359
|
+
continue; // not parseable this way — try repair, then next start
|
|
360
|
+
}
|
|
361
|
+
break; // prefer the strict span at this start over its repair
|
|
362
|
+
}
|
|
363
|
+
if (parsedHere === undefined) continue;
|
|
364
|
+
if (isPlainObject(parsedHere.value)) return parsedHere.value;
|
|
365
|
+
if (firstValue === undefined) firstValue = parsedHere;
|
|
366
|
+
}
|
|
367
|
+
if (firstValue !== undefined) return firstValue.value;
|
|
368
|
+
throw new Error('adjudicate: judge response is not valid JSON');
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function isPlainObject(value: unknown): boolean {
|
|
372
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Strip a single Markdown code fence that wraps the whole string.
|
|
376
|
+
function stripCodeFence(text: string): string {
|
|
377
|
+
const fence = text.match(/^```(?:json)?\s*\n?([\s\S]*?)\n?```$/);
|
|
378
|
+
return fence ? fence[1].trim() : text;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Scan from `start` (a `{`/`[` index), tracking string and
|
|
382
|
+
// bracket-nesting state, and emit the balanced JSON value rooted
|
|
383
|
+
// there. Anything after the top-level value closes is ignored (so a
|
|
384
|
+
// trailing code fence or commentary does not matter). With
|
|
385
|
+
// `repair === false` the span is returned only if it actually closes,
|
|
386
|
+
// and trailing commas are left intact — so the caller can prefer a
|
|
387
|
+
// cleanly-balanced span before attempting repair; if input ends
|
|
388
|
+
// before the value closes, undefined is returned. With
|
|
389
|
+
// `repair === true` common damage is fixed: a trailing comma before a
|
|
390
|
+
// close is removed, an unterminated string is closed, and any
|
|
391
|
+
// brackets still open at end-of-input are closed in order.
|
|
392
|
+
function extractJsonValue(
|
|
393
|
+
text: string,
|
|
394
|
+
start: number,
|
|
395
|
+
repair: boolean,
|
|
396
|
+
): string | undefined {
|
|
397
|
+
const stack: string[] = [];
|
|
398
|
+
let out = '';
|
|
399
|
+
let inString = false;
|
|
400
|
+
let escaped = false;
|
|
401
|
+
for (let i = start; i < text.length; i++) {
|
|
402
|
+
const ch = text[i];
|
|
403
|
+
if (inString) {
|
|
404
|
+
out += ch;
|
|
405
|
+
if (escaped) escaped = false;
|
|
406
|
+
else if (ch === '\\') escaped = true;
|
|
407
|
+
else if (ch === '"') inString = false;
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
if (ch === '"') {
|
|
411
|
+
inString = true;
|
|
412
|
+
out += ch;
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
if (ch === '{' || ch === '[') {
|
|
416
|
+
stack.push(ch === '{' ? '}' : ']');
|
|
417
|
+
out += ch;
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
if (ch === '}' || ch === ']') {
|
|
421
|
+
if (repair) out = dropTrailingComma(out);
|
|
422
|
+
out += ch;
|
|
423
|
+
stack.pop();
|
|
424
|
+
if (stack.length === 0) return out; // top-level value complete
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
out += ch;
|
|
428
|
+
}
|
|
429
|
+
// End of input before the top-level value closed.
|
|
430
|
+
if (!repair) return undefined; // strict pass: no balanced span here
|
|
431
|
+
if (inString) out += '"';
|
|
432
|
+
out = dropTrailingComma(out);
|
|
433
|
+
while (stack.length > 0) out += stack.pop();
|
|
434
|
+
return out;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Remove a trailing comma (and any whitespace after it) at the end of
|
|
438
|
+
// the accumulated output, so `{"a":1,}` / `[1,2,]` and truncated
|
|
439
|
+
// `{"a":1,` repair to valid JSON.
|
|
440
|
+
function dropTrailingComma(out: string): string {
|
|
441
|
+
return out.replace(/,(\s*)$/, '$1');
|
|
237
442
|
}
|
|
238
443
|
|
|
239
444
|
// Boss-event classifier — DR-004 §3.
|
|
@@ -277,7 +482,17 @@ async function classifyWithLlm(
|
|
|
277
482
|
const state = classifierState(snapshotOrState);
|
|
278
483
|
const prompt = buildClassifierPrompt(text, state);
|
|
279
484
|
const raw = await ports.callJudge(prompt, signal);
|
|
280
|
-
|
|
485
|
+
let parsed: unknown;
|
|
486
|
+
try {
|
|
487
|
+
parsed = parseJudgeJson(raw);
|
|
488
|
+
} catch {
|
|
489
|
+
// No JSON value could be recovered. Classification failures are
|
|
490
|
+
// non-fatal — unlike adjudication, which throws to the failure
|
|
491
|
+
// state (PBRT-10) — so surface one status and take no FSM action,
|
|
492
|
+
// consistent with the other invalid-reply paths below (PBRT-7).
|
|
493
|
+
await ports.emitStatus('Classifier reply was not recoverable JSON');
|
|
494
|
+
return undefined;
|
|
495
|
+
}
|
|
281
496
|
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
282
497
|
await ports.emitStatus('Classifier returned a non-object JSON response');
|
|
283
498
|
return undefined;
|
|
@@ -638,24 +853,37 @@ function pendingBossQuestionFromContext(
|
|
|
638
853
|
};
|
|
639
854
|
}
|
|
640
855
|
|
|
641
|
-
|
|
642
|
-
|
|
856
|
+
// PBRT-3 / PBRT-14: on entry to `awaitBossReply` the runtime surfaces
|
|
857
|
+
// the pending player question as a captain-speech act attributed to the
|
|
858
|
+
// asking player (`<player> asks: <full question>`), emitted with no
|
|
859
|
+
// glyph so the host renders it as captain speech. The question is
|
|
860
|
+
// carried verbatim and in full — the judge JSON that produced it rides
|
|
861
|
+
// a hidden callCaptain (PBRT-15), so this line is the Boss's only
|
|
862
|
+
// legible view of what was asked.
|
|
863
|
+
function formatAwaitBossReplyQuestion(
|
|
864
|
+
context: Record<string, unknown>,
|
|
865
|
+
): string {
|
|
866
|
+
const pending = pendingBossQuestionFromContext(context);
|
|
867
|
+
const player = pending?.player ?? 'unknown';
|
|
868
|
+
const question = pending?.question ?? '';
|
|
869
|
+
return `${player} asks: ${question}`;
|
|
643
870
|
}
|
|
644
871
|
|
|
645
|
-
|
|
872
|
+
// The rider-less routing marker emitted right after the question line.
|
|
873
|
+
// It carries only the resume target, asking player, and source item;
|
|
874
|
+
// the former `q="<first 80 chars>"` excerpt rider is dropped now that
|
|
875
|
+
// the full question rides the captain-speech line above.
|
|
876
|
+
function formatAwaitBossReplyMarker(
|
|
877
|
+
context: Record<string, unknown>,
|
|
878
|
+
): string {
|
|
646
879
|
const pending = pendingBossQuestionFromContext(context);
|
|
647
880
|
const resumeStateId = pending?.resumeStateId ?? 'unknown';
|
|
648
881
|
const player = pending?.player ?? 'unknown';
|
|
649
882
|
const sourceItem = pending?.sourceItem ?? 'unknown';
|
|
650
|
-
|
|
651
|
-
return `◆ awaiting Boss reply · ${resumeStateId} · ${player} · ${sourceItem} · q=${JSON.stringify(question)}`;
|
|
883
|
+
return `◆ awaiting Boss reply · ${resumeStateId} · ${player} · ${sourceItem}`;
|
|
652
884
|
}
|
|
653
885
|
|
|
654
|
-
function formatStateEntry(
|
|
655
|
-
stateId: string,
|
|
656
|
-
context: Record<string, unknown> = {},
|
|
657
|
-
): string | undefined {
|
|
658
|
-
if (stateId === 'awaitBossReply') return formatAwaitBossReplyEntry(context);
|
|
886
|
+
function formatStateEntry(stateId: string): string | undefined {
|
|
659
887
|
if (SUPPRESSED_ENTRY_STATES.has(stateId)) return undefined;
|
|
660
888
|
if (stateId === 'failed') return '◆ failed';
|
|
661
889
|
const meta = stateMetadata.get(stateId);
|
|
@@ -692,13 +920,23 @@ function stateTelemetryPayload(
|
|
|
692
920
|
event: unknown,
|
|
693
921
|
context: Record<string, unknown>,
|
|
694
922
|
): Record<string, unknown> {
|
|
695
|
-
const payload: Record<string, unknown> = {
|
|
923
|
+
const payload: Record<string, unknown> = {
|
|
924
|
+
from,
|
|
925
|
+
to,
|
|
926
|
+
event: normalizeEventForTelemetry(event),
|
|
927
|
+
};
|
|
696
928
|
if (to === 'awaitBossReply') {
|
|
697
929
|
const pendingBossQuestion = pendingBossQuestionFromContext(context);
|
|
698
930
|
if (pendingBossQuestion !== undefined) {
|
|
699
931
|
payload.pendingBossQuestion = pendingBossQuestion;
|
|
700
932
|
}
|
|
701
933
|
}
|
|
934
|
+
if (to === 'failed') {
|
|
935
|
+
const lastError = normalizeErrorFull(context.lastError);
|
|
936
|
+
if (lastError !== undefined) {
|
|
937
|
+
payload.lastError = lastError;
|
|
938
|
+
}
|
|
939
|
+
}
|
|
702
940
|
return payload;
|
|
703
941
|
}
|
|
704
942
|
|
|
@@ -715,11 +953,16 @@ export const _internal = {
|
|
|
715
953
|
STATE_LABELS,
|
|
716
954
|
stateMetadata,
|
|
717
955
|
pendingBossQuestionFromContext,
|
|
718
|
-
|
|
956
|
+
formatAwaitBossReplyQuestion,
|
|
957
|
+
formatAwaitBossReplyMarker,
|
|
719
958
|
formatStateEntry,
|
|
720
959
|
formatTransition,
|
|
721
960
|
formatClassification,
|
|
722
961
|
stateTelemetryPayload,
|
|
962
|
+
normalizeErrorCompact,
|
|
963
|
+
normalizeErrorFull,
|
|
964
|
+
normalizeEventForTelemetry,
|
|
965
|
+
VERBATIM_PAYLOAD_FIELDS,
|
|
723
966
|
};
|
|
724
967
|
|
|
725
968
|
export default function createPlaybookRuntime(
|
|
@@ -809,12 +1052,24 @@ export default function createPlaybookRuntime(
|
|
|
809
1052
|
if (transitionLine !== undefined) {
|
|
810
1053
|
enqueueEmit(() => ports.emitStatus(transitionLine));
|
|
811
1054
|
}
|
|
812
|
-
|
|
1055
|
+
// awaitBossReply surfaces two lines per PBRT-3 / PBRT-14: the
|
|
1056
|
+
// full player question as captain speech, then the rider-less
|
|
1057
|
+
// routing marker. The full-question telemetry rides
|
|
1058
|
+
// stateTelemetryPayload above.
|
|
1059
|
+
if (to === 'awaitBossReply') {
|
|
1060
|
+
const questionLine = formatAwaitBossReplyQuestion(context);
|
|
1061
|
+
const markerLine = formatAwaitBossReplyMarker(context);
|
|
1062
|
+
enqueueEmit(() => ports.emitStatus(questionLine));
|
|
1063
|
+
enqueueEmit(() => ports.emitStatus(markerLine));
|
|
1064
|
+
return;
|
|
1065
|
+
}
|
|
1066
|
+
const entryLine = formatStateEntry(to);
|
|
813
1067
|
if (entryLine === undefined) return;
|
|
814
1068
|
if (to === 'failed') {
|
|
815
1069
|
const lastError = (snap.context as { lastError?: unknown })
|
|
816
1070
|
?.lastError;
|
|
817
|
-
|
|
1071
|
+
const data = { lastError: normalizeErrorCompact(lastError) };
|
|
1072
|
+
enqueueEmit(() => ports.emitStatus(entryLine, data));
|
|
818
1073
|
} else {
|
|
819
1074
|
enqueueEmit(() => ports.emitStatus(entryLine));
|
|
820
1075
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { type CodePlaybookOptions, type PlaybookRuntime } from './code.playbook.js';
|
|
2
|
+
export declare const codeCopyPasteGuardNames: readonly ["accepted", "approved", "challengeAccepted", "challengeRejected", "challengesRaised", "changesMadeCode", "changesMadeCodeAndChallenged", "changesMadeMixed", "changesMadeMixedAndChallenged", "changesMadeSpecs", "changesMadeSpecsAndChallenged", "hasFindings", "needsRevision", "noFindings", "noOpenItems"];
|
|
3
|
+
export declare const codeStateCountLabels: {
|
|
4
|
+
readonly adjudicateChallenges: "rebuttal";
|
|
5
|
+
readonly reviewBossCommitSpecs: "review round";
|
|
6
|
+
readonly reviewBossCommitCode: "review round";
|
|
7
|
+
readonly reviewBossCommitMixed: "review round";
|
|
8
|
+
readonly reviewIrTaskCommitSpecs: "review round";
|
|
9
|
+
readonly reviewIrTaskCommitCode: "review round";
|
|
10
|
+
readonly reviewIrTaskCommitMixed: "review round";
|
|
11
|
+
readonly reviewChangesSpecs: "review round";
|
|
12
|
+
readonly reviewChangesCode: "review round";
|
|
13
|
+
readonly reviewChangesMixed: "review round";
|
|
14
|
+
readonly reviewChangesAndChallengesSpecs: "review round";
|
|
15
|
+
readonly reviewChangesAndChallengesCode: "review round";
|
|
16
|
+
readonly reviewChangesAndChallengesMixed: "review round";
|
|
17
|
+
};
|
|
18
|
+
export interface CodeOptions {
|
|
19
|
+
committer?: 'coder' | 'reviewer';
|
|
20
|
+
}
|
|
21
|
+
export interface RegistryPlayer {
|
|
22
|
+
id: string;
|
|
23
|
+
adapter?: string;
|
|
24
|
+
model?: string;
|
|
25
|
+
}
|
|
26
|
+
export interface CreateCodeRuntimeOptions {
|
|
27
|
+
captainOptions: unknown;
|
|
28
|
+
players: readonly RegistryPlayer[];
|
|
29
|
+
}
|
|
30
|
+
export interface CodePlaybookRegistryEntry {
|
|
31
|
+
id: 'code';
|
|
32
|
+
command: 'code';
|
|
33
|
+
intent: string;
|
|
34
|
+
idleStateId: 'ready';
|
|
35
|
+
finalStateId: 'done';
|
|
36
|
+
copyPasteGuardNames: readonly string[];
|
|
37
|
+
stateCountLabels: typeof codeStateCountLabels;
|
|
38
|
+
validateOptions(captainOptions: unknown): CodeOptions;
|
|
39
|
+
createRuntime(options: CreateCodeRuntimeOptions): PlaybookRuntime;
|
|
40
|
+
}
|
|
41
|
+
export declare function validateCodeOptions(captainOptions: unknown): CodeOptions;
|
|
42
|
+
export declare function createCodeRuntimeOptions({ captainOptions, players, }: CreateCodeRuntimeOptions): CodePlaybookOptions;
|
|
43
|
+
export declare const codePlaybookRegistryEntry: CodePlaybookRegistryEntry;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// SPDX-FileCopyrightText: 2026 SubLang International <https://sublang.ai>
|
|
3
|
+
import createPlaybookRuntime from './code.playbook.js';
|
|
4
|
+
// PBRT-29/30: CODE runtime options are carried under
|
|
5
|
+
// `captain.options.code`, a namespaced object the host forwards
|
|
6
|
+
// verbatim through `captain.options`. cligent neither reads nor
|
|
7
|
+
// validates `options.code`; the CODE registry entry is the sole
|
|
8
|
+
// validator. The CODE options schema defines one key, `committer`: an
|
|
9
|
+
// optional Committer-alias player id, one of the baked player ids
|
|
10
|
+
// `coder` / `reviewer`. A valid `options.code` is absent, `{}`, or
|
|
11
|
+
// `{ committer: 'coder' | 'reviewer' }`; every other key is unknown
|
|
12
|
+
// and rejected with a path-named error, and an out-of-range
|
|
13
|
+
// `committer` value is rejected naming `captain.options.code.committer`.
|
|
14
|
+
// A further CODE option shall be introduced as its own higher-numbered
|
|
15
|
+
// item that widens `CODE_OPTION_KEYS`; the validator still fails closed
|
|
16
|
+
// on stray keys.
|
|
17
|
+
const CODE_OPTION_KEYS = new Set(['committer']);
|
|
18
|
+
const COMMITTER_PLAYER_IDS = new Set(['coder', 'reviewer']);
|
|
19
|
+
export const codeCopyPasteGuardNames = [
|
|
20
|
+
'accepted',
|
|
21
|
+
'approved',
|
|
22
|
+
'challengeAccepted',
|
|
23
|
+
'challengeRejected',
|
|
24
|
+
'challengesRaised',
|
|
25
|
+
'changesMadeCode',
|
|
26
|
+
'changesMadeCodeAndChallenged',
|
|
27
|
+
'changesMadeMixed',
|
|
28
|
+
'changesMadeMixedAndChallenged',
|
|
29
|
+
'changesMadeSpecs',
|
|
30
|
+
'changesMadeSpecsAndChallenged',
|
|
31
|
+
'hasFindings',
|
|
32
|
+
'needsRevision',
|
|
33
|
+
'noFindings',
|
|
34
|
+
'noOpenItems',
|
|
35
|
+
];
|
|
36
|
+
export const codeStateCountLabels = {
|
|
37
|
+
adjudicateChallenges: 'rebuttal',
|
|
38
|
+
reviewBossCommitSpecs: 'review round',
|
|
39
|
+
reviewBossCommitCode: 'review round',
|
|
40
|
+
reviewBossCommitMixed: 'review round',
|
|
41
|
+
reviewIrTaskCommitSpecs: 'review round',
|
|
42
|
+
reviewIrTaskCommitCode: 'review round',
|
|
43
|
+
reviewIrTaskCommitMixed: 'review round',
|
|
44
|
+
reviewChangesSpecs: 'review round',
|
|
45
|
+
reviewChangesCode: 'review round',
|
|
46
|
+
reviewChangesMixed: 'review round',
|
|
47
|
+
reviewChangesAndChallengesSpecs: 'review round',
|
|
48
|
+
reviewChangesAndChallengesCode: 'review round',
|
|
49
|
+
reviewChangesAndChallengesMixed: 'review round',
|
|
50
|
+
};
|
|
51
|
+
export function validateCodeOptions(captainOptions) {
|
|
52
|
+
const code = readCodeNamespace(captainOptions);
|
|
53
|
+
if (code === undefined)
|
|
54
|
+
return {};
|
|
55
|
+
if (typeof code !== 'object' || code === null || Array.isArray(code)) {
|
|
56
|
+
throw new Error('captain.options.code must be an object');
|
|
57
|
+
}
|
|
58
|
+
for (const key of Object.keys(code)) {
|
|
59
|
+
if (!CODE_OPTION_KEYS.has(key)) {
|
|
60
|
+
throw new Error(`Unknown config field captain.options.code.${key}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const options = {};
|
|
64
|
+
const committer = code.committer;
|
|
65
|
+
if (committer !== undefined) {
|
|
66
|
+
if (typeof committer !== 'string' || !COMMITTER_PLAYER_IDS.has(committer)) {
|
|
67
|
+
throw new Error("captain.options.code.committer must be 'coder' or 'reviewer'");
|
|
68
|
+
}
|
|
69
|
+
options.committer = committer;
|
|
70
|
+
}
|
|
71
|
+
return options;
|
|
72
|
+
}
|
|
73
|
+
function readCodeNamespace(captainOptions) {
|
|
74
|
+
if (typeof captainOptions !== 'object' ||
|
|
75
|
+
captainOptions === null ||
|
|
76
|
+
Array.isArray(captainOptions)) {
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
return captainOptions.code;
|
|
80
|
+
}
|
|
81
|
+
function playerIdentity(players, id) {
|
|
82
|
+
const entry = players.find((p) => p.id === id);
|
|
83
|
+
return entry?.model ?? entry?.adapter;
|
|
84
|
+
}
|
|
85
|
+
export function createCodeRuntimeOptions({ captainOptions, players, }) {
|
|
86
|
+
const codeOptions = validateCodeOptions(captainOptions);
|
|
87
|
+
const coderPlayer = playerIdentity(players, 'coder');
|
|
88
|
+
const reviewerPlayer = playerIdentity(players, 'reviewer');
|
|
89
|
+
return {
|
|
90
|
+
coderPlayer,
|
|
91
|
+
reviewerPlayer,
|
|
92
|
+
...(codeOptions.committer !== undefined
|
|
93
|
+
? { committerPlayer: codeOptions.committer }
|
|
94
|
+
: {}),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
export const codePlaybookRegistryEntry = {
|
|
98
|
+
id: 'code',
|
|
99
|
+
command: 'code',
|
|
100
|
+
intent: 'software development / SDLC coding workflow',
|
|
101
|
+
idleStateId: 'ready',
|
|
102
|
+
finalStateId: 'done',
|
|
103
|
+
copyPasteGuardNames: codeCopyPasteGuardNames,
|
|
104
|
+
stateCountLabels: codeStateCountLabels,
|
|
105
|
+
validateOptions: validateCodeOptions,
|
|
106
|
+
createRuntime(options) {
|
|
107
|
+
return createPlaybookRuntime(createCodeRuntimeOptions(options));
|
|
108
|
+
},
|
|
109
|
+
};
|