@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
|
|
@@ -16,6 +18,65 @@ const BOSS_REPLY_ERRORS = {
|
|
|
16
18
|
missingQuestion: "needsBossReply outcome missing 'question' field",
|
|
17
19
|
unregisteredState: (stateId) => `state ${stateId} declared needsBossReply but is not registered as resumable`,
|
|
18
20
|
};
|
|
21
|
+
// Required-payload fields whose value is the player's verbatim long-form
|
|
22
|
+
// prose. The runtime carries `finalText.trim()` into these fields rather
|
|
23
|
+
// than asking the judge to round-trip the text through JSON. Short
|
|
24
|
+
// extracted fields (`question`, `taskDescription`, `irNumber`, …) stay
|
|
25
|
+
// judge-extracted — they are not in this set.
|
|
26
|
+
const VERBATIM_PAYLOAD_FIELDS = new Set([
|
|
27
|
+
'reviews',
|
|
28
|
+
'challenges',
|
|
29
|
+
]);
|
|
30
|
+
// Normalize an unknown error value to the compact `{ name, message }`
|
|
31
|
+
// shape used by Captain-pane / status emissions. Returns `undefined`
|
|
32
|
+
// for nullish input so callers can omit absent errors.
|
|
33
|
+
function normalizeErrorCompact(err) {
|
|
34
|
+
if (err === undefined || err === null)
|
|
35
|
+
return undefined;
|
|
36
|
+
if (err instanceof Error) {
|
|
37
|
+
return { name: err.name, message: err.message };
|
|
38
|
+
}
|
|
39
|
+
if (typeof err === 'object') {
|
|
40
|
+
const o = err;
|
|
41
|
+
if (typeof o.message === 'string') {
|
|
42
|
+
return {
|
|
43
|
+
name: typeof o.name === 'string' ? o.name : 'Error',
|
|
44
|
+
message: o.message,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return { name: 'Error', message: String(err) };
|
|
49
|
+
}
|
|
50
|
+
// Normalize an unknown error value to the full `{ name, message, stack }`
|
|
51
|
+
// shape used by telemetry emissions. Returns `undefined` for nullish
|
|
52
|
+
// input. `stack` is omitted when not available on the source value.
|
|
53
|
+
function normalizeErrorFull(err) {
|
|
54
|
+
const compact = normalizeErrorCompact(err);
|
|
55
|
+
if (compact === undefined)
|
|
56
|
+
return undefined;
|
|
57
|
+
if (err instanceof Error) {
|
|
58
|
+
return err.stack !== undefined ? { ...compact, stack: err.stack } : compact;
|
|
59
|
+
}
|
|
60
|
+
if (typeof err === 'object' && err !== null) {
|
|
61
|
+
const stack = err.stack;
|
|
62
|
+
if (typeof stack === 'string') {
|
|
63
|
+
return { ...compact, stack };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return compact;
|
|
67
|
+
}
|
|
68
|
+
// Normalize any `error` field inside a telemetry event so failed
|
|
69
|
+
// transitions don't leak raw Error instances through the channel.
|
|
70
|
+
function normalizeEventForTelemetry(event) {
|
|
71
|
+
if (event === null || typeof event !== 'object' || Array.isArray(event)) {
|
|
72
|
+
return event;
|
|
73
|
+
}
|
|
74
|
+
const e = event;
|
|
75
|
+
if (!('error' in e))
|
|
76
|
+
return event;
|
|
77
|
+
const normalized = normalizeErrorFull(e.error);
|
|
78
|
+
return { ...e, error: normalized };
|
|
79
|
+
}
|
|
19
80
|
// Internal capabilities (DR-004 §10). Each ships with its final
|
|
20
81
|
// signature; behavior lands in the per-capability task noted by the
|
|
21
82
|
// TODO marker.
|
|
@@ -63,15 +124,21 @@ function composePlayerPrompt(input) {
|
|
|
63
124
|
}
|
|
64
125
|
return blocks.join('\n\n');
|
|
65
126
|
}
|
|
66
|
-
// Player-id resolver — DR-004 §2.
|
|
127
|
+
// Player-id resolver — DR-004 §2 / PBRT-8.
|
|
67
128
|
// Non-composite: Coder→'coder', Reviewer→'reviewer'. The composite
|
|
68
|
-
// Committer (= Coder | Reviewer per code.gears.md) resolves
|
|
69
|
-
//
|
|
70
|
-
// `
|
|
71
|
-
//
|
|
72
|
-
// back to
|
|
73
|
-
//
|
|
74
|
-
//
|
|
129
|
+
// Committer (= Coder | Reviewer per code.gears.md) resolves to the
|
|
130
|
+
// configured alias when present: `input.committerPlayer`, the
|
|
131
|
+
// validated `captain.options.code.committer` (PBRT-8 / PBRT-30),
|
|
132
|
+
// already a baked player id ('coder' / 'reviewer'). Absent a
|
|
133
|
+
// configured alias it falls back to the DR-004 §2 baked binding by
|
|
134
|
+
// populated <playerName>Player field: prefer `coderPlayer` (CODE-18
|
|
135
|
+
// wires only coderPlayer; CODE-19 wires both so coderPlayer still wins
|
|
136
|
+
// as the alias's first alternative), fall back to `reviewerPlayer`
|
|
137
|
+
// only if a future gear ships a reviewer-only Committer item, and
|
|
138
|
+
// finally to the alias's first alternative (Coder) when neither is
|
|
139
|
+
// set. The alias selects only the host pane; it is not a PBRT-4
|
|
140
|
+
// identity string, so it leaves <coder-llm> / <reviewer-llm>
|
|
141
|
+
// untouched and `input.player` stays `Committer` (PLAYBOOK-3).
|
|
75
142
|
function resolvePlayerId(input) {
|
|
76
143
|
switch (input.player) {
|
|
77
144
|
case 'Coder':
|
|
@@ -79,6 +146,8 @@ function resolvePlayerId(input) {
|
|
|
79
146
|
case 'Reviewer':
|
|
80
147
|
return 'reviewer';
|
|
81
148
|
case 'Committer':
|
|
149
|
+
if (input.committerPlayer !== undefined)
|
|
150
|
+
return input.committerPlayer;
|
|
82
151
|
if (input.coderPlayer !== undefined)
|
|
83
152
|
return 'coder';
|
|
84
153
|
if (input.reviewerPlayer !== undefined)
|
|
@@ -116,8 +185,17 @@ async function adjudicate(input, finalText, ports, signal) {
|
|
|
116
185
|
// required fields with the literal phrase
|
|
117
186
|
// Output shall include `<fieldName>: <...>`
|
|
118
187
|
// so we extract those tokens and require each to be a string in
|
|
119
|
-
// the judge response
|
|
188
|
+
// the judge response — except for VERBATIM_PAYLOAD_FIELDS
|
|
189
|
+
// (`reviews`, `challenges`), where the runtime substitutes
|
|
190
|
+
// `finalText.trim()` so the long-form prose is not round-tripped
|
|
191
|
+
// through judge JSON. Short extracted fields like `question` and
|
|
192
|
+
// `taskDescription` keep the existing extract-and-validate path.
|
|
193
|
+
const verbatim = finalText.trim();
|
|
120
194
|
for (const field of extractRequiredFields(input.result[guard])) {
|
|
195
|
+
if (VERBATIM_PAYLOAD_FIELDS.has(field)) {
|
|
196
|
+
obj[field] = verbatim;
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
121
199
|
if (typeof obj[field] !== 'string') {
|
|
122
200
|
if (guard === 'needsBossReply' && field === 'question') {
|
|
123
201
|
throw new Error(BOSS_REPLY_ERRORS.missingQuestion);
|
|
@@ -145,24 +223,155 @@ function buildJudgePrompt(input, finalText) {
|
|
|
145
223
|
lines.push('');
|
|
146
224
|
lines.push('Pick exactly one outcome by `guard` and return JSON ' +
|
|
147
225
|
'`{ guard, …payloadFields }`. Required payload fields are ' +
|
|
148
|
-
'named in the outcome description.'
|
|
226
|
+
'named in the outcome description. Do not copy long-form ' +
|
|
227
|
+
'verbatim fields (`reviews`, `challenges`) — the runtime ' +
|
|
228
|
+
"carries the player's output into those fields verbatim, " +
|
|
229
|
+
'so any value you supply for them will be overwritten.');
|
|
149
230
|
lines.push('');
|
|
150
231
|
for (const [key, description] of Object.entries(input.result)) {
|
|
151
232
|
lines.push(`- \`${key}\` — ${description}`);
|
|
152
233
|
}
|
|
153
234
|
return lines.join('\n');
|
|
154
235
|
}
|
|
236
|
+
// Judge replies are meant to be a single JSON object, but LLMs
|
|
237
|
+
// routinely wrap them in prose ("Here is the result: …"), Markdown
|
|
238
|
+
// code fences, or trailing commentary, and occasionally emit a
|
|
239
|
+
// trailing comma or truncate the tail (a dropped closing brace, an
|
|
240
|
+
// unterminated string). parseJudgeJson is deliberately lenient: it
|
|
241
|
+
// first tries a strict parse of the (optionally fenced) body, then
|
|
242
|
+
// scans every `{`/`[` as a possible start in document order and
|
|
243
|
+
// returns the first recoverable object. At each start it prefers a
|
|
244
|
+
// strict balanced span and falls back to a repaired (trailing-comma /
|
|
245
|
+
// truncation) span, so a damaged object earlier in the prose is not
|
|
246
|
+
// overridden by a cleaner one later. Scanning every start (not just
|
|
247
|
+
// the first bracket) keeps a bracketed fragment in surrounding prose —
|
|
248
|
+
// e.g. an aside like `see [1]` or `{n/a}` before the real object —
|
|
249
|
+
// from masking a later, genuinely valid object. Both callers
|
|
250
|
+
// (classification and adjudication) expect an object, so plain objects
|
|
251
|
+
// win over arrays/scalars; the first value of any shape is remembered
|
|
252
|
+
// so a legitimately array/scalar reply still surfaces to the caller's
|
|
253
|
+
// own object check. Only a reply from which no JSON value can be
|
|
254
|
+
// recovered is treated as malformed and throws, preserving the
|
|
255
|
+
// control-plane error contract (PBRT-7, PBRT-10).
|
|
155
256
|
function parseJudgeJson(raw) {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
if (fence)
|
|
159
|
-
text = fence[1].trim();
|
|
257
|
+
const fenced = stripCodeFence(raw.trim());
|
|
258
|
+
// Fast path: a well-formed (optionally fenced) JSON body.
|
|
160
259
|
try {
|
|
161
|
-
return JSON.parse(
|
|
162
|
-
}
|
|
163
|
-
catch
|
|
164
|
-
|
|
165
|
-
}
|
|
260
|
+
return JSON.parse(fenced);
|
|
261
|
+
}
|
|
262
|
+
catch {
|
|
263
|
+
// Fall through to lenient extraction + repair.
|
|
264
|
+
}
|
|
265
|
+
const starts = [];
|
|
266
|
+
for (let i = 0; i < fenced.length; i++) {
|
|
267
|
+
const ch = fenced[i];
|
|
268
|
+
if (ch === '{' || ch === '[')
|
|
269
|
+
starts.push(i);
|
|
270
|
+
}
|
|
271
|
+
// Walk starts in document order. At each start prefer a strict
|
|
272
|
+
// balanced span (most trustworthy) and fall back to a repaired one
|
|
273
|
+
// for a trailing-comma / truncated tail, so the earliest intended
|
|
274
|
+
// object wins even when it needs repair. Return the first plain
|
|
275
|
+
// object; remember the first value of any shape so a legitimately
|
|
276
|
+
// array/scalar reply still surfaces to the caller's own object check.
|
|
277
|
+
let firstValue;
|
|
278
|
+
for (const start of starts) {
|
|
279
|
+
let parsedHere;
|
|
280
|
+
for (const repair of [false, true]) {
|
|
281
|
+
const candidate = extractJsonValue(fenced, start, repair);
|
|
282
|
+
if (candidate === undefined)
|
|
283
|
+
continue;
|
|
284
|
+
try {
|
|
285
|
+
parsedHere = { value: JSON.parse(candidate) };
|
|
286
|
+
}
|
|
287
|
+
catch {
|
|
288
|
+
continue; // not parseable this way — try repair, then next start
|
|
289
|
+
}
|
|
290
|
+
break; // prefer the strict span at this start over its repair
|
|
291
|
+
}
|
|
292
|
+
if (parsedHere === undefined)
|
|
293
|
+
continue;
|
|
294
|
+
if (isPlainObject(parsedHere.value))
|
|
295
|
+
return parsedHere.value;
|
|
296
|
+
if (firstValue === undefined)
|
|
297
|
+
firstValue = parsedHere;
|
|
298
|
+
}
|
|
299
|
+
if (firstValue !== undefined)
|
|
300
|
+
return firstValue.value;
|
|
301
|
+
throw new Error('adjudicate: judge response is not valid JSON');
|
|
302
|
+
}
|
|
303
|
+
function isPlainObject(value) {
|
|
304
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
305
|
+
}
|
|
306
|
+
// Strip a single Markdown code fence that wraps the whole string.
|
|
307
|
+
function stripCodeFence(text) {
|
|
308
|
+
const fence = text.match(/^```(?:json)?\s*\n?([\s\S]*?)\n?```$/);
|
|
309
|
+
return fence ? fence[1].trim() : text;
|
|
310
|
+
}
|
|
311
|
+
// Scan from `start` (a `{`/`[` index), tracking string and
|
|
312
|
+
// bracket-nesting state, and emit the balanced JSON value rooted
|
|
313
|
+
// there. Anything after the top-level value closes is ignored (so a
|
|
314
|
+
// trailing code fence or commentary does not matter). With
|
|
315
|
+
// `repair === false` the span is returned only if it actually closes,
|
|
316
|
+
// and trailing commas are left intact — so the caller can prefer a
|
|
317
|
+
// cleanly-balanced span before attempting repair; if input ends
|
|
318
|
+
// before the value closes, undefined is returned. With
|
|
319
|
+
// `repair === true` common damage is fixed: a trailing comma before a
|
|
320
|
+
// close is removed, an unterminated string is closed, and any
|
|
321
|
+
// brackets still open at end-of-input are closed in order.
|
|
322
|
+
function extractJsonValue(text, start, repair) {
|
|
323
|
+
const stack = [];
|
|
324
|
+
let out = '';
|
|
325
|
+
let inString = false;
|
|
326
|
+
let escaped = false;
|
|
327
|
+
for (let i = start; i < text.length; i++) {
|
|
328
|
+
const ch = text[i];
|
|
329
|
+
if (inString) {
|
|
330
|
+
out += ch;
|
|
331
|
+
if (escaped)
|
|
332
|
+
escaped = false;
|
|
333
|
+
else if (ch === '\\')
|
|
334
|
+
escaped = true;
|
|
335
|
+
else if (ch === '"')
|
|
336
|
+
inString = false;
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
if (ch === '"') {
|
|
340
|
+
inString = true;
|
|
341
|
+
out += ch;
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
if (ch === '{' || ch === '[') {
|
|
345
|
+
stack.push(ch === '{' ? '}' : ']');
|
|
346
|
+
out += ch;
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
if (ch === '}' || ch === ']') {
|
|
350
|
+
if (repair)
|
|
351
|
+
out = dropTrailingComma(out);
|
|
352
|
+
out += ch;
|
|
353
|
+
stack.pop();
|
|
354
|
+
if (stack.length === 0)
|
|
355
|
+
return out; // top-level value complete
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
out += ch;
|
|
359
|
+
}
|
|
360
|
+
// End of input before the top-level value closed.
|
|
361
|
+
if (!repair)
|
|
362
|
+
return undefined; // strict pass: no balanced span here
|
|
363
|
+
if (inString)
|
|
364
|
+
out += '"';
|
|
365
|
+
out = dropTrailingComma(out);
|
|
366
|
+
while (stack.length > 0)
|
|
367
|
+
out += stack.pop();
|
|
368
|
+
return out;
|
|
369
|
+
}
|
|
370
|
+
// Remove a trailing comma (and any whitespace after it) at the end of
|
|
371
|
+
// the accumulated output, so `{"a":1,}` / `[1,2,]` and truncated
|
|
372
|
+
// `{"a":1,` repair to valid JSON.
|
|
373
|
+
function dropTrailingComma(out) {
|
|
374
|
+
return out.replace(/,(\s*)$/, '$1');
|
|
166
375
|
}
|
|
167
376
|
// Boss-event classifier — DR-004 §3.
|
|
168
377
|
// Every non-empty Boss turn goes through ports.callJudge. Slash-prefixed
|
|
@@ -184,7 +393,18 @@ async function classifyWithLlm(text, ports, signal, snapshotOrState) {
|
|
|
184
393
|
const state = classifierState(snapshotOrState);
|
|
185
394
|
const prompt = buildClassifierPrompt(text, state);
|
|
186
395
|
const raw = await ports.callJudge(prompt, signal);
|
|
187
|
-
|
|
396
|
+
let parsed;
|
|
397
|
+
try {
|
|
398
|
+
parsed = parseJudgeJson(raw);
|
|
399
|
+
}
|
|
400
|
+
catch {
|
|
401
|
+
// No JSON value could be recovered. Classification failures are
|
|
402
|
+
// non-fatal — unlike adjudication, which throws to the failure
|
|
403
|
+
// state (PBRT-10) — so surface one status and take no FSM action,
|
|
404
|
+
// consistent with the other invalid-reply paths below (PBRT-7).
|
|
405
|
+
await ports.emitStatus('Classifier reply was not recoverable JSON');
|
|
406
|
+
return undefined;
|
|
407
|
+
}
|
|
188
408
|
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
189
409
|
await ports.emitStatus('Classifier returned a non-object JSON response');
|
|
190
410
|
return undefined;
|
|
@@ -444,20 +664,31 @@ function pendingBossQuestionFromContext(context) {
|
|
|
444
664
|
question: candidate.question,
|
|
445
665
|
};
|
|
446
666
|
}
|
|
447
|
-
|
|
448
|
-
|
|
667
|
+
// PBRT-3 / PBRT-14: on entry to `awaitBossReply` the runtime surfaces
|
|
668
|
+
// the pending player question as a captain-speech act attributed to the
|
|
669
|
+
// asking player (`<player> asks: <full question>`), emitted with no
|
|
670
|
+
// glyph so the host renders it as captain speech. The question is
|
|
671
|
+
// carried verbatim and in full — the judge JSON that produced it rides
|
|
672
|
+
// a hidden callCaptain (PBRT-15), so this line is the Boss's only
|
|
673
|
+
// legible view of what was asked.
|
|
674
|
+
function formatAwaitBossReplyQuestion(context) {
|
|
675
|
+
const pending = pendingBossQuestionFromContext(context);
|
|
676
|
+
const player = pending?.player ?? 'unknown';
|
|
677
|
+
const question = pending?.question ?? '';
|
|
678
|
+
return `${player} asks: ${question}`;
|
|
449
679
|
}
|
|
450
|
-
|
|
680
|
+
// The rider-less routing marker emitted right after the question line.
|
|
681
|
+
// It carries only the resume target, asking player, and source item;
|
|
682
|
+
// the former `q="<first 80 chars>"` excerpt rider is dropped now that
|
|
683
|
+
// the full question rides the captain-speech line above.
|
|
684
|
+
function formatAwaitBossReplyMarker(context) {
|
|
451
685
|
const pending = pendingBossQuestionFromContext(context);
|
|
452
686
|
const resumeStateId = pending?.resumeStateId ?? 'unknown';
|
|
453
687
|
const player = pending?.player ?? 'unknown';
|
|
454
688
|
const sourceItem = pending?.sourceItem ?? 'unknown';
|
|
455
|
-
|
|
456
|
-
return `◆ awaiting Boss reply · ${resumeStateId} · ${player} · ${sourceItem} · q=${JSON.stringify(question)}`;
|
|
689
|
+
return `◆ awaiting Boss reply · ${resumeStateId} · ${player} · ${sourceItem}`;
|
|
457
690
|
}
|
|
458
|
-
function formatStateEntry(stateId
|
|
459
|
-
if (stateId === 'awaitBossReply')
|
|
460
|
-
return formatAwaitBossReplyEntry(context);
|
|
691
|
+
function formatStateEntry(stateId) {
|
|
461
692
|
if (SUPPRESSED_ENTRY_STATES.has(stateId))
|
|
462
693
|
return undefined;
|
|
463
694
|
if (stateId === 'failed')
|
|
@@ -490,13 +721,23 @@ function formatClassification(eventType) {
|
|
|
490
721
|
return eventType;
|
|
491
722
|
}
|
|
492
723
|
function stateTelemetryPayload(from, to, event, context) {
|
|
493
|
-
const payload = {
|
|
724
|
+
const payload = {
|
|
725
|
+
from,
|
|
726
|
+
to,
|
|
727
|
+
event: normalizeEventForTelemetry(event),
|
|
728
|
+
};
|
|
494
729
|
if (to === 'awaitBossReply') {
|
|
495
730
|
const pendingBossQuestion = pendingBossQuestionFromContext(context);
|
|
496
731
|
if (pendingBossQuestion !== undefined) {
|
|
497
732
|
payload.pendingBossQuestion = pendingBossQuestion;
|
|
498
733
|
}
|
|
499
734
|
}
|
|
735
|
+
if (to === 'failed') {
|
|
736
|
+
const lastError = normalizeErrorFull(context.lastError);
|
|
737
|
+
if (lastError !== undefined) {
|
|
738
|
+
payload.lastError = lastError;
|
|
739
|
+
}
|
|
740
|
+
}
|
|
500
741
|
return payload;
|
|
501
742
|
}
|
|
502
743
|
// Internal export surface for tests. Not part of the stable public API;
|
|
@@ -512,11 +753,16 @@ export const _internal = {
|
|
|
512
753
|
STATE_LABELS,
|
|
513
754
|
stateMetadata,
|
|
514
755
|
pendingBossQuestionFromContext,
|
|
515
|
-
|
|
756
|
+
formatAwaitBossReplyQuestion,
|
|
757
|
+
formatAwaitBossReplyMarker,
|
|
516
758
|
formatStateEntry,
|
|
517
759
|
formatTransition,
|
|
518
760
|
formatClassification,
|
|
519
761
|
stateTelemetryPayload,
|
|
762
|
+
normalizeErrorCompact,
|
|
763
|
+
normalizeErrorFull,
|
|
764
|
+
normalizeEventForTelemetry,
|
|
765
|
+
VERBATIM_PAYLOAD_FIELDS,
|
|
520
766
|
};
|
|
521
767
|
export default function createPlaybookRuntime(options) {
|
|
522
768
|
let actor;
|
|
@@ -590,13 +836,25 @@ export default function createPlaybookRuntime(options) {
|
|
|
590
836
|
if (transitionLine !== undefined) {
|
|
591
837
|
enqueueEmit(() => ports.emitStatus(transitionLine));
|
|
592
838
|
}
|
|
593
|
-
|
|
839
|
+
// awaitBossReply surfaces two lines per PBRT-3 / PBRT-14: the
|
|
840
|
+
// full player question as captain speech, then the rider-less
|
|
841
|
+
// routing marker. The full-question telemetry rides
|
|
842
|
+
// stateTelemetryPayload above.
|
|
843
|
+
if (to === 'awaitBossReply') {
|
|
844
|
+
const questionLine = formatAwaitBossReplyQuestion(context);
|
|
845
|
+
const markerLine = formatAwaitBossReplyMarker(context);
|
|
846
|
+
enqueueEmit(() => ports.emitStatus(questionLine));
|
|
847
|
+
enqueueEmit(() => ports.emitStatus(markerLine));
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
const entryLine = formatStateEntry(to);
|
|
594
851
|
if (entryLine === undefined)
|
|
595
852
|
return;
|
|
596
853
|
if (to === 'failed') {
|
|
597
854
|
const lastError = snap.context
|
|
598
855
|
?.lastError;
|
|
599
|
-
|
|
856
|
+
const data = { lastError: normalizeErrorCompact(lastError) };
|
|
857
|
+
enqueueEmit(() => ports.emitStatus(entryLine, data));
|
|
600
858
|
}
|
|
601
859
|
else {
|
|
602
860
|
enqueueEmit(() => ports.emitStatus(entryLine));
|