@ouro.bot/cli 0.1.0-alpha.83 → 0.1.0-alpha.830

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 (647) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +154 -23
  3. package/RepairGuide.ouro/agent.json +5 -0
  4. package/RepairGuide.ouro/psyche/IDENTITY.md +19 -0
  5. package/RepairGuide.ouro/psyche/SOUL.md +55 -0
  6. package/RepairGuide.ouro/skills/diagnose-broken-remote.md +63 -0
  7. package/RepairGuide.ouro/skills/diagnose-stacked-typed-issues.md +35 -0
  8. package/RepairGuide.ouro/skills/diagnose-sync-blocked.md +54 -0
  9. package/RepairGuide.ouro/skills/diagnose-vault-expired.md +60 -0
  10. package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/agent.json +4 -2
  11. package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/SOUL.md +2 -2
  12. package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/the-serpent.md +1 -1
  13. package/assets/bluebubbles-host +264 -0
  14. package/assets/sanctuary-host-launcher.sh +16 -0
  15. package/changelog.json +5302 -16
  16. package/deploy/unraid/Dockerfile +54 -0
  17. package/deploy/unraid/README.txt +2983 -0
  18. package/deploy/unraid/audit-container-spec.sh +4 -0
  19. package/deploy/unraid/container-runtime.json +1 -0
  20. package/deploy/unraid/docker-man-template-transaction.mjs +711 -0
  21. package/deploy/unraid/docker-man-template-xml.cjs +105 -0
  22. package/deploy/unraid/migrate-sanctuary-bundle.mjs +60 -0
  23. package/deploy/unraid/ouro-events/bootstrap-spool.sh +92 -0
  24. package/deploy/unraid/ouro-events/emit-event.mjs +310 -0
  25. package/deploy/unraid/ouro-events/emit-usenet-event.sh +54 -0
  26. package/deploy/unraid/ouro-events/install-usenet-guard.sh +435 -0
  27. package/deploy/unraid/ouro-events/usenet-health.sh +176 -0
  28. package/deploy/unraid/sanctuary-acceptance-adapter.sh +43 -0
  29. package/deploy/unraid/sanctuary-acceptance-contract.json +198 -0
  30. package/deploy/unraid/sanctuary-acceptance-harness.sh +23 -0
  31. package/deploy/unraid/sanctuary-authority-installation.json +36 -0
  32. package/deploy/unraid/sanctuary-authority-service.sh +30 -0
  33. package/deploy/unraid/sanctuary-deployment-target.mjs +710 -0
  34. package/deploy/unraid/sanctuary-safe-rename.py +24 -0
  35. package/deploy/unraid/sanctuary-unit16-host-broker.mjs +2084 -0
  36. package/deploy/unraid/sanctuary-unit16-run.sh +560 -0
  37. package/deploy/unraid/sanctuary-unit18-target-audit.sh +27 -0
  38. package/deploy/unraid/sanctuary.ouro/agent.json +66 -0
  39. package/deploy/unraid/sanctuary.ouro/arc/README.md +3 -0
  40. package/deploy/unraid/sanctuary.ouro/bundle-meta.json +5 -0
  41. package/deploy/unraid/sanctuary.ouro/habits/sanctuary-health.md +8 -0
  42. package/deploy/unraid/sanctuary.ouro/mcp/media-mcp.mjs +795 -0
  43. package/deploy/unraid/sanctuary.ouro/provider-readiness.json +11 -0
  44. package/deploy/unraid/sanctuary.ouro/psyche/ASPIRATIONS.md +5 -0
  45. package/deploy/unraid/sanctuary.ouro/psyche/IDENTITY.md +5 -0
  46. package/deploy/unraid/sanctuary.ouro/psyche/LORE.md +15 -0
  47. package/deploy/unraid/sanctuary.ouro/psyche/SOUL.md +25 -0
  48. package/deploy/unraid/sanctuary.ouro/psyche/TACIT.md +15 -0
  49. package/deploy/unraid/sanctuary.ouro/state/policy/steward.json +7 -0
  50. package/deploy/unraid/sanctuary.ouro/tool-profiles.json +135 -0
  51. package/deploy/unraid/sanctuary.xml +28 -0
  52. package/dist/a2a/card.js +57 -0
  53. package/dist/a2a/client.js +180 -0
  54. package/dist/a2a/config.js +50 -0
  55. package/dist/a2a/delegation-stores.js +56 -0
  56. package/dist/a2a/did-resolution.js +93 -0
  57. package/dist/a2a/identity.js +170 -0
  58. package/dist/a2a/inbound-share.js +107 -0
  59. package/dist/a2a/mission-result-wire.js +196 -0
  60. package/dist/a2a/onboarding.js +115 -0
  61. package/dist/a2a/pin-store.js +132 -0
  62. package/dist/a2a/seen-ledger.js +120 -0
  63. package/dist/a2a/server.js +604 -0
  64. package/dist/a2a/task-store.js +69 -0
  65. package/dist/a2a/transport.js +45 -0
  66. package/dist/a2a/types.js +3 -0
  67. package/dist/arc/attention-types.js +8 -0
  68. package/dist/arc/cares.js +360 -0
  69. package/dist/arc/episodes.js +118 -0
  70. package/dist/arc/evolution.js +490 -0
  71. package/dist/arc/flight-recorder.js +1034 -0
  72. package/dist/arc/intentions.js +134 -0
  73. package/dist/arc/json-store.js +117 -0
  74. package/dist/arc/obligations.js +386 -0
  75. package/dist/arc/packets.js +336 -0
  76. package/dist/arc/presence.js +185 -0
  77. package/dist/arc/task-lifecycle.js +57 -0
  78. package/dist/commerce/store.js +755 -0
  79. package/dist/commerce/types.js +3 -0
  80. package/dist/heart/active-work.js +798 -40
  81. package/dist/heart/agent-entry.js +252 -3
  82. package/dist/heart/approval-files.js +113 -0
  83. package/dist/heart/approval-store.js +960 -0
  84. package/dist/heart/attachments/image-normalize.js +194 -0
  85. package/dist/heart/attachments/materialize.js +97 -0
  86. package/dist/heart/attachments/originals.js +88 -0
  87. package/dist/heart/attachments/render.js +29 -0
  88. package/dist/heart/attachments/sources/bluebubbles.js +156 -0
  89. package/dist/heart/attachments/sources/cli-local-file.js +78 -0
  90. package/dist/heart/attachments/sources/index.js +18 -0
  91. package/dist/heart/attachments/sources/telegram.js +71 -0
  92. package/dist/heart/attachments/store.js +132 -0
  93. package/dist/heart/attachments/types.js +93 -0
  94. package/dist/heart/auth/auth-flow.js +495 -0
  95. package/dist/heart/autonomy-budget.js +478 -0
  96. package/dist/heart/awaiting/await-alert.js +148 -0
  97. package/dist/heart/awaiting/await-expiry.js +143 -0
  98. package/dist/heart/awaiting/await-loader.js +91 -0
  99. package/dist/heart/awaiting/await-parser.js +149 -0
  100. package/dist/heart/awaiting/await-runtime-state.js +111 -0
  101. package/dist/heart/awaiting/await-scheduler.js +382 -0
  102. package/dist/heart/background-operations.js +281 -0
  103. package/dist/heart/bridges/manager.js +137 -17
  104. package/dist/heart/bridges/store.js +14 -2
  105. package/dist/heart/bundle-state.js +168 -0
  106. package/dist/heart/commitments.js +44 -18
  107. package/dist/heart/config-registry.js +331 -0
  108. package/dist/heart/config.js +174 -131
  109. package/dist/heart/context-loss-gauntlet.js +387 -0
  110. package/dist/heart/context-loss-sentinel.js +1157 -0
  111. package/dist/heart/core.js +2169 -581
  112. package/dist/heart/cross-chat-delivery.js +9 -22
  113. package/dist/heart/daemon/agent-config-check.js +451 -0
  114. package/dist/heart/daemon/agent-discovery.js +185 -3
  115. package/dist/heart/daemon/agent-service.js +542 -0
  116. package/dist/heart/daemon/agentic-repair.js +547 -0
  117. package/dist/heart/daemon/await-private-wake.js +59 -0
  118. package/dist/heart/daemon/bluebubbles-cli-integration.js +71 -0
  119. package/dist/heart/daemon/bluebubbles-health-diagnostics.js +122 -0
  120. package/dist/heart/daemon/bluebubbles-host-protocol.js +433 -0
  121. package/dist/heart/daemon/bluebubbles-host.js +273 -0
  122. package/dist/heart/daemon/boot-sync-probe.js +197 -0
  123. package/dist/heart/daemon/cadence.js +180 -0
  124. package/dist/heart/daemon/cli-defaults.js +854 -0
  125. package/dist/heart/daemon/cli-desk.js +322 -0
  126. package/dist/heart/daemon/cli-exec.js +8822 -0
  127. package/dist/heart/daemon/cli-help.js +716 -0
  128. package/dist/heart/daemon/cli-parse.js +2771 -0
  129. package/dist/heart/daemon/cli-render-doctor.js +57 -0
  130. package/dist/heart/daemon/cli-render.js +777 -0
  131. package/dist/heart/daemon/cli-types.js +8 -0
  132. package/dist/heart/daemon/connect-bay.js +323 -0
  133. package/dist/heart/daemon/container-credential-bootstrap.js +273 -0
  134. package/dist/heart/daemon/container-healthcheck.js +112 -0
  135. package/dist/heart/daemon/container-runtime.js +97 -0
  136. package/dist/heart/daemon/container-spec-auditor-main.js +171 -0
  137. package/dist/heart/daemon/container-spec-auditor.js +229 -0
  138. package/dist/heart/daemon/daemon-bootstrap-startup.js +115 -0
  139. package/dist/heart/daemon/daemon-cli.js +29 -2349
  140. package/dist/heart/daemon/daemon-entry.js +930 -11
  141. package/dist/heart/daemon/daemon-health.js +186 -0
  142. package/dist/heart/daemon/daemon-rollup.js +57 -0
  143. package/dist/heart/daemon/daemon-runtime-sync.js +88 -13
  144. package/dist/heart/daemon/daemon-tombstone.js +236 -0
  145. package/dist/heart/daemon/daemon.js +2084 -105
  146. package/dist/heart/daemon/dns-workflow.js +394 -0
  147. package/dist/heart/daemon/doctor-types.js +8 -0
  148. package/dist/heart/daemon/doctor.js +1755 -0
  149. package/dist/heart/daemon/freshness.js +345 -0
  150. package/dist/heart/daemon/habit-private-wake.js +61 -0
  151. package/dist/heart/daemon/health-monitor.js +142 -11
  152. package/dist/heart/daemon/hooks/agent-config-v2.js +33 -0
  153. package/dist/heart/daemon/hooks/bundle-meta.js +135 -1
  154. package/dist/heart/daemon/http-health-probe.js +80 -0
  155. package/dist/heart/daemon/human-command-screens.js +234 -0
  156. package/dist/heart/daemon/human-readiness.js +114 -0
  157. package/dist/heart/daemon/inner-status.js +78 -0
  158. package/dist/heart/daemon/interactive-repair.js +394 -0
  159. package/dist/heart/daemon/launchd.js +37 -8
  160. package/dist/heart/daemon/log-tailer.js +79 -10
  161. package/dist/heart/daemon/logs-prune.js +132 -0
  162. package/dist/heart/daemon/mcp-canary.js +524 -0
  163. package/dist/heart/daemon/message-router.js +65 -5
  164. package/dist/heart/daemon/migrate-to-desk.js +848 -0
  165. package/dist/heart/daemon/os-cron-deps.js +135 -0
  166. package/dist/heart/daemon/os-cron.js +22 -14
  167. package/dist/heart/daemon/ouro-bot-entry.js +4 -2
  168. package/dist/heart/daemon/ouro-entry.js +3 -1
  169. package/dist/heart/daemon/plugin-cli.js +432 -0
  170. package/dist/heart/daemon/process-manager.js +733 -61
  171. package/dist/heart/daemon/provider-discovery.js +137 -0
  172. package/dist/heart/daemon/provider-ping-progress.js +83 -0
  173. package/dist/heart/daemon/prunable-bundle.js +144 -0
  174. package/dist/heart/daemon/pulse.js +511 -0
  175. package/dist/heart/daemon/readiness-repair.js +365 -0
  176. package/dist/heart/daemon/run-hooks.js +2 -0
  177. package/dist/heart/daemon/runtime-logging.js +47 -14
  178. package/dist/heart/daemon/runtime-metadata.js +2 -30
  179. package/dist/heart/daemon/runtime-mode.js +13 -2
  180. package/dist/heart/daemon/safe-mode.js +161 -0
  181. package/dist/heart/daemon/sanctuary-acceptance-adapter.js +2654 -0
  182. package/dist/heart/daemon/sanctuary-acceptance-harness.js +1830 -0
  183. package/dist/heart/daemon/sanctuary-acceptance-marker.js +267 -0
  184. package/dist/heart/daemon/sanctuary-acceptance-scenarios.js +955 -0
  185. package/dist/heart/daemon/sanctuary-authority-codec.js +86 -0
  186. package/dist/heart/daemon/sanctuary-authority-epoch.js +256 -0
  187. package/dist/heart/daemon/sanctuary-authority-installation.js +140 -0
  188. package/dist/heart/daemon/sanctuary-authority-ledger.js +241 -0
  189. package/dist/heart/daemon/sanctuary-authority-root-lifecycle.js +819 -0
  190. package/dist/heart/daemon/sanctuary-authority-vault-migration.js +79 -0
  191. package/dist/heart/daemon/sanctuary-bundle-migration.js +729 -0
  192. package/dist/heart/daemon/sanctuary-host-authority.js +1008 -0
  193. package/dist/heart/daemon/sanctuary-host-detached-supervisor.js +494 -0
  194. package/dist/heart/daemon/sanctuary-host-executor.js +670 -0
  195. package/dist/heart/daemon/sanctuary-host-linux-kernel.js +334 -0
  196. package/dist/heart/daemon/sanctuary-host-supervisor-entry.js +207 -0
  197. package/dist/heart/daemon/sanctuary-host-supervisor.js +95 -0
  198. package/dist/heart/daemon/sanctuary-package-management.js +103 -0
  199. package/dist/heart/daemon/sanctuary-scheduler-liveness.js +227 -0
  200. package/dist/heart/daemon/sanctuary-scheduler-origin.js +132 -0
  201. package/dist/heart/daemon/sanctuary-telegram-authority-entry.js +445 -0
  202. package/dist/heart/daemon/sanctuary-telegram-authority-gateway.js +585 -0
  203. package/dist/heart/daemon/sanctuary-telegram-authority-service.js +630 -0
  204. package/dist/heart/daemon/sense-manager.js +699 -48
  205. package/dist/heart/daemon/session-id-resolver.js +131 -0
  206. package/dist/heart/daemon/skill-management-installer.js +1 -1
  207. package/dist/heart/daemon/socket-client.js +180 -12
  208. package/dist/heart/daemon/stale-bundle-prune.js +113 -0
  209. package/dist/heart/daemon/startup-tui.js +339 -0
  210. package/dist/heart/daemon/supercronic-supervisor.js +282 -0
  211. package/dist/heart/daemon/task-scheduler.js +117 -39
  212. package/dist/heart/daemon/terminal-ui.js +499 -0
  213. package/dist/heart/daemon/thoughts.js +138 -54
  214. package/dist/heart/daemon/up-progress.js +366 -0
  215. package/dist/heart/daemon/vault-items.js +56 -0
  216. package/dist/heart/delegation.js +1 -4
  217. package/dist/heart/external-events/router.js +1430 -0
  218. package/dist/heart/frontend-approval-runtime.js +506 -0
  219. package/dist/heart/frontend-journal.js +215 -0
  220. package/dist/heart/frontend-session-service.js +237 -0
  221. package/dist/heart/frontend-socket-client.js +154 -0
  222. package/dist/heart/frontend-socket.js +400 -0
  223. package/dist/heart/habits/habit-cancel.js +810 -0
  224. package/dist/heart/habits/habit-lifecycle.js +1327 -0
  225. package/dist/heart/habits/habit-migration.js +194 -0
  226. package/dist/heart/habits/habit-parser.js +278 -0
  227. package/dist/heart/habits/habit-runtime-state.js +93 -0
  228. package/dist/heart/habits/habit-scheduler.js +653 -0
  229. package/dist/heart/habits/habit-session-summary.js +318 -0
  230. package/dist/heart/habits/habit-session.js +636 -0
  231. package/dist/heart/{daemon → hatch}/hatch-flow.js +55 -62
  232. package/dist/heart/{daemon → hatch}/hatch-specialist.js +6 -8
  233. package/dist/heart/{daemon → hatch}/specialist-prompt.js +12 -9
  234. package/dist/heart/{daemon → hatch}/specialist-tools.js +53 -18
  235. package/dist/heart/identity.js +196 -59
  236. package/dist/heart/kept-notes.js +289 -0
  237. package/dist/heart/kicks.js +1 -1
  238. package/dist/heart/machine-identity.js +161 -0
  239. package/dist/heart/mail-import-discovery.js +356 -0
  240. package/dist/heart/mailbox/mailbox-http-hooks.js +98 -0
  241. package/dist/heart/mailbox/mailbox-http-response.js +7 -0
  242. package/dist/heart/mailbox/mailbox-http-routes.js +398 -0
  243. package/dist/heart/mailbox/mailbox-http-static.js +103 -0
  244. package/dist/heart/mailbox/mailbox-http-transport.js +129 -0
  245. package/dist/heart/mailbox/mailbox-http.js +99 -0
  246. package/dist/heart/mailbox/mailbox-read.js +39 -0
  247. package/dist/heart/mailbox/mailbox-types.js +27 -0
  248. package/dist/heart/mailbox/mailbox-view.js +197 -0
  249. package/dist/heart/mailbox/readers/agent-machine.js +428 -0
  250. package/dist/heart/mailbox/readers/continuity-readers.js +329 -0
  251. package/dist/heart/mailbox/readers/mail.js +375 -0
  252. package/dist/heart/mailbox/readers/runtime-readers.js +823 -0
  253. package/dist/heart/mailbox/readers/sessions.js +231 -0
  254. package/dist/heart/mailbox/readers/shared.js +111 -0
  255. package/dist/heart/mcp/mcp-server.js +696 -0
  256. package/dist/heart/migrate-config.js +100 -0
  257. package/dist/heart/model-capabilities.js +11 -0
  258. package/dist/heart/orientation-frame.js +345 -0
  259. package/dist/heart/platform.js +81 -0
  260. package/dist/heart/primitives.js +1 -1
  261. package/dist/heart/private-runtime/decision-renderer.js +158 -0
  262. package/dist/heart/private-runtime/index.js +18 -0
  263. package/dist/heart/private-runtime/ledger.js +221 -0
  264. package/dist/heart/private-runtime/policy.js +252 -0
  265. package/dist/heart/private-runtime/types.js +2 -0
  266. package/dist/heart/progress-story.js +1 -1
  267. package/dist/heart/provider-attempt.js +137 -0
  268. package/dist/heart/provider-binding-resolver.js +273 -0
  269. package/dist/heart/provider-credentials.js +449 -0
  270. package/dist/heart/provider-failover.js +311 -0
  271. package/dist/heart/provider-models.js +96 -0
  272. package/dist/heart/provider-ping.js +265 -0
  273. package/dist/heart/provider-readiness-cache.js +119 -0
  274. package/dist/heart/provider-visibility.js +188 -0
  275. package/dist/heart/providers/anthropic-token.js +131 -0
  276. package/dist/heart/providers/anthropic.js +173 -60
  277. package/dist/heart/providers/azure.js +23 -11
  278. package/dist/heart/providers/error-classification.js +127 -0
  279. package/dist/heart/providers/github-copilot.js +34 -38
  280. package/dist/heart/providers/minimax-vlm.js +189 -0
  281. package/dist/heart/providers/minimax.js +26 -8
  282. package/dist/heart/providers/openai-codex-token.js +349 -0
  283. package/dist/heart/providers/openai-codex.js +55 -40
  284. package/dist/heart/providers/openai-compatible.js +177 -0
  285. package/dist/heart/run-ledger.js +247 -0
  286. package/dist/heart/runtime-capability-check.js +170 -0
  287. package/dist/heart/runtime-credentials.js +607 -0
  288. package/dist/heart/runtime-cwd.js +87 -0
  289. package/dist/heart/sense-truth.js +22 -4
  290. package/dist/heart/session-activity.js +92 -24
  291. package/dist/heart/session-events.js +1504 -0
  292. package/dist/heart/session-playback-cli-main.js +5 -0
  293. package/dist/heart/session-playback-cli.js +36 -0
  294. package/dist/heart/session-playback.js +231 -0
  295. package/dist/heart/session-redaction-repair-cli-main.js +31 -0
  296. package/dist/heart/session-redaction-repair.js +552 -0
  297. package/dist/heart/session-stats-cli-main.js +5 -0
  298. package/dist/heart/session-stats.js +183 -0
  299. package/dist/heart/session-transcript.js +133 -0
  300. package/dist/heart/start-of-turn-packet.js +372 -0
  301. package/dist/heart/steward-policy.js +618 -0
  302. package/dist/heart/streaming.js +604 -207
  303. package/dist/heart/structured-output.js +196 -0
  304. package/dist/heart/sync-classification.js +176 -0
  305. package/dist/heart/sync.js +449 -0
  306. package/dist/heart/target-resolution.js +11 -7
  307. package/dist/heart/tempo.js +93 -0
  308. package/dist/heart/temporal-view.js +41 -0
  309. package/dist/heart/timeouts.js +101 -0
  310. package/dist/heart/tool-activity-callbacks.js +59 -0
  311. package/dist/heart/tool-approval.js +382 -0
  312. package/dist/heart/tool-description.js +155 -0
  313. package/dist/heart/tool-friction.js +55 -0
  314. package/dist/heart/tool-loop.js +200 -0
  315. package/dist/heart/turn-context.js +482 -0
  316. package/dist/heart/turn-execution-lease.js +30 -0
  317. package/dist/heart/{daemon → versioning}/ouro-bot-global-installer.js +6 -5
  318. package/dist/heart/{daemon → versioning}/ouro-bot-wrapper.js +1 -1
  319. package/dist/heart/versioning/ouro-path-installer.js +432 -0
  320. package/dist/heart/versioning/ouro-recovery-launcher.js +76 -0
  321. package/dist/heart/versioning/ouro-version-manager.js +409 -0
  322. package/dist/heart/{daemon → versioning}/staged-restart.js +40 -8
  323. package/dist/heart/{daemon → versioning}/update-checker.js +6 -1
  324. package/dist/heart/versioning/update-hooks.js +154 -0
  325. package/dist/heart/versioning/version-intent.js +114 -0
  326. package/dist/heart/work-card.js +380 -0
  327. package/dist/mailbox-ui/assets/index-Cyv4mw3Y.js +1 -0
  328. package/dist/mailbox-ui/assets/index-Du_9G9WO.css +1 -0
  329. package/dist/mailbox-ui/assets/vendor-CcN1XpQ9.js +61 -0
  330. package/dist/mailbox-ui/index.html +16 -0
  331. package/dist/mailroom/attention.js +167 -0
  332. package/dist/mailroom/autonomy.js +209 -0
  333. package/dist/mailroom/blob-store.js +889 -0
  334. package/dist/mailroom/body-cache.js +61 -0
  335. package/dist/mailroom/cache-sync-cli.js +58 -0
  336. package/dist/mailroom/core.js +803 -0
  337. package/dist/mailroom/entry.js +160 -0
  338. package/dist/mailroom/file-store.js +568 -0
  339. package/dist/mailroom/hosted-cache-sync.js +384 -0
  340. package/dist/mailroom/mbox-import.js +393 -0
  341. package/dist/mailroom/migration.js +164 -0
  342. package/dist/mailroom/outbound.js +380 -0
  343. package/dist/mailroom/policy.js +263 -0
  344. package/dist/mailroom/reader.js +292 -0
  345. package/dist/mailroom/search-cache.js +513 -0
  346. package/dist/mailroom/search-relevance.js +319 -0
  347. package/dist/mailroom/smtp-ingress.js +176 -0
  348. package/dist/mailroom/source-state.js +176 -0
  349. package/dist/mailroom/thread.js +109 -0
  350. package/dist/mailroom/travel-extract.js +89 -0
  351. package/dist/mind/bundle-manifest.js +24 -2
  352. package/dist/mind/context.js +309 -111
  353. package/dist/mind/desk-section.js +362 -0
  354. package/dist/mind/diary-integrity.js +60 -0
  355. package/dist/mind/{memory.js → diary.js} +68 -77
  356. package/dist/mind/embedding-provider.js +60 -0
  357. package/dist/mind/file-state.js +179 -0
  358. package/dist/mind/{associative-recall.js → note-search.js} +47 -58
  359. package/dist/mind/obligation-steering.js +199 -3
  360. package/dist/mind/pending.js +36 -10
  361. package/dist/mind/prompt-budget.js +479 -0
  362. package/dist/mind/prompt-refresh.js +3 -2
  363. package/dist/mind/prompt.js +1050 -189
  364. package/dist/mind/provenance-trust.js +26 -0
  365. package/dist/mind/record-paths.js +312 -0
  366. package/dist/mind/scrutiny.js +173 -0
  367. package/dist/mind/session-transaction.js +564 -0
  368. package/dist/nerves/cli-logging.js +7 -1
  369. package/dist/nerves/coverage/audit-rules.js +15 -6
  370. package/dist/nerves/coverage/audit.js +28 -2
  371. package/dist/nerves/coverage/cli.js +1 -1
  372. package/dist/nerves/coverage/contract.js +5 -5
  373. package/dist/nerves/coverage/file-completeness.js +139 -5
  374. package/dist/nerves/coverage/run-artifacts.js +55 -35
  375. package/dist/nerves/coverage/source-scanner.js +4 -4
  376. package/dist/nerves/event-buffer.js +111 -0
  377. package/dist/nerves/index.js +293 -9
  378. package/dist/nerves/observation.js +20 -0
  379. package/dist/nerves/redact.js +79 -0
  380. package/dist/nerves/review/cli-main.js +5 -0
  381. package/dist/nerves/review/cli.js +156 -0
  382. package/dist/nerves/review/core.js +159 -0
  383. package/dist/nerves/runtime.js +17 -1
  384. package/dist/repertoire/ado-client.js +15 -56
  385. package/dist/repertoire/ado-semantic.js +16 -10
  386. package/dist/repertoire/api-client.js +97 -0
  387. package/dist/repertoire/bitwarden-store.js +1045 -0
  388. package/dist/repertoire/bundle-templates.js +71 -0
  389. package/dist/repertoire/bw-installer.js +180 -0
  390. package/dist/repertoire/coding/codex-jsonl.js +64 -0
  391. package/dist/repertoire/coding/context-pack.js +331 -0
  392. package/dist/repertoire/coding/feedback.js +174 -32
  393. package/dist/repertoire/coding/index.js +15 -3
  394. package/dist/repertoire/coding/manager.js +159 -10
  395. package/dist/repertoire/coding/spawner.js +58 -11
  396. package/dist/repertoire/coding/tools.js +221 -15
  397. package/dist/repertoire/coding/workbench-client.js +272 -0
  398. package/dist/repertoire/coding/workbench-manager.js +346 -0
  399. package/dist/repertoire/commerce-errors.js +109 -0
  400. package/dist/repertoire/commerce-self-test.js +156 -0
  401. package/dist/repertoire/credential-access.js +178 -0
  402. package/dist/repertoire/desk/classifier.js +362 -0
  403. package/dist/repertoire/duffel-client.js +185 -0
  404. package/dist/repertoire/github-client.js +14 -55
  405. package/dist/repertoire/graph-client.js +11 -52
  406. package/dist/repertoire/guardrails.js +811 -18
  407. package/dist/repertoire/mcp-client.js +44 -3
  408. package/dist/repertoire/mcp-manager.js +427 -139
  409. package/dist/repertoire/mcp-tools.js +124 -0
  410. package/dist/repertoire/plugin-mcp.js +175 -0
  411. package/dist/repertoire/plugins.js +253 -0
  412. package/dist/repertoire/relationship-authorization.js +322 -0
  413. package/dist/repertoire/shell-sessions.js +134 -0
  414. package/dist/repertoire/skills.js +48 -4
  415. package/dist/repertoire/stripe-client.js +131 -0
  416. package/dist/repertoire/tool-arguments.js +170 -0
  417. package/dist/repertoire/tool-results.js +29 -0
  418. package/dist/repertoire/tools-a2a.js +699 -0
  419. package/dist/repertoire/tools-attachments.js +322 -0
  420. package/dist/repertoire/tools-awaiting.js +575 -0
  421. package/dist/repertoire/tools-base.js +77 -1138
  422. package/dist/repertoire/tools-bluebubbles.js +2 -0
  423. package/dist/repertoire/tools-bridge.js +144 -0
  424. package/dist/repertoire/tools-bundle.js +993 -0
  425. package/dist/repertoire/tools-commerce.js +253 -0
  426. package/dist/repertoire/tools-config.js +186 -0
  427. package/dist/repertoire/tools-continuity.js +575 -0
  428. package/dist/repertoire/tools-credential.js +383 -0
  429. package/dist/repertoire/tools-evolution.js +527 -0
  430. package/dist/repertoire/tools-files.js +344 -0
  431. package/dist/repertoire/tools-flight.js +290 -0
  432. package/dist/repertoire/tools-flow.js +119 -0
  433. package/dist/repertoire/tools-github.js +3 -8
  434. package/dist/repertoire/tools-habits.js +103 -0
  435. package/dist/repertoire/tools-mail.js +1828 -0
  436. package/dist/repertoire/tools-notes.js +485 -0
  437. package/dist/repertoire/tools-obligations.js +182 -0
  438. package/dist/repertoire/tools-orientation.js +31 -0
  439. package/dist/repertoire/tools-record.js +482 -0
  440. package/dist/repertoire/tools-rsvp.js +139 -0
  441. package/dist/repertoire/tools-runtime.js +150 -0
  442. package/dist/repertoire/tools-sanctuary-host.js +202 -0
  443. package/dist/repertoire/tools-session.js +1133 -0
  444. package/dist/repertoire/tools-shell.js +149 -0
  445. package/dist/repertoire/tools-steward-policy.js +119 -0
  446. package/dist/repertoire/tools-stripe.js +224 -0
  447. package/dist/repertoire/tools-surface.js +369 -0
  448. package/dist/repertoire/tools-teams.js +12 -39
  449. package/dist/repertoire/tools-telegram-contacts.js +37 -0
  450. package/dist/repertoire/tools-travel.js +125 -0
  451. package/dist/repertoire/tools-trip.js +982 -0
  452. package/dist/repertoire/tools-unraid.js +531 -0
  453. package/dist/repertoire/tools-user-profile.js +146 -0
  454. package/dist/repertoire/tools-vault.js +40 -0
  455. package/dist/repertoire/tools-voice.js +145 -0
  456. package/dist/repertoire/tools.js +494 -148
  457. package/dist/repertoire/travel-api-client.js +360 -0
  458. package/dist/repertoire/unraid-client.js +155 -0
  459. package/dist/repertoire/unraid-restart.js +339 -0
  460. package/dist/repertoire/user-profile.js +131 -0
  461. package/dist/repertoire/vault-setup.js +246 -0
  462. package/dist/repertoire/vault-unlock.js +628 -0
  463. package/dist/rsvp/aisleplanner-client.js +354 -0
  464. package/dist/rsvp/cli.js +926 -0
  465. package/dist/rsvp/config.js +327 -0
  466. package/dist/rsvp/cutover.js +433 -0
  467. package/dist/rsvp/diagnostics.js +232 -0
  468. package/dist/rsvp/diff-renderer.js +176 -0
  469. package/dist/rsvp/habit-policy.js +110 -0
  470. package/dist/rsvp/habit-stage.js +123 -0
  471. package/dist/rsvp/incident-bundle.js +97 -0
  472. package/dist/rsvp/migration.js +204 -0
  473. package/dist/rsvp/native-habit-runner.js +630 -0
  474. package/dist/rsvp/outbound-state.js +628 -0
  475. package/dist/rsvp/query.js +118 -0
  476. package/dist/rsvp/replay.js +233 -0
  477. package/dist/rsvp/snapshot.js +232 -0
  478. package/dist/rsvp/spend-ledger.js +175 -0
  479. package/dist/scripts/claude-code-hook.js +41 -0
  480. package/dist/scripts/claude-code-stop-hook.js +47 -0
  481. package/dist/senses/a2a-entry.js +115 -0
  482. package/dist/senses/acp-server.js +386 -0
  483. package/dist/senses/attention-queue.js +186 -0
  484. package/dist/senses/await-turn-message.js +58 -0
  485. package/dist/senses/bluebubbles/active-turns.js +216 -0
  486. package/dist/senses/bluebubbles/attachment-cache.js +53 -0
  487. package/dist/senses/bluebubbles/attachment-download.js +137 -0
  488. package/dist/senses/{bluebubbles-client.js → bluebubbles/client.js} +355 -40
  489. package/dist/senses/bluebubbles/context-packet.js +207 -0
  490. package/dist/senses/bluebubbles/context-smoke.js +144 -0
  491. package/dist/senses/bluebubbles/entry.js +90 -0
  492. package/dist/senses/{bluebubbles-inbound-log.js → bluebubbles/inbound-log.js} +21 -3
  493. package/dist/senses/bluebubbles/index.js +3841 -0
  494. package/dist/senses/bluebubbles/latest-turn.js +311 -0
  495. package/dist/senses/{bluebubbles-media.js → bluebubbles/media.js} +121 -71
  496. package/dist/senses/{bluebubbles-model.js → bluebubbles/model.js} +159 -28
  497. package/dist/senses/{bluebubbles-mutation-log.js → bluebubbles/mutation-log.js} +24 -13
  498. package/dist/senses/bluebubbles/outbound-state.js +308 -0
  499. package/dist/senses/bluebubbles/processed-log.js +133 -0
  500. package/dist/senses/bluebubbles/reaction-policy.js +47 -0
  501. package/dist/senses/bluebubbles/replay.js +284 -0
  502. package/dist/senses/{bluebubbles-runtime-state.js → bluebubbles/runtime-state.js} +30 -2
  503. package/dist/senses/bluebubbles/semantic-receipts.js +1986 -0
  504. package/dist/senses/{bluebubbles-session-cleanup.js → bluebubbles/session-cleanup.js} +1 -1
  505. package/dist/senses/bluebubbles/webhook-registration.js +228 -0
  506. package/dist/senses/bluebubbles-meta-guard.js +39 -0
  507. package/dist/senses/cli/bracketed-paste.js +82 -0
  508. package/dist/senses/cli/image-paste.js +287 -0
  509. package/dist/senses/cli/image-ref-navigation.js +75 -0
  510. package/dist/senses/cli/ink-app.js +156 -0
  511. package/dist/senses/cli/inline-diff.js +64 -0
  512. package/dist/senses/cli/input-keys.js +174 -0
  513. package/dist/senses/cli/kill-ring.js +86 -0
  514. package/dist/senses/cli/message-list.js +51 -0
  515. package/dist/senses/cli/ouro-tui.js +607 -0
  516. package/dist/senses/cli/spinner-imperative.js +135 -0
  517. package/dist/senses/cli/spinner.js +101 -0
  518. package/dist/senses/cli/status-line.js +60 -0
  519. package/dist/senses/cli/streaming-markdown.js +526 -0
  520. package/dist/senses/cli/tool-display.js +85 -0
  521. package/dist/senses/cli/tool-render.js +85 -0
  522. package/dist/senses/cli/tui-store.js +241 -0
  523. package/dist/senses/cli/virtual-list.js +35 -0
  524. package/dist/senses/cli-entry.js +60 -8
  525. package/dist/senses/cli.js +604 -320
  526. package/dist/senses/commands.js +66 -3
  527. package/dist/senses/context-packet-ledger.js +371 -0
  528. package/dist/senses/context-packets.js +154 -0
  529. package/dist/senses/habit-lifecycle-guard.js +24 -0
  530. package/dist/senses/habit-turn-message.js +160 -0
  531. package/dist/senses/ingress-evidence.js +2 -0
  532. package/dist/senses/mail-entry.js +66 -0
  533. package/dist/senses/mail.js +524 -0
  534. package/dist/senses/pipeline.js +1115 -204
  535. package/dist/senses/private-runtime-worker.js +859 -0
  536. package/dist/senses/private-runtime.js +1517 -0
  537. package/dist/senses/proactive-content-guard.js +51 -0
  538. package/dist/senses/root-host-approval-port.js +345 -0
  539. package/dist/senses/root-host-approval-runtime.js +393 -0
  540. package/dist/senses/sanctuary-authority-resident.js +102 -0
  541. package/dist/senses/sanctuary-download-credit-presentation.js +31 -0
  542. package/dist/senses/sanctuary-full-visibility-contract.js +302 -0
  543. package/dist/senses/sanctuary-grounding.js +118 -0
  544. package/dist/senses/sanctuary-health-acceptance-probe-entry.js +12 -0
  545. package/dist/senses/sanctuary-health-acceptance-probe.js +774 -0
  546. package/dist/senses/sanctuary-health-runner.js +100 -0
  547. package/dist/senses/sanctuary-health.js +408 -0
  548. package/dist/senses/sanctuary-install-state-contract.js +126 -0
  549. package/dist/senses/sanctuary-interactive-control.js +410 -0
  550. package/dist/senses/sanctuary-media-catalog-contract.js +297 -0
  551. package/dist/senses/sanctuary-media-optimization.js +514 -0
  552. package/dist/senses/sanctuary-runtime.js +340 -0
  553. package/dist/senses/sanctuary-sab.js +85 -0
  554. package/dist/senses/sanctuary-storage-optimization-contract.js +32 -0
  555. package/dist/senses/shared-turn.js +772 -0
  556. package/dist/senses/surface-tool.js +109 -0
  557. package/dist/senses/teams-entry.js +60 -8
  558. package/dist/senses/teams.js +519 -218
  559. package/dist/senses/telegram-admission.js +718 -0
  560. package/dist/senses/telegram-approval-runtime.js +793 -0
  561. package/dist/senses/telegram-attachments.js +186 -0
  562. package/dist/senses/telegram-audit-ledger.js +220 -0
  563. package/dist/senses/telegram-authority-transport.js +231 -0
  564. package/dist/senses/telegram-client.js +1717 -0
  565. package/dist/senses/telegram-effect-adapter.js +896 -0
  566. package/dist/senses/telegram-entry.js +83 -0
  567. package/dist/senses/telegram.js +2324 -0
  568. package/dist/senses/trust-gate.js +104 -9
  569. package/dist/senses/voice/audio-playback.js +237 -0
  570. package/dist/senses/voice/audio-routing.js +119 -0
  571. package/dist/senses/voice/elevenlabs.js +202 -0
  572. package/dist/senses/voice/floor-control.js +431 -0
  573. package/dist/senses/voice/floor-controller.js +115 -0
  574. package/dist/senses/voice/golden-path.js +116 -0
  575. package/dist/senses/voice/index.js +29 -0
  576. package/dist/senses/voice/meeting.js +113 -0
  577. package/dist/senses/voice/outbound.js +190 -0
  578. package/dist/senses/voice/phone.js +33 -0
  579. package/dist/senses/voice/playback.js +139 -0
  580. package/dist/senses/voice/realtime-eval.js +496 -0
  581. package/dist/senses/voice/realtime-trace.js +531 -0
  582. package/dist/senses/voice/transcript.js +70 -0
  583. package/dist/senses/voice/turn.js +191 -0
  584. package/dist/senses/voice/twilio-phone-runtime.js +807 -0
  585. package/dist/senses/voice/twilio-phone.js +5031 -0
  586. package/dist/senses/voice/types.js +2 -0
  587. package/dist/senses/voice/whisper.js +161 -0
  588. package/dist/senses/voice-entry.js +81 -0
  589. package/dist/senses/voice-realtime-eval-command.js +99 -0
  590. package/dist/senses/voice-realtime-eval-entry.js +21 -0
  591. package/dist/senses/voice-twilio-entry.js +87 -0
  592. package/dist/trips/core.js +138 -0
  593. package/dist/trips/store.js +265 -0
  594. package/dist/util/frontmatter.js +69 -0
  595. package/npm-shrinkwrap.json +8138 -0
  596. package/package.json +72 -13
  597. package/skills/agent-commerce.md +113 -0
  598. package/skills/browser-navigation.md +117 -0
  599. package/skills/commerce-setup-guide.md +116 -0
  600. package/skills/commerce-setup.md +84 -0
  601. package/skills/configure-dev-tools.md +99 -0
  602. package/skills/travel-planning.md +138 -0
  603. package/dist/heart/daemon/auth-flow.js +0 -430
  604. package/dist/heart/daemon/ouro-path-installer.js +0 -260
  605. package/dist/heart/daemon/ouro-version-manager.js +0 -171
  606. package/dist/heart/daemon/update-hooks.js +0 -138
  607. package/dist/heart/obligations.js +0 -191
  608. package/dist/heart/safe-workspace.js +0 -228
  609. package/dist/heart/session-recall.js +0 -116
  610. package/dist/mind/friends/channel.js +0 -84
  611. package/dist/mind/friends/group-context.js +0 -144
  612. package/dist/mind/friends/resolver.js +0 -84
  613. package/dist/mind/friends/store-file.js +0 -190
  614. package/dist/mind/friends/store.js +0 -4
  615. package/dist/mind/friends/tokens.js +0 -26
  616. package/dist/mind/friends/trust-explanation.js +0 -74
  617. package/dist/mind/friends/types.js +0 -29
  618. package/dist/repertoire/tasks/board.js +0 -134
  619. package/dist/repertoire/tasks/index.js +0 -224
  620. package/dist/repertoire/tasks/lifecycle.js +0 -80
  621. package/dist/repertoire/tasks/middleware.js +0 -65
  622. package/dist/repertoire/tasks/parser.js +0 -173
  623. package/dist/repertoire/tasks/scanner.js +0 -132
  624. package/dist/repertoire/tasks/transitions.js +0 -144
  625. package/dist/senses/bluebubbles-entry.js +0 -13
  626. package/dist/senses/bluebubbles.js +0 -1181
  627. package/dist/senses/debug-activity.js +0 -154
  628. package/dist/senses/inner-dialog-worker.js +0 -90
  629. package/dist/senses/inner-dialog.js +0 -525
  630. package/subagents/README.md +0 -7
  631. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/basilisk.md +0 -0
  632. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/jafar.md +0 -0
  633. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/jormungandr.md +0 -0
  634. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/kaa.md +0 -0
  635. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/medusa.md +0 -0
  636. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/monty.md +0 -0
  637. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/nagini.md +0 -0
  638. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/ouroboros.md +0 -0
  639. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/python.md +0 -0
  640. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/quetzalcoatl.md +0 -0
  641. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/sir-hiss.md +0 -0
  642. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/the-snake.md +0 -0
  643. /package/dist/{repertoire/tasks/types.js → heart/attachments/sources/adapter.js} +0 -0
  644. /package/dist/heart/{daemon → hatch}/hatch-animation.js +0 -0
  645. /package/dist/heart/{daemon → hatch}/specialist-orchestrator.js +0 -0
  646. /package/dist/heart/{daemon → versioning}/ouro-uti.js +0 -0
  647. /package/dist/heart/{daemon → versioning}/wrapper-publish-guard.js +0 -0
@@ -0,0 +1,1755 @@
1
+ "use strict";
2
+ /**
3
+ * System health check ("ouro doctor") — runs all diagnostic categories
4
+ * and aggregates results into a structured DoctorResult.
5
+ *
6
+ * Each category checker is isolated: if one throws, it produces a single
7
+ * "fail" check and the remaining categories still run.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.KNOWN_DOCTOR_CATEGORIES = exports.DEFAULT_SENSE_DELIVERY_THRESHOLDS = exports.DEFAULT_MAIL_INGEST_THRESHOLDS = void 0;
11
+ exports.checkCliPath = checkCliPath;
12
+ exports.checkDaemon = checkDaemon;
13
+ exports.senseInboundDeliveryCheck = senseInboundDeliveryCheck;
14
+ exports.checkAgents = checkAgents;
15
+ exports.checkSenses = checkSenses;
16
+ exports.checkHabits = checkHabits;
17
+ exports.checkRsvp = checkRsvp;
18
+ exports.checkSecurity = checkSecurity;
19
+ exports.checkTrips = checkTrips;
20
+ exports.checkMailroom = checkMailroom;
21
+ exports.checkFriends = checkFriends;
22
+ exports.checkDisk = checkDisk;
23
+ exports.checkLifecycle = checkLifecycle;
24
+ exports.runDoctorChecks = runDoctorChecks;
25
+ const runtime_1 = require("../../nerves/runtime");
26
+ const bluebubbles_health_diagnostics_1 = require("./bluebubbles-health-diagnostics");
27
+ const webhook_registration_1 = require("../../senses/bluebubbles/webhook-registration");
28
+ const freshness_1 = require("./freshness");
29
+ const ouro_path_installer_1 = require("../versioning/ouro-path-installer");
30
+ const runtime_credentials_1 = require("../runtime-credentials");
31
+ const machine_identity_1 = require("../machine-identity");
32
+ const habit_parser_1 = require("../habits/habit-parser");
33
+ const cadence_1 = require("./cadence");
34
+ const nerves_1 = require("../../nerves");
35
+ const config_1 = require("../../rsvp/config");
36
+ const cutover_1 = require("../../rsvp/cutover");
37
+ const diagnostics_1 = require("../../rsvp/diagnostics");
38
+ const blob_store_1 = require("../../mailroom/blob-store");
39
+ const hosted_cache_sync_1 = require("../../mailroom/hosted-cache-sync");
40
+ const search_cache_1 = require("../../mailroom/search-cache");
41
+ const prunable_bundle_1 = require("./prunable-bundle");
42
+ const DEFAULT_BLUEBUBBLES_REQUEST_TIMEOUT_MS = 30_000;
43
+ const DEFAULT_BLUEBUBBLES_PORT = 18_790;
44
+ const DEFAULT_BLUEBUBBLES_WEBHOOK_PATH = "/bluebubbles-webhook";
45
+ // ── Category checkers ──
46
+ function checkCliPath(deps) {
47
+ const resolution = (0, ouro_path_installer_1.diagnoseOuroPath)({
48
+ homeDir: deps.homedir,
49
+ envPath: deps.envPath ?? "",
50
+ existsSync: deps.existsSync,
51
+ readFileSync: (p) => deps.readFileSync(p),
52
+ });
53
+ const status = resolution.status === "ok"
54
+ ? "pass"
55
+ : resolution.status === "shadowed"
56
+ ? "fail"
57
+ : "warn";
58
+ return {
59
+ name: "CLI",
60
+ checks: [{
61
+ label: "ouro PATH resolution",
62
+ status,
63
+ detail: resolution.remediation
64
+ ? `${resolution.detail}; fix: ${resolution.remediation}`
65
+ : resolution.detail,
66
+ }],
67
+ };
68
+ }
69
+ async function checkDaemon(deps) {
70
+ const checks = [];
71
+ const socketExists = deps.existsSync(deps.socketPath);
72
+ checks.push({
73
+ label: "daemon socket exists",
74
+ status: socketExists ? "pass" : "fail",
75
+ detail: socketExists ? deps.socketPath : `not found at ${deps.socketPath}`,
76
+ });
77
+ if (socketExists) {
78
+ const alive = await deps.checkSocketAlive(deps.socketPath);
79
+ checks.push({
80
+ label: "daemon is responsive",
81
+ status: alive ? "pass" : "fail",
82
+ detail: alive ? "socket responded" : "socket exists but daemon unresponsive",
83
+ });
84
+ }
85
+ else {
86
+ checks.push({
87
+ label: "daemon is responsive",
88
+ status: "fail",
89
+ detail: "skipped — socket missing",
90
+ });
91
+ }
92
+ return { name: "Daemon", checks };
93
+ }
94
+ /** Discover doctor candidates: *.ouro directories with a present agent.json. */
95
+ function discoverAgents(deps) {
96
+ if (!deps.existsSync(deps.bundlesRoot))
97
+ return [];
98
+ return deps.readdirSync(deps.bundlesRoot).filter((name) => name.endsWith(".ouro") && deps.existsSync(`${deps.bundlesRoot}/${name}/agent.json`));
99
+ }
100
+ function asRecord(value) {
101
+ return value && typeof value === "object" && !Array.isArray(value)
102
+ ? value
103
+ : null;
104
+ }
105
+ function textField(record, key) {
106
+ const value = record?.[key];
107
+ return typeof value === "string" ? value.trim() : "";
108
+ }
109
+ function stringArrayField(record, key) {
110
+ const value = record?.[key];
111
+ return Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
112
+ }
113
+ function numberField(record, key, fallback) {
114
+ const value = record?.[key];
115
+ return typeof value === "number" && Number.isFinite(value) ? value : fallback;
116
+ }
117
+ function hasStringRecordValue(value) {
118
+ const record = asRecord(value);
119
+ return !!record && Object.values(record).some((entry) => typeof entry === "string" && entry.trim().length > 0);
120
+ }
121
+ function mailAutonomyDetail(mailroom) {
122
+ const policy = asRecord(mailroom?.autonomousSendPolicy);
123
+ const autonomy = policy?.enabled === true ? "autonomy enabled" : "autonomy disabled";
124
+ const killSwitch = policy?.killSwitch === true ? "kill switch on" : "kill switch off";
125
+ return `${autonomy}; ${killSwitch}`;
126
+ }
127
+ const SENSITIVE_CONFIG_KEYS = ["apiKey", "token", "secret", "password"];
128
+ function credentialKeyLeaks(raw) {
129
+ return SENSITIVE_CONFIG_KEYS.filter((key) => raw.includes(`"${key}"`));
130
+ }
131
+ // ── Pipeline liveness (dead-pipe detection) ──
132
+ //
133
+ // Configuration checks answer "is this wired up?". These answer "is anything
134
+ // actually flowing?". Both are needed: the 77-day mail outage and the
135
+ // multi-day BlueBubbles inbound outage were both invisible to configuration
136
+ // and connection checks that stayed green throughout.
137
+ /**
138
+ * Mail ingest thresholds.
139
+ *
140
+ * A delegated mailbox forwards a human's real inbox, so a full calendar day
141
+ * with zero inbound mail is already anomalous — that is the warn line. Fail
142
+ * waits until 72h so a genuinely quiet weekend on a low-traffic mailbox does
143
+ * not hard-fail doctor; the 2026-05-10 outage would have failed on day four
144
+ * instead of going unnoticed for seventy-seven. Override per agent with
145
+ * `senses.mail.freshness.{warnAfterHours,failAfterHours}` in `agent.json`.
146
+ */
147
+ exports.DEFAULT_MAIL_INGEST_THRESHOLDS = {
148
+ warnAfterMs: 24 * freshness_1.FRESHNESS_HOUR_MS,
149
+ failAfterMs: 72 * freshness_1.FRESHNESS_HOUR_MS,
150
+ };
151
+ /**
152
+ * Inbound sense-delivery thresholds.
153
+ *
154
+ * Human-driven chat surfaces are bursty — nobody texts an agent every day —
155
+ * so the warn line sits at 72h and failure waits a full week. Seven days
156
+ * without a single inbound message on an attached chat sense is not plausible
157
+ * quiet; it is a dead pipe. Override per agent with
158
+ * `senses.<sense>.freshness.{warnAfterHours,failAfterHours}` in `agent.json`.
159
+ */
160
+ exports.DEFAULT_SENSE_DELIVERY_THRESHOLDS = {
161
+ warnAfterMs: 72 * freshness_1.FRESHNESS_HOUR_MS,
162
+ failAfterMs: 7 * freshness_1.FRESHNESS_DAY_MS,
163
+ };
164
+ /** Hard bound on how many per-conversation inbound logs a sense probe stats. */
165
+ const SENSE_DELIVERY_MAX_LOG_SCAN = 500;
166
+ function pipelineLivenessCheck(input) {
167
+ const result = (0, freshness_1.evaluateFreshness)({
168
+ activity: input.activity,
169
+ unit: input.unit,
170
+ observation: input.probe.observation,
171
+ provenance: input.probe.provenance,
172
+ nowMs: input.nowMs,
173
+ thresholds: input.thresholds,
174
+ remediation: input.remediation,
175
+ configuredSinceMs: input.configuredSinceMs,
176
+ context: input.context,
177
+ });
178
+ return { id: input.id, label: input.label, status: result.status, detail: result.detail };
179
+ }
180
+ /** Best-effort mtime used as a "this pipe has been wired up since" floor. */
181
+ function pathMtimeMs(deps, filePath) {
182
+ if (!deps.existsSync(filePath))
183
+ return null;
184
+ try {
185
+ return deps.statSync(filePath).mtimeMs;
186
+ }
187
+ catch {
188
+ return null;
189
+ }
190
+ }
191
+ /** Read `senses.<sense>.freshness` from `agent.json`, if present. */
192
+ function senseFreshnessOverride(deps, agentDir, sense) {
193
+ const configPath = `${deps.bundlesRoot}/${agentDir}/agent.json`;
194
+ if (!deps.existsSync(configPath))
195
+ return null;
196
+ try {
197
+ const config = JSON.parse(deps.readFileSync(configPath));
198
+ return asRecord(asRecord(config.senses)?.[sense])?.freshness ?? null;
199
+ }
200
+ catch {
201
+ return null;
202
+ }
203
+ }
204
+ /**
205
+ * Inbound-delivery liveness for a sense that records what it received.
206
+ *
207
+ * This asserts on the *inbound* log, deliberately not on the upstream health
208
+ * probe: BlueBubbles reported `upstreamStatus: ok` for days while inbound
209
+ * delivery was dead because the server was POSTing to a stale webhook port.
210
+ * A sense opts in by calling this with its inbound log directory.
211
+ */
212
+ function senseInboundDeliveryCheck(input) {
213
+ const result = senseInboundDeliveryResult(input);
214
+ return { id: `senses.${input.sense}.inbound_liveness`, label: `${input.agentDir} ${input.sense} inbound delivery`, status: result.status, detail: result.detail };
215
+ }
216
+ function senseInboundDeliveryResult(input) {
217
+ const suffix = input.logSuffix ?? ".ndjson";
218
+ const probe = (0, freshness_1.observeAppendPerEventStore)(input.inboundDir, input.deps, { suffix, maxEntries: SENSE_DELIVERY_MAX_LOG_SCAN });
219
+ return (0, freshness_1.evaluateFreshness)({
220
+ activity: `${input.sense} inbound delivery`,
221
+ unit: "inbound message",
222
+ observation: probe.observation,
223
+ provenance: probe.provenance,
224
+ thresholds: (0, freshness_1.resolveFreshnessThresholds)(exports.DEFAULT_SENSE_DELIVERY_THRESHOLDS, senseFreshnessOverride(input.deps, input.agentDir, input.sense)),
225
+ remediation: input.remediation,
226
+ configuredSinceMs: input.configuredSincePath ? pathMtimeMs(input.deps, input.configuredSincePath) : null,
227
+ context: input.context,
228
+ nowMs: input.nowMs,
229
+ });
230
+ }
231
+ const DEFINITIVE_BLUEBUBBLES_FAILURES = new Set([
232
+ "auth-failure",
233
+ "rate-limit",
234
+ "server-error",
235
+ "usage-limit",
236
+ ]);
237
+ const DEFINITIVE_BLUEBUBBLES_WEBHOOK_STATES = new Set([
238
+ "missing",
239
+ "drifted",
240
+ "auth-failed",
241
+ "malformed",
242
+ "listener-not-ready",
243
+ ]);
244
+ function blueBubblesUpstreamEvidence(probe) {
245
+ if (probe.ok)
246
+ return "healthy";
247
+ if (DEFINITIVE_BLUEBUBBLES_FAILURES.has(probe.classification)) {
248
+ return "definitive-fault";
249
+ }
250
+ return "unavailable";
251
+ }
252
+ function blueBubblesInboundDeliveryCheck(input, evidence) {
253
+ const result = senseInboundDeliveryResult(input);
254
+ const base = {
255
+ id: "senses.bluebubbles.inbound_liveness",
256
+ label: `${input.agentDir} bluebubbles inbound delivery`,
257
+ };
258
+ if (result.state !== "stale" && result.state !== "never") {
259
+ return { ...base, status: result.status, detail: result.detail };
260
+ }
261
+ const webhookDefinitive = DEFINITIVE_BLUEBUBBLES_WEBHOOK_STATES.has(evidence.webhook);
262
+ if (evidence.upstream === "definitive-fault" || webhookDefinitive) {
263
+ const corroboration = evidence.webhook === "missing"
264
+ ? "missing owned webhook corroborates the silence"
265
+ : webhookDefinitive
266
+ ? `${evidence.webhook} owned-webhook evidence corroborates the silence`
267
+ : "a definitive upstream fault corroborates the silence";
268
+ return { ...base, status: "fail", detail: `${corroboration}; ${result.detail}` };
269
+ }
270
+ const orientation = evidence.upstream === "healthy" && evidence.webhook === "exact"
271
+ ? "no recent inbound was observed; upstream and exact owned webhook are healthy"
272
+ : evidence.upstream === "not-run"
273
+ ? "no recent inbound was observed; infrastructure probes were not run, so delivery is unverified"
274
+ : "no recent inbound was observed; infrastructure probes were unavailable, so delivery is unverified";
275
+ const observed = result.detail.replace(/; fix: .*$/u, "");
276
+ return {
277
+ ...base,
278
+ status: "warn",
279
+ detail: `${orientation}; quiet and delivery failure are indistinguishable without a new message; doctor did not send a test message; ${observed}`,
280
+ };
281
+ }
282
+ function checkAgents(deps) {
283
+ const checks = [];
284
+ if (!deps.existsSync(deps.bundlesRoot)) {
285
+ checks.push({ label: "bundles directory", status: "fail", detail: `${deps.bundlesRoot} not found` });
286
+ return { name: "Agents", checks };
287
+ }
288
+ const agents = discoverAgents(deps);
289
+ if (agents.length === 0) {
290
+ checks.push({ label: "agent bundles", status: "warn", detail: "no agent bundles found" });
291
+ return { name: "Agents", checks };
292
+ }
293
+ for (const agentDir of agents) {
294
+ const agentPath = `${deps.bundlesRoot}/${agentDir}`;
295
+ const configPath = `${agentPath}/agent.json`;
296
+ let config;
297
+ try {
298
+ config = JSON.parse(deps.readFileSync(configPath));
299
+ }
300
+ catch {
301
+ checks.push({ label: `${agentDir}/agent.json`, status: "fail", detail: "unparseable JSON" });
302
+ continue;
303
+ }
304
+ const missing = [];
305
+ if (!config.version)
306
+ missing.push("version");
307
+ if (!config.humanFacing || typeof config.humanFacing !== "object") {
308
+ missing.push("humanFacing");
309
+ }
310
+ else {
311
+ const hf = config.humanFacing;
312
+ if (!hf.provider)
313
+ missing.push("humanFacing.provider");
314
+ if (!hf.model)
315
+ missing.push("humanFacing.model");
316
+ }
317
+ if (!config.agentFacing || typeof config.agentFacing !== "object") {
318
+ missing.push("agentFacing");
319
+ }
320
+ else {
321
+ const af = config.agentFacing;
322
+ if (!af.provider)
323
+ missing.push("agentFacing.provider");
324
+ if (!af.model)
325
+ missing.push("agentFacing.model");
326
+ }
327
+ if (missing.length > 0) {
328
+ checks.push({ label: `${agentDir}/agent.json`, status: "warn", detail: `missing fields: ${missing.join(", ")}` });
329
+ }
330
+ else {
331
+ checks.push({ label: `${agentDir}/agent.json`, status: "pass", detail: "valid" });
332
+ }
333
+ }
334
+ return { name: "Agents", checks };
335
+ }
336
+ /**
337
+ * Mail key coverage, as recorded by the mail sense.
338
+ *
339
+ * Doctor reads the sense's own per-cycle verdict rather than re-deriving it:
340
+ * a health check must need neither network access nor Azure credentials, and
341
+ * the registry that declares the key ids lives in a Blob container in hosted
342
+ * mode. The mail sense already fetches it every cycle, so its record is both
343
+ * the cheapest and the only honest source here.
344
+ *
345
+ * Absent is a `fail` because mail encrypted to a key with no private half is
346
+ * permanently unreadable — unlike an API key, ciphertext cannot be reissued.
347
+ * Anything the sense could not verify is a `warn`, never a `fail`: a locked or
348
+ * unavailable vault must not read as key loss.
349
+ */
350
+ function mailKeyCoverageCheck(deps, agentDir) {
351
+ const id = "mail.key_coverage";
352
+ const label = `${agentDir} mail key coverage`;
353
+ const statePath = `${deps.bundlesRoot}/${agentDir}/state/senses/mail/runtime.json`;
354
+ const unverified = (detail) => ({ id, label, status: "warn", detail });
355
+ if (!deps.existsSync(statePath)) {
356
+ return unverified(`no mail sense runtime state at ${statePath} — coverage is unverified until the mail sense runs a scan`);
357
+ }
358
+ let coverage;
359
+ try {
360
+ coverage = asRecord(JSON.parse(deps.readFileSync(statePath)).keyCoverage) ?? {};
361
+ }
362
+ catch {
363
+ return unverified(`mail sense runtime state at ${statePath} could not be read`);
364
+ }
365
+ const declaredKeyIds = stringArrayField(coverage, "declaredKeyIds");
366
+ const absentKeyIds = stringArrayField(coverage, "absentKeyIds");
367
+ const checkedAt = textField(coverage, "checkedAt");
368
+ const provenance = checkedAt ? ` (recorded ${checkedAt})` : "";
369
+ if (coverage.status === "covered") {
370
+ return {
371
+ id,
372
+ label,
373
+ status: "pass",
374
+ detail: declaredKeyIds.length === 1
375
+ ? `the 1 registry-declared mail key has a private half in the vault${provenance}`
376
+ : `all ${declaredKeyIds.length} registry-declared mail keys have a private half in the vault${provenance}`,
377
+ };
378
+ }
379
+ if (coverage.status === "absent") {
380
+ return {
381
+ id,
382
+ label,
383
+ status: "fail",
384
+ detail: `the mailroom registry declares mail key${absentKeyIds.length === 1 ? "" : "s"} with no private half in this agent's vault — mail encrypted to ${absentKeyIds.join(", ")} cannot be decrypted and cannot be recovered${provenance}; run \`ouro connect mail --agent ${agentDir.replace(/\.ouro$/, "")}\` and, if Mail Control no longer holds the one-time key, rerun it with --rotate-missing-mail-keys to mint a replacement for future mail`,
385
+ };
386
+ }
387
+ return unverified(`mail sense could not verify key coverage${provenance}: ${textField(coverage, "reason") || "no reason recorded"}`);
388
+ }
389
+ async function checkSenses(deps) {
390
+ const checks = [];
391
+ const agents = discoverAgents(deps);
392
+ for (const agentDir of agents) {
393
+ const agentName = agentDir.replace(/\.ouro$/, "");
394
+ const configPath = `${deps.bundlesRoot}/${agentDir}/agent.json`;
395
+ if (!deps.existsSync(configPath))
396
+ continue;
397
+ let config;
398
+ try {
399
+ config = JSON.parse(deps.readFileSync(configPath));
400
+ }
401
+ catch {
402
+ checks.push({ label: `${agentDir} senses`, status: "fail", detail: "agent.json unparseable" });
403
+ continue;
404
+ }
405
+ if (!config.senses || typeof config.senses !== "object") {
406
+ checks.push({ label: `${agentDir} senses`, status: "warn", detail: "no senses config block" });
407
+ continue;
408
+ }
409
+ const senses = config.senses;
410
+ const senseNames = ["cli", "teams", "bluebubbles", "mail"];
411
+ for (const sense of senseNames) {
412
+ if (!(sense in senses))
413
+ continue;
414
+ const entry = senses[sense];
415
+ if (!entry || typeof entry !== "object") {
416
+ checks.push({ label: `${agentDir} ${sense}`, status: "fail", detail: "malformed sense entry" });
417
+ continue;
418
+ }
419
+ const senseObj = entry;
420
+ if (typeof senseObj.enabled !== "boolean") {
421
+ checks.push({ label: `${agentDir} ${sense}`, status: "warn", detail: "missing enabled boolean" });
422
+ }
423
+ else {
424
+ checks.push({
425
+ label: `${agentDir} ${sense}`,
426
+ status: "pass",
427
+ detail: senseObj.enabled ? "enabled" : "disabled",
428
+ });
429
+ }
430
+ if (sense === "bluebubbles" && senseObj.enabled === true) {
431
+ const machineId = (0, machine_identity_1.loadOrCreateMachineIdentity)({ homeDir: deps.homedir }).machineId;
432
+ const runtimeConfig = await (0, runtime_credentials_1.refreshMachineRuntimeCredentialConfig)(agentName, machineId, { preserveCachedOnFailure: true });
433
+ if (!runtimeConfig.ok) {
434
+ if (runtimeConfig.reason === "missing") {
435
+ checks.push({
436
+ label: `${agentDir} bluebubbles config`,
437
+ status: "pass",
438
+ detail: "not attached on this machine",
439
+ });
440
+ continue;
441
+ }
442
+ checks.push({
443
+ label: `${agentDir} bluebubbles config`,
444
+ status: "fail",
445
+ detail: `machine runtime config unavailable: ${runtimeConfig.error}`,
446
+ });
447
+ continue;
448
+ }
449
+ const bluebubbles = asRecord(runtimeConfig.config.bluebubbles);
450
+ const bluebubblesChannel = asRecord(runtimeConfig.config.bluebubblesChannel);
451
+ const serverUrl = textField(bluebubbles, "serverUrl");
452
+ const password = textField(bluebubbles, "password");
453
+ const missing = [];
454
+ if (!serverUrl)
455
+ missing.push("bluebubbles.serverUrl");
456
+ if (!password)
457
+ missing.push("bluebubbles.password");
458
+ if (missing.length > 0) {
459
+ checks.push({
460
+ label: `${agentDir} bluebubbles config`,
461
+ status: "fail",
462
+ detail: `missing ${missing.join("/")}`,
463
+ });
464
+ continue;
465
+ }
466
+ checks.push({
467
+ label: `${agentDir} bluebubbles config`,
468
+ status: "pass",
469
+ detail: serverUrl,
470
+ });
471
+ let upstreamContext = "upstream probe not run in this pass";
472
+ let infrastructure = { upstream: "not-run", webhook: null };
473
+ if (deps.fetchImpl) {
474
+ const requestTimeoutMs = numberField(bluebubblesChannel, "requestTimeoutMs", DEFAULT_BLUEBUBBLES_REQUEST_TIMEOUT_MS);
475
+ const probe = await (0, bluebubbles_health_diagnostics_1.probeBlueBubblesHealth)({
476
+ serverUrl,
477
+ password,
478
+ requestTimeoutMs,
479
+ fetchImpl: deps.fetchImpl,
480
+ });
481
+ checks.push({
482
+ label: `${agentDir} bluebubbles upstream`,
483
+ status: probe.ok ? "pass" : "fail",
484
+ detail: probe.detail,
485
+ });
486
+ upstreamContext = probe.ok
487
+ ? "upstream probe reachable — which does not prove inbound delivery"
488
+ : "upstream probe failing";
489
+ infrastructure = { ...infrastructure, upstream: blueBubblesUpstreamEvidence(probe) };
490
+ const webhook = await (0, webhook_registration_1.inspectBlueBubblesWebhookRegistration)({
491
+ serverUrl,
492
+ password,
493
+ callbackPort: numberField(bluebubblesChannel, "port", DEFAULT_BLUEBUBBLES_PORT),
494
+ callbackPath: textField(bluebubblesChannel, "webhookPath") || DEFAULT_BLUEBUBBLES_WEBHOOK_PATH,
495
+ agentName,
496
+ machineId,
497
+ requestTimeoutMs,
498
+ listenerReady: true,
499
+ }, { fetchImpl: deps.fetchImpl });
500
+ checks.push({
501
+ id: "senses.bluebubbles.webhook",
502
+ label: `${agentDir} bluebubbles webhook`,
503
+ status: webhook.ok ? "pass" : "fail",
504
+ detail: webhook.detail,
505
+ });
506
+ infrastructure = { ...infrastructure, webhook: webhook.state };
507
+ }
508
+ const bluebubblesStateRoot = `${deps.bundlesRoot}/${agentDir}/state/senses/bluebubbles`;
509
+ checks.push(blueBubblesInboundDeliveryCheck({
510
+ deps,
511
+ agentDir,
512
+ sense: "bluebubbles",
513
+ inboundDir: `${bluebubblesStateRoot}/inbound`,
514
+ configuredSincePath: bluebubblesStateRoot,
515
+ context: upstreamContext,
516
+ remediation: "no iMessage is reaching this agent — confirm the BlueBubbles server's configured webhook URL/port matches the port this daemon is listening on (a stale webhook port is the known cause), then re-attach with `ouro connect bluebubbles --agent <agent>`",
517
+ nowMs: Date.now(),
518
+ }, infrastructure));
519
+ }
520
+ if (sense === "mail" && senseObj.enabled === true) {
521
+ const runtimeConfig = await (0, runtime_credentials_1.refreshRuntimeCredentialConfig)(agentName, { preserveCachedOnFailure: true });
522
+ if (!runtimeConfig.ok) {
523
+ checks.push({
524
+ label: `${agentDir} mail config`,
525
+ status: "fail",
526
+ detail: `runtime config unavailable: ${runtimeConfig.error}`,
527
+ });
528
+ continue;
529
+ }
530
+ const mailroom = asRecord(runtimeConfig.config.mailroom);
531
+ const workSubstrate = asRecord(runtimeConfig.config.workSubstrate);
532
+ const mailboxAddress = textField(mailroom, "mailboxAddress");
533
+ const hosted = textField(workSubstrate, "mode") === "hosted";
534
+ const azureAccountUrl = textField(mailroom, "azureAccountUrl");
535
+ const azureContainer = textField(mailroom, "azureContainer") || "mailroom";
536
+ const missing = [];
537
+ if (!mailboxAddress)
538
+ missing.push("mailroom.mailboxAddress");
539
+ if (!hasStringRecordValue(mailroom?.privateKeys))
540
+ missing.push("mailroom.privateKeys");
541
+ if (hosted && !azureAccountUrl)
542
+ missing.push("mailroom.azureAccountUrl for hosted Blob reader");
543
+ if (missing.length > 0) {
544
+ checks.push({
545
+ label: `${agentDir} mail config`,
546
+ status: "fail",
547
+ detail: `missing ${missing.join("/")}`,
548
+ });
549
+ continue;
550
+ }
551
+ checks.push({
552
+ label: `${agentDir} mail config`,
553
+ status: "pass",
554
+ detail: [
555
+ mailboxAddress,
556
+ hosted ? `hosted azure-blob ${azureAccountUrl}/${azureContainer}` : "local file Mailroom",
557
+ mailAutonomyDetail(mailroom),
558
+ ].join("; "),
559
+ });
560
+ // Config being well-formed says nothing about whether the vault can
561
+ // still decrypt what the registry points mail at — the 2026-07-24 gap
562
+ // sat behind a passing mail config check for 23 hours.
563
+ checks.push(mailKeyCoverageCheck(deps, agentDir));
564
+ }
565
+ }
566
+ }
567
+ if (checks.length === 0) {
568
+ checks.push({ label: "senses", status: "warn", detail: "no agents with senses config found" });
569
+ }
570
+ return { name: "Senses", checks };
571
+ }
572
+ function checkHabits(deps) {
573
+ const checks = [];
574
+ const agents = discoverAgents(deps);
575
+ for (const agentDir of agents) {
576
+ const agentName = agentDir.replace(/\.ouro$/, "");
577
+ const habitsDir = `${deps.bundlesRoot}/${agentDir}/habits`;
578
+ if (!deps.existsSync(habitsDir)) {
579
+ checks.push({ label: `${agentDir} habits dir`, status: "warn", detail: "no habits directory" });
580
+ continue;
581
+ }
582
+ checks.push({ label: `${agentDir} habits dir`, status: "pass", detail: habitsDir });
583
+ const activeScheduledHabits = [];
584
+ const habitLifecycleRows = [];
585
+ let unreadableHabits = 0;
586
+ for (const file of deps.readdirSync(habitsDir).filter((entry) => entry.endsWith(".md") && entry !== "README.md").sort()) {
587
+ const filePath = `${habitsDir}/${file}`;
588
+ let habit;
589
+ try {
590
+ habit = (0, habit_parser_1.parseHabitFile)(deps.readFileSync(filePath), filePath);
591
+ }
592
+ catch (error) {
593
+ unreadableHabits += 1;
594
+ habit = (0, habit_parser_1.createDegradedHabitFile)(filePath, "read_error", "", error instanceof Error ? error.message : String(error));
595
+ }
596
+ habitLifecycleRows.push(habit);
597
+ if (habit.status === "active" && habit.cadence && (0, cadence_1.parseCadenceToCron)(habit.cadence) !== null) {
598
+ activeScheduledHabits.push(habit.name);
599
+ }
600
+ }
601
+ if (habitLifecycleRows.length > 0) {
602
+ const hasDegradedHabit = habitLifecycleRows.some((habit) => habit.status === "degraded");
603
+ checks.push({
604
+ id: "habits.lifecycle",
605
+ label: `${agentDir} habit lifecycle`,
606
+ status: hasDegradedHabit ? "fail" : "pass",
607
+ detail: habitLifecycleRows.map((habit) => habit.status === "degraded"
608
+ ? `${habit.name}=degraded(${habit.degradedReason})`
609
+ : `${habit.name}=${habit.status}`).join(", "),
610
+ });
611
+ }
612
+ if (unreadableHabits > 0) {
613
+ checks.push({
614
+ label: `${agentDir} habit files`,
615
+ status: "warn",
616
+ detail: `${unreadableHabits} unreadable habit file(s)`,
617
+ });
618
+ }
619
+ if (activeScheduledHabits.length === 0) {
620
+ checks.push({
621
+ label: `${agentDir} launchd plists`,
622
+ status: "pass",
623
+ detail: "no active scheduled habits require launchd",
624
+ });
625
+ continue;
626
+ }
627
+ // Check for launchd plists on macOS
628
+ const platform = deps.platform;
629
+ if (platform !== "darwin") {
630
+ checks.push({
631
+ label: `${agentDir} launchd plists`,
632
+ status: "pass",
633
+ detail: `launchd not applicable on ${platform}; scheduled habits use the platform cron manager`,
634
+ });
635
+ continue;
636
+ }
637
+ const launchAgentsDir = `${deps.homedir}/Library/LaunchAgents`;
638
+ if (deps.existsSync(launchAgentsDir)) {
639
+ const plists = new Set(deps.readdirSync(launchAgentsDir));
640
+ const missing = activeScheduledHabits
641
+ .map((habitName) => `bot.ouro.${agentName}.${habitName}.plist`)
642
+ .filter((plistName) => !plists.has(plistName));
643
+ if (missing.length === 0) {
644
+ checks.push({
645
+ label: `${agentDir} launchd plists`,
646
+ status: "pass",
647
+ detail: `${activeScheduledHabits.length} active scheduled habit(s) registered`,
648
+ });
649
+ }
650
+ else {
651
+ checks.push({
652
+ label: `${agentDir} launchd plists`,
653
+ status: "fail",
654
+ detail: `missing ${missing.join(", ")}`,
655
+ });
656
+ }
657
+ }
658
+ else {
659
+ checks.push({
660
+ label: `${agentDir} launchd plists`,
661
+ status: "fail",
662
+ detail: `${launchAgentsDir} not found for ${activeScheduledHabits.length} active scheduled habit(s)`,
663
+ });
664
+ }
665
+ }
666
+ if (checks.length === 0) {
667
+ checks.push({ label: "habits", status: "warn", detail: "no agents found" });
668
+ }
669
+ return { name: "Habits", checks };
670
+ }
671
+ function hasRsvpHabitSignal(deps, agentPath) {
672
+ const habitsDir = `${agentPath}/habits`;
673
+ if (!deps.existsSync(habitsDir))
674
+ return false;
675
+ try {
676
+ return deps.readdirSync(habitsDir).some((name) => name.toLowerCase().includes("rsvp") && name.endsWith(".md"));
677
+ }
678
+ catch {
679
+ return false;
680
+ }
681
+ }
682
+ function rsvpDoctorLabel(agentDir, checkId) {
683
+ const suffix = {
684
+ "rsvp.native_config": "RSVP native config",
685
+ "rsvp.aisleplanner_source": "RSVP AislePlanner source",
686
+ "rsvp.aisleplanner_credentials": "RSVP AislePlanner credentials",
687
+ "rsvp.bluebubbles_route": "RSVP BlueBubbles route",
688
+ "rsvp.bluebubbles_attachment": "RSVP BlueBubbles attachment",
689
+ };
690
+ return `${agentDir} ${suffix[checkId]}`;
691
+ }
692
+ function rsvpDoctorId(checkId) {
693
+ const ids = {
694
+ "rsvp.native_config": "rsvp.native_config",
695
+ "rsvp.aisleplanner_source": "rsvp.aisleplanner.source",
696
+ "rsvp.aisleplanner_credentials": "rsvp.aisleplanner.credentials",
697
+ "rsvp.bluebubbles_route": "rsvp.bluebubbles.route",
698
+ "rsvp.bluebubbles_attachment": "rsvp.bluebubbles.attachment",
699
+ };
700
+ return ids[checkId];
701
+ }
702
+ function rsvpDoctorDetail(readinessCheck) {
703
+ return readinessCheck.id === "rsvp.bluebubbles_route" && readinessCheck.status === "pass"
704
+ ? "configured"
705
+ : readinessCheck.detail;
706
+ }
707
+ function doctorStatus(status) {
708
+ return status;
709
+ }
710
+ function discoverRsvpCutoverLegacyRoot(deps, config) {
711
+ const configured = typeof config?.cutover?.legacyRoot === "string" ? config.cutover.legacyRoot.trim() : "";
712
+ if (configured)
713
+ return configured;
714
+ if (deps.rsvpCutoverLegacyRoot)
715
+ return deps.rsvpCutoverLegacyRoot;
716
+ const candidate = `${deps.homedir}/Projects/rsvp-tracker`;
717
+ return deps.existsSync(`${candidate}/config.json`) ? candidate : null;
718
+ }
719
+ function rsvpCutoverDetail(checks, sendAllowed, denialCount) {
720
+ return [
721
+ `sendAllowed=${sendAllowed}`,
722
+ `launchAgentInactive=${checks.launchAgentInactive}`,
723
+ `legacyProcessInactive=${checks.legacyProcessInactive}`,
724
+ `legacyConfigSendInactive=${checks.legacyConfigSendInactive}`,
725
+ `legacyLiveSendInactive=${checks.legacyLiveSendInactive}`,
726
+ `nativeBlueBubblesCredentialHealthy=${checks.nativeBlueBubblesCredentialHealthy}`,
727
+ `denialCount=${denialCount}`,
728
+ ].join("; ");
729
+ }
730
+ async function checkRsvp(deps) {
731
+ const checks = [];
732
+ const agents = discoverAgents(deps);
733
+ if (agents.length === 0) {
734
+ checks.push({ label: "RSVP", status: "warn", detail: "no agent bundles found" });
735
+ return { name: "RSVP", checks };
736
+ }
737
+ for (const agentDir of agents) {
738
+ const agentName = agentDir.replace(/\.ouro$/, "");
739
+ const agentPath = `${deps.bundlesRoot}/${agentDir}`;
740
+ const config = (0, config_1.readRsvpConfig)(agentPath);
741
+ const hasRsvpSignal = config.ok || hasRsvpHabitSignal(deps, agentPath);
742
+ if (!hasRsvpSignal) {
743
+ checks.push({ label: `${agentDir} RSVP`, status: "pass", detail: "not configured" });
744
+ continue;
745
+ }
746
+ const machineId = (0, machine_identity_1.loadOrCreateMachineIdentity)({ homeDir: deps.homedir }).machineId;
747
+ const runtimeConfig = await (0, runtime_credentials_1.refreshRuntimeCredentialConfig)(agentName, { preserveCachedOnFailure: true });
748
+ const machineRuntimeConfig = await (0, runtime_credentials_1.refreshMachineRuntimeCredentialConfig)(agentName, machineId, { preserveCachedOnFailure: true });
749
+ const readiness = (0, config_1.validateRsvpReadiness)({
750
+ agent: agentName,
751
+ agentRoot: agentPath,
752
+ runtimeConfig,
753
+ machineRuntimeConfig,
754
+ config,
755
+ });
756
+ for (const readinessCheck of readiness.checks) {
757
+ checks.push({
758
+ id: rsvpDoctorId(readinessCheck.id),
759
+ label: rsvpDoctorLabel(agentDir, readinessCheck.id),
760
+ status: readinessCheck.status === "pass" ? "pass" : "fail",
761
+ detail: rsvpDoctorDetail(readinessCheck),
762
+ });
763
+ }
764
+ const attachment = readiness.checks.find((check) => check.id === "rsvp.bluebubbles_attachment");
765
+ if (attachment) {
766
+ checks.push({
767
+ id: "rsvp.bluebubbles.attachment_identity",
768
+ label: `${agentDir} RSVP BlueBubbles attachment identity`,
769
+ status: attachment.status === "pass" ? "pass" : "fail",
770
+ detail: attachment.status === "pass" ? "machine attachment credential present" : attachment.detail,
771
+ });
772
+ }
773
+ const diagnostics = (0, diagnostics_1.collectRsvpDiagnostics)(agentPath, deps);
774
+ checks.push({
775
+ id: "rsvp.context_packet_ledger",
776
+ label: `${agentDir} RSVP context packet ledger`,
777
+ status: doctorStatus(diagnostics.contextPacketLedger.status),
778
+ detail: diagnostics.contextPacketLedger.detail,
779
+ }, {
780
+ id: "rsvp.habit.schedule",
781
+ label: `${agentDir} RSVP habit schedule`,
782
+ status: doctorStatus(diagnostics.habitSchedule.status),
783
+ detail: diagnostics.habitSchedule.detail,
784
+ }, {
785
+ id: "rsvp.latest_fetch",
786
+ label: `${agentDir} RSVP latest fetch`,
787
+ status: doctorStatus(diagnostics.latestFetch.status),
788
+ detail: diagnostics.latestFetch.detail,
789
+ }, {
790
+ id: "rsvp.delivery.reconciliation",
791
+ label: `${agentDir} RSVP delivery reconciliation`,
792
+ status: doctorStatus(diagnostics.deliveryReconciliation.status),
793
+ detail: diagnostics.deliveryReconciliation.detail,
794
+ }, {
795
+ id: "rsvp.spend_timeline",
796
+ label: `${agentDir} RSVP spend timeline`,
797
+ status: doctorStatus(diagnostics.spendTimeline.status),
798
+ detail: diagnostics.spendTimeline.detail,
799
+ });
800
+ const legacyRoot = discoverRsvpCutoverLegacyRoot(deps, config.ok ? config.config : undefined);
801
+ if (config.ok && legacyRoot) {
802
+ const cutover = await (0, cutover_1.checkRsvpCutover)({
803
+ agent: agentName,
804
+ legacyRoot,
805
+ ...(deps.rsvpCutoverDeps ? { deps: deps.rsvpCutoverDeps } : {}),
806
+ });
807
+ checks.push({
808
+ id: "rsvp.cutover.live_send_preflight",
809
+ label: `${agentDir} RSVP legacy live-send preflight`,
810
+ status: cutover.sendAllowed ? "pass" : "fail",
811
+ detail: rsvpCutoverDetail(cutover.checks, cutover.sendAllowed, cutover.denialReasons.length),
812
+ });
813
+ }
814
+ }
815
+ return { name: "RSVP", checks };
816
+ }
817
+ function checkSecurity(deps) {
818
+ const checks = [];
819
+ const agents = discoverAgents(deps);
820
+ for (const agentDir of agents) {
821
+ // Check agent.json for leaked credential keys
822
+ const configPath = `${deps.bundlesRoot}/${agentDir}/agent.json`;
823
+ if (deps.existsSync(configPath)) {
824
+ try {
825
+ const raw = deps.readFileSync(configPath);
826
+ const found = credentialKeyLeaks(raw);
827
+ if (found.length > 0) {
828
+ checks.push({ label: `${agentDir} credential leak`, status: "warn", detail: `agent.json contains keys: ${found.join(", ")}` });
829
+ }
830
+ else {
831
+ checks.push({ label: `${agentDir} credential leak`, status: "pass", detail: "no credential keys in agent.json" });
832
+ }
833
+ }
834
+ catch {
835
+ checks.push({ label: `${agentDir} credential leak`, status: "fail", detail: "could not read agent.json" });
836
+ }
837
+ }
838
+ }
839
+ if (checks.length === 0) {
840
+ checks.push({ label: "security", status: "warn", detail: "no agents found" });
841
+ }
842
+ return { name: "Security", checks };
843
+ }
844
+ function checkTrips(deps) {
845
+ const checks = [];
846
+ const agents = discoverAgents(deps);
847
+ if (agents.length === 0) {
848
+ checks.push({ label: "trip ledger", status: "warn", detail: "no agent bundles found" });
849
+ return { name: "Trips", checks };
850
+ }
851
+ for (const agentDir of agents) {
852
+ const durableTripsRoot = `${deps.bundlesRoot}/${agentDir}/trips`;
853
+ const legacyTripsRoot = `${deps.bundlesRoot}/${agentDir}/state/trips`;
854
+ const hasDurableTrips = deps.existsSync(durableTripsRoot);
855
+ const hasLegacyTrips = deps.existsSync(legacyTripsRoot);
856
+ if (!hasDurableTrips) {
857
+ if (hasLegacyTrips) {
858
+ checks.push({
859
+ label: `${agentDir} trip ledger`,
860
+ status: "warn",
861
+ detail: "legacy state/trips exists but durable trips/ missing — run any trip tool to copy legacy storage into trips/",
862
+ });
863
+ }
864
+ else {
865
+ // Trip ledger is optional; absence is fine. Pass with a hint.
866
+ checks.push({ label: `${agentDir} trip ledger`, status: "pass", detail: "no ledger directory (no trips ensured yet)" });
867
+ }
868
+ continue;
869
+ }
870
+ const ledgerPath = `${durableTripsRoot}/ledger.json`;
871
+ if (!deps.existsSync(ledgerPath)) {
872
+ checks.push({ label: `${agentDir} trip ledger`, status: "warn", detail: "trips/ exists but ledger.json missing — run trip_ensure_ledger" });
873
+ continue;
874
+ }
875
+ let raw;
876
+ /* v8 ignore start -- defensive: readFileSync failure after existsSync passes is a race-condition fallback @preserve */
877
+ try {
878
+ raw = deps.readFileSync(ledgerPath);
879
+ }
880
+ catch {
881
+ checks.push({ label: `${agentDir} trip ledger`, status: "fail", detail: "ledger.json could not be read" });
882
+ continue;
883
+ }
884
+ /* v8 ignore stop */
885
+ let parsed;
886
+ try {
887
+ parsed = JSON.parse(raw);
888
+ }
889
+ catch {
890
+ checks.push({ label: `${agentDir} trip ledger`, status: "fail", detail: "ledger.json is not valid JSON" });
891
+ continue;
892
+ }
893
+ const nestedLedger = parsed.ledger && typeof parsed.ledger === "object"
894
+ ? parsed.ledger
895
+ : null;
896
+ const ledgerId = typeof parsed.ledgerId === "string"
897
+ ? parsed.ledgerId
898
+ : typeof nestedLedger?.ledgerId === "string"
899
+ ? nestedLedger.ledgerId
900
+ : null;
901
+ const hasPrivateKey = typeof parsed.privateKeyPem === "string" && parsed.privateKeyPem.includes("BEGIN");
902
+ if (!ledgerId) {
903
+ checks.push({ label: `${agentDir} trip ledger`, status: "warn", detail: "ledger.json missing ledgerId field" });
904
+ continue;
905
+ }
906
+ if (!hasPrivateKey) {
907
+ checks.push({ label: `${agentDir} trip ledger`, status: "fail", detail: `${ledgerId}: privateKeyPem missing — encrypted trip records cannot be read` });
908
+ continue;
909
+ }
910
+ let recordCount = 0;
911
+ const recordsDir = `${durableTripsRoot}/records`;
912
+ /* v8 ignore start -- defensive: records dir presence and readdir error are filesystem-state branches not all exercised by tests; pluralization branch likewise depends on record count fixtures @preserve */
913
+ if (deps.existsSync(recordsDir)) {
914
+ try {
915
+ recordCount = deps.readdirSync(recordsDir).filter((name) => name.endsWith(".json")).length;
916
+ }
917
+ catch {
918
+ // ignore — the warn detail will still report 0 records
919
+ }
920
+ }
921
+ const legacyDiverges = hasLegacyTrips && tripStoresDiffer(deps, durableTripsRoot, legacyTripsRoot);
922
+ checks.push({
923
+ label: `${agentDir} trip ledger`,
924
+ status: legacyDiverges ? "warn" : "pass",
925
+ detail: `${ledgerId} (${recordCount} record${recordCount === 1 ? "" : "s"})${legacyDiverges ? "; legacy state/trips differs from durable trips/ — durable trips/ is authoritative" : ""}`,
926
+ });
927
+ /* v8 ignore stop */
928
+ }
929
+ return { name: "Trips", checks };
930
+ }
931
+ function listTripStoreFiles(deps, root) {
932
+ const files = [];
933
+ if (deps.existsSync(`${root}/ledger.json`)) {
934
+ files.push("ledger.json");
935
+ }
936
+ const recordsDir = `${root}/records`;
937
+ if (deps.existsSync(recordsDir)) {
938
+ for (const name of deps.readdirSync(recordsDir)) {
939
+ if (name.endsWith(".json")) {
940
+ files.push(`records/${name}`);
941
+ }
942
+ }
943
+ }
944
+ return files.sort();
945
+ }
946
+ function tripStoresDiffer(deps, durableRoot, legacyRoot) {
947
+ const relativePaths = new Set([
948
+ ...listTripStoreFiles(deps, durableRoot),
949
+ ...listTripStoreFiles(deps, legacyRoot),
950
+ ]);
951
+ for (const relativePath of relativePaths) {
952
+ const durablePath = `${durableRoot}/${relativePath}`;
953
+ const legacyPath = `${legacyRoot}/${relativePath}`;
954
+ const durableExists = deps.existsSync(durablePath);
955
+ const legacyExists = deps.existsSync(legacyPath);
956
+ if (durableExists !== legacyExists) {
957
+ return true;
958
+ }
959
+ if (durableExists && deps.readFileSync(durablePath) !== deps.readFileSync(legacyPath)) {
960
+ return true;
961
+ }
962
+ }
963
+ return false;
964
+ }
965
+ async function checkMailroom(deps) {
966
+ const checks = [];
967
+ const agents = discoverAgents(deps);
968
+ if (agents.length === 0) {
969
+ checks.push({ label: "mailroom", status: "warn", detail: "no agent bundles found" });
970
+ return { name: "Mailroom", checks };
971
+ }
972
+ for (const agentDir of agents) {
973
+ const mailroomRoot = `${deps.bundlesRoot}/${agentDir}/state/mailroom`;
974
+ if (!deps.existsSync(mailroomRoot)) {
975
+ checks.push({ label: `${agentDir} mailroom`, status: "pass", detail: "no mailroom directory (mail not connected)" });
976
+ continue;
977
+ }
978
+ const registryPath = `${mailroomRoot}/registry.json`;
979
+ if (!deps.existsSync(registryPath)) {
980
+ checks.push({ label: `${agentDir} mailroom`, status: "warn", detail: "state/mailroom/ exists but registry.json missing" });
981
+ continue;
982
+ }
983
+ let raw;
984
+ /* v8 ignore start -- defensive: readFileSync failure after existsSync passes is a race-condition fallback @preserve */
985
+ try {
986
+ raw = deps.readFileSync(registryPath);
987
+ }
988
+ catch {
989
+ checks.push({ label: `${agentDir} mailroom`, status: "fail", detail: "registry.json could not be read" });
990
+ continue;
991
+ }
992
+ /* v8 ignore stop */
993
+ let parsed;
994
+ try {
995
+ parsed = JSON.parse(raw);
996
+ }
997
+ catch {
998
+ checks.push({ label: `${agentDir} mailroom`, status: "fail", detail: "registry.json is not valid JSON" });
999
+ continue;
1000
+ }
1001
+ /* v8 ignore start -- defensive: registry shape is validated by mailroom code; non-array fallbacks are belt-and-suspenders @preserve */
1002
+ const mailboxes = Array.isArray(parsed.mailboxes) ? parsed.mailboxes : null;
1003
+ const sourceGrants = Array.isArray(parsed.sourceGrants) ? parsed.sourceGrants : [];
1004
+ /* v8 ignore stop */
1005
+ if (!mailboxes || mailboxes.length === 0) {
1006
+ checks.push({ label: `${agentDir} mailroom`, status: "warn", detail: "registry.json has no mailboxes — provision via `ouro connect mail`" });
1007
+ continue;
1008
+ }
1009
+ const messagesDir = `${mailroomRoot}/messages`;
1010
+ const listing = listJsonDocuments(deps, messagesDir);
1011
+ /* v8 ignore start -- defensive: pluralization branches depend on filesystem-state fixtures not exhaustively covered @preserve */
1012
+ checks.push({
1013
+ label: `${agentDir} mailroom`,
1014
+ status: "pass",
1015
+ detail: `${mailboxes.length} mailbox${mailboxes.length === 1 ? "" : "es"}, ${sourceGrants.length} source grant${sourceGrants.length === 1 ? "" : "s"}, ${listing.count} message${listing.count === 1 ? "" : "s"} stored (cumulative — not a liveness signal)`,
1016
+ });
1017
+ /* v8 ignore stop */
1018
+ const store = await resolveMailStore(agentDir);
1019
+ if (store.hosted === null) {
1020
+ checks.push({
1021
+ id: "mail.ingest_liveness",
1022
+ label: `${agentDir} mail ingest liveness`,
1023
+ status: store.status,
1024
+ detail: `mail store mode is unverified because fresh runtime credentials are unavailable: ${store.detail}; local-file and hosted liveness were not inferred from stale cached configuration`,
1025
+ });
1026
+ checks.push({
1027
+ id: "mail.cache_authority",
1028
+ label: `${agentDir} hosted mail cache authority`,
1029
+ status: store.status,
1030
+ detail: `fresh runtime credentials are unavailable: ${store.detail}; hosted authority was not probed from stale cached coordinates`,
1031
+ });
1032
+ continue;
1033
+ }
1034
+ const authorityCheck = store.hosted && deps.observeHostedMailAuthority
1035
+ ? await hostedMailCacheAuthorityCheck(deps, agentDir, store)
1036
+ : undefined;
1037
+ checks.push(mailIngestLivenessCheck(deps, agentDir, mailroomRoot, messagesDir, listing, store, authorityCheck));
1038
+ if (authorityCheck) {
1039
+ checks.push(authorityCheck);
1040
+ }
1041
+ }
1042
+ return { name: "Mailroom", checks };
1043
+ }
1044
+ function listJsonDocuments(deps, dir) {
1045
+ if (!deps.existsSync(dir))
1046
+ return { count: 0, readable: true };
1047
+ try {
1048
+ return { count: deps.readdirSync(dir).filter((name) => name.endsWith(".json")).length, readable: true };
1049
+ }
1050
+ catch {
1051
+ return { count: 0, readable: false };
1052
+ }
1053
+ }
1054
+ async function resolveMailStore(agentDir) {
1055
+ const agentName = agentDir.replace(/\.ouro$/, "");
1056
+ const runtimeConfig = await (0, runtime_credentials_1.refreshRuntimeCredentialConfig)(agentName, { preserveCachedOnFailure: false });
1057
+ if (!runtimeConfig.ok) {
1058
+ return {
1059
+ hosted: null,
1060
+ status: runtimeConfig.reason === "unavailable" ? "warn" : "fail",
1061
+ detail: `${runtimeConfig.error} (${runtimeConfig.itemPath})`,
1062
+ };
1063
+ }
1064
+ const mailroom = asRecord(runtimeConfig.config.mailroom);
1065
+ const azureAccountUrl = textField(mailroom, "azureAccountUrl");
1066
+ if (!azureAccountUrl)
1067
+ return { hosted: false };
1068
+ const azureContainer = textField(mailroom, "azureContainer") || "mailroom";
1069
+ const privateKeys = asRecord(mailroom?.privateKeys);
1070
+ return {
1071
+ hosted: true,
1072
+ label: `hosted azure-blob ${azureAccountUrl}/${azureContainer}`,
1073
+ privateKeyIds: new Set(Object.entries(privateKeys ?? {})
1074
+ .filter(([, value]) => typeof value === "string" && value.trim().length > 0)
1075
+ .map(([keyId]) => keyId)),
1076
+ };
1077
+ }
1078
+ function parseCacheDocument(deps, filePath) {
1079
+ try {
1080
+ const parsed = JSON.parse(deps.readFileSync(filePath));
1081
+ return asRecord(parsed);
1082
+ }
1083
+ catch {
1084
+ return null;
1085
+ }
1086
+ }
1087
+ async function hostedMailCacheAuthorityCheck(deps, agentDir, store) {
1088
+ const agentName = agentDir.replace(/\.ouro$/, "");
1089
+ const base = { id: "mail.cache_authority", label: `${agentDir} hosted mail cache authority` };
1090
+ let probe;
1091
+ try {
1092
+ probe = await deps.observeHostedMailAuthority(agentName);
1093
+ }
1094
+ catch (error) {
1095
+ return {
1096
+ ...base,
1097
+ status: "warn",
1098
+ detail: `hosted authority is temporarily unverified: ${error instanceof Error ? error.message : String(error)}; no local cache mutation was attempted`,
1099
+ };
1100
+ }
1101
+ if (!probe.ok) {
1102
+ return {
1103
+ ...base,
1104
+ status: probe.definitive ? "fail" : "warn",
1105
+ detail: `hosted authority unavailable: ${probe.detail}; no local cache mutation was attempted`,
1106
+ };
1107
+ }
1108
+ const { observation } = probe;
1109
+ if (observation.parseFailureCount > 0 || observation.duplicateIds.length > 0) {
1110
+ return {
1111
+ ...base,
1112
+ status: "warn",
1113
+ detail: `hosted authority is ambiguous (${observation.parseFailureCount} malformed index name(s), ${observation.duplicateIds.length} duplicate id(s)); no local cache mutation was attempted`,
1114
+ };
1115
+ }
1116
+ if (observation.records.length === 0) {
1117
+ return {
1118
+ ...base,
1119
+ status: "warn",
1120
+ detail: "hosted authority is empty and remains unverified because an empty index namespace cannot prove the mailbox is empty; no local cache mutation was attempted",
1121
+ };
1122
+ }
1123
+ const cacheDir = `${deps.bundlesRoot}/${agentDir}/state/mail-search`;
1124
+ let fileNames = [];
1125
+ if (deps.existsSync(cacheDir)) {
1126
+ try {
1127
+ fileNames = deps.readdirSync(cacheDir).filter((name) => name.endsWith(".json"));
1128
+ }
1129
+ catch (error) {
1130
+ return {
1131
+ ...base,
1132
+ status: "warn",
1133
+ detail: `hosted authority is sound but local cache is unreadable: ${error instanceof Error ? error.message : String(error)}; run \`ouro mail sync-cache --agent ${agentName}\``,
1134
+ };
1135
+ }
1136
+ }
1137
+ const recordsById = new Map(observation.records.map((record) => [record.id, record]));
1138
+ const documentsById = new Map();
1139
+ let malformedOrNoncanonical = 0;
1140
+ for (const fileName of fileNames) {
1141
+ const filePath = `${cacheDir}/${fileName}`;
1142
+ let regularFile = false;
1143
+ try {
1144
+ const fileStat = deps.lstatSync?.(filePath);
1145
+ regularFile = fileStat?.isFile?.() === true && !fileStat.isSymbolicLink();
1146
+ }
1147
+ catch {
1148
+ // A disappearing or unreadable entry is not usable cache evidence.
1149
+ }
1150
+ if (!regularFile) {
1151
+ malformedOrNoncanonical += 1;
1152
+ continue;
1153
+ }
1154
+ const document = parseCacheDocument(deps, filePath);
1155
+ if (!document || document.agentId !== agentName || fileName !== `${document.messageId}.json` || !recordsById.has(document.messageId)) {
1156
+ malformedOrNoncanonical += 1;
1157
+ continue;
1158
+ }
1159
+ documentsById.set(document.messageId, document);
1160
+ }
1161
+ const receiptsById = new Map();
1162
+ let invalidReceipts = 0;
1163
+ try {
1164
+ for (const inspected of (0, search_cache_1.inspectMailSearchSkipReceipts)(agentName, { cacheDirForAgent: () => cacheDir })) {
1165
+ const receipt = inspected.receipt;
1166
+ const record = receipt ? recordsById.get(receipt.messageId) : undefined;
1167
+ const currentFingerprint = record
1168
+ ? (0, blob_store_1.canonicalMailIndexRecordSnapshot)([record]).messageIndexFingerprint
1169
+ : null;
1170
+ if (!receipt ||
1171
+ !inspected.canonical ||
1172
+ !record ||
1173
+ documentsById.has(receipt.messageId) ||
1174
+ receipt.recordFingerprint !== currentFingerprint ||
1175
+ store.privateKeyIds.has(receipt.missingKeyId)) {
1176
+ invalidReceipts += 1;
1177
+ continue;
1178
+ }
1179
+ receiptsById.set(receipt.messageId, receipt.missingKeyId);
1180
+ }
1181
+ }
1182
+ catch (error) {
1183
+ return {
1184
+ ...base,
1185
+ status: "warn",
1186
+ detail: `hosted authority is sound but local skip receipts are unreadable: ${String(error)}; run \`ouro mail sync-cache --agent ${agentName}\``,
1187
+ };
1188
+ }
1189
+ let missing = 0;
1190
+ let receiptedMissing = 0;
1191
+ let stale = 0;
1192
+ let invalidKeyProvenance = 0;
1193
+ for (const record of observation.records) {
1194
+ const document = documentsById.get(record.id);
1195
+ if (!document) {
1196
+ if (receiptsById.has(record.id))
1197
+ receiptedMissing += 1;
1198
+ else
1199
+ missing += 1;
1200
+ continue;
1201
+ }
1202
+ if (!(0, hosted_cache_sync_1.mailSearchCacheDocumentMatchesRecord)(document, record, true)) {
1203
+ stale += 1;
1204
+ continue;
1205
+ }
1206
+ if (document.bodyForm === "encrypted" &&
1207
+ (!document.decryptionKeyId || !store.privateKeyIds.has(document.decryptionKeyId))) {
1208
+ invalidKeyProvenance += 1;
1209
+ }
1210
+ }
1211
+ const coverage = (0, search_cache_1.readMailSearchCoverageRecord)({
1212
+ agentId: agentName,
1213
+ storeKind: "azure-blob",
1214
+ }, { cacheDirForAgent: () => cacheDir });
1215
+ const expectedCachedCount = documentsById.size;
1216
+ const coverageMatches = coverage !== null
1217
+ && coverage.textProjectionVersion === search_cache_1.MAIL_SEARCH_TEXT_PROJECTION_VERSION
1218
+ && coverage.messageIndexFingerprint === observation.snapshot.messageIndexFingerprint
1219
+ && coverage.visibleMessageCount === observation.snapshot.visibleMessageCount
1220
+ && coverage.cachedMessageCount === expectedCachedCount
1221
+ && coverage.decryptableMessageCount === expectedCachedCount
1222
+ && coverage.skippedMessageCount === observation.snapshot.visibleMessageCount - expectedCachedCount;
1223
+ if (coverageMatches
1224
+ && coverage.skippedMessageCount > 0
1225
+ && receiptedMissing === coverage.skippedMessageCount
1226
+ && missing === 0
1227
+ && stale === 0
1228
+ && malformedOrNoncanonical === 0
1229
+ && invalidReceipts === 0
1230
+ && invalidKeyProvenance === 0) {
1231
+ return {
1232
+ ...base,
1233
+ status: "warn",
1234
+ detail: `hosted authority is sound; the last completed sync skipped ${receiptedMissing} hosted message(s) with current per-message missing-key receipts, so local search remains incomplete; retry only if conditions have changed since that sync`,
1235
+ };
1236
+ }
1237
+ if (malformedOrNoncanonical > 0 || missing > 0 || stale > 0 || invalidReceipts > 0 || invalidKeyProvenance > 0 || !coverageMatches) {
1238
+ return {
1239
+ ...base,
1240
+ status: "warn",
1241
+ detail: `hosted authority is sound but cache diverges: ${missing + stale} unreceipted-missing/stale canonical document(s), ${malformedOrNoncanonical} malformed/orphan/noncanonical/duplicate cache file(s), ${invalidReceipts} malformed/orphan/stale/recoverable skip receipt(s), ${invalidKeyProvenance} invalid key provenance document(s), all-scope coverage ${coverageMatches ? "matches" : "missing/malformed/stale"}; run \`ouro mail sync-cache --agent ${agentName}\``,
1242
+ };
1243
+ }
1244
+ const count = observation.records.length;
1245
+ return {
1246
+ ...base,
1247
+ status: "pass",
1248
+ detail: `${count} authoritative hosted message${count === 1 ? "" : "s"}; local search cache converged with current metadata, projection, and key provenance`,
1249
+ };
1250
+ }
1251
+ /**
1252
+ * Local-file Mailroom liveness signal.
1253
+ *
1254
+ * The `messages/` directory mtime is both the cheapest and the most correct
1255
+ * option. Cheapest: one stat regardless of store size, and the production store
1256
+ * holds ~45k message files. Most correct: a message's `receivedAt` is when the
1257
+ * mail was *sent* (an mbox backfill writes month-old values today), so parsing
1258
+ * message bodies would answer a different question than "did this machine
1259
+ * ingest anything recently?".
1260
+ */
1261
+ function localMailStoreProbe(deps, messagesDir, listing) {
1262
+ if (!listing.readable) {
1263
+ return {
1264
+ observation: { kind: "unknown", reason: `${messagesDir} exists but could not be listed` },
1265
+ provenance: `attempted directory listing of ${messagesDir}`,
1266
+ };
1267
+ }
1268
+ return (0, freshness_1.observeCreatePerEventStore)(messagesDir, deps, { hasEntries: listing.count > 0 });
1269
+ }
1270
+ /**
1271
+ * Hosted Mailroom liveness signal.
1272
+ *
1273
+ * Once an agent is cut over to the hosted store, messages live in the Blob
1274
+ * container and nothing writes to `state/mailroom/messages` any more, so its
1275
+ * mtime freezes at the cutover. Reading it in hosted mode is how this check
1276
+ * reported "no mail ingested in 77 days" on 2026-07-27 while the container was
1277
+ * taking mail that same morning — a false failure, which trains operators to
1278
+ * ignore the check and so undoes the reason it exists.
1279
+ *
1280
+ * The local artifact that does still move is the hosted reader's search cache:
1281
+ * `AzureBlobMailroomStore` writes `state/mail-search/<messageId>.json` for
1282
+ * every message it decrypts (see `mailroom/reader.ts`, which hands the hosted
1283
+ * store that cache directory). Message ids are content hashes, so a *new* file
1284
+ * appears exactly when this machine sees a message it has never seen before;
1285
+ * re-reading old mail rewrites existing files and leaves the directory mtime
1286
+ * untouched. Same create-per-event shape as the local store, same O(1) stat.
1287
+ *
1288
+ * It is deliberately one step downstream of ingest: it proves hosted mail
1289
+ * reached this machine, which is the strongest claim available without network
1290
+ * access or Azure credentials — a health check must need neither. The activity
1291
+ * wording, provenance and remediation all say so rather than implying doctor
1292
+ * measured the container itself, and an empty mirror reports `unknown` rather
1293
+ * than the hosted store being empty.
1294
+ */
1295
+ function hostedMailMirrorProbe(deps, agentDir, storeLabel) {
1296
+ const mirrorDir = `${deps.bundlesRoot}/${agentDir}/state/mail-search`;
1297
+ const listing = listJsonDocuments(deps, mirrorDir);
1298
+ if (!listing.readable) {
1299
+ return {
1300
+ observation: { kind: "unknown", reason: `${mirrorDir} exists but could not be listed` },
1301
+ provenance: `attempted directory listing of ${mirrorDir}`,
1302
+ };
1303
+ }
1304
+ return (0, freshness_1.observeMirroredStore)(mirrorDir, deps, {
1305
+ hasEntries: listing.count > 0,
1306
+ remote: storeLabel,
1307
+ authorityReadNote: "which the separate hosted cache authority check reads without mutation; this liveness check does not use remote data for recency",
1308
+ });
1309
+ }
1310
+ /**
1311
+ * Mail-ingest liveness — "is mail still arriving?", as opposed to the check
1312
+ * above, which only says "is a mailbox configured?".
1313
+ *
1314
+ * This is the check that would have caught the 2026-05-10 outage: the mailbox,
1315
+ * its config, and a cumulative count of 45,479 messages all reported ✔ for 77
1316
+ * days while zero mail was ingested.
1317
+ *
1318
+ * Which signal is honest depends on where the store lives, so the probe, the
1319
+ * activity wording and the remediation are all chosen per store kind. Reusing
1320
+ * the local path in hosted mode measures a directory nothing writes to.
1321
+ */
1322
+ function mailIngestLivenessCheck(deps, agentDir, mailroomRoot, messagesDir, listing, store, authorityCheck) {
1323
+ const hosted = store.hosted ? store : null;
1324
+ if (hosted && authorityCheck) {
1325
+ if (authorityCheck.status === "pass") {
1326
+ return {
1327
+ id: "mail.ingest_liveness",
1328
+ label: `${agentDir} mail ingest liveness`,
1329
+ status: "pass",
1330
+ detail: `hosted authority and local search cache are converged; this is healthy quiet even when the local mirror mtime is old or unchanged (${hosted.label}); mirror age is not treated as hosted-ingest authority`,
1331
+ };
1332
+ }
1333
+ return {
1334
+ id: "mail.ingest_liveness",
1335
+ label: `${agentDir} mail ingest liveness`,
1336
+ status: authorityCheck.status,
1337
+ detail: `hosted ingest liveness follows the available authority result rather than local mirror age: ${authorityCheck.detail}`,
1338
+ };
1339
+ }
1340
+ const probe = hosted
1341
+ ? hostedMailMirrorProbe(deps, agentDir, hosted.label)
1342
+ : localMailStoreProbe(deps, messagesDir, listing);
1343
+ return pipelineLivenessCheck({
1344
+ id: "mail.ingest_liveness",
1345
+ label: `${agentDir} mail ingest liveness`,
1346
+ activity: hosted ? "hosted mail observed locally" : "mail ingested",
1347
+ unit: "message",
1348
+ probe,
1349
+ thresholds: (0, freshness_1.resolveFreshnessThresholds)(exports.DEFAULT_MAIL_INGEST_THRESHOLDS, senseFreshnessOverride(deps, agentDir, "mail")),
1350
+ remediation: hosted
1351
+ ? `this liveness check measures recency only through this machine's local mirror; the separate hosted cache authority check reads index names without mutation — if the mirror is empty or stale while authority is current, run \`ouro mail sync-cache --agent ${agentDir.replace(/\.ouro$/, "")}\` and verify the hosted reader on this machine`
1352
+ : "mail is configured but nothing is arriving — re-check the mailbox grant and keyIds against the vault (a server-side key rotation silently orphans ingestion), run `ouro connect mail --agent <agent>`, and inspect the mailroom ingress logs",
1353
+ configuredSinceMs: pathMtimeMs(deps, `${mailroomRoot}/registry.json`),
1354
+ context: hosted ? `mailbox configured; ${hosted.label}` : "mailbox configured",
1355
+ nowMs: Date.now(),
1356
+ });
1357
+ }
1358
+ function checkFriends(deps) {
1359
+ const checks = [];
1360
+ const agents = discoverAgents(deps);
1361
+ if (agents.length === 0) {
1362
+ checks.push({ label: "friends", status: "warn", detail: "no agent bundles found" });
1363
+ return { name: "Friends", checks };
1364
+ }
1365
+ for (const agentDir of agents) {
1366
+ const friendsDir = `${deps.bundlesRoot}/${agentDir}/friends`;
1367
+ if (!deps.existsSync(friendsDir)) {
1368
+ checks.push({ label: `${agentDir} friends`, status: "pass", detail: "no friends directory (no friends recorded yet)" });
1369
+ continue;
1370
+ }
1371
+ let entries;
1372
+ /* v8 ignore start -- defensive: readdirSync failure after existsSync passes is a race-condition fallback @preserve */
1373
+ try {
1374
+ entries = deps.readdirSync(friendsDir).filter((name) => name.endsWith(".json"));
1375
+ }
1376
+ catch {
1377
+ checks.push({ label: `${agentDir} friends`, status: "fail", detail: "friends directory could not be read" });
1378
+ continue;
1379
+ }
1380
+ /* v8 ignore stop */
1381
+ if (entries.length === 0) {
1382
+ checks.push({ label: `${agentDir} friends`, status: "pass", detail: "0 friends recorded" });
1383
+ continue;
1384
+ }
1385
+ let parseFailures = 0;
1386
+ let trustFamily = 0;
1387
+ let trustFriend = 0;
1388
+ let trustStranger = 0;
1389
+ let trustOther = 0;
1390
+ /* v8 ignore start -- per-record trust-level tally branches: tests don't exhaustively combine all four trust buckets in one fixture @preserve */
1391
+ for (const name of entries) {
1392
+ const filePath = `${friendsDir}/${name}`;
1393
+ let raw;
1394
+ try {
1395
+ raw = deps.readFileSync(filePath);
1396
+ }
1397
+ catch {
1398
+ parseFailures += 1;
1399
+ continue;
1400
+ }
1401
+ let parsed;
1402
+ try {
1403
+ parsed = JSON.parse(raw);
1404
+ }
1405
+ catch {
1406
+ parseFailures += 1;
1407
+ continue;
1408
+ }
1409
+ const trustLevel = typeof parsed.trustLevel === "string" ? parsed.trustLevel : "friend";
1410
+ if (trustLevel === "family")
1411
+ trustFamily += 1;
1412
+ else if (trustLevel === "friend")
1413
+ trustFriend += 1;
1414
+ else if (trustLevel === "stranger")
1415
+ trustStranger += 1;
1416
+ else
1417
+ trustOther += 1;
1418
+ }
1419
+ if (parseFailures > 0) {
1420
+ checks.push({ label: `${agentDir} friends`, status: "warn", detail: `${entries.length} record${entries.length === 1 ? "" : "s"}, ${parseFailures} unparseable` });
1421
+ continue;
1422
+ }
1423
+ const parts = [
1424
+ `${entries.length} friend${entries.length === 1 ? "" : "s"}`,
1425
+ `${trustFamily} family`,
1426
+ `${trustFriend} friend`,
1427
+ `${trustStranger} stranger`,
1428
+ ];
1429
+ if (trustOther > 0)
1430
+ parts.push(`${trustOther} other`);
1431
+ checks.push({ label: `${agentDir} friends`, status: "pass", detail: parts.join(", ") });
1432
+ /* v8 ignore stop */
1433
+ }
1434
+ return { name: "Friends", checks };
1435
+ }
1436
+ function checkDisk(deps) {
1437
+ const checks = [];
1438
+ const isActiveLogStream = (name) => {
1439
+ if (name.endsWith(".ndjson"))
1440
+ return !/\.\d+\.ndjson$/.test(name);
1441
+ if (name.endsWith(".log"))
1442
+ return !/\.\d+\.log$/.test(name);
1443
+ return false;
1444
+ };
1445
+ const addLogSizeCheck = (labelPrefix, logsDir, pruneCommand) => {
1446
+ let totalSize = 0;
1447
+ let activeSize = 0;
1448
+ const oversizedActive = [];
1449
+ try {
1450
+ const files = deps.readdirSync(logsDir);
1451
+ for (const file of files) {
1452
+ try {
1453
+ const stat = deps.statSync(`${logsDir}/${file}`);
1454
+ totalSize += stat.size;
1455
+ if (isActiveLogStream(file)) {
1456
+ activeSize += stat.size;
1457
+ if (stat.size >= nerves_1.DEFAULT_MAX_LOG_SIZE_BYTES) {
1458
+ oversizedActive.push(file);
1459
+ }
1460
+ }
1461
+ }
1462
+ catch {
1463
+ // skip unreadable files
1464
+ }
1465
+ }
1466
+ }
1467
+ catch {
1468
+ // readdirSync failure handled below
1469
+ }
1470
+ const sizeMB = totalSize / (1024 * 1024);
1471
+ const activeSizeMB = activeSize / (1024 * 1024);
1472
+ const repair = pruneCommand ? ` — run \`${pruneCommand}\`` : "";
1473
+ if (activeSizeMB > 500) {
1474
+ checks.push({ label: `${labelPrefix} daemon log size`, status: "fail", detail: `${activeSizeMB.toFixed(1)}MB active / ${sizeMB.toFixed(1)}MB total — active logs exceed 500MB limit${repair}` });
1475
+ }
1476
+ else if (oversizedActive.length > 0) {
1477
+ checks.push({
1478
+ label: `${labelPrefix} daemon log size`,
1479
+ status: "warn",
1480
+ detail: `${activeSizeMB.toFixed(1)}MB active / ${sizeMB.toFixed(1)}MB total; active stream(s) over ${Math.round(nerves_1.DEFAULT_MAX_LOG_SIZE_BYTES / (1024 * 1024))}MB: ${oversizedActive.join(", ")}${repair}`,
1481
+ });
1482
+ }
1483
+ else {
1484
+ checks.push({ label: `${labelPrefix} daemon log size`, status: "pass", detail: `${activeSizeMB.toFixed(1)}MB active / ${sizeMB.toFixed(1)}MB total` });
1485
+ }
1486
+ };
1487
+ const agents = discoverAgents(deps);
1488
+ if (agents.length === 0) {
1489
+ checks.push({ label: "daemon logs dir", status: "warn", detail: "no agent bundles found for bundle-local logs" });
1490
+ }
1491
+ for (const agentDir of agents) {
1492
+ const logsDir = `${deps.bundlesRoot}/${agentDir}/state/daemon/logs`;
1493
+ let pruneCommand;
1494
+ if (deps.lstatSync && deps.realpathSync) {
1495
+ const io = {
1496
+ existsSync: deps.existsSync,
1497
+ lstatSync: deps.lstatSync,
1498
+ readdirSync: deps.readdirSync,
1499
+ realpathSync: deps.realpathSync,
1500
+ };
1501
+ try {
1502
+ const agentName = agentDir.slice(0, -".ouro".length);
1503
+ const target = (0, prunable_bundle_1.resolvePrunableAgentBundle)({ bundlesRoot: deps.bundlesRoot, agentName, fs: io });
1504
+ if (!deps.existsSync(target.logsDir))
1505
+ throw new Error("logs directory does not exist");
1506
+ const logsIo = {
1507
+ lstatSync: (filePath) => {
1508
+ const stat = deps.lstatSync(filePath);
1509
+ return {
1510
+ isDirectory: stat.isDirectory,
1511
+ isFile: stat.isFile ?? (() => false),
1512
+ isSymbolicLink: stat.isSymbolicLink,
1513
+ };
1514
+ },
1515
+ readdirSync: deps.readdirSync,
1516
+ realpathSync: deps.realpathSync,
1517
+ };
1518
+ (0, prunable_bundle_1.validatePrunableLogsTarget)(target, logsIo);
1519
+ pruneCommand = `ouro logs prune --agent ${agentName}`;
1520
+ }
1521
+ catch {
1522
+ // Doctor reports size but only offers repair for an exactly validated target.
1523
+ }
1524
+ }
1525
+ if (!deps.existsSync(logsDir)) {
1526
+ checks.push({ label: `${agentDir} daemon logs dir`, status: "warn", detail: `${logsDir} not found` });
1527
+ }
1528
+ else {
1529
+ addLogSizeCheck(agentDir, logsDir, pruneCommand);
1530
+ }
1531
+ }
1532
+ // Check AgentBundles root
1533
+ if (deps.existsSync(deps.bundlesRoot)) {
1534
+ checks.push({ label: "bundles root", status: "pass", detail: deps.bundlesRoot });
1535
+ }
1536
+ else {
1537
+ checks.push({ label: "bundles root", status: "warn", detail: `${deps.bundlesRoot} not found` });
1538
+ }
1539
+ return { name: "Disk", checks };
1540
+ }
1541
+ // ── Orchestrator ──
1542
+ function computeSummary(categories) {
1543
+ let passed = 0;
1544
+ let warnings = 0;
1545
+ let failed = 0;
1546
+ for (const cat of categories) {
1547
+ for (const check of cat.checks) {
1548
+ /* v8 ignore next 3 -- all three branches tested; v8 misreports compound if/else-if chain @preserve */
1549
+ if (check.status === "pass")
1550
+ passed++;
1551
+ else if (check.status === "warn")
1552
+ warnings++;
1553
+ else
1554
+ failed++;
1555
+ }
1556
+ }
1557
+ return { passed, warnings, failed };
1558
+ }
1559
+ /**
1560
+ * Recent daemon lifecycle: surfaces last activity timestamp, recent restarts,
1561
+ * version-install events, and process errors from the last hour. Designed
1562
+ * to answer the operator's question after the daemon has gone silent: "did
1563
+ * it crash? when did it last do anything? did it just upgrade?"
1564
+ *
1565
+ * Reads the machine-local daemon.ndjson when available, plus any
1566
+ * bundle-local copies left by older runtimes. The newest parseable event
1567
+ * is the liveness signal; stale bundle logs cannot mask a live daemon.
1568
+ */
1569
+ function checkLifecycle(deps) {
1570
+ const checks = [];
1571
+ const HOUR_MS = 60 * 60 * 1000;
1572
+ const STALE_THRESHOLD_MS = 5 * 60 * 1000;
1573
+ const now = Date.now();
1574
+ const cutoff = now - HOUR_MS;
1575
+ const logPaths = [];
1576
+ const seenLogPaths = new Set();
1577
+ const addLogPath = (candidate) => {
1578
+ if (seenLogPaths.has(candidate) || !deps.existsSync(candidate))
1579
+ return;
1580
+ seenLogPaths.add(candidate);
1581
+ logPaths.push(candidate);
1582
+ };
1583
+ if (deps.daemonLogsDir) {
1584
+ addLogPath(`${deps.daemonLogsDir}/daemon.ndjson`);
1585
+ }
1586
+ const agents = discoverAgents(deps);
1587
+ for (const agentDir of agents) {
1588
+ addLogPath(`${deps.bundlesRoot}/${agentDir}/state/daemon/logs/daemon.ndjson`);
1589
+ }
1590
+ if (logPaths.length === 0) {
1591
+ checks.push({ label: "daemon log readable", status: "warn", detail: "no daemon.ndjson found in daemon or agent bundle logs" });
1592
+ return { name: "Lifecycle", checks };
1593
+ }
1594
+ let lastTs = null;
1595
+ let lastEvent = null;
1596
+ let lastEventTimeMs = Number.NEGATIVE_INFINITY;
1597
+ let startCount = 0;
1598
+ let installCount = 0;
1599
+ let installVersions = [];
1600
+ let processErrors = [];
1601
+ let lastEntryAgeMs = Number.POSITIVE_INFINITY;
1602
+ let readCount = 0;
1603
+ const readErrors = [];
1604
+ for (const logPath of logPaths) {
1605
+ try {
1606
+ // Read the whole log via deps.readFileSync, then take the tail. For a
1607
+ // chatty daemon this can be a few MB; we only inspect the last 5000
1608
+ // lines which is enough for the last hour of activity. If the file is
1609
+ // small (typical case), reading it all is cheap.
1610
+ const raw = deps.readFileSync(logPath);
1611
+ readCount++;
1612
+ const allLines = raw.split("\n").filter((l) => l.trim());
1613
+ const usable = allLines.length > 5000 ? allLines.slice(-5000) : allLines;
1614
+ for (const line of usable) {
1615
+ let parsed;
1616
+ try {
1617
+ parsed = JSON.parse(line);
1618
+ }
1619
+ catch {
1620
+ continue;
1621
+ }
1622
+ const ts = typeof parsed.ts === "string" ? parsed.ts : null;
1623
+ const event = typeof parsed.event === "string" ? parsed.event : null;
1624
+ if (!ts || !event)
1625
+ continue;
1626
+ const tsMs = Date.parse(ts);
1627
+ if (Number.isNaN(tsMs))
1628
+ continue;
1629
+ if (tsMs > lastEventTimeMs) {
1630
+ lastEventTimeMs = tsMs;
1631
+ lastTs = ts;
1632
+ lastEvent = event;
1633
+ lastEntryAgeMs = Math.max(0, now - tsMs);
1634
+ }
1635
+ if (tsMs < cutoff)
1636
+ continue;
1637
+ if (event === "daemon.daemon_started")
1638
+ startCount++;
1639
+ if (event === "daemon.cli_version_install_end") {
1640
+ installCount++;
1641
+ const meta = parsed.meta;
1642
+ const ver = typeof meta?.version === "string" ? meta.version : null;
1643
+ if (ver)
1644
+ installVersions.push(ver);
1645
+ }
1646
+ if (event === "daemon.agent_process_error") {
1647
+ const meta = parsed.meta;
1648
+ const reason = typeof meta?.reason === "string" ? meta.reason : "unknown";
1649
+ const agent = typeof meta?.agent === "string" ? meta.agent : "unknown";
1650
+ processErrors.push(`${agent}: ${reason}`);
1651
+ }
1652
+ }
1653
+ }
1654
+ catch (error) {
1655
+ readErrors.push(`${logPath}: ${error instanceof Error ? error.message : /* v8 ignore next -- non-Error throw is unreachable from deps.readFileSync (always Error) @preserve */ String(error)}`);
1656
+ }
1657
+ }
1658
+ if (readCount === 0) {
1659
+ checks.push({ label: "daemon log readable", status: "fail", detail: `read failed: ${readErrors.join("; ")}` });
1660
+ return { name: "Lifecycle", checks };
1661
+ }
1662
+ if (readErrors.length > 0) {
1663
+ checks.push({ label: "daemon log readable", status: "warn", detail: `some daemon logs were unreadable: ${readErrors.join("; ")}` });
1664
+ }
1665
+ if (lastTs === null) {
1666
+ checks.push({ label: "recent daemon activity", status: "warn", detail: "no parseable events in tail of daemon.ndjson" });
1667
+ }
1668
+ else {
1669
+ const ageSec = Math.round(lastEntryAgeMs / 1000);
1670
+ const ageDetail = ageSec < 60 ? `${ageSec}s ago` : `${Math.round(ageSec / 60)}m ago`;
1671
+ if (lastEntryAgeMs > STALE_THRESHOLD_MS) {
1672
+ checks.push({
1673
+ label: "recent daemon activity",
1674
+ status: "warn",
1675
+ detail: `last event ${ageDetail} (${lastEvent}) — daemon may be silent or stopped`,
1676
+ });
1677
+ }
1678
+ else {
1679
+ checks.push({
1680
+ label: "recent daemon activity",
1681
+ status: "pass",
1682
+ detail: `last event ${ageDetail} (${lastEvent})`,
1683
+ });
1684
+ }
1685
+ }
1686
+ if (startCount > 0) {
1687
+ checks.push({
1688
+ label: "daemon restarts (last hour)",
1689
+ status: startCount > 3 ? "warn" : "pass",
1690
+ detail: `${startCount} restart${startCount === 1 ? "" : "s"}${startCount > 3 ? " — high churn, investigate" : ""}`,
1691
+ });
1692
+ }
1693
+ if (installCount > 0) {
1694
+ checks.push({
1695
+ label: "version installs (last hour)",
1696
+ status: "pass",
1697
+ detail: `installed: ${installVersions.join(", ")}`,
1698
+ });
1699
+ }
1700
+ if (processErrors.length > 0) {
1701
+ checks.push({
1702
+ label: "agent process errors (last hour)",
1703
+ status: "warn",
1704
+ detail: `${processErrors.length} error${processErrors.length === 1 ? "" : "s"}: ${processErrors.slice(0, 3).join("; ")}${processErrors.length > 3 ? "..." : ""}`,
1705
+ });
1706
+ }
1707
+ return { name: "Lifecycle", checks };
1708
+ }
1709
+ const CATEGORY_CHECKERS = [
1710
+ { name: "CLI", fn: checkCliPath },
1711
+ { name: "Daemon", fn: checkDaemon },
1712
+ { name: "Lifecycle", fn: checkLifecycle },
1713
+ { name: "Agents", fn: checkAgents },
1714
+ { name: "Senses", fn: checkSenses },
1715
+ { name: "Habits", fn: checkHabits },
1716
+ { name: "RSVP", fn: checkRsvp },
1717
+ { name: "Security", fn: checkSecurity },
1718
+ { name: "Trips", fn: checkTrips },
1719
+ { name: "Mailroom", fn: checkMailroom },
1720
+ { name: "Friends", fn: checkFriends },
1721
+ { name: "Disk", fn: checkDisk },
1722
+ ];
1723
+ exports.KNOWN_DOCTOR_CATEGORIES = CATEGORY_CHECKERS.map((c) => c.name);
1724
+ async function runDoctorChecks(deps, options = {}) {
1725
+ const categories = [];
1726
+ const filter = options.category?.toLowerCase();
1727
+ /* v8 ignore next -- branch: filter present vs absent — covered separately by --category and plain doctor tests but the filter-array generation isn't double-counted by both code paths in the same suite @preserve */
1728
+ const checkers = filter
1729
+ ? CATEGORY_CHECKERS.filter((c) => c.name.toLowerCase() === filter)
1730
+ : CATEGORY_CHECKERS;
1731
+ for (const checker of checkers) {
1732
+ try {
1733
+ const category = await Promise.resolve(checker.fn(deps));
1734
+ categories.push(category);
1735
+ }
1736
+ catch (error) {
1737
+ (0, runtime_1.emitNervesEvent)({
1738
+ level: "warn",
1739
+ component: "daemon",
1740
+ event: "daemon.doctor_check_error",
1741
+ message: `doctor check ${checker.name} failed`,
1742
+ meta: { category: checker.name, error: error instanceof Error ? error.message : String(error) },
1743
+ });
1744
+ categories.push({
1745
+ name: checker.name,
1746
+ checks: [{
1747
+ label: checker.name.toLowerCase(),
1748
+ status: "fail",
1749
+ detail: `check crashed: ${error instanceof Error ? error.message : String(error)}`,
1750
+ }],
1751
+ });
1752
+ }
1753
+ }
1754
+ return { categories, summary: computeSummary(categories) };
1755
+ }