sneakoscope 10.2.0 → 10.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +6 -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 +3 -1
  5. package/dist/cli/command-registry.js +9 -1
  6. package/dist/cli/install-helpers-menubar.js +57 -0
  7. package/dist/cli/install-helpers.js +10 -1
  8. package/dist/commands/decision.js +7 -0
  9. package/dist/config/skills-manifest.json +1 -1
  10. package/dist/core/agents/native-worker-backend-router.js +7 -2
  11. package/dist/core/codex-app/menubar/constants.js +2 -0
  12. package/dist/core/decisions/cli.js +324 -0
  13. package/dist/core/decisions/config.js +109 -0
  14. package/dist/core/decisions/evaluation.js +62 -0
  15. package/dist/core/decisions/integration.js +262 -0
  16. package/dist/core/decisions/openrouter.js +270 -0
  17. package/dist/core/decisions/policy.js +393 -0
  18. package/dist/core/decisions/questions.js +215 -0
  19. package/dist/core/decisions/receipt.js +41 -0
  20. package/dist/core/decisions/recovery.js +39 -0
  21. package/dist/core/decisions/routing.js +58 -0
  22. package/dist/core/decisions/state.js +173 -0
  23. package/dist/core/decisions/types.js +69 -0
  24. package/dist/core/feature-fixtures.js +1 -0
  25. package/dist/core/feature-registry.js +1 -0
  26. package/dist/core/hooks-runtime/subagent-context.js +5 -3
  27. package/dist/core/hooks-runtime/subagent-spawn-policy.js +9 -3
  28. package/dist/core/provider/model-router.js +3 -3
  29. package/dist/core/routes.js +2 -1
  30. package/dist/core/subagents/model-policy.js +15 -0
  31. package/dist/core/subagents/official-subagent-lock.js +15 -0
  32. package/dist/core/subagents/official-subagent-preparation.js +265 -24
  33. package/dist/core/subagents/official-subagent-prompt.js +56 -13
  34. package/dist/core/update/update-migration-state/retired-local-decision.js +203 -0
  35. package/dist/core/update/update-migration-state.js +6 -0
  36. package/dist/core/version.js +1 -1
  37. package/dist/native/sks-menubar/Sources/ControlCenterWindowController.swift +6 -3
  38. package/dist/native/sks-menubar/Sources/LocalDecisionModels.swift +166 -0
  39. package/dist/native/sks-menubar/Sources/LocalDecisionViewController.swift +200 -0
  40. package/dist/native/sks-menubar/Sources/OverviewViewController.swift +4 -0
  41. package/dist/native/sks-menubar/Sources/SidebarItem.swift +2 -0
  42. package/dist/scripts/current-surface-update-e2e-check.js +1 -0
  43. package/package.json +1 -1
@@ -0,0 +1,39 @@
1
+ export const RECOVERY_CAPABILITY = Object.freeze({
2
+ supported: false,
3
+ reason: 'unsupported_no_sks_handler',
4
+ evidence: [
5
+ 'src/core/subagents/official-subagent-preparation.ts::recoverOfficialSubagentPreparationTransaction is deterministic crash recovery, not an ambiguous-failure Choice consumer',
6
+ 'src/core/subagents/official-subagent-runner.ts OAuth hint is a deterministic auth/port-conflict path',
7
+ 'src/core/triwiki/context-graph/store journal recovery is deterministic store repair',
8
+ 'Codex spawn_agent/send_input remain native-host-owned; a saved plan is not dispatch proof'
9
+ ]
10
+ });
11
+ const testHandlers = new Map();
12
+ export function listProductionRecoveryCandidates() {
13
+ return [];
14
+ }
15
+ export function setRecoveryTestHandler(actionId, handler) {
16
+ if (process.env.SKS_JEV_DECISION_TEST_OVERRIDES !== '1') {
17
+ throw new Error('recovery_test_handler_requires_test_overrides');
18
+ }
19
+ if (!handler) {
20
+ testHandlers.delete(actionId);
21
+ return;
22
+ }
23
+ testHandlers.set(actionId, handler);
24
+ }
25
+ export async function applyRecoveryEffect(input) {
26
+ if (process.env.SKS_JEV_DECISION_TEST_OVERRIDES !== '1') {
27
+ return { ok: false, reason: 'capability_unavailable' };
28
+ }
29
+ const candidate = input.candidates.find((row) => row.id === input.actionId);
30
+ if (!candidate)
31
+ return { ok: false, reason: 'invalid_response' };
32
+ if (!candidate.authorized)
33
+ return { ok: false, reason: 'unauthorized' };
34
+ const handler = testHandlers.get(candidate.handlerId);
35
+ if (!handler)
36
+ return { ok: false, reason: 'capability_unavailable' };
37
+ const result = await handler({ actionId: candidate.id, diagnostic: input.diagnostic, candidate });
38
+ return { ok: true, actionId: result.actionId, consumptionEvidence: result.consumptionEvidence };
39
+ }
@@ -0,0 +1,58 @@
1
+ import { sealedRoutingModel } from './types.js';
2
+ const ROLE_ID = /^[a-z][a-z0-9_]{0,40}$/;
3
+ export const MAX_JEV_ROUTING_ROLES = 8;
4
+ export function buildRoutingCandidates(input) {
5
+ const out = [];
6
+ const seen = new Set();
7
+ for (const role of input.roles) {
8
+ const name = role.name.trim();
9
+ if (!role.dynamic || !ROLE_ID.test(name) || seen.has(name))
10
+ continue;
11
+ seen.add(name);
12
+ out.push({ id: name, summary: role.summary.trim().slice(0, 240) });
13
+ if (out.length >= MAX_JEV_ROUTING_ROLES)
14
+ break;
15
+ }
16
+ return out;
17
+ }
18
+ export function applySealedRouting(agents, selected) {
19
+ if (!selected || !agents)
20
+ return { ...(agents || {}) };
21
+ const next = { ...agents };
22
+ for (const [name, effort] of Object.entries(selected.efforts)) {
23
+ const model = selected.models[name];
24
+ const sealed = model ? sealedRoutingModel(model) : null;
25
+ if (!sealed || sealed.effort !== effort)
26
+ continue;
27
+ const row = next[name];
28
+ if (!row || row.routing_dynamic !== true || row.routed_model_policy === 'user_role_model_preference')
29
+ continue;
30
+ next[name] = {
31
+ ...row,
32
+ routed_model: sealed.id,
33
+ routed_model_reasoning_effort: effort,
34
+ routed_model_policy: 'jev_sealed_routing',
35
+ jev_routing_lane: selected.id
36
+ };
37
+ }
38
+ return next;
39
+ }
40
+ export function assembleRoutingSelection(effects) {
41
+ const models = {};
42
+ const efforts = {};
43
+ for (const effect of effects) {
44
+ const sealed = sealedRoutingModel(effect.model);
45
+ if (!sealed || !ROLE_ID.test(effect.roleId))
46
+ continue;
47
+ models[effect.roleId] = sealed.id;
48
+ efforts[effect.roleId] = sealed.effort;
49
+ }
50
+ if (Object.keys(models).length === 0)
51
+ return null;
52
+ return {
53
+ id: 'jev_role_fanout',
54
+ summary: 'Jev chose a sealed model for each dynamic Naruto role.',
55
+ models,
56
+ efforts
57
+ };
58
+ }
@@ -0,0 +1,173 @@
1
+ import fsp from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { runProcess, sha256 } from '../fsx.js';
4
+ import { inspectConfinedPath } from '../managed-path-safety.js';
5
+ import { redactSecrets } from '../secret-redaction.js';
6
+ import { DESIGN_DEFAULTS, POLICY_REVISION } from './types.js';
7
+ export const MAX_EXCERPT_CHARS = 800;
8
+ const SECRET_PATTERNS = [
9
+ /\b(?:sk|rk|pk)-[A-Za-z0-9_-]{16,}\b/g,
10
+ /\bAKIA[0-9A-Z]{16}\b/g,
11
+ /\bgh[pousr]_[A-Za-z0-9]{20,}\b/g,
12
+ /\bxox[abpr]-[A-Za-z0-9-]{10,}\b/g,
13
+ /\bhf_[A-Za-z0-9]{20,}\b/g,
14
+ /\bBearer\s+[A-Za-z0-9._~+/=-]{16,}/gi,
15
+ /-----BEGIN [A-Z ]+PRIVATE KEY-----[\s\S]*?-----END [A-Z ]+PRIVATE KEY-----/g
16
+ ];
17
+ export function redactDecisionText(text, maxChars = 2_000) {
18
+ let out = String(text || '').replace(/\r\n?/g, '\n').replace(/[ \t]+/g, ' ').replace(/\n{3,}/g, '\n\n').trim();
19
+ out = String(redactSecrets(out));
20
+ for (const pattern of SECRET_PATTERNS)
21
+ out = out.replace(pattern, '[redacted]');
22
+ if (out.length > maxChars)
23
+ out = `${out.slice(0, maxChars - 12).trimEnd()} [truncated]`;
24
+ return out;
25
+ }
26
+ export function stableDigest(value) {
27
+ return sha256(canonicalJson(value));
28
+ }
29
+ export function canonicalJson(value) {
30
+ if (value === null || typeof value !== 'object')
31
+ return JSON.stringify(value);
32
+ if (Array.isArray(value))
33
+ return `[${value.map((entry) => canonicalJson(entry)).join(',')}]`;
34
+ const record = value;
35
+ return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`).join(',')}}`;
36
+ }
37
+ export async function sourceSnapshotDigest(root) {
38
+ const cwd = path.resolve(root);
39
+ const head = await gitText(cwd, ['rev-parse', 'HEAD']);
40
+ const porcelain = await gitText(cwd, ['status', '--porcelain']);
41
+ const dirty = porcelain.split('\n').map((line) => line.trim()).filter(Boolean);
42
+ const dirtyHashes = [];
43
+ for (const line of dirty.slice(0, 64)) {
44
+ const relative = line.replace(/^[ MADRCU?!]{1,2}\s+/, '').split(' -> ').pop() || '';
45
+ if (!relative || relative.includes('..'))
46
+ continue;
47
+ const file = path.join(cwd, relative);
48
+ const inspected = await inspectConfinedPath(cwd, file).catch(() => null);
49
+ if (!inspected?.exists || inspected.leafSymlink || !inspected.stat?.isFile()) {
50
+ dirtyHashes.push(`${relative}:unreadable`);
51
+ continue;
52
+ }
53
+ const bytes = await fsp.readFile(file).catch(() => Buffer.alloc(0));
54
+ dirtyHashes.push(`${relative}:${sha256(bytes)}`);
55
+ }
56
+ return stableDigest({ head, dirty, dirtyHashes });
57
+ }
58
+ export async function graphFileDigest(root) {
59
+ const file = path.join(path.resolve(root), '.sneakoscope', 'wiki', 'context-graph.json');
60
+ const inspected = await inspectConfinedPath(path.resolve(root), file).catch(() => null);
61
+ if (!inspected?.exists || inspected.leafSymlink || !inspected.stat?.isFile())
62
+ return null;
63
+ const bytes = await fsp.readFile(file);
64
+ return sha256(bytes);
65
+ }
66
+ export function buildDecisionBinding(input) {
67
+ return {
68
+ projectId: input.projectId,
69
+ workflowRunId: input.workflowRunId,
70
+ workflowRevision: input.workflowRevision,
71
+ sourceDigest: input.sourceDigest,
72
+ graphDigest: input.graphDigest,
73
+ candidateDigest: stableDigest(input.candidates),
74
+ questionDigest: stableDigest(input.questions),
75
+ policyRevision: POLICY_REVISION,
76
+ requestedModel: input.requestedModel || DESIGN_DEFAULTS.model
77
+ };
78
+ }
79
+ export async function hydrateContextCandidates(root, attention, changedPaths = []) {
80
+ if (!attention?.available)
81
+ return [];
82
+ const pinnedPaths = new Set(changedPaths.map((entry) => normalizeRel(entry)));
83
+ const out = [];
84
+ for (const anchor of attention.anchors) {
85
+ const candidate = await hydrateAnchor(root, anchor, pinnedPaths);
86
+ if (candidate)
87
+ out.push(candidate);
88
+ }
89
+ return out;
90
+ }
91
+ export function applyOptionalContextSelection(attention, candidates, keepIds) {
92
+ const keep = new Set(keepIds);
93
+ for (const candidate of candidates) {
94
+ if (candidate.pinned)
95
+ keep.add(candidate.id);
96
+ }
97
+ const selected = attention.anchors.filter((anchor) => {
98
+ const candidate = candidates.find((row) => row.id === anchor.id);
99
+ if (!candidate)
100
+ return true;
101
+ return keep.has(anchor.id);
102
+ }).map((anchor) => attachExcerpt(anchor, candidates.find((row) => row.id === anchor.id)));
103
+ return {
104
+ ...attention,
105
+ anchors: selected,
106
+ token_cost: selected.reduce((sum, anchor) => sum + anchor.token_cost, 0)
107
+ };
108
+ }
109
+ async function hydrateAnchor(root, anchor, pinnedPaths) {
110
+ const provenance = anchor.provenance[0];
111
+ if (!provenance?.path || !provenance.hash) {
112
+ return {
113
+ id: anchor.id,
114
+ sourcePath: '',
115
+ sourceHash: anchor.source_hash || '',
116
+ excerpt: '',
117
+ pinned: true,
118
+ fresh: false,
119
+ reproducible: false
120
+ };
121
+ }
122
+ const relative = normalizeRel(provenance.path);
123
+ const excerpt = await readExcerpt(root, relative, provenance.line);
124
+ const named = pinnedPaths.has(relative);
125
+ const fresh = anchor.freshness === 'fresh';
126
+ const reproducible = Boolean(excerpt && provenance.hash);
127
+ const optional = !named && fresh && reproducible && Boolean(excerpt);
128
+ return {
129
+ id: anchor.id,
130
+ sourcePath: relative,
131
+ sourceHash: provenance.hash,
132
+ excerpt,
133
+ pinned: !optional,
134
+ fresh,
135
+ reproducible
136
+ };
137
+ }
138
+ async function readExcerpt(root, relative, line) {
139
+ const cwd = path.resolve(root);
140
+ const file = path.join(cwd, relative);
141
+ const inspected = await inspectConfinedPath(cwd, file).catch(() => null);
142
+ if (!inspected?.exists || inspected.leafSymlink || !inspected.stat?.isFile())
143
+ return '';
144
+ if (inspected.stat.size > 256 * 1024)
145
+ return '';
146
+ const raw = await fsp.readFile(file, 'utf8').catch(() => '');
147
+ if (!raw)
148
+ return '';
149
+ const lines = raw.split('\n');
150
+ const focus = Number.isInteger(line) && (line || 0) > 0 ? Math.min(lines.length, line || 1) : 1;
151
+ const start = Math.max(0, focus - 8);
152
+ const slice = lines.slice(start, start + 24).join('\n');
153
+ return redactDecisionText(slice, MAX_EXCERPT_CHARS);
154
+ }
155
+ function attachExcerpt(anchor, candidate) {
156
+ if (!candidate?.excerpt)
157
+ return anchor;
158
+ return {
159
+ ...anchor,
160
+ excerpt: candidate.excerpt,
161
+ source_path: candidate.sourcePath,
162
+ optional: !candidate.pinned
163
+ };
164
+ }
165
+ function normalizeRel(value) {
166
+ return value.replace(/\\/g, '/').replace(/^\.\//, '').trim();
167
+ }
168
+ async function gitText(cwd, args) {
169
+ const result = await runProcess('git', args, { cwd, timeoutMs: 2_000, envMode: 'merge' }).catch(() => null);
170
+ if (!result || result.code !== 0)
171
+ return '';
172
+ return String(result.stdout || '').trim();
173
+ }
@@ -0,0 +1,69 @@
1
+ export const SEALED_ROUTING_MODELS = Object.freeze([
2
+ Object.freeze({
3
+ id: 'gpt-5.6-luna',
4
+ effort: 'low',
5
+ summary: 'Fastest sealed model. Mechanical edits, renames, formatting, and other one-step changes.'
6
+ }),
7
+ Object.freeze({
8
+ id: 'gpt-5.6-sol',
9
+ effort: 'low',
10
+ summary: 'Fast sealed model. Simple coding whose result is already specified.'
11
+ }),
12
+ Object.freeze({
13
+ id: 'gpt-5.6-terra',
14
+ effort: 'medium',
15
+ summary: 'Context and tools. Search, multi-file reading, and broad exploration.'
16
+ }),
17
+ Object.freeze({
18
+ id: 'gpt-6-astra',
19
+ effort: 'max',
20
+ summary: 'Escalate. Judgment, architecture, ambiguity, security, or high-stakes work.'
21
+ })
22
+ ]);
23
+ export const ROUTING_DIFFICULTY_RUBRIC = Object.freeze([
24
+ 'Mechanical one-step work: rename, format, copy, or a single exact edit.',
25
+ 'Simple coding whose result is already specified.',
26
+ 'Broad context, search, or tool use across more than one file.',
27
+ 'Judgment, architecture, ambiguity, security, or other high-stakes work.'
28
+ ]);
29
+ export const ROUTING_RISK_NOUL_MIN = 0.70;
30
+ export function sealedRoutingModel(model) {
31
+ const match = SEALED_ROUTING_MODELS.find((row) => row.id === model);
32
+ return match ? { id: match.id, effort: match.effort } : null;
33
+ }
34
+ export const DESIGN_DEFAULTS = Object.freeze({
35
+ mode: 'off',
36
+ model: 'typesafe/jev-1.13',
37
+ endpoint: 'https://openrouter.ai/api/alpha/decisions',
38
+ deadlineMs: 1500,
39
+ retries: 0,
40
+ maxInFlight: 2,
41
+ maxQueued: 0,
42
+ maxQuestions: 32,
43
+ maxRequestBytes: 24 * 1024,
44
+ maxResponseBytes: 256 * 1024,
45
+ memoEntries: 128,
46
+ circuitFailures: 3,
47
+ circuitOpenMs: 30000
48
+ });
49
+ export const POLICY_REVISION = 'sks.jev-policy.v2';
50
+ export const QUESTION_REVISION = 'sks.jev-questions.v1';
51
+ export const RECEIPT_SCHEMA = 'sks.jev-decision.v1';
52
+ export const CHOICE_MIN_PROBABILITY = 0.85;
53
+ export const CHOICE_MIN_CONFIDENCE = 0.70;
54
+ export const CONTEXT_DROP_NOUL_MAX = 0.10;
55
+ export const DISTRIBUTION_SUM_TOLERANCE = 1e-3;
56
+ export const KEEP_BASELINE_CHOICE = 'keep_baseline';
57
+ export const NEEDS_EVIDENCE_CHOICE = 'needs_evidence';
58
+ export const CONTEXT_RELEVANCE_RUBRIC = Object.freeze([
59
+ 'No information needed for the stated task.',
60
+ 'Indirect background, with no task-specific constraint or evidence.',
61
+ 'Useful supporting evidence for at least one task requirement.',
62
+ 'Direct evidence needed to implement or verify a task requirement.'
63
+ ]);
64
+ export const UNKNOWN_USAGE = Object.freeze({
65
+ inputTokens: null,
66
+ outputTokens: null,
67
+ reportedCost: null,
68
+ evidence: 'unknown'
69
+ });
@@ -89,6 +89,7 @@ const FIXTURES = Object.freeze({
89
89
  'cli-bench': fixture('execute_and_validate_artifacts', 'sks bench core --tier npx-one-shot --json --iterations 1', ['.sneakoscope/reports/performance/core-bench.json'], 'pass'),
90
90
  'cli-code-structure': fixture('execute', 'sks code-structure scan --json', [], 'pass', { timeout_ms: 180000 }),
91
91
  'cli-rust': fixture('execute', 'sks rust smoke --json', [], 'pass'),
92
+ 'cli-decision': fixture('execute', 'sks decision status --json', [], 'pass'),
92
93
  'cli-skill-dream': fixture('execute', 'sks skill-dream status --json', [], 'pass'),
93
94
  'cli-gc': fixture('execute', 'sks gc --dry-run --json', [], 'pass'),
94
95
  'cli-cleanup': fixture('execute', 'sks cleanup plan --json', [], 'pass', {
@@ -541,6 +541,7 @@ const SAFE_EXECUTABLE_FIXTURE_ARGS = Object.freeze({
541
541
  'cli-bench': ['bench', 'core', '--tier', 'npx-one-shot', '--json', '--iterations', '1'],
542
542
  'cli-code-structure': ['code-structure', 'scan', '--json'],
543
543
  'cli-rust': ['rust', 'smoke', '--json'],
544
+ 'cli-decision': ['decision', 'status', '--json'],
544
545
  'cli-skill-dream': ['skill-dream', 'status', '--json'],
545
546
  'cli-gc': ['gc', '--dry-run', '--json'],
546
547
  'cli-cleanup': ['cleanup', 'plan', '--json'],
@@ -1,7 +1,7 @@
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 { ASTRA_SUBAGENT_MODEL } from '../subagents/model-policy.js';
4
+ import { ASTRA_SUBAGENT_MODEL, NARUTO_LUNA_MODEL, NARUTO_SOL_MODEL, NARUTO_TERRA_MODEL } from '../subagents/model-policy.js';
5
5
  export async function sealedSubagentRoutingContext(artifactDir, payload = {}) {
6
6
  const plan = await readJson(path.join(artifactDir, 'subagent-plan.json'), null).catch(() => null);
7
7
  if (!plan || plan.workflow !== 'official_codex_subagent')
@@ -12,9 +12,11 @@ export async function sealedSubagentRoutingContext(artifactDir, payload = {}) {
12
12
  const role = agentName ? managedOfficialSubagentRoleByName(agentName) : null;
13
13
  if (!agentName)
14
14
  return '';
15
- const model = ASTRA_SUBAGENT_MODEL;
15
+ const narutoPlan = plan.mode === 'naruto' || plan.route === '$Naruto';
16
16
  const plannedModel = String(planned?.routed_model || planned?.model || '').trim();
17
- const effort = String((plannedModel === ASTRA_SUBAGENT_MODEL
17
+ const sealedModels = new Set([ASTRA_SUBAGENT_MODEL, NARUTO_LUNA_MODEL, NARUTO_SOL_MODEL, NARUTO_TERRA_MODEL]);
18
+ const model = narutoPlan && sealedModels.has(plannedModel) ? plannedModel : ASTRA_SUBAGENT_MODEL;
19
+ const effort = String((model === plannedModel
18
20
  ? planned?.routed_model_reasoning_effort || planned?.model_reasoning_effort
19
21
  : null) || role?.model_reasoning_effort || 'medium').trim();
20
22
  return [
@@ -1,12 +1,18 @@
1
- import { ASTRA_SUBAGENT_MODEL } from '../subagents/model-policy.js';
1
+ import { ASTRA_SUBAGENT_MODEL, NARUTO_LUNA_MODEL, NARUTO_SOL_MODEL, NARUTO_TERRA_MODEL } from '../subagents/model-policy.js';
2
2
  const SPAWN_TOOLS = new Set(['spawn_agent', 'collaboration.spawn_agent', 'functions.spawn_agent']);
3
+ const SEALED_CHILD_MODELS = new Set([
4
+ ASTRA_SUBAGENT_MODEL,
5
+ NARUTO_LUNA_MODEL,
6
+ NARUTO_SOL_MODEL,
7
+ NARUTO_TERRA_MODEL
8
+ ]);
3
9
  export function subagentSpawnPolicyBlockReason(payload = {}) {
4
10
  const name = String(payload.tool_name || payload.toolName || payload.tool?.name || '');
5
11
  if (!SPAWN_TOOLS.has(name))
6
12
  return null;
7
13
  const input = payload.tool_input || payload.toolInput || payload.tool?.input || {};
8
- if (input.model !== ASTRA_SUBAGENT_MODEL) {
9
- return 'SKS children must use gpt-6-astra. Retry spawn_agent with model="gpt-6-astra", task-appropriate reasoning_effort, and fork_turns="none" or a positive bounded turn count. Include the complete slice contract in message; do not inherit the parent model.';
14
+ if (!SEALED_CHILD_MODELS.has(String(input.model || ''))) {
15
+ return 'SKS children must use a sealed model: gpt-5.6-luna, gpt-5.6-sol, gpt-5.6-terra, or gpt-6-astra. Retry spawn_agent with that model, task-appropriate reasoning_effort, and fork_turns="none" or a positive bounded turn count. Include the complete slice contract in message; do not inherit the parent model.';
10
16
  }
11
17
  if (input.fork_turns !== 'none' && !/^[1-9]\d*$/.test(String(input.fork_turns || ''))) {
12
18
  return 'SKS Astra 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.';
@@ -1,4 +1,4 @@
1
- import { ASTRA_SUBAGENT_MODEL, decideSubagentModel, subagentModelProfile } from '../subagents/model-policy.js';
1
+ import { ASTRA_SUBAGENT_MODEL, NARUTO_LUNA_MODEL, NARUTO_SOL_MODEL, NARUTO_TERRA_MODEL, decideSubagentModel, subagentModelProfile } from '../subagents/model-policy.js';
2
2
  const CATEGORY_POLICY = {
3
3
  quick: { reasoning: 'low', serviceTier: 'fast' },
4
4
  standard: { reasoning: 'medium', serviceTier: 'fast' },
@@ -10,7 +10,7 @@ const CATEGORY_POLICY = {
10
10
  refactor: { reasoning: 'max', serviceTier: 'fast' },
11
11
  strategy: { reasoning: 'max', serviceTier: 'fast' }
12
12
  };
13
- export const NARUTO_MODELS = [ASTRA_SUBAGENT_MODEL];
13
+ export const NARUTO_MODELS = [NARUTO_LUNA_MODEL, NARUTO_SOL_MODEL, NARUTO_TERRA_MODEL, ASTRA_SUBAGENT_MODEL];
14
14
  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;
15
15
  export async function routeModel(category, opts = {}) {
16
16
  if (opts.narutoOnly) {
@@ -47,7 +47,7 @@ export function routeNarutoGpt56Model(input = {}) {
47
47
  || category === 'ultrabrain'
48
48
  || explicitHighRisk
49
49
  });
50
- const preferred = explicit || automatic.model;
50
+ const preferred = explicit || ASTRA_SUBAGENT_MODEL;
51
51
  const available = input.availableModels == null
52
52
  ? [...NARUTO_MODELS]
53
53
  : input.availableModels.map(normalizeNarutoGpt56Model).filter((model) => Boolean(model));
@@ -706,7 +706,8 @@ export const COMMAND_CATALOG = [
706
706
  { name: 'gc', usage: 'sks gc [--dry-run] [--json]', description: 'Compact oversized logs and prune stale runtime artifacts.' },
707
707
  { name: 'stats', usage: 'sks stats [--full] [--json]', description: 'Show package and .sneakoscope storage size.' },
708
708
  { name: 'mcp-server', usage: 'sks mcp-server [--expose-exec] [--probe]', description: 'Run a modern stateless stdio MCP server exposing SKS read-only commands as tools for any MCP-capable agent host; --expose-exec also exposes non-read-only commands; --probe round-trips server/discover and tools/list, then exits.' },
709
- { name: 'agent-bridge', usage: 'sks agent-bridge setup [--trusted-project] [--json] | async --prompt "task" [--tools status,stats] [--json]', description: 'Publish the agent-bridge manifest or run selected read-only SKS tools with native Astra Async tool calling through the registered Codex-LB bridge.' }
709
+ { name: 'agent-bridge', usage: 'sks agent-bridge setup [--trusted-project] [--json] | async --prompt "task" [--tools status,stats] [--json]', description: 'Publish the agent-bridge manifest or run selected read-only SKS tools with native Astra Async tool calling through the registered Codex-LB bridge.' },
710
+ { name: 'decision', usage: 'sks decision status|enable|disable|probe|evaluate [--json]', description: 'Manage optional Jev decisions through the existing OpenRouter credential (off by default). Enable records cloud consent; a valid answer is compiled into SKS plan or context selection. There is no advisory mode and no local model runtime.' }
710
711
  ];
711
712
  export function routeById(id) {
712
713
  const key = String(id || '').replace(/^\$/, '').toLowerCase();
@@ -1,6 +1,9 @@
1
1
  export const NARUTO_PARENT_MODEL = 'gpt-6-astra';
2
2
  export const NARUTO_PARENT_EFFORT = 'max';
3
3
  export const ASTRA_SUBAGENT_MODEL = 'gpt-6-astra';
4
+ export const NARUTO_LUNA_MODEL = 'gpt-5.6-luna';
5
+ export const NARUTO_SOL_MODEL = 'gpt-5.6-sol';
6
+ export const NARUTO_TERRA_MODEL = 'gpt-5.6-terra';
4
7
  export const LUNA_SUBAGENT_MODEL = ASTRA_SUBAGENT_MODEL;
5
8
  export const TERRA_SUBAGENT_MODEL = ASTRA_SUBAGENT_MODEL;
6
9
  export const SOL_SUBAGENT_MODEL = ASTRA_SUBAGENT_MODEL;
@@ -271,6 +274,18 @@ const DOCUMENT_EXPLORATION_RE = /\b(?:read|scan|explore|compare|summarize|review
271
274
  export function subagentModelProfile(policy) {
272
275
  return SUBAGENT_MODEL_POLICIES[policy];
273
276
  }
277
+ export function narutoChildAssignment(policy) {
278
+ switch (policy) {
279
+ case 'luna_max_mechanical':
280
+ return { model: NARUTO_LUNA_MODEL, effort: 'low' };
281
+ case 'sol_high_implementation':
282
+ return { model: NARUTO_SOL_MODEL, effort: 'low' };
283
+ case 'terra_max_context_tools':
284
+ return { model: NARUTO_TERRA_MODEL, effort: 'medium' };
285
+ case 'sol_max_judgment':
286
+ return { model: ASTRA_SUBAGENT_MODEL, effort: 'max' };
287
+ }
288
+ }
274
289
  export function decideSubagentModel(input = {}) {
275
290
  const text = [
276
291
  input.title,
@@ -0,0 +1,15 @@
1
+ import path from 'node:path';
2
+ import { AsyncLocalStorage } from 'node:async_hooks';
3
+ import { withFileLock } from '../locks/file-lock.js';
4
+ export const OFFICIAL_SUBAGENT_LIFECYCLE_LOCK = '.subagent-evidence.lock';
5
+ const lockStore = new AsyncLocalStorage();
6
+ export function officialSubagentLifecycleLockHeld() {
7
+ return lockStore.getStore() === true;
8
+ }
9
+ export function withOfficialSubagentLifecycleLock(dir, fn) {
10
+ return withFileLock({
11
+ lockPath: path.join(dir, OFFICIAL_SUBAGENT_LIFECYCLE_LOCK),
12
+ timeoutMs: 5_000,
13
+ staleMs: 60_000
14
+ }, () => lockStore.run(true, fn));
15
+ }