sneakoscope 10.3.9 → 10.3.10

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 (68) hide show
  1. package/README.md +5 -5
  2. package/crates/sks-core/Cargo.lock +1 -1
  3. package/crates/sks-core/Cargo.toml +1 -1
  4. package/dist/config/skills-manifest.json +1 -1
  5. package/dist/core/agent-bridge/async-command.js +5 -3
  6. package/dist/core/agents/agent-effort-policy.js +3 -6
  7. package/dist/core/agents/agent-orchestrator.js +0 -1
  8. package/dist/core/agents/agent-roster.js +2 -4
  9. package/dist/core/agents/native-worker-backend-router.js +5 -5
  10. package/dist/core/codex-app/codex-model-catalog.js +0 -3
  11. package/dist/core/codex-app/openrouter-provider.js +0 -1
  12. package/dist/core/codex-compat/codex-capability-matrix.js +0 -9
  13. package/dist/core/codex-compat/codex-runtime-contract.js +0 -1
  14. package/dist/core/codex-control/codex-sdk-config-policy.js +2 -1
  15. package/dist/core/codex-lb/combined-catalog/builder.js +2 -2
  16. package/dist/core/codex-lb/combined-catalog/latest-generation.js +53 -0
  17. package/dist/core/codex-lb/desktop-controller-v3/live-probes.js +2 -3
  18. package/dist/core/codex-lb/desktop-controller-v3/preflight-probes.js +2 -3
  19. package/dist/core/codex-lb/desktop-controller-v3/probe-route.js +23 -0
  20. package/dist/core/codex-model-guard.js +0 -9
  21. package/dist/core/commands/naruto-command.js +2 -2
  22. package/dist/core/doctor/doctor-codex-startup-repair.js +8 -0
  23. package/dist/core/doctor/stale-subagent-default.js +30 -0
  24. package/dist/core/harness-conflicts.js +1 -1
  25. package/dist/core/hooks-runtime/exclusive-gui-surface-gate.js +155 -0
  26. package/dist/core/hooks-runtime/global-role-pin-refresh.js +25 -0
  27. package/dist/core/hooks-runtime/hook-context.js +3 -1
  28. package/dist/core/hooks-runtime/jev-spawn-routing.js +2 -6
  29. package/dist/core/hooks-runtime/managed-guidance-preflight.js +20 -2
  30. package/dist/core/hooks-runtime/parent-orchestration-gate.js +3 -3
  31. package/dist/core/hooks-runtime/payload-signals.js +1 -1
  32. package/dist/core/hooks-runtime/spawn-role-pin-heal.js +15 -0
  33. package/dist/core/hooks-runtime/spawn-tool-name.js +7 -0
  34. package/dist/core/hooks-runtime/subagent-spawn-policy.js +7 -4
  35. package/dist/core/hooks-runtime/subagent-stop-hook.js +2 -0
  36. package/dist/core/hooks-runtime.js +12 -1
  37. package/dist/core/init.js +1 -0
  38. package/dist/core/managed-assets/managed-assets-manifest.js +1 -1
  39. package/dist/core/pipeline-internals/runtime-core.js +2 -3
  40. package/dist/core/provider/model-router.js +8 -11
  41. package/dist/core/recallpulse/policy.js +3 -3
  42. package/dist/core/recallpulse.js +1 -1
  43. package/dist/core/research/mock-result.js +3 -3
  44. package/dist/core/research/research-adversarial-review.js +12 -12
  45. package/dist/core/research/research-claim-synthesizer.js +5 -5
  46. package/dist/core/research/research-falsification-runner.js +5 -5
  47. package/dist/core/research/research-super-search.js +2 -2
  48. package/dist/core/research/research-synthesis-writer.js +4 -4
  49. package/dist/core/research.js +7 -7
  50. package/dist/core/routes.js +5 -3
  51. package/dist/core/subagents/agent-catalog.js +3 -0
  52. package/dist/core/subagents/exclusive-surface-rule.js +3 -0
  53. package/dist/core/subagents/exclusive-tool-surface.js +19 -0
  54. package/dist/core/subagents/model-policy.js +8 -11
  55. package/dist/core/subagents/model-tiers.js +99 -22
  56. package/dist/core/subagents/naruto-help-contract.js +7 -7
  57. package/dist/core/subagents/naruto-host-credentials.js +3 -3
  58. package/dist/core/subagents/official-subagent-config.js +11 -5
  59. package/dist/core/subagents/official-subagent-prompt.js +3 -1
  60. package/dist/core/subagents/role-model-pins.js +84 -0
  61. package/dist/core/version.js +1 -1
  62. package/dist/native/sks-menubar/Sources/SettingsViewController.swift +1 -1
  63. package/dist/scripts/codex-sdk-backend-router-check.js +4 -5
  64. package/package.json +1 -1
  65. package/dist/core/codex-app/codex-desktop-routing-ownership.js +0 -90
  66. package/dist/core/codex-app/codex-models-cache.js +0 -153
  67. package/dist/core/codex-app/multi-provider-router-support.js +0 -25
  68. package/dist/core/codex-app/role-model-catalog-priority.js +0 -61
@@ -0,0 +1,155 @@
1
+ import path from 'node:path';
2
+ import { readJson, writeJsonAtomic } from '../fsx.js';
3
+ import { withFileLock } from '../locks/file-lock.js';
4
+ import { ensureConfinedDirectory } from '../managed-path-safety.js';
5
+ import { EXCLUSIVE_SURFACE_LABEL, exclusiveSurfaceOfRole } from '../subagents/exclusive-tool-surface.js';
6
+ import { officialSubagentArtifactDir } from './official-subagent-lifecycle.js';
7
+ import { isSpawnAgentToolName, spawnPayloadToolName } from './spawn-tool-name.js';
8
+ const EXCLUSIVE_GUI_SURFACE_LEDGER_SCHEMA = 'sks.exclusive-gui-surface-ledger.v1';
9
+ export const EXCLUSIVE_GUI_SURFACE_LEDGER_FILENAME = 'exclusive-gui-surfaces.json';
10
+ export const PENDING_CLAIM_TTL_MS = 30_000;
11
+ export const RUNNING_CLAIM_TTL_MS = 20 * 60_000;
12
+ const CLAIM_REFRESH_INTERVAL_MS = 30_000;
13
+ const ALLOW = { action: 'allow' };
14
+ function text(value) {
15
+ return String(value ?? '').trim();
16
+ }
17
+ function spawnToolInput(payload) {
18
+ const input = payload?.tool_input || payload?.toolInput || payload?.tool?.input || payload?.input;
19
+ return input && typeof input === 'object' && !Array.isArray(input) ? input : {};
20
+ }
21
+ function ledgerPaths(root, sessionKey) {
22
+ const dir = officialSubagentArtifactDir(root, {}, sessionKey);
23
+ return {
24
+ dir,
25
+ file: path.join(dir, EXCLUSIVE_GUI_SURFACE_LEDGER_FILENAME),
26
+ lock: path.join(dir, '.exclusive-gui-surface.lock')
27
+ };
28
+ }
29
+ function validClaim(raw) {
30
+ return Boolean(raw)
31
+ && (raw.surface === 'computer_use' || raw.surface === 'browser')
32
+ && (raw.state === 'pending' || raw.state === 'running')
33
+ && (raw.agent_id === null || typeof raw.agent_id === 'string')
34
+ && Number.isFinite(Date.parse(raw.claimed_at))
35
+ && Number.isFinite(Date.parse(raw.last_seen_at));
36
+ }
37
+ async function readClaims(file) {
38
+ const raw = await readJson(file, null).catch(() => null);
39
+ return Array.isArray(raw?.claims) ? raw.claims.filter(validClaim) : [];
40
+ }
41
+ function isLive(claim, at) {
42
+ return claim.state === 'pending'
43
+ ? at - Date.parse(claim.claimed_at) <= PENDING_CLAIM_TTL_MS
44
+ : at - Date.parse(claim.last_seen_at) <= RUNNING_CLAIM_TTL_MS;
45
+ }
46
+ async function withClaims(input, change) {
47
+ const paths = ledgerPaths(input.root, input.sessionKey);
48
+ await ensureConfinedDirectory(path.resolve(input.root), paths.dir);
49
+ return withFileLock({ lockPath: paths.lock, timeoutMs: 5_000, staleMs: 60_000 }, async () => {
50
+ const at = input.now ?? Date.now();
51
+ const before = await readClaims(paths.file);
52
+ const beforeJson = JSON.stringify(before);
53
+ const outcome = change(before.filter((claim) => isLive(claim, at)), at);
54
+ if (JSON.stringify(outcome.claims) !== beforeJson) {
55
+ await writeJsonAtomic(paths.file, { schema: EXCLUSIVE_GUI_SURFACE_LEDGER_SCHEMA, claims: outcome.claims });
56
+ }
57
+ return outcome.result;
58
+ });
59
+ }
60
+ function denyMessage(surface, agentType, owner, at) {
61
+ const label = EXCLUSIVE_SURFACE_LABEL[surface];
62
+ const seconds = Math.max(0, Math.round((at - Date.parse(owner.claimed_at)) / 1000));
63
+ const who = owner.agent_id
64
+ ? `child ${owner.agent_id}, which has not stopped (it frees itself ${RUNNING_CLAIM_TTL_MS / 60_000} minutes after its last tool call)`
65
+ : `a spawn allowed ${seconds} s ago that has not started yet (if Codex rejected that spawn, the surface frees itself ${PENDING_CLAIM_TTL_MS / 1000} s after it)`;
66
+ return `SKS exclusive GUI surface gate denied spawn_agent (agent_type "${agentType}"): the ${label} surface is already owned by ${who}. ${label} drives one shared screen or session with one pointer and keyboard focus, so only one child may operate it at a time and a second operator would only repeat the same task. Wait for that child to finish and use its returned evidence, or fold this task into that single operator slice; spawn another ${label} child only after it stops. Do not give the same task to another child and do not do it in the parent while that child runs.`;
67
+ }
68
+ export async function claimExclusiveGuiSurface(input) {
69
+ if (!isSpawnAgentToolName(spawnPayloadToolName(input.payload)))
70
+ return ALLOW;
71
+ const spawn = spawnToolInput(input.payload);
72
+ const agentType = text(spawn.agent_type ?? spawn.agentType);
73
+ const surface = exclusiveSurfaceOfRole(agentType);
74
+ if (!surface)
75
+ return ALLOW;
76
+ const toolUseId = text(input.payload?.tool_use_id ?? input.payload?.toolUseId) || null;
77
+ try {
78
+ return await withClaims(input, (claims, at) => {
79
+ const owner = claims.find((claim) => claim.surface === surface);
80
+ if (owner?.state === 'pending' && toolUseId && owner.tool_use_id === toolUseId)
81
+ return { claims, result: ALLOW };
82
+ if (owner)
83
+ return { claims, result: { action: 'block', message: denyMessage(surface, agentType, owner, at) } };
84
+ const iso = new Date(at).toISOString();
85
+ const pending = { surface, state: 'pending', agent_type: agentType, agent_id: null, tool_use_id: toolUseId, claimed_at: iso, last_seen_at: iso };
86
+ return { claims: [...claims, pending], result: ALLOW };
87
+ });
88
+ }
89
+ catch {
90
+ return ALLOW;
91
+ }
92
+ }
93
+ export async function bindExclusiveGuiSurfaceStart(input) {
94
+ const agentId = text(input.payload?.agent_id ?? input.payload?.agentId);
95
+ const agentType = text(input.payload?.agent_type ?? input.payload?.agentType);
96
+ const surface = exclusiveSurfaceOfRole(agentType);
97
+ if (!agentId || !surface)
98
+ return;
99
+ await withClaims(input, (claims, at) => {
100
+ const iso = new Date(at).toISOString();
101
+ const rest = claims.filter((claim) => claim.agent_id !== agentId || claim.surface === surface);
102
+ const target = rest.find((claim) => claim.agent_id === agentId)
103
+ ?? rest
104
+ .filter((claim) => claim.state === 'pending' && claim.surface === surface)
105
+ .sort((left, right) => Date.parse(left.claimed_at) - Date.parse(right.claimed_at))[0];
106
+ if (target) {
107
+ target.state = 'running';
108
+ target.agent_id = agentId;
109
+ target.last_seen_at = iso;
110
+ return { claims: rest, result: undefined };
111
+ }
112
+ rest.push({ surface, state: 'running', agent_type: agentType, agent_id: agentId, claimed_at: iso, last_seen_at: iso });
113
+ return { claims: rest, result: undefined };
114
+ });
115
+ }
116
+ export async function releaseExclusiveGuiSurface(input) {
117
+ const agentId = text(input.payload?.agent_id ?? input.payload?.agentId);
118
+ if (!agentId)
119
+ return;
120
+ const { file } = ledgerPaths(input.root, input.sessionKey);
121
+ if (!(await readClaims(file)).some((claim) => claim.agent_id === agentId))
122
+ return;
123
+ await withClaims(input, (claims) => ({
124
+ claims: claims.filter((claim) => claim.agent_id !== agentId),
125
+ result: undefined
126
+ }));
127
+ }
128
+ export async function refreshExclusiveGuiSurfaceOwner(input) {
129
+ const agentId = text(input.payload?.agent_id ?? input.payload?.agentId);
130
+ if (!agentId)
131
+ return;
132
+ const agentType = text(input.payload?.agent_type ?? input.payload?.agentType);
133
+ const surface = exclusiveSurfaceOfRole(agentType);
134
+ const { file } = ledgerPaths(input.root, input.sessionKey);
135
+ const at = input.now ?? Date.now();
136
+ const claims = await readClaims(file);
137
+ const mine = claims.find((claim) => claim.agent_id === agentId);
138
+ if (mine) {
139
+ if (at - Date.parse(mine.last_seen_at) < CLAIM_REFRESH_INTERVAL_MS)
140
+ return;
141
+ }
142
+ else if (!surface || claims.some((claim) => claim.surface === surface && isLive(claim, at))) {
143
+ return;
144
+ }
145
+ await withClaims(input, (live, now) => {
146
+ const iso = new Date(now).toISOString();
147
+ const claim = live.find((row) => row.agent_id === agentId);
148
+ if (claim)
149
+ claim.last_seen_at = iso;
150
+ else if (surface && !live.some((row) => row.surface === surface)) {
151
+ live.push({ surface, state: 'running', agent_type: agentType, agent_id: agentId, claimed_at: iso, last_seen_at: iso });
152
+ }
153
+ return { claims: live, result: undefined };
154
+ });
155
+ }
@@ -0,0 +1,25 @@
1
+ import path from 'node:path';
2
+ import { globalSksRoot, nowIso, readJson, writeJsonAtomic } from '../fsx.js';
3
+ import { tierModelsFingerprint } from '../subagents/model-tiers.js';
4
+ import { refreshStaleManagedRolePins } from '../subagents/role-model-pins.js';
5
+ export const GLOBAL_ROLE_PINS_STAMP_SCHEMA = 'sks.global-role-pins-stamp.v1';
6
+ export function globalRolePinsStampPath() {
7
+ return path.join(globalSksRoot(), 'state', 'global-role-pins.json');
8
+ }
9
+ export async function maybeRefreshGlobalRolePins() {
10
+ const tierModels = tierModelsFingerprint();
11
+ if (tierModels === null)
12
+ return null;
13
+ const stampPath = globalRolePinsStampPath();
14
+ const stamp = await readJson(stampPath, null).catch(() => null);
15
+ if (stamp?.schema === GLOBAL_ROLE_PINS_STAMP_SCHEMA && stamp?.tier_models === tierModels)
16
+ return null;
17
+ const result = await refreshStaleManagedRolePins({ globalOnly: true });
18
+ await writeJsonAtomic(stampPath, {
19
+ schema: GLOBAL_ROLE_PINS_STAMP_SCHEMA,
20
+ tier_models: tierModels,
21
+ role_pins: result,
22
+ checked_at: nowIso()
23
+ });
24
+ return result;
25
+ }
@@ -9,6 +9,7 @@ import { resolveManagedSkillSourcesForAdmission } from './managed-skill-admissio
9
9
  import { managedSkillDigestBlocksEnforced } from '../verification-profile.js';
10
10
  import { looksLikeActiveContinuationPrompt } from './naruto-decision-gate.js';
11
11
  import { effectiveChildModelAllowlist } from '../subagents/child-model-allowlist.js';
12
+ import { EXCLUSIVE_SURFACE_SPAWN_LINE } from '../subagents/exclusive-surface-rule.js';
12
13
  import { renderChildModelCriteria, SUBAGENT_MODELS_SETTINGS_HINT } from './subagent-spawn-policy.js';
13
14
  import { extractUserPrompt, looksLikeCodexGitAction, looksLikeCodexUiSettingsEvent } from './payload-signals.js';
14
15
  const STANDALONE_PARENT_BASE_SKILLS = [
@@ -20,7 +21,8 @@ const STANDALONE_PARENT_BASE_SKILLS = [
20
21
  const OFFICIAL_SUBAGENT_SPAWN_CONTRACT_HEAD = [
21
22
  'SKS official-subagent spawn contract:',
22
23
  '- Full-history forks (`fork_turns="all"`, including the omitted/default full-history mode) inherit the parent agent type, model, and reasoning effort.',
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
+ '- 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`.',
25
+ `- ${EXCLUSIVE_SURFACE_SPAWN_LINE}`
24
26
  ];
25
27
  const OFFICIAL_SUBAGENT_SPAWN_COMPATIBILITY_CONTEXT = [
26
28
  ...OFFICIAL_SUBAGENT_SPAWN_CONTRACT_HEAD,
@@ -7,10 +7,9 @@ import { effortForTier, latestModelForTier, latestTierModelSet } from '../subage
7
7
  import { readRoleModelPreferences } from '../subagents/role-model-preferences.js';
8
8
  import { READ_ONLY_LIST_ROLE } from '../subagents/read-only-list-role.js';
9
9
  import { boundedForkTurns, fullHistoryForkContext, renderChildModelList, SUBAGENT_MODELS_SETTINGS_HINT } from './subagent-spawn-policy.js';
10
- const SPAWN_TOOLS = new Set(['spawn_agent', 'collaboration.spawn_agent', 'functions.spawn_agent']);
10
+ import { isSpawnAgentToolName, spawnPayloadToolName } from './spawn-tool-name.js';
11
11
  function spawnInput(payload) {
12
- const name = String(payload?.tool_name || payload?.toolName || payload?.tool?.name || '');
13
- if (!SPAWN_TOOLS.has(name))
12
+ if (!isSpawnAgentToolName(spawnPayloadToolName(payload)))
14
13
  return null;
15
14
  const input = payload?.tool_input || payload?.toolInput || payload?.tool?.input;
16
15
  if (!input || typeof input !== 'object' || Array.isArray(input))
@@ -54,9 +53,6 @@ export async function jevSpawnRouting(root, state, payload) {
54
53
  return openRouterOnlySpawnRouting(root, state, input, allowlist);
55
54
  return { input: await tierSpawnRewrite(root, state, input), route: null };
56
55
  }
57
- export async function jevSpawnModelRewrite(root, state, payload) {
58
- return (await jevSpawnRouting(root, state, payload)).input;
59
- }
60
56
  function listState(allowlist) {
61
57
  return { schema: OPENROUTER_ONLY_SCHEMA, enabled: true, subagent_models: allowlist.entries, restore: null, updated_at: null };
62
58
  }
@@ -3,21 +3,37 @@ import path from 'node:path';
3
3
  import { nowIso, PACKAGE_VERSION, readJson, writeJsonAtomic } from '../fsx.js';
4
4
  import { ensureConfinedDirectory } from '../managed-path-safety.js';
5
5
  import { reconcileManagedProjectPromptGuidance } from '../doctor/current-project-guidance.js';
6
+ import { tierModelsFingerprint } from '../subagents/model-tiers.js';
7
+ import { refreshStaleManagedRolePins } from '../subagents/role-model-pins.js';
8
+ import { maybeRefreshGlobalRolePins } from './global-role-pin-refresh.js';
6
9
  export const MANAGED_GUIDANCE_STAMP_SCHEMA = 'sks.managed-guidance-generation.v1';
7
10
  export function managedGuidanceStampPath(root) {
8
11
  return path.join(root, '.sneakoscope', 'state', 'managed-guidance-generation.json');
9
12
  }
10
13
  export async function maybeReconcileManagedGuidancePreflight(root) {
14
+ await maybeRefreshGlobalRolePins().catch(() => null);
11
15
  const projectRoot = path.resolve(root);
12
16
  const stampPath = managedGuidanceStampPath(projectRoot);
13
17
  const stamp = await readJson(stampPath, null).catch(() => null);
14
- if (stamp?.schema === MANAGED_GUIDANCE_STAMP_SCHEMA && stamp?.version === PACKAGE_VERSION)
18
+ const tierModels = tierModelsFingerprint();
19
+ if (stamp?.schema === MANAGED_GUIDANCE_STAMP_SCHEMA && stamp?.version === PACKAGE_VERSION
20
+ && (tierModels === null || stamp?.tier_models === tierModels))
15
21
  return null;
16
22
  if (!(await isSksProject(projectRoot)))
17
23
  return null;
18
24
  const report = await reconcileManagedProjectPromptGuidance(projectRoot);
19
25
  const migration = await queueProjectMigration(projectRoot).catch(() => null);
20
- if (report.errors > 0)
26
+ let rolePins = null;
27
+ let rolePinsFailed = false;
28
+ if (tierModels !== null) {
29
+ try {
30
+ rolePins = await refreshStaleManagedRolePins({ root: projectRoot });
31
+ }
32
+ catch {
33
+ rolePinsFailed = true;
34
+ }
35
+ }
36
+ if (report.errors > 0 || rolePinsFailed)
21
37
  return { refreshed: report.refreshed, migration };
22
38
  await ensureConfinedDirectory(projectRoot, path.dirname(stampPath));
23
39
  await writeJsonAtomic(stampPath, {
@@ -25,6 +41,8 @@ export async function maybeReconcileManagedGuidancePreflight(root) {
25
41
  version: PACKAGE_VERSION,
26
42
  refreshed: report.refreshed,
27
43
  migration,
44
+ tier_models: tierModels,
45
+ role_pins: rolePins,
28
46
  checked_at: nowIso()
29
47
  });
30
48
  return { refreshed: report.refreshed, migration };
@@ -5,11 +5,11 @@ import { nowIso, readJson, sha256, writeJsonAtomic } from '../fsx.js';
5
5
  import { ensureConfinedDirectory } from '../managed-path-safety.js';
6
6
  import { missionDir } from '../mission.js';
7
7
  import { readSubagentEvents } from '../subagents/subagent-evidence.js';
8
+ import { isSpawnAgentToolName } from './spawn-tool-name.js';
8
9
  export const PARENT_ORCHESTRATION_GATE_FILENAME = 'parent-orchestration-gate.json';
9
10
  export const PARENT_ORCHESTRATION_GATE_SCHEMA = 'sks.parent-orchestration-gate.v1';
10
11
  export const PARENT_ORCHESTRATION_MAX_BLOCKS = 2;
11
12
  const MAX_TARGETS = 16;
12
- const SPAWN_TOOL_RE = /^(?:functions\.|collaboration\.)?spawn_agent$/;
13
13
  const FILE_EDIT_TOOL_RE = /^(?:functions\.)?(?:apply_patch|edit|write|multiedit|notebookedit|str_replace_editor|str_replace_based_edit_tool|file_write|fs_write|write_file|create_file|edit_file|update_file|delete_file)$/i;
14
14
  const SHELL_TOOL_RE = /^(?:functions\.)?(?:shell|shell_command|exec_command|local_shell|bash|container\.exec)$/i;
15
15
  const SHELL_WRITE_RE = new RegExp([
@@ -37,7 +37,7 @@ function spawnRouteOf(raw) {
37
37
  return route.mode && route.model && route.source ? route : null;
38
38
  }
39
39
  export function isSpawnToolPayload(payload = {}) {
40
- return SPAWN_TOOL_RE.test(toolNameOf(payload));
40
+ return isSpawnAgentToolName(toolNameOf(payload));
41
41
  }
42
42
  function toolNameOf(payload = {}) {
43
43
  return String(payload?.tool_name || payload?.toolName || payload?.tool?.name || payload?.name || '').trim();
@@ -138,7 +138,7 @@ function collectShellTargets(command, targets) {
138
138
  }
139
139
  export function parentMutationIntent(payload = {}) {
140
140
  const toolName = toolNameOf(payload);
141
- if (!toolName || SPAWN_TOOL_RE.test(toolName))
141
+ if (!toolName || isSpawnAgentToolName(toolName))
142
142
  return null;
143
143
  const input = toolInputOf(payload);
144
144
  const command = commandText(input, payload);
@@ -155,7 +155,7 @@ export function observedParentModelMismatch(model, expectedModel) {
155
155
  const value = String(model || '').trim();
156
156
  if (!value)
157
157
  return false;
158
- return value.toLowerCase() !== expectedModel && !/gpt[-_. ]?5\.6[-_. ]?sol|\bsol(?:\s+max)?\b/i.test(value);
158
+ return value.toLowerCase() !== String(expectedModel || '').trim().toLowerCase();
159
159
  }
160
160
  export function looksLikeCodexUiSettingsEvent(payload = {}) {
161
161
  const prompt = stripVisibleDecisionAnswerBlocks(extractUserPrompt(payload));
@@ -0,0 +1,15 @@
1
+ import { effectiveChildModelAllowlist } from '../subagents/child-model-allowlist.js';
2
+ import { refreshStaleManagedRolePins, stalePinForAgentType } from '../subagents/role-model-pins.js';
3
+ import { isSpawnAgentToolName, spawnPayloadToolName } from './spawn-tool-name.js';
4
+ export async function healStaleRolePinForSpawn(payload, root) {
5
+ if (!isSpawnAgentToolName(spawnPayloadToolName(payload)))
6
+ return false;
7
+ const input = payload?.tool_input || payload?.toolInput || payload?.tool?.input || {};
8
+ const agent = String(input.agent_type || input.agentType || '').trim();
9
+ if (!agent || !stalePinForAgentType(agent, { root }))
10
+ return false;
11
+ if (effectiveChildModelAllowlist().mode === 'openrouter_only')
12
+ return false;
13
+ await refreshStaleManagedRolePins({ root });
14
+ return true;
15
+ }
@@ -0,0 +1,7 @@
1
+ const SPAWN_AGENT_TOOL_RE = /^(?:functions\.)?(?:(?:collaboration|multi_agent_v\d+)[._]?)?spawn_agent$/;
2
+ export function isSpawnAgentToolName(name) {
3
+ return SPAWN_AGENT_TOOL_RE.test(String(name ?? '').trim());
4
+ }
5
+ export function spawnPayloadToolName(payload = {}) {
6
+ return String(payload?.tool_name || payload?.toolName || payload?.tool?.name || '');
7
+ }
@@ -1,7 +1,8 @@
1
1
  import { managedOfficialSubagentRoleByName } from '../managed-assets/managed-assets-manifest.js';
2
2
  import { READ_ONLY_LIST_ROLE } from '../subagents/read-only-list-role.js';
3
+ import { stalePinBlockReason, stalePinForAgentType } from '../subagents/role-model-pins.js';
4
+ import { isSpawnAgentToolName, spawnPayloadToolName } from './spawn-tool-name.js';
3
5
  import { effectiveChildModelAllowlist, isAllowedChildModel } from '../subagents/child-model-allowlist.js';
4
- const SPAWN_TOOLS = new Set(['spawn_agent', 'collaboration.spawn_agent', 'functions.spawn_agent']);
5
6
  export const SUBAGENT_MODELS_SETTINGS_HINT = 'SKS Control Center > Subagent Models';
6
7
  export function renderChildModelList(allowlist) {
7
8
  if (allowlist.mode !== 'openrouter_only')
@@ -31,9 +32,8 @@ export function boundedForkTurns(value) {
31
32
  return value === 'none' || /^[1-9]\d*$/.test(String(value || ''));
32
33
  }
33
34
  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.';
34
- export function subagentSpawnPolicyBlockReason(payload = {}) {
35
- const name = String(payload.tool_name || payload.toolName || payload.tool?.name || '');
36
- if (!SPAWN_TOOLS.has(name))
35
+ export function subagentSpawnPolicyBlockReason(payload = {}, opts = {}) {
36
+ if (!isSpawnAgentToolName(spawnPayloadToolName(payload)))
37
37
  return null;
38
38
  const input = payload.tool_input || payload.toolInput || payload.tool?.input || {};
39
39
  const allowlist = effectiveChildModelAllowlist();
@@ -52,6 +52,9 @@ export function subagentSpawnPolicyBlockReason(payload = {}) {
52
52
  if (!allowed.has(String(input.model || ''))) {
53
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
54
  }
55
+ const stale = opts.root ? stalePinForAgentType(String(input.agent_type || input.agentType || ''), { root: opts.root }) : null;
56
+ if (stale)
57
+ return stalePinBlockReason(stale);
55
58
  }
56
59
  if (!boundedForkTurns(input.fork_turns))
57
60
  return FORK_BLOCK_REASON;
@@ -1,6 +1,8 @@
1
+ import { releaseExclusiveGuiSurface } from './exclusive-gui-surface-gate.js';
1
2
  import { officialSubagentArtifactDir, recordAndRefreshSubagentEvidence, recordOfficialSubagentLifecycleCaptureFailure } from './official-subagent-lifecycle.js';
2
3
  import { clearSubagentSkillAvailabilityGuards } from './subagent-skill-availability.js';
3
4
  export async function handleSubagentStop(root, state, payload = {}, sessionKey = null) {
5
+ await releaseExclusiveGuiSurface({ root, sessionKey, payload }).catch(() => null);
4
6
  const artifactDir = officialSubagentArtifactDir(root, state, sessionKey);
5
7
  let lifecycleFailure = null;
6
8
  let capturedEvent = null;
@@ -36,6 +36,9 @@ import { effectiveChildModelAllowlist } from './subagents/child-model-allowlist.
36
36
  import { maybeReconcileManagedGuidancePreflight } from './hooks-runtime/managed-guidance-preflight.js';
37
37
  import { evaluateParentOrchestrationGate, isSpawnToolPayload, recordParentOrchestrationSpawn } from './hooks-runtime/parent-orchestration-gate.js';
38
38
  import { subagentSpawnPolicyBlockReason } from './hooks-runtime/subagent-spawn-policy.js';
39
+ import { healStaleRolePinForSpawn } from './hooks-runtime/spawn-role-pin-heal.js';
40
+ import { bindExclusiveGuiSurfaceStart, claimExclusiveGuiSurface, refreshExclusiveGuiSurfaceOwner } from './hooks-runtime/exclusive-gui-surface-gate.js';
41
+ import { EXCLUSIVE_SURFACE_CHILD_RULE } from './subagents/exclusive-surface-rule.js';
39
42
  import { withFileLock } from './locks/file-lock.js';
40
43
  import { ensureConfinedDirectory, inspectConfinedPath, removeManagedPathVerified } from './managed-path-safety.js';
41
44
  import { buildBoundWaveParentGuidance, renderWaveParentGuidance } from './subagents/wave-parent-guidance.js';
@@ -156,6 +159,7 @@ async function evaluateHookPayloadWithPlan(name, payload, opts, jevPlan) {
156
159
  return withNarutoDecision({ continue: true });
157
160
  }
158
161
  async function hookSubagentStart(root, state, payload = {}, sessionKey = null) {
162
+ await bindExclusiveGuiSurfaceStart({ root, sessionKey, payload }).catch(() => null);
159
163
  const artifactDir = officialSubagentArtifactDir(root, state, sessionKey);
160
164
  const sessionArtifactDir = officialSubagentArtifactDir(root, {}, sessionKey);
161
165
  const skillGuardBinding = officialSubagentSkillGuardBinding(state);
@@ -184,6 +188,7 @@ async function hookSubagentStart(root, state, payload = {}, sessionKey = null) {
184
188
  'Keep the model and reasoning effort your spawn call sealed; do not retarget them.',
185
189
  'Use max_depth=1. Naruto children must not spawn children.',
186
190
  'Do not duplicate an already assigned slice.',
191
+ EXCLUSIVE_SURFACE_CHILD_RULE,
187
192
  'Parallel writes require disjoint paths; serialize overlapping paths.',
188
193
  'Finish only your assigned slice, return a concise result, then stop so the Naruto parent can close this thread.'
189
194
  ].join(' ') : '';
@@ -648,12 +653,14 @@ async function consumeActiveOfficialWorkflowQueue(root, missionId, workflowRunId
648
653
  }
649
654
  }
650
655
  async function hookPreTool(root, state, payload, noQuestion, sessionKey = null) {
656
+ await refreshExclusiveGuiSurfaceOwner({ root, sessionKey, payload }).catch(() => null);
657
+ await healStaleRolePinForSpawn(payload, root).catch(() => false);
651
658
  const jevSpawn = await jevSpawnRouting(root, state, payload).catch(() => null);
652
659
  const jevSpawnInput = jevSpawn?.input ?? null;
653
660
  const spawnPayload = jevSpawnInput
654
661
  ? { ...payload, tool_input: jevSpawnInput, toolInput: jevSpawnInput }
655
662
  : payload;
656
- const spawnPolicyBlock = subagentSpawnPolicyBlockReason(spawnPayload);
663
+ const spawnPolicyBlock = subagentSpawnPolicyBlockReason(spawnPayload, { root });
657
664
  if (spawnPolicyBlock)
658
665
  return { decision: 'block', permissionDecision: 'deny', reason: spawnPolicyBlock };
659
666
  const artifactDir = officialSubagentArtifactDir(root, state, sessionKey);
@@ -725,6 +732,10 @@ async function hookPreTool(root, state, payload, noQuestion, sessionKey = null)
725
732
  if (hostCapabilityDecision && hostCapabilityDecision.continue !== true)
726
733
  return hostCapabilityDecision;
727
734
  if (isSpawnToolPayload(spawnPayload)) {
735
+ const guiSurface = await claimExclusiveGuiSurface({ root, sessionKey, payload: spawnPayload }).catch(() => null);
736
+ if (guiSurface?.action === 'block') {
737
+ return { decision: 'block', permissionDecision: 'deny', reason: guiSurface.message };
738
+ }
728
739
  await recordParentOrchestrationSpawn(root, state, jevSpawn?.route ? { spawnRoute: jevSpawn.route } : {}).catch(() => null);
729
740
  }
730
741
  const waveGuidance = await parentWaveGuidanceContext(root, state, sessionKey).catch(() => '');
package/dist/core/init.js CHANGED
@@ -214,6 +214,7 @@ const AGENTS_BLOCK = [
214
214
  '- Codex native `/goal` is the only persisted goal owner. Goal objectives must state the outcome, scope, constraints, verification, done-when conditions, stop conditions, and non-goals.',
215
215
  '- Answer, Help, Goal, and tiny DFix stay parent-owned. Implementation routes to Naruto (`$sks-naruto`, alias `$sks-work`; standalone `sks naruto run`), which is parent orchestration only: decompose, assign disjoint slices, spawn, integrate, and verify. Do not implement those slices in the parent thread. The SKS PreToolUse hook denies parent-thread source edits until the first child thread starts and while children are still running; .sneakoscope artifacts stay parent-writable, and Jev may release a confirmed orchestration-scaffolding edit.',
216
216
  '- The parent owns decomposition, integration, verification, and the final answer. Delegate implementation slices with disjoint write scopes, reuse capacity across root-owned waves, and never nest subagents.',
217
+ '- Computer Use and browser are exclusive GUI surfaces: one child per surface at a time, with all of that surface\'s work in one slice. Never fan the same GUI task out to several children.',
217
218
  '- Every child runs the newest model of the tier its work needs (fast, balanced, context, or deep); no model family is pinned. When Jev mode is on, Jev picks the tier for each new Naruto child spawn and SKS seals it. A user role preference stays authoritative. Do not pick a child model yourself.',
218
219
  '- Preserve the user-selected parent model, reasoning effort, and service tier. Parent settings stay on the parent thread. A role preference wins over the Jev seal for that role.',
219
220
  '- SKS child spawns must pass the sealed model and reasoning effort with `fork_turns="none"` or a positive bounded turn count; carry the complete bounded slice contract in `message`. Never use full-history inheritance for children. Full-history forks (`fork_turns="all"`, including the omitted default) inherit the parent agent type, model, and reasoning effort and must not be combined with `agent_type`, `model`, or `reasoning_effort`.',
@@ -590,7 +590,7 @@ function officialSubagentRole(input) {
590
590
  codex_name: input.codexName,
591
591
  description: input.description,
592
592
  model_policy: profile.policy,
593
- model: profile.model,
593
+ get model() { return subagentModelProfile(input.policy).model; },
594
594
  model_reasoning_effort: profile.modelReasoningEffort,
595
595
  ...(input.sandbox ? { sandbox: input.sandbox } : {}),
596
596
  nickname_candidates: input.nicknames,
@@ -27,7 +27,6 @@ import { coreEngineeringDirectiveReferenceText, engineeringSanityPolicyText } fr
27
27
  import { classifyTaskProfile, gateProfileForTask } from '../runtime/task-profile.js';
28
28
  import { chooseVerificationBudget } from '../runtime/verification-budget.js';
29
29
  import { stopFinalizationRitualsEnforced } from '../verification-profile.js';
30
- import { NARUTO_PARENT_MODEL } from '../subagents/model-policy.js';
31
30
  import { HARD_NARUTO_MAX_THREADS } from '../subagents/thread-budget.js';
32
31
  import { NARUTO_GATE_FILENAME, NARUTO_SUMMARY_FILENAME, SUBAGENT_PLAN_FILENAME, prepareOfficialSubagentMission } from '../subagents/official-subagent-preparation.js';
33
32
  import { buildSubagentEvidence, readSubagentEvents, recordSubagentEvent, SUBAGENT_EVIDENCE_FILENAME, SUBAGENT_EVENT_LOG_FILENAME, SUBAGENT_PARENT_SUMMARY_FILENAME } from '../subagents/subagent-evidence.js';
@@ -1320,7 +1319,7 @@ async function prepareNaruto(root, route, task, required, opts = {}) {
1320
1319
  prompt: cleanTask
1321
1320
  })
1322
1321
  });
1323
- const { budget, delegationPrompt, workflowRunId, taskProfile, officialConfig, triwikiAttention, parentModelMatch } = preparation;
1322
+ const { budget, delegationPrompt, workflowRunId, taskProfile, officialConfig, triwikiAttention, parentModelMatch, plan } = preparation;
1324
1323
  const routeContextPayload = {
1325
1324
  route: 'Naruto',
1326
1325
  command: '$Naruto',
@@ -1337,7 +1336,7 @@ async function prepareNaruto(root, route, task, required, opts = {}) {
1337
1336
  requested_subagents_explicit: requestedSubagents !== undefined,
1338
1337
  max_threads: budget.maxThreads,
1339
1338
  max_depth: budget.maxDepth,
1340
- parent_model_policy: NARUTO_PARENT_MODEL,
1339
+ parent_model_policy: plan.parent_model_policy,
1341
1340
  observed_parent_model: observedParentModel,
1342
1341
  parent_model_match: parentModelMatch,
1343
1342
  config_sources: officialConfig.sources,
@@ -17,7 +17,7 @@ export function narutoModels() {
17
17
  const E2E_WORK_RE = /(e2e|end[-\s]?to[-\s]?end|test_execution|browser|chrome|computer[-\s]?use|computer\s+use|cross[-\s]?app|playwright|selenium|puppeteer|브라우저|컴퓨터\s*유즈)/i;
18
18
  export async function routeModel(category, opts = {}) {
19
19
  if (opts.narutoOnly) {
20
- return routeNarutoGpt56Model({
20
+ return routeNarutoTierModel({
21
21
  category,
22
22
  ...(opts.taskText !== undefined ? { taskText: opts.taskText } : {}),
23
23
  ...(opts.riskText !== undefined ? { riskText: opts.riskText } : {}),
@@ -33,10 +33,10 @@ export async function routeModel(category, opts = {}) {
33
33
  const reasoning = opts.lbHealth?.quota_low && policy.reasoning === 'xhigh' ? 'high' : policy.reasoning;
34
34
  return { model, reasoning, serviceTier: policy.serviceTier };
35
35
  }
36
- export function routeNarutoGpt56Model(input = {}) {
36
+ export function routeNarutoTierModel(input = {}) {
37
37
  const category = input.category || 'agentic';
38
38
  const explicitRequested = String(input.explicitModel || '').trim();
39
- const explicit = normalizeNarutoGpt56Model(input.explicitModel);
39
+ const explicit = normalizeNarutoTierModel(input.explicitModel);
40
40
  const invalidExplicit = Boolean(explicitRequested && !explicit);
41
41
  const explicitHighRisk = /critical|forensic|security|database|migration|release|production|high[- ]?risk|data\s*loss|permission|auth|보안|데이터베이스|마이그레이션|릴리스|운영|고위험/i.test(String(input.riskText || ''));
42
42
  const automatic = decideSubagentModel({
@@ -53,7 +53,7 @@ export function routeNarutoGpt56Model(input = {}) {
53
53
  const preferred = explicit || automatic.model;
54
54
  const available = input.availableModels == null
55
55
  ? narutoModels()
56
- : input.availableModels.map(normalizeNarutoGpt56Model).filter((model) => Boolean(model));
56
+ : input.availableModels.map(normalizeNarutoTierModel).filter((model) => Boolean(model));
57
57
  const degraded = new Set((input.degradedModels || []).map((model) => String(model).toLowerCase()));
58
58
  const usable = available.filter((model) => !degraded.has(model));
59
59
  const availableEfforts = effortsForModel(input.availableModelEfforts, preferred);
@@ -63,16 +63,13 @@ export function routeNarutoGpt56Model(input = {}) {
63
63
  const model = !invalidExplicit && usable.includes(preferred) && (availableEfforts == null || availableEfforts.includes(intendedReasoning)) ? preferred : '';
64
64
  return { model, reasoning: intendedReasoning, serviceTier: 'fast' };
65
65
  }
66
- export function isNarutoGpt56Model(value) {
67
- return normalizeNarutoGpt56Model(value) !== null;
66
+ export function isNarutoTierModel(value) {
67
+ return normalizeNarutoTierModel(value) !== null;
68
68
  }
69
- export function normalizeNarutoGpt56Model(value) {
69
+ export function normalizeNarutoTierModel(value) {
70
70
  const model = String(value || '').trim().toLowerCase();
71
71
  return narutoModels().includes(model) ? model : null;
72
72
  }
73
- export function childInheritsActiveMainModel(_value) {
74
- return false;
75
- }
76
73
  export function categoryForWorkerRole(role, taskText = '') {
77
74
  const text = `${String(role || '')} ${String(taskText || '')}`.toLowerCase();
78
75
  if (/(refactor|re-?architect|리팩터|아키텍처)/i.test(text))
@@ -95,7 +92,7 @@ export function modelRouteReason(category, choice, opts = {}) {
95
92
  return `${category}->blocked (explicit model unavailable)`;
96
93
  if (opts.explicit)
97
94
  return `${category}->${model} (explicit model preserved)`;
98
- if (isNarutoGpt56Model(choice.model))
95
+ if (isNarutoTierModel(choice.model))
99
96
  return `${category}->${model}@${choice.reasoning} (official subagent model policy)`;
100
97
  const suffix = opts.quotaLow ? 'quota discipline' : 'Codex catalog passthrough';
101
98
  return `${category}->${model} (${suffix})`;
@@ -1,4 +1,4 @@
1
- import { thinkingSubagentModel, SUBAGENT_EFFORT } from '../subagents/model-policy.js';
1
+ import { judgmentSubagentModel, DEEP_SUBAGENT_EFFORT } from '../subagents/model-policy.js';
2
2
  export const RECALLPULSE_DECISION_ARTIFACT = 'recallpulse-decision.json';
3
3
  export const RECALLPULSE_HISTORY_ARTIFACT = 'recallpulse-history.jsonl';
4
4
  export const MISSION_STATUS_LEDGER_ARTIFACT = 'mission-status-ledger.json';
@@ -201,7 +201,7 @@ export const RESEARCH_REVIEWER_CONTRACT = Object.freeze([
201
201
  ...agent,
202
202
  persona_boundary: 'Apply only the assigned review dimension and report evidence-bound findings.',
203
203
  custom_agent: 'research_reviewer',
204
- model: thinkingSubagentModel(),
205
- reasoning_effort: SUBAGENT_EFFORT,
204
+ model: judgmentSubagentModel(),
205
+ reasoning_effort: DEEP_SUBAGENT_EFFORT,
206
206
  service_tier: 'fast'
207
207
  })));
@@ -199,7 +199,7 @@ export async function evaluateRecallPulseFixtures(root, opts = {}) {
199
199
  fixture('repeated-stop-hook-blocker', true, 'Duplicate suppression keys collapse repeated blocker text into one durable status row.'),
200
200
  fixture('hook-only-status-visibility', true, 'mission-status-ledger.json preserves recoverable user-visible status.'),
201
201
  fixture('research-persona-missing', true, 'Research validation blocks missing agent display_name/persona/persona_boundary.'),
202
- fixture('research-model-policy-not-sol-max', true, 'Research validation blocks reviewer rows that are not bound to the research_reviewer latest deep-tier max policy.'),
202
+ fixture('research-model-policy-not-deep-max', true, 'Research validation blocks reviewer rows that are not bound to the research_reviewer latest deep-tier max policy.'),
203
203
  fixture('research-review-evidence-missing', true, 'Research validation blocks reviewer outcomes without source evidence, falsifiers, or probes.'),
204
204
  fixture('research-impersonation', true, 'Research validation blocks persona-boundary violations.'),
205
205
  fixture('oversized-l1', true, 'L1 token and item limits reject oversized active recall.'),
@@ -1,5 +1,5 @@
1
1
  import path from 'node:path';
2
- import { thinkingSubagentModel, SUBAGENT_EFFORT } from '../subagents/model-policy.js';
2
+ import { judgmentSubagentModel, DEEP_SUBAGENT_EFFORT } from '../subagents/model-policy.js';
3
3
  import { nowIso, writeJsonAtomic, writeTextAtomic } from '../fsx.js';
4
4
  import { buildClaimEvidenceMatrixFromLedgers, writeClaimEvidenceMatrix } from './claim-evidence-matrix.js';
5
5
  import { DEFAULT_RESEARCH_QUALITY_CONTRACT, writeResearchQualityContract } from './research-quality-contract.js';
@@ -174,8 +174,8 @@ export async function writeMockResearchResult(dir, plan) {
174
174
  mandate: agent.mandate,
175
175
  model_policy: {
176
176
  custom_agent: RESEARCH_REVIEWER_CUSTOM_AGENT,
177
- model: thinkingSubagentModel(),
178
- reasoning_effort: SUBAGENT_EFFORT,
177
+ model: judgmentSubagentModel(),
178
+ reasoning_effort: DEEP_SUBAGENT_EFFORT,
179
179
  enforcement_source: 'mock_fixture'
180
180
  },
181
181
  query_set: sourceLedger.queries.filter((query) => query.agent_id === agent.id).map((query) => query.query),