@sublang/playbook 10.0.0 → 12.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 (59) hide show
  1. package/README.md +1 -1
  2. package/docs/cli.md +67 -19
  3. package/docs/configuration.md +103 -40
  4. package/docs/embedding.md +88 -0
  5. package/package.json +30 -4
  6. package/reference/sdlc/code.md +35 -15
  7. package/reference/sdlc/code.playbook/bin/interactive-session.js +58 -6
  8. package/reference/sdlc/code.playbook/bin/launch-config.js +499 -241
  9. package/reference/sdlc/code.playbook/bin/playbook.js +236 -187
  10. package/reference/sdlc/code.playbook/bin/replay-observer.js +221 -0
  11. package/reference/sdlc/code.playbook/bin/run.js +355 -203
  12. package/reference/sdlc/code.playbook/bin/session-store.js +1512 -136
  13. package/reference/sdlc/code.playbook/code.fsm.d.ts +22 -19
  14. package/reference/sdlc/code.playbook/code.fsm.js +116 -52
  15. package/reference/sdlc/code.playbook/code.fsm.ts +149 -64
  16. package/reference/sdlc/code.playbook/code.gears.md +40 -20
  17. package/reference/sdlc/code.playbook/code.playbook.js +23 -2
  18. package/reference/sdlc/code.playbook/code.playbook.ts +23 -2
  19. package/reference/sdlc/code.playbook/playbook-captain.d.ts +4 -1
  20. package/reference/sdlc/code.playbook/playbook-captain.js +21 -3
  21. package/reference/sdlc/code.playbook/playbook-captain.ts +42 -6
  22. package/reference/sdlc/code.playbook/playbook.config.template.yaml +31 -11
  23. package/reference/sdlc/code.playbook/session-store.d.ts +82 -0
  24. package/reference/sdlc/code.playbook/session-store.js +113 -0
  25. package/reference/sdlc/decide.md +24 -15
  26. package/reference/sdlc/decide.playbook/decide.fsm.d.ts +13 -6
  27. package/reference/sdlc/decide.playbook/decide.fsm.js +54 -27
  28. package/reference/sdlc/decide.playbook/decide.fsm.ts +68 -29
  29. package/reference/sdlc/decide.playbook/decide.gears.md +25 -19
  30. package/reference/sdlc/decide.playbook/decide.playbook.js +11 -3
  31. package/reference/sdlc/decide.playbook/decide.playbook.ts +11 -3
  32. package/reference/sdlc/decide.playbook/decide.registry.js +1 -1
  33. package/reference/sdlc/decide.playbook/decide.registry.ts +1 -1
  34. package/reference/sdlc/dev.md +52 -0
  35. package/reference/sdlc/dev.playbook/dev.fsm.d.ts +261 -0
  36. package/reference/sdlc/dev.playbook/dev.fsm.js +723 -0
  37. package/reference/sdlc/dev.playbook/dev.fsm.ts +988 -0
  38. package/reference/sdlc/dev.playbook/dev.gears.md +91 -0
  39. package/reference/sdlc/dev.playbook/dev.playbook.d.ts +21 -0
  40. package/reference/sdlc/dev.playbook/dev.playbook.js +143 -0
  41. package/reference/sdlc/dev.playbook/dev.playbook.ts +246 -0
  42. package/reference/sdlc/dev.playbook/dev.registry.d.ts +40 -0
  43. package/reference/sdlc/dev.playbook/dev.registry.js +64 -0
  44. package/reference/sdlc/dev.playbook/dev.registry.ts +120 -0
  45. package/reference/sdlc/review.md +36 -18
  46. package/reference/sdlc/review.playbook/review.fsm.d.ts +15 -2
  47. package/reference/sdlc/review.playbook/review.fsm.js +77 -27
  48. package/reference/sdlc/review.playbook/review.fsm.ts +96 -30
  49. package/reference/sdlc/review.playbook/review.gears.md +52 -26
  50. package/reference/sdlc/review.playbook/review.playbook.js +17 -7
  51. package/reference/sdlc/review.playbook/review.playbook.ts +17 -7
  52. package/reference/sdlc/review.playbook/review.registry.js +1 -1
  53. package/reference/sdlc/review.playbook/review.registry.ts +1 -1
  54. package/slc/link.md +12 -5
  55. package/slc/text2gears.md +3 -0
  56. package/src/xstate-playbook-runtime.js +5 -2
  57. package/src/xstate-playbook-runtime.ts +5 -2
  58. package/src/xstate-runtime.js +13 -1
  59. package/src/xstate-runtime.ts +13 -1
@@ -6,7 +6,8 @@
6
6
  // Role metadata: Coder→coder (canonical local role)
7
7
  // Boss event: deterministic START_CODE entry; exact Boss text becomes
8
8
  // callerInput; pending Coder questions retain BOSS_REPLY
9
- // Adjudication: LLM judge per player state; coderOutput is carried verbatim
9
+ // Adjudication: LLM judge per player state; coderOutput is carried verbatim;
10
+ // the first phase decides among four semantic outcomes
10
11
  // Nested call: literal review target through the shared bridge
11
12
  // Compat: artifact schema 3 / runtime ABI 1
12
13
 
@@ -195,7 +196,7 @@ const runtimeSpec = {
195
196
  runFirstPhase: {
196
197
  role: 'coder',
197
198
  label:
198
- 'Coder is implementing one direct phase or committing a new intent record.',
199
+ 'Coder is running the first coding phase: a direct implementation, a new intent record, or an existing intent-record task.',
199
200
  },
200
201
  runIrTask: {
201
202
  role: 'coder',
@@ -213,6 +214,23 @@ const runtimeSpec = {
213
214
  repositoryDisposition: 'one-descendant-commit',
214
215
  },
215
216
  irCommit: {
217
+ fields: {
218
+ coderOutput: 'presentation',
219
+ latestCommit: 'effect',
220
+ irNumber: 'semantic',
221
+ },
222
+ repositoryDisposition: 'one-descendant-commit',
223
+ },
224
+ moreTasks: {
225
+ fields: {
226
+ coderOutput: 'presentation',
227
+ latestCommit: 'effect',
228
+ irNumber: 'semantic',
229
+ irTask: 'semantic',
230
+ },
231
+ repositoryDisposition: 'one-descendant-commit',
232
+ },
233
+ finalTask: {
216
234
  fields: {
217
235
  coderOutput: 'presentation',
218
236
  latestCommit: 'effect',
@@ -231,6 +249,7 @@ const runtimeSpec = {
231
249
  fields: {
232
250
  coderOutput: 'presentation',
233
251
  latestCommit: 'effect',
252
+ irNumber: 'semantic',
234
253
  irTask: 'semantic',
235
254
  },
236
255
  repositoryDisposition: 'one-descendant-commit',
@@ -239,6 +258,8 @@ const runtimeSpec = {
239
258
  fields: {
240
259
  coderOutput: 'presentation',
241
260
  latestCommit: 'effect',
261
+ irNumber: 'semantic',
262
+ irTask: 'semantic',
242
263
  },
243
264
  repositoryDisposition: 'one-descendant-commit',
244
265
  },
@@ -7,6 +7,9 @@ interface SessionAgent {
7
7
  readonly adapter: string;
8
8
  readonly model: TuningSelection;
9
9
  readonly effort: TuningSelection<Effort>;
10
+ /** Adapter-scoped fast mode. Absence is the provider default; `false` is a
11
+ * literal request, so this carries no provider-default sentinel. */
12
+ readonly fastMode?: boolean;
10
13
  readonly instruction?: string;
11
14
  readonly permissions?: PermissionPolicy;
12
15
  }
@@ -29,7 +32,7 @@ interface PlaybookCaptainUnresolvedEffectSettlementInput {
29
32
  readonly rootPlaybookId: string;
30
33
  readonly unresolvedEffects: readonly PlaybookCaptainUnresolvedEffect[];
31
34
  }
32
- type SnapshotAgentEnvelope = DeepReadonly<Omit<SessionAgent, 'model' | 'effort'>>;
35
+ type SnapshotAgentEnvelope = DeepReadonly<Omit<SessionAgent, 'model' | 'effort' | 'fastMode'>>;
33
36
  type PlayerLedgerSnapshotEntry = DeepReadonly<PlayerLedgerEntry>;
34
37
  export interface PlaybookCaptainDeps {
35
38
  loadModule?: (specifier: string) => Promise<unknown>;
@@ -1379,10 +1379,16 @@ function snapshotEffortSelection(value, path) {
1379
1379
  }
1380
1380
  return selection;
1381
1381
  }
1382
+ function snapshotFastMode(value, path) {
1383
+ if (value !== undefined && typeof value !== 'boolean') {
1384
+ throw new TypeError(`${path} must be a boolean`);
1385
+ }
1386
+ return value;
1387
+ }
1382
1388
  function snapshotSessionAgent(value, path) {
1383
1389
  const agent = snapshotRecord(value, path);
1384
- rejectSnapshotKeys(agent, ['adapter', 'model', 'effort', 'instruction', 'permissions'], path);
1385
- const fixed = snapshotFixedAgent(Object.fromEntries(Object.entries(agent).filter(([key]) => key !== 'model' && key !== 'effort')), path);
1390
+ rejectSnapshotKeys(agent, ['adapter', 'model', 'effort', 'fastMode', 'instruction', 'permissions'], path);
1391
+ const fixed = snapshotFixedAgent(Object.fromEntries(Object.entries(agent).filter(([key]) => key !== 'model' && key !== 'effort' && key !== 'fastMode')), path);
1386
1392
  return {
1387
1393
  adapter: fixed.adapter,
1388
1394
  ...(fixed.instruction === undefined
@@ -1393,6 +1399,9 @@ function snapshotSessionAgent(value, path) {
1393
1399
  : { permissions: livePermissions(fixed.permissions) }),
1394
1400
  model: snapshotTuningSelection(agent.model, `${path}.model`),
1395
1401
  effort: snapshotEffortSelection(agent.effort, `${path}.effort`),
1402
+ ...(agent.fastMode === undefined
1403
+ ? {}
1404
+ : { fastMode: snapshotFastMode(agent.fastMode, `${path}.fastMode`) }),
1396
1405
  };
1397
1406
  }
1398
1407
  function fixedAgent(agent) {
@@ -1403,9 +1412,13 @@ function fixedAgent(agent) {
1403
1412
  };
1404
1413
  }
1405
1414
  function callSettings(agent, tuning = agent) {
1415
+ // cligent treats supplied call settings as a complete replacement, so an
1416
+ // omitted fastMode here is a request for the provider default, never an
1417
+ // inheritance of whatever the previous call left behind.
1406
1418
  return {
1407
1419
  model: tuning.model,
1408
1420
  effort: tuning.effort,
1421
+ ...(tuning.fastMode === undefined ? {} : { fastMode: tuning.fastMode }),
1409
1422
  ...(agent.instruction === undefined ? {} : { instruction: agent.instruction }),
1410
1423
  ...(agent.permissions === undefined ? {} : { permissions: agent.permissions }),
1411
1424
  };
@@ -1533,7 +1546,7 @@ async function buildEnablements(options, loadModule, hostCapabilities) {
1533
1546
  for (const role of entry.requiredRoleIds) {
1534
1547
  const path = `captain.options.playbooks.${id}.roles.${role}`;
1535
1548
  const rawBinding = snapshotRecord(roleRecord[role], path);
1536
- rejectSnapshotKeys(rawBinding, ['playerId', 'model', 'effort'], path);
1549
+ rejectSnapshotKeys(rawBinding, ['playerId', 'model', 'effort', 'fastMode'], path);
1537
1550
  const playerId = snapshotString(rawBinding.playerId, `${path}.playerId`);
1538
1551
  if (!PLAYER_ID_PATTERN.test(playerId) || playerId === INTERNAL_CAPTAIN_ID) {
1539
1552
  throw new Error(`${path}.playerId is not a canonical player id`);
@@ -1546,6 +1559,11 @@ async function buildEnablements(options, loadModule, hostCapabilities) {
1546
1559
  playerId,
1547
1560
  model: snapshotTuningSelection(rawBinding.model, `${path}.model`),
1548
1561
  effort: snapshotEffortSelection(rawBinding.effort, `${path}.effort`),
1562
+ ...(rawBinding.fastMode === undefined
1563
+ ? {}
1564
+ : {
1565
+ fastMode: snapshotFastMode(rawBinding.fastMode, `${path}.fastMode`),
1566
+ }),
1549
1567
  agent,
1550
1568
  });
1551
1569
  }
@@ -55,6 +55,9 @@ interface SessionAgent {
55
55
  readonly adapter: string;
56
56
  readonly model: TuningSelection;
57
57
  readonly effort: TuningSelection<Effort>;
58
+ /** Adapter-scoped fast mode. Absence is the provider default; `false` is a
59
+ * literal request, so this carries no provider-default sentinel. */
60
+ readonly fastMode?: boolean;
58
61
  readonly instruction?: string;
59
62
  readonly permissions?: PermissionPolicy;
60
63
  }
@@ -94,7 +97,7 @@ interface PlaybookCaptainUnresolvedEffectSettlementInput {
94
97
  }
95
98
 
96
99
  type SnapshotAgentEnvelope = DeepReadonly<
97
- Omit<SessionAgent, 'model' | 'effort'>
100
+ Omit<SessionAgent, 'model' | 'effort' | 'fastMode'>
98
101
  >;
99
102
 
100
103
  type PlayerLedgerSnapshotEntry = DeepReadonly<PlayerLedgerEntry>;
@@ -377,6 +380,7 @@ interface EffectivePlayerBinding {
377
380
  readonly playerId: string;
378
381
  readonly model: TuningSelection;
379
382
  readonly effort: TuningSelection<Effort>;
383
+ readonly fastMode?: boolean;
380
384
  readonly agent: SessionAgent;
381
385
  }
382
386
 
@@ -2562,6 +2566,16 @@ function snapshotEffortSelection(
2562
2566
  return selection as TuningSelection<Effort>;
2563
2567
  }
2564
2568
 
2569
+ function snapshotFastMode(
2570
+ value: JsonValue | undefined,
2571
+ path: string,
2572
+ ): boolean | undefined {
2573
+ if (value !== undefined && typeof value !== 'boolean') {
2574
+ throw new TypeError(`${path} must be a boolean`);
2575
+ }
2576
+ return value;
2577
+ }
2578
+
2565
2579
  function snapshotSessionAgent(
2566
2580
  value: JsonValue | undefined,
2567
2581
  path: string,
@@ -2569,13 +2583,14 @@ function snapshotSessionAgent(
2569
2583
  const agent = snapshotRecord(value, path);
2570
2584
  rejectSnapshotKeys(
2571
2585
  agent,
2572
- ['adapter', 'model', 'effort', 'instruction', 'permissions'],
2586
+ ['adapter', 'model', 'effort', 'fastMode', 'instruction', 'permissions'],
2573
2587
  path,
2574
2588
  );
2575
2589
  const fixed = snapshotFixedAgent(
2576
2590
  Object.fromEntries(
2577
2591
  Object.entries(agent).filter(
2578
- ([key]) => key !== 'model' && key !== 'effort',
2592
+ ([key]) =>
2593
+ key !== 'model' && key !== 'effort' && key !== 'fastMode',
2579
2594
  ),
2580
2595
  ) as JsonValue,
2581
2596
  path,
@@ -2590,10 +2605,15 @@ function snapshotSessionAgent(
2590
2605
  : { permissions: livePermissions(fixed.permissions) }),
2591
2606
  model: snapshotTuningSelection(agent.model, `${path}.model`),
2592
2607
  effort: snapshotEffortSelection(agent.effort, `${path}.effort`),
2608
+ ...(agent.fastMode === undefined
2609
+ ? {}
2610
+ : { fastMode: snapshotFastMode(agent.fastMode, `${path}.fastMode`) }),
2593
2611
  };
2594
2612
  }
2595
2613
 
2596
- function fixedAgent(agent: SessionAgent): Omit<SessionAgent, 'model' | 'effort'> {
2614
+ function fixedAgent(
2615
+ agent: SessionAgent,
2616
+ ): Omit<SessionAgent, 'model' | 'effort' | 'fastMode'> {
2597
2617
  return {
2598
2618
  adapter: agent.adapter,
2599
2619
  ...(agent.instruction === undefined ? {} : { instruction: agent.instruction }),
@@ -2603,11 +2623,15 @@ function fixedAgent(agent: SessionAgent): Omit<SessionAgent, 'model' | 'effort'>
2603
2623
 
2604
2624
  function callSettings(
2605
2625
  agent: SessionAgent,
2606
- tuning: Pick<SessionAgent, 'model' | 'effort'> = agent,
2626
+ tuning: Pick<SessionAgent, 'model' | 'effort' | 'fastMode'> = agent,
2607
2627
  ): AgentCallSettings {
2628
+ // cligent treats supplied call settings as a complete replacement, so an
2629
+ // omitted fastMode here is a request for the provider default, never an
2630
+ // inheritance of whatever the previous call left behind.
2608
2631
  return {
2609
2632
  model: tuning.model,
2610
2633
  effort: tuning.effort,
2634
+ ...(tuning.fastMode === undefined ? {} : { fastMode: tuning.fastMode }),
2611
2635
  ...(agent.instruction === undefined ? {} : { instruction: agent.instruction }),
2612
2636
  ...(agent.permissions === undefined ? {} : { permissions: agent.permissions }),
2613
2637
  };
@@ -2833,7 +2857,11 @@ async function buildEnablements(
2833
2857
  for (const role of entry.requiredRoleIds) {
2834
2858
  const path = `captain.options.playbooks.${id}.roles.${role}`;
2835
2859
  const rawBinding = snapshotRecord(roleRecord[role], path);
2836
- rejectSnapshotKeys(rawBinding, ['playerId', 'model', 'effort'], path);
2860
+ rejectSnapshotKeys(
2861
+ rawBinding,
2862
+ ['playerId', 'model', 'effort', 'fastMode'],
2863
+ path,
2864
+ );
2837
2865
  const playerId = snapshotString(rawBinding.playerId, `${path}.playerId`);
2838
2866
  if (!PLAYER_ID_PATTERN.test(playerId) || playerId === INTERNAL_CAPTAIN_ID) {
2839
2867
  throw new Error(`${path}.playerId is not a canonical player id`);
@@ -2848,6 +2876,14 @@ async function buildEnablements(
2848
2876
  playerId,
2849
2877
  model: snapshotTuningSelection(rawBinding.model, `${path}.model`),
2850
2878
  effort: snapshotEffortSelection(rawBinding.effort, `${path}.effort`),
2879
+ ...(rawBinding.fastMode === undefined
2880
+ ? {}
2881
+ : {
2882
+ fastMode: snapshotFastMode(
2883
+ rawBinding.fastMode,
2884
+ `${path}.fastMode`,
2885
+ ),
2886
+ }),
2851
2887
  agent,
2852
2888
  });
2853
2889
  }
@@ -8,9 +8,10 @@
8
8
 
9
9
  # The Captain and each stable session player carries its own defaults inline:
10
10
  # an adapter shorthand (claude, codex) or a block with
11
- # adapter/model/effort/permissions. A role may override only model or effort.
12
- # Omit an override to inherit the player default; set it to false to select
13
- # that provider's current default explicitly.
11
+ # adapter/model/effort/fastMode/permissions. A role may override only model,
12
+ # effort, or fastMode.
13
+ # Omit an override to inherit the player default. For model and effort, false
14
+ # selects the provider default; fastMode false is a literal disabled request.
14
15
 
15
16
  # Each adapter needs its vendor SDK installed as its own top-level
16
17
  # install root — they are optional peer dependencies, so you pay only
@@ -29,7 +30,7 @@
29
30
  # list, so those captains degrade to a prompt-level restriction (DR-013 A1).
30
31
  captain:
31
32
  adapter: claude
32
- model: claude-opus-4-8
33
+ model: claude-opus-5
33
34
  effort: high
34
35
  permissions:
35
36
  mode: auto
@@ -45,27 +46,41 @@ notifications:
45
46
  # shares one provider conversation across those playbooks.
46
47
  players:
47
48
  dev.coder:
49
+ adapter: codex
50
+ model: gpt-5.6-sol
51
+ effort: ultra
52
+ # Adapter-scoped fast mode is validated by the installed Cligent contract;
53
+ # omitting it takes the provider default.
54
+ fastMode: true
55
+ permissions:
56
+ mode: auto
57
+ writablePaths: ['.git']
58
+
59
+ dev.reviewer:
48
60
  adapter: claude
49
- model: claude-opus-4-8[1m]
61
+ model: claude-opus-5
50
62
  effort: xhigh
51
63
  permissions:
52
64
  mode: auto
53
65
 
54
- dev.reviewer:
55
- adapter: codex
56
- model: gpt-5.5
66
+ # DEV's planner stays a distinct player: equal ids deliberately share one
67
+ # provider conversation, and planning context must not bleed into review
68
+ # conversations (DR-044).
69
+ dev.analyst:
70
+ adapter: claude
71
+ model: claude-opus-5
57
72
  effort: xhigh
58
73
  permissions:
59
74
  mode: auto
60
- writablePaths: ['.git']
61
75
 
62
76
  # Enabled playbooks. Each is loaded from its explicit `from` module; the
63
77
  # `<id>` key must equal that module's manifest id. `from`, `command`, and
64
78
  # `roles` are launcher-owned; every other key is that playbook's option
65
79
  # slice.
66
80
  #
67
- # Every manifest role is bound explicitly. These three playbooks intentionally
68
- # reuse the same two stable players; change an id to isolate its conversation.
81
+ # Every manifest role is bound explicitly. CODE, REVIEW, and DECIDE
82
+ # intentionally reuse the same two stable players, while DEV keeps its own
83
+ # Analyst; change an id to isolate its conversation.
69
84
  playbooks:
70
85
  code:
71
86
  from: "@sublang/playbook/code/registry"
@@ -83,3 +98,8 @@ playbooks:
83
98
  roles:
84
99
  coder: dev.coder
85
100
  reviewer: dev.reviewer
101
+
102
+ dev:
103
+ from: "@sublang/playbook/dev/registry"
104
+ roles:
105
+ analyst: dev.analyst
@@ -0,0 +1,82 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // SPDX-FileCopyrightText: 2026 SubLang International <https://sublang.ai>
3
+
4
+ export declare const RECORDS_STREAM_VERSION: 1;
5
+ export declare function defaultSessionsDir(): string;
6
+ export declare function openSessionStore(
7
+ sessionsDir: string,
8
+ ): PlaybookSessionStore;
9
+
10
+ export type ReplayJsonValue =
11
+ | null
12
+ | boolean
13
+ | number
14
+ | string
15
+ | readonly ReplayJsonValue[]
16
+ | ReplayRecord;
17
+ export type ReplayRecord = {
18
+ readonly [key: string]: ReplayJsonValue;
19
+ };
20
+ export interface ReplayStreamEntry {
21
+ readonly v: 1;
22
+ readonly seq: number;
23
+ readonly role?: string;
24
+ readonly record: ReplayRecord;
25
+ }
26
+ export interface ReplayStreamReadOptions {
27
+ readonly afterSeq?: number;
28
+ }
29
+ export interface ReplayStreamReadResult {
30
+ readonly entries: readonly ReplayStreamEntry[];
31
+ readonly lastReadableSeq: number;
32
+ }
33
+ export interface LeaseReplayStreamReadResult extends ReplayStreamReadResult {
34
+ readonly lastDurableSeq: number;
35
+ readonly incomplete: boolean;
36
+ }
37
+ export type ReplayStreamStatus =
38
+ | {
39
+ readonly lastReadableSeq: number;
40
+ readonly lastDurableSeq: number;
41
+ readonly incomplete: boolean;
42
+ }
43
+ | {
44
+ readonly lastReadableSeq: null;
45
+ readonly lastDurableSeq: null;
46
+ readonly incomplete: true;
47
+ };
48
+ export interface PlaybookSessionSummary {
49
+ readonly schemaVersion: number;
50
+ readonly sessionId: string;
51
+ readonly state: 'settled' | 'uncertain';
52
+ readonly cwd: string;
53
+ readonly updatedAt: string;
54
+ }
55
+ export interface SkippedPlaybookSession {
56
+ readonly sessionId: string;
57
+ readonly reason: string;
58
+ }
59
+ export interface PlaybookSessionListResult {
60
+ readonly sessions: readonly PlaybookSessionSummary[];
61
+ readonly skipped: readonly SkippedPlaybookSession[];
62
+ }
63
+ export interface PlaybookSessionStore {
64
+ readonly sessionsDir: string;
65
+ list(): Promise<PlaybookSessionListResult>;
66
+ read(sessionId: string): Promise<PlaybookSessionSummary>;
67
+ readStream(
68
+ sessionId: string,
69
+ options?: ReplayStreamReadOptions,
70
+ ): Promise<ReplayStreamReadResult>;
71
+ acquire(sessionId: string): Promise<PlaybookSessionLease>;
72
+ }
73
+ export interface PlaybookSessionLease {
74
+ readonly sessionId: string;
75
+ readonly ownerToken: string;
76
+ append(record: object, role?: string): Promise<void>;
77
+ readStream(
78
+ options?: ReplayStreamReadOptions,
79
+ ): Promise<LeaseReplayStreamReadResult>;
80
+ streamStatus(): ReplayStreamStatus;
81
+ release(): Promise<ReplayStreamStatus>;
82
+ }
@@ -0,0 +1,113 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // SPDX-FileCopyrightText: 2026 SubLang International <https://sublang.ai>
3
+
4
+ import {
5
+ RECORDS_STREAM_VERSION,
6
+ createCaptainSessionStore,
7
+ defaultCaptainSessionsDir,
8
+ } from './bin/session-store.js';
9
+
10
+ export { RECORDS_STREAM_VERSION };
11
+
12
+ export function defaultSessionsDir() {
13
+ return defaultCaptainSessionsDir();
14
+ }
15
+
16
+ export function openSessionStore(sessionsDir) {
17
+ if (typeof sessionsDir !== 'string') {
18
+ throw new TypeError('session store path must be a string');
19
+ }
20
+ return wrapStore(createCaptainSessionStore({ sessionsDir }));
21
+ }
22
+
23
+ function wrapStore(store) {
24
+ return Object.freeze({
25
+ sessionsDir: store.sessionsDir,
26
+ list: async () => projectListResult(await store.listSummaries()),
27
+ read: async (sessionId) =>
28
+ projectSummary(await store.readSummary(sessionId)),
29
+ readStream: async (sessionId, options) =>
30
+ projectReadResult(await store.readStream(sessionId, options)),
31
+ acquire: async (sessionId) => wrapLease(await store.acquire(sessionId)),
32
+ });
33
+ }
34
+
35
+ function wrapLease(lease) {
36
+ let finalStatus;
37
+ let releaseInFlight;
38
+
39
+ const release = () => {
40
+ if (finalStatus !== undefined) return Promise.resolve(finalStatus);
41
+ if (releaseInFlight !== undefined) return releaseInFlight;
42
+ const operation = lease.release();
43
+ releaseInFlight = operation.then(
44
+ (status) => {
45
+ finalStatus = projectStatus(status);
46
+ return finalStatus;
47
+ },
48
+ (cause) => {
49
+ releaseInFlight = undefined;
50
+ throw cause;
51
+ },
52
+ );
53
+ return releaseInFlight;
54
+ };
55
+
56
+ return Object.freeze({
57
+ sessionId: lease.sessionId,
58
+ ownerToken: lease.ownerToken,
59
+ append: (record, role) => lease.append(record, role),
60
+ readStream: async (options) => {
61
+ if (finalStatus !== undefined) {
62
+ throw new Error('released session lease has no live replay reader');
63
+ }
64
+ return projectLeaseReadResult(await lease.readStream(options));
65
+ },
66
+ streamStatus: () => finalStatus ?? projectStatus(lease.streamStatus()),
67
+ release,
68
+ });
69
+ }
70
+
71
+ function projectListResult(result) {
72
+ return Object.freeze({
73
+ sessions: Object.freeze(result.sessions.map(projectSummary)),
74
+ skipped: Object.freeze(
75
+ result.skipped.map(({ sessionId, reason }) =>
76
+ Object.freeze({ sessionId, reason }),
77
+ ),
78
+ ),
79
+ });
80
+ }
81
+
82
+ function projectSummary(summary) {
83
+ return Object.freeze({
84
+ schemaVersion: summary.schemaVersion,
85
+ sessionId: summary.sessionId,
86
+ state: summary.state,
87
+ cwd: summary.cwd,
88
+ updatedAt: summary.updatedAt,
89
+ });
90
+ }
91
+
92
+ function projectReadResult(result) {
93
+ return Object.freeze({
94
+ entries: Object.freeze([...result.entries]),
95
+ lastReadableSeq: result.lastReadableSeq,
96
+ });
97
+ }
98
+
99
+ function projectLeaseReadResult(result) {
100
+ return Object.freeze({
101
+ ...projectReadResult(result),
102
+ lastDurableSeq: result.lastDurableSeq,
103
+ incomplete: result.incomplete,
104
+ });
105
+ }
106
+
107
+ function projectStatus(status) {
108
+ return Object.freeze({
109
+ lastReadableSeq: status.lastReadableSeq,
110
+ lastDurableSeq: status.lastDurableSeq,
111
+ incomplete: status.incomplete,
112
+ });
113
+ }
@@ -23,31 +23,40 @@ Consult @specs/map.md for relevant context and @specs/meta.md for spec requireme
23
23
  Do not change any files.
24
24
  ```
25
25
 
26
- Neither role's player shall receive the other role's proposal until both proposals are complete and Coder has committed Coder's own proposal.
26
+ Neither role's player shall receive the other role's proposal until both proposals are complete.
27
+ Each proposal is complete only when its player affirmatively provides a complete design proposal.
28
+ A progress report, status update, or promise of a later proposal supports no proposal outcome.
27
29
  A Boss interrupt during the parallel proposal pair shall restart the whole pair so both players receive the same new topic and remain independent.
28
30
 
29
- When both proposals are complete, Captain shall then give Coder the following instruction:
31
+ When both proposals are complete, Captain shall give Coder the following instruction, with the complete topic and Reviewer's complete proposal inserted under their own labels in quotes (`>`):
30
32
 
31
33
  ```markdown
32
- Turn your proposal into the necessary spec items or DRs.
34
+ Synthesize your independent proposal with Reviewer's proposal below.
35
+ Keep to the original topic below and follow what it asks.
36
+ Keep the best, essential parts of either proposal and reject any point that is unsound, unnecessary, or outside the topic.
37
+ Turn the resulting design into the necessary DRs and/or spec items.
33
38
  Follow @specs/meta.md and update @specs/map.md when needed.
34
- Do not inspect or incorporate Reviewer's proposal before this commit.
35
- Do not change code or implement the proposal.
39
+ Do not change code or implement the design.
36
40
 
37
41
  Commit the result as one new commit, following @specs/packages/git.md.
38
42
  Make the commit message explain concisely what changed and why.
39
- Coder is <coder-llm>; format the model token in conventional human form.
43
+ Identify every new commit you make.
44
+ Coder is <coder-llm> and Reviewer is <reviewer-llm>.
45
+
46
+ > Original topic: \<caller-topic\>
47
+ > Reviewer's independent proposal: \<reviewer-proposal\>
40
48
  ```
41
49
 
42
- After Coder commits, Captain shall call playbook `review`:
50
+ No transition shall depend on a fixed presentation format of either player's reply.
51
+ Captain shall use the repository-effect receipt as the authoritative identity of Coder's new `decide`-owned commit.
52
+
53
+ After Coder commits, Captain shall call playbook `review` with the following input in quotes (`>`):
43
54
 
44
- > Review the latest commit as a spec-design change against the initial intent.
45
- > Compare it with your independent proposal and take the best of both.
46
- > Make your suggestions.
47
- >
48
- > Initial intent: \<caller-topic\>.
49
- > Coder's independent proposal: \<coder-proposal\>.
55
+ > Original intent: \<caller-topic\>
56
+ > Review scope: the `decide`-owned commit \<decide-commit\> and its resulting repository state.
57
+ > Coder output: \<coder-output\>
50
58
 
51
- `decide` is complete when `review` returns the approved latest commit and the fact that no unsettled findings remain.
52
- When `review` returns an authored abort or failure, or a terminal result that does not prove exact approval, `decide` shall report the failure and the last `decide`-owned commit to its caller.
59
+ `decide` is complete only when `review` returns a result that applies to the supplied review scope, gives the exact evaluated repository revision, and affirmatively establishes that no unsettled findings remain.
60
+ It then returns the `decide`-owned commit and that evaluated revision to its caller.
61
+ When `review` returns an authored abort or failure, or a terminal result that does not establish those facts, `decide` shall report the failure and the last `decide`-owned commit to its caller.
53
62
  When the nested `review` call fails outside that authored result contract, `decide` shall park as failed and retain the control-plane error instead of reporting an authored review outcome.
@@ -19,7 +19,12 @@ type PendingBossQuestions = Partial<Record<ResumableStateId, PendingBossQuestion
19
19
  type BossReplies = Partial<Record<ResumableStateId, string>>;
20
20
  type PendingBossQuestionParams = Omit<PendingBossQuestion, 'questionId'>;
21
21
  export interface ReviewSuccessOutput {
22
- approvedCommit: 'latest';
22
+ evaluatedRevision: string;
23
+ noUnsettledFindings: true;
24
+ }
25
+ export interface DecideSuccessOutput {
26
+ decideCommit: string;
27
+ evaluatedRevision: string;
23
28
  noUnsettledFindings: true;
24
29
  }
25
30
  export interface DecideFailureOutput {
@@ -31,7 +36,7 @@ export interface DecideFailureOutput {
31
36
  message: string;
32
37
  };
33
38
  }
34
- export type DecideOutput = ReviewSuccessOutput | DecideFailureOutput;
39
+ export type DecideOutput = DecideSuccessOutput | DecideFailureOutput;
35
40
  interface ChildFailure {
36
41
  status: 'aborted' | 'error';
37
42
  playbookId: 'review';
@@ -45,6 +50,7 @@ export interface DecideContext {
45
50
  coderProposal?: string;
46
51
  reviewerProposal?: string;
47
52
  latestCommit?: string;
53
+ coderOutput?: string;
48
54
  reviewResult?: ReviewSuccessOutput;
49
55
  reviewFailure?: ChildFailure;
50
56
  lastResult?: PlayerOutput;
@@ -74,6 +80,7 @@ export interface PlayerInput {
74
80
  result: Record<string, string>;
75
81
  role: Role;
76
82
  callerTopic?: string;
83
+ reviewerProposal?: string;
77
84
  pendingBossQuestion?: PendingBossQuestion;
78
85
  bossReply?: string;
79
86
  }
@@ -139,9 +146,6 @@ export declare const decideMachine: import("xstate").StateMachine<DecideContext,
139
146
  } | {
140
147
  type: "rememberBossReply";
141
148
  params: import("xstate").NonReducibleUnknown;
142
- } | {
143
- type: "clearBossReplyContext";
144
- params: import("xstate").NonReducibleUnknown;
145
149
  } | {
146
150
  type: "copyInterruptedTopic";
147
151
  params: import("xstate").NonReducibleUnknown;
@@ -186,6 +190,9 @@ export declare const decideMachine: import("xstate").StateMachine<DecideContext,
186
190
  } | {
187
191
  type: "clearProposalRoundContext";
188
192
  params: import("xstate").NonReducibleUnknown;
193
+ } | {
194
+ type: "clearBossReplyContext";
195
+ params: import("xstate").NonReducibleUnknown;
189
196
  }, {
190
197
  type: "authoredReviewFailure";
191
198
  params: unknown;
@@ -212,7 +219,7 @@ export declare const decideMachine: import("xstate").StateMachine<DecideContext,
212
219
  coder: "complete" | "working" | "waiting";
213
220
  reviewer: "complete" | "working" | "waiting";
214
221
  };
215
- }, string, Readonly<Record<string, never>>, ReviewSuccessOutput | DecideFailureOutput, import("xstate").EventObject, import("xstate").MetaObject, {
222
+ }, string, Readonly<Record<string, never>>, DecideSuccessOutput | DecideFailureOutput, import("xstate").EventObject, import("xstate").MetaObject, {
216
223
  id: "decide";
217
224
  states: {
218
225
  readonly ready: {