sneakoscope 10.3.2 → 10.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/README.md +22 -12
  2. package/config/skills-hash-ledger.v1.json +7 -1
  3. package/crates/sks-core/Cargo.lock +1 -1
  4. package/crates/sks-core/Cargo.toml +1 -1
  5. package/dist/commands/doctor.js +2 -2
  6. package/dist/config/skills-manifest.json +13 -7
  7. package/dist/core/agents/agent-effort-policy.js +15 -13
  8. package/dist/core/agents/native-worker-backend-router.js +13 -8
  9. package/dist/core/codex-hooks/codex-hook-managed-install.js +56 -2
  10. package/dist/core/codex-lb/desktop-bridge-migration/retired-runtime-cleanup.js +1 -0
  11. package/dist/core/codex-native/core-skill-manifest.js +4 -4
  12. package/dist/core/decisions/cli.js +9 -2
  13. package/dist/core/decisions/config.js +7 -0
  14. package/dist/core/decisions/integration.js +54 -12
  15. package/dist/core/decisions/policy.js +27 -4
  16. package/dist/core/decisions/questions.js +39 -9
  17. package/dist/core/decisions/routing.js +15 -10
  18. package/dist/core/decisions/types.js +11 -25
  19. package/dist/core/doctor/current-project-guidance.js +38 -1
  20. package/dist/core/doctor/skill-legacy-surface.js +2 -1
  21. package/dist/core/hooks-runtime/hook-context.js +1 -1
  22. package/dist/core/hooks-runtime/jev-spawn-routing.js +21 -2
  23. package/dist/core/hooks-runtime/managed-guidance-preflight.js +30 -0
  24. package/dist/core/hooks-runtime/official-subagent-lifecycle.js +3 -3
  25. package/dist/core/hooks-runtime/parent-orchestration-gate.js +372 -0
  26. package/dist/core/hooks-runtime/subagent-context.js +13 -3
  27. package/dist/core/hooks-runtime/subagent-spawn-policy.js +5 -10
  28. package/dist/core/hooks-runtime.js +46 -8
  29. package/dist/core/init/skills/inventory.js +5 -1
  30. package/dist/core/init/skills.js +4 -4
  31. package/dist/core/init.js +39 -7
  32. package/dist/core/managed-assets/managed-assets-manifest.js +17 -17
  33. package/dist/core/pipeline-internals/runtime-core.js +2 -2
  34. package/dist/core/provider/model-router.js +8 -5
  35. package/dist/core/recallpulse/policy.js +2 -2
  36. package/dist/core/recallpulse.js +3 -3
  37. package/dist/core/release/gate-affected-globs.js +0 -1
  38. package/dist/core/research/mock-result.js +2 -2
  39. package/dist/core/research/research-adversarial-review.js +7 -7
  40. package/dist/core/research/research-claim-synthesizer.js +3 -3
  41. package/dist/core/research/research-falsification-runner.js +3 -3
  42. package/dist/core/research/research-super-search.js +5 -2
  43. package/dist/core/research/research-synthesis-writer.js +2 -2
  44. package/dist/core/research.js +5 -5
  45. package/dist/core/routes/dollar-manifest-lite.js +1 -1
  46. package/dist/core/routes.js +10 -10
  47. package/dist/core/runtime/task-profile.js +9 -5
  48. package/dist/core/subagents/model-policy.js +27 -45
  49. package/dist/core/subagents/model-tiers.js +153 -0
  50. package/dist/core/subagents/naruto-command-args.js +3 -3
  51. package/dist/core/subagents/naruto-help-contract.js +18 -13
  52. package/dist/core/subagents/naruto-host-credentials.js +8 -13
  53. package/dist/core/subagents/official-subagent-config.js +11 -7
  54. package/dist/core/subagents/official-subagent-preparation.js +12 -8
  55. package/dist/core/subagents/official-subagent-prompt.js +33 -33
  56. package/dist/core/subagents/official-subagent-runner.js +3 -3
  57. package/dist/core/subagents/role-model-preferences.js +26 -21
  58. package/dist/core/update/managed-permission-repair.js +462 -0
  59. package/dist/core/update/update-migration-state/retired-local-decision.js +32 -2
  60. package/dist/core/update/update-migration-state/simple-stages.js +42 -5
  61. package/dist/core/update/update-migration-state.js +63 -1
  62. package/dist/core/update-check.js +35 -8
  63. package/dist/core/version.js +1 -1
  64. package/dist/scripts/codex-native-agent-role-content-check.js +10 -5
  65. package/dist/scripts/codex-native-gate-lib.js +3 -1
  66. package/dist/scripts/current-surface-update-e2e-check.js +1 -0
  67. package/dist/scripts/mutation-callsite-coverage-check.js +2 -2
  68. package/dist/scripts/release-affected-selector-check.js +1 -2
  69. package/dist/scripts/typed-routing-gate-lib.js +3 -1
  70. package/package.json +1 -1
  71. package/release-gates.v2.json +2 -6
@@ -1,4 +1,4 @@
1
- import { CHOICE_MIN_CONFIDENCE, CHOICE_MIN_PROBABILITY, CONTEXT_DROP_NOUL_MAX, DISTRIBUTION_SUM_TOLERANCE, KEEP_BASELINE_CHOICE, NEEDS_EVIDENCE_CHOICE, POLICY_REVISION, ROUTING_RISK_NOUL_MIN, ROUTING_ROLE_OMIT_NOUL_MAX, UNKNOWN_USAGE, sealedRoutingModel } from './types.js';
1
+ import { CHOICE_MIN_CONFIDENCE, CHOICE_MIN_PROBABILITY, CONTEXT_DROP_NOUL_MAX, DISTRIBUTION_SUM_TOLERANCE, DELEGATION_CHOICES, KEEP_BASELINE_CHOICE, NEEDS_EVIDENCE_CHOICE, POLICY_REVISION, ROUTING_RISK_NOUL_MIN, ROUTING_ROLE_OMIT_NOUL_MAX, UNKNOWN_USAGE, ESCALATION_ROUTING_TIER, routingTier } from './types.js';
2
2
  export function compileDecision(bundle, response) {
3
3
  const usage = usageFromWire(response.usage);
4
4
  const decoded = decodeWireResponse(bundle, response);
@@ -33,6 +33,14 @@ export function compileDecision(bundle, response) {
33
33
  else if (!fallbackReason)
34
34
  fallbackReason = compiled.reason;
35
35
  }
36
+ const delegationBinding = Object.entries(bundle.questionBindings).find(([, binding]) => binding.kind === 'delegation');
37
+ if (delegationBinding) {
38
+ const compiled = compileDelegation(decoded.response.answers[delegationBinding[0]]);
39
+ if (compiled.kind === 'effect')
40
+ effects.push(compiled.effect);
41
+ else if (!fallbackReason)
42
+ fallbackReason = compiled.reason;
43
+ }
36
44
  if (effects.length === 0) {
37
45
  return {
38
46
  kind: 'keep_baseline',
@@ -172,7 +180,7 @@ function compileRoleRouting(bundle, answers, roleId) {
172
180
  return { kind: 'baseline', reason: 'invalid_response' };
173
181
  if (answer.choice === KEEP_BASELINE_CHOICE)
174
182
  return { kind: 'baseline', reason: 'keep_baseline_selected' };
175
- const selected = sealedRoutingModel(answer.choice);
183
+ const selected = routingTier(answer.choice);
176
184
  if (!selected)
177
185
  return { kind: 'baseline', reason: 'invalid_response' };
178
186
  const choiceQuestion = choiceId ? bundle.request.questions[choiceId] : undefined;
@@ -180,8 +188,8 @@ function compileRoleRouting(bundle, answers, roleId) {
180
188
  const uncertainty = requiredChoiceUncertainty(answer, labels);
181
189
  if (!uncertainty.ok)
182
190
  return { kind: 'baseline', reason: uncertainty.reason };
183
- const model = escalateRole(bundle, answers, roleId) ? 'gpt-6-astra' : selected.id;
184
- return { kind: 'effect', effect: { kind: 'select_routing', roleId, model } };
191
+ const tier = escalateRole(bundle, answers, roleId) ? ESCALATION_ROUTING_TIER : selected.id;
192
+ return { kind: 'effect', effect: { kind: 'select_routing', roleId, tier } };
185
193
  }
186
194
  function escalateRole(bundle, answers, roleId) {
187
195
  const difficultyId = questionIdFor(bundle, 'routing_difficulty', roleId);
@@ -264,6 +272,21 @@ function compileRecovery(bundle, answer) {
264
272
  return { kind: 'baseline', reason: uncertainty.reason };
265
273
  return { kind: 'effect', effect: { kind: 'dispatch_recovery', actionId: candidate.id } };
266
274
  }
275
+ function compileDelegation(answer) {
276
+ if (!answer)
277
+ return { kind: 'baseline', reason: 'missing_answer' };
278
+ if (answer.type !== 'choice')
279
+ return { kind: 'baseline', reason: 'invalid_response' };
280
+ if (answer.choice === KEEP_BASELINE_CHOICE)
281
+ return { kind: 'baseline', reason: 'keep_baseline_selected' };
282
+ const choice = DELEGATION_CHOICES.find((row) => row === answer.choice);
283
+ if (!choice)
284
+ return { kind: 'baseline', reason: 'invalid_response' };
285
+ const uncertainty = requiredChoiceUncertainty(answer, [...DELEGATION_CHOICES, KEEP_BASELINE_CHOICE]);
286
+ if (!uncertainty.ok)
287
+ return { kind: 'baseline', reason: uncertainty.reason };
288
+ return { kind: 'effect', effect: { kind: 'select_delegation', choice } };
289
+ }
267
290
  function canDropOptional(candidate, noul) {
268
291
  return !candidate.pinned
269
292
  && candidate.fresh
@@ -1,4 +1,4 @@
1
- import { CONTEXT_RELEVANCE_RUBRIC, DESIGN_DEFAULTS, KEEP_BASELINE_CHOICE, NEEDS_EVIDENCE_CHOICE, QUESTION_REVISION, ROUTING_DIFFICULTY_RUBRIC, SEALED_ROUTING_MODELS } from './types.js';
1
+ import { CONTEXT_RELEVANCE_RUBRIC, DELEGATION_CHOICES, DESIGN_DEFAULTS, KEEP_BASELINE_CHOICE, NEEDS_EVIDENCE_CHOICE, QUESTION_REVISION, ROUTING_DIFFICULTY_RUBRIC, ROUTING_TIERS } from './types.js';
2
2
  import { buildDecisionBinding, redactDecisionText } from './state.js';
3
3
  const SAME_BATCH_DEPENDENCY = /\b(selected above|answer above|previous answer|the (plan|action) selected|if the (plan|action) selected)\b/i;
4
4
  export const CONTEXT_RELEVANCE_LEVELS = CONTEXT_RELEVANCE_RUBRIC.length;
@@ -33,6 +33,7 @@ export function buildDecisionBundle(input) {
33
33
  const contextCandidates = [...(input.contextCandidates || [])];
34
34
  const recoveryCandidates = [...(input.recoveryCandidates || [])];
35
35
  const routingCandidates = [...(input.routingCandidates || [])];
36
+ const delegationCandidate = input.delegationCandidate || null;
36
37
  const questions = {};
37
38
  const questionBindings = {};
38
39
  const recoverySlot = recoveryCandidates.length > 0 ? 1 : 0;
@@ -50,6 +51,7 @@ export function buildDecisionBundle(input) {
50
51
  questionBindings.plan = { kind: 'plan' };
51
52
  }
52
53
  const routedRoles = appendRoutingChoices(routingCandidates, questions, questionBindings, recoverySlot);
54
+ appendDelegationChoice(delegationCandidate, questions, questionBindings, recoverySlot);
53
55
  for (const candidate of contextCandidates.filter((row) => !row.pinned && row.excerpt)) {
54
56
  if (Object.keys(questions).length + 2 > DESIGN_DEFAULTS.maxQuestions - recoverySlot)
55
57
  break;
@@ -123,8 +125,16 @@ export function buildDecisionBundle(input) {
123
125
  diagnostic: redactDecisionText(input.recoveryDiagnostic || '', 800),
124
126
  handlers: recoveryCandidates
125
127
  },
128
+ delegation: delegationCandidate
129
+ ? {
130
+ tool: redactDecisionText(delegationCandidate.toolName, 120),
131
+ targets: delegationCandidate.targets.slice(0, 16).map((target) => redactDecisionText(target, 240)),
132
+ mission_goal: redactDecisionText(delegationCandidate.missionGoal, 800),
133
+ children_started: 0
134
+ }
135
+ : null,
126
136
  routing: {
127
- models: Object.fromEntries(SEALED_ROUTING_MODELS.map((row) => [row.id, {
137
+ tiers: Object.fromEntries(ROUTING_TIERS.map((row) => [row.id, {
128
138
  effort: row.effort,
129
139
  summary: row.summary
130
140
  }])),
@@ -146,7 +156,7 @@ export function buildDecisionBundle(input) {
146
156
  workflowRevision: input.workflowRevision,
147
157
  sourceDigest: input.sourceDigest,
148
158
  graphDigest: input.graphDigest,
149
- candidates: { planCandidates, contextCandidates, recoveryCandidates, routingCandidates: routedRoles },
159
+ candidates: { planCandidates, contextCandidates, recoveryCandidates, routingCandidates: routedRoles, delegationCandidate },
150
160
  questions,
151
161
  requestedModel: DESIGN_DEFAULTS.model
152
162
  }),
@@ -155,6 +165,7 @@ export function buildDecisionBundle(input) {
155
165
  contextCandidates,
156
166
  recoveryCandidates,
157
167
  routingCandidates: routedRoles,
168
+ delegationCandidate,
158
169
  baselinePlanId: input.baselinePlanId ?? planCandidates[0]?.id ?? null,
159
170
  questionBindings
160
171
  };
@@ -168,14 +179,14 @@ function appendRoutingChoices(roles, questions, bindings, recoverySlot) {
168
179
  if (questionRoom(questions, recoverySlot) < 1)
169
180
  break;
170
181
  const id = `route_${role.id}`;
171
- const instructions = redactDecisionText(`Choose the sealed model for routing.roles.${role.id} using state.task and that role summary. Prefer the fastest sealed model that can do the work. Use gpt-6-astra for judgment, ambiguity, or high-stakes work. Select ${KEEP_BASELINE_CHOICE} when the work is not distinguished. Do not invent a model.`);
182
+ const instructions = redactDecisionText(`Choose the model tier for routing.roles.${role.id} using state.task and that role summary. Each tier is the newest model for that speed and accuracy. Prefer the fastest tier that can do the work correctly. Use deep for judgment, ambiguity, or high-stakes work. Select ${KEEP_BASELINE_CHOICE} when the work is not distinguished. Do not invent a tier.`);
172
183
  assertIndependentQuestion(instructions);
173
184
  questions[id] = {
174
185
  type: 'choice',
175
186
  instructions,
176
187
  criteria: {
177
- ...Object.fromEntries(SEALED_ROUTING_MODELS.map((row) => [row.id, row.summary])),
178
- [KEEP_BASELINE_CHOICE]: 'The role work is not distinguished enough to leave the baseline model.'
188
+ ...Object.fromEntries(ROUTING_TIERS.map((row) => [row.id, row.summary])),
189
+ [KEEP_BASELINE_CHOICE]: 'The role work is not distinguished enough to leave the baseline tier.'
179
190
  }
180
191
  };
181
192
  bindings[id] = { kind: 'routing', roleId: role.id };
@@ -183,6 +194,25 @@ function appendRoutingChoices(roles, questions, bindings, recoverySlot) {
183
194
  }
184
195
  return included;
185
196
  }
197
+ function appendDelegationChoice(candidate, questions, bindings, recoverySlot) {
198
+ if (!candidate || questionRoom(questions, recoverySlot) < 1)
199
+ return;
200
+ const instructions = redactDecisionText(`state.delegation describes a tool call the Naruto parent thread wants to make before any child thread exists. Using state.task, state.delegation.tool, and state.delegation.targets, choose whether that edit is slice implementation that a child must own, or orchestration scaffolding that no slice owns and that children depend on (a shared interface or type stub, workspace or build wiring, or a plan file). Any change to feature, fix, or test code is slice work, however small. Select ${KEEP_BASELINE_CHOICE} when the evidence does not distinguish them.`);
201
+ assertIndependentQuestion(instructions);
202
+ questions.delegation = {
203
+ type: 'choice',
204
+ instructions,
205
+ criteria: {
206
+ ...Object.fromEntries(DELEGATION_CHOICES.map((choice) => [choice, DELEGATION_CHOICE_SUMMARIES[choice]])),
207
+ [KEEP_BASELINE_CHOICE]: 'The evidence does not distinguish orchestration scaffolding from slice work.'
208
+ }
209
+ };
210
+ bindings.delegation = { kind: 'delegation' };
211
+ }
212
+ const DELEGATION_CHOICE_SUMMARIES = Object.freeze({
213
+ delegate_child: 'The edit implements assigned slice work. The parent must spawn a child for it first.',
214
+ parent_owned: 'The edit is orchestration scaffolding that no slice owns and that children depend on.'
215
+ });
186
216
  function appendRoutingSpeculation(roles, questions, bindings, recoverySlot) {
187
217
  for (const role of roles) {
188
218
  if (questionRoom(questions, recoverySlot) < 1)
@@ -199,14 +229,14 @@ function appendRoutingSpeculation(roles, questions, bindings, recoverySlot) {
199
229
  if (questionRoom(questions, recoverySlot) < 1)
200
230
  break;
201
231
  const riskId = `risk_${role.id}`;
202
- const riskInstructions = redactDecisionText(`Does routing.roles.${role.id} need gpt-6-astra because the work is high-stakes, ambiguous, or unsafe on a faster sealed model?`);
232
+ const riskInstructions = redactDecisionText(`Does routing.roles.${role.id} need the deep tier because the work is high-stakes, ambiguous, or unsafe on a faster tier?`);
203
233
  assertIndependentQuestion(riskInstructions);
204
234
  questions[riskId] = {
205
235
  type: 'noul',
206
236
  instructions: riskInstructions,
207
237
  criteria: {
208
- true: 'The work needs the most capable sealed model.',
209
- false: 'A faster sealed model can do this work.'
238
+ true: 'The work needs the most capable latest model.',
239
+ false: 'A faster latest model can do this work.'
210
240
  }
211
241
  };
212
242
  bindings[riskId] = { kind: 'routing_risk', roleId: role.id };
@@ -1,4 +1,5 @@
1
- import { sealedRoutingModel } from './types.js';
1
+ import { effortForTier, latestTierModelSet, resolveLatestModelTiers } from '../subagents/model-tiers.js';
2
+ import { routingTier } from './types.js';
2
3
  const ROLE_ID = /^[a-z][a-z0-9_]{0,40}$/;
3
4
  export const MAX_JEV_ROUTING_ROLES = 8;
4
5
  export function buildRoutingCandidates(input) {
@@ -19,17 +20,17 @@ export function applySealedRouting(agents, selected) {
19
20
  if (!selected || !agents)
20
21
  return { ...(agents || {}) };
21
22
  const next = { ...agents };
23
+ const current = latestTierModelSet();
22
24
  for (const [name, effort] of Object.entries(selected.efforts)) {
23
25
  const model = selected.models[name];
24
- const sealed = model ? sealedRoutingModel(model) : null;
25
- if (!sealed || sealed.effort !== effort)
26
+ if (!model || !current.has(model))
26
27
  continue;
27
28
  const row = next[name];
28
29
  if (!row || row.routing_dynamic !== true || row.routed_model_policy === 'user_role_model_preference')
29
30
  continue;
30
31
  next[name] = {
31
32
  ...row,
32
- routed_model: sealed.id,
33
+ routed_model: model,
33
34
  routed_model_reasoning_effort: effort,
34
35
  routed_model_policy: 'jev_sealed_routing',
35
36
  jev_routing_lane: selected.id
@@ -38,21 +39,25 @@ export function applySealedRouting(agents, selected) {
38
39
  return next;
39
40
  }
40
41
  export function assembleRoutingSelection(effects) {
42
+ const resolved = resolveLatestModelTiers();
41
43
  const models = {};
42
44
  const efforts = {};
45
+ const tiers = {};
43
46
  for (const effect of effects) {
44
- const sealed = sealedRoutingModel(effect.model);
45
- if (!sealed || !ROLE_ID.test(effect.roleId))
47
+ const tier = routingTier(effect.tier);
48
+ if (!tier || !ROLE_ID.test(effect.roleId))
46
49
  continue;
47
- models[effect.roleId] = sealed.id;
48
- efforts[effect.roleId] = sealed.effort;
50
+ models[effect.roleId] = resolved.models[tier.id];
51
+ efforts[effect.roleId] = effortForTier(tier.id, resolved);
52
+ tiers[effect.roleId] = tier.id;
49
53
  }
50
54
  if (Object.keys(models).length === 0)
51
55
  return null;
52
56
  return {
53
57
  id: 'jev_role_fanout',
54
- summary: 'Jev chose a sealed model for each dynamic Naruto role.',
58
+ summary: 'Jev chose a tier for each dynamic Naruto role; each tier is its newest model.',
55
59
  models,
56
- efforts
60
+ efforts,
61
+ tiers
57
62
  };
58
63
  }
@@ -1,25 +1,14 @@
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
- })
1
+ export const ROUTING_TIERS = Object.freeze([
2
+ Object.freeze({ id: 'fast', effort: 'low', summary: 'Fastest latest model. Mechanical edits, renames, formatting, and other one-step changes.' }),
3
+ Object.freeze({ id: 'balanced', effort: 'low', summary: 'Fast latest model. Simple coding whose result is already specified.' }),
4
+ Object.freeze({ id: 'context', effort: 'medium', summary: 'Latest model for context and tools. Search, multi-file reading, and broad exploration.' }),
5
+ Object.freeze({ id: 'deep', effort: 'max', summary: 'Escalate to the most capable latest model. Judgment, architecture, ambiguity, security, or high-stakes work.' })
22
6
  ]);
7
+ export const ESCALATION_ROUTING_TIER = 'deep';
8
+ export function routingTier(value) {
9
+ const match = ROUTING_TIERS.find((row) => row.id === value);
10
+ return match ? { id: match.id, effort: match.effort } : null;
11
+ }
23
12
  export const ROUTING_DIFFICULTY_RUBRIC = Object.freeze([
24
13
  'Mechanical one-step work: rename, format, copy, or a single exact edit.',
25
14
  'Simple coding whose result is already specified.',
@@ -28,10 +17,7 @@ export const ROUTING_DIFFICULTY_RUBRIC = Object.freeze([
28
17
  ]);
29
18
  export const ROUTING_RISK_NOUL_MIN = 0.70;
30
19
  export const ROUTING_ROLE_OMIT_NOUL_MAX = 0.35;
31
- export function sealedRoutingModel(model) {
32
- const match = SEALED_ROUTING_MODELS.find((row) => row.id === model);
33
- return match ? { id: match.id, effort: match.effort } : null;
34
- }
20
+ export const DELEGATION_CHOICES = Object.freeze(['delegate_child', 'parent_owned']);
35
21
  export const DESIGN_DEFAULTS = Object.freeze({
36
22
  mode: 'off',
37
23
  model: 'typesafe/jev-1.13',
@@ -11,7 +11,7 @@ import { collectNestedProjectRoots } from './current-project-guidance-nested.js'
11
11
  import { escapeRegExp } from '../text/regex.js';
12
12
  export const CURRENT_PROJECT_GUIDANCE_SCHEMA = 'sks.current-project-guidance.v1';
13
13
  const AGENTS_MARKER = 'BEGIN Sneakoscope Codex GX MANAGED BLOCK';
14
- const RETIRED_COMMAND_NAMES = ['team', 'mad-db', 'tmux', 'xai', 'swarm', 'agent', 'ralph', 'db', 'ui', 'glm'];
14
+ const RETIRED_COMMAND_NAMES = ['team', 'mad-db', 'tmux', 'xai', 'swarm', 'agent', 'ralph', 'loop', 'db', 'ui', 'glm'];
15
15
  const RETIRED_DOLLAR_COMMAND_NAMES = ['Agent', 'Team', 'MAD-DB', 'Swarm', 'ShadowClone', 'Kagebunshin', 'Ralph'];
16
16
  const LEGACY_UNPREFIXED_DOLLAR_COMMAND_NAMES = Array.from(new Set([
17
17
  ...RETIRED_DOLLAR_COMMAND_NAMES,
@@ -69,6 +69,43 @@ export async function reconcileCurrentProjectGuidance(opts) {
69
69
  warnings: counters.warnings
70
70
  };
71
71
  }
72
+ export async function reconcileManagedProjectPromptGuidance(root) {
73
+ const projectRoot = path.resolve(root);
74
+ const refreshed = [];
75
+ let errors = 0;
76
+ const agentsFile = path.join(projectRoot, 'AGENTS.md');
77
+ const agentsStat = await fsp.lstat(agentsFile).catch(() => null);
78
+ if (agentsStat?.isFile() && !agentsStat.isSymbolicLink()) {
79
+ const before = await readText(agentsFile, '');
80
+ if (before.includes(AGENTS_MARKER) && managedAgentsBlockNeedsReconcile(before)) {
81
+ try {
82
+ await mergeManagedBlock(agentsFile, 'Sneakoscope Codex GX MANAGED BLOCK', agentsBlockText());
83
+ refreshed.push('AGENTS.md');
84
+ }
85
+ catch {
86
+ errors += 1;
87
+ }
88
+ }
89
+ }
90
+ const quickFile = path.join(projectRoot, '.codex', 'SNEAKOSCOPE.md');
91
+ const quickStat = await fsp.lstat(quickFile).catch(() => null);
92
+ if (quickStat?.isFile() && !quickStat.isSymbolicLink()) {
93
+ const before = await readText(quickFile, '');
94
+ if (isManagedQuickReference(before)) {
95
+ const expected = codexAppQuickReference(quickReferenceInstallScope(before, 'project'), quickReferenceCommandPrefix(before, 'project'));
96
+ if (before !== expected) {
97
+ try {
98
+ await writeTextAtomic(quickFile, expected);
99
+ refreshed.push('.codex/SNEAKOSCOPE.md');
100
+ }
101
+ catch {
102
+ errors += 1;
103
+ }
104
+ }
105
+ }
106
+ }
107
+ return { refreshed, errors };
108
+ }
72
109
  async function reconcileGuidanceScope(scope, fix, runId, counters) {
73
110
  const rootStat = await fsp.lstat(scope.root).catch((error) => error?.code === 'ENOENT' ? null : Promise.reject(error));
74
111
  if (!rootStat)
@@ -31,7 +31,8 @@ const REWRITE_RULES = [
31
31
  { id: 'cli-team', pattern: new RegExp(`${LEFT}sks\\s+team${RIGHT}`, 'gi'), replace: '$1sks naruto' },
32
32
  { id: 'cli-agent', pattern: new RegExp(`${LEFT}sks\\s+agent${RIGHT}`, 'gi'), replace: '$1sks naruto' },
33
33
  { id: 'cli-swarm', pattern: new RegExp(`${LEFT}sks\\s+swarm${RIGHT}`, 'gi'), replace: '$1sks naruto' },
34
- { id: 'cli-ralph', pattern: new RegExp(`${LEFT}sks\\s+ralph${RIGHT}`, 'gi'), replace: '$1sks loop' },
34
+ { id: 'cli-ralph', pattern: new RegExp(`${LEFT}sks\\s+ralph${RIGHT}`, 'gi'), replace: '$1sks naruto' },
35
+ { id: 'cli-loop', pattern: new RegExp(`${LEFT}sks\\s+loop${RIGHT}`, 'gi'), replace: '$1sks naruto' },
35
36
  { id: 'cli-tmux', pattern: new RegExp(`${LEFT}sks\\s+tmux${RIGHT}`, 'gi'), replace: '$1sks --mad' },
36
37
  { id: 'cli-xai', pattern: new RegExp(`${LEFT}sks\\s+xai${RIGHT}`, 'gi'), replace: '$1sks bridge provider configure openrouter --api-key-stdin' },
37
38
  { id: 'cli-glm', pattern: new RegExp(`${LEFT}sks\\s+glm${RIGHT}`, 'gi'), replace: '$1sks bridge provider configure openrouter --api-key-stdin' },
@@ -19,7 +19,7 @@ const OFFICIAL_SUBAGENT_SPAWN_COMPATIBILITY_CONTEXT = [
19
19
  'SKS Codex 0.145 official-subagent spawn compatibility:',
20
20
  '- Full-history forks (`fork_turns="all"`, including the omitted/default full-history mode) inherit the parent agent type, model, and reasoning effort.',
21
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`.',
22
- '- SKS children must use explicit `model=\"gpt-6-astra\"`, task-appropriate `reasoning_effort`, and `fork_turns=\"none\"` or a positive bounded turn count. Do not use omitted/default or full-history forks, which can inherit a non-Astra parent.'
22
+ '- 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. Do not use omitted/default or full-history forks, which inherit the parent model.'
23
23
  ].join('\n');
24
24
  export function officialSubagentSpawnCompatibilityContext() {
25
25
  return OFFICIAL_SUBAGENT_SPAWN_COMPATIBILITY_CONTEXT;
@@ -1,4 +1,7 @@
1
1
  import { consultJevTurnModel } from '../decisions/integration.js';
2
+ import { managedOfficialSubagentRoleByName } from '../managed-assets/managed-assets-manifest.js';
3
+ import { subagentModelProfile } from '../subagents/model-policy.js';
4
+ import { effortForTier, latestModelForTier, latestTierModelSet } from '../subagents/model-tiers.js';
2
5
  import { readRoleModelPreferences } from '../subagents/role-model-preferences.js';
3
6
  const SPAWN_TOOLS = new Set(['spawn_agent', 'collaboration.spawn_agent', 'functions.spawn_agent']);
4
7
  function spawnInput(payload) {
@@ -22,6 +25,14 @@ function spawnTask(input) {
22
25
  .join('\n')
23
26
  .slice(0, 1200);
24
27
  }
28
+ export function roleTierFallback(agentType) {
29
+ const role = agentType ? managedOfficialSubagentRoleByName(agentType) : null;
30
+ if (role?.model_policy) {
31
+ const profile = subagentModelProfile(role.model_policy);
32
+ return { model: profile.model, effort: effortForTier(profile.tier) };
33
+ }
34
+ return { model: latestModelForTier('deep'), effort: effortForTier('deep') };
35
+ }
25
36
  export async function jevSpawnModelRewrite(root, state, payload) {
26
37
  if (!narutoParent(state))
27
38
  return null;
@@ -38,13 +49,21 @@ export async function jevSpawnModelRewrite(root, state, payload) {
38
49
  if (!task)
39
50
  return null;
40
51
  const decision = await consultJevTurnModel({ root, prompt: task, roleId: 'spawn' }).catch(() => null);
41
- if (!decision?.model || !decision.effort)
52
+ if (!decision?.called)
42
53
  return null;
54
+ const forkTurns = input.fork_turns === undefined ? { fork_turns: 'none' } : {};
55
+ if (!decision.model || !decision.effort) {
56
+ if (latestTierModelSet().has(String(input.model || '')))
57
+ return null;
58
+ const fallback = roleTierFallback(agent);
59
+ return { ...input, model: fallback.model, reasoning_effort: fallback.effort, ...forkTurns };
60
+ }
43
61
  if (input.model === decision.model && input.reasoning_effort === decision.effort)
44
62
  return null;
45
63
  return {
46
64
  ...input,
47
65
  model: decision.model,
48
- reasoning_effort: decision.effort
66
+ reasoning_effort: decision.effort,
67
+ ...forkTurns
49
68
  };
50
69
  }
@@ -0,0 +1,30 @@
1
+ import fsp from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { nowIso, PACKAGE_VERSION, readJson, writeJsonAtomic } from '../fsx.js';
4
+ import { ensureConfinedDirectory } from '../managed-path-safety.js';
5
+ import { reconcileManagedProjectPromptGuidance } from '../doctor/current-project-guidance.js';
6
+ export const MANAGED_GUIDANCE_STAMP_SCHEMA = 'sks.managed-guidance-generation.v1';
7
+ export function managedGuidanceStampPath(root) {
8
+ return path.join(root, '.sneakoscope', 'state', 'managed-guidance-generation.json');
9
+ }
10
+ export async function maybeReconcileManagedGuidancePreflight(root) {
11
+ const projectRoot = path.resolve(root);
12
+ const stampPath = managedGuidanceStampPath(projectRoot);
13
+ const stamp = await readJson(stampPath, null).catch(() => null);
14
+ if (stamp?.schema === MANAGED_GUIDANCE_STAMP_SCHEMA && stamp?.version === PACKAGE_VERSION)
15
+ return null;
16
+ const sksDir = await fsp.lstat(path.join(projectRoot, '.sneakoscope')).catch(() => null);
17
+ if (!sksDir?.isDirectory() || sksDir.isSymbolicLink())
18
+ return null;
19
+ const report = await reconcileManagedProjectPromptGuidance(projectRoot);
20
+ if (report.errors > 0)
21
+ return { refreshed: report.refreshed };
22
+ await ensureConfinedDirectory(projectRoot, path.dirname(stampPath));
23
+ await writeJsonAtomic(stampPath, {
24
+ schema: MANAGED_GUIDANCE_STAMP_SCHEMA,
25
+ version: PACKAGE_VERSION,
26
+ refreshed: report.refreshed,
27
+ checked_at: nowIso()
28
+ });
29
+ return { refreshed: report.refreshed };
30
+ }
@@ -3,7 +3,7 @@ import path from 'node:path';
3
3
  import { appendJsonl, nowIso, readJson, sha256, writeJsonAtomic } from '../fsx.js';
4
4
  import { missionDir, updateCurrentIfMissionAndRun } from '../mission.js';
5
5
  import { ensureConfinedDirectory } from '../managed-path-safety.js';
6
- import { NARUTO_PARENT_EFFORT, NARUTO_PARENT_MODEL } from '../subagents/model-policy.js';
6
+ import { NARUTO_PARENT_EFFORT, narutoParentModel } from '../subagents/model-policy.js';
7
7
  import { officialSubagentRolePlan } from '../subagents/agent-catalog.js';
8
8
  import { bindTrustworthySubagentParentSummaryToRun, normalizeSubagentEvent, normalizeSubagentParentSummary, persistOrReuseTrustworthySubagentParentSummary, readSubagentEvents, recordSubagentEvent, SUBAGENT_EVIDENCE_FILENAME, SUBAGENT_PARENT_SUMMARY_FILENAME, writeSubagentEvidence } from '../subagents/subagent-evidence.js';
9
9
  import { officialSubagentPreparationInProgress, SUBAGENT_LIFECYCLE_CAPTURE_FAILURE_DIR, withOfficialSubagentLifecycleLock, writeNarutoGate } from '../subagents/official-subagent-preparation.js';
@@ -308,7 +308,7 @@ 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, NARUTO_PARENT_MODEL);
311
+ const parentModelMismatch = previousGate.parent_model_match === false || observedParentModelMismatch(parentModel, narutoParentModel());
312
312
  const blockers = [...new Set([
313
313
  ...evidence.blockers,
314
314
  ...(Array.isArray(previousGate.config_blockers) ? previousGate.config_blockers.map(String) : []),
@@ -329,7 +329,7 @@ async function refreshOfficialSubagentCompletionArtifactsLocked(root, state, par
329
329
  route: '$Naruto',
330
330
  status: passed ? 'completed' : evidence.status,
331
331
  parent: {
332
- model: NARUTO_PARENT_MODEL,
332
+ model: narutoParentModel(),
333
333
  model_reasoning_effort: NARUTO_PARENT_EFFORT,
334
334
  observed_model: parentModel,
335
335
  observed_model_match: parentModel ? !parentModelMismatch : null