@sublang/playbook 0.7.0 → 0.9.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 +93 -74
- package/package.json +10 -15
- package/reference/sdlc/code.playbook/bin/playbook.js +438 -0
- package/reference/sdlc/code.playbook/code.fsm.ts +3 -2
- package/reference/sdlc/code.playbook/code.playbook.js +3 -2
- package/reference/sdlc/code.playbook/code.playbook.ts +3 -2
- package/reference/sdlc/code.playbook/code.registry.d.ts +18 -3
- package/reference/sdlc/code.playbook/code.registry.js +50 -32
- package/reference/sdlc/code.playbook/code.registry.ts +77 -37
- package/reference/sdlc/code.playbook/playbook-captain.d.ts +8 -5
- package/reference/sdlc/code.playbook/playbook-captain.js +141 -55
- package/reference/sdlc/code.playbook/playbook-captain.ts +203 -73
- package/reference/sdlc/code.playbook/playbook.config.template.yaml +58 -0
- package/reference/sdlc/code.playbook/bin/playbook-code.js +0 -453
- package/reference/sdlc/code.playbook/code.tmux-play.d.ts +0 -4
- package/reference/sdlc/code.playbook/code.tmux-play.js +0 -11
- package/reference/sdlc/code.playbook/code.tmux-play.ts +0 -29
- package/reference/sdlc/code.playbook/playbook-code.config.template.yaml +0 -72
- package/reference/sdlc/code.playbook/tmux-play.config.yaml +0 -50
- package/reference/sdlc/code.playbook/tmux-play.production.config.yaml +0 -33
|
@@ -11,9 +11,9 @@ import type {
|
|
|
11
11
|
PlaybookPorts,
|
|
12
12
|
PlaybookRuntime,
|
|
13
13
|
} from './code.playbook.js';
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
import type {
|
|
15
|
+
PlaybookSummaryPolicy,
|
|
16
|
+
RegistryPlayer,
|
|
17
17
|
} from './code.registry.js';
|
|
18
18
|
|
|
19
19
|
export interface CreatePlaybookRuntimeOptions {
|
|
@@ -21,20 +21,38 @@ export interface CreatePlaybookRuntimeOptions {
|
|
|
21
21
|
players: readonly RegistryPlayer[];
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
export interface PlaybookCaptainDeps {
|
|
25
|
+
loadModule?: (specifier: string) => Promise<unknown>;
|
|
26
|
+
}
|
|
27
|
+
|
|
24
28
|
export interface PlaybookCaptainRegistryEntry {
|
|
25
29
|
id: string;
|
|
26
30
|
command: string;
|
|
27
31
|
intent: string;
|
|
32
|
+
requiredRoleIds: readonly string[];
|
|
28
33
|
idleStateId: string;
|
|
29
34
|
finalStateId: string;
|
|
30
|
-
|
|
31
|
-
|
|
35
|
+
parkStateIds: readonly string[];
|
|
36
|
+
summaryPolicy?: PlaybookSummaryPolicy;
|
|
32
37
|
validateOptions(captainOptions: unknown): unknown;
|
|
33
38
|
createRuntime(options: CreatePlaybookRuntimeOptions): PlaybookRuntime;
|
|
34
39
|
}
|
|
35
40
|
|
|
41
|
+
// Per-enabled-playbook binding the shell resolves at init from
|
|
42
|
+
// `captain.options.playbooks`: each playbook binds its local roles to
|
|
43
|
+
// `<id>-<role>` host players and carries the generated visible set.
|
|
44
|
+
interface Enablement {
|
|
45
|
+
entry: PlaybookCaptainRegistryEntry;
|
|
46
|
+
command: string;
|
|
47
|
+
optionInput: unknown;
|
|
48
|
+
boundPlayers: readonly RegistryPlayer[];
|
|
49
|
+
hostPlayerId: (localRole: string) => string;
|
|
50
|
+
visiblePlayerIds?: readonly string[];
|
|
51
|
+
}
|
|
52
|
+
|
|
36
53
|
interface ActiveEngagement {
|
|
37
54
|
entry: PlaybookCaptainRegistryEntry;
|
|
55
|
+
enablement: Enablement;
|
|
38
56
|
runtime: PlaybookRuntime;
|
|
39
57
|
}
|
|
40
58
|
|
|
@@ -70,10 +88,6 @@ interface ActiveTurnSummary {
|
|
|
70
88
|
stateCounts: Map<string, number>;
|
|
71
89
|
}
|
|
72
90
|
|
|
73
|
-
export const playbookCaptainRegistry: readonly PlaybookCaptainRegistryEntry[] = [
|
|
74
|
-
codePlaybookRegistryEntry,
|
|
75
|
-
];
|
|
76
|
-
|
|
77
91
|
function parseRegisteredCommand(
|
|
78
92
|
prompt: string,
|
|
79
93
|
): { command: string; text: string } | undefined {
|
|
@@ -84,10 +98,6 @@ function parseRegisteredCommand(
|
|
|
84
98
|
return { command: match[1], text: (match[2] ?? '').trim() };
|
|
85
99
|
}
|
|
86
100
|
|
|
87
|
-
function playbookCommandLabel(entry: PlaybookCaptainRegistryEntry): string {
|
|
88
|
-
return `/${entry.command}`;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
101
|
function visibleChatEnvelope(message: string): string {
|
|
92
102
|
return [
|
|
93
103
|
'You are the Playbook Captain shell.',
|
|
@@ -101,9 +111,9 @@ function visibleTurnSummaryEnvelope(input: {
|
|
|
101
111
|
submittedText: string;
|
|
102
112
|
counts: TurnSummaryCounts;
|
|
103
113
|
progressPhrase: string;
|
|
104
|
-
|
|
114
|
+
progressRounds: number;
|
|
115
|
+
savedLine: string;
|
|
105
116
|
}): string {
|
|
106
|
-
const savedLine = savedCountsLine(input.counts, input.reviewRebuttalRounds);
|
|
107
117
|
return [
|
|
108
118
|
'You are the Playbook Captain shell.',
|
|
109
119
|
'This is visible Boss chat after a sub-playbook command completed. Do not reveal hidden control JSON, hidden router decisions, or hidden judge replies.',
|
|
@@ -112,39 +122,20 @@ function visibleTurnSummaryEnvelope(input: {
|
|
|
112
122
|
'State only what was done or what changed; do not explain how it was done.',
|
|
113
123
|
'Do not list raw state names, transitions, guard names, prompts, tools, hidden calls, or reasoning.',
|
|
114
124
|
'If progress detail is useful, use only the aggregate progress phrase supplied below.',
|
|
115
|
-
|
|
116
|
-
`Then write the saved-counts line exactly: ${savedLine}`,
|
|
125
|
+
"Do not mention counts for states the active playbook's summary policy does not label.",
|
|
126
|
+
`Then write the saved-counts line exactly: ${input.savedLine}`,
|
|
117
127
|
'Use the exact counts supplied; do not change them.',
|
|
118
|
-
'Do not repeat the exact
|
|
128
|
+
'Do not repeat the exact progress round count outside the saved-counts line.',
|
|
119
129
|
`Playbook: ${input.playbookId}`,
|
|
120
130
|
`Submitted Boss text:\n${input.submittedText}`,
|
|
121
131
|
`Progress counts:\n${input.progressPhrase}`,
|
|
122
132
|
`Counts:\n${JSON.stringify({
|
|
123
133
|
...input.counts,
|
|
124
|
-
|
|
134
|
+
progressRounds: input.progressRounds,
|
|
125
135
|
})}`,
|
|
126
136
|
].join('\n\n');
|
|
127
137
|
}
|
|
128
138
|
|
|
129
|
-
function countNoun(count: number, singular: string, plural = `${singular}s`): string {
|
|
130
|
-
return `${count} ${count === 1 ? singular : plural}`;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function savedCountsLine(
|
|
134
|
-
counts: TurnSummaryCounts,
|
|
135
|
-
reviewRebuttalRounds: number,
|
|
136
|
-
): string {
|
|
137
|
-
return [
|
|
138
|
-
'Saved you',
|
|
139
|
-
countNoun(counts.interruptions, 'interruption'),
|
|
140
|
-
'and',
|
|
141
|
-
countNoun(counts.copyPastes, 'copy-paste'),
|
|
142
|
-
'across',
|
|
143
|
-
countNoun(reviewRebuttalRounds, 'round'),
|
|
144
|
-
'of reviews/rebuttals.',
|
|
145
|
-
].join(' ');
|
|
146
|
-
}
|
|
147
|
-
|
|
148
139
|
function stateCountLabel(
|
|
149
140
|
stateId: string,
|
|
150
141
|
entry: PlaybookCaptainRegistryEntry,
|
|
@@ -152,7 +143,7 @@ function stateCountLabel(
|
|
|
152
143
|
if (stateId === entry.idleStateId || stateId === entry.finalStateId) {
|
|
153
144
|
return undefined;
|
|
154
145
|
}
|
|
155
|
-
const registryLabel = entry.stateCountLabels?.[stateId]?.trim();
|
|
146
|
+
const registryLabel = entry.summaryPolicy?.stateCountLabels?.[stateId]?.trim();
|
|
156
147
|
return registryLabel || undefined;
|
|
157
148
|
}
|
|
158
149
|
|
|
@@ -178,27 +169,144 @@ function guardFromJudgeReply(finalText: string): string | undefined {
|
|
|
178
169
|
return /"guard"\s*:\s*"([^"]+)"/.exec(finalText)?.[1];
|
|
179
170
|
}
|
|
180
171
|
|
|
181
|
-
function
|
|
182
|
-
|
|
183
|
-
): {
|
|
172
|
+
function isValidRegistryEntry(
|
|
173
|
+
value: unknown,
|
|
174
|
+
): value is PlaybookCaptainRegistryEntry {
|
|
175
|
+
if (typeof value !== 'object' || value === null) return false;
|
|
176
|
+
const e = value as Record<string, unknown>;
|
|
177
|
+
return (
|
|
178
|
+
typeof e.id === 'string' &&
|
|
179
|
+
typeof e.command === 'string' &&
|
|
180
|
+
typeof e.intent === 'string' &&
|
|
181
|
+
Array.isArray(e.requiredRoleIds) &&
|
|
182
|
+
typeof e.idleStateId === 'string' &&
|
|
183
|
+
typeof e.finalStateId === 'string' &&
|
|
184
|
+
Array.isArray(e.parkStateIds) &&
|
|
185
|
+
typeof e.validateOptions === 'function' &&
|
|
186
|
+
typeof e.createRuntime === 'function'
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function readPlaybooksConfig(
|
|
191
|
+
options: unknown,
|
|
192
|
+
): Record<string, unknown> | undefined {
|
|
193
|
+
if (typeof options !== 'object' || options === null) return undefined;
|
|
194
|
+
const pb = (options as Record<string, unknown>).playbooks;
|
|
195
|
+
if (typeof pb !== 'object' || pb === null || Array.isArray(pb)) {
|
|
196
|
+
return undefined;
|
|
197
|
+
}
|
|
198
|
+
return pb as Record<string, unknown>;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
interface BuiltRegistry {
|
|
184
202
|
entries: readonly PlaybookCaptainRegistryEntry[];
|
|
185
203
|
byCommand: Map<string, PlaybookCaptainRegistryEntry>;
|
|
186
204
|
byId: Map<string, PlaybookCaptainRegistryEntry>;
|
|
187
|
-
|
|
205
|
+
enablementById: Map<string, Enablement>;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Resolve the active registry at init from `captain.options.playbooks`
|
|
209
|
+
// (CAPTAIN-16): each enabled playbook is loaded from its explicit `from`
|
|
210
|
+
// module and bound to namespaced `<id>-<role>` host players.
|
|
211
|
+
async function buildEnablements(
|
|
212
|
+
options: unknown,
|
|
213
|
+
players: readonly RegistryPlayer[],
|
|
214
|
+
loadModule: (specifier: string) => Promise<unknown>,
|
|
215
|
+
): Promise<BuiltRegistry> {
|
|
216
|
+
const entries: PlaybookCaptainRegistryEntry[] = [];
|
|
188
217
|
const byCommand = new Map<string, PlaybookCaptainRegistryEntry>();
|
|
189
218
|
const byId = new Map<string, PlaybookCaptainRegistryEntry>();
|
|
190
|
-
|
|
191
|
-
|
|
219
|
+
const enablementById = new Map<string, Enablement>();
|
|
220
|
+
|
|
221
|
+
const config = readPlaybooksConfig(options);
|
|
222
|
+
if (config === undefined) {
|
|
223
|
+
throw new Error('captain.options.playbooks is required');
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const ids = Object.keys(config);
|
|
227
|
+
if (ids.length === 0) {
|
|
228
|
+
throw new Error(
|
|
229
|
+
'captain.options.playbooks must enable at least one playbook',
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
for (const id of ids) {
|
|
233
|
+
const block = config[id];
|
|
234
|
+
if (typeof block !== 'object' || block === null || Array.isArray(block)) {
|
|
235
|
+
throw new Error(`captain.options.playbooks.${id} must be an object`);
|
|
236
|
+
}
|
|
237
|
+
const record = block as Record<string, unknown>;
|
|
238
|
+
const from = record.from;
|
|
239
|
+
if (typeof from !== 'string' || from.length === 0) {
|
|
240
|
+
throw new Error(
|
|
241
|
+
`captain.options.playbooks.${id}.from must be a module specifier`,
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
let mod: unknown;
|
|
245
|
+
try {
|
|
246
|
+
mod = await loadModule(from);
|
|
247
|
+
} catch (cause) {
|
|
248
|
+
throw new Error(
|
|
249
|
+
`captain.options.playbooks.${id}.from "${from}" failed to import: ${String(
|
|
250
|
+
(cause as { message?: unknown })?.message ?? cause,
|
|
251
|
+
)}`,
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
const entry = (mod as { default?: unknown })?.default;
|
|
255
|
+
if (!isValidRegistryEntry(entry)) {
|
|
256
|
+
throw new Error(
|
|
257
|
+
`captain.options.playbooks.${id}.from "${from}" exposes no valid registry entry`,
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
if (entry.id !== id) {
|
|
261
|
+
throw new Error(
|
|
262
|
+
`captain.options.playbooks.${id} key must equal the module manifest id "${entry.id}"`,
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
if (byId.has(entry.id)) {
|
|
266
|
+
throw new Error(
|
|
267
|
+
`captain.options.playbooks has a duplicate playbook id "${entry.id}"`,
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
const command =
|
|
271
|
+
typeof record.command === 'string' && record.command.length > 0
|
|
272
|
+
? record.command
|
|
273
|
+
: entry.command;
|
|
274
|
+
if (byCommand.has(command)) {
|
|
275
|
+
throw new Error(
|
|
276
|
+
`captain.options.playbooks has a duplicate effective command "${command}"`,
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
const boundPlayers = entry.requiredRoleIds.map((role) => {
|
|
280
|
+
const host = players.find((p) => p.id === `${entry.id}-${role}`);
|
|
281
|
+
return { id: role, adapter: host?.adapter, model: host?.model };
|
|
282
|
+
});
|
|
283
|
+
entries.push(entry);
|
|
192
284
|
byId.set(entry.id, entry);
|
|
285
|
+
byCommand.set(command, entry);
|
|
286
|
+
enablementById.set(entry.id, {
|
|
287
|
+
entry,
|
|
288
|
+
command,
|
|
289
|
+
optionInput: record.options,
|
|
290
|
+
boundPlayers,
|
|
291
|
+
hostPlayerId: (localRole) => `${entry.id}-${localRole}`,
|
|
292
|
+
visiblePlayerIds: entry.requiredRoleIds.map(
|
|
293
|
+
(role) => `${entry.id}-${role}`,
|
|
294
|
+
),
|
|
295
|
+
});
|
|
193
296
|
}
|
|
194
|
-
return { entries
|
|
297
|
+
return { entries, byCommand, byId, enablementById };
|
|
195
298
|
}
|
|
196
299
|
|
|
197
300
|
export function createPlaybookCaptainShell(
|
|
198
301
|
options: unknown,
|
|
199
|
-
|
|
302
|
+
deps: PlaybookCaptainDeps = {},
|
|
200
303
|
): Captain {
|
|
201
|
-
const
|
|
304
|
+
const loadModule =
|
|
305
|
+
deps.loadModule ?? ((specifier: string) => import(specifier));
|
|
306
|
+
let entries: readonly PlaybookCaptainRegistryEntry[] = [];
|
|
307
|
+
let byCommand = new Map<string, PlaybookCaptainRegistryEntry>();
|
|
308
|
+
let byId = new Map<string, PlaybookCaptainRegistryEntry>();
|
|
309
|
+
let enablementById = new Map<string, Enablement>();
|
|
202
310
|
let session: CaptainSession | undefined;
|
|
203
311
|
let players: readonly RegistryPlayer[] = [];
|
|
204
312
|
let activeContext: CaptainContext | undefined;
|
|
@@ -315,8 +423,7 @@ export function createPlaybookCaptainShell(
|
|
|
315
423
|
|
|
316
424
|
if (
|
|
317
425
|
stateId === active.entry.idleStateId ||
|
|
318
|
-
stateId
|
|
319
|
-
stateId === 'awaitBossReply'
|
|
426
|
+
active.entry.parkStateIds.includes(stateId)
|
|
320
427
|
) {
|
|
321
428
|
await setMode('engaged.parked', `sub-runtime:${stateId}`);
|
|
322
429
|
}
|
|
@@ -327,7 +434,10 @@ export function createPlaybookCaptainShell(
|
|
|
327
434
|
if (!activeContext) {
|
|
328
435
|
throw new Error('callPlayer invoked outside a Boss turn');
|
|
329
436
|
}
|
|
330
|
-
const
|
|
437
|
+
const hostPlayerId = active
|
|
438
|
+
? active.enablement.hostPlayerId(playerId)
|
|
439
|
+
: playerId;
|
|
440
|
+
const result = await activeContext.callPlayer(hostPlayerId, prompt);
|
|
331
441
|
if (activeTurnSummary) {
|
|
332
442
|
activeTurnSummary.counts.interruptions++;
|
|
333
443
|
}
|
|
@@ -355,7 +465,7 @@ export function createPlaybookCaptainShell(
|
|
|
355
465
|
const guard = guardFromJudgeReply(result.finalText);
|
|
356
466
|
if (
|
|
357
467
|
guard &&
|
|
358
|
-
active?.entry.copyPasteGuardNames.includes(guard) &&
|
|
468
|
+
active?.entry.summaryPolicy?.copyPasteGuardNames.includes(guard) &&
|
|
359
469
|
activeTurnSummary
|
|
360
470
|
) {
|
|
361
471
|
activeTurnSummary.counts.copyPastes++;
|
|
@@ -376,24 +486,33 @@ export function createPlaybookCaptainShell(
|
|
|
376
486
|
},
|
|
377
487
|
});
|
|
378
488
|
|
|
489
|
+
// CAPTAIN-22: before dispatching to a playbook, request tmux-play
|
|
490
|
+
// visibility for that playbook's generated host players. A pane
|
|
491
|
+
// reconciliation failure is display-only in tmux-play and does not
|
|
492
|
+
// reject; the legacy path carries no generated set and skips this.
|
|
493
|
+
const requestVisibility = async (enablement: Enablement): Promise<void> => {
|
|
494
|
+
const ids = enablement.visiblePlayerIds;
|
|
495
|
+
if (!ids || ids.length === 0 || !activeContext) return;
|
|
496
|
+
await activeContext.setVisiblePlayers(ids);
|
|
497
|
+
};
|
|
498
|
+
|
|
379
499
|
const engage = async (
|
|
380
500
|
entry: PlaybookCaptainRegistryEntry,
|
|
381
501
|
): Promise<ActiveEngagement> => {
|
|
382
502
|
if (active?.entry.id === entry.id) return active;
|
|
503
|
+
const enablement = enablementById.get(entry.id)!;
|
|
383
504
|
const runtime = entry.createRuntime({
|
|
384
|
-
captainOptions:
|
|
385
|
-
players,
|
|
505
|
+
captainOptions: enablement.optionInput,
|
|
506
|
+
players: enablement.boundPlayers,
|
|
386
507
|
});
|
|
387
|
-
active = { entry, runtime };
|
|
508
|
+
active = { entry, enablement, runtime };
|
|
388
509
|
latestSubRuntimeStateId = undefined;
|
|
389
510
|
pendingBossQuestion = undefined;
|
|
390
511
|
lastError = undefined;
|
|
391
512
|
finalDisposalRequested = undefined;
|
|
392
513
|
await setMode('engaged.parked', 'engage', entry.id);
|
|
393
514
|
await runtime.init(createPorts());
|
|
394
|
-
await requireSession().emitStatus(
|
|
395
|
-
`◇ ${playbookCommandLabel(entry)} started`,
|
|
396
|
-
);
|
|
515
|
+
await requireSession().emitStatus(`◇ /${enablement.command} started`);
|
|
397
516
|
return active;
|
|
398
517
|
};
|
|
399
518
|
|
|
@@ -402,23 +521,24 @@ export function createPlaybookCaptainShell(
|
|
|
402
521
|
text: string,
|
|
403
522
|
context: CaptainContext,
|
|
404
523
|
): Promise<void> => {
|
|
524
|
+
await requestVisibility(engagement.enablement);
|
|
525
|
+
const policy = engagement.entry.summaryPolicy;
|
|
405
526
|
const summaryCounts: TurnSummaryCounts = {
|
|
406
527
|
interruptions: 0,
|
|
407
528
|
copyPastes: 0,
|
|
408
529
|
};
|
|
409
530
|
const summaryStateCounts = new Map<string, number>();
|
|
410
531
|
let shouldSummarize = false;
|
|
411
|
-
activeTurnSummary =
|
|
412
|
-
counts: summaryCounts,
|
|
413
|
-
|
|
414
|
-
};
|
|
532
|
+
activeTurnSummary = policy
|
|
533
|
+
? { counts: summaryCounts, stateCounts: summaryStateCounts }
|
|
534
|
+
: undefined;
|
|
415
535
|
await setMode('engaged.driving', 'submit');
|
|
416
536
|
try {
|
|
417
537
|
await engagement.runtime.handleBossInput({
|
|
418
538
|
text,
|
|
419
539
|
signal: context.signal,
|
|
420
540
|
});
|
|
421
|
-
shouldSummarize =
|
|
541
|
+
shouldSummarize = policy !== undefined;
|
|
422
542
|
} finally {
|
|
423
543
|
activeTurnSummary = undefined;
|
|
424
544
|
if (active === engagement && finalDisposalRequested === engagement) {
|
|
@@ -428,13 +548,15 @@ export function createPlaybookCaptainShell(
|
|
|
428
548
|
await setMode('engaged.parked', 'turn.settled');
|
|
429
549
|
}
|
|
430
550
|
}
|
|
431
|
-
if (shouldSummarize) {
|
|
551
|
+
if (shouldSummarize && policy) {
|
|
552
|
+
const progressRounds = summaryProgressRoundCount(summaryStateCounts);
|
|
432
553
|
await callVisibleTurnSummary(context, {
|
|
433
554
|
playbookId: engagement.entry.id,
|
|
434
555
|
submittedText: text,
|
|
435
556
|
counts: summaryCounts,
|
|
436
557
|
progressPhrase: summaryProgressPhrase(summaryStateCounts),
|
|
437
|
-
|
|
558
|
+
progressRounds,
|
|
559
|
+
savedLine: policy.savedCountsLine(summaryCounts, progressRounds),
|
|
438
560
|
});
|
|
439
561
|
}
|
|
440
562
|
};
|
|
@@ -445,7 +567,7 @@ export function createPlaybookCaptainShell(
|
|
|
445
567
|
const engagement = active;
|
|
446
568
|
if (!engagement) return;
|
|
447
569
|
const playbookId = engagement.entry.id;
|
|
448
|
-
const commandLabel =
|
|
570
|
+
const commandLabel = `/${engagement.enablement.command}`;
|
|
449
571
|
active = undefined;
|
|
450
572
|
finalDisposalRequested = undefined;
|
|
451
573
|
if (reason === 'dispose') {
|
|
@@ -487,7 +609,8 @@ export function createPlaybookCaptainShell(
|
|
|
487
609
|
submittedText: string;
|
|
488
610
|
counts: TurnSummaryCounts;
|
|
489
611
|
progressPhrase: string;
|
|
490
|
-
|
|
612
|
+
progressRounds: number;
|
|
613
|
+
savedLine: string;
|
|
491
614
|
},
|
|
492
615
|
): Promise<void> => {
|
|
493
616
|
const result = await context.callCaptain(visibleTurnSummaryEnvelope(input));
|
|
@@ -513,7 +636,7 @@ export function createPlaybookCaptainShell(
|
|
|
513
636
|
`Registry:\n${JSON.stringify(
|
|
514
637
|
entries.map((entry) => ({
|
|
515
638
|
id: entry.id,
|
|
516
|
-
command: entry.command,
|
|
639
|
+
command: enablementById.get(entry.id)?.command ?? entry.command,
|
|
517
640
|
intent: entry.intent,
|
|
518
641
|
})),
|
|
519
642
|
)}`,
|
|
@@ -626,7 +749,7 @@ export function createPlaybookCaptainShell(
|
|
|
626
749
|
return;
|
|
627
750
|
}
|
|
628
751
|
|
|
629
|
-
const dismissedCommandLabel =
|
|
752
|
+
const dismissedCommandLabel = `/${active.enablement.command}`;
|
|
630
753
|
await disposeActive('dismiss');
|
|
631
754
|
await callVisibleChat(
|
|
632
755
|
context,
|
|
@@ -639,19 +762,21 @@ export function createPlaybookCaptainShell(
|
|
|
639
762
|
text: string,
|
|
640
763
|
context: CaptainContext,
|
|
641
764
|
): Promise<void> => {
|
|
765
|
+
const enablement = enablementById.get(entry.id)!;
|
|
642
766
|
if (active && active.entry.id !== entry.id) {
|
|
643
767
|
await callVisibleChat(
|
|
644
768
|
context,
|
|
645
|
-
`/${active.
|
|
769
|
+
`/${active.enablement.command} is already running. Finish or stop it before starting /${enablement.command}.`,
|
|
646
770
|
);
|
|
647
771
|
return;
|
|
648
772
|
}
|
|
649
773
|
|
|
650
774
|
const engagement = await engage(entry);
|
|
651
775
|
if (text.length === 0) {
|
|
776
|
+
await requestVisibility(engagement.enablement);
|
|
652
777
|
await callVisibleChat(
|
|
653
778
|
context,
|
|
654
|
-
`Ask what task to run with /${
|
|
779
|
+
`Ask what task to run with /${enablement.command}.`,
|
|
655
780
|
);
|
|
656
781
|
return;
|
|
657
782
|
}
|
|
@@ -663,8 +788,13 @@ export function createPlaybookCaptainShell(
|
|
|
663
788
|
async init(initSession: CaptainSession): Promise<void> {
|
|
664
789
|
session = initSession;
|
|
665
790
|
players = initSession.players;
|
|
666
|
-
|
|
667
|
-
|
|
791
|
+
const built = await buildEnablements(options, players, loadModule);
|
|
792
|
+
entries = built.entries;
|
|
793
|
+
byCommand = built.byCommand;
|
|
794
|
+
byId = built.byId;
|
|
795
|
+
enablementById = built.enablementById;
|
|
796
|
+
for (const enablement of enablementById.values()) {
|
|
797
|
+
enablement.entry.validateOptions(enablement.optionInput);
|
|
668
798
|
}
|
|
669
799
|
await setMode('chat', 'init');
|
|
670
800
|
},
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# SPDX-FileCopyrightText: 2026 SubLang International <https://sublang.ai>
|
|
3
|
+
|
|
4
|
+
# Generic `playbook` launcher config.
|
|
5
|
+
# Top-level host fields only — no `config:` wrapper, no top-level `players`.
|
|
6
|
+
# The launcher injects captain.from and the namespaced <id>-<role> host
|
|
7
|
+
# players, then launches cligent's tmux-play under the Playbook Captain shell.
|
|
8
|
+
|
|
9
|
+
# Reusable agent settings. A profile id must not be an adapter shorthand
|
|
10
|
+
# (claude, codex). Reference a profile by id from captain / players below.
|
|
11
|
+
# Profile ids name the underlying agent/model (e.g. claude-opus,
|
|
12
|
+
# codex-gpt) so they read distinctly from the captain / coder / reviewer
|
|
13
|
+
# player roles that reference them.
|
|
14
|
+
# Every seeded agent runs in cligent's protected auto mode
|
|
15
|
+
# (permissions.mode: auto): claude maps it to permissionMode auto, codex to
|
|
16
|
+
# on-request + auto_review. Codex roles also grant writablePaths: ['.git']
|
|
17
|
+
# so commit turns can write git metadata under the codex sandbox.
|
|
18
|
+
profiles:
|
|
19
|
+
claude-opus:
|
|
20
|
+
adapter: claude
|
|
21
|
+
model: claude-opus-4-8
|
|
22
|
+
reasoningEffort: high
|
|
23
|
+
permissions:
|
|
24
|
+
mode: auto
|
|
25
|
+
claude-opus-1m:
|
|
26
|
+
adapter: claude
|
|
27
|
+
model: claude-opus-4-8[1m]
|
|
28
|
+
reasoningEffort: xhigh
|
|
29
|
+
permissions:
|
|
30
|
+
mode: auto
|
|
31
|
+
codex-gpt:
|
|
32
|
+
adapter: codex
|
|
33
|
+
model: gpt-5.5
|
|
34
|
+
reasoningEffort: xhigh
|
|
35
|
+
permissions:
|
|
36
|
+
mode: auto
|
|
37
|
+
writablePaths: ['.git']
|
|
38
|
+
|
|
39
|
+
# The Captain/Judge agent: a profile id, an adapter shorthand, or a full
|
|
40
|
+
# tmux-play agent block (which may carry a `profile:` key).
|
|
41
|
+
captain: claude-opus
|
|
42
|
+
|
|
43
|
+
# Host notifications. Omitting turn_aborted resolves it to off.
|
|
44
|
+
notifications:
|
|
45
|
+
player_finished: bell
|
|
46
|
+
turn_finished: desktop
|
|
47
|
+
|
|
48
|
+
# Enabled playbooks. Each is loaded from its explicit `from` module; the
|
|
49
|
+
# `<id>` key must equal that module's manifest id. `from`, `command`, and
|
|
50
|
+
# `players` are launcher-owned; every other key (e.g. CODE's `committer`)
|
|
51
|
+
# is that playbook's option slice.
|
|
52
|
+
playbooks:
|
|
53
|
+
code:
|
|
54
|
+
from: "@sublang/playbook/code/registry"
|
|
55
|
+
players:
|
|
56
|
+
coder: claude-opus-1m
|
|
57
|
+
reviewer: codex-gpt
|
|
58
|
+
committer: coder
|