sneakoscope 6.7.0 → 7.0.2

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 (40) hide show
  1. package/README.md +3 -3
  2. package/crates/sks-core/Cargo.lock +1 -1
  3. package/crates/sks-core/Cargo.toml +1 -1
  4. package/dist/cli/install-helpers-codex-lb-config.js +71 -0
  5. package/dist/cli/install-helpers-codex-lb-selftest.js +11 -14
  6. package/dist/cli/install-helpers.js +305 -53
  7. package/dist/config/skills-manifest.json +52 -52
  8. package/dist/core/agents/agent-orchestrator.js +56 -5
  9. package/dist/core/agents/agent-roster.js +4 -2
  10. package/dist/core/codex-app/codex-app-fast-ui-repair.js +18 -1
  11. package/dist/core/codex-app/codex-app-ui-state-snapshot.js +16 -0
  12. package/dist/core/codex-app.js +52 -6
  13. package/dist/core/commands/naruto-command.js +23 -5
  14. package/dist/core/hooks-runtime.js +77 -3
  15. package/dist/core/managed-assets/managed-assets-manifest.js +1 -1
  16. package/dist/core/mission.js +74 -4
  17. package/dist/core/release/package-size-budget.js +1 -1
  18. package/dist/core/subagents/naruto-proof-projection.js +2 -2
  19. package/dist/core/subagents/official-subagent-config.js +8 -5
  20. package/dist/core/subagents/official-subagent-preparation.js +24 -1
  21. package/dist/core/subagents/official-subagent-prompt.js +3 -2
  22. package/dist/core/subagents/official-subagent-runner.js +8 -1
  23. package/dist/core/subagents/wave-lifecycle.js +20 -0
  24. package/dist/core/subagents/wave-parent-guidance.js +42 -0
  25. package/dist/core/version.js +1 -1
  26. package/dist/native/sks-menubar/Sources/AlertFactory.swift +21 -2
  27. package/dist/native/sks-menubar/Sources/AppDelegate.swift +3 -0
  28. package/dist/native/sks-menubar/Sources/AppIdentity.swift +28 -1
  29. package/dist/native/sks-menubar/Sources/ControlCenterWindowController.swift +14 -8
  30. package/dist/native/sks-menubar/Sources/DiagnosticsViewController.swift +71 -7
  31. package/dist/native/sks-menubar/Sources/MCPServersViewController.swift +50 -9
  32. package/dist/native/sks-menubar/Sources/OverviewViewController.swift +61 -8
  33. package/dist/native/sks-menubar/Sources/ProvidersViewController.swift +169 -31
  34. package/dist/native/sks-menubar/Sources/RemoteTelegramViewController.swift +58 -23
  35. package/dist/native/sks-menubar/Sources/SettingsViewController.swift +41 -7
  36. package/dist/native/sks-menubar/Sources/StatusItemController.swift +34 -3
  37. package/dist/native/sks-menubar/Sources/UpdatesViewController.swift +38 -8
  38. package/dist/scripts/docs-truthfulness-check.js +2 -2
  39. package/dist/scripts/doctor-fixes-codex-app-fast-ui-check.js +7 -3
  40. package/package.json +1 -1
@@ -10,6 +10,8 @@ import { GLM_CODEX_CONFIG_PROVIDER_ID, GLM_CODEX_CONFIG_REASONING_PROFILES } fro
10
10
  import { GLM_52_OPENROUTER_MODEL } from './providers/glm/glm-52-settings.js';
11
11
  import { resolveOpenRouterApiKey } from './providers/openrouter/openrouter-secret-store.js';
12
12
  import { codexLbEnvPath, loadCodexLbEnv, parseShellEnvValue, readCodexLbModelCatalog } from './codex-lb/codex-lb-env.js';
13
+ import { codexLbToolCatalogPath, inspectCodexLbToolCatalog } from './codex-lb/codex-lb-tool-catalog.js';
14
+ import { isSksOwnedGlobalUiLock } from './codex-app/codex-app-ui-state-snapshot.js';
13
15
  import { redactString } from './secret-redaction.js';
14
16
  export const CODEX_APP_DOCS_URL = 'https://developers.openai.com/codex/app/features';
15
17
  export const CODEX_CHANGELOG_URL = 'https://developers.openai.com/codex/changelog';
@@ -865,9 +867,18 @@ async function codexFastModeConfigStatus(opts = {}) {
865
867
  for (const config of configs) {
866
868
  if (!config.text)
867
869
  continue;
868
- const topLevel = topLevelToml(config.text);
869
- if (/(^|\n)\s*model_reasoning_effort\s*=/.test(topLevel))
870
- blockers.push(`${config.scope}:top_level_model_reasoning_effort`);
870
+ const lines = String(config.text).split(/\r?\n/);
871
+ const topLevelEnd = lines.findIndex((line) => /^\s*\[/.test(line));
872
+ const topLevelBound = topLevelEnd === -1 ? lines.length : topLevelEnd;
873
+ for (let index = 0; index < topLevelBound; index += 1) {
874
+ const line = lines[index] || '';
875
+ if (!/^\s*model_reasoning_effort\s*=/.test(line))
876
+ continue;
877
+ // Preserve unmarked user reasoning choices. Only SKS-provenanced locks hide Fast UI.
878
+ if (config.scope === 'global' ? isSksOwnedGlobalUiLock(lines, index) : true) {
879
+ blockers.push(`${config.scope}:top_level_model_reasoning_effort`);
880
+ }
881
+ }
871
882
  if (/(^|\n)\s*fast_default_opt_out\s*=\s*true\s*(?:#.*)?(?=\n|$)/.test(tomlTable(config.text, 'notice')))
872
883
  blockers.push(`${config.scope}:fast_default_opt_out`);
873
884
  }
@@ -946,11 +957,39 @@ export async function codexProviderModelUiStatus(opts = {}) {
946
957
  && hasTomlBoolean(codexLbProvider, 'supports_websockets', true)
947
958
  && hasTomlBoolean(codexLbProvider, 'requires_openai_auth', true);
948
959
  const codexLbSelectedDefault = /(?:^|\n)\s*model_provider\s*=\s*"codex-lb"\s*(?:#.*)?(?=\n|$)/.test(topLevelToml(globalConfig));
949
- let codexLbModelCatalog = opts.codexLbModelCatalog || null;
950
- if (!codexLbModelCatalog && codexLbProviderContractOk && codexLbApiKeySource !== 'missing' && codexLbBaseUrlSource !== 'missing') {
960
+ const managedCatalogPath = codexLbToolCatalogPath(path.join(home || '', '.codex'));
961
+ const configuredCatalogPath = topLevelToml(globalConfig).match(/(?:^|\n)\s*model_catalog_json\s*=\s*"([^"]+)"\s*(?:#.*)?(?=\n|$)/)?.[1] || '';
962
+ const managedCatalogConfigured = Boolean(configuredCatalogPath)
963
+ && path.resolve(configuredCatalogPath) === path.resolve(managedCatalogPath);
964
+ let persistedCatalog = null;
965
+ if (!opts.codexLbModelCatalog && managedCatalogConfigured) {
966
+ persistedCatalog = await inspectCodexLbToolCatalog(managedCatalogPath).catch(() => null);
967
+ }
968
+ let liveCatalog = opts.codexLbModelCatalog || null;
969
+ if (!liveCatalog && !persistedCatalog?.ok && codexLbProviderContractOk && codexLbApiKeySource !== 'missing' && codexLbBaseUrlSource !== 'missing') {
951
970
  const loadedEnv = await loadCodexLbEnv({ home, envPath: codexLbEnvFilePath }).catch(() => null);
952
- codexLbModelCatalog = loadedEnv ? await readCodexLbModelCatalog({ loadedEnv }).catch(() => null) : null;
971
+ liveCatalog = loadedEnv ? await readCodexLbModelCatalog({ loadedEnv }).catch(() => null) : null;
953
972
  }
973
+ // Prefer the persisted Codex-owned catalog that Desktop actually loads. Live
974
+ // /models remains freshness telemetry and a fallback when no managed file exists.
975
+ const codexLbModelCatalog = opts.codexLbModelCatalog || (persistedCatalog?.ok
976
+ ? {
977
+ ok: true,
978
+ models: Array.isArray(persistedCatalog.gpt56_models) ? persistedCatalog.gpt56_models : [],
979
+ blockers: [],
980
+ source: 'persisted_model_catalog_json',
981
+ tools_transport: persistedCatalog.tools_transport || null
982
+ }
983
+ : liveCatalog
984
+ ? { ...liveCatalog, source: liveCatalog.source || 'live_models' }
985
+ : persistedCatalog
986
+ ? {
987
+ ok: false,
988
+ models: Array.isArray(persistedCatalog.gpt56_models) ? persistedCatalog.gpt56_models : [],
989
+ blockers: persistedCatalog.blockers || ['codex_lb_persisted_catalog_invalid'],
990
+ source: 'persisted_model_catalog_json'
991
+ }
992
+ : null);
954
993
  const expectedCodexLbModels = ['gpt-5.6-luna', 'gpt-5.6-terra', 'gpt-5.6-sol'];
955
994
  const catalogModels = Array.isArray(codexLbModelCatalog?.models) ? codexLbModelCatalog.models.map(String) : [];
956
995
  const expectedCodexLbModelsPresent = expectedCodexLbModels.every((model) => catalogModels.includes(model));
@@ -964,6 +1003,9 @@ export async function codexProviderModelUiStatus(opts = {}) {
964
1003
  ...(codexLbProviderPresent && !codexLbProviderContractOk ? ['codex_lb_provider_contract_drift'] : []),
965
1004
  ...(codexLbApiKeySource !== 'missing' ? [] : ['codex_lb_api_key_missing']),
966
1005
  ...(codexLbBaseUrlSource !== 'missing' ? [] : ['codex_lb_base_url_missing']),
1006
+ ...(codexLbSelectedDefault && !managedCatalogConfigured && !expectedCodexLbModelsPresent
1007
+ ? ['codex_lb_model_catalog_json_unselected']
1008
+ : []),
967
1009
  ...(codexLbProviderContractOk && codexLbApiKeySource !== 'missing' && codexLbBaseUrlSource !== 'missing' && !expectedCodexLbModelsPresent
968
1010
  ? ['codex_lb_gpt_5_6_catalog_unverified']
969
1011
  : [])
@@ -1022,9 +1064,13 @@ export async function codexProviderModelUiStatus(opts = {}) {
1022
1064
  base_url_source: codexLbBaseUrlSource,
1023
1065
  model_catalog_checked: Boolean(codexLbModelCatalog),
1024
1066
  model_catalog_ok: codexLbModelCatalog?.ok === true,
1067
+ model_catalog_source: codexLbModelCatalog?.source || null,
1068
+ model_catalog_json_configured: managedCatalogConfigured,
1069
+ model_catalog_json_path: managedCatalogConfigured ? managedCatalogPath : configuredCatalogPath || null,
1025
1070
  model_catalog_models: catalogModels,
1026
1071
  expected_models: expectedCodexLbModels,
1027
1072
  expected_models_present: expectedCodexLbModelsPresent,
1073
+ tools_transport: codexLbModelCatalog?.tools_transport || null,
1028
1074
  model_catalog_blockers: codexLbModelCatalog?.blockers || [],
1029
1075
  setup_command: setupCommand,
1030
1076
  set_key_command: setKeyCommand,
@@ -1,6 +1,6 @@
1
1
  import path from 'node:path';
2
2
  import { ui as cliUi } from '../../cli/cli-theme.js';
3
- import { createMission, findLatestMission, getOrCreateSessionMission, loadStateForSession, loadMission, sessionStateKey, setCurrent, updateCurrentIfMissionAndRun } from '../mission.js';
3
+ import { createMission, findLatestMission, getOrCreateExplicitNarutoMission, getOrCreateSessionMission, loadStateForSession, loadMission, sessionStateKey, setCurrent, updateCurrentIfMissionAndRun } from '../mission.js';
4
4
  import { closeWorkOrderLedgerForRouteResult, createAndWriteWorkOrderLedgerForPrompt } from '../work-order-ledger.js';
5
5
  import { appendJsonl, exists, nowIso, readJson, sksRoot, writeJsonAtomic } from '../fsx.js';
6
6
  import { SUBAGENT_EVENT_LOG_FILENAME, SUBAGENT_PARENT_SUMMARY_FILENAME, bindTrustworthySubagentParentSummaryToRun, persistOrReuseTrustworthySubagentParentSummary, readSubagentEvents, writeSubagentEvidence } from '../subagents/subagent-evidence.js';
@@ -54,6 +54,8 @@ async function narutoRun(parsed) {
54
54
  const mission = await resolveRunMission(root, parsed, sessionKey);
55
55
  if (!mission)
56
56
  return missingRunMission(parsed);
57
+ if (!mission.ok)
58
+ return blockedRunMission(parsed, mission.blockers);
57
59
  const admission = await withNarutoMissionRunAdmission({
58
60
  missionId: mission.id,
59
61
  missionDir: mission.dir,
@@ -70,6 +72,8 @@ async function narutoRunTransaction(parsed, root, appSession, sessionKey, resolv
70
72
  const mission = resolvedMission || await resolveRunMission(root, parsed, sessionKey);
71
73
  if (!mission)
72
74
  return missingRunMission(parsed);
75
+ if (!mission.ok)
76
+ return blockedRunMission(parsed, mission.blockers);
73
77
  const { id, dir } = mission;
74
78
  if (appSession && sessionKey) {
75
79
  const pending = await readPendingAppNarutoRun(root, { id, dir }, sessionKey, parsed.prompt);
@@ -442,8 +446,14 @@ function narutoHelp(parsed) {
442
446
  }
443
447
  async function resolveRunMission(root, parsed, sessionKey = null) {
444
448
  if (parsed.missionId && parsed.missionId !== 'latest') {
445
- const loaded = await loadMission(root, parsed.missionId).catch(() => null);
446
- return loaded ? { id: parsed.missionId, dir: loaded.dir } : null;
449
+ const resolved = await getOrCreateExplicitNarutoMission(root, {
450
+ requestedId: parsed.missionId,
451
+ prompt: parsed.prompt,
452
+ sessionKey
453
+ });
454
+ if (!resolved.ok)
455
+ return { ok: false, blockers: resolved.blockers };
456
+ return { ok: true, id: resolved.id, dir: resolved.dir };
447
457
  }
448
458
  if (sessionKey) {
449
459
  const resolved = await getOrCreateSessionMission(root, {
@@ -459,10 +469,10 @@ async function resolveRunMission(root, parsed, sessionKey = null) {
459
469
  : null;
460
470
  }
461
471
  });
462
- return { id: resolved.id, dir: resolved.dir };
472
+ return { ok: true, id: String(resolved.id), dir: String(resolved.dir) };
463
473
  }
464
474
  const created = await createMission(root, { mode: 'naruto', prompt: parsed.prompt, sessionKey });
465
- return { id: created.id, dir: created.dir };
475
+ return { ok: true, id: String(created.id), dir: String(created.dir) };
466
476
  }
467
477
  async function resolveReadMission(parsed) {
468
478
  const root = await sksRoot();
@@ -696,6 +706,14 @@ function missingRunMission(parsed) {
696
706
  blockers: [`naruto_mission_not_found:${parsed.missionId}`]
697
707
  }, () => console.error(`Naruto mission not found: ${parsed.missionId}`), true);
698
708
  }
709
+ function blockedRunMission(parsed, blockers) {
710
+ return emit(parsed, {
711
+ schema: NARUTO_RESULT_SCHEMA,
712
+ ok: false,
713
+ status: 'blocked',
714
+ blockers
715
+ }, () => console.error(`Naruto mission blocked: ${blockers.join(', ')}`), true);
716
+ }
699
717
  function emit(parsed, result, human, failed = false) {
700
718
  if (failed)
701
719
  process.exitCode = 1;
@@ -34,6 +34,8 @@ import { recordOfficialSubagentParentOutcomesTelemetry, recordOfficialSubagentZe
34
34
  import { bindTrustworthySubagentParentSummaryToRun, normalizeSubagentEvent, normalizeSubagentParentSummary, persistOrReuseTrustworthySubagentParentSummary, readSubagentEvents, recordSubagentEvent, SUBAGENT_EVIDENCE_FILENAME, SUBAGENT_PARENT_SUMMARY_FILENAME, writeSubagentEvidence } from './subagents/subagent-evidence.js';
35
35
  import { officialSubagentPreparationInProgress, withOfficialSubagentLifecycleLock, writeNarutoGate } from './subagents/official-subagent-preparation.js';
36
36
  import { effectiveSubagentTarget, normalizeLegacySubagentCountFields, refreshSubagentWaveLifecycle, subagentCountContractBlockers } from './subagents/wave-lifecycle.js';
37
+ import { buildWaveParentGuidance, renderWaveParentGuidance } from './subagents/wave-parent-guidance.js';
38
+ import { managedOfficialSubagentRoleByName } from './managed-assets/managed-assets-manifest.js';
37
39
  import { SSOT_GUARD_ARTIFACT, validateSsotGuardArtifact } from './safety/ssot-guard.js';
38
40
  const LIGHT_ROUTE_STOP_ARTIFACT = 'light-route-stop.json';
39
41
  const CODEX_GIT_ACTION_STOP_ARTIFACT = 'codex-git-action-stop-bypass.json';
@@ -134,16 +136,60 @@ async function hookSubagentStart(root, state, payload = {}, sessionKey = null) {
134
136
  const config = await readOfficialSubagentConfig(root);
135
137
  const budget = resolveSubagentThreadBudget({ configuredMaxThreads: config.maxThreads });
136
138
  const active = subagentRouteContext(state);
139
+ const routingContext = await sealedSubagentRoutingContext(artifactDir, payload);
137
140
  const resourceGuard = [
138
141
  `SKS subagent policy: Codex [agents].max_threads is ${budget.maxThreads}.`,
139
142
  'Use max_depth=1. Subagents must not spawn subagents.',
140
143
  'Do not duplicate an already assigned slice.',
141
144
  'Parallel writes require disjoint paths; serialize overlapping paths.',
142
- 'Close completed agent threads when no longer needed.'
145
+ 'Finish only your assigned slice, return a concise result, then stop so the root parent can close this thread.'
143
146
  ].join(' ');
144
- const additionalContext = [coreEngineeringDirectiveReferenceText(), resourceGuard, active].filter(Boolean).join('\n\n');
147
+ const additionalContext = [coreEngineeringDirectiveReferenceText(), resourceGuard, routingContext, active].filter(Boolean).join('\n\n');
145
148
  return { continue: true, additionalContext };
146
149
  }
150
+ async function sealedSubagentRoutingContext(artifactDir, payload = {}) {
151
+ const plan = await readJson(path.join(artifactDir, 'subagent-plan.json'), null).catch(() => null);
152
+ if (!plan || plan.workflow !== 'official_codex_subagent')
153
+ return '';
154
+ const agentName = extractSubagentAgentName(payload);
155
+ const agents = plan.agents && typeof plan.agents === 'object' ? plan.agents : {};
156
+ const planned = agentName && agents[agentName] ? agents[agentName] : null;
157
+ const role = agentName ? managedOfficialSubagentRoleByName(agentName) : null;
158
+ const model = String(planned?.routed_model || planned?.model || role?.model || '').trim();
159
+ const effort = String(planned?.routed_model_reasoning_effort || planned?.model_reasoning_effort || role?.model_reasoning_effort || '').trim();
160
+ if (!agentName && !model)
161
+ return '';
162
+ return [
163
+ 'SKS sealed child routing:',
164
+ agentName ? `- custom agent: ${agentName}` : null,
165
+ model ? `- model: ${model}` : null,
166
+ effort ? `- model_reasoning_effort: ${effort}` : null,
167
+ '- keep this sealed profile; do not retarget model/effort or spawn nested agents'
168
+ ].filter(Boolean).join('\n');
169
+ }
170
+ function extractSubagentAgentName(payload = {}) {
171
+ const candidates = [
172
+ payload.agent_type,
173
+ payload.agentType,
174
+ payload.subagent_type,
175
+ payload.subagentType,
176
+ payload.agent_name,
177
+ payload.agentName,
178
+ payload.agent,
179
+ payload.role,
180
+ payload.payload?.agent_type,
181
+ payload.payload?.agentType,
182
+ payload.payload?.subagent_type,
183
+ payload.data?.agent_type,
184
+ payload.input?.agent_type
185
+ ];
186
+ for (const value of candidates) {
187
+ const name = String(value || '').trim();
188
+ if (name)
189
+ return name;
190
+ }
191
+ return '';
192
+ }
147
193
  function subagentRouteContext(state = {}) {
148
194
  if (!state?.route && !state?.mode)
149
195
  return '';
@@ -486,7 +532,6 @@ function activeGoalOverlayContext(state = {}, route = null) {
486
532
  ].join('\n');
487
533
  }
488
534
  async function hookPreTool(root, state, payload, noQuestion, sessionKey = null) {
489
- void sessionKey;
490
535
  if (needsMutationSafetyCheck(payload)) {
491
536
  const madSksImmutableDecision = await checkMadSksImmutableModification(root, state, payload);
492
537
  if (madSksImmutableDecision.action === 'block') {
@@ -510,8 +555,37 @@ async function hookPreTool(root, state, payload, noQuestion, sessionKey = null)
510
555
  return agentRecursionDecision;
511
556
  if (noQuestion && looksInteractiveCommand(command))
512
557
  return { decision: 'block', reason: interactiveCommandReason(command) };
558
+ const waveGuidance = await parentWaveGuidanceContext(root, state, sessionKey).catch(() => '');
559
+ if (waveGuidance) {
560
+ return {
561
+ continue: true,
562
+ additionalContext: waveGuidance,
563
+ systemMessage: visibleHookMessage('pre-tool', 'SKS Naruto wave lifecycle requires root-parent follow-up.')
564
+ };
565
+ }
513
566
  return { continue: true };
514
567
  }
568
+ async function parentWaveGuidanceContext(root, state = {}, sessionKey = null) {
569
+ if (!state?.mission_id && !state?.official_subagent_run_id)
570
+ return '';
571
+ const isNaruto = String(state?.mode || '').toUpperCase() === 'NARUTO'
572
+ || String(state?.route || state?.route_command || '').replace(/^\$/, '').toUpperCase() === 'NARUTO'
573
+ || state?.subagents_required === true;
574
+ if (!isNaruto)
575
+ return '';
576
+ // Child worker hooks must not receive parent wave spawn instructions.
577
+ if (agentWorkerHookContext(state, {}))
578
+ return '';
579
+ const artifactDir = officialSubagentArtifactDir(root, state, sessionKey);
580
+ const plan = await readJson(path.join(artifactDir, 'subagent-plan.json'), null).catch(() => null);
581
+ if (!plan || plan.workflow !== 'official_codex_subagent')
582
+ return '';
583
+ const lifecycle = plan.wave_lifecycle || null;
584
+ const guidance = lifecycle?.parent_guidance || buildWaveParentGuidance(lifecycle);
585
+ if (!guidance?.required)
586
+ return '';
587
+ return renderWaveParentGuidance(guidance);
588
+ }
515
589
  function agentWorkerHookRecursionDecision(state = {}, payload = {}, command = '') {
516
590
  if (!agentWorkerHookContext(state, payload))
517
591
  return null;
@@ -1,7 +1,7 @@
1
1
  import { sha256 } from '../fsx.js';
2
2
  import { subagentModelProfile } from '../subagents/model-policy.js';
3
3
  export const MANAGED_ASSET_SCHEMA_VERSION = 1;
4
- export const MANAGED_ASSET_VERSION = '6.7.0';
4
+ export const MANAGED_ASSET_VERSION = '7.0.2';
5
5
  export const MANAGED_ASSET_MARKER = 'SKS-MANAGED-ASSET';
6
6
  export const MANAGED_OFFICIAL_SUBAGENT_MARKER = 'SKS-MANAGED-OFFICIAL-SUBAGENT';
7
7
  /** Internal cleanup tombstones for SKS-owned role files retired from the installed catalog. */
@@ -21,11 +21,81 @@ export function sessionStateKey(sessionKey = 'default') {
21
21
  export function stateFileForSession(root, sessionKey) {
22
22
  return path.join(stateSessionsDir(root), `${sessionStateKey(sessionKey)}.json`);
23
23
  }
24
- export async function createMission(root, { mode, prompt, sessionKey }) {
25
- return withStateLock(root, () => createMissionUnlocked(root, { mode, prompt, sessionKey }));
24
+ export function validateExternallyReservedMissionId(rawId) {
25
+ const id = String(rawId ?? '');
26
+ if (!id.startsWith('M-'))
27
+ return { ok: false, reason: 'missing_m_prefix' };
28
+ if (id.length < 3 || id.length > 160)
29
+ return { ok: false, reason: 'length' };
30
+ if (id.trim() !== id)
31
+ return { ok: false, reason: 'normalization' };
32
+ if (id.includes('/') || id.includes('\\') || id.includes('\0') || /\s/.test(id) || id.includes('..')) {
33
+ return { ok: false, reason: 'path' };
34
+ }
35
+ if (!/^[A-Za-z0-9._-]+$/.test(id))
36
+ return { ok: false, reason: 'charset' };
37
+ return { ok: true, id };
38
+ }
39
+ export async function createMission(root, { mode, prompt, sessionKey, requestedId }) {
40
+ return withStateLock(root, () => createMissionUnlocked(root, { mode, prompt, sessionKey, requestedId }));
41
+ }
42
+ export async function getOrCreateExplicitNarutoMission(root, input) {
43
+ const validated = validateExternallyReservedMissionId(input.requestedId);
44
+ if (!validated.ok) {
45
+ return { ok: false, blockers: [`naruto_mission_id_invalid:${validated.reason}`] };
46
+ }
47
+ const normalizedPrompt = String(input.prompt || '').trim();
48
+ return withStateLock(root, async () => {
49
+ let loaded = await loadMission(root, validated.id).catch(() => null);
50
+ if (!loaded) {
51
+ try {
52
+ const created = await createMissionUnlocked(root, {
53
+ mode: 'naruto',
54
+ prompt: normalizedPrompt,
55
+ sessionKey: input.sessionKey,
56
+ requestedId: validated.id
57
+ });
58
+ return { ok: true, id: String(created.id), dir: String(created.dir) };
59
+ }
60
+ catch (error) {
61
+ if (error?.code !== 'mission_already_exists')
62
+ throw error;
63
+ loaded = await loadMission(root, validated.id).catch(() => null);
64
+ if (!loaded) {
65
+ return { ok: false, blockers: [`naruto_mission_not_found:${validated.id}`] };
66
+ }
67
+ }
68
+ }
69
+ if (String(loaded.mission?.mode || '') !== 'naruto') {
70
+ return { ok: false, blockers: ['naruto_mission_identity_conflict:mission_mode'] };
71
+ }
72
+ if (String(loaded.mission?.prompt || '').trim() !== normalizedPrompt) {
73
+ return { ok: false, blockers: ['naruto_mission_identity_conflict:mission_prompt'] };
74
+ }
75
+ return { ok: true, id: String(loaded.id), dir: String(loaded.dir) };
76
+ });
26
77
  }
27
- async function createMissionUnlocked(root, { mode, prompt, sessionKey }) {
28
- const id = missionId();
78
+ async function createMissionUnlocked(root, { mode, prompt, sessionKey, requestedId }) {
79
+ let id;
80
+ if (requestedId != null && String(requestedId).length > 0) {
81
+ const validated = validateExternallyReservedMissionId(requestedId);
82
+ if (!validated.ok) {
83
+ const err = new Error(`invalid_mission_id:${validated.reason}`);
84
+ err.code = 'invalid_mission_id';
85
+ err.reason = validated.reason;
86
+ throw err;
87
+ }
88
+ id = validated.id;
89
+ if (await exists(path.join(missionDir(root, id), 'mission.json'))) {
90
+ const err = new Error('mission_already_exists');
91
+ err.code = 'mission_already_exists';
92
+ err.id = id;
93
+ throw err;
94
+ }
95
+ }
96
+ else {
97
+ id = missionId();
98
+ }
29
99
  const dir = missionDir(root, id);
30
100
  await ensureDir(dir);
31
101
  await ensureDir(path.join(dir, 'bus'));
@@ -1,2 +1,2 @@
1
- export const DEFAULT_MAX_PACK_BYTES = 2448 * 1024;
1
+ export const DEFAULT_MAX_PACK_BYTES = 2464 * 1024;
2
2
  export const DEFAULT_MAX_UNPACKED_BYTES = 11_141_120;
@@ -162,12 +162,12 @@ export function projectNarutoProofSnapshot(input) {
162
162
  mission_id: missionId,
163
163
  workflow: 'official_codex_subagent',
164
164
  workflow_run_id: workflowRunId,
165
+ blockers: uniqueBlockers,
165
166
  evidence: persistedEvidence,
166
167
  summary,
167
168
  gate,
168
169
  result,
169
- proof_fingerprint: proofFingerprint,
170
- ...(uniqueBlockers.length > 0 ? { blockers: uniqueBlockers } : {})
170
+ proof_fingerprint: proofFingerprint
171
171
  };
172
172
  }
173
173
  export function validateNarutoProofStatus(value) {
@@ -58,17 +58,20 @@ export async function readOfficialSubagentConfig(root, opts = {}) {
58
58
  const maxDepth = resolveLayeredValue(projectLayer.agents.max_depth, globalLayer.agents.max_depth, DEFAULT_OFFICIAL_SUBAGENT_MAX_DEPTH, positiveInteger);
59
59
  const jobMaxRuntimeSeconds = resolveLayeredValue(projectLayer.agents.job_max_runtime_seconds, globalLayer.agents.job_max_runtime_seconds, DEFAULT_OFFICIAL_SUBAGENT_JOB_MAX_RUNTIME_SECONDS, positiveInteger);
60
60
  const interruptMessage = resolveLayeredValue(projectLayer.agents.interrupt_message, globalLayer.agents.interrupt_message, DEFAULT_OFFICIAL_SUBAGENT_INTERRUPT_MESSAGE, booleanValue);
61
- const warnings = maxDepth.value > 1
62
- ? [`official_subagent_max_depth_above_one_preserved:${maxDepth.value}:${maxDepth.source}`]
61
+ // Official Naruto admits only depth=1. Coerce higher values so they cannot
62
+ // contradict the launcher hard-enforcement or look "supported".
63
+ const depthCoerced = maxDepth.value > 1;
64
+ const warnings = depthCoerced
65
+ ? [`official_subagent_max_depth_coerced_to_one:${maxDepth.value}:${maxDepth.source}`]
63
66
  : [];
64
67
  return {
65
68
  maxThreads: maxThreads.value,
66
- maxDepth: maxDepth.value,
69
+ maxDepth: depthCoerced ? DEFAULT_OFFICIAL_SUBAGENT_MAX_DEPTH : maxDepth.value,
67
70
  jobMaxRuntimeSeconds: jobMaxRuntimeSeconds.value,
68
71
  interruptMessage: interruptMessage.value,
69
72
  sources: {
70
73
  maxThreads: maxThreads.source,
71
- maxDepth: maxDepth.source,
74
+ maxDepth: depthCoerced ? 'default' : maxDepth.source,
72
75
  jobMaxRuntimeSeconds: jobMaxRuntimeSeconds.source,
73
76
  interruptMessage: interruptMessage.source
74
77
  },
@@ -85,7 +88,7 @@ export function officialSubagentConfigWarnings(text = '', inheritedText = '') {
85
88
  return [];
86
89
  const maxDepth = resolveLayeredValue(project.agents.max_depth, inherited.agents.max_depth, DEFAULT_OFFICIAL_SUBAGENT_MAX_DEPTH, positiveInteger);
87
90
  return maxDepth.value > 1
88
- ? [`official_subagent_max_depth_above_one_preserved:${maxDepth.value}:${maxDepth.source}`]
91
+ ? [`official_subagent_max_depth_coerced_to_one:${maxDepth.value}:${maxDepth.source}`]
89
92
  : [];
90
93
  }
91
94
  export async function readInheritedOfficialSubagentConfigText(projectConfigPath, opts = {}) {
@@ -10,6 +10,7 @@ import { NARUTO_PARENT_EFFORT, NARUTO_PARENT_MODEL } from './model-policy.js';
10
10
  import { MAX_ON_DEMAND_SUBAGENT_ROLE_COUNT, officialSubagentFanoutPolicy, officialSubagentOnDemandRolePlan, officialSubagentRoleCatalog, recommendOfficialSubagentRoles } from './agent-catalog.js';
11
11
  import { resolveSubagentThreadBudget } from './thread-budget.js';
12
12
  import { createSubagentWaveLifecycle } from './wave-lifecycle.js';
13
+ import { decideOfficialSubagentModel } from '../agents/agent-effort-policy.js';
13
14
  import { readBoundedTriwikiAttention } from './triwiki-attention.js';
14
15
  import { SUBAGENT_EVIDENCE_FILENAME, SUBAGENT_EVENT_LOG_FILENAME, SUBAGENT_PARENT_SUMMARY_FILENAME, normalizeSubagentParentSummary, writeSubagentEvidence } from './subagent-evidence.js';
15
16
  import { sksPrefixedDollarCommand, unprefixedSksSkillName } from '../routes/dollar-prefix.js';
@@ -113,6 +114,28 @@ async function prepareOfficialSubagentMissionLocked(input) {
113
114
  recommendedAgents: suggestedAgents
114
115
  });
115
116
  const selectedAgentPlan = officialSubagentOnDemandRolePlan(suggestedAgents);
117
+ const agentRouting = Object.fromEntries(Object.entries(selectedAgentPlan).map(([name, config]) => {
118
+ const decision = decideOfficialSubagentModel({
119
+ persona: {
120
+ role: name,
121
+ naruto_role: name,
122
+ write_policy: config.sandbox_mode === 'read-only' ? 'read-only' : 'route-local-artifact',
123
+ read_only: config.sandbox_mode === 'read-only'
124
+ },
125
+ prompt: goal,
126
+ agentId: name,
127
+ readonly: config.sandbox_mode === 'read-only'
128
+ });
129
+ return [name, {
130
+ ...config,
131
+ // Catalog TOML remains the spawn-type contract; dynamic decision records why
132
+ // this role maps onto the sealed four-profile matrix for this goal.
133
+ routed_model: decision.model,
134
+ routed_model_reasoning_effort: decision.model_reasoning_effort,
135
+ routed_model_policy: decision.model_selection_reason,
136
+ routing_dynamic: true
137
+ }];
138
+ }));
116
139
  const agentCatalog = onDemandAgentCatalogMetadata(selectedAgentPlan);
117
140
  const ssotGuard = buildSsotGuard({ route: input.route, mode: mode === 'naruto' ? 'NARUTO' : 'OFFICIAL_SUBAGENT', task: goal });
118
141
  const ssotGuardValidation = validateSsotGuardArtifact(ssotGuard);
@@ -164,7 +187,7 @@ async function prepareOfficialSubagentMissionLocked(input) {
164
187
  model_reasoning_effort: NARUTO_PARENT_EFFORT
165
188
  },
166
189
  agent_catalog: agentCatalog,
167
- agents: selectedAgentPlan,
190
+ agents: agentRouting,
168
191
  verification_budget: verification,
169
192
  verification_checks: [],
170
193
  verification: { budget: verification },
@@ -88,11 +88,12 @@ Subagent rules:
88
88
  - if paths overlap, run those slices serially
89
89
  - reject duplicate slice fingerprints and homogeneous clone work; diversity may come from roles, disjoint shards, or different tool surfaces
90
90
  - security, database, release, authorization, and irreversible-effect checks are protected strata; aggregate speed or accuracy never offsets a failed protected gate
91
- - after each settled wave: collect results, close completed threads, refresh evidence and the wave lifecycle ledger, rescan the ready DAG, then launch the next defensible direct-child wave when useful work remains
91
+ - after each settled wave: collect results, close completed threads, refresh evidence and the wave lifecycle ledger, read \`wave_lifecycle.next_parent_actions\` / \`parent_guidance\`, rescan the ready DAG, then launch the next defensible direct-child wave when \`remaining_to_start > 0\`
92
92
  - recovered thread capacity is reusable by later root-owned waves; completed child threads do not permanently consume the mission fan-out budget
93
+ - when PreTool/UserPrompt guidance says \`spawn_next_direct_child_wave_upto:N\`, spawn that next wave immediately with sealed custom-agent model/effort profiles; do not wait for another user message
93
94
  - automatic targets may resize between waves when the ready DAG changes, but update plan/evidence before spawning; explicit operator and route-owned counts remain exact
94
95
  - wait for every final planned subagent before integrating
95
- - close completed threads after collecting results
96
+ - close completed threads after collecting results so capacity returns to the root parent
96
97
  ${parentDecompositionRequired ? `- decomposition status: parent_required
97
98
  - before spawning, decompose the goal into independent, non-overlapping slices
98
99
  - do not invent write scopes merely to reach the requested count
@@ -39,7 +39,14 @@ const OFFICIAL_SUBAGENT_CHILD_ENV_ALLOWLIST = Object.freeze([
39
39
  'CODEX_CI',
40
40
  'CODEX_MANAGED_BY_NPM',
41
41
  'CODEX_INTERNAL_ORIGINATOR',
42
- 'CODEX_SANDBOX_NETWORK_DISABLED'
42
+ 'CODEX_SANDBOX_NETWORK_DISABLED',
43
+ 'SKS_AGENT_MODE',
44
+ 'ACAS_AGENT_SLUG',
45
+ 'ACAS_AGENT_WORKSPACE',
46
+ 'ALFREDO_AGENT_SOULS_FILE',
47
+ 'ACAS_CHROME_PATH',
48
+ 'ACAS_HTML_TO_PDF_ENGINE',
49
+ 'ACAS_HTML_TO_PDF_ALLOW_CHROME_CLI_FALLBACK'
43
50
  ]);
44
51
  const SECRET_ENV_KEY_RE = /(?:api[_-]?key|token|secret|password|credential|authorization|cookie)/i;
45
52
  const SECRET_BEARING_ENV_KEYS = new Set([
@@ -2,6 +2,7 @@ import path from 'node:path';
2
2
  import { nowIso, readJson, writeJsonAtomic } from '../fsx.js';
3
3
  import { readSubagentEvents } from './subagent-evidence.js';
4
4
  import { MAX_AUTOMATIC_SUBAGENT_COUNT } from './agent-catalog.js';
5
+ import { buildWaveParentGuidance } from './wave-parent-guidance.js';
5
6
  export const SUBAGENT_WAVE_LIFECYCLE_SCHEMA = 'sks.subagent-wave-lifecycle.v1';
6
7
  export function subagentCountPolicy(plan) {
7
8
  const lifecycle = plan?.wave_lifecycle;
@@ -89,6 +90,15 @@ export function createSubagentWaveLifecycle(input) {
89
90
  remaining_to_start: normalizeCount(input.targetSubagents),
90
91
  post_wave_rescan_required: false,
91
92
  recovered_capacity: 0,
93
+ next_parent_actions: [],
94
+ parent_guidance: buildWaveParentGuidance({
95
+ remaining_to_start: normalizeCount(input.targetSubagents),
96
+ open_threads: 0,
97
+ recovered_capacity: 0,
98
+ post_wave_rescan_required: false,
99
+ current_wave: 0,
100
+ completed_waves: 0
101
+ }),
92
102
  waves: [],
93
103
  last_event: null,
94
104
  updated_at: nowIso()
@@ -188,6 +198,14 @@ function projectLifecycle(previous, input) {
188
198
  const postWaveRescanRequired = Boolean(lastWave?.status === 'settled'
189
199
  && openThreads === 0
190
200
  && remainingToStart > 0);
201
+ const parentGuidance = buildWaveParentGuidance({
202
+ remaining_to_start: remainingToStart,
203
+ open_threads: openThreads,
204
+ recovered_capacity: cumulativeSettled,
205
+ post_wave_rescan_required: postWaveRescanRequired,
206
+ current_wave: lastWave?.wave || 0,
207
+ completed_waves: waves.filter((wave) => wave.status === 'settled').length
208
+ });
191
209
  return {
192
210
  ...previous,
193
211
  workflow_run_id: input.workflowRunId,
@@ -205,6 +223,8 @@ function projectLifecycle(previous, input) {
205
223
  remaining_to_start: remainingToStart,
206
224
  post_wave_rescan_required: postWaveRescanRequired,
207
225
  recovered_capacity: cumulativeSettled,
226
+ next_parent_actions: parentGuidance.actions,
227
+ parent_guidance: parentGuidance,
208
228
  waves,
209
229
  last_event: input.lastEvent,
210
230
  updated_at: nowIso()
@@ -0,0 +1,42 @@
1
+ export const WAVE_PARENT_GUIDANCE_SCHEMA = 'sks.subagent-wave-parent-guidance.v1';
2
+ export function buildWaveParentGuidance(lifecycle) {
3
+ const remaining = Math.max(0, Math.floor(Number(lifecycle?.remaining_to_start || 0)));
4
+ const openThreads = Math.max(0, Math.floor(Number(lifecycle?.open_threads || 0)));
5
+ const recovered = Math.max(0, Math.floor(Number(lifecycle?.recovered_capacity || 0)));
6
+ const rescan = lifecycle?.post_wave_rescan_required === true;
7
+ const actions = [];
8
+ if (openThreads > 0) {
9
+ actions.push('close_completed_child_threads_after_collecting_results');
10
+ }
11
+ if (rescan || remaining > 0) {
12
+ actions.push('refresh_wave_lifecycle_and_ready_dag');
13
+ actions.push(`spawn_next_direct_child_wave_upto:${Math.max(1, Math.min(remaining || recovered, recovered || remaining || 1))}`);
14
+ }
15
+ if (remaining === 0 && openThreads === 0 && !rescan) {
16
+ actions.push('integrate_settled_child_results_and_emit_parent_summary');
17
+ }
18
+ return {
19
+ schema: WAVE_PARENT_GUIDANCE_SCHEMA,
20
+ required: actions.some((action) => action.startsWith('spawn_next_') || action.startsWith('close_')),
21
+ actions: [...new Set(actions)],
22
+ remaining_to_start: remaining,
23
+ open_threads: openThreads,
24
+ recovered_capacity: recovered,
25
+ post_wave_rescan_required: rescan,
26
+ current_wave: Math.max(0, Math.floor(Number(lifecycle?.current_wave || 0))),
27
+ completed_waves: Math.max(0, Math.floor(Number(lifecycle?.completed_waves || 0)))
28
+ };
29
+ }
30
+ export function renderWaveParentGuidance(guidance) {
31
+ if (!guidance.required && guidance.actions.length === 0)
32
+ return '';
33
+ return [
34
+ 'SKS Naruto wave lifecycle (root parent only):',
35
+ `- current_wave=${guidance.current_wave}; completed_waves=${guidance.completed_waves}`,
36
+ `- open_threads=${guidance.open_threads}; remaining_to_start=${guidance.remaining_to_start}; recovered_capacity=${guidance.recovered_capacity}`,
37
+ `- post_wave_rescan_required=${guidance.post_wave_rescan_required}`,
38
+ ...guidance.actions.map((action) => `- action: ${action}`),
39
+ '- max_depth=1: children must not spawn children; only this root may launch later direct-child waves',
40
+ '- after collecting results, close completed child threads so recovered capacity can be reused'
41
+ ].join('\n');
42
+ }
@@ -1 +1 @@
1
- export const PACKAGE_VERSION = '6.7.0';
1
+ export const PACKAGE_VERSION = '7.0.2';