@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.
- package/README.md +1 -1
- package/docs/cli.md +67 -19
- package/docs/configuration.md +103 -40
- package/docs/embedding.md +88 -0
- package/package.json +30 -4
- package/reference/sdlc/code.md +35 -15
- package/reference/sdlc/code.playbook/bin/interactive-session.js +58 -6
- package/reference/sdlc/code.playbook/bin/launch-config.js +499 -241
- package/reference/sdlc/code.playbook/bin/playbook.js +236 -187
- package/reference/sdlc/code.playbook/bin/replay-observer.js +221 -0
- package/reference/sdlc/code.playbook/bin/run.js +355 -203
- package/reference/sdlc/code.playbook/bin/session-store.js +1512 -136
- package/reference/sdlc/code.playbook/code.fsm.d.ts +22 -19
- package/reference/sdlc/code.playbook/code.fsm.js +116 -52
- package/reference/sdlc/code.playbook/code.fsm.ts +149 -64
- package/reference/sdlc/code.playbook/code.gears.md +40 -20
- package/reference/sdlc/code.playbook/code.playbook.js +23 -2
- package/reference/sdlc/code.playbook/code.playbook.ts +23 -2
- package/reference/sdlc/code.playbook/playbook-captain.d.ts +4 -1
- package/reference/sdlc/code.playbook/playbook-captain.js +21 -3
- package/reference/sdlc/code.playbook/playbook-captain.ts +42 -6
- package/reference/sdlc/code.playbook/playbook.config.template.yaml +31 -11
- package/reference/sdlc/code.playbook/session-store.d.ts +82 -0
- package/reference/sdlc/code.playbook/session-store.js +113 -0
- package/reference/sdlc/decide.md +24 -15
- package/reference/sdlc/decide.playbook/decide.fsm.d.ts +13 -6
- package/reference/sdlc/decide.playbook/decide.fsm.js +54 -27
- package/reference/sdlc/decide.playbook/decide.fsm.ts +68 -29
- package/reference/sdlc/decide.playbook/decide.gears.md +25 -19
- package/reference/sdlc/decide.playbook/decide.playbook.js +11 -3
- package/reference/sdlc/decide.playbook/decide.playbook.ts +11 -3
- package/reference/sdlc/decide.playbook/decide.registry.js +1 -1
- package/reference/sdlc/decide.playbook/decide.registry.ts +1 -1
- package/reference/sdlc/dev.md +52 -0
- package/reference/sdlc/dev.playbook/dev.fsm.d.ts +261 -0
- package/reference/sdlc/dev.playbook/dev.fsm.js +723 -0
- package/reference/sdlc/dev.playbook/dev.fsm.ts +988 -0
- package/reference/sdlc/dev.playbook/dev.gears.md +91 -0
- package/reference/sdlc/dev.playbook/dev.playbook.d.ts +21 -0
- package/reference/sdlc/dev.playbook/dev.playbook.js +143 -0
- package/reference/sdlc/dev.playbook/dev.playbook.ts +246 -0
- package/reference/sdlc/dev.playbook/dev.registry.d.ts +40 -0
- package/reference/sdlc/dev.playbook/dev.registry.js +64 -0
- package/reference/sdlc/dev.playbook/dev.registry.ts +120 -0
- package/reference/sdlc/review.md +36 -18
- package/reference/sdlc/review.playbook/review.fsm.d.ts +15 -2
- package/reference/sdlc/review.playbook/review.fsm.js +77 -27
- package/reference/sdlc/review.playbook/review.fsm.ts +96 -30
- package/reference/sdlc/review.playbook/review.gears.md +52 -26
- package/reference/sdlc/review.playbook/review.playbook.js +17 -7
- package/reference/sdlc/review.playbook/review.playbook.ts +17 -7
- package/reference/sdlc/review.playbook/review.registry.js +1 -1
- package/reference/sdlc/review.playbook/review.registry.ts +1 -1
- package/slc/link.md +12 -5
- package/slc/text2gears.md +3 -0
- package/src/xstate-playbook-runtime.js +5 -2
- package/src/xstate-playbook-runtime.ts +5 -2
- package/src/xstate-runtime.js +13 -1
- 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
|
|
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]) =>
|
|
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(
|
|
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(
|
|
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
|
|
12
|
-
#
|
|
13
|
-
#
|
|
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-
|
|
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-
|
|
61
|
+
model: claude-opus-5
|
|
50
62
|
effort: xhigh
|
|
51
63
|
permissions:
|
|
52
64
|
mode: auto
|
|
53
65
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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.
|
|
68
|
-
# reuse the same two stable players
|
|
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
|
+
}
|
package/reference/sdlc/decide.md
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
>
|
|
45
|
-
>
|
|
46
|
-
>
|
|
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
|
|
52
|
-
|
|
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
|
-
|
|
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 =
|
|
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>>,
|
|
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: {
|