@sublang/playbook 0.4.2 → 0.5.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 CHANGED
@@ -82,7 +82,10 @@ playbook-code --help
82
82
  ```
83
83
 
84
84
  The seed template runs each agent in cligent's protected auto mode
85
- (`permissions.mode: auto`), suppressing routine approval prompts.
85
+ (`permissions.mode: auto`), suppressing routine approval prompts. Its
86
+ Codex Coder also grants `permissions.writablePaths: [.git]` so git
87
+ metadata writes stay available under auto mode without switching to
88
+ bypass permissions.
86
89
 
87
90
  ### Configure agents
88
91
 
@@ -95,20 +98,26 @@ $EDITOR "${XDG_CONFIG_HOME:-$HOME/.config}/playbook/playbook-code.config.yaml"
95
98
  Both CODE players can use `claude` or `codex`; other adapter ids are
96
99
  passed through to `tmux-play` with a warning because `playbook-code`
97
100
  does not know how to preflight their auth. The safe tuning points are
98
- `captain.adapter`, `captain.model`, each player's `adapter`, and each
99
- player's `model`. Keep `captain.from` and the `players[].id` values
100
- fixed; the runtime binds to those host-configuration invariants per
101
- [PBRT-4](specs/user/playbook-runtime.md#pbrt-4) and derives the
102
- `<coder-llm>` / `<reviewer-llm>` substitution strings from each
103
- player entry's `model` when pinned and `adapter` otherwise — so the
104
- Committer's commit-message trailers can name the concrete model
105
- (e.g. `claude-opus-4-7`) rather than the adapter family (`claude`).
106
-
107
- For example, this swaps the Coder to Codex and the Reviewer to Claude:
101
+ `captain.adapter`, `captain.model`, and each role's `adapter` and
102
+ `model` under `players.coder` / `players.reviewer`. The composer owns
103
+ `captain.from` and the `coder` / `reviewer` role keys, so leave those
104
+ keys as-is; the runtime binds to those host-configuration invariants
105
+ per [PBRT-4](specs/user/playbook-runtime.md#pbrt-4) and derives the
106
+ `<coder-llm>` / `<reviewer-llm>` substitution strings from each role's
107
+ `model` when pinned and `adapter` otherwise — so the Committer's
108
+ commit-message trailers can name the concrete model
109
+ (e.g. `claude-opus-4-8`) rather than the adapter family (`claude`).
110
+
111
+ `players.committer` is an optional alias naming which role — `coder`
112
+ or `reviewer` — runs the commit turn; the seeded overlay points it at
113
+ the Reviewer. Absent the alias the Committer falls back to the Coder
114
+ ([PBRT-8](specs/user/playbook-runtime.md#pbrt-8)).
115
+
116
+ For example, the seeded overlay runs the Coder on Codex and the
117
+ Reviewer on Claude, with the Committer aliased to the Reviewer:
108
118
 
109
119
  ```yaml
110
120
  captain:
111
- from: "@sublang/playbook/code/tmux-play"
112
121
  adapter: claude
113
122
  model: claude-sonnet-4-6
114
123
  reasoningEffort: high
@@ -116,18 +125,21 @@ captain:
116
125
  mode: auto
117
126
 
118
127
  players:
119
- - id: coder # must stay `coder` — see PBRT-4
128
+ coder: # role key must stay `coder` — see PBRT-4
120
129
  adapter: codex
121
130
  model: gpt-5.5
122
131
  reasoningEffort: xhigh
123
132
  permissions:
124
133
  mode: auto
125
- - id: reviewer # must stay `reviewer` — see PBRT-4
134
+ writablePaths:
135
+ - .git # allow git metadata writes under Codex auto mode
136
+ reviewer: # role key must stay `reviewer` — see PBRT-4
126
137
  adapter: claude
127
- model: claude-opus-4-7
138
+ model: claude-opus-4-8
128
139
  reasoningEffort: xhigh
129
140
  permissions:
130
141
  mode: auto
142
+ committer: reviewer # which role commits — `coder` or `reviewer`
131
143
  ```
132
144
 
133
145
  Normal `playbook-code` runs use the seeded path above. If you need a
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sublang/playbook",
3
- "version": "0.4.2",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "description": "Reference CODE playbook — XState v5 FSM, host-agnostic runtime, and tmux-play adapter for a coder/reviewer/committer loop driven by GEARS spec items.",
6
6
  "license": "Apache-2.0",
@@ -41,7 +41,7 @@
41
41
  "reference/sdlc/code.playbook/tmux-play.config.yaml",
42
42
  "reference/sdlc/code.playbook/tmux-play.production.config.yaml",
43
43
  "reference/sdlc/code.playbook/playbook-code.config.template.yaml",
44
- "reference/sdlc/code.playbook/bin"
44
+ "reference/sdlc/code.playbook/bin/playbook-code.js"
45
45
  ],
46
46
  "bin": {
47
47
  "playbook-code": "reference/sdlc/code.playbook/bin/playbook-code.js"
@@ -58,7 +58,8 @@
58
58
  },
59
59
  "scripts": {
60
60
  "build": "tsc",
61
- "test": "vitest run"
61
+ "test": "vitest run",
62
+ "playbook-code": "node reference/sdlc/code.playbook/bin/playbook-code.js"
62
63
  },
63
64
  "publishConfig": {
64
65
  "access": "public",
@@ -68,7 +69,8 @@
68
69
  "@anthropic-ai/claude-agent-sdk": "^0.3.143",
69
70
  "@openai/codex-sdk": "^0.130.0",
70
71
  "@sublang/cligent": "latest",
71
- "xstate": "^5.19.4"
72
+ "xstate": "^5.19.4",
73
+ "yaml": "^2.9.0"
72
74
  },
73
75
  "devDependencies": {
74
76
  "@types/node": "^22.0.0",
@@ -8,16 +8,39 @@ import {
8
8
  copyFileSync,
9
9
  existsSync,
10
10
  mkdirSync,
11
+ mkdtempSync,
11
12
  readFileSync,
12
13
  realpathSync,
14
+ rmSync,
15
+ writeFileSync,
13
16
  } from 'node:fs';
14
- import { homedir } from 'node:os';
17
+ import { homedir, tmpdir } from 'node:os';
15
18
  import { dirname, join, resolve } from 'node:path';
16
19
  import { fileURLToPath } from 'node:url';
20
+ import {
21
+ findTmuxPlayConfig,
22
+ loadTmuxPlayConfig,
23
+ } from '@sublang/cligent/tmux-play';
24
+ import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
17
25
 
18
26
  const here = dirname(fileURLToPath(import.meta.url));
19
27
  const templatePath = resolve(here, '..', 'playbook-code.config.template.yaml');
20
28
  const READINESS_FAILURE_EXIT_CODE = 2;
29
+ const COMPOSITION_FAILURE_EXIT_CODE = 1;
30
+
31
+ // PBCODE-16: the composer injects `captain.from` (the CODE adapter
32
+ // module) and the `coder` / `reviewer` player ids, so the user-edited
33
+ // overlay carries neither.
34
+ export const CODE_ADAPTER_MODULE = '@sublang/playbook/code/tmux-play';
35
+ const CODE_ROLES = ['coder', 'reviewer'];
36
+ // Accepted `players` keys: the two fixed CODE roles plus the optional
37
+ // `committer` alias (a string naming one of the roles); PBCODE-17.
38
+ const CODE_PLAYER_KEYS = [...CODE_ROLES, 'committer'];
39
+ // Captain-judge fields inherited from a base config when the overlay
40
+ // leaves them unset (PBCODE-16); `adapter` is handled separately
41
+ // because it is required in the composed config.
42
+ const CAPTAIN_INHERITED_FIELDS = ['model', 'reasoningEffort', 'permissions'];
43
+ const PLAYER_FIELDS = ['model', 'reasoningEffort', 'permissions'];
21
44
 
22
45
  export async function runPlaybookCodeCli(options = {}) {
23
46
  const argv = [...(options.argv ?? process.argv.slice(2))];
@@ -27,6 +50,8 @@ export async function runPlaybookCodeCli(options = {}) {
27
50
  const spawnFn = options.spawn ?? spawn;
28
51
  const tmuxPlayBin = options.tmuxPlayBin ?? resolveTmuxPlayBin();
29
52
  const home = options.homeDir ?? env.HOME ?? homedir();
53
+ const cwd = options.cwd ?? process.cwd();
54
+ const configHome = resolveConfigHome(env, home);
30
55
  const userConfigPath = resolveUserConfigPath(env, home);
31
56
 
32
57
  if (argv.includes('--help') || argv.includes('-h')) {
@@ -34,92 +59,226 @@ export async function runPlaybookCodeCli(options = {}) {
34
59
  return { code: 0 };
35
60
  }
36
61
 
37
- const explicitConfig = hasExplicitConfig(argv);
38
- const configPath = explicitConfig ? undefined : userConfigPath;
62
+ // PBCODE-1: explicit `--config <path>` bypasses seeding, readiness,
63
+ // and composition the path is launched verbatim.
64
+ if (hasExplicitConfig(argv)) {
65
+ return await launchTmuxPlay(spawnFn, [tmuxPlayBin, ...argv], stderr);
66
+ }
39
67
 
40
- if (!explicitConfig) {
41
- seedUserConfigIfMissing(userConfigPath, stderr);
42
- const readiness = checkReadiness(userConfigPath, env, home);
43
- for (const adapter of readiness.unknownAdapters) {
44
- stderr.write(
45
- `playbook-code: warning: no readiness check for adapter "${adapter}"\n`,
46
- );
47
- }
48
- if (readiness.failingAdapters.length > 0) {
49
- stderr.write(
50
- helpText({
51
- userConfigPath,
52
- failingAdapters: readiness.failingAdapters,
53
- }),
54
- );
55
- return { code: READINESS_FAILURE_EXIT_CODE };
56
- }
68
+ seedUserConfigIfMissing(userConfigPath, stderr);
69
+
70
+ // PBCODE-16/17: compose the launched config from the overlay plus an
71
+ // optional base tmux-play config. Composition failures (missing role,
72
+ // non-CODE role id, missing `captain.adapter`) surface a path-named
73
+ // error and abort before launch.
74
+ let composed;
75
+ try {
76
+ composed = await composeLaunchConfig({
77
+ overlayPath: userConfigPath,
78
+ cwd,
79
+ configHome,
80
+ });
81
+ } catch (error) {
82
+ stderr.write(`playbook-code: ${errorMessage(error)}\n`);
83
+ return { code: COMPOSITION_FAILURE_EXIT_CODE };
57
84
  }
58
85
 
59
- const childArgs = explicitConfig
60
- ? [tmuxPlayBin, ...argv]
61
- : [tmuxPlayBin, '--config', configPath, ...argv];
62
- return await launchTmuxPlay(spawnFn, childArgs, stderr);
86
+ // PBCODE-8: readiness reads the adapters of the composed config —
87
+ // including a captain adapter that may have been inherited from the
88
+ // base not the raw overlay.
89
+ const readiness = checkReadiness(
90
+ adaptersFromComposedConfig(composed),
91
+ env,
92
+ home,
93
+ );
94
+ for (const adapter of readiness.unknownAdapters) {
95
+ stderr.write(
96
+ `playbook-code: warning: no readiness check for adapter "${adapter}"\n`,
97
+ );
98
+ }
99
+ if (readiness.failingAdapters.length > 0) {
100
+ stderr.write(
101
+ helpText({
102
+ userConfigPath,
103
+ failingAdapters: readiness.failingAdapters,
104
+ }),
105
+ );
106
+ return { code: READINESS_FAILURE_EXIT_CODE };
107
+ }
108
+
109
+ // PBCODE-16: materialize the composed config to a temp file, launch
110
+ // against it, and remove it before the shim exits — on normal exit,
111
+ // on non-zero child exit, and before re-raising a forwarded signal.
112
+ const { dir: tempDir, path: composedPath } = writeComposedConfig(composed);
113
+ try {
114
+ return await launchTmuxPlay(
115
+ spawnFn,
116
+ [tmuxPlayBin, '--config', composedPath, ...argv],
117
+ stderr,
118
+ );
119
+ } finally {
120
+ rmSync(tempDir, { recursive: true, force: true });
121
+ }
122
+ }
123
+
124
+ export function resolveConfigHome(env = process.env, home = homedir()) {
125
+ return env.XDG_CONFIG_HOME || join(home, '.config');
63
126
  }
64
127
 
65
128
  export function resolveUserConfigPath(env = process.env, home = homedir()) {
66
- const configHome = env.XDG_CONFIG_HOME || join(home, '.config');
67
- return join(configHome, 'playbook', 'playbook-code.config.yaml');
129
+ return join(resolveConfigHome(env, home), 'playbook', 'playbook-code.config.yaml');
68
130
  }
69
131
 
70
- export function collectAdaptersFromConfig(source) {
71
- const adapters = new Set();
72
- let section = '';
73
- let captainChildIndent;
74
- let playerItemIndent;
75
- let playerChildIndent;
76
-
77
- for (const line of source.split(/\r?\n/)) {
78
- const uncommented = stripYamlComment(line);
79
- if (uncommented.trim() === '') continue;
80
-
81
- const indent = leadingSpaceCount(uncommented);
82
- const trimmed = uncommented.trim();
83
-
84
- if (indent === 0) {
85
- if (trimmed === 'captain:') section = 'captain';
86
- else if (trimmed === 'players:') section = 'players';
87
- else section = '';
88
- captainChildIndent = undefined;
89
- playerItemIndent = undefined;
90
- playerChildIndent = undefined;
91
- continue;
132
+ // PBCODE-17: read the overlay, locate an optional base tmux-play config
133
+ // with cligent's exported `findTmuxPlayConfig` (never the bare
134
+ // `loadTmuxPlayConfig`, which writes a default config when none is
135
+ // found), load the base only when a path is located, and compose.
136
+ async function composeLaunchConfig({ overlayPath, cwd, configHome }) {
137
+ const overlay = parseYaml(readFileSync(overlayPath, 'utf8')) ?? {};
138
+ const basePath = findTmuxPlayConfig(cwd, configHome);
139
+ let base;
140
+ if (basePath) {
141
+ base = (await loadTmuxPlayConfig({ configPath: basePath })).config;
142
+ // cligent's loader normalizes a discovered base to captain / players
143
+ // / theme and drops host fields it does not model yet — notably
144
+ // `layout`. Recover a base `layout` from the raw base YAML so base
145
+ // inheritance (PBCODE-16/17) holds; the `=== undefined` guard makes
146
+ // this inert once a future loader preserves `layout` itself.
147
+ if (base && base.layout === undefined) {
148
+ const rawBase = parseYaml(readFileSync(basePath, 'utf8'));
149
+ if (isObject(rawBase) && rawBase.layout !== undefined) {
150
+ base.layout = rawBase.layout;
151
+ }
92
152
  }
153
+ }
154
+ return composeRuntimeConfig(overlay, base);
155
+ }
93
156
 
94
- if (section === 'captain') {
95
- captainChildIndent ??= indent;
96
- if (indent === captainChildIndent) {
97
- collectAdapterValue(trimmed, adapters);
98
- }
99
- continue;
157
+ /**
158
+ * Compose the launched tmux-play config (PBCODE-16/17) from the CODE
159
+ * overlay and an optional base tmux-play config.
160
+ *
161
+ * @returns {import('@sublang/cligent/tmux-play').TmuxPlayConfig}
162
+ */
163
+ export function composeRuntimeConfig(
164
+ overlay,
165
+ base,
166
+ adapterModule = CODE_ADAPTER_MODULE,
167
+ ) {
168
+ const overlayConfig = requireObject(overlay, 'config');
169
+ const overlayPlayers = requireObject(overlayConfig.players, 'players');
170
+
171
+ // Reject any players key other than the two fixed CODE roles and the
172
+ // optional `committer` alias.
173
+ for (const key of Object.keys(overlayPlayers)) {
174
+ if (!CODE_PLAYER_KEYS.includes(key)) {
175
+ throw new Error(`Unknown config field players.${key}`);
100
176
  }
177
+ }
101
178
 
102
- if (section === 'players') {
103
- if (trimmed.startsWith('- ')) {
104
- playerItemIndent = indent;
105
- playerChildIndent = undefined;
106
- collectAdapterValue(trimmed.slice(2).trim(), adapters);
107
- continue;
108
- }
109
- if (playerItemIndent !== undefined && indent > playerItemIndent) {
110
- playerChildIndent ??= indent;
111
- if (indent === playerChildIndent) {
112
- collectAdapterValue(trimmed, adapters);
113
- }
179
+ // PBCODE-17: `players.committer` is an optional string naming `coder`
180
+ // or `reviewer`. The composer resolves it into the composed
181
+ // `captain.options.code.committer` below and emits no extra
182
+ // `players[]` entry, so the roster stays coder + reviewer.
183
+ let committerAlias;
184
+ if (overlayPlayers.committer !== undefined) {
185
+ const value = overlayPlayers.committer;
186
+ if (!CODE_ROLES.includes(value)) {
187
+ throw new Error(
188
+ `players.committer must name one of: ${CODE_ROLES.join(', ')}`,
189
+ );
190
+ }
191
+ committerAlias = value;
192
+ }
193
+
194
+ // One composed `players[]` entry per role, with `id` = the role key
195
+ // and that role's required `adapter` plus optional fields.
196
+ const players = CODE_ROLES.map((role) => {
197
+ if (overlayPlayers[role] === undefined) {
198
+ throw new Error(`Missing required field players.${role}`);
199
+ }
200
+ const block = requireObject(overlayPlayers[role], `players.${role}`);
201
+ if (block.adapter === undefined) {
202
+ throw new Error(`Missing required field players.${role}.adapter`);
203
+ }
204
+ const entry = { id: role, adapter: block.adapter };
205
+ for (const field of PLAYER_FIELDS) {
206
+ if (block[field] !== undefined) entry[field] = block[field];
207
+ }
208
+ return entry;
209
+ });
210
+
211
+ const overlayCaptain =
212
+ overlayConfig.captain === undefined
213
+ ? {}
214
+ : requireObject(overlayConfig.captain, 'captain');
215
+ const baseCaptain = isObject(base?.captain) ? base.captain : {};
216
+
217
+ // `captain.adapter` comes from the overlay when present, else the
218
+ // base; composition fails with a path-named error when neither
219
+ // supplies it. Role adapters are required in the overlay and are not
220
+ // inherited.
221
+ const captainAdapter = overlayCaptain.adapter ?? baseCaptain.adapter;
222
+ if (captainAdapter === undefined) {
223
+ throw new Error('Missing required field captain.adapter');
224
+ }
225
+
226
+ const captain = { from: adapterModule, adapter: captainAdapter };
227
+ for (const field of CAPTAIN_INHERITED_FIELDS) {
228
+ const value = overlayCaptain[field] ?? baseCaptain[field];
229
+ if (value !== undefined) captain[field] = value;
230
+ }
231
+
232
+ // PBCODE-17: the shim is the sole writer of the composed
233
+ // `captain.options.code.committer`. Read the overlay's
234
+ // `captain.options.code` (carried through unchanged), reject a
235
+ // directly-set `committer` there, then layer the resolved
236
+ // `players.committer` alias on top.
237
+ let overlayCode;
238
+ if (overlayCaptain.options !== undefined) {
239
+ const captainOptions = requireObject(
240
+ overlayCaptain.options,
241
+ 'captain.options',
242
+ );
243
+ if (captainOptions.code !== undefined) {
244
+ overlayCode = requireObject(captainOptions.code, 'captain.options.code');
245
+ if (overlayCode.committer !== undefined) {
246
+ throw new Error(
247
+ 'captain.options.code.committer is composer-owned; ' +
248
+ 'set players.committer instead',
249
+ );
114
250
  }
115
251
  }
116
252
  }
253
+ if (overlayCode !== undefined || committerAlias !== undefined) {
254
+ const code = { ...(overlayCode ?? {}) };
255
+ if (committerAlias !== undefined) code.committer = committerAlias;
256
+ captain.options = { code };
257
+ }
258
+
259
+ // Inherit `theme` and `layout` from the base when the overlay omits
260
+ // them; never the base `players[]` roster.
261
+ const composed = {};
262
+ const theme = overlayConfig.theme ?? base?.theme;
263
+ if (theme !== undefined) composed.theme = theme;
264
+ const layout = overlayConfig.layout ?? base?.layout;
265
+ if (layout !== undefined) composed.layout = layout;
266
+ composed.captain = captain;
267
+ composed.players = players;
268
+ return composed;
269
+ }
270
+
271
+ export function adaptersFromComposedConfig(config) {
272
+ const adapters = new Set();
273
+ const captainAdapter = config?.captain?.adapter;
274
+ if (captainAdapter) adapters.add(captainAdapter);
275
+ for (const player of config?.players ?? []) {
276
+ if (player?.adapter) adapters.add(player.adapter);
277
+ }
117
278
  return [...adapters];
118
279
  }
119
280
 
120
- export function checkReadiness(configPath, env = process.env, home = homedir()) {
121
- const config = readFileSync(configPath, 'utf8');
122
- const adapters = collectAdaptersFromConfig(config);
281
+ export function checkReadiness(adapters, env = process.env, home = homedir()) {
123
282
  const failingAdapters = [];
124
283
  const unknownAdapters = [];
125
284
 
@@ -142,6 +301,15 @@ export function checkReadiness(configPath, env = process.env, home = homedir())
142
301
  return { failingAdapters, unknownAdapters };
143
302
  }
144
303
 
304
+ function writeComposedConfig(composed) {
305
+ const dir = mkdtempSync(join(tmpdir(), 'playbook-code-'));
306
+ // cligent's loader only accepts a `.yaml` extension; name the temp
307
+ // file accordingly.
308
+ const path = join(dir, 'tmux-play.config.yaml');
309
+ writeFileSync(path, stringifyYaml(composed));
310
+ return { dir, path };
311
+ }
312
+
145
313
  function seedUserConfigIfMissing(userConfigPath, stderr) {
146
314
  if (existsSync(userConfigPath)) return;
147
315
  mkdirSync(dirname(userConfigPath), { recursive: true });
@@ -175,10 +343,11 @@ function helpText({ userConfigPath, failingAdapters = [] }) {
175
343
  '',
176
344
  'Agent swap recipe:',
177
345
  ' - change captain.adapter and captain.model for the Captain/Judge',
178
- ' - change each player adapter and (optional) model for the Coder',
179
- ' and Reviewer; model when pinned, else adapter, is substituted',
180
- ' into <coder-llm>/<reviewer-llm> player prompts (PBRT-4)',
181
- ' - keep captain.from and players[].id fixed',
346
+ ' - change adapter and model under players.coder and players.reviewer',
347
+ ' for the Coder and Reviewer; model when pinned, else adapter, is',
348
+ ' substituted into <coder-llm>/<reviewer-llm> player prompts (PBRT-4)',
349
+ ' - the composer injects captain.from; the role keys coder and',
350
+ ' reviewer are fixed',
182
351
  '',
183
352
  ].join('\n');
184
353
  }
@@ -220,41 +389,15 @@ function resolveTmuxPlayBin() {
220
389
  return join(dirname(fileURLToPath(tmuxPlayIndexUrl)), 'cli.js');
221
390
  }
222
391
 
223
- function stripYamlComment(line) {
224
- let quote = '';
225
- for (let index = 0; index < line.length; index += 1) {
226
- const char = line[index];
227
- if ((char === '"' || char === "'") && line[index - 1] !== '\\') {
228
- quote = quote === char ? '' : quote || char;
229
- continue;
230
- }
231
- if (char === '#' && !quote) {
232
- return line.slice(0, index);
233
- }
234
- }
235
- return line;
392
+ function isObject(value) {
393
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
236
394
  }
237
395
 
238
- function unquoteYamlScalar(value) {
239
- const trimmed = value.trim();
240
- if (
241
- (trimmed.startsWith('"') && trimmed.endsWith('"')) ||
242
- (trimmed.startsWith("'") && trimmed.endsWith("'"))
243
- ) {
244
- return trimmed.slice(1, -1).trim();
396
+ function requireObject(value, path) {
397
+ if (!isObject(value)) {
398
+ throw new Error(`${path} must be an object`);
245
399
  }
246
- return trimmed;
247
- }
248
-
249
- function collectAdapterValue(trimmedLine, adapters) {
250
- const match = trimmedLine.match(/^adapter\s*:\s*(.+?)\s*$/);
251
- if (!match) return;
252
- const value = unquoteYamlScalar(match[1].trim());
253
- if (value) adapters.add(value);
254
- }
255
-
256
- function leadingSpaceCount(line) {
257
- return line.length - line.trimStart().length;
400
+ return value;
258
401
  }
259
402
 
260
403
  function errorMessage(error) {
@@ -23,6 +23,7 @@ export type CaptainInput = {
23
23
  challenges?: string;
24
24
  coderPlayer?: string;
25
25
  reviewerPlayer?: string;
26
+ committerPlayer?: string;
26
27
  pendingBossQuestion?: PendingBossQuestion;
27
28
  bossReply?: string;
28
29
  };