@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.
Files changed (65) hide show
  1. package/README.md +11 -7
  2. package/docs/cli.md +38 -35
  3. package/docs/configuration.md +58 -15
  4. package/docs/embedding.md +24 -16
  5. package/package.json +40 -21
  6. package/reference/sdlc/captain.playbook/captain.playbook.js +2 -0
  7. package/reference/sdlc/captain.playbook/captain.playbook.ts +2 -0
  8. package/reference/sdlc/code.md +55 -97
  9. package/reference/sdlc/code.playbook/code.fsm.d.ts +229 -94
  10. package/reference/sdlc/code.playbook/code.fsm.introspect.d.ts +26 -44
  11. package/reference/sdlc/code.playbook/code.fsm.introspect.js +61 -66
  12. package/reference/sdlc/code.playbook/code.fsm.introspect.ts +100 -149
  13. package/reference/sdlc/code.playbook/code.fsm.js +587 -1347
  14. package/reference/sdlc/code.playbook/code.fsm.ts +809 -1650
  15. package/reference/sdlc/code.playbook/code.gears.md +51 -263
  16. package/reference/sdlc/code.playbook/code.playbook.d.ts +8 -47
  17. package/reference/sdlc/code.playbook/code.playbook.js +69 -656
  18. package/reference/sdlc/code.playbook/code.playbook.ts +90 -867
  19. package/reference/sdlc/code.playbook/code.registry.d.ts +9 -25
  20. package/reference/sdlc/code.playbook/code.registry.js +20 -78
  21. package/reference/sdlc/code.playbook/code.registry.ts +58 -122
  22. package/reference/sdlc/code.playbook/playbook-captain.js +93 -15
  23. package/reference/sdlc/code.playbook/playbook-captain.ts +115 -19
  24. package/reference/sdlc/code.playbook/playbook.config.template.yaml +33 -22
  25. package/reference/sdlc/decide.md +54 -0
  26. package/reference/sdlc/decide.playbook/decide.fsm.d.ts +261 -0
  27. package/reference/sdlc/decide.playbook/decide.fsm.js +894 -0
  28. package/reference/sdlc/decide.playbook/decide.fsm.ts +1152 -0
  29. package/reference/sdlc/decide.playbook/decide.gears.md +88 -0
  30. package/reference/sdlc/decide.playbook/decide.playbook.d.ts +67 -0
  31. package/reference/sdlc/{discuss.playbook/discuss.playbook.js → decide.playbook/decide.playbook.js} +471 -362
  32. package/reference/sdlc/{discuss.playbook/discuss.playbook.ts → decide.playbook/decide.playbook.ts} +575 -443
  33. package/reference/sdlc/decide.playbook/decide.registry.d.ts +41 -0
  34. package/reference/sdlc/decide.playbook/decide.registry.js +60 -0
  35. package/reference/sdlc/decide.playbook/decide.registry.ts +125 -0
  36. package/reference/sdlc/review.md +81 -0
  37. package/reference/sdlc/review.playbook/review.fsm.d.ts +183 -0
  38. package/reference/sdlc/review.playbook/review.fsm.js +524 -0
  39. package/reference/sdlc/review.playbook/review.fsm.ts +652 -0
  40. package/reference/sdlc/review.playbook/review.gears.md +112 -0
  41. package/reference/sdlc/review.playbook/review.playbook.d.ts +12 -0
  42. package/reference/sdlc/review.playbook/review.playbook.js +112 -0
  43. package/reference/sdlc/review.playbook/review.playbook.ts +201 -0
  44. package/reference/sdlc/review.playbook/review.registry.d.ts +43 -0
  45. package/reference/sdlc/review.playbook/review.registry.js +73 -0
  46. package/reference/sdlc/review.playbook/review.registry.ts +138 -0
  47. package/slc/gears2fsm.md +13 -4
  48. package/slc/link.md +48 -2
  49. package/slc/text2gears.md +22 -2
  50. package/src/runtime.d.ts +7 -0
  51. package/src/runtime.ts +12 -0
  52. package/src/xstate-playbook-runtime.d.ts +9 -2
  53. package/src/xstate-playbook-runtime.js +255 -21
  54. package/src/xstate-playbook-runtime.ts +333 -28
  55. package/src/xstate-runtime.js +25 -0
  56. package/src/xstate-runtime.ts +51 -0
  57. package/reference/sdlc/discuss.md +0 -93
  58. package/reference/sdlc/discuss.playbook/discuss.fsm.d.ts +0 -396
  59. package/reference/sdlc/discuss.playbook/discuss.fsm.js +0 -2067
  60. package/reference/sdlc/discuss.playbook/discuss.fsm.ts +0 -2465
  61. package/reference/sdlc/discuss.playbook/discuss.gears.md +0 -258
  62. package/reference/sdlc/discuss.playbook/discuss.playbook.d.ts +0 -113
  63. package/reference/sdlc/discuss.playbook/discuss.registry.d.ts +0 -58
  64. package/reference/sdlc/discuss.playbook/discuss.registry.js +0 -97
  65. 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 string[];
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
- // PBRT-30: the CODE registry entry validates the option slice the shell
5
- // passes it (`captain.options.playbooks.code.options`), not a namespace
6
- // it extracts from the full Captain options bag. The schema defines one
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
- 'accepted',
17
- 'approved',
18
- 'challengeAccepted',
19
- 'challengeRejected',
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 (typeof optionSlice !== 'object' ||
73
- optionSlice === null ||
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 slice = optionSlice;
78
- for (const key of Object.keys(slice)) {
79
- if (!CODE_OPTION_KEYS.has(key)) {
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 options;
44
+ return Object.freeze({});
93
45
  }
94
46
  function playerIdentity(players, id) {
95
- const entry = players.find((p) => p.id === id);
96
- return entry?.model ?? entry?.adapter;
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
- const codeOptions = validateCodeOptions(captainOptions);
51
+ validateCodeOptions(captainOptions);
100
52
  const coderPlayer = playerIdentity(players, 'coder');
101
- const reviewerPlayer = playerIdentity(players, 'reviewer');
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: 'software development / SDLC coding workflow',
114
- requiredRoleIds: ['coder', 'reviewer'],
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 string[];
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 slice = optionSlice as Record<string, unknown>;
133
- for (const key of Object.keys(slice)) {
134
- if (!CODE_OPTION_KEYS.has(key)) {
135
- throw new Error(
136
- `Unknown config field captain.options.playbooks.code.options.${key}`,
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 options;
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 entry = players.find((p) => p.id === id);
159
- return entry?.model ?? entry?.adapter;
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
- const codeOptions = validateCodeOptions(captainOptions);
111
+ validateCodeOptions(captainOptions);
167
112
  const coderPlayer = playerIdentity(players, 'coder');
168
- const reviewerPlayer = playerIdentity(players, 'reviewer');
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: 'software development / SDLC coding workflow',
182
- requiredRoleIds: ['coder', 'reviewer'],
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
- if (activeTurnSummary?.owner === frame) {
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
- activeTurnSummary.stateCounts.set(countLabel, (activeTurnSummary.stateCounts.get(countLabel) ?? 0) + 1);
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.enablement.hostPlayerId(playerId);
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
- if (activeTurnSummary?.owner === frame && result.status === 'ok') {
763
- activeTurnSummary.counts.interruptions++;
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
- activeTurnSummary?.owner === frame &&
830
+ summary &&
831
+ summaryIncludes(frame) &&
811
832
  frame.entry.summaryPolicy?.copyPasteGuardNames.includes(guard)) {
812
- activeTurnSummary.counts.copyPastes++;
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 (enablement) => {
845
- const ids = enablement.visiblePlayerIds;
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: enablement.boundPlayers,
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.enablement);
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.enablement);
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.enablement);
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.enablement);
1499
+ await requestVisibility(parent);
1422
1500
  }
1423
1501
  catch (visibilityError) {
1424
1502
  visibilityControlFailure = true;