@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.
Files changed (31) hide show
  1. package/README.md +64 -99
  2. package/docs/assets/playbook-venn.svg +13 -0
  3. package/docs/cli.md +83 -9
  4. package/docs/configuration.md +5 -3
  5. package/package.json +7 -4
  6. package/reference/sdlc/captain.md +70 -83
  7. package/reference/sdlc/captain.playbook/captain.fsm.d.ts +127 -142
  8. package/reference/sdlc/captain.playbook/captain.fsm.js +349 -470
  9. package/reference/sdlc/captain.playbook/captain.fsm.ts +535 -598
  10. package/reference/sdlc/captain.playbook/captain.gears.md +37 -41
  11. package/reference/sdlc/captain.playbook/captain.playbook.d.ts +90 -15
  12. package/reference/sdlc/captain.playbook/captain.playbook.js +464 -968
  13. package/reference/sdlc/captain.playbook/captain.playbook.ts +696 -993
  14. package/reference/sdlc/code.playbook/bin/adapter-sdk.js +247 -0
  15. package/reference/sdlc/code.playbook/bin/playbook.js +54 -9
  16. package/reference/sdlc/code.playbook/bin/run.js +97 -0
  17. package/reference/sdlc/code.playbook/code.playbook.js +17 -0
  18. package/reference/sdlc/code.playbook/code.playbook.ts +17 -0
  19. package/reference/sdlc/code.playbook/playbook-captain.d.ts +2 -0
  20. package/reference/sdlc/code.playbook/playbook-captain.js +1784 -215
  21. package/reference/sdlc/code.playbook/playbook-captain.ts +2293 -330
  22. package/reference/sdlc/code.playbook/playbook.config.template.yaml +7 -0
  23. package/reference/sdlc/discuss.playbook/discuss.playbook.js +41 -9
  24. package/reference/sdlc/discuss.playbook/discuss.playbook.ts +42 -9
  25. package/slc/gears2fsm.md +54 -2
  26. package/slc/link.md +293 -25
  27. package/src/runtime.d.ts +29 -1
  28. package/src/runtime.ts +47 -0
  29. package/src/xstate-playbook-runtime.d.ts +97 -5
  30. package/src/xstate-playbook-runtime.js +769 -29
  31. 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
- const { playerId, result } = await callPlayer(input, combined);
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
- if (result.finalText === undefined) {
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, result.finalText);
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
- actor?.stop();
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
- actor.stop();
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
- if (actor) {
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
- const { playerId, result } = await callPlayer(input, combined);
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
- if (result.finalText === undefined) {
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, result.finalText);
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
- actor?.stop();
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
- actor.stop();
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
- if (actor) {
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, the compiler shall add
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