@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,164 @@
1
+ /**
2
+ * Maestro — Channel registry
3
+ *
4
+ * Lightweight in-process registry for channel plugins. Each channel
5
+ * registers itself via `registerChannel(name, factory, capabilities)`
6
+ * and the daemon can later iterate over registered channels to:
7
+ * - start their inbound listeners
8
+ * - send outbound replies to the right channel
9
+ * - feature-detect (e.g. "does this channel support voice notes?")
10
+ *
11
+ * Channels are NOT auto-discovered on the filesystem in this minimal
12
+ * v1 — the operator wires them up explicitly in
13
+ * `scripts/daemon/maestro-daemon.mjs` (or a new
14
+ * `scripts/daemon/channel-bus.mjs` worker). Auto-discovery via
15
+ * config/channels/*.yaml can come later without breaking changes.
16
+ *
17
+ * The registry is a singleton so any code path can reach it.
18
+ */
19
+
20
+ import { validateChannel } from "./channel.mjs";
21
+
22
+ const _channels = new Map(); // name → { instance, factory, capabilities }
23
+
24
+ /**
25
+ * Register a channel instance.
26
+ *
27
+ * Registering a name that is already present is a programming error by
28
+ * default — silently overwriting the previous channel would orphan its
29
+ * running listener and leak it. Pass `{ replace: true }` to deliberately
30
+ * swap a channel; in that case the previous instance's `stop()` is
31
+ * awaited (best-effort) before the new one takes its place.
32
+ *
33
+ * @param {string} name
34
+ * @param {object} channelInstance Conforming to the Channel interface
35
+ * @param {object} [opts]
36
+ * @param {boolean} [opts.replace=false] Replace an existing channel of the same name.
37
+ * @returns {Promise<void>|void} A promise when replacing (so the old stop() is awaited), otherwise void.
38
+ */
39
+ export function registerChannel(name, channelInstance, opts = {}) {
40
+ if (!name || typeof name !== "string") throw new Error("registerChannel: name required");
41
+ validateChannel(channelInstance);
42
+ const existing = _channels.get(name);
43
+ if (existing && !opts.replace) {
44
+ throw new Error(
45
+ `registerChannel: channel "${name}" already registered; pass { replace: true } to override`
46
+ );
47
+ }
48
+ const set = () =>
49
+ _channels.set(name, {
50
+ instance: channelInstance,
51
+ capabilities: channelInstance.capabilities || [],
52
+ });
53
+ if (existing && opts.replace) {
54
+ // Stop the previous instance before swapping so its listener does not
55
+ // leak. Best-effort: a failing stop() must not block the replacement.
56
+ return Promise.resolve()
57
+ .then(() => existing.instance.stop?.())
58
+ .catch(() => {})
59
+ .then(set);
60
+ }
61
+ set();
62
+ }
63
+
64
+ export function getChannel(name) {
65
+ return _channels.get(name)?.instance;
66
+ }
67
+
68
+ export function listChannels() {
69
+ return Array.from(_channels.keys()).sort();
70
+ }
71
+
72
+ export function findChannelsWithCapability(cap) {
73
+ const out = [];
74
+ for (const [name, entry] of _channels.entries()) {
75
+ if (entry.capabilities.includes(cap)) out.push(name);
76
+ }
77
+ return out;
78
+ }
79
+
80
+ export function clearChannels() {
81
+ _channels.clear();
82
+ }
83
+
84
+ /**
85
+ * Start all registered channels concurrently and wire their inbound
86
+ * messages through a single onInbound handler. Returns a teardown fn.
87
+ *
88
+ * Per-channel start results are surfaced two ways so a failing start is
89
+ * never swallowed:
90
+ * - the returned teardown fn carries `.results` (array of
91
+ * `{ name, ok, error? }`), and
92
+ * - `opts.onError(name, err)` is invoked for each failure if supplied.
93
+ * Both synchronous throws and rejected start() promises are captured.
94
+ *
95
+ * The teardown fn only stops channels that actually started, so we never
96
+ * call `stop()` on a channel whose `start()` failed (which would otherwise
97
+ * trip on half-initialised state).
98
+ *
99
+ * @param {(msg: import("./channel.mjs").ChannelMessage) => void} onInbound
100
+ * @param {object} [opts]
101
+ * @param {(name: string, err: Error) => void} [opts.onError]
102
+ * @returns {Promise<(() => Promise<void>) & { results: Array<{name:string,ok:boolean,error?:Error}> }>}
103
+ */
104
+ export async function startAllChannels(onInbound, opts = {}) {
105
+ if (typeof onInbound !== "function") throw new Error("startAllChannels: onInbound required");
106
+ const controller = new AbortController();
107
+ const started = new Set(); // names whose start() resolved successfully
108
+ const pending = [];
109
+ const results = [];
110
+ for (const [name, entry] of _channels.entries()) {
111
+ let p;
112
+ try {
113
+ p = Promise.resolve(
114
+ entry.instance.start({
115
+ onInbound: (msg) => {
116
+ // Stamp the channel name in case the implementation forgot.
117
+ if (!msg.channel) msg.channel = name;
118
+ onInbound(msg);
119
+ },
120
+ signal: controller.signal,
121
+ ...(opts.channelOpts?.[name] || {}),
122
+ })
123
+ );
124
+ } catch (err) {
125
+ // Synchronous throw from start().
126
+ results.push({ name, ok: false, error: err });
127
+ if (typeof opts.onError === "function") opts.onError(name, err);
128
+ continue;
129
+ }
130
+ pending.push(
131
+ p.then(
132
+ () => {
133
+ started.add(name);
134
+ results.push({ name, ok: true });
135
+ },
136
+ (err) => {
137
+ // Asynchronous rejection from start().
138
+ results.push({ name, ok: false, error: err });
139
+ if (typeof opts.onError === "function") opts.onError(name, err);
140
+ }
141
+ )
142
+ );
143
+ }
144
+ await Promise.all(pending);
145
+ const stopAll = async function stopAll() {
146
+ controller.abort();
147
+ await Promise.allSettled(
148
+ Array.from(started).map((name) =>
149
+ Promise.resolve().then(() => _channels.get(name)?.instance.stop?.())
150
+ )
151
+ );
152
+ };
153
+ stopAll.results = results;
154
+ return stopAll;
155
+ }
156
+
157
+ export default {
158
+ registerChannel,
159
+ getChannel,
160
+ listChannels,
161
+ findChannelsWithCapability,
162
+ clearChannels,
163
+ startAllChannels,
164
+ };
@@ -0,0 +1,317 @@
1
+ /**
2
+ * Maestro — Slack channel adapter (Socket Mode, re-homed translation)
3
+ *
4
+ * WS2 (design §5) makes the channel layer the *live* Slack ingestion path
5
+ * without a big-bang rewrite. This adapter:
6
+ *
7
+ * - extends BaseAdapter (pairingMode "open" — Slack membership + self/peer
8
+ * filtering already happen in `shouldKeepEvent`, and the daemon's
9
+ * directed-gate governs the rest, so default-deny pairing must NOT apply
10
+ * to the live Slack path or it would drop the principal's DMs);
11
+ * - drives the *real* `createSocketModeClient` from
12
+ * scripts/poller/slack-socket-mode.mjs in `_connect` (the connection, ack,
13
+ * reconnect, and emergency-stop logic are unchanged);
14
+ * - re-homes the legacy `eventToInboxItem` translation: the socket client's
15
+ * `writeInbox` injection is redirected so each kept event becomes a
16
+ * MessageEvent (`inboxItemToEvent`) flowing through BaseAdapter → onInbound;
17
+ * the daemon's onInbound re-serializes via `eventToInboxItem` →
18
+ * `writeInboxItem`. Net effect on disk: identical bytes for message events,
19
+ * plus populated `kind` / `source` / `thread_context`.
20
+ * - closes the 3 Slack gaps via an `enrich` hook (uses the bot token):
21
+ * (a) reactions → kind:"reaction" (target text via conversations.history)
22
+ * (b) thread replies → thread_context via conversations.replies
23
+ * (c) cc_channels → kind:"channel_cc" (opt-in, config/slack.yaml)
24
+ *
25
+ * The existing send/react/healthCheck (script-backed) are preserved so the
26
+ * channel bus + channels.test.mjs keep working.
27
+ *
28
+ * @module lib/channels/slack/adapter
29
+ */
30
+
31
+ import { execFileSync } from "node:child_process";
32
+ import { existsSync, readFileSync } from "node:fs";
33
+ import { join, resolve } from "node:path";
34
+ import { CHANNEL_CAPABILITIES } from "../channel.mjs";
35
+ import { BaseAdapter } from "../base-adapter.mjs";
36
+ import { definePlatform } from "../contract.mjs";
37
+ import { inboxItemToEvent } from "../inbox-item.mjs";
38
+ import {
39
+ createSocketModeClient,
40
+ loadAgentIdentity,
41
+ loadPeerSlackIds,
42
+ loadEnvFromFile,
43
+ } from "../../../scripts/poller/slack-socket-mode.mjs";
44
+
45
+ const DEFAULT_CAPABILITIES = [
46
+ CHANNEL_CAPABILITIES.SEND,
47
+ CHANNEL_CAPABILITIES.REACT,
48
+ CHANNEL_CAPABILITIES.THREAD,
49
+ CHANNEL_CAPABILITIES.MEDIA_IMAGE,
50
+ CHANNEL_CAPABILITIES.MEDIA_DOCUMENT,
51
+ CHANNEL_CAPABILITIES.MENTIONS,
52
+ CHANNEL_CAPABILITIES.RICH_BLOCKS,
53
+ CHANNEL_CAPABILITIES.GROUP_CHAT,
54
+ ];
55
+
56
+ const SLACK_API = "https://slack.com/api";
57
+
58
+ export class SlackAdapter extends BaseAdapter {
59
+ constructor(opts = {}) {
60
+ super({ name: "slack", capabilities: DEFAULT_CAPABILITIES, pairingMode: "open", ...opts });
61
+ this.sendScript = opts.sendScript || join(this.agentRoot, "scripts/slack-send.sh");
62
+ this._client = null;
63
+ this._slackConfig = opts.slackConfig || loadSlackConfig(this.agentRoot);
64
+ this._ccChannels = new Set(this._slackConfig.cc_channels || []);
65
+ this._reactionsOn = this._slackConfig.reactions !== false; // default on
66
+ this._botToken = opts.botToken || process.env.SLACK_BOT_TOKEN || "";
67
+ this._fetchFn = opts.fetchFn || globalThis.fetch;
68
+ // Injection seams for tests.
69
+ this._createClient = opts.createClient || createSocketModeClient;
70
+ }
71
+
72
+ /**
73
+ * `_connect` drives the real Socket Mode client. The client's `writeInbox`
74
+ * callback is redirected here: instead of writing the inbox item itself, it
75
+ * hands the item to the adapter, which converts it to a MessageEvent and
76
+ * lets BaseAdapter forward it to onInbound. The daemon's onInbound performs
77
+ * the actual `writeInboxItem`, so there is exactly one write per event.
78
+ */
79
+ async _connect({ onRaw, signal }) {
80
+ loadEnvFromFile(this.agentRoot);
81
+ const identity = this._identity || loadAgentIdentity(this.agentRoot);
82
+ const peerSlackIds = this._peerSlackIds || loadPeerSlackIds(this.agentRoot, identity.slackMemberId);
83
+ if (!this._botToken) this._botToken = process.env.SLACK_BOT_TOKEN || "";
84
+ const appToken = this._appToken || process.env.SLACK_APP_LEVEL_TOKEN || "";
85
+
86
+ // The socket client emits legacy inbox items via writeInbox. We funnel
87
+ // them through onRaw so BaseAdapter.start's _toEvent → pairing-gate →
88
+ // onInbound pipeline runs uniformly. `_toEvent` is identity here (the item
89
+ // was already turned into a MessageEvent by the writeInbox shim).
90
+ const writeInbox = (service, item) => {
91
+ try {
92
+ const ev = inboxItemToEvent(item);
93
+ ev.channel = "slack";
94
+ onRaw(ev);
95
+ } catch (err) {
96
+ this.log("error", `slack item→event failed: ${err.message}`);
97
+ }
98
+ };
99
+
100
+ this._client = this._createClient({
101
+ agentRoot: this.agentRoot,
102
+ appToken,
103
+ identity,
104
+ peerSlackIds,
105
+ writeInbox,
106
+ reactions: this._reactionsOn,
107
+ ccChannels: this._ccChannels.size ? this._ccChannels : null,
108
+ enrich: (envelope) => this._enrich(envelope),
109
+ ...(this._clientOpts || {}),
110
+ });
111
+ await this._client.start();
112
+ if (signal) signal.addEventListener?.("abort", () => { try { this._client?.stop(); } catch { /* */ } });
113
+ }
114
+
115
+ // The writeInbox shim already produced a MessageEvent; _toEvent is identity.
116
+ _toEvent(raw) {
117
+ return raw || null;
118
+ }
119
+
120
+ /**
121
+ * Resolve enrichment for a kept event before translation:
122
+ * - reaction → fetch the target message text (conversations.history)
123
+ * - thread reply → fetch the prior thread history (conversations.replies)
124
+ * Returns `{ threadContext }` consumed by eventToInboxItem. Fail-open: any
125
+ * error returns `{}` so the event still flows (without context).
126
+ */
127
+ async _enrich(envelope) {
128
+ const event = envelope?.payload?.event;
129
+ if (!event || !this._botToken) return {};
130
+ try {
131
+ if (event.type === "reaction_added" || event.type === "reaction_removed") {
132
+ const targetText = await this._fetchMessageText(event.item?.channel, event.item?.ts);
133
+ return targetText ? { threadContext: targetText } : {};
134
+ }
135
+ if (event.type === "message") {
136
+ const threadTs = event.thread_ts || "";
137
+ const isReply = !!threadTs && threadTs !== event.ts;
138
+ if (isReply) {
139
+ const ctx = await this._fetchThreadContext(event.channel, threadTs, event.ts);
140
+ return ctx ? { threadContext: ctx } : {};
141
+ }
142
+ }
143
+ } catch (err) {
144
+ this.log("warn", `enrich failed (fail-open): ${err.message}`);
145
+ }
146
+ return {};
147
+ }
148
+
149
+ async _slackGet(method, params) {
150
+ const url = new URL(`${SLACK_API}/${method}`);
151
+ for (const [k, v] of Object.entries(params)) url.searchParams.set(k, String(v));
152
+ const res = await this._fetchFn(url, { headers: { Authorization: `Bearer ${this._botToken}` } });
153
+ return res.json();
154
+ }
155
+
156
+ async _fetchMessageText(channel, ts) {
157
+ if (!channel || !ts) return "";
158
+ const body = await this._slackGet("conversations.history", { channel, latest: ts, oldest: ts, inclusive: true, limit: 1 });
159
+ if (!body.ok || !Array.isArray(body.messages) || body.messages.length === 0) return "";
160
+ return (body.messages[0].text || "").slice(0, 1000);
161
+ }
162
+
163
+ async _fetchThreadContext(channel, threadTs, beforeTs) {
164
+ if (!channel || !threadTs) return "";
165
+ const body = await this._slackGet("conversations.replies", { channel, ts: threadTs, limit: 20 });
166
+ if (!body.ok || !Array.isArray(body.messages)) return "";
167
+ const before = parseFloat(beforeTs);
168
+ const agentFirst = (this._identity?.firstName || loadAgentIdentity(this.agentRoot).firstName || "Agent");
169
+ return body.messages
170
+ .filter((m) => !beforeTs || parseFloat(m.ts) < before)
171
+ .map((m) => {
172
+ const who = m.bot_id ? "bot" : (m.user || "user");
173
+ return `${who}: ${(m.text || "").slice(0, 500)}`;
174
+ })
175
+ .join("\n");
176
+ }
177
+
178
+ // -- outbound (script-backed, preserved from the v1 adapter) ---------------
179
+
180
+ async _send(out) {
181
+ if (!out || !out.to) throw new Error("Slack.send: msg.to required");
182
+ if (!existsSync(this.sendScript)) {
183
+ throw new Error(`Slack.send: ${this.sendScript} not found; install maestro scripts first`);
184
+ }
185
+ const args = [out.to, "--text", out.text || ""];
186
+ const threadRef = out.thread_ref ?? out.source?.threadRef;
187
+ if (threadRef) args.push("--thread", String(threadRef));
188
+ if (out.blocks) args.push("--blocks", JSON.stringify(out.blocks));
189
+ try {
190
+ const stdout = execFileSync(this.sendScript, args, {
191
+ encoding: "utf-8",
192
+ timeout: 30_000,
193
+ env: { ...process.env, AGENT_ROOT: this.agentRoot, AGENT_DIR: this.agentRoot },
194
+ });
195
+ const tsMatch = stdout.match(/ts=([0-9.]+)/);
196
+ return { ok: true, message_id: tsMatch?.[1] || "", raw: { stdout } };
197
+ } catch (err) {
198
+ const stderr = err.stderr?.toString?.() || err.message;
199
+ throw new Error(`Slack.send failed: ${stderr}`);
200
+ }
201
+ }
202
+
203
+ async _react({ messageRef, emoji }) {
204
+ const reactScript = join(this.agentRoot, "scripts/slack-react.sh");
205
+ if (!existsSync(reactScript)) {
206
+ this.log("warn", "scripts/slack-react.sh not present — reaction skipped");
207
+ return { ok: false, error: "react-script-missing" };
208
+ }
209
+ execFileSync(reactScript, [String(messageRef), emoji], {
210
+ encoding: "utf-8",
211
+ timeout: 10_000,
212
+ env: { ...process.env, AGENT_ROOT: this.agentRoot, AGENT_DIR: this.agentRoot },
213
+ });
214
+ return { ok: true };
215
+ }
216
+
217
+ async _setTyping(source, on) {
218
+ // Slack typing via assistant.threads.setStatus requires the bot token +
219
+ // an assistant-enabled app; best-effort and fail-open. Only meaningful for
220
+ // on=true (Slack clears status when the next message posts).
221
+ if (!on || !this._botToken) return;
222
+ const channel = source?.chatId;
223
+ const threadTs = source?.threadRef;
224
+ if (!channel || !threadTs) return; // setStatus is thread-scoped
225
+ try {
226
+ await this._fetchFn(`${SLACK_API}/assistant.threads.setStatus`, {
227
+ method: "POST",
228
+ headers: { Authorization: `Bearer ${this._botToken}`, "Content-Type": "application/json" },
229
+ body: JSON.stringify({ channel_id: channel, thread_ts: threadTs, status: "is typing…" }),
230
+ });
231
+ } catch { /* fail-open */ }
232
+ }
233
+
234
+ async healthCheck() {
235
+ const present = existsSync(this.sendScript);
236
+ return { ok: present, detail: present ? "slack-send.sh present" : `missing ${this.sendScript}` };
237
+ }
238
+
239
+ async stop() {
240
+ await super.stop();
241
+ try { this._client?.stop(); } catch { /* */ }
242
+ this._client = null;
243
+ }
244
+ }
245
+
246
+ /** Load config/slack.yaml (cc_channels, reactions, ingest mode). Best-effort. */
247
+ export function loadSlackConfig(agentRoot) {
248
+ const path = join(resolve(agentRoot || "."), "config/slack.yaml");
249
+ if (!existsSync(path)) return {};
250
+ try {
251
+ const body = readFileSync(path, "utf-8");
252
+ return parseSlackConfig(body);
253
+ } catch {
254
+ return {};
255
+ }
256
+ }
257
+
258
+ /**
259
+ * Tiny dependency-free parser for the slack.yaml subset we need
260
+ * (`ingest_mode`, `reactions`, `cc_channels: [..]` or list items). Avoids a
261
+ * dynamic js-yaml import on the channel hot path.
262
+ */
263
+ export function parseSlackConfig(body) {
264
+ const out = {};
265
+ let inCc = false;
266
+ for (const raw of String(body).split("\n")) {
267
+ const line = raw.replace(/\t/g, " ");
268
+ if (!line.trim() || line.trim().startsWith("#")) continue;
269
+ const ingest = line.match(/^ingest_mode:\s*(\S+)/);
270
+ if (ingest) { out.ingest_mode = unquote(ingest[1]); inCc = false; continue; }
271
+ const reactions = line.match(/^reactions:\s*(\S+)/);
272
+ if (reactions) { out.reactions = /^(true|on|yes)$/i.test(unquote(reactions[1])); inCc = false; continue; }
273
+ const ccInline = line.match(/^cc_channels:\s*\[(.*)\]\s*$/);
274
+ if (ccInline) {
275
+ out.cc_channels = ccInline[1].split(",").map((s) => unquote(s.trim())).filter(Boolean);
276
+ inCc = false;
277
+ continue;
278
+ }
279
+ if (/^cc_channels:\s*$/.test(line)) { out.cc_channels = out.cc_channels || []; inCc = true; continue; }
280
+ const item = line.match(/^\s*-\s*(.+)$/);
281
+ if (inCc && item) { out.cc_channels.push(unquote(item[1].trim())); continue; }
282
+ if (!/^\s/.test(line)) inCc = false; // a new top-level key closes the list
283
+ }
284
+ return out;
285
+ }
286
+
287
+ function unquote(s) {
288
+ let t = String(s).trim();
289
+ // strip inline comment for unquoted
290
+ if (!(t.startsWith('"') || t.startsWith("'"))) {
291
+ const h = t.indexOf("#");
292
+ if (h >= 0) t = t.slice(0, h).trim();
293
+ }
294
+ if ((t.startsWith('"') && t.endsWith('"')) || (t.startsWith("'") && t.endsWith("'"))) t = t.slice(1, -1);
295
+ return t;
296
+ }
297
+
298
+ /** Factory matching the channel/platform contract. */
299
+ export function createSlackChannel(opts = {}) {
300
+ return new SlackAdapter(opts);
301
+ }
302
+
303
+ // Self-register so the platform registry knows about Slack (factory + gate
304
+ // file + capabilities). NOTE (single-owner rule, H2): the maestro daemon's
305
+ // generic channel loop DELIBERATELY SKIPS Slack — Slack's real-time connection
306
+ // is owned solely by the dedicated launchd `slack-socket` process
307
+ // (scripts/poller/slack-socket-mode.mjs), which reads config/slack.yaml itself.
308
+ // Starting Slack here too would double-ingest every DM. This registration is
309
+ // still used by tests + any non-daemon caller that wants the adapter factory.
310
+ definePlatform({
311
+ name: "slack",
312
+ gateFile: "config/slack.yaml",
313
+ factory: createSlackChannel,
314
+ capabilities: DEFAULT_CAPABILITIES,
315
+ });
316
+
317
+ export default createSlackChannel;
@@ -0,0 +1,212 @@
1
+ /**
2
+ * slack-adapter.test.mjs — node:test coverage for slack/adapter.mjs
3
+ *
4
+ * Proves the re-homed translation is behaviour-preserving (round-trip through
5
+ * the writeInbox shim → MessageEvent → daemon eventToInboxItem reproduces the
6
+ * legacy socket-mode item) and that the enrich hook closes the 3 gaps
7
+ * (reaction kind, thread_context, channel_cc). Injects a fake socket client +
8
+ * fake fetch — no network, no real Slack.
9
+ */
10
+
11
+ import { test } from "node:test";
12
+ import assert from "node:assert/strict";
13
+ import { promises as fsp } from "node:fs";
14
+ import { tmpdir } from "node:os";
15
+ import { join } from "node:path";
16
+
17
+ import { SlackAdapter, parseSlackConfig } from "./slack/adapter.mjs";
18
+ import { eventToInboxItem as daemonEventToInboxItem } from "./inbox-item.mjs";
19
+ import { eventToInboxItem as slackLegacyItem } from "../../scripts/poller/slack-socket-mode.mjs";
20
+
21
+ async function makeRoot() {
22
+ const p = join(tmpdir(), `slack-adapter-test-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`);
23
+ await fsp.mkdir(p, { recursive: true });
24
+ return p;
25
+ }
26
+ async function rmRoot(p) { try { await fsp.rm(p, { recursive: true, force: true }); } catch { /* */ } }
27
+
28
+ const IDS = { firstName: "Ravi", fullName: "Jamie Lee", slackMemberId: "UAGENT0001", principalSlackId: "UCEO000001" };
29
+
30
+ /**
31
+ * A fake socket client capturing the opts the adapter passed (writeInbox,
32
+ * enrich, reactions, ccChannels) so the test can drive the seam directly.
33
+ */
34
+ function fakeClientFactory(captured) {
35
+ return (opts) => {
36
+ captured.opts = opts;
37
+ return {
38
+ start: async () => { captured.started = true; },
39
+ stop: () => { captured.stopped = true; },
40
+ getStats: () => ({}),
41
+ getState: () => ({}),
42
+ };
43
+ };
44
+ }
45
+
46
+ function makeAdapter(root, extra = {}) {
47
+ const captured = {};
48
+ const a = new SlackAdapter({
49
+ agentRoot: root,
50
+ botToken: "xoxb-test",
51
+ appToken: "xapp-test",
52
+ createClient: fakeClientFactory(captured),
53
+ ...extra,
54
+ });
55
+ // Pre-seed identity/peers so _connect doesn't read config/agent.json.
56
+ a._identity = IDS;
57
+ a._peerSlackIds = new Set();
58
+ a._appToken = "xapp-test";
59
+ return { a, captured };
60
+ }
61
+
62
+ // ---------------------------------------------------------------------------
63
+ // behaviour-preserving round-trip
64
+ // ---------------------------------------------------------------------------
65
+
66
+ test("writeInbox shim → MessageEvent → daemon item == legacy socket-mode item (DM)", async () => {
67
+ const root = await makeRoot();
68
+ try {
69
+ const { a, captured } = makeAdapter(root);
70
+ const received = [];
71
+ await a.start({ onInbound: (ev) => received.push(ev) });
72
+ assert.ok(captured.started, "adapter started the socket client");
73
+ assert.equal(captured.opts.reactions, true, "reactions on by default");
74
+
75
+ // Build the legacy item exactly as the socket client would, then feed it
76
+ // through the adapter's writeInbox shim (what the real client calls).
77
+ const envelope = {
78
+ payload: { event: { type: "message", user: "UHUMAN0001", text: "hey, draft the memo", ts: "1735689600.001000", channel: "DABCDEF01" } },
79
+ };
80
+ const legacy = slackLegacyItem(envelope, { ownSlackId: IDS.slackMemberId, principalSlackId: IDS.principalSlackId, agentFirstName: IDS.firstName });
81
+
82
+ captured.opts.writeInbox("slack", legacy);
83
+ assert.equal(received.length, 1, "event reached onInbound");
84
+ const ev = received[0];
85
+ assert.equal(ev.kind, "message");
86
+ assert.equal(ev.source.channel, "slack");
87
+ assert.equal(ev.source.chatId, "DABCDEF01");
88
+ assert.equal(ev.source.userScope, "dm");
89
+
90
+ // The daemon re-serializes the event — must reproduce the legacy item.
91
+ const reserialized = daemonEventToInboxItem(ev);
92
+ assert.deepEqual(reserialized, legacy, "round-trip must be byte-identical");
93
+ } finally { await rmRoot(root); }
94
+ });
95
+
96
+ test("Slack adapter uses pairingMode 'open' so DMs are never default-denied", async () => {
97
+ const root = await makeRoot();
98
+ try {
99
+ const { a, captured } = makeAdapter(root);
100
+ const received = [];
101
+ await a.start({ onInbound: (ev) => received.push(ev) });
102
+ // An un-allowlisted DM sender — pairing "open" must let it through.
103
+ const legacy = slackLegacyItem(
104
+ { payload: { event: { type: "message", user: "USTRANGER", text: "hi", ts: "1735689600.001000", channel: "DZZZ" } } },
105
+ { ownSlackId: IDS.slackMemberId, principalSlackId: IDS.principalSlackId, agentFirstName: IDS.firstName },
106
+ );
107
+ captured.opts.writeInbox("slack", legacy);
108
+ assert.equal(received.length, 1, "Slack DM must not be blocked by pairing (live-path preserved)");
109
+ } finally { await rmRoot(root); }
110
+ });
111
+
112
+ // ---------------------------------------------------------------------------
113
+ // enrich hook — gap (b) thread_context, gap (a) reaction target text
114
+ // ---------------------------------------------------------------------------
115
+
116
+ test("enrich fills thread_context for a thread reply via conversations.replies", async () => {
117
+ const root = await makeRoot();
118
+ try {
119
+ const { a } = makeAdapter(root, {
120
+ fetchFn: async (url) => {
121
+ const u = String(url);
122
+ if (u.includes("conversations.replies")) {
123
+ return { json: async () => ({ ok: true, messages: [
124
+ { ts: "1735689500.000100", user: "UHUMAN0001", text: "original question" },
125
+ { ts: "1735689800.003000", user: "UHUMAN0001", text: "the reply itself" },
126
+ ] }) };
127
+ }
128
+ return { json: async () => ({ ok: false }) };
129
+ },
130
+ });
131
+ const envelope = { payload: { event: { type: "message", channel: "C1", ts: "1735689800.003000", thread_ts: "1735689500.000100", text: "the reply" } } };
132
+ const extra = await a._enrich(envelope);
133
+ assert.match(extra.threadContext, /original question/);
134
+ // The reply itself (>= beforeTs) is excluded from the prior context.
135
+ assert.doesNotMatch(extra.threadContext, /the reply itself/);
136
+ } finally { await rmRoot(root); }
137
+ });
138
+
139
+ test("enrich fills reaction target text via conversations.history", async () => {
140
+ const root = await makeRoot();
141
+ try {
142
+ const { a } = makeAdapter(root, {
143
+ fetchFn: async (url) => {
144
+ if (String(url).includes("conversations.history")) {
145
+ return { json: async () => ({ ok: true, messages: [{ ts: "1735689600.001000", text: "the message they reacted to" }] }) };
146
+ }
147
+ return { json: async () => ({ ok: false }) };
148
+ },
149
+ });
150
+ const envelope = { payload: { event: { type: "reaction_added", user: "UHUMAN0001", reaction: "eyes", item: { type: "message", channel: "C1", ts: "1735689600.001000" } } } };
151
+ const extra = await a._enrich(envelope);
152
+ assert.match(extra.threadContext, /reacted to/);
153
+ } finally { await rmRoot(root); }
154
+ });
155
+
156
+ test("enrich is fail-open: a fetch throw yields {} (event still flows)", async () => {
157
+ const root = await makeRoot();
158
+ try {
159
+ const { a } = makeAdapter(root, { fetchFn: async () => { throw new Error("slack 500"); } });
160
+ const envelope = { payload: { event: { type: "message", channel: "C1", ts: "2", thread_ts: "1", text: "x" } } };
161
+ const extra = await a._enrich(envelope);
162
+ assert.deepEqual(extra, {});
163
+ } finally { await rmRoot(root); }
164
+ });
165
+
166
+ test("enrich returns {} for a non-threaded message (no API call)", async () => {
167
+ const root = await makeRoot();
168
+ try {
169
+ let called = false;
170
+ const { a } = makeAdapter(root, { fetchFn: async () => { called = true; return { json: async () => ({ ok: true, messages: [] }) }; } });
171
+ const envelope = { payload: { event: { type: "message", channel: "C1", ts: "1735689600.001000", text: "top-level" } } };
172
+ const extra = await a._enrich(envelope);
173
+ assert.deepEqual(extra, {});
174
+ assert.equal(called, false, "top-level messages need no enrichment fetch");
175
+ } finally { await rmRoot(root); }
176
+ });
177
+
178
+ // ---------------------------------------------------------------------------
179
+ // config parsing — cc_channels + reactions
180
+ // ---------------------------------------------------------------------------
181
+
182
+ test("parseSlackConfig reads ingest_mode, reactions, and cc_channels (list + inline)", () => {
183
+ const list = parseSlackConfig([
184
+ "ingest_mode: socket",
185
+ "reactions: on",
186
+ "cc_channels:",
187
+ " - C0AAA # eng",
188
+ ' - "C0BBB"',
189
+ ].join("\n"));
190
+ assert.equal(list.ingest_mode, "socket");
191
+ assert.equal(list.reactions, true);
192
+ assert.deepEqual(list.cc_channels, ["C0AAA", "C0BBB"]);
193
+
194
+ const inline = parseSlackConfig("cc_channels: [C1, \"C2\"]\nreactions: false\n");
195
+ assert.deepEqual(inline.cc_channels, ["C1", "C2"]);
196
+ assert.equal(inline.reactions, false);
197
+ });
198
+
199
+ test("cc_channels from config flow into the socket client opts", async () => {
200
+ const root = await makeRoot();
201
+ try {
202
+ await fsp.mkdir(join(root, "config"), { recursive: true });
203
+ await fsp.writeFile(join(root, "config/slack.yaml"), "ingest_mode: socket\nreactions: on\ncc_channels:\n - C0WATCH\n");
204
+ const { a, captured } = makeAdapter(root, { slackConfig: undefined });
205
+ // slackConfig undefined → adapter loads config/slack.yaml itself.
206
+ const a2 = new SlackAdapter({ agentRoot: root, botToken: "x", createClient: () => ({ start: async () => {}, stop: () => {} }) });
207
+ assert.ok(a2._ccChannels.has("C0WATCH"));
208
+ assert.equal(a2._reactionsOn, true);
209
+ await a.start({ onInbound: () => {} });
210
+ void captured;
211
+ } finally { await rmRoot(root); }
212
+ });