sneakoscope 7.3.0 → 7.6.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 (283) hide show
  1. package/README.md +129 -10
  2. package/crates/sks-core/Cargo.lock +1 -1
  3. package/crates/sks-core/Cargo.toml +1 -1
  4. package/dist/cli/codex-app-command.js +1 -1
  5. package/dist/cli/command-manifest-lite.js +4 -1
  6. package/dist/cli/command-registry.js +2 -0
  7. package/dist/cli/install-helpers-codex-lb-chain.js +42 -0
  8. package/dist/cli/install-helpers-codex-lb-config.js +114 -9
  9. package/dist/cli/install-helpers-install-support.js +6 -16
  10. package/dist/cli/install-helpers.js +788 -600
  11. package/dist/cli/router.js +30 -2
  12. package/dist/commands/codex-app.js +37 -1
  13. package/dist/commands/codex-lb.js +491 -300
  14. package/dist/config/skills-manifest.json +57 -71
  15. package/dist/core/align/align-route.js +127 -0
  16. package/dist/core/codex-app/codex-app-fast-ui-repair.js +8 -4
  17. package/dist/core/codex-app/codex-app-restart.js +83 -0
  18. package/dist/core/codex-app/codex-app-ui-state-snapshot.js +8 -4
  19. package/dist/core/codex-app/desktop-routing-snapshot.js +1 -1
  20. package/dist/core/codex-app/menubar/constants.js +5 -0
  21. package/dist/core/codex-app/menubar/generation-transaction-journal.js +1 -3
  22. package/dist/core/codex-app/menubar/installer.js +18 -0
  23. package/dist/core/codex-app/multi-provider-router.js +22 -10
  24. package/dist/core/codex-app/openrouter-activate.js +26 -1
  25. package/dist/core/codex-app.js +118 -26
  26. package/dist/core/codex-control/codex-app-server-runtime-env.js +86 -0
  27. package/dist/core/codex-control/codex-app-server-v2-client.js +78 -8
  28. package/dist/core/codex-control/codex-task-runner.js +2 -2
  29. package/dist/core/codex-lb/capability-runner.js +204 -0
  30. package/dist/core/codex-lb/capability-types.js +1 -0
  31. package/dist/core/codex-lb/codex-lb-env.js +9 -13
  32. package/dist/core/codex-lb/codex-lb-setup.js +45 -8
  33. package/dist/core/codex-lb/codex-lb-tool-catalog.js +52 -74
  34. package/dist/core/codex-lb/codex-lb-tool-output-recovery.js +1 -1
  35. package/dist/core/codex-lb/desktop-auth-invariant.js +185 -0
  36. package/dist/core/codex-lb/desktop-bridge/client.js +34 -0
  37. package/dist/core/codex-lb/desktop-bridge/header-policy.js +106 -0
  38. package/dist/core/codex-lb/desktop-bridge/http-forward.js +101 -0
  39. package/dist/core/codex-lb/desktop-bridge/index.js +10 -0
  40. package/dist/core/codex-lb/desktop-bridge/launchd.js +81 -0
  41. package/dist/core/codex-lb/desktop-bridge/location-rewrite.js +27 -0
  42. package/dist/core/codex-lb/desktop-bridge/security.js +274 -0
  43. package/dist/core/codex-lb/desktop-bridge/server.js +179 -0
  44. package/dist/core/codex-lb/desktop-bridge/state.js +119 -0
  45. package/dist/core/codex-lb/desktop-bridge/types.js +16 -0
  46. package/dist/core/codex-lb/desktop-bridge/websocket-forward.js +151 -0
  47. package/dist/core/codex-lb/desktop-center-credentials.js +189 -0
  48. package/dist/core/codex-lb/desktop-controller.js +1410 -0
  49. package/dist/core/codex-lb/desktop-mode.js +32 -0
  50. package/dist/core/codex-lb/desktop-real-evidence.js +168 -0
  51. package/dist/core/codex-lb/desktop-service.js +613 -0
  52. package/dist/core/codex-lb/legacy-migration.js +575 -0
  53. package/dist/core/codex-lb/migration-receipt.js +246 -0
  54. package/dist/core/codex-lb/probes/auxiliary-surfaces-probe.js +60 -0
  55. package/dist/core/codex-lb/probes/bridge-probe.js +75 -0
  56. package/dist/core/codex-lb/probes/catalog-probe.js +128 -0
  57. package/dist/core/codex-lb/probes/computer-use-probe.js +50 -0
  58. package/dist/core/codex-lb/probes/image-generation-probe.js +47 -0
  59. package/dist/core/codex-lb/probes/probe-evidence.js +39 -0
  60. package/dist/core/codex-lb/probes/voice-realtime-probe.js +44 -0
  61. package/dist/core/codex-lb/trusted-deep-evidence.js +403 -0
  62. package/dist/core/codex-native/core-skill-manifest.js +22 -6
  63. package/dist/core/codex-runtime/codex-desktop-config-policy.js +2 -1
  64. package/dist/core/commands/align-command.js +220 -0
  65. package/dist/core/commands/image-ux-review-command.js +280 -68
  66. package/dist/core/commands/naruto-command.js +257 -8
  67. package/dist/core/commands/ppt-command.js +2 -2
  68. package/dist/core/commands/telegram-command.js +402 -34
  69. package/dist/core/commands/triwiki-command.js +11 -1
  70. package/dist/core/commands/triwiki-graph-command.js +160 -0
  71. package/dist/core/commands/wiki-command.js +100 -20
  72. package/dist/core/doctor/skill-legacy-surface.js +1 -3
  73. package/dist/core/errors/message.js +3 -0
  74. package/dist/core/evidence/evidence-freshness.js +14 -0
  75. package/dist/core/feature-fixtures.js +9 -1
  76. package/dist/core/feature-registry.js +5 -3
  77. package/dist/core/fsx.js +7 -3
  78. package/dist/core/hooks-runtime/codex-commit-hooks-selftest.js +82 -0
  79. package/dist/core/hooks-runtime/context-graph-freshness-preflight.js +113 -0
  80. package/dist/core/hooks-runtime/hook-context.js +15 -2
  81. package/dist/core/hooks-runtime/managed-skill-generation-heal.js +46 -0
  82. package/dist/core/hooks-runtime/naruto-terminal-finalization.js +1 -0
  83. package/dist/core/hooks-runtime/official-subagent-lifecycle.js +1 -1
  84. package/dist/core/hooks-runtime/tool-output-quarantine.js +1 -1
  85. package/dist/core/hooks-runtime.js +33 -89
  86. package/dist/core/image-ux-review/callout-extraction.js +6 -0
  87. package/dist/core/image-ux-review/imagegen-adapter.js +64 -23
  88. package/dist/core/image-ux-review/real-callout-extractor.js +1 -0
  89. package/dist/core/image-ux-review.js +230 -12
  90. package/dist/core/imagegen/codex-lb-imagegen-target.js +21 -7
  91. package/dist/core/imagegen/imagegen-capability.js +10 -4
  92. package/dist/core/imagegen/require-imagegen.js +29 -5
  93. package/dist/core/init/skills.js +25 -13
  94. package/dist/core/init.js +1 -1
  95. package/dist/core/install/installed-package-smoke.js +157 -4
  96. package/dist/core/managed-assets/managed-assets-manifest.js +2 -1
  97. package/dist/core/managed-path-safety.js +8 -10
  98. package/dist/core/mcp-config/backup.js +2 -4
  99. package/dist/core/mcp-config/config-reader.js +2 -4
  100. package/dist/core/mcp-config/scope.js +3 -5
  101. package/dist/core/naruto/context-graph-advisor-scope.js +242 -0
  102. package/dist/core/naruto/context-graph-advisor.js +0 -0
  103. package/dist/core/perf/release-latency-slo.js +12 -6
  104. package/dist/core/pipeline-internals/runtime-core.js +78 -0
  105. package/dist/core/pipeline-internals/runtime-gates.js +5 -2
  106. package/dist/core/policy/latest-version-guidance.js +113 -0
  107. package/dist/core/ppt.js +8 -8
  108. package/dist/core/preflight/parallel-preflight-engine.js +1 -1
  109. package/dist/core/proof/auto-finalize.js +98 -6
  110. package/dist/core/proof/route-proof-gate.js +12 -1
  111. package/dist/core/proof/route-proof-policy.js +8 -8
  112. package/dist/core/release/gate-manifest.js +32 -1
  113. package/dist/core/release/npm-stage-contract.js +21 -0
  114. package/dist/core/release/package-size-budget.js +2 -2
  115. package/dist/core/release/release-gate-contract.js +5 -0
  116. package/dist/core/release/release-proof-io.js +1 -3
  117. package/dist/core/release/release-real-contract.js +5 -4
  118. package/dist/core/release/release-stamp-proof.js +1 -3
  119. package/dist/core/release/stage-publish.js +27 -0
  120. package/dist/core/remote/audit-idempotency.js +50 -18
  121. package/dist/core/remote/codex-public-activity.js +283 -0
  122. package/dist/core/remote/index.js +1 -0
  123. package/dist/core/remote/ssh-worker-client.js +4 -1
  124. package/dist/core/remote/worker.js +206 -27
  125. package/dist/core/research/research-adversarial-review.js +1 -3
  126. package/dist/core/research/research-claim-synthesizer.js +1 -3
  127. package/dist/core/research/research-falsification-runner.js +1 -3
  128. package/dist/core/research/research-source-evidence.js +1 -3
  129. package/dist/core/responses-stream.js +65 -19
  130. package/dist/core/routes/constants.js +1 -1
  131. package/dist/core/routes/design-policy.js +7 -2
  132. package/dist/core/routes/dollar-manifest-lite.js +2 -4
  133. package/dist/core/routes/evidence.js +1 -1
  134. package/dist/core/routes.js +30 -10
  135. package/dist/core/safety/command-contract/contracts.js +2 -0
  136. package/dist/core/safety/command-contract/types.js +1 -1
  137. package/dist/core/search/context-graph-seeds.js +205 -0
  138. package/dist/core/search/context.js +0 -0
  139. package/dist/core/search/provider.js +2 -2
  140. package/dist/core/secret-redaction.js +6 -0
  141. package/dist/core/subagents/naruto-help-contract.js +2 -1
  142. package/dist/core/subagents/naruto-host-credentials.js +161 -0
  143. package/dist/core/subagents/official-subagent-preparation.js +4 -1
  144. package/dist/core/subagents/official-subagent-prompt.js +44 -2
  145. package/dist/core/subagents/official-subagent-runner.js +26 -8
  146. package/dist/core/subagents/triwiki-attention.js +135 -123
  147. package/dist/core/telegram/bot-api.js +85 -10
  148. package/dist/core/telegram/center-snapshot.js +718 -0
  149. package/dist/core/telegram/config.js +158 -31
  150. package/dist/core/telegram/hub.js +288 -33
  151. package/dist/core/telegram/index.js +2 -0
  152. package/dist/core/telegram/ingress-spool.js +419 -0
  153. package/dist/core/telegram/ledgers.js +188 -29
  154. package/dist/core/telegram/messages.js +26 -6
  155. package/dist/core/telegram/owner-lock.js +10 -7
  156. package/dist/core/telegram/runtime-projection.js +38 -3
  157. package/dist/core/telegram/runtime.js +298 -63
  158. package/dist/core/telegram/service.js +137 -8
  159. package/dist/core/telegram/setup.js +291 -73
  160. package/dist/core/text/strings.js +3 -0
  161. package/dist/core/triwiki/code-pack.js +12 -120
  162. package/dist/core/triwiki/context-graph/compiler/cache-key.js +277 -0
  163. package/dist/core/triwiki/context-graph/compiler/extract.js +104 -0
  164. package/dist/core/triwiki/context-graph/compiler/freshness.js +108 -0
  165. package/dist/core/triwiki/context-graph/compiler/index.js +237 -0
  166. package/dist/core/triwiki/context-graph/compiler/merge.js +0 -0
  167. package/dist/core/triwiki/context-graph/compiler/serialize.js +115 -0
  168. package/dist/core/triwiki/context-graph/contracts.js +236 -0
  169. package/dist/core/triwiki/context-graph/extractors/code/builders.js +150 -0
  170. package/dist/core/triwiki/context-graph/extractors/code/declarations.js +161 -0
  171. package/dist/core/triwiki/context-graph/extractors/code/index.js +265 -0
  172. package/dist/core/triwiki/context-graph/extractors/code/inventory.js +237 -0
  173. package/dist/core/triwiki/context-graph/extractors/code/module-graph.js +252 -0
  174. package/dist/core/triwiki/context-graph/extractors/code/modules.js +79 -0
  175. package/dist/core/triwiki/context-graph/extractors/code/references.js +184 -0
  176. package/dist/core/triwiki/context-graph/extractors/code/selection.js +100 -0
  177. package/dist/core/triwiki/context-graph/extractors/code/ts-config.js +161 -0
  178. package/dist/core/triwiki/context-graph/extractors/code/types.js +18 -0
  179. package/dist/core/triwiki/context-graph/extractors/evidence/claims.js +289 -0
  180. package/dist/core/triwiki/context-graph/extractors/evidence/index.js +54 -0
  181. package/dist/core/triwiki/context-graph/extractors/evidence/proof-index.js +213 -0
  182. package/dist/core/triwiki/context-graph/extractors/evidence/proofs.js +266 -0
  183. package/dist/core/triwiki/context-graph/extractors/evidence/redaction.js +177 -0
  184. package/dist/core/triwiki/context-graph/extractors/evidence/shared.js +0 -0
  185. package/dist/core/triwiki/context-graph/extractors/evidence/sources.js +154 -0
  186. package/dist/core/triwiki/context-graph/extractors/index.js +7 -0
  187. package/dist/core/triwiki/context-graph/extractors/topology/commands.js +207 -0
  188. package/dist/core/triwiki/context-graph/extractors/topology/gate-edges.js +201 -0
  189. package/dist/core/triwiki/context-graph/extractors/topology/gates.js +182 -0
  190. package/dist/core/triwiki/context-graph/extractors/topology/globs.js +124 -0
  191. package/dist/core/triwiki/context-graph/extractors/topology/index.js +92 -0
  192. package/dist/core/triwiki/context-graph/extractors/topology/routes.js +135 -0
  193. package/dist/core/triwiki/context-graph/extractors/topology/shared.js +215 -0
  194. package/dist/core/triwiki/context-graph/graph-index.js +143 -0
  195. package/dist/core/triwiki/context-graph/ids.js +0 -0
  196. package/dist/core/triwiki/context-graph/lint/index.js +0 -0
  197. package/dist/core/triwiki/context-graph/lint/rules.js +232 -0
  198. package/dist/core/triwiki/context-graph/lint/warnings.js +93 -0
  199. package/dist/core/triwiki/context-graph/paths.js +116 -0
  200. package/dist/core/triwiki/context-graph/profiles.js +78 -0
  201. package/dist/core/triwiki/context-graph/projections/anchors.js +91 -0
  202. package/dist/core/triwiki/context-graph/projections/attention.js +58 -0
  203. package/dist/core/triwiki/context-graph/projections/code-pack-workspace.js +62 -0
  204. package/dist/core/triwiki/context-graph/projections/code-pack.js +144 -0
  205. package/dist/core/triwiki/context-graph/projections/index.js +7 -0
  206. package/dist/core/triwiki/context-graph/projections/module-view.js +140 -0
  207. package/dist/core/triwiki/context-graph/projections/node-summary.js +118 -0
  208. package/dist/core/triwiki/context-graph/projections/pack-contract.js +6 -0
  209. package/dist/core/triwiki/context-graph/query/explain.js +0 -0
  210. package/dist/core/triwiki/context-graph/query/index.js +178 -0
  211. package/dist/core/triwiki/context-graph/query/load.js +116 -0
  212. package/dist/core/triwiki/context-graph/query/pack.js +117 -0
  213. package/dist/core/triwiki/context-graph/query/rank.js +142 -0
  214. package/dist/core/triwiki/context-graph/query/ranking-config.js +64 -0
  215. package/dist/core/triwiki/context-graph/query/seeds.js +241 -0
  216. package/dist/core/triwiki/context-graph/query/snapshot-cache.js +72 -0
  217. package/dist/core/triwiki/context-graph/query/traverse.js +154 -0
  218. package/dist/core/triwiki/context-graph/query-types.js +29 -0
  219. package/dist/core/triwiki/context-graph/store/compile-lock.js +21 -0
  220. package/dist/core/triwiki/context-graph/store/event-log.js +48 -0
  221. package/dist/core/triwiki/context-graph/store/fragment-cache.js +85 -0
  222. package/dist/core/triwiki/context-graph/store/graph-status.js +85 -0
  223. package/dist/core/triwiki/context-graph/store/snapshot-store.js +155 -0
  224. package/dist/core/triwiki/triwiki-proof-bank-index-repair.js +115 -0
  225. package/dist/core/triwiki/triwiki-proof-bank-index-store.js +290 -0
  226. package/dist/core/triwiki/triwiki-proof-bank-index.js +126 -0
  227. package/dist/core/triwiki/triwiki-proof-bank.js +7 -1
  228. package/dist/core/verification/context-graph-affected.js +271 -0
  229. package/dist/core/version.js +1 -1
  230. package/dist/native/sks-menubar/Sources/AlertFactory.swift +11 -3
  231. package/dist/native/sks-menubar/Sources/AppDelegate.swift +9 -0
  232. package/dist/native/sks-menubar/Sources/ControlCenterWindowController.swift +25 -3
  233. package/dist/native/sks-menubar/Sources/ControlKit.swift +19 -8
  234. package/dist/native/sks-menubar/Sources/DiagnosticsViewController.swift +1 -2
  235. package/dist/native/sks-menubar/Sources/MCPServersViewController.swift +45 -17
  236. package/dist/native/sks-menubar/Sources/OverviewSummary.swift +162 -0
  237. package/dist/native/sks-menubar/Sources/OverviewViewController.swift +136 -190
  238. package/dist/native/sks-menubar/Sources/ProcessClient.swift +107 -11
  239. package/dist/native/sks-menubar/Sources/ProvidersMultiProvider.swift +5 -1
  240. package/dist/native/sks-menubar/Sources/ProvidersOpenRouter.swift +16 -4
  241. package/dist/native/sks-menubar/Sources/ProvidersRoleModels.swift +4 -1
  242. package/dist/native/sks-menubar/Sources/ProvidersRoutingTruth.swift +131 -0
  243. package/dist/native/sks-menubar/Sources/ProvidersViewController.swift +288 -341
  244. package/dist/native/sks-menubar/Sources/RemoteTelegramCenterState.swift +447 -0
  245. package/dist/native/sks-menubar/Sources/RemoteTelegramRendering.swift +484 -0
  246. package/dist/native/sks-menubar/Sources/RemoteTelegramViewController.swift +364 -115
  247. package/dist/native/sks-menubar/Sources/SettingsViewController.swift +73 -12
  248. package/dist/native/sks-menubar/Sources/SidebarItem.swift +3 -3
  249. package/dist/native/sks-menubar/Sources/StatusItemController.swift +13 -13
  250. package/dist/native/sks-menubar/Sources/TelegramHubSupervisor.swift +469 -0
  251. package/dist/native/sks-menubar/Sources/UpdatesViewController.swift +1 -6
  252. package/dist/native/sks-menubar/Tests/AppRuntimeTestSupport.swift +12 -0
  253. package/dist/native/sks-menubar/Tests/NativeTelegramRegressionRunner.swift +55 -0
  254. package/dist/native/sks-menubar/Tests/ProcessClientTests.swift +33 -0
  255. package/dist/native/sks-menubar/Tests/ProvidersViewControllerTests.swift +142 -0
  256. package/dist/native/sks-menubar/Tests/RemoteTelegramCenterStateTests.swift +56 -0
  257. package/dist/native/sks-menubar/Tests/TelegramHubSupervisorTests.swift +55 -0
  258. package/dist/scripts/codex-lb-catalog-passthrough-check.js +120 -0
  259. package/dist/scripts/codex-lb-desktop-auth-invariant-check.js +169 -0
  260. package/dist/scripts/codex-lb-desktop-bridge-check.js +394 -0
  261. package/dist/scripts/codex-lb-desktop-bridge-latency-check.js +196 -0
  262. package/dist/scripts/codex-lb-desktop-capabilities-check.js +287 -0
  263. package/dist/scripts/codex-lb-desktop-real-evidence-check.js +123 -0
  264. package/dist/scripts/codex-lb-fast-mode-truth-check.js +10 -3
  265. package/dist/scripts/codex-lb-fast-ui-preservation-check.js +81 -40
  266. package/dist/scripts/codex-lb-gpt56-fast-profile-check.js +99 -37
  267. package/dist/scripts/context-graph-check.js +191 -0
  268. package/dist/scripts/doctor-fixes-codex-app-fast-ui-check.js +1 -1
  269. package/dist/scripts/gate-policy-audit-check.js +1 -1
  270. package/dist/scripts/helper-dedup-check.js +0 -2
  271. package/dist/scripts/installed-package-smoke-check.js +4 -1
  272. package/dist/scripts/latest-version-guidance-check.js +74 -0
  273. package/dist/scripts/release-metadata-check.js +21 -4
  274. package/dist/scripts/release-real-check.js +7 -0
  275. package/dist/scripts/ux-review-imagegen-blackbox-check.js +25 -1
  276. package/dist/scripts/ux-review-run-wires-imagegen-check.js +9 -1
  277. package/package.json +8 -3
  278. package/release-gates.v2.json +176 -1
  279. package/runtime-required-scripts.json +30 -0
  280. package/schemas/triwiki/context-graph.schema.json +119 -0
  281. package/dist/cli/install-helpers-codex-lb-selftest-chain.js +0 -112
  282. package/dist/cli/install-helpers-codex-lb-selftest.js +0 -481
  283. package/dist/core/triwiki/code-index-scanner.js +0 -309
@@ -16,6 +16,9 @@ enum NativeView {
16
16
  static func title(_ value: String) -> NSTextField {
17
17
  let field = NSTextField(labelWithString: value)
18
18
  field.font = NSFont.systemFont(ofSize: 18, weight: .semibold)
19
+ field.alignment = .left
20
+ field.setAccessibilityLabel(value)
21
+ field.setAccessibilityIdentifier("sks-center-heading-\(identifier(value))")
19
22
  return field
20
23
  }
21
24
 
@@ -23,12 +26,18 @@ enum NativeView {
23
26
  let field = NSTextField(wrappingLabelWithString: value)
24
27
  field.font = NSFont.systemFont(ofSize: 12)
25
28
  field.textColor = .secondaryLabelColor
29
+ field.alignment = .left
30
+ // Long status and help copy must wrap inside the current window instead
31
+ // of contributing an intrinsic minimum width while users change pages.
32
+ field.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
26
33
  return field
27
34
  }
28
35
 
29
36
  static func sectionTitle(_ value: String) -> NSTextField {
30
37
  let field = NSTextField(labelWithString: value)
31
38
  field.font = NSFont.systemFont(ofSize: 14, weight: .semibold)
39
+ field.alignment = .left
40
+ field.setAccessibilityLabel(value)
32
41
  return field
33
42
  }
34
43
 
@@ -36,6 +45,7 @@ enum NativeView {
36
45
  let button = NSButton(title: title, target: target, action: action)
37
46
  button.bezelStyle = .rounded
38
47
  button.setAccessibilityLabel(title)
48
+ button.setAccessibilityIdentifier("sks-center-button-\(identifier(title))")
39
49
  return button
40
50
  }
41
51
 
@@ -51,27 +61,34 @@ enum NativeView {
51
61
  static func page(_ views: [NSView]) -> NSStackView {
52
62
  let stack = stack(views)
53
63
  stack.alignment = .width
64
+ for view in stack.arrangedSubviews {
65
+ view.widthAnchor.constraint(equalTo: stack.widthAnchor, constant: -48).isActive = true
66
+ }
54
67
  return stack
55
68
  }
56
69
 
57
70
  static func row(_ views: [NSView], spacing: CGFloat = 8) -> NSStackView {
58
71
  let row = NSStackView(views: views)
59
72
  row.orientation = .horizontal; row.alignment = .centerY; row.spacing = spacing
73
+ row.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
60
74
  return row
61
75
  }
62
76
 
63
- static func card(title: String, subtitle: String, views: [NSView]) -> NSBox {
77
+ static func card(title: String, subtitle: String, views: [NSView], fullWidthLeadingContent: Bool = false) -> NSBox {
64
78
  let box = NSBox()
65
79
  box.boxType = .custom; box.titlePosition = .noTitle
66
80
  box.cornerRadius = 10; box.borderWidth = 1
67
81
  box.borderColor = .separatorColor; box.fillColor = .controlBackgroundColor
68
- let heading = sectionTitle(title)
69
- let help = detail(subtitle)
82
+ box.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
83
+ let heading = sectionTitle(title); let help = detail(subtitle)
70
84
  let content = NSStackView(views: [heading, help] + views)
71
- content.orientation = .vertical
72
- content.alignment = .width
73
- content.spacing = 10
74
- content.edgeInsets = NSEdgeInsets(top: 16, left: 16, bottom: 16, right: 16)
85
+ content.orientation = .vertical; content.alignment = .width
86
+ content.spacing = 10; content.edgeInsets = NSEdgeInsets(top: 16, left: 16, bottom: 16, right: 16)
87
+ content.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
88
+ for view in content.arrangedSubviews {
89
+ view.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
90
+ view.widthAnchor.constraint(equalTo: content.widthAnchor, constant: -32).isActive = true
91
+ }
75
92
  content.translatesAutoresizingMaskIntoConstraints = false
76
93
  box.contentView?.addSubview(content)
77
94
  if let host = box.contentView {
@@ -83,9 +100,39 @@ enum NativeView {
83
100
  ])
84
101
  }
85
102
  box.setAccessibilityLabel(title)
103
+ box.setAccessibilityHelp(subtitle)
104
+ box.setAccessibilityRole(.group)
105
+ box.setAccessibilityIdentifier("sks-center-card-\(identifier(title))")
86
106
  return box
87
107
  }
88
108
 
109
+ static func badge(_ text: String, color: NSColor) -> NSView {
110
+ let dot = NSTextField(labelWithString: "●")
111
+ dot.font = NSFont.systemFont(ofSize: 10)
112
+ dot.textColor = color
113
+ dot.setAccessibilityHidden(true)
114
+ let label = NSTextField(labelWithString: text)
115
+ label.font = NSFont.systemFont(ofSize: 12, weight: .medium)
116
+ label.alignment = .left
117
+ label.lineBreakMode = .byTruncatingTail
118
+ label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
119
+ label.setAccessibilityLabel(text)
120
+ let row = NSStackView(views: [dot, label])
121
+ row.orientation = .horizontal
122
+ row.alignment = .centerY
123
+ row.spacing = 5
124
+ return row
125
+ }
126
+
127
+ static func setBadge(_ view: NSView, text: String, color: NSColor) {
128
+ guard let row = view as? NSStackView, row.arrangedSubviews.count >= 2,
129
+ let dot = row.arrangedSubviews[0] as? NSTextField,
130
+ let label = row.arrangedSubviews[1] as? NSTextField else { return }
131
+ dot.textColor = color
132
+ label.stringValue = text
133
+ label.setAccessibilityLabel(text)
134
+ }
135
+
89
136
  static func spinner(label: String) -> NSProgressIndicator {
90
137
  let indicator = NSProgressIndicator()
91
138
  indicator.style = .spinning
@@ -100,10 +147,13 @@ enum NativeView {
100
147
  scroll.drawsBackground = false
101
148
  scroll.hasVerticalScroller = true
102
149
  scroll.hasHorizontalScroller = false
150
+ scroll.scrollerStyle = .overlay
103
151
  scroll.borderType = .noBorder
104
152
  scroll.autohidesScrollers = true
105
153
  scroll.translatesAutoresizingMaskIntoConstraints = false
154
+ scroll.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
106
155
  document.translatesAutoresizingMaskIntoConstraints = false
156
+ document.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
107
157
  scroll.documentView = document
108
158
  if let content = scroll.contentView.documentView {
109
159
  NSLayoutConstraint.activate([
@@ -121,166 +171,12 @@ enum NativeView {
121
171
  if compact.count <= limit { return compact }
122
172
  return String(compact.prefix(limit)) + "…"
123
173
  }
124
- }
125
-
126
- enum OverviewSummary {
127
- static func render(
128
- update: [String: Any]?,
129
- mcp: [String: Any]?,
130
- telegram: [String: Any]?,
131
- menuBarBuild: String,
132
- codexRunning: Bool?,
133
- operationSummary: String
134
- ) -> String {
135
- let codexAppState = codexRunning.map { $0 ? "Running" : "Not running" } ?? "Not configured"
136
- var lines: [String] = []
137
- let update = validatedUpdate(update)
138
- let mcp = validatedMCP(mcp)
139
- let telegram = validatedTelegram(telegram)
140
-
141
- if let update = update {
142
- let sks = update["sks"] as? [String: Any]
143
- let codex = update["codex_cli"] as? [String: Any]
144
- let menu = update["menubar"] as? [String: Any]
145
- lines.append("SKS install: \(versionSummary(sks))")
146
- lines.append("Codex CLI: \(versionSummary(codex)) · Codex app: \(codexAppState)")
147
- if let inducement = codexUpdateInducement(codex) {
148
- lines.append(inducement)
149
- }
150
-
151
- var menuParts = ["running build \(menuBarBuild)"]
152
- if let expected = menu?["expected_version"] as? String { menuParts.append("expected \(expected)") }
153
- if let installed = menu?["installed_version"] as? String, installed != menuBarBuild {
154
- menuParts.append("snapshot installed \(installed)")
155
- }
156
- if let rebuildRequired = menu?["rebuild_required"] as? Bool {
157
- menuParts.append(rebuildRequired ? "rebuild required" : "current")
158
- } else {
159
- menuParts.append("rebuild state unknown")
160
- }
161
- menuParts.append("signature \(verificationState(menu?["signature_ok"]))")
162
- menuParts.append("resources \(verificationState(menu?["resources_ok"]))")
163
- lines.append("Menu Bar: \(menuParts.joined(separator: " · "))")
164
-
165
- let source = snapshotSource(update["source"] as? String)
166
- var updateParts = [integer(update["update_count"]).map { "\($0) pending" } ?? "pending count unknown", "\(source) snapshot"]
167
- if let error = diagnosticNotice(update["public_error"] as? String, update: update) {
168
- updateParts.append("notice: \(error)")
169
- } else if let warnings = update["warnings"] as? [String], !warnings.isEmpty {
170
- updateParts.append("\(warnings.count) warning\(warnings.count == 1 ? "" : "s")")
171
- }
172
- lines.append("Updates: \(updateParts.joined(separator: " · "))")
173
- } else {
174
- lines.append("SKS install: unavailable")
175
- lines.append("Codex CLI: unavailable · Codex app: \(codexAppState)")
176
- lines.append("Menu Bar: running build \(menuBarBuild) · update status unavailable")
177
- lines.append("Updates: unavailable")
178
- }
179
-
180
- if let mcp = mcp,
181
- let enabled = integer(mcp["enabled_count"]),
182
- let failed = integer(mcp["failed_count"]) {
183
- lines.append("MCP: \(enabled) enabled · \(failed) failed")
184
- } else {
185
- lines.append("MCP: unavailable")
186
- }
187
-
188
- if let telegram = telegram {
189
- let owner = telegram["owner"] as? [String: Any]
190
- let configured = telegram["configured"] as? Bool
191
- let hubState = owner != nil ? "Running" : configured == true ? "Stopped" : configured == false ? "Not configured" : "Unknown"
192
- let machines = integer(telegram["machine_count"]).map { "\($0) registered Macs" } ?? "registered Macs unknown"
193
- let targets = integer(telegram["target_count"]).map { "\($0) configured targets" } ?? "configured targets unknown"
194
- let issues = ((telegram["config_issues"] as? [String]) ?? []).count
195
- + ((telegram["remote_config_issues"] as? [String]) ?? []).count
196
- var fleet = "Remote fleet: \(machines) · \(targets)"
197
- if issues > 0 { fleet += " · \(issues) issue\(issues == 1 ? "" : "s")" }
198
- lines.append("Telegram Hub: \(hubState) · \(fleet)")
199
- } else {
200
- lines.append("Telegram Hub: unavailable · Remote fleet: unavailable")
201
- }
202
-
203
- lines.append("Last operation: \(operationSummary) · Logs and snapshots use mode 0600")
204
- return lines.joined(separator: "\n")
205
- }
206
-
207
- private static func validatedUpdate(_ value: [String: Any]?) -> [String: Any]? {
208
- guard value?["schema"] as? String == "sks.update-status.v3",
209
- value?["source"] is String,
210
- integer(value?["update_count"]) != nil,
211
- let sks = value?["sks"] as? [String: Any],
212
- let codex = value?["codex_cli"] as? [String: Any],
213
- let menu = value?["menubar"] as? [String: Any],
214
- sks["update_available"] is Bool,
215
- codex["update_available"] is Bool,
216
- menu["expected_version"] is String,
217
- menu["rebuild_required"] is Bool else { return nil }
218
- return value
219
- }
220
-
221
- private static func validatedMCP(_ value: [String: Any]?) -> [String: Any]? {
222
- guard value?["schema"] as? String == "sks.mcp-inventory.v2",
223
- integer(value?["enabled_count"]) != nil,
224
- integer(value?["failed_count"]) != nil else { return nil }
225
- return value
226
- }
227
-
228
- private static func validatedTelegram(_ value: [String: Any]?) -> [String: Any]? {
229
- guard value?["schema"] as? String == "sks.telegram-status.v1",
230
- value?["configured"] is Bool,
231
- integer(value?["machine_count"]) != nil,
232
- integer(value?["target_count"]) != nil,
233
- value?["config_issues"] is [String],
234
- value?["remote_config_issues"] is [String] else { return nil }
235
- return value
236
- }
237
-
238
- private static func versionSummary(_ value: [String: Any]?) -> String {
239
- guard let current = nonEmpty(value?["current"] as? String) else { return "not detected" }
240
- guard let latest = nonEmpty(value?["latest"] as? String) else { return current }
241
- if value?["update_available"] as? Bool == true, latest != current { return "\(current) → \(latest) available" }
242
- if latest == current { return "\(current) (current)" }
243
- return "\(current) · registry last seen \(latest)"
244
- }
245
174
 
246
- private static func codexUpdateInducement(_ value: [String: Any]?) -> String? {
247
- guard value?["update_available"] as? Bool == true else { return nil }
248
- let current = nonEmpty(value?["current"] as? String) ?? "installed"
249
- let latest = nonEmpty(value?["latest"] as? String) ?? "preferred latest"
250
- return "Action: update Codex CLI (\(current) → \(latest)) from Updates, or choose Update Codex CLI Now in the menu bar."
251
- }
252
-
253
- private static func verificationState(_ value: Any?) -> String {
254
- guard let value = value as? Bool else { return "unknown" }
255
- return value ? "verified" : "needs attention"
256
- }
257
-
258
- private static func snapshotSource(_ value: String?) -> String {
259
- guard let value = nonEmpty(value) else { return "unknown" }
260
- return value.replacingOccurrences(of: "_", with: " ")
261
- }
262
-
263
- private static func diagnosticNotice(_ value: String?, update: [String: Any]) -> String? {
264
- guard let value = nonEmpty(value) else { return nil }
265
- let versions = [
266
- (update["sks"] as? [String: Any])?["current"] as? String,
267
- (update["sks"] as? [String: Any])?["latest"] as? String,
268
- (update["codex_cli"] as? [String: Any])?["current"] as? String,
269
- (update["codex_cli"] as? [String: Any])?["latest"] as? String
270
- ].compactMap { $0 }
271
- if versions.contains(value) { return nil }
272
- if value.range(of: #"^v?\d+(?:\.\d+){1,3}(?:[-+][A-Za-z0-9.-]+)?$"#, options: .regularExpression) != nil { return nil }
273
- return value
274
- }
275
-
276
- private static func integer(_ value: Any?) -> Int? {
277
- if let value = value as? Int { return value }
278
- return (value as? NSNumber)?.intValue
279
- }
280
-
281
- private static func nonEmpty(_ value: String?) -> String? {
282
- guard let value = value?.trimmingCharacters(in: .whitespacesAndNewlines), !value.isEmpty else { return nil }
283
- return value
175
+ private static func identifier(_ value: String) -> String {
176
+ value.lowercased()
177
+ .components(separatedBy: CharacterSet.alphanumerics.inverted)
178
+ .filter { !$0.isEmpty }
179
+ .joined(separator: "-")
284
180
  }
285
181
  }
286
182
 
@@ -289,7 +185,10 @@ final class OverviewViewController: NSViewController, ControlCenterPage {
289
185
  private let operations: OperationCoordinator
290
186
  private let status = NativeView.detail("Loading local SKS status…")
291
187
  private let notificationInbox = NativeView.detail("Notifications: checking authorization…")
188
+ private let healthBadge = NativeView.badge("Checking local status", color: .systemBlue)
189
+ private let statusSpinner = NativeView.spinner(label: "Checking SKS Center status")
292
190
  private var generation = 0
191
+ private var completedGeneration = 0
293
192
  private var doctorButton: NSButton!
294
193
  private var refreshButton: NSButton!
295
194
  private var updateCodexButton: NSButton!
@@ -308,28 +207,32 @@ final class OverviewViewController: NSViewController, ControlCenterPage {
308
207
  refreshButton = NativeView.button("Refresh", target: self, action: #selector(refreshStatus))
309
208
  updateCodexButton = NativeView.button("Update Codex CLI", target: self, action: #selector(updateCodexCLI))
310
209
  updateCodexButton.setAccessibilityHelp("Update the operator Codex CLI to the preferred latest channel.")
311
- let buttons = NSStackView(views: [doctorButton, refreshButton, updateCodexButton])
312
- buttons.orientation = .horizontal
313
- buttons.spacing = 8
314
- let shortcuts = NSStackView(views: [
210
+ let buttons = NativeView.row([refreshButton, doctorButton, updateCodexButton])
211
+ let shortcuts = NativeView.row([
212
+ NativeView.button("Set Up Telegram…", target: self, action: #selector(openTelegram)),
315
213
  NativeView.button("Providers…", target: self, action: #selector(openProviders)),
316
214
  NativeView.button("Updates…", target: self, action: #selector(openUpdates)),
317
215
  NativeView.button("Diagnostics…", target: self, action: #selector(openDiagnostics))
318
216
  ])
319
- shortcuts.orientation = .horizontal
320
- shortcuts.spacing = 8
321
217
  shortcuts.setAccessibilityLabel("Open Control Center sections")
322
- view = NativeView.stack([
218
+ let healthCard = NativeView.card(
219
+ title: "System health",
220
+ subtitle: "A bounded local snapshot of versions, services, and the latest operation.",
221
+ views: [NativeView.row([healthBadge, statusSpinner]), status, buttons]
222
+ )
223
+ let nextStepsCard = NativeView.card(
224
+ title: "Notifications & next steps",
225
+ subtitle: "Results remain available here even when macOS notifications are disabled.",
226
+ views: [notificationInbox, shortcuts]
227
+ )
228
+ view = NativeView.page([
323
229
  NativeView.title("Overview"),
324
230
  NativeView.detail("Menu Bar build \(AppRuntime.packageVersion) · Local health for SKS, Codex CLI, MCP, Remote, and operations. Prefer the latest Codex CLI; SKS stays version-agnostic and capability-gates features."),
325
- status,
326
- notificationInbox,
327
- buttons,
328
- shortcuts
231
+ healthCard,
232
+ nextStepsCard
329
233
  ])
330
- loadStatus(forceUpdateRefresh: false)
331
234
  }
332
-
235
+ @objc private func openTelegram() { openSection?("Remote & Telegram") }
333
236
  @objc private func openProviders() { openSection?("Providers") }
334
237
  @objc private func openUpdates() { openSection?("Updates") }
335
238
  @objc private func openDiagnostics() { openSection?("Diagnostics") }
@@ -353,9 +256,10 @@ final class OverviewViewController: NSViewController, ControlCenterPage {
353
256
  status.stringValue = "Another update or MCP mutation is already running. Open Updates to review it."
354
257
  return
355
258
  }
356
- updateCodexButton?.isEnabled = false
357
- doctorButton?.isEnabled = false
358
- refreshButton?.isEnabled = false
259
+ generation += 1
260
+ setActionBusy(true)
261
+ NativeView.setBadge(healthBadge, text: "Updating Codex CLI", color: .systemBlue)
262
+ statusSpinner.startAnimation(nil)
359
263
  status.stringValue = "Updating Codex CLI to the preferred latest…"
360
264
  _ = operations.update(operation, state: .running, stage: "running", progress: nil, summary: status.stringValue)
361
265
  processClient.run(["codex", "update", "--json"], timeout: nil) { [weak self] result in
@@ -371,9 +275,8 @@ final class OverviewViewController: NSViewController, ControlCenterPage {
371
275
  progress: 1,
372
276
  summary: ok ? "Codex CLI update completed" : "Codex CLI update failed"
373
277
  )
374
- self.updateCodexButton?.isEnabled = true
375
- self.doctorButton?.isEnabled = true
376
- self.refreshButton?.isEnabled = true
278
+ self.setActionBusy(false)
279
+ self.statusSpinner.stopAnimation(nil)
377
280
  self.status.stringValue = ok
378
281
  ? "Codex CLI update completed. Refreshing shared update status…"
379
282
  : "Codex CLI update needs attention. Open Updates or Diagnostics for structured guidance."
@@ -385,6 +288,8 @@ final class OverviewViewController: NSViewController, ControlCenterPage {
385
288
  generation += 1
386
289
  let requestGeneration = generation
387
290
  refreshButton?.isEnabled = false
291
+ NativeView.setBadge(healthBadge, text: "Checking local status", color: .systemBlue)
292
+ statusSpinner.startAnimation(nil)
388
293
  status.stringValue = "Checking versions, MCP servers, Telegram Hub, remote fleet, and operations…"
389
294
  var update: [String: Any]?
390
295
  var mcp: [String: Any]?
@@ -422,30 +327,71 @@ final class OverviewViewController: NSViewController, ControlCenterPage {
422
327
  }
423
328
  group.notify(queue: .main) { [weak self] in
424
329
  guard let self = self, self.generation == requestGeneration else { return }
330
+ self.completedGeneration = requestGeneration
425
331
  self.refreshButton?.isEnabled = true
426
- self.status.stringValue = self.summary(update: update, mcp: mcp, telegram: telegram)
332
+ self.statusSpinner.stopAnimation(nil)
333
+ self.renderStatus(update: update, mcp: mcp, telegram: telegram, partial: false)
427
334
  }
428
335
  DispatchQueue.main.asyncAfter(deadline: .now() + 5) { [weak self] in
429
- guard let self = self, self.generation == requestGeneration else { return }
336
+ guard let self = self,
337
+ self.generation == requestGeneration,
338
+ self.completedGeneration != requestGeneration else { return }
430
339
  self.refreshButton?.isEnabled = true
431
- self.status.stringValue = self.summary(update: update, mcp: mcp, telegram: telegram)
340
+ self.renderStatus(update: update, mcp: mcp, telegram: telegram, partial: true)
432
341
  }
433
342
  }
434
343
 
435
344
  @objc private func doctor() {
436
- doctorButton?.isEnabled = false
345
+ guard let operation = operations.begin(kind: "doctor", mutationGroup: nil, summary: "Run Doctor") else {
346
+ status.stringValue = "Another guarded operation is already running. Open Diagnostics to review it."
347
+ return
348
+ }
349
+ generation += 1
350
+ setActionBusy(true)
351
+ NativeView.setBadge(healthBadge, text: "Doctor is running", color: .systemBlue)
352
+ statusSpinner.startAnimation(nil)
437
353
  status.stringValue = "Doctor is running…"
354
+ _ = operations.update(operation, state: .running, stage: "running", progress: nil, summary: status.stringValue)
438
355
  processClient.run(["doctor", "--json"], timeout: NativeView.mutationTimeout) { [weak self] result in
439
356
  guard let self = self else { return }
440
- self.doctorButton?.isEnabled = true
357
+ self.setActionBusy(false)
358
+ self.statusSpinner.stopAnimation(nil)
359
+ _ = self.operations.update(
360
+ operation,
361
+ state: result.code == 0 ? .succeeded : .failed,
362
+ stage: "complete",
363
+ progress: 1,
364
+ summary: result.code == 0 ? "Doctor completed" : "Doctor found an issue"
365
+ )
441
366
  if result.code == 0 {
367
+ NativeView.setBadge(self.healthBadge, text: "Doctor completed", color: .systemGreen)
442
368
  self.status.stringValue = "Doctor completed. No blocking issue was reported."
443
369
  } else {
370
+ NativeView.setBadge(self.healthBadge, text: "Doctor needs attention", color: .systemOrange)
444
371
  self.status.stringValue = "Doctor found an issue. Open Diagnostics · \(NativeView.redactPreview(result.output))"
445
372
  }
446
373
  }
447
374
  }
448
375
 
376
+ private func setActionBusy(_ busy: Bool) {
377
+ updateCodexButton?.isEnabled = !busy
378
+ doctorButton?.isEnabled = !busy
379
+ refreshButton?.isEnabled = !busy
380
+ }
381
+
382
+ private func renderStatus(update: [String: Any]?, mcp: [String: Any]?, telegram: [String: Any]?, partial: Bool) {
383
+ let rendered = summary(update: update, mcp: mcp, telegram: telegram)
384
+ status.stringValue = rendered
385
+ if partial {
386
+ NativeView.setBadge(healthBadge, text: "Partial status · still checking", color: .systemBlue)
387
+ } else if rendered.localizedCaseInsensitiveContains("unavailable")
388
+ || rendered.localizedCaseInsensitiveContains("needs attention") {
389
+ NativeView.setBadge(healthBadge, text: "Status refreshed · attention needed", color: .systemOrange)
390
+ } else {
391
+ NativeView.setBadge(healthBadge, text: "Status refreshed", color: .systemGreen)
392
+ }
393
+ }
394
+
449
395
  private func summary(update: [String: Any]?, mcp: [String: Any]?, telegram: [String: Any]?) -> String {
450
396
  let codexRunning = AppRuntime.codexBundleId.map { bundle in
451
397
  NSWorkspace.shared.runningApplications.contains { $0.bundleIdentifier == bundle }
@@ -5,9 +5,13 @@ struct ProcessResult {
5
5
  let code: Int32
6
6
  let output: String
7
7
  let truncated: Bool
8
+ let timedOut: Bool
9
+ let emptyOutput: Bool
8
10
  }
9
11
 
10
12
  final class ProcessClient {
13
+ static let telegramStatusTimeout: TimeInterval = 12
14
+
11
15
  private let actionScript: String
12
16
  private let logPath: String
13
17
  private let projectRoot: String
@@ -24,12 +28,17 @@ final class ProcessClient {
24
28
  stdin: String? = nil,
25
29
  environment: [String: String] = [:],
26
30
  timeout: TimeInterval? = nil,
31
+ maxOutputBytes: Int? = nil,
32
+ logOutput: Bool = true,
27
33
  completion: @escaping (ProcessResult) -> Void
28
34
  ) {
29
35
  let process = Process()
30
36
  let output = Pipe()
37
+ let effectiveOutputLimit = max(1024, min(1024 * 1024, maxOutputBytes ?? outputLimit))
31
38
  let sensitiveValues = sensitiveStdinValues(arguments: arguments, stdin: stdin)
32
39
  let childEnvironment = ProcessInfo.processInfo.environment.merging(environment) { _, override in override }
40
+ let effectiveTimeout = Self.alignedTimeout(arguments: arguments, requested: timeout)
41
+ let execution = ProcessExecutionState()
33
42
  process.executableURL = URL(fileURLWithPath: actionScript)
34
43
  process.arguments = arguments
35
44
  if !environment.isEmpty {
@@ -60,9 +69,10 @@ final class ProcessClient {
60
69
  input.fileHandleForWriting.closeFile()
61
70
  }
62
71
  var timeoutWorkItem: DispatchWorkItem?
63
- if let timeout = timeout, timeout > 0 {
72
+ if let timeout = effectiveTimeout, timeout > 0 {
64
73
  let item = DispatchWorkItem {
65
74
  guard process.isRunning else { return }
75
+ execution.markTimedOut()
66
76
  let pid = process.processIdentifier
67
77
  process.terminate()
68
78
  DispatchQueue.global(qos: .utility).asyncAfter(deadline: .now() + 1) {
@@ -76,17 +86,43 @@ final class ProcessClient {
76
86
  let data = output.fileHandleForReading.readDataToEndOfFile()
77
87
  process.waitUntilExit()
78
88
  timeoutWorkItem?.cancel()
79
- let truncated = data.count > self.outputLimit
80
- let bounded = Data(data.suffix(self.outputLimit))
89
+ let truncated = data.count > effectiveOutputLimit
90
+ let bounded = Data(data.suffix(effectiveOutputLimit))
81
91
  let rawText = String(data: bounded, encoding: .utf8) ?? ""
82
- let text = self.publicOutput(rawText, code: process.terminationStatus, sensitiveValues: sensitiveValues)
83
- self.writeLog(command: arguments, output: text, sensitiveValues: sensitiveValues)
84
- DispatchQueue.main.async { completion(ProcessResult(code: process.terminationStatus, output: text, truncated: truncated)) }
92
+ let timedOut = execution.timedOut
93
+ let emptyOutput = !timedOut
94
+ && rawText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
95
+ let resultCode: Int32 = timedOut ? -2 : emptyOutput ? -3 : process.terminationStatus
96
+ let text = timedOut
97
+ ? self.timeoutOutput(rawText, sensitiveValues: sensitiveValues)
98
+ : emptyOutput
99
+ ? Self.nativeFailureOutput("native_process_empty_output")
100
+ : self.publicOutput(rawText, code: resultCode, sensitiveValues: sensitiveValues)
101
+ if logOutput {
102
+ self.writeLog(command: arguments, output: text, sensitiveValues: sensitiveValues)
103
+ }
104
+ DispatchQueue.main.async {
105
+ completion(ProcessResult(
106
+ code: resultCode,
107
+ output: text,
108
+ truncated: truncated,
109
+ timedOut: timedOut,
110
+ emptyOutput: emptyOutput
111
+ ))
112
+ }
85
113
  }
86
114
  } catch {
87
115
  let text = publicOutput(String(describing: error), code: -1, sensitiveValues: sensitiveValues)
88
- writeLog(command: arguments, output: text, sensitiveValues: sensitiveValues)
89
- completion(ProcessResult(code: -1, output: text, truncated: false))
116
+ if logOutput {
117
+ writeLog(command: arguments, output: text, sensitiveValues: sensitiveValues)
118
+ }
119
+ completion(ProcessResult(
120
+ code: -1,
121
+ output: text,
122
+ truncated: false,
123
+ timedOut: false,
124
+ emptyOutput: false
125
+ ))
90
126
  }
91
127
  }
92
128
 
@@ -108,6 +144,24 @@ final class ProcessClient {
108
144
  return URL(fileURLWithPath: home, isDirectory: true)
109
145
  }
110
146
 
147
+ static func alignedTimeout(arguments: [String], requested: TimeInterval?) -> TimeInterval? {
148
+ guard arguments.starts(with: ["telegram", "status"]) else { return requested }
149
+ return max(requested ?? 0, telegramStatusTimeout)
150
+ }
151
+
152
+ static func nativeFailureOutput(_ error: String) -> String {
153
+ #"{"schema":"sks.native-process-error.v1","ok":false,"error":"\#(error)"}"#
154
+ }
155
+
156
+ private func timeoutOutput(_ partialOutput: String, sensitiveValues: [String]) -> String {
157
+ let failure = Self.nativeFailureOutput("native_process_timeout")
158
+ guard !partialOutput.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
159
+ return failure
160
+ }
161
+ let partial = publicOutput(partialOutput, code: -2, sensitiveValues: sensitiveValues)
162
+ return "\(partial)\n\(failure)"
163
+ }
164
+
111
165
  func redact(_ value: String, sensitiveValues: [String] = []) -> String {
112
166
  var text = value
113
167
  for sensitiveValue in sensitiveValues where !sensitiveValue.isEmpty {
@@ -119,7 +173,7 @@ final class ProcessClient {
119
173
  #"github_pat_[A-Za-z0-9_]{20,}"#,
120
174
  #"eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}"#,
121
175
  #"(?i)(api[_-]?key|secret|token)\s*[:=]\s*[^\s\"',}]+"#,
122
- #"/Users/[^/\s]+"#,
176
+ #"(?:\\/|/)Users(?:\\/|/)[A-Za-z0-9._-]+"#,
123
177
  NSRegularExpression.escapedPattern(for: projectRoot)
124
178
  ]
125
179
  for pattern in patterns where !pattern.isEmpty {
@@ -140,9 +194,34 @@ final class ProcessClient {
140
194
  private func publicOutput(_ value: String, code: Int32, sensitiveValues: [String]) -> String {
141
195
  let redacted = redact(value, sensitiveValues: sensitiveValues)
142
196
  guard sensitiveValues.isEmpty else {
197
+ let payload = extractJsonPayload(redacted)
198
+ var envelope: [String: Any] = [
199
+ "schema": "sks.secure-input-operation.v1",
200
+ "ok": code == 0,
201
+ "output_suppressed": true
202
+ ]
203
+ if let data = payload.data(using: .utf8),
204
+ let object = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {
205
+ if let sourceSchema = object["schema"] as? String, !sourceSchema.isEmpty {
206
+ envelope["source_schema"] = String(sourceSchema.prefix(120))
207
+ }
208
+ if let status = object["status"] as? String, !status.isEmpty {
209
+ envelope["status"] = String(status.prefix(160))
210
+ }
211
+ if code != 0, let error = object["error"] as? String, !error.isEmpty {
212
+ envelope["error"] = String(error.prefix(240))
213
+ }
214
+ }
215
+ if code != 0, envelope["error"] == nil {
216
+ envelope["error"] = "secure_input_operation_failed_exit_\(code)"
217
+ }
218
+ if let data = try? JSONSerialization.data(withJSONObject: envelope, options: [.sortedKeys]),
219
+ let text = String(data: data, encoding: .utf8) {
220
+ return text
221
+ }
143
222
  return code == 0
144
- ? "Secure input operation completed. Child output was suppressed."
145
- : "Secure input operation failed with exit code \(code). Child output was suppressed."
223
+ ? #"{"schema":"sks.secure-input-operation.v1","ok":true,"output_suppressed":true}"#
224
+ : #"{"schema":"sks.secure-input-operation.v1","ok":false,"error":"secure_input_operation_failed","output_suppressed":true}"#
146
225
  }
147
226
  return extractJsonPayload(redacted)
148
227
  }
@@ -163,3 +242,20 @@ final class ProcessClient {
163
242
  try? FileManager.default.setAttributes([.posixPermissions: 0o600], ofItemAtPath: logPath)
164
243
  }
165
244
  }
245
+
246
+ private final class ProcessExecutionState {
247
+ private let lock = NSLock()
248
+ private var didTimeOut = false
249
+
250
+ var timedOut: Bool {
251
+ lock.lock()
252
+ defer { lock.unlock() }
253
+ return didTimeOut
254
+ }
255
+
256
+ func markTimedOut() {
257
+ lock.lock()
258
+ didTimeOut = true
259
+ lock.unlock()
260
+ }
261
+ }