@sublang/playbook 3.1.0 → 5.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 +64 -99
- package/docs/assets/playbook-venn.svg +13 -0
- package/docs/cli.md +83 -9
- package/docs/configuration.md +5 -3
- package/package.json +7 -4
- package/reference/sdlc/captain.md +70 -83
- package/reference/sdlc/captain.playbook/captain.fsm.d.ts +127 -142
- package/reference/sdlc/captain.playbook/captain.fsm.js +349 -470
- package/reference/sdlc/captain.playbook/captain.fsm.ts +535 -598
- package/reference/sdlc/captain.playbook/captain.gears.md +37 -41
- package/reference/sdlc/captain.playbook/captain.playbook.d.ts +90 -15
- package/reference/sdlc/captain.playbook/captain.playbook.js +464 -968
- package/reference/sdlc/captain.playbook/captain.playbook.ts +696 -993
- package/reference/sdlc/code.playbook/bin/adapter-sdk.js +247 -0
- package/reference/sdlc/code.playbook/bin/playbook.js +54 -9
- package/reference/sdlc/code.playbook/bin/run.js +97 -0
- package/reference/sdlc/code.playbook/code.playbook.js +17 -0
- package/reference/sdlc/code.playbook/code.playbook.ts +17 -0
- package/reference/sdlc/code.playbook/playbook-captain.d.ts +2 -0
- package/reference/sdlc/code.playbook/playbook-captain.js +1784 -215
- package/reference/sdlc/code.playbook/playbook-captain.ts +2293 -330
- package/reference/sdlc/code.playbook/playbook.config.template.yaml +7 -0
- package/reference/sdlc/discuss.playbook/discuss.playbook.js +41 -9
- package/reference/sdlc/discuss.playbook/discuss.playbook.ts +42 -9
- package/slc/gears2fsm.md +54 -2
- package/slc/link.md +293 -25
- package/src/runtime.d.ts +29 -1
- package/src/runtime.ts +47 -0
- package/src/xstate-playbook-runtime.d.ts +97 -5
- package/src/xstate-playbook-runtime.js +769 -29
- package/src/xstate-playbook-runtime.ts +962 -34
|
@@ -10,6 +10,13 @@
|
|
|
10
10
|
# settings inline: an adapter shorthand (claude, codex) or a block with
|
|
11
11
|
# adapter/model/effort/permissions. Retuning one agent never changes
|
|
12
12
|
# another.
|
|
13
|
+
|
|
14
|
+
# Each adapter needs its vendor SDK installed as its own top-level
|
|
15
|
+
# install root — they are optional peer dependencies, so you pay only
|
|
16
|
+
# for the vendors named below:
|
|
17
|
+
# claude -> npm install -g @anthropic-ai/claude-agent-sdk
|
|
18
|
+
# codex -> npm install -g @openai/codex-sdk
|
|
19
|
+
# Drop an adapter from this file and you can skip its SDK entirely.
|
|
13
20
|
# Every seeded agent runs in cligent's protected auto mode
|
|
14
21
|
# (permissions.mode: auto): claude maps it to permissionMode auto, codex to
|
|
15
22
|
# on-request + auto_review. Codex roles also grant writablePaths: ['.git']
|
|
@@ -520,6 +520,13 @@ function normalizeErrorCompact(err) {
|
|
|
520
520
|
function normalizeErrorFull(err) {
|
|
521
521
|
return err === undefined || err === null ? undefined : normalizeError(err);
|
|
522
522
|
}
|
|
523
|
+
// DR-028's unified empty predicate: a missing, empty, or whitespace-only
|
|
524
|
+
// `finalText` on an `ok` player result is the one shape that earns exactly
|
|
525
|
+
// one corrective re-ask before the existing failure path applies. Mirrors
|
|
526
|
+
// the shared engine's predicate (PBRT-9).
|
|
527
|
+
function isEmptyFinalText(finalText) {
|
|
528
|
+
return finalText === undefined || finalText.trim().length === 0;
|
|
529
|
+
}
|
|
523
530
|
function isAbortFailure(error, signal) {
|
|
524
531
|
if (!signal.aborted)
|
|
525
532
|
return false;
|
|
@@ -944,16 +951,29 @@ export const createPlaybookRuntime = (options) => {
|
|
|
944
951
|
throw error;
|
|
945
952
|
}
|
|
946
953
|
combined.throwIfAborted();
|
|
947
|
-
|
|
954
|
+
let { playerId, result } = await callPlayer(input, combined);
|
|
955
|
+
if (result.status === 'ok' && isEmptyFinalText(result.finalText)) {
|
|
956
|
+
// DR-028: an `ok` result whose finalText is missing, empty, or
|
|
957
|
+
// whitespace-only earns exactly one corrective re-ask — the same
|
|
958
|
+
// composed call repeated, traced by runPlayerCall as its own
|
|
959
|
+
// player-call pair, with the resume selection re-read from the
|
|
960
|
+
// token map the first result left (PBRT-38). An abort that lands
|
|
961
|
+
// between the two calls ends the turn without the re-ask (aborts
|
|
962
|
+
// are never retried), and a rejecting finish emission rejects
|
|
963
|
+
// `callPlayer` itself, so it never reaches this branch (PBRT-47).
|
|
964
|
+
combined.throwIfAborted();
|
|
965
|
+
({ playerId, result } = await callPlayer(input, combined));
|
|
966
|
+
}
|
|
948
967
|
if (result.status !== 'ok') {
|
|
949
968
|
throw new Error(`player "${playerId}" returned status "${result.status}"${result.error ? `: ${result.error}` : ''}`);
|
|
950
969
|
}
|
|
951
|
-
|
|
970
|
+
const finalText = result.finalText ?? '';
|
|
971
|
+
if (isEmptyFinalText(finalText)) {
|
|
952
972
|
throw new Error(`player "${playerId}" returned status "ok" with no finalText`);
|
|
953
973
|
}
|
|
954
974
|
combined.throwIfAborted();
|
|
955
975
|
try {
|
|
956
|
-
const prompt = buildAdjudicatorPrompt(input,
|
|
976
|
+
const prompt = buildAdjudicatorPrompt(input, finalText);
|
|
957
977
|
return parseAdjudication(await callJudge(prompt, combined, 'player-output-adjudication', input.stateId), input);
|
|
958
978
|
}
|
|
959
979
|
catch (error) {
|
|
@@ -1028,8 +1048,23 @@ export const createPlaybookRuntime = (options) => {
|
|
|
1028
1048
|
inspect,
|
|
1029
1049
|
});
|
|
1030
1050
|
};
|
|
1051
|
+
// PBRT-6: the single seam that stops this runtime's actor. Stopping a
|
|
1052
|
+
// still-running actor fires one more `@xstate.snapshot` for the *unchanged*
|
|
1053
|
+
// state value with `status: 'stopped'`; `inspect` cannot tell that disposal
|
|
1054
|
+
// artifact from a state entry, so unsuppressed it re-emits the parked
|
|
1055
|
+
// state's telemetry and a phantom self-loop transition. Suppression is a
|
|
1056
|
+
// property of stopping, not a rule each caller must remember — every stop
|
|
1057
|
+
// goes through here so no later site can reintroduce the omission.
|
|
1058
|
+
const stopActor = () => {
|
|
1059
|
+
if (!actor)
|
|
1060
|
+
return;
|
|
1061
|
+
suppressInspectionEmissions = true;
|
|
1062
|
+
actor.stop();
|
|
1063
|
+
};
|
|
1031
1064
|
const startActor = () => {
|
|
1032
1065
|
createRuntimeActor();
|
|
1066
|
+
// A fresh actor's emissions are real state entries again.
|
|
1067
|
+
suppressInspectionEmissions = false;
|
|
1033
1068
|
actor?.start();
|
|
1034
1069
|
};
|
|
1035
1070
|
const driveToQuiescence = async () => {
|
|
@@ -1114,9 +1149,8 @@ export const createPlaybookRuntime = (options) => {
|
|
|
1114
1149
|
// A state that cannot even normalize has no disposal descriptor.
|
|
1115
1150
|
}
|
|
1116
1151
|
}
|
|
1117
|
-
suppressInspectionEmissions = true;
|
|
1118
1152
|
try {
|
|
1119
|
-
|
|
1153
|
+
stopActor();
|
|
1120
1154
|
}
|
|
1121
1155
|
catch {
|
|
1122
1156
|
// Preserve the original startup failure.
|
|
@@ -1327,7 +1361,7 @@ export const createPlaybookRuntime = (options) => {
|
|
|
1327
1361
|
else {
|
|
1328
1362
|
await emitBoundaryStatus(event.type, currentState());
|
|
1329
1363
|
if (actor.getSnapshot().status === 'done') {
|
|
1330
|
-
|
|
1364
|
+
stopActor();
|
|
1331
1365
|
startActor();
|
|
1332
1366
|
}
|
|
1333
1367
|
actor.send(event);
|
|
@@ -1442,9 +1476,7 @@ export const createPlaybookRuntime = (options) => {
|
|
|
1442
1476
|
}
|
|
1443
1477
|
const finalState = currentState();
|
|
1444
1478
|
const failures = [];
|
|
1445
|
-
|
|
1446
|
-
actor.stop();
|
|
1447
|
-
}
|
|
1479
|
+
stopActor();
|
|
1448
1480
|
try {
|
|
1449
1481
|
await drainBoundaryCallsAndEmissions();
|
|
1450
1482
|
}
|
|
@@ -713,6 +713,14 @@ function normalizeErrorFull(
|
|
|
713
713
|
return err === undefined || err === null ? undefined : normalizeError(err);
|
|
714
714
|
}
|
|
715
715
|
|
|
716
|
+
// DR-028's unified empty predicate: a missing, empty, or whitespace-only
|
|
717
|
+
// `finalText` on an `ok` player result is the one shape that earns exactly
|
|
718
|
+
// one corrective re-ask before the existing failure path applies. Mirrors
|
|
719
|
+
// the shared engine's predicate (PBRT-9).
|
|
720
|
+
function isEmptyFinalText(finalText: string | undefined): boolean {
|
|
721
|
+
return finalText === undefined || finalText.trim().length === 0;
|
|
722
|
+
}
|
|
723
|
+
|
|
716
724
|
function isAbortFailure(error: unknown, signal: AbortSignal): boolean {
|
|
717
725
|
if (!signal.aborted) return false;
|
|
718
726
|
if (Object.is(error, signal.reason)) return true;
|
|
@@ -1263,7 +1271,19 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1263
1271
|
}
|
|
1264
1272
|
combined.throwIfAborted();
|
|
1265
1273
|
|
|
1266
|
-
|
|
1274
|
+
let { playerId, result } = await callPlayer(input, combined);
|
|
1275
|
+
if (result.status === 'ok' && isEmptyFinalText(result.finalText)) {
|
|
1276
|
+
// DR-028: an `ok` result whose finalText is missing, empty, or
|
|
1277
|
+
// whitespace-only earns exactly one corrective re-ask — the same
|
|
1278
|
+
// composed call repeated, traced by runPlayerCall as its own
|
|
1279
|
+
// player-call pair, with the resume selection re-read from the
|
|
1280
|
+
// token map the first result left (PBRT-38). An abort that lands
|
|
1281
|
+
// between the two calls ends the turn without the re-ask (aborts
|
|
1282
|
+
// are never retried), and a rejecting finish emission rejects
|
|
1283
|
+
// `callPlayer` itself, so it never reaches this branch (PBRT-47).
|
|
1284
|
+
combined.throwIfAborted();
|
|
1285
|
+
({ playerId, result } = await callPlayer(input, combined));
|
|
1286
|
+
}
|
|
1267
1287
|
if (result.status !== 'ok') {
|
|
1268
1288
|
throw new Error(
|
|
1269
1289
|
`player "${playerId}" returned status "${result.status}"${
|
|
@@ -1271,7 +1291,8 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1271
1291
|
}`,
|
|
1272
1292
|
);
|
|
1273
1293
|
}
|
|
1274
|
-
|
|
1294
|
+
const finalText = result.finalText ?? '';
|
|
1295
|
+
if (isEmptyFinalText(finalText)) {
|
|
1275
1296
|
throw new Error(
|
|
1276
1297
|
`player "${playerId}" returned status "ok" with no finalText`,
|
|
1277
1298
|
);
|
|
@@ -1279,7 +1300,7 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1279
1300
|
combined.throwIfAborted();
|
|
1280
1301
|
|
|
1281
1302
|
try {
|
|
1282
|
-
const prompt = buildAdjudicatorPrompt(input,
|
|
1303
|
+
const prompt = buildAdjudicatorPrompt(input, finalText);
|
|
1283
1304
|
return parseAdjudication(
|
|
1284
1305
|
await callJudge(
|
|
1285
1306
|
prompt,
|
|
@@ -1399,8 +1420,23 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1399
1420
|
});
|
|
1400
1421
|
};
|
|
1401
1422
|
|
|
1423
|
+
// PBRT-6: the single seam that stops this runtime's actor. Stopping a
|
|
1424
|
+
// still-running actor fires one more `@xstate.snapshot` for the *unchanged*
|
|
1425
|
+
// state value with `status: 'stopped'`; `inspect` cannot tell that disposal
|
|
1426
|
+
// artifact from a state entry, so unsuppressed it re-emits the parked
|
|
1427
|
+
// state's telemetry and a phantom self-loop transition. Suppression is a
|
|
1428
|
+
// property of stopping, not a rule each caller must remember — every stop
|
|
1429
|
+
// goes through here so no later site can reintroduce the omission.
|
|
1430
|
+
const stopActor = (): void => {
|
|
1431
|
+
if (!actor) return;
|
|
1432
|
+
suppressInspectionEmissions = true;
|
|
1433
|
+
actor.stop();
|
|
1434
|
+
};
|
|
1435
|
+
|
|
1402
1436
|
const startActor = (): void => {
|
|
1403
1437
|
createRuntimeActor();
|
|
1438
|
+
// A fresh actor's emissions are real state entries again.
|
|
1439
|
+
suppressInspectionEmissions = false;
|
|
1404
1440
|
actor?.start();
|
|
1405
1441
|
};
|
|
1406
1442
|
|
|
@@ -1499,9 +1535,8 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1499
1535
|
// A state that cannot even normalize has no disposal descriptor.
|
|
1500
1536
|
}
|
|
1501
1537
|
}
|
|
1502
|
-
suppressInspectionEmissions = true;
|
|
1503
1538
|
try {
|
|
1504
|
-
|
|
1539
|
+
stopActor();
|
|
1505
1540
|
} catch {
|
|
1506
1541
|
// Preserve the original startup failure.
|
|
1507
1542
|
}
|
|
@@ -1736,7 +1771,7 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1736
1771
|
} else {
|
|
1737
1772
|
await emitBoundaryStatus(event.type, currentState());
|
|
1738
1773
|
if (actor.getSnapshot().status === 'done') {
|
|
1739
|
-
|
|
1774
|
+
stopActor();
|
|
1740
1775
|
startActor();
|
|
1741
1776
|
}
|
|
1742
1777
|
|
|
@@ -1859,9 +1894,7 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1859
1894
|
}
|
|
1860
1895
|
const finalState = currentState();
|
|
1861
1896
|
const failures: unknown[] = [];
|
|
1862
|
-
|
|
1863
|
-
actor.stop();
|
|
1864
|
-
}
|
|
1897
|
+
stopActor();
|
|
1865
1898
|
try {
|
|
1866
1899
|
await drainBoundaryCallsAndEmissions();
|
|
1867
1900
|
} catch (error) {
|
package/slc/gears2fsm.md
CHANGED
|
@@ -193,6 +193,49 @@ Both direct-Captain states additionally receive the universal
|
|
|
193
193
|
`needsBossReply` result. Their guards and actions shall use those exact
|
|
194
194
|
case-sensitive names so the compiled adjudication contract remains stable.
|
|
195
195
|
|
|
196
|
+
For a controller playbook — one whose Source declares DR-029's session-scoped
|
|
197
|
+
controller policy: a session Captain that runs for the whole host session,
|
|
198
|
+
receives every Boss turn, and operates the working playbooks from outside the
|
|
199
|
+
engagement stack (DR-029) — the compiler shall apply the additive
|
|
200
|
+
controller decision-state class below. The class joins the stable compiler
|
|
201
|
+
contract beside the decide-call-observe vocabulary above; that vocabulary and
|
|
202
|
+
the universal `needsBossReply` rule stay untouched for the artifacts that
|
|
203
|
+
consume them.
|
|
204
|
+
The controller machine shall be a session loop, not a finite errand: a
|
|
205
|
+
quiescent conversational hub (tag `playbook.parked`) receives each Boss turn;
|
|
206
|
+
the controller decision state decides it over the closed action set; a
|
|
207
|
+
`respond` selection settles its turn in the decision call itself, its
|
|
208
|
+
validated `text` being the turn's captain speech; an acting selection's host
|
|
209
|
+
settlement becomes the outcome report grounding one closing-reply call; and
|
|
210
|
+
the machine returns to the hub for the next turn. Returning to the hub after
|
|
211
|
+
a settled turn completes the session loop's turn; it is not the idle-hub
|
|
212
|
+
routing that [Transitions](#transitions) reserves for recovery.
|
|
213
|
+
Because the hub receives every Boss turn, a controller state carries no
|
|
214
|
+
Boss-reply suspension: the compiler shall not add `needsBossReply` to a
|
|
215
|
+
controller machine's `invoke.input.result` maps — a clarifying question to
|
|
216
|
+
Boss is a `respond` selection.
|
|
217
|
+
The machine shall declare no terminal result output and shall keep exactly
|
|
218
|
+
one reachable `type: 'final'` shutdown state entered only by the host's
|
|
219
|
+
teardown event. The completion rule of
|
|
220
|
+
[Errors and termination](#errors-and-termination) applies unamended: its
|
|
221
|
+
output clause binds only where Source declares a terminal result, which a
|
|
222
|
+
controller Source does not.
|
|
223
|
+
The controller decision state's direct-Captain result contract discriminates
|
|
224
|
+
the closed action set of DR-029. Its guard discriminants are a stable
|
|
225
|
+
compiler contract, not names the compiler may invent — `respond`, `start`,
|
|
226
|
+
`switch`, `dismiss`, `deliver`, and `runtime` — with each guard's required
|
|
227
|
+
payload fields:
|
|
228
|
+
|
|
229
|
+
- `respond` requires `text`;
|
|
230
|
+
- `start` and `switch` each require `playbookId` and `input`;
|
|
231
|
+
- `runtime` requires `actionId`;
|
|
232
|
+
- `dismiss` and `deliver` require none — a `deliver` result in particular
|
|
233
|
+
carries no text payload: the host is authoritative for the delivered text,
|
|
234
|
+
so the contract declares no field for it.
|
|
235
|
+
|
|
236
|
+
The decision state's guards and actions shall use those exact case-sensitive
|
|
237
|
+
names so the compiled controller contract remains stable.
|
|
238
|
+
|
|
196
239
|
## States
|
|
197
240
|
|
|
198
241
|
Each state shall declare:
|
|
@@ -588,11 +631,20 @@ cannot supply alone, the machine shall suspend that task in a quiescent wait
|
|
|
588
631
|
state and resume the same task with the Q+A in the next prompt.
|
|
589
632
|
This is a third Boss surface alongside `BOSS_INTERRUPT` and Boss entry events.
|
|
590
633
|
|
|
591
|
-
Every captain- and player-invoking state supports this path
|
|
634
|
+
Every captain- and player-invoking state supports this path, with one
|
|
635
|
+
exception the compiler shall apply, not infer: the states of a controller
|
|
636
|
+
machine ([Setup](#setup), controller decision-state class) carry no
|
|
637
|
+
Boss-reply suspension, because its hub already receives every Boss turn and a
|
|
638
|
+
clarifying question to Boss is a `respond` selection over the closed action
|
|
639
|
+
set. The rule below is therefore universal over workflow states and silent
|
|
640
|
+
about that class; in particular, adding `needsBossReply` to the controller
|
|
641
|
+
decision state would add a seventh outcome to a closed six-action contract
|
|
642
|
+
whose guard discriminants [Setup](#setup) fixes, and is nonconformant.
|
|
592
643
|
There is no source-level opt-in annotation and no `needsBossReply` result metadata in GEARS output.
|
|
593
644
|
The FSM compiler shall preserve the GEARS blockquote as the state's domain `prompt` body and shall not inject any Boss-question instruction into `invoke.input.prompt`.
|
|
594
645
|
|
|
595
|
-
For every captain- and player-invoking state
|
|
646
|
+
For every captain- and player-invoking state outside a controller machine,
|
|
647
|
+
the compiler shall add
|
|
596
648
|
`needsBossReply` to the state's `invoke.input.result` map.
|
|
597
649
|
The description shall be the standard adjudicator-facing text:
|
|
598
650
|
|