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
@@ -17,35 +17,91 @@ import { readRoleModelPreferences } from './role-model-preferences.js';
17
17
  import { readConfiguredCodexModelRoutingContext } from '../codex-app/codex-model-catalog.js';
18
18
  import { SUBAGENT_EVIDENCE_FILENAME, SUBAGENT_EVENT_LOG_FILENAME, SUBAGENT_PARENT_SUMMARY_FILENAME, normalizeSubagentParentSummary, writeSubagentEvidence } from './subagent-evidence.js';
19
19
  import { sksPrefixedDollarCommand, unprefixedSksSkillName } from '../routes/dollar-prefix.js';
20
- import { withFileLock, tryWithFileLock } from '../locks/file-lock.js';
20
+ import { tryWithFileLock } from '../locks/file-lock.js';
21
21
  import { updateCurrentIfMissionAndRun } from '../mission.js';
22
22
  import { HOST_CAPABILITY_HOOK_EVIDENCE_FILENAME, HOST_CAPABILITY_HOOK_OBSERVATIONS_FILENAME, HOST_CAPABILITY_HOOK_PENDING_RUNTIME_FILENAME, HOST_CAPABILITY_HOOK_RUNTIME_FILENAME } from '../agent-bridge/host-capability-runtime.js';
23
23
  import { uniqueStrings } from '../text/strings.js';
24
24
  import { officialSubagentEvidenceReady } from './terminal-subagent-state.js';
25
+ import { OFFICIAL_SUBAGENT_LIFECYCLE_LOCK, withOfficialSubagentLifecycleLock } from './official-subagent-lock.js';
26
+ import { decideOfficialSubagentPreparation } from '../decisions/integration.js';
27
+ import { applySealedRouting } from '../decisions/routing.js';
28
+ import { graphFileDigest, sourceSnapshotDigest } from '../decisions/state.js';
29
+ export { OFFICIAL_SUBAGENT_LIFECYCLE_LOCK, withOfficialSubagentLifecycleLock };
25
30
  export const NARUTO_RESULT_SCHEMA = 'sks.naruto-subagent-workflow.v1';
26
31
  export const SUBAGENT_PLAN_FILENAME = 'subagent-plan.json';
27
32
  export const NARUTO_SUMMARY_FILENAME = 'naruto-summary.json';
28
33
  export const NARUTO_GATE_FILENAME = 'naruto-gate.json';
29
- export const OFFICIAL_SUBAGENT_LIFECYCLE_LOCK = '.subagent-evidence.lock';
30
34
  export const OFFICIAL_SUBAGENT_PREPARATION_TRANSACTION = '.official-subagent-preparation-transaction.json';
31
35
  export const SUBAGENT_LIFECYCLE_CAPTURE_FAILURE_DIR = 'subagent-lifecycle-capture-failures';
32
36
  const OFFICIAL_SUBAGENT_PREPARATION_STAGE_PREFIX = '.official-subagent-preparation-stage-';
33
- export function withOfficialSubagentLifecycleLock(dir, fn) {
34
- return withFileLock({
35
- lockPath: path.join(dir, OFFICIAL_SUBAGENT_LIFECYCLE_LOCK),
36
- timeoutMs: 5_000,
37
- staleMs: 60_000
38
- }, fn);
39
- }
40
37
  export async function prepareOfficialSubagentMission(input) {
41
- return withOfficialSubagentLifecycleLock(input.dir, () => prepareOfficialSubagentMissionLocked(input));
38
+ const opened = await withOfficialSubagentLifecycleLock(input.dir, () => openOfficialSubagentPreparation(input));
39
+ if (opened.kind === 'recovered')
40
+ return opened.result;
41
+ const derived = await deriveOfficialSubagentPreparation(input, opened.snapshot);
42
+ const decided = await decideOfficialSubagentPreparation({
43
+ root: input.root,
44
+ dir: input.dir,
45
+ missionId: input.missionId,
46
+ goal: String(input.goal || '').trim(),
47
+ workflowRunId: derived.workflowRunId,
48
+ workflowRevision: opened.snapshot.workflowRevision,
49
+ slices: derived.slices,
50
+ requestedSource: derived.requestedSource,
51
+ requestedSubagents: derived.budget.requestedSubagents,
52
+ attention: derived.triwikiAttention,
53
+ changedPaths: derived.sliceWriteScopes,
54
+ massParallel: derived.fanoutPolicy.mass_parallel === true,
55
+ routingRoles: derived.mode === 'naruto'
56
+ ? routingRolesFromAgents(derived.agentRouting, derived.slices.map((slice) => slice.agent || ''))
57
+ : [],
58
+ env: input.env || process.env
59
+ });
60
+ const rebuilt = applyOfficialSubagentDecision(derived, decided);
61
+ return withOfficialSubagentLifecycleLock(input.dir, () => commitOfficialSubagentPreparation(input, opened.snapshot, rebuilt));
42
62
  }
43
- async function prepareOfficialSubagentMissionLocked(input) {
63
+ async function openOfficialSubagentPreparation(input) {
44
64
  const recovered = await recoverOfficialSubagentPreparationTransaction(input);
45
65
  if (recovered)
46
- return recovered;
66
+ return { kind: 'recovered', result: recovered };
47
67
  const previousPlan = await readJson(path.join(input.dir, SUBAGENT_PLAN_FILENAME), null).catch(() => null);
48
- const expectedWorkflowRunId = String(previousPlan?.workflow_run_id || '').trim();
68
+ const sourceDigest = await sourceSnapshotDigest(input.root);
69
+ const graphDigest = await graphFileDigest(input.root);
70
+ return {
71
+ kind: 'snapshot',
72
+ snapshot: {
73
+ previousWorkflowRunId: String(previousPlan?.workflow_run_id || '').trim(),
74
+ sourceDigest,
75
+ graphDigest,
76
+ workflowRevision: `${sourceDigest}:${graphDigest || 'no-graph'}`
77
+ }
78
+ };
79
+ }
80
+ async function commitOfficialSubagentPreparation(input, snapshot, derived) {
81
+ const recovered = await recoverOfficialSubagentPreparationTransaction(input);
82
+ if (recovered)
83
+ return recovered;
84
+ const currentPlan = await readJson(path.join(input.dir, SUBAGENT_PLAN_FILENAME), null).catch(() => null);
85
+ const currentRunId = String(currentPlan?.workflow_run_id || '').trim();
86
+ const currentSource = await sourceSnapshotDigest(input.root);
87
+ const currentGraph = await graphFileDigest(input.root);
88
+ if ((currentRunId && currentRunId !== snapshot.previousWorkflowRunId)
89
+ || currentSource !== snapshot.sourceDigest
90
+ || currentGraph !== snapshot.graphDigest) {
91
+ if (currentPlan && String(currentPlan.goal || '') === String(input.goal || '').trim()
92
+ && String(currentPlan.route || '') === String(input.route || '')) {
93
+ return {
94
+ ...derived.preparedResultBase,
95
+ plan: currentPlan,
96
+ evidence: await readJson(path.join(input.dir, SUBAGENT_EVIDENCE_FILENAME), null).catch(() => null)
97
+ };
98
+ }
99
+ throw new Error('official_subagent_preparation_stale_snapshot');
100
+ }
101
+ return promoteOfficialSubagentDerived(input, derived, snapshot.previousWorkflowRunId);
102
+ }
103
+ async function deriveOfficialSubagentPreparation(input, snapshot) {
104
+ const expectedWorkflowRunId = snapshot.previousWorkflowRunId;
49
105
  const goal = String(input.goal || '').trim();
50
106
  const mode = input.mode === 'naruto' ? 'naruto' : 'generic';
51
107
  const taskProfile = classifyTaskProfile(goal);
@@ -208,15 +264,13 @@ async function prepareOfficialSubagentMissionLocked(input) {
208
264
  readonly: config.sandbox_mode === 'read-only'
209
265
  });
210
266
  const preference = roleModelPreferences.store.roles[name];
211
- const routedProvider = 'openai';
212
- const routedModel = ASTRA_SUBAGENT_MODEL;
213
267
  const routedReasoning = preference?.reasoning_effort
214
268
  || config.model_reasoning_effort
215
269
  || decision.model_reasoning_effort;
216
270
  return [name, {
217
271
  ...config,
218
- routed_provider: routedProvider,
219
- routed_model: routedModel,
272
+ routed_provider: 'openai',
273
+ routed_model: preference?.model || ASTRA_SUBAGENT_MODEL,
220
274
  routed_model_reasoning_effort: routedReasoning,
221
275
  routed_model_policy: preference
222
276
  ? 'user_role_model_preference'
@@ -320,7 +374,6 @@ async function prepareOfficialSubagentMissionLocked(input) {
320
374
  verification: { budget: verification },
321
375
  created_at: nowIso()
322
376
  };
323
- const statePatch = input.statePatch?.({ plan, budget, workflowRunId }) || null;
324
377
  const preparedResultBase = {
325
378
  plan,
326
379
  budget,
@@ -336,19 +389,202 @@ async function prepareOfficialSubagentMissionLocked(input) {
336
389
  observedParentModel,
337
390
  parentModelMatch
338
391
  };
392
+ return {
393
+ preparedResultBase,
394
+ slices,
395
+ requestedSource,
396
+ sliceWriteScopes,
397
+ sliceSafety,
398
+ suggestedAgents,
399
+ roleModelPreferences,
400
+ roleModelRouting,
401
+ activeMainModel,
402
+ officialConfig,
403
+ selectedFanoutPolicy,
404
+ configuredMaxThreads,
405
+ externalCodexHostCap,
406
+ externalCodexHostCapVerification,
407
+ taskProfile,
408
+ mode,
409
+ goal,
410
+ delegationGoal,
411
+ observedParentModel,
412
+ parentModelMatch,
413
+ workflowRunId,
414
+ expectedWorkflowRunId,
415
+ ssotGuard,
416
+ verification,
417
+ budget,
418
+ fanoutPolicy,
419
+ agentRouting,
420
+ agentCatalog,
421
+ routeContract,
422
+ operatorRequested,
423
+ requestedSubagents,
424
+ triwikiAttention,
425
+ configBlockers,
426
+ plan,
427
+ sessionScope: input.sessionScope || null
428
+ };
429
+ }
430
+ function routingRolesFromAgents(agentRouting, preferredNames = []) {
431
+ const preferred = new Set(preferredNames.filter(Boolean));
432
+ const entries = Object.entries(agentRouting);
433
+ const ordered = [
434
+ ...entries.filter(([name]) => preferred.has(name)),
435
+ ...entries.filter(([name]) => !preferred.has(name))
436
+ ];
437
+ return ordered.flatMap(([name, row]) => {
438
+ if (row?.routing_dynamic !== true || row.routed_model_policy === 'user_role_model_preference')
439
+ return [];
440
+ return [{
441
+ name,
442
+ dynamic: true,
443
+ summary: String(row.description || name)
444
+ }];
445
+ });
446
+ }
447
+ function applyOfficialSubagentDecision(derived, decided) {
448
+ const attention = decided.attention;
449
+ const selectedPlan = derived.requestedSource === 'automatic' ? decided.selectedPlan : null;
450
+ const selectedRouting = derived.requestedSource === 'automatic' ? decided.selectedRouting : null;
451
+ const agents = applySealedRouting(derived.plan.agents, selectedRouting);
452
+ const budget = selectedPlan
453
+ ? rebuildBudget(derived, selectedPlan)
454
+ : derived.budget;
455
+ let fanoutPolicy = selectedPlan
456
+ ? {
457
+ ...derived.fanoutPolicy,
458
+ requested_subagents: budget.requestedSubagents,
459
+ jev_selected_plan: selectedPlan.id
460
+ }
461
+ : { ...derived.fanoutPolicy };
462
+ if (selectedRouting)
463
+ fanoutPolicy = { ...fanoutPolicy, jev_selected_routing: selectedRouting.id };
464
+ const routingPreferences = selectedRouting
465
+ ? Object.fromEntries(Object.entries(selectedRouting.efforts)
466
+ .filter(([name]) => !derived.roleModelPreferences.store.roles[name])
467
+ .map(([name, effort]) => [name, {
468
+ provider: 'openai',
469
+ model: selectedRouting.models[name] || 'gpt-6-astra',
470
+ reasoning_effort: effort,
471
+ updated_at: derived.plan.workflow_run_id
472
+ }]))
473
+ : {};
474
+ const delegationPrompt = buildOfficialSubagentPrompt({
475
+ goal: derived.delegationGoal,
476
+ slices: derived.slices,
477
+ requestedSubagents: budget.requestedSubagents,
478
+ requestedSubagentsExplicit: derived.requestedSource === 'operator',
479
+ requestedSubagentsSource: derived.requestedSource,
480
+ maxThreads: budget.maxThreads,
481
+ decompositionStatus: derived.plan.decomposition_status,
482
+ firstWave: budget.firstWave,
483
+ waveCount: budget.waveCount,
484
+ capacity: budget.capacity,
485
+ triwikiAttention: attention,
486
+ recommendedAgents: derived.suggestedAgents,
487
+ roleModelPreferences: {
488
+ ...derived.roleModelPreferences.store.roles,
489
+ ...routingPreferences
490
+ },
491
+ routedAgents: agents,
492
+ narutoChildRouting: derived.mode === 'naruto',
493
+ activeMainModel: derived.activeMainModel,
494
+ parentOutputMode: derived.mode === 'naruto' && derived.sessionScope ? 'app_naruto_stdin' : 'raw_json',
495
+ missionId: derived.plan.mission_id,
496
+ workflowRunId: derived.workflowRunId,
497
+ decisionContract: decided.compiled.kind === 'apply'
498
+ ? {
499
+ planId: selectedPlan?.id ?? null,
500
+ keepContextIds: decided.selectedContextIds,
501
+ routingLane: selectedRouting
502
+ ? Object.entries(selectedRouting.models).map(([name, model]) => `${name}=${model}`).join(', ')
503
+ : null,
504
+ executeSelectedIds: true
505
+ }
506
+ : null
507
+ });
508
+ const plan = {
509
+ ...derived.plan,
510
+ delegation_prompt: delegationPrompt,
511
+ requested_subagents: budget.requestedSubagents,
512
+ first_wave: budget.firstWave,
513
+ wave_count: budget.waveCount,
514
+ wave_lifecycle: createSubagentWaveLifecycle({
515
+ workflowRunId: derived.workflowRunId,
516
+ targetSubagents: budget.requestedSubagents,
517
+ countPolicy: derived.requestedSource === 'automatic' ? 'dynamic_automatic' : 'exact',
518
+ waveCapacity: budget.firstWave
519
+ }),
520
+ triwiki_attention: attention,
521
+ agents,
522
+ fanout_policy: fanoutPolicy,
523
+ capacity_controller: budget.capacity,
524
+ jev_decision: decided.receipt,
525
+ native_host_dispatch: 'unverified'
526
+ };
527
+ return {
528
+ ...derived,
529
+ budget,
530
+ fanoutPolicy,
531
+ triwikiAttention: attention,
532
+ plan,
533
+ preparedResultBase: {
534
+ ...derived.preparedResultBase,
535
+ plan,
536
+ budget,
537
+ delegationPrompt,
538
+ triwikiAttention: attention,
539
+ fanoutPolicy
540
+ }
541
+ };
542
+ }
543
+ function rebuildBudget(derived, selectedPlan) {
544
+ const requested = selectedPlan.workerCount;
545
+ const waveGovernor = decideNarutoConcurrency({
546
+ requestedWorkers: Math.min(requested, derived.configuredMaxThreads),
547
+ totalWorkItems: requested,
548
+ backend: 'official-subagent',
549
+ parallelismMode: 'extreme',
550
+ maxThreads: derived.configuredMaxThreads,
551
+ ...(derived.externalCodexHostCap === undefined ? {} : { externalCodexHostCap: derived.externalCodexHostCap })
552
+ });
553
+ return resolveSubagentThreadBudget({
554
+ requested,
555
+ configuredMaxThreads: derived.configuredMaxThreads,
556
+ ...(derived.externalCodexHostCap === undefined ? {} : { externalCodexHostCap: derived.externalCodexHostCap }),
557
+ marginalUsefulWorkers: waveGovernor.safe_active_workers,
558
+ reviewerReservedThreads: String(derived.fanoutPolicy.selection_reason || '').includes('reviewer')
559
+ || derived.fanoutPolicy.critical_multi_domain === true
560
+ ? Math.max(1, Number(derived.fanoutPolicy.automatic_reviewer_ceiling || 1))
561
+ : 0,
562
+ ...(derived.slices.length > 0
563
+ ? {
564
+ independentSliceCount: derived.slices.length,
565
+ readyDagWidth: derived.slices.length,
566
+ disjointOwnershipCount: derived.sliceSafety.safe ? derived.slices.length : 0
567
+ }
568
+ : {})
569
+ });
570
+ }
571
+ async function promoteOfficialSubagentDerived(input, derived, expectedWorkflowRunId) {
572
+ const { plan, budget, verification, workflowRunId, officialConfig, triwikiAttention, suggestedAgents, fanoutPolicy, configBlockers, observedParentModel, parentModelMatch, taskProfile, delegationPrompt } = derived.preparedResultBase;
573
+ const statePatch = input.statePatch?.({ plan, budget, workflowRunId }) || null;
574
+ const preparedResultBase = derived.preparedResultBase;
339
575
  const transactionFile = path.join(input.dir, OFFICIAL_SUBAGENT_PREPARATION_TRANSACTION);
340
576
  const stageName = `${OFFICIAL_SUBAGENT_PREPARATION_STAGE_PREFIX}${safePreparationStageId(workflowRunId)}`;
341
577
  const stageDir = path.join(input.dir, stageName);
342
- const artifactInventory = officialSubagentPreparationArtifactInventory(mode);
343
- const tombstoneInventory = officialSubagentPreparationTombstoneInventory(mode);
578
+ const artifactInventory = officialSubagentPreparationArtifactInventory(derived.mode);
579
+ const tombstoneInventory = officialSubagentPreparationTombstoneInventory(derived.mode);
344
580
  let transaction = {
345
581
  schema: 'sks.official-subagent-preparation-transaction.v2',
346
582
  status: 'staging',
347
583
  mission_id: input.missionId,
348
584
  session_scope: input.sessionScope || null,
349
585
  route: input.route,
350
- mode,
351
- goal,
586
+ mode: derived.mode,
587
+ goal: derived.goal,
352
588
  previous_workflow_run_id: expectedWorkflowRunId || null,
353
589
  target_workflow_run_id: workflowRunId,
354
590
  stage_dir: stageName,
@@ -365,9 +601,9 @@ async function prepareOfficialSubagentMissionLocked(input) {
365
601
  await fsp.rm(stageDir, { recursive: true, force: true });
366
602
  await fsp.mkdir(stageDir, { recursive: true });
367
603
  const evidence = await writeOfficialSubagentPreparationStage(stageDir, {
368
- mode,
604
+ mode: derived.mode,
369
605
  plan,
370
- ssotGuard,
606
+ ssotGuard: derived.ssotGuard,
371
607
  budget,
372
608
  workflowRunId,
373
609
  preparationOnly: input.preparationOnly !== false,
@@ -409,6 +645,11 @@ async function prepareOfficialSubagentMissionLocked(input) {
409
645
  return {
410
646
  ...preparedResultBase,
411
647
  evidence,
648
+ officialConfig,
649
+ triwikiAttention,
650
+ fanoutPolicy,
651
+ taskProfile,
652
+ delegationPrompt
412
653
  };
413
654
  }
414
655
  async function recoverOfficialSubagentPreparationTransaction(input) {
@@ -23,6 +23,7 @@ export function buildOfficialSubagentPrompt(input) {
23
23
  ? `${requestedSubagents} (route-owned exact orchestration contract)`
24
24
  : `${requestedSubagents} (dynamic automatic target; keep the final decomposed plan and evidence count exact)`;
25
25
  const triwiki = renderBoundedTriwikiAttention(input.triwikiAttention);
26
+ const decisionContract = renderDecisionContract(input.decisionContract);
26
27
  const resolvedSlices = input.slices.map((slice) => ({
27
28
  slice,
28
29
  agentName: slice.agent || selectOfficialSubagentRole({
@@ -48,7 +49,23 @@ export function buildOfficialSubagentPrompt(input) {
48
49
  && ['low', 'medium', 'high', 'xhigh', 'max', 'ultra'].includes(row.reasoning_effort)
49
50
  && officialSubagentOnDemandRoleCatalog([name]).some((role) => role.name === name))
50
51
  .map(([name, row]) => [name, row.reasoning_effort]));
51
- const spawnModelRouting = renderSpawnModelRouting();
52
+ const narutoChildren = input.narutoChildRouting === true;
53
+ const spawnModelRouting = renderSpawnModelRouting(narutoChildren);
54
+ const childModelRules = narutoChildren
55
+ ? [
56
+ '- use the model and reasoning_effort named in each slice spawn contract',
57
+ '- sealed child models are gpt-5.6-luna, gpt-5.6-sol, gpt-5.6-terra, and gpt-6-astra',
58
+ '- keep a stored user role-model preference for that role'
59
+ ].join('\n')
60
+ : [
61
+ '- use `worker` with gpt-6-astra and low reasoning for tiny short-context mechanical work such as simple search, typing, rename, copy, label, or one-line edits with no exploration or judgment',
62
+ '- use gpt-6-astra with low reasoning for ordinary UI, logic, backend, and native implementation with established instructions',
63
+ '- use gpt-6-astra with max reasoning for planning, analysis, review, focused unresolved, high-risk, architecture, security, database, research, release, or other explicit judgment slices',
64
+ '- use gpt-6-astra with medium reasoning for long context/memory, large docs/repository reads or exploration, large-scale first-draft code processing, and direct Computer Use, Browser/Chrome, or image generation',
65
+ '- explicit task class and phase win over incidental keywords: Astra Medium explores, Astra Low executes mechanical and instructed coding tasks, and Astra Max judges',
66
+ '- in mass fan-out, use worker/Astra Low for tiny mechanical shards and explorer/Astra Medium for broad exploration; use Astra Low for instructed implementation and Astra Max for judgment',
67
+ '- keep context, exploration, review, debugging, planning, and direct tool operation on their assigned defaults; preserve each sealed role model and effort instead of applying the parent profile to every child'
68
+ ].join('\n');
52
69
  const parentOutputMode = input.parentOutputMode === 'app_naruto_stdin'
53
70
  ? 'app_naruto_stdin'
54
71
  : 'raw_json';
@@ -56,9 +73,14 @@ export function buildOfficialSubagentPrompt(input) {
56
73
  const mode = slice.readOnly ? 'read-only' : 'use the parent permission mode';
57
74
  const paths = (slice.paths || []).map((entry) => String(entry).trim()).filter(Boolean);
58
75
  const role = officialSubagentOnDemandRoleCatalog([agentName])[0];
59
- const sealedReasoning = effortPreferences[agentName] || role?.model_reasoning_effort || 'medium';
76
+ const routed = input.routedAgents?.[agentName];
77
+ const sealedReasoning = routed?.routed_model_reasoning_effort
78
+ || effortPreferences[agentName]
79
+ || role?.model_reasoning_effort
80
+ || 'medium';
81
+ const sealedModel = routed?.routed_model || role?.model;
60
82
  const spawnContract = role
61
- ? `pass model=${JSON.stringify(role.model)} and reasoning_effort=${JSON.stringify(sealedReasoning)} from the sealed role policy`
83
+ ? `pass model=${JSON.stringify(sealedModel)} and reasoning_effort=${JSON.stringify(sealedReasoning)} from the sealed role policy`
62
84
  : 'stop before spawning: resolve an installed sealed Astra role and its effort first';
63
85
  return [
64
86
  `${index + 1}. [${slice.id}] use custom agent \`${agentName}\``,
@@ -94,18 +116,12 @@ Subagent rules:
94
116
  - use only Codex official subagent threads; do not launch shell workers, a custom scheduler, a worker pool, or model fanout
95
117
  - select the narrowest matching project custom agent by its description; the custom agent name is the spawn type
96
118
  - custom \`agent_type\` selection and spawn-time \`model\`/\`reasoning_effort\` overrides must use \`fork_turns="none"\` or a positive bounded turn count, with the complete bounded slice contract in \`message\`; context contract: pass fork_turns="none" for listed slices
97
- - \`spawn_agent\` has no provider argument; every child uses the exact model slug gpt-6-astra
119
+ - \`spawn_agent\` has no provider argument; ${narutoChildren ? 'Naruto children use the sealed model named in the spawn contract (gpt-5.6-luna, gpt-5.6-sol, gpt-5.6-terra, or gpt-6-astra)' : 'every child uses the exact model slug gpt-6-astra'}
98
120
  - never combine \`fork_turns="all"\` or the omitted/default full-history mode with \`agent_type\`, \`model\`, or \`reasoning_effort\`; Codex rejects that start before SubagentStart
99
121
  - never use a full-history fork for SKS children
100
122
  ${spawnModelRouting}
101
123
  ${Object.keys(effortPreferences).length ? `- explicit Astra effort preferences override role defaults, including later slices: ${JSON.stringify(effortPreferences)}` : ''}
102
- - use \`worker\` with gpt-6-astra and low reasoning for tiny short-context mechanical work such as simple search, typing, rename, copy, label, or one-line edits with no exploration or judgment
103
- - use gpt-6-astra with low reasoning for ordinary UI, logic, backend, and native implementation with established instructions
104
- - use gpt-6-astra with max reasoning for planning, analysis, review, focused unresolved, high-risk, architecture, security, database, research, release, or other explicit judgment slices
105
- - use gpt-6-astra with medium reasoning for long context/memory, large docs/repository reads or exploration, large-scale first-draft code processing, and direct Computer Use, Browser/Chrome, or image generation
106
- - explicit task class and phase win over incidental keywords: Astra Medium explores, Astra Low executes mechanical and instructed coding tasks, and Astra Max judges
107
- - in mass fan-out, use worker/Astra Low for tiny mechanical shards and explorer/Astra Medium for broad exploration; use Astra Low for instructed implementation and Astra Max for judgment
108
- - keep context, exploration, review, debugging, planning, and direct tool operation on their assigned defaults; preserve each sealed role model and effort instead of applying the parent profile to every child
124
+ ${childModelRules}
109
125
 
110
126
  Plan and capacity:
111
127
  - automatic fan-out is capacity-derived up to ${MAX_AUTOMATIC_SUBAGENT_COUNT}: after decomposition, use every safe useful child slot supported by the ready DAG, disjoint ownership, verifier/tool capacity, and actual host limits; the historical 4/6/8/16 task-class values are fallback hints, not clamps
@@ -141,6 +157,7 @@ ${renderSliceSafety(sliceSafety, parentDecompositionRequired)}
141
157
 
142
158
  Central TriWiki context:
143
159
  ${triwiki}
160
+ ${decisionContract}
144
161
 
145
162
  Project custom agent catalog:
146
163
  ${catalog}
@@ -209,7 +226,15 @@ function normalizedActiveMainModel(value) {
209
226
  const model = String(value?.model || '').trim();
210
227
  return provider && model ? { provider, model } : null;
211
228
  }
212
- function renderSpawnModelRouting() {
229
+ function renderSpawnModelRouting(narutoChildRouting) {
230
+ if (narutoChildRouting) {
231
+ return [
232
+ '- Naruto child models come from the spawn contract, including slices created after parent decomposition',
233
+ '- when Jev mode selected a role, that contract names gpt-5.6-luna at low, gpt-5.6-sol at low, gpt-5.6-terra at medium, or gpt-6-astra at max',
234
+ '- explicit user role-model preferences stay authoritative',
235
+ '- preserve the user-selected parent model, reasoning effort, and service tier'
236
+ ].join('\n');
237
+ }
213
238
  return [
214
239
  '- model routing applies to every child, including slices created after parent decomposition: gpt-6-astra only, with the selected role effort',
215
240
  '- use sealed Astra Low/Astra Medium/Astra Max role defaults across four task-class profiles; parent selection and saved non-Astra preferences never override the child model; explicit Astra effort preferences, including High, may override role defaults',
@@ -282,15 +307,33 @@ function renderBoundedTriwikiAttention(value) {
282
307
  id: anchor.id,
283
308
  claim_hash: anchor.claim_hash,
284
309
  source_hash: anchor.source_hash,
285
- hydrate_hint: anchor.hydrate_hint
310
+ hydrate_hint: anchor.hydrate_hint,
311
+ ...(anchor.source_path ? { source_path: anchor.source_path } : {}),
312
+ ...(anchor.excerpt ? { excerpt: anchor.excerpt } : {})
286
313
  }));
314
+ const hasExcerpt = value.anchors.some((anchor) => Boolean(anchor.excerpt));
287
315
  return [
288
316
  `- consume these ${anchors.length} attention.use_first anchors before broad discovery`,
289
317
  '- hydrate a referenced source only when its anchor is relevant to the assigned slice or a risky decision',
290
318
  '- do not inject the full context pack or make each subagent repeat repository-wide context discovery',
319
+ ...(hasExcerpt
320
+ ? ['- keep exact source text and provenance; do not summarize selected excerpts with another model']
321
+ : []),
291
322
  `- bounded anchors: ${JSON.stringify(anchors)}`
292
323
  ].join('\n');
293
324
  }
325
+ function renderDecisionContract(value) {
326
+ if (!value || value.executeSelectedIds !== true)
327
+ return '';
328
+ return [
329
+ 'Selected decision contract:',
330
+ `- execute the selected plan${value.planId ? ` ${value.planId}` : ''} and retained optional context IDs; do not choose them again`,
331
+ value.routingLane ? `- Jev sealed models: ${value.routingLane}` : '',
332
+ value.keepContextIds?.length
333
+ ? `- retained optional context IDs: ${value.keepContextIds.join(', ')}`
334
+ : '- pinned and retained context already unioned by code'
335
+ ].filter(Boolean).join('\n');
336
+ }
294
337
  function renderAgentCatalog(requested) {
295
338
  const names = [...new Set(requested.map(String).map((name) => name.trim()).filter(Boolean))];
296
339
  const selected = officialSubagentOnDemandRoleCatalog(names.length ? names : ['expert']);