sneakoscope 0.8.4 → 0.8.6

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`, writes `model_provider = "codex-lb"` into `~/.codex/config.toml` for Codex App routing, 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 and restore the Codex App provider selection 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
 
@@ -251,7 +251,7 @@ Clarification asks only for ambiguity that changes execution; predictable defaul
251
251
  $PPT create a customer proposal deck as HTML/PDF
252
252
  ```
253
253
 
254
- `$PPT` seals presentation context before artifact work and grounds design in `design.md`, getdesign inputs, and source material.
254
+ `$PPT` seals presentation context before artifact work and grounds design in `design.md`, getdesign inputs, and source material. The route loads `imagegen`; when the sealed deck needs generated raster assets or generated slide visual critique, use Codex App `$imagegen`/`gpt-image-2` and record the real output path in the PPT image/review ledgers.
255
255
 
256
256
  ## Codex App Usage
257
257
 
@@ -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-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.
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. When codex-lb is configured, SKS keeps it selected as the top-level Codex App provider while still preserving required app flags and plugin settings. 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.4",
4
+ "version": "0.8.6",
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);
@@ -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 = {}) {
@@ -360,8 +376,18 @@ function codexLbProviderBaseUrl(text = '') {
360
376
  export async function repairCodexLbAuth(opts = {}) {
361
377
  let status = await codexLbStatus(opts);
362
378
  let configRepaired = false;
379
+ let legacyAuthMigrated = false;
380
+ let legacyAuthPath = null;
363
381
  const currentConfig = await readText(status.config_path, '');
364
- if (status.env_key_configured && status.base_url && (!status.ok || status.selected || hasTopLevelCodexModeLock(currentConfig))) {
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))) {
365
391
  await ensureDir(path.dirname(status.config_path));
366
392
  const next = normalizeCodexFastModeUiConfig(upsertCodexLbConfig(currentConfig, status.base_url));
367
393
  await writeTextAtomic(status.config_path, next);
@@ -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
  }
@@ -394,35 +426,58 @@ export async function ensureCodexLbAuthDuringInstall(opts = {}) {
394
426
  if (process.env.SKS_SKIP_POSTINSTALL_CODEX_LB_AUTH === '1' && !opts.force) return { status: 'skipped', reason: 'SKS_SKIP_POSTINSTALL_CODEX_LB_AUTH=1' };
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 };
429
+ if (status.ok && !status.selected) return repairCodexLbAuth(opts);
397
430
  if (!status.ok) {
398
- if (status.env_key_configured && status.base_url) return repairCodexLbAuth(opts);
431
+ if (status.base_url && (status.env_key_configured || status.provider_configured || status.selected || status.env_base_url_configured)) return repairCodexLbAuth(opts);
399
432
  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
433
  }
401
- const codexLogin = await ensureCodexLbLoginFromEnv(status, { ...opts, force: true });
434
+ const codexEnvironment = await syncCodexLbProviderEnvironment(status, opts);
435
+ const apiKey = parseCodexLbEnvKey(await readText(status.env_path, ''));
436
+ const codexLogin = await maybeSyncCodexLbSharedLogin(apiKey, { ...opts, home: opts.home || process.env.HOME || os.homedir(), force: true });
437
+ const ok = Boolean(codexEnvironment.ok && codexLogin.ok);
402
438
  return {
403
- ok: Boolean(codexLogin.ok),
404
- status: codexLogin.status,
439
+ ok,
440
+ status: ok ? 'present' : (codexEnvironment.status || codexLogin.status),
405
441
  config_path: status.config_path,
406
442
  env_path: status.env_path,
407
443
  base_url: status.base_url,
408
444
  codex_lb: status,
445
+ codex_environment: codexEnvironment,
409
446
  codex_login: codexLogin,
410
- error: codexLogin.error || null
447
+ error: codexEnvironment.error || codexLogin.error || null
411
448
  };
412
449
  }
413
450
 
451
+ async function restoreCodexLbEnvFromSharedLogin(status = {}, opts = {}) {
452
+ const home = opts.home || process.env.HOME || os.homedir();
453
+ const authPath = opts.authPath || codexAuthPath(home);
454
+ const envPath = opts.envPath || status.env_path || codexLbEnvPath(home);
455
+ const authText = await readText(authPath, '');
456
+ const apiKey = parseCodexSharedLoginApiKey(authText);
457
+ if (!apiKey) return { ok: false, status: 'missing_legacy_login_key', auth_path: authPath, env_path: envPath };
458
+ const baseUrl = status.base_url || parseCodexLbEnvBaseUrl(await readText(envPath, ''));
459
+ if (!baseUrl) return { ok: false, status: 'missing_base_url', auth_path: authPath, env_path: envPath };
460
+ await ensureDir(path.dirname(envPath));
461
+ await writeTextAtomic(envPath, `export CODEX_LB_BASE_URL=${shellSingleQuote(normalizeCodexLbBaseUrl(baseUrl))}\nexport CODEX_LB_API_KEY=${shellSingleQuote(apiKey)}\n`);
462
+ await fsp.chmod(envPath, 0o600).catch(() => {});
463
+ return { ok: true, status: 'migrated_login_cache', auth_path: authPath, env_path: envPath, base_url: normalizeCodexLbBaseUrl(baseUrl) };
464
+ }
465
+
414
466
  export async function maybePromptCodexLbSetupForLaunch(args = [], opts = {}) {
415
467
  if (args.includes('--json') || args.includes('--skip-codex-lb') || process.env.SKS_SKIP_CODEX_LB_PROMPT === '1') return { status: 'skipped' };
416
468
  const status = await codexLbStatus(opts);
417
469
  if (status.ok) {
418
- const codexLogin = await ensureCodexLbLoginFromEnv(status, opts);
419
- if (codexLogin.status === 'synced') console.log('codex-lb auth synced with Codex CLI.');
470
+ const codexEnvironment = await syncCodexLbProviderEnvironment(status, opts);
471
+ if (codexEnvironment.status === 'synced') console.log('codex-lb provider auth synced for this user session.');
472
+ const apiKey = parseCodexLbEnvKey(await readText(status.env_path, ''));
473
+ const codexLogin = await maybeSyncCodexLbSharedLogin(apiKey, { ...opts, home: opts.home || process.env.HOME || os.homedir() });
474
+ if (codexLogin.status === 'synced') console.log('codex-lb auth synced with Codex CLI login cache.');
420
475
  const chainHealth = await checkCodexLbResponseChain(status, opts);
421
476
  if (!chainHealth.ok && chainHealth.chain_unhealthy) {
422
477
  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 };
478
+ return { status: 'chain_unhealthy', ...status, ok: false, codex_environment: codexEnvironment, codex_login: codexLogin, chain_health: chainHealth, bypass_codex_lb: true };
424
479
  }
425
- return { status: 'present', ...status, codex_login: codexLogin, chain_health: chainHealth };
480
+ return { status: 'present', ...status, codex_environment: codexEnvironment, codex_login: codexLogin, chain_health: chainHealth };
426
481
  }
427
482
  if (!canAskYesNo()) return { status: 'non_interactive', codex_lb: status };
428
483
  const useCodexLb = (await askPostinstallQuestion('\nAuthenticate and route Codex through codex-lb? [y/N] ')).trim();
@@ -435,12 +490,57 @@ export async function maybePromptCodexLbSetupForLaunch(args = [], opts = {}) {
435
490
  return configured;
436
491
  }
437
492
 
438
- async function ensureCodexLbLoginFromEnv(status = {}, opts = {}) {
493
+ async function syncCodexLbProviderEnvironment(status = {}, opts = {}) {
439
494
  const home = opts.home || process.env.HOME || os.homedir();
440
495
  const envPath = opts.envPath || status.env_path || codexLbEnvPath(home);
441
- const apiKey = parseCodexLbEnvKey(await readText(envPath, ''));
496
+ const envText = await readText(envPath, '');
497
+ const apiKey = parseCodexLbEnvKey(envText);
498
+ if (!apiKey) return { ok: false, status: 'missing_env_key' };
499
+ const baseUrl = status.base_url || parseCodexLbEnvBaseUrl(envText);
500
+ process.env.CODEX_LB_API_KEY = apiKey;
501
+ if (baseUrl) process.env.CODEX_LB_BASE_URL = baseUrl;
502
+ const launchEnv = await syncCodexLbMacLaunchEnvironment({ CODEX_LB_API_KEY: apiKey, ...(baseUrl ? { CODEX_LB_BASE_URL: baseUrl } : {}) }, opts);
503
+ const ok = launchEnv.ok || launchEnv.skipped || launchEnv.status === 'not_macos';
504
+ return {
505
+ ok,
506
+ status: launchEnv.status === 'synced' ? 'synced' : ok ? 'process_env' : launchEnv.status,
507
+ env_path: envPath,
508
+ base_url: baseUrl || null,
509
+ launch_environment: launchEnv,
510
+ error: launchEnv.error || null
511
+ };
512
+ }
513
+
514
+ async function syncCodexLbMacLaunchEnvironment(values = {}, opts = {}) {
515
+ 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' };
516
+ if (process.platform !== 'darwin' && !opts.forceLaunchEnv) return { ok: true, status: 'not_macos', skipped: true };
517
+ const launchctl = opts.launchctlBin || await which('launchctl').catch(() => null) || await exists('/bin/launchctl').then((ok) => ok ? '/bin/launchctl' : null).catch(() => null);
518
+ if (!launchctl) return { ok: false, status: 'launchctl_missing', error: 'launchctl not found on PATH' };
519
+ const variables = Object.entries(values).filter(([, value]) => value);
520
+ const results = [];
521
+ for (const [key, value] of variables) {
522
+ const result = await runProcess(launchctl, ['setenv', key, value], { timeoutMs: 5000, maxOutputBytes: 8192 });
523
+ results.push({
524
+ key,
525
+ ok: result.code === 0,
526
+ error: result.code === 0 ? null : redactSecretText(result.stderr || result.stdout || 'launchctl setenv failed', [value]).trim()
527
+ });
528
+ }
529
+ const failed = results.filter((result) => !result.ok);
530
+ 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('; ') };
531
+ return { ok: true, status: 'synced', variables: results.map((result) => result.key) };
532
+ }
533
+
534
+ async function maybeSyncCodexLbSharedLogin(apiKey, opts = {}) {
442
535
  if (!apiKey) return { ok: false, status: 'missing_env_key' };
443
- return syncCodexApiKeyLogin(apiKey, { ...opts, home, force: true });
536
+ if (!shouldSyncCodexLbSharedLogin(opts)) {
537
+ 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.' };
538
+ }
539
+ return syncCodexApiKeyLogin(apiKey, opts);
540
+ }
541
+
542
+ function shouldSyncCodexLbSharedLogin(opts = {}) {
543
+ return opts.syncCodexLogin === true || process.env.SKS_CODEX_LB_SYNC_CODEX_LOGIN === '1';
444
544
  }
445
545
 
446
546
  async function syncCodexApiKeyLogin(apiKey, opts = {}) {
@@ -460,7 +560,7 @@ async function syncCodexApiKeyLogin(apiKey, opts = {}) {
460
560
  }
461
561
 
462
562
  function upsertCodexLbConfig(text = '', baseUrl) {
463
- let next = removeTopLevelTomlKeyIfValue(text, 'model_provider', 'codex-lb');
563
+ let next = upsertTopLevelTomlString(text, 'model_provider', 'codex-lb');
464
564
  const block = [
465
565
  '[model_providers.codex-lb]',
466
566
  'name = "OpenAI"',
@@ -663,6 +763,19 @@ function parseCodexLbEnvBaseUrl(text = '') {
663
763
  return value ? normalizeCodexLbBaseUrl(value) : '';
664
764
  }
665
765
 
766
+ function parseCodexSharedLoginApiKey(text = '') {
767
+ try {
768
+ const parsed = JSON.parse(String(text || ''));
769
+ const authMode = String(parsed?.auth_mode || parsed?.authMode || parsed?.mode || '').toLowerCase();
770
+ const key = parsed?.key || parsed?.api_key || parsed?.apiKey || parsed?.openai_api_key || parsed?.OPENAI_API_KEY;
771
+ if (!key || typeof key !== 'string') return '';
772
+ if (authMode && !/api[-_]?key|apikey/.test(authMode)) return '';
773
+ return key.trim();
774
+ } catch {
775
+ return '';
776
+ }
777
+ }
778
+
666
779
  function parseShellEnvValue(text = '', key = '') {
667
780
  const re = new RegExp(`^\\s*(?:export\\s+)?${escapeRegExp(key)}\\s*=\\s*(.+?)\\s*$`, 'm');
668
781
  const envMatch = String(text || '').match(re);
@@ -1082,6 +1195,24 @@ async function safeReadText(file, fallback = '') {
1082
1195
  }
1083
1196
  }
1084
1197
 
1198
+ async function codexLbLoginCallCount(home) {
1199
+ return (await safeReadText(path.join(home, '.codex', 'login-calls.log'))).trim().split(/\r?\n/).filter(Boolean).length;
1200
+ }
1201
+
1202
+ function codexLbPostinstallEnv(baseEnv, overrides = {}) {
1203
+ return {
1204
+ ...baseEnv,
1205
+ SKS_POSTINSTALL_NO_BOOTSTRAP: '1',
1206
+ SKS_SKIP_POSTINSTALL_SHIM: '1',
1207
+ SKS_SKIP_POSTINSTALL_CONTEXT7: '1',
1208
+ SKS_SKIP_POSTINSTALL_GETDESIGN: '1',
1209
+ SKS_SKIP_POSTINSTALL_GLOBAL_SKILLS: '1',
1210
+ SKS_SKIP_POSTINSTALL_CODEX_LB_AUTH: '0',
1211
+ SKS_SKIP_CODEX_LB_LAUNCH_ENV: '1',
1212
+ ...overrides
1213
+ };
1214
+ }
1215
+
1085
1216
  export async function selftestCodexLb(tmp) {
1086
1217
  const codexLbHome = path.join(tmp, 'codex-lb-home');
1087
1218
  await ensureDir(path.join(codexLbHome, '.codex'));
@@ -1091,7 +1222,7 @@ export async function selftestCodexLb(tmp) {
1091
1222
  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");
1092
1223
  await fsp.chmod(codexLbFakeCodex, 0o755);
1093
1224
  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');
1094
- const codexLbEnvForSelftest = { HOME: codexLbHome, SKS_GLOBAL_ROOT: path.join(tmp, 'codex-lb-global'), PATH: `${codexLbFakeBin}${path.delimiter}${process.env.PATH || ''}` };
1225
+ 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' };
1095
1226
  const codexLbSetup = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'codex-lb', 'setup', '--host', 'lb.example.test', '--api-key', 'sk-test', '--json'], {
1096
1227
  cwd: tmp,
1097
1228
  env: codexLbEnvForSelftest,
@@ -1103,48 +1234,50 @@ export async function selftestCodexLb(tmp) {
1103
1234
  const codexLbConfig = await safeReadText(path.join(codexLbHome, '.codex', 'config.toml'));
1104
1235
  const codexLbEnv = await safeReadText(path.join(codexLbHome, '.codex', 'sks-codex-lb.env'));
1105
1236
  const codexLbAuth = await safeReadText(path.join(codexLbHome, '.codex', 'auth.json'));
1106
- 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'") || !/(\"auth_mode\"\s*:\s*\"apikey\")/.test(codexLbAuth)) throw new Error('selftest: codex-lb setup');
1237
+ 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');
1238
+ const codexLbFailLaunchctl = path.join(codexLbFakeBin, 'launchctl-fail');
1239
+ await writeTextAtomic(codexLbFailLaunchctl, '#!/bin/sh\necho "launchctl denied" >&2\nexit 7\n');
1240
+ await fsp.chmod(codexLbFailLaunchctl, 0o755);
1241
+ const codexLbFailedLaunchEnv = await configureCodexLb({ home: path.join(tmp, 'codex-lb-launch-fail-home'), host: 'lb.example.test', apiKey: 'sk-fail', forceLaunchEnv: true, launchctlBin: codexLbFailLaunchctl });
1242
+ 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');
1107
1243
  if (!hasCodexUnstableFeatureWarningSuppression(codexLbConfig)) throw new Error('selftest: codex-lb setup did not suppress Codex unstable feature warning');
1108
1244
  await initProject(codexLbHome, { installScope: 'global', force: true, repair: true });
1109
1245
  const codexLbRepairSetupConfig = await safeReadText(path.join(codexLbHome, '.codex', 'config.toml'));
1110
- 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');
1246
+ 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');
1111
1247
  if (!hasCodexUnstableFeatureWarningSuppression(codexLbRepairSetupConfig)) throw new Error('selftest: init codex-lb did not suppress Codex unstable feature warning');
1112
1248
  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`);
1113
1249
  const ptmp = path.join(tmp, 'codex-lb-project-config'), prevHome = process.env.HOME;
1114
1250
  try { process.env.HOME = codexLbHome; await initProject(ptmp, { installScope: 'global' }); }
1115
1251
  finally { if (prevHome === undefined) delete process.env.HOME; else process.env.HOME = prevHome; }
1116
1252
  const pcfg = await safeReadText(path.join(ptmp, '.codex', 'config.toml'));
1117
- 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');
1253
+ 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');
1118
1254
  if (!hasCodexUnstableFeatureWarningSuppression(pcfg)) throw new Error('selftest: project codex-lb config did not suppress Codex unstable feature warning');
1119
1255
  await writeTextAtomic(path.join(codexLbHome, '.codex', 'auth.json'), '{"auth_mode":"browser"}\n');
1120
1256
  const codexLbRepair = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'auth', 'repair', '--json'], { cwd: tmp, env: codexLbEnvForSelftest, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
1121
1257
  if (codexLbRepair.code !== 0) throw new Error(`selftest: codex-lb repair exited ${codexLbRepair.code}: ${codexLbRepair.stderr}`);
1122
1258
  const codexLbRepairJson = JSON.parse(codexLbRepair.stdout);
1123
1259
  const codexLbRepairedAuth = await safeReadText(path.join(codexLbHome, '.codex', 'auth.json'));
1124
- if (!codexLbRepairJson.ok || codexLbRepairJson.status !== 'repaired' || !codexLbRepairedAuth.includes('"auth_mode":"apikey"') || !codexLbRepairedAuth.includes('sk-test')) throw new Error('selftest: codex-lb repair');
1125
- const codexLbLoginCallsBeforePostinstall = (await safeReadText(path.join(codexLbHome, '.codex', 'login-calls.log'))).trim().split(/\r?\n/).filter(Boolean).length;
1260
+ 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');
1261
+ 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 });
1262
+ if (codexLbLegacyRepair.code !== 0) throw new Error(`selftest: codex-lb legacy login repair exited ${codexLbLegacyRepair.code}: ${codexLbLegacyRepair.stderr}`);
1263
+ const codexLbLegacyRepairJson = JSON.parse(codexLbLegacyRepair.stdout);
1264
+ const codexLbLegacyAuth = await safeReadText(path.join(codexLbHome, '.codex', 'auth.json'));
1265
+ 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');
1266
+ const codexLbLoginCallsBeforePostinstall = await codexLbLoginCallCount(codexLbHome);
1126
1267
  await writeTextAtomic(path.join(codexLbHome, '.codex', 'auth.json'), '{"auth_mode":"browser"}\n');
1127
1268
  const codexLbPostinstall = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'postinstall'], {
1128
1269
  cwd: tmp,
1129
- env: {
1130
- ...codexLbEnvForSelftest,
1131
- SKS_POSTINSTALL_NO_BOOTSTRAP: '1',
1132
- SKS_SKIP_POSTINSTALL_SHIM: '1',
1133
- SKS_SKIP_POSTINSTALL_CONTEXT7: '1',
1134
- SKS_SKIP_POSTINSTALL_GETDESIGN: '1',
1135
- SKS_SKIP_POSTINSTALL_GLOBAL_SKILLS: '1',
1136
- SKS_SKIP_POSTINSTALL_CODEX_LB_AUTH: '0'
1137
- },
1270
+ env: codexLbPostinstallEnv(codexLbEnvForSelftest),
1138
1271
  timeoutMs: 15000,
1139
1272
  maxOutputBytes: 128 * 1024
1140
1273
  });
1141
1274
  if (codexLbPostinstall.code !== 0) throw new Error(`selftest: codex-lb postinstall auth preservation exited ${codexLbPostinstall.code}: ${codexLbPostinstall.stderr}`);
1142
1275
  const codexLbPostinstallAuth = await safeReadText(path.join(codexLbHome, '.codex', 'auth.json'));
1143
- const codexLbLoginCallsAfterPostinstall = (await safeReadText(path.join(codexLbHome, '.codex', 'login-calls.log'))).trim().split(/\r?\n/).filter(Boolean).length;
1144
- 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');
1145
- 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'];
1276
+ const codexLbLoginCallsAfterPostinstall = await codexLbLoginCallCount(codexLbHome);
1277
+ 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');
1278
+ 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'];
1146
1279
  const postinstallEnvBefore = Object.fromEntries(postinstallEnvKeys.map((key) => [key, process.env[key]]));
1147
- const codexLbLoginCallsBeforeBootstrap = (await safeReadText(path.join(codexLbHome, '.codex', 'login-calls.log'))).trim().split(/\r?\n/).filter(Boolean).length;
1280
+ const codexLbLoginCallsBeforeBootstrap = await codexLbLoginCallCount(codexLbHome);
1148
1281
  try {
1149
1282
  for (const key of postinstallEnvKeys) delete process.env[key];
1150
1283
  Object.assign(process.env, {
@@ -1157,7 +1290,8 @@ export async function selftestCodexLb(tmp) {
1157
1290
  SKS_SKIP_POSTINSTALL_CONTEXT7: '1',
1158
1291
  SKS_SKIP_POSTINSTALL_GETDESIGN: '1',
1159
1292
  SKS_SKIP_POSTINSTALL_GLOBAL_SKILLS: '1',
1160
- SKS_SKIP_POSTINSTALL_CODEX_LB_AUTH: '0'
1293
+ SKS_SKIP_POSTINSTALL_CODEX_LB_AUTH: '0',
1294
+ SKS_SKIP_CODEX_LB_LAUNCH_ENV: '1'
1161
1295
  });
1162
1296
  await postinstall({
1163
1297
  bootstrap: async () => {
@@ -1173,9 +1307,9 @@ export async function selftestCodexLb(tmp) {
1173
1307
  }
1174
1308
  const codexLbPostBootstrapAuth = await safeReadText(path.join(codexLbHome, '.codex', 'auth.json'));
1175
1309
  const codexLbPostBootstrapConfig = await safeReadText(path.join(codexLbHome, '.codex', 'config.toml'));
1176
- const codexLbLoginCallsAfterBootstrap = (await safeReadText(path.join(codexLbHome, '.codex', 'login-calls.log'))).trim().split(/\r?\n/).filter(Boolean).length;
1177
- if (!codexLbPostBootstrapAuth.includes('"auth_mode":"apikey"') || !codexLbPostBootstrapAuth.includes('sk-test') || codexLbLoginCallsAfterBootstrap <= codexLbLoginCallsBeforeBootstrap) throw new Error('selftest: postinstall drift auth');
1178
- 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');
1310
+ const codexLbLoginCallsAfterBootstrap = await codexLbLoginCallCount(codexLbHome);
1311
+ if (!codexLbPostBootstrapAuth.includes('"auth_mode":"browser"') || codexLbPostBootstrapAuth.includes('sk-test') || codexLbLoginCallsAfterBootstrap !== codexLbLoginCallsBeforeBootstrap) throw new Error('selftest: postinstall drift auth');
1312
+ 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');
1179
1313
  const doctorProject = tmpdir();
1180
1314
  await ensureDir(path.join(doctorProject, '.git'));
1181
1315
  await writeTextAtomic(path.join(doctorProject, 'package.json'), '{"name":"codex-lb-doctor-project","version":"0.0.0"}\n');
@@ -1192,7 +1326,7 @@ export async function selftestCodexLb(tmp) {
1192
1326
  const codexLbDoctorJson = JSON.parse(codexLbDoctorRepair.stdout);
1193
1327
  const codexLbDoctorAuth = await safeReadText(path.join(codexLbHome, '.codex', 'auth.json'));
1194
1328
  const codexLbDoctorConfig = await safeReadText(path.join(codexLbHome, '.codex', 'config.toml'));
1195
- 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') || hasTopLevelCodexLbSelected(codexLbDoctorConfig) || !codexLbDoctorConfig.includes('https://lb.example.test/backend-api/codex') || !hasCodexUnstableFeatureWarningSuppression(codexLbDoctorConfig)) throw new Error('selftest: doctor codex-lb');
1329
+ 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');
1196
1330
  const codexLbContext7Bin = path.join(tmp, 'codex-lb-context7-bin');
1197
1331
  await ensureDir(codexLbContext7Bin);
1198
1332
  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');
@@ -1214,23 +1348,82 @@ export async function selftestCodexLb(tmp) {
1214
1348
  });
1215
1349
  if (codexLbContext7Postinstall.code !== 0 || String(`${codexLbContext7Postinstall.stdout}\n${codexLbContext7Postinstall.stderr}`).includes('leaked CODEX_LB_API_KEY')) throw new Error('selftest: Context7 key leak');
1216
1350
  await writeTextAtomic(path.join(codexLbHome, '.codex', 'sks-codex-lb.env'), "export CODEX_LB_API_KEY='unterminated\n");
1217
- const codexLbLoginCallsBeforeMalformed = (await safeReadText(path.join(codexLbHome, '.codex', 'login-calls.log'))).trim().split(/\r?\n/).filter(Boolean).length;
1351
+ const codexLbLoginCallsBeforeMalformed = await codexLbLoginCallCount(codexLbHome);
1218
1352
  const codexLbMalformedPostinstall = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'postinstall'], {
1219
1353
  cwd: tmp,
1220
- env: {
1221
- ...codexLbEnvForSelftest,
1222
- SKS_POSTINSTALL_NO_BOOTSTRAP: '1',
1223
- SKS_SKIP_POSTINSTALL_SHIM: '1',
1224
- SKS_SKIP_POSTINSTALL_CONTEXT7: '1',
1225
- SKS_SKIP_POSTINSTALL_GETDESIGN: '1',
1226
- SKS_SKIP_POSTINSTALL_GLOBAL_SKILLS: '1',
1227
- SKS_SKIP_POSTINSTALL_CODEX_LB_AUTH: '0'
1228
- },
1354
+ env: codexLbPostinstallEnv(codexLbEnvForSelftest),
1229
1355
  timeoutMs: 15000,
1230
1356
  maxOutputBytes: 128 * 1024
1231
1357
  });
1232
- const codexLbLoginCallsAfterMalformed = (await safeReadText(path.join(codexLbHome, '.codex', 'login-calls.log'))).trim().split(/\r?\n/).filter(Boolean).length;
1358
+ const codexLbLoginCallsAfterMalformed = await codexLbLoginCallCount(codexLbHome);
1233
1359
  if (codexLbMalformedPostinstall.code !== 0 || !String(codexLbMalformedPostinstall.stdout || '').includes('codex-lb auth: stored key missing') || codexLbLoginCallsAfterMalformed !== codexLbLoginCallsBeforeMalformed) throw new Error('selftest: bad codex-lb env');
1360
+ await fsp.rm(path.join(codexLbHome, '.codex', 'sks-codex-lb.env'), { force: true });
1361
+ 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');
1362
+ await writeTextAtomic(path.join(codexLbHome, '.codex', 'auth.json'), '{"auth_mode":"apikey","key":"sk-legacy"}\n');
1363
+ const codexLbLoginCallsBeforeLegacyPostinstall = await codexLbLoginCallCount(codexLbHome);
1364
+ const codexLbLegacyPostinstall = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'postinstall'], {
1365
+ cwd: tmp,
1366
+ env: codexLbPostinstallEnv(codexLbEnvForSelftest),
1367
+ timeoutMs: 15000,
1368
+ maxOutputBytes: 128 * 1024
1369
+ });
1370
+ if (codexLbLegacyPostinstall.code !== 0) throw new Error(`selftest: legacy codex-lb postinstall restore exited ${codexLbLegacyPostinstall.code}: ${codexLbLegacyPostinstall.stderr}`);
1371
+ const codexLbLegacyPostinstallEnv = await safeReadText(path.join(codexLbHome, '.codex', 'sks-codex-lb.env'));
1372
+ const codexLbLegacyPostinstallAuth = await safeReadText(path.join(codexLbHome, '.codex', 'auth.json'));
1373
+ const codexLbLoginCallsAfterLegacyPostinstall = await codexLbLoginCallCount(codexLbHome);
1374
+ 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');
1375
+ await fsp.rm(path.join(codexLbHome, '.codex', 'sks-codex-lb.env'), { force: true });
1376
+ 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');
1377
+ await writeTextAtomic(path.join(codexLbHome, '.codex', 'auth.json'), '{"auth_mode":"apikey","key":"sk-legacy-doctor"}\n');
1378
+ const codexLbLegacyDoctorProject = tmpdir();
1379
+ await ensureDir(path.join(codexLbLegacyDoctorProject, '.git'));
1380
+ await writeTextAtomic(path.join(codexLbLegacyDoctorProject, 'package.json'), '{"name":"codex-lb-legacy-doctor-project","version":"0.0.0"}\n');
1381
+ const codexLbLegacyDoctorRepair = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'doctor', '--fix', '--json'], {
1382
+ cwd: codexLbLegacyDoctorProject,
1383
+ env: { ...codexLbEnvForSelftest, SKS_GLOBAL_ROOT: path.join(tmp, 'codex-lb-legacy-doctor-global'), SKS_SKIP_CODEX_LB_LAUNCH_ENV: '1' },
1384
+ timeoutMs: 30000,
1385
+ maxOutputBytes: 256 * 1024
1386
+ });
1387
+ if (codexLbLegacyDoctorRepair.code !== 0) throw new Error(`selftest: legacy doctor --fix codex-lb restore exited ${codexLbLegacyDoctorRepair.code}: ${codexLbLegacyDoctorRepair.stderr}`);
1388
+ const codexLbLegacyDoctorJson = JSON.parse(codexLbLegacyDoctorRepair.stdout);
1389
+ const codexLbLegacyDoctorEnv = await safeReadText(path.join(codexLbHome, '.codex', 'sks-codex-lb.env'));
1390
+ const codexLbLegacyDoctorConfig = await safeReadText(path.join(codexLbHome, '.codex', 'config.toml'));
1391
+ const codexLbLegacyDoctorAuth = await safeReadText(path.join(codexLbHome, '.codex', 'auth.json'));
1392
+ 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');
1393
+ await writeTextAtomic(path.join(codexLbHome, '.codex', 'sks-codex-lb.env'), "export CODEX_LB_BASE_URL='https://lb.example.test/backend-api/codex'\n");
1394
+ await writeTextAtomic(path.join(codexLbHome, '.codex', 'config.toml'), 'model = "gpt-5.5"\nservice_tier = "fast"\n');
1395
+ await writeTextAtomic(path.join(codexLbHome, '.codex', 'auth.json'), '{"auth_mode":"apikey","key":"sk-env-only"}\n');
1396
+ const codexLbLoginCallsBeforeEnvOnlyPostinstall = await codexLbLoginCallCount(codexLbHome);
1397
+ const codexLbEnvOnlyPostinstall = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'postinstall'], {
1398
+ cwd: tmp,
1399
+ env: codexLbPostinstallEnv(codexLbEnvForSelftest),
1400
+ timeoutMs: 15000,
1401
+ maxOutputBytes: 128 * 1024
1402
+ });
1403
+ if (codexLbEnvOnlyPostinstall.code !== 0) throw new Error(`selftest: env-only codex-lb postinstall restore exited ${codexLbEnvOnlyPostinstall.code}: ${codexLbEnvOnlyPostinstall.stderr}`);
1404
+ const codexLbEnvOnlyPostinstallEnv = await safeReadText(path.join(codexLbHome, '.codex', 'sks-codex-lb.env'));
1405
+ const codexLbEnvOnlyPostinstallConfig = await safeReadText(path.join(codexLbHome, '.codex', 'config.toml'));
1406
+ const codexLbEnvOnlyPostinstallAuth = await safeReadText(path.join(codexLbHome, '.codex', 'auth.json'));
1407
+ const codexLbLoginCallsAfterEnvOnlyPostinstall = await codexLbLoginCallCount(codexLbHome);
1408
+ 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"') || !hasTopLevelCodexLbSelected(codexLbEnvOnlyPostinstallConfig) || !codexLbEnvOnlyPostinstallAuth.includes('sk-env-only') || codexLbLoginCallsAfterEnvOnlyPostinstall !== codexLbLoginCallsBeforeEnvOnlyPostinstall) throw new Error('selftest: env-only codex-lb postinstall restore');
1409
+ await writeTextAtomic(path.join(codexLbHome, '.codex', 'sks-codex-lb.env'), "export CODEX_LB_BASE_URL='https://lb.example.test/backend-api/codex'\n");
1410
+ await writeTextAtomic(path.join(codexLbHome, '.codex', 'config.toml'), 'model = "gpt-5.5"\nservice_tier = "fast"\n');
1411
+ await writeTextAtomic(path.join(codexLbHome, '.codex', 'auth.json'), '{"auth_mode":"apikey","key":"sk-env-only-doctor"}\n');
1412
+ const codexLbEnvOnlyDoctorProject = tmpdir();
1413
+ await ensureDir(path.join(codexLbEnvOnlyDoctorProject, '.git'));
1414
+ await writeTextAtomic(path.join(codexLbEnvOnlyDoctorProject, 'package.json'), '{"name":"codex-lb-env-only-doctor-project","version":"0.0.0"}\n');
1415
+ const codexLbEnvOnlyDoctorRepair = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'doctor', '--fix', '--json'], {
1416
+ cwd: codexLbEnvOnlyDoctorProject,
1417
+ env: { ...codexLbEnvForSelftest, SKS_GLOBAL_ROOT: path.join(tmp, 'codex-lb-env-only-doctor-global'), SKS_SKIP_CODEX_LB_LAUNCH_ENV: '1' },
1418
+ timeoutMs: 30000,
1419
+ maxOutputBytes: 256 * 1024
1420
+ });
1421
+ if (codexLbEnvOnlyDoctorRepair.code !== 0) throw new Error(`selftest: env-only doctor --fix codex-lb restore exited ${codexLbEnvOnlyDoctorRepair.code}: ${codexLbEnvOnlyDoctorRepair.stderr}`);
1422
+ const codexLbEnvOnlyDoctorJson = JSON.parse(codexLbEnvOnlyDoctorRepair.stdout);
1423
+ const codexLbEnvOnlyDoctorEnv = await safeReadText(path.join(codexLbHome, '.codex', 'sks-codex-lb.env'));
1424
+ const codexLbEnvOnlyDoctorConfig = await safeReadText(path.join(codexLbHome, '.codex', 'config.toml'));
1425
+ const codexLbEnvOnlyDoctorAuth = await safeReadText(path.join(codexLbHome, '.codex', 'auth.json'));
1426
+ 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"') || !hasTopLevelCodexLbSelected(codexLbEnvOnlyDoctorConfig) || !codexLbEnvOnlyDoctorAuth.includes('sk-env-only-doctor')) throw new Error('selftest: env-only doctor codex-lb restore');
1234
1427
  await writeTextAtomic(path.join(codexLbHome, '.codex', 'sks-codex-lb.env'), "export CODEX_LB_API_KEY='sk-test'\n");
1235
1428
  const codexLbMissingCli = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'postinstall'], {
1236
1429
  cwd: tmp,
@@ -1243,12 +1436,13 @@ export async function selftestCodexLb(tmp) {
1243
1436
  SKS_SKIP_POSTINSTALL_CONTEXT7: '1',
1244
1437
  SKS_SKIP_POSTINSTALL_GETDESIGN: '1',
1245
1438
  SKS_SKIP_POSTINSTALL_GLOBAL_SKILLS: '1',
1246
- SKS_SKIP_POSTINSTALL_CODEX_LB_AUTH: '0'
1439
+ SKS_SKIP_POSTINSTALL_CODEX_LB_AUTH: '0',
1440
+ SKS_SKIP_CODEX_LB_LAUNCH_ENV: '1'
1247
1441
  },
1248
1442
  timeoutMs: 15000,
1249
1443
  maxOutputBytes: 128 * 1024
1250
1444
  });
1251
- if (codexLbMissingCli.code !== 0 || !String(codexLbMissingCli.stdout || '').includes('codex-lb auth: repair skipped (codex_missing')) throw new Error('selftest: codex missing');
1445
+ 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');
1252
1446
  const codexLbNotConfiguredHome = path.join(tmp, 'codex-lb-not-configured-home');
1253
1447
  const codexLbNotConfigured = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'postinstall'], {
1254
1448
  cwd: tmp,
@@ -1268,7 +1462,7 @@ export async function selftestCodexLb(tmp) {
1268
1462
  });
1269
1463
  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');
1270
1464
  const codexLbStatusText = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'codex-lb', 'status'], { cwd: tmp, env: codexLbEnvForSelftest, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
1271
- if (!String(codexLbStatusText.stdout || '').includes('Repair auth: sks codex-lb repair')) throw new Error('selftest: codex-lb status did not advertise repair command');
1465
+ if (!String(codexLbStatusText.stdout || '').includes('Repair provider auth: sks codex-lb repair')) throw new Error('selftest: codex-lb status did not advertise repair command');
1272
1466
  const nonInteractiveLaunchChainCalls = [];
1273
1467
  const nonInteractiveLaunch = await maybePromptCodexLbSetupForLaunch([], {
1274
1468
  home: codexLbHome,
@@ -1334,7 +1528,7 @@ function hasTopLevelCodexModeLock(text = '') {
1334
1528
  const lines = String(text || '').split('\n');
1335
1529
  const firstTable = lines.findIndex((x) => /^\s*\[.+\]\s*$/.test(x));
1336
1530
  const top = (firstTable === -1 ? lines : lines.slice(0, firstTable)).join('\n');
1337
- return /(^|\n)\s*model_provider\s*=\s*"codex-lb"\s*(\n|$)/.test(top) || /(^|\n)\s*model_reasoning_effort\s*=/.test(top);
1531
+ return /(^|\n)\s*model_reasoning_effort\s*=/.test(top);
1338
1532
  }
1339
1533
 
1340
1534
  function hasDeprecatedCodexHooksFeatureFlag(text = '') {
package/src/cli/main.mjs CHANGED
@@ -1148,8 +1148,9 @@ async function codexLbCommand(action = 'status', args = []) {
1148
1148
  console.log(`Provider: ${status.provider_configured ? 'yes' : 'no'}`);
1149
1149
  console.log(`Env file: ${status.env_file ? status.env_path : 'missing'}`);
1150
1150
  if (status.base_url) console.log(`Base URL: ${status.base_url}`);
1151
- if (!status.ok) console.log('\nRun: sks codex-lb setup --host <domain> --api-key <key>');
1152
- else console.log('\nRepair auth: sks codex-lb repair');
1151
+ if (status.ok && !status.selected) console.log('\nRun: sks codex-lb repair to activate codex-lb for Codex App.');
1152
+ else if (!status.ok) console.log('\nRun: sks codex-lb setup --host <domain> --api-key <key>');
1153
+ else console.log('\nRepair provider auth: sks codex-lb repair');
1153
1154
  return;
1154
1155
  }
1155
1156
  if (sub === 'health' || sub === 'verify-chain' || sub === 'chain') {
@@ -1176,7 +1177,7 @@ async function codexLbCommand(action = 'status', args = []) {
1176
1177
  process.exitCode = 1;
1177
1178
  return;
1178
1179
  }
1179
- console.log('codex-lb auth repaired for Codex CLI.');
1180
+ console.log('codex-lb provider auth repaired for Codex CLI/App environment.');
1180
1181
  console.log(`Config: ${result.config_path}`);
1181
1182
  console.log(`Key env: ${result.env_path}`);
1182
1183
  return;
@@ -1193,7 +1194,7 @@ async function codexLbCommand(action = 'status', args = []) {
1193
1194
  const result = await configureCodexLb({ host, apiKey });
1194
1195
  if (json) return console.log(JSON.stringify(result, null, 2));
1195
1196
  if (!result.ok) {
1196
- console.error(`codex-lb setup failed: ${result.status}`);
1197
+ console.error(`codex-lb setup failed: ${result.status}${result.error ? `: ${result.error}` : ''}`);
1197
1198
  process.exitCode = 1;
1198
1199
  return;
1199
1200
  }
@@ -1498,7 +1499,7 @@ function usage(args = []) {
1498
1499
  openclaw: ['OpenClaw', '', ' sks openclaw install', ' sks openclaw path', ' sks openclaw print SKILL.md', '', 'Installs an OpenClaw skill package under ~/.openclaw/skills/sneakoscope-codex so OpenClaw agents can attach skills: [sneakoscope-codex] with the shell tool and call local SKS commands from a project root.'],
1499
1500
  team: ['Team', '', ' sks team "task" executor:5 reviewer:6 user:1', ' sks team open-tmux latest', ' sks team watch latest', ' sks team lane latest --agent analysis_scout_1 --follow', ' sks team message latest --from analysis_scout_1 --to executor_1 --message "handoff note"', ' sks team cleanup-tmux latest', '', '$Team auto-seals a route contract, opens scout-first tmux lanes when available, then runs scouts -> TriWiki attention -> debate -> runtime graph/inbox -> fresh executors -> review -> cleanup -> reflection -> Honest.'],
1500
1501
  'qa-loop': ['QA-LOOP', '', ' sks qa-loop prepare "QA this app"', ' sks qa-loop answer <MISSION_ID> answers.json', ' sks qa-loop run <MISSION_ID> --max-cycles 8', '', 'Report: YYYY-MM-DD-v<version>-qa-report.md'],
1501
- ppt: ['PPT', '', ' $PPT 투자자용 피치덱을 HTML 기반 PDF로 만들어줘', ' $PPT 우리 SaaS 소개자료 만들어줘', ' sks ppt build latest --json', ' sks ppt status latest --json', '', '$PPT infers delivery context, audience profile, STP strategy, decision context, and 3+ pain-point/solution/aha mappings before source research, design-system work, HTML/PDF export, render QA, fact-ledger validation, and bounded review-loop validation. Independent strategy/render/file-write phases run in parallel where inputs allow and are recorded in ppt-parallel-report.json. The visual system must stay simple, restrained, and information-first; editable source HTML is kept under source-html/, PPT-only temporary build files are cleaned, and installed skills/MCPs outside the $PPT allowlist are ignored. Design uses getdesign-reference plus the built-in PPT design pipeline; Codex App $imagegen/gpt-image-2 and Context7 are conditional only when the sealed PPT contract needs raster assets, slide visual critique, or current external docs. Missing required $imagegen/gpt-image-2 output blocks instead of being simulated.'],
1502
+ ppt: ['PPT', '', ' $PPT 투자자용 피치덱을 HTML 기반 PDF로 만들어줘', ' $PPT 우리 SaaS 소개자료 만들어줘', ' sks ppt build latest --json', ' sks ppt status latest --json', '', '$PPT infers delivery context, audience profile, STP strategy, decision context, and 3+ pain-point/solution/aha mappings before source research, design-system work, HTML/PDF export, render QA, fact-ledger validation, and bounded review-loop validation. Independent strategy/render/file-write phases run in parallel where inputs allow and are recorded in ppt-parallel-report.json. The visual system must stay simple, restrained, and information-first; editable source HTML is kept under source-html/, PPT-only temporary build files are cleaned, and installed skills/MCPs outside the $PPT allowlist are ignored. Design uses getdesign-reference plus the built-in PPT design pipeline; imagegen is a required PPT skill so any needed raster assets or generated slide visual critique must invoke Codex App $imagegen/gpt-image-2 and save real outputs into the mission assets/review evidence paths. Context7 is conditional only when the sealed PPT contract needs current external docs. Missing required $imagegen/gpt-image-2 output blocks instead of being simulated.'],
1502
1503
  'image-ux-review': ['Image UX Review', '', ' $Image-UX-Review localhost 화면을 이미지 생성 리뷰 루프로 검수해줘', ' $UX-Review 이 스크린샷을 gpt-image-2 콜아웃 리뷰로 분석하고 고쳐줘', ' sks image-ux-review status latest --json', '', '$Image-UX-Review captures or receives source UI screenshots, runs Codex App $imagegen/gpt-image-2 to create generated annotated review images with numbered callouts, then extracts those generated images into image-ux-issue-ledger.json. Text-only screenshot critique cannot pass image-ux-review-gate.json; missing generated review images remain an explicit blocker.'],
1503
1504
  goal: ['Goal', '', ' sks goal create "task"', ' sks goal status latest', ' sks goal pause latest', ' sks goal resume latest', ' sks goal clear latest'],
1504
1505
  'codex-app': ['Codex App', '', ' sks bootstrap', ' sks codex-app check', ' sks codex-app remote-control --status', ' sks dollar-commands', ' cat .codex/SNEAKOSCOPE.md'],
@@ -1731,7 +1732,7 @@ async function doctor(args) {
1731
1732
  const skillStatus = await checkRequiredSkills(root);
1732
1733
  const globalSkillStatus = await checkRequiredSkills(null, globalCodexSkillsRoot());
1733
1734
  const codexLb = await codexLbStatus();
1734
- const codexLbReady = (!codexLb.selected && !codexLb.provider_configured && !codexLb.env_file) || codexLb.ok;
1735
+ const codexLbReady = (!codexLb.selected && !codexLb.provider_configured && !codexLb.env_file) || (codexLb.ok && codexLb.selected);
1735
1736
  const guardStatus = await harnessGuardStatus(root);
1736
1737
  const versioningInfo = await versioningStatus(root);
1737
1738
  const codexApp = await codexAppFilesStatus(root, skillStatus, versioningInfo);
@@ -1779,7 +1780,7 @@ async function doctor(args) {
1779
1780
  const cleanup = removed.length ? ` removed stale generated skill shadow(s): ${removed.join(', ')}` : '';
1780
1781
  console.log(`Global $ repair: ${globalSkillsRepair.status} ${globalSkillsRepair.root || ''}${cleanup}`.trimEnd());
1781
1782
  }
1782
- if (codexLbRepair?.ok) console.log(`codex-lb repair: ${codexLbRepair.config_repaired ? 'config+auth resynced' : 'auth resynced'} from stored env`);
1783
+ if (codexLbRepair?.ok) console.log(`codex-lb repair: ${codexLbRepair.config_repaired ? 'config+provider auth resynced' : 'provider auth resynced'} from stored env`);
1783
1784
  else if (codexLbRepair && codexLbRepair.status !== 'missing_env_key') console.log(`codex-lb repair: skipped (${codexLbRepair.status})`);
1784
1785
  if (flag(args, '--fix') && result.harness_conflicts.hard_block) console.log('Repair: skipped because another Codex harness needs human-approved removal first');
1785
1786
  console.log(`Rust acc.: ${rust.available ? rust.version : 'optional-missing'}`);
@@ -3650,6 +3651,7 @@ async function selftest() {
3650
3651
  const pptRoute = routePrompt('$PPT 투자자용 피치덱 만들어줘');
3651
3652
  if (pptRoute?.id !== 'PPT') throw new Error('selftest: $PPT did not route to presentation pipeline');
3652
3653
  if (JSON.stringify(pptRoute.requiredSkills) !== JSON.stringify(PPT_PIPELINE_SKILL_ALLOWLIST)) throw new Error(`selftest: PPT route required skills are not allowlisted: ${pptRoute.requiredSkills.join(',')}`);
3654
+ if (!pptRoute.requiredSkills.includes('imagegen')) throw new Error('selftest: PPT route must load imagegen so required PPT raster assets use Codex App $imagegen');
3653
3655
  if (pptRoute.requiredSkills.includes('design-artifact-expert') || pptRoute.requiredSkills.includes('design-ui-editor') || pptRoute.requiredSkills.includes('design-system-builder')) throw new Error('selftest: PPT route still requires generic design skills');
3654
3656
  const pptSchema = buildQuestionSchema('$PPT 투자자용 피치덱 만들어줘');
3655
3657
  const pptSlotIds = pptSchema.slots.map((s) => s.id);
@@ -3661,7 +3663,7 @@ async function selftest() {
3661
3663
  if (!pptSkillText.includes('simple, restrained, and information-first') || !pptSkillText.includes('over-designed decoration') || !pptSkillText.includes(CODEX_APP_IMAGE_GENERATION_DOC_URL) || !pptSkillText.includes(CODEX_IMAGEGEN_REQUIRED_POLICY) || !pptSkillText.includes(AWESOME_DESIGN_MD_REFERENCE.url) || !pptSkillText.includes('only design decision SSOT') || !pptSkillText.includes('instead of treating references as parallel authorities')) throw new Error('selftest: generated PPT skill missing restrained design/imagegen/fused-SSOT guidance');
3662
3664
  if (!pptSkillText.includes('PPT pipeline allowlist') || !pptSkillText.includes('ignore installed skills and MCPs') || !pptSkillText.includes('prevent AI-like generic presentation design') || !pptSkillText.includes('Do not use generic design skills such as design-artifact-expert')) throw new Error('selftest: generated PPT skill missing pipeline allowlist enforcement');
3663
3665
  if (!pptSkillText.includes('source-html/') || !pptSkillText.includes('temporary build files') || !pptSkillText.includes('ppt-parallel-report.json')) throw new Error('selftest: generated PPT skill missing source preservation/temp cleanup/parallel guidance');
3664
- if (!pptSkillText.includes('ppt-fact-ledger.json') || !pptSkillText.includes('ppt-image-asset-ledger.json') || !pptSkillText.includes('direct API fallback') || !pptSkillText.includes('ppt-review-ledger.json') || !pptSkillText.includes('ppt-iteration-report.json') || !pptSkillText.includes('never simulate missing gpt-image-2 output')) throw new Error('selftest: generated PPT skill missing fact/image/review loop anti-fake guidance');
3666
+ if (!pptSkillText.includes('ppt-fact-ledger.json') || !pptSkillText.includes('ppt-image-asset-ledger.json') || !pptSkillText.includes('direct API fallback') || !pptSkillText.includes('ppt-review-ledger.json') || !pptSkillText.includes('ppt-iteration-report.json') || !pptSkillText.includes('never simulate missing gpt-image-2 output') || !pptSkillText.includes('always loads imagegen') || !pptSkillText.includes('immediately invoke Codex App `$imagegen`')) throw new Error('selftest: generated PPT skill missing fact/image/review loop anti-fake guidance');
3665
3667
  if (routeRequiresSubagents(pptRoute, '$PPT 투자자용 피치덱 만들어줘')) throw new Error('selftest: PPT route should not require subagents by default');
3666
3668
  if (!reflectionRequiredForRoute(pptRoute)) throw new Error('selftest: PPT route should require reflection');
3667
3669
  const pptMission = await createMission(tmp, { mode: 'ppt', prompt: '$PPT 투자자용 피치덱 만들어줘' });
@@ -3753,6 +3755,7 @@ async function selftest() {
3753
3755
  const requiredImageBuild = JSON.parse(requiredImageBuildResult.stdout);
3754
3756
  const requiredImageLedger = await readJson(path.join(requiredImagePptMission.dir, PPT_IMAGE_ASSET_LEDGER_ARTIFACT));
3755
3757
  if (requiredImageBuild.ok || requiredImageBuild.gate?.passed || !requiredImageBuild.gate?.image_asset_ledger_created || requiredImageBuild.gate?.image_asset_policy_satisfied !== false || !requiredImageLedger.required || requiredImageLedger.passed || !requiredImageLedger.blockers?.includes('missing_codex_app_imagegen_gpt_image_2_asset_evidence') || requiredImageLedger.generated_count !== 0) throw new Error('selftest: required PPT image assets were not blocked without Codex App imagegen evidence');
3758
+ if (requiredImageLedger.imagegen_execution?.command !== '$imagegen' || requiredImageLedger.imagegen_execution?.required_skill !== 'imagegen' || !requiredImageLedger.assets?.every((asset) => asset.imagegen_invocation?.command === '$imagegen')) throw new Error('selftest: required PPT image assets did not carry Codex App $imagegen invocation instructions');
3756
3759
  const installUxSchema = buildQuestionSchema('SKS first install/bootstrap UX and Context7 MCP setup improvement');
3757
3760
  const installUxSlotIds = installUxSchema.slots.map((s) => s.id);
3758
3761
  if (installUxSchema.domain_hints.includes('uiux') || installUxSlotIds.includes('VISUAL_REGRESSION_REQUIRED')) throw new Error('selftest: CLI UX install prompt should not ask visual UI questions');
@@ -401,7 +401,6 @@ 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`);
405
404
  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`);
406
405
  }
407
406
  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.4';
8
+ export const PACKAGE_VERSION = '0.8.6';
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) || /^model_provider\s*=\s*"codex-lb"/m.test(top);
51
+ return (Boolean(model) && model !== 'gpt-5.5') || /^model_reasoning_effort\s*=/m.test(top);
52
52
  }
53
53
 
54
54
  export function hasDeprecatedCodexHooksFeatureFlag(text = '') {
@@ -502,7 +502,6 @@ 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');
506
505
  next = removeTomlTableKey(next, 'notice', 'fast_default_opt_out');
507
506
  next = removeTomlTableKey(next, 'features', 'codex_hooks');
508
507
  next = upsertTopLevelTomlString(next, 'model', 'gpt-5.5');
@@ -548,14 +547,14 @@ async function mergeGlobalCodexConfigIfAvailable(configText = '', configPath = '
548
547
  let next = mergeGlobalMcpServers(configText, globalConfig);
549
548
  next = mergeGlobalCodexAppRuntimeTables(next, globalConfig);
550
549
  if (selectedRe.test(next) && /\[model_providers\.codex-lb\]/.test(next)) {
551
- return `${removeTopLevelTomlKeyIfValue(next, 'model_provider', 'codex-lb').trim()}\n`;
550
+ return `${next.trim()}\n`;
552
551
  }
553
552
  const envPath = path.join(home, '.codex', 'sks-codex-lb.env');
554
553
  if (!(await exists(envPath))) return next;
555
554
  const envText = await readText(envPath, '');
556
555
  const baseUrl = globalConfig.match(/(^|\n)\[model_providers\.codex-lb\][\s\S]*?\n\s*base_url\s*=\s*"([^"]+)"/)?.[2] || parseCodexLbEnvBaseUrl(envText);
557
- if (!parseCodexLbEnvKey(envText) || !baseUrl || (!selectedRe.test(globalConfig) && !parseCodexLbEnvBaseUrl(envText))) return next;
558
- next = removeTopLevelTomlKeyIfValue(next, 'model_provider', 'codex-lb');
556
+ if (!parseCodexLbEnvKey(envText) || !baseUrl) return next;
557
+ next = upsertTopLevelTomlString(next, 'model_provider', 'codex-lb');
559
558
  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`);
560
559
  return `${next.trim()}\n`;
561
560
  }
@@ -916,7 +915,7 @@ export async function installSkills(root) {
916
915
  'team': `---\nname: team\ndescription: SKS Team orchestration for $Team/code work; $From-Chat-IMG is the explicit chat-image alias.\n---\n\nUse for $Team/code work. Auto-seal the route contract from prompt, TriWiki/current-code defaults, and conservative policy; do not surface a prequestion sheet. Read pipeline-plan.json or run sks pipeline plan to see the runtime lane, kept/skipped stages, and verification before implementation. Write team-roster.json; team-gate.json needs team_roster_confirmed=true. executor:N means N scouts, N debate voices, then fresh N executors. ${MIN_TEAM_REVIEW_POLICY_TEXT} After consensus, compile team-graph.json, team-runtime-tasks.json, team-decomposition-report.json, and team-inbox/ so worker handoff uses concrete runtime task ids with role/path/domain/lane hints. Refresh/validate TriWiki before debate, implementation, review, and final; consume attention.use_first and hydrate attention.hydrate_first before risky decisions. ${outcomeRubricPolicyText()} ${speedLanePolicyText()} ${solutionScoutPolicyText('fix this broken behavior')} ${skillDreamPolicyText()} Log events and use sks team message for bounded inter-agent communication in transcript/lane panes. Color-coded tmux lanes distinguish overview/scout/planning/execution/review/safety sessions in one tmux window using split panes when tmux is available. $Team/$team plus sks --mad uses the MAD-SKS permission gate module: live server work, normal DB writes, Supabase MCP writes, direct SQL, schema cleanup, and needed migrations are open for the active invocation; only catastrophic DB wipe/all-row/project-management guards remain. End with cleanup-tmux or a cleanup event so follow panes show cleanup and stop; pass team-session-cleanup.json, then reflection and Honest Mode. Parent integrates/verifies.\n\n${chatCaptureIntakeText()}\n`,
917
916
  'from-chat-img': `---\nname: from-chat-img\ndescription: Explicit $From-Chat-IMG Team alias for chat screenshot plus attachment analysis.\n---\n\nUse only for From-Chat-IMG/$From-Chat-IMG. It enters the normal Team pipeline. Treat uploads as chat screenshot plus originals. Use Codex Computer Use visual inspection when available, list requirements first, match regions to attachments with confidence, write ${FROM_CHAT_IMG_COVERAGE_ARTIFACT}, ${FROM_CHAT_IMG_CHECKLIST_ARTIFACT}, ${FROM_CHAT_IMG_TEMP_TRIWIKI_ARTIFACT}, and ${FROM_CHAT_IMG_QA_LOOP_ARTIFACT}, then continue Team gates, review, reflection, and Honest Mode. ${CODEX_COMPUTER_USE_ONLY_POLICY} The ledger must account for every visible customer request, screenshot image region, and separate attachment; ${FROM_CHAT_IMG_CHECKLIST_ARTIFACT} must have a checked item for each request, image-region/attachment match, work item, scoped QA-LOOP, and verification step; ${FROM_CHAT_IMG_TEMP_TRIWIKI_ARTIFACT} stores temporary TriWiki-backed session context with expires_after_sessions=${FROM_CHAT_IMG_TEMP_TRIWIKI_SESSIONS}. ${FROM_CHAT_IMG_QA_LOOP_ARTIFACT} must prove QA-LOOP ran over the exact customer-request work-order range after implementation, with every work item covered, post-fix verification complete, and zero unresolved findings. team-gate.json cannot pass From-Chat-IMG completion until unresolved_items is empty, every checklist box is checked, and scoped_qa_loop_completed=true.\n`,
918
917
  'qa-loop': `---\nname: qa-loop\ndescription: $QA-LOOP dogfoods UI/API as human proxy with safety gates, Codex Computer Use-only UI evidence, safe fixes, rechecks, and a QA report.\n---\n\nUse only $QA-LOOP. Infer scope, target, mutation policy, and login boundary from the prompt plus TriWiki/current-code defaults; do not surface a prequestion sheet. Credentials are runtime-only; never save secrets. UI-level E2E needs official Codex Computer Use evidence or must be marked unverified; Chrome MCP, Browser Use, Playwright, Selenium, Puppeteer, and other browser automation do not satisfy UI/browser verification. Deployed targets are read-only; destructive removal is forbidden. After answer/run, dogfood real flows, apply safe contract-allowed code/test/docs fixes, recheck, and do not pass qa-gate.json with unresolved findings or without post_fix_verification_complete. Finish qa-ledger, date/version report, gate, completion summary, and Honest Mode.\n`,
919
- 'ppt': `---\nname: ppt\ndescription: $PPT information-first HTML/PDF presentation pipeline with inferred STP, audience, pain-point, format, research, design-system, and verification contract.\n---\n\nUse only when the user invokes $PPT or asks to create a presentation, deck, slides, pitch deck, proposal deck, HTML presentation, or PDF presentation artifact. Before artifact work, auto-seal presentation-specific answers from prompt, TriWiki/current-code defaults, and conservative policy: delivery context, target audience profile including role/average age/job/industry/topic familiarity/decision power, STP strategy, decision context and objections, and 3+ pain-point to solution mappings with expected aha moments. Do not surface a prequestion sheet. Presentation design must be simple, restrained, and information-first: avoid over-designed decoration, ornamental gradients, nested cards, and effects that compete with the message. Design detail should be embedded through typography hierarchy, spacing, alignment, thin rules, source clarity, and subtle accents. ${pptPipelineAllowlistPolicyText()} Use design.md as the only design decision SSOT. If design.md is missing, use docs/Design-Sys-Prompt.md plus getdesign-reference and curated DESIGN.md examples from ${AWESOME_DESIGN_MD_REFERENCE.url} only as source inputs, then fuse them into route-local PPT style tokens with a recorded design_ssot instead of treating references as parallel authorities. If generated image assets or slide visual critique are needed, use Codex App $imagegen/gpt-image-2 only when that asset/review need is explicitly sealed in the $PPT contract; direct API fallback, placeholder files, and prose-only substitutes do not satisfy the route gate. ${CODEX_IMAGEGEN_REQUIRED_POLICY} Use web or Context7 evidence only when external facts/libraries/current docs are required by the PPT contract, record verified claims in ppt-fact-ledger.json, record generated image asset plans/results/blockers in ppt-image-asset-ledger.json, then create the PDF plus editable source HTML under source-html/, keep independent strategy/render/file-write phases parallel where inputs allow, record ppt-parallel-report.json, run the bounded ppt-review-policy/ppt-review-ledger/ppt-iteration-report loop, and verify readability, overlap, format fit, source coverage, export state, unsupported-claim status, image-asset completion, review-loop termination, and temporary build files cleanup. Finish with reflection and Honest Mode.\n`,
918
+ 'ppt': `---\nname: ppt\ndescription: $PPT information-first HTML/PDF presentation pipeline with inferred STP, audience, pain-point, format, research, design-system, and verification contract.\n---\n\nUse only when the user invokes $PPT or asks to create a presentation, deck, slides, pitch deck, proposal deck, HTML presentation, or PDF presentation artifact. Before artifact work, auto-seal presentation-specific answers from prompt, TriWiki/current-code defaults, and conservative policy: delivery context, target audience profile including role/average age/job/industry/topic familiarity/decision power, STP strategy, decision context and objections, and 3+ pain-point to solution mappings with expected aha moments. Do not surface a prequestion sheet. Presentation design must be simple, restrained, and information-first: avoid over-designed decoration, ornamental gradients, nested cards, and effects that compete with the message. Design detail should be embedded through typography hierarchy, spacing, alignment, thin rules, source clarity, and subtle accents. ${pptPipelineAllowlistPolicyText()} Use design.md as the only design decision SSOT. If design.md is missing, use docs/Design-Sys-Prompt.md plus getdesign-reference and curated DESIGN.md examples from ${AWESOME_DESIGN_MD_REFERENCE.url} only as source inputs, then fuse them into route-local PPT style tokens with a recorded design_ssot instead of treating references as parallel authorities. The $PPT route always loads imagegen as a required skill. When the sealed contract needs a generated raster asset or generated slide visual critique, immediately invoke Codex App \`$imagegen\` with gpt-image-2, move/copy the selected output into the mission assets or review evidence path, and record the real file path in ppt-image-asset-ledger.json or ppt-review-ledger.json before building or passing the gate. Direct API fallback, placeholder files, HTML/CSS stand-ins, and prose-only substitutes do not satisfy the route gate. ${CODEX_IMAGEGEN_REQUIRED_POLICY} Use web or Context7 evidence only when external facts/libraries/current docs are required by the PPT contract, record verified claims in ppt-fact-ledger.json, record generated image asset plans/results/blockers in ppt-image-asset-ledger.json, then create the PDF plus editable source HTML under source-html/, keep independent strategy/render/file-write phases parallel where inputs allow, record ppt-parallel-report.json, run the bounded ppt-review-policy/ppt-review-ledger/ppt-iteration-report loop, and verify readability, overlap, format fit, source coverage, export state, unsupported-claim status, image-asset completion, review-loop termination, and temporary build files cleanup. Finish with reflection and Honest Mode.\n`,
920
919
  'computer-use-fast': `---\nname: computer-use-fast\ndescription: Alias for the maximum-speed $Computer-Use/$CU Codex Computer Use lane.\n---\n\nUse the same rules as computer-use: skip Team debate, QA-LOOP clarification, upfront TriWiki refresh, Context7, subagents, and reflection unless explicitly requested. Use Codex Computer Use directly; never substitute Playwright, Chrome MCP, Browser Use, Selenium, Puppeteer, or other browser automation for UI/browser evidence. At the end only, refresh/pack TriWiki, validate it, then provide a concise completion summary plus Honest Mode.\n`,
921
920
  'cu': `---\nname: cu\ndescription: Short alias for the maximum-speed $Computer-Use Codex Computer Use lane.\n---\n\nUse the same rules as computer-use. This is a speed lane for focused UI/browser/visual tasks that require Codex Computer Use evidence, with TriWiki refresh/validate and Honest Mode deferred to final closeout.\n`,
922
921
  'goal': `---\nname: goal\ndescription: Fast $Goal/$goal bridge overlay for Codex native persisted /goal workflows.\n---\n\nUse when the user invokes $Goal/$goal or asks to persist a workflow with Codex native /goal continuation. Prepare with sks goal create or the $Goal route, write only the lightweight bridge artifacts, then use native Codex /goal create, pause, resume, and clear controls where available. Goal does not replace Team, QA, DB, or other SKS execution routes; continue implementation through the selected route and use Context7 only when external API/library docs are involved. Do not recreate the old no-question loop.\n`,
@@ -313,7 +313,7 @@ export function promptPipelineContext(prompt, route = null) {
313
313
  if (reflectionRequiredForRoute(route)) lines.push(reflectionInstructionText());
314
314
  if (route?.id === 'Team') lines.push(`Team route: scouts, TriWiki refresh, debate, consensus, runtime graph compile with concrete task ids and worker inboxes, close planning agents, fresh executors, minimum ${MIN_TEAM_REVIEWER_LANES}-lane review/integration, ${TEAM_SESSION_CLEANUP_ARTIFACT}, reflection, and Honest Mode. ${MIN_TEAM_REVIEW_POLICY_TEXT}`);
315
315
  if (route?.id === 'Goal') lines.push('Goal route: write SKS goal bridge artifacts, then use Codex native /goal persistence for create, pause, resume, and clear continuation controls.');
316
- if (route?.id === 'PPT') lines.push(`PPT route: before design or PDF work, infer and seal delivery context, audience profile including average age/job/industry, STP strategy, decision context, and at least three pain-point to solution mappings from the prompt, TriWiki/current-code defaults, and conservative policy. Keep the visual system simple, restrained, and information-first; design detail should come from hierarchy, spacing, alignment, rules, and subtle accents rather than decorative overdesign. ${pptPipelineAllowlistPolicyText()} If generated image assets or slide visual critique are needed, required evidence must come from Codex App $imagegen/gpt-image-2 (${CODEX_APP_IMAGE_GENERATION_DOC_URL}); direct API fallback, placeholders, and prose-only substitutes do not satisfy the route gate. ${CODEX_IMAGEGEN_REQUIRED_POLICY} Then build source ledger, fact ledger, image asset ledger, storyboard with aha moments, style tokens, editable source HTML under source-html/, PDF artifact, render QA, bounded review ledger/iteration report, PPT-only temporary build file cleanup, and ppt-parallel-report.json so independent strategy/render/file-write phases stay parallel-friendly, then reflection and Honest Mode.`);
316
+ if (route?.id === 'PPT') lines.push(`PPT route: before design or PDF work, infer and seal delivery context, audience profile including average age/job/industry, STP strategy, decision context, and at least three pain-point to solution mappings from the prompt, TriWiki/current-code defaults, and conservative policy. Keep the visual system simple, restrained, and information-first; design detail should come from hierarchy, spacing, alignment, rules, and subtle accents rather than decorative overdesign. ${pptPipelineAllowlistPolicyText()} If generated image assets or slide visual critique are needed, actively invoke the loaded imagegen skill through Codex App $imagegen/gpt-image-2 (${CODEX_APP_IMAGE_GENERATION_DOC_URL}), save the selected raster output into the mission assets/review evidence path, and record that real path before build/final. Direct API fallback, placeholders, HTML/CSS stand-ins, and prose-only substitutes do not satisfy the route gate. ${CODEX_IMAGEGEN_REQUIRED_POLICY} Then build source ledger, fact ledger, image asset ledger, storyboard with aha moments, style tokens, editable source HTML under source-html/, PDF artifact, render QA, bounded review ledger/iteration report, PPT-only temporary build file cleanup, and ppt-parallel-report.json so independent strategy/render/file-write phases stay parallel-friendly, then reflection and Honest Mode.`);
317
317
  if (route?.id === 'ImageUXReview') lines.push(`Image UX Review route: ${imageUxReviewPipelinePolicyText()} Use ${IMAGE_UX_REVIEW_POLICY_ARTIFACT}, ${IMAGE_UX_REVIEW_SCREEN_INVENTORY_ARTIFACT}, ${IMAGE_UX_REVIEW_GENERATED_REVIEW_LEDGER_ARTIFACT}, ${IMAGE_UX_REVIEW_ISSUE_LEDGER_ARTIFACT}, ${IMAGE_UX_REVIEW_ITERATION_REPORT_ARTIFACT}, and ${IMAGE_UX_REVIEW_GATE_ARTIFACT} as the route evidence set. The route may suggest safe fixes only when the user requested fixing; otherwise report findings and blockers.`);
318
318
  if (route?.id === 'AutoResearch') lines.push('AutoResearch route: load autoresearch-loop plus seo-geo-optimizer when SEO/GEO, discoverability, README, npm, GitHub stars, ranking, or AI-search visibility is relevant.');
319
319
  if (route?.id === 'DB') lines.push('DB route: scan/check database risk first; destructive DB operations remain forbidden.');
package/src/core/ppt.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import path from 'node:path';
2
2
  import fsp from 'node:fs/promises';
3
3
  import { nowIso, readJson, sha256, writeJsonAtomic, writeTextAtomic } from './fsx.mjs';
4
- import { AWESOME_DESIGN_MD_REFERENCE, DESIGN_SYSTEM_SSOT, GETDESIGN_REFERENCE, PPT_CONDITIONAL_SKILL_ALLOWLIST, PPT_PIPELINE_MCP_ALLOWLIST, PPT_PIPELINE_SKILL_ALLOWLIST } from './routes.mjs';
4
+ import { AWESOME_DESIGN_MD_REFERENCE, CODEX_APP_IMAGE_GENERATION_DOC_URL, CODEX_IMAGEGEN_EVIDENCE_SOURCE, DESIGN_SYSTEM_SSOT, GETDESIGN_REFERENCE, PPT_CONDITIONAL_SKILL_ALLOWLIST, PPT_PIPELINE_MCP_ALLOWLIST, PPT_PIPELINE_SKILL_ALLOWLIST } from './routes.mjs';
5
5
 
6
6
  export const PPT_AUDIENCE_STRATEGY_ARTIFACT = 'ppt-audience-strategy.json';
7
7
  export const PPT_GATE_ARTIFACT = 'ppt-gate.json';
@@ -482,18 +482,30 @@ export function planPptImageAssets(contract = {}, storyboard = buildPptStoryboar
482
482
  ].filter(Boolean);
483
483
  return selected.slice(0, maxAssets).map(({ request, page }, index) => {
484
484
  const id = compactId('ppt-image', `${index + 1}:${request || page?.claim || page?.kind}`);
485
+ const prompt = buildImageAssetPrompt({ contract, page, request, styleTokens });
486
+ const relPath = path.join(PPT_ASSET_DIR, `${safeFileSlug(id)}.png`);
485
487
  return {
486
488
  id,
487
489
  slide: page?.number || index + 1,
488
490
  role: index === 0 ? 'hero_visual' : 'supporting_visual',
489
491
  status: 'planned',
490
- prompt: buildImageAssetPrompt({ contract, page, request, styleTokens }),
492
+ prompt,
491
493
  model: 'gpt-image-2',
492
494
  size: cleanText(contract.answers?.PRESENTATION_IMAGE_SIZE, '1536x1024'),
493
495
  quality: cleanText(contract.answers?.PRESENTATION_IMAGE_QUALITY, 'medium'),
494
496
  output_format: 'png',
495
- rel_path: path.join(PPT_ASSET_DIR, `${safeFileSlug(id)}.png`),
496
- html_src: `../${path.join(PPT_ASSET_DIR, `${safeFileSlug(id)}.png`)}`
497
+ rel_path: relPath,
498
+ html_src: `../${relPath}`,
499
+ imagegen_invocation: {
500
+ required_skill: 'imagegen',
501
+ command: '$imagegen',
502
+ surface: 'codex_app_builtin_image_generation',
503
+ evidence_source: CODEX_IMAGEGEN_EVIDENCE_SOURCE,
504
+ model: 'gpt-image-2',
505
+ tool_mode: 'built_in_image_gen',
506
+ prompt,
507
+ save_policy: `After generation, move or copy the selected output into ${relPath} and record output_path.`
508
+ }
497
509
  };
498
510
  });
499
511
  }
@@ -541,7 +553,16 @@ export async function buildPptImageAssetLedger(dir, contract = {}, storyboard =
541
553
  contract_hash: contract.sealed_hash || null,
542
554
  required,
543
555
  policy: 'Required PPT image resources must be generated through Codex App $imagegen/gpt-image-2 and recorded as real output files; direct API fallback, fabricated files, and placeholder ledgers do not satisfy this gate.',
544
- codex_app_imagegen_doc: 'https://developers.openai.com/codex/app/features#image-generation',
556
+ codex_app_imagegen_doc: CODEX_APP_IMAGE_GENERATION_DOC_URL,
557
+ imagegen_execution: {
558
+ required_skill: 'imagegen',
559
+ command: '$imagegen',
560
+ surface: 'codex_app_builtin_image_generation',
561
+ evidence_source: CODEX_IMAGEGEN_EVIDENCE_SOURCE,
562
+ model: 'gpt-image-2',
563
+ tool_mode: 'built_in_image_gen',
564
+ output_requirement: 'Generated raster files must be copied into the mission assets/ directory and referenced by output_path.'
565
+ },
545
566
  provider: {
546
567
  model: 'gpt-image-2',
547
568
  surface: 'codex_app_$imagegen',
@@ -559,7 +580,7 @@ export async function buildPptImageAssetLedger(dir, contract = {}, storyboard =
559
580
  required
560
581
  ? 'The sealed PPT contract requires generated image assets; missing Codex App $imagegen/gpt-image-2 output blocks the PPT gate.'
561
582
  : 'No generated image asset requirement was detected; assets remain optional and are not generated to avoid unrequested API cost.',
562
- 'Run Codex App $imagegen/gpt-image-2 for each blocked asset, place the generated raster under assets/, then rerun the PPT build so existing generated files are verified.'
583
+ 'Invoke the loaded imagegen skill with Codex App $imagegen/gpt-image-2 for each blocked asset, place the generated raster under assets/, then rerun the PPT build so existing generated files are verified.'
563
584
  ]
564
585
  };
565
586
  }
@@ -592,8 +613,12 @@ export function buildPptReviewPolicy(contract = {}, storyboard = buildPptStorybo
592
613
  },
593
614
  visual_review: {
594
615
  model: 'gpt-image-2',
616
+ required_skill: 'imagegen',
617
+ command: '$imagegen',
618
+ surface: 'codex_app_builtin_image_generation',
619
+ evidence_source: CODEX_IMAGEGEN_EVIDENCE_SOURCE,
595
620
  persona: '대한민국 TOSS UI/UX 시니어 총괄 디자이너',
596
- codex_app_imagegen_doc: 'https://developers.openai.com/codex/app/features#image-generation',
621
+ codex_app_imagegen_doc: CODEX_APP_IMAGE_GENERATION_DOC_URL,
597
622
  model_doc: 'https://developers.openai.com/api/docs/models/gpt-image-2',
598
623
  mode: explicitlyRequired ? 'required_by_contract' : 'codex_app_when_available',
599
624
  required_for_gate: explicitlyRequired,
@@ -685,7 +710,7 @@ export function buildPptReviewLedger({ contract = {}, storyboard, styleTokens, f
685
710
  title: 'Required gpt-image-2 visual review evidence missing',
686
711
  detail: 'The sealed PPT contract explicitly requested image/gpt-image-2 visual critique, but no Codex App imagegen review evidence was supplied.',
687
712
  source: 'codex_app_imagegen_gate',
688
- action: 'Run the bounded gpt-image-2 slide review loop in Codex App and record evidence paths before final output.'
713
+ action: 'Invoke the loaded imagegen skill through Codex App $imagegen/gpt-image-2, run the bounded slide review loop, and record evidence paths before final output.'
689
714
  }));
690
715
  }
691
716
  const blocking = issues.filter((issue) => ['P0', 'P1'].includes(issue.severity));
@@ -92,18 +92,14 @@ export const RECOMMENDED_DESIGN_REFERENCES = [GETDESIGN_REFERENCE, AWESOME_DESIG
92
92
 
93
93
  export const PPT_PIPELINE_SKILL_ALLOWLIST = Object.freeze([
94
94
  'ppt',
95
+ 'imagegen',
95
96
  'getdesign-reference',
96
97
  'prompt-pipeline',
97
98
  REFLECTION_SKILL_NAME,
98
99
  'honest-mode'
99
100
  ]);
100
101
 
101
- export const PPT_CONDITIONAL_SKILL_ALLOWLIST = Object.freeze([
102
- {
103
- skill: 'imagegen',
104
- condition: 'only_when_the_sealed_ppt_contract_explicitly_requires_generated_raster_assets'
105
- }
106
- ]);
102
+ export const PPT_CONDITIONAL_SKILL_ALLOWLIST = Object.freeze([]);
107
103
 
108
104
  export const PPT_PIPELINE_MCP_ALLOWLIST = Object.freeze([
109
105
  {
@@ -113,7 +109,10 @@ export const PPT_PIPELINE_MCP_ALLOWLIST = Object.freeze([
113
109
  ]);
114
110
 
115
111
  export function pptPipelineAllowlistPolicyText() {
116
- return `PPT pipeline allowlist: during $PPT design/render work, ignore installed skills and MCPs that are not explicitly part of the $PPT pipeline. The purpose is to prevent AI-like generic presentation design: decorative gradients, nested cards, vague SaaS visuals, and style choices not grounded in the audience, source material, getdesign reference, or the project design SSOT. Required skills are ${PPT_PIPELINE_SKILL_ALLOWLIST.join(', ')}. Do not use generic design skills such as design-artifact-expert, design-ui-editor, or design-system-builder for $PPT just because they are installed. $PPT design must use getdesign-reference plus the built-in PPT design implementation pipeline: ${DESIGN_SYSTEM_SSOT.authority_file} when present, ${DESIGN_SYSTEM_SSOT.builder_prompt} as the builder prompt when missing, and route-local ppt-style-tokens.json as the fused design projection. Conditional skills/MCPs are allowed only when their condition is sealed in the contract: ${PPT_CONDITIONAL_SKILL_ALLOWLIST.map((entry) => `${entry.skill}=${entry.condition}`).join('; ')}; ${PPT_PIPELINE_MCP_ALLOWLIST.map((entry) => `${entry.mcp}=${entry.condition}`).join('; ')}. Fact, image, and review evidence are first-class artifacts: gather user-provided context and required web/Context7 evidence into ppt-fact-ledger.json, block unsupported critical external claims, plan required image resources through ppt-image-asset-ledger.json, then run a bounded review loop recorded in ppt-review-policy.json, ppt-review-ledger.json, and ppt-iteration-report.json. Required raster asset or generated visual-review evidence must come from Codex App $imagegen/gpt-image-2; direct API fallback, placeholder files, and prose-only substitutes do not satisfy the route gate. The review loop caps full-deck passes at 2, slide retries at 2, requires P0/P1 issue count to be zero, targets score >= 0.88, and stops when improvement delta is below 0.03 or evidence is missing. For Codex App visual critique, use imagegen/gpt-image-2 (${CODEX_APP_IMAGE_GENERATION_DOC_URL}) when required or available; never simulate missing gpt-image-2 output. If required image-review evidence is unavailable, record the blocker instead of passing the gate. ${CODEX_IMAGEGEN_REQUIRED_POLICY}`;
112
+ const conditionalSkills = PPT_CONDITIONAL_SKILL_ALLOWLIST.length
113
+ ? PPT_CONDITIONAL_SKILL_ALLOWLIST.map((entry) => `${entry.skill}=${entry.condition}`).join('; ')
114
+ : 'none';
115
+ return `PPT pipeline allowlist: during $PPT design/render work, ignore installed skills and MCPs that are not explicitly part of the $PPT pipeline. The purpose is to prevent AI-like generic presentation design: decorative gradients, nested cards, vague SaaS visuals, and style choices not grounded in the audience, source material, getdesign reference, or the project design SSOT. Required skills are ${PPT_PIPELINE_SKILL_ALLOWLIST.join(', ')}. The imagegen skill is required for $PPT so Codex App can invoke official built-in $imagegen/gpt-image-2 for every generated raster asset or generated visual-review image; do not route PPT imagery through direct API fallback. Do not use generic design skills such as design-artifact-expert, design-ui-editor, or design-system-builder for $PPT just because they are installed. $PPT design must use getdesign-reference plus the built-in PPT design implementation pipeline: ${DESIGN_SYSTEM_SSOT.authority_file} when present, ${DESIGN_SYSTEM_SSOT.builder_prompt} as the builder prompt when missing, and route-local ppt-style-tokens.json as the fused design projection. Conditional skills/MCPs are allowed only when their condition is sealed in the contract: ${conditionalSkills}; ${PPT_PIPELINE_MCP_ALLOWLIST.map((entry) => `${entry.mcp}=${entry.condition}`).join('; ')}. Fact, image, and review evidence are first-class artifacts: gather user-provided context and required web/Context7 evidence into ppt-fact-ledger.json, block unsupported critical claims, plan required image resources through ppt-image-asset-ledger.json, then run a bounded review loop recorded in ppt-review-policy.json, ppt-review-ledger.json, and ppt-iteration-report.json. Required raster asset or generated visual-review evidence must come from Codex App $imagegen/gpt-image-2; direct API fallback, placeholder files, and prose-only substitutes do not satisfy the route gate. The review loop caps full-deck passes at 2, slide retries at 2, requires P0/P1 issue count to be zero, targets score >= 0.88, and stops when improvement delta is below 0.03 or evidence is missing. For Codex App visual critique, invoke $imagegen/gpt-image-2 (${CODEX_APP_IMAGE_GENERATION_DOC_URL}) when required; never simulate missing gpt-image-2 output. If required image-review evidence is unavailable, record the blocker instead of passing the gate. ${CODEX_IMAGEGEN_REQUIRED_POLICY}`;
117
116
  }
118
117
 
119
118
  export function getdesignReferencePolicyText() {
@@ -523,8 +522,8 @@ export const COMMAND_CATALOG = [
523
522
  { name: 'root', usage: 'sks root [--json]', description: 'Show whether SKS is using a project root or the per-user global SKS runtime root.' },
524
523
  { 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
524
  { 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.' },
525
+ { 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.' },
526
+ { 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
527
  { 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
528
  { 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
529
  { name: 'mad', usage: 'sks --mad [--high]', description: 'Open a one-shot tmux Codex CLI workspace with the SKS MAD full-access auto-review profile.' },