@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,917 @@
1
+ #!/usr/bin/env node
2
+ // =============================================================================
3
+ // Slack Socket Mode Listener — realtime DM + @mention ingestion
4
+ // =============================================================================
5
+ //
6
+ // Replaces the 60s `slack-poller.mjs` and the 5s Railway webhook relay polling
7
+ // for DM / @mention ingestion. Holds a persistent WebSocket to Slack's
8
+ // Socket Mode endpoint and translates inbound message events to the same
9
+ // inbox-item shape that `slack-poller.mjs` produces, then hands them to
10
+ // `writeInboxItem` from `utils.mjs` (preserving the existing contract).
11
+ //
12
+ // Why Socket Mode over polling
13
+ // ----------------------------
14
+ // 1. Realtime — events arrive in <500ms instead of the 60s poll cycle.
15
+ // 2. No rate-limit pressure — one persistent socket vs. ~25 API calls/cycle.
16
+ // 3. No public webhook endpoint required — Slack pushes via outbound WSS.
17
+ // 4. Aligns with Slack's recommended pattern for self-hosted agents.
18
+ //
19
+ // Requirements
20
+ // ------------
21
+ // - SLACK_APP_LEVEL_TOKEN (xapp-…) with `connections:write` scope
22
+ // - SLACK_BOT_TOKEN (xoxb-…) for the optional API enrichment fallbacks
23
+ // - Socket Mode enabled in the Slack app config
24
+ // - Event subscriptions enabled with: message.channels, message.groups,
25
+ // message.im, message.mpim, app_mention
26
+ //
27
+ // Run: node scripts/poller/slack-socket-mode.mjs
28
+ // Install: launchd plist with KeepAlive (SuccessfulExit:false, Crashed:true).
29
+ //
30
+ // Module exports a programmable surface (createSocketModeClient,
31
+ // eventToInboxItem, etc.) so the test file can exercise translation +
32
+ // reconnect logic without touching the network. The CLI invocation
33
+ // (process.argv[1]) starts the daemon directly.
34
+ // =============================================================================
35
+
36
+ import { existsSync, readFileSync, mkdirSync, appendFileSync } from "node:fs";
37
+ import { dirname, join, resolve } from "node:path";
38
+ import { fileURLToPath } from "node:url";
39
+
40
+ import { writeInboxItem, resolvePrivilege, resolveName, AGENT_REPO_DIR } from "./utils.mjs";
41
+ import { handleSlashCommand, postSlashResponse } from "./lib/slash-command-handlers.mjs";
42
+
43
+ /**
44
+ * Process a slash_commands envelope: invoke the handler, then POST the
45
+ * formatted response back to Slack via the payload's response_url. Errors are
46
+ * caught and logged — Slack times out at 3s, so we keep the work short.
47
+ */
48
+ async function handleSlashCommandEnvelope(envelope, agentRoot, identity) {
49
+ const payload = envelope.payload;
50
+ const result = await handleSlashCommand(payload, { agentRoot, identity });
51
+ if (!result) {
52
+ // Unknown command — let Slack show its default "command not configured"
53
+ // error to the user. Don't POST to response_url at all.
54
+ return;
55
+ }
56
+ await postSlashResponse(payload.response_url, result);
57
+ }
58
+
59
+ // ---------------------------------------------------------------------------
60
+ // Constants
61
+ // ---------------------------------------------------------------------------
62
+
63
+ const __filename = fileURLToPath(import.meta.url);
64
+ const __dirname = dirname(__filename);
65
+
66
+ // Slack endpoint for opening a Socket Mode WSS connection.
67
+ const APPS_CONNECTIONS_OPEN_URL = "https://slack.com/api/apps.connections.open";
68
+
69
+ // Backoff defaults — exponential with cap. The first retry waits 1s, then
70
+ // doubles each failure up to 60s. Reset on a clean `hello`.
71
+ export const RECONNECT_INITIAL_MS = 1_000;
72
+ export const RECONNECT_MAX_MS = 60_000;
73
+
74
+ // How often we check .emergency-stop and refresh keep-alive state. Slack
75
+ // sends `disconnect` (type: "disconnect") ~30 min into a session asking
76
+ // us to reconnect; we just close + reconnect on the next tick.
77
+ export const SUPERVISOR_INTERVAL_MS = 5_000;
78
+
79
+ // Slack's recommended ping interval. The WS server sends `pings`; we
80
+ // respond with `pong`. Most node ws implementations handle this for us,
81
+ // but we also enqueue a heartbeat to surface stalls.
82
+ export const HEARTBEAT_INTERVAL_MS = 30_000;
83
+ export const HEARTBEAT_TIMEOUT_MS = 90_000;
84
+
85
+ // ---------------------------------------------------------------------------
86
+ // Env loader (mirrors slack-events-server.mjs — no dotenv dep)
87
+ // ---------------------------------------------------------------------------
88
+
89
+ /**
90
+ * Read .env from the agent root and merge into process.env (existing keys
91
+ * win — we don't clobber values the operator deliberately set in their
92
+ * shell). No-op when .env is missing.
93
+ */
94
+ export function loadEnvFromFile(agentRoot) {
95
+ const envPath = join(agentRoot, ".env");
96
+ if (!existsSync(envPath)) return;
97
+ const body = readFileSync(envPath, "utf-8");
98
+ for (const line of body.split("\n")) {
99
+ const m = line.match(/^([A-Z_][A-Z0-9_]*)=(.*)$/);
100
+ if (!m) continue;
101
+ let val = m[2].trim();
102
+ if ((val.startsWith('"') && val.endsWith('"')) ||
103
+ (val.startsWith("'") && val.endsWith("'"))) {
104
+ val = val.slice(1, -1);
105
+ }
106
+ if (process.env[m[1]] === undefined || process.env[m[1]] === "") {
107
+ process.env[m[1]] = val;
108
+ }
109
+ }
110
+ }
111
+
112
+ // ---------------------------------------------------------------------------
113
+ // Logging — JSONL to logs/polling/<date>-slack-socket.jsonl
114
+ // ---------------------------------------------------------------------------
115
+
116
+ /**
117
+ * Append a structured log line. Best-effort; logging failures must never
118
+ * take the listener down. The log path mirrors the existing poller layout
119
+ * so downstream tooling (doctor, log rotation) keeps working unchanged.
120
+ */
121
+ export function logLine(agentRoot, entry) {
122
+ try {
123
+ const dir = join(agentRoot, "logs", "polling");
124
+ mkdirSync(dir, { recursive: true });
125
+ const today = new Date().toISOString().slice(0, 10);
126
+ const file = join(dir, `${today}-slack-socket.jsonl`);
127
+ const line = JSON.stringify({ ts: new Date().toISOString(), ...entry }) + "\n";
128
+ appendFileSync(file, line);
129
+ } catch {
130
+ /* logging must never propagate */
131
+ }
132
+ }
133
+
134
+ function info(agentRoot, message, data = {}) {
135
+ console.log(`[slack-socket] ${message}`);
136
+ logLine(agentRoot, { level: "info", message, ...data });
137
+ }
138
+
139
+ function warn(agentRoot, message, data = {}) {
140
+ console.warn(`[slack-socket] ${message}`);
141
+ logLine(agentRoot, { level: "warn", message, ...data });
142
+ }
143
+
144
+ function error(agentRoot, message, data = {}) {
145
+ console.error(`[slack-socket] ${message}`);
146
+ logLine(agentRoot, { level: "error", message, ...data });
147
+ }
148
+
149
+ // ---------------------------------------------------------------------------
150
+ // Agent identity + known-agents registry
151
+ // ---------------------------------------------------------------------------
152
+
153
+ /**
154
+ * Load `config/agent.json` and return a canonical identity object with safe
155
+ * defaults so callers never need to null-check.
156
+ */
157
+ export function loadAgentIdentity(agentRoot) {
158
+ try {
159
+ const agent = JSON.parse(readFileSync(join(agentRoot, "config/agent.json"), "utf-8"));
160
+ return {
161
+ firstName: agent.firstName || "Agent",
162
+ fullName: agent.fullName || "Agent",
163
+ slackMemberId: agent.slackMemberId || "",
164
+ principalSlackId: agent.principal?.slackMemberId || "",
165
+ };
166
+ } catch {
167
+ return { firstName: "Agent", fullName: "Agent", slackMemberId: "", principalSlackId: "" };
168
+ }
169
+ }
170
+
171
+ /**
172
+ * Load peer agent Slack IDs from `config/known-agents.json`. Excludes the
173
+ * running agent itself. Used by the filter that drops messages from other
174
+ * agents — so e.g. Robin's Slack traffic doesn't trigger Ravi's daemon.
175
+ *
176
+ * Reads only from the agent's own config — each agent maintains its own
177
+ * peer registry (shipped by `npx @cohortapp/agent-sdk upgrade`).
178
+ */
179
+ export function loadPeerSlackIds(agentRoot, ownSlackId) {
180
+ const candidates = [
181
+ join(agentRoot, "config/known-agents.json"),
182
+ ];
183
+ for (const path of candidates) {
184
+ try {
185
+ const raw = readFileSync(path, "utf-8");
186
+ const reg = JSON.parse(raw);
187
+ const ids = new Set();
188
+ for (const a of reg.agents || []) {
189
+ if (a?.slackId && a.slackId !== ownSlackId) ids.add(a.slackId);
190
+ }
191
+ if (ids.size > 0) return ids;
192
+ } catch {
193
+ /* try next */
194
+ }
195
+ }
196
+ return new Set();
197
+ }
198
+
199
+ // ---------------------------------------------------------------------------
200
+ // Slack envelope filtering — DM, @mention, thread reply
201
+ // ---------------------------------------------------------------------------
202
+
203
+ /**
204
+ * Decide whether a Slack event envelope is interesting to this agent.
205
+ *
206
+ * Returns { keep: boolean, reason: string }. The reason exists so the
207
+ * test suite (and ops triage) can assert exactly *why* an event was
208
+ * dropped or kept. Keep this function pure — no I/O, no globals beyond
209
+ * the identity/peers arguments.
210
+ */
211
+ export function shouldKeepEvent(envelope, { ownSlackId, peerSlackIds, reactions, ccChannels } = {}) {
212
+ if (!envelope || typeof envelope !== "object") {
213
+ return { keep: false, reason: "envelope-missing" };
214
+ }
215
+ if (envelope.type !== "events_api") {
216
+ return { keep: false, reason: `envelope-type-${envelope.type}` };
217
+ }
218
+ const event = envelope.payload?.event;
219
+ if (!event) return { keep: false, reason: "no-event" };
220
+
221
+ // WS2 gap (a): reaction events. Opt-in via `reactions` so the historical
222
+ // default (drop reactions) is preserved for callers that don't enable it.
223
+ // Drop the agent's own reactions + peer-agent reactions (same loop-guard).
224
+ if (event.type === "reaction_added" || event.type === "reaction_removed") {
225
+ if (!reactions) return { keep: false, reason: `event-type-${event.type}` };
226
+ const reactor = event.user || "";
227
+ if (ownSlackId && reactor === ownSlackId) return { keep: false, reason: "self-reaction" };
228
+ if (peerSlackIds && peerSlackIds.has(reactor)) return { keep: false, reason: "peer-agent" };
229
+ return { keep: true, reason: "reaction" };
230
+ }
231
+
232
+ // Only message events. app_mention events come as `type: app_mention`
233
+ // but Slack also delivers a parallel `type: message` for the same text,
234
+ // so we normalise on the message handler and detect mentions inline.
235
+ // Allow `thread_broadcast` (a thread reply also broadcast to channel)
236
+ // through as a normal message; drop everything else.
237
+ if (event.type !== "message") return { keep: false, reason: `event-type-${event.type}` };
238
+ if (event.subtype && event.subtype !== "thread_broadcast" && event.subtype !== "file_share") {
239
+ return { keep: false, reason: `event-subtype-${event.subtype}` };
240
+ }
241
+
242
+ // Drop bot messages and the agent's own messages.
243
+ if (event.bot_id) return { keep: false, reason: "bot-message" };
244
+ if (event.user && ownSlackId && event.user === ownSlackId) {
245
+ return { keep: false, reason: "self-message" };
246
+ }
247
+
248
+ // Drop messages from peer agents — they're not customers and replying
249
+ // would loop two daemons forever.
250
+ if (event.user && peerSlackIds && peerSlackIds.has(event.user)) {
251
+ return { keep: false, reason: "peer-agent" };
252
+ }
253
+
254
+ // DM channels start with "D"; keep all of them.
255
+ const isDm = typeof event.channel === "string" && event.channel.startsWith("D");
256
+ if (isDm) return { keep: true, reason: "dm" };
257
+
258
+ // Channel messages — only keep if the agent is @-mentioned.
259
+ const text = typeof event.text === "string" ? event.text : "";
260
+ const mentionPattern = ownSlackId ? new RegExp(`<@${ownSlackId}(?:\\|[^>]*)?>`) : null;
261
+ if (mentionPattern && mentionPattern.test(text)) {
262
+ return { keep: true, reason: "mention" };
263
+ }
264
+
265
+ // WS2 gap (c): opt-in CC channels. A non-@mention message in a watched
266
+ // channel is kept as a "channel_cc" so the agent can observe (not reply).
267
+ // Routed to observe-not-reply by the downstream directed-gate.
268
+ if (ccChannels && typeof event.channel === "string" && ccChannels.has(event.channel)) {
269
+ return { keep: true, reason: "channel_cc" };
270
+ }
271
+
272
+ return { keep: false, reason: "no-mention-no-dm" };
273
+ }
274
+
275
+ // ---------------------------------------------------------------------------
276
+ // Translation — Slack envelope → poller-compatible inbox item
277
+ // ---------------------------------------------------------------------------
278
+
279
+ /**
280
+ * Convert a Slack Socket Mode envelope to the same inbox-item shape that
281
+ * `slack-poller.mjs` produces. Pure function; all I/O happens at the caller.
282
+ *
283
+ * The output schema mirrors `slack-poller.mjs` exactly so `writeInboxItem`
284
+ * + the downstream daemon pipeline keep working without changes:
285
+ * - `id` = ts with the dot replaced by a dash (matches poller convention)
286
+ * - `service` = "slack"
287
+ * - `channel` = friendly channel label ("dm/<sender-name>" for DMs,
288
+ * channel id for channels — channel name resolution requires an API
289
+ * call we'd rather avoid on the hot path; the daemon downstream can
290
+ * fill in if needed)
291
+ * - `priority_signals.mentions_agent` = true for DMs (1:1 context), true
292
+ * when the regex matched, false otherwise.
293
+ */
294
+ export function eventToInboxItem(envelope, { ownSlackId, principalSlackId, agentFirstName, threadContext, ccChannels } = {}) {
295
+ const event = envelope.payload.event;
296
+
297
+ // WS2 gap (a): reaction events have a different shape (item.ts is the
298
+ // *target* message). Produce a reaction-kinded item. `threadContext` (when
299
+ // the adapter resolved the target message text via conversations.history)
300
+ // becomes the content so the classifier has something to reason over.
301
+ if (event.type === "reaction_added" || event.type === "reaction_removed") {
302
+ const reactor = event.user || "";
303
+ const targetTs = event.item?.ts || "";
304
+ const channelId = event.item?.channel || "";
305
+ const isDm = typeof channelId === "string" && channelId.startsWith("D");
306
+ const senderName = resolveName(reactor);
307
+ const channelLabel = isDm ? `dm/${senderName}` : channelId;
308
+ const verb = event.type === "reaction_added" ? "added" : "removed";
309
+ const targetText = typeof threadContext === "string" ? threadContext : "";
310
+ return {
311
+ id: `${targetTs.replace(".", "-")}-reaction-${reactor}`,
312
+ service: "slack",
313
+ channel: channelLabel,
314
+ channel_id: channelId,
315
+ sender: senderName,
316
+ sender_privilege: resolvePrivilege(reactor),
317
+ timestamp: new Date((event.event_ts ? parseFloat(event.event_ts) : Date.now() / 1000) * 1000).toISOString(),
318
+ subject: `:${event.reaction}: ${verb} on a message`,
319
+ content: targetText ? `[${senderName} ${verb} :${event.reaction}:]\n${targetText}` : `${senderName} ${verb} :${event.reaction}:`,
320
+ thread_id: "",
321
+ thread_context: targetText || null,
322
+ is_reply: false,
323
+ priority_signals: {
324
+ from_ceo: !!principalSlackId && reactor === principalSlackId,
325
+ tagged_urgent: false,
326
+ contains_deadline: false,
327
+ mentions_agent: isDm,
328
+ },
329
+ raw_ref: `slack:${channelId}:${targetTs}:reaction`,
330
+ source: "socket-mode",
331
+ kind: "reaction",
332
+ };
333
+ }
334
+
335
+ const userId = event.user || "";
336
+ const text = event.text || "";
337
+ const ts = event.ts || "";
338
+ const channelId = event.channel || "";
339
+ const threadTs = event.thread_ts || "";
340
+ const isReply = !!threadTs && threadTs !== ts;
341
+ const isDm = typeof channelId === "string" && channelId.startsWith("D");
342
+
343
+ // Friendly labels. We can't resolve a channel name without an API call
344
+ // we'd rather not make per event; the agent's downstream tooling already
345
+ // handles channel-id → name resolution where needed.
346
+ const senderName = resolveName(userId);
347
+ const channelLabel = isDm ? `dm/${senderName}` : channelId;
348
+ const subject = isDm ? `DM from ${senderName}` : `#${channelLabel}`;
349
+
350
+ // Priority signal detection mirrors slack-poller.mjs.
351
+ const isCeo = !!principalSlackId && userId === principalSlackId;
352
+ const isUrgent = /\b(urgent|emergency|asap|blocker|critical)\b/i.test(text);
353
+ const mentionPattern = ownSlackId ? new RegExp(`<@${ownSlackId}(?:\\|[^>]*)?>`) : null;
354
+ const explicitlyMentioned = mentionPattern ? mentionPattern.test(text) : false;
355
+ const nameMention = !!agentFirstName && text.toLowerCase().includes(agentFirstName.toLowerCase());
356
+ const mentionsAgent = isDm || explicitlyMentioned || nameMention;
357
+
358
+ // WS2 gap (c): non-@mention message in a watched CC channel → channel_cc.
359
+ const isCc = !isDm && !explicitlyMentioned && !nameMention &&
360
+ ccChannels && typeof channelId === "string" && ccChannels.has(channelId);
361
+
362
+ const item = {
363
+ id: ts.replace(".", "-"),
364
+ service: "slack",
365
+ channel: channelLabel,
366
+ channel_id: channelId,
367
+ sender: senderName,
368
+ sender_privilege: resolvePrivilege(userId),
369
+ timestamp: new Date(parseFloat(ts) * 1000).toISOString(),
370
+ subject,
371
+ content: text,
372
+ thread_id: threadTs || "",
373
+ // WS2 gap (b): thread replies get their prior-thread history filled by the
374
+ // adapter (conversations.replies). Default null preserves the legacy
375
+ // Socket Mode behaviour (no thread history) byte-for-byte.
376
+ thread_context: typeof threadContext === "string" ? threadContext : null,
377
+ is_reply: isReply,
378
+ priority_signals: {
379
+ from_ceo: isCeo,
380
+ tagged_urgent: isUrgent,
381
+ contains_deadline: false,
382
+ mentions_agent: mentionsAgent,
383
+ },
384
+ raw_ref: `slack:${channelId}:${ts}`,
385
+ source: "socket-mode",
386
+ };
387
+ if (isCc) item.kind = "channel_cc";
388
+ return item;
389
+ }
390
+
391
+ // ---------------------------------------------------------------------------
392
+ // Slack `apps.connections.open` — get a fresh WSS URL
393
+ // ---------------------------------------------------------------------------
394
+
395
+ /**
396
+ * Call apps.connections.open with the app-level token to obtain a WSS URL.
397
+ * Returns `{ url }` on success or `{ error }` on failure. The URL is
398
+ * single-use and expires quickly — call this every time we open a socket.
399
+ */
400
+ export async function openSocketConnection({ appToken, fetchFn = fetch }) {
401
+ if (!appToken) {
402
+ return { error: "missing-app-token" };
403
+ }
404
+ let response;
405
+ try {
406
+ response = await fetchFn(APPS_CONNECTIONS_OPEN_URL, {
407
+ method: "POST",
408
+ headers: {
409
+ Authorization: `Bearer ${appToken}`,
410
+ "Content-Type": "application/x-www-form-urlencoded",
411
+ },
412
+ });
413
+ } catch (err) {
414
+ return { error: `network: ${err.message}` };
415
+ }
416
+ let body;
417
+ try {
418
+ body = await response.json();
419
+ } catch (err) {
420
+ return { error: `parse: ${err.message}` };
421
+ }
422
+ if (!body.ok) {
423
+ return { error: body.error || "unknown-error" };
424
+ }
425
+ if (!body.url) {
426
+ return { error: "no-url-in-response" };
427
+ }
428
+ return { url: body.url };
429
+ }
430
+
431
+ // ---------------------------------------------------------------------------
432
+ // Reconnect supervisor — exponential back-off with jitter
433
+ // ---------------------------------------------------------------------------
434
+
435
+ /**
436
+ * Compute the next reconnect delay using exponential back-off plus a small
437
+ * (0-25%) random jitter to avoid thundering-herd if multiple agents
438
+ * reconnect simultaneously.
439
+ */
440
+ export function nextBackoffMs(currentMs, max = RECONNECT_MAX_MS) {
441
+ const doubled = Math.min(max, Math.max(RECONNECT_INITIAL_MS, currentMs * 2));
442
+ const jitter = Math.floor(Math.random() * 0.25 * doubled);
443
+ return Math.min(max, doubled + jitter);
444
+ }
445
+
446
+ // ---------------------------------------------------------------------------
447
+ // Socket Mode client — the main orchestration loop
448
+ // ---------------------------------------------------------------------------
449
+
450
+ /**
451
+ * Create a Socket Mode client. The client is *programmable*: every external
452
+ * dependency (fetch, WebSocket constructor, clock, write-inbox-item) is
453
+ * injectable so the test suite can run end-to-end without ever touching
454
+ * the network or the file system.
455
+ *
456
+ * @param {object} opts
457
+ * @param {string} opts.agentRoot
458
+ * @param {string} opts.appToken xapp-… app-level token
459
+ * @param {object} opts.identity loadAgentIdentity() result
460
+ * @param {Set<string>} opts.peerSlackIds
461
+ * @param {Function} [opts.fetchFn] defaults to global fetch
462
+ * @param {Function} [opts.WebSocketCtor] defaults to global WebSocket
463
+ * @param {Function} [opts.writeInbox] defaults to writeInboxItem
464
+ * @param {Function} [opts.now] defaults to Date.now
465
+ * @param {Function} [opts.setTimeout] defaults to globalThis.setTimeout
466
+ * @param {Function} [opts.clearTimeout] defaults to globalThis.clearTimeout
467
+ * @param {boolean} [opts.reactions] WS2: keep reaction_added/removed events
468
+ * @param {Set<string>} [opts.ccChannels] WS2: channel ids to observe as channel_cc
469
+ * @param {(envelope:object)=>Promise<{threadContext?:string}>|undefined} [opts.enrich]
470
+ * WS2: async hook called for kept events before translation; returns
471
+ * e.g. `{ threadContext }` resolved via conversations.replies/history.
472
+ */
473
+ export function createSocketModeClient(opts) {
474
+ const {
475
+ agentRoot,
476
+ appToken,
477
+ identity,
478
+ peerSlackIds,
479
+ fetchFn = fetch,
480
+ WebSocketCtor = globalThis.WebSocket,
481
+ writeInbox = writeInboxItem,
482
+ now = Date.now,
483
+ setTimeout: setTimeoutFn = globalThis.setTimeout,
484
+ clearTimeout: clearTimeoutFn = globalThis.clearTimeout,
485
+ reactions = false,
486
+ ccChannels = null,
487
+ enrich = null,
488
+ } = opts;
489
+
490
+ if (!WebSocketCtor) {
491
+ throw new Error(
492
+ "Socket Mode requires a WebSocket implementation. Use Node 22+ (global WebSocket) or pass WebSocketCtor.",
493
+ );
494
+ }
495
+
496
+ const state = {
497
+ socket: null,
498
+ backoffMs: RECONNECT_INITIAL_MS,
499
+ stopped: false,
500
+ reconnectTimer: null,
501
+ heartbeatTimer: null,
502
+ lastPingAt: 0,
503
+ stats: {
504
+ connects: 0,
505
+ reconnects: 0,
506
+ eventsReceived: 0,
507
+ eventsKept: 0,
508
+ eventsDropped: 0,
509
+ acksSent: 0,
510
+ lastDisconnectReason: null,
511
+ },
512
+ };
513
+
514
+ // -------------------------------------------------------------------------
515
+ // ack — Slack requires every envelope to be acked back over the same socket.
516
+ // -------------------------------------------------------------------------
517
+ function ack(envelopeId, ws) {
518
+ if (!envelopeId || !ws || ws.readyState !== 1 /* WebSocket.OPEN */) return;
519
+ try {
520
+ ws.send(JSON.stringify({ envelope_id: envelopeId }));
521
+ state.stats.acksSent++;
522
+ } catch (err) {
523
+ warn(agentRoot, `ack send failed: ${err.message}`, { envelopeId });
524
+ }
525
+ }
526
+
527
+ // -------------------------------------------------------------------------
528
+ // handleEnvelope — translate + persist via writeInbox.
529
+ // -------------------------------------------------------------------------
530
+ function handleEnvelope(envelope, ws) {
531
+ state.stats.eventsReceived++;
532
+
533
+ // Slack sends multiple envelope types over the same socket: hello,
534
+ // disconnect, events_api, slash_commands, interactive, etc. We only
535
+ // care about the events_api stream for inbox ingestion.
536
+ if (envelope.type === "hello") {
537
+ info(agentRoot, "hello — connected to Slack Socket Mode", {
538
+ num_connections: envelope.num_connections,
539
+ debug_info: envelope.debug_info,
540
+ });
541
+ state.backoffMs = RECONNECT_INITIAL_MS; // reset on a clean hello
542
+ return;
543
+ }
544
+ if (envelope.type === "disconnect") {
545
+ info(agentRoot, "disconnect requested by Slack — will reconnect", {
546
+ reason: envelope.reason,
547
+ });
548
+ state.stats.lastDisconnectReason = envelope.reason || "slack-requested";
549
+ // Slack always wants an ack on this, but most importantly we should
550
+ // close + reconnect to pick up the new server-side endpoint.
551
+ try { ws.close(); } catch { /* */ }
552
+ return;
553
+ }
554
+
555
+ // Every other envelope type needs an ack — do this BEFORE any
556
+ // potentially-slow processing so Slack doesn't re-deliver.
557
+ if (envelope.envelope_id) ack(envelope.envelope_id, ws);
558
+
559
+ // Slash commands take a different path: they don't go to the inbox,
560
+ // they get dispatched directly to a handler that responds via the
561
+ // payload's response_url. This is how /voice (Slack-initiated outbound
562
+ // call) works — our practical equivalent of huddle integration since
563
+ // Slack doesn't expose a programmatic huddle-join API.
564
+ if (envelope.type === "slash_commands" && envelope.payload?.command) {
565
+ handleSlashCommandEnvelope(envelope, agentRoot, identity).catch((err) => {
566
+ warn(agentRoot, `slash command handler failed: ${err.message}`, {
567
+ command: envelope.payload?.command,
568
+ envelopeId: envelope.envelope_id,
569
+ });
570
+ });
571
+ return;
572
+ }
573
+
574
+ const decision = shouldKeepEvent(envelope, {
575
+ ownSlackId: identity.slackMemberId,
576
+ peerSlackIds,
577
+ reactions,
578
+ ccChannels,
579
+ });
580
+ if (!decision.keep) {
581
+ state.stats.eventsDropped++;
582
+ logLine(agentRoot, { level: "debug", message: "event-dropped", reason: decision.reason });
583
+ return;
584
+ }
585
+
586
+ // WS2: when an enrich hook is configured (the channel adapter wires one to
587
+ // resolve thread_context / reaction target text via the Slack Web API), run
588
+ // the async path. Without a hook we take the historical *synchronous* path
589
+ // so byte-for-byte behaviour and the existing test timing are preserved.
590
+ if (typeof enrich === "function") {
591
+ Promise.resolve()
592
+ .then(() => enrich(envelope))
593
+ .catch((err) => {
594
+ warn(agentRoot, `enrich failed (fail-open): ${err.message}`);
595
+ return {};
596
+ })
597
+ .then((extra) => translateAndWrite(envelope, extra || {}));
598
+ return;
599
+ }
600
+ translateAndWrite(envelope, {});
601
+ }
602
+
603
+ function translateAndWrite(envelope, extra) {
604
+ let item;
605
+ try {
606
+ item = eventToInboxItem(envelope, {
607
+ ownSlackId: identity.slackMemberId,
608
+ principalSlackId: identity.principalSlackId,
609
+ agentFirstName: identity.firstName,
610
+ ccChannels,
611
+ threadContext: extra && typeof extra.threadContext === "string" ? extra.threadContext : undefined,
612
+ });
613
+ } catch (err) {
614
+ error(agentRoot, `translation failed: ${err.message}`, { stack: err.stack });
615
+ return;
616
+ }
617
+
618
+ try {
619
+ writeInbox("slack", item);
620
+ state.stats.eventsKept++;
621
+ info(agentRoot, "inbox-item-written", {
622
+ id: item.id,
623
+ channel: item.channel,
624
+ sender: item.sender,
625
+ is_reply: item.is_reply,
626
+ kind: item.kind || "message",
627
+ mentions_agent: item.priority_signals.mentions_agent,
628
+ });
629
+ } catch (err) {
630
+ error(agentRoot, `writeInbox failed: ${err.message}`, { stack: err.stack, id: item.id });
631
+ }
632
+ }
633
+
634
+ // -------------------------------------------------------------------------
635
+ // connect — single attempt; on close, schedule a reconnect.
636
+ // -------------------------------------------------------------------------
637
+ async function connect() {
638
+ if (state.stopped) return;
639
+ if (existsSync(join(agentRoot, ".emergency-stop"))) {
640
+ info(agentRoot, ".emergency-stop present — refusing to connect");
641
+ return;
642
+ }
643
+
644
+ const open = await openSocketConnection({ appToken, fetchFn });
645
+ if (open.error) {
646
+ state.stats.lastDisconnectReason = `open-failed:${open.error}`;
647
+ error(agentRoot, `apps.connections.open failed: ${open.error}`);
648
+ scheduleReconnect();
649
+ return;
650
+ }
651
+ info(agentRoot, "opening WebSocket", { url_prefix: open.url.slice(0, 64) });
652
+
653
+ let ws;
654
+ try {
655
+ ws = new WebSocketCtor(open.url);
656
+ } catch (err) {
657
+ error(agentRoot, `WebSocket construction failed: ${err.message}`);
658
+ scheduleReconnect();
659
+ return;
660
+ }
661
+ state.socket = ws;
662
+ state.stats.connects++;
663
+ if (state.stats.connects > 1) state.stats.reconnects++;
664
+
665
+ // Both event-emitter and addEventListener APIs work on global WebSocket;
666
+ // use addEventListener so a `ws` polyfill from a future test injection
667
+ // doesn't bite us.
668
+ const onOpen = () => {
669
+ state.lastPingAt = now();
670
+ };
671
+ const onMessage = (event) => {
672
+ const raw = typeof event === "string" ? event : (event?.data ?? "");
673
+ if (!raw) return;
674
+ let envelope;
675
+ try {
676
+ envelope = JSON.parse(typeof raw === "string" ? raw : raw.toString());
677
+ } catch (err) {
678
+ warn(agentRoot, `non-JSON frame: ${err.message}`, { raw_snippet: String(raw).slice(0, 200) });
679
+ return;
680
+ }
681
+ try {
682
+ handleEnvelope(envelope, ws);
683
+ } catch (err) {
684
+ error(agentRoot, `handleEnvelope threw: ${err.message}`, { stack: err.stack });
685
+ }
686
+ };
687
+ const onError = (event) => {
688
+ warn(agentRoot, `socket error: ${event?.message || "unknown"}`);
689
+ };
690
+ const onClose = (event) => {
691
+ const code = event?.code ?? 0;
692
+ const reason = event?.reason ?? "";
693
+ info(agentRoot, "socket closed", { code, reason });
694
+ state.socket = null;
695
+ scheduleReconnect();
696
+ };
697
+
698
+ ws.addEventListener?.("open", onOpen);
699
+ ws.addEventListener?.("message", onMessage);
700
+ ws.addEventListener?.("error", onError);
701
+ ws.addEventListener?.("close", onClose);
702
+ // The `ws` npm package uses Node's EventEmitter; addEventListener may
703
+ // be missing. Fall back to `on` if present.
704
+ if (typeof ws.addEventListener !== "function" && typeof ws.on === "function") {
705
+ ws.on("open", onOpen);
706
+ ws.on("message", (data) => onMessage({ data }));
707
+ ws.on("error", (err) => onError({ message: err?.message }));
708
+ ws.on("close", (code, reason) => onClose({ code, reason: String(reason ?? "") }));
709
+ }
710
+ }
711
+
712
+ // -------------------------------------------------------------------------
713
+ // scheduleReconnect — exponential back-off; resets on each hello.
714
+ // -------------------------------------------------------------------------
715
+ function scheduleReconnect() {
716
+ if (state.stopped) return;
717
+ if (state.reconnectTimer) return; // already scheduled
718
+ state.backoffMs = nextBackoffMs(state.backoffMs);
719
+ info(agentRoot, `reconnect in ${state.backoffMs}ms`, { backoff_ms: state.backoffMs });
720
+ state.reconnectTimer = setTimeoutFn(() => {
721
+ state.reconnectTimer = null;
722
+ connect().catch((err) => {
723
+ error(agentRoot, `connect rejected: ${err.message}`, { stack: err.stack });
724
+ scheduleReconnect();
725
+ });
726
+ }, state.backoffMs);
727
+ if (state.reconnectTimer && typeof state.reconnectTimer.unref === "function") {
728
+ state.reconnectTimer.unref();
729
+ }
730
+ }
731
+
732
+ // -------------------------------------------------------------------------
733
+ // supervisor — checks .emergency-stop, prunes stale sockets.
734
+ //
735
+ // Timers are unref()-ed so they don't pin the Node event loop alive on
736
+ // test exit. In production the WebSocket itself holds the loop open,
737
+ // and the supervisor's job is purely to detect .emergency-stop while
738
+ // the listener is otherwise idle.
739
+ // -------------------------------------------------------------------------
740
+ function startSupervisor() {
741
+ state.heartbeatTimer = setTimeoutFn(function tick() {
742
+ if (state.stopped) return;
743
+ // Emergency-stop short-circuit. Exit 0 so KeepAlive(SuccessfulExit:false)
744
+ // refuses to restart us.
745
+ if (existsSync(join(agentRoot, ".emergency-stop"))) {
746
+ info(agentRoot, ".emergency-stop detected — exiting 0");
747
+ stop();
748
+ process.exit(0);
749
+ }
750
+ state.heartbeatTimer = setTimeoutFn(tick, SUPERVISOR_INTERVAL_MS);
751
+ if (state.heartbeatTimer && typeof state.heartbeatTimer.unref === "function") {
752
+ state.heartbeatTimer.unref();
753
+ }
754
+ }, SUPERVISOR_INTERVAL_MS);
755
+ if (state.heartbeatTimer && typeof state.heartbeatTimer.unref === "function") {
756
+ state.heartbeatTimer.unref();
757
+ }
758
+ }
759
+
760
+ // -------------------------------------------------------------------------
761
+ // start / stop lifecycle
762
+ // -------------------------------------------------------------------------
763
+ async function start() {
764
+ state.stopped = false;
765
+ startSupervisor();
766
+ await connect();
767
+ }
768
+
769
+ function stop() {
770
+ state.stopped = true;
771
+ if (state.reconnectTimer) {
772
+ clearTimeoutFn(state.reconnectTimer);
773
+ state.reconnectTimer = null;
774
+ }
775
+ if (state.heartbeatTimer) {
776
+ clearTimeoutFn(state.heartbeatTimer);
777
+ state.heartbeatTimer = null;
778
+ }
779
+ if (state.socket) {
780
+ try { state.socket.close(); } catch { /* */ }
781
+ state.socket = null;
782
+ }
783
+ }
784
+
785
+ function getStats() {
786
+ return { ...state.stats };
787
+ }
788
+
789
+ function getState() {
790
+ return {
791
+ connected: !!state.socket && state.socket.readyState === 1,
792
+ backoffMs: state.backoffMs,
793
+ stopped: state.stopped,
794
+ };
795
+ }
796
+
797
+ return { start, stop, getStats, getState, _internals: { handleEnvelope, scheduleReconnect, state } };
798
+ }
799
+
800
+ // ---------------------------------------------------------------------------
801
+ // config/slack.yaml enhancements (single-owner rule, H2)
802
+ // ---------------------------------------------------------------------------
803
+
804
+ /**
805
+ * Resolve the reactions / cc-channels / enrich opts this standalone listener
806
+ * should pass to createSocketModeClient, from config/slack.yaml. This is the
807
+ * mechanism by which enabling config/slack.yaml ENHANCES the single Slack
808
+ * connection (rather than the daemon spawning a second one — which it no longer
809
+ * does, see maestro-daemon.mjs). Exported so the wiring is unit-testable.
810
+ *
811
+ * The enrich hook reuses SlackAdapter's blessed Web-API enrichment via a
812
+ * no-socket adapter instance (constructing it opens no connection — the socket
813
+ * only opens in _connect, which we never call). Dynamic import avoids an
814
+ * eval-time cycle (adapter.mjs imports from this module). Fail-open: any error
815
+ * yields the historical no-enrich defaults so the listener still runs.
816
+ *
817
+ * @returns {Promise<{reactions:boolean, ccChannels:Set<string>|null, enrich:Function|null}>}
818
+ */
819
+ export async function resolveSocketModeEnhancements(agentRoot, identity, { botToken = process.env.SLACK_BOT_TOKEN || "" } = {}) {
820
+ let slackConfig = {};
821
+ let enrich = null;
822
+ try {
823
+ const adapterMod = await import("../../lib/channels/slack/adapter.mjs");
824
+ slackConfig = adapterMod.loadSlackConfig(agentRoot) || {};
825
+ const enrichAdapter = adapterMod.createSlackChannel({ agentRoot, botToken, slackConfig });
826
+ enrichAdapter._identity = identity;
827
+ enrich = (envelope) => enrichAdapter._enrich(envelope);
828
+ } catch (err) {
829
+ warn(agentRoot, `slack.yaml enrichment unavailable (fail-open): ${err.message}`);
830
+ }
831
+ const reactions = slackConfig.reactions !== false; // default on, mirrors the adapter
832
+ const ccChannels = Array.isArray(slackConfig.cc_channels) && slackConfig.cc_channels.length
833
+ ? new Set(slackConfig.cc_channels)
834
+ : null;
835
+ return { reactions, ccChannels, enrich };
836
+ }
837
+
838
+ // ---------------------------------------------------------------------------
839
+ // CLI entry point
840
+ // ---------------------------------------------------------------------------
841
+
842
+ /**
843
+ * Boot the daemon when invoked as a script (not when imported by tests).
844
+ */
845
+ async function main() {
846
+ const agentRoot = process.env.AGENT_ROOT || process.env.AGENT_DIR || AGENT_REPO_DIR || resolve(__dirname, "..", "..");
847
+
848
+ // Emergency-stop check before anything else (mirror maestro-daemon.mjs).
849
+ if (existsSync(join(agentRoot, ".emergency-stop"))) {
850
+ console.error("[slack-socket] .emergency-stop flag present — refusing to start.");
851
+ console.error("[slack-socket] Lift with: scripts/resume-operations.sh");
852
+ // Hold for a moment so KeepAlive's throttle has something to throttle.
853
+ await new Promise((r) => setTimeout(r, 30_000));
854
+ process.exit(0);
855
+ }
856
+
857
+ loadEnvFromFile(agentRoot);
858
+
859
+ const appToken = process.env.SLACK_APP_LEVEL_TOKEN || "";
860
+ if (!appToken || !appToken.startsWith("xapp-")) {
861
+ error(agentRoot, "SLACK_APP_LEVEL_TOKEN missing or malformed (must start with xapp-). " +
862
+ "Run: node scripts/setup/init-slack-socket-mode.mjs");
863
+ process.exit(78); // EX_CONFIG
864
+ }
865
+
866
+ const identity = loadAgentIdentity(agentRoot);
867
+ const peerSlackIds = loadPeerSlackIds(agentRoot, identity.slackMemberId);
868
+
869
+ // SINGLE-OWNER RULE (H2): this standalone process is the ONE owner of the
870
+ // Slack real-time connection (the maestro daemon's generic channel loop
871
+ // deliberately skips Slack — see maestro-daemon.mjs). Enabling config/slack.yaml
872
+ // must therefore ENHANCE this single connection (reactions / thread context /
873
+ // cc channels), never spawn a second one — so we feed reactions/ccChannels/
874
+ // enrich straight into the one client.
875
+ const { reactions, ccChannels, enrich } = await resolveSocketModeEnhancements(agentRoot, identity);
876
+
877
+ info(agentRoot, "starting Socket Mode listener", {
878
+ agent: identity.firstName,
879
+ slack_id: identity.slackMemberId || "(unset)",
880
+ peers: peerSlackIds.size,
881
+ reactions,
882
+ cc_channels: ccChannels ? ccChannels.size : 0,
883
+ });
884
+
885
+ const client = createSocketModeClient({ agentRoot, appToken, identity, peerSlackIds, reactions, ccChannels, enrich });
886
+ const shutdown = (signal) => {
887
+ info(agentRoot, `received ${signal}, shutting down`);
888
+ client.stop();
889
+ setTimeout(() => process.exit(0), 250);
890
+ };
891
+ process.on("SIGTERM", () => shutdown("SIGTERM"));
892
+ process.on("SIGINT", () => shutdown("SIGINT"));
893
+ process.on("unhandledRejection", (reason) => {
894
+ error(agentRoot, `unhandled rejection: ${reason?.message || reason}`, {
895
+ stack: reason?.stack,
896
+ });
897
+ });
898
+ process.on("uncaughtException", (err) => {
899
+ error(agentRoot, `uncaught exception: ${err.message}`, { stack: err.stack });
900
+ // Let launchd restart us — non-zero exit triggers KeepAlive.Crashed.
901
+ setTimeout(() => process.exit(1), 250);
902
+ });
903
+
904
+ await client.start();
905
+ }
906
+
907
+ // Only run main() when this file is executed directly (not when imported
908
+ // as a module by the test suite). The `process.argv[1]` check matches the
909
+ // pattern used elsewhere in the framework.
910
+ const isDirectInvocation = process.argv[1] && resolve(process.argv[1]) === __filename;
911
+ if (isDirectInvocation) {
912
+ main().catch((err) => {
913
+ console.error(`[slack-socket] fatal: ${err.message}`);
914
+ console.error(err.stack);
915
+ process.exit(1);
916
+ });
917
+ }