sneakoscope 7.0.5 → 7.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +5 -1
  2. package/crates/sks-core/Cargo.lock +1 -1
  3. package/crates/sks-core/Cargo.toml +1 -1
  4. package/dist/cli/command-manifest-lite.js +1 -1
  5. package/dist/cli/command-registry.js +1 -1
  6. package/dist/cli/install-helpers-codex-lb-config.js +54 -6
  7. package/dist/cli/install-helpers.js +12 -3
  8. package/dist/commands/codex-app.js +27 -1
  9. package/dist/commands/doctor.js +54 -25
  10. package/dist/config/skills-manifest.json +52 -52
  11. package/dist/core/agent-bridge/agent-manifest.js +8 -2
  12. package/dist/core/agent-bridge/host-capability-runtime.js +86 -30
  13. package/dist/core/codex/codex-config-toml.js +16 -2
  14. package/dist/core/codex-app/glm-profile-installer.js +38 -8
  15. package/dist/core/codex-app/openrouter-activate.js +75 -10
  16. package/dist/core/codex-app/openrouter-provider.js +7 -0
  17. package/dist/core/commands/basic-cli.js +16 -22
  18. package/dist/core/harness-conflicts.js +27 -13
  19. package/dist/core/hooks-runtime/hook-context.js +47 -2
  20. package/dist/core/hooks-runtime/naruto-terminal-finalization.js +114 -1
  21. package/dist/core/hooks-runtime/subagent-skill-availability-guards.js +64 -4
  22. package/dist/core/hooks-runtime/subagent-skill-availability.js +101 -2
  23. package/dist/core/hooks-runtime.js +21 -5
  24. package/dist/core/init/skills.js +13 -4
  25. package/dist/core/init.js +3 -1
  26. package/dist/core/managed-assets/managed-assets-manifest.js +1 -1
  27. package/dist/core/providers/openrouter/openrouter-account.js +306 -0
  28. package/dist/core/providers/openrouter/openrouter-error.js +7 -7
  29. package/dist/core/release/package-size-budget.js +1 -1
  30. package/dist/core/release/release-gate-affected-selector.js +3 -2
  31. package/dist/core/subagents/official-subagent-preparation.js +17 -5
  32. package/dist/core/subagents/official-subagent-prompt.js +21 -0
  33. package/dist/core/subagents/role-model-preferences.js +179 -0
  34. package/dist/core/update/update-migration-state.js +24 -16
  35. package/dist/core/update/update-status.js +31 -3
  36. package/dist/core/version.js +1 -1
  37. package/dist/native/sks-menubar/Resources/AppIcon.icns +0 -0
  38. package/dist/native/sks-menubar/Resources/SKSStatusTemplate.pdf +0 -0
  39. package/dist/native/sks-menubar/Sources/OverviewViewController.swift +85 -4
  40. package/dist/native/sks-menubar/Sources/ProvidersOpenRouter.swift +401 -23
  41. package/dist/native/sks-menubar/Sources/ProvidersViewController.swift +65 -23
  42. package/dist/native/sks-menubar/Sources/StatusItemController.swift +3 -4
  43. package/dist/scripts/sks-menubar-install-check.js +1 -1
  44. package/package.json +1 -1
  45. package/release-gates.v2.json +13 -8
package/README.md CHANGED
@@ -6,6 +6,10 @@
6
6
 
7
7
  Proof-first orchestration for Codex CLI, ChatGPT Desktop, AI coding agents, multi-agent workflows, release verification, and the macOS menu bar.
8
8
 
9
+ <p align="center">
10
+ <img src="docs/assets/sks-logo.svg" alt="Sneakoscope Codex logo" width="160" height="160" />
11
+ </p>
12
+
9
13
  [![npm version](https://img.shields.io/npm/v/sneakoscope?color=cb3837&logo=npm)](https://www.npmjs.com/package/sneakoscope)
10
14
  [![node](https://img.shields.io/badge/node-%3E%3D20.11-339933?logo=node.js&logoColor=white)](#requirements)
11
15
  [![license](https://img.shields.io/badge/license-MIT-blue)](#license)
@@ -18,7 +22,7 @@ Proof-first orchestration for Codex CLI, ChatGPT Desktop, AI coding agents, mult
18
22
  Sneakoscope Codex (`sks`) is an open-source trust layer for Codex CLI and ChatGPT Desktop. It coordinates bounded AI coding agents, records machine-verifiable evidence, preserves project memory, and blocks release claims that are not supported by current tests or artifacts. Search visibility outcomes are measured separately; SKS does not promise rankings or traffic.
19
23
  <!-- END SKS SEARCH VISIBILITY MARKETING -->
20
24
 
21
- Current release: **SKS 7.0.5**, with the package preferred Codex channel at **CLI 0.145.0**. SKS stays version-agnostic: older hosts keep working where capabilities allow, while Menu Bar / Center induce updates to the preferred latest. It resolves managed SKS skills from the authoritative global install, preserves a runnable Naruto child slot when `max_threads=2`, and keeps current-version Menu Bar repair transactional so stamped generations remain verifiable. Naruto uses stable opt-in multi-agent V2 when the host exposes it. See [CHANGELOG.md](CHANGELOG.md).
25
+ Current release: **SKS 7.1.0**, with the package preferred Codex channel at **CLI 0.145.0**. SKS stays version-agnostic: older hosts keep working where capabilities allow, while Menu Bar / Center induce updates to the preferred latest. It resolves managed SKS skills from the authoritative global install, preserves a runnable Naruto child slot when `max_threads=2`, and keeps current-version Menu Bar repair transactional so stamped generations remain verifiable. Naruto uses stable opt-in multi-agent V2 when the host exposes it. See [CHANGELOG.md](CHANGELOG.md).
22
26
 
23
27
  ## What 7.0.0 Ships
24
28
 
@@ -76,7 +76,7 @@ dependencies = [
76
76
 
77
77
  [[package]]
78
78
  name = "sks-core"
79
- version = "7.0.5"
79
+ version = "7.1.0"
80
80
  dependencies = [
81
81
  "serde_json",
82
82
  ]
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "sks-core"
3
- version = "7.0.5"
3
+ version = "7.1.0"
4
4
  edition = "2021"
5
5
 
6
6
  [dependencies]
@@ -16,7 +16,7 @@ export const COMMAND_MANIFEST_LITE = [
16
16
  { name: 'update', summary: 'Inspect, review, apply, or roll back the global SKS update', maturity: 'stable' },
17
17
  { name: 'uninstall', summary: 'Uninstall SKS global skills, hooks, config, menu bar, and optional project residue', maturity: 'stable', allowedDuringActiveRoute: true },
18
18
  { name: 'update-check', summary: 'Show the shared SKS, Codex CLI, and Menu Bar update status', maturity: 'stable', readonly: true, skipMigrationGate: true, allowedDuringActiveRoute: true, diagnostic: true },
19
- { name: 'mcp', summary: 'Manage scoped Codex MCP configuration', maturity: 'beta' },
19
+ { name: 'mcp', summary: 'Manage scoped Codex MCP configuration', maturity: 'beta', skipMigrationGate: true },
20
20
  { name: 'wizard', summary: 'Open setup wizard help', maturity: 'stable' },
21
21
  { name: 'usage', summary: 'Show focused usage topic', maturity: 'stable', readonly: true, allowedDuringActiveRoute: true, diagnostic: true },
22
22
  { name: 'quickstart', summary: 'Show quickstart flow', maturity: 'stable' },
@@ -135,7 +135,7 @@ const COMMAND_DEFINITIONS = {
135
135
  activeRoutePolicy: 'always'
136
136
  }),
137
137
  'update-check': readOnly(entry('stable', 'Show the shared SKS, Codex CLI, and Menu Bar update status', 'dist/core/commands/basic-cli.js', basicArgs('updateCheckCommand'))),
138
- mcp: entry('beta', 'Manage scoped Codex MCP configuration', 'dist/core/commands/mcp-config-command.js', argsCommand(() => import('../core/commands/mcp-config-command.js'), 'mcpConfigCommand', 'dist/core/commands/mcp-config-command.js')),
138
+ mcp: skipMigrationGate(entry('beta', 'Manage scoped Codex MCP configuration', 'dist/core/commands/mcp-config-command.js', argsCommand(() => import('../core/commands/mcp-config-command.js'), 'mcpConfigCommand', 'dist/core/commands/mcp-config-command.js'))),
139
139
  wizard: entry('stable', 'Open setup wizard help', 'dist/core/commands/basic-cli.js', basicNoArgs('quickstartCommand')),
140
140
  usage: readOnly(entry('stable', 'Show focused usage topic', 'dist/core/commands/basic-cli.js', basicArgs('usageCommand'))),
141
141
  quickstart: entry('stable', 'Show quickstart flow', 'dist/core/commands/basic-cli.js', basicNoArgs('quickstartCommand')),
@@ -1,7 +1,8 @@
1
1
  import path from 'node:path';
2
2
  import os from 'node:os';
3
3
  import { ensureDir, readText } from '../core/fsx.js';
4
- import { GLM_CODEX_CONFIG_PROVIDER_ID, GLM_52_OPENROUTER_MODEL, OPENROUTER_DEFAULT_PROFILE_ID, RETIRED_GLM_DESKTOP_CONFIG_PROFILE_IDS } from '../core/codex-app/openrouter-provider.js';
4
+ import { GLM_CODEX_CONFIG_PROVIDER_ID, GLM_52_OPENROUTER_MODEL, OPENROUTER_AUTH_COMMAND, OPENROUTER_AUTH_REFRESH_INTERVAL_MS, OPENROUTER_AUTH_TIMEOUT_MS, openRouterAuthCommandArgs, OPENROUTER_DEFAULT_PROFILE_ID, RETIRED_GLM_DESKTOP_CONFIG_PROFILE_IDS } from '../core/codex-app/openrouter-provider.js';
5
+ import { openRouterSecretPaths, resolveOpenRouterApiKey } from '../core/providers/openrouter/openrouter-secret-store.js';
5
6
  import { reconcileRetiredSksConfigText } from '../core/auto-review.js';
6
7
  import { removeTopLevelTomlKeyIfValue, safeWriteCodexConfigToml, upsertTopLevelTomlString, upsertTomlTable } from '../core/codex-runtime/codex-desktop-config-policy.js';
7
8
  import { codexLbConfigPath, normalizeCodexLbBaseUrl } from './install-helpers-codex-lb-shared.js';
@@ -58,30 +59,41 @@ export function removeCodexLbSharedOpenAiRouting(text = '', baseUrl = '') {
58
59
  text: removeTopLevelLine(withoutValue, CODEX_LB_SHARED_OPENAI_ROUTING_MARKER)
59
60
  };
60
61
  }
61
- export function upsertCodexAppGlmConfig(text = '') {
62
+ export function upsertCodexAppGlmConfig(text = '', input = {}) {
62
63
  let next = String(text || '');
64
+ const authEnv = { ...(input.env || process.env), ...(input.home ? { HOME: input.home } : {}) };
65
+ const authArgs = openRouterAuthCommandArgs(openRouterSecretPaths(authEnv).keyPath);
63
66
  const providerBlock = [
64
67
  `[model_providers.${GLM_CODEX_CONFIG_PROVIDER_ID}]`,
65
68
  'name = "OpenRouter"',
66
69
  'base_url = "https://openrouter.ai/api/v1"',
67
70
  'wire_api = "responses"',
68
- 'env_key = "OPENROUTER_API_KEY"',
69
71
  'requires_openai_auth = false'
70
72
  ].join('\n');
71
73
  next = upsertTomlTable(next, `model_providers.${GLM_CODEX_CONFIG_PROVIDER_ID}`, providerBlock);
74
+ const authBlock = [
75
+ `[model_providers.${GLM_CODEX_CONFIG_PROVIDER_ID}.auth]`,
76
+ `command = ${JSON.stringify(OPENROUTER_AUTH_COMMAND)}`,
77
+ `args = [${authArgs.map((value) => JSON.stringify(value)).join(', ')}]`,
78
+ `timeout_ms = ${OPENROUTER_AUTH_TIMEOUT_MS}`,
79
+ `refresh_interval_ms = ${OPENROUTER_AUTH_REFRESH_INTERVAL_MS}`
80
+ ].join('\n');
81
+ next = upsertTomlTable(next, `model_providers.${GLM_CODEX_CONFIG_PROVIDER_ID}.auth`, authBlock);
82
+ next = next.replace(new RegExp(`\\n+\\[model_providers\\.${GLM_CODEX_CONFIG_PROVIDER_ID}\\.auth\\]`), `\n\n[model_providers.${GLM_CODEX_CONFIG_PROVIDER_ID}.auth]`);
72
83
  next = reconcileRetiredSksConfigText(next).text;
73
84
  return `${next.trim()}\n`;
74
85
  }
75
86
  export async function ensureGlobalCodexAppGlmProfile(opts = {}) {
76
- if (process.env.SKS_SKIP_CODEX_GLM_PROFILE_REPAIR === '1' && opts.force !== true) {
87
+ const env = opts.env || process.env;
88
+ if (env.SKS_SKIP_CODEX_GLM_PROFILE_REPAIR === '1' && opts.force !== true) {
77
89
  return { ok: true, status: 'skipped', reason: 'SKS_SKIP_CODEX_GLM_PROFILE_REPAIR=1' };
78
90
  }
79
- const home = opts.home || process.env.HOME || os.homedir();
91
+ const home = opts.home || env.HOME || os.homedir();
80
92
  const configPath = opts.configPath || codexLbConfigPath(home);
81
93
  try {
82
94
  await ensureDir(path.dirname(configPath));
83
95
  const current = await readText(configPath, '');
84
- const next = upsertCodexAppGlmConfig(current);
96
+ const next = upsertCodexAppGlmConfig(current, { home, env: opts.env });
85
97
  const safeWrite = await safeWriteCodexConfigToml(configPath, current, next, 'openrouter-provider');
86
98
  return {
87
99
  ...safeWrite,
@@ -107,6 +119,42 @@ export async function ensureGlobalCodexAppGlmProfile(opts = {}) {
107
119
  };
108
120
  }
109
121
  }
122
+ export async function ensureStoredOpenRouterProviderDuringInstall(opts = {}) {
123
+ const home = opts.home || opts.env?.HOME || process.env.HOME || os.homedir();
124
+ const env = { ...(opts.env || process.env), HOME: home };
125
+ const key = await resolveOpenRouterApiKey({ env });
126
+ if (!key.key) {
127
+ return {
128
+ schema: 'sks.openrouter-provider-upgrade-repair.v1',
129
+ ok: true,
130
+ status: 'skipped',
131
+ reason: 'openrouter_key_missing',
132
+ key_present: false,
133
+ key_source: null,
134
+ blockers: [],
135
+ warnings: key.warnings
136
+ };
137
+ }
138
+ const repair = await ensureGlobalCodexAppGlmProfile({
139
+ ...opts,
140
+ home,
141
+ env
142
+ });
143
+ const ok = repair?.ok !== false;
144
+ return {
145
+ schema: 'sks.openrouter-provider-upgrade-repair.v1',
146
+ ok,
147
+ status: ok ? repair?.status || 'present' : 'failed',
148
+ reason: ok ? 'stored_openrouter_key_provider_reconciled' : 'stored_openrouter_key_provider_repair_failed',
149
+ key_present: true,
150
+ key_source: key.source,
151
+ config_path: repair?.config_path || opts.configPath || codexLbConfigPath(home),
152
+ provider: GLM_CODEX_CONFIG_PROVIDER_ID,
153
+ blockers: ok ? [] : ['openrouter_provider_repair_failed'],
154
+ warnings: key.warnings,
155
+ repair
156
+ };
157
+ }
110
158
  export function detectCodexLbSetupDrift(state = {}) {
111
159
  const drift = [];
112
160
  if (state.useDefaultProvider && state.selected !== true)
@@ -27,7 +27,7 @@ import { repairCodexImagegen } from '../core/doctor/imagegen-repair.js';
27
27
  import { canAskYesNo, compareVersions, hasCodexUnstableFeatureWarningSuppression, hasDeprecatedCodexHooksFeatureFlag, hasTopLevelCodexModeLock, isProjectSetupCandidate } from './install-tool-helpers.js';
28
28
  import { checkCodexLbResponseChain } from './install-helpers-codex-lb-chain.js';
29
29
  import { CODEX_LB_CANONICAL_FAST_SERVICE_TIER, CODEX_LB_PROVIDER_ENV_KEY, CODEX_LB_PROVIDER_NAME, askPostinstallQuestion, codexAuthChatgptBackupPath, codexAuthPath, codexLbConfigPath, codexLbEnvPath, hasTopLevelCodexLbSelected, normalizeCodexLbBaseUrl, parseCodexLbEnvKey, redactSecretText } from './install-helpers-codex-lb-shared.js';
30
- import { appliedCodexLbPersistenceModes, captureCodexLbSetupWriteState, codexLbSharedOpenAiRoutingState, detectCodexLbSetupDrift, ensureGlobalCodexAppGlmProfile, parseCodexLbEnvBaseUrl, parseCodexSharedLoginApiKey, removeCodexLbSharedOpenAiRouting, sha256Text, shellSingleQuote, upsertCodexAppGlmConfig, upsertCodexLbConfig, upsertCodexLbSharedOpenAiRouting } from './install-helpers-codex-lb-config.js';
30
+ import { appliedCodexLbPersistenceModes, captureCodexLbSetupWriteState, codexLbSharedOpenAiRoutingState, detectCodexLbSetupDrift, ensureGlobalCodexAppGlmProfile, ensureStoredOpenRouterProviderDuringInstall, parseCodexLbEnvBaseUrl, parseCodexSharedLoginApiKey, removeCodexLbSharedOpenAiRouting, sha256Text, shellSingleQuote, upsertCodexAppGlmConfig, upsertCodexLbConfig, upsertCodexLbSharedOpenAiRouting } from './install-helpers-codex-lb-config.js';
31
31
  import { ensureCodexImagegenDuringInstall, ensureGlobalCodexSkillsDuringInstall, ensureGlobalContext7DuringInstall, ensureGlobalGetdesignSkillDuringInstall, ensureSksCommandDuringInstall } from './install-helpers-install-support.js';
32
32
  function packagedSksEntrypoint() {
33
33
  return path.join(packageRoot(), 'dist', 'bin', 'sks.js');
@@ -89,6 +89,15 @@ export async function postinstall({ bootstrap, args = [] }) {
89
89
  console.log(`Codex App Fast mode: skipped (${fastModeRepair.reason}).`);
90
90
  else if (fastModeRepair.status === 'failed')
91
91
  console.log(`Codex App Fast mode: auto repair failed. Run \`sks setup\`. ${fastModeRepair.error || ''}`.trim());
92
+ const openRouterProviderRepair = await ensureStoredOpenRouterProviderDuringInstall();
93
+ if (openRouterProviderRepair.status === 'updated')
94
+ console.log('OpenRouter provider: repaired for the stored key (credentials and active model were preserved).');
95
+ else if (openRouterProviderRepair.status === 'present')
96
+ console.log('OpenRouter provider: stored-key configuration already compatible.');
97
+ else if (openRouterProviderRepair.status === 'skipped')
98
+ console.log('OpenRouter provider: no stored key; no provider configuration was added.');
99
+ else if (openRouterProviderRepair.status === 'failed')
100
+ console.log('OpenRouter provider: stored key was preserved, but provider repair failed. Run `sks doctor --fix`.');
92
101
  const imagegenRepair = await ensureCodexImagegenDuringInstall();
93
102
  if (imagegenRepair.status === 'ready')
94
103
  console.log('Codex App Image Gen: ready ($imagegen/gpt-image-2 detected).');
@@ -258,7 +267,7 @@ async function reportPostinstallCodexLbAuth(snapshot = null) {
258
267
  async function postinstallHarnessConflictNotice(conflictScan) {
259
268
  console.log('\nSneakoscope Codex package installed, but SKS setup is blocked.');
260
269
  console.log(formatHarnessConflictReport(conflictScan, { includePrompt: false }));
261
- console.log('\nWhat this means: npm can finish installing the package. Conflicting OMX/DCodex markers are quarantined automatically by `sks setup`, `sks doctor --fix`, `sks update`, or `sks conflicts cleanup --yes`.');
270
+ console.log('\nWhat this means: npm can finish installing the package. Conflicting OMX/DCodex markers must be removed explicitly with `sks conflicts cleanup --yes` before `sks setup`, `sks doctor --fix`, or `sks update` can proceed.');
262
271
  console.log('No files were removed by postinstall.');
263
272
  console.log('Cleanup requires a human-approved Codex App session. Keep the model selected in Codex and use high reasoning effort.');
264
273
  if (shouldAskPostinstallQuestion()) {
@@ -1924,7 +1933,7 @@ export { checkContext7, ensureCodexCliTool, ensureMadLaunchDependencies, ensureR
1924
1933
  export { codexFastModeDesktopStatus, ensureGlobalCodexFastModeDuringInstall, normalizeCodexFastModeUiConfig, safeWriteCodexConfigToml } from '../core/codex-runtime/codex-desktop-config-policy.js';
1925
1934
  export { checkRequiredSkills, context7GlobalMcpStatus, ensureCodexImagegenDuringInstall, ensureGlobalCodexSkillsDuringInstall, ensureProjectContext7Config, ensureSksCommandDuringInstall, globalCodexSkillsRoot, selftestSksShimRepair } from './install-helpers-install-support.js';
1926
1935
  export { askPostinstallQuestion, codexLbConfigPath, codexLbEnvPath, normalizeCodexLbBaseUrl } from './install-helpers-codex-lb-shared.js';
1927
- export { ensureGlobalCodexAppGlmProfile, upsertCodexAppGlmConfig, upsertCodexLbConfig } from './install-helpers-codex-lb-config.js';
1936
+ export { ensureGlobalCodexAppGlmProfile, ensureStoredOpenRouterProviderDuringInstall, upsertCodexAppGlmConfig, upsertCodexLbConfig } from './install-helpers-codex-lb-config.js';
1928
1937
  export { checkCodexLbResponseChain } from './install-helpers-codex-lb-chain.js';
1929
1938
  export { selftestCodexLb } from './install-helpers-codex-lb-selftest.js';
1930
1939
  function escapeRegExp(value) {
@@ -14,7 +14,9 @@ import { doctorCodexAppGlmProfile, installCodexAppGlmProfile } from '../core/cod
14
14
  import { openRouterStatus, useOpenRouter } from '../core/codex-app/openrouter-activate.js';
15
15
  import { OPENROUTER_DEFAULT_MODEL } from '../core/codex-app/openrouter-provider.js';
16
16
  import { promptForOpenRouterKeyHidden, writeStoredOpenRouterKey } from '../core/providers/openrouter/openrouter-secret-store.js';
17
+ import { compactOpenRouterModelsResult, listOpenRouterModels, testOpenRouterConnection } from '../core/providers/openrouter/openrouter-account.js';
17
18
  import { restartCodexApp } from '../core/codex-app/codex-app-restart.js';
19
+ import { resetRoleModelPreference, roleModelPreferencesStatus, setRoleModelPreference } from '../core/subagents/role-model-preferences.js';
18
20
  export async function run(_command, args = []) {
19
21
  const action = args[0] || 'check';
20
22
  if (action === 'restart')
@@ -82,6 +84,30 @@ export async function run(_command, args = []) {
82
84
  const result = await openRouterStatus({});
83
85
  return printCodexAppResult(args, result);
84
86
  }
87
+ if (action === 'openrouter-models') {
88
+ const models = await listOpenRouterModels({});
89
+ const result = flag(args, '--ids-only') ? compactOpenRouterModelsResult(models) : models;
90
+ return printCodexAppResult(args, result);
91
+ }
92
+ if (action === 'openrouter-test') {
93
+ const result = await testOpenRouterConnection({ model: readOption(args, '--model', '') });
94
+ return printCodexAppResult(args, result);
95
+ }
96
+ if (action === 'role-models') {
97
+ return printCodexAppResult(args, await roleModelPreferencesStatus());
98
+ }
99
+ if (action === 'set-role-model') {
100
+ const result = await setRoleModelPreference({
101
+ role: readOption(args, '--role', ''),
102
+ model: readOption(args, '--model', ''),
103
+ reasoning: readOption(args, '--reasoning', '')
104
+ });
105
+ return printCodexAppResult(args, result);
106
+ }
107
+ if (action === 'reset-role-model') {
108
+ const result = await resetRoleModelPreference({ role: readOption(args, '--role', '') });
109
+ return printCodexAppResult(args, result);
110
+ }
85
111
  if (action === 'product-design' || action === 'design-product' || action === 'ensure-product-design') {
86
112
  const checkOnly = flag(args, '--check-only') || flag(args, '--no-install');
87
113
  const status = await codexProductDesignPluginStatus({
@@ -142,7 +168,7 @@ export async function run(_command, args = []) {
142
168
  process.exitCode = 1;
143
169
  return;
144
170
  }
145
- console.error('Usage: sks codex-app check|status|harness-matrix|skill-sync|agent-role-sync|init-deep|hook-lifecycle|execution-profile|set-openrouter-key [--api-key-stdin]|use-openrouter --model <id>|openrouter-status|product-design [--check-only]|ensure-product-design|chrome-extension|pat status|remote-control [--json]');
171
+ console.error('Usage: sks codex-app check|status|harness-matrix|skill-sync|agent-role-sync|init-deep|hook-lifecycle|execution-profile|set-openrouter-key [--api-key-stdin]|use-openrouter --model <id>|openrouter-status|openrouter-models [--ids-only]|openrouter-test [--model <id>]|role-models|set-role-model --role <name> --model <id> --reasoning <effort>|reset-role-model --role <name>|product-design [--check-only]|ensure-product-design|chrome-extension|pat status|remote-control [--json]');
146
172
  console.error('Note: glm-profile is retired (strips leftover Desktop GLM profiles); use set-openrouter-key / use-openrouter.');
147
173
  process.exitCode = 1;
148
174
  }
@@ -35,32 +35,27 @@ export async function run(_command, args = [], deps = {}) {
35
35
  const doctorFix = flag(args, '--fix');
36
36
  const globalOnly = doctorFix && flag(args, '--global-only');
37
37
  if (doctorFix) {
38
- const { cleanupOtherHarnessConflicts } = await import('../core/harness-conflicts.js');
39
38
  const conflictScan = await scanHarnessConflicts(root);
40
39
  if (conflictScan.hard_block) {
41
- const cleanup = await cleanupOtherHarnessConflicts(root);
42
- if (!cleanup.ok) {
43
- const blocked = {
44
- schema: 'sks.doctor-status.v3',
45
- ok: false,
46
- status: 'blocked_harness_conflict',
47
- diagnostic_depth: 'fix',
48
- root,
49
- blockers: (cleanup.remaining || conflictScan.hard).map((item) => `${item.name || 'harness'}:${item.path}`),
50
- conflicts: cleanup.after?.conflicts || conflictScan.conflicts,
51
- other_harness_cleanup: cleanup,
52
- cleanup_prompt_command: 'sks conflicts cleanup --yes',
53
- no_fix_writes_performed: cleanup.cleaned.length === 0
54
- };
55
- process.exitCode = 1;
56
- if (flag(args, '--json')) {
57
- printJson(blocked);
58
- return blocked;
59
- }
60
- console.error(formatHarnessConflictReport(cleanup.after || conflictScan, { includePrompt: false }));
61
- console.error('Automatic OMX/DCodex removal did not clear every conflict. Live markers must be removed; inspect backup under .sneakoscope/quarantine/other-harness/.');
40
+ const blocked = {
41
+ schema: 'sks.doctor-status.v3',
42
+ ok: false,
43
+ status: 'blocked_harness_conflict',
44
+ diagnostic_depth: 'fix',
45
+ root,
46
+ blockers: conflictScan.hard.map((item) => `${item.name || 'harness'}:${item.path}`),
47
+ conflicts: conflictScan.conflicts,
48
+ cleanup_prompt_command: 'sks conflicts cleanup --yes',
49
+ no_fix_writes_performed: true
50
+ };
51
+ process.exitCode = 1;
52
+ if (flag(args, '--json')) {
53
+ printJson(blocked);
62
54
  return blocked;
63
55
  }
56
+ console.error(formatHarnessConflictReport(conflictScan, { includePrompt: false }));
57
+ console.error('Run `sks conflicts cleanup --yes` to quarantine OMX/DCodex markers before doctor --fix.');
58
+ return blocked;
64
59
  }
65
60
  }
66
61
  const doctorProfile = doctorProfileFromArgs(args, doctorFix);
@@ -89,6 +84,8 @@ export async function executeDoctorGlobalOnlyFix(args = [], root, deps = {}) {
89
84
  || (await import('../core/doctor/command-alias-cleanup.js')).runDoctorCommandAliasCleanup;
90
85
  const ensureGlobalFastModeImpl = deps.ensureGlobalCodexFastModeDuringInstallImpl
91
86
  || (await import('../cli/install-helpers.js')).ensureGlobalCodexFastModeDuringInstall;
87
+ const ensureStoredOpenRouterProviderImpl = deps.ensureStoredOpenRouterProviderDuringInstallImpl
88
+ || (await import('../cli/install-helpers.js')).ensureStoredOpenRouterProviderDuringInstall;
92
89
  const installMenuBarImpl = deps.installSksMenuBarImpl || installSksMenuBar;
93
90
  const codexLbStatusImpl = deps.codexLbStatusImpl || codexLbStatus;
94
91
  const providerStatus = await codexLbStatusImpl({
@@ -131,6 +128,16 @@ export async function executeDoctorGlobalOnlyFix(args = [], root, deps = {}) {
131
128
  status: 'failed',
132
129
  error: err?.message || String(err)
133
130
  }));
131
+ const openRouterEnv = deps.env || (deps.home
132
+ ? { ...process.env, HOME: home, SKS_HOME: path.join(home, '.sneakoscope') }
133
+ : { ...process.env, HOME: home });
134
+ const openRouterProvider = await ensureStoredOpenRouterProviderImpl({ home, env: openRouterEnv }).catch((err) => ({
135
+ schema: 'sks.openrouter-provider-upgrade-repair.v1',
136
+ ok: false,
137
+ status: 'failed',
138
+ blockers: ['openrouter_provider_repair_failed'],
139
+ error: err?.message || String(err)
140
+ }));
134
141
  const menuBar = await installMenuBarImpl({
135
142
  home,
136
143
  root: home,
@@ -151,10 +158,12 @@ export async function executeDoctorGlobalOnlyFix(args = [], root, deps = {}) {
151
158
  const globalFastModeReady = globalFastMode?.status !== 'failed'
152
159
  && globalFastMode?.ok !== false;
153
160
  const menuBarReady = menuBar?.ok !== false;
161
+ const openRouterProviderReady = openRouterProvider?.ok !== false;
154
162
  const blockers = [...new Set([
155
163
  ...(!globalSkillsReady ? [`global_skills_reconcile_failed:${globalSkills?.error || 'core_skill_integrity'}`] : []),
156
164
  ...(currentSurface?.ok !== true ? (currentSurface?.blockers || ['global_current_surface_reconcile_failed']) : []),
157
165
  ...(!globalFastModeReady ? [`global_fast_mode_repair_failed:${globalFastMode?.error || globalFastMode?.status || 'unknown'}`] : []),
166
+ ...(!openRouterProviderReady ? (openRouterProvider?.blockers || ['openrouter_provider_repair_failed']) : []),
158
167
  ...(!menuBarReady ? (menuBar?.blockers || ['sks_menubar_repair_failed']) : []),
159
168
  ...(!recoveryReady ? (providerStatus?.tool_output_recovery?.blockers || ['codex_lb_tool_output_recovery_unverified']) : [])
160
169
  ].map(String).filter(Boolean))];
@@ -183,6 +192,7 @@ export async function executeDoctorGlobalOnlyFix(args = [], root, deps = {}) {
183
192
  skills: { global: globalSkills, project: { skipped: true, reason: 'global_only_doctor' } },
184
193
  current_public_surface: currentSurface,
185
194
  codex_app_fast_mode: globalFastMode,
195
+ openrouter_provider: openRouterProvider,
186
196
  sks_menubar: menuBar,
187
197
  codex_lb: {
188
198
  provider_status: providerStatus,
@@ -192,6 +202,7 @@ export async function executeDoctorGlobalOnlyFix(args = [], root, deps = {}) {
192
202
  blockers,
193
203
  next_actions: [
194
204
  ...(recoveryReady ? [] : (providerStatus?.tool_output_recovery?.operator_actions || [])),
205
+ ...(!openRouterProviderReady ? ['Run `sks codex-app set-openrouter-key` to repair the stored-key provider configuration without switching models.'] : []),
195
206
  'Run `sks doctor --fix --json` from a specific project directory when project-scoped repair is required.'
196
207
  ]
197
208
  };
@@ -432,10 +443,20 @@ async function runDoctor(args = [], root, doctorFix) {
432
443
  const doctorDirtyPlan = doctorFix ? (await import('../core/doctor/doctor-dirty-planner.js')).planDoctorDirtyRepair(root, doctorPhaseIds) : null;
433
444
  let setupRepair = null;
434
445
  let sksUpdate = null;
446
+ let openRouterProviderRepair = { ok: true, status: 'skipped', reason: 'doctor_without_fix' };
435
447
  let migrationPreFix = null;
436
448
  if (doctorFix) {
437
449
  migrationPreFix = await captureCodexConfigSnapshot();
438
450
  const installScope = installScopeFromArgs(args);
451
+ openRouterProviderRepair = flag(args, '--local-only')
452
+ ? { ok: true, status: 'skipped', reason: 'local-only repair' }
453
+ : await (await import('../cli/install-helpers.js')).ensureStoredOpenRouterProviderDuringInstall().catch((err) => ({
454
+ schema: 'sks.openrouter-provider-upgrade-repair.v1',
455
+ ok: false,
456
+ status: 'failed',
457
+ blockers: ['openrouter_provider_repair_failed'],
458
+ error: err?.message || String(err)
459
+ }));
439
460
  setupRepair = {
440
461
  schema: 'sks.doctor-setup-phase.v2',
441
462
  ok: true,
@@ -449,7 +470,8 @@ async function runDoctor(args = [], root, doctorFix) {
449
470
  : { status: 'skipped', reason: 'project or local-only repair' },
450
471
  codex_app_fast_mode: flag(args, '--local-only')
451
472
  ? { status: 'skipped', reason: 'local-only repair' }
452
- : await (await import('../cli/install-helpers.js')).ensureGlobalCodexFastModeDuringInstall().catch((err) => ({ status: 'failed', error: err?.message || String(err) }))
473
+ : await (await import('../cli/install-helpers.js')).ensureGlobalCodexFastModeDuringInstall().catch((err) => ({ status: 'failed', error: err?.message || String(err) })),
474
+ openrouter_provider: openRouterProviderRepair
453
475
  };
454
476
  }
455
477
  const skillsReconcile = await reconcileDoctorSkills(root, doctorFix);
@@ -1217,7 +1239,12 @@ async function runDoctor(args = [], root, doctorFix) {
1217
1239
  ]
1218
1240
  });
1219
1241
  if (doctorFix) {
1220
- const readinessBlockers = Array.isArray(ready.blockers) ? ready.blockers.map(String).filter(Boolean) : [];
1242
+ const readinessBlockers = [
1243
+ ...(Array.isArray(ready.blockers) ? ready.blockers.map(String).filter(Boolean) : []),
1244
+ ...(openRouterProviderRepair?.ok === false
1245
+ ? ((openRouterProviderRepair?.blockers || ['openrouter_provider_repair_failed']).map(String))
1246
+ : [])
1247
+ ];
1221
1248
  const migrationWarnings = [
1222
1249
  ...(doctorNativeCapabilityRepair?.optional_warnings || []),
1223
1250
  ...(doctorFixPostcheck?.optional_warnings || [])
@@ -1265,6 +1292,7 @@ async function runDoctor(args = [], root, doctorFix) {
1265
1292
  && commandAliasCleanup.ok !== false
1266
1293
  && codexStartupRepair.ok !== false
1267
1294
  && agentRoleConfigRepair.ok !== false
1295
+ && openRouterProviderRepair.ok !== false
1268
1296
  && (officialSubagentConfig.blockers || []).length === 0
1269
1297
  && codexLbRecoveryReady;
1270
1298
  const result = {
@@ -1284,6 +1312,7 @@ async function runDoctor(args = [], root, doctorFix) {
1284
1312
  rust,
1285
1313
  codex_app: codexApp,
1286
1314
  codex_app_ui: codexAppUi,
1315
+ openrouter_provider: openRouterProviderRepair,
1287
1316
  sks_menubar: sksMenuBar,
1288
1317
  provider_context: providerContext,
1289
1318
  codex_lb: codexLb,
@@ -1344,7 +1373,7 @@ async function runDoctor(args = [], root, doctorFix) {
1344
1373
  sneakoscope: { ok: await exists(`${root}/.sneakoscope`) },
1345
1374
  package: { bytes: pkgBytes, human: formatBytes(pkgBytes) },
1346
1375
  skills: skillsReconcile,
1347
- repair: { sks_update: sksUpdate, setup: setupRepair, codex_config: configRepair, migration_journal: migrationJournal, global_sks_installs: globalSksInstallCleanup, agent_role_config: agentRoleConfigRepair, zellij: zellijRepair, context7: context7Repair, codex_startup: codexStartupRepair, startup_config: startupConfigRepair, context7_mcp: context7McpRepair, supabase_mcp: supabaseMcpRepair, mcp_transport_collision: mcpTransportCollisionRepair, imagegen: imagegenRepair, computer_use: computerUseRepair, browser_use: browserUseRepair, hook_trust: hookTrustRepair, sks_menubar: sksMenuBar, doctor_transaction: doctorFixTransaction, doctor_dirty_plan: doctorDirtyPlan, doctor_postcheck: doctorFixPostcheck, codex_native: codexNativeRepair, doctor_native_capability: doctorNativeCapabilityRepair, command_aliases: commandAliasCleanup, skills: skillsReconcile, sks_temp_sweep: sksTempSweep }
1376
+ repair: { sks_update: sksUpdate, setup: setupRepair, openrouter_provider: openRouterProviderRepair, codex_config: configRepair, migration_journal: migrationJournal, global_sks_installs: globalSksInstallCleanup, agent_role_config: agentRoleConfigRepair, zellij: zellijRepair, context7: context7Repair, codex_startup: codexStartupRepair, startup_config: startupConfigRepair, context7_mcp: context7McpRepair, supabase_mcp: supabaseMcpRepair, mcp_transport_collision: mcpTransportCollisionRepair, imagegen: imagegenRepair, computer_use: computerUseRepair, browser_use: browserUseRepair, hook_trust: hookTrustRepair, sks_menubar: sksMenuBar, doctor_transaction: doctorFixTransaction, doctor_dirty_plan: doctorDirtyPlan, doctor_postcheck: doctorFixPostcheck, codex_native: codexNativeRepair, doctor_native_capability: doctorNativeCapabilityRepair, command_aliases: commandAliasCleanup, skills: skillsReconcile, sks_temp_sweep: sksTempSweep }
1348
1377
  };
1349
1378
  if (reportFile)
1350
1379
  await writeJsonReportFile(reportFile, result);