@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
|
@@ -6,19 +6,15 @@ import createPlaybookRuntime, {
|
|
|
6
6
|
type PlaybookRuntime,
|
|
7
7
|
} from './code.playbook.js';
|
|
8
8
|
|
|
9
|
-
// PBRT-
|
|
10
|
-
// `captain.options.code
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
// `committer` value is rejected naming `captain.options.code.committer`.
|
|
19
|
-
// A further CODE option shall be introduced as its own higher-numbered
|
|
20
|
-
// item that widens `CODE_OPTION_KEYS`; the validator still fails closed
|
|
21
|
-
// on stray keys.
|
|
9
|
+
// PBRT-30: the CODE registry entry validates the option slice the shell
|
|
10
|
+
// passes it (`captain.options.playbooks.code.options`), not a namespace
|
|
11
|
+
// it extracts from the full Captain options bag. The schema defines one
|
|
12
|
+
// key, `committer`: an optional Committer-alias player id, one of the
|
|
13
|
+
// baked role ids `coder` / `reviewer`. A valid slice is absent, `{}`, or
|
|
14
|
+
// `{ committer: 'coder' | 'reviewer' }`; every other key is unknown and
|
|
15
|
+
// rejected with a path-named error. A further CODE option shall be
|
|
16
|
+
// introduced as its own higher-numbered item that widens
|
|
17
|
+
// `CODE_OPTION_KEYS`; the validator still fails closed on stray keys.
|
|
22
18
|
const CODE_OPTION_KEYS = new Set<string>(['committer']);
|
|
23
19
|
const COMMITTER_PLAYER_IDS = new Set<string>(['coder', 'reviewer']);
|
|
24
20
|
export const codeCopyPasteGuardNames = [
|
|
@@ -55,6 +51,47 @@ export const codeStateCountLabels = {
|
|
|
55
51
|
reviewChangesAndChallengesMixed: 'review round',
|
|
56
52
|
} as const;
|
|
57
53
|
|
|
54
|
+
function countNoun(count: number, singular: string, plural = `${singular}s`): string {
|
|
55
|
+
return `${count} ${count === 1 ? singular : plural}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// PBRT-15 / CAPTAIN-19: the CODE saved-counts line wording is
|
|
59
|
+
// registry-owned. The shell renders this exact line through the active
|
|
60
|
+
// entry's summary policy rather than hardcoding CODE phrasing.
|
|
61
|
+
export function codeSavedCountsLine(
|
|
62
|
+
counts: { interruptions: number; copyPastes: number },
|
|
63
|
+
rounds: number,
|
|
64
|
+
): string {
|
|
65
|
+
return [
|
|
66
|
+
'Saved you',
|
|
67
|
+
countNoun(counts.interruptions, 'interruption'),
|
|
68
|
+
'and',
|
|
69
|
+
countNoun(counts.copyPastes, 'copy-paste'),
|
|
70
|
+
'across',
|
|
71
|
+
countNoun(rounds, 'round'),
|
|
72
|
+
'of reviews/rebuttals.',
|
|
73
|
+
].join(' ');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// CAPTAIN-20 summary policy: the counted state-id labels, the
|
|
77
|
+
// copy-paste guard names, and the saved-counts line wording an entry
|
|
78
|
+
// owns for the shell's turn-summary aggregation. An entry without a
|
|
79
|
+
// summary policy opts out of visible turn summaries.
|
|
80
|
+
export interface PlaybookSummaryPolicy {
|
|
81
|
+
stateCountLabels: Readonly<Record<string, string>>;
|
|
82
|
+
copyPasteGuardNames: readonly string[];
|
|
83
|
+
savedCountsLine(
|
|
84
|
+
counts: { interruptions: number; copyPastes: number },
|
|
85
|
+
rounds: number,
|
|
86
|
+
): string;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export const codeSummaryPolicy: PlaybookSummaryPolicy = {
|
|
90
|
+
stateCountLabels: codeStateCountLabels,
|
|
91
|
+
copyPasteGuardNames: codeCopyPasteGuardNames,
|
|
92
|
+
savedCountsLine: codeSavedCountsLine,
|
|
93
|
+
};
|
|
94
|
+
|
|
58
95
|
// The validated CODE options set. `committer`, when present, is the
|
|
59
96
|
// resolved Committer-alias player id (PBRT-8 / PBRT-30); a future CODE
|
|
60
97
|
// option widens both this type and `CODE_OPTION_KEYS`.
|
|
@@ -77,31 +114,39 @@ export interface CodePlaybookRegistryEntry {
|
|
|
77
114
|
id: 'code';
|
|
78
115
|
command: 'code';
|
|
79
116
|
intent: string;
|
|
117
|
+
requiredRoleIds: readonly string[];
|
|
80
118
|
idleStateId: 'ready';
|
|
81
119
|
finalStateId: 'done';
|
|
82
|
-
|
|
83
|
-
|
|
120
|
+
parkStateIds: readonly string[];
|
|
121
|
+
summaryPolicy: PlaybookSummaryPolicy;
|
|
84
122
|
validateOptions(captainOptions: unknown): CodeOptions;
|
|
85
123
|
createRuntime(options: CreateCodeRuntimeOptions): PlaybookRuntime;
|
|
86
124
|
}
|
|
87
125
|
|
|
88
|
-
export function validateCodeOptions(
|
|
89
|
-
|
|
90
|
-
if (
|
|
91
|
-
|
|
92
|
-
|
|
126
|
+
export function validateCodeOptions(optionSlice: unknown): CodeOptions {
|
|
127
|
+
if (optionSlice === undefined) return {};
|
|
128
|
+
if (
|
|
129
|
+
typeof optionSlice !== 'object' ||
|
|
130
|
+
optionSlice === null ||
|
|
131
|
+
Array.isArray(optionSlice)
|
|
132
|
+
) {
|
|
133
|
+
throw new Error('captain.options.playbooks.code.options must be an object');
|
|
93
134
|
}
|
|
94
|
-
|
|
135
|
+
const slice = optionSlice as Record<string, unknown>;
|
|
136
|
+
for (const key of Object.keys(slice)) {
|
|
95
137
|
if (!CODE_OPTION_KEYS.has(key)) {
|
|
96
|
-
throw new Error(
|
|
138
|
+
throw new Error(
|
|
139
|
+
`Unknown config field captain.options.playbooks.code.options.${key}`,
|
|
140
|
+
);
|
|
97
141
|
}
|
|
98
142
|
}
|
|
99
143
|
const options: CodeOptions = {};
|
|
100
|
-
const committer =
|
|
144
|
+
const committer = slice.committer;
|
|
101
145
|
if (committer !== undefined) {
|
|
102
146
|
if (typeof committer !== 'string' || !COMMITTER_PLAYER_IDS.has(committer)) {
|
|
103
147
|
throw new Error(
|
|
104
|
-
"captain.options.code.committer must be
|
|
148
|
+
"captain.options.playbooks.code.options.committer must be " +
|
|
149
|
+
"'coder' or 'reviewer'",
|
|
105
150
|
);
|
|
106
151
|
}
|
|
107
152
|
options.committer = committer as 'coder' | 'reviewer';
|
|
@@ -109,17 +154,6 @@ export function validateCodeOptions(captainOptions: unknown): CodeOptions {
|
|
|
109
154
|
return options;
|
|
110
155
|
}
|
|
111
156
|
|
|
112
|
-
function readCodeNamespace(captainOptions: unknown): unknown {
|
|
113
|
-
if (
|
|
114
|
-
typeof captainOptions !== 'object' ||
|
|
115
|
-
captainOptions === null ||
|
|
116
|
-
Array.isArray(captainOptions)
|
|
117
|
-
) {
|
|
118
|
-
return undefined;
|
|
119
|
-
}
|
|
120
|
-
return (captainOptions as Record<string, unknown>).code;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
157
|
function playerIdentity(
|
|
124
158
|
players: readonly RegistryPlayer[],
|
|
125
159
|
id: string,
|
|
@@ -148,12 +182,18 @@ export const codePlaybookRegistryEntry: CodePlaybookRegistryEntry = {
|
|
|
148
182
|
id: 'code',
|
|
149
183
|
command: 'code',
|
|
150
184
|
intent: 'software development / SDLC coding workflow',
|
|
185
|
+
requiredRoleIds: ['coder', 'reviewer'],
|
|
151
186
|
idleStateId: 'ready',
|
|
152
187
|
finalStateId: 'done',
|
|
153
|
-
|
|
154
|
-
|
|
188
|
+
parkStateIds: ['failed', 'awaitBossReply'],
|
|
189
|
+
summaryPolicy: codeSummaryPolicy,
|
|
155
190
|
validateOptions: validateCodeOptions,
|
|
156
191
|
createRuntime(options) {
|
|
157
192
|
return createPlaybookRuntime(createCodeRuntimeOptions(options));
|
|
158
193
|
},
|
|
159
194
|
};
|
|
195
|
+
|
|
196
|
+
// CAPTAIN-16 / PBRT-16: the published `@sublang/playbook/code/registry`
|
|
197
|
+
// module's default export is the CODE registry entry the Playbook Captain
|
|
198
|
+
// shell loads when a playbook block's `from` names this module.
|
|
199
|
+
export default codePlaybookRegistryEntry;
|
|
@@ -1,21 +1,24 @@
|
|
|
1
1
|
import type { Captain } from '@sublang/cligent/tmux-play';
|
|
2
2
|
import type { PlaybookRuntime } from './code.playbook.js';
|
|
3
|
-
import {
|
|
3
|
+
import type { PlaybookSummaryPolicy, RegistryPlayer } from './code.registry.js';
|
|
4
4
|
export interface CreatePlaybookRuntimeOptions {
|
|
5
5
|
captainOptions: unknown;
|
|
6
6
|
players: readonly RegistryPlayer[];
|
|
7
7
|
}
|
|
8
|
+
export interface PlaybookCaptainDeps {
|
|
9
|
+
loadModule?: (specifier: string) => Promise<unknown>;
|
|
10
|
+
}
|
|
8
11
|
export interface PlaybookCaptainRegistryEntry {
|
|
9
12
|
id: string;
|
|
10
13
|
command: string;
|
|
11
14
|
intent: string;
|
|
15
|
+
requiredRoleIds: readonly string[];
|
|
12
16
|
idleStateId: string;
|
|
13
17
|
finalStateId: string;
|
|
14
|
-
|
|
15
|
-
|
|
18
|
+
parkStateIds: readonly string[];
|
|
19
|
+
summaryPolicy?: PlaybookSummaryPolicy;
|
|
16
20
|
validateOptions(captainOptions: unknown): unknown;
|
|
17
21
|
createRuntime(options: CreatePlaybookRuntimeOptions): PlaybookRuntime;
|
|
18
22
|
}
|
|
19
|
-
export declare
|
|
20
|
-
export declare function createPlaybookCaptainShell(options: unknown, registry?: readonly PlaybookCaptainRegistryEntry[]): Captain;
|
|
23
|
+
export declare function createPlaybookCaptainShell(options: unknown, deps?: PlaybookCaptainDeps): Captain;
|
|
21
24
|
export default createPlaybookCaptainShell;
|
|
@@ -1,20 +1,13 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
// SPDX-FileCopyrightText: 2026 SubLang International <https://sublang.ai>
|
|
3
|
-
import { codePlaybookRegistryEntry, } from './code.registry.js';
|
|
4
3
|
const SUB_RUNTIME_FSM_TOPIC = 'playbook.fsm.state';
|
|
5
4
|
const SHELL_FSM_TOPIC = 'playbook.captain.fsm.state';
|
|
6
|
-
export const playbookCaptainRegistry = [
|
|
7
|
-
codePlaybookRegistryEntry,
|
|
8
|
-
];
|
|
9
5
|
function parseRegisteredCommand(prompt) {
|
|
10
6
|
const match = /^\/([A-Za-z][A-Za-z0-9_-]*)(?:\s+([\s\S]*))?$/.exec(prompt.trim());
|
|
11
7
|
if (!match)
|
|
12
8
|
return undefined;
|
|
13
9
|
return { command: match[1], text: (match[2] ?? '').trim() };
|
|
14
10
|
}
|
|
15
|
-
function playbookCommandLabel(entry) {
|
|
16
|
-
return `/${entry.command}`;
|
|
17
|
-
}
|
|
18
11
|
function visibleChatEnvelope(message) {
|
|
19
12
|
return [
|
|
20
13
|
'You are the Playbook Captain shell.',
|
|
@@ -23,7 +16,6 @@ function visibleChatEnvelope(message) {
|
|
|
23
16
|
].join('\n\n');
|
|
24
17
|
}
|
|
25
18
|
function visibleTurnSummaryEnvelope(input) {
|
|
26
|
-
const savedLine = savedCountsLine(input.counts, input.reviewRebuttalRounds);
|
|
27
19
|
return [
|
|
28
20
|
'You are the Playbook Captain shell.',
|
|
29
21
|
'This is visible Boss chat after a sub-playbook command completed. Do not reveal hidden control JSON, hidden router decisions, or hidden judge replies.',
|
|
@@ -32,38 +24,24 @@ function visibleTurnSummaryEnvelope(input) {
|
|
|
32
24
|
'State only what was done or what changed; do not explain how it was done.',
|
|
33
25
|
'Do not list raw state names, transitions, guard names, prompts, tools, hidden calls, or reasoning.',
|
|
34
26
|
'If progress detail is useful, use only the aggregate progress phrase supplied below.',
|
|
35
|
-
|
|
36
|
-
`Then write the saved-counts line exactly: ${savedLine}`,
|
|
27
|
+
"Do not mention counts for states the active playbook's summary policy does not label.",
|
|
28
|
+
`Then write the saved-counts line exactly: ${input.savedLine}`,
|
|
37
29
|
'Use the exact counts supplied; do not change them.',
|
|
38
|
-
'Do not repeat the exact
|
|
30
|
+
'Do not repeat the exact progress round count outside the saved-counts line.',
|
|
39
31
|
`Playbook: ${input.playbookId}`,
|
|
40
32
|
`Submitted Boss text:\n${input.submittedText}`,
|
|
41
33
|
`Progress counts:\n${input.progressPhrase}`,
|
|
42
34
|
`Counts:\n${JSON.stringify({
|
|
43
35
|
...input.counts,
|
|
44
|
-
|
|
36
|
+
progressRounds: input.progressRounds,
|
|
45
37
|
})}`,
|
|
46
38
|
].join('\n\n');
|
|
47
39
|
}
|
|
48
|
-
function countNoun(count, singular, plural = `${singular}s`) {
|
|
49
|
-
return `${count} ${count === 1 ? singular : plural}`;
|
|
50
|
-
}
|
|
51
|
-
function savedCountsLine(counts, reviewRebuttalRounds) {
|
|
52
|
-
return [
|
|
53
|
-
'Saved you',
|
|
54
|
-
countNoun(counts.interruptions, 'interruption'),
|
|
55
|
-
'and',
|
|
56
|
-
countNoun(counts.copyPastes, 'copy-paste'),
|
|
57
|
-
'across',
|
|
58
|
-
countNoun(reviewRebuttalRounds, 'round'),
|
|
59
|
-
'of reviews/rebuttals.',
|
|
60
|
-
].join(' ');
|
|
61
|
-
}
|
|
62
40
|
function stateCountLabel(stateId, entry) {
|
|
63
41
|
if (stateId === entry.idleStateId || stateId === entry.finalStateId) {
|
|
64
42
|
return undefined;
|
|
65
43
|
}
|
|
66
|
-
const registryLabel = entry.stateCountLabels?.[stateId]?.trim();
|
|
44
|
+
const registryLabel = entry.summaryPolicy?.stateCountLabels?.[stateId]?.trim();
|
|
67
45
|
return registryLabel || undefined;
|
|
68
46
|
}
|
|
69
47
|
function pluralizeStateCount(label, count) {
|
|
@@ -88,17 +66,102 @@ function summaryProgressRoundCount(stateCounts) {
|
|
|
88
66
|
function guardFromJudgeReply(finalText) {
|
|
89
67
|
return /"guard"\s*:\s*"([^"]+)"/.exec(finalText)?.[1];
|
|
90
68
|
}
|
|
91
|
-
function
|
|
69
|
+
function isValidRegistryEntry(value) {
|
|
70
|
+
if (typeof value !== 'object' || value === null)
|
|
71
|
+
return false;
|
|
72
|
+
const e = value;
|
|
73
|
+
return (typeof e.id === 'string' &&
|
|
74
|
+
typeof e.command === 'string' &&
|
|
75
|
+
typeof e.intent === 'string' &&
|
|
76
|
+
Array.isArray(e.requiredRoleIds) &&
|
|
77
|
+
typeof e.idleStateId === 'string' &&
|
|
78
|
+
typeof e.finalStateId === 'string' &&
|
|
79
|
+
Array.isArray(e.parkStateIds) &&
|
|
80
|
+
typeof e.validateOptions === 'function' &&
|
|
81
|
+
typeof e.createRuntime === 'function');
|
|
82
|
+
}
|
|
83
|
+
function readPlaybooksConfig(options) {
|
|
84
|
+
if (typeof options !== 'object' || options === null)
|
|
85
|
+
return undefined;
|
|
86
|
+
const pb = options.playbooks;
|
|
87
|
+
if (typeof pb !== 'object' || pb === null || Array.isArray(pb)) {
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
return pb;
|
|
91
|
+
}
|
|
92
|
+
// Resolve the active registry at init from `captain.options.playbooks`
|
|
93
|
+
// (CAPTAIN-16): each enabled playbook is loaded from its explicit `from`
|
|
94
|
+
// module and bound to namespaced `<id>-<role>` host players.
|
|
95
|
+
async function buildEnablements(options, players, loadModule) {
|
|
96
|
+
const entries = [];
|
|
92
97
|
const byCommand = new Map();
|
|
93
98
|
const byId = new Map();
|
|
94
|
-
|
|
95
|
-
|
|
99
|
+
const enablementById = new Map();
|
|
100
|
+
const config = readPlaybooksConfig(options);
|
|
101
|
+
if (config === undefined) {
|
|
102
|
+
throw new Error('captain.options.playbooks is required');
|
|
103
|
+
}
|
|
104
|
+
const ids = Object.keys(config);
|
|
105
|
+
if (ids.length === 0) {
|
|
106
|
+
throw new Error('captain.options.playbooks must enable at least one playbook');
|
|
107
|
+
}
|
|
108
|
+
for (const id of ids) {
|
|
109
|
+
const block = config[id];
|
|
110
|
+
if (typeof block !== 'object' || block === null || Array.isArray(block)) {
|
|
111
|
+
throw new Error(`captain.options.playbooks.${id} must be an object`);
|
|
112
|
+
}
|
|
113
|
+
const record = block;
|
|
114
|
+
const from = record.from;
|
|
115
|
+
if (typeof from !== 'string' || from.length === 0) {
|
|
116
|
+
throw new Error(`captain.options.playbooks.${id}.from must be a module specifier`);
|
|
117
|
+
}
|
|
118
|
+
let mod;
|
|
119
|
+
try {
|
|
120
|
+
mod = await loadModule(from);
|
|
121
|
+
}
|
|
122
|
+
catch (cause) {
|
|
123
|
+
throw new Error(`captain.options.playbooks.${id}.from "${from}" failed to import: ${String(cause?.message ?? cause)}`);
|
|
124
|
+
}
|
|
125
|
+
const entry = mod?.default;
|
|
126
|
+
if (!isValidRegistryEntry(entry)) {
|
|
127
|
+
throw new Error(`captain.options.playbooks.${id}.from "${from}" exposes no valid registry entry`);
|
|
128
|
+
}
|
|
129
|
+
if (entry.id !== id) {
|
|
130
|
+
throw new Error(`captain.options.playbooks.${id} key must equal the module manifest id "${entry.id}"`);
|
|
131
|
+
}
|
|
132
|
+
if (byId.has(entry.id)) {
|
|
133
|
+
throw new Error(`captain.options.playbooks has a duplicate playbook id "${entry.id}"`);
|
|
134
|
+
}
|
|
135
|
+
const command = typeof record.command === 'string' && record.command.length > 0
|
|
136
|
+
? record.command
|
|
137
|
+
: entry.command;
|
|
138
|
+
if (byCommand.has(command)) {
|
|
139
|
+
throw new Error(`captain.options.playbooks has a duplicate effective command "${command}"`);
|
|
140
|
+
}
|
|
141
|
+
const boundPlayers = entry.requiredRoleIds.map((role) => {
|
|
142
|
+
const host = players.find((p) => p.id === `${entry.id}-${role}`);
|
|
143
|
+
return { id: role, adapter: host?.adapter, model: host?.model };
|
|
144
|
+
});
|
|
145
|
+
entries.push(entry);
|
|
96
146
|
byId.set(entry.id, entry);
|
|
147
|
+
byCommand.set(command, entry);
|
|
148
|
+
enablementById.set(entry.id, {
|
|
149
|
+
entry,
|
|
150
|
+
command,
|
|
151
|
+
optionInput: record.options,
|
|
152
|
+
boundPlayers,
|
|
153
|
+
hostPlayerId: (localRole) => `${entry.id}-${localRole}`,
|
|
154
|
+
visiblePlayerIds: entry.requiredRoleIds.map((role) => `${entry.id}-${role}`),
|
|
155
|
+
});
|
|
97
156
|
}
|
|
98
|
-
return { entries
|
|
157
|
+
return { entries, byCommand, byId, enablementById };
|
|
99
158
|
}
|
|
100
|
-
export function createPlaybookCaptainShell(options,
|
|
101
|
-
const
|
|
159
|
+
export function createPlaybookCaptainShell(options, deps = {}) {
|
|
160
|
+
const loadModule = deps.loadModule ?? ((specifier) => import(specifier));
|
|
161
|
+
let entries = [];
|
|
162
|
+
let byCommand = new Map();
|
|
163
|
+
let byId = new Map();
|
|
164
|
+
let enablementById = new Map();
|
|
102
165
|
let session;
|
|
103
166
|
let players = [];
|
|
104
167
|
let activeContext;
|
|
@@ -189,8 +252,7 @@ export function createPlaybookCaptainShell(options, registry = playbookCaptainRe
|
|
|
189
252
|
return;
|
|
190
253
|
}
|
|
191
254
|
if (stateId === active.entry.idleStateId ||
|
|
192
|
-
stateId
|
|
193
|
-
stateId === 'awaitBossReply') {
|
|
255
|
+
active.entry.parkStateIds.includes(stateId)) {
|
|
194
256
|
await setMode('engaged.parked', `sub-runtime:${stateId}`);
|
|
195
257
|
}
|
|
196
258
|
};
|
|
@@ -199,7 +261,10 @@ export function createPlaybookCaptainShell(options, registry = playbookCaptainRe
|
|
|
199
261
|
if (!activeContext) {
|
|
200
262
|
throw new Error('callPlayer invoked outside a Boss turn');
|
|
201
263
|
}
|
|
202
|
-
const
|
|
264
|
+
const hostPlayerId = active
|
|
265
|
+
? active.enablement.hostPlayerId(playerId)
|
|
266
|
+
: playerId;
|
|
267
|
+
const result = await activeContext.callPlayer(hostPlayerId, prompt);
|
|
203
268
|
if (activeTurnSummary) {
|
|
204
269
|
activeTurnSummary.counts.interruptions++;
|
|
205
270
|
}
|
|
@@ -224,7 +289,7 @@ export function createPlaybookCaptainShell(options, registry = playbookCaptainRe
|
|
|
224
289
|
}
|
|
225
290
|
const guard = guardFromJudgeReply(result.finalText);
|
|
226
291
|
if (guard &&
|
|
227
|
-
active?.entry.copyPasteGuardNames.includes(guard) &&
|
|
292
|
+
active?.entry.summaryPolicy?.copyPasteGuardNames.includes(guard) &&
|
|
228
293
|
activeTurnSummary) {
|
|
229
294
|
activeTurnSummary.counts.copyPastes++;
|
|
230
295
|
}
|
|
@@ -240,41 +305,53 @@ export function createPlaybookCaptainShell(options, registry = playbookCaptainRe
|
|
|
240
305
|
await requireSession().emitTelemetry(event);
|
|
241
306
|
},
|
|
242
307
|
});
|
|
308
|
+
// CAPTAIN-22: before dispatching to a playbook, request tmux-play
|
|
309
|
+
// visibility for that playbook's generated host players. A pane
|
|
310
|
+
// reconciliation failure is display-only in tmux-play and does not
|
|
311
|
+
// reject; the legacy path carries no generated set and skips this.
|
|
312
|
+
const requestVisibility = async (enablement) => {
|
|
313
|
+
const ids = enablement.visiblePlayerIds;
|
|
314
|
+
if (!ids || ids.length === 0 || !activeContext)
|
|
315
|
+
return;
|
|
316
|
+
await activeContext.setVisiblePlayers(ids);
|
|
317
|
+
};
|
|
243
318
|
const engage = async (entry) => {
|
|
244
319
|
if (active?.entry.id === entry.id)
|
|
245
320
|
return active;
|
|
321
|
+
const enablement = enablementById.get(entry.id);
|
|
246
322
|
const runtime = entry.createRuntime({
|
|
247
|
-
captainOptions:
|
|
248
|
-
players,
|
|
323
|
+
captainOptions: enablement.optionInput,
|
|
324
|
+
players: enablement.boundPlayers,
|
|
249
325
|
});
|
|
250
|
-
active = { entry, runtime };
|
|
326
|
+
active = { entry, enablement, runtime };
|
|
251
327
|
latestSubRuntimeStateId = undefined;
|
|
252
328
|
pendingBossQuestion = undefined;
|
|
253
329
|
lastError = undefined;
|
|
254
330
|
finalDisposalRequested = undefined;
|
|
255
331
|
await setMode('engaged.parked', 'engage', entry.id);
|
|
256
332
|
await runtime.init(createPorts());
|
|
257
|
-
await requireSession().emitStatus(`◇
|
|
333
|
+
await requireSession().emitStatus(`◇ /${enablement.command} started`);
|
|
258
334
|
return active;
|
|
259
335
|
};
|
|
260
336
|
const submitToActive = async (engagement, text, context) => {
|
|
337
|
+
await requestVisibility(engagement.enablement);
|
|
338
|
+
const policy = engagement.entry.summaryPolicy;
|
|
261
339
|
const summaryCounts = {
|
|
262
340
|
interruptions: 0,
|
|
263
341
|
copyPastes: 0,
|
|
264
342
|
};
|
|
265
343
|
const summaryStateCounts = new Map();
|
|
266
344
|
let shouldSummarize = false;
|
|
267
|
-
activeTurnSummary =
|
|
268
|
-
counts: summaryCounts,
|
|
269
|
-
|
|
270
|
-
};
|
|
345
|
+
activeTurnSummary = policy
|
|
346
|
+
? { counts: summaryCounts, stateCounts: summaryStateCounts }
|
|
347
|
+
: undefined;
|
|
271
348
|
await setMode('engaged.driving', 'submit');
|
|
272
349
|
try {
|
|
273
350
|
await engagement.runtime.handleBossInput({
|
|
274
351
|
text,
|
|
275
352
|
signal: context.signal,
|
|
276
353
|
});
|
|
277
|
-
shouldSummarize =
|
|
354
|
+
shouldSummarize = policy !== undefined;
|
|
278
355
|
}
|
|
279
356
|
finally {
|
|
280
357
|
activeTurnSummary = undefined;
|
|
@@ -286,13 +363,15 @@ export function createPlaybookCaptainShell(options, registry = playbookCaptainRe
|
|
|
286
363
|
await setMode('engaged.parked', 'turn.settled');
|
|
287
364
|
}
|
|
288
365
|
}
|
|
289
|
-
if (shouldSummarize) {
|
|
366
|
+
if (shouldSummarize && policy) {
|
|
367
|
+
const progressRounds = summaryProgressRoundCount(summaryStateCounts);
|
|
290
368
|
await callVisibleTurnSummary(context, {
|
|
291
369
|
playbookId: engagement.entry.id,
|
|
292
370
|
submittedText: text,
|
|
293
371
|
counts: summaryCounts,
|
|
294
372
|
progressPhrase: summaryProgressPhrase(summaryStateCounts),
|
|
295
|
-
|
|
373
|
+
progressRounds,
|
|
374
|
+
savedLine: policy.savedCountsLine(summaryCounts, progressRounds),
|
|
296
375
|
});
|
|
297
376
|
}
|
|
298
377
|
};
|
|
@@ -301,7 +380,7 @@ export function createPlaybookCaptainShell(options, registry = playbookCaptainRe
|
|
|
301
380
|
if (!engagement)
|
|
302
381
|
return;
|
|
303
382
|
const playbookId = engagement.entry.id;
|
|
304
|
-
const commandLabel =
|
|
383
|
+
const commandLabel = `/${engagement.enablement.command}`;
|
|
305
384
|
active = undefined;
|
|
306
385
|
finalDisposalRequested = undefined;
|
|
307
386
|
if (reason === 'dispose') {
|
|
@@ -349,7 +428,7 @@ export function createPlaybookCaptainShell(options, registry = playbookCaptainRe
|
|
|
349
428
|
`Ledger:\n${JSON.stringify(ledgerSnapshot())}`,
|
|
350
429
|
`Registry:\n${JSON.stringify(entries.map((entry) => ({
|
|
351
430
|
id: entry.id,
|
|
352
|
-
command: entry.command,
|
|
431
|
+
command: enablementById.get(entry.id)?.command ?? entry.command,
|
|
353
432
|
intent: entry.intent,
|
|
354
433
|
})))}`,
|
|
355
434
|
`Boss message:\n${prompt}`,
|
|
@@ -439,18 +518,20 @@ export function createPlaybookCaptainShell(options, registry = playbookCaptainRe
|
|
|
439
518
|
await routerClarification(context);
|
|
440
519
|
return;
|
|
441
520
|
}
|
|
442
|
-
const dismissedCommandLabel =
|
|
521
|
+
const dismissedCommandLabel = `/${active.enablement.command}`;
|
|
443
522
|
await disposeActive('dismiss');
|
|
444
523
|
await callVisibleChat(context, decision.text ?? `${dismissedCommandLabel} stopped.`);
|
|
445
524
|
};
|
|
446
525
|
const handleRegisteredCommand = async (entry, text, context) => {
|
|
526
|
+
const enablement = enablementById.get(entry.id);
|
|
447
527
|
if (active && active.entry.id !== entry.id) {
|
|
448
|
-
await callVisibleChat(context, `/${active.
|
|
528
|
+
await callVisibleChat(context, `/${active.enablement.command} is already running. Finish or stop it before starting /${enablement.command}.`);
|
|
449
529
|
return;
|
|
450
530
|
}
|
|
451
531
|
const engagement = await engage(entry);
|
|
452
532
|
if (text.length === 0) {
|
|
453
|
-
await
|
|
533
|
+
await requestVisibility(engagement.enablement);
|
|
534
|
+
await callVisibleChat(context, `Ask what task to run with /${enablement.command}.`);
|
|
454
535
|
return;
|
|
455
536
|
}
|
|
456
537
|
await submitToActive(engagement, text, context);
|
|
@@ -459,8 +540,13 @@ export function createPlaybookCaptainShell(options, registry = playbookCaptainRe
|
|
|
459
540
|
async init(initSession) {
|
|
460
541
|
session = initSession;
|
|
461
542
|
players = initSession.players;
|
|
462
|
-
|
|
463
|
-
|
|
543
|
+
const built = await buildEnablements(options, players, loadModule);
|
|
544
|
+
entries = built.entries;
|
|
545
|
+
byCommand = built.byCommand;
|
|
546
|
+
byId = built.byId;
|
|
547
|
+
enablementById = built.enablementById;
|
|
548
|
+
for (const enablement of enablementById.values()) {
|
|
549
|
+
enablement.entry.validateOptions(enablement.optionInput);
|
|
464
550
|
}
|
|
465
551
|
await setMode('chat', 'init');
|
|
466
552
|
},
|