sneakoscope 6.7.0 → 7.0.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 (34) 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/hooks-runtime.js +77 -3
  14. package/dist/core/managed-assets/managed-assets-manifest.js +1 -1
  15. package/dist/core/release/package-size-budget.js +1 -1
  16. package/dist/core/subagents/official-subagent-config.js +8 -5
  17. package/dist/core/subagents/official-subagent-preparation.js +24 -1
  18. package/dist/core/subagents/official-subagent-prompt.js +3 -2
  19. package/dist/core/subagents/wave-lifecycle.js +20 -0
  20. package/dist/core/subagents/wave-parent-guidance.js +42 -0
  21. package/dist/core/version.js +1 -1
  22. package/dist/native/sks-menubar/Sources/AppIdentity.swift +3 -1
  23. package/dist/native/sks-menubar/Sources/ControlCenterWindowController.swift +14 -8
  24. package/dist/native/sks-menubar/Sources/DiagnosticsViewController.swift +71 -7
  25. package/dist/native/sks-menubar/Sources/MCPServersViewController.swift +50 -9
  26. package/dist/native/sks-menubar/Sources/OverviewViewController.swift +61 -8
  27. package/dist/native/sks-menubar/Sources/ProvidersViewController.swift +120 -29
  28. package/dist/native/sks-menubar/Sources/RemoteTelegramViewController.swift +58 -23
  29. package/dist/native/sks-menubar/Sources/SettingsViewController.swift +41 -7
  30. package/dist/native/sks-menubar/Sources/StatusItemController.swift +34 -3
  31. package/dist/native/sks-menubar/Sources/UpdatesViewController.swift +38 -8
  32. package/dist/scripts/docs-truthfulness-check.js +2 -2
  33. package/dist/scripts/doctor-fixes-codex-app-fast-ui-check.js +7 -3
  34. 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,
@@ -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.0';
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. */
@@ -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;
@@ -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
@@ -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.0';
@@ -13,7 +13,9 @@ enum AppIdentity {
13
13
  }
14
14
 
15
15
  static func statusImage(resource: String, symbol: String) -> NSImage? {
16
- let image = NSImage(systemSymbolName: symbol, accessibilityDescription: "SKS status") ?? Bundle.main.image(forResource: resource)
16
+ // Prefer the validated custom status PDFs; fall back to SF Symbols only when missing.
17
+ let image = Bundle.main.image(forResource: resource)
18
+ ?? NSImage(systemSymbolName: symbol, accessibilityDescription: "SKS status")
17
19
  image?.isTemplate = true
18
20
  return image
19
21
  }
@@ -6,6 +6,7 @@ final class ControlCenterWindowController: NSWindowController, NSTableViewDataSo
6
6
  private let controllers: [SidebarItem: NSViewController]
7
7
  private let overviewController: OverviewViewController
8
8
  private var selected: SidebarItem = .overview
9
+ private var hasPresented = false
9
10
 
10
11
  init(processClient: ProcessClient, operations: OperationCoordinator, notifications: NotificationCoordinator) {
11
12
  let overview = OverviewViewController(processClient: processClient, operations: operations)
@@ -14,9 +15,9 @@ final class ControlCenterWindowController: NSWindowController, NSTableViewDataSo
14
15
  .overview: overview,
15
16
  .updates: UpdatesViewController(processClient: processClient, operations: operations, notifications: notifications),
16
17
  .mcpServers: MCPServersViewController(processClient: processClient, operations: operations, notifications: notifications),
17
- .providers: ProvidersViewController(processClient: processClient),
18
+ .providers: ProvidersViewController(processClient: processClient, operations: operations),
18
19
  .remoteTelegram: RemoteTelegramViewController(processClient: processClient),
19
- .diagnostics: DiagnosticsViewController(processClient: processClient),
20
+ .diagnostics: DiagnosticsViewController(processClient: processClient, operations: operations),
20
21
  .settings: SettingsViewController(notifications: notifications)
21
22
  ]
22
23
  let window = NSWindow(
@@ -38,7 +39,10 @@ final class ControlCenterWindowController: NSWindowController, NSTableViewDataSo
38
39
  if let row = SidebarItem.allCases.firstIndex(of: section) { sidebar.selectRowIndexes(IndexSet(integer: row), byExtendingSelection: false) }
39
40
  display(section)
40
41
  NSApp.activate(ignoringOtherApps: true)
41
- window?.center()
42
+ if !hasPresented {
43
+ window?.center()
44
+ hasPresented = true
45
+ }
42
46
  showWindow(nil)
43
47
  window?.makeKeyAndOrderFront(nil)
44
48
  }
@@ -97,12 +101,14 @@ final class ControlCenterWindowController: NSWindowController, NSTableViewDataSo
97
101
  private func display(_ section: SidebarItem) {
98
102
  contentHost.subviews.forEach { $0.removeFromSuperview() }
99
103
  guard let controller = controllers[section] else { return }
100
- let view = controller.view
101
- view.translatesAutoresizingMaskIntoConstraints = false
102
- contentHost.addSubview(view)
104
+ let page = NativeView.scrollable(controller.view)
105
+ contentHost.addSubview(page)
103
106
  NSLayoutConstraint.activate([
104
- view.leadingAnchor.constraint(equalTo: contentHost.leadingAnchor), view.trailingAnchor.constraint(equalTo: contentHost.trailingAnchor),
105
- view.topAnchor.constraint(equalTo: contentHost.topAnchor), view.bottomAnchor.constraint(equalTo: contentHost.bottomAnchor)
107
+ page.leadingAnchor.constraint(equalTo: contentHost.leadingAnchor),
108
+ page.trailingAnchor.constraint(equalTo: contentHost.trailingAnchor),
109
+ page.topAnchor.constraint(equalTo: contentHost.topAnchor),
110
+ page.bottomAnchor.constraint(equalTo: contentHost.bottomAnchor)
106
111
  ])
112
+ (controller as? ControlCenterPage)?.refreshOnAppear()
107
113
  }
108
114
  }
@@ -1,9 +1,16 @@
1
1
  import Cocoa
2
2
 
3
- final class DiagnosticsViewController: NSViewController {
3
+ final class DiagnosticsViewController: NSViewController, ControlCenterPage {
4
4
  private let processClient: ProcessClient
5
+ private let operations: OperationCoordinator
5
6
  private let status = NativeView.detail("Diagnostics are idle.")
6
- init(processClient: ProcessClient) { self.processClient = processClient; super.init(nibName: nil, bundle: nil) }
7
+ private var busy = false
8
+
9
+ init(processClient: ProcessClient, operations: OperationCoordinator) {
10
+ self.processClient = processClient
11
+ self.operations = operations
12
+ super.init(nibName: nil, bundle: nil)
13
+ }
7
14
  required init?(coder: NSCoder) { nil }
8
15
 
9
16
  override func loadView() {
@@ -15,15 +22,72 @@ final class DiagnosticsViewController: NSViewController {
15
22
  buttons.orientation = .horizontal; buttons.spacing = 8
16
23
  view = NativeView.stack([
17
24
  NativeView.title("Diagnostics"),
18
- NativeView.detail("Diagnostic output is bounded, redacted, and written with owner-only permissions."),
25
+ NativeView.detail("Diagnostic output is bounded, redacted, and written with owner-only permissions. Use this page when another Center action reports a blocker."),
19
26
  status, buttons
20
27
  ])
28
+ refreshOnAppear()
29
+ }
30
+
31
+ func refreshOnAppear() {
32
+ guard !busy else { return }
33
+ if let latest = operations.latestSnapshot() {
34
+ status.stringValue = "Last operation: \(latest.kind) · \(latest.state.rawValue) · \(latest.publicSummary)"
35
+ } else if FileManager.default.fileExists(atPath: AppRuntime.lastActionLogPath) {
36
+ status.stringValue = "Diagnostics idle. A previous action log is available via Open Last Log."
37
+ } else {
38
+ status.stringValue = "Diagnostics are idle. No operation log exists yet."
39
+ }
40
+ }
41
+
42
+ @objc private func doctor() {
43
+ busy = true
44
+ status.stringValue = "Doctor is running…"
45
+ processClient.run(["doctor", "--json"], timeout: NativeView.mutationTimeout) { [weak self] result in
46
+ guard let self = self else { return }
47
+ self.busy = false
48
+ if result.code == 0 {
49
+ self.status.stringValue = "Doctor completed with no blocking issue."
50
+ return
51
+ }
52
+ if let json = self.json(result.output) {
53
+ let ok = json["ok"] as? Bool
54
+ let blockers = (json["blockers"] as? [String])?.count ?? (json["issues"] as? [Any])?.count
55
+ if let blockers = blockers {
56
+ self.status.stringValue = "Doctor reported \(blockers) issue\(blockers == 1 ? "" : "s"). Open Last Log for redacted detail."
57
+ return
58
+ }
59
+ if ok == false {
60
+ self.status.stringValue = "Doctor reported a blocker. Open Last Log for redacted detail."
61
+ return
62
+ }
63
+ }
64
+ self.status.stringValue = "Doctor reported a blocker · \(NativeView.redactPreview(result.output))"
65
+ }
21
66
  }
22
67
 
23
- @objc private func doctor() { status.stringValue = "Doctor is running…"; processClient.run(["doctor", "--json"]) { [weak self] result in self?.status.stringValue = result.code == 0 ? "Doctor completed." : "Doctor reported a blocker." } }
24
68
  @objc private func openLog() {
25
- if FileManager.default.fileExists(atPath: AppRuntime.lastActionLogPath) { NSWorkspace.shared.open(URL(fileURLWithPath: AppRuntime.lastActionLogPath)) }
26
- else { status.stringValue = "No operation log exists yet." }
69
+ if FileManager.default.fileExists(atPath: AppRuntime.lastActionLogPath) {
70
+ NSWorkspace.shared.open(URL(fileURLWithPath: AppRuntime.lastActionLogPath))
71
+ status.stringValue = "Opened the latest redacted action log."
72
+ } else {
73
+ status.stringValue = "No operation log exists yet. Run Doctor or another Center action first."
74
+ }
75
+ }
76
+
77
+ @objc private func restart() {
78
+ busy = true
79
+ status.stringValue = "Restarting Menu Bar…"
80
+ processClient.run(["menubar", "restart", "--json"], timeout: NativeView.mutationTimeout) { [weak self] result in
81
+ guard let self = self else { return }
82
+ self.busy = false
83
+ self.status.stringValue = result.code == 0
84
+ ? "Restart requested. Control Center will reopen after the Menu Bar process returns."
85
+ : "Restart failed · \(NativeView.redactPreview(result.output))"
86
+ }
87
+ }
88
+
89
+ private func json(_ text: String) -> [String: Any]? {
90
+ guard let data = text.data(using: .utf8) else { return nil }
91
+ return try? JSONSerialization.jsonObject(with: data) as? [String: Any]
27
92
  }
28
- @objc private func restart() { processClient.run(["menubar", "restart", "--json"]) { [weak self] result in self?.status.stringValue = result.code == 0 ? "Restart requested." : "Restart failed." } }
29
93
  }