@sublang/playbook 5.0.0 → 6.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 +11 -7
- package/docs/cli.md +38 -35
- package/docs/configuration.md +58 -15
- package/docs/embedding.md +24 -16
- package/package.json +40 -21
- package/reference/sdlc/captain.playbook/captain.playbook.js +2 -0
- package/reference/sdlc/captain.playbook/captain.playbook.ts +2 -0
- package/reference/sdlc/code.md +55 -97
- package/reference/sdlc/code.playbook/code.fsm.d.ts +229 -94
- package/reference/sdlc/code.playbook/code.fsm.introspect.d.ts +26 -44
- package/reference/sdlc/code.playbook/code.fsm.introspect.js +61 -66
- package/reference/sdlc/code.playbook/code.fsm.introspect.ts +100 -149
- package/reference/sdlc/code.playbook/code.fsm.js +587 -1347
- package/reference/sdlc/code.playbook/code.fsm.ts +809 -1650
- package/reference/sdlc/code.playbook/code.gears.md +51 -263
- package/reference/sdlc/code.playbook/code.playbook.d.ts +8 -47
- package/reference/sdlc/code.playbook/code.playbook.js +69 -656
- package/reference/sdlc/code.playbook/code.playbook.ts +90 -867
- package/reference/sdlc/code.playbook/code.registry.d.ts +9 -25
- package/reference/sdlc/code.playbook/code.registry.js +20 -78
- package/reference/sdlc/code.playbook/code.registry.ts +58 -122
- package/reference/sdlc/code.playbook/playbook-captain.js +93 -15
- package/reference/sdlc/code.playbook/playbook-captain.ts +115 -19
- package/reference/sdlc/code.playbook/playbook.config.template.yaml +33 -22
- package/reference/sdlc/decide.md +54 -0
- package/reference/sdlc/decide.playbook/decide.fsm.d.ts +261 -0
- package/reference/sdlc/decide.playbook/decide.fsm.js +894 -0
- package/reference/sdlc/decide.playbook/decide.fsm.ts +1152 -0
- package/reference/sdlc/decide.playbook/decide.gears.md +88 -0
- package/reference/sdlc/decide.playbook/decide.playbook.d.ts +67 -0
- package/reference/sdlc/{discuss.playbook/discuss.playbook.js → decide.playbook/decide.playbook.js} +471 -362
- package/reference/sdlc/{discuss.playbook/discuss.playbook.ts → decide.playbook/decide.playbook.ts} +575 -443
- package/reference/sdlc/decide.playbook/decide.registry.d.ts +41 -0
- package/reference/sdlc/decide.playbook/decide.registry.js +60 -0
- package/reference/sdlc/decide.playbook/decide.registry.ts +125 -0
- package/reference/sdlc/review.md +81 -0
- package/reference/sdlc/review.playbook/review.fsm.d.ts +183 -0
- package/reference/sdlc/review.playbook/review.fsm.js +524 -0
- package/reference/sdlc/review.playbook/review.fsm.ts +652 -0
- package/reference/sdlc/review.playbook/review.gears.md +112 -0
- package/reference/sdlc/review.playbook/review.playbook.d.ts +12 -0
- package/reference/sdlc/review.playbook/review.playbook.js +112 -0
- package/reference/sdlc/review.playbook/review.playbook.ts +201 -0
- package/reference/sdlc/review.playbook/review.registry.d.ts +43 -0
- package/reference/sdlc/review.playbook/review.registry.js +73 -0
- package/reference/sdlc/review.playbook/review.registry.ts +138 -0
- package/slc/gears2fsm.md +13 -4
- package/slc/link.md +48 -2
- package/slc/text2gears.md +22 -2
- package/src/runtime.d.ts +7 -0
- package/src/runtime.ts +12 -0
- package/src/xstate-playbook-runtime.d.ts +9 -2
- package/src/xstate-playbook-runtime.js +255 -21
- package/src/xstate-playbook-runtime.ts +333 -28
- package/src/xstate-runtime.js +25 -0
- package/src/xstate-runtime.ts +51 -0
- package/reference/sdlc/discuss.md +0 -93
- package/reference/sdlc/discuss.playbook/discuss.fsm.d.ts +0 -396
- package/reference/sdlc/discuss.playbook/discuss.fsm.js +0 -2067
- package/reference/sdlc/discuss.playbook/discuss.fsm.ts +0 -2465
- package/reference/sdlc/discuss.playbook/discuss.gears.md +0 -258
- package/reference/sdlc/discuss.playbook/discuss.playbook.d.ts +0 -113
- package/reference/sdlc/discuss.playbook/discuss.registry.d.ts +0 -58
- package/reference/sdlc/discuss.playbook/discuss.registry.js +0 -97
- package/reference/sdlc/discuss.playbook/discuss.registry.ts +0 -153
|
@@ -1,24 +1,4 @@
|
|
|
1
1
|
import { type CodePlaybookOptions, type PlaybookRuntime } from './code.playbook.js';
|
|
2
|
-
export declare const codeCopyPasteGuardNames: readonly ["accepted", "approved", "challengeAccepted", "challengeRejected", "challengesRaised", "changesMadeCode", "changesMadeCodeAndChallenged", "changesMadeMixed", "changesMadeMixedAndChallenged", "changesMadeSpecs", "changesMadeSpecsAndChallenged", "hasFindings", "needsRevision", "noFindings", "noOpenItems"];
|
|
3
|
-
export declare const codeStateCountLabels: {
|
|
4
|
-
readonly adjudicateChallenges: "rebuttal";
|
|
5
|
-
readonly reviewBossCommitSpecs: "review round";
|
|
6
|
-
readonly reviewBossCommitCode: "review round";
|
|
7
|
-
readonly reviewBossCommitMixed: "review round";
|
|
8
|
-
readonly reviewIrTaskCommitSpecs: "review round";
|
|
9
|
-
readonly reviewIrTaskCommitCode: "review round";
|
|
10
|
-
readonly reviewIrTaskCommitMixed: "review round";
|
|
11
|
-
readonly reviewChangesSpecs: "review round";
|
|
12
|
-
readonly reviewChangesCode: "review round";
|
|
13
|
-
readonly reviewChangesMixed: "review round";
|
|
14
|
-
readonly reviewChangesAndChallengesSpecs: "review round";
|
|
15
|
-
readonly reviewChangesAndChallengesCode: "review round";
|
|
16
|
-
readonly reviewChangesAndChallengesMixed: "review round";
|
|
17
|
-
};
|
|
18
|
-
export declare function codeSavedCountsLine(counts: {
|
|
19
|
-
interruptions: number;
|
|
20
|
-
copyPastes: number;
|
|
21
|
-
}, rounds: number): string;
|
|
22
2
|
export interface PlaybookSummaryPolicy {
|
|
23
3
|
stateCountLabels: Readonly<Record<string, string>>;
|
|
24
4
|
copyPasteGuardNames: readonly string[];
|
|
@@ -27,10 +7,6 @@ export interface PlaybookSummaryPolicy {
|
|
|
27
7
|
copyPastes: number;
|
|
28
8
|
}, rounds: number): string;
|
|
29
9
|
}
|
|
30
|
-
export declare const codeSummaryPolicy: PlaybookSummaryPolicy;
|
|
31
|
-
export interface CodeOptions {
|
|
32
|
-
committer?: 'coder' | 'reviewer';
|
|
33
|
-
}
|
|
34
10
|
export interface RegistryPlayer {
|
|
35
11
|
id: string;
|
|
36
12
|
adapter?: string;
|
|
@@ -40,15 +16,23 @@ export interface CreateCodeRuntimeOptions {
|
|
|
40
16
|
captainOptions: unknown;
|
|
41
17
|
players: readonly RegistryPlayer[];
|
|
42
18
|
}
|
|
19
|
+
export type CodeOptions = Readonly<Record<string, never>>;
|
|
43
20
|
export interface CodePlaybookRegistryEntry {
|
|
44
21
|
id: 'code';
|
|
45
22
|
command: 'code';
|
|
46
23
|
intent: string;
|
|
47
|
-
requiredRoleIds: readonly
|
|
24
|
+
requiredRoleIds: readonly ['coder'];
|
|
48
25
|
summaryPolicy: PlaybookSummaryPolicy;
|
|
49
26
|
validateOptions(captainOptions: unknown): CodeOptions;
|
|
50
27
|
createRuntime(options: CreateCodeRuntimeOptions): PlaybookRuntime;
|
|
51
28
|
}
|
|
29
|
+
export declare const codeStateCountLabels: {};
|
|
30
|
+
export declare const codeCopyPasteGuardNames: readonly ["directCommit", "irCommit", "moreTasks", "finalTask"];
|
|
31
|
+
export declare function codeSavedCountsLine(counts: {
|
|
32
|
+
interruptions: number;
|
|
33
|
+
copyPastes: number;
|
|
34
|
+
}, rounds: number): string;
|
|
35
|
+
export declare const codeSummaryPolicy: PlaybookSummaryPolicy;
|
|
52
36
|
export declare function validateCodeOptions(optionSlice: unknown): CodeOptions;
|
|
53
37
|
export declare function createCodeRuntimeOptions({ captainOptions, players, }: CreateCodeRuntimeOptions): CodePlaybookOptions;
|
|
54
38
|
export declare const codePlaybookRegistryEntry: CodePlaybookRegistryEntry;
|
|
@@ -1,55 +1,18 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
// SPDX-FileCopyrightText: 2026 SubLang International <https://sublang.ai>
|
|
3
3
|
import createPlaybookRuntime from './code.playbook.js';
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
|
|
7
|
-
// key, `committer`: an optional Committer-alias player id, one of the
|
|
8
|
-
// baked role ids `coder` / `reviewer`. A valid slice is absent, `{}`, or
|
|
9
|
-
// `{ committer: 'coder' | 'reviewer' }`; every other key is unknown and
|
|
10
|
-
// rejected with a path-named error. A further CODE option shall be
|
|
11
|
-
// introduced as its own higher-numbered item that widens
|
|
12
|
-
// `CODE_OPTION_KEYS`; the validator still fails closed on stray keys.
|
|
13
|
-
const CODE_OPTION_KEYS = new Set(['committer']);
|
|
14
|
-
const COMMITTER_PLAYER_IDS = new Set(['coder', 'reviewer']);
|
|
4
|
+
// REVIEW owns and labels its real review rounds. CODE's two suspended wrapper
|
|
5
|
+
// states only delegate to that child and must not double-count those rounds.
|
|
6
|
+
export const codeStateCountLabels = {};
|
|
15
7
|
export const codeCopyPasteGuardNames = [
|
|
16
|
-
'
|
|
17
|
-
'
|
|
18
|
-
'
|
|
19
|
-
'
|
|
20
|
-
'challengesRaised',
|
|
21
|
-
'changesMadeCode',
|
|
22
|
-
'changesMadeCodeAndChallenged',
|
|
23
|
-
'changesMadeMixed',
|
|
24
|
-
'changesMadeMixedAndChallenged',
|
|
25
|
-
'changesMadeSpecs',
|
|
26
|
-
'changesMadeSpecsAndChallenged',
|
|
27
|
-
'hasFindings',
|
|
28
|
-
'needsRevision',
|
|
29
|
-
'noFindings',
|
|
30
|
-
'noOpenItems',
|
|
8
|
+
'directCommit',
|
|
9
|
+
'irCommit',
|
|
10
|
+
'moreTasks',
|
|
11
|
+
'finalTask',
|
|
31
12
|
];
|
|
32
|
-
export const codeStateCountLabels = {
|
|
33
|
-
adjudicateChallenges: 'rebuttal',
|
|
34
|
-
reviewBossCommitSpecs: 'review round',
|
|
35
|
-
reviewBossCommitCode: 'review round',
|
|
36
|
-
reviewBossCommitMixed: 'review round',
|
|
37
|
-
reviewIrTaskCommitSpecs: 'review round',
|
|
38
|
-
reviewIrTaskCommitCode: 'review round',
|
|
39
|
-
reviewIrTaskCommitMixed: 'review round',
|
|
40
|
-
reviewChangesSpecs: 'review round',
|
|
41
|
-
reviewChangesCode: 'review round',
|
|
42
|
-
reviewChangesMixed: 'review round',
|
|
43
|
-
reviewChangesAndChallengesSpecs: 'review round',
|
|
44
|
-
reviewChangesAndChallengesCode: 'review round',
|
|
45
|
-
reviewChangesAndChallengesMixed: 'review round',
|
|
46
|
-
};
|
|
47
13
|
function countNoun(count, singular, plural = `${singular}s`) {
|
|
48
14
|
return `${count} ${count === 1 ? singular : plural}`;
|
|
49
15
|
}
|
|
50
|
-
// PBRT-15 / CAPTAIN-19: the CODE saved-counts line wording is
|
|
51
|
-
// registry-owned. The shell renders this exact line through the active
|
|
52
|
-
// entry's summary policy rather than hardcoding CODE phrasing.
|
|
53
16
|
export function codeSavedCountsLine(counts, rounds) {
|
|
54
17
|
return [
|
|
55
18
|
'Saved you',
|
|
@@ -68,57 +31,36 @@ export const codeSummaryPolicy = {
|
|
|
68
31
|
};
|
|
69
32
|
export function validateCodeOptions(optionSlice) {
|
|
70
33
|
if (optionSlice === undefined)
|
|
71
|
-
return {};
|
|
72
|
-
if (
|
|
73
|
-
optionSlice
|
|
34
|
+
return Object.freeze({});
|
|
35
|
+
if (optionSlice === null ||
|
|
36
|
+
typeof optionSlice !== 'object' ||
|
|
74
37
|
Array.isArray(optionSlice)) {
|
|
75
38
|
throw new Error('captain.options.playbooks.code.options must be an object');
|
|
76
39
|
}
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
throw new Error(`Unknown config field captain.options.playbooks.code.options.${key}`);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
const options = {};
|
|
84
|
-
const committer = slice.committer;
|
|
85
|
-
if (committer !== undefined) {
|
|
86
|
-
if (typeof committer !== 'string' || !COMMITTER_PLAYER_IDS.has(committer)) {
|
|
87
|
-
throw new Error("captain.options.playbooks.code.options.committer must be " +
|
|
88
|
-
"'coder' or 'reviewer'");
|
|
89
|
-
}
|
|
90
|
-
options.committer = committer;
|
|
40
|
+
const keys = Object.keys(optionSlice);
|
|
41
|
+
if (keys.length > 0) {
|
|
42
|
+
throw new Error(`Unknown config field captain.options.playbooks.code.options.${keys[0]}`);
|
|
91
43
|
}
|
|
92
|
-
return
|
|
44
|
+
return Object.freeze({});
|
|
93
45
|
}
|
|
94
46
|
function playerIdentity(players, id) {
|
|
95
|
-
const
|
|
96
|
-
return
|
|
47
|
+
const player = players.find((entry) => entry.id === id);
|
|
48
|
+
return player?.model ?? player?.adapter;
|
|
97
49
|
}
|
|
98
50
|
export function createCodeRuntimeOptions({ captainOptions, players, }) {
|
|
99
|
-
|
|
51
|
+
validateCodeOptions(captainOptions);
|
|
100
52
|
const coderPlayer = playerIdentity(players, 'coder');
|
|
101
|
-
|
|
102
|
-
return {
|
|
103
|
-
coderPlayer,
|
|
104
|
-
reviewerPlayer,
|
|
105
|
-
...(codeOptions.committer !== undefined
|
|
106
|
-
? { committerPlayer: codeOptions.committer }
|
|
107
|
-
: {}),
|
|
108
|
-
};
|
|
53
|
+
return coderPlayer === undefined ? {} : { coderPlayer };
|
|
109
54
|
}
|
|
110
55
|
export const codePlaybookRegistryEntry = {
|
|
111
56
|
id: 'code',
|
|
112
57
|
command: 'code',
|
|
113
|
-
intent: '
|
|
114
|
-
requiredRoleIds: ['coder'
|
|
58
|
+
intent: 'implement a coding intent in reviewed, one-commit phases, using an intent record when needed',
|
|
59
|
+
requiredRoleIds: ['coder'],
|
|
115
60
|
summaryPolicy: codeSummaryPolicy,
|
|
116
61
|
validateOptions: validateCodeOptions,
|
|
117
62
|
createRuntime(options) {
|
|
118
63
|
return createPlaybookRuntime(createCodeRuntimeOptions(options));
|
|
119
64
|
},
|
|
120
65
|
};
|
|
121
|
-
// CAPTAIN-16 / PBRT-16: the published `@sublang/playbook/code/registry`
|
|
122
|
-
// module's default export is the CODE registry entry the Playbook Captain
|
|
123
|
-
// shell loads when a playbook block's `from` names this module.
|
|
124
66
|
export default codePlaybookRegistryEntry;
|
|
@@ -6,77 +6,6 @@ import createPlaybookRuntime, {
|
|
|
6
6
|
type PlaybookRuntime,
|
|
7
7
|
} from './code.playbook.js';
|
|
8
8
|
|
|
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.
|
|
18
|
-
const CODE_OPTION_KEYS = new Set<string>(['committer']);
|
|
19
|
-
const COMMITTER_PLAYER_IDS = new Set<string>(['coder', 'reviewer']);
|
|
20
|
-
export const codeCopyPasteGuardNames = [
|
|
21
|
-
'accepted',
|
|
22
|
-
'approved',
|
|
23
|
-
'challengeAccepted',
|
|
24
|
-
'challengeRejected',
|
|
25
|
-
'challengesRaised',
|
|
26
|
-
'changesMadeCode',
|
|
27
|
-
'changesMadeCodeAndChallenged',
|
|
28
|
-
'changesMadeMixed',
|
|
29
|
-
'changesMadeMixedAndChallenged',
|
|
30
|
-
'changesMadeSpecs',
|
|
31
|
-
'changesMadeSpecsAndChallenged',
|
|
32
|
-
'hasFindings',
|
|
33
|
-
'needsRevision',
|
|
34
|
-
'noFindings',
|
|
35
|
-
'noOpenItems',
|
|
36
|
-
] as const;
|
|
37
|
-
|
|
38
|
-
export const codeStateCountLabels = {
|
|
39
|
-
adjudicateChallenges: 'rebuttal',
|
|
40
|
-
reviewBossCommitSpecs: 'review round',
|
|
41
|
-
reviewBossCommitCode: 'review round',
|
|
42
|
-
reviewBossCommitMixed: 'review round',
|
|
43
|
-
reviewIrTaskCommitSpecs: 'review round',
|
|
44
|
-
reviewIrTaskCommitCode: 'review round',
|
|
45
|
-
reviewIrTaskCommitMixed: 'review round',
|
|
46
|
-
reviewChangesSpecs: 'review round',
|
|
47
|
-
reviewChangesCode: 'review round',
|
|
48
|
-
reviewChangesMixed: 'review round',
|
|
49
|
-
reviewChangesAndChallengesSpecs: 'review round',
|
|
50
|
-
reviewChangesAndChallengesCode: 'review round',
|
|
51
|
-
reviewChangesAndChallengesMixed: 'review round',
|
|
52
|
-
} as const;
|
|
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
9
|
export interface PlaybookSummaryPolicy {
|
|
81
10
|
stateCountLabels: Readonly<Record<string, string>>;
|
|
82
11
|
copyPasteGuardNames: readonly string[];
|
|
@@ -86,19 +15,6 @@ export interface PlaybookSummaryPolicy {
|
|
|
86
15
|
): string;
|
|
87
16
|
}
|
|
88
17
|
|
|
89
|
-
export const codeSummaryPolicy: PlaybookSummaryPolicy = {
|
|
90
|
-
stateCountLabels: codeStateCountLabels,
|
|
91
|
-
copyPasteGuardNames: codeCopyPasteGuardNames,
|
|
92
|
-
savedCountsLine: codeSavedCountsLine,
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
// The validated CODE options set. `committer`, when present, is the
|
|
96
|
-
// resolved Committer-alias player id (PBRT-8 / PBRT-30); a future CODE
|
|
97
|
-
// option widens both this type and `CODE_OPTION_KEYS`.
|
|
98
|
-
export interface CodeOptions {
|
|
99
|
-
committer?: 'coder' | 'reviewer';
|
|
100
|
-
}
|
|
101
|
-
|
|
102
18
|
export interface RegistryPlayer {
|
|
103
19
|
id: string;
|
|
104
20
|
adapter?: string;
|
|
@@ -110,76 +26,99 @@ export interface CreateCodeRuntimeOptions {
|
|
|
110
26
|
players: readonly RegistryPlayer[];
|
|
111
27
|
}
|
|
112
28
|
|
|
29
|
+
export type CodeOptions = Readonly<Record<string, never>>;
|
|
30
|
+
|
|
113
31
|
export interface CodePlaybookRegistryEntry {
|
|
114
32
|
id: 'code';
|
|
115
33
|
command: 'code';
|
|
116
34
|
intent: string;
|
|
117
|
-
requiredRoleIds: readonly
|
|
35
|
+
requiredRoleIds: readonly ['coder'];
|
|
118
36
|
summaryPolicy: PlaybookSummaryPolicy;
|
|
119
37
|
validateOptions(captainOptions: unknown): CodeOptions;
|
|
120
38
|
createRuntime(options: CreateCodeRuntimeOptions): PlaybookRuntime;
|
|
121
39
|
}
|
|
122
40
|
|
|
41
|
+
// REVIEW owns and labels its real review rounds. CODE's two suspended wrapper
|
|
42
|
+
// states only delegate to that child and must not double-count those rounds.
|
|
43
|
+
export const codeStateCountLabels = {} as const;
|
|
44
|
+
|
|
45
|
+
export const codeCopyPasteGuardNames = [
|
|
46
|
+
'directCommit',
|
|
47
|
+
'irCommit',
|
|
48
|
+
'moreTasks',
|
|
49
|
+
'finalTask',
|
|
50
|
+
] as const;
|
|
51
|
+
|
|
52
|
+
function countNoun(
|
|
53
|
+
count: number,
|
|
54
|
+
singular: string,
|
|
55
|
+
plural = `${singular}s`,
|
|
56
|
+
): string {
|
|
57
|
+
return `${count} ${count === 1 ? singular : plural}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function codeSavedCountsLine(
|
|
61
|
+
counts: { interruptions: number; copyPastes: number },
|
|
62
|
+
rounds: number,
|
|
63
|
+
): string {
|
|
64
|
+
return [
|
|
65
|
+
'Saved you',
|
|
66
|
+
countNoun(counts.interruptions, 'interruption'),
|
|
67
|
+
'and',
|
|
68
|
+
countNoun(counts.copyPastes, 'copy-paste'),
|
|
69
|
+
'across',
|
|
70
|
+
countNoun(rounds, 'round'),
|
|
71
|
+
'of reviews/rebuttals.',
|
|
72
|
+
].join(' ');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export const codeSummaryPolicy: PlaybookSummaryPolicy = {
|
|
76
|
+
stateCountLabels: codeStateCountLabels,
|
|
77
|
+
copyPasteGuardNames: codeCopyPasteGuardNames,
|
|
78
|
+
savedCountsLine: codeSavedCountsLine,
|
|
79
|
+
};
|
|
80
|
+
|
|
123
81
|
export function validateCodeOptions(optionSlice: unknown): CodeOptions {
|
|
124
|
-
if (optionSlice === undefined) return {};
|
|
82
|
+
if (optionSlice === undefined) return Object.freeze({});
|
|
125
83
|
if (
|
|
126
|
-
typeof optionSlice !== 'object' ||
|
|
127
84
|
optionSlice === null ||
|
|
85
|
+
typeof optionSlice !== 'object' ||
|
|
128
86
|
Array.isArray(optionSlice)
|
|
129
87
|
) {
|
|
130
88
|
throw new Error('captain.options.playbooks.code.options must be an object');
|
|
131
89
|
}
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
);
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
const options: CodeOptions = {};
|
|
141
|
-
const committer = slice.committer;
|
|
142
|
-
if (committer !== undefined) {
|
|
143
|
-
if (typeof committer !== 'string' || !COMMITTER_PLAYER_IDS.has(committer)) {
|
|
144
|
-
throw new Error(
|
|
145
|
-
"captain.options.playbooks.code.options.committer must be " +
|
|
146
|
-
"'coder' or 'reviewer'",
|
|
147
|
-
);
|
|
148
|
-
}
|
|
149
|
-
options.committer = committer as 'coder' | 'reviewer';
|
|
90
|
+
const keys = Object.keys(optionSlice);
|
|
91
|
+
if (keys.length > 0) {
|
|
92
|
+
throw new Error(
|
|
93
|
+
`Unknown config field captain.options.playbooks.code.options.${keys[0]}`,
|
|
94
|
+
);
|
|
150
95
|
}
|
|
151
|
-
return
|
|
96
|
+
return Object.freeze({});
|
|
152
97
|
}
|
|
153
98
|
|
|
154
99
|
function playerIdentity(
|
|
155
100
|
players: readonly RegistryPlayer[],
|
|
156
101
|
id: string,
|
|
157
102
|
): string | undefined {
|
|
158
|
-
const
|
|
159
|
-
return
|
|
103
|
+
const player = players.find((entry) => entry.id === id);
|
|
104
|
+
return player?.model ?? player?.adapter;
|
|
160
105
|
}
|
|
161
106
|
|
|
162
107
|
export function createCodeRuntimeOptions({
|
|
163
108
|
captainOptions,
|
|
164
109
|
players,
|
|
165
110
|
}: CreateCodeRuntimeOptions): CodePlaybookOptions {
|
|
166
|
-
|
|
111
|
+
validateCodeOptions(captainOptions);
|
|
167
112
|
const coderPlayer = playerIdentity(players, 'coder');
|
|
168
|
-
|
|
169
|
-
return {
|
|
170
|
-
coderPlayer,
|
|
171
|
-
reviewerPlayer,
|
|
172
|
-
...(codeOptions.committer !== undefined
|
|
173
|
-
? { committerPlayer: codeOptions.committer }
|
|
174
|
-
: {}),
|
|
175
|
-
};
|
|
113
|
+
return coderPlayer === undefined ? {} : { coderPlayer };
|
|
176
114
|
}
|
|
177
115
|
|
|
178
116
|
export const codePlaybookRegistryEntry: CodePlaybookRegistryEntry = {
|
|
179
117
|
id: 'code',
|
|
180
118
|
command: 'code',
|
|
181
|
-
intent:
|
|
182
|
-
|
|
119
|
+
intent:
|
|
120
|
+
'implement a coding intent in reviewed, one-commit phases, using an intent record when needed',
|
|
121
|
+
requiredRoleIds: ['coder'],
|
|
183
122
|
summaryPolicy: codeSummaryPolicy,
|
|
184
123
|
validateOptions: validateCodeOptions,
|
|
185
124
|
createRuntime(options) {
|
|
@@ -187,7 +126,4 @@ export const codePlaybookRegistryEntry: CodePlaybookRegistryEntry = {
|
|
|
187
126
|
},
|
|
188
127
|
};
|
|
189
128
|
|
|
190
|
-
// CAPTAIN-16 / PBRT-16: the published `@sublang/playbook/code/registry`
|
|
191
|
-
// module's default export is the CODE registry entry the Playbook Captain
|
|
192
|
-
// shell loads when a playbook block's `from` names this module.
|
|
193
129
|
export default codePlaybookRegistryEntry;
|
|
@@ -495,7 +495,6 @@ async function buildEnablements(options, players, loadModule) {
|
|
|
495
495
|
optionInput: record.options,
|
|
496
496
|
boundPlayers,
|
|
497
497
|
hostPlayerId: (localRole) => `${entry.id}-${localRole}`,
|
|
498
|
-
visiblePlayerIds: entry.requiredRoleIds.map((role) => `${entry.id}-${role}`),
|
|
499
498
|
});
|
|
500
499
|
}
|
|
501
500
|
return { entries, byCommand, byId, enablementById };
|
|
@@ -555,6 +554,24 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
555
554
|
const rootFrame = () => frames[0];
|
|
556
555
|
const leafFrame = () => frames.at(-1);
|
|
557
556
|
const frameLabel = (frame) => `/${frame.enablement.command}`;
|
|
557
|
+
const bindingFor = (frame, localRole) => {
|
|
558
|
+
const binding = frame.playerBindings.get(localRole);
|
|
559
|
+
if (!binding) {
|
|
560
|
+
throw new Error(`${frameLabel(frame)} resolved undeclared player role ${JSON.stringify(localRole)}`);
|
|
561
|
+
}
|
|
562
|
+
return binding;
|
|
563
|
+
};
|
|
564
|
+
const summaryIncludes = (frame) => {
|
|
565
|
+
const owner = activeTurnSummary?.owner;
|
|
566
|
+
if (!owner)
|
|
567
|
+
return false;
|
|
568
|
+
for (let current = frame; current;) {
|
|
569
|
+
if (current === owner)
|
|
570
|
+
return true;
|
|
571
|
+
current = current.parent?.frame;
|
|
572
|
+
}
|
|
573
|
+
return false;
|
|
574
|
+
};
|
|
558
575
|
const requireSession = () => {
|
|
559
576
|
if (!session) {
|
|
560
577
|
throw new Error('init must be called first');
|
|
@@ -717,7 +734,8 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
717
734
|
return;
|
|
718
735
|
const previousActiveIds = new Set(frame.state?.activeStateIds ?? []);
|
|
719
736
|
frame.state = state;
|
|
720
|
-
|
|
737
|
+
const summary = activeTurnSummary;
|
|
738
|
+
if (summary && summaryIncludes(frame)) {
|
|
721
739
|
for (const stateId of state.activeStateIds) {
|
|
722
740
|
const newlyActive = !previousActiveIds.has(stateId);
|
|
723
741
|
const structuredEntry = stateValueContains(record.to, stateId) &&
|
|
@@ -726,7 +744,7 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
726
744
|
continue;
|
|
727
745
|
const countLabel = stateCountLabel(stateId, frame.entry);
|
|
728
746
|
if (countLabel) {
|
|
729
|
-
|
|
747
|
+
summary.stateCounts.set(countLabel, (summary.stateCounts.get(countLabel) ?? 0) + 1);
|
|
730
748
|
}
|
|
731
749
|
}
|
|
732
750
|
}
|
|
@@ -747,7 +765,7 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
747
765
|
}
|
|
748
766
|
const context = activeContext;
|
|
749
767
|
signal.throwIfAborted();
|
|
750
|
-
const hostPlayerId = frame.
|
|
768
|
+
const hostPlayerId = bindingFor(frame, playerId).hostPlayerId;
|
|
751
769
|
const result = await trackHostCall(frame, context.callPlayer(hostPlayerId, prompt, {
|
|
752
770
|
resume: options.resume,
|
|
753
771
|
}));
|
|
@@ -759,8 +777,9 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
759
777
|
// CAPTAIN-20: only a player call that actually produced work is an
|
|
760
778
|
// interruption the Boss was spared. A call that errored or aborted
|
|
761
779
|
// saved nothing, so it never feeds the saved-counts gate.
|
|
762
|
-
|
|
763
|
-
|
|
780
|
+
const summary = activeTurnSummary;
|
|
781
|
+
if (summary && summaryIncludes(frame) && result.status === 'ok') {
|
|
782
|
+
summary.counts.interruptions++;
|
|
764
783
|
}
|
|
765
784
|
return {
|
|
766
785
|
status: result.status,
|
|
@@ -806,10 +825,12 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
806
825
|
throw new Error('callCaptain returned status=ok with no finalText');
|
|
807
826
|
}
|
|
808
827
|
const guard = guardFromJudgeReply(result.finalText);
|
|
828
|
+
const summary = activeTurnSummary;
|
|
809
829
|
if (guard &&
|
|
810
|
-
|
|
830
|
+
summary &&
|
|
831
|
+
summaryIncludes(frame) &&
|
|
811
832
|
frame.entry.summaryPolicy?.copyPasteGuardNames.includes(guard)) {
|
|
812
|
-
|
|
833
|
+
summary.counts.copyPastes++;
|
|
813
834
|
}
|
|
814
835
|
return result.finalText;
|
|
815
836
|
},
|
|
@@ -841,8 +862,8 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
841
862
|
// visibility for that playbook's generated host players. A pane
|
|
842
863
|
// reconciliation failure is display-only in tmux-play and does not
|
|
843
864
|
// reject; the legacy path carries no generated set and skips this.
|
|
844
|
-
const requestVisibility = async (
|
|
845
|
-
const ids =
|
|
865
|
+
const requestVisibility = async (frame) => {
|
|
866
|
+
const ids = [...new Set([...frame.playerBindings.values()].map(({ hostPlayerId }) => hostPlayerId))];
|
|
846
867
|
if (!ids || ids.length === 0 || !activeContext)
|
|
847
868
|
return;
|
|
848
869
|
try {
|
|
@@ -878,9 +899,30 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
878
899
|
const makeFrame = (enablement, parent) => {
|
|
879
900
|
const entry = enablement.entry;
|
|
880
901
|
const sessionId = allocateSessionId();
|
|
902
|
+
const playerBindings = new Map();
|
|
903
|
+
for (const role of entry.requiredRoleIds) {
|
|
904
|
+
let inherited;
|
|
905
|
+
for (let ancestor = parent?.frame; ancestor && inherited === undefined; ancestor = ancestor.parent?.frame) {
|
|
906
|
+
inherited = ancestor.playerBindings.get(role);
|
|
907
|
+
}
|
|
908
|
+
if (inherited) {
|
|
909
|
+
playerBindings.set(role, inherited);
|
|
910
|
+
continue;
|
|
911
|
+
}
|
|
912
|
+
const configured = enablement.boundPlayers.find((player) => player.id === role) ?? { id: role };
|
|
913
|
+
playerBindings.set(role, {
|
|
914
|
+
hostPlayerId: enablement.hostPlayerId(role),
|
|
915
|
+
player: configured,
|
|
916
|
+
});
|
|
917
|
+
}
|
|
918
|
+
const playerResumeTokens = parent?.frame.playerResumeTokens ?? new Map();
|
|
881
919
|
const runtime = entry.createRuntime({
|
|
882
920
|
captainOptions: enablement.optionInput,
|
|
883
|
-
players:
|
|
921
|
+
players: [...playerBindings].map(([role, { player }]) => ({
|
|
922
|
+
id: role,
|
|
923
|
+
...(player.adapter === undefined ? {} : { adapter: player.adapter }),
|
|
924
|
+
...(player.model === undefined ? {} : { model: player.model }),
|
|
925
|
+
})),
|
|
884
926
|
});
|
|
885
927
|
return {
|
|
886
928
|
entry,
|
|
@@ -889,10 +931,45 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
889
931
|
sessionId,
|
|
890
932
|
rootSessionId: parent?.frame.rootSessionId ?? sessionId,
|
|
891
933
|
depth: parent ? parent.frame.depth + 1 : 0,
|
|
934
|
+
playerBindings,
|
|
935
|
+
playerResumeTokens,
|
|
892
936
|
...(parent ? { parent } : {}),
|
|
893
937
|
inFlightHostCalls: new Set(),
|
|
894
938
|
};
|
|
895
939
|
};
|
|
940
|
+
const playerSessionStore = (frame) => ({
|
|
941
|
+
select(playerId) {
|
|
942
|
+
const binding = bindingFor(frame, playerId);
|
|
943
|
+
return frame.playerResumeTokens.get(binding.hostPlayerId) ?? false;
|
|
944
|
+
},
|
|
945
|
+
update(playerId, resumeToken) {
|
|
946
|
+
const binding = bindingFor(frame, playerId);
|
|
947
|
+
if (resumeToken === undefined) {
|
|
948
|
+
frame.playerResumeTokens.delete(binding.hostPlayerId);
|
|
949
|
+
}
|
|
950
|
+
else {
|
|
951
|
+
frame.playerResumeTokens.set(binding.hostPlayerId, resumeToken);
|
|
952
|
+
}
|
|
953
|
+
},
|
|
954
|
+
snapshot() {
|
|
955
|
+
const tokens = {};
|
|
956
|
+
for (const [playerId, binding] of frame.playerBindings) {
|
|
957
|
+
const token = frame.playerResumeTokens.get(binding.hostPlayerId);
|
|
958
|
+
if (token !== undefined)
|
|
959
|
+
tokens[playerId] = token;
|
|
960
|
+
}
|
|
961
|
+
return tokens;
|
|
962
|
+
},
|
|
963
|
+
restore(tokens) {
|
|
964
|
+
for (const binding of frame.playerBindings.values()) {
|
|
965
|
+
frame.playerResumeTokens.delete(binding.hostPlayerId);
|
|
966
|
+
}
|
|
967
|
+
for (const [playerId, token] of Object.entries(tokens)) {
|
|
968
|
+
const binding = bindingFor(frame, playerId);
|
|
969
|
+
frame.playerResumeTokens.set(binding.hostPlayerId, token);
|
|
970
|
+
}
|
|
971
|
+
},
|
|
972
|
+
});
|
|
896
973
|
const initFrame = async (frame) => {
|
|
897
974
|
await frame.runtime.init({
|
|
898
975
|
sessionId: frame.sessionId,
|
|
@@ -905,6 +982,7 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
905
982
|
}
|
|
906
983
|
: {}),
|
|
907
984
|
depth: frame.depth,
|
|
985
|
+
playerSessions: playerSessionStore(frame),
|
|
908
986
|
ports: createPorts(frame),
|
|
909
987
|
});
|
|
910
988
|
};
|
|
@@ -1076,7 +1154,7 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
1076
1154
|
}
|
|
1077
1155
|
let visibilityError;
|
|
1078
1156
|
try {
|
|
1079
|
-
await requestVisibility(parent
|
|
1157
|
+
await requestVisibility(parent);
|
|
1080
1158
|
}
|
|
1081
1159
|
catch (error) {
|
|
1082
1160
|
visibilityError = error;
|
|
@@ -1184,7 +1262,7 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
1184
1262
|
// boundary — a `setVisiblePlayers` or telemetry rejection here leaves the
|
|
1185
1263
|
// runtime uninvoked and owes the Boss the CAPTAIN-34 reply rather than an
|
|
1186
1264
|
// exception filed against an effect that never ran.
|
|
1187
|
-
await requestVisibility(frame
|
|
1265
|
+
await requestVisibility(frame);
|
|
1188
1266
|
await setMode('engaged.driving', 'submit');
|
|
1189
1267
|
const result = await runEffect(() => frame.runtime.handleBossInput({ text, signal }));
|
|
1190
1268
|
frame.state = result.state;
|
|
@@ -1306,7 +1384,7 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
1306
1384
|
cleanupError = error;
|
|
1307
1385
|
}
|
|
1308
1386
|
if (frames.includes(parent)) {
|
|
1309
|
-
await requestVisibility(parent
|
|
1387
|
+
await requestVisibility(parent);
|
|
1310
1388
|
}
|
|
1311
1389
|
if (cleanupError !== undefined)
|
|
1312
1390
|
throw cleanupError;
|
|
@@ -1418,7 +1496,7 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
1418
1496
|
}
|
|
1419
1497
|
if (frames.includes(parent)) {
|
|
1420
1498
|
try {
|
|
1421
|
-
await requestVisibility(parent
|
|
1499
|
+
await requestVisibility(parent);
|
|
1422
1500
|
}
|
|
1423
1501
|
catch (visibilityError) {
|
|
1424
1502
|
visibilityControlFailure = true;
|