@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,719 @@
1
+ /**
2
+ * Maestro — BaseAdapter (cross-cutting channel logic)
3
+ *
4
+ * WS2 (design §5) factors the logic every real-time channel shares into one
5
+ * base class so concrete adapters implement only the platform-specific hooks:
6
+ *
7
+ * abstract _connect({ onRaw, signal }) open the live stream; call onRaw(raw)
8
+ * per provider frame.
9
+ * abstract _toEvent(raw) → MessageEvent|null translate a raw frame to a
10
+ * contract MessageEvent (null to drop).
11
+ * abstract _send(out) → SendResult send an outbound message.
12
+ * optional _react(out) → SendResult add/remove a reaction.
13
+ * optional _setTyping(source, on) set/clear a typing indicator.
14
+ *
15
+ * BaseAdapter provides:
16
+ * - `start({ onInbound, signal })` — wires _connect → _toEvent → (pairing
17
+ * gate) → onInbound, stamping `ev.channel`. The pairing gate runs in the
18
+ * onInbound wrapper so every channel inherits default-deny DMs + pair-code
19
+ * interception (lib/channels/pairing.mjs). Pairing/gate failures are
20
+ * fail-open-logged, never thrown into the live loop.
21
+ * - `send(out)` — thin wrapper over `_send` with uniform error shaping.
22
+ * - `startTypingHeartbeat(source)` / `stopTypingHeartbeat(source)` — a
23
+ * ~4s setInterval that calls `_setTyping(source, true)`, timeout-guarded so
24
+ * a hung provider call can't wedge the loop. The dispatcher starts the
25
+ * heartbeat on session spawn and stops it on session close.
26
+ * - `react(out)`, `setTyping(...)`, `stop()`, `healthCheck()`.
27
+ *
28
+ * The base is transport-agnostic and hermetic: it never imports a provider SDK.
29
+ * Concrete adapters are dynamic-import-gated (like baileys/grammy).
30
+ *
31
+ * @module lib/channels/base-adapter
32
+ */
33
+
34
+ "use strict";
35
+
36
+ import { resolve } from "node:path";
37
+ import { checkAllowed, startPairing, redeemPairing } from "./pairing.mjs";
38
+ import { screenOutbound as defaultScreenOutbound } from "../comms/send-gate.mjs";
39
+ import { getHookBus } from "../hooks/bus.mjs";
40
+ import { superviseConnection as defaultSupervise, CONN_STATE } from "../util/reconnect.mjs";
41
+ import { bump as defaultBump } from "../diagnostics/counters.mjs";
42
+
43
+ const DEFAULT_TYPING_INTERVAL_MS = 4_000;
44
+ const DEFAULT_TYPING_CALL_TIMEOUT_MS = 3_000;
45
+
46
+ /* ───────────────────── bot-loop / reply-storm guard knobs ─────────────────── */
47
+ // Two agents sharing one channel can ping-pong forever ("ok" → "ok" → …). The
48
+ // pair guard is a per-conversation sliding window: if more than
49
+ // PAIR_GUARD_MAX outbound replies land inside PAIR_GUARD_WINDOW_MS for the same
50
+ // conversation, further sends are suppressed until the window drains. This is a
51
+ // last-ditch circuit breaker BELOW the ownership filter (which already drops a
52
+ // known peer-agent inbound) — it catches the storm even when the other party
53
+ // isn't a *registered* peer agent. Cheap, in-memory, fail-open.
54
+ const PAIR_GUARD_WINDOW_MS = 60_000;
55
+ const PAIR_GUARD_MAX = 6;
56
+
57
+ /** Diagnostic counter names (gaps-product-quality P0-6: count every fail-open). */
58
+ export const CHANNEL_COUNTERS = Object.freeze({
59
+ SEND_GATE_BLOCKED: "channel.send_gate.blocked",
60
+ SEND_GATE_FAILOPEN: "channel.send_gate.fail_open",
61
+ PAIR_GUARD_TRIPPED: "channel.pair_guard.tripped",
62
+ PEER_AGENT_DROPPED: "channel.peer_agent.dropped",
63
+ RECONNECT_DOWN: "channel.reconnect.down",
64
+ RECONNECT_GAVE_UP: "channel.reconnect.gave_up",
65
+ RECONNECT_RECOVERED: "channel.reconnect.recovered",
66
+ });
67
+
68
+ export class BaseAdapter {
69
+ /**
70
+ * @param {object} opts
71
+ * @param {string} opts.name channel name (e.g. "telegram")
72
+ * @param {string} [opts.agentRoot]
73
+ * @param {string[]} [opts.capabilities]
74
+ * @param {object} [opts.config] loaded config/<name>.yaml
75
+ * @param {(lvl:string,msg:string)=>void} [opts.log]
76
+ * @param {string} [opts.ownId] the agent's own peer id on this channel
77
+ * @param {Set<string>|string[]} [opts.peerAgentIds]
78
+ * @param {number} [opts.typingIntervalMs]
79
+ * @param {(source:object)=>Promise<void>|void} [opts.onPairingPrompt] one-time
80
+ * reply to an unknown DM sender (e.g. "DM me `pair <code>`"). Optional.
81
+ * @param {"deny"|"open"} [opts.pairingMode="deny"] "deny" = default-deny
82
+ * unknown DM senders (new channels: Telegram/WhatsApp). "open" = skip
83
+ * the pairing gate entirely because channel membership/identity is
84
+ * already governed upstream (Slack: shouldKeepEvent + directed-gate).
85
+ * Behaviour-preserving for the live Slack path.
86
+ * @param {boolean} [opts.sendGate=true] run the shared outbound send-gate
87
+ * (lib/comms/send-gate + hook-bus beforeSend) before every _send. Set
88
+ * false only for a path already gated upstream.
89
+ * @param {boolean} [opts.supervise=true] wrap _connect in the bounded-backoff
90
+ * reconnect supervisor (lib/util/reconnect) so a dropped channel
91
+ * reconnects and a dead channel turns healthCheck() red.
92
+ * @param {object} [opts.screenOutbound] inject the send-gate fn (tests).
93
+ * @param {object} [opts.hookBus] inject a HookBus (tests); default = the
94
+ * process-wide singleton getHookBus().
95
+ * @param {Function} [opts.superviseConnection] inject the reconnect supervisor.
96
+ * @param {Function} [opts.bump] inject the diagnostics counter (tests).
97
+ * @param {string} [opts.jurisdiction] recipient jurisdiction for disclosure.
98
+ * @param {string[]} [opts.internalDomains] email domains treated as internal.
99
+ * @param {() => number} [opts.rng] jitter RNG for the reconnect supervisor.
100
+ */
101
+ constructor(opts = {}) {
102
+ this.name = opts.name || "channel";
103
+ this.agentRoot = resolve(opts.agentRoot || process.env.AGENT_ROOT || process.env.AGENT_DIR || process.cwd());
104
+ this.capabilities = Array.isArray(opts.capabilities) ? opts.capabilities.slice() : [];
105
+ this.config = opts.config || {};
106
+ this.log = opts.log || ((lvl, m) => process.stdout.write(`[${this.name}:${lvl}] ${m}\n`));
107
+ this.ownId = opts.ownId || "";
108
+ this.peerAgentIds = toPeerSet(opts.peerAgentIds);
109
+ this.onPairingPrompt = typeof opts.onPairingPrompt === "function" ? opts.onPairingPrompt : null;
110
+ this.pairingMode = opts.pairingMode === "open" ? "open" : "deny";
111
+
112
+ this._typingIntervalMs = opts.typingIntervalMs || DEFAULT_TYPING_INTERVAL_MS;
113
+ this._setTimeout = opts.setTimeout || globalThis.setTimeout;
114
+ this._clearTimeout = opts.clearTimeout || globalThis.clearTimeout;
115
+ this._setInterval = opts.setInterval || globalThis.setInterval;
116
+ this._clearInterval = opts.clearInterval || globalThis.clearInterval;
117
+
118
+ // -- outbound governance (P0-3): the shared send-gate runs before _send. --
119
+ this._sendGateEnabled = opts.sendGate !== false;
120
+ this._screenOutbound =
121
+ typeof opts.screenOutbound === "function" ? opts.screenOutbound : defaultScreenOutbound;
122
+ this._hookBus = opts.hookBus || null; // resolved lazily so the singleton is shared
123
+ this._jurisdiction = opts.jurisdiction || undefined;
124
+ this._internalDomains = Array.isArray(opts.internalDomains) ? opts.internalDomains.slice() : [];
125
+
126
+ // -- reconnect supervision (P0-5/P0-6). --
127
+ this._superviseEnabled = opts.supervise !== false;
128
+ this._superviseConnection =
129
+ typeof opts.superviseConnection === "function" ? opts.superviseConnection : defaultSupervise;
130
+ // Health-poll interval for the reconnect supervisor: without it the poll is
131
+ // off and a SILENT drop (transport unhealthy but _connect never rejected)
132
+ // goes undetected. Default 30s; 0 disables (tests/adapters with push-based
133
+ // drop signalling via notifyDown).
134
+ this._healthIntervalMs = Number.isFinite(opts.healthIntervalMs) ? opts.healthIntervalMs : 30_000;
135
+ this._supervisor = null;
136
+ this._connState = CONN_STATE.CONNECTING;
137
+ this._rng = typeof opts.rng === "function" ? opts.rng : undefined;
138
+
139
+ // -- diagnostics counter (P0-6): every fail-open is counted somewhere. --
140
+ this._bump = typeof opts.bump === "function" ? opts.bump : defaultBump;
141
+
142
+ this._onInbound = null;
143
+ this._signal = null;
144
+ this._stopping = false;
145
+ /** @type {Map<string, any>} sessionKey → interval handle */
146
+ this._typingTimers = new Map();
147
+ // De-dupe one-time pairing prompts per unknown sender so we don't spam.
148
+ this._promptedPeers = new Set();
149
+ // Sliding-window reply timestamps per conversation key (bot-loop guard).
150
+ /** @type {Map<string, number[]>} */
151
+ this._replyWindow = new Map();
152
+ }
153
+
154
+ /** Resolve the HookBus (the shared singleton unless one was injected). */
155
+ _bus() {
156
+ if (this._hookBus) return this._hookBus;
157
+ try {
158
+ this._hookBus = getHookBus();
159
+ } catch {
160
+ this._hookBus = null; // never let bus construction wedge a send
161
+ }
162
+ return this._hookBus;
163
+ }
164
+
165
+ /** Count a guard event without ever throwing into the observed path. */
166
+ _count(name, attrs) {
167
+ try {
168
+ this._bump(name, { channel: this.name, ...(attrs || {}) }, { agentRoot: this.agentRoot });
169
+ } catch {
170
+ /* a counter must never crash the path it observes */
171
+ }
172
+ }
173
+
174
+ // -- Abstract hooks (subclasses must override _connect/_toEvent/_send) -----
175
+
176
+ /* eslint-disable no-unused-vars */
177
+ async _connect({ onRaw, signal }) {
178
+ throw new Error(`${this.name}: _connect() not implemented`);
179
+ }
180
+ _toEvent(raw) {
181
+ throw new Error(`${this.name}: _toEvent() not implemented`);
182
+ }
183
+ async _send(out) {
184
+ throw new Error(`${this.name}: _send() not implemented`);
185
+ }
186
+ async _react(out) {
187
+ // Optional — default no-op for channels without reactions.
188
+ return { ok: false, error: "react-not-supported" };
189
+ }
190
+ async _setTyping(source, on) {
191
+ // Optional — default no-op for channels without typing (SMS/Gmail).
192
+ }
193
+ /* eslint-enable no-unused-vars */
194
+
195
+ // -- start: connect → translate → gate → onInbound ------------------------
196
+
197
+ /**
198
+ * @param {object} args
199
+ * @param {(item:object)=>void} args.onInbound called per accepted MessageEvent
200
+ * @param {AbortSignal} [args.signal]
201
+ */
202
+ async start({ onInbound, signal }) {
203
+ if (typeof onInbound !== "function") throw new Error(`${this.name} start(): onInbound required`);
204
+ this._onInbound = onInbound;
205
+ this._signal = signal || null;
206
+ this._stopping = false;
207
+ if (signal) signal.addEventListener?.("abort", () => { this._stopping = true; });
208
+
209
+ const onRaw = (raw) => {
210
+ // Translation + gating must never throw into the provider's frame loop.
211
+ let ev;
212
+ try {
213
+ ev = this._toEvent(raw);
214
+ } catch (err) {
215
+ this.log("error", `_toEvent threw: ${err.message}`);
216
+ return;
217
+ }
218
+ if (!ev) return; // adapter chose to drop this frame
219
+ if (!ev.channel) ev.channel = this.name;
220
+ this._handleEvent(ev);
221
+ };
222
+
223
+ // P0-5/P0-6: a live channel that drops must reconnect (bounded backoff) and
224
+ // a dead one must turn healthCheck() red — today nothing supervises this.
225
+ // The supervisor owns the connect loop: each (re)connect attempt calls
226
+ // _connect, and every state transition is surfaced + counted so the channel
227
+ // can never silently die. We still AWAIT the first connect so callers that
228
+ // act immediately after start() (e.g. driving onRaw) see a live adapter.
229
+ if (this._superviseEnabled && typeof this._superviseConnection === "function") {
230
+ this._supervisor = this._superviseConnection({
231
+ connect: () => this._connect({ onRaw, signal }),
232
+ isHealthy: () => this._connectionHealthy(),
233
+ onState: (state, info) => this._onConnState(state, info),
234
+ healthIntervalMs: this._healthIntervalMs,
235
+ setTimeout: this._setTimeout,
236
+ clearTimeout: this._clearTimeout,
237
+ rng: this._rng,
238
+ });
239
+ if (signal) {
240
+ signal.addEventListener?.("abort", () => {
241
+ try { this._supervisor?.stop?.("aborted"); } catch { /* */ }
242
+ });
243
+ }
244
+ try {
245
+ await this._supervisor.started; // first connect attempt
246
+ } catch {
247
+ /* a failed first connect is handled by the supervisor's backoff loop */
248
+ }
249
+ return;
250
+ }
251
+
252
+ // Unsupervised path (supervise:false): connect once, no reconnect.
253
+ await this._connect({ onRaw, signal });
254
+ }
255
+
256
+ /**
257
+ * Per-adapter liveness probe the reconnect supervisor polls (and the daemon
258
+ * health sweep can reuse via healthCheck). Default: live unless the supervisor
259
+ * has reported the connection down/giving-up. Adapters with a cheap socket
260
+ * probe can override. Never throws — a thrown probe is an unhealthy probe.
261
+ * @returns {Promise<boolean>|boolean}
262
+ */
263
+ _connectionHealthy() {
264
+ return this._connState === CONN_STATE.UP || this._connState === CONN_STATE.CONNECTING;
265
+ }
266
+
267
+ /**
268
+ * Reconnect-supervisor state sink. Records the current state (so healthCheck
269
+ * reflects it) and counts every transition so a drop/giving-up is never
270
+ * silent (P0-6). Fail-open: a throwing counter can't break the supervisor.
271
+ */
272
+ _onConnState(state, info = {}) {
273
+ this._connState = state;
274
+ if (state === CONN_STATE.UP) {
275
+ // A reconnection (reconnected:true) is the noteworthy recovery; the very
276
+ // first connect (reconnected:false) is just normal startup.
277
+ if (info.reconnected) {
278
+ this._count(CHANNEL_COUNTERS.RECONNECT_RECOVERED, { attempt: info.attempt });
279
+ this.log("info", `${this.name}: reconnected after a drop`);
280
+ }
281
+ } else if (state === CONN_STATE.DOWN) {
282
+ this._count(CHANNEL_COUNTERS.RECONNECT_DOWN, { reason: info.reason, delayMs: info.delayMs });
283
+ this.log("warn", `${this.name}: connection down (${info.reason || "?"}) — retrying in ${info.delayMs ?? "?"}ms`);
284
+ } else if (state === CONN_STATE.GIVING_UP) {
285
+ this._count(CHANNEL_COUNTERS.RECONNECT_GAVE_UP, { attempts: info.attempts, reason: info.reason });
286
+ this.log("error", `${this.name}: gave up reconnecting after ${info.attempts ?? "?"} attempts — channel is DEAD`);
287
+ }
288
+ }
289
+
290
+ /**
291
+ * Report an observed connection drop to the supervisor so it reconnects with
292
+ * bounded backoff. Adapters call this from their provider's close/error
293
+ * listener (the channel-adapter path). No-op when unsupervised.
294
+ * @param {string} [reason]
295
+ */
296
+ notifyConnectionDown(reason) {
297
+ try { this._supervisor?.notifyDown?.(reason); } catch { /* */ }
298
+ }
299
+
300
+ /**
301
+ * Run the pairing gate, intercept `pair <code>`, then forward to onInbound.
302
+ * Fail-open: any error here is logged and the event is dropped rather than
303
+ * crashing the loop (a single bad event must not take the channel down).
304
+ */
305
+ _handleEvent(ev) {
306
+ // Ownership / bot-loop backstop (P0-5): an inbound from the agent's OWN id
307
+ // or a KNOWN peer-agent id is never auto-replied — two collaborating agents
308
+ // in one channel would otherwise reply-storm. This runs in BOTH pairing
309
+ // modes, including "open" (Slack), where the bus factory now wires ownId /
310
+ // peerAgentIds but the directed-gate alone wouldn't stop a peer loop.
311
+ if (this._isOwnOrPeer(ev)) {
312
+ this._count(CHANNEL_COUNTERS.PEER_AGENT_DROPPED, { from: this._eventSenderId(ev) });
313
+ this.log("debug", `dropping own/peer-agent inbound (no auto-reply)`);
314
+ return;
315
+ }
316
+
317
+ // "open" channels (Slack) bypass the pairing gate — their membership and
318
+ // self/peer filtering already happened upstream (shouldKeepEvent) and the
319
+ // daemon's directed-gate governs the rest. Forwarding straight through
320
+ // keeps the live path byte-for-byte unchanged.
321
+ if (this.pairingMode === "open") {
322
+ try { this._onInbound(ev); }
323
+ catch (err) { this.log("error", `onInbound threw: ${err.message}`); }
324
+ return;
325
+ }
326
+
327
+ let gate;
328
+ try {
329
+ gate = checkAllowed(ev, {
330
+ agentRoot: this.agentRoot,
331
+ ownId: this.ownId,
332
+ peerAgentIds: this.peerAgentIds,
333
+ });
334
+ } catch (err) {
335
+ this.log("error", `pairing gate threw, dropping event: ${err.message}`);
336
+ return;
337
+ }
338
+
339
+ // `pair <code>` is intercepted BEFORE the classifier — it never reaches
340
+ // the inbox. Redeem it and (best-effort) acknowledge.
341
+ if (gate.isPairingCommand && gate.code) {
342
+ this._redeemAndAck(ev, gate.code);
343
+ return;
344
+ }
345
+
346
+ if (gate.allowed) {
347
+ try {
348
+ this._onInbound(ev);
349
+ } catch (err) {
350
+ this.log("error", `onInbound threw: ${err.message}`);
351
+ }
352
+ return;
353
+ }
354
+
355
+ // Denied. For an unknown DM sender, emit a single pairing prompt (once)
356
+ // so a legitimate human knows how to pair. Other reasons (self/peer) are
357
+ // silent drops.
358
+ if (gate.reason === "not-paired") {
359
+ this._maybePromptPairing(ev);
360
+ }
361
+ this.log("debug", `event dropped: ${gate.reason}`);
362
+ }
363
+
364
+ _redeemAndAck(ev, code) {
365
+ const source = ev.source || {};
366
+ const channel = source.channel || ev.channel || this.name;
367
+ const peerId = String(ev.from?.id ?? source.peerId ?? "");
368
+ let res = { ok: false, reason: "no-peer" };
369
+ try {
370
+ res = redeemPairing(channel, peerId, code, { agentRoot: this.agentRoot });
371
+ } catch (err) {
372
+ this.log("error", `redeemPairing threw: ${err.message}`);
373
+ }
374
+ // Best-effort acknowledgement back to the sender. This is a fixed,
375
+ // framework-authored control string (not model output and not user-facing
376
+ // content under disclosure/barrier policy), so it bypasses the outbound
377
+ // policy gate — otherwise the fail-closed external default would silently
378
+ // swallow the pairing handshake itself.
379
+ const text = res.ok
380
+ ? "Paired. You can message me here now."
381
+ : `That pairing code didn't work (${res.reason}). Ask for a fresh one.`;
382
+ this._systemSend({ to: source.chatId || peerId, text, source }).catch(() => {});
383
+ this.log("info", `pair attempt from ${peerId}: ${res.ok ? "ok" : res.reason}`);
384
+ }
385
+
386
+ _maybePromptPairing(ev) {
387
+ const peerId = String(ev.from?.id ?? ev.source?.peerId ?? "");
388
+ if (!peerId || this._promptedPeers.has(peerId)) return;
389
+ this._promptedPeers.add(peerId);
390
+ if (!this.onPairingPrompt) return;
391
+ Promise.resolve()
392
+ .then(() => this.onPairingPrompt(ev.source || { channel: this.name, chatId: peerId }))
393
+ .catch((err) => this.log("warn", `pairing prompt failed: ${err.message}`));
394
+ }
395
+
396
+ // -- ownership / bot-loop guard (P0-5) ------------------------------------
397
+
398
+ /** The sender peer id of an inbound event (best-effort, stringified). */
399
+ _eventSenderId(ev) {
400
+ return String(ev?.from?.id ?? ev?.source?.peerId ?? "");
401
+ }
402
+
403
+ /**
404
+ * Is this inbound from the agent's own id or a registered peer agent? Used to
405
+ * suppress self-echo and agent-to-agent reply storms in every pairing mode.
406
+ * @returns {boolean}
407
+ */
408
+ _isOwnOrPeer(ev) {
409
+ const id = this._eventSenderId(ev);
410
+ if (!id) return false;
411
+ if (this.ownId && id === String(this.ownId)) return true;
412
+ return this.peerAgentIds instanceof Set && this.peerAgentIds.has(id);
413
+ }
414
+
415
+ /**
416
+ * Sliding-window reply-storm guard. Records that we're about to send to a
417
+ * conversation and returns true if the per-conversation reply rate is over
418
+ * the ceiling inside the window — meaning we should suppress this send to
419
+ * break a ping-pong. Pruning is lazy (drop timestamps older than the window).
420
+ * Fail-open: any internal slip allows the send.
421
+ *
422
+ * @param {string} convKey conversation key (channel:chatId:threadRef)
423
+ * @returns {boolean} true ⇒ suppress (storm); false ⇒ allow
424
+ */
425
+ _pairGuardTrips(convKey) {
426
+ try {
427
+ const now = Date.now();
428
+ const cutoff = now - PAIR_GUARD_WINDOW_MS;
429
+ const arr = (this._replyWindow.get(convKey) || []).filter((t) => t >= cutoff);
430
+ if (arr.length >= PAIR_GUARD_MAX) {
431
+ this._replyWindow.set(convKey, arr); // keep the pruned window
432
+ return true;
433
+ }
434
+ arr.push(now);
435
+ this._replyWindow.set(convKey, arr);
436
+ return false;
437
+ } catch {
438
+ return false; // fail-open: never let the guard itself block a send
439
+ }
440
+ }
441
+
442
+ // -- send / react ---------------------------------------------------------
443
+
444
+ /**
445
+ * Send an outbound message through the ONE shared outbound perimeter, then the
446
+ * adapter's _send.
447
+ *
448
+ * Order (gaps-product-quality P0-3/P0-5):
449
+ * 1. send-gate — the hook-bus `beforeSend` cancellable chain AND the shared
450
+ * lib/comms/send-gate.screenOutbound. EITHER vetoing blocks the send.
451
+ * Now EVERY adapter send passes the gate, not just Gmail.
452
+ * 2. pair guard — a per-conversation sliding window so two agents in one
453
+ * channel can't reply-storm.
454
+ * 3. _send — the adapter transport.
455
+ *
456
+ * A blocked send returns a uniform SendResult `{ ok:false, blocked:true,
457
+ * reason }` (NOT thrown) so callers can branch on `res.blocked`.
458
+ *
459
+ * @param {object} out
460
+ * @returns {Promise<import("./contract.mjs").SendResult>}
461
+ */
462
+ async send(out) {
463
+ // 1. Outbound governance — the shared send-gate (hook-bus + screenOutbound).
464
+ if (this._sendGateEnabled) {
465
+ const gate = await this._screenSend(out);
466
+ if (!gate.allow) {
467
+ this._count(CHANNEL_COUNTERS.SEND_GATE_BLOCKED, { reason: gate.reason });
468
+ this.log("warn", `send blocked by gate: ${gate.reason}`);
469
+ return { ok: false, blocked: true, reason: gate.reason };
470
+ }
471
+ // The gate may rewrite the body (redaction); honour it.
472
+ if (typeof gate.text === "string" && gate.text !== out.text) {
473
+ out = { ...out, text: gate.text };
474
+ }
475
+ }
476
+
477
+ // 2. Bot-loop / reply-storm guard (per conversation).
478
+ const convKey = this._sendConvKey(out);
479
+ if (convKey && this._pairGuardTrips(convKey)) {
480
+ this._count(CHANNEL_COUNTERS.PAIR_GUARD_TRIPPED, { conversation: convKey });
481
+ this.log("warn", `send suppressed: reply-storm guard tripped for ${convKey}`);
482
+ return { ok: false, blocked: true, reason: "reply-storm-guard" };
483
+ }
484
+
485
+ // 3. Adapter transport.
486
+ try {
487
+ const res = await this._send(out);
488
+ // Normalise into a SendResult.
489
+ if (res && typeof res === "object" && "ok" in res) return res;
490
+ return { ok: true, message_id: res?.message_id, raw: res };
491
+ } catch (err) {
492
+ return { ok: false, error: err.message };
493
+ }
494
+ }
495
+
496
+ /**
497
+ * Send a fixed, framework-authored control message (pairing acks, prompts)
498
+ * that bypasses the outbound policy gate and the reply-storm guard. These are
499
+ * NOT model output and NOT user-facing content under disclosure/barrier
500
+ * policy, so gating them would only let the fail-closed external default
501
+ * swallow the pairing handshake. Errors are still shaped into a SendResult.
502
+ * @param {object} out
503
+ * @returns {Promise<import("./contract.mjs").SendResult>}
504
+ */
505
+ async _systemSend(out) {
506
+ try {
507
+ const res = await this._send(out);
508
+ if (res && typeof res === "object" && "ok" in res) return res;
509
+ return { ok: true, message_id: res?.message_id, raw: res };
510
+ } catch (err) {
511
+ return { ok: false, error: err.message };
512
+ }
513
+ }
514
+
515
+ /** Conversation key for the pair guard, derived from the outbound shape. */
516
+ _sendConvKey(out) {
517
+ const channel = out?.source?.channel || this.name;
518
+ const chatId = out?.to ?? out?.source?.chatId ?? "";
519
+ const threadRef = out?.thread_ref ?? out?.source?.threadRef ?? "";
520
+ if (chatId === "" || chatId == null) return "";
521
+ return `${channel}:${chatId}:${threadRef}`;
522
+ }
523
+
524
+ /**
525
+ * Run the shared outbound send-gate: the hook-bus `beforeSend` cancellable
526
+ * chain (so any subscriber — disclosure, custom policy — can veto) AND
527
+ * lib/comms/send-gate.screenOutbound (banned phrases, AI-disclosure,
528
+ * information barriers, allowlist). EITHER veto blocks.
529
+ *
530
+ * Fail posture: screenOutbound itself fails CLOSED for external recipients and
531
+ * OPEN for internal (it owns that policy). If our wiring around it throws
532
+ * unexpectedly we count a fail-open and allow — a buggy gate must never
533
+ * silently wedge all outbound traffic (house idiom), but the fail-open is
534
+ * LOUD (counted) so doctor/a human sees it.
535
+ *
536
+ * @returns {Promise<{allow:boolean, reason:string, text?:string}>}
537
+ */
538
+ async _screenSend(out) {
539
+ const channel = out?.source?.channel || this.name;
540
+ const recipient = String(out?.to ?? out?.source?.chatId ?? "");
541
+ const text = typeof out?.text === "string" ? out.text : "";
542
+
543
+ // (a) hook-bus beforeSend chain — only `beforeSend` is cancellable.
544
+ try {
545
+ const bus = this._bus();
546
+ if (bus) {
547
+ const res = await bus.emit("beforeSend", {
548
+ tool: `${this.name}_send`,
549
+ channel,
550
+ recipient,
551
+ text,
552
+ source: out?.source || null,
553
+ });
554
+ if (res && res.cancelled) {
555
+ return { allow: false, reason: res.reason || "beforeSend-cancelled" };
556
+ }
557
+ }
558
+ } catch (err) {
559
+ // A bus fault is fail-open (counted) — the screenOutbound layer below is
560
+ // the authoritative policy gate and runs regardless.
561
+ this._count(CHANNEL_COUNTERS.SEND_GATE_FAILOPEN, { stage: "hookbus", error: err && err.message });
562
+ }
563
+
564
+ // (b) the shared policy gate (the authoritative chokepoint).
565
+ try {
566
+ const verdict = await this._screenOutbound({
567
+ channel,
568
+ recipient,
569
+ text,
570
+ agentRoot: this.agentRoot,
571
+ jurisdiction: this._jurisdiction,
572
+ firstContact: out?.firstContact !== false,
573
+ internalDomains: this._internalDomains,
574
+ allowlist: Array.isArray(out?.allowlist) ? out.allowlist : undefined,
575
+ });
576
+ if (verdict && verdict.allow === false) {
577
+ return { allow: false, reason: verdict.reason || "blocked" };
578
+ }
579
+ return { allow: true, reason: "", text: verdict?.redactedText };
580
+ } catch (err) {
581
+ // screenOutbound is itself never supposed to throw (it has its own
582
+ // fail-closed/open policy). If our call site does, count the fail-open and
583
+ // allow rather than wedge every send — but loudly.
584
+ this._count(CHANNEL_COUNTERS.SEND_GATE_FAILOPEN, { stage: "screen", error: err && err.message });
585
+ this.log("error", `send-gate threw (fail-open): ${err && err.message}`);
586
+ return { allow: true, reason: "" };
587
+ }
588
+ }
589
+
590
+ async react(out) {
591
+ try {
592
+ return await this._react(out);
593
+ } catch (err) {
594
+ return { ok: false, error: err.message };
595
+ }
596
+ }
597
+
598
+ async setTyping(source, on) {
599
+ try {
600
+ await this._setTyping(source, on);
601
+ return { ok: true };
602
+ } catch (err) {
603
+ return { ok: false, error: err.message };
604
+ }
605
+ }
606
+
607
+ // -- typing heartbeat -----------------------------------------------------
608
+
609
+ /**
610
+ * Start a typing heartbeat for a conversation. Calls `_setTyping(source,
611
+ * true)` immediately and then every ~4s until stopped. Each call is
612
+ * timeout-guarded so a hung provider can't wedge the interval. Keyed by the
613
+ * source's chat/thread so concurrent sessions don't collide.
614
+ *
615
+ * @param {import("./contract.mjs").SessionSource} source
616
+ * @returns {string} the heartbeat key (pass to stopTypingHeartbeat)
617
+ */
618
+ startTypingHeartbeat(source) {
619
+ if (!source) return "";
620
+ const key = this._typingKey(source);
621
+ if (this._typingTimers.has(key)) return key; // already beating
622
+
623
+ const beat = () => {
624
+ this._guardedTyping(source, true);
625
+ };
626
+ // Fire once immediately so the indicator shows without waiting a full tick.
627
+ beat();
628
+ const handle = this._setInterval(beat, this._typingIntervalMs);
629
+ if (handle && typeof handle.unref === "function") handle.unref();
630
+ this._typingTimers.set(key, handle);
631
+ return key;
632
+ }
633
+
634
+ /**
635
+ * Stop the typing heartbeat for a conversation and clear the indicator.
636
+ * @param {import("./contract.mjs").SessionSource|string} sourceOrKey
637
+ */
638
+ stopTypingHeartbeat(sourceOrKey) {
639
+ const key = typeof sourceOrKey === "string" ? sourceOrKey : this._typingKey(sourceOrKey);
640
+ const handle = this._typingTimers.get(key);
641
+ if (handle != null) {
642
+ this._clearInterval(handle);
643
+ this._typingTimers.delete(key);
644
+ }
645
+ // Best-effort clear of the indicator (channels where typing auto-expires
646
+ // can no-op _setTyping(false)).
647
+ if (typeof sourceOrKey === "object" && sourceOrKey) this._guardedTyping(sourceOrKey, false);
648
+ }
649
+
650
+ /** Stop every active typing heartbeat (called on stop()). */
651
+ stopAllTyping() {
652
+ for (const handle of this._typingTimers.values()) this._clearInterval(handle);
653
+ this._typingTimers.clear();
654
+ }
655
+
656
+ _typingKey(source) {
657
+ return `${source.channel || this.name}:${source.chatId || ""}:${source.threadRef || ""}`;
658
+ }
659
+
660
+ _guardedTyping(source, on) {
661
+ // Run _setTyping but never let it run longer than the guard window or
662
+ // reject into the interval; swallow + log.
663
+ let settled = false;
664
+ const timer = this._setTimeout(() => {
665
+ if (settled) return;
666
+ settled = true;
667
+ this.log("debug", "typing call timed out");
668
+ }, DEFAULT_TYPING_CALL_TIMEOUT_MS);
669
+ if (timer && typeof timer.unref === "function") timer.unref();
670
+ Promise.resolve()
671
+ .then(() => this._setTyping(source, on))
672
+ .catch((err) => this.log("debug", `_setTyping failed: ${err.message}`))
673
+ .finally(() => {
674
+ settled = true;
675
+ this._clearTimeout(timer);
676
+ });
677
+ }
678
+
679
+ // -- lifecycle ------------------------------------------------------------
680
+
681
+ /**
682
+ * Liveness for the daemon health sweep. Reflects the reconnect supervisor's
683
+ * state so a dead channel reports RED instead of the old hardcoded green
684
+ * (P0-6: "nothing anywhere calls healthCheck; dead channels report healthy").
685
+ * A channel that has given up reconnecting is unhealthy; one that is down but
686
+ * still retrying is degraded-but-not-yet-dead (reported ok:false too so the
687
+ * sweep notices). Unsupervised adapters keep the prior always-ok behaviour.
688
+ */
689
+ async healthCheck() {
690
+ if (!this._superviseEnabled || !this._supervisor) {
691
+ return { ok: true, detail: `${this.name} adapter` };
692
+ }
693
+ const state = this._connState;
694
+ const ok = state === CONN_STATE.UP;
695
+ return {
696
+ ok,
697
+ state,
698
+ detail: ok
699
+ ? `${this.name} adapter (${state})`
700
+ : `${this.name} adapter ${state === CONN_STATE.GIVING_UP ? "DEAD" : "degraded"} (${state})`,
701
+ };
702
+ }
703
+
704
+ async stop() {
705
+ this._stopping = true;
706
+ try { this._supervisor?.stop?.("adapter-stop"); } catch { /* */ }
707
+ this.stopAllTyping();
708
+ this._replyWindow.clear();
709
+ }
710
+ }
711
+
712
+ /** Coerce peerAgentIds (Set | array | undefined) to a Set<string>. */
713
+ function toPeerSet(v) {
714
+ if (v instanceof Set) return new Set([...v].map(String));
715
+ if (Array.isArray(v)) return new Set(v.map(String));
716
+ return new Set();
717
+ }
718
+
719
+ export default BaseAdapter;