sneakoscope 0.8.2 → 0.8.5

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
@@ -174,9 +174,9 @@ sks codex-lb repair
174
174
  sks
175
175
  ```
176
176
 
177
- Bare `sks` can also prompt for codex-lb auth; SKS stores the base URL/key in `~/.codex/sks-codex-lb.env`, syncs `codex login --with-api-key`, and loads it in tmux. When codex-lb is active, SKS opens a fresh `sks-codex-lb-*` tmux session and sweeps older detached codex-lb sessions for the same repo before launch so stale Responses API chains are not reused. Configured launch paths, including non-interactive runs, verify that codex-lb can continue a Responses API chain with `previous_response_id`; if that check fails, SKS bypasses codex-lb for that launch with `model_provider="openai"` instead of letting the Codex session fail mid-work.
177
+ Bare `sks` can also prompt for codex-lb auth; SKS stores the base URL/key in `~/.codex/sks-codex-lb.env`, loads the provider env key for tmux launches, and syncs the macOS user launch environment so the Codex App can see `CODEX_LB_API_KEY` after restart. npm postinstall upgrades resync that stored env file when postinstall is not stopped by a hard harness conflict, and `sks doctor --fix` does the same during repair. If an older SKS release left the codex-lb dashboard key only in the shared Codex `auth.json` login cache, SKS migrates that key back into `~/.codex/sks-codex-lb.env` when a codex-lb provider or env base URL is already recoverable. It does not rewrite the shared Codex `auth.json` login cache by default; set `SKS_CODEX_LB_SYNC_CODEX_LOGIN=1` only if you intentionally want the old API-key login-cache behavior. When codex-lb is active, SKS opens a fresh `sks-codex-lb-*` tmux session and sweeps older detached codex-lb sessions for the same repo before launch so stale Responses API chains are not reused. Configured launch paths, including non-interactive runs, verify that codex-lb can continue a Responses API chain with `previous_response_id`; if that check fails, SKS bypasses codex-lb for that launch with `model_provider="openai"` instead of letting the Codex session fail mid-work.
178
178
 
179
- If Codex CLI auth drifts after launch/reinstall, run `sks doctor --fix` or `sks codex-lb repair`; to replace it, run `sks codex-lb reconfigure --host <domain> --api-key <key>`.
179
+ If codex-lb provider auth drifts after launch/reinstall, run `sks doctor --fix` or `sks codex-lb repair`; to replace it, run `sks codex-lb reconfigure --host <domain> --api-key <key>`.
180
180
 
181
181
  ### MAD tmux Launch
182
182
 
@@ -185,7 +185,7 @@ sks --mad
185
185
  sks --mad --yes
186
186
  ```
187
187
 
188
- This syncs existing codex-lb/Codex CLI auth, creates/uses the `sks-mad-high` full-access profile, opens the MAD-SKS permission gate for that tmux run, and launches a single Codex CLI pane. The session recreates the named session so stale split-pane MAD sessions collapse back to one pane. Catastrophic database wipe/all-row/project-management safeguards remain active, and the pipeline contract still forbids unrequested fallback implementation code.
188
+ This syncs existing codex-lb provider auth, creates/uses the `sks-mad-high` full-access profile, opens the MAD-SKS permission gate for that tmux run, and launches a single Codex CLI pane. The session recreates the named session so stale split-pane MAD sessions collapse back to one pane. Catastrophic database wipe/all-row/project-management safeguards remain active, and the pipeline contract still forbids unrequested fallback implementation code.
189
189
 
190
190
  Before launching, SKS checks npm for a newer `sneakoscope`; answer `y` to update or `n` to continue. Use `--yes` to approve missing dependency installs automatically.
191
191
 
@@ -275,7 +275,7 @@ For headless remotely controllable Codex App/server sessions on Codex CLI 0.130.
275
275
  sks codex-app remote-control -- --help
276
276
  ```
277
277
 
278
- `sks codex-app check` reports whether the installed Codex CLI is new enough, whether the required app flags are visible, whether Fast/speed-selector config is unlocked, and whether installed OpenAI default plugins such as Browser, Chrome, Computer Use, Documents, Presentations, Spreadsheets, and LaTeX are enabled. Codex CLI 0.130.0+ app-server/remote-control threads can pick up config changes live; older CLI/TUI sessions should still be restarted after `.codex/config.toml` or MCP/plugin changes.
278
+ `sks codex-app check` reports whether the installed Codex CLI is new enough, whether the required app flags are visible, whether Fast/speed-selector config is unlocked, and whether installed OpenAI default plugins such as Browser, Chrome, Computer Use, Documents, Presentations, Spreadsheets, and LaTeX are enabled. codex-lb can remain configured as a custom provider, but SKS keeps it off the top-level Codex App provider setting so native model, speed, and built-in feature UI stay visible. Codex CLI 0.130.0+ app-server/remote-control threads can pick up config changes live; older CLI/TUI sessions should still be restarted after `.codex/config.toml` or MCP/plugin changes.
279
279
 
280
280
  Then open Codex App and use prompt commands directly in the chat. Examples:
281
281
 
@@ -407,6 +407,10 @@ codex mcp list
407
407
 
408
408
  Codex App workflows need the app installed. UI/browser evidence requires first-party Codex Computer Use, and generated raster/image-review evidence requires real `$imagegen`/`gpt-image-2` output. After setup/upgrade, start a fresh thread so Codex reloads plugin tools.
409
409
 
410
+ ### Codex App UI looks stale after codex-lb changes
411
+
412
+ If Codex App UI panels or auth-dependent controls still look wrong after codex-lb setup, repair, or upgrade, restart the app first. If the UI still does not recover, sign out of Codex App, sign back in, then run `sks codex-app check` or `sks codex-lb repair` as needed.
413
+
410
414
  ### Setup is blocked by another harness
411
415
 
412
416
  ```sh
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sneakoscope",
3
3
  "displayName": "ㅅㅋㅅ",
4
- "version": "0.8.2",
4
+ "version": "0.8.5",
5
5
  "description": "Sneakoscope Codex: database-safe Codex CLI/App harness with Team, Goal, AutoResearch, TriWiki, and Honest Mode.",
6
6
  "type": "module",
7
7
  "homepage": "https://github.com/mandarange/Sneakoscope-Codex#readme",
@@ -81,7 +81,8 @@ export async function postinstall({ bootstrap }) {
81
81
 
82
82
  async function reportPostinstallCodexLbAuth() {
83
83
  const codexLbAuth = await ensureCodexLbAuthDuringInstall();
84
- if (codexLbAuth.status === 'synced' || codexLbAuth.status === 'present' || codexLbAuth.status === 'repaired') console.log(`codex-lb auth: preserved from ${codexLbAuth.env_path}.`);
84
+ if (codexLbAuth.legacy_auth_migrated) console.log(`codex-lb auth: restored from existing Codex login cache into ${codexLbAuth.env_path}.`);
85
+ else if (codexLbAuth.status === 'synced' || codexLbAuth.status === 'present' || codexLbAuth.status === 'repaired') console.log(`codex-lb auth: preserved from ${codexLbAuth.env_path}.`);
85
86
  else if (codexLbAuth.status === 'skipped') console.log(`codex-lb auth: skipped (${codexLbAuth.reason}).`);
86
87
  else if (codexLbAuth.status === 'missing_env_key') console.log('codex-lb auth: stored key missing. Run `sks codex-lb setup --host <domain> --api-key <key>` to repair.');
87
88
  else if (codexLbAuth.status === 'missing_base_url') console.log('codex-lb auth: stored key has no recoverable base URL. Run `sks codex-lb reconfigure --host <domain> --api-key <key>` once.');
@@ -155,6 +156,10 @@ export function codexLbEnvPath(home = process.env.HOME || os.homedir()) {
155
156
  return path.join(home, '.codex', 'sks-codex-lb.env');
156
157
  }
157
158
 
159
+ function codexAuthPath(home = process.env.HOME || os.homedir()) {
160
+ return path.join(home, '.codex', 'auth.json');
161
+ }
162
+
158
163
  async function capturePostinstallCodexLbConfigSnapshot(home = process.env.HOME || os.homedir()) {
159
164
  const configPath = codexLbConfigPath(home);
160
165
  const envPath = codexLbEnvPath(home);
@@ -169,10 +174,10 @@ async function capturePostinstallCodexLbConfigSnapshot(home = process.env.HOME |
169
174
  async function restorePostinstallCodexLbConfigSnapshot(snapshot) {
170
175
  if (!snapshot?.base_url) return { status: 'skipped', reason: 'no_snapshot' };
171
176
  const current = await readText(snapshot.config_path, '');
172
- if (hasTopLevelCodexLbSelected(current) && codexLbProviderBaseUrl(current)) {
177
+ const next = normalizeCodexFastModeUiConfig(upsertCodexLbConfig(current, snapshot.base_url));
178
+ if (next === ensureTrailingNewline(current) && codexLbProviderBaseUrl(current)) {
173
179
  return { status: 'present', config_path: snapshot.config_path };
174
180
  }
175
- const next = normalizeCodexFastModeUiConfig(upsertCodexLbConfig(current, snapshot.base_url));
176
181
  await writeTextAtomic(snapshot.config_path, next);
177
182
  return { status: 'restored', config_path: snapshot.config_path };
178
183
  }
@@ -198,9 +203,20 @@ export async function configureCodexLb(opts = {}) {
198
203
  await writeTextAtomic(configPath, next);
199
204
  await writeTextAtomic(envPath, `export CODEX_LB_BASE_URL=${shellSingleQuote(baseUrl)}\nexport CODEX_LB_API_KEY=${shellSingleQuote(apiKey)}\n`);
200
205
  await fsp.chmod(envPath, 0o600).catch(() => {});
201
- process.env.CODEX_LB_API_KEY = apiKey;
202
- const codexLogin = await syncCodexApiKeyLogin(apiKey, { home, force: true });
203
- return { ok: true, status: 'configured', config_path: configPath, env_path: envPath, base_url: baseUrl, env_key: 'CODEX_LB_API_KEY', codex_login: codexLogin };
206
+ const codexEnvironment = await syncCodexLbProviderEnvironment({ env_path: envPath, base_url: baseUrl }, { ...opts, home });
207
+ const codexLogin = await maybeSyncCodexLbSharedLogin(apiKey, { ...opts, home, force: true });
208
+ const ok = Boolean(codexEnvironment.ok && codexLogin.ok);
209
+ return {
210
+ ok,
211
+ status: ok ? 'configured' : (codexEnvironment.status || codexLogin.status),
212
+ config_path: configPath,
213
+ env_path: envPath,
214
+ base_url: baseUrl,
215
+ env_key: 'CODEX_LB_API_KEY',
216
+ codex_environment: codexEnvironment,
217
+ codex_login: codexLogin,
218
+ error: codexEnvironment.error || codexLogin.error || null
219
+ };
204
220
  }
205
221
 
206
222
  export async function codexLbStatus(opts = {}) {
@@ -212,10 +228,10 @@ export async function codexLbStatus(opts = {}) {
212
228
  const envText = envExists ? await readText(envPath, '') : '';
213
229
  const envKeyConfigured = Boolean(parseCodexLbEnvKey(envText));
214
230
  const providerConfigured = /\[model_providers\.codex-lb\]/.test(config);
215
- const selected = /model_provider\s*=\s*"codex-lb"/.test(config);
231
+ const selected = hasTopLevelCodexLbSelected(config);
216
232
  const baseUrl = codexLbProviderBaseUrl(config) || parseCodexLbEnvBaseUrl(envText) || null;
217
233
  return {
218
- ok: selected && providerConfigured && envKeyConfigured && Boolean(baseUrl),
234
+ ok: providerConfigured && envKeyConfigured && Boolean(baseUrl),
219
235
  config_path: configPath,
220
236
  env_path: envPath,
221
237
  provider_configured: providerConfigured,
@@ -360,10 +376,20 @@ function codexLbProviderBaseUrl(text = '') {
360
376
  export async function repairCodexLbAuth(opts = {}) {
361
377
  let status = await codexLbStatus(opts);
362
378
  let configRepaired = false;
363
- if (!status.ok && status.env_key_configured && status.base_url) {
379
+ let legacyAuthMigrated = false;
380
+ let legacyAuthPath = null;
381
+ const currentConfig = await readText(status.config_path, '');
382
+ if (!status.env_key_configured && status.base_url && (status.provider_configured || status.selected || status.env_base_url_configured)) {
383
+ const legacyAuth = await restoreCodexLbEnvFromSharedLogin(status, opts);
384
+ if (legacyAuth.ok) {
385
+ legacyAuthMigrated = true;
386
+ legacyAuthPath = legacyAuth.auth_path;
387
+ status = await codexLbStatus(opts);
388
+ }
389
+ }
390
+ if (status.env_key_configured && status.base_url && (!status.ok || status.selected || legacyAuthMigrated || hasTopLevelCodexModeLock(currentConfig))) {
364
391
  await ensureDir(path.dirname(status.config_path));
365
- const current = await readText(status.config_path, '');
366
- const next = normalizeCodexFastModeUiConfig(upsertCodexLbConfig(current, status.base_url));
392
+ const next = normalizeCodexFastModeUiConfig(upsertCodexLbConfig(currentConfig, status.base_url));
367
393
  await writeTextAtomic(status.config_path, next);
368
394
  configRepaired = true;
369
395
  status = await codexLbStatus(opts);
@@ -377,15 +403,21 @@ export async function repairCodexLbAuth(opts = {}) {
377
403
  codex_lb: status
378
404
  };
379
405
  }
380
- const codexLogin = await ensureCodexLbLoginFromEnv(status, opts);
406
+ const codexEnvironment = await syncCodexLbProviderEnvironment(status, opts);
407
+ const apiKey = parseCodexLbEnvKey(await readText(status.env_path, ''));
408
+ const codexLogin = await maybeSyncCodexLbSharedLogin(apiKey, { ...opts, home: opts.home || process.env.HOME || os.homedir(), force: true });
409
+ const ok = Boolean(codexEnvironment.ok && codexLogin.ok);
381
410
  return {
382
- ok: Boolean(codexLogin.ok),
383
- status: codexLogin.ok ? 'repaired' : codexLogin.status,
411
+ ok,
412
+ status: ok ? 'repaired' : (codexEnvironment.status || codexLogin.status),
384
413
  config_path: status.config_path,
385
414
  env_path: status.env_path,
386
415
  base_url: status.base_url,
387
416
  config_repaired: configRepaired,
417
+ legacy_auth_migrated: legacyAuthMigrated,
418
+ legacy_auth_path: legacyAuthPath,
388
419
  codex_lb: status,
420
+ codex_environment: codexEnvironment,
389
421
  codex_login: codexLogin
390
422
  };
391
423
  }
@@ -395,34 +427,56 @@ export async function ensureCodexLbAuthDuringInstall(opts = {}) {
395
427
  const status = await codexLbStatus(opts);
396
428
  if (!status.selected && !status.provider_configured && !status.env_file) return { status: 'not_configured', codex_lb: status };
397
429
  if (!status.ok) {
398
- if (status.env_key_configured && status.base_url) return repairCodexLbAuth(opts);
430
+ if (status.base_url && (status.env_key_configured || status.provider_configured || status.selected || status.env_base_url_configured)) return repairCodexLbAuth(opts);
399
431
  return { status: status.env_key_configured ? 'missing_base_url' : 'missing_env_key', codex_lb: status, config_path: status.config_path, env_path: status.env_path };
400
432
  }
401
- const codexLogin = await ensureCodexLbLoginFromEnv(status, { ...opts, force: true });
433
+ const codexEnvironment = await syncCodexLbProviderEnvironment(status, opts);
434
+ const apiKey = parseCodexLbEnvKey(await readText(status.env_path, ''));
435
+ const codexLogin = await maybeSyncCodexLbSharedLogin(apiKey, { ...opts, home: opts.home || process.env.HOME || os.homedir(), force: true });
436
+ const ok = Boolean(codexEnvironment.ok && codexLogin.ok);
402
437
  return {
403
- ok: Boolean(codexLogin.ok),
404
- status: codexLogin.status,
438
+ ok,
439
+ status: ok ? 'present' : (codexEnvironment.status || codexLogin.status),
405
440
  config_path: status.config_path,
406
441
  env_path: status.env_path,
407
442
  base_url: status.base_url,
408
443
  codex_lb: status,
444
+ codex_environment: codexEnvironment,
409
445
  codex_login: codexLogin,
410
- error: codexLogin.error || null
446
+ error: codexEnvironment.error || codexLogin.error || null
411
447
  };
412
448
  }
413
449
 
450
+ async function restoreCodexLbEnvFromSharedLogin(status = {}, opts = {}) {
451
+ const home = opts.home || process.env.HOME || os.homedir();
452
+ const authPath = opts.authPath || codexAuthPath(home);
453
+ const envPath = opts.envPath || status.env_path || codexLbEnvPath(home);
454
+ const authText = await readText(authPath, '');
455
+ const apiKey = parseCodexSharedLoginApiKey(authText);
456
+ if (!apiKey) return { ok: false, status: 'missing_legacy_login_key', auth_path: authPath, env_path: envPath };
457
+ const baseUrl = status.base_url || parseCodexLbEnvBaseUrl(await readText(envPath, ''));
458
+ if (!baseUrl) return { ok: false, status: 'missing_base_url', auth_path: authPath, env_path: envPath };
459
+ await ensureDir(path.dirname(envPath));
460
+ await writeTextAtomic(envPath, `export CODEX_LB_BASE_URL=${shellSingleQuote(normalizeCodexLbBaseUrl(baseUrl))}\nexport CODEX_LB_API_KEY=${shellSingleQuote(apiKey)}\n`);
461
+ await fsp.chmod(envPath, 0o600).catch(() => {});
462
+ return { ok: true, status: 'migrated_login_cache', auth_path: authPath, env_path: envPath, base_url: normalizeCodexLbBaseUrl(baseUrl) };
463
+ }
464
+
414
465
  export async function maybePromptCodexLbSetupForLaunch(args = [], opts = {}) {
415
466
  if (args.includes('--json') || args.includes('--skip-codex-lb') || process.env.SKS_SKIP_CODEX_LB_PROMPT === '1') return { status: 'skipped' };
416
467
  const status = await codexLbStatus(opts);
417
468
  if (status.ok) {
418
- const codexLogin = await ensureCodexLbLoginFromEnv(status, opts);
419
- if (codexLogin.status === 'synced') console.log('codex-lb auth synced with Codex CLI.');
469
+ const codexEnvironment = await syncCodexLbProviderEnvironment(status, opts);
470
+ if (codexEnvironment.status === 'synced') console.log('codex-lb provider auth synced for this user session.');
471
+ const apiKey = parseCodexLbEnvKey(await readText(status.env_path, ''));
472
+ const codexLogin = await maybeSyncCodexLbSharedLogin(apiKey, { ...opts, home: opts.home || process.env.HOME || os.homedir() });
473
+ if (codexLogin.status === 'synced') console.log('codex-lb auth synced with Codex CLI login cache.');
420
474
  const chainHealth = await checkCodexLbResponseChain(status, opts);
421
475
  if (!chainHealth.ok && chainHealth.chain_unhealthy) {
422
476
  console.log(`codex-lb response chain check failed (${chainHealth.status}); bypassing codex-lb for this launch.`);
423
- return { status: 'chain_unhealthy', ...status, ok: false, codex_login: codexLogin, chain_health: chainHealth, bypass_codex_lb: true };
477
+ return { status: 'chain_unhealthy', ...status, ok: false, codex_environment: codexEnvironment, codex_login: codexLogin, chain_health: chainHealth, bypass_codex_lb: true };
424
478
  }
425
- return { status: 'present', ...status, codex_login: codexLogin, chain_health: chainHealth };
479
+ return { status: 'present', ...status, codex_environment: codexEnvironment, codex_login: codexLogin, chain_health: chainHealth };
426
480
  }
427
481
  if (!canAskYesNo()) return { status: 'non_interactive', codex_lb: status };
428
482
  const useCodexLb = (await askPostinstallQuestion('\nAuthenticate and route Codex through codex-lb? [y/N] ')).trim();
@@ -435,12 +489,57 @@ export async function maybePromptCodexLbSetupForLaunch(args = [], opts = {}) {
435
489
  return configured;
436
490
  }
437
491
 
438
- async function ensureCodexLbLoginFromEnv(status = {}, opts = {}) {
492
+ async function syncCodexLbProviderEnvironment(status = {}, opts = {}) {
439
493
  const home = opts.home || process.env.HOME || os.homedir();
440
494
  const envPath = opts.envPath || status.env_path || codexLbEnvPath(home);
441
- const apiKey = parseCodexLbEnvKey(await readText(envPath, ''));
495
+ const envText = await readText(envPath, '');
496
+ const apiKey = parseCodexLbEnvKey(envText);
442
497
  if (!apiKey) return { ok: false, status: 'missing_env_key' };
443
- return syncCodexApiKeyLogin(apiKey, { ...opts, home, force: true });
498
+ const baseUrl = status.base_url || parseCodexLbEnvBaseUrl(envText);
499
+ process.env.CODEX_LB_API_KEY = apiKey;
500
+ if (baseUrl) process.env.CODEX_LB_BASE_URL = baseUrl;
501
+ const launchEnv = await syncCodexLbMacLaunchEnvironment({ CODEX_LB_API_KEY: apiKey, ...(baseUrl ? { CODEX_LB_BASE_URL: baseUrl } : {}) }, opts);
502
+ const ok = launchEnv.ok || launchEnv.skipped || launchEnv.status === 'not_macos';
503
+ return {
504
+ ok,
505
+ status: launchEnv.status === 'synced' ? 'synced' : ok ? 'process_env' : launchEnv.status,
506
+ env_path: envPath,
507
+ base_url: baseUrl || null,
508
+ launch_environment: launchEnv,
509
+ error: launchEnv.error || null
510
+ };
511
+ }
512
+
513
+ async function syncCodexLbMacLaunchEnvironment(values = {}, opts = {}) {
514
+ if (opts.syncLaunchEnv === false || process.env.SKS_SKIP_CODEX_LB_LAUNCH_ENV === '1') return { ok: true, status: 'skipped', skipped: true, reason: 'SKS_SKIP_CODEX_LB_LAUNCH_ENV=1' };
515
+ if (process.platform !== 'darwin' && !opts.forceLaunchEnv) return { ok: true, status: 'not_macos', skipped: true };
516
+ const launchctl = opts.launchctlBin || await which('launchctl').catch(() => null) || await exists('/bin/launchctl').then((ok) => ok ? '/bin/launchctl' : null).catch(() => null);
517
+ if (!launchctl) return { ok: false, status: 'launchctl_missing', error: 'launchctl not found on PATH' };
518
+ const variables = Object.entries(values).filter(([, value]) => value);
519
+ const results = [];
520
+ for (const [key, value] of variables) {
521
+ const result = await runProcess(launchctl, ['setenv', key, value], { timeoutMs: 5000, maxOutputBytes: 8192 });
522
+ results.push({
523
+ key,
524
+ ok: result.code === 0,
525
+ error: result.code === 0 ? null : redactSecretText(result.stderr || result.stdout || 'launchctl setenv failed', [value]).trim()
526
+ });
527
+ }
528
+ const failed = results.filter((result) => !result.ok);
529
+ if (failed.length) return { ok: false, status: 'launch_env_failed', variables: results.map((result) => result.key), failed, error: failed.map((result) => `${result.key}: ${result.error}`).join('; ') };
530
+ return { ok: true, status: 'synced', variables: results.map((result) => result.key) };
531
+ }
532
+
533
+ async function maybeSyncCodexLbSharedLogin(apiKey, opts = {}) {
534
+ if (!apiKey) return { ok: false, status: 'missing_env_key' };
535
+ if (!shouldSyncCodexLbSharedLogin(opts)) {
536
+ return { ok: true, status: 'skipped', reason: 'codex-lb uses provider env_key auth; set SKS_CODEX_LB_SYNC_CODEX_LOGIN=1 to also rewrite Codex shared login cache.' };
537
+ }
538
+ return syncCodexApiKeyLogin(apiKey, opts);
539
+ }
540
+
541
+ function shouldSyncCodexLbSharedLogin(opts = {}) {
542
+ return opts.syncCodexLogin === true || process.env.SKS_CODEX_LB_SYNC_CODEX_LOGIN === '1';
444
543
  }
445
544
 
446
545
  async function syncCodexApiKeyLogin(apiKey, opts = {}) {
@@ -460,7 +559,7 @@ async function syncCodexApiKeyLogin(apiKey, opts = {}) {
460
559
  }
461
560
 
462
561
  function upsertCodexLbConfig(text = '', baseUrl) {
463
- let next = upsertTopLevelTomlString(text, 'model_provider', 'codex-lb');
562
+ let next = removeTopLevelTomlKeyIfValue(text, 'model_provider', 'codex-lb');
464
563
  const block = [
465
564
  '[model_providers.codex-lb]',
466
565
  'name = "OpenAI"',
@@ -547,6 +646,14 @@ function removeLegacyTopLevelCodexModeLocks(text = '') {
547
646
  }).join('\n').replace(/^\n+/, '').replace(/\n{3,}/g, '\n\n');
548
647
  }
549
648
 
649
+ function removeTopLevelTomlKeyIfValue(text = '', key = '', value = '') {
650
+ const lines = String(text || '').split('\n');
651
+ const firstTable = lines.findIndex((x) => /^\s*\[.+\]\s*$/.test(x));
652
+ const end = firstTable === -1 ? lines.length : firstTable;
653
+ const keyPattern = new RegExp(`^\\s*${escapeRegExp(key)}\\s*=\\s*"${escapeRegExp(value)}"\\s*(?:#.*)?$`);
654
+ return lines.filter((line, index) => index >= end || !keyPattern.test(line)).join('\n').replace(/^\n+/, '').replace(/\n{3,}/g, '\n\n');
655
+ }
656
+
550
657
  function removeTomlTableKey(text, table, key) {
551
658
  const lines = String(text || '').trimEnd().split('\n');
552
659
  if (lines.length === 1 && lines[0] === '') return '';
@@ -655,6 +762,19 @@ function parseCodexLbEnvBaseUrl(text = '') {
655
762
  return value ? normalizeCodexLbBaseUrl(value) : '';
656
763
  }
657
764
 
765
+ function parseCodexSharedLoginApiKey(text = '') {
766
+ try {
767
+ const parsed = JSON.parse(String(text || ''));
768
+ const authMode = String(parsed?.auth_mode || parsed?.authMode || parsed?.mode || '').toLowerCase();
769
+ const key = parsed?.key || parsed?.api_key || parsed?.apiKey || parsed?.openai_api_key || parsed?.OPENAI_API_KEY;
770
+ if (!key || typeof key !== 'string') return '';
771
+ if (authMode && !/api[-_]?key|apikey/.test(authMode)) return '';
772
+ return key.trim();
773
+ } catch {
774
+ return '';
775
+ }
776
+ }
777
+
658
778
  function parseShellEnvValue(text = '', key = '') {
659
779
  const re = new RegExp(`^\\s*(?:export\\s+)?${escapeRegExp(key)}\\s*=\\s*(.+?)\\s*$`, 'm');
660
780
  const envMatch = String(text || '').match(re);
@@ -1074,6 +1194,24 @@ async function safeReadText(file, fallback = '') {
1074
1194
  }
1075
1195
  }
1076
1196
 
1197
+ async function codexLbLoginCallCount(home) {
1198
+ return (await safeReadText(path.join(home, '.codex', 'login-calls.log'))).trim().split(/\r?\n/).filter(Boolean).length;
1199
+ }
1200
+
1201
+ function codexLbPostinstallEnv(baseEnv, overrides = {}) {
1202
+ return {
1203
+ ...baseEnv,
1204
+ SKS_POSTINSTALL_NO_BOOTSTRAP: '1',
1205
+ SKS_SKIP_POSTINSTALL_SHIM: '1',
1206
+ SKS_SKIP_POSTINSTALL_CONTEXT7: '1',
1207
+ SKS_SKIP_POSTINSTALL_GETDESIGN: '1',
1208
+ SKS_SKIP_POSTINSTALL_GLOBAL_SKILLS: '1',
1209
+ SKS_SKIP_POSTINSTALL_CODEX_LB_AUTH: '0',
1210
+ SKS_SKIP_CODEX_LB_LAUNCH_ENV: '1',
1211
+ ...overrides
1212
+ };
1213
+ }
1214
+
1077
1215
  export async function selftestCodexLb(tmp) {
1078
1216
  const codexLbHome = path.join(tmp, 'codex-lb-home');
1079
1217
  await ensureDir(path.join(codexLbHome, '.codex'));
@@ -1083,7 +1221,7 @@ export async function selftestCodexLb(tmp) {
1083
1221
  await writeTextAtomic(codexLbFakeCodex, "#!/bin/sh\nif [ \"$1\" = \"--version\" ]; then echo \"codex-cli 99.0.0\"; exit 0; fi\nif [ \"$1\" = \"login\" ] && [ \"$2\" = \"status\" ]; then echo \"logged in with browser auth\"; exit 0; fi\nif [ \"$1\" = \"login\" ] && [ \"$2\" = \"--with-api-key\" ]; then read key; mkdir -p \"$HOME/.codex\"; printf '{\\\"auth_mode\\\":\\\"apikey\\\",\\\"key\\\":\\\"%s\\\"}\\n' \"$key\" > \"$HOME/.codex/auth.json\"; printf '%s\\n' \"$key\" >> \"$HOME/.codex/login-calls.log\"; exit 0; fi\necho \"fake codex unsupported\" >&2\nexit 1\n");
1084
1222
  await fsp.chmod(codexLbFakeCodex, 0o755);
1085
1223
  await writeTextAtomic(path.join(codexLbHome, '.codex', 'config.toml'), 'model = "gpt-5.5"\nmodel_reasoning_effort = "low"\nservice_tier = "fast"\n\n[profiles.custom]\nmodel_reasoning_effort = "low"\n\n[notice]\nfast_default_opt_out = true\n\n[features]\ncodex_hooks = true\n');
1086
- const codexLbEnvForSelftest = { HOME: codexLbHome, SKS_GLOBAL_ROOT: path.join(tmp, 'codex-lb-global'), PATH: `${codexLbFakeBin}${path.delimiter}${process.env.PATH || ''}` };
1224
+ const codexLbEnvForSelftest = { HOME: codexLbHome, SKS_GLOBAL_ROOT: path.join(tmp, 'codex-lb-global'), PATH: `${codexLbFakeBin}${path.delimiter}${process.env.PATH || ''}`, SKS_SKIP_CODEX_LB_LAUNCH_ENV: '1' };
1087
1225
  const codexLbSetup = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'codex-lb', 'setup', '--host', 'lb.example.test', '--api-key', 'sk-test', '--json'], {
1088
1226
  cwd: tmp,
1089
1227
  env: codexLbEnvForSelftest,
@@ -1095,48 +1233,50 @@ export async function selftestCodexLb(tmp) {
1095
1233
  const codexLbConfig = await safeReadText(path.join(codexLbHome, '.codex', 'config.toml'));
1096
1234
  const codexLbEnv = await safeReadText(path.join(codexLbHome, '.codex', 'sks-codex-lb.env'));
1097
1235
  const codexLbAuth = await safeReadText(path.join(codexLbHome, '.codex', 'auth.json'));
1098
- if (!codexLbSetupJson.ok || codexLbSetupJson.base_url !== 'https://lb.example.test/backend-api/codex' || !codexLbConfig.includes('model_provider = "codex-lb"') || !codexLbConfig.includes('[model_providers.codex-lb]') || !codexLbEnv.includes("CODEX_LB_BASE_URL='https://lb.example.test/backend-api/codex'") || !codexLbEnv.includes("CODEX_LB_API_KEY='sk-test'") || !/(\"auth_mode\"\s*:\s*\"apikey\")/.test(codexLbAuth)) throw new Error('selftest: codex-lb setup');
1236
+ if (!codexLbSetupJson.ok || codexLbSetupJson.base_url !== 'https://lb.example.test/backend-api/codex' || hasTopLevelCodexLbSelected(codexLbConfig) || !codexLbConfig.includes('[model_providers.codex-lb]') || !codexLbEnv.includes("CODEX_LB_BASE_URL='https://lb.example.test/backend-api/codex'") || !codexLbEnv.includes("CODEX_LB_API_KEY='sk-test'") || codexLbSetupJson.codex_environment?.ok !== true || codexLbSetupJson.codex_login?.status !== 'skipped' || codexLbAuth.trim()) throw new Error('selftest: codex-lb setup');
1237
+ const codexLbFailLaunchctl = path.join(codexLbFakeBin, 'launchctl-fail');
1238
+ await writeTextAtomic(codexLbFailLaunchctl, '#!/bin/sh\necho "launchctl denied" >&2\nexit 7\n');
1239
+ await fsp.chmod(codexLbFailLaunchctl, 0o755);
1240
+ const codexLbFailedLaunchEnv = await configureCodexLb({ home: path.join(tmp, 'codex-lb-launch-fail-home'), host: 'lb.example.test', apiKey: 'sk-fail', forceLaunchEnv: true, launchctlBin: codexLbFailLaunchctl });
1241
+ if (codexLbFailedLaunchEnv.ok || codexLbFailedLaunchEnv.status !== 'launch_env_failed' || !/launchctl denied/.test(codexLbFailedLaunchEnv.error || '')) throw new Error('selftest: codex-lb setup must expose launch-env failure');
1099
1242
  if (!hasCodexUnstableFeatureWarningSuppression(codexLbConfig)) throw new Error('selftest: codex-lb setup did not suppress Codex unstable feature warning');
1100
1243
  await initProject(codexLbHome, { installScope: 'global', force: true, repair: true });
1101
1244
  const codexLbRepairSetupConfig = await safeReadText(path.join(codexLbHome, '.codex', 'config.toml'));
1102
- if (!codexLbRepairSetupConfig.includes('model_provider = "codex-lb"') || !codexLbRepairSetupConfig.includes('[model_providers.codex-lb]') || !codexLbRepairSetupConfig.includes('https://lb.example.test/backend-api/codex') || codexLbRepairSetupConfig.includes('sk-test')) throw new Error('selftest: init codex-lb');
1245
+ if (hasTopLevelCodexLbSelected(codexLbRepairSetupConfig) || !codexLbRepairSetupConfig.includes('[model_providers.codex-lb]') || !codexLbRepairSetupConfig.includes('https://lb.example.test/backend-api/codex') || codexLbRepairSetupConfig.includes('sk-test')) throw new Error('selftest: init codex-lb');
1103
1246
  if (!hasCodexUnstableFeatureWarningSuppression(codexLbRepairSetupConfig)) throw new Error('selftest: init codex-lb did not suppress Codex unstable feature warning');
1104
1247
  await writeTextAtomic(path.join(codexLbHome, '.codex', 'config.toml'), `${codexLbConfig}\n[mcp_servers.supabase]\nurl = "https://mcp.supabase.com/mcp?project_ref=ref&read_only=true&features=database,docs"\n`);
1105
1248
  const ptmp = path.join(tmp, 'codex-lb-project-config'), prevHome = process.env.HOME;
1106
1249
  try { process.env.HOME = codexLbHome; await initProject(ptmp, { installScope: 'global' }); }
1107
1250
  finally { if (prevHome === undefined) delete process.env.HOME; else process.env.HOME = prevHome; }
1108
1251
  const pcfg = await safeReadText(path.join(ptmp, '.codex', 'config.toml'));
1109
- if (!pcfg.includes('model_provider = "codex-lb"') || !pcfg.includes('[model_providers.codex-lb]') || !pcfg.includes('[mcp_servers.supabase]') || !pcfg.includes('read_only=true')) throw new Error('selftest: project codex-lb');
1252
+ if (hasTopLevelCodexLbSelected(pcfg) || !pcfg.includes('[model_providers.codex-lb]') || !pcfg.includes('[mcp_servers.supabase]') || !pcfg.includes('read_only=true')) throw new Error('selftest: project codex-lb');
1110
1253
  if (!hasCodexUnstableFeatureWarningSuppression(pcfg)) throw new Error('selftest: project codex-lb config did not suppress Codex unstable feature warning');
1111
1254
  await writeTextAtomic(path.join(codexLbHome, '.codex', 'auth.json'), '{"auth_mode":"browser"}\n');
1112
1255
  const codexLbRepair = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'auth', 'repair', '--json'], { cwd: tmp, env: codexLbEnvForSelftest, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
1113
1256
  if (codexLbRepair.code !== 0) throw new Error(`selftest: codex-lb repair exited ${codexLbRepair.code}: ${codexLbRepair.stderr}`);
1114
1257
  const codexLbRepairJson = JSON.parse(codexLbRepair.stdout);
1115
1258
  const codexLbRepairedAuth = await safeReadText(path.join(codexLbHome, '.codex', 'auth.json'));
1116
- if (!codexLbRepairJson.ok || codexLbRepairJson.status !== 'repaired' || !codexLbRepairedAuth.includes('"auth_mode":"apikey"') || !codexLbRepairedAuth.includes('sk-test')) throw new Error('selftest: codex-lb repair');
1117
- const codexLbLoginCallsBeforePostinstall = (await safeReadText(path.join(codexLbHome, '.codex', 'login-calls.log'))).trim().split(/\r?\n/).filter(Boolean).length;
1259
+ if (!codexLbRepairJson.ok || codexLbRepairJson.status !== 'repaired' || codexLbRepairJson.codex_environment?.ok !== true || codexLbRepairJson.codex_login?.status !== 'skipped' || !codexLbRepairedAuth.includes('"auth_mode":"browser"') || codexLbRepairedAuth.includes('sk-test')) throw new Error('selftest: codex-lb repair');
1260
+ const codexLbLegacyRepair = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'auth', 'repair', '--json'], { cwd: tmp, env: { ...codexLbEnvForSelftest, SKS_CODEX_LB_SYNC_CODEX_LOGIN: '1' }, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
1261
+ if (codexLbLegacyRepair.code !== 0) throw new Error(`selftest: codex-lb legacy login repair exited ${codexLbLegacyRepair.code}: ${codexLbLegacyRepair.stderr}`);
1262
+ const codexLbLegacyRepairJson = JSON.parse(codexLbLegacyRepair.stdout);
1263
+ const codexLbLegacyAuth = await safeReadText(path.join(codexLbHome, '.codex', 'auth.json'));
1264
+ if (!codexLbLegacyRepairJson.ok || codexLbLegacyRepairJson.codex_login?.status !== 'synced' || !codexLbLegacyAuth.includes('"auth_mode":"apikey"') || !codexLbLegacyAuth.includes('sk-test')) throw new Error('selftest: codex-lb legacy login repair');
1265
+ const codexLbLoginCallsBeforePostinstall = await codexLbLoginCallCount(codexLbHome);
1118
1266
  await writeTextAtomic(path.join(codexLbHome, '.codex', 'auth.json'), '{"auth_mode":"browser"}\n');
1119
1267
  const codexLbPostinstall = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'postinstall'], {
1120
1268
  cwd: tmp,
1121
- env: {
1122
- ...codexLbEnvForSelftest,
1123
- SKS_POSTINSTALL_NO_BOOTSTRAP: '1',
1124
- SKS_SKIP_POSTINSTALL_SHIM: '1',
1125
- SKS_SKIP_POSTINSTALL_CONTEXT7: '1',
1126
- SKS_SKIP_POSTINSTALL_GETDESIGN: '1',
1127
- SKS_SKIP_POSTINSTALL_GLOBAL_SKILLS: '1',
1128
- SKS_SKIP_POSTINSTALL_CODEX_LB_AUTH: '0'
1129
- },
1269
+ env: codexLbPostinstallEnv(codexLbEnvForSelftest),
1130
1270
  timeoutMs: 15000,
1131
1271
  maxOutputBytes: 128 * 1024
1132
1272
  });
1133
1273
  if (codexLbPostinstall.code !== 0) throw new Error(`selftest: codex-lb postinstall auth preservation exited ${codexLbPostinstall.code}: ${codexLbPostinstall.stderr}`);
1134
1274
  const codexLbPostinstallAuth = await safeReadText(path.join(codexLbHome, '.codex', 'auth.json'));
1135
- const codexLbLoginCallsAfterPostinstall = (await safeReadText(path.join(codexLbHome, '.codex', 'login-calls.log'))).trim().split(/\r?\n/).filter(Boolean).length;
1136
- if (!String(codexLbPostinstall.stdout || '').includes('codex-lb auth: preserved') || !codexLbPostinstallAuth.includes('"auth_mode":"apikey"') || !codexLbPostinstallAuth.includes('sk-test') || codexLbLoginCallsAfterPostinstall <= codexLbLoginCallsBeforePostinstall) throw new Error('selftest: postinstall auth');
1137
- const postinstallEnvKeys = ['HOME', 'PATH', 'INIT_CWD', 'SKS_GLOBAL_ROOT', 'SKS_POSTINSTALL_BOOTSTRAP', 'SKS_POSTINSTALL_NO_BOOTSTRAP', 'SKS_SKIP_POSTINSTALL_SHIM', 'SKS_SKIP_POSTINSTALL_CONTEXT7', 'SKS_SKIP_POSTINSTALL_GETDESIGN', 'SKS_SKIP_POSTINSTALL_GLOBAL_SKILLS', 'SKS_SKIP_POSTINSTALL_CODEX_LB_AUTH'];
1275
+ const codexLbLoginCallsAfterPostinstall = await codexLbLoginCallCount(codexLbHome);
1276
+ if (!String(codexLbPostinstall.stdout || '').includes('codex-lb auth: preserved') || !codexLbPostinstallAuth.includes('"auth_mode":"browser"') || codexLbPostinstallAuth.includes('sk-test') || codexLbLoginCallsAfterPostinstall !== codexLbLoginCallsBeforePostinstall) throw new Error('selftest: postinstall auth');
1277
+ const postinstallEnvKeys = ['HOME', 'PATH', 'INIT_CWD', 'SKS_GLOBAL_ROOT', 'SKS_POSTINSTALL_BOOTSTRAP', 'SKS_POSTINSTALL_NO_BOOTSTRAP', 'SKS_SKIP_POSTINSTALL_SHIM', 'SKS_SKIP_POSTINSTALL_CONTEXT7', 'SKS_SKIP_POSTINSTALL_GETDESIGN', 'SKS_SKIP_POSTINSTALL_GLOBAL_SKILLS', 'SKS_SKIP_POSTINSTALL_CODEX_LB_AUTH', 'SKS_SKIP_CODEX_LB_LAUNCH_ENV', 'SKS_CODEX_LB_SYNC_CODEX_LOGIN'];
1138
1278
  const postinstallEnvBefore = Object.fromEntries(postinstallEnvKeys.map((key) => [key, process.env[key]]));
1139
- const codexLbLoginCallsBeforeBootstrap = (await safeReadText(path.join(codexLbHome, '.codex', 'login-calls.log'))).trim().split(/\r?\n/).filter(Boolean).length;
1279
+ const codexLbLoginCallsBeforeBootstrap = await codexLbLoginCallCount(codexLbHome);
1140
1280
  try {
1141
1281
  for (const key of postinstallEnvKeys) delete process.env[key];
1142
1282
  Object.assign(process.env, {
@@ -1149,7 +1289,8 @@ export async function selftestCodexLb(tmp) {
1149
1289
  SKS_SKIP_POSTINSTALL_CONTEXT7: '1',
1150
1290
  SKS_SKIP_POSTINSTALL_GETDESIGN: '1',
1151
1291
  SKS_SKIP_POSTINSTALL_GLOBAL_SKILLS: '1',
1152
- SKS_SKIP_POSTINSTALL_CODEX_LB_AUTH: '0'
1292
+ SKS_SKIP_POSTINSTALL_CODEX_LB_AUTH: '0',
1293
+ SKS_SKIP_CODEX_LB_LAUNCH_ENV: '1'
1153
1294
  });
1154
1295
  await postinstall({
1155
1296
  bootstrap: async () => {
@@ -1165,9 +1306,9 @@ export async function selftestCodexLb(tmp) {
1165
1306
  }
1166
1307
  const codexLbPostBootstrapAuth = await safeReadText(path.join(codexLbHome, '.codex', 'auth.json'));
1167
1308
  const codexLbPostBootstrapConfig = await safeReadText(path.join(codexLbHome, '.codex', 'config.toml'));
1168
- const codexLbLoginCallsAfterBootstrap = (await safeReadText(path.join(codexLbHome, '.codex', 'login-calls.log'))).trim().split(/\r?\n/).filter(Boolean).length;
1169
- if (!codexLbPostBootstrapAuth.includes('"auth_mode":"apikey"') || !codexLbPostBootstrapAuth.includes('sk-test') || codexLbLoginCallsAfterBootstrap <= codexLbLoginCallsBeforeBootstrap) throw new Error('selftest: postinstall drift auth');
1170
- if (!codexLbPostBootstrapConfig.includes('model_provider = "codex-lb"') || !codexLbPostBootstrapConfig.includes('[model_providers.codex-lb]') || !codexLbPostBootstrapConfig.includes('https://lb.example.test/backend-api/codex') || codexLbPostBootstrapConfig.includes('sk-test')) throw new Error('selftest: postinstall drift config');
1309
+ const codexLbLoginCallsAfterBootstrap = await codexLbLoginCallCount(codexLbHome);
1310
+ if (!codexLbPostBootstrapAuth.includes('"auth_mode":"browser"') || codexLbPostBootstrapAuth.includes('sk-test') || codexLbLoginCallsAfterBootstrap !== codexLbLoginCallsBeforeBootstrap) throw new Error('selftest: postinstall drift auth');
1311
+ if (hasTopLevelCodexLbSelected(codexLbPostBootstrapConfig) || !codexLbPostBootstrapConfig.includes('[model_providers.codex-lb]') || !codexLbPostBootstrapConfig.includes('https://lb.example.test/backend-api/codex') || codexLbPostBootstrapConfig.includes('sk-test')) throw new Error('selftest: postinstall drift config');
1171
1312
  const doctorProject = tmpdir();
1172
1313
  await ensureDir(path.join(doctorProject, '.git'));
1173
1314
  await writeTextAtomic(path.join(doctorProject, 'package.json'), '{"name":"codex-lb-doctor-project","version":"0.0.0"}\n');
@@ -1184,7 +1325,7 @@ export async function selftestCodexLb(tmp) {
1184
1325
  const codexLbDoctorJson = JSON.parse(codexLbDoctorRepair.stdout);
1185
1326
  const codexLbDoctorAuth = await safeReadText(path.join(codexLbHome, '.codex', 'auth.json'));
1186
1327
  const codexLbDoctorConfig = await safeReadText(path.join(codexLbHome, '.codex', 'config.toml'));
1187
- if (!codexLbDoctorJson.repair?.codex_lb?.ok || !codexLbDoctorJson.repair.codex_lb.config_repaired || !codexLbDoctorJson.codex_lb?.ok || !codexLbDoctorAuth.includes('"auth_mode":"apikey"') || !codexLbDoctorAuth.includes('sk-test') || !codexLbDoctorConfig.includes('model_provider = "codex-lb"') || !codexLbDoctorConfig.includes('https://lb.example.test/backend-api/codex') || !hasCodexUnstableFeatureWarningSuppression(codexLbDoctorConfig)) throw new Error('selftest: doctor codex-lb');
1328
+ if (!codexLbDoctorJson.repair?.codex_lb?.ok || !codexLbDoctorJson.repair.codex_lb.config_repaired || !codexLbDoctorJson.codex_lb?.ok || !codexLbDoctorAuth.includes('"auth_mode":"browser"') || codexLbDoctorAuth.includes('sk-test') || hasTopLevelCodexLbSelected(codexLbDoctorConfig) || !codexLbDoctorConfig.includes('https://lb.example.test/backend-api/codex') || !hasCodexUnstableFeatureWarningSuppression(codexLbDoctorConfig)) throw new Error('selftest: doctor codex-lb');
1188
1329
  const codexLbContext7Bin = path.join(tmp, 'codex-lb-context7-bin');
1189
1330
  await ensureDir(codexLbContext7Bin);
1190
1331
  await writeTextAtomic(path.join(codexLbContext7Bin, 'codex'), '#!/bin/sh\nif [ "$1" = "--version" ]; then echo "codex-cli 99.0.0"; exit 0; fi\nif [ "$CODEX_LB_API_KEY" ]; then echo "context7 leaked CODEX_LB_API_KEY" >&2; exit 77; fi\nif [ "$1" = "mcp" ] && [ "$2" = "list" ]; then echo ""; exit 0; fi\nif [ "$1" = "mcp" ] && [ "$2" = "add" ]; then echo "context7 added"; exit 0; fi\necho "unexpected codex $*" >&2\nexit 2\n');
@@ -1206,23 +1347,82 @@ export async function selftestCodexLb(tmp) {
1206
1347
  });
1207
1348
  if (codexLbContext7Postinstall.code !== 0 || String(`${codexLbContext7Postinstall.stdout}\n${codexLbContext7Postinstall.stderr}`).includes('leaked CODEX_LB_API_KEY')) throw new Error('selftest: Context7 key leak');
1208
1349
  await writeTextAtomic(path.join(codexLbHome, '.codex', 'sks-codex-lb.env'), "export CODEX_LB_API_KEY='unterminated\n");
1209
- const codexLbLoginCallsBeforeMalformed = (await safeReadText(path.join(codexLbHome, '.codex', 'login-calls.log'))).trim().split(/\r?\n/).filter(Boolean).length;
1350
+ const codexLbLoginCallsBeforeMalformed = await codexLbLoginCallCount(codexLbHome);
1210
1351
  const codexLbMalformedPostinstall = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'postinstall'], {
1211
1352
  cwd: tmp,
1212
- env: {
1213
- ...codexLbEnvForSelftest,
1214
- SKS_POSTINSTALL_NO_BOOTSTRAP: '1',
1215
- SKS_SKIP_POSTINSTALL_SHIM: '1',
1216
- SKS_SKIP_POSTINSTALL_CONTEXT7: '1',
1217
- SKS_SKIP_POSTINSTALL_GETDESIGN: '1',
1218
- SKS_SKIP_POSTINSTALL_GLOBAL_SKILLS: '1',
1219
- SKS_SKIP_POSTINSTALL_CODEX_LB_AUTH: '0'
1220
- },
1353
+ env: codexLbPostinstallEnv(codexLbEnvForSelftest),
1221
1354
  timeoutMs: 15000,
1222
1355
  maxOutputBytes: 128 * 1024
1223
1356
  });
1224
- const codexLbLoginCallsAfterMalformed = (await safeReadText(path.join(codexLbHome, '.codex', 'login-calls.log'))).trim().split(/\r?\n/).filter(Boolean).length;
1357
+ const codexLbLoginCallsAfterMalformed = await codexLbLoginCallCount(codexLbHome);
1225
1358
  if (codexLbMalformedPostinstall.code !== 0 || !String(codexLbMalformedPostinstall.stdout || '').includes('codex-lb auth: stored key missing') || codexLbLoginCallsAfterMalformed !== codexLbLoginCallsBeforeMalformed) throw new Error('selftest: bad codex-lb env');
1359
+ await fsp.rm(path.join(codexLbHome, '.codex', 'sks-codex-lb.env'), { force: true });
1360
+ await writeTextAtomic(path.join(codexLbHome, '.codex', 'config.toml'), '[model_providers.codex-lb]\nname = "OpenAI"\nbase_url = "https://lb.example.test/backend-api/codex"\nwire_api = "responses"\nsupports_websockets = true\nrequires_openai_auth = true\n');
1361
+ await writeTextAtomic(path.join(codexLbHome, '.codex', 'auth.json'), '{"auth_mode":"apikey","key":"sk-legacy"}\n');
1362
+ const codexLbLoginCallsBeforeLegacyPostinstall = await codexLbLoginCallCount(codexLbHome);
1363
+ const codexLbLegacyPostinstall = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'postinstall'], {
1364
+ cwd: tmp,
1365
+ env: codexLbPostinstallEnv(codexLbEnvForSelftest),
1366
+ timeoutMs: 15000,
1367
+ maxOutputBytes: 128 * 1024
1368
+ });
1369
+ if (codexLbLegacyPostinstall.code !== 0) throw new Error(`selftest: legacy codex-lb postinstall restore exited ${codexLbLegacyPostinstall.code}: ${codexLbLegacyPostinstall.stderr}`);
1370
+ const codexLbLegacyPostinstallEnv = await safeReadText(path.join(codexLbHome, '.codex', 'sks-codex-lb.env'));
1371
+ const codexLbLegacyPostinstallAuth = await safeReadText(path.join(codexLbHome, '.codex', 'auth.json'));
1372
+ const codexLbLoginCallsAfterLegacyPostinstall = await codexLbLoginCallCount(codexLbHome);
1373
+ if (!String(codexLbLegacyPostinstall.stdout || '').includes('codex-lb auth: restored from existing Codex login cache') || !codexLbLegacyPostinstallEnv.includes("CODEX_LB_API_KEY='sk-legacy'") || !codexLbLegacyPostinstallEnv.includes("CODEX_LB_BASE_URL='https://lb.example.test/backend-api/codex'") || !codexLbLegacyPostinstallAuth.includes('"auth_mode":"apikey"') || !codexLbLegacyPostinstallAuth.includes('sk-legacy') || codexLbLoginCallsAfterLegacyPostinstall !== codexLbLoginCallsBeforeLegacyPostinstall) throw new Error('selftest: legacy codex-lb postinstall restore');
1374
+ await fsp.rm(path.join(codexLbHome, '.codex', 'sks-codex-lb.env'), { force: true });
1375
+ await writeTextAtomic(path.join(codexLbHome, '.codex', 'config.toml'), 'model_provider = "codex-lb"\n\n[model_providers.codex-lb]\nname = "OpenAI"\nbase_url = "https://lb.example.test/backend-api/codex"\nwire_api = "responses"\nsupports_websockets = true\nrequires_openai_auth = true\n');
1376
+ await writeTextAtomic(path.join(codexLbHome, '.codex', 'auth.json'), '{"auth_mode":"apikey","key":"sk-legacy-doctor"}\n');
1377
+ const codexLbLegacyDoctorProject = tmpdir();
1378
+ await ensureDir(path.join(codexLbLegacyDoctorProject, '.git'));
1379
+ await writeTextAtomic(path.join(codexLbLegacyDoctorProject, 'package.json'), '{"name":"codex-lb-legacy-doctor-project","version":"0.0.0"}\n');
1380
+ const codexLbLegacyDoctorRepair = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'doctor', '--fix', '--json'], {
1381
+ cwd: codexLbLegacyDoctorProject,
1382
+ env: { ...codexLbEnvForSelftest, SKS_GLOBAL_ROOT: path.join(tmp, 'codex-lb-legacy-doctor-global'), SKS_SKIP_CODEX_LB_LAUNCH_ENV: '1' },
1383
+ timeoutMs: 30000,
1384
+ maxOutputBytes: 256 * 1024
1385
+ });
1386
+ if (codexLbLegacyDoctorRepair.code !== 0) throw new Error(`selftest: legacy doctor --fix codex-lb restore exited ${codexLbLegacyDoctorRepair.code}: ${codexLbLegacyDoctorRepair.stderr}`);
1387
+ const codexLbLegacyDoctorJson = JSON.parse(codexLbLegacyDoctorRepair.stdout);
1388
+ const codexLbLegacyDoctorEnv = await safeReadText(path.join(codexLbHome, '.codex', 'sks-codex-lb.env'));
1389
+ const codexLbLegacyDoctorConfig = await safeReadText(path.join(codexLbHome, '.codex', 'config.toml'));
1390
+ const codexLbLegacyDoctorAuth = await safeReadText(path.join(codexLbHome, '.codex', 'auth.json'));
1391
+ if (!codexLbLegacyDoctorJson.repair?.codex_lb?.ok || !codexLbLegacyDoctorJson.repair.codex_lb.legacy_auth_migrated || !codexLbLegacyDoctorEnv.includes("CODEX_LB_API_KEY='sk-legacy-doctor'") || !codexLbLegacyDoctorAuth.includes('"auth_mode":"apikey"') || !codexLbLegacyDoctorAuth.includes('sk-legacy-doctor') || hasTopLevelCodexLbSelected(codexLbLegacyDoctorConfig) || !codexLbLegacyDoctorConfig.includes('env_key = "CODEX_LB_API_KEY"')) throw new Error('selftest: legacy doctor codex-lb restore');
1392
+ await writeTextAtomic(path.join(codexLbHome, '.codex', 'sks-codex-lb.env'), "export CODEX_LB_BASE_URL='https://lb.example.test/backend-api/codex'\n");
1393
+ await writeTextAtomic(path.join(codexLbHome, '.codex', 'config.toml'), 'model = "gpt-5.5"\nservice_tier = "fast"\n');
1394
+ await writeTextAtomic(path.join(codexLbHome, '.codex', 'auth.json'), '{"auth_mode":"apikey","key":"sk-env-only"}\n');
1395
+ const codexLbLoginCallsBeforeEnvOnlyPostinstall = await codexLbLoginCallCount(codexLbHome);
1396
+ const codexLbEnvOnlyPostinstall = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'postinstall'], {
1397
+ cwd: tmp,
1398
+ env: codexLbPostinstallEnv(codexLbEnvForSelftest),
1399
+ timeoutMs: 15000,
1400
+ maxOutputBytes: 128 * 1024
1401
+ });
1402
+ if (codexLbEnvOnlyPostinstall.code !== 0) throw new Error(`selftest: env-only codex-lb postinstall restore exited ${codexLbEnvOnlyPostinstall.code}: ${codexLbEnvOnlyPostinstall.stderr}`);
1403
+ const codexLbEnvOnlyPostinstallEnv = await safeReadText(path.join(codexLbHome, '.codex', 'sks-codex-lb.env'));
1404
+ const codexLbEnvOnlyPostinstallConfig = await safeReadText(path.join(codexLbHome, '.codex', 'config.toml'));
1405
+ const codexLbEnvOnlyPostinstallAuth = await safeReadText(path.join(codexLbHome, '.codex', 'auth.json'));
1406
+ const codexLbLoginCallsAfterEnvOnlyPostinstall = await codexLbLoginCallCount(codexLbHome);
1407
+ if (!String(codexLbEnvOnlyPostinstall.stdout || '').includes('codex-lb auth: restored from existing Codex login cache') || !codexLbEnvOnlyPostinstallEnv.includes("CODEX_LB_API_KEY='sk-env-only'") || !codexLbEnvOnlyPostinstallConfig.includes('env_key = "CODEX_LB_API_KEY"') || !codexLbEnvOnlyPostinstallAuth.includes('sk-env-only') || codexLbLoginCallsAfterEnvOnlyPostinstall !== codexLbLoginCallsBeforeEnvOnlyPostinstall) throw new Error('selftest: env-only codex-lb postinstall restore');
1408
+ await writeTextAtomic(path.join(codexLbHome, '.codex', 'sks-codex-lb.env'), "export CODEX_LB_BASE_URL='https://lb.example.test/backend-api/codex'\n");
1409
+ await writeTextAtomic(path.join(codexLbHome, '.codex', 'config.toml'), 'model = "gpt-5.5"\nservice_tier = "fast"\n');
1410
+ await writeTextAtomic(path.join(codexLbHome, '.codex', 'auth.json'), '{"auth_mode":"apikey","key":"sk-env-only-doctor"}\n');
1411
+ const codexLbEnvOnlyDoctorProject = tmpdir();
1412
+ await ensureDir(path.join(codexLbEnvOnlyDoctorProject, '.git'));
1413
+ await writeTextAtomic(path.join(codexLbEnvOnlyDoctorProject, 'package.json'), '{"name":"codex-lb-env-only-doctor-project","version":"0.0.0"}\n');
1414
+ const codexLbEnvOnlyDoctorRepair = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'doctor', '--fix', '--json'], {
1415
+ cwd: codexLbEnvOnlyDoctorProject,
1416
+ env: { ...codexLbEnvForSelftest, SKS_GLOBAL_ROOT: path.join(tmp, 'codex-lb-env-only-doctor-global'), SKS_SKIP_CODEX_LB_LAUNCH_ENV: '1' },
1417
+ timeoutMs: 30000,
1418
+ maxOutputBytes: 256 * 1024
1419
+ });
1420
+ if (codexLbEnvOnlyDoctorRepair.code !== 0) throw new Error(`selftest: env-only doctor --fix codex-lb restore exited ${codexLbEnvOnlyDoctorRepair.code}: ${codexLbEnvOnlyDoctorRepair.stderr}`);
1421
+ const codexLbEnvOnlyDoctorJson = JSON.parse(codexLbEnvOnlyDoctorRepair.stdout);
1422
+ const codexLbEnvOnlyDoctorEnv = await safeReadText(path.join(codexLbHome, '.codex', 'sks-codex-lb.env'));
1423
+ const codexLbEnvOnlyDoctorConfig = await safeReadText(path.join(codexLbHome, '.codex', 'config.toml'));
1424
+ const codexLbEnvOnlyDoctorAuth = await safeReadText(path.join(codexLbHome, '.codex', 'auth.json'));
1425
+ if (!codexLbEnvOnlyDoctorJson.repair?.codex_lb?.ok || !codexLbEnvOnlyDoctorJson.repair.codex_lb.legacy_auth_migrated || !codexLbEnvOnlyDoctorEnv.includes("CODEX_LB_API_KEY='sk-env-only-doctor'") || !codexLbEnvOnlyDoctorConfig.includes('env_key = "CODEX_LB_API_KEY"') || !codexLbEnvOnlyDoctorAuth.includes('sk-env-only-doctor')) throw new Error('selftest: env-only doctor codex-lb restore');
1226
1426
  await writeTextAtomic(path.join(codexLbHome, '.codex', 'sks-codex-lb.env'), "export CODEX_LB_API_KEY='sk-test'\n");
1227
1427
  const codexLbMissingCli = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'postinstall'], {
1228
1428
  cwd: tmp,
@@ -1235,12 +1435,13 @@ export async function selftestCodexLb(tmp) {
1235
1435
  SKS_SKIP_POSTINSTALL_CONTEXT7: '1',
1236
1436
  SKS_SKIP_POSTINSTALL_GETDESIGN: '1',
1237
1437
  SKS_SKIP_POSTINSTALL_GLOBAL_SKILLS: '1',
1238
- SKS_SKIP_POSTINSTALL_CODEX_LB_AUTH: '0'
1438
+ SKS_SKIP_POSTINSTALL_CODEX_LB_AUTH: '0',
1439
+ SKS_SKIP_CODEX_LB_LAUNCH_ENV: '1'
1239
1440
  },
1240
1441
  timeoutMs: 15000,
1241
1442
  maxOutputBytes: 128 * 1024
1242
1443
  });
1243
- if (codexLbMissingCli.code !== 0 || !String(codexLbMissingCli.stdout || '').includes('codex-lb auth: repair skipped (codex_missing')) throw new Error('selftest: codex missing');
1444
+ if (codexLbMissingCli.code !== 0 || !String(codexLbMissingCli.stdout || '').includes('codex-lb auth: preserved') || String(codexLbMissingCli.stdout || '').includes('codex_missing')) throw new Error('selftest: codex-lb provider auth should not require Codex CLI login');
1244
1445
  const codexLbNotConfiguredHome = path.join(tmp, 'codex-lb-not-configured-home');
1245
1446
  const codexLbNotConfigured = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'postinstall'], {
1246
1447
  cwd: tmp,
@@ -1260,7 +1461,7 @@ export async function selftestCodexLb(tmp) {
1260
1461
  });
1261
1462
  if (codexLbNotConfigured.code !== 0 || String(codexLbNotConfigured.stdout || '').includes('codex-lb auth:')) throw new Error('selftest: postinstall should stay quiet when codex-lb is not configured');
1262
1463
  const codexLbStatusText = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'codex-lb', 'status'], { cwd: tmp, env: codexLbEnvForSelftest, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
1263
- if (!String(codexLbStatusText.stdout || '').includes('Repair auth: sks codex-lb repair')) throw new Error('selftest: codex-lb status did not advertise repair command');
1464
+ if (!String(codexLbStatusText.stdout || '').includes('Repair provider auth: sks codex-lb repair')) throw new Error('selftest: codex-lb status did not advertise repair command');
1264
1465
  const nonInteractiveLaunchChainCalls = [];
1265
1466
  const nonInteractiveLaunch = await maybePromptCodexLbSetupForLaunch([], {
1266
1467
  home: codexLbHome,
@@ -1326,7 +1527,7 @@ function hasTopLevelCodexModeLock(text = '') {
1326
1527
  const lines = String(text || '').split('\n');
1327
1528
  const firstTable = lines.findIndex((x) => /^\s*\[.+\]\s*$/.test(x));
1328
1529
  const top = (firstTable === -1 ? lines : lines.slice(0, firstTable)).join('\n');
1329
- return /(^|\n)\s*model\s*=\s*"codex-lb"\s*(\n|$)/.test(top) || /(^|\n)\s*model_provider\s*=\s*"openai"\s*(\n|$)/.test(top) || /(^|\n)\s*model_reasoning_effort\s*=/.test(top);
1530
+ return /(^|\n)\s*model_provider\s*=\s*"codex-lb"\s*(\n|$)/.test(top) || /(^|\n)\s*model_reasoning_effort\s*=/.test(top);
1330
1531
  }
1331
1532
 
1332
1533
  function hasDeprecatedCodexHooksFeatureFlag(text = '') {
package/src/cli/main.mjs CHANGED
@@ -22,7 +22,7 @@ import { bumpProjectVersion, disableVersionGitHook, runVersionPreCommit, version
22
22
  import { rustInfo } from '../core/rust-accelerator.mjs';
23
23
  import { renderCartridge, validateCartridge, driftCartridge, snapshotCartridge } from '../core/gx-renderer.mjs';
24
24
  import { defaultEvaluationScenario, runEvaluationBenchmark } from '../core/evaluation.mjs';
25
- import { buildResearchPrompt, evaluateResearchGate, writeMockResearchResult, writeResearchPlan } from '../core/research.mjs';
25
+ import { buildResearchPrompt, evaluateResearchGate, isDatedResearchPaperArtifact, writeMockResearchResult, writeResearchPlan } from '../core/research.mjs';
26
26
  import { evaluateRecallPulseFixtures, readMissionStatusLedger, writeRecallPulseArtifacts } from '../core/recallpulse.mjs';
27
27
  import {
28
28
  PPT_AUDIENCE_STRATEGY_ARTIFACT,
@@ -155,11 +155,19 @@ function codexLbImmediateLaunchOpts(args = [], lb = {}, opts = {}) {
155
155
  return { ...opts, session, codexArgs: [...(opts.codexArgs || []), '-c', 'model_provider="openai"'], codexLbBypassed: true };
156
156
  }
157
157
  if (!lb?.ok) return opts;
158
- if (explicitSession) return opts;
158
+ const nextOpts = withCodexLbProviderArgs(opts);
159
+ if (explicitSession) return nextOpts;
159
160
  const session = sanitizeTmuxSessionName(`sks-codex-lb-${Date.now().toString(36)}-${defaultTmuxSessionName(root)}`);
160
161
  console.log(`codex-lb active for this launch: ${lb.env_path || lb.base_url || 'configured'}`);
161
162
  console.log(`Using fresh tmux session: ${session}`);
162
- return { ...opts, session, codexLbFreshSession: true };
163
+ return { ...nextOpts, session, codexLbFreshSession: true };
164
+ }
165
+
166
+ function withCodexLbProviderArgs(opts = {}) {
167
+ const codexArgs = [...(opts.codexArgs || [])];
168
+ const hasProviderOverride = codexArgs.some((arg) => /model_provider\s*=/.test(String(arg || '')));
169
+ if (!hasProviderOverride) codexArgs.push('-c', 'model_provider="codex-lb"');
170
+ return { ...opts, codexArgs };
163
171
  }
164
172
 
165
173
  function help(args = []) {
@@ -1141,7 +1149,7 @@ async function codexLbCommand(action = 'status', args = []) {
1141
1149
  console.log(`Env file: ${status.env_file ? status.env_path : 'missing'}`);
1142
1150
  if (status.base_url) console.log(`Base URL: ${status.base_url}`);
1143
1151
  if (!status.ok) console.log('\nRun: sks codex-lb setup --host <domain> --api-key <key>');
1144
- else console.log('\nRepair auth: sks codex-lb repair');
1152
+ else console.log('\nRepair provider auth: sks codex-lb repair');
1145
1153
  return;
1146
1154
  }
1147
1155
  if (sub === 'health' || sub === 'verify-chain' || sub === 'chain') {
@@ -1168,7 +1176,7 @@ async function codexLbCommand(action = 'status', args = []) {
1168
1176
  process.exitCode = 1;
1169
1177
  return;
1170
1178
  }
1171
- console.log('codex-lb auth repaired for Codex CLI.');
1179
+ console.log('codex-lb provider auth repaired for Codex CLI/App environment.');
1172
1180
  console.log(`Config: ${result.config_path}`);
1173
1181
  console.log(`Key env: ${result.env_path}`);
1174
1182
  return;
@@ -1185,7 +1193,7 @@ async function codexLbCommand(action = 'status', args = []) {
1185
1193
  const result = await configureCodexLb({ host, apiKey });
1186
1194
  if (json) return console.log(JSON.stringify(result, null, 2));
1187
1195
  if (!result.ok) {
1188
- console.error(`codex-lb setup failed: ${result.status}`);
1196
+ console.error(`codex-lb setup failed: ${result.status}${result.error ? `: ${result.error}` : ''}`);
1189
1197
  process.exitCode = 1;
1190
1198
  return;
1191
1199
  }
@@ -1771,7 +1779,7 @@ async function doctor(args) {
1771
1779
  const cleanup = removed.length ? ` removed stale generated skill shadow(s): ${removed.join(', ')}` : '';
1772
1780
  console.log(`Global $ repair: ${globalSkillsRepair.status} ${globalSkillsRepair.root || ''}${cleanup}`.trimEnd());
1773
1781
  }
1774
- if (codexLbRepair?.ok) console.log(`codex-lb repair: ${codexLbRepair.config_repaired ? 'config+auth resynced' : 'auth resynced'} from stored env`);
1782
+ if (codexLbRepair?.ok) console.log(`codex-lb repair: ${codexLbRepair.config_repaired ? 'config+provider auth resynced' : 'provider auth resynced'} from stored env`);
1775
1783
  else if (codexLbRepair && codexLbRepair.status !== 'missing_env_key') console.log(`codex-lb repair: skipped (${codexLbRepair.status})`);
1776
1784
  if (flag(args, '--fix') && result.harness_conflicts.hard_block) console.log('Repair: skipped because another Codex harness needs human-approved removal first');
1777
1785
  console.log(`Rust acc.: ${rust.available ? rust.version : 'optional-missing'}`);
@@ -3946,17 +3954,21 @@ async function selftest() {
3946
3954
  const researchPlan = await writeResearchPlan(researchDir, researchMission.prompt, {});
3947
3955
  if (researchPlan.methodology !== 'genius-scout-council-frontier-discovery-loop' || researchPlan.web_research_policy?.mode !== 'layered_source_retrieval_and_triangulation') throw new Error('selftest: research plan contract');
3948
3956
  if (researchPlan.execution_policy?.default_max_cycles !== 12 || researchPlan.mutation_policy?.implementation_allowed !== false || !String(researchPlan.research_council?.debate_policy?.rule || '').includes('every scout records final agreement')) throw new Error('selftest: research consensus/no-code contract');
3949
- if (!researchPlan.research_council?.scouts?.every((scout) => scout.display_name && scout.persona && scout.persona_boundary && scout.reasoning_effort === 'xhigh')) throw new Error('selftest: research scout persona contract missing from plan');
3957
+ if (!researchPlan.research_council?.scouts?.every((scout) => scout.agent_name && scout.display_name && scout.persona && scout.persona_boundary && scout.reasoning_effort === 'xhigh') || !researchPlan.research_council.scouts.some((scout) => scout.agent_name === 'Einstein Scout')) throw new Error('selftest: research scout persona contract missing from plan');
3958
+ const researchPaperArtifact = researchPlan.artifacts?.research_paper;
3959
+ if (!isDatedResearchPaperArtifact(researchPaperArtifact) || researchPaperArtifact === 'research-paper.md') throw new Error('selftest: research paper artifact filename is not dated and titled');
3950
3960
  const researchPrompt = buildResearchPrompt({ id: researchMission.id, mission: researchMission, plan: researchPlan, cycle: 1, previous: '' });
3951
- if (!researchPrompt.includes('NO-CODE-MUTATION POLICY') || !researchPrompt.includes('not a fixed three-cycle run') || !researchPrompt.includes('unanimous_consensus=true')) throw new Error('selftest: research prompt missing no-code unanimous consensus policy');
3961
+ if (!researchPrompt.includes('NO-CODE-MUTATION POLICY') || !researchPrompt.includes('not a fixed three-cycle run') || !researchPrompt.includes('unanimous_consensus=true') || !researchPrompt.includes('agent_name') || !researchPrompt.includes(researchPaperArtifact)) throw new Error('selftest: research prompt missing no-code unanimous consensus policy');
3952
3962
  const rArts = researchPlan.required_artifacts || [];
3953
3963
  for (const a of [rss, 'source-ledger.json', 'scout-ledger.json', 'debate-ledger.json', 'falsification-ledger.json']) if (!rArts.includes(a) || !(await exists(path.join(researchDir, a)))) throw new Error('selftest: research artifact');
3954
- if (!rArts.includes('research-paper.md') || !rArts.includes(gos)) throw new Error('selftest: research paper');
3964
+ if (!rArts.includes(researchPaperArtifact) || rArts.includes('research-paper.md') || !rArts.includes(gos)) throw new Error('selftest: research paper');
3955
3965
  const initialResearchGate = await evaluateResearchGate(researchDir);
3956
3966
  if (initialResearchGate.passed || ['web_search_pass_missing', 'eureka_missing', 'debate_exchanges_missing', 'research_paper_missing', 'consensus_iteration_missing', 'unanimous_consensus_missing'].some((r) => !initialResearchGate.reasons.includes(r))) throw new Error('selftest: research gate');
3957
3967
  const researchGate = await writeMockResearchResult(researchDir, researchPlan);
3958
3968
  if (!researchGate.passed) throw new Error('selftest: mock research gate did not pass');
3969
+ if (!(await exists(path.join(researchDir, researchPaperArtifact))) || await exists(path.join(researchDir, 'research-paper.md'))) throw new Error('selftest: mock research paper filename did not use dated title artifact');
3959
3970
  const rm = researchGate.metrics || {};
3971
+ if (rm.research_paper_artifact !== researchPaperArtifact) throw new Error('selftest: research gate did not report dated paper artifact');
3960
3972
  if (rm.scout_persona_contract_ok !== true || (rm.scout_persona_issues || []).length) throw new Error('selftest: research scout persona contract did not pass');
3961
3973
  if (['independent_scouts', 'xhigh_scouts', 'eureka_moments', 'debate_participants', 'genius_opinion_summaries'].some((m) => rm[m] < 5) || ['counterevidence_sources', 'falsification_cases', 'triangulation_checks'].some((m) => rm[m] < 1) || rm.paper_sections < 8 || rm.citation_coverage !== true || rm.source_layers_covered < 7 || rm.consensus_iterations < 1 || rm.unanimous_consensus !== true || rm.consensus_agreed_scouts < 5) throw new Error('selftest: research metrics');
3962
3974
  await writeJsonAtomic(path.join(dir, 'done-gate.json'), { passed: true, unsupported_critical_claims: 0, database_safety_violation: false, database_safety_reviewed: true, visual_drift: 'low', wiki_drift: 'low', tests_required: false });
@@ -9,7 +9,7 @@ import { buildQuestionSchema, writeQuestions } from '../core/questions.mjs';
9
9
  import { sealContract } from '../core/decision-contract.mjs';
10
10
  import { buildQaLoopQuestionSchema, buildQaLoopPrompt, evaluateQaGate, qaStatus, writeMockQaResult, writeQaLoopArtifacts } from '../core/qa-loop.mjs';
11
11
  import { containsUserQuestion, noQuestionContinuationReason } from '../core/no-question-guard.mjs';
12
- import { RESEARCH_GENIUS_SUMMARY_ARTIFACT, RESEARCH_PAPER_ARTIFACT, RESEARCH_SOURCE_SKILL_ARTIFACT, countGeniusOpinionSummaries, countResearchPaperSections, buildResearchPrompt, evaluateResearchGate, writeMockResearchResult, writeResearchPlan } from '../core/research.mjs';
12
+ import { RESEARCH_GENIUS_SUMMARY_ARTIFACT, RESEARCH_SOURCE_SKILL_ARTIFACT, countGeniusOpinionSummaries, countResearchPaperSections, buildResearchPrompt, evaluateResearchGate, findResearchPaperArtifact, researchPaperArtifactForPlan, writeMockResearchResult, writeResearchPlan } from '../core/research.mjs';
13
13
  import { storageReport, enforceRetention, pruneWikiArtifacts } from '../core/retention.mjs';
14
14
  import { evaluateDoneGate } from '../core/hproof.mjs';
15
15
  import { renderCartridge, validateCartridge, driftCartridge, snapshotCartridge } from '../core/gx-renderer.mjs';
@@ -76,11 +76,19 @@ function codexLbImmediateLaunchOpts(args = [], lb = {}, opts = {}) {
76
76
  return { ...opts, session, codexArgs: [...(opts.codexArgs || []), '-c', 'model_provider="openai"'], codexLbBypassed: true };
77
77
  }
78
78
  if (!lb?.ok) return opts;
79
- if (explicitSession) return opts;
79
+ const nextOpts = withCodexLbProviderArgs(opts);
80
+ if (explicitSession) return nextOpts;
80
81
  const session = sanitizeTmuxSessionName(`sks-codex-lb-${Date.now().toString(36)}-${defaultTmuxSessionName(root)}`);
81
82
  console.log(`codex-lb active for this launch: ${lb.env_path || lb.base_url || 'configured'}`);
82
83
  console.log(`Using fresh tmux session: ${session}`);
83
- return { ...opts, session, codexLbFreshSession: true };
84
+ return { ...nextOpts, session, codexLbFreshSession: true };
85
+ }
86
+
87
+ function withCodexLbProviderArgs(opts = {}) {
88
+ const codexArgs = [...(opts.codexArgs || [])];
89
+ const hasProviderOverride = codexArgs.some((arg) => /model_provider\s*=/.test(String(arg || '')));
90
+ if (!hasProviderOverride) codexArgs.push('-c', 'model_provider="codex-lb"');
91
+ return { ...opts, codexArgs };
84
92
  }
85
93
 
86
94
  export async function madHighCommand(args = [], deps = {}) {
@@ -491,7 +499,7 @@ async function researchPrepare(args) {
491
499
  console.log(`Methodology: ${plan.methodology}`);
492
500
  console.log(`Plan: ${path.relative(root, path.join(dir, 'research-plan.md'))}`);
493
501
  console.log(`Pipeline: ${path.relative(root, path.join(dir, PIPELINE_PLAN_ARTIFACT))}`);
494
- console.log(`Paper: ${RESEARCH_PAPER_ARTIFACT}`);
502
+ console.log(`Paper: ${researchPaperArtifactForPlan(plan)}`);
495
503
  console.log(`Genius summary: ${RESEARCH_GENIUS_SUMMARY_ARTIFACT}`);
496
504
  console.log(`Source skill: ${RESEARCH_SOURCE_SKILL_ARTIFACT}`);
497
505
  console.log('Ledgers: source-ledger.json, scout-ledger.json, debate-ledger.json, novelty-ledger.json, falsification-ledger.json');
@@ -612,7 +620,9 @@ async function researchStatus(args) {
612
620
  const falsificationLedger = await readJson(path.join(dir, 'falsification-ledger.json'), null);
613
621
  const sourceSkillText = await readText(path.join(dir, RESEARCH_SOURCE_SKILL_ARTIFACT), '');
614
622
  const geniusSummaryText = await readText(path.join(dir, RESEARCH_GENIUS_SUMMARY_ARTIFACT), '');
615
- const paperText = await readText(path.join(dir, RESEARCH_PAPER_ARTIFACT), '');
623
+ const plan = await readJson(path.join(dir, 'research-plan.json'), null);
624
+ const paperArtifact = await findResearchPaperArtifact(dir, plan);
625
+ const paperText = paperArtifact.exists ? await readText(paperArtifact.path, '') : '';
616
626
  const scoutRows = Array.isArray(scoutLedger?.scouts) ? scoutLedger.scouts : [];
617
627
  const sourceLayerRows = Array.isArray(sourceLedger?.source_layers) ? sourceLedger.source_layers : [];
618
628
  const sourceLayersCovered = sourceLayerRows.filter((layer) => layer.status === 'covered' && ((Array.isArray(layer.source_ids) && layer.source_ids.length) || (Array.isArray(layer.counterevidence_ids) && layer.counterevidence_ids.length))).length;
@@ -635,6 +645,7 @@ async function researchStatus(args) {
635
645
  unanimous_consensus: gate?.metrics?.unanimous_consensus ?? gate?.unanimous_consensus ?? debateLedger?.unanimous_consensus ?? false,
636
646
  research_source_skill_present: Boolean(sourceSkillText.trim()),
637
647
  genius_opinion_summary_present: Boolean(geniusSummaryText.trim()),
648
+ research_paper_artifact: paperArtifact.name,
638
649
  paper_present: Boolean(paperText.trim()),
639
650
  paper_sections: countResearchPaperSections(paperText),
640
651
  falsification_cases: falsificationLedger?.cases?.length ?? null
@@ -401,6 +401,7 @@ async function codexFastModeConfigStatus(opts = {}) {
401
401
  if (!config.text) continue;
402
402
  const topLevel = topLevelToml(config.text);
403
403
  if (/(^|\n)\s*model_reasoning_effort\s*=/.test(topLevel)) blockers.push(`${config.scope}:top_level_model_reasoning_effort`);
404
+ if (/(^|\n)\s*model_provider\s*=\s*"codex-lb"\s*(?:#.*)?(?=\n|$)/.test(topLevel)) blockers.push(`${config.scope}:top_level_codex_lb_provider`);
404
405
  if (/(^|\n)\s*fast_default_opt_out\s*=\s*true\s*(?:#.*)?(?=\n|$)/.test(tomlTable(config.text, 'notice'))) blockers.push(`${config.scope}:fast_default_opt_out`);
405
406
  }
406
407
  const merged = configs.map((config) => config.text).join('\n');
package/src/core/fsx.mjs CHANGED
@@ -5,7 +5,7 @@ import os from 'node:os';
5
5
  import crypto from 'node:crypto';
6
6
  import { spawn } from 'node:child_process';
7
7
 
8
- export const PACKAGE_VERSION = '0.8.2';
8
+ export const PACKAGE_VERSION = '0.8.5';
9
9
  export const DEFAULT_PROCESS_TAIL_BYTES = 256 * 1024;
10
10
  export const DEFAULT_PROCESS_TIMEOUT_MS = 30 * 60 * 1000;
11
11
 
package/src/core/init.mjs CHANGED
@@ -48,7 +48,7 @@ export function hasTopLevelCodexModeLock(text = '') {
48
48
  const firstTable = lines.findIndex((x) => /^\s*\[.+\]\s*$/.test(x));
49
49
  const top = (firstTable === -1 ? lines : lines.slice(0, firstTable)).join('\n');
50
50
  const model = top.match(/^model\s*=\s*"([^"]+)"/m)?.[1];
51
- return (Boolean(model) && model !== 'gpt-5.5') || /^model_reasoning_effort\s*=/m.test(top);
51
+ return (Boolean(model) && model !== 'gpt-5.5') || /^model_reasoning_effort\s*=/m.test(top) || /^model_provider\s*=\s*"codex-lb"/m.test(top);
52
52
  }
53
53
 
54
54
  export function hasDeprecatedCodexHooksFeatureFlag(text = '') {
@@ -502,6 +502,7 @@ function installPolicy(scope, commandPrefix) {
502
502
 
503
503
  function mergeManagedCodexConfigToml(existingContent = '') {
504
504
  let next = removeLegacyTopLevelCodexModeLocks(String(existingContent || '').trimEnd());
505
+ next = removeTopLevelTomlKeyIfValue(next, 'model_provider', 'codex-lb');
505
506
  next = removeTomlTableKey(next, 'notice', 'fast_default_opt_out');
506
507
  next = removeTomlTableKey(next, 'features', 'codex_hooks');
507
508
  next = upsertTopLevelTomlString(next, 'model', 'gpt-5.5');
@@ -546,13 +547,15 @@ async function mergeGlobalCodexConfigIfAvailable(configText = '', configPath = '
546
547
  const globalConfig = await readText(globalConfigPath, '');
547
548
  let next = mergeGlobalMcpServers(configText, globalConfig);
548
549
  next = mergeGlobalCodexAppRuntimeTables(next, globalConfig);
549
- if (selectedRe.test(next) && /\[model_providers\.codex-lb\]/.test(next)) return `${String(next || '').trim()}\n`;
550
+ if (selectedRe.test(next) && /\[model_providers\.codex-lb\]/.test(next)) {
551
+ return `${removeTopLevelTomlKeyIfValue(next, 'model_provider', 'codex-lb').trim()}\n`;
552
+ }
550
553
  const envPath = path.join(home, '.codex', 'sks-codex-lb.env');
551
554
  if (!(await exists(envPath))) return next;
552
555
  const envText = await readText(envPath, '');
553
556
  const baseUrl = globalConfig.match(/(^|\n)\[model_providers\.codex-lb\][\s\S]*?\n\s*base_url\s*=\s*"([^"]+)"/)?.[2] || parseCodexLbEnvBaseUrl(envText);
554
557
  if (!parseCodexLbEnvKey(envText) || !baseUrl || (!selectedRe.test(globalConfig) && !parseCodexLbEnvBaseUrl(envText))) return next;
555
- next = upsertTopLevelTomlString(next, 'model_provider', 'codex-lb');
558
+ next = removeTopLevelTomlKeyIfValue(next, 'model_provider', 'codex-lb');
556
559
  next = upsertTomlTable(next, 'model_providers.codex-lb', `[model_providers.codex-lb]\nname = "OpenAI"\nbase_url = "${baseUrl}"\nwire_api = "responses"\nenv_key = "CODEX_LB_API_KEY"\nsupports_websockets = true\nrequires_openai_auth = true`);
557
560
  return `${next.trim()}\n`;
558
561
  }
@@ -612,6 +615,14 @@ function removeLegacyTopLevelCodexModeLocks(text = '') {
612
615
  }).join('\n').replace(/^\n+/, '').replace(/\n{3,}/g, '\n\n');
613
616
  }
614
617
 
618
+ function removeTopLevelTomlKeyIfValue(text = '', key = '', value = '') {
619
+ const lines = String(text || '').split('\n');
620
+ const firstTable = lines.findIndex((x) => /^\s*\[.+\]\s*$/.test(x));
621
+ const end = firstTable === -1 ? lines.length : firstTable;
622
+ const keyPattern = new RegExp(`^\\s*${escapeRegExp(key)}\\s*=\\s*"${escapeRegExp(value)}"\\s*(?:#.*)?$`);
623
+ return lines.filter((line, index) => index >= end || !keyPattern.test(line)).join('\n').replace(/^\n+/, '').replace(/\n{3,}/g, '\n\n');
624
+ }
625
+
615
626
  function upsertTopLevelTomlString(text, key, value) {
616
627
  const line = `${key} = "${value}"`;
617
628
  const lines = String(text || '').split('\n');
@@ -17,11 +17,62 @@ export const RESEARCH_PAPER_SECTION_GROUPS = Object.freeze([
17
17
  ['references', 'sources']
18
18
  ]);
19
19
 
20
- export const RESEARCH_SCOUT_COUNCIL = Object.freeze(RESEARCH_SCOUT_PERSONA_CONTRACT.map((scout) => Object.freeze({
21
- ...scout,
22
- label: scout.display_name,
23
- required_outputs: scout.required_outputs
24
- })));
20
+ function cleanResearchArtifactDate(value = '') {
21
+ const match = String(value || '').match(/\d{4}-\d{2}-\d{2}/);
22
+ return match ? match[0] : nowIso().slice(0, 10);
23
+ }
24
+
25
+ function researchTitleSlug(prompt = '') {
26
+ const cleaned = String(prompt || '')
27
+ .normalize('NFKC')
28
+ .replace(/[`"'<>]/g, ' ')
29
+ .replace(/[^\p{L}\p{N}]+/gu, '-')
30
+ .replace(/^-+|-+$/g, '')
31
+ .toLowerCase();
32
+ const slug = cleaned.split('-').filter(Boolean).slice(0, 10).join('-').slice(0, 90).replace(/-+$/g, '');
33
+ return slug || 'research';
34
+ }
35
+
36
+ export function researchPaperArtifactName(prompt = '', createdAt = nowIso(), opts = {}) {
37
+ const titleSource = opts.title || opts.paperTitle || prompt;
38
+ return `${cleanResearchArtifactDate(createdAt)}-${researchTitleSlug(titleSource)}-research-paper.md`;
39
+ }
40
+
41
+ export function isDatedResearchPaperArtifact(name = '') {
42
+ return /^\d{4}-\d{2}-\d{2}-[^\s/\\]+-research-paper\.md$/u.test(String(name || ''));
43
+ }
44
+
45
+ export function researchPaperArtifactForPlan(plan = null) {
46
+ const artifact = plan?.artifacts?.research_paper || plan?.paper_artifact;
47
+ return artifact ? path.basename(String(artifact)) : RESEARCH_PAPER_ARTIFACT;
48
+ }
49
+
50
+ export async function findResearchPaperArtifact(dir, plan = null, opts = {}) {
51
+ const preferred = researchPaperArtifactForPlan(plan);
52
+ const allowLegacyFallback = opts.allowLegacyFallback === true || preferred === RESEARCH_PAPER_ARTIFACT;
53
+ const names = [...new Set([preferred, allowLegacyFallback ? RESEARCH_PAPER_ARTIFACT : null].filter(Boolean))];
54
+ for (const name of names) {
55
+ const file = path.join(dir, name);
56
+ if (await exists(file)) return { name, path: file, exists: true, preferred: name === preferred, legacy: name === RESEARCH_PAPER_ARTIFACT };
57
+ }
58
+ return { name: preferred, path: path.join(dir, preferred), exists: false, preferred: true, legacy: false };
59
+ }
60
+
61
+ export function researchScoutAgentName(scout = {}) {
62
+ return String(scout.agent_name || scout.display_name || scout.label || scout.id || 'Research Scout').trim();
63
+ }
64
+
65
+ export const RESEARCH_SCOUT_COUNCIL = Object.freeze(RESEARCH_SCOUT_PERSONA_CONTRACT.map((scout) => {
66
+ const displayName = scout.display_name || scout.label || scout.id;
67
+ return Object.freeze({
68
+ ...scout,
69
+ display_name: displayName,
70
+ label: displayName,
71
+ agent_name: displayName,
72
+ codex_agent_name: displayName,
73
+ required_outputs: scout.required_outputs
74
+ });
75
+ }));
25
76
 
26
77
  export const RESEARCH_SOURCE_LAYERS = Object.freeze([
27
78
  {
@@ -86,12 +137,21 @@ export const RESEARCH_SOURCE_LAYER_IDS = Object.freeze(RESEARCH_SOURCE_LAYERS.ma
86
137
 
87
138
  export function createResearchPlan(prompt, opts = {}) {
88
139
  const depth = opts.depth || 'frontier';
140
+ const createdAt = nowIso();
141
+ const paperArtifact = researchPaperArtifactName(prompt, createdAt, opts);
89
142
  return {
90
143
  schema_version: 1,
91
144
  prompt,
92
145
  depth,
93
- created_at: nowIso(),
146
+ created_at: createdAt,
94
147
  methodology: 'genius-scout-council-frontier-discovery-loop',
148
+ paper_artifact: paperArtifact,
149
+ artifacts: {
150
+ research_paper: paperArtifact,
151
+ legacy_research_paper: RESEARCH_PAPER_ARTIFACT,
152
+ genius_opinion_summary: RESEARCH_GENIUS_SUMMARY_ARTIFACT,
153
+ research_source_skill: RESEARCH_SOURCE_SKILL_ARTIFACT
154
+ },
95
155
  objective: 'Find the shortest useful mechanism that can be falsified or applied, grounded in maximum available source retrieval rather than broad summary.',
96
156
  execution_policy: {
97
157
  normal_run: 'real_long_running_research_until_unanimous_scout_consensus',
@@ -165,6 +225,10 @@ export function createResearchPlan(prompt, opts = {}) {
165
225
  allowed_write_scope: 'route-local mission artifacts only',
166
226
  rule: 'Normal Research must not modify repository source, package, docs, config, or generated harness files. It may write only artifacts under its own .sneakoscope/missions/<mission-id>/ directory.'
167
227
  },
228
+ artifact_policy: {
229
+ research_paper: paperArtifact,
230
+ rule: 'Write the final manuscript to the dated topic-specific research_paper artifact from this plan, not the legacy generic filename.'
231
+ },
168
232
  rules: [
169
233
  'Do not modify code or project source files during Research. Research writes only route-local mission artifacts; implementation belongs to $Team or another execution route.',
170
234
  'Do not claim novelty without a novelty ledger entry.',
@@ -175,7 +239,7 @@ export function createResearchPlan(prompt, opts = {}) {
175
239
  'Maximize safe web/source search as layered source retrieval and record queries, source layers, citations, quality notes, triangulation checks, and blockers in source-ledger.json.',
176
240
  `Create ${RESEARCH_SOURCE_SKILL_ARTIFACT} as a route-local source collection skill before synthesis; do not edit generated .agents/skills during the research run.`,
177
241
  'Actively seek disconfirming evidence before synthesis.',
178
- 'Turn the surviving research result into research-paper.md with paper-style sections and references.',
242
+ `Turn the surviving research result into ${paperArtifact} with paper-style sections and references.`,
179
243
  `End every run with ${RESEARCH_GENIUS_SUMMARY_ARTIFACT}, summarizing each genius-lens scout's final opinion, strongest evidence, disagreement, and changed mind.`,
180
244
  'Keep unsupported source-free claims as hypotheses only.',
181
245
  'Prefer the smallest testable mechanism or implementation probe, but do not stop source gathering early for speed when the research question needs a longer pass.',
@@ -194,7 +258,7 @@ export function createResearchPlan(prompt, opts = {}) {
194
258
  ],
195
259
  required_artifacts: [
196
260
  'research-report.md',
197
- RESEARCH_PAPER_ARTIFACT,
261
+ paperArtifact,
198
262
  RESEARCH_GENIUS_SUMMARY_ARTIFACT,
199
263
  RESEARCH_SOURCE_SKILL_ARTIFACT,
200
264
  'source-ledger.json',
@@ -214,6 +278,7 @@ export function researchPlanMarkdown(plan) {
214
278
  lines.push(`Prompt: ${plan.prompt}`);
215
279
  lines.push(`Depth: ${plan.depth}`);
216
280
  lines.push(`Methodology: ${plan.methodology}`);
281
+ lines.push(`Research paper: ${researchPaperArtifactForPlan(plan)}`);
217
282
  if (plan.execution_policy) {
218
283
  lines.push(`Execution: ${plan.execution_policy.normal_run}; default cycle timeout ${plan.execution_policy.default_cycle_timeout_minutes} minutes`);
219
284
  if (plan.execution_policy.default_max_cycles) lines.push(`Consensus loop: repeat until unanimous scout consensus; default safety cap ${plan.execution_policy.default_max_cycles} cycles`);
@@ -227,7 +292,7 @@ export function researchPlanMarkdown(plan) {
227
292
  if (plan.research_council?.scouts?.length) {
228
293
  lines.push('## Genius Scout Council');
229
294
  lines.push(`Policy: ${plan.research_council.policy}`);
230
- for (const scout of plan.research_council.scouts) lines.push(`- ${scout.display_name || scout.label || scout.id}: ${scout.persona || scout.role} - ${scout.mandate} (${scout.persona_boundary || 'persona-inspired lens only'})`);
295
+ for (const scout of plan.research_council.scouts) lines.push(`- ${researchScoutAgentName(scout)}: ${scout.persona || scout.role} - ${scout.mandate} (${scout.persona_boundary || 'persona-inspired lens only'})`);
231
296
  lines.push('');
232
297
  }
233
298
  if (plan.web_research_policy) {
@@ -381,6 +446,7 @@ export function defaultScoutLedger(plan = null) {
381
446
  created_at: nowIso(),
382
447
  scouts: scouts.map((scout) => ({
383
448
  id: scout.id,
449
+ agent_name: researchScoutAgentName(scout),
384
450
  display_name: scout.display_name || scout.label || scout.id,
385
451
  historical_inspiration: scout.historical_inspiration || null,
386
452
  persona: scout.persona || scout.role,
@@ -417,10 +483,13 @@ export function defaultDebateLedger(plan = null) {
417
483
  created_at: nowIso(),
418
484
  mode: 'vigorous_evidence_bound_debate_until_unanimous_consensus',
419
485
  required_participants: scouts.map((scout) => scout.id),
486
+ participant_display_names: scouts.map((scout) => researchScoutAgentName(scout)),
420
487
  consensus_iterations: 0,
421
488
  unanimous_consensus: false,
422
489
  scout_agreements: scouts.map((scout) => ({
423
490
  scout_id: scout.id,
491
+ agent_name: researchScoutAgentName(scout),
492
+ display_name: scout.display_name || scout.label || scout.id,
424
493
  agrees: false,
425
494
  final_position: '',
426
495
  source_ids: []
@@ -506,6 +575,7 @@ export function defaultResearchGate() {
506
575
  return {
507
576
  passed: false,
508
577
  report_present: false,
578
+ research_paper_artifact: null,
509
579
  paper_present: false,
510
580
  paper_sections: 0,
511
581
  genius_opinion_summary_present: false,
@@ -548,8 +618,10 @@ export async function evaluateResearchGate(dir) {
548
618
  const gate = await readJson(path.join(dir, 'research-gate.json'), defaultResearchGate());
549
619
  const plan = await readJson(path.join(dir, 'research-plan.json'), null);
550
620
  const reportPresent = await exists(path.join(dir, 'research-report.md'));
551
- const paperPresent = await exists(path.join(dir, RESEARCH_PAPER_ARTIFACT));
552
- const paperSections = paperPresent ? countResearchPaperSections(await readText(path.join(dir, RESEARCH_PAPER_ARTIFACT), '')) : 0;
621
+ const paperArtifact = await findResearchPaperArtifact(dir, plan);
622
+ const paperPresent = paperArtifact.exists;
623
+ const paperText = paperPresent ? await readText(paperArtifact.path, '') : '';
624
+ const paperSections = paperPresent ? countResearchPaperSections(paperText) : 0;
553
625
  const geniusSummaryPresent = await exists(path.join(dir, RESEARCH_GENIUS_SUMMARY_ARTIFACT));
554
626
  const geniusSummaryCount = geniusSummaryPresent ? countGeniusOpinionSummaries(await readText(path.join(dir, RESEARCH_GENIUS_SUMMARY_ARTIFACT), '')) : 0;
555
627
  const sourceSkillPresent = await exists(path.join(dir, RESEARCH_SOURCE_SKILL_ARTIFACT));
@@ -624,6 +696,8 @@ export async function evaluateResearchGate(dir) {
624
696
  passed: gate.passed === true && reasons.length === 0,
625
697
  reasons,
626
698
  metrics: {
699
+ research_paper_artifact: paperArtifact.name,
700
+ paper_present: paperPresent || gate.paper_present === true,
627
701
  web_search_passes: webSearchPasses,
628
702
  paper_sections: Math.max(Number(gate.paper_sections || 0), paperSections),
629
703
  genius_opinion_summary_present: geniusSummaryPresent || gate.genius_opinion_summary_present === true,
@@ -651,13 +725,18 @@ export async function evaluateResearchGate(dir) {
651
725
  citation_coverage: citationCoverage,
652
726
  web_search_blockers: searchBlockers.length
653
727
  },
654
- gate
728
+ gate: {
729
+ ...gate,
730
+ research_paper_artifact: paperArtifact.name,
731
+ paper_present: paperPresent || gate.paper_present === true
732
+ }
655
733
  };
656
734
  await writeJsonAtomic(path.join(dir, 'research-gate.evaluated.json'), result);
657
735
  return result;
658
736
  }
659
737
 
660
738
  export async function writeMockResearchResult(dir, plan) {
739
+ const paperArtifact = researchPaperArtifactForPlan(plan);
661
740
  const mockLayerSources = RESEARCH_SOURCE_LAYERS.map((layer, index) => ({
662
741
  id: `mock-source-${index + 1}`,
663
742
  layer: layer.id,
@@ -751,6 +830,7 @@ export async function writeMockResearchResult(dir, plan) {
751
830
  ...defaultScoutLedger(plan),
752
831
  scouts: RESEARCH_SCOUT_COUNCIL.map((scout) => ({
753
832
  id: scout.id,
833
+ agent_name: researchScoutAgentName(scout),
754
834
  display_name: scout.display_name || scout.label,
755
835
  historical_inspiration: scout.historical_inspiration || null,
756
836
  persona: scout.persona || scout.role,
@@ -790,10 +870,13 @@ export async function writeMockResearchResult(dir, plan) {
790
870
  created_at: nowIso(),
791
871
  mode: 'vigorous_evidence_bound_debate_until_unanimous_consensus',
792
872
  required_participants: RESEARCH_SCOUT_COUNCIL.map((scout) => scout.id),
873
+ participant_display_names: RESEARCH_SCOUT_COUNCIL.map((scout) => researchScoutAgentName(scout)),
793
874
  consensus_iterations: 2,
794
875
  unanimous_consensus: true,
795
876
  scout_agreements: RESEARCH_SCOUT_COUNCIL.map((scout) => ({
796
877
  scout_id: scout.id,
878
+ agent_name: researchScoutAgentName(scout),
879
+ display_name: scout.display_name || scout.label,
797
880
  agrees: true,
798
881
  final_position: 'Agrees to keep the falsifiable, source-cited research mechanism as the surviving claim.',
799
882
  source_ids: ['mock-source-1', 'mock-counter-1']
@@ -868,11 +951,12 @@ export async function writeMockResearchResult(dir, plan) {
868
951
  await writeJsonAtomic(path.join(dir, 'novelty-ledger.json'), ledger);
869
952
  await writeTextAtomic(path.join(dir, RESEARCH_GENIUS_SUMMARY_ARTIFACT), `${geniusSummary}\n`);
870
953
  await writeTextAtomic(path.join(dir, 'research-report.md'), `# SKS Research Report\n\nPrompt: ${plan.prompt}\n\n## Scout Council Synthesis\n\nThe mock council keeps one cited methodological insight: a research mode should force layered, falsifiable novelty rather than summarize known material from one corpus [mock-source-1].\n\n## Source Coverage\n\nThis is a selftest fixture. It records mock coverage for academic literature, official data, standards, news, public discourse, developer knowledge, and counterevidence layers, but does not perform live web browsing in --mock mode.\n\n## Candidate Insight\n\nA useful research run must produce source-cited, cross-layer triangulated, falsifiable novelty with scout findings and a cheap probe.\n\n## Falsification\n\nThe claim is weak if no new testable prediction, counterevidence source, cross-layer check, or experiment is produced [mock-counter-1].\n\n## Next Test\n\nCompare this mode against a summary-only run and score candidate insights, falsification passes, citation coverage, source-layer coverage, triangulation checks, and testability.\n`);
871
- await writeTextAtomic(path.join(dir, RESEARCH_PAPER_ARTIFACT), `# Research Paper: ${plan.prompt}\n\n## Abstract\nA source-cited research run should produce cross-layer, falsifiable novelty rather than only summarize known material.\n\n## Introduction\nThe mock topic is evaluated as a research workflow outcome with layered source coverage [mock-source-1].\n\n## Methodology\nFive xhigh scouts produce Eureka ideas, debate, triangulate source layers, and falsify the strongest claim.\n\n## Findings\nThe surviving finding is that useful research needs cited novelty, source-layer coverage, cross-layer triangulation, and a cheap decisive probe.\n\n## Discussion\nThe debate favors gate-backed evidence over narrative confidence, and treats public discourse as signal rather than truth.\n\n## Limitations and Falsification\nThe claim fails without sources, counterevidence, triangulation checks, or testable predictions [mock-counter-1].\n\n## Conclusion and Next Experiment\nCompare this loop against a summary-only baseline and score testable insights.\n\n## References\n- [mock-source-1] Mock academic literature coverage.\n- [mock-source-2] Mock official government and leading-institution knowledge coverage.\n- [mock-source-3] Mock standards and primary documents coverage.\n- [mock-source-4] Mock current news and global reporting coverage.\n- [mock-source-5] Mock public discourse coverage.\n- [mock-source-6] Mock developer and practitioner knowledge coverage.\n- [mock-source-7] Mock counterevidence and fact-checking coverage.\n- [mock-counter-1] Mock overclaim counterexample.\n`);
954
+ await writeTextAtomic(path.join(dir, paperArtifact), `# Research Paper: ${plan.prompt}\n\n## Abstract\nA source-cited research run should produce cross-layer, falsifiable novelty rather than only summarize known material.\n\n## Introduction\nThe mock topic is evaluated as a research workflow outcome with layered source coverage [mock-source-1].\n\n## Methodology\nFive xhigh scouts produce Eureka ideas, debate, triangulate source layers, and falsify the strongest claim.\n\n## Findings\nThe surviving finding is that useful research needs cited novelty, source-layer coverage, cross-layer triangulation, and a cheap decisive probe.\n\n## Discussion\nThe debate favors gate-backed evidence over narrative confidence, and treats public discourse as signal rather than truth.\n\n## Limitations and Falsification\nThe claim fails without sources, counterevidence, triangulation checks, or testable predictions [mock-counter-1].\n\n## Conclusion and Next Experiment\nCompare this loop against a summary-only baseline and score testable insights.\n\n## References\n- [mock-source-1] Mock academic literature coverage.\n- [mock-source-2] Mock official government and leading-institution knowledge coverage.\n- [mock-source-3] Mock standards and primary documents coverage.\n- [mock-source-4] Mock current news and global reporting coverage.\n- [mock-source-5] Mock public discourse coverage.\n- [mock-source-6] Mock developer and practitioner knowledge coverage.\n- [mock-source-7] Mock counterevidence and fact-checking coverage.\n- [mock-counter-1] Mock overclaim counterexample.\n`);
872
955
  await writeJsonAtomic(path.join(dir, 'research-gate.json'), {
873
956
  ...defaultResearchGate(),
874
957
  passed: true,
875
958
  report_present: true,
959
+ research_paper_artifact: paperArtifact,
876
960
  paper_present: true,
877
961
  paper_sections: RESEARCH_PAPER_SECTION_GROUPS.length,
878
962
  genius_opinion_summary_present: true,
@@ -902,12 +986,14 @@ export async function writeMockResearchResult(dir, plan) {
902
986
  falsification_cases: 1,
903
987
  testable_predictions: 1,
904
988
  citation_coverage: true,
905
- evidence: ['mock research report', 'mock research paper', 'mock genius opinion summary', 'mock research source skill', 'mock layered source ledger', 'mock scout ledger', 'mock debate ledger', 'mock novelty ledger', 'mock falsification ledger'],
989
+ evidence: ['mock research report', `mock research paper: ${paperArtifact}`, 'mock genius opinion summary', 'mock research source skill', 'mock layered source ledger', 'mock scout ledger', 'mock debate ledger', 'mock novelty ledger', 'mock falsification ledger'],
906
990
  notes: ['mock mode records the new contract but does not call a model or perform live web browsing']
907
991
  });
908
992
  return evaluateResearchGate(dir);
909
993
  }
910
994
 
911
995
  export function buildResearchPrompt({ id, mission, plan, cycle, previous }) {
912
- return `You are running SKS Research Mode.\nMISSION: ${id}\nTOPIC: ${mission.prompt}\nCYCLE: ${cycle}\nMODE: Genius Scout Council + frontier discovery loop. Use maximum reasoning depth available under the current Codex profile.\nLONG-RUN REAL-RESEARCH POLICY: Normal Research is allowed to take one or two hours when the question requires it. Do real source gathering and evidence comparison; do not shortcut into mock, fixture, or summary-only output. If live source access is unavailable, write the blocker and keep the gate unpassed.\nNO-CODE-MUTATION POLICY: Do not edit repository source, package metadata, docs, config, generated skills, or harness files. Write only route-local artifacts under .sneakoscope/missions/${id}/. If a needed implementation change is discovered, record it as a recommendation or blocker for a later execution route.\nNO-QUESTION LOCK: Do not ask the user. Resolve scope from research-plan.json and current project evidence.\nSAFETY: Destructive database operations and unsafe external actions are forbidden. Prefer read-only inspection, local files, and cited public sources.\nPERSONA POLICY: Use Einstein/Feynman/Turing/von Neumann-inspired scout lenses only as cognitive roles. Do not impersonate, roleplay private identity, or speak as the historical people.\nSCOUT PERSONA POLICY: Every Research scout row must include display_name, persona, persona_boundary, reasoning_effort: "xhigh", service_tier when available, falsifiers, cheap_probes, and challenge_or_response. Persona names are Einstein Scout, Feynman Scout, Turing Scout, von Neumann Scout, and Skeptic Scout; they are cognitive lenses, not impersonations.\nSCOUT EFFORT POLICY: Every Research scout agent must use reasoning_effort=xhigh. Record effort: "xhigh" for every scout in scout-ledger.json. Any lower-effort scout output must keep research-gate.json unpassed.\nEUREKA POLICY: Every scout must literally write "Eureka!" and one non-obvious, source-linked idea before debate.\nCONSENSUS LOOP POLICY: This is not a fixed three-cycle run. Repeat source-gathering, scout Eureka ideas, debate, falsification, and synthesis pressure until every scout records final agreement with the surviving mechanism. If unanimous agreement is not reached, keep research-gate.json unpassed and continue until the explicit max-cycle safety cap pauses the run.\nDEBATE POLICY: The scouts must debate vigorously but stay evidence-bound. Every scout must challenge or respond at least once, and debate-ledger.json must record exchanges, consensus_iterations, unanimous_consensus, and per-scout agreements before synthesis.\nPAPER POLICY: After the report and ledgers, write research-paper.md as a concise manuscript with Abstract, Introduction, Methodology, Findings/Results, Discussion, Limitations/Falsification, Conclusion/Next Experiment, and References.\nSOURCE SKILL POLICY: Create or update ${RESEARCH_SOURCE_SKILL_ARTIFACT} as a route-local source collection skill before synthesis. It must name the selected source layers, query routes, quality fields, blockers, and cross-layer triangulation checks. Do not edit generated .agents/skills during the research run.\nWEB/SOURCE POLICY: Run layered source retrieval across every safely available layer before synthesis: latest public papers, official government or leading-institution data, standards or primary docs, current news including BBC/CNN/GDELT-style sources when relevant, public discourse including X/Twitter and Reddit when available, developer/practitioner sources such as Stack Overflow/Stack Exchange/GitHub, and counterevidence or fact-checking sources. Treat public discourse as signal, not truth. If a layer cannot be searched, record the blocker in source-ledger.json and do not pass the gate.\nRESEARCH PLAN:\n${JSON.stringify(plan, null, 2)}\n\nOBJECTIVE: Produce genuinely useful candidate discoveries: non-obvious hypotheses, mechanisms, predictions, or experiments. Do not merely summarize. Mark uncertainty clearly.\n\nREQUIRED PROCESS:\n1. Source skill first: create ${RESEARCH_SOURCE_SKILL_ARTIFACT} with source layers, query templates, quality fields, blockers, and triangulation rules.\n2. Layered source search: create source-ledger.json with source_layers, queries, source ids, source quality notes, counterevidence sources, triangulation.cross_layer_checks, citation coverage, and blockers.\n3. Independent xhigh scouts: create scout-ledger.json with display_name/persona/persona_boundary, effort=xhigh, reasoning_effort=xhigh, a literal Eureka! idea, findings, source_ids, falsifiers, cheap_probes, and challenge_or_response for every scout lens.\n4. Debate to agreement: create debate-ledger.json with evidence-bound challenge/response exchanges involving every scout, consensus_iterations >= 1, unanimous_consensus=true only when all scouts agree, and scout_agreements for every scout.\n5. Falsification: create falsification-ledger.json with attacks, missing evidence, source conflicts, and decisive next tests.\n6. Synthesis: write research-report.md and novelty-ledger.json only after cited scout findings, Eureka ideas, unanimous debate agreement, cross-layer triangulation, and falsification are recorded.\n7. Paper: write research-paper.md as a paper-style manuscript with source-ledger references and limitations.\n\nREQUIRED OUTPUT FILES in .sneakoscope/missions/${id}/:\n- research-report.md: concise report with framing, source coverage, scout synthesis, debate synthesis, hypotheses, falsification, predictions, and next experiments. Cite source-ledger ids for factual claims.\n- research-paper.md: paper manuscript with Abstract, Introduction, Methodology, Findings/Results, Discussion, Limitations/Falsification, Conclusion/Next Experiment, and References using source-ledger ids.\n- ${RESEARCH_SOURCE_SKILL_ARTIFACT}: route-local source collection skill; it is evidence for the Skill Creator step and must not mutate generated .agents/skills.\n- source-ledger.json: layered web/source queries, source ids, source priority, source quality notes, counterevidence sources, citation coverage, triangulation checks, and blockers.\n- scout-ledger.json: one entry per scout lens with display_name, persona, persona_boundary, effort, reasoning_effort, service_tier, eureka, query_set, findings, source_ids, falsifiers, cheap_probes, and challenge_or_response.\n- debate-ledger.json: evidence-bound challenge/response exchanges, participants, changed minds, unresolved conflicts, consensus_iterations, unanimous_consensus, and scout_agreements for every scout.\n- novelty-ledger.json: entries with claim, novelty, confidence, falsifiability, evidence source ids, falsifiers, next_experiment.\n- falsification-ledger.json: attacks/counterexamples/source conflicts, result, and next_decisive_tests.\n- research-gate.json: set passed only when all ledgers exist, ${RESEARCH_SOURCE_SKILL_ARTIFACT} exists, research-paper.md exists with required paper sections, layered web/source retrieval covered every required source layer, at least one cross-layer triangulation check exists, all scouts have display_name/persona/persona_boundary, all scouts have effort=xhigh, all scouts have literal Eureka! ideas, every scout participated in debate, consensus_iterations >= 1, unanimous_consensus=true with every scout agreement recorded, at least one counterevidence source exists, citation coverage is complete, at least one insight survived falsification, at least one testable prediction exists, and unsupported breakthrough claims are zero.\n\nPrevious cycle tail:\n${String(previous || '').slice(-2500)}\n`;
996
+ const paperArtifact = researchPaperArtifactForPlan(plan);
997
+ const scoutAgentNames = (plan?.research_council?.scouts || RESEARCH_SCOUT_COUNCIL).map((scout) => researchScoutAgentName(scout)).join(', ');
998
+ return `You are running SKS Research Mode.\nMISSION: ${id}\nTOPIC: ${mission.prompt}\nCYCLE: ${cycle}\nMODE: Genius Scout Council + frontier discovery loop. Use maximum reasoning depth available under the current Codex profile.\nLONG-RUN REAL-RESEARCH POLICY: Normal Research is allowed to take one or two hours when the question requires it. Do real source gathering and evidence comparison; do not shortcut into mock, fixture, or summary-only output. If live source access is unavailable, write the blocker and keep the gate unpassed.\nNO-CODE-MUTATION POLICY: Do not edit repository source, package metadata, docs, config, generated skills, or harness files. Write only route-local artifacts under .sneakoscope/missions/${id}/. If a needed implementation change is discovered, record it as a recommendation or blocker for a later execution route.\nNO-QUESTION LOCK: Do not ask the user. Resolve scope from research-plan.json and current project evidence.\nSAFETY: Destructive database operations and unsafe external actions are forbidden. Prefer read-only inspection, local files, and cited public sources.\nPERSONA POLICY: Use Einstein/Feynman/Turing/von Neumann-inspired scout lenses only as cognitive roles. Do not impersonate, roleplay private identity, or speak as the historical people.\nSCOUT PERSONA POLICY: Every Research scout row must include agent_name, display_name, persona, persona_boundary, reasoning_effort: "xhigh", service_tier when available, falsifiers, cheap_probes, and challenge_or_response. Use these agent_name values exactly: ${scoutAgentNames}. Persona names are cognitive lenses, not impersonations.\nSCOUT EFFORT POLICY: Every Research scout agent must use reasoning_effort=xhigh. Record effort: "xhigh" for every scout in scout-ledger.json. Any lower-effort scout output must keep research-gate.json unpassed.\nEUREKA POLICY: Every scout must literally write "Eureka!" and one non-obvious, source-linked idea before debate.\nCONSENSUS LOOP POLICY: This is not a fixed three-cycle run. Repeat source-gathering, scout Eureka ideas, debate, falsification, and synthesis pressure until every scout records final agreement with the surviving mechanism. If unanimous agreement is not reached, keep research-gate.json unpassed and continue until the explicit max-cycle safety cap pauses the run.\nDEBATE POLICY: The scouts must debate vigorously but stay evidence-bound. Every scout must challenge or respond at least once, and debate-ledger.json must record exchanges, consensus_iterations, unanimous_consensus, and per-scout agreements before synthesis.\nPAPER POLICY: After the report and ledgers, write ${paperArtifact} as a concise manuscript with Abstract, Introduction, Methodology, Findings/Results, Discussion, Limitations/Falsification, Conclusion/Next Experiment, and References.\nSOURCE SKILL POLICY: Create or update ${RESEARCH_SOURCE_SKILL_ARTIFACT} as a route-local source collection skill before synthesis. It must name the selected source layers, query routes, quality fields, blockers, and cross-layer triangulation checks. Do not edit generated .agents/skills during the research run.\nWEB/SOURCE POLICY: Run layered source retrieval across every safely available layer before synthesis: latest public papers, official government or leading-institution data, standards or primary docs, current news including BBC/CNN/GDELT-style sources when relevant, public discourse including X/Twitter and Reddit when available, developer/practitioner sources such as Stack Overflow/Stack Exchange/GitHub, and counterevidence or fact-checking sources. Treat public discourse as signal, not truth. If a layer cannot be searched, record the blocker in source-ledger.json and do not pass the gate.\nRESEARCH PLAN:\n${JSON.stringify(plan, null, 2)}\n\nOBJECTIVE: Produce genuinely useful candidate discoveries: non-obvious hypotheses, mechanisms, predictions, or experiments. Do not merely summarize. Mark uncertainty clearly.\n\nREQUIRED PROCESS:\n1. Source skill first: create ${RESEARCH_SOURCE_SKILL_ARTIFACT} with source layers, query templates, quality fields, blockers, and triangulation rules.\n2. Layered source search: create source-ledger.json with source_layers, queries, source ids, source quality notes, counterevidence sources, triangulation.cross_layer_checks, citation coverage, and blockers.\n3. Independent xhigh scouts: create scout-ledger.json with agent_name/display_name/persona/persona_boundary, effort=xhigh, reasoning_effort=xhigh, a literal Eureka! idea, findings, source_ids, falsifiers, cheap_probes, and challenge_or_response for every scout lens.\n4. Debate to agreement: create debate-ledger.json with evidence-bound challenge/response exchanges involving every scout, consensus_iterations >= 1, unanimous_consensus=true only when all scouts agree, and scout_agreements for every scout.\n5. Falsification: create falsification-ledger.json with attacks, missing evidence, source conflicts, and decisive next tests.\n6. Synthesis: write research-report.md and novelty-ledger.json only after cited scout findings, Eureka ideas, unanimous debate agreement, cross-layer triangulation, and falsification are recorded.\n7. Paper: write ${paperArtifact} as a paper-style manuscript with source-ledger references and limitations.\n\nREQUIRED OUTPUT FILES in .sneakoscope/missions/${id}/:\n- research-report.md: concise report with framing, source coverage, scout synthesis, debate synthesis, hypotheses, falsification, predictions, and next experiments. Cite source-ledger ids for factual claims.\n- ${paperArtifact}: paper manuscript with Abstract, Introduction, Methodology, Findings/Results, Discussion, Limitations/Falsification, Conclusion/Next Experiment, and References using source-ledger ids.\n- ${RESEARCH_SOURCE_SKILL_ARTIFACT}: route-local source collection skill; it is evidence for the Skill Creator step and must not mutate generated .agents/skills.\n- source-ledger.json: layered web/source queries, source ids, source priority, source quality notes, counterevidence sources, citation coverage, triangulation checks, and blockers.\n- scout-ledger.json: one entry per scout lens with agent_name, display_name, persona, persona_boundary, effort, reasoning_effort, service_tier, eureka, query_set, findings, source_ids, falsifiers, cheap_probes, and challenge_or_response.\n- debate-ledger.json: evidence-bound challenge/response exchanges, participants, changed minds, unresolved conflicts, consensus_iterations, unanimous_consensus, and scout_agreements for every scout.\n- novelty-ledger.json: entries with claim, novelty, confidence, falsifiability, evidence source ids, falsifiers, next_experiment.\n- falsification-ledger.json: attacks/counterexamples/source conflicts, result, and next_decisive_tests.\n- research-gate.json: set passed only when all ledgers exist, ${RESEARCH_SOURCE_SKILL_ARTIFACT} exists, ${paperArtifact} exists with required paper sections, layered web/source retrieval covered every required source layer, at least one cross-layer triangulation check exists, all scouts have agent_name/display_name/persona/persona_boundary, all scouts have effort=xhigh, all scouts have literal Eureka! ideas, every scout participated in debate, consensus_iterations >= 1, unanimous_consensus=true with every scout agreement recorded, at least one counterevidence source exists, citation coverage is complete, at least one insight survived falsification, at least one testable prediction exists, and unsupported breakthrough claims are zero.\n\nPrevious cycle tail:\n${String(previous || '').slice(-2500)}\n`;
913
999
  }
@@ -523,8 +523,8 @@ export const COMMAND_CATALOG = [
523
523
  { name: 'root', usage: 'sks root [--json]', description: 'Show whether SKS is using a project root or the per-user global SKS runtime root.' },
524
524
  { name: 'deps', usage: 'sks deps check|install [tmux|codex|context7|all] [--yes]', description: 'Check or guided-install Node/npm PATH, Codex CLI/App, Context7, Browser tooling, Computer Use, tmux, and Homebrew on macOS.' },
525
525
  { name: 'codex-app', usage: 'sks codex-app [check|open|remote-control]', description: 'Check Codex App install and first-party MCP/plugin readiness, then show app setup files, examples, and Codex CLI 0.130.0+ remote-control availability.' },
526
- { name: 'codex-lb', usage: 'sks codex-lb status|health|repair|setup --host <domain> --api-key <key>', description: 'Configure, health-check, or repair codex-lb Codex CLI auth by writing ~/.codex/config.toml, syncing auth.json, and loading the CODEX_LB_API_KEY env file.' },
527
- { name: 'auth', usage: 'sks auth status|health|repair|setup --host <domain> --api-key <key>', description: 'Shortcut for codex-lb auth status, health, repair, and setup commands.' },
526
+ { name: 'codex-lb', usage: 'sks codex-lb status|health|repair|setup --host <domain> --api-key <key>', description: 'Configure, health-check, or repair codex-lb provider auth by writing ~/.codex/config.toml, restoring CODEX_LB_API_KEY env auth from stored or legacy login-cache state, and preserving the shared Codex login cache unless explicitly requested.' },
527
+ { name: 'auth', usage: 'sks auth status|health|repair|setup --host <domain> --api-key <key>', description: 'Shortcut for codex-lb provider auth status, health, repair, and setup commands.' },
528
528
  { name: 'openclaw', usage: 'sks openclaw install|path|print [--dir path] [--force] [--json]', description: 'Generate an OpenClaw skill package so OpenClaw agents can discover and use local SKS workflows.' },
529
529
  { name: 'tmux', usage: 'sks | sks tmux open|check|status [--workspace name]', description: 'Open the default SKS tmux runtime with bare sks, or use tmux subcommands for explicit launch/check/status.' },
530
530
  { name: 'mad', usage: 'sks --mad [--high]', description: 'Open a one-shot tmux Codex CLI workspace with the SKS MAD full-access auto-review profile.' },