@cohortapp/agent-sdk 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (731) hide show
  1. package/.claude/commands/init-agent.md +104 -0
  2. package/.claude/commands/init-maestro.md +1187 -0
  3. package/.claude/settings.json +161 -0
  4. package/.env.example +216 -0
  5. package/README.md +632 -0
  6. package/agents/browser-operator/agent.md +52 -0
  7. package/agents/calendar-ops/agent.md +50 -0
  8. package/agents/communications/agent.md +96 -0
  9. package/agents/decision-log/agent.md +65 -0
  10. package/agents/desktop-operator/agent.md +59 -0
  11. package/agents/gmail-operator/agent.md +62 -0
  12. package/agents/inbound-dispatcher/agent.md +66 -0
  13. package/agents/inbox-processor/agent.md +39 -0
  14. package/agents/pmo-execution/agent.md +60 -0
  15. package/agents/session-spawner/agent.md +64 -0
  16. package/agents/slack-operator/agent.md +60 -0
  17. package/agents/whatsapp-operator/agent.md +60 -0
  18. package/agents/workflow-automation/agent.md +61 -0
  19. package/archetypes/altitudes/c-suite.yaml +58 -0
  20. package/archetypes/altitudes/founder.yaml +68 -0
  21. package/archetypes/altitudes/senior-manager.yaml +63 -0
  22. package/archetypes/altitudes/svp.yaml +60 -0
  23. package/archetypes/altitudes/vp.yaml +50 -0
  24. package/archetypes/archetype.schema.json +77 -0
  25. package/archetypes/base.yaml +47 -0
  26. package/archetypes/capabilities/commercial-leader.yaml +159 -0
  27. package/archetypes/capabilities/compliance-officer.yaml +159 -0
  28. package/archetypes/capabilities/executive-operator.yaml +169 -0
  29. package/archetypes/capabilities/finance-leader.yaml +162 -0
  30. package/archetypes/capabilities/operations-leader.yaml +154 -0
  31. package/archetypes/capabilities/product-leader.yaml +148 -0
  32. package/archetypes/capabilities/technical-leader.yaml +146 -0
  33. package/archetypes/functions/commercial-leader.yaml +62 -0
  34. package/archetypes/functions/compliance-officer.yaml +64 -0
  35. package/archetypes/functions/executive-operator.yaml +70 -0
  36. package/archetypes/functions/finance-leader.yaml +70 -0
  37. package/archetypes/functions/operations-leader.yaml +62 -0
  38. package/archetypes/functions/product-leader.yaml +61 -0
  39. package/archetypes/functions/technical-leader.yaml +57 -0
  40. package/bin/cohort-mcp.mjs +81 -0
  41. package/bin/maestro.mjs +3516 -0
  42. package/bin/maestro.test.mjs +1015 -0
  43. package/desktop-control/README.md +56 -0
  44. package/desktop-control/app-profiles/gmail.yaml +120 -0
  45. package/desktop-control/app-profiles/slack.yaml +315 -0
  46. package/desktop-control/app-profiles/whatsapp.yaml +107 -0
  47. package/docs/architecture/agent-topology.md +2239 -0
  48. package/docs/architecture/archetype-agent-factory.md +110 -0
  49. package/docs/architecture/collective-memory-and-org-mesh.md +115 -0
  50. package/docs/architecture/continuous-monitoring.md +221 -0
  51. package/docs/architecture/mcp-capability-map.md +585 -0
  52. package/docs/architecture/system-architecture.md +1272 -0
  53. package/docs/company-context/README.md +40 -0
  54. package/docs/guides/agent-persona-setup.md +600 -0
  55. package/docs/guides/agents-observe-setup.md +64 -0
  56. package/docs/guides/billing-console-keys.md +88 -0
  57. package/docs/guides/ccxray-diagnostics.md +65 -0
  58. package/docs/guides/channel-bus.md +127 -0
  59. package/docs/guides/claude-mem-setup.md +79 -0
  60. package/docs/guides/claude-pace-setup.md +56 -0
  61. package/docs/guides/claudraband-sessions.md +98 -0
  62. package/docs/guides/clawteam-swarm.md +116 -0
  63. package/docs/guides/code-review-graph-setup.md +86 -0
  64. package/docs/guides/email-setup.md +431 -0
  65. package/docs/guides/mac-mini.md +119 -0
  66. package/docs/guides/media-generation-setup.md +349 -0
  67. package/docs/guides/model-routing.md +162 -0
  68. package/docs/guides/observability-otel.md +265 -0
  69. package/docs/guides/org-onboarding.md +132 -0
  70. package/docs/guides/outbound-governance-setup.md +437 -0
  71. package/docs/guides/pdf-generation-setup.md +315 -0
  72. package/docs/guides/poller-daemon-setup.md +563 -0
  73. package/docs/guides/rag-context-setup.md +459 -0
  74. package/docs/guides/self-optimization-pattern.md +82 -0
  75. package/docs/guides/setup-wizard.md +178 -0
  76. package/docs/guides/slack-setup.md +350 -0
  77. package/docs/guides/telegram-setup.md +227 -0
  78. package/docs/guides/twilio-subaccounts-setup.md +223 -0
  79. package/docs/guides/verification.md +128 -0
  80. package/docs/guides/voice-mode.md +188 -0
  81. package/docs/guides/voice-sms-setup.md +698 -0
  82. package/docs/guides/webhook-relay-setup.md +349 -0
  83. package/docs/guides/whatsapp-setup.md +288 -0
  84. package/docs/prompts/board-pack-cover-template.md +36 -0
  85. package/docs/prompts/decision-recommendation-template.md +88 -0
  86. package/docs/prompts/followup-message-template.md +141 -0
  87. package/docs/prompts/investor-letter-template.md +52 -0
  88. package/docs/prompts/morning-brief-template.md +82 -0
  89. package/docs/prompts/presentation-template.md +58 -0
  90. package/docs/prompts/weekly-strategic-memo-template.md +104 -0
  91. package/docs/research/hallucinated-tool-output-investigation.md +151 -0
  92. package/docs/runbooks/backup-restore.md +205 -0
  93. package/docs/runbooks/cohort-cutover.md +129 -0
  94. package/docs/runbooks/fleet-operations.md +200 -0
  95. package/docs/runbooks/incident-response.md +226 -0
  96. package/docs/runbooks/mac-mini-bootstrap.md +431 -0
  97. package/docs/runbooks/perpetual-operations.md +509 -0
  98. package/docs/runbooks/recovery-and-failover.md +260 -0
  99. package/framework-features.json +267 -0
  100. package/ingest/README.md +87 -0
  101. package/lib/action-executor.js +689 -0
  102. package/lib/action-executor.test.mjs +871 -0
  103. package/lib/agent-root.mjs +37 -0
  104. package/lib/archetype.mjs +236 -0
  105. package/lib/archetype.test.mjs +132 -0
  106. package/lib/autonomy.mjs +114 -0
  107. package/lib/autonomy.test.mjs +66 -0
  108. package/lib/backlog.mjs +358 -0
  109. package/lib/backlog.test.mjs +266 -0
  110. package/lib/budget-guard.mjs +279 -0
  111. package/lib/budget-guard.test.mjs +291 -0
  112. package/lib/cadence-bus-schedule.test.mjs +194 -0
  113. package/lib/cadence-bus.mjs +1120 -0
  114. package/lib/cadence-bus.test.mjs +720 -0
  115. package/lib/cadences.mjs +205 -0
  116. package/lib/cadences.test.mjs +125 -0
  117. package/lib/capability.mjs +154 -0
  118. package/lib/capability.test.mjs +78 -0
  119. package/lib/channels/base-adapter.mjs +719 -0
  120. package/lib/channels/base-adapter.test.mjs +590 -0
  121. package/lib/channels/channel.mjs +128 -0
  122. package/lib/channels/channels.test.mjs +371 -0
  123. package/lib/channels/contract.mjs +215 -0
  124. package/lib/channels/contract.test.mjs +137 -0
  125. package/lib/channels/conversation-resolver.mjs +95 -0
  126. package/lib/channels/gmail/adapter.mjs +87 -0
  127. package/lib/channels/inbox-item.mjs +255 -0
  128. package/lib/channels/inbox-item.test.mjs +335 -0
  129. package/lib/channels/index.mjs +94 -0
  130. package/lib/channels/orgmail/adapter.mjs +353 -0
  131. package/lib/channels/orgmail/adapter.test.mjs +311 -0
  132. package/lib/channels/pairing.mjs +363 -0
  133. package/lib/channels/pairing.test.mjs +270 -0
  134. package/lib/channels/registry.mjs +164 -0
  135. package/lib/channels/slack/adapter.mjs +317 -0
  136. package/lib/channels/slack-adapter.test.mjs +212 -0
  137. package/lib/channels/sms/adapter.mjs +43 -0
  138. package/lib/channels/telegram/adapter.mjs +432 -0
  139. package/lib/channels/telegram-adapter.test.mjs +306 -0
  140. package/lib/channels/voice/adapter.mjs +301 -0
  141. package/lib/channels/voice/adapter.test.mjs +278 -0
  142. package/lib/channels/whatsapp/adapter-baileys.mjs +587 -0
  143. package/lib/channels/whatsapp/adapter-baileys.test.mjs +359 -0
  144. package/lib/channels/whatsapp/adapter-twilio.mjs +65 -0
  145. package/lib/channels/whatsapp/baileys-typing.test.mjs +154 -0
  146. package/lib/charter.mjs +256 -0
  147. package/lib/charter.test.mjs +89 -0
  148. package/lib/claude-bin.mjs +134 -0
  149. package/lib/claude-bin.test.mjs +75 -0
  150. package/lib/collective/capture.mjs +185 -0
  151. package/lib/collective/capture.test.mjs +121 -0
  152. package/lib/collective/cards.mjs +201 -0
  153. package/lib/collective/cards.test.mjs +114 -0
  154. package/lib/collective/config.mjs +186 -0
  155. package/lib/collective/config.test.mjs +123 -0
  156. package/lib/collective/global-config.mjs +113 -0
  157. package/lib/collective/global-config.test.mjs +75 -0
  158. package/lib/collective/presence.mjs +201 -0
  159. package/lib/collective/presence.test.mjs +95 -0
  160. package/lib/collective/recall.mjs +215 -0
  161. package/lib/collective/recall.test.mjs +116 -0
  162. package/lib/comms/send-gate.mjs +554 -0
  163. package/lib/comms/send-gate.test.mjs +577 -0
  164. package/lib/comms.mjs +67 -0
  165. package/lib/comms.test.mjs +41 -0
  166. package/lib/diagnostics/alerts.mjs +424 -0
  167. package/lib/diagnostics/alerts.test.mjs +318 -0
  168. package/lib/diagnostics/backup-freshness.mjs +188 -0
  169. package/lib/diagnostics/backup-freshness.test.mjs +185 -0
  170. package/lib/diagnostics/counters.mjs +269 -0
  171. package/lib/diagnostics/counters.test.mjs +206 -0
  172. package/lib/diagnostics/events.mjs +188 -0
  173. package/lib/diagnostics/events.test.mjs +290 -0
  174. package/lib/diagnostics/otel.mjs +237 -0
  175. package/lib/diagnostics/otel.test.mjs +196 -0
  176. package/lib/diagnostics/trace.mjs +216 -0
  177. package/lib/diagnostics/trace.test.mjs +251 -0
  178. package/lib/env-compat.mjs +74 -0
  179. package/lib/env-compat.test.mjs +104 -0
  180. package/lib/feature-init.mjs +331 -0
  181. package/lib/fs-atomic.mjs +112 -0
  182. package/lib/fs-atomic.test.mjs +72 -0
  183. package/lib/fs-ownership.mjs +111 -0
  184. package/lib/fs-ownership.test.mjs +158 -0
  185. package/lib/hooks/bus.mjs +347 -0
  186. package/lib/hooks/bus.test.mjs +387 -0
  187. package/lib/index.js +16 -0
  188. package/lib/learning/config.mjs +106 -0
  189. package/lib/learning/config.test.mjs +75 -0
  190. package/lib/learning/counters.mjs +156 -0
  191. package/lib/learning/counters.test.mjs +69 -0
  192. package/lib/learning/curator-consolidate.test.mjs +238 -0
  193. package/lib/learning/curator.mjs +453 -0
  194. package/lib/learning/curator.test.mjs +106 -0
  195. package/lib/learning/log.mjs +40 -0
  196. package/lib/learning/reflect.mjs +534 -0
  197. package/lib/learning/reflect.test.mjs +0 -0
  198. package/lib/learning/session-index.mjs +352 -0
  199. package/lib/learning/session-index.test.mjs +125 -0
  200. package/lib/learning/skill-writer.mjs +474 -0
  201. package/lib/learning/skill-writer.test.mjs +210 -0
  202. package/lib/mcp/server.mjs +328 -0
  203. package/lib/mcp/server.test.mjs +400 -0
  204. package/lib/model-router/auth-profiles.mjs +758 -0
  205. package/lib/model-router/auth-profiles.test.mjs +580 -0
  206. package/lib/model-router/catalog/anthropic.yaml +153 -0
  207. package/lib/model-router/catalog/deepseek.yaml +86 -0
  208. package/lib/model-router/catalog/moonshot.yaml +81 -0
  209. package/lib/model-router/catalog/qwen.yaml +114 -0
  210. package/lib/model-router/catalog.mjs +925 -0
  211. package/lib/model-router/catalog.test.mjs +385 -0
  212. package/lib/model-router/economics.mjs +564 -0
  213. package/lib/model-router/economics.test.mjs +344 -0
  214. package/lib/model-router/failover.mjs +298 -0
  215. package/lib/model-router/failover.test.mjs +439 -0
  216. package/lib/model-router/health.mjs +453 -0
  217. package/lib/model-router/health.test.mjs +338 -0
  218. package/lib/model-router/integration-coverage.test.mjs +829 -0
  219. package/lib/model-router/integration.test.mjs +564 -0
  220. package/lib/model-router/ledger.mjs +402 -0
  221. package/lib/model-router/ledger.test.mjs +382 -0
  222. package/lib/model-router/llm-task.mjs +515 -0
  223. package/lib/model-router/llm-task.test.mjs +392 -0
  224. package/lib/model-router/org-credentials.mjs +260 -0
  225. package/lib/model-router/org-credentials.test.mjs +265 -0
  226. package/lib/model-router/pricing-refresh.mjs +463 -0
  227. package/lib/model-router/pricing-refresh.test.mjs +286 -0
  228. package/lib/model-router/reconcile.mjs +429 -0
  229. package/lib/model-router/reconcile.test.mjs +316 -0
  230. package/lib/model-router/repair.mjs +471 -0
  231. package/lib/model-router/repair.test.mjs +180 -0
  232. package/lib/model-router/resolve.mjs +1206 -0
  233. package/lib/model-router/spawn.mjs +497 -0
  234. package/lib/model-router/spawn.test.mjs +425 -0
  235. package/lib/model-router/taxonomy.mjs +893 -0
  236. package/lib/model-router/taxonomy.test.mjs +410 -0
  237. package/lib/model-router.mjs +677 -0
  238. package/lib/model-router.test.mjs +907 -0
  239. package/lib/org/activity.mjs +211 -0
  240. package/lib/org/activity.test.mjs +134 -0
  241. package/lib/org/approvals.mjs +448 -0
  242. package/lib/org/approvals.test.mjs +216 -0
  243. package/lib/org/awareness.mjs +222 -0
  244. package/lib/org/awareness.test.mjs +159 -0
  245. package/lib/org/board.mjs +229 -0
  246. package/lib/org/board.test.mjs +177 -0
  247. package/lib/org/bootstrap-context.mjs +169 -0
  248. package/lib/org/bootstrap-context.test.mjs +153 -0
  249. package/lib/org/client.mjs +1628 -0
  250. package/lib/org/client.test.mjs +1107 -0
  251. package/lib/org/cohort-client.mjs +67 -0
  252. package/lib/org/cohort-client.test.mjs +126 -0
  253. package/lib/org/cost-sync.mjs +227 -0
  254. package/lib/org/cost-sync.test.mjs +153 -0
  255. package/lib/org/doctor.mjs +212 -0
  256. package/lib/org/doctor.test.mjs +212 -0
  257. package/lib/org/handoff.mjs +293 -0
  258. package/lib/org/handoff.test.mjs +269 -0
  259. package/lib/org/integration-tools.mjs +182 -0
  260. package/lib/org/integration-tools.test.mjs +160 -0
  261. package/lib/org/keys.mjs +131 -0
  262. package/lib/org/keys.test.mjs +92 -0
  263. package/lib/org/knowledge.mjs +463 -0
  264. package/lib/org/knowledge.test.mjs +319 -0
  265. package/lib/org/leases.mjs +335 -0
  266. package/lib/org/leases.test.mjs +235 -0
  267. package/lib/org/mesh-integration.test.mjs +127 -0
  268. package/lib/org/mesh.mjs +459 -0
  269. package/lib/org/mesh.test.mjs +345 -0
  270. package/lib/org/messaging.mjs +503 -0
  271. package/lib/org/messaging.test.mjs +238 -0
  272. package/lib/org/policy.mjs +345 -0
  273. package/lib/org/policy.test.mjs +237 -0
  274. package/lib/org/protocol.checksum +1 -0
  275. package/lib/org/protocol.checksum.test.mjs +90 -0
  276. package/lib/org/protocol.mjs +967 -0
  277. package/lib/org/protocol.test.mjs +264 -0
  278. package/lib/org/registry.mjs +194 -0
  279. package/lib/org/registry.test.mjs +100 -0
  280. package/lib/org/tool-surface-integration.test.mjs +120 -0
  281. package/lib/org/tool-surface.mjs +2535 -0
  282. package/lib/org/tool-surface.test.mjs +589 -0
  283. package/lib/org/ui-parity.mjs +3236 -0
  284. package/lib/org/ui-parity.test.mjs +348 -0
  285. package/lib/org/verify.mjs +176 -0
  286. package/lib/org/verify.test.mjs +194 -0
  287. package/lib/rag/embed.mjs +188 -0
  288. package/lib/rag/indexer.mjs +425 -0
  289. package/lib/rag/rag.test.mjs +505 -0
  290. package/lib/rag/search.mjs +475 -0
  291. package/lib/rate-guard.mjs +246 -0
  292. package/lib/rate-guard.test.mjs +201 -0
  293. package/lib/render.mjs +112 -0
  294. package/lib/render.test.mjs +68 -0
  295. package/lib/resource-governor.mjs +297 -0
  296. package/lib/resource-governor.test.mjs +262 -0
  297. package/lib/scheduling/dynamic-jobs.mjs +675 -0
  298. package/lib/scheduling/dynamic-jobs.test.mjs +344 -0
  299. package/lib/scheduling/jitter.mjs +0 -0
  300. package/lib/scheduling/jitter.test.mjs +140 -0
  301. package/lib/secrets/broker.mjs +315 -0
  302. package/lib/secrets/broker.test.mjs +280 -0
  303. package/lib/secrets/providers.mjs +461 -0
  304. package/lib/secrets/providers.test.mjs +274 -0
  305. package/lib/security/audit-engine.mjs +684 -0
  306. package/lib/security/audit-engine.test.mjs +389 -0
  307. package/lib/security/coerce-args.mjs +552 -0
  308. package/lib/security/coerce-args.test.mjs +281 -0
  309. package/lib/security/dangerous-tools.mjs +97 -0
  310. package/lib/security/dangerous-tools.test.mjs +68 -0
  311. package/lib/security/external-content.mjs +145 -0
  312. package/lib/security/external-content.test.mjs +67 -0
  313. package/lib/security/redact.mjs +592 -0
  314. package/lib/security/redact.test.mjs +441 -0
  315. package/lib/security/secret-equal.mjs +73 -0
  316. package/lib/security/secret-equal.test.mjs +55 -0
  317. package/lib/session-permissions.mjs +101 -0
  318. package/lib/session-permissions.test.mjs +100 -0
  319. package/lib/setup/claude-probe.mjs +74 -0
  320. package/lib/setup/completeness.mjs +175 -0
  321. package/lib/setup/completeness.test.mjs +110 -0
  322. package/lib/setup/context-pack.mjs +173 -0
  323. package/lib/setup/context-pack.test.mjs +89 -0
  324. package/lib/setup/enrich.mjs +277 -0
  325. package/lib/setup/enrich.test.mjs +115 -0
  326. package/lib/setup/enroll-from-cohort.mjs +441 -0
  327. package/lib/setup/enroll-from-cohort.test.mjs +233 -0
  328. package/lib/setup/integration.test.mjs +162 -0
  329. package/lib/setup/io.mjs +360 -0
  330. package/lib/setup/io.test.mjs +77 -0
  331. package/lib/setup/run-generator.mjs +81 -0
  332. package/lib/setup/runner.mjs +244 -0
  333. package/lib/setup/runner.test.mjs +132 -0
  334. package/lib/setup/sections/comms.mjs +173 -0
  335. package/lib/setup/sections/company.mjs +120 -0
  336. package/lib/setup/sections/enrich.mjs +138 -0
  337. package/lib/setup/sections/identity.mjs +182 -0
  338. package/lib/setup/sections/identity.test.mjs +140 -0
  339. package/lib/setup/sections/learning.mjs +153 -0
  340. package/lib/setup/sections/learning.test.mjs +81 -0
  341. package/lib/setup/sections/messaging.mjs +219 -0
  342. package/lib/setup/sections/messaging.test.mjs +127 -0
  343. package/lib/setup/sections/model.mjs +102 -0
  344. package/lib/setup/sections/operating-model.mjs +78 -0
  345. package/lib/setup/sections/org.mjs +475 -0
  346. package/lib/setup/sections/org.test.mjs +313 -0
  347. package/lib/setup/sections/orgmail.mjs +173 -0
  348. package/lib/setup/sections/orgmail.test.mjs +118 -0
  349. package/lib/setup/sections/recovery.mjs +159 -0
  350. package/lib/setup/sections/recovery.test.mjs +98 -0
  351. package/lib/setup/sections/tools.mjs +132 -0
  352. package/lib/setup/sections/verify.mjs +97 -0
  353. package/lib/setup/sot.mjs +205 -0
  354. package/lib/setup/sot.test.mjs +81 -0
  355. package/lib/setup/state.mjs +151 -0
  356. package/lib/setup/state.test.mjs +92 -0
  357. package/lib/singleton.js +229 -0
  358. package/lib/singleton.test.mjs +135 -0
  359. package/lib/telemetry/alerts.mjs +216 -0
  360. package/lib/telemetry/alerts.test.mjs +109 -0
  361. package/lib/telemetry/collect.mjs +512 -0
  362. package/lib/telemetry/collect.test.mjs +202 -0
  363. package/lib/tool-definitions-integration.test.mjs +83 -0
  364. package/lib/tool-definitions.js +738 -0
  365. package/lib/tool-definitions.test.mjs +437 -0
  366. package/lib/util/fetch-timeout.mjs +136 -0
  367. package/lib/util/fetch-timeout.test.mjs +202 -0
  368. package/lib/util/reconnect.mjs +343 -0
  369. package/lib/util/reconnect.test.mjs +369 -0
  370. package/lib/util/unhandled.mjs +205 -0
  371. package/lib/util/unhandled.test.mjs +216 -0
  372. package/lib/voice/context-loader.mjs +466 -0
  373. package/lib/voice/index.mjs +100 -0
  374. package/lib/voice/openai-realtime.mjs +510 -0
  375. package/lib/voice/outbound.mjs +542 -0
  376. package/lib/voice/outbound.test.mjs +69 -0
  377. package/lib/voice/post-call-brief.mjs +428 -0
  378. package/lib/voice/provider.mjs +52 -0
  379. package/lib/voice/session-rotation.mjs +257 -0
  380. package/lib/voice/stt.mjs +161 -0
  381. package/lib/voice/stt.test.mjs +226 -0
  382. package/lib/voice/tool-bridge.mjs +370 -0
  383. package/lib/voice/tts.mjs +104 -0
  384. package/lib/voice/twilio-sip-bridge.mjs +288 -0
  385. package/lib/voice/voice.test.mjs +990 -0
  386. package/mcp/README.md +80 -0
  387. package/package.json +151 -0
  388. package/plugins/maestro-skills/plugin.json +139 -0
  389. package/plugins/maestro-skills/skills/agents-observe.md +110 -0
  390. package/plugins/maestro-skills/skills/board-deck.md +68 -0
  391. package/plugins/maestro-skills/skills/books-close.md +77 -0
  392. package/plugins/maestro-skills/skills/brand-steward.md +121 -0
  393. package/plugins/maestro-skills/skills/calendar-plan.md +57 -0
  394. package/plugins/maestro-skills/skills/call-working-sessions.md +124 -0
  395. package/plugins/maestro-skills/skills/ccxray-diagnostics.md +91 -0
  396. package/plugins/maestro-skills/skills/claude-pace.md +61 -0
  397. package/plugins/maestro-skills/skills/code-review-graph.md +99 -0
  398. package/plugins/maestro-skills/skills/crm-pipeline.md +65 -0
  399. package/plugins/maestro-skills/skills/decision-brief.md +89 -0
  400. package/plugins/maestro-skills/skills/directory-hygiene.md +125 -0
  401. package/plugins/maestro-skills/skills/draft-comms.md +84 -0
  402. package/plugins/maestro-skills/skills/evening-wrap.md +53 -0
  403. package/plugins/maestro-skills/skills/files-find.md +65 -0
  404. package/plugins/maestro-skills/skills/generative-ui.md +228 -0
  405. package/plugins/maestro-skills/skills/hiring-triage.md +74 -0
  406. package/plugins/maestro-skills/skills/inbox-triage.md +61 -0
  407. package/plugins/maestro-skills/skills/mail-triage.md +86 -0
  408. package/plugins/maestro-skills/skills/morning-brief.md +54 -0
  409. package/plugins/maestro-skills/skills/native-artifacts.md +157 -0
  410. package/plugins/maestro-skills/skills/org-board.md +133 -0
  411. package/plugins/maestro-skills/skills/org-credential.md +68 -0
  412. package/plugins/maestro-skills/skills/org-recall.md +81 -0
  413. package/plugins/maestro-skills/skills/pipeline-review.md +76 -0
  414. package/plugins/maestro-skills/skills/regulatory-status.md +81 -0
  415. package/plugins/maestro-skills/skills/router-why.md +78 -0
  416. package/plugins/maestro-skills/skills/schedule-meeting.md +91 -0
  417. package/plugins/maestro-skills/skills/session-search.md +71 -0
  418. package/plugins/maestro-skills/skills/set-reminder.md +93 -0
  419. package/plugins/maestro-skills/skills/slack-followup.md +64 -0
  420. package/plugins/maestro-skills/skills/team-activity.md +86 -0
  421. package/plugins/maestro-skills/skills/weekly-memo.md +70 -0
  422. package/policies/action-classification.yaml +114 -0
  423. package/policies/ai-disclosure.yaml +294 -0
  424. package/policies/communication-style.md +139 -0
  425. package/policies/information-barriers.yaml +118 -0
  426. package/policies/prompt-injection-defence.yaml +138 -0
  427. package/public/assets/icon-dark.png +0 -0
  428. package/public/assets/icon-dark.svg +9 -0
  429. package/public/assets/icon-light.svg +9 -0
  430. package/public/assets/logo-dark.svg +15 -0
  431. package/public/assets/logo-light.svg +15 -0
  432. package/scaffold/.mcp.json +7 -0
  433. package/scaffold/CLAUDE.md +368 -0
  434. package/scaffold/config/agent.json +55 -0
  435. package/scaffold/config/agent.ts +76 -0
  436. package/scaffold/config/agent.ts.example +89 -0
  437. package/scaffold/config/alerts.yaml +23 -0
  438. package/scaffold/config/allowlist.yaml.example +25 -0
  439. package/scaffold/config/caller-id-map.yaml +46 -0
  440. package/scaffold/config/collective.yaml +49 -0
  441. package/scaffold/config/company.json +20 -0
  442. package/scaffold/config/known-agents.json +6 -0
  443. package/scaffold/config/learning.yaml +55 -0
  444. package/scaffold/config/model-routing.yaml.example +104 -0
  445. package/scaffold/config/org.yaml +25 -0
  446. package/scaffold/config/orgmail.yaml.example +19 -0
  447. package/scaffold/config/recovery.yaml +72 -0
  448. package/scaffold/config/secrets.yaml +27 -0
  449. package/scaffold/config/slack.yaml.example +35 -0
  450. package/scaffold/config/telegram.yaml.example +38 -0
  451. package/scaffold/config/voice.yaml.example +89 -0
  452. package/scaffold/config/whatsapp.yaml.example +39 -0
  453. package/schedules/README.md +49 -0
  454. package/schedules/triggers/backlog-executor.md +102 -0
  455. package/schedules/triggers/brand-steward.md +72 -0
  456. package/schedules/triggers/daily-evening-wrap.md +159 -0
  457. package/schedules/triggers/daily-midday-sweep.md +58 -0
  458. package/schedules/triggers/daily-morning-brief.md +55 -0
  459. package/schedules/triggers/directory-hygiene.md +81 -0
  460. package/schedules/triggers/dynamic-jobs.md +40 -0
  461. package/schedules/triggers/inbox-processor.md +115 -0
  462. package/schedules/triggers/meeting-action-capture.md +60 -0
  463. package/schedules/triggers/meeting-prep.md +69 -0
  464. package/schedules/triggers/messaging-inbound.md +50 -0
  465. package/schedules/triggers/org-pulse.md +24 -0
  466. package/schedules/triggers/quarterly-self-assessment.md +54 -0
  467. package/schedules/triggers/weekly-engineering-health.md +37 -0
  468. package/schedules/triggers/weekly-execution.md +65 -0
  469. package/schedules/triggers/weekly-hiring.md +53 -0
  470. package/schedules/triggers/weekly-priorities.md +38 -0
  471. package/schedules/triggers/weekly-strategic-memo.md +124 -0
  472. package/scripts/archive-email.sh +55 -0
  473. package/scripts/cadence/cadence-status.mjs +36 -0
  474. package/scripts/cadence/enqueue-cadence-tick.mjs +174 -0
  475. package/scripts/cadence/enqueue-cadence-tick.test.mjs +187 -0
  476. package/scripts/cadence/launchd-cadence-wrapper.sh +85 -0
  477. package/scripts/cadence/launchd-cloud-relay-wrapper.sh +95 -0
  478. package/scripts/cadence/launchd-socket-mode-wrapper.sh +95 -0
  479. package/scripts/ci/check-docs-accuracy.mjs +493 -0
  480. package/scripts/ci/check-docs-accuracy.test.mjs +409 -0
  481. package/scripts/ci/check-exports-exist.mjs +140 -0
  482. package/scripts/ci/check-files-exist.mjs +107 -0
  483. package/scripts/ci/check-no-build-artifacts.mjs +111 -0
  484. package/scripts/ci/check-no-build-artifacts.test.mjs +71 -0
  485. package/scripts/ci/check-no-confidential.mjs +198 -0
  486. package/scripts/ci/check-no-conflict-markers.mjs +169 -0
  487. package/scripts/ci/check-no-residual-identity.mjs +163 -0
  488. package/scripts/ci/check-no-residual-identity.test.mjs +89 -0
  489. package/scripts/ci/check-tarball-fidelity.mjs +205 -0
  490. package/scripts/ci/check-unresolved-tokens.mjs +83 -0
  491. package/scripts/ci/check.mjs +109 -0
  492. package/scripts/ci/check.test.mjs +194 -0
  493. package/scripts/ci/run-coverage.mjs +82 -0
  494. package/scripts/ci/run-tests.mjs +71 -0
  495. package/scripts/cloud-relay/README.md +59 -0
  496. package/scripts/cloud-relay/index.mjs +233 -0
  497. package/scripts/cloud-relay/package.json +15 -0
  498. package/scripts/cloud-relay/railway.json +13 -0
  499. package/scripts/cloud-relay/voice/README.md +94 -0
  500. package/scripts/cloud-relay/voice/package-lock.json +39 -0
  501. package/scripts/cloud-relay/voice/package.json +16 -0
  502. package/scripts/cloud-relay/voice/railway.json +13 -0
  503. package/scripts/cloud-relay/voice/server.mjs +532 -0
  504. package/scripts/collective/hook-runner.mjs +211 -0
  505. package/scripts/collective/hook-runner.test.mjs +90 -0
  506. package/scripts/collective/org-pulse.mjs +72 -0
  507. package/scripts/collective/org-sync.mjs +61 -0
  508. package/scripts/collective/recall.mjs +45 -0
  509. package/scripts/collective/who.mjs +30 -0
  510. package/scripts/comms-monitor.sh +288 -0
  511. package/scripts/configure-whatsapp-sandbox.sh +201 -0
  512. package/scripts/continuous-monitor.sh +91 -0
  513. package/scripts/cost/fleet-digest.mjs +407 -0
  514. package/scripts/cost/fleet-digest.test.mjs +207 -0
  515. package/scripts/cost/track-claude-usage.mjs +169 -0
  516. package/scripts/daemon/agent-daemon.mjs +989 -0
  517. package/scripts/daemon/agent-daemon.test.mjs +525 -0
  518. package/scripts/daemon/cadence-consumer-governance.test.mjs +220 -0
  519. package/scripts/daemon/cadence-consumer.mjs +1080 -0
  520. package/scripts/daemon/cadence-consumer.test.mjs +770 -0
  521. package/scripts/daemon/cadence-handlers.mjs +1121 -0
  522. package/scripts/daemon/cadence-handlers.test.mjs +617 -0
  523. package/scripts/daemon/classifier.mjs +704 -0
  524. package/scripts/daemon/classifier.test.mjs +238 -0
  525. package/scripts/daemon/classify-kind.mjs +54 -0
  526. package/scripts/daemon/classify-kind.test.mjs +40 -0
  527. package/scripts/daemon/context-compiler.mjs +605 -0
  528. package/scripts/daemon/context-compiler.test.mjs +300 -0
  529. package/scripts/daemon/dispatcher-cooldown.test.mjs +122 -0
  530. package/scripts/daemon/dispatcher-governance.test.mjs +886 -0
  531. package/scripts/daemon/dispatcher.mjs +1516 -0
  532. package/scripts/daemon/health.mjs +72 -0
  533. package/scripts/daemon/inbox-deferral.mjs +210 -0
  534. package/scripts/daemon/inbox-deferral.test.mjs +242 -0
  535. package/scripts/daemon/integration.test.mjs +149 -0
  536. package/scripts/daemon/launchd-wrapper-generic.sh +96 -0
  537. package/scripts/daemon/launchd-wrapper-slack-events.sh +37 -0
  538. package/scripts/daemon/launchd-wrapper.sh +91 -0
  539. package/scripts/daemon/lib/session-router.mjs +274 -0
  540. package/scripts/daemon/lib/session-router.test.mjs +295 -0
  541. package/scripts/daemon/maestro-daemon.mjs +275 -0
  542. package/scripts/daemon/prompt-builder.mjs +685 -0
  543. package/scripts/daemon/prompt-builder.test.mjs +213 -0
  544. package/scripts/daemon/responder.mjs +854 -0
  545. package/scripts/daemon/session-lock.mjs +721 -0
  546. package/scripts/daemon/session-lock.test.mjs +252 -0
  547. package/scripts/daemon/session-outcomes.mjs +640 -0
  548. package/scripts/daemon/session-outcomes.test.mjs +533 -0
  549. package/scripts/daemon/typing-registry.mjs +90 -0
  550. package/scripts/daemon/typing-registry.test.mjs +77 -0
  551. package/scripts/daemon/voice-webhook-server.mjs +804 -0
  552. package/scripts/decisions/capture-decision.mjs +116 -0
  553. package/scripts/disclosure_assessment.py +873 -0
  554. package/scripts/disclosure_boundaries.py +562 -0
  555. package/scripts/email-signature-principal.html +52 -0
  556. package/scripts/email-signature.html +60 -0
  557. package/scripts/email_quote_thread.py +167 -0
  558. package/scripts/email_thread_dedup.py +362 -0
  559. package/scripts/emergency-stop.sh +81 -0
  560. package/scripts/healthcheck.sh +116 -0
  561. package/scripts/hooks/block-mcp-cohort-send.sh +15 -0
  562. package/scripts/hooks/block-mcp-slack-send.sh +7 -0
  563. package/scripts/hooks/post-action-log.sh +126 -0
  564. package/scripts/hooks/pre-send-audit.sh +174 -0
  565. package/scripts/hooks/pre-send-audit.test.mjs +215 -0
  566. package/scripts/hooks/session-end-log.sh +27 -0
  567. package/scripts/hooks/session-start-banner.sh +115 -0
  568. package/scripts/huddle/audio-bridge.mjs +664 -0
  569. package/scripts/huddle/boot-slack-cdp.sh +102 -0
  570. package/scripts/huddle/huddle-controller.mjs +942 -0
  571. package/scripts/huddle/huddle-server.mjs +1229 -0
  572. package/scripts/huddle/launch-slack.sh +232 -0
  573. package/scripts/huddle/openai-realtime-bridge.mjs +462 -0
  574. package/scripts/huddle/package-lock.json +62 -0
  575. package/scripts/huddle/package.json +22 -0
  576. package/scripts/huddle/setup-audio.sh +239 -0
  577. package/scripts/huddle/start-call.mjs +318 -0
  578. package/scripts/huddle/test-pipeline.mjs +263 -0
  579. package/scripts/learning/consolidate-skills.mjs +72 -0
  580. package/scripts/learning/session-search.mjs +125 -0
  581. package/scripts/llm_email_dedup.py +442 -0
  582. package/scripts/local-triggers/generate-plists.sh +432 -0
  583. package/scripts/local-triggers/generate-plists.test.mjs +413 -0
  584. package/scripts/local-triggers/install-all.sh +49 -0
  585. package/scripts/local-triggers/plists/.gitkeep +0 -0
  586. package/scripts/local-triggers/run-trigger.sh +63 -0
  587. package/scripts/local-triggers/templates/rag-reindex.plist.template +47 -0
  588. package/scripts/local-triggers/templates/voice-relay-poller.plist.template +54 -0
  589. package/scripts/local-triggers/templates/voice-tunnel.plist.template +55 -0
  590. package/scripts/local-triggers/templates/voice-webhook.plist.template +51 -0
  591. package/scripts/maintenance/backup-to-cloud.sh +124 -0
  592. package/scripts/maintenance/health-check.sh +377 -0
  593. package/scripts/media-generation/README.md +105 -0
  594. package/scripts/media-generation/gemini-image-client.mjs +173 -0
  595. package/scripts/media-generation/generate-assets.mjs +289 -0
  596. package/scripts/media-generation/veo-video-client.mjs +219 -0
  597. package/scripts/org/send-orgmail.mjs +227 -0
  598. package/scripts/outbound-dedup-cleanup.sh +43 -0
  599. package/scripts/outbound-dedup.sh +477 -0
  600. package/scripts/outbound_dedup.py +115 -0
  601. package/scripts/parse-voice-transcript.mjs +481 -0
  602. package/scripts/pdf-generation/README.md +63 -0
  603. package/scripts/pdf-generation/build-document.mjs +247 -0
  604. package/scripts/pdf-generation/templates/board-pack.latex +136 -0
  605. package/scripts/pdf-generation/templates/corporate-letter.latex +126 -0
  606. package/scripts/pdf-generation/templates/memo.latex +114 -0
  607. package/scripts/poll-slack-events.sh +35 -0
  608. package/scripts/poller/calendar-poller.mjs +12 -0
  609. package/scripts/poller/gmail-poller.mjs +192 -0
  610. package/scripts/poller/imap-client.mjs +289 -0
  611. package/scripts/poller/inbox-scan-poller.mjs +156 -0
  612. package/scripts/poller/inbox-scan-poller.test.mjs +231 -0
  613. package/scripts/poller/index.mjs +73 -0
  614. package/scripts/poller/intra-session-check.mjs +285 -0
  615. package/scripts/poller/lib/cloud-relay-dedup.mjs +88 -0
  616. package/scripts/poller/lib/cloud-relay-dedup.test.mjs +133 -0
  617. package/scripts/poller/lib/slash-command-handlers.mjs +177 -0
  618. package/scripts/poller/secondary-gmail-poller.mjs +132 -0
  619. package/scripts/poller/slack-cloud-relay-client.mjs +368 -0
  620. package/scripts/poller/slack-poller.mjs +854 -0
  621. package/scripts/poller/slack-socket-mode.mjs +917 -0
  622. package/scripts/poller/slack-socket-mode.test.mjs +753 -0
  623. package/scripts/poller/trigger.mjs +75 -0
  624. package/scripts/poller/utils.mjs +371 -0
  625. package/scripts/poller/voice-cloud-relay-client.mjs +179 -0
  626. package/scripts/poller/voice-poller.mjs +236 -0
  627. package/scripts/poller-launchd/install.sh +66 -0
  628. package/scripts/poller-launchd/poller.plist.template +40 -0
  629. package/scripts/poller-launchd/whatsapp-handler.plist.template +39 -0
  630. package/scripts/post-interaction-indexer.py +1598 -0
  631. package/scripts/pre-draft-context.py +994 -0
  632. package/scripts/pre_draft_lookup.py +258 -0
  633. package/scripts/rag/build-index.mjs +47 -0
  634. package/scripts/rag/ingest.mjs +111 -0
  635. package/scripts/rag/search.mjs +119 -0
  636. package/scripts/rag-indexer.py +629 -0
  637. package/scripts/restore-from-backup.sh +248 -0
  638. package/scripts/restore-from-backup.test.mjs +178 -0
  639. package/scripts/resume-operations.sh +80 -0
  640. package/scripts/search-secondary-inbox.py +181 -0
  641. package/scripts/secondary-inbox-poller.py +437 -0
  642. package/scripts/self-optimization/compute-metrics.py +398 -0
  643. package/scripts/send-email-as-principal.py +369 -0
  644. package/scripts/send-email-threaded.py +392 -0
  645. package/scripts/send-email-with-attachment.py +377 -0
  646. package/scripts/send-email.sh +131 -0
  647. package/scripts/send-sms.sh +175 -0
  648. package/scripts/send-whatsapp.sh +292 -0
  649. package/scripts/session-start.sh +106 -0
  650. package/scripts/setup/boot-claude-session.sh +94 -0
  651. package/scripts/setup/configure-macos.sh +674 -0
  652. package/scripts/setup/configure-twilio-sip-trunk.mjs +207 -0
  653. package/scripts/setup/configure-voice-tunnel.mjs +182 -0
  654. package/scripts/setup/generate-agent-env.mjs +92 -0
  655. package/scripts/setup/generate-agent-package-json.mjs +222 -0
  656. package/scripts/setup/generate-agent-package-json.test.mjs +143 -0
  657. package/scripts/setup/generate-autonomy.mjs +60 -0
  658. package/scripts/setup/generate-backlog.mjs +101 -0
  659. package/scripts/setup/generate-cadences.mjs +92 -0
  660. package/scripts/setup/generate-capability.mjs +162 -0
  661. package/scripts/setup/generate-charter.mjs +76 -0
  662. package/scripts/setup/generate-comms.mjs +59 -0
  663. package/scripts/setup/generate-company.mjs +92 -0
  664. package/scripts/setup/init-agent.sh +547 -0
  665. package/scripts/setup/init-agent.test.mjs +151 -0
  666. package/scripts/setup/init-archetype.mjs +74 -0
  667. package/scripts/setup/init-backup.mjs +54 -0
  668. package/scripts/setup/init-cadence-bus.mjs +60 -0
  669. package/scripts/setup/init-channel-bus.mjs +46 -0
  670. package/scripts/setup/init-cost-tracking.mjs +45 -0
  671. package/scripts/setup/init-decision-capture.mjs +66 -0
  672. package/scripts/setup/init-known-agents.mjs +57 -0
  673. package/scripts/setup/init-learning.mjs +70 -0
  674. package/scripts/setup/init-memory-executive.mjs +45 -0
  675. package/scripts/setup/init-model-router.mjs +124 -0
  676. package/scripts/setup/init-rag.mjs +174 -0
  677. package/scripts/setup/init-session-router.mjs +38 -0
  678. package/scripts/setup/init-slack-socket-mode.mjs +260 -0
  679. package/scripts/setup/init-telegram.mjs +165 -0
  680. package/scripts/setup/init-voice-realtime.mjs +204 -0
  681. package/scripts/setup/init-whatsapp-baileys.mjs +77 -0
  682. package/scripts/setup/install-dev-tools.sh +150 -0
  683. package/scripts/setup/lib/install-plist.mjs +95 -0
  684. package/scripts/setup/migrate-agent-to-sot.mjs +192 -0
  685. package/scripts/setup/render-environment-yaml.mjs +133 -0
  686. package/scripts/slack-events-ctl.sh +177 -0
  687. package/scripts/slack-events-server.mjs +1045 -0
  688. package/scripts/slack-react.mjs +89 -0
  689. package/scripts/slack-responded.sh +232 -0
  690. package/scripts/slack-send.sh +287 -0
  691. package/scripts/slack-typing.mjs +196 -0
  692. package/scripts/slack-upload-v2.py +95 -0
  693. package/scripts/sms-handler.mjs +450 -0
  694. package/scripts/spawn-session.sh +120 -0
  695. package/scripts/sync-protocol.mjs +217 -0
  696. package/scripts/system-verify.sh +184 -0
  697. package/scripts/test-email-thread-dedup.py +239 -0
  698. package/scripts/test-information-barriers.py +484 -0
  699. package/scripts/test-llm-email-dedup.py +251 -0
  700. package/scripts/test-pre-draft-integration.py +203 -0
  701. package/scripts/test-rag-phase2.sh +442 -0
  702. package/scripts/test-rag-search.sh +251 -0
  703. package/scripts/test-voice-parser.mjs +316 -0
  704. package/scripts/user-context-search.py +659 -0
  705. package/scripts/validate_outbound.py +1504 -0
  706. package/scripts/watchdog/ai.maestro.memory-watchdog.plist +41 -0
  707. package/scripts/watchdog/force-reboot.sh +157 -0
  708. package/scripts/watchdog/memory-watchdog.sh +473 -0
  709. package/scripts/whatsapp-handler.mjs +538 -0
  710. package/teams/desktop-operations.yaml +34 -0
  711. package/teams/executive-office.yaml +27 -0
  712. package/teams/legal-and-regulatory.yaml +24 -0
  713. package/teams/platform-and-engineering.yaml +23 -0
  714. package/teams/strategy-and-growth.yaml +29 -0
  715. package/workflows/continuous/backlog-executor.yaml +141 -0
  716. package/workflows/continuous/inbound-monitor.yaml +168 -0
  717. package/workflows/daily/applicant-triage.yaml +197 -0
  718. package/workflows/daily/comms-triage.yaml +80 -0
  719. package/workflows/daily/evening-wrap.yaml +105 -0
  720. package/workflows/daily/morning-brief.yaml +164 -0
  721. package/workflows/daily/slack-followup-sweep.yaml +87 -0
  722. package/workflows/event-driven/README.md +50 -0
  723. package/workflows/event-driven/agent-failure-investigation.yaml +137 -0
  724. package/workflows/event-driven/pr-review.yaml +107 -0
  725. package/workflows/monthly/board-readiness.yaml +76 -0
  726. package/workflows/quarterly/strategic-scenario-analysis.yaml +85 -0
  727. package/workflows/session-protocol.md +171 -0
  728. package/workflows/weekly/engineering-health.yaml +154 -0
  729. package/workflows/weekly/hiring-review.yaml +169 -0
  730. package/workflows/weekly/rollup-pipeline-review.yaml +76 -0
  731. package/workflows/weekly/strategic-memo.yaml +79 -0
@@ -0,0 +1,369 @@
1
+ /**
2
+ * lib/util/reconnect.test.mjs
3
+ *
4
+ * node:test coverage for the bounded-backoff reconnect supervisor. Everything is
5
+ * driven by an injected fake clock + timer queue + RNG, so there are no real
6
+ * sockets and no real time — the backoff-and-recover lifecycle is asserted
7
+ * deterministically.
8
+ *
9
+ * Covered:
10
+ * - first connect surfaces connecting → up
11
+ * - a drop (notifyDown) drives down → connecting → up, recovering the connection
12
+ * - backoff grows decorrelated-jittered between attempts and is bounded by max
13
+ * - the attempt budget is honoured: exhausting it surfaces giving-up (never silent)
14
+ * - a failing connect() is treated as a drop and retried
15
+ * - isHealthy polling trips a reconnect when it returns false / throws
16
+ * - notifyDown is idempotent while a reconnect is already in flight
17
+ * - stop() cancels pending backoff + health poll and prevents further reconnects
18
+ * - decorrelatedBackoff bounds with an injected rng
19
+ * - a throwing onState sink can't crash the supervisor
20
+ *
21
+ * Run: cd /Users/layla/maestro && node --test lib/util/reconnect.test.mjs
22
+ */
23
+
24
+ import test from "node:test";
25
+ import assert from "node:assert/strict";
26
+
27
+ import {
28
+ superviseConnection,
29
+ decorrelatedBackoff,
30
+ CONN_STATE,
31
+ DEFAULT_BASE_BACKOFF_MS,
32
+ DEFAULT_MAX_BACKOFF_MS,
33
+ } from "./reconnect.mjs";
34
+
35
+ /**
36
+ * A deterministic timer harness: setTimeout enqueues a callback with its delay;
37
+ * `tick()` fires the earliest-due timer (advancing a logical clock), and
38
+ * `flush()` runs everything queued. Returns the injectables to pass as cfg.
39
+ */
40
+ function fakeTimers() {
41
+ let nowMs = 0;
42
+ let seq = 0;
43
+ const timers = new Map(); // id -> { at, fn }
44
+ const api = {
45
+ now: () => nowMs,
46
+ setTimeout(fn, ms) {
47
+ const id = ++seq;
48
+ timers.set(id, { at: nowMs + Math.max(0, Number(ms) || 0), fn });
49
+ return { id, unref() { return this; } };
50
+ },
51
+ clearTimeout(handle) {
52
+ if (handle && timers.has(handle.id)) timers.delete(handle.id);
53
+ },
54
+ /** Fire the single earliest-due timer; advances the clock to its `at`. */
55
+ async tick() {
56
+ let next = null;
57
+ for (const [id, t] of timers) {
58
+ if (!next || t.at < next.t.at) next = { id, t };
59
+ }
60
+ if (!next) return false;
61
+ timers.delete(next.id);
62
+ nowMs = next.t.at;
63
+ // The fn may return a promise (the async connect attempt); await it so a
64
+ // failed attempt's freshly-armed backoff timer is visible to the next loop.
65
+ await next.t.fn();
66
+ // Yield a microtask turn so any .then/.catch scheduled by the connect
67
+ // settles before we pick the next timer.
68
+ await Promise.resolve();
69
+ return true;
70
+ },
71
+ /** Drain all currently-pending timers (and any they enqueue), to a cap. */
72
+ async flush(maxIters = 1000) {
73
+ let n = 0;
74
+ // Let any already-in-flight async attempt arm its first timer before we
75
+ // start draining.
76
+ await Promise.resolve();
77
+ while (timers.size && n < maxIters) {
78
+ await api.tick();
79
+ n++;
80
+ }
81
+ },
82
+ pending() { return timers.size; },
83
+ };
84
+ return api;
85
+ }
86
+
87
+ /** A connect() that resolves/rejects on demand, recording call order. */
88
+ function scriptedConnect(plan) {
89
+ let i = 0;
90
+ const calls = [];
91
+ const connect = async () => {
92
+ const step = plan[Math.min(i, plan.length - 1)];
93
+ calls.push(step);
94
+ i++;
95
+ if (step && step.fail) throw new Error(step.error || "connect failed");
96
+ return step && step.handle !== undefined ? step.handle : `conn-${i}`;
97
+ };
98
+ connect.calls = calls;
99
+ connect.count = () => i;
100
+ return connect;
101
+ }
102
+
103
+ /** Collect onState transitions for assertions. */
104
+ function recorder() {
105
+ const states = [];
106
+ const fn = (state, info) => states.push({ state, info });
107
+ fn.states = states;
108
+ fn.names = () => states.map((s) => s.state);
109
+ return fn;
110
+ }
111
+
112
+ test("first connect surfaces connecting → up and exposes the connection", async () => {
113
+ const T = fakeTimers();
114
+ const onState = recorder();
115
+ const connect = scriptedConnect([{ handle: "socket-A" }]);
116
+ const sup = superviseConnection({ connect, onState, ...T });
117
+ await sup.started;
118
+ assert.deepEqual(onState.names(), [CONN_STATE.CONNECTING, CONN_STATE.UP]);
119
+ assert.equal(sup.state, CONN_STATE.UP);
120
+ assert.equal(sup.connection, "socket-A");
121
+ const up = onState.states.find((s) => s.state === CONN_STATE.UP);
122
+ assert.equal(up.info.reconnected, false);
123
+ });
124
+
125
+ test("a drop reconnects: down → connecting(backoff) → up, connection recovered", async () => {
126
+ const T = fakeTimers();
127
+ const onState = recorder();
128
+ const connect = scriptedConnect([{ handle: "first" }, { handle: "second" }]);
129
+ const sup = superviseConnection({
130
+ connect,
131
+ onState,
132
+ rng: () => 0, // pin jitter to the floor for a predictable delay
133
+ baseBackoffMs: 100,
134
+ ...T,
135
+ });
136
+ await sup.started;
137
+ assert.equal(sup.state, CONN_STATE.UP);
138
+
139
+ // The owner observes a socket close.
140
+ sup.notifyDown("socket-closed");
141
+ assert.equal(sup.state, CONN_STATE.DOWN);
142
+ assert.equal(connect.count(), 1, "should not reconnect until the backoff fires");
143
+
144
+ // Fire the backoff timer → reconnect attempt → up again.
145
+ await T.flush();
146
+ assert.equal(sup.state, CONN_STATE.UP);
147
+ assert.equal(sup.connection, "second");
148
+ assert.equal(connect.count(), 2);
149
+
150
+ const names = onState.names();
151
+ // The reconnected "up" carries reconnected:true.
152
+ const lastUp = onState.states.filter((s) => s.state === CONN_STATE.UP).at(-1);
153
+ assert.equal(lastUp.info.reconnected, true);
154
+ assert.ok(names.includes(CONN_STATE.DOWN));
155
+ // The failure budget reset after recovery.
156
+ assert.equal(sup.attempts, 0);
157
+ });
158
+
159
+ test("backoff grows between attempts and is bounded by maxBackoffMs", async () => {
160
+ const T = fakeTimers();
161
+ const onState = recorder();
162
+ // Always fail so we can watch successive backoff windows grow.
163
+ const connect = scriptedConnect([{ fail: true }]);
164
+ const sup = superviseConnection({
165
+ connect,
166
+ onState,
167
+ rng: () => 0.999999, // pin jitter to the TOP of each range
168
+ baseBackoffMs: 100,
169
+ maxBackoffMs: 1000,
170
+ backoffFactor: 3,
171
+ maxAttempts: 10,
172
+ ...T,
173
+ });
174
+ await sup.started; // first (failing) attempt completes and arms the first backoff
175
+ // Drive a handful of attempts and collect the scheduled backoff delays.
176
+ await T.flush(20);
177
+
178
+ // While a backoff timer is pending the connection is DOWN, and that event
179
+ // carries delayMs (the window until the next attempt).
180
+ const delays = onState.states
181
+ .filter((s) => s.state === CONN_STATE.DOWN && typeof s.info.delayMs === "number")
182
+ .map((s) => s.info.delayMs);
183
+
184
+ assert.ok(delays.length >= 3, `expected several backoff windows, got ${delays.length}`);
185
+ // Decorrelated growth with rng pinned to the top of each range:
186
+ // prev=0 → [100,100] → 100
187
+ // prev=100 → [100,300] → ~300
188
+ // prev=300 → [100,900] → ~900
189
+ // prev=900 → [100,2700] capped at 1000
190
+ assert.ok(delays[1] >= delays[0], `windows should trend upward: ${delays.slice(0, 3)}`);
191
+ // Every window respects the ceiling.
192
+ for (const d of delays) assert.ok(d <= 1000, `window ${d} exceeded max`);
193
+ // And none drops below the floor.
194
+ for (const d of delays) assert.ok(d >= 100, `window ${d} below base`);
195
+ });
196
+
197
+ test("exhausting the attempt budget surfaces giving-up (never silent)", async () => {
198
+ const T = fakeTimers();
199
+ const onState = recorder();
200
+ const connect = scriptedConnect([{ fail: true }]); // always fails
201
+ const sup = superviseConnection({
202
+ connect,
203
+ onState,
204
+ maxAttempts: 3,
205
+ baseBackoffMs: 10,
206
+ rng: () => 0,
207
+ ...T,
208
+ });
209
+ await sup.started; // first failing attempt completes and arms the first backoff
210
+ await T.flush();
211
+ assert.equal(sup.state, CONN_STATE.GIVING_UP, "must reach the terminal state, not die silently");
212
+ const giveUp = onState.states.find((s) => s.state === CONN_STATE.GIVING_UP);
213
+ assert.ok(giveUp, "a giving-up transition must be emitted");
214
+ assert.equal(giveUp.info.attempts, 3);
215
+ // No more reconnects after giving up.
216
+ const before = connect.count();
217
+ sup.notifyDown("late drop");
218
+ await T.flush();
219
+ assert.equal(connect.count(), before, "no reconnect attempts after giving-up");
220
+ });
221
+
222
+ test("a failing connect is treated as a drop and retried until it succeeds", async () => {
223
+ const T = fakeTimers();
224
+ const onState = recorder();
225
+ // Fail twice, then succeed.
226
+ const connect = scriptedConnect([{ fail: true }, { fail: true }, { handle: "live" }]);
227
+ const sup = superviseConnection({
228
+ connect,
229
+ onState,
230
+ maxAttempts: 5,
231
+ baseBackoffMs: 10,
232
+ rng: () => 0,
233
+ ...T,
234
+ });
235
+ await sup.started; // first failing attempt completes and arms the first backoff
236
+ await T.flush();
237
+ assert.equal(sup.state, CONN_STATE.UP);
238
+ assert.equal(sup.connection, "live");
239
+ assert.equal(connect.count(), 3);
240
+ });
241
+
242
+ test("isHealthy polling trips a reconnect when it returns false", async () => {
243
+ const T = fakeTimers();
244
+ const onState = recorder();
245
+ const connect = scriptedConnect([{ handle: "a" }, { handle: "b" }]);
246
+ let healthy = true;
247
+ const sup = superviseConnection({
248
+ connect,
249
+ onState,
250
+ isHealthy: () => healthy,
251
+ healthIntervalMs: 1000,
252
+ baseBackoffMs: 10,
253
+ rng: () => 0,
254
+ ...T,
255
+ });
256
+ await sup.started;
257
+ assert.equal(sup.state, CONN_STATE.UP);
258
+
259
+ // Mark the connection unhealthy; the next poll should see it and reconnect.
260
+ healthy = false;
261
+ await T.tick(); // fire the health poll → reportDown
262
+ // After the poll trips, make subsequent connects healthy again.
263
+ healthy = true;
264
+ await T.flush(); // backoff + reconnect + re-arm poll
265
+ assert.equal(sup.state, CONN_STATE.UP);
266
+ assert.equal(connect.count(), 2, "reconnected after a failed health poll");
267
+ });
268
+
269
+ test("a throwing isHealthy counts as unhealthy and reconnects", async () => {
270
+ const T = fakeTimers();
271
+ const onState = recorder();
272
+ const connect = scriptedConnect([{ handle: "a" }, { handle: "b" }]);
273
+ let mode = "ok";
274
+ const sup = superviseConnection({
275
+ connect,
276
+ onState,
277
+ isHealthy: () => {
278
+ if (mode === "throw") throw new Error("health probe blew up");
279
+ return true;
280
+ },
281
+ healthIntervalMs: 1000,
282
+ baseBackoffMs: 10,
283
+ rng: () => 0,
284
+ ...T,
285
+ });
286
+ await sup.started;
287
+ mode = "throw";
288
+ await T.tick(); // health poll throws → treated as down
289
+ mode = "ok";
290
+ await T.flush();
291
+ assert.equal(sup.state, CONN_STATE.UP);
292
+ assert.equal(connect.count(), 2);
293
+ });
294
+
295
+ test("notifyDown is idempotent while a reconnect is already in flight", async () => {
296
+ const T = fakeTimers();
297
+ const onState = recorder();
298
+ const connect = scriptedConnect([{ handle: "a" }, { handle: "b" }]);
299
+ const sup = superviseConnection({ connect, onState, baseBackoffMs: 50, rng: () => 0, ...T });
300
+ await sup.started;
301
+
302
+ // Several rapid close events should collapse into ONE reconnect cycle.
303
+ sup.notifyDown("close-1");
304
+ sup.notifyDown("close-2");
305
+ sup.notifyDown("close-3");
306
+ assert.equal(T.pending(), 1, "only one backoff timer should be armed");
307
+ await T.flush();
308
+ assert.equal(connect.count(), 2, "exactly one reconnect, not three");
309
+ });
310
+
311
+ test("stop() cancels pending backoff + health poll and blocks further reconnects", async () => {
312
+ const T = fakeTimers();
313
+ const onState = recorder();
314
+ const connect = scriptedConnect([{ handle: "a" }]);
315
+ const sup = superviseConnection({
316
+ connect,
317
+ onState,
318
+ isHealthy: () => true,
319
+ healthIntervalMs: 1000,
320
+ baseBackoffMs: 50,
321
+ rng: () => 0,
322
+ ...T,
323
+ });
324
+ await sup.started;
325
+ assert.ok(T.pending() >= 1, "health poll timer should be armed while up");
326
+
327
+ sup.notifyDown("drop"); // arms a backoff timer
328
+ sup.stop("shutting down");
329
+ assert.equal(T.pending(), 0, "stop() must clear all pending timers");
330
+
331
+ const before = connect.count();
332
+ sup.notifyDown("late drop");
333
+ await T.flush();
334
+ assert.equal(connect.count(), before, "no reconnect after stop()");
335
+ });
336
+
337
+ test("a throwing onState sink cannot crash the supervisor", async () => {
338
+ const T = fakeTimers();
339
+ const connect = scriptedConnect([{ handle: "a" }]);
340
+ const sup = superviseConnection({
341
+ connect,
342
+ onState: () => { throw new Error("bad sink"); },
343
+ ...T,
344
+ });
345
+ await assert.doesNotReject(() => sup.started);
346
+ assert.equal(sup.state, CONN_STATE.UP);
347
+ });
348
+
349
+ test("decorrelatedBackoff bounds the window with an injected rng", () => {
350
+ // rng=0 → floor; rng→1 → top of [base, prev*factor] capped at max.
351
+ assert.equal(decorrelatedBackoff(0, { base: 100, max: 5000, factor: 3, rng: () => 0 }), 100);
352
+ const top = decorrelatedBackoff(100, { base: 100, max: 5000, factor: 3, rng: () => 0.999999 });
353
+ assert.ok(top <= 300 && top >= 100, `expected within [100,300], got ${top}`);
354
+ // The cap holds even with a large prev.
355
+ assert.equal(
356
+ decorrelatedBackoff(100000, { base: 100, max: 1000, factor: 3, rng: () => 0.999999 }),
357
+ 1000
358
+ );
359
+ });
360
+
361
+ test("exposes sane default backoff constants", () => {
362
+ assert.equal(typeof DEFAULT_BASE_BACKOFF_MS, "number");
363
+ assert.equal(typeof DEFAULT_MAX_BACKOFF_MS, "number");
364
+ assert.ok(DEFAULT_MAX_BACKOFF_MS > DEFAULT_BASE_BACKOFF_MS);
365
+ });
366
+
367
+ test("superviseConnection requires a connect function", () => {
368
+ assert.throws(() => superviseConnection({}), /connect must be a function/);
369
+ });
@@ -0,0 +1,205 @@
1
+ /**
2
+ * lib/util/unhandled.mjs — process-level unhandled-failure handlers that LOG
3
+ * instead of dying silently.
4
+ *
5
+ * The audit (gaps-product-quality P0-6) found "no unhandledRejection handler in
6
+ * any daemon entrypoint (one stray rejection from an async third-party listener
7
+ * crashes the daemon)". Node's defaults make this a silent killer: an
8
+ * unhandledRejection prints a warning and (since Node 15) exits with a non-zero
9
+ * code with no structured record, and an uncaughtException tears the process down
10
+ * the same way. On a 24/7 Mac mini that is a dead agent with nothing in the logs
11
+ * a human or doctor reads.
12
+ *
13
+ * `installUnhandledHandlers({ onEvent })` installs the two process listeners with
14
+ * a SAFE default policy that matches what a resilient daemon wants:
15
+ * - unhandledRejection → LOG and KEEP RUNNING. A stray rejection from a
16
+ * third-party listener should not take down a healthy daemon; it should be
17
+ * counted and surfaced. (The pattern already exists in scripts/poller/.)
18
+ * - uncaughtException → LOG, run the caller's flush, then exit. An uncaught
19
+ * exception means corrupted in-process state; the correct move is a clean,
20
+ * LOUD exit (so launchd restarts the process) rather than limping on. The
21
+ * exit is overridable so tests — and callers with their own supervisor —
22
+ * can observe the event without the test runner dying.
23
+ *
24
+ * Every event is routed through the injected `onEvent` (and, by default, the
25
+ * loud diagnostics counters) so the failure is counted somewhere doctor looks.
26
+ *
27
+ * The installer is IDEMPOTENT — calling it twice does not double-register — and
28
+ * RETURNS an `uninstall()` so tests (and a clean daemon shutdown) can remove the
29
+ * listeners. The handlers themselves NEVER throw; a failure inside the handler
30
+ * must not become a second uncaught exception.
31
+ *
32
+ * Constraints (CLAUDE.md): ESM, Node built-ins + lib/diagnostics/counters only,
33
+ * everything (process, onEvent, exit, counter) injectable for hermetic tests.
34
+ *
35
+ * @module lib/util/unhandled
36
+ */
37
+
38
+ import { bump as defaultBump } from "../diagnostics/counters.mjs";
39
+
40
+ /** Counter names so doctor and tests can reference the exact keys. Frozen. */
41
+ export const UNHANDLED_COUNTERS = Object.freeze({
42
+ REJECTION: "process.unhandled_rejection",
43
+ EXCEPTION: "process.uncaught_exception",
44
+ });
45
+
46
+ /**
47
+ * Module-level guard tracking the live installation on a given process object,
48
+ * so a second install on the same process is a no-op (idempotent) and uninstall
49
+ * removes exactly the listeners we added. Keyed by the process object so a test
50
+ * driving a fake process and the real daemon don't collide.
51
+ * @type {WeakMap<object, { onRejection: Function, onException: Function }>}
52
+ */
53
+ const INSTALLED = new WeakMap();
54
+
55
+ /**
56
+ * Coerce a thrown/rejected value into a structured, serializable shape — we must
57
+ * never let formatting the error throw a second time inside the handler.
58
+ * @param {unknown} value
59
+ * @returns {{ name?: string, message: string, stack?: string }}
60
+ */
61
+ function describe(value) {
62
+ try {
63
+ if (value instanceof Error) {
64
+ return {
65
+ name: value.name,
66
+ message: value.message,
67
+ stack: typeof value.stack === "string" ? value.stack : undefined,
68
+ };
69
+ }
70
+ if (value == null) return { message: String(value) };
71
+ if (typeof value === "object") {
72
+ // A rejected non-Error (e.g. `reject("nope")` or `reject({code})`).
73
+ const msg = "message" in value ? String(value.message) : safeJson(value);
74
+ return { message: msg };
75
+ }
76
+ return { message: String(value) };
77
+ } catch {
78
+ return { message: "<undescribable>" };
79
+ }
80
+ }
81
+
82
+ function safeJson(v) {
83
+ try {
84
+ return JSON.stringify(v);
85
+ } catch {
86
+ return String(v);
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Install process-level unhandledRejection + uncaughtException handlers that log
92
+ * (never silently die). Idempotent and removable.
93
+ *
94
+ * @param {object} [opts]
95
+ * @param {(event: {kind:string, error:object, fatal:boolean}) => void} [opts.onEvent]
96
+ * Called for every captured failure. `kind` is "unhandledRejection" |
97
+ * "uncaughtException"; `error` is the structured {name,message,stack};
98
+ * `fatal` is true for the uncaught-exception path (process will exit).
99
+ * A throwing onEvent is swallowed — the handler must stay safe.
100
+ * @param {object} [opts.process] the process to attach to (default global process).
101
+ * Injectable so tests drive a fake EventEmitter-shaped object.
102
+ * @param {(code: number) => void} [opts.exit] called to terminate on an uncaught
103
+ * exception (default process.exit). Injectable so tests don't kill the
104
+ * runner; pass a no-op to log-only.
105
+ * @param {boolean} [opts.exitOnUncaught] exit the process on uncaughtException
106
+ * (default true). Set false to log-and-continue (a caller with its own
107
+ * supervisor). unhandledRejection NEVER exits regardless.
108
+ * @param {number} [opts.exitCode] exit code for the uncaught path (default 1).
109
+ * @param {(name:string, attrs?:object) => void} [opts.bump] counter (default the
110
+ * loud diagnostics counter). Injectable for tests.
111
+ * @returns {{ uninstall: () => void, installed: boolean }}
112
+ * `installed` is false when a prior install was already live (idempotent).
113
+ */
114
+ export function installUnhandledHandlers(opts = {}) {
115
+ const proc = opts.process || process;
116
+ const onEvent = typeof opts.onEvent === "function" ? opts.onEvent : null;
117
+ const exit =
118
+ typeof opts.exit === "function"
119
+ ? opts.exit
120
+ : (code) => proc.exit?.(code);
121
+ const exitOnUncaught = opts.exitOnUncaught !== false;
122
+ const exitCode = Number.isInteger(opts.exitCode) ? opts.exitCode : 1;
123
+ const bump = typeof opts.bump === "function" ? opts.bump : defaultBump;
124
+
125
+ // Idempotent: if we already installed on this process, do nothing and report
126
+ // it so a double-install (e.g. daemon re-entry) is a safe no-op.
127
+ if (INSTALLED.has(proc)) {
128
+ return { uninstall: () => uninstallFor(proc), installed: false };
129
+ }
130
+
131
+ /** Route one failure through the counter, the sink, and (if fatal) the exit. */
132
+ function dispatch(kind, value, fatal) {
133
+ const error = describe(value);
134
+ // 1. Count it — loud, durable, where doctor looks. Never throws.
135
+ try {
136
+ bump(
137
+ kind === "uncaughtException"
138
+ ? UNHANDLED_COUNTERS.EXCEPTION
139
+ : UNHANDLED_COUNTERS.REJECTION,
140
+ { name: error.name || null, message: error.message, fatal: !!fatal }
141
+ );
142
+ } catch {
143
+ /* counter must never crash the handler */
144
+ }
145
+ // 2. Tell the caller. A throwing sink is isolated.
146
+ if (onEvent) {
147
+ try {
148
+ onEvent({ kind, error, fatal: !!fatal });
149
+ } catch {
150
+ /* a buggy sink must not become a second uncaught exception */
151
+ }
152
+ }
153
+ }
154
+
155
+ function onRejection(reason) {
156
+ // Log + keep running. A stray rejection should never down a healthy daemon.
157
+ dispatch("unhandledRejection", reason, false);
158
+ }
159
+
160
+ function onException(err) {
161
+ // Log + flush via onEvent, then exit LOUDLY (default) so launchd restarts a
162
+ // clean process rather than limping on with corrupted state.
163
+ dispatch("uncaughtException", err, true);
164
+ if (exitOnUncaught) {
165
+ try {
166
+ exit(exitCode);
167
+ } catch {
168
+ /* if even exit throws, there's nothing more we can safely do */
169
+ }
170
+ }
171
+ }
172
+
173
+ proc.on?.("unhandledRejection", onRejection);
174
+ proc.on?.("uncaughtException", onException);
175
+ INSTALLED.set(proc, { onRejection, onException });
176
+
177
+ return { uninstall: () => uninstallFor(proc), installed: true };
178
+ }
179
+
180
+ /**
181
+ * Remove the handlers this module installed on `proc`, if any. Safe to call
182
+ * multiple times. Never throws.
183
+ * @param {object} proc
184
+ * @returns {boolean} true if listeners were removed
185
+ */
186
+ function uninstallFor(proc) {
187
+ const live = INSTALLED.get(proc);
188
+ if (!live) return false;
189
+ try { proc.removeListener?.("unhandledRejection", live.onRejection); } catch { /* */ }
190
+ try { proc.removeListener?.("uncaughtException", live.onException); } catch { /* */ }
191
+ INSTALLED.delete(proc);
192
+ return true;
193
+ }
194
+
195
+ /**
196
+ * True if this module currently has handlers installed on `proc` (default global
197
+ * process). Lets a caller/doctor check without re-installing.
198
+ * @param {object} [proc]
199
+ * @returns {boolean}
200
+ */
201
+ export function isInstalled(proc = process) {
202
+ return INSTALLED.has(proc);
203
+ }
204
+
205
+ export default { installUnhandledHandlers, isInstalled, UNHANDLED_COUNTERS };