sneakoscope 10.3.7 → 10.3.8

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 (63) hide show
  1. package/README.md +12 -181
  2. package/crates/sks-core/Cargo.lock +1 -1
  3. package/crates/sks-core/Cargo.toml +1 -1
  4. package/dist/commands/bridge.js +61 -1
  5. package/dist/commands/codex-app.js +4 -1
  6. package/dist/config/skills-manifest.json +1 -1
  7. package/dist/core/agents/native-worker-backend-router.js +55 -0
  8. package/dist/core/codex-app/codex-app-restart-policy.js +20 -0
  9. package/dist/core/codex-app/codex-context-window.js +61 -24
  10. package/dist/core/codex-app/menubar/constants.js +4 -0
  11. package/dist/core/codex-compat/codex-hook-output-builders.js +3 -1
  12. package/dist/core/codex-lb/bridge-cli-contract.js +2 -0
  13. package/dist/core/codex-lb/desktop-bridge/exclusive-provider-guard.js +59 -0
  14. package/dist/core/codex-lb/desktop-bridge/http-forward.js +23 -15
  15. package/dist/core/codex-lb/desktop-bridge/index.js +1 -0
  16. package/dist/core/codex-lb/desktop-bridge/responses-websocket.js +10 -3
  17. package/dist/core/codex-lb/desktop-bridge/security.js +63 -1
  18. package/dist/core/codex-lb/desktop-bridge/server.js +5 -0
  19. package/dist/core/codex-lb/desktop-bridge/state.js +3 -0
  20. package/dist/core/codex-lb/desktop-bridge/websocket-forward.js +5 -1
  21. package/dist/core/codex-lb/desktop-controller-v3/catalog.js +31 -5
  22. package/dist/core/codex-lb/desktop-controller-v3/lifecycle-commands.js +23 -4
  23. package/dist/core/codex-lb/desktop-controller-v3/openrouter-only-catalog.js +39 -0
  24. package/dist/core/codex-lb/desktop-controller-v3/openrouter-only-commands.js +286 -0
  25. package/dist/core/codex-lb/desktop-controller-v3/openrouter-only-main-model.js +101 -0
  26. package/dist/core/codex-lb/desktop-controller-v3/openrouter-only-status.js +68 -0
  27. package/dist/core/codex-lb/desktop-controller-v3/status.js +8 -5
  28. package/dist/core/codex-lb/desktop-controller-v3.js +26 -2
  29. package/dist/core/codex-lb/desktop-service.js +13 -2
  30. package/dist/core/commands/naruto-command.js +1 -1
  31. package/dist/core/decisions/child-model-choice.js +107 -0
  32. package/dist/core/decisions/cli.js +19 -2
  33. package/dist/core/decisions/routing.js +5 -4
  34. package/dist/core/hooks-runtime/hook-context.js +21 -7
  35. package/dist/core/hooks-runtime/hook-io.js +3 -2
  36. package/dist/core/hooks-runtime/jev-spawn-routing.js +83 -5
  37. package/dist/core/hooks-runtime/official-subagent-lifecycle.js +3 -2
  38. package/dist/core/hooks-runtime/parent-orchestration-gate.js +18 -1
  39. package/dist/core/hooks-runtime/subagent-context.js +13 -2
  40. package/dist/core/hooks-runtime/subagent-spawn-policy.js +49 -6
  41. package/dist/core/hooks-runtime.js +12 -5
  42. package/dist/core/managed-assets/managed-assets-manifest.js +9 -4
  43. package/dist/core/subagents/child-model-allowlist.js +179 -0
  44. package/dist/core/subagents/child-model-plan.js +117 -0
  45. package/dist/core/subagents/naruto-host-credentials.js +82 -13
  46. package/dist/core/subagents/official-subagent-config.js +66 -15
  47. package/dist/core/subagents/official-subagent-preparation.js +45 -12
  48. package/dist/core/subagents/official-subagent-prompt.js +82 -11
  49. package/dist/core/subagents/official-subagent-runner.js +3 -0
  50. package/dist/core/subagents/read-only-list-role.js +73 -0
  51. package/dist/core/subagents/role-model-preferences.js +54 -17
  52. package/dist/core/version.js +1 -1
  53. package/dist/native/sks-menubar/Sources/ControlCenterWindowController.swift +2 -0
  54. package/dist/native/sks-menubar/Sources/OpenRouterOnlyState.swift +314 -0
  55. package/dist/native/sks-menubar/Sources/ProcessClient.swift +2 -1
  56. package/dist/native/sks-menubar/Sources/ProvidersModelExposure.swift +15 -3
  57. package/dist/native/sks-menubar/Sources/ProvidersOpenRouterOnly.swift +162 -0
  58. package/dist/native/sks-menubar/Sources/ProvidersViewController.swift +36 -18
  59. package/dist/native/sks-menubar/Sources/SettingsViewController.swift +10 -4
  60. package/dist/native/sks-menubar/Sources/SidebarItem.swift +2 -0
  61. package/dist/native/sks-menubar/Sources/SubagentModelsModels.swift +201 -0
  62. package/dist/native/sks-menubar/Sources/SubagentModelsViewController.swift +373 -0
  63. package/package.json +1 -1
@@ -0,0 +1,107 @@
1
+ import { consultJevOptions } from './integration.js';
2
+ import { canonicalChildModelId, defaultSubagentEntry, subagentEntryForModel } from '../subagents/child-model-allowlist.js';
3
+ const QUESTION_BUDGET_BYTES = 16_000;
4
+ const TASK_CHARS = 1200;
5
+ function optionId(index) {
6
+ return `m${index + 1}`;
7
+ }
8
+ function optionSummary(entry) {
9
+ const criteria = entry.criteria || 'General work; no special criteria.';
10
+ return `${entry.model}${entry.default ? ' (default)' : ''}: ${criteria}`;
11
+ }
12
+ function laneQuestionId(lane, index) {
13
+ const slug = String(lane.id || '').toLowerCase().replace(/[^a-z0-9_]+/g, '_').replace(/^_+|_+$/g, '').slice(0, 24);
14
+ return `child_model_${slug || index}`.slice(0, 40);
15
+ }
16
+ export function childModelQuestion(id, entries, lane) {
17
+ const options = {};
18
+ entries.forEach((entry, index) => { options[optionId(index)] = optionSummary(entry); });
19
+ const fallback = entries.findIndex((entry) => entry.default);
20
+ return {
21
+ id,
22
+ instructions: 'Choose the model on the user\'s list that should run the child task in state.task. Each option is the user\'s rule for when to use that model; follow those rules. When several fit equally, choose the default.',
23
+ options,
24
+ state: {
25
+ task: String(lane.task || '').slice(0, TASK_CHARS),
26
+ role: lane.role || null,
27
+ requested_model: lane.requestedModel || null,
28
+ default_option: optionId(fallback === -1 ? 0 : fallback)
29
+ }
30
+ };
31
+ }
32
+ export function fallbackChildModel(state, requestedModel, reason) {
33
+ const requested = subagentEntryForModel(state, requestedModel);
34
+ if (requested)
35
+ return { entry: requested, source: 'requested', reason };
36
+ const fallback = defaultSubagentEntry(state);
37
+ return fallback ? { entry: fallback, source: 'default', reason } : null;
38
+ }
39
+ export async function chooseChildModels(input) {
40
+ const entries = input.state.subagent_models;
41
+ const result = {};
42
+ if (!entries.length)
43
+ return result;
44
+ const fallbackAll = (reason) => {
45
+ for (const lane of input.lanes) {
46
+ const choice = fallbackChildModel(input.state, lane.requestedModel, reason);
47
+ if (choice)
48
+ result[lane.id] = choice;
49
+ }
50
+ return result;
51
+ };
52
+ if (entries.length === 1)
53
+ return fallbackAll('single_entry');
54
+ const asked = [];
55
+ const questions = [];
56
+ let bytes = 0;
57
+ input.lanes.forEach((lane, index) => {
58
+ const questionId = laneQuestionId(lane, index);
59
+ if (asked.some((row) => row.questionId === questionId))
60
+ return;
61
+ const question = childModelQuestion(questionId, entries, lane);
62
+ const size = Buffer.byteLength(JSON.stringify(question));
63
+ if (bytes + size > QUESTION_BUDGET_BYTES)
64
+ return;
65
+ bytes += size;
66
+ asked.push({ lane, questionId });
67
+ questions.push(question);
68
+ });
69
+ const decision = questions.length
70
+ ? await consultJevOptions({
71
+ root: input.root,
72
+ workflowId: input.workflowId,
73
+ goal: input.goal,
74
+ questions,
75
+ facts: { openrouter_only: true, subagent_models: entries.map((entry) => canonicalChildModelId(entry.model)) },
76
+ ...(input.env ? { env: input.env } : {}),
77
+ ...(input.deadlineMs === undefined ? {} : { deadlineMs: input.deadlineMs })
78
+ })
79
+ : { called: false, choices: {}, reason: 'budget' };
80
+ for (const lane of input.lanes) {
81
+ const questionId = asked.find((row) => row.lane === lane)?.questionId;
82
+ const picked = questionId ? decision.choices[questionId] : undefined;
83
+ const pickedIndex = picked && /^m\d+$/.test(picked) ? Number(picked.slice(1)) - 1 : -1;
84
+ const entry = pickedIndex >= 0 ? entries[pickedIndex] : undefined;
85
+ if (entry) {
86
+ result[lane.id] = { entry, source: 'jev', reason: 'applied' };
87
+ continue;
88
+ }
89
+ const reason = !questionId ? 'budget' : decision.reason === 'applied' ? 'keep_baseline' : decision.reason;
90
+ const choice = fallbackChildModel(input.state, lane.requestedModel, reason);
91
+ if (choice)
92
+ result[lane.id] = choice;
93
+ }
94
+ return result;
95
+ }
96
+ export async function chooseChildModel(input) {
97
+ const choices = await chooseChildModels({
98
+ root: input.root,
99
+ workflowId: 'spawn-child-model',
100
+ goal: input.task || 'Spawn a child agent.',
101
+ lanes: [{ id: 'spawn', task: input.task, role: input.role ?? null, requestedModel: input.requestedModel ?? null }],
102
+ state: input.state,
103
+ ...(input.env ? { env: input.env } : {}),
104
+ ...(input.deadlineMs === undefined ? {} : { deadlineMs: input.deadlineMs })
105
+ });
106
+ return choices.spawn ?? null;
107
+ }
@@ -3,6 +3,7 @@ import path from 'node:path';
3
3
  import { printJson } from '../../cli/output.js';
4
4
  import { nowIso, writeJsonAtomic } from '../fsx.js';
5
5
  import { resolveOpenRouterApiKey } from '../providers/openrouter/openrouter-secret-store.js';
6
+ import { effectiveChildModelAllowlist } from '../subagents/child-model-allowlist.js';
6
7
  import { jevEnabled, readDecisionConfig, writeDecisionConfig, jevCapabilityActive } from './config.js';
7
8
  import { buildEvaluationReport } from './evaluation.js';
8
9
  import { OPENROUTER_DECISIONS_ENDPOINT, OPENROUTER_DECISIONS_MODEL, requestOpenRouterDecision } from './openrouter.js';
@@ -103,7 +104,10 @@ export async function runDecisionCommand(args, env = process.env) {
103
104
  output(report, json, () => [
104
105
  `mode: ${report.mode} consent: ${report.consentCloud} credential: ${report.credential.present ? report.credential.source : 'missing'}`,
105
106
  `model: ${report.model} endpoint: ${report.endpoint}`,
106
- `capabilities: context=${report.capabilities.context.ready ? 'ready' : 'off'} plan=${report.capabilities.plan.ready ? 'ready' : 'off'} recovery=${report.capabilities.recovery.reason}`
107
+ `capabilities: context=${report.capabilities.context.ready ? 'ready' : 'off'} plan=${report.capabilities.plan.ready ? 'ready' : 'off'} recovery=${report.capabilities.recovery.reason}`,
108
+ ...(report.openrouter_only.enabled
109
+ ? [`openrouter-only: on · child models by ${report.openrouter_only.child_model_decision} · ${report.openrouter_only.subagent_model_count} listed`]
110
+ : [])
107
111
  ]);
108
112
  return 0;
109
113
  }
@@ -139,6 +143,11 @@ export async function statusReport(env) {
139
143
  : !jevEnabled(config)
140
144
  ? 'enable'
141
145
  : 'ready';
146
+ const childModels = effectiveChildModelAllowlist({ env });
147
+ const openRouterOnly = childModels.mode === 'openrouter_only';
148
+ const childTierPoints = new Set(['spawn_tier', 'worker_tier', 'role_tiers', 'role_omission']);
149
+ 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));
142
151
  return {
143
152
  schema: 'sks.jev-decision-status.v1',
144
153
  ok: true,
@@ -158,8 +167,16 @@ export async function statusReport(env) {
158
167
  recovery: config.capabilities.recovery
159
168
  },
160
169
  decision_points: jevEnabled(config)
161
- ? ['turn_route', 'turn_tier', 'spawn_tier', 'worker_tier', 'role_tiers', 'role_omission', 'plan', 'context', 'parent_edit_delegation', 'image_need', 'image_parameters', 'qa_effort_escalation']
170
+ ? [...decisionPoints, ...(openRouterOnly ? ['child_model'] : [])]
162
171
  : [],
172
+ openrouter_only: {
173
+ enabled: openRouterOnly,
174
+ subagent_model_count: openRouterOnly ? childModels.models.length : 0,
175
+ default_subagent_model: childModels.default_model,
176
+ child_model_decision: !openRouterOnly
177
+ ? null
178
+ : jevEnabled(config) && resolved.key && childModels.models.length > 1 ? 'jev' : 'default_entry'
179
+ },
163
180
  recovery: RECOVERY_CAPABILITY,
164
181
  nextStep,
165
182
  notes: [
@@ -1,4 +1,5 @@
1
- import { effortForTier, latestTierModelSet, resolveLatestModelTiers } from '../subagents/model-tiers.js';
1
+ import { effortForTier, resolveLatestModelTiers } from '../subagents/model-tiers.js';
2
+ import { effectiveChildModelAllowlist, isAllowedChildModel } from '../subagents/child-model-allowlist.js';
2
3
  import { routingTier } from './types.js';
3
4
  const ROLE_ID = /^[a-z][a-z0-9_]{0,40}$/;
4
5
  export const MAX_JEV_ROUTING_ROLES = 8;
@@ -16,14 +17,14 @@ export function buildRoutingCandidates(input) {
16
17
  }
17
18
  return out;
18
19
  }
19
- export function applySealedRouting(agents, selected) {
20
+ export function applySealedRouting(agents, selected, allowlist) {
20
21
  if (!selected || !agents)
21
22
  return { ...(agents || {}) };
22
23
  const next = { ...agents };
23
- const current = latestTierModelSet();
24
+ const childModels = allowlist ?? effectiveChildModelAllowlist();
24
25
  for (const [name, effort] of Object.entries(selected.efforts)) {
25
26
  const model = selected.models[name];
26
- if (!model || !current.has(model))
27
+ if (!model || !isAllowedChildModel(model, childModels))
27
28
  continue;
28
29
  const row = next[name];
29
30
  if (!row || row.routing_dynamic !== true || row.routed_model_policy === 'user_role_model_preference')
@@ -8,6 +8,8 @@ import { renderAuthoritativeSksSkillContext } from '../codex-native/sks-skill-pa
8
8
  import { resolveManagedSkillSourcesForAdmission } from './managed-skill-admission.js';
9
9
  import { managedSkillDigestBlocksEnforced } from '../verification-profile.js';
10
10
  import { looksLikeActiveContinuationPrompt } from './naruto-decision-gate.js';
11
+ import { effectiveChildModelAllowlist } from '../subagents/child-model-allowlist.js';
12
+ import { renderChildModelCriteria, SUBAGENT_MODELS_SETTINGS_HINT } from './subagent-spawn-policy.js';
11
13
  import { extractUserPrompt, looksLikeCodexGitAction, looksLikeCodexUiSettingsEvent } from './payload-signals.js';
12
14
  const STANDALONE_PARENT_BASE_SKILLS = [
13
15
  'sks-naruto',
@@ -15,14 +17,25 @@ const STANDALONE_PARENT_BASE_SKILLS = [
15
17
  'sks-prompt-pipeline',
16
18
  'sks-honest-mode'
17
19
  ];
18
- const OFFICIAL_SUBAGENT_SPAWN_COMPATIBILITY_CONTEXT = [
20
+ const OFFICIAL_SUBAGENT_SPAWN_CONTRACT_HEAD = [
19
21
  'SKS official-subagent spawn contract:',
20
22
  '- Full-history forks (`fork_turns="all"`, including the omitted/default full-history mode) inherit the parent agent type, model, and reasoning effort.',
21
- '- When selecting a custom `agent_type` or overriding `model`/`reasoning_effort`, set `fork_turns="none"` or a positive bounded turn count and put the complete bounded slice contract in `message`.',
23
+ '- When selecting a custom `agent_type` or overriding `model`/`reasoning_effort`, set `fork_turns="none"` or a positive bounded turn count and put the complete bounded slice contract in `message`.'
24
+ ];
25
+ const OFFICIAL_SUBAGENT_SPAWN_COMPATIBILITY_CONTEXT = [
26
+ ...OFFICIAL_SUBAGENT_SPAWN_CONTRACT_HEAD,
22
27
  '- SKS children must pass the slice contract `model` (the newest model of the role tier), its `reasoning_effort`, and `fork_turns=\"none\"` or a positive bounded turn count. When Jev mode is on, the SKS PreToolUse hook seals Jev\'s tier on every spawn, so do not tune model or effort yourself. A stored user role-model preference wins.'
23
28
  ].join('\n');
24
- export function officialSubagentSpawnCompatibilityContext() {
25
- return OFFICIAL_SUBAGENT_SPAWN_COMPATIBILITY_CONTEXT;
29
+ function openRouterOnlyChildModelLine(allowlist) {
30
+ if (!allowlist.models.length) {
31
+ 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}.`;
32
+ }
33
+ 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.`;
34
+ }
35
+ export function officialSubagentSpawnCompatibilityContext(allowlist = effectiveChildModelAllowlist()) {
36
+ if (allowlist.mode !== 'openrouter_only')
37
+ return OFFICIAL_SUBAGENT_SPAWN_COMPATIBILITY_CONTEXT;
38
+ return [...OFFICIAL_SUBAGENT_SPAWN_CONTRACT_HEAD, openRouterOnlyChildModelLine(allowlist)].join('\n');
26
39
  }
27
40
  export function attachOfficialSubagentSpawnCompatibilityContext(state, payload, result) {
28
41
  if (result?.decision === 'block' || result?.sksTaskProfile === 'passthrough')
@@ -35,11 +48,12 @@ export function attachOfficialSubagentSpawnCompatibilityContext(state, payload,
35
48
  if (!officialSubagentsRequiredForPromptOrState(state, prompt, result))
36
49
  return result;
37
50
  const existing = String(result?.additionalContext || '');
38
- if (existing.includes(OFFICIAL_SUBAGENT_SPAWN_COMPATIBILITY_CONTEXT))
51
+ const spawnContract = officialSubagentSpawnCompatibilityContext();
52
+ if (existing.includes(spawnContract))
39
53
  return result;
40
54
  return {
41
55
  ...result,
42
- additionalContext: [OFFICIAL_SUBAGENT_SPAWN_COMPATIBILITY_CONTEXT, existing].filter(Boolean).join('\n\n')
56
+ additionalContext: [spawnContract, existing].filter(Boolean).join('\n\n')
43
57
  };
44
58
  }
45
59
  export async function attachAuthoritativeSksSkillContext(root, state, payload, result) {
@@ -207,7 +221,7 @@ export async function hookActiveSkillContextRefresh(root, state, name) {
207
221
  };
208
222
  }
209
223
  const spawnCompatibility = officialSubagentsRequiredForPromptOrState(state, '', {})
210
- ? OFFICIAL_SUBAGENT_SPAWN_COMPATIBILITY_CONTEXT
224
+ ? officialSubagentSpawnCompatibilityContext()
211
225
  : '';
212
226
  const refresh = await activeAuthoritativeSksSkillRefresh(root, state, { includeContext: true });
213
227
  if (refresh.blocked) {
@@ -32,8 +32,9 @@ export function normalizeHookResult(name, result = {}) {
32
32
  if (out.decision === 'block' || out.permissionDecision === 'deny' || out.decision === 'deny') {
33
33
  return buildPreToolUseDeny(reason, { systemMessage });
34
34
  }
35
- if (out.updatedInput)
36
- return buildPreToolUseAllowRewrite(out.updatedInput, { systemMessage });
35
+ if (out.updatedInput) {
36
+ return buildPreToolUseAllowRewrite(out.updatedInput, { additionalContext: out.additionalContext, systemMessage });
37
+ }
37
38
  return buildPreToolUseContinue({ additionalContext: out.additionalContext, systemMessage });
38
39
  }
39
40
  if (eventName === 'PermissionRequest') {
@@ -1,8 +1,12 @@
1
1
  import { consultJevTurnModel } from '../decisions/integration.js';
2
+ import { chooseChildModel, fallbackChildModel } from '../decisions/child-model-choice.js';
2
3
  import { managedOfficialSubagentRoleByName } from '../managed-assets/managed-assets-manifest.js';
4
+ import { OPENROUTER_ONLY_SCHEMA, effectiveChildModelAllowlist, isSubagentModelEffort } from '../subagents/child-model-allowlist.js';
3
5
  import { subagentModelProfile } from '../subagents/model-policy.js';
4
6
  import { effortForTier, latestModelForTier, latestTierModelSet } from '../subagents/model-tiers.js';
5
7
  import { readRoleModelPreferences } from '../subagents/role-model-preferences.js';
8
+ import { READ_ONLY_LIST_ROLE } from '../subagents/read-only-list-role.js';
9
+ import { boundedForkTurns, fullHistoryForkContext, renderChildModelList, SUBAGENT_MODELS_SETTINGS_HINT } from './subagent-spawn-policy.js';
6
10
  const SPAWN_TOOLS = new Set(['spawn_agent', 'collaboration.spawn_agent', 'functions.spawn_agent']);
7
11
  function spawnInput(payload) {
8
12
  const name = String(payload?.tool_name || payload?.toolName || payload?.tool?.name || '');
@@ -18,8 +22,16 @@ function narutoParent(state) {
18
22
  const route = String(state?.route || state?.route_command || '').replace(/^\$/, '').toUpperCase();
19
23
  return mode === 'NARUTO' || route === 'NARUTO' || state?.subagents_required === true;
20
24
  }
21
- function spawnTask(input) {
22
- return [input.message, input.prompt, input.task, input.agent_type, input.agentType]
25
+ function itemTexts(items) {
26
+ if (!Array.isArray(items))
27
+ return [];
28
+ return items
29
+ .filter((item) => item && typeof item === 'object' && item.type === 'text')
30
+ .map((item) => item.text);
31
+ }
32
+ function spawnTask(input, opts = {}) {
33
+ const items = opts.items ? itemTexts(input.items) : [];
34
+ return [input.message, input.prompt, input.task, ...items, input.agent_type, input.agentType]
23
35
  .map((value) => String(value || '').trim())
24
36
  .filter(Boolean)
25
37
  .join('\n')
@@ -33,11 +45,68 @@ export function roleTierFallback(agentType) {
33
45
  }
34
46
  return { model: latestModelForTier('deep'), effort: effortForTier('deep') };
35
47
  }
36
- export async function jevSpawnModelRewrite(root, state, payload) {
37
- if (!narutoParent(state))
38
- return null;
48
+ export async function jevSpawnRouting(root, state, payload) {
39
49
  const input = spawnInput(payload);
40
50
  if (!input)
51
+ return { input: null, route: null };
52
+ const allowlist = effectiveChildModelAllowlist();
53
+ if (allowlist.mode === 'openrouter_only')
54
+ return openRouterOnlySpawnRouting(root, state, input, allowlist);
55
+ return { input: await tierSpawnRewrite(root, state, input), route: null };
56
+ }
57
+ export async function jevSpawnModelRewrite(root, state, payload) {
58
+ return (await jevSpawnRouting(root, state, payload)).input;
59
+ }
60
+ function listState(allowlist) {
61
+ return { schema: OPENROUTER_ONLY_SCHEMA, enabled: true, subagent_models: allowlist.entries, restore: null, updated_at: null };
62
+ }
63
+ async function openRouterOnlySpawnRouting(root, parentState, input, allowlist) {
64
+ if (fullHistoryForkContext(input))
65
+ return { input: null, route: null };
66
+ const forkTurns = input.fork_turns === undefined && narutoParent(parentState) ? 'none' : input.fork_turns;
67
+ if (!boundedForkTurns(forkTurns))
68
+ return { input: null, route: null };
69
+ const state = listState(allowlist);
70
+ const requested = String(input.model || '').trim() || null;
71
+ const agent = String(input.agent_type || input.agentType || '').trim() || null;
72
+ const task = spawnTask(input, { items: true });
73
+ const choice = task
74
+ ? await chooseChildModel({ root, task, role: agent, requestedModel: requested, state })
75
+ .catch(() => fallbackChildModel(state, requested, 'consult_failed'))
76
+ : fallbackChildModel(state, requested, 'empty_task');
77
+ if (!choice)
78
+ return { input: null, route: null };
79
+ const route = {
80
+ mode: 'openrouter_only',
81
+ model: choice.entry.model,
82
+ source: choice.source,
83
+ reason: choice.reason
84
+ };
85
+ const next = { ...input, model: choice.entry.model };
86
+ const managed = agent ? managedOfficialSubagentRoleByName(agent) : null;
87
+ if (managed) {
88
+ delete next.agentType;
89
+ if (managed.sandbox === 'read-only')
90
+ next.agent_type = READ_ONLY_LIST_ROLE.codex_name;
91
+ else
92
+ delete next.agent_type;
93
+ }
94
+ const effort = choice.entry.reasoning_effort
95
+ || (isSubagentModelEffort(input.reasoning_effort) ? input.reasoning_effort : null);
96
+ if (effort)
97
+ next.reasoning_effort = effort;
98
+ else
99
+ delete next.reasoning_effort;
100
+ next.fork_turns = forkTurns;
101
+ const unchanged = !managed
102
+ && input.model === next.model
103
+ && input.reasoning_effort === next.reasoning_effort
104
+ && Object.hasOwn(input, 'reasoning_effort') === Object.hasOwn(next, 'reasoning_effort')
105
+ && input.fork_turns === next.fork_turns;
106
+ return { input: unchanged ? null : next, route };
107
+ }
108
+ async function tierSpawnRewrite(root, state, input) {
109
+ if (!narutoParent(state))
41
110
  return null;
42
111
  const agent = String(input.agent_type || input.agentType || '').trim();
43
112
  if (agent) {
@@ -67,3 +136,12 @@ export async function jevSpawnModelRewrite(root, state, payload) {
67
136
  ...forkTurns
68
137
  };
69
138
  }
139
+ export function openRouterOnlyJevTurnLine(allowlist, effort, orchestrationRequired) {
140
+ const hint = effort
141
+ ? `Jev rated this ${orchestrationRequired ? 'task' : 'turn'} as ${effort}-effort work; the parent model, effort, and service tier stay as the user set them. `
142
+ : '';
143
+ const list = allowlist.entries.length
144
+ ? `every spawn_agent child runs a model from the user's subagent list (${renderChildModelList(allowlist)}); Jev picks the list model for each spawn from the user's criteria, and any other model is denied.`
145
+ : `the subagent model list is empty, so spawn_agent is denied until the user adds models in ${SUBAGENT_MODELS_SETTINGS_HINT}.`;
146
+ return `${hint}SKS OpenRouter Only mode: ${list}`;
147
+ }
@@ -308,7 +308,8 @@ async function refreshOfficialSubagentCompletionArtifactsLocked(root, state, par
308
308
  }
309
309
  const previousGate = existingGate || {};
310
310
  const parentModel = plan.observed_parent_model || state.observed_parent_model || null;
311
- const parentModelMismatch = previousGate.parent_model_match === false || observedParentModelMismatch(parentModel, narutoParentModel());
311
+ const expectedParentModel = typeof plan.parent_model_policy === 'string' && plan.parent_model_policy ? plan.parent_model_policy : narutoParentModel();
312
+ const parentModelMismatch = previousGate.parent_model_match === false || observedParentModelMismatch(parentModel, expectedParentModel);
312
313
  const blockers = [...new Set([
313
314
  ...evidence.blockers,
314
315
  ...(Array.isArray(previousGate.config_blockers) ? previousGate.config_blockers.map(String) : []),
@@ -329,7 +330,7 @@ async function refreshOfficialSubagentCompletionArtifactsLocked(root, state, par
329
330
  route: '$Naruto',
330
331
  status: passed ? 'completed' : evidence.status,
331
332
  parent: {
332
- model: narutoParentModel(),
333
+ model: expectedParentModel,
333
334
  model_reasoning_effort: NARUTO_PARENT_EFFORT,
334
335
  observed_model: parentModel,
335
336
  observed_model_match: parentModel ? !parentModelMismatch : null
@@ -28,6 +28,14 @@ const SHELL_WRITE_RE = new RegExp([
28
28
  const PATCH_FILE_RE = /\*\*\*\s+(?:Update|Add|Delete|Move to)\s+File:\s*([^\n\r]+)/g;
29
29
  const PATCH_HEADER_RE = /\*\*\*\s+(?:Update|Add|Delete|Move to)\s+File:/;
30
30
  const SHELL_PATH_TOKEN_RE = /(?:^|\s)((?:\.{1,2}\/|\/|~\/)?[\w@.+-]+(?:\/[\w@.+-]+)+|[\w@+-]+\.[a-z0-9]{1,8})(?=\s|$)/gi;
31
+ function spawnRouteOf(raw) {
32
+ if (!raw || typeof raw !== 'object')
33
+ return null;
34
+ const row = raw;
35
+ const text = (value) => String(value ?? '').trim().slice(0, 200);
36
+ const route = { mode: text(row.mode), model: text(row.model), source: text(row.source), reason: text(row.reason) };
37
+ return route.mode && route.model && route.source ? route : null;
38
+ }
31
39
  export function isSpawnToolPayload(payload = {}) {
32
40
  return SPAWN_TOOL_RE.test(toolNameOf(payload));
33
41
  }
@@ -193,6 +201,7 @@ export async function readParentOrchestrationLedger(root, missionId, workflowRun
193
201
  return fresh;
194
202
  if (workflowRunId && raw.workflow_run_id && String(raw.workflow_run_id) !== workflowRunId)
195
203
  return fresh;
204
+ const spawnRoute = spawnRouteOf(raw.last_spawn_route);
196
205
  return {
197
206
  ...fresh,
198
207
  workflow_run_id: workflowRunId || (typeof raw.workflow_run_id === 'string' ? raw.workflow_run_id : null),
@@ -207,6 +216,7 @@ export async function readParentOrchestrationLedger(root, missionId, workflowRun
207
216
  last_tool: typeof raw.last_tool === 'string' ? raw.last_tool : null,
208
217
  last_targets: Array.isArray(raw.last_targets) ? raw.last_targets.map((row) => String(row)).slice(0, MAX_TARGETS) : [],
209
218
  last_jev_reason: typeof raw.last_jev_reason === 'string' ? raw.last_jev_reason : null,
219
+ ...(spawnRoute ? { last_spawn_route: spawnRoute } : {}),
210
220
  updated_at: typeof raw.updated_at === 'string' ? raw.updated_at : null
211
221
  };
212
222
  }
@@ -227,7 +237,14 @@ export async function recordParentOrchestrationSpawn(root, state = {}, opts = {}
227
237
  const ledger = await readParentOrchestrationLedger(root, missionId, workflowRunId);
228
238
  if (opts.atLeastOne && ledger.spawns > 0)
229
239
  return ledger;
230
- const next = { ...ledger, spawns: ledger.spawns + 1, last_action: 'spawn' };
240
+ const spawnRoute = spawnRouteOf(opts.spawnRoute);
241
+ const { last_spawn_route: _previousRoute, ...rest } = ledger;
242
+ const next = {
243
+ ...rest,
244
+ spawns: ledger.spawns + 1,
245
+ last_action: 'spawn',
246
+ ...(spawnRoute ? { last_spawn_route: spawnRoute } : {})
247
+ };
231
248
  await writeLedger(root, next);
232
249
  return next;
233
250
  }
@@ -1,8 +1,10 @@
1
1
  import path from 'node:path';
2
2
  import { readJson } from '../fsx.js';
3
3
  import { managedOfficialSubagentRoleByName } from '../managed-assets/managed-assets-manifest.js';
4
- import { latestModelForTier, latestTierModelSet } from '../subagents/model-tiers.js';
4
+ import { latestModelForTier } from '../subagents/model-tiers.js';
5
+ import { effectiveChildModelAllowlist } from '../subagents/child-model-allowlist.js';
5
6
  import { jevEnabled, readDecisionConfig } from '../decisions/config.js';
7
+ import { renderChildModelList } from './subagent-spawn-policy.js';
6
8
  export async function sealedSubagentRoutingContext(artifactDir, payload = {}) {
7
9
  const plan = await readJson(path.join(artifactDir, 'subagent-plan.json'), null).catch(() => null);
8
10
  if (!plan || plan.workflow !== 'official_codex_subagent')
@@ -13,6 +15,15 @@ export async function sealedSubagentRoutingContext(artifactDir, payload = {}) {
13
15
  const role = agentName ? managedOfficialSubagentRoleByName(agentName) : null;
14
16
  if (!agentName)
15
17
  return '';
18
+ const allowlist = effectiveChildModelAllowlist();
19
+ if (allowlist.mode === 'openrouter_only') {
20
+ return [
21
+ 'SKS sealed child routing:',
22
+ `- custom agent: ${agentName}`,
23
+ `- model and model_reasoning_effort: sealed on the spawn call from the OpenRouter Only subagent list (${renderChildModelList(allowlist) || 'empty'})`,
24
+ '- keep this sealed profile; do not retarget model/effort or spawn nested agents'
25
+ ].join('\n');
26
+ }
16
27
  const narutoPlan = plan.mode === 'naruto' || plan.route === '$Naruto';
17
28
  const jevSealed = await readDecisionConfig().then(jevEnabled).catch(() => false);
18
29
  if (jevSealed) {
@@ -24,7 +35,7 @@ export async function sealedSubagentRoutingContext(artifactDir, payload = {}) {
24
35
  ].join('\n');
25
36
  }
26
37
  const plannedModel = String(planned?.routed_model || planned?.model || '').trim();
27
- const sealedModels = latestTierModelSet();
38
+ const sealedModels = new Set(allowlist.models);
28
39
  const model = narutoPlan && sealedModels.has(plannedModel) ? plannedModel : (role?.model || latestModelForTier('deep'));
29
40
  const effort = String((model === plannedModel
30
41
  ? planned?.routed_model_reasoning_effort || planned?.model_reasoning_effort
@@ -1,16 +1,59 @@
1
- import { latestTierModelSet } from '../subagents/model-tiers.js';
1
+ import { managedOfficialSubagentRoleByName } from '../managed-assets/managed-assets-manifest.js';
2
+ import { READ_ONLY_LIST_ROLE } from '../subagents/read-only-list-role.js';
3
+ import { effectiveChildModelAllowlist, isAllowedChildModel } from '../subagents/child-model-allowlist.js';
2
4
  const SPAWN_TOOLS = new Set(['spawn_agent', 'collaboration.spawn_agent', 'functions.spawn_agent']);
5
+ export const SUBAGENT_MODELS_SETTINGS_HINT = 'SKS Control Center > Subagent Models';
6
+ export function renderChildModelList(allowlist) {
7
+ if (allowlist.mode !== 'openrouter_only')
8
+ return allowlist.models.join(', ');
9
+ return allowlist.entries.map((entry) => `${entry.model}${entry.default ? ' (default)' : ''}`).join(', ');
10
+ }
11
+ export function renderChildModelCriteria(allowlist) {
12
+ if (allowlist.mode !== 'openrouter_only')
13
+ return allowlist.models.join(', ');
14
+ return allowlist.entries
15
+ .map((entry) => `${entry.model}${entry.default ? ' (default)' : ''}: ${entry.criteria || 'general work'}`)
16
+ .join('; ');
17
+ }
18
+ function openRouterOnlyBlockReason(allowlist) {
19
+ if (!allowlist.models.length && allowlist.mode === 'openrouter_only' && allowlist.unroutable?.length) {
20
+ return `SKS OpenRouter Only mode: no model on the subagent list has an OpenRouter route right now (${allowlist.unroutable.join(', ')}), so no child may be spawned. Ask the user to replace them in ${SUBAGENT_MODELS_SETTINGS_HINT}, then retry spawn_agent. Do not implement the slice in the parent instead.`;
21
+ }
22
+ if (!allowlist.models.length) {
23
+ return `SKS OpenRouter Only mode is on but the subagent model list is empty, so no child may be spawned. Ask the user to add models in ${SUBAGENT_MODELS_SETTINGS_HINT} (or to turn OpenRouter Only off), then retry spawn_agent. Do not implement the slice in the parent instead.`;
24
+ }
25
+ return `SKS OpenRouter Only mode: children may run only a model on the user's subagent list: ${renderChildModelList(allowlist)}. Every other model is denied. Retry spawn_agent with one of them (the SKS hook routes each spawn to a list model, by Jev when Jev mode is on) and fork_turns="none" or a positive bounded turn count. Include the complete slice contract in message; do not inherit the parent model. The user adds models in ${SUBAGENT_MODELS_SETTINGS_HINT}.`;
26
+ }
27
+ export function fullHistoryForkContext(input) {
28
+ return input.fork_context === true || String(input.fork_context ?? '').trim().toLowerCase() === 'true';
29
+ }
30
+ export function boundedForkTurns(value) {
31
+ return value === 'none' || /^[1-9]\d*$/.test(String(value || ''));
32
+ }
33
+ const FORK_BLOCK_REASON = 'SKS child spawns require fork_turns="none" or a positive bounded turn count. Retry with the complete slice contract in message; full-history/default forks cannot carry an explicit child model.';
3
34
  export function subagentSpawnPolicyBlockReason(payload = {}) {
4
35
  const name = String(payload.tool_name || payload.toolName || payload.tool?.name || '');
5
36
  if (!SPAWN_TOOLS.has(name))
6
37
  return null;
7
38
  const input = payload.tool_input || payload.toolInput || payload.tool?.input || {};
8
- const allowed = latestTierModelSet();
9
- if (!allowed.has(String(input.model || ''))) {
10
- return `SKS children must name a current model: ${[...allowed].join(', ')} (the latest fast, balanced, context, or deep tier). Retry spawn_agent with the slice contract model, which the SKS hook re-seals when Jev mode is on, and fork_turns="none" or a positive bounded turn count. Include the complete slice contract in message; do not inherit the parent model.`;
39
+ const allowlist = effectiveChildModelAllowlist();
40
+ if (allowlist.mode === 'openrouter_only') {
41
+ if (!isAllowedChildModel(input.model, allowlist))
42
+ return openRouterOnlyBlockReason(allowlist);
43
+ if (fullHistoryForkContext(input))
44
+ return `${FORK_BLOCK_REASON} Omit fork_context (fork_context=true is a full-history fork).`;
45
+ const agent = String(input.agent_type || input.agentType || '').trim();
46
+ if (agent && managedOfficialSubagentRoleByName(agent)) {
47
+ return `SKS OpenRouter Only mode: agent_type "${agent}" pins a tier model, so it cannot run a list model. Omit agent_type and put the role brief in message; read-only slices pass agent_type="${READ_ONLY_LIST_ROLE.codex_name}".`;
48
+ }
11
49
  }
12
- if (input.fork_turns !== 'none' && !/^[1-9]\d*$/.test(String(input.fork_turns || ''))) {
13
- return 'SKS child spawns require fork_turns="none" or a positive bounded turn count. Retry with the complete slice contract in message; full-history/default forks cannot carry an explicit child model.';
50
+ else {
51
+ const allowed = new Set(allowlist.models);
52
+ if (!allowed.has(String(input.model || ''))) {
53
+ return `SKS children must name a current model: ${[...allowed].join(', ')} (the latest fast, balanced, context, or deep tier). Retry spawn_agent with the slice contract model, which the SKS hook re-seals when Jev mode is on, and fork_turns="none" or a positive bounded turn count. Include the complete slice contract in message; do not inherit the parent model.`;
54
+ }
14
55
  }
56
+ if (!boundedForkTurns(input.fork_turns))
57
+ return FORK_BLOCK_REASON;
15
58
  return null;
16
59
  }
@@ -31,7 +31,8 @@ import { activeNarutoParentLaunchMissionId, claimStandaloneParentHostCapabilityR
31
31
  import { classifyTaskProfile } from './runtime/task-profile.js';
32
32
  import { resolveSubagentThreadBudget } from './subagents/thread-budget.js';
33
33
  import { readOfficialSubagentConfig } from './subagents/official-subagent-config.js';
34
- import { jevSpawnModelRewrite } from './hooks-runtime/jev-spawn-routing.js';
34
+ import { jevSpawnRouting, openRouterOnlyJevTurnLine } from './hooks-runtime/jev-spawn-routing.js';
35
+ import { effectiveChildModelAllowlist } from './subagents/child-model-allowlist.js';
35
36
  import { maybeReconcileManagedGuidancePreflight } from './hooks-runtime/managed-guidance-preflight.js';
36
37
  import { evaluateParentOrchestrationGate, isSpawnToolPayload, recordParentOrchestrationSpawn } from './hooks-runtime/parent-orchestration-gate.js';
37
38
  import { subagentSpawnPolicyBlockReason } from './hooks-runtime/subagent-spawn-policy.js';
@@ -295,11 +296,15 @@ async function attachJevTurnRouting(root, payload, result, orchestrationRequired
295
296
  if (plan?.routeOverride) {
296
297
  result = attachContextLine(result, `Jev routed this prompt to the ${plan.routeOverride.routeId} pipeline (the keyword router guessed ${plan.baselineRouteId || 'none'}).`);
297
298
  }
298
- const line = decision.model
299
+ const tierLine = decision.model
299
300
  ? orchestrationRequired
300
301
  ? `Jev rated this task as ${decision.model} (${decision.effort}). Use that as the default child seal; Jev re-seals every spawn_agent call and the parent model stays as the user set it.`
301
302
  : `Jev rated this turn as ${decision.effort}-effort work (${decision.model} tier). Treat it as this turn's reasoning hint; the parent model, effort, and service tier stay as the user set them.`
302
303
  : '';
304
+ const allowlist = effectiveChildModelAllowlist();
305
+ const line = allowlist.mode === 'openrouter_only'
306
+ ? openRouterOnlyJevTurnLine(allowlist, decision.effort, orchestrationRequired)
307
+ : tierLine;
303
308
  if (!line)
304
309
  return { ...result, jev_turn: decision };
305
310
  const additionalContext = [result.additionalContext, line].filter(Boolean).join('\n\n');
@@ -649,7 +654,8 @@ async function consumeActiveOfficialWorkflowQueue(root, missionId, workflowRunId
649
654
  }
650
655
  }
651
656
  async function hookPreTool(root, state, payload, noQuestion, sessionKey = null) {
652
- const jevSpawnInput = await jevSpawnModelRewrite(root, state, payload).catch(() => null);
657
+ const jevSpawn = await jevSpawnRouting(root, state, payload).catch(() => null);
658
+ const jevSpawnInput = jevSpawn?.input ?? null;
653
659
  const spawnPayload = jevSpawnInput
654
660
  ? { ...payload, tool_input: jevSpawnInput, toolInput: jevSpawnInput }
655
661
  : payload;
@@ -724,8 +730,9 @@ async function hookPreTool(root, state, payload, noQuestion, sessionKey = null)
724
730
  const hostCapabilityDecision = await enforceHostCapabilityPreTool(root, state, payload, sessionKey);
725
731
  if (hostCapabilityDecision && hostCapabilityDecision.continue !== true)
726
732
  return hostCapabilityDecision;
727
- if (isSpawnToolPayload(spawnPayload))
728
- await recordParentOrchestrationSpawn(root, state).catch(() => null);
733
+ if (isSpawnToolPayload(spawnPayload)) {
734
+ await recordParentOrchestrationSpawn(root, state, jevSpawn?.route ? { spawnRoute: jevSpawn.route } : {}).catch(() => null);
735
+ }
729
736
  const waveGuidance = await parentWaveGuidanceContext(root, state, sessionKey).catch(() => '');
730
737
  const escapeNote = orchestration?.action === 'escape' ? orchestration.message || '' : '';
731
738
  const additionalContext = [skillRefresh.context, waveGuidance, escapeNote].filter(Boolean).join('\n\n');
@@ -551,21 +551,26 @@ export function managedOfficialSubagentRoleBody(role) {
551
551
  ].join('\n');
552
552
  }
553
553
  export function managedOfficialSubagentRoleContent(role) {
554
- const body = managedOfficialSubagentRoleBody(role);
554
+ return managedOfficialSubagentFileContent(role.id, role.schema_version, managedOfficialSubagentRoleBody(role));
555
+ }
556
+ export function managedOfficialSubagentFileContent(id, schemaVersion, body) {
555
557
  return [
556
558
  `# ${MANAGED_OFFICIAL_SUBAGENT_MARKER}`,
557
- `# sks_managed_schema = ${role.schema_version}`,
558
- `# sks_managed_id = "${role.id}"`,
559
+ `# sks_managed_schema = ${schemaVersion}`,
560
+ `# sks_managed_id = "${id}"`,
559
561
  `# sks_managed_body_sha256 = "${sha256(body)}"`,
560
562
  '',
561
563
  body
562
564
  ].join('\n');
563
565
  }
564
566
  export function managedOfficialSubagentRoleOwnsText(text, role) {
567
+ return managedOfficialSubagentFileOwnsText(text, role.id);
568
+ }
569
+ export function managedOfficialSubagentFileOwnsText(text, id) {
565
570
  const source = String(text || '');
566
571
  if (!source.includes(`# ${MANAGED_OFFICIAL_SUBAGENT_MARKER}`))
567
572
  return false;
568
- if (!source.includes(`sks_managed_id = "${role.id}"`))
573
+ if (!source.includes(`sks_managed_id = "${id}"`))
569
574
  return false;
570
575
  const lines = source.split('\n');
571
576
  const hashIndex = lines.findIndex((line) => /^#\s*sks_managed_body_sha256\s*=/.test(line.trim()));