@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,1045 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Slack Events API Server — Real-Time Event Receiver
4
+ *
5
+ * Receives real-time events from Slack via the bot's Event Subscriptions.
6
+ * Writes inbound events to state/inbox/slack/ in YAML format (matching poller output).
7
+ * Creates priority trigger files for CEO DMs.
8
+ *
9
+ * This replaces polling — Slack pushes events to us instantly.
10
+ *
11
+ * Events handled:
12
+ * - message (DM, channel messages, thread replies — all channel types)
13
+ * - app_mention (the agent @mentioned)
14
+ * - reaction_added (emoji reactions)
15
+ *
16
+ * Deduplication:
17
+ * - Tracks event_id in state/locks/slack-events/ to prevent duplicate processing
18
+ * - Also deduplicates against the poller's tracker in state/slack-thread-tracker/
19
+ *
20
+ * Usage: node scripts/slack-events-server.mjs
21
+ * Requires: SLACK_BOT_TOKEN (or SLACK_TOKEN), SLACK_SIGNING_SECRET in .env
22
+ * Port: 3100 (configurable via SLACK_EVENTS_PORT)
23
+ */
24
+
25
+ import http from "node:http";
26
+ import crypto from "node:crypto";
27
+ import fs from "node:fs";
28
+ import path from "node:path";
29
+ import { fileURLToPath } from "node:url";
30
+
31
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
32
+ const ROOT = path.resolve(__dirname, "..");
33
+
34
+ // ── Singleton guard — prevent duplicate instances ────────────────────────────
35
+ try {
36
+ process.env.AGENT_ROOT = process.env.AGENT_ROOT || ROOT;
37
+ const { acquireLock } = await import("../lib/singleton.js");
38
+ if (!acquireLock("slack-events")) {
39
+ console.log("[INIT] Slack events server already running — exiting");
40
+ process.exit(0);
41
+ }
42
+ } catch { /* maestro not available — skip guard */ }
43
+
44
+ // ── Load environment ──────────────────────────────────────────────────────────
45
+
46
+ const envPath = path.join(ROOT, ".env");
47
+ if (fs.existsSync(envPath)) {
48
+ const envContent = fs.readFileSync(envPath, "utf-8");
49
+ for (const line of envContent.split("\n")) {
50
+ const match = line.match(/^([A-Z_][A-Z0-9_]*)=(.*)$/);
51
+ if (match) {
52
+ let val = match[2].trim();
53
+ // Strip surrounding quotes
54
+ if (
55
+ (val.startsWith('"') && val.endsWith('"')) ||
56
+ (val.startsWith("'") && val.endsWith("'"))
57
+ ) {
58
+ val = val.slice(1, -1);
59
+ }
60
+ process.env[match[1]] = val;
61
+ }
62
+ }
63
+ }
64
+
65
+ // ── Constants ─────────────────────────────────────────────────────────────────
66
+
67
+ const PORT = parseInt(process.env.SLACK_EVENTS_PORT || "3100", 10);
68
+ const SIGNING_SECRET = process.env.SLACK_SIGNING_SECRET || "";
69
+
70
+ // Fail-closed: this endpoint accepts inbound Slack events and injects them into
71
+ // the inbox. Without the signing secret it cannot verify requests, which would
72
+ // let anyone on the internet forge inbound messages. Refuse to start rather
73
+ // than degrade to "skip verification". (Mirrors scripts/cloud-relay/index.mjs.)
74
+ if (!SIGNING_SECRET) {
75
+ console.error("FATAL: SLACK_SIGNING_SECRET env var is required (refusing to start fail-open)");
76
+ process.exit(78); // EX_CONFIG
77
+ }
78
+
79
+ const INBOX_DIR = path.join(ROOT, "state/inbox/slack");
80
+ const DEDUP_DIR = path.join(ROOT, "state/locks/slack-events");
81
+ const POLLER_TRACKER_DIR = path.join(ROOT, "state/slack-thread-tracker");
82
+ const TRIGGER_DIR = path.join(ROOT, "state/triggers/priority");
83
+ const LOG_DIR = path.join(ROOT, "logs/polling");
84
+
85
+ // Load the running agent's identity (canonical SOT) so we can filter
86
+ // self-messages and resolve the principal's user ID without hardcoding.
87
+ let _agent = null;
88
+ function loadAgent() {
89
+ if (_agent) return _agent;
90
+ try {
91
+ _agent = JSON.parse(fs.readFileSync(path.join(ROOT, "config/agent.json"), "utf-8"));
92
+ } catch {
93
+ _agent = { firstName: "Agent", slackMemberId: "", fullName: "Agent", principal: {} };
94
+ }
95
+ return _agent;
96
+ }
97
+
98
+ const AGENT_USER_ID = loadAgent().slackMemberId || "";
99
+ const PRINCIPAL_USER_ID = loadAgent().principal?.slackMemberId || "";
100
+ // DM channel between the principal and the agent — resolved at runtime from
101
+ // Slack API on first DM event. The hardcoded MEHRAN_DM_CHANNEL constant was
102
+ // removed; the server detects DM channels via the Slack channel ID prefix
103
+ // (channels starting with "D" are IM channels).
104
+
105
+ // Known users for name/privilege resolution — built from agent.json so each
106
+ // agent's principal gets `privilege: ceo` and the agent itself gets `system`.
107
+ const KNOWN_USERS = (() => {
108
+ const a = loadAgent();
109
+ const map = {};
110
+ if (a.principal?.slackMemberId) {
111
+ map[a.principal.slackMemberId] = {
112
+ name: (a.principal.fullName || "").toLowerCase().replace(/\s+/g, "-"),
113
+ privilege: "ceo",
114
+ };
115
+ }
116
+ if (a.slackMemberId) {
117
+ map[a.slackMemberId] = {
118
+ name: (a.fullName || "").toLowerCase().replace(/\s+/g, "-"),
119
+ privilege: "system",
120
+ };
121
+ }
122
+ return map;
123
+ })();
124
+
125
+ // Monitored channels (matches poller/slack-poller.mjs)
126
+ const CHANNEL_INFO = {
127
+ C0ARASMQE1W: { name: "ceo-office", priority: "critical" },
128
+ C099NT6NKK7: { name: "engineering", priority: "high" },
129
+ C097N5R8HUJ: { name: "all-company", priority: "high" },
130
+ C09916RV399: { name: "engineering-platform", priority: "high" },
131
+ C0AQEGXKYT0: { name: "regulatory-remediation", priority: "high" },
132
+ C0AQGK5TLNN: { name: "strategic-initiative", priority: "high" },
133
+ C0AQL7QL2G4: { name: "legal-compliance", priority: "high" },
134
+ C0996CRGF1S: { name: "company-app", priority: "normal" },
135
+ C0992UF9469: { name: "platform", priority: "normal" },
136
+ C0ARAU328JU: { name: "general", priority: "normal" },
137
+ C09CTAFUB09: { name: "product", priority: "normal" },
138
+ };
139
+
140
+ const ATTACHMENTS_DIR = path.join(ROOT, "state/inbox/attachments");
141
+
142
+ // Ensure attachments directory exists
143
+ fs.mkdirSync(ATTACHMENTS_DIR, { recursive: true });
144
+
145
+ /**
146
+ * Extract attachment metadata from Slack event files array.
147
+ * Also attempts to download files for local access by Claude Code sessions.
148
+ */
149
+ function extractAttachments(files) {
150
+ if (!files || !Array.isArray(files) || files.length === 0) return [];
151
+ const token = process.env.SLACK_BOT_TOKEN || process.env.SLACK_TOKEN || "";
152
+ return files.map((f) => {
153
+ const att = {
154
+ id: f.id || "",
155
+ name: f.name || "unnamed",
156
+ mimetype: f.mimetype || f.filetype || "unknown",
157
+ size: f.size || 0,
158
+ url_private: f.url_private || "",
159
+ local_path: null,
160
+ };
161
+ // Attempt synchronous-style download via fetch (non-blocking best-effort)
162
+ if (token && (f.url_private_download || f.url_private)) {
163
+ const safeId = (f.id || "unknown").replace(/[^a-zA-Z0-9_-]/g, "");
164
+ const safeName = (f.name || "file").replace(/[^a-zA-Z0-9._-]/g, "_");
165
+ const localPath = path.join(ATTACHMENTS_DIR, `${safeId}-${safeName}`);
166
+ att.local_path = localPath; // Set optimistically; download happens async
167
+ // Fire-and-forget async download
168
+ downloadAttachment(f.url_private_download || f.url_private, localPath, token);
169
+ }
170
+ return att;
171
+ });
172
+ }
173
+
174
+ /**
175
+ * Download a Slack attachment to local storage (async, fire-and-forget).
176
+ */
177
+ async function downloadAttachment(url, localPath, token) {
178
+ try {
179
+ if (fs.existsSync(localPath)) return; // Already downloaded
180
+ const res = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
181
+ if (!res.ok) {
182
+ log("WARN", `Attachment download failed: HTTP ${res.status}`, { url });
183
+ return;
184
+ }
185
+ const buffer = Buffer.from(await res.arrayBuffer());
186
+ fs.writeFileSync(localPath, buffer);
187
+ log("INFO", `Attachment downloaded: ${path.basename(localPath)}`, { size: buffer.length });
188
+ } catch (err) {
189
+ log("WARN", `Attachment download error: ${err.message}`, { url });
190
+ }
191
+ }
192
+
193
+ // ── Edit Deduplication ───────────────────────────────────────────────────────
194
+
195
+ /**
196
+ * Calculate word-level similarity between two strings (Jaccard-like).
197
+ * Returns 0.0 (completely different) to 1.0 (identical).
198
+ * Used to distinguish typo fixes from material edits.
199
+ */
200
+ function wordSimilarity(a, b) {
201
+ if (a === b) return 1.0;
202
+ if (!a && !b) return 1.0;
203
+ if (!a || !b) return 0.0;
204
+
205
+ const wordsA = a.toLowerCase().split(/\s+/).filter(Boolean);
206
+ const wordsB = b.toLowerCase().split(/\s+/).filter(Boolean);
207
+
208
+ if (wordsA.length === 0 && wordsB.length === 0) return 1.0;
209
+ if (wordsA.length === 0 || wordsB.length === 0) return 0.0;
210
+
211
+ const setA = new Set(wordsA);
212
+ const setB = new Set(wordsB);
213
+ const intersection = new Set([...setA].filter((w) => setB.has(w)));
214
+ const union = new Set([...setA, ...setB]);
215
+
216
+ return intersection.size / union.size;
217
+ }
218
+
219
+ /**
220
+ * Mark an existing inbox item as superseded by a message edit.
221
+ * Renames the YAML file with a .superseded suffix so the inbox processor
222
+ * skips it and processes only the new version.
223
+ */
224
+ function supersedeInboxItem(channelId, messageTs) {
225
+ try {
226
+ const tsFormatted = messageTs.replace(".", "-");
227
+ const files = fs.readdirSync(INBOX_DIR);
228
+ for (const file of files) {
229
+ if (file.includes(tsFormatted) && file.endsWith(".yaml") && !file.endsWith(".superseded")) {
230
+ const oldPath = path.join(INBOX_DIR, file);
231
+ const newPath = path.join(INBOX_DIR, file + ".superseded");
232
+ fs.renameSync(oldPath, newPath);
233
+ log("INFO", `Superseded inbox item: ${file}`, { channel: channelId, ts: messageTs });
234
+ }
235
+ }
236
+ } catch (err) {
237
+ log("WARN", `Failed to supersede inbox item: ${err.message}`, { channel: channelId, ts: messageTs });
238
+ }
239
+ }
240
+
241
+ // Dedup cache: keep event_ids in memory for fast lookup (last 10,000)
242
+ const eventIdCache = new Set();
243
+ const MAX_CACHE_SIZE = 10000;
244
+
245
+ // Stats for health endpoint
246
+ const stats = {
247
+ started_at: new Date().toISOString(),
248
+ events_received: 0,
249
+ events_processed: 0,
250
+ events_deduplicated: 0,
251
+ events_skipped_bot: 0,
252
+ last_event_at: null,
253
+ errors: 0,
254
+ };
255
+
256
+ // ── Ensure directories exist ──────────────────────────────────────────────────
257
+
258
+ for (const dir of [INBOX_DIR, DEDUP_DIR, TRIGGER_DIR, LOG_DIR]) {
259
+ fs.mkdirSync(dir, { recursive: true });
260
+ }
261
+
262
+ // ── Helpers ───────────────────────────────────────────────────────────────────
263
+
264
+ function resolveName(userId) {
265
+ return KNOWN_USERS[userId]?.name || userId;
266
+ }
267
+
268
+ function resolvePrivilege(userId) {
269
+ return KNOWN_USERS[userId]?.privilege || "unknown";
270
+ }
271
+
272
+ function resolveChannelName(channelId, channelType) {
273
+ if (CHANNEL_INFO[channelId]) {
274
+ return CHANNEL_INFO[channelId].name;
275
+ }
276
+ if (channelType === "im") {
277
+ return `dm/${channelId}`;
278
+ }
279
+ return channelId;
280
+ }
281
+
282
+ /**
283
+ * Verify Slack request signature (v0 HMAC-SHA256)
284
+ * Returns true if valid, false if invalid.
285
+ * Fails closed: a missing secret must never degrade to "skip verification".
286
+ * (The boot guard already refuses to start without it; this is defence in depth.)
287
+ */
288
+ function verifySlackSignature(timestamp, body, signature) {
289
+ if (!SIGNING_SECRET) return false;
290
+
291
+ // Reject requests older than 5 minutes (replay protection)
292
+ const now = Math.floor(Date.now() / 1000);
293
+ if (Math.abs(now - parseInt(timestamp, 10)) > 300) {
294
+ log("WARN", "Request timestamp too old, possible replay attack");
295
+ return false;
296
+ }
297
+
298
+ const sigBasestring = `v0:${timestamp}:${body}`;
299
+ const mySignature =
300
+ "v0=" +
301
+ crypto
302
+ .createHmac("sha256", SIGNING_SECRET)
303
+ .update(sigBasestring, "utf8")
304
+ .digest("hex");
305
+
306
+ try {
307
+ return crypto.timingSafeEqual(
308
+ Buffer.from(mySignature),
309
+ Buffer.from(signature || ""),
310
+ );
311
+ } catch {
312
+ return false;
313
+ }
314
+ }
315
+
316
+ /**
317
+ * Check if an event has already been processed.
318
+ * Uses both in-memory cache and filesystem dedup directory.
319
+ */
320
+ function isDuplicate(eventId) {
321
+ if (!eventId) return false;
322
+
323
+ // Check memory cache first
324
+ if (eventIdCache.has(eventId)) return true;
325
+
326
+ // Check filesystem
327
+ const dedupFile = path.join(DEDUP_DIR, eventId);
328
+ if (fs.existsSync(dedupFile)) {
329
+ eventIdCache.add(eventId);
330
+ return true;
331
+ }
332
+
333
+ return false;
334
+ }
335
+
336
+ /**
337
+ * Mark an event as processed for deduplication.
338
+ */
339
+ function markProcessed(eventId) {
340
+ if (!eventId) return;
341
+
342
+ // Add to memory cache
343
+ eventIdCache.add(eventId);
344
+
345
+ // Evict oldest entries if cache is too large
346
+ if (eventIdCache.size > MAX_CACHE_SIZE) {
347
+ const iterator = eventIdCache.values();
348
+ for (let i = 0; i < 1000; i++) {
349
+ eventIdCache.delete(iterator.next().value);
350
+ }
351
+ }
352
+
353
+ // Write to filesystem
354
+ try {
355
+ fs.writeFileSync(path.join(DEDUP_DIR, eventId), String(Date.now()));
356
+ } catch (err) {
357
+ log("ERROR", `Failed to write dedup file for ${eventId}: ${err.message}`);
358
+ }
359
+ }
360
+
361
+ /**
362
+ * Also mark in the poller's tracker so the comms-monitor.sh doesn't re-process.
363
+ */
364
+ function markInPollerTracker(messageTs) {
365
+ if (!messageTs) return;
366
+ try {
367
+ const trackerFile = path.join(POLLER_TRACKER_DIR, messageTs);
368
+ if (!fs.existsSync(trackerFile)) {
369
+ fs.mkdirSync(POLLER_TRACKER_DIR, { recursive: true });
370
+ fs.writeFileSync(trackerFile, messageTs);
371
+ }
372
+ } catch {
373
+ // Non-critical — poller will just reprocess
374
+ }
375
+ }
376
+
377
+ /**
378
+ * Write structured log entry
379
+ */
380
+ function log(level, message, data = {}) {
381
+ const entry = {
382
+ timestamp: new Date().toISOString(),
383
+ service: "slack-events-server",
384
+ level,
385
+ message,
386
+ ...data,
387
+ };
388
+ const line = JSON.stringify(entry);
389
+ console.log(`[${level}] ${message}`);
390
+
391
+ // Append to daily log file
392
+ try {
393
+ const today = new Date().toISOString().split("T")[0];
394
+ const logFile = path.join(LOG_DIR, `${today}-slack-events.jsonl`);
395
+ fs.appendFileSync(logFile, line + "\n");
396
+ } catch {
397
+ // Logging failure should not break event processing
398
+ }
399
+ }
400
+
401
+ // ── Inbox Writers ─────────────────────────────────────────────────────────────
402
+
403
+ /**
404
+ * Write an inbox item in YAML format — matching the poller's output exactly.
405
+ * This ensures the inbox processor handles events from both sources identically.
406
+ */
407
+ function writeInboxYaml(item) {
408
+ const ts = (item.timestamp || new Date().toISOString()).replace(/[:.]/g, "-");
409
+ const filename = `${ts}-${item.id}.yaml`;
410
+ const filepath = path.join(INBOX_DIR, filename);
411
+
412
+ const content = (item.content || "").replace(/\n/g, "\n ");
413
+ let yaml = `id: "${item.id}"
414
+ service: "${item.service}"
415
+ channel: "${item.channel}"
416
+ sender: "${item.sender}"
417
+ sender_privilege: "${item.sender_privilege}"
418
+ timestamp: "${item.timestamp}"
419
+ subject: "${item.subject || ""}"
420
+ content: |
421
+ ${content}
422
+ thread_id: "${item.thread_id || ""}"
423
+ is_reply: ${item.is_reply || false}
424
+ is_voice_transcript: false
425
+ priority_signals:
426
+ from_ceo: ${item.priority_signals?.from_ceo || false}
427
+ tagged_urgent: ${item.priority_signals?.tagged_urgent || false}
428
+ contains_deadline: ${item.priority_signals?.contains_deadline || false}
429
+ mentions_agent: ${item.priority_signals?.mentions_agent || false}
430
+ raw_ref: "${item.raw_ref || ""}"
431
+ source: "events-api"
432
+ `;
433
+
434
+ // Add attachments if present
435
+ if (item.attachments && item.attachments.length > 0) {
436
+ yaml += `attachments:\n`;
437
+ for (const att of item.attachments) {
438
+ yaml += ` - id: "${att.id}"\n`;
439
+ yaml += ` name: "${att.name}"\n`;
440
+ yaml += ` mimetype: "${att.mimetype}"\n`;
441
+ yaml += ` size: ${att.size}\n`;
442
+ if (att.local_path) {
443
+ yaml += ` local_path: "${att.local_path}"\n`;
444
+ }
445
+ if (att.url_private) {
446
+ yaml += ` url_private: "${att.url_private}"\n`;
447
+ }
448
+ }
449
+ }
450
+
451
+ fs.writeFileSync(filepath, yaml);
452
+ log("INFO", `Inbox item written: ${filename}`, {
453
+ event_type: item._eventType,
454
+ sender: item.sender,
455
+ channel: item.channel,
456
+ });
457
+ return filepath;
458
+ }
459
+
460
+ /**
461
+ * Write a priority trigger file for CEO DMs and urgent items.
462
+ * This triggers immediate session pickup by the inbox processor.
463
+ */
464
+ function writePriorityTrigger(event, reason) {
465
+ const ts = Date.now();
466
+ const triggerFile = path.join(TRIGGER_DIR, `ceo-dm-${ts}.yaml`);
467
+ const content = (event.text || "").replace(/\n/g, "\n ");
468
+ const triggerContent = `type: priority_trigger
469
+ reason: "${reason}"
470
+ source: "slack:${event.channel}:${event.ts}"
471
+ sender: "${resolveName(event.user)}"
472
+ channel: "${resolveChannelName(event.channel, event.channel_type)}"
473
+ timestamp: "${new Date().toISOString()}"
474
+ detected_by: slack-events-server
475
+ content: |
476
+ ${content}
477
+ `;
478
+ fs.writeFileSync(triggerFile, triggerContent);
479
+ log("INFO", `Priority trigger written: ceo-dm-${ts}.yaml`, { reason });
480
+ return triggerFile;
481
+ }
482
+
483
+ // ── Event Handlers ────────────────────────────────────────────────────────────
484
+
485
+ /**
486
+ * Process a message event (DM, channel, thread reply — any channel type).
487
+ */
488
+ function handleMessageEvent(event) {
489
+ const userId = event.user || "";
490
+ // Skip the agent's own messages — defensive check (outer dispatch also filters,
491
+ // but message_changed normalization can bypass it)
492
+ if (userId === AGENT_USER_ID) return;
493
+
494
+ const msgText = event.text || "";
495
+ const channelId = event.channel || "";
496
+ const channelType = event.channel_type || "";
497
+ const isThread = !!event.thread_ts && event.thread_ts !== event.ts;
498
+ const isDM = channelType === "im";
499
+ const isPrincipal = userId === PRINCIPAL_USER_ID;
500
+ // The principal's DM channel is whichever IM channel they happen to use to
501
+ // contact the agent — Slack assigns DM channel IDs dynamically. Detect
502
+ // principal-DM context from `isPrincipal && isDM` rather than a hardcode.
503
+ const isPrincipalDmChannel = isPrincipal && isDM;
504
+ const agentFirstName = (loadAgent().firstName || "").toLowerCase();
505
+ const mentionsAgent =
506
+ (agentFirstName && msgText.toLowerCase().includes(agentFirstName)) ||
507
+ (AGENT_USER_ID && msgText.includes(`<@${AGENT_USER_ID}>`));
508
+ const isUrgent = /\b(urgent|emergency|asap|blocker|critical)\b/i.test(
509
+ msgText,
510
+ );
511
+
512
+ // Determine channel name and subject
513
+ let channelName, subject;
514
+ if (isDM) {
515
+ channelName = `dm/${resolveName(userId)}`;
516
+ subject = `DM from ${resolveName(userId)}`;
517
+ } else {
518
+ channelName = resolveChannelName(channelId, channelType);
519
+ subject = `#${channelName}`;
520
+ if (isThread) {
521
+ subject += " (thread reply)";
522
+ }
523
+ }
524
+
525
+ log("INFO", `Message event`, {
526
+ from: resolveName(userId),
527
+ channel: channelName,
528
+ isDM,
529
+ isThread,
530
+ isPrincipal,
531
+ mentionsAgent,
532
+ isUrgent,
533
+ });
534
+
535
+ // Extract attachment metadata from event.files (Slack Events API includes these)
536
+ const attachments = extractAttachments(event.files);
537
+
538
+ // Build inbox item matching poller format
539
+ const msgTs = event.ts || "";
540
+ const inboxItem = {
541
+ id: msgTs.replace(".", "-"),
542
+ service: "slack",
543
+ channel: channelName,
544
+ sender: resolveName(userId),
545
+ sender_privilege: resolvePrivilege(userId),
546
+ timestamp: new Date(parseFloat(msgTs) * 1000).toISOString(),
547
+ subject,
548
+ content: msgText,
549
+ thread_id: event.thread_ts || "",
550
+ is_reply: isThread,
551
+ attachments: attachments.length > 0 ? attachments : undefined,
552
+ priority_signals: {
553
+ from_ceo: isPrincipal,
554
+ tagged_urgent: isUrgent,
555
+ contains_deadline: false,
556
+ mentions_agent: mentionsAgent || isDM,
557
+ },
558
+ raw_ref: `slack:${channelId}:${msgTs}`,
559
+ _eventType: isThread ? "thread_reply" : isDM ? "dm" : "channel_message",
560
+ };
561
+
562
+ writeInboxYaml(inboxItem);
563
+
564
+ // Mark in poller tracker to prevent comms-monitor.sh from re-processing
565
+ markInPollerTracker(msgTs);
566
+
567
+ // Write priority trigger for CEO DMs, @mentions, and urgent messages
568
+ if (isPrincipal && (isDM || isPrincipalDmChannel)) {
569
+ writePriorityTrigger(event, "CEO DM via Events API");
570
+ } else if (isPrincipal && !isDM) {
571
+ writePriorityTrigger(event, "CEO channel message via Events API");
572
+ } else if (mentionsAgent) {
573
+ writePriorityTrigger(event, "Agent @mentioned via Events API");
574
+ } else if (isUrgent) {
575
+ writePriorityTrigger(event, "Urgent message via Events API");
576
+ }
577
+ }
578
+
579
+ /**
580
+ * Process an app_mention event (@-mention of the running agent).
581
+ */
582
+ function handleMentionEvent(event) {
583
+ const msgText = event.text || "";
584
+ const userId = event.user || "";
585
+ const channelId = event.channel || "";
586
+ const isPrincipal = userId === PRINCIPAL_USER_ID;
587
+ const isUrgent = /\b(urgent|emergency|asap|blocker|critical)\b/i.test(
588
+ msgText,
589
+ );
590
+ const channelName = resolveChannelName(channelId, "channel");
591
+ const msgTs = event.ts || "";
592
+
593
+ log("INFO", `App mention event`, {
594
+ from: resolveName(userId),
595
+ channel: channelName,
596
+ });
597
+
598
+ const inboxItem = {
599
+ id: msgTs.replace(".", "-"),
600
+ service: "slack",
601
+ channel: channelName,
602
+ sender: resolveName(userId),
603
+ sender_privilege: resolvePrivilege(userId),
604
+ timestamp: new Date(parseFloat(msgTs) * 1000).toISOString(),
605
+ subject: `@${loadAgent().firstName || "Agent"} in #${channelName}`,
606
+ content: msgText,
607
+ thread_id: event.thread_ts || "",
608
+ is_reply: !!event.thread_ts && event.thread_ts !== event.ts,
609
+ priority_signals: {
610
+ from_ceo: isPrincipal,
611
+ tagged_urgent: isUrgent,
612
+ contains_deadline: false,
613
+ mentions_agent: true,
614
+ },
615
+ raw_ref: `slack:${channelId}:${msgTs}`,
616
+ _eventType: "mention",
617
+ };
618
+
619
+ writeInboxYaml(inboxItem);
620
+ markInPollerTracker(msgTs);
621
+
622
+ // Mentions always get a priority trigger
623
+ writePriorityTrigger(
624
+ event,
625
+ isPrincipal
626
+ ? "CEO @mention via Events API"
627
+ : "Agent @mentioned via Events API",
628
+ );
629
+ }
630
+
631
+ /**
632
+ * Process a reaction_added event.
633
+ */
634
+ function handleReactionEvent(event) {
635
+ const userId = event.user || "";
636
+ const reaction = event.reaction || "";
637
+ const itemTs = event.item?.ts || "";
638
+ const itemChannel = event.item?.channel || "";
639
+ const isPrincipal = userId === PRINCIPAL_USER_ID;
640
+ const channelName = resolveChannelName(itemChannel, "channel");
641
+
642
+ log("INFO", `Reaction event: :${reaction}: from ${resolveName(userId)}`, {
643
+ channel: channelName,
644
+ on_message: itemTs,
645
+ });
646
+
647
+ // Write reaction as inbox item — reactions from CEO are priority signals
648
+ const inboxItem = {
649
+ id: `${itemTs}-${reaction}-${userId}`.replace(/\./g, "-"),
650
+ service: "slack",
651
+ channel: channelName,
652
+ sender: resolveName(userId),
653
+ sender_privilege: resolvePrivilege(userId),
654
+ timestamp: new Date().toISOString(),
655
+ subject: `:${reaction}: on message in #${channelName}`,
656
+ content: `Reaction :${reaction}: added by ${resolveName(userId)} on message ${itemTs}`,
657
+ thread_id: "",
658
+ is_reply: false,
659
+ priority_signals: {
660
+ from_ceo: isPrincipal,
661
+ tagged_urgent: false,
662
+ contains_deadline: false,
663
+ mentions_agent: false,
664
+ },
665
+ raw_ref: `slack:${itemChannel}:${itemTs}:reaction:${reaction}`,
666
+ _eventType: "reaction",
667
+ };
668
+
669
+ writeInboxYaml(inboxItem);
670
+ }
671
+
672
+ // ── Huddle Event Handler ──────────────────────────────────────────────────────
673
+
674
+ /**
675
+ * Handle user_huddle_changed events — forwarded to the huddle server
676
+ * for real-time huddle detection and auto-join.
677
+ */
678
+ function handleHuddleEvent(event) {
679
+ const userId = event.user || "";
680
+ const huddleState = event.huddle_state || "";
681
+
682
+ // Don't react to the agent's own huddle changes
683
+ if (userId === AGENT_USER_ID) return;
684
+
685
+ const userName = resolveName(userId);
686
+
687
+ log("INFO", `Huddle event: ${userName} → ${huddleState}`, {
688
+ user: userId,
689
+ huddle_state: huddleState,
690
+ });
691
+
692
+ // Forward to huddle server's event listener
693
+ const huddleServerUrl = `http://127.0.0.1:${process.env.HUDDLE_EVENTS_PORT || 3200}/huddle-event`;
694
+
695
+ const payload = JSON.stringify({
696
+ user: userId,
697
+ user_name: userName,
698
+ huddle_state: huddleState,
699
+ // user_huddle_changed doesn't include channel info directly,
700
+ // but we can derive it from context if available
701
+ channel: event.channel || null,
702
+ channel_type: event.channel_type || null,
703
+ timestamp: new Date().toISOString(),
704
+ });
705
+
706
+ // Fire-and-forget POST to huddle server
707
+ fetch(huddleServerUrl, {
708
+ method: "POST",
709
+ headers: { "Content-Type": "application/json" },
710
+ body: payload,
711
+ }).catch((err) => {
712
+ // Huddle server may not be running — that's fine
713
+ log("DEBUG", `Huddle server unreachable: ${err.message}`);
714
+ });
715
+ }
716
+
717
+ // ── Main Event Router ─────────────────────────────────────────────────────────
718
+
719
+ /**
720
+ * Route an incoming Slack event to the appropriate handler.
721
+ */
722
+ function handleEvent(payload) {
723
+ const event = payload.event;
724
+ if (!event) return;
725
+
726
+ const eventId = payload.event_id;
727
+
728
+ // Deduplication check
729
+ if (isDuplicate(eventId)) {
730
+ stats.events_deduplicated++;
731
+ log("DEBUG", `Duplicate event skipped: ${eventId}`);
732
+ return;
733
+ }
734
+
735
+ stats.events_received++;
736
+ stats.last_event_at = new Date().toISOString();
737
+
738
+ // Skip bot messages (including the agent's own messages)
739
+ if (event.bot_id || event.subtype === "bot_message") {
740
+ stats.events_skipped_bot++;
741
+ log("DEBUG", `Bot message skipped`, {
742
+ bot_id: event.bot_id,
743
+ subtype: event.subtype,
744
+ });
745
+ markProcessed(eventId);
746
+ return;
747
+ }
748
+
749
+ // Skip the agent's own messages
750
+ if (event.user === AGENT_USER_ID) {
751
+ stats.events_skipped_bot++;
752
+ markProcessed(eventId);
753
+ return;
754
+ }
755
+
756
+ // Handle message_changed: Slack sends both old and new text.
757
+ // Compare to determine if the edit is material (rewrite) or immaterial (typo fix).
758
+ if (event.subtype === "message_changed" && event.message) {
759
+ const oldText = event.previous_message?.text || "";
760
+ const newText = event.message.text || "";
761
+ const similarity = wordSimilarity(oldText, newText);
762
+
763
+ if (similarity >= 0.90) {
764
+ // Immaterial edit (typo fix, formatting) — skip reprocessing
765
+ stats.events_deduplicated++;
766
+ log("INFO", `Immaterial edit skipped (${(similarity * 100).toFixed(0)}% similar)`, {
767
+ channel: event.channel,
768
+ ts: event.message.ts,
769
+ });
770
+ markProcessed(eventId);
771
+ return;
772
+ }
773
+
774
+ // Material edit — supersede the old inbox item and process the new version
775
+ log("INFO", `Material edit detected (${(similarity * 100).toFixed(0)}% similar)`, {
776
+ channel: event.channel,
777
+ ts: event.message.ts,
778
+ });
779
+ supersedeInboxItem(event.channel, event.message.ts);
780
+
781
+ // Normalize event shape for the message handler
782
+ event.text = newText;
783
+ event.user = event.message.user || event.user;
784
+ event.ts = event.message.ts || event.ts;
785
+ event.thread_ts = event.message.thread_ts || event.thread_ts;
786
+ event.files = event.message.files || event.files;
787
+ event._is_edit = true;
788
+
789
+ // Re-check after normalization: the original event.user for
790
+ // message_changed is often empty/system; the real author is in
791
+ // event.message.user. Without this, the agent's own edited messages
792
+ // bypass the earlier AGENT_USER_ID filter and trigger self-replies.
793
+ if (event.user === AGENT_USER_ID) {
794
+ stats.events_skipped_bot++;
795
+ markProcessed(eventId);
796
+ return;
797
+ }
798
+ // Fall through to message handler with updated content
799
+ }
800
+
801
+ // Skip message subtypes that are not actual user messages
802
+ // (channel_join, channel_leave, channel_topic, etc.)
803
+ const ignoredSubtypes = new Set([
804
+ "channel_join",
805
+ "channel_leave",
806
+ "channel_topic",
807
+ "channel_purpose",
808
+ "channel_name",
809
+ "channel_archive",
810
+ "channel_unarchive",
811
+ "bot_add",
812
+ "bot_remove",
813
+ "pinned_item",
814
+ "unpinned_item",
815
+ ]);
816
+ if (event.subtype && ignoredSubtypes.has(event.subtype)) {
817
+ markProcessed(eventId);
818
+ return;
819
+ }
820
+
821
+ // Route by event type
822
+ try {
823
+ switch (event.type) {
824
+ case "message":
825
+ handleMessageEvent(event);
826
+ break;
827
+ case "app_mention":
828
+ handleMentionEvent(event);
829
+ break;
830
+ case "reaction_added":
831
+ handleReactionEvent(event);
832
+ break;
833
+ case "user_huddle_changed":
834
+ handleHuddleEvent(event);
835
+ break;
836
+ default:
837
+ log("WARN", `Unhandled event type: ${event.type}`);
838
+ }
839
+ stats.events_processed++;
840
+ } catch (err) {
841
+ stats.errors++;
842
+ log("ERROR", `Failed to process event: ${err.message}`, {
843
+ event_type: event.type,
844
+ event_id: eventId,
845
+ stack: err.stack,
846
+ });
847
+ }
848
+
849
+ // Mark as processed after handling
850
+ markProcessed(eventId);
851
+ }
852
+
853
+ // ── Dedup Cleanup (runs hourly) ───────────────────────────────────────────────
854
+
855
+ /**
856
+ * Clean up old dedup files (older than 24 hours) to prevent disk bloat.
857
+ */
858
+ function cleanupDedupFiles() {
859
+ try {
860
+ const files = fs.readdirSync(DEDUP_DIR);
861
+ const cutoff = Date.now() - 24 * 60 * 60 * 1000; // 24 hours ago
862
+ let cleaned = 0;
863
+
864
+ for (const file of files) {
865
+ const filepath = path.join(DEDUP_DIR, file);
866
+ try {
867
+ const content = fs.readFileSync(filepath, "utf-8").trim();
868
+ const ts = parseInt(content, 10);
869
+ if (!isNaN(ts) && ts < cutoff) {
870
+ fs.unlinkSync(filepath);
871
+ cleaned++;
872
+ }
873
+ } catch {
874
+ // If we can't read it, check mtime instead
875
+ try {
876
+ const stat = fs.statSync(filepath);
877
+ if (stat.mtimeMs < cutoff) {
878
+ fs.unlinkSync(filepath);
879
+ cleaned++;
880
+ }
881
+ } catch {
882
+ // Skip
883
+ }
884
+ }
885
+ }
886
+
887
+ if (cleaned > 0) {
888
+ log("INFO", `Dedup cleanup: removed ${cleaned} old entries`);
889
+ }
890
+ } catch (err) {
891
+ log("ERROR", `Dedup cleanup failed: ${err.message}`);
892
+ }
893
+ }
894
+
895
+ // Run cleanup every hour
896
+ setInterval(cleanupDedupFiles, 60 * 60 * 1000);
897
+
898
+ // ── HTTP Server ───────────────────────────────────────────────────────────────
899
+
900
+ const server = http.createServer((req, res) => {
901
+ // Health check endpoint
902
+ if (req.method === "GET" && req.url === "/health") {
903
+ res.writeHead(200, { "Content-Type": "application/json" });
904
+ res.end(
905
+ JSON.stringify({
906
+ status: "ok",
907
+ service: "slack-events-server",
908
+ port: PORT,
909
+ uptime: process.uptime(),
910
+ ...stats,
911
+ }),
912
+ );
913
+ return;
914
+ }
915
+
916
+ // Slack events endpoint
917
+ if (req.method === "POST" && req.url === "/slack/events") {
918
+ let body = "";
919
+ req.on("data", (chunk) => {
920
+ body += chunk;
921
+ });
922
+ req.on("end", () => {
923
+ // Check for Slack retry header — if this is a retry and we already
924
+ // processed it, acknowledge immediately
925
+ const retryNum = req.headers["x-slack-retry-num"];
926
+ const retryReason = req.headers["x-slack-retry-reason"];
927
+ if (retryNum) {
928
+ log("WARN", `Slack retry #${retryNum} (reason: ${retryReason})`);
929
+ // If Slack is retrying because we were slow, just ack
930
+ // The original event was likely already processed
931
+ if (retryReason === "http_timeout") {
932
+ res.writeHead(200, { "X-Slack-No-Retry": "1" });
933
+ res.end("ok");
934
+ return;
935
+ }
936
+ }
937
+
938
+ // Verify Slack signature — always (SIGNING_SECRET is guaranteed set by
939
+ // the boot guard; verification never gets skipped).
940
+ const timestamp = req.headers["x-slack-request-timestamp"];
941
+ const signature = req.headers["x-slack-signature"];
942
+ if (!verifySlackSignature(timestamp, body, signature)) {
943
+ log("ERROR", "Invalid Slack signature — rejecting request");
944
+ stats.errors++;
945
+ res.writeHead(401);
946
+ res.end("Invalid signature");
947
+ return;
948
+ }
949
+
950
+ let payload;
951
+ try {
952
+ payload = JSON.parse(body);
953
+ } catch {
954
+ res.writeHead(400);
955
+ res.end("Bad JSON");
956
+ return;
957
+ }
958
+
959
+ // URL verification challenge (one-time setup when registering the endpoint)
960
+ if (payload.type === "url_verification") {
961
+ log("INFO", "URL verification challenge received");
962
+ res.writeHead(200, { "Content-Type": "application/json" });
963
+ res.end(JSON.stringify({ challenge: payload.challenge }));
964
+ return;
965
+ }
966
+
967
+ // Event callback — respond 200 immediately, then process async
968
+ // Slack requires response within 3 seconds or it retries
969
+ res.writeHead(200, { "X-Slack-No-Retry": "1" });
970
+ res.end("ok");
971
+
972
+ // Process event asynchronously (after we've already sent 200)
973
+ if (payload.type === "event_callback") {
974
+ // Use setImmediate to ensure the response is sent before processing
975
+ setImmediate(() => {
976
+ try {
977
+ handleEvent(payload);
978
+ } catch (err) {
979
+ stats.errors++;
980
+ log("ERROR", `Unhandled error processing event: ${err.message}`, {
981
+ stack: err.stack,
982
+ });
983
+ }
984
+ });
985
+ }
986
+ });
987
+ return;
988
+ }
989
+
990
+ // Any other route
991
+ res.writeHead(404, { "Content-Type": "application/json" });
992
+ res.end(
993
+ JSON.stringify({
994
+ error: "Not found",
995
+ endpoints: ["/slack/events", "/health"],
996
+ }),
997
+ );
998
+ });
999
+
1000
+ // ── Startup ───────────────────────────────────────────────────────────────────
1001
+
1002
+ server.listen(PORT, () => {
1003
+ log("INFO", `Slack Events Server started`, { port: PORT });
1004
+ console.log(`[SLACK-EVENTS] Listening on port ${PORT}`);
1005
+ console.log(
1006
+ `[SLACK-EVENTS] Webhook URL: http://localhost:${PORT}/slack/events`,
1007
+ );
1008
+ console.log(`[SLACK-EVENTS] Health: http://localhost:${PORT}/health`);
1009
+ console.log(`[SLACK-EVENTS] Dedup dir: ${DEDUP_DIR}`);
1010
+ console.log(`[SLACK-EVENTS] Inbox dir: ${INBOX_DIR}`);
1011
+ console.log(`[SLACK-EVENTS] Trigger dir: ${TRIGGER_DIR}`);
1012
+ });
1013
+
1014
+ // ── Graceful Shutdown ─────────────────────────────────────────────────────────
1015
+
1016
+ function shutdown(signal) {
1017
+ log("INFO", `Received ${signal}, shutting down gracefully...`);
1018
+ server.close(() => {
1019
+ log("INFO", "Server closed");
1020
+ process.exit(0);
1021
+ });
1022
+ // Force exit after 5 seconds if server hasn't closed
1023
+ setTimeout(() => {
1024
+ log("WARN", "Forced shutdown after timeout");
1025
+ process.exit(1);
1026
+ }, 5000);
1027
+ }
1028
+
1029
+ process.on("SIGTERM", () => shutdown("SIGTERM"));
1030
+ process.on("SIGINT", () => shutdown("SIGINT"));
1031
+
1032
+ // Prevent unhandled rejections from crashing the server
1033
+ process.on("unhandledRejection", (err) => {
1034
+ stats.errors++;
1035
+ log("ERROR", `Unhandled rejection: ${err.message || err}`, {
1036
+ stack: err.stack,
1037
+ });
1038
+ });
1039
+
1040
+ process.on("uncaughtException", (err) => {
1041
+ stats.errors++;
1042
+ log("ERROR", `Uncaught exception: ${err.message}`, { stack: err.stack });
1043
+ // For uncaught exceptions, exit after logging — let launchd restart us
1044
+ setTimeout(() => process.exit(1), 1000);
1045
+ });