sneakoscope 10.5.2 → 10.5.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 (28) 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/config/skills-manifest.json +1 -1
  7. package/dist/core/codex-app/codex-app-restart-policy.js +4 -0
  8. package/dist/core/codex-lb/desktop-controller-v3/catalog.js +15 -1
  9. package/dist/core/codex-lb/desktop-controller-v3/lifecycle-commands.js +2 -1
  10. package/dist/core/codex-lb/desktop-controller-v3/openrouter-only-commands.js +1 -5
  11. package/dist/core/commands/align-command.js +2 -1
  12. package/dist/core/commands/naruto-command.js +44 -0
  13. package/dist/core/commands/naruto-config-command.js +23 -0
  14. package/dist/core/commands/pipeline-command.js +2 -1
  15. package/dist/core/pipeline-internals/runtime-core.js +2 -1
  16. package/dist/core/routes.js +1 -1
  17. package/dist/core/safety/command-contract/contracts.js +11 -1
  18. package/dist/core/safety/command-contract/types.js +1 -1
  19. package/dist/core/subagents/naruto-command-args.js +48 -7
  20. package/dist/core/subagents/naruto-command-input-contract.js +40 -4
  21. package/dist/core/subagents/naruto-execution-mode.js +62 -0
  22. package/dist/core/subagents/naruto-help-contract.js +9 -0
  23. package/dist/core/subagents/official-subagent-runner.js +2 -5
  24. package/dist/core/update/update-migration-state.js +10 -0
  25. package/dist/core/version.js +1 -1
  26. package/dist/native/sks-menubar/Sources/ProvidersModelExposure.swift +15 -4
  27. package/dist/native/sks-menubar/Sources/SettingsViewController.swift +105 -1
  28. 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.3**. 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.3"
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.3"
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)
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schema": "sks.skills-manifest.v1",
3
- "package_version": "10.5.2",
3
+ "package_version": "10.5.3",
4
4
  "skills": [
5
5
  {
6
6
  "canonical_name": "sks",
@@ -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,5 +1,5 @@
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
5
  import { MAX_SUBAGENT_MODELS, normalizeSubagentModelList, readOpenRouterOnlyStateSync, writeOpenRouterOnlyState } from '../../subagents/child-model-allowlist.js';
@@ -32,11 +32,7 @@ function syncBlockers(sync) {
32
32
  const blockers = stringArray(activation.blockers);
33
33
  return blockers.length > 0 ? blockers : ['combined_catalog_sync_failed'];
34
34
  }
35
- export const CODEX_RESTART_FAILED_BLOCKER = 'codex_restart_failed_manual_restart_required';
36
35
  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
36
  function restartCodex(changed, noRestart, options) {
41
37
  return maybeRestartRunningCodexApp({
42
38
  env: controllerEnv(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') {
@@ -29,6 +29,7 @@ import { classifyTaskProfile, gateProfileForTask } from '../runtime/task-profile
29
29
  import { chooseVerificationBudget } from '../runtime/verification-budget.js';
30
30
  import { stopFinalizationRitualsEnforced } from '../verification-profile.js';
31
31
  import { HARD_NARUTO_MAX_THREADS } from '../subagents/thread-budget.js';
32
+ import { narutoUsesCurrentSession } from '../subagents/naruto-execution-mode.js';
32
33
  import { NARUTO_GATE_FILENAME, NARUTO_SUMMARY_FILENAME, SUBAGENT_PLAN_FILENAME, prepareOfficialSubagentMission } from '../subagents/official-subagent-preparation.js';
33
34
  import { buildSubagentEvidence, readSubagentEvents, recordSubagentEvent, SUBAGENT_EVIDENCE_FILENAME, SUBAGENT_EVENT_LOG_FILENAME, SUBAGENT_PARENT_SUMMARY_FILENAME } from '../subagents/subagent-evidence.js';
34
35
  import { effectiveSubagentTarget, normalizeLegacySubagentCountFields, subagentCountContractBlockers } from '../subagents/wave-lifecycle.js';
@@ -601,7 +602,7 @@ export async function prepareRoute(root, prompt, state = {}, opts = {}) {
601
602
  ...selectedRoute,
602
603
  requiredSkills: managedSkillNamesForPrompt(selectedRoute, cleanPrompt)
603
604
  } : null;
604
- const codexThreadId = String(process.env.CODEX_THREAD_ID || '').trim();
605
+ const codexThreadId = narutoUsesCurrentSession() ? String(process.env.CODEX_THREAD_ID || '').trim() : '';
605
606
  const sessionKey = opts.sessionKey || codexThreadId || state?._session_key || null;
606
607
  const madSksAuthorization = hasMadSksSignal(cleanPrompt);
607
608
  const task = stripDollarCommand(stripMadSksSignal(cleanPrompt)) || stripMadSksSignal(stripDollarCommand(cleanPrompt)) || String(cleanPrompt || '').trim();
@@ -206,7 +206,7 @@ export const ROUTES = [
206
206
  reasoningPolicy: 'high',
207
207
  stopGate: 'naruto-gate.json',
208
208
  coverage_required: true,
209
- cliEntrypoint: 'sks naruto run "task" [--agents N] [--max-threads N] [--trusted-project] | sks naruto status|subagents|proof | sks naruto parent-summary --mission M-... --stdin',
209
+ cliEntrypoint: 'sks naruto run "task" [--agents N] [--max-threads N] [--trusted-project] | sks naruto status|subagents|proof | sks naruto execution status | sks naruto execution set --mode auto|current-session|standalone [--restart] | sks naruto parent-summary --mission M-... --stdin',
210
210
  examples: ['$Naruto run review twelve independent packages with --agents 12', '$Work']
211
211
  },
212
212
  {
@@ -4,12 +4,22 @@ const ARGUMENT_BUILDERS = {
4
4
  none: () => [],
5
5
  'json-only': jsonFlag,
6
6
  naruto: (input) => {
7
+ const action = stringValue(input.action, input.prompt || input.task ? 'run' : 'help');
8
+ if (action === 'execution') {
9
+ const executionCommand = stringValue(input.execution_command, 'status');
10
+ return [
11
+ 'execution',
12
+ ...(executionCommand === 'status' ? [] : [executionCommand]),
13
+ ...valueFlag(input, 'mode', '--mode'),
14
+ ...booleanFlag(input, 'restart', '--restart'),
15
+ ...jsonFlag(input)
16
+ ];
17
+ }
7
18
  const task = typeof input.prompt === 'string'
8
19
  ? input.prompt
9
20
  : typeof input.task === 'string'
10
21
  ? input.task
11
22
  : '';
12
- const action = stringValue(input.action, task ? 'run' : 'help');
13
23
  return [
14
24
  action,
15
25
  ...(task ? [task] : []),
@@ -1 +1 @@
1
- export const NARUTO_ACTIONS = ['run', 'status', 'subagents', 'proof', 'parent-summary', 'help'];
1
+ export const NARUTO_ACTIONS = ['run', 'status', 'subagents', 'proof', 'parent-summary', 'execution', 'help'];
@@ -3,6 +3,7 @@ import { NARUTO_CREDENTIAL_BOOLEAN_FLAGS, NARUTO_CREDENTIAL_VALUE_FLAGS } from '
3
3
  import { resolveNarutoCredentialPolicy } from './naruto-host-credentials.js';
4
4
  import { DEFAULT_SUBAGENT_EFFORT, defaultSubagentModel, NARUTO_PARENT_EFFORT, narutoParentModel } from './model-policy.js';
5
5
  import { HARD_NARUTO_MAX_THREADS } from './thread-budget.js';
6
+ import { isNarutoExecutionMode } from './naruto-execution-mode.js';
6
7
  export function parseNarutoArgs(args) {
7
8
  const helpRequested = args.includes('--help') || args.includes('-h');
8
9
  const validationArgs = [...args];
@@ -11,7 +12,7 @@ export function parseNarutoArgs(args) {
11
12
  : args;
12
13
  const first = normalized[0] && !normalized[0].startsWith('-') ? normalized[0] : '';
13
14
  const actionName = first;
14
- const actions = new Set(['run', 'status', 'subagents', 'proof', 'parent-summary', 'help']);
15
+ const actions = new Set(['run', 'status', 'subagents', 'proof', 'parent-summary', 'execution', 'help']);
15
16
  const action = (actions.has(actionName) ? actionName : 'run');
16
17
  const explicitAction = actions.has(actionName);
17
18
  const rest = explicitAction ? normalized.slice(1) : normalized;
@@ -20,6 +21,7 @@ export function parseNarutoArgs(args) {
20
21
  const maxThreadsOption = optionValue(optionArgs, '--max-threads');
21
22
  const missionOption = optionValue(optionArgs, '--mission');
22
23
  const missionIdOption = optionValue(optionArgs, '--mission-id');
24
+ const modeOption = optionValue(optionArgs, '--mode');
23
25
  const credentialOptions = NARUTO_CREDENTIAL_VALUE_FLAGS.map((name) => ({
24
26
  name,
25
27
  option: optionValue(optionArgs, name)
@@ -29,6 +31,7 @@ export function parseNarutoArgs(args) {
29
31
  ...optionErrors('--max-threads', maxThreadsOption, true),
30
32
  ...optionErrors('--mission', missionOption, false),
31
33
  ...optionErrors('--mission-id', missionIdOption, false),
34
+ ...optionErrors('--mode', modeOption, false),
32
35
  ...credentialOptions.flatMap(({ name, option }) => optionErrors(name, option, false)),
33
36
  ...booleanOptionErrors(validationArgs),
34
37
  ...unknownOptionErrors(validationArgs)
@@ -50,7 +53,7 @@ export function parseNarutoArgs(args) {
50
53
  : undefined;
51
54
  const prompt = action === 'run' ? positional.join(' ').trim() : '';
52
55
  const positionalHead = String(positional[0] || '').toLowerCase();
53
- const subcommandNames = new Set(['run', 'status', 'subagents', 'proof', 'parent-summary', 'help']);
56
+ const subcommandNames = new Set(['run', 'status', 'subagents', 'proof', 'parent-summary', 'execution', 'help']);
54
57
  if (!first && !explicitAction && positionalHead && !subcommandNames.has(positionalHead)) {
55
58
  argumentErrors.push(`unknown_subcommand:${positionalHead}`);
56
59
  }
@@ -60,7 +63,32 @@ export function parseNarutoArgs(args) {
60
63
  else if (!explicitAction && subcommandNames.has(positionalHead)) {
61
64
  argumentErrors.push(`misplaced_subcommand:${positionalHead}`);
62
65
  }
63
- if (action !== 'run') {
66
+ let executionCommand = 'status';
67
+ if (action === 'execution') {
68
+ const command = String(positional[0] || 'status').toLowerCase();
69
+ if (command === 'status' || command === 'set')
70
+ executionCommand = command;
71
+ else
72
+ argumentErrors.push(`invalid_naruto_execution_command:${command}`);
73
+ if (positional.length > 1) {
74
+ for (const value of positional.slice(1))
75
+ argumentErrors.push(`unexpected_positional:${value}`);
76
+ }
77
+ if (executionCommand === 'status') {
78
+ if (modeOption.present)
79
+ argumentErrors.push('option_not_supported_for_action:execution:--mode');
80
+ if (normalized.includes('--restart'))
81
+ argumentErrors.push('option_not_supported_for_action:execution:--restart');
82
+ }
83
+ else {
84
+ if (!modeOption.present || !modeOption.value)
85
+ argumentErrors.push('naruto_execution_set_requires_mode');
86
+ if (modeOption.value !== undefined && !isNarutoExecutionMode(modeOption.value)) {
87
+ argumentErrors.push(`invalid_naruto_execution_mode:${modeOption.value}`);
88
+ }
89
+ }
90
+ }
91
+ else if (action !== 'run') {
64
92
  let missionConsumed = false;
65
93
  for (const value of positional) {
66
94
  if (!missionConsumed && positionalMission !== undefined && value === positionalMission) {
@@ -87,6 +115,10 @@ export function parseNarutoArgs(args) {
87
115
  ...(normalized.includes('--trusted-project') ? ['--trusted-project'] : []),
88
116
  ...(normalized.includes('--no-forced-login-method') ? ['--no-forced-login-method'] : [])
89
117
  ];
118
+ const presentExecutionOptions = [
119
+ ...(modeOption.present ? ['--mode'] : []),
120
+ ...(normalized.includes('--restart') ? ['--restart'] : [])
121
+ ];
90
122
  if (action === 'parent-summary') {
91
123
  if (!missionOption.present || !missionOption.value || missionOption.value === 'latest') {
92
124
  argumentErrors.push('parent_summary_requires_explicit_mission');
@@ -104,6 +136,11 @@ export function parseNarutoArgs(args) {
104
136
  argumentErrors.push(`option_not_supported_for_action:${action}:${name}`);
105
137
  }
106
138
  }
139
+ if (action !== 'execution' || executionCommand === 'status') {
140
+ for (const name of presentExecutionOptions) {
141
+ argumentErrors.push(`option_not_supported_for_action:${action}:${name}`);
142
+ }
143
+ }
107
144
  if (normalized.includes('--stdin'))
108
145
  argumentErrors.push('stdin_only_supported_for_parent_summary');
109
146
  }
@@ -113,6 +150,9 @@ export function parseNarutoArgs(args) {
113
150
  requestedSubagents,
114
151
  maxThreads,
115
152
  missionId: String(missionFlag || positionalMission || 'latest'),
153
+ executionCommand,
154
+ executionMode: modeOption.value && isNarutoExecutionMode(modeOption.value) ? modeOption.value : undefined,
155
+ restart: normalized.includes('--restart'),
116
156
  json: normalized.includes('--json'),
117
157
  stdin: normalized.includes('--stdin'),
118
158
  readOnly: normalized.includes('--readonly') || normalized.includes('--read-only'),
@@ -130,11 +170,11 @@ export function parseNarutoArgs(args) {
130
170
  }
131
171
  function positionalValues(args) {
132
172
  const valueFlags = new Set([
133
- '--agents', '--max-threads', '--mission', '--mission-id',
173
+ '--agents', '--max-threads', '--mission', '--mission-id', '--mode',
134
174
  ...NARUTO_CREDENTIAL_VALUE_FLAGS
135
175
  ]);
136
176
  const booleanFlags = new Set([
137
- '--json', '--stdin', '--readonly', '--read-only', '--trusted-project',
177
+ '--json', '--stdin', '--readonly', '--read-only', '--trusted-project', '--restart',
138
178
  ...NARUTO_CREDENTIAL_BOOLEAN_FLAGS
139
179
  ]);
140
180
  const result = [];
@@ -197,8 +237,8 @@ function optionErrors(name, option, numeric) {
197
237
  }
198
238
  function unknownOptionErrors(args) {
199
239
  const canonical = new Set([
200
- '--agents', '--max-threads', '--mission', '--mission-id',
201
- '--json', '--stdin', '--readonly', '--read-only', '--trusted-project', '--help', '-h', '--',
240
+ '--agents', '--max-threads', '--mission', '--mission-id', '--mode',
241
+ '--json', '--stdin', '--readonly', '--read-only', '--trusted-project', '--restart', '--help', '-h', '--',
202
242
  ...NARUTO_CREDENTIAL_VALUE_FLAGS,
203
243
  ...NARUTO_CREDENTIAL_BOOLEAN_FLAGS
204
244
  ]);
@@ -220,6 +260,7 @@ function booleanOptionErrors(args) {
220
260
  '--readonly',
221
261
  '--read-only',
222
262
  '--trusted-project',
263
+ '--restart',
223
264
  ...NARUTO_CREDENTIAL_BOOLEAN_FLAGS,
224
265
  '--help',
225
266
  '-h'
@@ -1,5 +1,6 @@
1
1
  import { NARUTO_ACTIONS } from '../safety/command-contract/types.js';
2
2
  import { NARUTO_AUTH_MODES, NARUTO_EFFORT_TIERS } from './naruto-host-credentials.js';
3
+ import { NARUTO_EXECUTION_MODES } from './naruto-execution-mode.js';
3
4
  import { HARD_NARUTO_MAX_THREADS } from './thread-budget.js';
4
5
  export const NARUTO_CREDENTIAL_VALUE_FLAGS = Object.freeze([
5
6
  '--auth-mode',
@@ -29,6 +30,11 @@ export const NARUTO_RUN_ONLY_INPUT_FIELDS = Object.freeze([
29
30
  'subagent_effort',
30
31
  'no_forced_login_method'
31
32
  ]);
33
+ export const NARUTO_EXECUTION_INPUT_FIELDS = Object.freeze([
34
+ 'execution_command',
35
+ 'mode',
36
+ 'restart'
37
+ ]);
32
38
  export function narutoCommandInputSchema() {
33
39
  const properties = {
34
40
  action: { type: 'string', enum: [...NARUTO_ACTIONS] },
@@ -48,6 +54,9 @@ export function narutoCommandInputSchema() {
48
54
  subagent_model: identifierSchema(),
49
55
  subagent_effort: { type: 'string', enum: [...NARUTO_EFFORT_TIERS] },
50
56
  no_forced_login_method: { type: 'boolean' },
57
+ execution_command: { type: 'string', enum: ['status', 'set'] },
58
+ mode: { type: 'string', enum: [...NARUTO_EXECUTION_MODES] },
59
+ restart: { type: 'boolean' },
51
60
  json: { type: 'boolean' }
52
61
  };
53
62
  const exactlyOneTask = {
@@ -60,16 +69,16 @@ export function narutoCommandInputSchema() {
60
69
  const runBranch = {
61
70
  properties: { action: { const: 'run' } },
62
71
  required: ['action'],
63
- allOf: [exactlyOneTask, absentFields(['stdin'])]
72
+ allOf: [exactlyOneTask, absentFields(['stdin', ...NARUTO_EXECUTION_INPUT_FIELDS])]
64
73
  };
65
74
  const implicitRunBranch = {
66
75
  not: { required: ['action'] },
67
- allOf: [exactlyOneTask, absentFields(['stdin'])]
76
+ allOf: [exactlyOneTask, absentFields(['stdin', ...NARUTO_EXECUTION_INPUT_FIELDS])]
68
77
  };
69
78
  const observationBranch = {
70
79
  properties: { action: { enum: ['status', 'subagents', 'proof'] } },
71
80
  required: ['action'],
72
- allOf: [runOnlyAbsent, absentFields(['stdin'])]
81
+ allOf: [runOnlyAbsent, absentFields(['stdin', ...NARUTO_EXECUTION_INPUT_FIELDS])]
73
82
  };
74
83
  const parentSummaryBranch = {
75
84
  properties: {
@@ -77,11 +86,34 @@ export function narutoCommandInputSchema() {
77
86
  stdin: { const: true }
78
87
  },
79
88
  required: ['action', 'mission', 'stdin'],
80
- allOf: [runOnlyAbsent]
89
+ allOf: [runOnlyAbsent, absentFields(NARUTO_EXECUTION_INPUT_FIELDS)]
81
90
  };
82
91
  const helpBranch = {
83
92
  properties: { action: { const: 'help' } },
84
93
  required: ['action'],
94
+ allOf: [runOnlyAbsent, absentFields(['mission', 'stdin', ...NARUTO_EXECUTION_INPUT_FIELDS])]
95
+ };
96
+ const executionStatusBranch = {
97
+ properties: { action: { const: 'execution' } },
98
+ required: ['action'],
99
+ allOf: [runOnlyAbsent, absentFields(['mission', 'stdin', ...NARUTO_EXECUTION_INPUT_FIELDS])]
100
+ };
101
+ const executionStatusExplicitBranch = {
102
+ properties: {
103
+ action: { const: 'execution' },
104
+ execution_command: { const: 'status' }
105
+ },
106
+ required: ['action', 'execution_command'],
107
+ allOf: [runOnlyAbsent, absentFields(['mission', 'stdin', 'mode', 'restart'])]
108
+ };
109
+ const executionSetBranch = {
110
+ properties: {
111
+ action: { const: 'execution' },
112
+ execution_command: { const: 'set' },
113
+ mode: { type: 'string', enum: [...NARUTO_EXECUTION_MODES] },
114
+ restart: { type: 'boolean' }
115
+ },
116
+ required: ['action', 'execution_command', 'mode'],
85
117
  allOf: [runOnlyAbsent, absentFields(['mission', 'stdin'])]
86
118
  };
87
119
  const implicitHelpBranch = {
@@ -90,6 +122,7 @@ export function narutoCommandInputSchema() {
90
122
  { required: ['action'] },
91
123
  { required: ['mission'] },
92
124
  { required: ['stdin'] },
125
+ ...NARUTO_EXECUTION_INPUT_FIELDS.map((field) => ({ required: [field] })),
93
126
  ...NARUTO_RUN_ONLY_INPUT_FIELDS.map((field) => ({ required: [field] }))
94
127
  ]
95
128
  }
@@ -103,6 +136,9 @@ export function narutoCommandInputSchema() {
103
136
  implicitRunBranch,
104
137
  observationBranch,
105
138
  parentSummaryBranch,
139
+ executionStatusBranch,
140
+ executionStatusExplicitBranch,
141
+ executionSetBranch,
106
142
  helpBranch,
107
143
  implicitHelpBranch
108
144
  ]
@@ -0,0 +1,62 @@
1
+ import fs from 'node:fs';
2
+ import fsp from 'node:fs/promises';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { writeJsonAtomic } from '../fsx.js';
6
+ import { withFileLock } from '../locks/file-lock.js';
7
+ import { ensureConfinedDirectory, inspectConfinedPath } from '../managed-path-safety.js';
8
+ export const NARUTO_EXECUTION_MODES = ['auto', 'current-session', 'standalone'];
9
+ export const NARUTO_EXECUTION_SCHEMA = 'sks.naruto-execution.v1';
10
+ export function narutoExecutionPath(env = process.env) {
11
+ return path.join(path.resolve(env.SKS_HOME || path.join(env.HOME || os.homedir(), '.sneakoscope')), 'preferences', 'naruto-execution.json');
12
+ }
13
+ export function isNarutoExecutionMode(value) {
14
+ return NARUTO_EXECUTION_MODES.some(mode => mode === value);
15
+ }
16
+ export function readNarutoExecutionMode(env = process.env) {
17
+ const file = narutoExecutionPath(env);
18
+ try {
19
+ const raw = JSON.parse(fs.readFileSync(file, 'utf8'));
20
+ if (raw?.schema !== NARUTO_EXECUTION_SCHEMA || !isNarutoExecutionMode(raw.mode))
21
+ throw new Error('invalid');
22
+ return { schema: NARUTO_EXECUTION_SCHEMA, mode: raw.mode, path: file, stored: true, blockers: [] };
23
+ }
24
+ catch (error) {
25
+ return {
26
+ schema: NARUTO_EXECUTION_SCHEMA, mode: 'auto', path: file, stored: false,
27
+ blockers: error?.code === 'ENOENT' ? [] : ['naruto_execution_preference_unreadable']
28
+ };
29
+ }
30
+ }
31
+ export async function writeNarutoExecutionMode(mode, env = process.env, onlyIfAbsent = false) {
32
+ if (!isNarutoExecutionMode(mode))
33
+ throw new Error('naruto_execution_mode_invalid');
34
+ const file = narutoExecutionPath(env);
35
+ const root = path.dirname(path.dirname(file));
36
+ await fsp.mkdir(root, { recursive: true });
37
+ await ensureConfinedDirectory(root, path.dirname(file));
38
+ return withFileLock({ lockPath: `${file}.lock`, timeoutMs: 5_000, staleMs: 30_000 }, async () => {
39
+ const safety = await inspectConfinedPath(root, file);
40
+ if (safety.leafSymlink)
41
+ throw new Error('naruto_execution_preference_unsafe_path');
42
+ const before = readNarutoExecutionMode(env);
43
+ if (before.blockers.length)
44
+ throw new Error(before.blockers[0]);
45
+ if ((onlyIfAbsent && before.stored) || (before.stored && before.mode === mode))
46
+ return { ...before, changed: false };
47
+ await writeJsonAtomic(file, { schema: NARUTO_EXECUTION_SCHEMA, mode }, { mode: 0o600 });
48
+ return { ...readNarutoExecutionMode(env), changed: true };
49
+ });
50
+ }
51
+ export function narutoUsesCurrentSession(env = process.env) {
52
+ if (env.SKS_NARUTO_STANDALONE_CLI === '1')
53
+ return false;
54
+ if (env.SKS_NARUTO_APP_SESSION === '1')
55
+ return true;
56
+ const mode = readNarutoExecutionMode(env).mode;
57
+ if (mode === 'standalone')
58
+ return false;
59
+ if (mode === 'current-session')
60
+ return Boolean(env.CODEX_THREAD_ID?.trim());
61
+ return Boolean(env.CODEX_THREAD_ID?.trim());
62
+ }
@@ -9,6 +9,7 @@ export function renderNarutoUsage() {
9
9
  '',
10
10
  'Usage: sks naruto run "<task>" [options]',
11
11
  ' sks naruto status|subagents|proof [latest|M-...] [--json]',
12
+ ' sks naruto execution status [--json] | sks naruto execution set --mode auto|current-session|standalone [--restart] [--json]',
12
13
  ' sks naruto parent-summary --mission M-... --stdin [--json]',
13
14
  '',
14
15
  'Run or inspect the Codex official-subagent Naruto workflow.',
@@ -28,6 +29,12 @@ export function renderNarutoUsage() {
28
29
  ' --no-forced-login-method Do not inject a forced login method.',
29
30
  ' --json Emit machine-readable output.',
30
31
  '',
32
+ 'Execution mode:',
33
+ ' auto Preserve the Codex App session contract when CODEX_THREAD_ID is present (default when no update-selected lane exists).',
34
+ ' current-session Require CODEX_THREAD_ID and use the current Codex App parent.',
35
+ ' standalone Launch the official Codex child workflow from a terminal, even if a shell inherited a thread id.',
36
+ ' Apply the setting from SKS Center to save it and restart a running Codex App when macOS permits it.',
37
+ '',
31
38
  'Model tiers: every child runs the newest model of its tier: fast for tiny mechanical work, balanced for instructed coding, context for reads and tools, deep for planning, analysis, and review judgment.',
32
39
  'Automatic fan-out starts at 4/6/8, or 16 for eligible mass mechanical or exploration work on the fast and context tiers.',
33
40
  'After decomposition, either lane may expand to 256 independent useful children.',
@@ -48,6 +55,8 @@ export function buildNarutoHelpResult() {
48
55
  'sks naruto status [latest|M-...] [--json]',
49
56
  'sks naruto subagents [latest|M-...] [--json]',
50
57
  'sks naruto proof [latest|M-...] [--json]',
58
+ 'sks naruto execution status [--json]',
59
+ 'sks naruto execution set --mode auto|current-session|standalone [--restart] [--json]',
51
60
  'sks naruto parent-summary --mission M-... --stdin [--json] (active Codex App Naruto only)'
52
61
  ],
53
62
  commands: [...NARUTO_ACTIONS],
@@ -4,6 +4,7 @@ import path from 'node:path';
4
4
  import { randomId, runProcess, writeJsonAtomic } from '../fsx.js';
5
5
  import { DEFAULT_SUBAGENT_EFFORT, defaultSubagentModel, NARUTO_PARENT_EFFORT, narutoParentModel } from './model-policy.js';
6
6
  import { nextAgentGenerationEnv } from '../agents/agent-recursion-guard.js';
7
+ import { narutoUsesCurrentSession } from './naruto-execution-mode.js';
7
8
  import { inspectDesktopBridgeCliLaunchGuard, stripRetiredDirectProviderEnv } from '../codex-control/desktop-bridge-launch-guard.js';
8
9
  import { prepareCodexAppServerRuntimeEnv } from '../codex-control/codex-app-server-runtime-env.js';
9
10
  import { assertCodexFloor } from '../codex-compat/codex-version-policy.js';
@@ -77,11 +78,7 @@ const SECRET_BEARING_ENV_KEYS = new Set([
77
78
  'all_proxy'
78
79
  ]);
79
80
  export function detectCodexAppSession(env = process.env) {
80
- if (env.SKS_NARUTO_STANDALONE_CLI === '1')
81
- return false;
82
- if (env.SKS_NARUTO_APP_SESSION === '1')
83
- return true;
84
- return Boolean(env.CODEX_THREAD_ID);
81
+ return narutoUsesCurrentSession(env);
85
82
  }
86
83
  export function codexAppSessionKey(env = process.env) {
87
84
  if (!detectCodexAppSession(env))
@@ -283,6 +283,16 @@ const UPDATE_MIGRATION_STAGES = [
283
283
  min_from_version: '0.0.0',
284
284
  run: runCurrentPublicSurfaceReconcileStage
285
285
  },
286
+ {
287
+ id: 'naruto-execution-mode',
288
+ min_from_version: '0.0.0',
289
+ run: async () => {
290
+ const { writeNarutoExecutionMode } = await import('../subagents/naruto-execution-mode.js');
291
+ const initialMode = process.env.SKS_NARUTO_APP_SESSION === '1' ? 'current-session' : 'standalone';
292
+ const result = await writeNarutoExecutionMode(initialMode, process.env, true);
293
+ return { ok: true, status: 'ok', actions: [result.changed ? `naruto_execution_${initialMode}_initialized` : 'naruto_execution_preference_preserved'], blockers: [], warnings: [], detail: { mode: result.mode, initial_mode: initialMode } };
294
+ }
295
+ },
286
296
  {
287
297
  id: 'session-state-split',
288
298
  min_from_version: '0.0.0',
@@ -1 +1 @@
1
- export const PACKAGE_VERSION = '10.5.2';
1
+ export const PACKAGE_VERSION = '10.5.3';
@@ -78,7 +78,7 @@ extension ProvidersViewController: NSTableViewDataSource, NSTableViewDelegate {
78
78
  clear.setAccessibilityIdentifier("sks-provider-exposure-clear")
79
79
  let card = NativeView.card(
80
80
  title: "Models in Codex",
81
- subtitle: "Select OpenRouter models, apply, then relaunch Codex to update its picker. Codex-LB models are included unless OpenRouter Only is on.",
81
+ subtitle: "Select OpenRouter models and apply. SKS rebuilds the bridge catalog and restarts a running Codex App so its picker is current. Codex-LB models are included unless OpenRouter Only is on.",
82
82
  views: [exposureStatus, exposureSearchField, scroll, ControlKit.actionRow([exposureApplyButton, clear])]
83
83
  )
84
84
  card.setAccessibilityIdentifier("sks-provider-card-model-exposure")
@@ -154,9 +154,20 @@ extension ProvidersViewController: NSTableViewDataSource, NSTableViewDelegate {
154
154
  let blocker = truth?.blockers.first.map(ProviderSecretRedactor.redact)
155
155
  let extra = self.openRouterOnlyEnabled == true ? " plus the subagent list models; Codex-LB models stay hidden while OpenRouter Only is on"
156
156
  : self.openRouterOnlyEnabled == false ? " plus every Codex-LB model" : ""
157
- let summary = completed
158
- ? "Exposure applied · \(self.exposurePending.count) OpenRouter model(s)\(extra). Restart Codex to refresh its picker."
159
- : blocker.map { "Exposure not applied · \($0)" } ?? "Exposure result schema invalid"
157
+ let restart = self.json(result.output)?["result"] as? [String: Any]
158
+ let codexRestart = restart?["codex_restart"] as? [String: Any]
159
+ let summary: String
160
+ if completed, codexRestart?["status"] as? String == "restarted" {
161
+ summary = "Exposure applied · \(self.exposurePending.count) OpenRouter model(s)\(extra). Codex restarted; its picker is current."
162
+ } else if completed, codexRestart?["reason"] as? String == "codex_not_running" {
163
+ summary = "Exposure applied · \(self.exposurePending.count) OpenRouter model(s)\(extra). Codex is not running; the picker updates on its next launch."
164
+ } else if completed, codexRestart?["reason"] as? String == "not_macos" {
165
+ summary = "Exposure applied · \(self.exposurePending.count) OpenRouter model(s)\(extra). Restart Codex on macOS to refresh its picker."
166
+ } else if completed {
167
+ summary = "Exposure applied · \(self.exposurePending.count) OpenRouter model(s)\(extra). Codex restart was skipped by policy."
168
+ } else {
169
+ summary = blocker.map { "Exposure not applied · \($0)" } ?? "Exposure result schema invalid"
170
+ }
160
171
  _ = self.operations.update(snapshot, state: completed ? .succeeded : .failed, stage: "complete", progress: 1, summary: summary)
161
172
  self.exposureStatus.stringValue = summary
162
173
  self.exposureStatus.textColor = completed ? .systemGreen : .systemRed
@@ -14,8 +14,11 @@ final class SettingsViewController: NSViewController, ControlCenterPage {
14
14
  private let followCodexLifecycle = NSButton(checkboxWithTitle: "Show SKS Menu only while Codex is running", target: nil, action: nil)
15
15
  private let status = NativeView.detail("Settings use the native app configuration file.")
16
16
  private let contextStatus = NativeView.detail("Codex 1M context: checking current state…")
17
+ private let narutoModePopup = NSPopUpButton()
18
+ private let narutoModeStatus = NativeView.detail("Naruto execution mode: checking saved preference…")
17
19
  private var notificationButton: NSButton!
18
20
  private var contextToggleButton: NSButton!
21
+ private var narutoApplyButton: NSButton!
19
22
  private var contextEnabled: Bool?
20
23
  private var contextBusy = false
21
24
  private var contextGeneration = 0
@@ -24,6 +27,8 @@ final class SettingsViewController: NSViewController, ControlCenterPage {
24
27
  private var memoryEnabled: Bool?
25
28
  private var memoryBusy = false
26
29
  private var memoryGeneration = 0
30
+ private var narutoBusy = false
31
+ private var narutoGeneration = 0
27
32
  init(processClient: ProcessClient, operations: OperationCoordinator, notifications: NotificationCoordinator) {
28
33
  self.processClient = processClient
29
34
  self.operations = operations
@@ -44,6 +49,24 @@ final class SettingsViewController: NSViewController, ControlCenterPage {
44
49
  subtitle: "Experimental · Keep notes and retrieve earlier messages and tool results. Enabled by default in SKS. Applies to new tasks with supported Codex and eligible ChatGPT sign-in; API-key and custom-provider sessions may not activate it.",
45
50
  views: [NativeView.row([memoryToggle, NativeView.detail("Enable experimental context management")]), memoryStatus]
46
51
  )
52
+ narutoModePopup.removeAllItems()
53
+ for mode in ["auto", "current-session", "standalone"] {
54
+ narutoModePopup.addItem(withTitle: Self.narutoModeTitle(mode))
55
+ narutoModePopup.lastItem?.representedObject = mode
56
+ }
57
+ narutoModePopup.isEnabled = false
58
+ narutoModePopup.setAccessibilityLabel("Naruto execution mode")
59
+ narutoModePopup.setAccessibilityIdentifier("sks-naruto-execution-mode")
60
+ narutoApplyButton = ControlKit.primaryButton("Apply", target: self, action: #selector(applyNarutoExecution))
61
+ narutoApplyButton.isEnabled = false
62
+ narutoApplyButton.setAccessibilityLabel("Apply Naruto execution mode and restart Codex")
63
+ narutoApplyButton.setAccessibilityIdentifier("sks-naruto-execution-apply")
64
+ narutoModeStatus.setAccessibilityIdentifier("sks-naruto-execution-status")
65
+ let narutoCard = NativeView.card(
66
+ title: "Naruto execution",
67
+ subtitle: "Choose whether SKS Naruto uses the current Codex App parent or launches the official workflow as a standalone terminal command. Apply saves the preference and restarts a running Codex App so the new mode is active.",
68
+ views: [NativeView.row([narutoModePopup, narutoApplyButton]), narutoModeStatus]
69
+ )
47
70
  followCodexLifecycle.target = self; followCodexLifecycle.action = #selector(save)
48
71
  followCodexLifecycle.setAccessibilityLabel("Show SKS Menu only while Codex is running")
49
72
  notificationButton = NativeView.button("Enable Notifications", target: self, action: #selector(enableNotifications))
@@ -66,13 +89,14 @@ final class SettingsViewController: NSViewController, ControlCenterPage {
66
89
  )
67
90
  view = NativeView.page([
68
91
  ControlKit.header("Settings", "Choose how SKS works on this Mac."),
69
- memoryCard, lifecycleCard, notificationsCard, NativeDisclosure("Advanced", views: [contextCard])
92
+ memoryCard, narutoCard, lifecycleCard, notificationsCard, NativeDisclosure("Advanced", views: [contextCard])
70
93
  ])
71
94
  }
72
95
 
73
96
  func refreshOnAppear() {
74
97
  refreshContextManagement()
75
98
  refreshContext1m()
99
+ refreshNarutoExecution()
76
100
  let configResult = readConfig()
77
101
  switch configResult {
78
102
  case .loaded(let config):
@@ -104,6 +128,86 @@ final class SettingsViewController: NSViewController, ControlCenterPage {
104
128
  }
105
129
  }
106
130
 
131
+ private static func narutoModeTitle(_ mode: String) -> String {
132
+ switch mode {
133
+ case "current-session": return "Current Codex App session"
134
+ case "standalone": return "Standalone terminal workflow"
135
+ default: return "Auto (Codex App when available)"
136
+ }
137
+ }
138
+
139
+ private func refreshNarutoExecution() {
140
+ guard !narutoBusy else { return }
141
+ narutoGeneration += 1
142
+ let requestGeneration = narutoGeneration
143
+ processClient.run(["naruto", "execution", "status", "--json"], timeout: NativeView.statusTimeout) { [weak self] result in
144
+ guard let self, requestGeneration == self.narutoGeneration, !self.narutoBusy else { return }
145
+ let payload = self.json(result.output)
146
+ guard result.code == 0,
147
+ payload?["schema"] as? String == "sks.naruto-execution-result.v1",
148
+ payload?["ok"] as? Bool == true,
149
+ let mode = payload?["mode"] as? String,
150
+ ["auto", "current-session", "standalone"].contains(mode) else {
151
+ self.narutoModePopup.isEnabled = false
152
+ self.narutoApplyButton.isEnabled = false
153
+ self.narutoModeStatus.stringValue = "Naruto execution preference unavailable. Run sks update, then reopen Settings."
154
+ self.narutoModeStatus.textColor = .systemOrange
155
+ return
156
+ }
157
+ self.narutoModePopup.selectItem(withTitle: Self.narutoModeTitle(mode))
158
+ self.narutoModePopup.isEnabled = true
159
+ self.narutoApplyButton.isEnabled = true
160
+ let stored = payload?["stored"] as? Bool == true
161
+ self.narutoModeStatus.stringValue = "\(Self.narutoModeTitle(mode)) · \(stored ? "saved preference" : "default until the next update")"
162
+ self.narutoModeStatus.textColor = .secondaryLabelColor
163
+ }
164
+ }
165
+
166
+ @objc private func applyNarutoExecution() {
167
+ guard !narutoBusy,
168
+ let mode = narutoModePopup.selectedItem?.representedObject as? String,
169
+ ["auto", "current-session", "standalone"].contains(mode) else { return }
170
+ guard let operation = operations.begin(kind: "naruto-execution-mode", mutationGroup: "codex-config", summary: "Apply Naruto execution mode") else {
171
+ narutoModeStatus.stringValue = "Another configuration change is running. Try again when it finishes."
172
+ narutoModeStatus.textColor = .systemOrange
173
+ return
174
+ }
175
+ narutoBusy = true
176
+ narutoGeneration += 1
177
+ narutoModePopup.isEnabled = false
178
+ narutoApplyButton.isEnabled = false
179
+ narutoModeStatus.stringValue = "Saving Naruto execution mode and restarting Codex if it is running…"
180
+ narutoModeStatus.textColor = .secondaryLabelColor
181
+ _ = operations.update(operation, state: .running, stage: "applying", progress: nil, summary: narutoModeStatus.stringValue)
182
+ processClient.run(["naruto", "execution", "set", "--mode", mode, "--restart", "--json"], timeout: NativeView.mutationTimeout) { [weak self] result in
183
+ guard let self else { return }
184
+ self.narutoBusy = false
185
+ let payload = self.json(result.output)
186
+ let ok = result.code == 0
187
+ && payload?["schema"] as? String == "sks.naruto-execution-result.v1"
188
+ && payload?["ok"] as? Bool == true
189
+ && payload?["mode"] as? String == mode
190
+ let restart = payload?["restart"] as? [String: Any]
191
+ let summary: String
192
+ if ok, restart?["status"] as? String == "restarted" {
193
+ summary = "Naruto mode saved · Codex restarted. Start a new task to use it."
194
+ } else if ok, restart?["reason"] as? String == "codex_not_running" {
195
+ summary = "Naruto mode saved · Codex is not running; it applies on the next launch."
196
+ } else if ok, restart?["reason"] as? String == "not_macos" {
197
+ summary = "Naruto mode saved · restart Codex on macOS to apply it."
198
+ } else if ok {
199
+ summary = "Naruto mode saved · Codex restart was skipped by policy."
200
+ } else {
201
+ let blocker = (payload?["blockers"] as? [String])?.first
202
+ summary = blocker.map { "Naruto mode was not confirmed · \($0)" } ?? "Naruto mode was not confirmed · unexpected CLI response."
203
+ }
204
+ _ = self.operations.update(operation, state: ok ? .succeeded : .failed, stage: "complete", progress: 1, summary: summary)
205
+ self.narutoModeStatus.stringValue = summary
206
+ self.narutoModeStatus.textColor = ok ? .systemGreen : .systemRed
207
+ self.refreshNarutoExecution()
208
+ }
209
+ }
210
+
107
211
  private func refreshContextManagement() {
108
212
  guard !memoryBusy else { return }
109
213
  memoryGeneration += 1
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sneakoscope",
3
3
  "displayName": "ㅅㅋㅅ",
4
- "version": "10.5.2",
4
+ "version": "10.5.3",
5
5
  "description": "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.",
6
6
  "type": "module",
7
7
  "homepage": "https://github.com/mandarange/Sneakoscope-Codex#readme",