sneakoscope 10.7.0 → 10.7.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 (66) hide show
  1. package/README.md +27 -5
  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 +1 -1
  5. package/dist/cli/install-helpers-codex-lb-config.js +55 -20
  6. package/dist/commands/bridge.js +14 -1
  7. package/dist/commands/codex.js +5 -1
  8. package/dist/commands/doctor-console.js +2 -0
  9. package/dist/commands/doctor.js +8 -9
  10. package/dist/config/skills-manifest.json +1 -1
  11. package/dist/core/agents/agent-role-config.js +6 -2
  12. package/dist/core/agents/sks-model-pin-hygiene.js +150 -0
  13. package/dist/core/codex/codex-config-guard.js +18 -13
  14. package/dist/core/codex-app/codex-app-ui-state-snapshot.js +7 -1
  15. package/dist/core/codex-app/menubar/constants.js +0 -1
  16. package/dist/core/codex-lb/bridge-contracts.js +5 -0
  17. package/dist/core/codex-lb/combined-catalog/builder.js +4 -2
  18. package/dist/core/codex-lb/combined-catalog/latest-generation.js +7 -34
  19. package/dist/core/codex-lb/combined-catalog/normalize.js +3 -1
  20. package/dist/core/codex-lb/combined-catalog/persistence.js +4 -0
  21. package/dist/core/codex-lb/combined-catalog/storage.js +4 -1
  22. package/dist/core/codex-lb/desktop-bridge/codex-lb-turn-state.js +35 -0
  23. package/dist/core/codex-lb/desktop-bridge/compaction-adapter.js +3 -0
  24. package/dist/core/codex-lb/desktop-bridge/header-policy.js +10 -4
  25. package/dist/core/codex-lb/desktop-bridge/http-forward.js +138 -36
  26. package/dist/core/codex-lb/desktop-bridge/models-endpoint.js +62 -0
  27. package/dist/core/codex-lb/desktop-bridge/provider-handoff.js +88 -0
  28. package/dist/core/codex-lb/desktop-bridge/provider-request-compat.js +89 -0
  29. package/dist/core/codex-lb/desktop-bridge/rejection-log.js +61 -0
  30. package/dist/core/codex-lb/desktop-bridge/responses-websocket.js +108 -16
  31. package/dist/core/codex-lb/desktop-bridge/security.js +101 -55
  32. package/dist/core/codex-lb/desktop-bridge/server.js +15 -2
  33. package/dist/core/codex-lb/desktop-bridge/types.js +3 -0
  34. package/dist/core/codex-lb/desktop-bridge/websocket-forward.js +3 -0
  35. package/dist/core/codex-lb/desktop-bridge-migration/historical-config.js +2 -6
  36. package/dist/core/codex-lb/desktop-bridge-migration.js +14 -2
  37. package/dist/core/codex-lb/desktop-controller-v3/catalog.js +125 -3
  38. package/dist/core/codex-lb/desktop-controller-v3/model-refresh.js +140 -0
  39. package/dist/core/codex-lb/desktop-controller-v3/openrouter-only-commands.js +5 -5
  40. package/dist/core/codex-lb/desktop-controller-v3/provider-commands.js +14 -11
  41. package/dist/core/codex-lb/desktop-controller-v3/status.js +2 -3
  42. package/dist/core/codex-lb/desktop-controller-v3.js +3 -1
  43. package/dist/core/codex-lb/desktop-service.js +51 -17
  44. package/dist/core/codex-lb/model-refresh-after.js +16 -0
  45. package/dist/core/codex-lb/request-route-resolver.js +1 -1
  46. package/dist/core/codex-runtime/codex-desktop-config-policy.js +0 -1
  47. package/dist/core/commands/basic-cli.js +5 -0
  48. package/dist/core/commands/menubar-command.js +5 -1
  49. package/dist/core/doctor/desktop-bridge-catalog-repair.js +20 -17
  50. package/dist/core/hooks-runtime/jev-spawn-routing.js +1 -5
  51. package/dist/core/hooks-runtime/parent-orchestration-gate.js +6 -8
  52. package/dist/core/hooks-runtime.js +13 -9
  53. package/dist/core/init/skills.js +22 -3
  54. package/dist/core/provider/provider-context.js +4 -1
  55. package/dist/core/search/context.js +2 -2
  56. package/dist/core/subagents/model-tiers.js +19 -3
  57. package/dist/core/update/update-migration-state/desktop-bridge-catalog-repair-stage.js +4 -3
  58. package/dist/core/update/update-migration-state.js +10 -40
  59. package/dist/core/version.js +1 -1
  60. package/dist/native/sks-menubar/Sources/ControlCenterWindowController.swift +0 -1
  61. package/dist/native/sks-menubar/Sources/OverviewSummary.swift +32 -0
  62. package/dist/native/sks-menubar/Sources/OverviewViewController.swift +15 -5
  63. package/dist/native/sks-menubar/Sources/SidebarItem.swift +0 -2
  64. package/dist/native/sks-menubar/Sources/UpdatesViewController.swift +3 -0
  65. package/package.json +2 -2
  66. package/dist/native/sks-menubar/Sources/RemoteCodingViewController.swift +0 -49
@@ -335,17 +335,18 @@ async function hookUserPrompt(root, state, payload, noQuestion, sessionKey = nul
335
335
  const submittedPrompt = stripVisibleDecisionAnswerBlocks(extractUserPrompt(payload));
336
336
  let stagedMemory = null;
337
337
  const disposition = jevPlan?.memoryDisposition;
338
- if (state?.mission_id && jevPlan?.executionPlan) {
338
+ const plan = jevPlan?.executionPlan || null;
339
+ if (state?.mission_id && (plan || state?.jev_execution_plan)) {
339
340
  await setCurrent(root, {
340
- jev_execution_plan: jevPlan.executionPlan,
341
- jev_turn_binding: jevPlan.decisionBinding ? {
342
- plan_id: jevPlan.executionPlan.plan_id,
343
- source_digest: jevPlan.executionPlan.source_digest,
344
- graph_digest: jevPlan.executionPlan.graph_digest,
345
- policy_revision: jevPlan.executionPlan.policy_revision,
346
- config_digest: jevPlan.executionPlan.config_digest
341
+ jev_execution_plan: plan,
342
+ jev_turn_binding: plan && jevPlan?.decisionBinding ? {
343
+ plan_id: plan.plan_id,
344
+ source_digest: plan.source_digest,
345
+ graph_digest: plan.graph_digest,
346
+ policy_revision: plan.policy_revision,
347
+ config_digest: plan.config_digest
347
348
  } : null,
348
- jev_turn_tier: jevPlan.decision.tier ? { ...jevPlan.decision.tier } : null
349
+ jev_turn_tier: plan && jevPlan?.decision.tier ? { ...jevPlan.decision.tier } : null
349
350
  }, { sessionKey, replace: false }).catch(() => null);
350
351
  }
351
352
  if (state?.mission_id && jevPlan?.decisionBinding && disposition
@@ -1135,6 +1136,9 @@ function clarificationPauseBlockReason(state = {}) {
1135
1136
  }
1136
1137
  async function hookStop(root, state, payload, noQuestion, sessionKey = null) {
1137
1138
  const last = extractLastMessage(payload);
1139
+ if (state?.jev_execution_plan) {
1140
+ await setCurrent(root, { jev_execution_plan: null, jev_turn_binding: null, jev_turn_tier: null }, { sessionKey, replace: false }).catch(() => null);
1141
+ }
1138
1142
  if (!noQuestion) {
1139
1143
  const lightTurn = await consumeLightTurnStopBypass(root, { sessionKey, turnId: hookTurnId(payload) });
1140
1144
  if (lightTurn.accepted)
@@ -262,6 +262,8 @@ async function withManagedSkillCleanupLocks(locks, index, run) {
262
262
  return withManagedSkillGenerationLock(lock.ownerRoot, lock.scope, () => withManagedSkillCleanupLocks(locks, index + 1, run));
263
263
  }
264
264
  async function reconcileRemovedSkillTargets(targets, fix) {
265
+ const manifest = await loadSkillsManifest();
266
+ const currentNames = new Set(manifest.skills.map((skill) => canonicalSkillNameFromValue(skill.canonical_name)));
265
267
  const detected = [];
266
268
  const removed = [];
267
269
  const quarantinedUserCollisions = [];
@@ -348,12 +350,22 @@ async function reconcileRemovedSkillTargets(targets, fix) {
348
350
  const rowByName = new Map(rows.map((row) => [row.name, row]));
349
351
  for (const row of rows) {
350
352
  const name = canonicalSkillNameFromValue(row.name);
351
- if (!REMOVED_SKS_SKILL_NAME_SET.has(name))
353
+ const retiredName = REMOVED_SKS_SKILL_NAME_SET.has(name);
354
+ if (!retiredName && (currentNames.has(name) || !row.isDirectory()))
352
355
  continue;
353
356
  const dir = path.join(targetDir, row.name);
354
357
  const displayPath = displayRemovedSkillPath(target, dir);
355
358
  let managed = false;
356
359
  try {
360
+ if (!retiredName) {
361
+ const skillPath = path.join(dir, 'SKILL.md');
362
+ const inspected = await inspectConfinedPath(ownerRoot, skillPath);
363
+ if (!inspected.exists || inspected.leafSymlink || !inspected.stat?.isFile())
364
+ continue;
365
+ const text = await fsp.readFile(skillPath, 'utf8');
366
+ if (!MANAGED_SKILL_MARKER_RE.test(text) || FORGE_SKILL_MARKER_RE.test(text))
367
+ continue;
368
+ }
357
369
  managed = await isManagedRemovedSkillPath(ownerRoot, dir);
358
370
  }
359
371
  catch (error) {
@@ -361,7 +373,7 @@ async function reconcileRemovedSkillTargets(targets, fix) {
361
373
  remaining.push(displayPath);
362
374
  continue;
363
375
  }
364
- if (!managed && target.managedOnly)
376
+ if (!managed && target.managedOnly && retiredName)
365
377
  continue;
366
378
  detected.push({
367
379
  scope: target.scope,
@@ -737,6 +749,10 @@ async function reconcileSkillsUnlocked(opts) {
737
749
  }, ...hostExtraRemovedSkillTargets(root));
738
750
  }
739
751
  const removedResidue = await reconcileRemovedSkillTargets(removedResidueTargets, opts.fix);
752
+ const quarantinedResiduePaths = new Set(removedResidue.quarantined_user_collisions);
753
+ report.quarantined_user_collisions.push(...removedResidue.detected
754
+ .filter((entry) => quarantinedResiduePaths.has(entry.path))
755
+ .map((entry) => entry.name));
740
756
  report.retired_residue = {
741
757
  detected_count: removedResidue.detected.length,
742
758
  removed_count: removedResidue.removed.length,
@@ -813,7 +829,10 @@ async function reconcileSkillsUnlocked(opts) {
813
829
  report.core_skill_integrity = install?.core_skill_integrity || { ok: true, installed_count: 0, restored_count: 0, user_collision_count: 0 };
814
830
  report.quarantined_user_collisions.push(...(install?.quarantined_user_collisions || []));
815
831
  report.quarantined_manifest_collisions = [...new Set(install?.quarantined_manifest_collisions || [])].sort();
816
- report.removed_stale_generated_skills = currentSurfaceSkillPaths(install?.removed_stale_generated_skills || report.removed);
832
+ report.removed_stale_generated_skills = currentSurfaceSkillPaths([
833
+ ...(install?.removed_stale_generated_skills || report.removed),
834
+ ...removedResidue.removed.map((file) => file.replace(/^~\//, ''))
835
+ ]);
817
836
  report.removed_agent_skill_aliases = currentSurfaceSkillPaths(install?.removed_agent_skill_aliases || []);
818
837
  report.removed_codex_skill_mirrors = currentSurfaceSkillPaths(install?.removed_codex_skill_mirrors || []);
819
838
  report.removed = currentSurfaceSkillPaths(report.removed);
@@ -83,9 +83,12 @@ function selectedBridgeProvider(status) {
83
83
  const selected = status?.routing.selected_route?.provider_id;
84
84
  return (selected === 'codex-lb' || selected === 'openrouter' ? selected : null)
85
85
  || status?.routing.policy?.default_provider_id
86
- || status?.routing.session_pin?.provider_id
86
+ || providerPinOf(status?.routing.session_pin?.provider_id)
87
87
  || null;
88
88
  }
89
+ function providerPinOf(pinned) {
90
+ return pinned === 'codex-lb' || pinned === 'openrouter' ? pinned : null;
91
+ }
89
92
  async function currentDesktopBridgeStatus(options) {
90
93
  const controller = await import('../codex-lb/desktop-controller.js');
91
94
  return controller.desktopBridgeStatusV3(options);
@@ -4,7 +4,7 @@ import { cacheGet, cacheSet } from './cache.js';
4
4
  import { contextGraphMetaOf, emptyGraphMeta, firstProvenanceHash, omissionReasonsOf, overallConfidence, projectMatches } from './context-projection.js';
5
5
  import { defaultSearchLimits, emptySkipped, SEARCH_PROVIDER_SCHEMA, SEARCH_SCHEMA_VERSION } from './types.js';
6
6
  import { CONTEXT_GRAPH_CORRUPT_ERROR, CONTEXT_GRAPH_MISSING_ERROR, CONTEXT_GRAPH_REPAIR_COMMAND, CONTEXT_GRAPH_STALE_ERROR } from '../triwiki/context-graph/contracts.js';
7
- import { contextGraphExtractors } from '../triwiki/context-graph/extractors/index.js';
7
+ import { alignGraphExtractors } from '../triwiki/context-graph/extractors/index.js';
8
8
  import { isWorkspaceRelativePosixPath } from '../triwiki/context-graph/paths.js';
9
9
  import { CONTEXT_GRAPH_TRAVERSAL_CAPS, DEFAULT_CONTEXT_GRAPH_QUERY_PROFILE, isContextGraphQueryProfileName } from '../triwiki/context-graph/profiles.js';
10
10
  import { changedPathKernelSeeds, changedPathSeeds, openWorkspaceContextIndex, queryWorkspaceContext, sharedContextIndexCache, workspaceContextFailureOf } from '../triwiki/context-graph/query/index.js';
@@ -132,7 +132,7 @@ async function preflightStatus(root, options) {
132
132
  const cached = cacheGet(key);
133
133
  if (cached)
134
134
  return cached;
135
- const status = await contextIndexFreshness(root, { extractors: contextGraphExtractors(), verifySources: false });
135
+ const status = await contextIndexFreshness(root, { extractors: alignGraphExtractors(), verifySources: false });
136
136
  cacheSet(key, status, STATUS_MEMO_TTL_MS);
137
137
  return status;
138
138
  }
@@ -56,16 +56,32 @@ function readableRows(file) {
56
56
  }
57
57
  }
58
58
  let catalogPathMemo = null;
59
+ const BRIDGE_BASE_URL_MARKER = '# sks-desktop-bridge-managed-base-url';
60
+ function activeBridgeCatalogPath(codexHome) {
61
+ try {
62
+ const pointer = JSON.parse(fs.readFileSync(path.join(codexHome, 'sks', 'sks-bridge-active-generation.json'), 'utf8'));
63
+ const bundle = path.resolve(codexHome, 'sks', String(pointer?.bundle_directory || ''));
64
+ const file = path.join(bundle, String(pointer?.catalog_filename || ''));
65
+ return bundle.startsWith(path.join(codexHome, 'sks', '.sks-bridge-generations') + path.sep) && path.dirname(file) === bundle ? file : null;
66
+ }
67
+ catch {
68
+ return null;
69
+ }
70
+ }
59
71
  function configuredCatalogPath(input) {
60
- const configPath = path.join(codexHomePath(input), 'config.toml');
61
- const key = fileKey(configPath);
72
+ const codexHome = codexHomePath(input);
73
+ const configPath = path.join(codexHome, 'config.toml');
74
+ const key = `${fileKey(configPath)}|${fileKey(path.join(codexHome, 'sks', 'sks-bridge-active-generation.json'))}`;
62
75
  if (catalogPathMemo?.key === key)
63
76
  return catalogPathMemo.path;
64
77
  let resolved = null;
65
78
  try {
66
79
  const stat = fs.statSync(configPath);
67
80
  if (stat.isFile() && stat.size <= 4 * 1024 * 1024) {
68
- const value = readTopLevelTomlString(fs.readFileSync(configPath, 'utf8'), 'model_catalog_json')?.trim();
81
+ const config = fs.readFileSync(configPath, 'utf8');
82
+ const value = readTopLevelTomlString(config, 'model_catalog_json')?.trim();
83
+ if (!value && config.split(/\r?\n/).some((line) => line.trim() === BRIDGE_BASE_URL_MARKER))
84
+ resolved = activeBridgeCatalogPath(codexHome);
69
85
  if (value) {
70
86
  const env = input.env || process.env;
71
87
  const home = input.home || env.HOME || os.homedir();
@@ -17,17 +17,18 @@ export async function runDesktopBridgeCatalogRepairStage() {
17
17
  if (!(await exists(desktopBridgeServicePaths(home).settings_path))) {
18
18
  return skip('desktop_bridge_catalog_repair_no_managed_bridge');
19
19
  }
20
- const repair = await repairDoctorDesktopBridgeCatalog({ fix: true }).catch((error) => ({
20
+ const repair = await repairDoctorDesktopBridgeCatalog({ fix: true, reason: 'update' }).catch((error) => ({
21
21
  ok: false,
22
22
  repaired: false,
23
23
  warnings: [],
24
24
  blockers: [`desktop_bridge_catalog_repair_failed:${error?.message || String(error)}`]
25
25
  }));
26
+ const refreshLine = typeof repair.model_refresh?.line === 'string' ? [repair.model_refresh.line] : [];
26
27
  if (repair.ok) {
27
28
  return {
28
29
  ok: true,
29
30
  status: 'ok',
30
- actions: [repair.repaired ? 'desktop_bridge_catalog_repaired' : 'desktop_bridge_catalog_clean'],
31
+ actions: [repair.repaired ? 'desktop_bridge_catalog_repaired' : 'desktop_bridge_catalog_clean', ...refreshLine],
31
32
  blockers: [],
32
33
  warnings: repair.warnings || []
33
34
  };
@@ -35,7 +36,7 @@ export async function runDesktopBridgeCatalogRepairStage() {
35
36
  return {
36
37
  ok: true,
37
38
  status: 'ok',
38
- actions: [],
39
+ actions: refreshLine,
39
40
  blockers: [],
40
41
  warnings: [
41
42
  ...(repair.blockers || []).map((blocker) => `desktop_bridge_catalog_repair_incomplete:${blocker}`),
@@ -16,7 +16,6 @@ import { runDesktopBridgeRestageStage } from './update-migration-state/desktop-b
16
16
  import { runSessionStateSplitStage } from './update-migration-state/session-state-split.js';
17
17
  import { runRetiredLocalDecisionStage } from './update-migration-state/retired-local-decision.js';
18
18
  import { runConfigSyntaxRepairStage, runHookTrustRefreshStage, runOtherHarnessCleanupStage } from './update-migration-state/simple-stages.js';
19
- import { compareSemVer } from './semver.js';
20
19
  import { repairManagedPermissions } from './managed-permission-repair.js';
21
20
  import { recordSksProject, runOtherProjectsMigrationStage, SKS_PROJECT_MARKERS } from './sks-project-registry.js';
22
21
  export const UPDATE_MIGRATION_SCHEMA = 'sks.project-migration-receipt.v2';
@@ -265,27 +264,22 @@ export async function runUpdateRetentionCleanup(root, source = 'update-migration
265
264
  const UPDATE_MIGRATION_STAGES = [
266
265
  {
267
266
  id: 'other-harness-cleanup',
268
- min_from_version: '0.0.0',
269
267
  run: runOtherHarnessCleanupStage
270
268
  },
271
269
  {
272
270
  id: 'retired-local-decision',
273
- min_from_version: '0.0.0',
274
271
  run: () => runRetiredLocalDecisionStage()
275
272
  },
276
273
  {
277
274
  id: 'skills-reconcile',
278
- min_from_version: '0.0.0',
279
275
  run: runSkillsReconcileStage
280
276
  },
281
277
  {
282
278
  id: 'current-public-surface-reconcile',
283
- min_from_version: '0.0.0',
284
279
  run: runCurrentPublicSurfaceReconcileStage
285
280
  },
286
281
  {
287
282
  id: 'naruto-execution-mode',
288
- min_from_version: '0.0.0',
289
283
  run: async () => {
290
284
  const { writeNarutoExecutionMode } = await import('../subagents/naruto-execution-mode.js');
291
285
  const initialMode = process.env.SKS_NARUTO_APP_SESSION === '1' ? 'current-session' : 'standalone';
@@ -295,42 +289,34 @@ const UPDATE_MIGRATION_STAGES = [
295
289
  },
296
290
  {
297
291
  id: 'session-state-split',
298
- min_from_version: '0.0.0',
299
292
  run: runSessionStateSplitStage
300
293
  },
301
294
  {
302
295
  id: 'menubar-retarget',
303
- min_from_version: '0.0.0',
304
296
  run: runMenubarRetargetStage
305
297
  },
306
298
  {
307
299
  id: 'config-fastmode-normalize',
308
- min_from_version: '0.0.0',
309
300
  run: runConfigFastModeNormalizeStage
310
301
  },
311
302
  {
312
303
  id: 'config-syntax-repair',
313
- min_from_version: '0.0.0',
314
304
  run: runConfigSyntaxRepairStage
315
305
  },
316
306
  {
317
307
  id: 'hook-trust-refresh',
318
- min_from_version: '0.0.0',
319
308
  run: runHookTrustRefreshStage
320
309
  },
321
310
  {
322
311
  id: 'receipt-rotation',
323
- min_from_version: '0.0.0',
324
312
  run: runReceiptRotationStage
325
313
  },
326
314
  {
327
315
  id: 'desktop-bridge-restage',
328
- min_from_version: '0.0.0',
329
316
  run: runDesktopBridgeRestageStage
330
317
  },
331
318
  {
332
319
  id: 'desktop-bridge-catalog-repair',
333
- min_from_version: '0.0.0',
334
320
  run: runDesktopBridgeCatalogRepairStage
335
321
  }
336
322
  ];
@@ -340,26 +326,12 @@ export async function runUpdateMigrationStages(root, opts = {}) {
340
326
  for (const stage of UPDATE_MIGRATION_STAGES) {
341
327
  if (opts.only && !opts.only.has(stage.id))
342
328
  continue;
343
- if (!legacyStageApplies(fromVersion, stage.min_from_version)) {
344
- runs.push({
345
- schema: 'sks.update-migration-stage.v2',
346
- id: stage.id,
347
- ok: true,
348
- status: 'skipped',
349
- min_from_version: stage.min_from_version,
350
- from_version: fromVersion,
351
- actions: ['skipped_by_from_version'],
352
- blockers: [],
353
- warnings: []
354
- });
355
- continue;
356
- }
357
329
  try {
358
330
  const result = await stage.run(root, fromVersion);
359
331
  runs.push({
360
332
  schema: 'sks.update-migration-stage.v2',
361
333
  id: stage.id,
362
- min_from_version: stage.min_from_version,
334
+ min_from_version: '0.0.0',
363
335
  from_version: fromVersion,
364
336
  ...result
365
337
  });
@@ -370,7 +342,7 @@ export async function runUpdateMigrationStages(root, opts = {}) {
370
342
  id: stage.id,
371
343
  ok: false,
372
344
  status: 'failed',
373
- min_from_version: stage.min_from_version,
345
+ min_from_version: '0.0.0',
374
346
  from_version: fromVersion,
375
347
  actions: [],
376
348
  blockers: [err?.message || String(err)],
@@ -410,6 +382,7 @@ async function runSkillsReconcileStage(root) {
410
382
  const runtimeRemaining = convergence.retired_runtime_scopes.reduce((sum, report) => sum + report.remaining_managed_artifact_count, 0);
411
383
  const runtimeErrors = convergence.retired_runtime_scopes.reduce((sum, report) => sum + report.error_count, 0);
412
384
  const blockers = [
385
+ ...(convergence.ok ? [] : ['legacy_generation_convergence_failed']),
413
386
  ...skillReports.flatMap((report) => report.ok ? [] : [
414
387
  `${report.scope}:${'error' in report ? report.error : 'failed'}`
415
388
  ]),
@@ -418,6 +391,12 @@ async function runSkillsReconcileStage(root) {
418
391
  ...(convergence.retired_agent_roles.ok ? [] : ['retired_agent_role_reconcile_failed']),
419
392
  ...(runtimeRemaining ? [`retired_runtime_residue_remaining:${runtimeRemaining}`] : []),
420
393
  ...(runtimeErrors ? [`retired_runtime_reconcile_failed:${runtimeErrors}`] : []),
394
+ ...(convergence.runtime_data_gc.remaining_count
395
+ ? [`legacy_runtime_data_remaining:${convergence.runtime_data_gc.remaining_count}`]
396
+ : []),
397
+ ...(convergence.runtime_data_gc.error_count
398
+ ? [`legacy_runtime_data_reconcile_failed:${convergence.runtime_data_gc.error_count}`]
399
+ : []),
421
400
  ...(convergence.managed_configs.remaining_count
422
401
  ? [`retired_config_residue_remaining:${convergence.managed_configs.remaining_count}`]
423
402
  : []),
@@ -451,6 +430,7 @@ async function runSkillsReconcileStage(root) {
451
430
  residue_remaining_count: globalRemaining
452
431
  + projectRemaining
453
432
  + runtimeRemaining
433
+ + convergence.runtime_data_gc.remaining_count
454
434
  + convergence.managed_configs.remaining_count
455
435
  }
456
436
  };
@@ -521,16 +501,6 @@ async function runReceiptRotationStage(root) {
521
501
  detail: { removed: removable.length }
522
502
  };
523
503
  }
524
- function legacyStageApplies(fromVersion, minFromVersion) {
525
- if (!fromVersion)
526
- return true;
527
- if (compareVersionLike(fromVersion, PACKAGE_VERSION) > 0)
528
- return false;
529
- return compareVersionLike(fromVersion, minFromVersion) >= 0;
530
- }
531
- function compareVersionLike(a, b) {
532
- return compareSemVer(a, b) ?? 0;
533
- }
534
504
  export async function ensureCurrentMigrationBeforeCommand(input) {
535
505
  const env = input.env || process.env;
536
506
  const command = input.command;
@@ -1 +1 @@
1
- export const PACKAGE_VERSION = '10.7.0';
1
+ export const PACKAGE_VERSION = '10.7.2';
@@ -33,7 +33,6 @@ final class ControlCenterWindowController: NSWindowController, NSTableViewDataSo
33
33
  .mcpServers: MCPServersViewController(processClient: processClient, operations: operations, notifications: notifications),
34
34
  .providers: ProvidersViewController(processClient: processClient, operations: operations),
35
35
  .subagentModels: SubagentModelsViewController(processClient: processClient, operations: operations),
36
- .remoteCoding: RemoteCodingViewController(),
37
36
  .localDecision: LocalDecisionViewController(processClient: processClient, operations: operations),
38
37
  .imageGeneration: ImageGenerationViewController(processClient: processClient, operations: operations),
39
38
  .diagnostics: DiagnosticsViewController(processClient: processClient, operations: operations),
@@ -134,3 +134,35 @@ enum OverviewSummary {
134
134
  return value
135
135
  }
136
136
  }
137
+
138
+ /// What `sks bridge models refresh --json` reports, as SKS Center shows it.
139
+ /// Success is the payload's own `ok == true` at both levels; a result that only
140
+ /// carries a schema key is not one.
141
+ enum ModelRefreshSummary {
142
+ struct Outcome: Equatable {
143
+ let ok: Bool
144
+ let text: String
145
+ }
146
+
147
+ static func outcome(fromJSONText text: String, at date: Date = Date()) -> Outcome {
148
+ outcome(fromJSON: jsonObject(text), at: date)
149
+ }
150
+
151
+ static func outcome(fromJSON object: [String: Any]?, at date: Date = Date()) -> Outcome {
152
+ let refresh = (object?["result"] as? [String: Any])?["model_refresh"] as? [String: Any]
153
+ guard object?["ok"] as? Bool == true, refresh?["ok"] as? Bool == true else {
154
+ return Outcome(ok: false, text: refresh?["line"] as? String ?? "Model list refresh failed. Run `sks bridge models refresh` to retry.")
155
+ }
156
+ let total = (refresh?["total"] as? NSNumber)?.intValue ?? 0
157
+ let formatter = DateFormatter()
158
+ formatter.dateFormat = "HH:mm"
159
+ let restart = refresh?["restart_codex_required"] as? Bool == true ? " · restart Codex to see it" : ""
160
+ return Outcome(ok: true, text: "Model list updated at \(formatter.string(from: date)) (\(total) models)\(restart)")
161
+ }
162
+
163
+ private static func jsonObject(_ text: String) -> [String: Any]? {
164
+ let start = text.firstIndex(of: "{") ?? text.startIndex
165
+ guard let data = String(text[start...]).data(using: .utf8) else { return nil }
166
+ return (try? JSONSerialization.jsonObject(with: data)) as? [String: Any]
167
+ }
168
+ }
@@ -6,6 +6,7 @@ final class OverviewViewController: NSViewController, ControlCenterPage {
6
6
  private let status = NativeView.detail("Loading local SKS status…")
7
7
  private let notificationInbox = NativeView.detail("Notifications: checking authorization…")
8
8
  private let recoveryStatus = NativeView.detail("Progress recovery: no operation state loaded yet.")
9
+ private let modelListStatus = NativeView.detail("Model list: refreshed by Refresh, Run Doctor and updates.")
9
10
  private let healthBadge = NativeView.badge("Checking local status", color: .systemBlue)
10
11
  private let statusSpinner = NativeView.spinner(label: "Checking SKS Center status")
11
12
  private let snapshotDetails = NativeView.detail("")
@@ -36,7 +37,6 @@ final class OverviewViewController: NSViewController, ControlCenterPage {
36
37
  updateCodexButton.setAccessibilityHelp("Update the operator Codex CLI to the preferred latest channel.")
37
38
  let buttons = NativeView.row([refreshButton, doctorButton, updateCodexButton])
38
39
  let shortcuts = NativeView.row([
39
- NativeView.button("Remote Coding…", target: self, action: #selector(openRemoteCoding)),
40
40
  connectionsShortcut(),
41
41
  NativeView.button("Decisions…", target: self, action: #selector(openDecisions)),
42
42
  NativeView.button("Updates…", target: self, action: #selector(openUpdates)),
@@ -50,7 +50,7 @@ final class OverviewViewController: NSViewController, ControlCenterPage {
50
50
  status.setAccessibilityIdentifier("sks-overview-next-action")
51
51
  let healthCard = NativeView.card(
52
52
  title: "System health", subtitle: "",
53
- views: [NativeView.row([healthBadge, statusSpinner]), status, components, buttons]
53
+ views: [NativeView.row([healthBadge, statusSpinner]), status, modelListStatus, components, buttons]
54
54
  )
55
55
  recoveryCard = NativeView.card(
56
56
  title: "Progress, pause & recovery", subtitle: "Review the current operation before retrying.",
@@ -72,7 +72,6 @@ final class OverviewViewController: NSViewController, ControlCenterPage {
72
72
  return button
73
73
  }
74
74
 
75
- @objc private func openRemoteCoding() { openSection?("Remote Coding") }
76
75
  @objc private func openProviders() { openSection?("Providers") }
77
76
  @objc private func openDecisions() { openSection?("Decisions") }
78
77
  @objc private func openUpdates() { openSection?("Updates") }
@@ -113,7 +112,15 @@ final class OverviewViewController: NSViewController, ControlCenterPage {
113
112
  }
114
113
 
115
114
  @objc private func refreshStatus() {
116
- loadStatus(forceUpdateRefresh: true)
115
+ loadStatus(forceUpdateRefresh: true) { [weak self] in self?.refreshModelList() }
116
+ }
117
+
118
+ /// Refresh, Run Doctor and a Codex CLI update each end with a fresh model list.
119
+ private func refreshModelList() {
120
+ modelListStatus.stringValue = "Refreshing the model list…"
121
+ processClient.run(["bridge", "models", "refresh", "--json"], timeout: NativeView.longMutationTimeout) { [weak self] result in
122
+ self?.modelListStatus.stringValue = ModelRefreshSummary.outcome(fromJSONText: result.output).text
123
+ }
117
124
  }
118
125
 
119
126
  @objc private func updateCodexCLI() {
@@ -145,11 +152,12 @@ final class OverviewViewController: NSViewController, ControlCenterPage {
145
152
  self.status.stringValue = ok
146
153
  ? "Codex CLI update completed. Refreshing shared update status…"
147
154
  : "Codex CLI update needs attention. Open Updates or Diagnostics for structured guidance."
155
+ if ok { self.refreshModelList() }
148
156
  self.loadStatus(forceUpdateRefresh: true)
149
157
  }
150
158
  }
151
159
 
152
- private func loadStatus(forceUpdateRefresh: Bool) {
160
+ private func loadStatus(forceUpdateRefresh: Bool, then completion: (() -> Void)? = nil) {
153
161
  generation += 1
154
162
  let requestGeneration = generation
155
163
  refreshButton?.isEnabled = false
@@ -196,6 +204,7 @@ final class OverviewViewController: NSViewController, ControlCenterPage {
196
204
  self.refreshButton?.isEnabled = true
197
205
  self.statusSpinner.stopAnimation(nil)
198
206
  self.renderStatus(update: update, mcp: mcp, partial: false)
207
+ completion?()
199
208
  }
200
209
  DispatchQueue.main.asyncAfter(deadline: .now() + 5) { [weak self] in
201
210
  guard let self = self,
@@ -232,6 +241,7 @@ final class OverviewViewController: NSViewController, ControlCenterPage {
232
241
  if result.code == 0 {
233
242
  NativeView.setBadge(self.healthBadge, text: "Doctor completed", color: .systemGreen)
234
243
  self.status.stringValue = "Doctor completed. No blocking issue was reported."
244
+ self.refreshModelList()
235
245
  } else {
236
246
  NativeView.setBadge(self.healthBadge, text: "Doctor needs attention", color: .systemOrange)
237
247
  self.status.stringValue = "Doctor found an issue. Open Diagnostics · \(NativeView.redactPreview(result.output))"
@@ -6,7 +6,6 @@ enum SidebarItem: String, CaseIterable {
6
6
  case overview = "Overview"
7
7
  case providers = "Providers"
8
8
  case subagentModels = "Subagent Models"
9
- case remoteCoding = "Remote Coding"
10
9
  case updates = "Updates"
11
10
  case mcpServers = "MCP Servers"
12
11
  case localDecision = "Decisions"
@@ -25,7 +24,6 @@ enum SidebarItem: String, CaseIterable {
25
24
  case .mcpServers: return "server.rack"
26
25
  case .localDecision: return "cpu"
27
26
  case .imageGeneration: return "photo"
28
- case .remoteCoding: return "laptopcomputer.and.iphone"
29
27
  case .diagnostics: return "stethoscope"
30
28
  case .settings: return "gearshape"
31
29
  }
@@ -231,6 +231,9 @@ final class UpdatesViewController: NSViewController, ControlCenterPage {
231
231
  self.status.stringValue = "Review ready. Confirm to continue, or cancel to leave the current install unchanged."
232
232
  }
233
233
  }
234
+ if kind == "update", authoritativeState == .succeeded { // The update refreshed the model list; show what it holds now.
235
+ self.processClient.run(["bridge", "models", "refresh", "--json"], timeout: NativeView.mutationTimeout) { [weak self] refreshed in self?.status.stringValue = ModelRefreshSummary.outcome(fromJSONText: refreshed.output).text }
236
+ }
234
237
  let updateOutput = kind == "update-status" ? result.output : nil
235
238
  let approvalRequired = kind == "update-review" && result.code == 0
236
239
  let updateAvailable = updateOutput != nil && NotificationCoordinator.updateIsAvailable(in: result.output)
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sneakoscope",
3
3
  "displayName": "ㅅㅋㅅ",
4
- "version": "10.7.0",
4
+ "version": "10.7.2",
5
5
  "description": "Sneakoscope Codex (`sks`) is an open-source trust layer for Codex CLI and ChatGPT Desktop. It coordinates bounded AI coding agents, records machine-verifiable evidence, preserves project memory, and blocks release claims that are not supported by current tests or artifacts. Search visibility outcomes are measured separately; SKS does not promise rankings or traffic.",
6
6
  "type": "module",
7
7
  "homepage": "https://github.com/mandarange/Sneakoscope-Codex#readme",
@@ -201,6 +201,6 @@
201
201
  "@types/node": "^24.19.2",
202
202
  "@types/ws": "^8.18.2",
203
203
  "jsdom": "30.1.3",
204
- "mermaid": "12.1.0"
204
+ "mermaid": "12.2.0"
205
205
  }
206
206
  }
@@ -1,49 +0,0 @@
1
- import Cocoa
2
-
3
- final class RemoteCodingViewController: NSViewController {
4
- private static let websiteURL = URL(string: "https://paseo.sh/")!
5
- private static let docsURL = URL(string: "https://paseo.sh/docs")!
6
-
7
- override func loadView() {
8
- let websiteButton = NativeView.button(
9
- "Visit Paseo",
10
- target: self,
11
- action: #selector(openWebsite)
12
- )
13
- websiteButton.setAccessibilityLabel("Visit Paseo website")
14
- websiteButton.setAccessibilityHelp("Open the independent Paseo website in your default browser.")
15
-
16
- let docsButton = NativeView.button(
17
- "Read Paseo Docs",
18
- target: self,
19
- action: #selector(openDocs)
20
- )
21
- docsButton.setAccessibilityLabel("Read Paseo documentation")
22
- docsButton.setAccessibilityHelp("Open the official Paseo documentation in your default browser.")
23
-
24
- let recommendation = NativeView.card(
25
- title: "Paseo (recommended)",
26
- subtitle: "Run Codex from your desktop, phone, web browser, or terminal.",
27
- views: [
28
- NativeView.row([websiteButton, docsButton]),
29
- NativeView.detail("Paseo is a separate, open-source app. Follow its setup and security guidance to connect your devices.")
30
- ]
31
- )
32
-
33
- view = NativeView.page([
34
- NativeView.title("Remote Coding"),
35
- NativeView.detail(
36
- "Continue coding from another device."
37
- ),
38
- recommendation
39
- ])
40
- }
41
-
42
- @objc private func openWebsite() {
43
- NSWorkspace.shared.open(Self.websiteURL)
44
- }
45
-
46
- @objc private func openDocs() {
47
- NSWorkspace.shared.open(Self.docsURL)
48
- }
49
- }