sneakoscope 10.5.2 → 10.5.4

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 (51) hide show
  1. package/README.md +1 -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/router.js +9 -3
  6. package/dist/commands/bridge.js +8 -3
  7. package/dist/config/skills-manifest.json +1 -1
  8. package/dist/core/agents/native-worker-backend-router.js +20 -12
  9. package/dist/core/codex-app/codex-app-restart-policy.js +4 -0
  10. package/dist/core/codex-lb/desktop-controller-v3/catalog.js +15 -1
  11. package/dist/core/codex-lb/desktop-controller-v3/lifecycle-commands.js +2 -1
  12. package/dist/core/codex-lb/desktop-controller-v3/openrouter-only-commands.js +72 -12
  13. package/dist/core/codex-lb/desktop-controller-v3.js +1 -1
  14. package/dist/core/commands/align-command.js +2 -1
  15. package/dist/core/commands/naruto-command.js +44 -0
  16. package/dist/core/commands/naruto-config-command.js +23 -0
  17. package/dist/core/commands/pipeline-command.js +2 -1
  18. package/dist/core/decisions/child-model-choice.js +2 -1
  19. package/dist/core/decisions/cli.js +9 -2
  20. package/dist/core/hooks-runtime/hook-context.js +4 -4
  21. package/dist/core/hooks-runtime/jev-spawn-routing.js +8 -8
  22. package/dist/core/hooks-runtime/spawn-role-pin-heal.js +1 -1
  23. package/dist/core/hooks-runtime/subagent-context.js +3 -3
  24. package/dist/core/hooks-runtime/subagent-spawn-policy.js +18 -11
  25. package/dist/core/hooks-runtime.js +1 -1
  26. package/dist/core/pipeline-internals/runtime-core.js +2 -1
  27. package/dist/core/routes.js +1 -1
  28. package/dist/core/safety/command-contract/contracts.js +11 -1
  29. package/dist/core/safety/command-contract/types.js +1 -1
  30. package/dist/core/subagents/child-model-allowlist.js +107 -8
  31. package/dist/core/subagents/child-model-plan.js +18 -27
  32. package/dist/core/subagents/model-tiers.js +45 -10
  33. package/dist/core/subagents/naruto-command-args.js +48 -7
  34. package/dist/core/subagents/naruto-command-input-contract.js +40 -4
  35. package/dist/core/subagents/naruto-execution-mode.js +62 -0
  36. package/dist/core/subagents/naruto-help-contract.js +9 -0
  37. package/dist/core/subagents/naruto-host-credentials.js +24 -5
  38. package/dist/core/subagents/official-subagent-config.js +6 -6
  39. package/dist/core/subagents/official-subagent-preparation.js +8 -6
  40. package/dist/core/subagents/official-subagent-prompt.js +11 -12
  41. package/dist/core/subagents/official-subagent-runner.js +5 -8
  42. package/dist/core/subagents/read-only-list-role.js +27 -6
  43. package/dist/core/subagents/role-model-preferences.js +21 -8
  44. package/dist/core/update/update-migration-state.js +10 -0
  45. package/dist/core/version.js +1 -1
  46. package/dist/native/sks-menubar/Sources/OpenRouterOnlyState.swift +20 -4
  47. package/dist/native/sks-menubar/Sources/ProvidersModelExposure.swift +15 -4
  48. package/dist/native/sks-menubar/Sources/SettingsViewController.swift +105 -1
  49. package/dist/native/sks-menubar/Sources/SubagentModelsModels.swift +66 -11
  50. package/dist/native/sks-menubar/Sources/SubagentModelsViewController.swift +43 -22
  51. package/package.json +1 -1
package/README.md CHANGED
@@ -17,7 +17,7 @@
17
17
  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.
18
18
  <!-- END SKS SEARCH VISIBILITY MARKETING -->
19
19
 
20
- Current package: **SKS 10.5.2**. Install the latest stable release from npm.
20
+ Current package: **SKS 10.5.4**. Install the latest stable release from npm.
21
21
 
22
22
  Sneakoscope Codex (`sks`) is a local trust layer for Codex CLI and ChatGPT Desktop. It keeps agent work bounded, preserves project context, records machine-verifiable evidence, and prevents release claims that current tests or artifacts cannot support.
23
23
 
@@ -259,7 +259,7 @@ dependencies = [
259
259
 
260
260
  [[package]]
261
261
  name = "sks-core"
262
- version = "10.5.2"
262
+ version = "10.5.4"
263
263
  dependencies = [
264
264
  "globset",
265
265
  "grep-matcher",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "sks-core"
3
- version = "10.5.2"
3
+ version = "10.5.4"
4
4
  edition = "2021"
5
5
 
6
6
  [dependencies]
@@ -377,7 +377,7 @@ const COMMAND_HELP_LITE = {
377
377
  description: 'Project TriWiki context-pack memory into managed AGENTS.md blocks or run bounded memory cleanup.'
378
378
  },
379
379
  naruto: {
380
- usage: 'sks naruto run "task" [--agents N] [--max-threads N] [--trusted-project] [--json] | sks naruto status|subagents|proof [latest|M-...] [--json] | sks naruto parent-summary --mission M-... --stdin [--json]',
380
+ usage: 'sks naruto run "task" [--agents N] [--max-threads N] [--trusted-project] [--json] | sks naruto status|subagents|proof [latest|M-...] [--json] | sks naruto execution status [--json] | sks naruto execution set --mode auto|current-session|standalone [--restart] [--json] | sks naruto parent-summary --mission M-... --stdin [--json]',
381
381
  description: 'Run or inspect the Codex official subagent workflow: an orchestrating parent (latest deep-tier standalone default), children on the newest model of their tier (Jev picks the tier on spawn when Jev mode is on), max_depth=1, and structured parent-thread completion evidence.'
382
382
  },
383
383
  reasoning: {
@@ -5,6 +5,7 @@ import { ui as cliUi } from './cli-theme.js';
5
5
  import { helpResult, isHelpRequest, renderManifestHelp } from './help.js';
6
6
  import { findRetiredGlobalExecutionArgumentErrors } from './global-mode-router.js';
7
7
  import { normalizeIntentCommand, } from '../core/commands/intent-normalization/normalizer.js';
8
+ import { narutoUsesCurrentSession } from '../core/subagents/naruto-execution-mode.js';
8
9
  const routerIntentStorage = new AsyncLocalStorage();
9
10
  export function prepareRouterExecutionIntent(argv, inheritance = {}) {
10
11
  const effectiveArgv = argv.length ? [...argv] : ['doctor'];
@@ -241,9 +242,9 @@ async function ensureActiveRouteCommandGate(command, args) {
241
242
  import('../core/mission.js')
242
243
  ]);
243
244
  const root = await projectRoot(process.cwd()).catch(() => process.cwd());
244
- const appSessionKey = process.env.SKS_NARUTO_STANDALONE_CLI === '1'
245
- ? ''
246
- : String(process.env.CODEX_THREAD_ID || '').trim();
245
+ const appSessionKey = narutoUsesCurrentSession()
246
+ ? String(process.env.CODEX_THREAD_ID || '').trim()
247
+ : '';
247
248
  const state = await loadOwnedRouteState(root, appSessionKey);
248
249
  if (safeActiveRouteContinuation(command, args, state))
249
250
  return { ok: true, status: 'allowed_active_route_continuation' };
@@ -296,6 +297,9 @@ export function safeReadOnlySubcommand(command, args) {
296
297
  if (command === 'naruto' && ['status', 'subagents', 'proof'].includes(sub)) {
297
298
  return !args.some((arg) => ['--fix', '--yes', '-y', '--write', '--apply', '--execute', '--force', '--real'].includes(String(arg)));
298
299
  }
300
+ if (command === 'naruto' && sub === 'execution' && nested === 'status') {
301
+ return !args.some((arg) => ['--fix', '--yes', '-y', '--write', '--apply', '--execute', '--force', '--real'].includes(String(arg)));
302
+ }
299
303
  if (command === 'mcp' && sub === 'config' && ['list', 'test', 'backups', 'show'].includes(nested)) {
300
304
  return !args.some((arg) => ['--fix', '--yes', '-y', '--write', '--apply', '--execute', '--force', '--real'].includes(String(arg)));
301
305
  }
@@ -316,6 +320,8 @@ export function safeActiveRouteContinuation(command, args, state = {}) {
316
320
  const subcommand = String(args[0] || '').toLowerCase();
317
321
  const activeRoute = String(state.route || state.route_command || state.mode || '').replace(/^\$/, '').replace(/[-_]/g, '').toUpperCase();
318
322
  if (command === 'naruto') {
323
+ if (subcommand === 'execution' && String(args[1] || '').toLowerCase() === 'set')
324
+ return true;
319
325
  if (subcommand === 'parent-summary') {
320
326
  const explicitParentSummaryMission = optionValue(args, ['--mission']);
321
327
  return Boolean(state.mission_id)
@@ -40,7 +40,8 @@ export function usage(command = 'bridge') {
40
40
  '',
41
41
  'Provider secrets are accepted only through --api-key-stdin. Readiness changes the exit code only with --strict or --require-ready.',
42
42
  'OpenRouter Only Mode and Codex-LB authentication priority are mutually exclusive; turning one on turns the other off.',
43
- 'subagent-models set reads {"subagent_models":[{"model","criteria","reasoning_effort","default"}]} (max 16) from stdin.'
43
+ 'subagent-models set reads {"profile":"codex_lb|openai|openrouter_only","subagent_models":[{"model","criteria","reasoning_effort","default"}]} (max 16) from stdin.',
44
+ 'The profile must match the current connection. Omitting profile preserves the legacy OpenRouter-list staging behavior.'
44
45
  ].join('\n');
45
46
  }
46
47
  export async function run(_command, args = []) {
@@ -152,7 +153,7 @@ async function parseInvocation(args, io) {
152
153
  ...base,
153
154
  request: {
154
155
  operation: 'subagent-models.set',
155
- subagent_models: subagentModelsFromStdin(await io.readStdin()),
156
+ ...subagentModelsFromStdin(await io.readStdin()),
156
157
  no_restart: parsed.flags.has('--no-restart')
157
158
  },
158
159
  label: 'Subagent model list update'
@@ -376,7 +377,11 @@ function subagentModelsFromStdin(text) {
376
377
  : undefined;
377
378
  if (!Array.isArray(models))
378
379
  throw new BridgeCliError('bridge_subagent_models_stdin_invalid', 'retry_with_stdin');
379
- return models;
380
+ const profile = parsed.profile;
381
+ if (profile !== undefined && profile !== 'openrouter_only' && profile !== 'codex_lb' && profile !== 'openai') {
382
+ throw new BridgeCliError('bridge_subagent_models_profile_invalid', 'retry_with_stdin');
383
+ }
384
+ return { subagent_models: models, ...(profile === undefined ? {} : { profile }) };
380
385
  }
381
386
  function rejectSecretArgv(args) {
382
387
  for (const arg of args) {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schema": "sks.skills-manifest.v1",
3
- "package_version": "10.5.2",
3
+ "package_version": "10.5.4",
4
4
  "skills": [
5
5
  {
6
6
  "canonical_name": "sks",
@@ -14,7 +14,7 @@ import { decideSubagentModel } from '../subagents/model-policy.js';
14
14
  import { latestTierModelSet } from '../subagents/model-tiers.js';
15
15
  import { consultJevTurnModel } from '../decisions/integration.js';
16
16
  import { chooseChildModel, fallbackChildModel } from '../decisions/child-model-choice.js';
17
- import { OPENROUTER_ONLY_SCHEMA, allowlistedChildModel, effectiveChildModelAllowlist, isSubagentModelEffort } from '../subagents/child-model-allowlist.js';
17
+ import { allowlistedChildModel, effectiveChildModelAllowlist, childModelListProfile, listChildModelEffort, isSubagentModelEffort } from '../subagents/child-model-allowlist.js';
18
18
  export const NATIVE_WORKER_BACKEND_ROUTER_SCHEMA = 'sks.native-worker-backend-router.v1';
19
19
  export async function runNativeWorkerBackendRouter(input) {
20
20
  const root = path.resolve(input.agentRoot);
@@ -93,8 +93,10 @@ export async function runNativeWorkerBackendRouter(input) {
93
93
  cwd: String(input.intake.cwd || root),
94
94
  prompt: buildWorkerPrompt(input.slice),
95
95
  model: modelRouting.choice.model,
96
- reasoningEffort: modelRouting.choice.reasoning,
97
- modelReasoningEffort: modelRouting.choice.reasoning,
96
+ ...(modelRouting.choice.reasoning ? {
97
+ reasoningEffort: modelRouting.choice.reasoning,
98
+ modelReasoningEffort: modelRouting.choice.reasoning
99
+ } : {}),
98
100
  serviceTier: modelRouting.choice.serviceTier,
99
101
  inputFiles: input.intake.input_files || [],
100
102
  inputImages: input.intake.input_images || [],
@@ -232,7 +234,7 @@ export async function resolveWorkerModelRouting(input, deps = {}) {
232
234
  const category = categoryForWorkerRole(String(input.agent?.role || 'executor'), taskKindText);
233
235
  const env = deps.env || process.env;
234
236
  const allowlist = effectiveChildModelAllowlist({ env });
235
- if (allowlist.mode === 'openrouter_only') {
237
+ if (allowlist.mode !== 'tiers') {
236
238
  return resolveOpenRouterOnlyWorkerRouting(input, deps, { allowlist, category, env, taskKindText, riskText });
237
239
  }
238
240
  const lbCatalog = narutoOnly
@@ -306,11 +308,12 @@ export async function resolveWorkerModelRouting(input, deps = {}) {
306
308
  }
307
309
  async function resolveOpenRouterOnlyWorkerRouting(input, deps, ctx) {
308
310
  const { allowlist, category, env } = ctx;
309
- const state = { schema: OPENROUTER_ONLY_SCHEMA, enabled: true, subagent_models: allowlist.entries, restore: null, updated_at: null };
311
+ const state = { subagent_models: allowlist.entries };
310
312
  const explicitModel = String(env.SKS_WORKER_MODEL || env.SKS_AGENT_MODEL || '').trim();
311
- const explicitReasoning = listReasoning(env.SKS_WORKER_REASONING || env.SKS_WORKER_MODEL_REASONING);
313
+ const rawEffort = env.SKS_WORKER_REASONING || env.SKS_WORKER_MODEL_REASONING;
314
+ const explicitReasoning = allowlist.mode === 'openrouter_only' ? listReasoning(rawEffort) : normalizeModelReasoning(rawEffort);
312
315
  const explicitTier = normalizeServiceTier(String(env.SKS_WORKER_SERVICE_TIER || env.SKS_SERVICE_TIER || '').trim());
313
- const planned = /^openrouter_only_/.test(String(input.agent?.routed_model_policy || ''))
316
+ const planned = /^(openrouter_only|configured_list)_/.test(String(input.agent?.routed_model_policy || ''))
314
317
  ? allowlistedChildModel(input.agent?.routed_model, allowlist)
315
318
  : null;
316
319
  const requested = String(input.agent?.routed_model || input.agent?.model || '').trim() || null;
@@ -322,18 +325,23 @@ async function resolveOpenRouterOnlyWorkerRouting(input, deps, ctx) {
322
325
  ? fallbackChildModel(state, planned, 'planned')
323
326
  : deps.consultJev === false || !task
324
327
  ? fallbackChildModel(state, requested, deps.consultJev === false ? 'jev_skipped' : 'empty_task')
325
- : await chooseChildModel({ root: deps.root || process.cwd(), task, role: input.agent?.role || null, requestedModel: requested, state, env })
328
+ : await chooseChildModel({ root: deps.root || process.cwd(), task, role: input.agent?.role || null, requestedModel: requested, state, env, profile: childModelListProfile(allowlist) })
326
329
  .catch(() => fallbackChildModel(state, requested, 'consult_failed'));
327
330
  const taskPolicy = decideSubagentModel({ title: ctx.taskKindText, description: ctx.riskText, role: input.agent?.role });
328
331
  const choiceModel = choice?.entry.model || '';
329
332
  const routed = {
330
333
  model: choiceModel,
331
- reasoning: explicitReasoning || choice?.entry.reasoning_effort || listReasoning(taskPolicy.modelReasoningEffort) || 'medium',
334
+ reasoning: allowlist.mode === 'openrouter_only'
335
+ ? explicitReasoning || choice?.entry.reasoning_effort || listReasoning(taskPolicy.modelReasoningEffort) || 'medium'
336
+ : explicitReasoning || (choice ? listChildModelEffort(choice.entry, null) : null),
332
337
  serviceTier: explicitTier || input.fastModePolicy.service_tier || 'fast'
333
338
  };
334
339
  const blockers = [
335
- ...(!allowlist.entries.length ? ['openrouter_only_subagent_list_empty'] : []),
336
- ...(explicitModel && !explicitListed ? ['openrouter_only_subagent_model_not_listed'] : []),
340
+ ...(!allowlist.entries.length ? [allowlist.mode === 'openrouter_only' ? 'openrouter_only_subagent_list_empty' : 'configured_subagent_list_unavailable'] : []),
341
+ ...(explicitModel && !explicitListed ? [allowlist.mode === 'openrouter_only' ? 'openrouter_only_subagent_model_not_listed' : 'configured_subagent_model_not_listed'] : []),
342
+ ...(allowlist.mode === 'configured' && rawEffort
343
+ && (!explicitReasoning || !choice?.entry.supported_reasoning_efforts?.includes(explicitReasoning))
344
+ ? ['configured_subagent_effort_unsupported'] : []),
337
345
  ...(!choiceModel ? ['naruto_worker_model_unavailable'] : [])
338
346
  ];
339
347
  const why = choice ? `${choice.source}${choice.reason === 'applied' ? '' : `:${choice.reason}`}` : 'blocked';
@@ -343,7 +351,7 @@ async function resolveOpenRouterOnlyWorkerRouting(input, deps, ctx) {
343
351
  explicit: Boolean(explicitModel),
344
352
  lb_catalog: null,
345
353
  blockers: [...new Set(blockers)],
346
- reason: `${category}->${choiceModel || 'blocked'}@${routed.reasoning} (openrouter only list: ${why})`
354
+ reason: `${category}->${choiceModel || 'blocked'}@${routed.reasoning || 'default'} (${allowlist.mode === 'openrouter_only' ? 'openrouter only' : allowlist.profile} list: ${why})`
347
355
  };
348
356
  }
349
357
  function listReasoning(value) {
@@ -1,5 +1,9 @@
1
1
  import { isCodexAppRunningByBundleId } from './menubar/config.js';
2
2
  import { restartCodexApp } from './codex-app-restart.js';
3
+ export const CODEX_RESTART_FAILED_BLOCKER = 'codex_restart_failed_manual_restart_required';
4
+ export function codexRestartBlockers(restart) {
5
+ return restart?.attempted && !restart.ok ? [CODEX_RESTART_FAILED_BLOCKER] : [];
6
+ }
3
7
  export async function maybeRestartRunningCodexApp(input) {
4
8
  const skipped = (reason) => ({ attempted: false, running: null, status: 'skipped', reason, ok: true, blockers: [] });
5
9
  if (input.noRestart)
@@ -1,5 +1,6 @@
1
1
  import { readText, writeJsonAtomic } from '../../fsx.js';
2
2
  import { readTopLevelTomlString } from '../../codex-app/codex-model-catalog.js';
3
+ import { codexRestartBlockers, maybeRestartRunningCodexApp } from '../../codex-app/codex-app-restart-policy.js';
3
4
  import { BRIDGE_MODEL_SELECTION_SCHEMA, MAX_SELECTED_OPENROUTER_MODELS, bridgeAvailableModelsPath, pruneSelection, readBridgeModelSelectionState, writeBridgeModelSelection } from '../combined-catalog/model-selection.js';
4
5
  import { listOpenRouterModels } from '../../providers/openrouter/openrouter-account.js';
5
6
  import { buildCombinedBridgeCatalog, readActiveCombinedBridgeCatalog, stageCombinedBridgeCatalog } from '../combined-catalog.js';
@@ -338,6 +339,7 @@ export async function listSelectableModels(options) {
338
339
  }
339
340
  export async function selectExposedModels(publicIds, options) {
340
341
  const paths = controllerPaths(options);
342
+ const before = await readBridgeModelSelectionState(paths.home, nowIso(options));
341
343
  const requested = [...new Set(publicIds.map((id) => String(id || '').trim()).filter(Boolean))].sort();
342
344
  if (requested.length > MAX_SELECTED_OPENROUTER_MODELS) {
343
345
  throw new Error('bridge_model_selection_limit_exceeded');
@@ -352,7 +354,19 @@ export async function selectExposedModels(publicIds, options) {
352
354
  const activation = sync.activation && typeof sync.activation === 'object'
353
355
  ? sync.activation
354
356
  : {};
355
- return commandResult('models.select', sync.ok === true, status, { selected_count: requested.length, catalog_sync: sync }, sync.ok === true ? [] : stringArray(activation.blockers), options);
357
+ const changed = before.selection.openrouter.public_ids.join('\0') !== requested.join('\0');
358
+ const restart = sync.ok === true
359
+ ? await maybeRestartRunningCodexApp({
360
+ env: controllerEnv(options),
361
+ changed,
362
+ noRestart: false,
363
+ ...(options.platform ? { platform: options.platform } : {}),
364
+ ...(options.codexAppRunningImpl ? { isRunningImpl: options.codexAppRunningImpl } : {}),
365
+ ...(options.codexAppRestartImpl ? { restartImpl: options.codexAppRestartImpl } : {})
366
+ })
367
+ : null;
368
+ const restartBlockers = codexRestartBlockers(restart);
369
+ return commandResult('models.select', sync.ok === true && restartBlockers.length === 0, status, { selected_count: requested.length, changed, catalog_sync: sync, codex_restart: restart }, [...(sync.ok === true ? [] : stringArray(activation.blockers)), ...restartBlockers], options);
356
370
  }
357
371
  function seedSelectionFromConfiguredModel(selection, configText) {
358
372
  const configured = readTopLevelTomlString(configText, 'model');
@@ -8,7 +8,8 @@ import { DESKTOP_BRIDGE_OFFICIAL_UPSTREAM_BASE_URL } from '../desktop-bridge/ind
8
8
  import { resolveBridgeRequestRoute } from '../request-route-resolver.js';
9
9
  import { applyOfficialModelPassthrough, buildBridgeRoutingPolicy, setBridgeRoutingDefault, writeBridgeRoutingPolicy } from '../provider-route-policy.js';
10
10
  import { syncCatalogInternal } from './catalog.js';
11
- import { codexRestartBlockers, releaseOpenRouterOnlyForAuthPriority, releaseOpenRouterOnlyWithoutBridge, restartCodexAfterOpenRouterOnlyOff } from './openrouter-only-commands.js';
11
+ import { codexRestartBlockers } from '../../codex-app/codex-app-restart-policy.js';
12
+ import { releaseOpenRouterOnlyForAuthPriority, releaseOpenRouterOnlyWithoutBridge, restartCodexAfterOpenRouterOnlyOff } from './openrouter-only-commands.js';
12
13
  import { openRouterOnlyStatus } from './openrouter-only-status.js';
13
14
  import { commandResult, controllerPaths, nowIso, persistRuntimeSettings, providerCode, providerRegistrySnapshot, stringArray } from './shared.js';
14
15
  import { desktopBridgeStatusV3, loadCore, statusFromCore } from './status.js';
@@ -1,8 +1,9 @@
1
1
  import { readTopLevelTomlString } from '../../codex-app/codex-model-catalog.js';
2
- import { maybeRestartRunningCodexApp } from '../../codex-app/codex-app-restart-policy.js';
2
+ import { codexRestartBlockers, maybeRestartRunningCodexApp } from '../../codex-app/codex-app-restart-policy.js';
3
3
  import { readText } from '../../fsx.js';
4
4
  import { isOpenRouterModelId } from '../../imagegen/imagegen-config.js';
5
- import { MAX_SUBAGENT_MODELS, normalizeSubagentModelList, readOpenRouterOnlyStateSync, writeOpenRouterOnlyState } from '../../subagents/child-model-allowlist.js';
5
+ import { MAX_SUBAGENT_MODELS, normalizeSubagentModelList, readOpenRouterOnlyStateSync, readNativeSubagentModelStore, writeNativeSubagentModels, writeOpenRouterOnlyState } from '../../subagents/child-model-allowlist.js';
6
+ import { selectableChildModels } from '../../subagents/model-tiers.js';
6
7
  import { assertDesktopBridgeStatusV3 } from '../bridge-runtime-validation.js';
7
8
  import { readActiveCombinedBridgeCatalog } from '../combined-catalog.js';
8
9
  import { MAX_SELECTED_OPENROUTER_MODELS, bridgeAvailableModelsPath, readBridgeModelSelectionState, writeBridgeModelSelection } from '../combined-catalog/model-selection.js';
@@ -19,12 +20,43 @@ async function finish(operation, ok, blockers, extra, options) {
19
20
  const status = statusFromCore(core, options);
20
21
  assertDesktopBridgeStatusV3(status);
21
22
  const openRouterOnly = await openRouterOnlyStatusFromCore(core, options);
23
+ const subagentSettings = operation.startsWith('subagent-models.')
24
+ ? await subagentModelSettings(core, openRouterOnly)
25
+ : null;
22
26
  return commandResult(operation, ok, status, {
23
27
  openrouter_only: openRouterOnly,
24
28
  auth_priority: status.auth_priority,
25
- ...extra
29
+ ...extra,
30
+ ...(subagentSettings ? { subagent_model_settings: subagentSettings, available: subagentSettings.available } : {})
26
31
  }, blockers, options);
27
32
  }
33
+ async function subagentModelSettings(core, openRouterOnly) {
34
+ if (openRouterOnly.enabled) {
35
+ return {
36
+ schema: 'sks.subagent-model-settings.v1', profile: 'openrouter_only',
37
+ configured: true, editable: true, error: openRouterOnly.error,
38
+ subagent_models: openRouterOnly.subagent_models,
39
+ jev_enabled: openRouterOnly.jev_enabled, warnings: openRouterOnly.warnings,
40
+ available: (await availableOpenRouterModels(core.paths.home)).map((row) => ({ ...row, reasoning_efforts: ['low', 'medium', 'high', 'xhigh'] }))
41
+ };
42
+ }
43
+ const profile = core.authPriorityEnabled ? 'codex_lb' : 'openai';
44
+ const location = openRouterOnlyStoreLocation(core.paths);
45
+ const saved = readNativeSubagentModelStore(location);
46
+ const available = selectableChildModels(profile, location);
47
+ const entries = saved.store.profiles[profile];
48
+ return {
49
+ schema: 'sks.subagent-model-settings.v1', profile,
50
+ configured: entries.length > 0, editable: saved.blockers.length === 0,
51
+ error: saved.blockers[0] ?? (available.length ? null : 'subagent_model_catalog_unavailable'),
52
+ subagent_models: entries.map((entry) => ({
53
+ ...entry,
54
+ routable: available.some((row) => row.public_id === entry.model
55
+ && (!entry.reasoning_effort || row.reasoning_efforts.includes(entry.reasoning_effort)))
56
+ })),
57
+ jev_enabled: openRouterOnly.jev_enabled, warnings: saved.blockers, available
58
+ };
59
+ }
28
60
  function syncBlockers(sync) {
29
61
  const activation = sync.activation && typeof sync.activation === 'object' && !Array.isArray(sync.activation)
30
62
  ? sync.activation
@@ -32,11 +64,7 @@ function syncBlockers(sync) {
32
64
  const blockers = stringArray(activation.blockers);
33
65
  return blockers.length > 0 ? blockers : ['combined_catalog_sync_failed'];
34
66
  }
35
- export const CODEX_RESTART_FAILED_BLOCKER = 'codex_restart_failed_manual_restart_required';
36
67
  export const CODEX_RELAUNCH_FOR_SUBAGENT_MODELS_WARNING = 'codex_relaunch_required_for_new_subagent_models';
37
- export function codexRestartBlockers(restart) {
38
- return restart?.attempted && !restart.ok ? [CODEX_RESTART_FAILED_BLOCKER] : [];
39
- }
40
68
  function restartCodex(changed, noRestart, options) {
41
69
  return maybeRestartRunningCodexApp({
42
70
  env: controllerEnv(options),
@@ -241,23 +269,55 @@ async function availableOpenRouterModels(home) {
241
269
  }
242
270
  }
243
271
  export async function listSubagentModels(options) {
244
- const available = await availableOpenRouterModels(controllerPaths(options).home);
245
- return finish('subagent-models.list', true, [], { available }, options);
272
+ return finish('subagent-models.list', true, [], {}, options);
246
273
  }
247
- export async function setSubagentModels(raw, noRestart, options) {
274
+ export async function setSubagentModels(raw, noRestart, options, expectedProfile) {
248
275
  if (!Array.isArray(raw))
249
276
  return finish('subagent-models.set', false, ['subagent_models_payload_invalid'], {}, options);
250
- const { entries, issues } = normalizeSubagentModelList(raw);
277
+ const core = await loadCore(options);
278
+ const activeProfile = core.openRouterOnlyEnabled ? 'openrouter_only' : core.authPriorityEnabled ? 'codex_lb' : 'openai';
279
+ if (expectedProfile && activeProfile !== expectedProfile) {
280
+ return finish('subagent-models.set', false, ['subagent_model_profile_changed'], {}, options);
281
+ }
282
+ const profile = expectedProfile ?? 'openrouter_only';
283
+ const { entries, issues } = normalizeSubagentModelList(raw, profile);
251
284
  if (issues.length > 0) {
252
285
  return finish('subagent-models.set', false, issues.map((issue) => `${issue.code}:${issue.index}`), {}, options);
253
286
  }
254
287
  const paths = controllerPaths(options);
255
288
  const location = openRouterOnlyStoreLocation(paths);
256
289
  const before = readOpenRouterOnlyStateSync(location);
257
- if (!before.enabled) {
290
+ if (!before.enabled && profile === 'openrouter_only') {
258
291
  await writeOpenRouterOnlyState({ subagent_models: entries }, location);
259
292
  return finish('subagent-models.set', true, [], { catalog_sync: null }, options);
260
293
  }
294
+ if (profile !== 'openrouter_only') {
295
+ const nativeProfile = core.authPriorityEnabled ? 'codex_lb' : 'openai';
296
+ const available = selectableChildModels(nativeProfile, location);
297
+ const blockers = entries.flatMap((entry, index) => {
298
+ const model = available.find((row) => row.public_id === entry.model);
299
+ return !model ? [`subagent_model_not_available:${index}`]
300
+ : entry.reasoning_effort && !model.reasoning_efforts.includes(entry.reasoning_effort)
301
+ ? [`subagent_model_effort_unsupported:${index}`] : [];
302
+ });
303
+ const saved = readNativeSubagentModelStore(location);
304
+ if (saved.blockers.length || blockers.length) {
305
+ return finish('subagent-models.set', false, [...saved.blockers, ...blockers], {}, options);
306
+ }
307
+ const role = entries.length ? await ensureUserReadOnlyListRole(paths.home) : 'unchanged';
308
+ if (role === 'preserved_user_file') {
309
+ return finish('subagent-models.set', false, ['subagent_model_read_only_role_unavailable'], {}, options);
310
+ }
311
+ await writeNativeSubagentModels(nativeProfile, entries, location);
312
+ const changed = JSON.stringify(saved.store.profiles[nativeProfile]) !== JSON.stringify(entries);
313
+ const roleChanged = role === 'created' || role === 'updated';
314
+ const restart = await restartCodex(roleChanged, noRestart, options);
315
+ const relaunchPending = roleChanged && !restart.attempted && restart.reason !== 'codex_not_running';
316
+ return finish('subagent-models.set', true, codexRestartBlockers(restart), {
317
+ changed, catalog_sync: null, read_only_role: role, codex_restart: restart,
318
+ warnings: relaunchPending ? [CODEX_RELAUNCH_FOR_SUBAGENT_MODELS_WARNING] : []
319
+ }, options);
320
+ }
261
321
  const exposedBefore = await exposedModelIds(paths);
262
322
  const sync = await syncCatalogInternal(options, {
263
323
  openRouterOnly: { ...before, subagent_models: entries },
@@ -73,7 +73,7 @@ export async function executeDesktopBridgeCommandV3(request, options = {}) {
73
73
  return setOpenRouterOnly(request.enabled, request.no_restart === true, options);
74
74
  }
75
75
  if (request.operation === 'subagent-models.set') {
76
- return setSubagentModels(request.subagent_models, request.no_restart === true, options);
76
+ return setSubagentModels(request.subagent_models, request.no_restart === true, options, request.profile);
77
77
  }
78
78
  if (request.operation === 'route.official-models')
79
79
  return setOfficialModelsMode(request.mode, options);
@@ -9,6 +9,7 @@ import { maybeFinalizeRoute } from '../proof/auto-finalize.js';
9
9
  import { sksPrefixedDollarCommand } from '../routes/dollar-prefix.js';
10
10
  import { closeWorkOrderLedgerForRouteResult } from '../work-order-ledger.js';
11
11
  import { flag, positionalArgs, promptOf, warnOnMultipleActiveSessions } from './command-utils.js';
12
+ import { narutoUsesCurrentSession } from '../subagents/naruto-execution-mode.js';
12
13
  const ACTIONS = new Set(['prepare', 'run', 'status', 'proof', 'fixture', 'help', '--help', '-h']);
13
14
  export async function alignCommand(sub = 'prepare', args = []) {
14
15
  const action = String(sub || 'prepare').toLowerCase();
@@ -262,7 +263,7 @@ async function closeVerifiedAlignRoute(root, missionId, sessionKey) {
262
263
  return closed?.ok === true;
263
264
  }
264
265
  function ownerSessionKey() {
265
- if (process.env.SKS_NARUTO_STANDALONE_CLI === '1')
266
+ if (!narutoUsesCurrentSession())
266
267
  return null;
267
268
  return String(process.env.CODEX_THREAD_ID || '').trim() || null;
268
269
  }
@@ -16,6 +16,7 @@ import { effectiveSubagentTarget, refreshSubagentWaveLifecycle } from '../subage
16
16
  import { HOST_CAPABILITY_HOOK_RUNTIME_FILENAME, bindParentSummaryToHostCapabilityEvidence, createHostCapabilityHookRuntimeBinding, resolveHostCapabilityHookRuntimeBinding } from '../agent-bridge/host-capability-runtime.js';
17
17
  import { renderHostCapabilityBlockedLines } from '../agent-bridge/host-capability-policy.js';
18
18
  import { uniqueStrings } from '../text/strings.js';
19
+ import { configureNarutoExecution } from './naruto-config-command.js';
19
20
  import { completeNarutoTerminalBundle, refreshOfficialSubagentCompletionArtifacts } from '../hooks-runtime/official-subagent-lifecycle.js';
20
21
  export { buildNarutoGateResult } from '../subagents/official-subagent-preparation.js';
21
22
  export { attachNarutoLaunchDiagnostics } from '../subagents/naruto-launch-diagnostics.js';
@@ -65,10 +66,53 @@ export async function narutoCommand(commandOrArgs = 'naruto', maybeArgs = []) {
65
66
  return narutoSubagents(parsed);
66
67
  if (parsed.action === 'proof')
67
68
  return narutoProof(parsed);
69
+ if (parsed.action === 'execution')
70
+ return narutoExecution(parsed);
68
71
  if (parsed.action === 'parent-summary')
69
72
  return narutoParentSummary(parsed);
70
73
  return narutoRun(parsed);
71
74
  }
75
+ async function narutoExecution(parsed) {
76
+ const configuration = await configureNarutoExecution({
77
+ ...(parsed.executionMode ? { mode: parsed.executionMode } : {}),
78
+ restart: parsed.restart
79
+ });
80
+ const result = {
81
+ schema: 'sks.naruto-execution-result.v1',
82
+ ok: configuration.ok,
83
+ action: 'execution',
84
+ command: parsed.executionCommand,
85
+ mode: configuration.mode,
86
+ stored: configuration.stored,
87
+ changed: configuration.changed,
88
+ path: configuration.path,
89
+ blockers: configuration.blockers,
90
+ restart: configuration.restart
91
+ };
92
+ return emit(parsed, result, () => {
93
+ if (!configuration.ok) {
94
+ for (const blocker of configuration.blockers)
95
+ console.error(`Naruto execution setting blocked: ${blocker}`);
96
+ return;
97
+ }
98
+ if (parsed.executionCommand === 'status') {
99
+ console.log(`Naruto execution mode: ${configuration.mode} (${configuration.stored ? 'saved' : 'default'})`);
100
+ console.log(`Preference: ${configuration.path}`);
101
+ return;
102
+ }
103
+ const restart = configuration.restart;
104
+ const suffix = restart?.status === 'restarted'
105
+ ? ' Codex was restarted; start a new task.'
106
+ : restart?.reason === 'codex_not_running'
107
+ ? ' Codex is not running; the setting applies on its next launch.'
108
+ : restart?.reason === 'not_macos'
109
+ ? ' The setting is saved; restart Codex on macOS to apply it.'
110
+ : restart?.status === 'skipped'
111
+ ? ` The setting is saved; restart skipped (${restart.reason || 'policy'}).`
112
+ : '';
113
+ console.log(`Naruto execution mode saved: ${configuration.mode}.${suffix}`);
114
+ }, !configuration.ok);
115
+ }
72
116
  async function narutoParentSummary(parsed) {
73
117
  const root = await sksRoot();
74
118
  const appSession = detectCodexAppSession();
@@ -0,0 +1,23 @@
1
+ import { codexRestartBlockers, maybeRestartRunningCodexApp } from '../codex-app/codex-app-restart-policy.js';
2
+ import { readNarutoExecutionMode, writeNarutoExecutionMode } from '../subagents/naruto-execution-mode.js';
3
+ export async function configureNarutoExecution(input) {
4
+ const env = input.env || process.env;
5
+ try {
6
+ const preference = input.mode
7
+ ? await writeNarutoExecutionMode(input.mode, env)
8
+ : { ...readNarutoExecutionMode(env), changed: false };
9
+ if (preference.blockers.length)
10
+ return { ...preference, ok: false, restart: null };
11
+ const restart = input.mode && input.restart
12
+ ? await maybeRestartRunningCodexApp({ env, changed: true, noRestart: false, ...input.restartOptions })
13
+ : null;
14
+ const blockers = restart && !restart.ok
15
+ ? [...restart.blockers, ...codexRestartBlockers(restart)]
16
+ : [];
17
+ return { ...preference, ok: blockers.length === 0, blockers, restart };
18
+ }
19
+ catch (error) {
20
+ return { ...readNarutoExecutionMode(env), ok: false, changed: false, restart: null,
21
+ blockers: [error instanceof Error ? error.message : 'naruto_execution_config_failed'] };
22
+ }
23
+ }
@@ -7,10 +7,11 @@ import { PIPELINE_PLAN_ARTIFACT, pipelinePlanState, projectGateStatus, sealRoute
7
7
  import { routePrompt } from '../routes.js';
8
8
  import { positionalArgs } from '../../cli/args.js';
9
9
  import { flag, readFlagValue, resolveMissionId } from './command-utils.js';
10
+ import { narutoUsesCurrentSession } from '../subagents/naruto-execution-mode.js';
10
11
  export async function pipelineCommand(args = []) {
11
12
  const root = await projectRoot();
12
13
  const action = args[0] || 'status';
13
- const sessionKey = process.env.SKS_NARUTO_STANDALONE_CLI === '1' ? '' : process.env.CODEX_THREAD_ID;
14
+ const sessionKey = narutoUsesCurrentSession() ? process.env.CODEX_THREAD_ID : '';
14
15
  const state = await loadOwnedRouteState(root, sessionKey);
15
16
  const sessions = await listSessionStates(root);
16
17
  if (action === 'status') {
@@ -72,7 +72,7 @@ export async function chooseChildModels(input) {
72
72
  workflowId: input.workflowId,
73
73
  goal: input.goal,
74
74
  questions,
75
- facts: { openrouter_only: true, subagent_models: entries.map((entry) => canonicalChildModelId(entry.model)) },
75
+ facts: { openrouter_only: (input.profile ?? 'openrouter_only') === 'openrouter_only', subagent_model_profile: input.profile ?? 'openrouter_only', subagent_models: entries.map((entry) => canonicalChildModelId(entry.model)) },
76
76
  ...(input.env ? { env: input.env } : {}),
77
77
  ...(input.deadlineMs === undefined ? {} : { deadlineMs: input.deadlineMs })
78
78
  })
@@ -100,6 +100,7 @@ export async function chooseChildModel(input) {
100
100
  goal: input.task || 'Spawn a child agent.',
101
101
  lanes: [{ id: 'spawn', task: input.task, role: input.role ?? null, requestedModel: input.requestedModel ?? null }],
102
102
  state: input.state,
103
+ ...(input.profile ? { profile: input.profile } : {}),
103
104
  ...(input.env ? { env: input.env } : {}),
104
105
  ...(input.deadlineMs === undefined ? {} : { deadlineMs: input.deadlineMs })
105
106
  });
@@ -145,9 +145,10 @@ export async function statusReport(env) {
145
145
  : 'ready';
146
146
  const childModels = effectiveChildModelAllowlist({ env });
147
147
  const openRouterOnly = childModels.mode === 'openrouter_only';
148
+ const listMode = childModels.mode !== 'tiers';
148
149
  const childTierPoints = new Set(['spawn_tier', 'worker_tier', 'role_tiers', 'role_omission']);
149
150
  const decisionPoints = ['turn_route', 'turn_tier', 'spawn_tier', 'worker_tier', 'role_tiers', 'role_omission', 'plan', 'context', 'parent_edit_delegation', 'image_need', 'image_parameters', 'qa_effort_escalation']
150
- .filter((point) => !openRouterOnly || !childTierPoints.has(point));
151
+ .filter((point) => !listMode || !childTierPoints.has(point));
151
152
  return {
152
153
  schema: 'sks.jev-decision-status.v1',
153
154
  ok: true,
@@ -167,8 +168,14 @@ export async function statusReport(env) {
167
168
  recovery: config.capabilities.recovery
168
169
  },
169
170
  decision_points: jevEnabled(config)
170
- ? [...decisionPoints, ...(openRouterOnly ? ['child_model'] : [])]
171
+ ? [...decisionPoints, ...(listMode ? ['child_model'] : [])]
171
172
  : [],
173
+ subagent_model_list: {
174
+ active: listMode,
175
+ profile: childModels.mode === 'configured' ? childModels.profile : openRouterOnly ? 'openrouter_only' : null,
176
+ models: listMode ? childModels.models : [],
177
+ default_model: childModels.default_model
178
+ },
172
179
  openrouter_only: {
173
180
  enabled: openRouterOnly,
174
181
  subagent_model_count: openRouterOnly ? childModels.models.length : 0,
@@ -9,7 +9,7 @@ import { renderAuthoritativeSksSkillContext } from '../codex-native/sks-skill-pa
9
9
  import { resolveManagedSkillSourcesForAdmission } from './managed-skill-admission.js';
10
10
  import { managedSkillDigestBlocksEnforced } from '../verification-profile.js';
11
11
  import { looksLikeActiveContinuationPrompt } from './naruto-decision-gate.js';
12
- import { effectiveChildModelAllowlist } from '../subagents/child-model-allowlist.js';
12
+ import { effectiveChildModelAllowlist, childModelListLabel } from '../subagents/child-model-allowlist.js';
13
13
  import { EXCLUSIVE_SURFACE_SPAWN_LINE } from '../subagents/exclusive-surface-rule.js';
14
14
  import { renderChildModelCriteria, SUBAGENT_MODELS_SETTINGS_HINT } from './subagent-spawn-policy.js';
15
15
  import { extractUserPrompt, looksLikeCodexGitAction, looksLikeCodexUiSettingsEvent } from './payload-signals.js';
@@ -31,12 +31,12 @@ const OFFICIAL_SUBAGENT_SPAWN_COMPATIBILITY_CONTEXT = [
31
31
  ].join('\n');
32
32
  function openRouterOnlyChildModelLine(allowlist) {
33
33
  if (!allowlist.models.length) {
34
- return `- SKS OpenRouter Only mode is on and the subagent model list is empty: every spawn_agent call is denied until the user adds models in ${SUBAGENT_MODELS_SETTINGS_HINT}.`;
34
+ return `- SKS ${childModelListLabel(allowlist)} mode is on and the subagent model list is empty: every spawn_agent call is denied until the user adds models in ${SUBAGENT_MODELS_SETTINGS_HINT}.`;
35
35
  }
36
- return `- SKS OpenRouter Only mode is on: every child runs a model from the user's subagent list, and the SKS PreToolUse hook denies any other model. The list, with the user's rule for each model: ${renderChildModelCriteria(allowlist)}. The hook routes each spawn_agent call to a list model (Jev picks by these rules when Jev mode is on; otherwise a listed requested model, else the default), so pass \`fork_turns="none"\` or a positive bounded turn count and do not tune model or effort yourself.`;
36
+ return `- SKS ${childModelListLabel(allowlist)} mode is on: every child runs a model from the user's subagent list, and the SKS PreToolUse hook denies any other model. The list, with the user's rule for each model: ${renderChildModelCriteria(allowlist)}. The hook routes each spawn_agent call to a list model (Jev picks by these rules when Jev mode is on; otherwise a listed requested model, else the default), so pass \`fork_turns="none"\` or a positive bounded turn count and do not tune model or effort yourself.`;
37
37
  }
38
38
  export function officialSubagentSpawnCompatibilityContext(allowlist = effectiveChildModelAllowlist()) {
39
- if (allowlist.mode !== 'openrouter_only')
39
+ if (allowlist.mode === 'tiers')
40
40
  return OFFICIAL_SUBAGENT_SPAWN_COMPATIBILITY_CONTEXT;
41
41
  return [...OFFICIAL_SUBAGENT_SPAWN_CONTRACT_HEAD, openRouterOnlyChildModelLine(allowlist)].join('\n');
42
42
  }