sneakoscope 6.0.2 → 6.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +3 -1
  2. package/crates/sks-core/Cargo.lock +1 -1
  3. package/crates/sks-core/Cargo.toml +1 -1
  4. package/dist/bin/fast-inline.js +5 -0
  5. package/dist/bin/sks-dispatch.js +2 -0
  6. package/dist/cli/install-helpers.js +41 -6
  7. package/dist/commands/codex-app.js +8 -3
  8. package/dist/commands/codex-lb.js +37 -12
  9. package/dist/config/skills-manifest.json +57 -57
  10. package/dist/core/agents/agent-effort-policy.js +46 -33
  11. package/dist/core/agents/agent-orchestrator.js +1 -1
  12. package/dist/core/agents/agent-proof-evidence.js +16 -2
  13. package/dist/core/agents/agent-roster.js +2 -2
  14. package/dist/core/agents/native-cli-session-swarm.js +18 -1
  15. package/dist/core/agents/native-worker-backend-router.js +179 -108
  16. package/dist/core/agents/parallel-write-fixture.js +15 -10
  17. package/dist/core/agents/runtime-proof-summary.js +21 -3
  18. package/dist/core/codex-app/codex-app-restart.js +38 -8
  19. package/dist/core/codex-app/sks-menubar.js +92 -37
  20. package/dist/core/codex-app.js +26 -4
  21. package/dist/core/codex-control/codex-model-metadata.js +8 -4
  22. package/dist/core/codex-lb/codex-lb-env.js +78 -0
  23. package/dist/core/commands/basic-cli.js +15 -0
  24. package/dist/core/commands/naruto-command.js +37 -9
  25. package/dist/core/naruto/naruto-write-e2e.js +3 -4
  26. package/dist/core/provider/model-router.js +64 -3
  27. package/dist/core/release/package-size-budget.js +2 -0
  28. package/dist/core/routes/constants.js +1 -1
  29. package/dist/core/routes/model-mode-router.js +3 -0
  30. package/dist/core/routes.js +7 -9
  31. package/dist/core/update/update-migration-state.js +1 -1
  32. package/dist/core/update/update-notice.js +23 -0
  33. package/dist/core/update-check.js +46 -10
  34. package/dist/core/version.js +1 -1
  35. package/dist/scripts/codex-app-provider-model-ui-check.js +17 -2
  36. package/dist/scripts/doctor-fix-proves-codex-read-check.js +1 -1
  37. package/dist/scripts/gate-policy-audit-check.js +2 -2
  38. package/dist/scripts/legacy-upgrade-matrix-check.js +1 -1
  39. package/dist/scripts/lib/native-cli-session-swarm-check-lib.js +2 -1
  40. package/dist/scripts/naruto-shadow-clone-swarm-check.js +88 -50
  41. package/dist/scripts/npm-publish-performance-check.js +2 -1
  42. package/dist/scripts/packlist-performance-check.js +5 -2
  43. package/dist/scripts/release-dag-full-coverage-check.js +7 -7
  44. package/dist/scripts/release-gate-existence-audit.js +8 -9
  45. package/dist/scripts/scheduler-batch-dispatch-check.js +1 -1
  46. package/dist/scripts/sizecheck.js +7 -2
  47. package/dist/scripts/sks-menubar-install-check.js +7 -0
  48. package/dist/scripts/update-default-command-check.js +4 -2
  49. package/package.json +3 -16
@@ -444,7 +444,7 @@ async function resolveCodexBundleId(input) {
444
444
  }
445
445
  export async function smokeSksMenuBarAction(actionScriptPath) {
446
446
  if (!(await exists(actionScriptPath)))
447
- return { ok: false, code: null, output: null, versionDetected: false, executable: false };
447
+ return { ok: false, code: null, output: null, versionDetected: false, detectedVersion: null, executable: false };
448
448
  // The Swift app runs the script directly (which requires the executable bit), so the smoke
449
449
  // check must invoke it the same way. Running it via `/bin/zsh <script>` — as this check used
450
450
  // to — succeeds even when +x is missing, which let doctor/status report a healthy action
@@ -456,6 +456,7 @@ export async function smokeSksMenuBarAction(actionScriptPath) {
456
456
  code: null,
457
457
  output: 'action script is not executable (missing +x); the menu bar app cannot run it',
458
458
  versionDetected: false,
459
+ detectedVersion: null,
459
460
  executable: false
460
461
  };
461
462
  }
@@ -464,12 +465,14 @@ export async function smokeSksMenuBarAction(actionScriptPath) {
464
465
  maxOutputBytes: 16 * 1024
465
466
  }).catch((err) => ({ code: 1, stdout: '', stderr: err?.message || String(err) }));
466
467
  const output = String(`${result.stdout || ''}\n${result.stderr || ''}`).trim();
467
- const versionDetected = /\b(?:sks|sneakoscope)?\s*v?\d+\.\d+\.\d+\b/i.test(output);
468
+ const detectedVersion = output.match(/\b(?:sks|sneakoscope)?\s*v?(\d+\.\d+\.\d+)\b/i)?.[1] || null;
469
+ const versionDetected = Boolean(detectedVersion);
468
470
  return {
469
471
  ok: result.code === 0 && versionDetected,
470
472
  code: result.code,
471
473
  output: output ? output.slice(0, 700) : null,
472
474
  versionDetected,
475
+ detectedVersion,
473
476
  executable: true
474
477
  };
475
478
  }
@@ -505,6 +508,10 @@ export async function inspectSksMenuBarStatus(opts = {}) {
505
508
  warning: config.codex_bundle_id ? null : 'codex_sync_disabled'
506
509
  };
507
510
  const buildStamp = await readJson(paths.build_stamp_path, null);
511
+ const actionIntegrity = evaluateActionScriptIntegrity(actionText, buildStamp);
512
+ const expectedActionVersion = buildStamp?.package_version || PACKAGE_VERSION;
513
+ const actionVersionMatchesExpected = actionSmoke.detectedVersion === expectedActionVersion
514
+ && actionSmoke.detectedVersion === PACKAGE_VERSION;
508
515
  const launchd = await inspectLaunchdService(opts.env);
509
516
  const signature = await inspectSignature(paths.app_path, opts.env);
510
517
  const blockers = [];
@@ -517,6 +524,12 @@ export async function inspectSksMenuBarStatus(opts = {}) {
517
524
  blockers.push('action_script_not_executable');
518
525
  if (installed && !actionSmoke.ok)
519
526
  blockers.push('action_script_smoke_failed');
527
+ if (installed && actionSmoke.ok && !actionVersionMatchesExpected)
528
+ blockers.push('action_target_version_mismatch');
529
+ if (installed && !buildStamp)
530
+ blockers.push('build_stamp_missing');
531
+ if (installed && buildStamp && !actionIntegrity.script_hash_matches_stamp)
532
+ blockers.push('action_script_hash_mismatch');
520
533
  if (installed && signature.checked && !signature.ok)
521
534
  warnings.push('codesign_identifier_unexpected');
522
535
  if (!codexSync.ok)
@@ -539,8 +552,12 @@ export async function inspectSksMenuBarStatus(opts = {}) {
539
552
  smoke_code: actionSmoke.code,
540
553
  smoke_output: actionSmoke.output,
541
554
  version_detected: actionSmoke.versionDetected,
555
+ detected_version: actionSmoke.detectedVersion,
556
+ expected_version: expectedActionVersion,
557
+ version_matches_expected: actionVersionMatchesExpected,
558
+ ...actionIntegrity,
542
559
  executable: actionSmoke.executable,
543
- ok: actionSmoke.ok
560
+ ok: actionSmoke.ok && actionVersionMatchesExpected && actionIntegrity.script_hash_matches_stamp
544
561
  },
545
562
  codex_sync: codexSync,
546
563
  build_stamp: buildStamp,
@@ -551,6 +568,15 @@ export async function inspectSksMenuBarStatus(opts = {}) {
551
568
  next_actions: blockers.length || warnings.length ? defaultNextActions() : ['sks menubar status --json']
552
569
  };
553
570
  }
571
+ export function evaluateActionScriptIntegrity(actionText, buildStamp) {
572
+ const scriptSha256 = actionText ? sha256(actionText) : null;
573
+ const expectedScriptSha256 = buildStamp?.action_script_sha256 || null;
574
+ return {
575
+ script_sha256: scriptSha256,
576
+ expected_script_sha256: expectedScriptSha256,
577
+ script_hash_matches_stamp: Boolean(scriptSha256 && expectedScriptSha256 && scriptSha256 === expectedScriptSha256)
578
+ };
579
+ }
554
580
  export async function restartSksMenuBar(opts = {}) {
555
581
  const env = opts.env || process.env;
556
582
  const paths = sksMenuBarPaths(opts.home || env.HOME, opts.root);
@@ -757,6 +783,14 @@ run_node_entry() {
757
783
  exec "$node_bin" "$entry" "$@"
758
784
  }
759
785
 
786
+ # The installer pins the entry that produced this menu build. Prefer it over
787
+ # PATH/global copies so a newly updated 6.0.3 companion cannot execute stale
788
+ # 6.0.2 auth/restart logic. Fall back only when the pinned file or Node runtime
789
+ # is unavailable.
790
+ if [ -f "$SKS_ENTRY" ]; then
791
+ run_node_entry "$SKS_ENTRY" "$@" || true
792
+ fi
793
+
760
794
  SKS_BIN="$(/bin/zsh -lc 'command -v sks' 2>/dev/null | /usr/bin/head -n 1 || true)"
761
795
  if [ -n "$SKS_BIN" ] && [ -x "$SKS_BIN" ]; then
762
796
  exec "$SKS_BIN" "$@"
@@ -772,7 +806,6 @@ for entry in "$HOME"/.nvm/versions/node/*/lib/node_modules/sneakoscope/dist/bin/
772
806
  run_node_entry "$entry" "$@" || true
773
807
  fi
774
808
  done
775
- run_node_entry "$SKS_ENTRY" "$@" || true
776
809
  notify_sks_missing
777
810
  echo "SKS command not found. Run npm install -g sneakoscope or sks doctor --fix, then try again." >&2
778
811
  exit 127
@@ -1176,9 +1209,10 @@ ${codexLifecycleSource}
1176
1209
  let cachePath = NSString(string: "~/.sneakoscope/cache/update-notice.json").expandingTildeInPath
1177
1210
  guard let data = FileManager.default.contents(atPath: cachePath) else { return false }
1178
1211
  guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { return false }
1179
- if let available = json["update_available"] as? Bool { return available }
1180
- guard let latest = json["latest_version"] as? String else { return false }
1181
- return latest != packageVersion && !latest.isEmpty
1212
+ if let latest = json["latest_version"] as? String, !latest.isEmpty {
1213
+ return latest.compare(packageVersion, options: .numeric) == .orderedDescending
1214
+ }
1215
+ return json["update_available"] as? Bool == true
1182
1216
  }
1183
1217
 
1184
1218
  func runSksCapture(_ args: [String], title: String, stdinText: String? = nil, notify: Bool = true, completion: ((Int32, String) -> Void)? = nil) {
@@ -1238,12 +1272,18 @@ ${codexLifecycleSource}
1238
1272
  runSksSilent(["codex-lb", "status", "--json"]) { [weak self] code, output in
1239
1273
  guard let self = self else { return }
1240
1274
  let json = parseJsonObject(output)
1241
- let selected = json?["selected"] as? Bool == true
1242
- let provider = (json?["model_provider"] as? String)?.lowercased() == "codex-lb"
1243
- let mode = (json?["mode"] as? String)?.lowercased() == "codex-lb"
1244
- let codexLbActive = code == 0 && (selected || provider || mode)
1275
+ guard code == 0, let json = json else {
1276
+ self.codexLbItem.state = .off
1277
+ self.oauthItem.state = .off
1278
+ return
1279
+ }
1280
+ let selected = json["selected"] as? Bool == true
1281
+ let providerContract = json["provider_contract_ok"] as? Bool == true
1282
+ let authMode = (json["auth_mode"] as? String)?.lowercased() ?? ""
1283
+ let codexLbActive = selected && providerContract && authMode == "apikey"
1284
+ let oauthActive = !selected && (authMode.contains("chatgpt") || authMode.contains("oauth") || authMode.contains("browser"))
1245
1285
  self.codexLbItem.state = codexLbActive ? .on : .off
1246
- self.oauthItem.state = codexLbActive ? .off : .on
1286
+ self.oauthItem.state = oauthActive ? .on : .off
1247
1287
  }
1248
1288
  }
1249
1289
 
@@ -1251,8 +1291,13 @@ ${codexLifecycleSource}
1251
1291
  runSksSilent(["fast-mode", "status", "--json"]) { [weak self] code, output in
1252
1292
  guard let self = self else { return }
1253
1293
  let json = parseJsonObject(output)
1254
- let fastMode = json?["fast_mode"] as? Bool == true
1255
- let serviceTier = (json?["service_tier"] as? String)?.lowercased()
1294
+ guard code == 0, let json = json else {
1295
+ self.fastModeOnItem.state = .off
1296
+ self.fastModeOffItem.state = .off
1297
+ return
1298
+ }
1299
+ let fastMode = json["fast_mode"] as? Bool == true
1300
+ let serviceTier = (json["service_tier"] as? String)?.lowercased()
1256
1301
  let fastActive = code == 0 && (fastMode || serviceTier == "fast" || serviceTier == "priority")
1257
1302
  self.fastModeOnItem.state = fastActive ? .on : .off
1258
1303
  self.fastModeOffItem.state = fastActive ? .off : .on
@@ -1260,13 +1305,13 @@ ${codexLifecycleSource}
1260
1305
  }
1261
1306
 
1262
1307
  @objc func useCodexLb() {
1263
- runSksBackground(["codex-lb", "use-codex-lb", "--json"], title: "Use codex-lb") { [weak self] code, _ in
1308
+ runSksBackground(["codex-lb", "use-codex-lb", "--restart-app", "--json"], title: "Use codex-lb") { [weak self] code, _ in
1264
1309
  if code == 0 { self?.updateAuthModeChecks() }
1265
1310
  }
1266
1311
  }
1267
1312
 
1268
1313
  @objc func useChatGptOAuth() {
1269
- runSksBackground(["codex-lb", "use-oauth", "--json"], title: "Use ChatGPT OAuth") { [weak self] code, _ in
1314
+ runSksBackground(["codex-lb", "use-oauth", "--restart-app", "--json"], title: "Use ChatGPT OAuth") { [weak self] code, _ in
1270
1315
  if code == 0 { self?.updateAuthModeChecks() }
1271
1316
  }
1272
1317
  }
@@ -1274,14 +1319,14 @@ ${codexLifecycleSource}
1274
1319
  @objc func setCodexLbDomainAndKey() {
1275
1320
  guard let domain = promptText(title: "Set codex-lb Domain", message: "Enter just your codex-lb domain or base URL — the /backend-api/codex path is added automatically.", placeholder: "lb.example.com") else { return }
1276
1321
  guard let key = promptText(title: "Set codex-lb Key", message: "Enter your codex-lb API key.", placeholder: "sk-clb-...", secure: true) else { return }
1277
- runSksBackground(["codex-lb", "setup", "--host", domain, "--api-key-stdin", "--yes", "--json"], title: "Set codex-lb", stdinText: key + "\\n") { [weak self] code, _ in
1322
+ runSksBackground(["codex-lb", "setup", "--host", domain, "--api-key-stdin", "--yes", "--restart-app", "--json"], title: "Set codex-lb", stdinText: key + "\\n") { [weak self] code, _ in
1278
1323
  if code == 0 { self?.updateAuthModeChecks() }
1279
1324
  }
1280
1325
  }
1281
1326
 
1282
1327
  @objc func setOpenRouterKey() {
1283
1328
  guard let key = promptText(title: "Set OpenRouter Key", message: "Enter your OpenRouter API key.", placeholder: "sk-or-v1-...", secure: true) else { return }
1284
- runSksBackground(["codex-app", "set-openrouter-key", "--api-key-stdin", "--json"], title: "Set OpenRouter Key", stdinText: key + "\\n")
1329
+ runSksBackground(["codex-app", "set-openrouter-key", "--api-key-stdin", "--restart-app", "--json"], title: "Set OpenRouter Key", stdinText: key + "\\n")
1285
1330
  }
1286
1331
 
1287
1332
  @objc func fastModeOn() {
@@ -1312,36 +1357,46 @@ ${codexLifecycleSource}
1312
1357
  let urlString = "http://127.0.0.1:4477"
1313
1358
  runProcess("/usr/bin/curl", ["-fsS", "--max-time", "1", urlString]) { [weak self] code, _ in
1314
1359
  DispatchQueue.main.async {
1315
- if code != 0 {
1360
+ if code == 0 {
1361
+ if let url = URL(string: urlString) { NSWorkspace.shared.open(url) }
1362
+ } else {
1316
1363
  self?.startSksDetached(["ui"], title: "SKS Dashboard")
1364
+ self?.waitForDashboard(urlString, attempts: 20)
1365
+ }
1366
+ }
1367
+ }
1368
+ }
1369
+
1370
+ func waitForDashboard(_ urlString: String, attempts: Int) {
1371
+ runProcess("/usr/bin/curl", ["-fsS", "--max-time", "1", urlString]) { [weak self] code, _ in
1372
+ if code == 0 {
1373
+ DispatchQueue.main.async {
1374
+ if let url = URL(string: urlString) { NSWorkspace.shared.open(url) }
1375
+ }
1376
+ } else if attempts > 1 {
1377
+ DispatchQueue.global().asyncAfter(deadline: .now() + 0.25) {
1378
+ self?.waitForDashboard(urlString, attempts: attempts - 1)
1317
1379
  }
1318
- if let url = URL(string: urlString) {
1319
- NSWorkspace.shared.open(url)
1380
+ } else {
1381
+ DispatchQueue.main.async {
1382
+ showAlert("SKS Dashboard failed", informative: "Dashboard did not become ready at \(urlString). See View Last Log.")
1320
1383
  }
1321
1384
  }
1322
1385
  }
1323
1386
  }
1324
1387
 
1325
1388
  @objc func openCodexSettings() {
1326
- runProcess("/usr/bin/open", ["codex://settings"])
1389
+ runProcess("/usr/bin/open", ["codex://settings"]) { code, output in
1390
+ if code != 0 {
1391
+ DispatchQueue.main.async {
1392
+ showAlert("Open Codex Settings failed", informative: humanizeSksFailure(redactSecrets(output)))
1393
+ }
1394
+ }
1395
+ }
1327
1396
  }
1328
1397
 
1329
1398
  @objc func restartCodex() {
1330
- let running = NSWorkspace.shared.runningApplications.filter { app in
1331
- if let bundle = codexBundleId, app.bundleIdentifier == bundle { return true }
1332
- return app.localizedName == "Codex"
1333
- }
1334
- for app in running {
1335
- app.terminate()
1336
- }
1337
- DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
1338
- if let bundle = codexBundleId, let url = NSWorkspace.shared.urlForApplication(withBundleIdentifier: bundle) {
1339
- NSWorkspace.shared.open(url)
1340
- showNotification("Restart Codex", "requested")
1341
- } else {
1342
- showAlert("Restart Codex failed", informative: "Codex app bundle could not be resolved.")
1343
- }
1344
- }
1399
+ runSksBackground(["codex-app", "restart", "--json"], title: "Restart Codex")
1345
1400
  }
1346
1401
 
1347
1402
  @objc func viewLastLog() {
@@ -9,7 +9,7 @@ import { PRODUCT_DESIGN_AUTO_INSTALL_ENV, ensureProductDesignPluginInstalled, pr
9
9
  import { GLM_CODEX_CONFIG_PROVIDER_ID, GLM_CODEX_CONFIG_REASONING_PROFILES } from './codex-app/glm-model-profile.js';
10
10
  import { GLM_52_OPENROUTER_MODEL } from './providers/glm/glm-52-settings.js';
11
11
  import { resolveOpenRouterApiKey } from './providers/openrouter/openrouter-secret-store.js';
12
- import { codexLbEnvPath, parseShellEnvValue } from './codex-lb/codex-lb-env.js';
12
+ import { codexLbEnvPath, loadCodexLbEnv, parseShellEnvValue, readCodexLbModelCatalog } from './codex-lb/codex-lb-env.js';
13
13
  export const CODEX_APP_DOCS_URL = 'https://developers.openai.com/codex/app/features';
14
14
  export const CODEX_CHANGELOG_URL = 'https://developers.openai.com/codex/changelog';
15
15
  export const CODEX_ACCESS_TOKENS_DOCS_URL = 'https://developers.openai.com/codex/enterprise/access-tokens';
@@ -35,6 +35,8 @@ export function codexAppCandidatePaths(home = os.homedir(), env = process.env) {
35
35
  if (env.SKS_CODEX_APP_PATH)
36
36
  candidates.push(env.SKS_CODEX_APP_PATH);
37
37
  if (process.platform === 'darwin') {
38
+ candidates.push('/Applications/ChatGPT.app');
39
+ candidates.push(path.join(home || '', 'Applications', 'ChatGPT.app'));
38
40
  candidates.push('/Applications/Codex.app');
39
41
  candidates.push(path.join(home || '', 'Applications', 'Codex.app'));
40
42
  }
@@ -545,7 +547,8 @@ export function codexAppGuidance({ appInstalled, codex, mcpList, featureList, re
545
547
  lines.push(`Run: ${providerModelUi.glm.key_command}; then ${providerModelUi.glm.install_command}; restart Codex App if the model picker was already open.`);
546
548
  }
547
549
  if (providerModelUi?.codex_lb?.key_entry_visible && providerModelUi.codex_lb.key_present && providerModelUi.codex_lb.provider_present && providerModelUi.codex_lb.provider_contract_ok && providerModelUi.codex_lb.base_url_present) {
548
- lines.push(`codex-lb key entry path is visible and configured; update command ${providerModelUi.codex_lb.set_key_command}.`);
550
+ lines.push(`codex-lb key entry path is configured; provider catalog ${providerModelUi.codex_lb.expected_models_present ? 'contains GPT-5.6 Sol/Terra/Luna' : 'is not verified'}. Update command: ${providerModelUi.codex_lb.set_key_command}.`);
551
+ lines.push('Provider/config readiness does not by itself prove the already-running Desktop picker; auth/profile actions restart ChatGPT/Codex so it reloads model/list.');
549
552
  }
550
553
  else if (providerModelUi?.codex_lb?.key_entry_visible) {
551
554
  lines.push(`codex-lb key entry path is visible, but setup is incomplete: ${providerModelUi.codex_lb.blockers.join(', ') || 'setup required'}.`);
@@ -938,6 +941,14 @@ export async function codexProviderModelUiStatus(opts = {}) {
938
941
  && hasTomlBoolean(codexLbProvider, 'supports_websockets', true)
939
942
  && hasTomlBoolean(codexLbProvider, 'requires_openai_auth', true);
940
943
  const codexLbSelectedDefault = /(?:^|\n)\s*model_provider\s*=\s*"codex-lb"\s*(?:#.*)?(?=\n|$)/.test(topLevelToml(globalConfig));
944
+ let codexLbModelCatalog = opts.codexLbModelCatalog || null;
945
+ if (!codexLbModelCatalog && codexLbProviderContractOk && codexLbApiKeySource !== 'missing' && codexLbBaseUrlSource !== 'missing') {
946
+ const loadedEnv = await loadCodexLbEnv({ home, envPath: codexLbEnvFilePath }).catch(() => null);
947
+ codexLbModelCatalog = loadedEnv ? await readCodexLbModelCatalog({ loadedEnv }).catch(() => null) : null;
948
+ }
949
+ const expectedCodexLbModels = ['gpt-5.6-luna', 'gpt-5.6-terra', 'gpt-5.6-sol'];
950
+ const catalogModels = Array.isArray(codexLbModelCatalog?.models) ? codexLbModelCatalog.models.map(String) : [];
951
+ const expectedCodexLbModelsPresent = expectedCodexLbModels.every((model) => catalogModels.includes(model));
941
952
  const glmBlockers = [
942
953
  ...(glmProviderPresent ? [] : ['glm_openrouter_provider_missing']),
943
954
  ...glmProfileBlockers,
@@ -947,7 +958,10 @@ export async function codexProviderModelUiStatus(opts = {}) {
947
958
  ...(codexLbProviderPresent ? [] : ['codex_lb_provider_missing']),
948
959
  ...(codexLbProviderPresent && !codexLbProviderContractOk ? ['codex_lb_provider_contract_drift'] : []),
949
960
  ...(codexLbApiKeySource !== 'missing' ? [] : ['codex_lb_api_key_missing']),
950
- ...(codexLbBaseUrlSource !== 'missing' ? [] : ['codex_lb_base_url_missing'])
961
+ ...(codexLbBaseUrlSource !== 'missing' ? [] : ['codex_lb_base_url_missing']),
962
+ ...(codexLbProviderContractOk && codexLbApiKeySource !== 'missing' && codexLbBaseUrlSource !== 'missing' && !expectedCodexLbModelsPresent
963
+ ? ['codex_lb_gpt_5_6_catalog_unverified']
964
+ : [])
951
965
  ];
952
966
  const blockers = [...glmBlockers, ...codexLbBlockers];
953
967
  const keyCommand = 'sks codex-app set-openrouter-key --api-key-stdin';
@@ -958,6 +972,8 @@ export async function codexProviderModelUiStatus(opts = {}) {
958
972
  schema: 'sks.codex-app-provider-model-ui.v1',
959
973
  ok: blockers.length === 0,
960
974
  checked: true,
975
+ verification_scope: 'config_and_provider_catalog',
976
+ desktop_picker_verified: false,
961
977
  status: blockers.length === 0 ? 'ready' : 'setup_required',
962
978
  config_paths: {
963
979
  codex_config: globalConfigPath,
@@ -988,6 +1004,12 @@ export async function codexProviderModelUiStatus(opts = {}) {
988
1004
  api_key_source: codexLbApiKeySource,
989
1005
  base_url_present: codexLbBaseUrlSource !== 'missing',
990
1006
  base_url_source: codexLbBaseUrlSource,
1007
+ model_catalog_checked: Boolean(codexLbModelCatalog),
1008
+ model_catalog_ok: codexLbModelCatalog?.ok === true,
1009
+ model_catalog_models: catalogModels,
1010
+ expected_models: expectedCodexLbModels,
1011
+ expected_models_present: expectedCodexLbModelsPresent,
1012
+ model_catalog_blockers: codexLbModelCatalog?.blockers || [],
991
1013
  setup_command: setupCommand,
992
1014
  set_key_command: setKeyCommand,
993
1015
  status_command: 'sks codex-lb status',
@@ -1066,7 +1088,7 @@ function providerModelUiSummary(status = {}) {
1066
1088
  if (!status?.checked)
1067
1089
  return ' not checked';
1068
1090
  if (status.ok)
1069
- return ' ok (GLM + codex-lb)';
1091
+ return ' config/catalog ready (live picker reload required)';
1070
1092
  return ` setup ${(status.blockers || []).join(', ') || 'required'}`;
1071
1093
  }
1072
1094
  function sksNativeMenuSummary(status = {}) {
@@ -57,19 +57,23 @@ async function readCodexCliMetadata(model) {
57
57
  return null;
58
58
  }
59
59
  function normalizePayload(payload, fallbackModel, source) {
60
- const catalog = Array.isArray(payload?.models) ? payload.models : null;
60
+ const catalog = Array.isArray(payload?.data) ? payload.data : Array.isArray(payload?.models) ? payload.models : null;
61
61
  const reportedSelection = String(payload?.selected_model || payload?.current_model || payload?.active_model || '').trim();
62
62
  const requestedModel = String(fallbackModel || reportedSelection).trim();
63
63
  const row = catalog
64
64
  ? requestedModel
65
- ? catalog.find((candidate) => String(candidate?.id || candidate?.model || candidate?.name || '') === requestedModel) || null
65
+ ? catalog.find((candidate) => String(candidate?.id || candidate?.model || candidate?.slug || candidate?.name || '') === requestedModel) || null
66
66
  : null
67
67
  : payload?.model_metadata || payload?.metadata || payload;
68
68
  if (!row) {
69
69
  return metadata(requestedModel, [], 'medium', source, [requestedModel ? 'codex_model_not_found_in_advertised_catalog' : 'codex_model_selection_unknown']);
70
70
  }
71
- const efforts = normalizeAdvertisedEfforts(row?.advertised_efforts || row?.advertisedEfforts || row?.reasoning_efforts || row?.reasoningEfforts || payload?.advertised_efforts);
72
- return metadata(String(row?.model || row?.id || row?.name || requestedModel), efforts, row?.default_effort || row?.defaultEffort || payload?.default_effort || 'medium', source, efforts.length ? [] : ['codex_model_metadata_efforts_missing']);
71
+ const effortRows = row?.supportedReasoningEfforts || row?.supported_reasoning_levels || row?.supported_reasoning_efforts || [];
72
+ const structuredEfforts = Array.isArray(effortRows)
73
+ ? effortRows.map((entry) => entry?.reasoningEffort || entry?.effort || entry)
74
+ : [];
75
+ const efforts = normalizeAdvertisedEfforts(structuredEfforts.length ? structuredEfforts : row?.advertised_efforts || row?.advertisedEfforts || row?.reasoning_efforts || row?.reasoningEfforts || payload?.advertised_efforts);
76
+ return metadata(String(row?.model || row?.id || row?.slug || row?.name || requestedModel), efforts, row?.default_reasoning_level || row?.defaultReasoningLevel || row?.default_effort || row?.defaultEffort || payload?.default_effort || 'medium', source, efforts.length ? [] : ['codex_model_metadata_efforts_missing']);
73
77
  }
74
78
  function metadata(model, efforts, defaultEffort, source, blockers) {
75
79
  const advertised = normalizeAdvertisedEfforts(efforts);
@@ -29,6 +29,84 @@ export async function readLbHealth(home = process.env.HOME || os.homedir()) {
29
29
  updated_at: typeof raw.updated_at === 'string' ? raw.updated_at : null
30
30
  };
31
31
  }
32
+ export async function readCodexLbModelCatalog(opts = {}) {
33
+ const loaded = opts.loadedEnv || await loadCodexLbEnv();
34
+ if (!loaded.configured || !loaded.base_url || !loaded.secret_api_key) {
35
+ return {
36
+ schema: 'sks.codex-lb-model-catalog.v1',
37
+ ok: false,
38
+ status: 'blocked',
39
+ models: [],
40
+ model_efforts: {},
41
+ http_status: null,
42
+ blockers: loaded.missing.length ? loaded.missing.map((item) => `codex_lb_missing:${item}`) : ['codex_lb_not_configured']
43
+ };
44
+ }
45
+ const fetchImpl = opts.fetchImpl || fetch;
46
+ try {
47
+ const response = await fetchImpl(`${loaded.base_url}/models`, {
48
+ headers: { Authorization: `Bearer ${loaded.secret_api_key}` },
49
+ signal: AbortSignal.timeout(Math.max(250, Number(opts.timeoutMs || 5000)))
50
+ });
51
+ const payload = await response.json().catch(() => null);
52
+ const models = normalizeCodexLbModelCatalogPayload(payload);
53
+ const apiEfforts = normalizeCodexModelEffortCatalogPayload(payload);
54
+ const cachePath = path.join(String(process.env.CODEX_HOME || path.join(os.homedir(), '.codex')), 'models_cache.json');
55
+ const cachePayload = await readJson(cachePath, null).catch(() => null);
56
+ const cachedEfforts = normalizeCodexModelEffortCatalogPayload(cachePayload);
57
+ const modelEfforts = Object.fromEntries(models.map((model) => [model, apiEfforts[model] || cachedEfforts[model] || []]));
58
+ const ok = response.ok && models.length > 0;
59
+ return {
60
+ schema: 'sks.codex-lb-model-catalog.v1',
61
+ ok,
62
+ status: ok ? 'ready' : 'blocked',
63
+ models,
64
+ model_efforts: modelEfforts,
65
+ http_status: response.status,
66
+ blockers: [
67
+ ...(response.ok ? [] : [`codex_lb_models_http_${response.status}`]),
68
+ ...(models.length ? [] : ['codex_lb_model_catalog_empty'])
69
+ ]
70
+ };
71
+ }
72
+ catch {
73
+ return {
74
+ schema: 'sks.codex-lb-model-catalog.v1',
75
+ ok: false,
76
+ status: 'blocked',
77
+ models: [],
78
+ model_efforts: {},
79
+ http_status: null,
80
+ blockers: ['codex_lb_model_catalog_unavailable']
81
+ };
82
+ }
83
+ }
84
+ export function normalizeCodexModelEffortCatalogPayload(payload) {
85
+ const rows = Array.isArray(payload?.data) ? payload.data : Array.isArray(payload?.models) ? payload.models : [];
86
+ const result = {};
87
+ for (const row of rows) {
88
+ const model = String(row?.id || row?.model || row?.slug || row?.name || '').trim();
89
+ if (!model)
90
+ continue;
91
+ const effortRows = row?.supportedReasoningEfforts
92
+ || row?.supported_reasoning_levels
93
+ || row?.supported_reasoning_efforts
94
+ || row?.reasoning_efforts
95
+ || [];
96
+ const efforts = (Array.isArray(effortRows) ? effortRows : [])
97
+ .map((entry) => String(entry?.reasoningEffort || entry?.effort || entry || '').trim().toLowerCase())
98
+ .filter(Boolean);
99
+ result[model] = [...new Set(efforts)];
100
+ }
101
+ return result;
102
+ }
103
+ export function normalizeCodexLbModelCatalogPayload(payload) {
104
+ const rows = Array.isArray(payload?.data) ? payload.data : Array.isArray(payload?.models) ? payload.models : [];
105
+ const models = rows
106
+ .map((row) => String(row?.id || row?.model || row?.slug || row?.name || '').trim())
107
+ .filter(Boolean);
108
+ return [...new Set(models)];
109
+ }
32
110
  export function normalizeCodexLbBaseUrl(input = '') {
33
111
  let host = String(input || '').trim();
34
112
  if (!host)
@@ -15,6 +15,7 @@ import { hooksExplainReport } from '../../cli/feature-commands.js';
15
15
  import { writeSelftestRouteProof } from '../proof/selftest-proof-fixtures.js';
16
16
  import { createMission } from '../mission.js';
17
17
  import { formatSksUpdateCheckText, runSksUpdateCheck, runSksUpdateNow } from '../update-check.js';
18
+ import { persistSksUpdateNoticeFromVersions } from '../update/update-notice.js';
18
19
  import { withSecretPreservationGuard } from '../config/config-migration-journal.js';
19
20
  export async function helpCommand(args = []) {
20
21
  const topic = args[0];
@@ -121,6 +122,12 @@ For implementation work, use Codex App prompt routes such as $Naruto, $Goal, $QA
121
122
  }
122
123
  export async function updateCheckCommand(args = []) {
123
124
  const result = await runSksUpdateCheck();
125
+ await persistSksUpdateNoticeFromVersions({
126
+ packageName: result.package,
127
+ currentVersion: result.current,
128
+ latestVersion: result.latest,
129
+ error: result.error
130
+ }).catch(() => undefined);
124
131
  if (flag(args, '--json'))
125
132
  return printJson(result);
126
133
  cliUi.banner('update');
@@ -151,6 +158,14 @@ export async function updateCommand(sub = 'now', args = []) {
151
158
  timeoutMs: 10 * 60 * 1000,
152
159
  maxOutputBytes: 128 * 1024
153
160
  }));
161
+ if (!flag(effectiveArgs, '--dry-run')) {
162
+ await persistSksUpdateNoticeFromVersions({
163
+ packageName: result.package,
164
+ currentVersion: result.new_version || result.from,
165
+ latestVersion: result.latest,
166
+ error: result.ok ? null : result.error
167
+ }).catch(() => undefined);
168
+ }
154
169
  if (flag(effectiveArgs, '--json'))
155
170
  return printJson(result);
156
171
  cliUi.banner('update');
@@ -42,10 +42,26 @@ const NARUTO_ROUTE = '$Naruto';
42
42
  // writes). The standard 20-agent ceiling is lifted only for this route.
43
43
  export async function narutoCommand(commandOrArgs = 'naruto', maybeArgs = []) {
44
44
  const args = Array.isArray(commandOrArgs) ? commandOrArgs : maybeArgs;
45
- // 4.0.9: `sks naruto --glm` delegates to GLM Naruto before legacy Naruto starts.
45
+ // Normal Naruto is a GPT-5.6-family-only route. Keep the historical GLM
46
+ // implementation reachable only through the explicitly separate
47
+ // `sks --mad --glm --naruto` global mode; a command-local --glm flag must
48
+ // never bypass the Luna/Terra/Sol catalog and backend guards below.
46
49
  if (args.includes('--glm')) {
47
- const { glmNarutoCommand } = await import('../providers/glm/naruto/glm-naruto-command.js');
48
- return glmNarutoCommand(args.filter((arg) => arg !== '--glm'));
50
+ const result = {
51
+ schema: 'sks.naruto-command-result.v1',
52
+ ok: false,
53
+ mode: 'NARUTO',
54
+ status: 'blocked',
55
+ reason: 'naruto_gpt_5_6_family_only_glm_override_forbidden',
56
+ blockers: ['naruto_gpt_5_6_family_only_glm_override_forbidden'],
57
+ hint: 'Use normal sks naruto for Luna/Terra/Sol. The separate legacy GLM route requires: sks --mad --glm --naruto.'
58
+ };
59
+ process.exitCode = 1;
60
+ if (args.includes('--json'))
61
+ console.log(JSON.stringify(result, null, 2));
62
+ else
63
+ console.error(`$Naruto blocked: ${result.reason}. ${result.hint}`);
64
+ return result;
49
65
  }
50
66
  const parsed = parseNarutoArgs(args);
51
67
  if (!parsed.json)
@@ -84,13 +100,21 @@ async function narutoRun(parsed) {
84
100
  const writeCapable = parsed.readonly !== true && parsed.writeMode !== 'off';
85
101
  const patchEnvelopeBasePath = '.sneakoscope/naruto/patch-envelopes';
86
102
  const explicitPromptPaths = writeCapable ? extractNarutoPromptPaths(parsed.prompt) : [];
87
- const targetPaths = explicitPromptPaths.length ? explicitPromptPaths : [patchEnvelopeBasePath];
103
+ // Keep an empty target list when the prompt does not name a concrete file.
104
+ // buildNarutoWorkGraph then derives one patch-envelope path per work item;
105
+ // passing the base directory as a target would collapse every write lease
106
+ // onto the same path and force an avoidable serial merge bottleneck.
107
+ const targetPaths = explicitPromptPaths;
108
+ const fixtureLeaseTargetsAllowed = parsed.backend === 'fake' || parsed.mock;
88
109
  const placeholderGuard = checkPromptPlaceholders({
89
110
  prompt: parsed.prompt,
90
111
  writeCapable,
91
- targetPaths: writeCapable ? targetPaths : []
112
+ targetPaths: writeCapable
113
+ ? (targetPaths.length ? targetPaths : fixtureLeaseTargetsAllowed ? [patchEnvelopeBasePath] : [])
114
+ : []
92
115
  });
93
116
  if (!placeholderGuard.ok) {
117
+ process.exitCode = 1;
94
118
  return emit(parsed, {
95
119
  schema: NARUTO_RESULT_SCHEMA,
96
120
  ok: false,
@@ -98,7 +122,10 @@ async function narutoRun(parsed) {
98
122
  action: 'run',
99
123
  status: 'blocked',
100
124
  prompt_placeholder_guard: placeholderGuard,
101
- blockers: placeholderGuard.blockers
125
+ blockers: placeholderGuard.blockers,
126
+ hint: placeholderGuard.blockers.includes('write_capable_prompt_target_paths_empty')
127
+ ? 'Name the target source path(s), or run Naruto read-only discovery before starting a write-capable worker swarm.'
128
+ : null
102
129
  }, () => {
103
130
  console.log('$Naruto blocked before work graph creation: unresolved prompt placeholder or empty write target path.');
104
131
  for (const blocker of placeholderGuard.blockers)
@@ -137,7 +164,7 @@ async function narutoRun(parsed) {
137
164
  // system-safe number so naruto never spawns the whole count at once unless an
138
165
  // explicit operator override asks for a higher target.
139
166
  const localWorker = await resolveNarutoLocalWorkerMode(parsed);
140
- const schedulerBackend = localWorker.auto_select_eligible ? 'local-llm' : parsed.backend;
167
+ const schedulerBackend = parsed.backend;
141
168
  const safe = systemSafeNarutoConcurrency({ backend: schedulerBackend });
142
169
  const baseWorkGraph = buildNarutoWorkGraph({
143
170
  prompt: parsed.prompt,
@@ -1176,7 +1203,7 @@ async function resolveNarutoLocalWorkerMode(parsed) {
1176
1203
  description: parsed.prompt,
1177
1204
  write_paths: parsed.readonly ? [] : ['<lease-scoped-worker-path>']
1178
1205
  }, { route: NARUTO_ROUTE, agent: { role: parsed.readonly ? 'collector' : 'implementer' } });
1179
- const autoSelectEligible = parsed.backend === 'codex-sdk'
1206
+ const localAutoSelectCandidate = parsed.backend === 'codex-sdk'
1180
1207
  && parsed.backendExplicit !== true
1181
1208
  && parsed.noOllama !== true
1182
1209
  && config?.ok === true
@@ -1189,13 +1216,14 @@ async function resolveNarutoLocalWorkerMode(parsed) {
1189
1216
  model: config?.model || null,
1190
1217
  requested_backend: parsed.backend,
1191
1218
  backend_explicit: parsed.backendExplicit,
1192
- auto_select_eligible: autoSelectEligible,
1219
+ auto_select_eligible: false,
1193
1220
  worker_only: true,
1194
1221
  no_strategy_planning_design: true,
1195
1222
  policy,
1196
1223
  blockers: [
1197
1224
  ...(config?.blockers || (config ? [] : ['ollama_worker_config_unavailable'])),
1198
1225
  ...(policy.blockers || []),
1226
+ ...(localAutoSelectCandidate ? ['naruto_gpt_5_6_family_only_local_auto_select_forbidden'] : []),
1199
1227
  ...(parsed.backendExplicit ? ['backend_explicit'] : []),
1200
1228
  ...(parsed.noOllama ? ['no_ollama_requested'] : [])
1201
1229
  ]
@@ -1,17 +1,16 @@
1
1
  import fs from 'node:fs/promises';
2
- import os from 'node:os';
3
2
  import path from 'node:path';
4
3
  import { fileURLToPath } from 'node:url';
5
4
  import { applyNarutoPatchEnvelopes } from './naruto-parallel-patch-apply.js';
6
5
  import { realisticNarutoRealWriteProofFixture, validateNarutoRealWriteProof } from './naruto-real-write-proof.js';
7
- import { ensureDir, runProcess, writeJsonAtomic, writeTextAtomic } from '../fsx.js';
6
+ import { ensureDir, runProcess, tmpdir, writeJsonAtomic, writeTextAtomic } from '../fsx.js';
8
7
  export async function runNarutoWriteE2e(mode) {
9
8
  if (mode === 'real-codex')
10
9
  return runRealCodexWriteE2e();
11
10
  return runHermeticWriteE2e();
12
11
  }
13
12
  export async function runHermeticWriteE2e() {
14
- const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'sks-naruto-write-e2e-'));
13
+ const tempRoot = tmpdir('naruto-write-e2e-');
15
14
  const mergeArtifactPath = path.join(tempRoot, '.sneakoscope', 'naruto-write-e2e-parent-merge.json');
16
15
  const blockers = [];
17
16
  let cleanup = { ok: false, temp_root_removed: false, blockers: [] };
@@ -284,7 +283,7 @@ async function probeRealCodexRuntime() {
284
283
  return { ok: result.code === 0, blockers: result.code === 0 ? [] : [`codex_version_exit_${result.code}`] };
285
284
  }
286
285
  async function runRealNarutoCommandWriteE2e() {
287
- const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'sks-naruto-real-write-e2e-'));
286
+ const tempRoot = tmpdir('naruto-real-write-e2e-');
288
287
  const mergeArtifactPath = path.join(tempRoot, '.sneakoscope', 'naruto-real-write-e2e-parent-merge.json');
289
288
  const blockers = [];
290
289
  let cleanup = { ok: false, temp_root_removed: false, blockers: [] };