@lyupro/codex-bridge 0.5.4 → 0.6.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.
@@ -16,11 +16,13 @@
16
16
  * The file is optional. Its absence is the default, not an error; a malformed one is an
17
17
  * error, because a typo must not silently change how runs behave.
18
18
  */
19
- import fs from 'node:fs';
20
19
  import path from 'node:path';
21
20
  import { fileURLToPath } from 'node:url';
22
21
  import { BRAND_CONFIG_PATH, BRAND_HOME } from './brand-home.mjs';
23
22
  import { readJsonFileSync } from './json-file.mjs';
23
+ import { editRunConfig } from './config-edit.mjs';
24
+ import { DEFAULTS, ROLES, SWITCH_KEYS, LIST_KEYS, OBJECT_KEYS, validateRunConfig } from './config-validate.mjs';
25
+ export { DEFAULTS, DEFAULT_ENVIRONMENT_PATHS } from './config-validate.mjs';
24
26
 
25
27
  export const CONFIG_PATH = BRAND_CONFIG_PATH;
26
28
 
@@ -32,98 +34,6 @@ export const CONFIG_PATH = BRAND_CONFIG_PATH;
32
34
  const ORIGIN =
33
35
  BRAND_HOME.source === 'CODEX_BRIDGE_HOME' ? 'from CODEX_BRIDGE_HOME' : 'default location';
34
36
 
35
- const BUDGET_KEY = 'budgets';
36
- const BUDGET_MODES = ['scout', 'build', 'review'];
37
- const DEFAULT_BUDGETS = { scout: 15, build: 25, review: 20 };
38
- const RETENTION_KEY = 'retention';
39
- const RETENTION_FIELDS = ['enabled', 'days'];
40
- const DEFAULT_RETENTION = { enabled: true, days: 30 };
41
-
42
- /**
43
- * Files the surrounding tooling writes on its own, in any repository a run touches.
44
- *
45
- * A run is graded by comparing the worktree either side of it, which silently assumes the
46
- * run is the only writer. It is not: OMC rewrites .omc/ and Claude Code rewrites its own
47
- * caches while Codex works, and on 2026-08-02 an honest run was failed for
48
- * “out-of-scope changes: .omc/project-memory.json” — a file Codex never opened. These paths are
49
- * therefore attributed to the environment rather than to the run. They are not hidden: the
50
- * reply and meta.json list them separately, so a run that really did edit them is still visible.
51
- */
52
- export const DEFAULT_ENVIRONMENT_PATHS = [
53
- '.omc/**',
54
- '.claude/settings.local.json',
55
- // Claude Code's own state, which only exists when the repository under a run is ~/.claude.
56
- 'mcp-needs-auth-cache.json',
57
- 'plugins/installed_plugins.json',
58
- 'plugins/known_marketplaces.json',
59
- ];
60
-
61
- /** Off means the flag is passed to Codex as `--disable <key>`. */
62
- export const DEFAULTS = {
63
- hooks: false,
64
- plugins: false,
65
- models: {},
66
- budgets: DEFAULT_BUDGETS,
67
- retention: DEFAULT_RETENTION,
68
- environmentPaths: DEFAULT_ENVIRONMENT_PATHS,
69
- answerLanguage: 'English',
70
- };
71
-
72
- /** Booleans the operator flips from the command line; structured keys are edited in the file. */
73
- const SWITCH_KEYS = ['hooks', 'plugins'];
74
- const LIST_KEYS = ['environmentPaths'];
75
- const OBJECT_KEYS = ['models'];
76
- const MODEL_KEYS = ['scout', 'build', 'review'];
77
- /**
78
- * A mode is configured as a pair, not as a model alone: reasoning depth is half of what a
79
- * model is worth. A cheap model at its default depth is a different worker from the same
80
- * model at "max", and pinning only the name would have silently kept every run at the
81
- * fallback depth the dispatcher happens to pass.
82
- */
83
- const PROFILE_KEYS = ['model', 'effort'];
84
- // Exactly the values the service enumerates, and no more. `minimal` sat here until 2026-08-26,
85
- // passed validation and was rejected by the model itself after the run had started
86
- // ("Unsupported value: 'minimal' is not supported with the … model"), so an order died four
87
- // seconds in on quota already spent. A validator that admits a known-dead value is worse than none.
88
- export const ALLOWED_EFFORTS = Object.freeze(['none', 'low', 'medium', 'high', 'xhigh', 'max']);
89
- /**
90
- * The language a run answers in. Left to the model it followed the task, the surrounding docs or
91
- * its own default: an English order came back in Russian, and artifacts of one project ended up in
92
- * two languages. English is the default because the package is read by strangers.
93
- */
94
- const STRING_KEYS = ['answerLanguage'];
95
- const KEYS = [...SWITCH_KEYS, ...LIST_KEYS, ...OBJECT_KEYS, ...STRING_KEYS, BUDGET_KEY, RETENTION_KEY];
96
-
97
- function readRetention(file, value) {
98
- if (!value || typeof value !== 'object' || Array.isArray(value)) {
99
- throw new Error(
100
- `${file}: key “${RETENTION_KEY}” must be an object with enabled and days, not ${JSON.stringify(value)}`,
101
- );
102
- }
103
- for (const key of Object.keys(value)) {
104
- if (!RETENTION_FIELDS.includes(key)) {
105
- throw new Error(
106
- `${file}: key “${RETENTION_KEY}” has unknown field “${key}”. ` +
107
- `Only ${RETENTION_FIELDS.join(', ')} are allowed`,
108
- );
109
- }
110
- }
111
- if (typeof value.enabled !== 'boolean') {
112
- throw new Error(
113
- `${file}: key “${RETENTION_KEY}.enabled” must be true or false, not ${JSON.stringify(value.enabled)}`,
114
- );
115
- }
116
- // Plan_17 step 4 does not read days when disabled, so stale values cannot revive deletion after
117
- // the step 3 incident where a copied default silently turned cleanup back on.
118
- if (!value.enabled) return { enabled: false };
119
- if (typeof value.days !== 'number' || !Number.isFinite(value.days) || value.days <= 0) {
120
- throw new Error(
121
- `${file}: key “${RETENTION_KEY}.days” must be a positive number of days, not ${JSON.stringify(value.days)}`,
122
- );
123
- }
124
- return { enabled: true, days: value.days };
125
- }
126
-
127
37
  export function retentionNotice(config) {
128
38
  if (config?.retention?.enabled) {
129
39
  const days = config.retention.days;
@@ -138,43 +48,6 @@ export function retentionNotice(config) {
138
48
  };
139
49
  }
140
50
 
141
- /**
142
- * A run gets a hard wall-clock budget, because the caller's timeout is not a run contract:
143
- * on 2026-08-03 one order restarted six times and spent 170,293 accounted tokens while four
144
- * killed callers left their Codex processes and token spend unrecorded.
145
- */
146
- function readBudgets(file, value) {
147
- if (!value || typeof value !== 'object' || Array.isArray(value)) {
148
- throw new Error(
149
- `${file}: key “${BUDGET_KEY}” must be an object keyed by ${BUDGET_MODES.join(', ')}, ` +
150
- `each holding a positive number of minutes, not ${JSON.stringify(value)}`,
151
- );
152
- }
153
- const budgets = { ...DEFAULT_BUDGETS };
154
- for (const [mode, minutes] of Object.entries(value)) {
155
- if (!BUDGET_MODES.includes(mode)) {
156
- throw new Error(
157
- `${file}: key “${BUDGET_KEY}” has unknown mode “${mode}”. ` +
158
- `Only ${BUDGET_MODES.join(', ')} are allowed`,
159
- );
160
- }
161
- if (typeof minutes === 'string' && !minutes.trim()) {
162
- throw new Error(
163
- `${file}: key “${BUDGET_KEY}.${mode}” is empty; remove the field to use the default ` +
164
- `(${DEFAULT_BUDGETS[mode]} minutes), or give it a positive number of minutes`,
165
- );
166
- }
167
- if (typeof minutes !== 'number' || !Number.isFinite(minutes) || minutes <= 0) {
168
- throw new Error(
169
- `${file}: key “${BUDGET_KEY}.${mode}” must be a positive number of minutes, ` +
170
- `not ${JSON.stringify(minutes)}`,
171
- );
172
- }
173
- budgets[mode] = minutes;
174
- }
175
- return budgets;
176
- }
177
-
178
51
  export function readRunConfig(file = CONFIG_PATH) {
179
52
  let parsed;
180
53
  try {
@@ -186,113 +59,7 @@ export function readRunConfig(file = CONFIG_PATH) {
186
59
  'the default mode applies: hooks and plugins are disabled.',
187
60
  );
188
61
  }
189
- if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
190
- throw new Error(`${file} must contain an object like {"hooks": false, "plugins": false}`);
191
- }
192
- const config = { ...DEFAULTS };
193
- for (const [key, value] of Object.entries(parsed)) {
194
- if (!KEYS.includes(key)) {
195
- throw new Error(`${file}: unknown key “${key}”. Only ${KEYS.join(', ')} are allowed`);
196
- }
197
- if (STRING_KEYS.includes(key)) {
198
- if (typeof value !== 'string' || !value.trim()) {
199
- throw new Error(
200
- `${file}: key “${key}” must be a non-empty string, not ${JSON.stringify(value)}. ` +
201
- `Remove the key to keep the default (${DEFAULTS[key]}).`,
202
- );
203
- }
204
- config[key] = value.trim();
205
- continue;
206
- }
207
- if (key === BUDGET_KEY) {
208
- config[key] = readBudgets(file, value);
209
- continue;
210
- }
211
- if (key === RETENTION_KEY) {
212
- config[key] = readRetention(file, value);
213
- continue;
214
- }
215
- if (LIST_KEYS.includes(key)) {
216
- if (!Array.isArray(value) || value.some((item) => typeof item !== 'string')) {
217
- throw new Error(
218
- `${file}: key “${key}” must be a list of string patterns, not ${JSON.stringify(value)}. ` +
219
- 'An empty list means “the environment writes nothing”; an absent key uses the default.',
220
- );
221
- }
222
- config[key] = value.map((item) => item.trim()).filter(Boolean);
223
- continue;
224
- }
225
- if (OBJECT_KEYS.includes(key)) {
226
- if (!value || typeof value !== 'object' || Array.isArray(value)) {
227
- throw new Error(
228
- `${file}: key “${key}” must be an object keyed by ${MODEL_KEYS.join(', ')}, ` +
229
- `each holding {"model": "...", "effort": "..."}, not ${JSON.stringify(value)}`,
230
- );
231
- }
232
- const models = {};
233
- for (const [mode, profile] of Object.entries(value)) {
234
- if (!MODEL_KEYS.includes(mode)) {
235
- throw new Error(
236
- `${file}: key “${key}” has unknown mode “${mode}”. Only ${MODEL_KEYS.join(', ')} are allowed`,
237
- );
238
- }
239
- if (!profile || typeof profile !== 'object' || Array.isArray(profile)) {
240
- throw new Error(
241
- `${file}: key “${key}.${mode}” must be an object like ` +
242
- `{"model": "...", "effort": "..."}, not ${JSON.stringify(profile)}`,
243
- );
244
- }
245
- const resolved = {};
246
- for (const [field, fieldValue] of Object.entries(profile)) {
247
- if (!PROFILE_KEYS.includes(field)) {
248
- throw new Error(
249
- `${file}: key “${key}.${mode}” has unknown field “${field}”. ` +
250
- `Only ${PROFILE_KEYS.join(', ')} are allowed`,
251
- );
252
- }
253
- if (typeof fieldValue !== 'string') {
254
- throw new Error(
255
- `${file}: key “${key}.${mode}.${field}” must be a string, not ${JSON.stringify(fieldValue)}`,
256
- );
257
- }
258
- const trimmed = fieldValue.trim();
259
- // A field written as an empty string is a mistake, not an absence: dropping it silently
260
- // would send the run to another profile's depth past every check below.
261
- if (!trimmed) {
262
- throw new Error(
263
- `${file}: key “${key}.${mode}.${field}” is empty; remove the field to fall back, ` +
264
- 'or give it a value',
265
- );
266
- }
267
- resolved[field] = trimmed;
268
- }
269
- if (resolved.effort && /\s/.test(resolved.effort)) {
270
- throw new Error(
271
- `${file}: key “${key}.${mode}.effort” must be a single word; ` +
272
- `allowed values: ${ALLOWED_EFFORTS.join(', ')}`,
273
- );
274
- }
275
- if (resolved.effort && !ALLOWED_EFFORTS.includes(resolved.effort)) {
276
- throw new Error(
277
- `${file}: key “${key}.${mode}.effort” must be one of: ` +
278
- `${ALLOWED_EFFORTS.join(', ')}; got ${JSON.stringify(resolved.effort)}`,
279
- );
280
- }
281
- if (Object.keys(resolved).length) models[mode] = resolved;
282
- }
283
- config[key] = models;
284
- continue;
285
- }
286
- if (typeof value !== 'boolean') {
287
- throw new Error(`${file}: key “${key}” must be true or false, not ${JSON.stringify(value)}`);
288
- }
289
- config[key] = value;
290
- }
291
- return config;
292
- }
293
-
294
- export function writeRunConfig(config, file = CONFIG_PATH) {
295
- fs.writeFileSync(file, `${JSON.stringify(config, null, 2)}\n`);
62
+ return validateRunConfig(file, parsed);
296
63
  }
297
64
 
298
65
  /** Flags for `codex exec`: a switch that is off becomes an explicit --disable. */
@@ -305,17 +72,18 @@ const state = (config) => [
305
72
  ),
306
73
  `environmentPaths: ${(config.environmentPaths || []).length} patterns — changes in them are ` +
307
74
  'treated as environment work, not run work',
308
- `models: ${MODEL_KEYS.map((key) => {
75
+ `models: ${ROLES.map((key) => {
309
76
  const profile = config.models?.[key];
310
- if (!profile?.model && !profile?.effort) return `${key}: default — chosen by Codex`;
77
+ if (!profile?.model && !profile?.effort && !profile?.speed) return `${key}: default — chosen by Codex`;
311
78
  const model = profile.model || 'default model';
312
- return `${key}: ${model}${profile.effort ? ` at ${profile.effort} effort` : ''}`;
79
+ return `${key}: ${model}${profile.effort ? ` at ${profile.effort} effort` : ''}`
80
+ + (profile.speed ? ` on ${profile.speed} tier` : '');
313
81
  }).join('; ')}`,
314
- `budgets: ${BUDGET_MODES.map((mode) => `${mode}: ${config.budgets?.[mode] ?? DEFAULT_BUDGETS[mode]} minutes`).join('; ')}`,
82
+ `budgets: ${ROLES.map((role) => `${role}: ${config.budgets?.[role] ?? DEFAULTS.budgets[role]} minutes`).join('; ')}`,
315
83
  `retention: ${config.retention?.enabled ? `on — transport older than ${config.retention.days} days` : 'off — automatic cleanup disabled'}`,
316
84
  ];
317
85
 
318
- function main(argv) {
86
+ async function main(argv) {
319
87
  const [key, value] = argv;
320
88
 
321
89
  if (!key) {
@@ -325,7 +93,7 @@ function main(argv) {
325
93
  }
326
94
 
327
95
  if (key === 'reset') {
328
- writeRunConfig({ ...DEFAULTS });
96
+ await editRunConfig({ reset: true }, undefined);
329
97
  console.log([...state(DEFAULTS), `Reset to defaults · ${CONFIG_PATH} (${ORIGIN})`].join('\n'));
330
98
  return 0;
331
99
  }
@@ -334,7 +102,8 @@ function main(argv) {
334
102
  const known = LIST_KEYS.includes(key)
335
103
  ? `“${key}” is a pattern list, not a switch: edit it directly in ${CONFIG_PATH}`
336
104
  : OBJECT_KEYS.includes(key)
337
- ? `“${key}” is configured in the file, not as a switch: edit it directly in ${CONFIG_PATH}`
105
+ ? `“${key}” is not a switch: change it with “codex-bridge model set <role> <model> [effort]” `
106
+ + `or “codex-bridge model unset <role>”, which check the pair against the live catalogue`
338
107
  : `unknown switch “${key}”. Allowed: ${SWITCH_KEYS.join(', ')}, reset`;
339
108
  console.error(`run-config: ${known}`);
340
109
  return 2;
@@ -344,8 +113,7 @@ function main(argv) {
344
113
  return 2;
345
114
  }
346
115
 
347
- const config = { ...readRunConfig(), [key]: value === 'on' };
348
- writeRunConfig(config);
116
+ const config = await editRunConfig(key, () => value === 'on');
349
117
  console.log([...state(config), `File: ${CONFIG_PATH} (${ORIGIN})`].join('\n'));
350
118
  return 0;
351
119
  }
@@ -355,7 +123,7 @@ const invokedDirectly =
355
123
 
356
124
  if (invokedDirectly) {
357
125
  try {
358
- process.exit(main(process.argv.slice(2)));
126
+ process.exit(await main(process.argv.slice(2)));
359
127
  } catch (err) {
360
128
  console.error(`run-config: ${err.message}`);
361
129
  process.exit(2);
@@ -8,7 +8,6 @@
8
8
  import fs from 'node:fs';
9
9
  import path from 'node:path';
10
10
  import { AGENTS } from '../write-meta.mjs';
11
- import { ALLOWED_EFFORTS } from '../run-config.mjs';
12
11
  import { isAbsoluteTaskFilePath, requiredInputsFor } from '../required-inputs.mjs';
13
12
  import { firstShellUnsafeSequence } from '../shell-unsafe.mjs';
14
13
  import { parseTaskDocument } from './task-file.mjs';
@@ -88,7 +87,7 @@ const SHELL_CHECKED_FLAGS = Object.freeze([
88
87
  'repo',
89
88
  'agent',
90
89
  'effort',
91
- 'mode',
90
+ 'changeset',
92
91
  'task-file',
93
92
  ]);
94
93
  const BOOLEAN_YES = /^(1|true|yes)$/i;
@@ -100,6 +99,8 @@ export function parseArgs(argv) {
100
99
  const key = argv[i];
101
100
  if (!key.startsWith('--')) die(`unexpected argument: ${key}`);
102
101
  const name = key.slice(2);
102
+ // Plan_56 D10 requires --changeset: a stale reviewer flag must not silently select uncommitted work.
103
+ if (name === 'mode') die('unknown flag: --mode; use --changeset instead');
103
104
  const value = argv[i + 1];
104
105
  if (BOOLEAN_FLAGS.has(name)) {
105
106
  if (value !== undefined && !value.startsWith('--')) {
@@ -163,14 +164,11 @@ export function parseArgs(argv) {
163
164
  die('--question must not be empty for codex-scout; no quota was spent');
164
165
  }
165
166
  }
166
- const allowedEfforts = ALLOWED_EFFORTS.join(', ');
167
- // Left unset when not given, rather than defaulted here: the mode's configured profile is
167
+ // Left unset when not given, rather than defaulted here: the role's configured profile is
168
168
  // what fills the gap, and a default applied this early would always win over it.
169
- if (opts.effort !== undefined && /\s/.test(opts.effort)) {
170
- die(`--effort must be a single word; allowed values: ${allowedEfforts}`);
171
- }
172
- if (opts.effort !== undefined && !ALLOWED_EFFORTS.includes(opts.effort)) {
173
- die(`--effort must be one of: ${allowedEfforts}; got ${JSON.stringify(opts.effort)}`);
169
+ // Plan_56 step 3: support belongs to the live catalogue on write and Codex at run start.
170
+ if (opts.effort !== undefined && (!opts.effort || /\s/.test(opts.effort))) {
171
+ die(`--effort must be a non-empty single word with no whitespace; got ${JSON.stringify(opts.effort)}`);
174
172
  }
175
173
  opts.repo = path.resolve(opts.repo || process.cwd());
176
174
  const slugSource = opts.slug ? '--slug' : '--order-id';
@@ -183,7 +181,7 @@ export function parseArgs(argv) {
183
181
  `${JSON.stringify(opts.slug)} must contain a letter or digit.`,
184
182
  );
185
183
  }
186
- opts.mode = opts.mode || 'uncommitted';
184
+ opts.changeset = opts.changeset || 'uncommitted';
187
185
  // A writing run without a declared scope is how `!Plans/*.md` got edited by a run that was
188
186
  // told in prose not to touch them: prose does not bind, a file list does. Required rather
189
187
  // than defaulted to "everything", and checked here — before the folder exists and before a
@@ -14,15 +14,16 @@
14
14
  * run from starting a process on Windows).
15
15
  */
16
16
  import path from 'node:path';
17
+ import { agentRole } from '../agents.mjs';
17
18
  import { CLEAN_ENV, RUN_ENV } from './run-env.mjs';
18
19
  import { platformSandboxArgs } from './sandbox-flags.mjs';
19
20
 
20
21
  /**
21
- * Which model runs a mode, and how deep it reasons.
22
+ * Which model runs a role, and how deep it reasons.
22
23
  *
23
- * Three sources, in this order: what the dispatcher asked for this one task, the mode's
24
+ * Three sources, in this order: what the dispatcher asked for this one task, the role's
24
25
  * configured profile, and finally a depth to fall back on. The order is the whole point —
25
- * pinning a model without its depth leaves every run at the fallback, which is how a mode
26
+ * pinning a model without its depth leaves every run at the fallback, which is how a role
26
27
  * configured for maximum reasoning would quietly keep working at the shallowest setting.
27
28
  */
28
29
  const FALLBACK_EFFORT = 'medium';
@@ -37,34 +38,25 @@ const FALLBACK_EFFORT = 'medium';
37
38
  const NO_SUBAGENTS = ['-c', 'agents.enabled=false'];
38
39
 
39
40
  /**
40
- * Which configured mode an agent is. One table, because the launcher needs the same answer to
41
- * pick a run's time budget: a second copy of this mapping is a second place to forget a mode.
42
- */
43
- export const runMode = (agent) =>
44
- ({
45
- 'codex-scout': 'scout',
46
- 'codex-build': 'build',
47
- 'codex-review': 'review',
48
- })[agent];
49
-
50
- /**
51
- * Which model and depth this run gets, and — as part of the same answer — where each came from.
41
+ * Which model, depth and tier this run requests, and where each came from.
52
42
  *
53
43
  * The provenance is not decoration. A configured profile failed to reach any run for three
54
44
  * releases (2026-08-26) and nothing said so: the reply named no model, meta.json recorded none,
55
45
  * and a depth silently served by the fallback is indistinguishable from one the operator chose.
56
- * `model_source` is `config` or `codex default`; `effort_source` is `request`, `config` or
57
- * `fallback`, in the order the values are consulted above.
46
+ * `model_source` and `speed_source` are `config` or `codex default`; `effort_source` is
47
+ * `request`, `config` or `fallback`, in the order the values are consulted above.
58
48
  */
59
49
  export function runProfile(opts) {
60
- const mode = runMode(opts.agent);
50
+ const role = agentRole(opts.agent);
61
51
  // opts.models is how tests state a configuration; a real run reads the one loaded for it.
62
- const configured = (opts.models || RUN_ENV?.models || {})[mode] || {};
52
+ const configured = (opts.models || RUN_ENV?.models || {})[role] || {};
63
53
  return {
64
54
  model: configured.model || '',
65
55
  model_source: configured.model ? 'config' : 'codex default',
66
56
  effort: opts.effort || configured.effort || FALLBACK_EFFORT,
67
57
  effort_source: opts.effort ? 'request' : configured.effort ? 'config' : 'fallback',
58
+ speed: configured.speed || '',
59
+ speed_source: configured.speed ? 'config' : 'codex default',
68
60
  };
69
61
  }
70
62
 
@@ -73,6 +65,8 @@ export function codexArgs(opts, runDir, isGitRepo) {
73
65
  const profile = runProfile(opts);
74
66
  const effort = `model_reasoning_effort=${profile.effort}`;
75
67
  const modelArgs = profile.model ? ['-m', profile.model] : [];
68
+ // Plan_56 D35: there is no identifier for ordinary speed; an unpinned tier adds no override.
69
+ const speedArgs = profile.speed ? ['-c', `service_tier=${profile.speed}`] : [];
76
70
  if (opts.agent === 'codex-scout') {
77
71
  return [
78
72
  'exec',
@@ -81,6 +75,7 @@ export function codexArgs(opts, runDir, isGitRepo) {
81
75
  '--ignore-user-config',
82
76
  '-c',
83
77
  effort,
78
+ ...speedArgs,
84
79
  ...NO_SUBAGENTS,
85
80
  ...platformSandboxArgs(),
86
81
  ...modelArgs,
@@ -105,6 +100,7 @@ export function codexArgs(opts, runDir, isGitRepo) {
105
100
  ...CLEAN_ENV,
106
101
  '-c',
107
102
  effort,
103
+ ...speedArgs,
108
104
  ...NO_SUBAGENTS,
109
105
  ...platformSandboxArgs(),
110
106
  ...modelArgs,
@@ -138,6 +134,7 @@ export function codexArgs(opts, runDir, isGitRepo) {
138
134
  '--ignore-user-config',
139
135
  '-c',
140
136
  effort,
137
+ ...speedArgs,
141
138
  ...NO_SUBAGENTS,
142
139
  ...platformSandboxArgs(),
143
140
  ...modelArgs,
@@ -112,9 +112,9 @@ export function findFakeDone(repo) {
112
112
  }
113
113
 
114
114
  /** What exactly is under review, resolved from git rather than from wording. */
115
- export function reviewScope(repo, mode) {
116
- if (mode.startsWith('base:')) {
117
- const base = mode.slice(5);
115
+ export function reviewScope(repo, changeset) {
116
+ if (changeset.startsWith('base:')) {
117
+ const base = changeset.slice(5);
118
118
  return {
119
119
  label: `branch changes against base ${base}`,
120
120
  diffCommand: `git diff ${base}...HEAD`,
@@ -123,8 +123,8 @@ export function reviewScope(repo, mode) {
123
123
  .filter(Boolean),
124
124
  };
125
125
  }
126
- if (mode.startsWith('commit:')) {
127
- const sha = mode.slice(7);
126
+ if (changeset.startsWith('commit:')) {
127
+ const sha = changeset.slice(7);
128
128
  return {
129
129
  label: `commit ${sha}`,
130
130
  diffCommand: `git show ${sha}`,
@@ -31,7 +31,8 @@ import { continuationRefusal } from './continuation.mjs';
31
31
  import { SCHEMAS } from './schemas.mjs';
32
32
  import { INSTRUCTIONS } from './prompts.mjs';
33
33
  import { git, headSha, branchName, worktreeSnapshot, reviewScope } from './git-state.mjs';
34
- import { codexArgs, runMode, runProfile } from './codex-args.mjs';
34
+ import { agentRole } from '../agents.mjs';
35
+ import { codexArgs, runProfile } from './codex-args.mjs';
35
36
  import { writeWorkerOrder } from './worker-order.mjs';
36
37
  import { requireCodex, unsafeForCmd } from './codex-cmd.mjs';
37
38
  import { runsRoot } from './runs-root.mjs';
@@ -254,7 +255,7 @@ export async function launcher(argv = process.argv.slice(2)) {
254
255
 
255
256
  requireCodex(runDir, opts.agent);
256
257
 
257
- const scope = opts.agent === 'codex-review' ? reviewScope(repoRoot, opts.mode) : null;
258
+ const scope = opts.agent === 'codex-review' ? reviewScope(repoRoot, opts.changeset) : null;
258
259
  if (scope) {
259
260
  fs.writeFileSync(
260
261
  path.join(runDir, 'scope.txt'),
@@ -347,7 +348,7 @@ export async function launcher(argv = process.argv.slice(2)) {
347
348
  repo: repoRoot,
348
349
  isGitRepo,
349
350
  launcherPid: process.pid,
350
- budgetMinutes: RUN_ENV?.budgets?.[runMode(opts.agent)],
351
+ budgetMinutes: RUN_ENV?.budgets?.[agentRole(opts.agent)],
351
352
  scopeNew: opts.scopeNewPatterns,
352
353
  profile: runProfile({ ...opts, repo: repoRoot }),
353
354
  args: codexArgv,