@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,854 @@
1
+ // Slack Poller — checks Slack API for new messages every 60 seconds
2
+ // Requires SLACK_USER_TOKEN or SLACK_TOKEN environment variable
3
+
4
+ import { readFileSync, writeFileSync, readdirSync, renameSync, existsSync } from "fs";
5
+ import { join } from "path";
6
+ import {
7
+ writeInboxItem,
8
+ readCursor,
9
+ writeCursor,
10
+ resolvePrivilege,
11
+ resolveName,
12
+ AGENT_REPO_DIR,
13
+ extractSlackAttachments,
14
+ downloadSlackAttachment,
15
+ } from "./utils.mjs";
16
+
17
+ import {
18
+ isVoiceTranscript,
19
+ parseVoiceTranscript,
20
+ toInboxItems,
21
+ } from "../parse-voice-transcript.mjs";
22
+
23
+ const ACTIVE_THREADS_FILE = join(
24
+ AGENT_REPO_DIR,
25
+ "state",
26
+ "slack-active-threads.json",
27
+ );
28
+ const MAX_THREADS_PER_CHANNEL = 10;
29
+
30
+ const SLACK_TOKEN = process.env.SLACK_USER_TOKEN || process.env.SLACK_TOKEN;
31
+
32
+ // Load agent identity from canonical SOT for self-filtering + display names.
33
+ let _agent = null;
34
+ function loadAgent() {
35
+ if (_agent) return _agent;
36
+ try {
37
+ _agent = JSON.parse(readFileSync(join(AGENT_REPO_DIR, "config/agent.json"), "utf-8"));
38
+ } catch {
39
+ _agent = { firstName: "Agent", fullName: "Agent", slackMemberId: "" };
40
+ }
41
+ return _agent;
42
+ }
43
+
44
+ // Priority overrides for known channels — everything else defaults to "normal"
45
+ const CHANNEL_PRIORITY_OVERRIDES = {
46
+ "ceo-office": "critical",
47
+ "engineering": "high",
48
+ "all-company": "high",
49
+ "engineering-platform": "high",
50
+ "regulatory-remediation": "high",
51
+ "strategic-initiative": "high",
52
+ "legal-compliance": "high",
53
+ };
54
+
55
+ // Dynamic channel list — fetched once per daemon lifecycle, refreshed every 10 min
56
+ let cachedChannels = null;
57
+ let channelsCachedAt = 0;
58
+ const CHANNEL_CACHE_TTL = 10 * 60 * 1000; // 10 min
59
+
60
+ /**
61
+ * Fetch all channels the agent is a member of.
62
+ * Replaces the static MONITORED_CHANNELS list so new channels are auto-discovered.
63
+ * Per CEO standing instruction si-001: "Monitor ALL Slack channels."
64
+ */
65
+ async function getMonitoredChannels() {
66
+ if (cachedChannels && (Date.now() - channelsCachedAt) < CHANNEL_CACHE_TTL) {
67
+ return cachedChannels;
68
+ }
69
+ try {
70
+ const result = await slackApi("conversations.list", {
71
+ types: "public_channel,private_channel",
72
+ limit: 200,
73
+ exclude_archived: true,
74
+ });
75
+ if (!result.ok) {
76
+ console.error(`[slack-poller] conversations.list failed: ${result.error}`);
77
+ return cachedChannels || [];
78
+ }
79
+ cachedChannels = (result.channels || [])
80
+ .filter((c) => c.is_member)
81
+ .map((c) => ({
82
+ id: c.id,
83
+ name: c.name || c.id,
84
+ priority: CHANNEL_PRIORITY_OVERRIDES[c.name] || "normal",
85
+ }));
86
+ channelsCachedAt = Date.now();
87
+ return cachedChannels;
88
+ } catch (err) {
89
+ console.error(`[slack-poller] Channel discovery failed: ${err.message}`);
90
+ return cachedChannels || [];
91
+ }
92
+ }
93
+
94
+ const CEO_USER_ID = "U097N5R0M7U";
95
+ const AGENT_USER_ID = "U099N1JFPRQ";
96
+
97
+ // Rate-limit-aware delay with random jitter to reduce Slack API rate limiting.
98
+ // Base delay 400ms + random 0-600ms jitter = 400-1000ms between requests.
99
+ // With broader-fetch elimination and DM/thread rotation, total calls per cycle
100
+ // are ~20-30 (down from 90+), so this timing keeps us well under both the
101
+ // Slack Tier 3 limit (~50 req/min) and the 50s cycle budget.
102
+ // Previous: fixed 1500ms with redundant broader fetches caused 25-30 min cycles.
103
+ const INTER_REQUEST_BASE_MS = 400;
104
+ const INTER_REQUEST_JITTER_MS = 600;
105
+ const MAX_RETRIES = 3;
106
+
107
+ // Cycle time budget — abort non-critical work if approaching this limit.
108
+ // Launchd fires every 60s; we must finish well before the next invocation.
109
+ const CYCLE_TIME_BUDGET_MS = 55_000;
110
+ let cycleStartTime = 0;
111
+
112
+ function sleep(ms) {
113
+ return new Promise((resolve) => setTimeout(resolve, ms));
114
+ }
115
+
116
+ let lastRequestTime = 0;
117
+
118
+ /**
119
+ * Check whether the cycle time budget has been exceeded.
120
+ * Returns true if we should skip non-critical work to finish on time.
121
+ */
122
+ function isCycleBudgetExceeded() {
123
+ return cycleStartTime > 0 && (Date.now() - cycleStartTime) > CYCLE_TIME_BUDGET_MS;
124
+ }
125
+
126
+ async function slackApi(method, params = {}) {
127
+ // Enforce minimum spacing between requests with random jitter
128
+ const now = Date.now();
129
+ const jitteredDelay = INTER_REQUEST_BASE_MS + Math.floor(Math.random() * INTER_REQUEST_JITTER_MS);
130
+ const elapsed = now - lastRequestTime;
131
+ if (elapsed < jitteredDelay) {
132
+ await sleep(jitteredDelay - elapsed);
133
+ }
134
+ lastRequestTime = Date.now();
135
+
136
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
137
+ const url = new URL(`https://slack.com/api/${method}`);
138
+ Object.entries(params).forEach(([k, v]) =>
139
+ url.searchParams.set(k, String(v)),
140
+ );
141
+ const res = await fetch(url, {
142
+ headers: { Authorization: `Bearer ${SLACK_TOKEN}` },
143
+ });
144
+
145
+ // Handle HTTP 429 rate limiting
146
+ if (res.status === 429) {
147
+ const retryAfter = parseInt(res.headers.get("Retry-After") || "5", 10);
148
+ if (attempt < MAX_RETRIES) {
149
+ await sleep(retryAfter * 1000);
150
+ lastRequestTime = Date.now();
151
+ continue;
152
+ }
153
+ // Exhausted retries — return the rate-limited response
154
+ return { ok: false, error: "ratelimited" };
155
+ }
156
+
157
+ const body = await res.json();
158
+
159
+ // Handle in-body rate limit errors (Slack sometimes returns 200 with error)
160
+ if (!body.ok && body.error === "ratelimited" && attempt < MAX_RETRIES) {
161
+ const retryAfter = parseInt(res.headers.get("Retry-After") || "3", 10);
162
+ await sleep(retryAfter * 1000);
163
+ lastRequestTime = Date.now();
164
+ continue;
165
+ }
166
+
167
+ return body;
168
+ }
169
+ }
170
+
171
+ // ── Context fetching ────────────────────────────────────────────────────
172
+ // Fetch surrounding conversation context for a message so the session
173
+ // knows what was said before. This is the fix for "are you referring to
174
+ // Jordan?" when the conversation was clearly about Graham.
175
+
176
+ const CONTEXT_MESSAGES_LIMIT = 8; // Last 8 messages from the conversation
177
+
178
+ /**
179
+ * Fetch recent conversation context for a DM message.
180
+ * Returns the last N messages (including the agent's) formatted as readable text.
181
+ * This gives the session awareness of the conversation flow.
182
+ */
183
+ async function fetchDMContext(channelId, beforeTs) {
184
+ try {
185
+ const result = await slackApi("conversations.history", {
186
+ channel: channelId,
187
+ latest: beforeTs,
188
+ limit: CONTEXT_MESSAGES_LIMIT,
189
+ inclusive: false,
190
+ });
191
+ if (!result.ok || !result.messages || result.messages.length === 0) return null;
192
+
193
+ // Messages come newest-first from Slack, reverse for chronological order
194
+ const msgs = result.messages.reverse();
195
+ const lines = msgs.map((m) => {
196
+ const who = m.user === AGENT_USER_ID ? (loadAgent().firstName || "Agent") : resolveName(m.user);
197
+ const text = (m.text || "").substring(0, 500);
198
+ return `${who}: ${text}`;
199
+ });
200
+ return lines.join("\n");
201
+ } catch {
202
+ return null;
203
+ }
204
+ }
205
+
206
+ /**
207
+ * Fetch thread context (parent message + preceding replies) for a thread reply.
208
+ * Returns formatted text with the full thread conversation up to this point.
209
+ */
210
+ async function fetchThreadContext(channelId, threadTs, beforeTs) {
211
+ try {
212
+ const result = await slackApi("conversations.replies", {
213
+ channel: channelId,
214
+ ts: threadTs,
215
+ limit: CONTEXT_MESSAGES_LIMIT,
216
+ });
217
+ if (!result.ok || !result.messages || result.messages.length === 0) return null;
218
+
219
+ // Only include messages before the current one
220
+ const preceding = result.messages.filter(
221
+ (m) => parseFloat(m.ts) < parseFloat(beforeTs)
222
+ );
223
+ if (preceding.length === 0) return null;
224
+
225
+ const lines = preceding.map((m) => {
226
+ const who = m.user === AGENT_USER_ID ? (loadAgent().firstName || "Agent") : resolveName(m.user);
227
+ const text = (m.text || "").substring(0, 500);
228
+ return `${who}: ${text}`;
229
+ });
230
+ return lines.join("\n");
231
+ } catch {
232
+ return null;
233
+ }
234
+ }
235
+
236
+ // ── Channel rotation ────────────────────────────────────────────────────
237
+ // With dynamic discovery (potentially 30+ channels), rotation is essential.
238
+ // Critical/high channels polled every cycle; normal channels rotate in thirds.
239
+ let pollCycleCount = 0;
240
+
241
+ function getChannelsForCycle(allChannels) {
242
+ pollCycleCount++;
243
+ const critical = allChannels.filter((c) => c.priority === "critical" || c.priority === "high");
244
+ const normal = allChannels.filter((c) => c.priority === "normal" || c.priority === "low");
245
+ // Always poll critical/high; rotate normal channels in thirds
246
+ const normalSubset = normal.filter((_, i) => i % 3 === pollCycleCount % 3);
247
+ return [...critical, ...normalSubset];
248
+ }
249
+
250
+ export async function pollSlack() {
251
+ cycleStartTime = Date.now();
252
+
253
+ if (!SLACK_TOKEN) {
254
+ return { items: [], errors: ["SLACK_TOKEN not set"] };
255
+ }
256
+
257
+ const lastChecked = readCursor("slack");
258
+ const oldest = lastChecked
259
+ ? String(new Date(lastChecked).getTime() / 1000)
260
+ : String((Date.now() - 5 * 60 * 1000) / 1000);
261
+ const items = [];
262
+ const errors = [];
263
+ const allChannels = await getMonitoredChannels();
264
+ const channelsThisCycle = getChannelsForCycle(allChannels);
265
+
266
+ // Load active threads state (used for both channel and DM thread scanning)
267
+ let activeThreads = {};
268
+ try {
269
+ if (existsSync(ACTIVE_THREADS_FILE)) {
270
+ activeThreads = JSON.parse(readFileSync(ACTIVE_THREADS_FILE, "utf-8"));
271
+ }
272
+ } catch {
273
+ activeThreads = {};
274
+ }
275
+
276
+ // ── CEO DM pre-pass ──────────────────────────────────────────────────
277
+ // Alex's DMs are the single most time-sensitive signal. The channel
278
+ // loop below routinely eats the entire 55s cycle budget on a busy
279
+ // workspace and skips DMs entirely. Poll the CEO DMs FIRST (with a
280
+ // small dedicated budget) so they always get fresh data regardless of
281
+ // how the broader cycle plays out. The downstream DM section will
282
+ // re-iterate but the items dedup on raw_ref so duplicates are dropped.
283
+ const ceoDmItemsSeen = new Set(); // raw_refs we already pushed in the pre-pass
284
+ try {
285
+ const convos = await slackApi("conversations.list", { types: "im", limit: 50 });
286
+ if (convos.ok) {
287
+ const ceoDMs = (convos.channels || []).filter((im) => im.user === CEO_USER_ID);
288
+ for (const im of ceoDMs) {
289
+ const history = await slackApi("conversations.history", {
290
+ channel: im.id,
291
+ oldest,
292
+ limit: 10,
293
+ });
294
+ if (!history.ok) continue;
295
+ for (const msg of history.messages || []) {
296
+ if (msg.bot_id || msg.user === AGENT_USER_ID) continue;
297
+ if (isVoiceTranscript(msg.text || "")) continue; // handled in main DM loop
298
+ const rawRef = `slack:${im.id}:${msg.ts}`;
299
+ ceoDmItemsSeen.add(rawRef);
300
+ items.push({
301
+ id: msg.ts.replace(".", "-"),
302
+ service: "slack",
303
+ channel: `dm/${resolveName(msg.user)}`,
304
+ sender: resolveName(msg.user),
305
+ sender_privilege: resolvePrivilege(msg.user),
306
+ timestamp: new Date(parseFloat(msg.ts) * 1000).toISOString(),
307
+ subject: `DM from ${resolveName(msg.user)}`,
308
+ content: msg.text || "",
309
+ thread_id: msg.thread_ts && msg.thread_ts !== msg.ts ? msg.thread_ts : "",
310
+ is_reply: false,
311
+ is_voice_transcript: false,
312
+ priority_signals: {
313
+ from_ceo: true,
314
+ tagged_urgent:
315
+ /\b(urgent|emergency|asap|blocker|critical)\b/i.test(msg.text || ""),
316
+ contains_deadline: false,
317
+ mentions_agent: true,
318
+ },
319
+ raw_ref: rawRef,
320
+ channel_id: im.id,
321
+ });
322
+ }
323
+ }
324
+ }
325
+ } catch (err) {
326
+ errors.push(`CEO DM pre-pass: ${err.message}`);
327
+ }
328
+
329
+ for (const channel of channelsThisCycle) {
330
+ // Check cycle time budget — skip remaining channels if running long
331
+ if (isCycleBudgetExceeded()) {
332
+ errors.push("Cycle time budget exceeded — skipping remaining channels");
333
+ break;
334
+ }
335
+
336
+ try {
337
+ const result = await slackApi("conversations.history", {
338
+ channel: channel.id,
339
+ oldest,
340
+ limit: 20,
341
+ });
342
+
343
+ if (!result.ok) {
344
+ errors.push(`Channel ${channel.name}: ${result.error}`);
345
+ continue;
346
+ }
347
+
348
+ // Track channel threads with replies so the thread scanner picks them up.
349
+ // Thread discovery relies on persisted activeThreads state (which survives
350
+ // across cycles) plus any threaded messages found in the current history
351
+ // response. The previous "broader" second fetch (without oldest filter)
352
+ // has been removed — it doubled API calls per channel and was the primary
353
+ // cause of rate-limiting. Active threads with old parent messages are
354
+ // already tracked in activeThreads from prior cycles.
355
+ const channelThreads = new Set(activeThreads[channel.id] || []);
356
+
357
+ for (const msg of result.messages || []) {
358
+ // Register any message with replies as an active thread (even the agent's)
359
+ if (msg.reply_count > 0 && msg.ts) {
360
+ channelThreads.add(msg.ts);
361
+ }
362
+
363
+ if (msg.bot_id || msg.user === AGENT_USER_ID) continue;
364
+
365
+ const msgText = msg.text || "";
366
+ const isCeo = msg.user === CEO_USER_ID;
367
+
368
+ // Check if this is a voice call transcript posted by post-call routing
369
+ if (isVoiceTranscript(msgText)) {
370
+ const parsed = parseVoiceTranscript(msgText, {
371
+ ts: msg.ts,
372
+ channel: channel.id,
373
+ });
374
+ const voiceItems = toInboxItems(parsed);
375
+ for (const vi of voiceItems) {
376
+ items.push(vi);
377
+ }
378
+ continue;
379
+ }
380
+
381
+ const agentNameLower = (loadAgent().firstName || "").toLowerCase();
382
+ const mentionsAgent = !!agentNameLower && msgText.toLowerCase().includes(agentNameLower);
383
+ const isUrgent = /\b(urgent|emergency|asap|blocker|critical)\b/i.test(
384
+ msgText,
385
+ );
386
+
387
+ // Fetch thread context for channel thread replies
388
+ let channelThreadContext = null;
389
+ if (msg.thread_ts && msg.thread_ts !== msg.ts) {
390
+ channelThreadContext = await fetchThreadContext(channel.id, msg.thread_ts, msg.ts);
391
+ }
392
+
393
+ // Extract and download attachments
394
+ const attachments = extractSlackAttachments(msg.files);
395
+ for (const att of attachments) {
396
+ const file = (msg.files || []).find((f) => f.id === att.id);
397
+ const localPath = await downloadSlackAttachment(file, SLACK_TOKEN);
398
+ if (localPath) att.local_path = localPath;
399
+ }
400
+
401
+ items.push({
402
+ id: msg.ts.replace(".", "-"),
403
+ service: "slack",
404
+ channel: channel.name,
405
+ channel_id: channel.id,
406
+ sender: resolveName(msg.user),
407
+ sender_privilege: resolvePrivilege(msg.user),
408
+ timestamp: new Date(parseFloat(msg.ts) * 1000).toISOString(),
409
+ subject: `#${channel.name}`,
410
+ content: msgText,
411
+ thread_id: msg.thread_ts || "",
412
+ thread_context: channelThreadContext,
413
+ is_reply: !!msg.thread_ts,
414
+ attachments: attachments.length > 0 ? attachments : undefined,
415
+ priority_signals: {
416
+ from_ceo: isCeo,
417
+ tagged_urgent: isUrgent,
418
+ contains_deadline: false,
419
+ mentions_agent: mentionsAgent,
420
+ },
421
+ raw_ref: `slack:${channel.id}:${msg.ts}`,
422
+ });
423
+ }
424
+
425
+ // Persist channel thread tracking (bounded)
426
+ activeThreads[channel.id] = [...channelThreads]
427
+ .sort((a, b) => parseFloat(b) - parseFloat(a))
428
+ .slice(0, MAX_THREADS_PER_CHANNEL);
429
+ } catch (err) {
430
+ errors.push(`Channel ${channel.name}: ${err.message}`);
431
+ }
432
+ }
433
+
434
+ // Also check DMs — with rotation.
435
+ // CEO DMs are polled every cycle (critical). Other DMs rotate in halves
436
+ // to reduce API calls. DM thread discovery uses persisted activeThreads
437
+ // state instead of a redundant broader fetch (same fix as channels above).
438
+ // Cache CEO DM channel IDs for thread scanning priority later.
439
+ const ceoDMChannelIds = new Set();
440
+ try {
441
+ const convos = await slackApi("conversations.list", {
442
+ types: "im",
443
+ limit: 50,
444
+ });
445
+ if (convos.ok) {
446
+ const allDMs = convos.channels || [];
447
+ // Separate CEO DMs (always poll) from other DMs (rotate in halves).
448
+ // We still track ceoDMChannelIds for the downstream thread-scan
449
+ // priority pass, but skip CEO DMs in the per-channel polling loop
450
+ // below because the CEO DM pre-pass at the top of the cycle has
451
+ // already fetched + pushed their messages with fresh budget.
452
+ const ceoDMs = allDMs.filter((im) => im.user === CEO_USER_ID);
453
+ for (const dm of ceoDMs) ceoDMChannelIds.add(dm.id);
454
+ const otherDMs = allDMs.filter((im) => im.user !== CEO_USER_ID);
455
+ const rotatedOtherDMs = otherDMs.filter((_, i) => i % 2 === pollCycleCount % 2);
456
+ const dmsThisCycle = rotatedOtherDMs;
457
+
458
+ for (const im of dmsThisCycle) {
459
+ // Check cycle time budget — skip remaining DMs if running long
460
+ if (isCycleBudgetExceeded()) {
461
+ errors.push("Cycle time budget exceeded — skipping remaining DMs");
462
+ break;
463
+ }
464
+
465
+ const history = await slackApi("conversations.history", {
466
+ channel: im.id,
467
+ oldest,
468
+ limit: 10,
469
+ });
470
+ if (!history.ok) continue;
471
+
472
+ // Track threads discovered from DM history — uses persisted state
473
+ // from prior cycles instead of a redundant broader API call.
474
+ const channelThreads = new Set(activeThreads[im.id] || []);
475
+
476
+ for (const msg of history.messages || []) {
477
+ // Register any message with replies as an active thread
478
+ if (msg.reply_count > 0 && msg.ts) {
479
+ channelThreads.add(msg.ts);
480
+ }
481
+
482
+ if (msg.bot_id || msg.user === AGENT_USER_ID) continue;
483
+
484
+ const msgText = msg.text || "";
485
+ const isCeo = msg.user === CEO_USER_ID;
486
+
487
+ // Check if this is a voice call transcript posted by post-call routing
488
+ if (isVoiceTranscript(msgText)) {
489
+ const parsed = parseVoiceTranscript(msgText, {
490
+ ts: msg.ts,
491
+ channel: im.id,
492
+ });
493
+ const voiceItems = toInboxItems(parsed);
494
+ for (const vi of voiceItems) {
495
+ items.push(vi);
496
+ }
497
+ continue;
498
+ }
499
+
500
+ // Fetch conversation context — thread context for thread replies,
501
+ // recent DM history for standalone messages
502
+ let threadContext = null;
503
+ if (msg.thread_ts && msg.thread_ts !== msg.ts) {
504
+ // Thread reply — fetch parent + preceding replies
505
+ threadContext = await fetchThreadContext(im.id, msg.thread_ts, msg.ts);
506
+ } else {
507
+ // Standalone DM — fetch recent conversation history
508
+ threadContext = await fetchDMContext(im.id, msg.ts);
509
+ }
510
+
511
+ // Extract and download attachments
512
+ const dmAttachments = extractSlackAttachments(msg.files);
513
+ for (const att of dmAttachments) {
514
+ const file = (msg.files || []).find((f) => f.id === att.id);
515
+ const localPath = await downloadSlackAttachment(file, SLACK_TOKEN);
516
+ if (localPath) att.local_path = localPath;
517
+ }
518
+
519
+ items.push({
520
+ id: msg.ts.replace(".", "-"),
521
+ service: "slack",
522
+ channel: `dm/${resolveName(msg.user)}`,
523
+ channel_id: im.id,
524
+ sender: resolveName(msg.user),
525
+ sender_privilege: resolvePrivilege(msg.user),
526
+ timestamp: new Date(parseFloat(msg.ts) * 1000).toISOString(),
527
+ subject: `DM from ${resolveName(msg.user)}`,
528
+ content: msgText,
529
+ thread_id: msg.thread_ts || "",
530
+ thread_context: threadContext,
531
+ is_reply: !!msg.thread_ts,
532
+ attachments: dmAttachments.length > 0 ? dmAttachments : undefined,
533
+ priority_signals: {
534
+ from_ceo: isCeo,
535
+ tagged_urgent:
536
+ /\b(urgent|emergency|asap|blocker|critical)\b/i.test(msgText),
537
+ contains_deadline: false,
538
+ mentions_agent: true,
539
+ },
540
+ raw_ref: `slack:${im.id}:${msg.ts}`,
541
+ });
542
+ }
543
+
544
+ // Update active threads for this channel (bounded to MAX_THREADS_PER_CHANNEL, most recent first)
545
+ activeThreads[im.id] = [...channelThreads]
546
+ .sort((a, b) => parseFloat(b) - parseFloat(a))
547
+ .slice(0, MAX_THREADS_PER_CHANNEL);
548
+ }
549
+ }
550
+ } catch (err) {
551
+ errors.push(`DM polling: ${err.message}`);
552
+ }
553
+
554
+ // ── Scan active threads (DMs AND channels) for new replies ──
555
+ // This catches thread replies that conversations.history misses.
556
+ // Use a 30-minute lookback window instead of the exact cursor to avoid
557
+ // missing replies that arrived between poll cycles or during processing.
558
+ //
559
+ // Thread scanning rotation: threads in high-priority channels (CEO DMs,
560
+ // critical/high channels) are scanned every cycle. Threads in normal
561
+ // channels rotate in halves to reduce API calls. If the cycle time
562
+ // budget is exceeded, remaining thread scans are skipped.
563
+ const THREAD_LOOKBACK_MS = 30 * 60 * 1000;
564
+ const threadOldest = String((Date.now() - THREAD_LOOKBACK_MS) / 1000);
565
+ const threadOldestTs = parseFloat(threadOldest);
566
+ const threadItems = [];
567
+
568
+ // Pre-load the list of existing inbox files once for fast dedup lookups.
569
+ // The 30-min lookback re-fetches replies that were already processed in
570
+ // previous poll cycles; without this check the daemon re-classifies and
571
+ // re-dispatches the same thread reply every 60 seconds until the reply
572
+ // ages past the lookback window (the original "thread scanning bug").
573
+ const SLACK_INBOX = join(AGENT_REPO_DIR, "state", "inbox", "slack");
574
+ let existingInboxFiles = [];
575
+ try {
576
+ existingInboxFiles = readdirSync(SLACK_INBOX);
577
+ } catch { /* inbox dir may not exist yet */ }
578
+
579
+ // Determine which channels are high-priority for thread scanning.
580
+ // ceoDMChannelIds was populated during DM polling above — no extra API call needed.
581
+ const highPriorityChannelIds = new Set(
582
+ allChannels
583
+ .filter((c) => c.priority === "critical" || c.priority === "high")
584
+ .map((c) => c.id),
585
+ );
586
+
587
+ const threadEntries = Object.entries(activeThreads);
588
+ // Sort: high-priority channels first, then rotate others
589
+ const priorityThreadEntries = threadEntries.filter(
590
+ ([chId]) => highPriorityChannelIds.has(chId) || ceoDMChannelIds.has(chId),
591
+ );
592
+ const normalThreadEntries = threadEntries.filter(
593
+ ([chId]) => !highPriorityChannelIds.has(chId) && !ceoDMChannelIds.has(chId),
594
+ );
595
+ // Rotate normal thread entries in halves
596
+ const rotatedNormalThreadEntries = normalThreadEntries.filter(
597
+ (_, i) => i % 2 === pollCycleCount % 2,
598
+ );
599
+ const threadEntriesToScan = [...priorityThreadEntries, ...rotatedNormalThreadEntries];
600
+
601
+ for (const [channelId, threadTsList] of threadEntriesToScan) {
602
+ if (!threadTsList || threadTsList.length === 0) continue;
603
+
604
+ // Check cycle time budget before scanning this channel's threads
605
+ if (isCycleBudgetExceeded()) {
606
+ errors.push("Cycle time budget exceeded — skipping remaining thread scans");
607
+ break;
608
+ }
609
+
610
+ for (const threadTs of threadTsList) {
611
+ try {
612
+ const repliesResult = await slackApi("conversations.replies", {
613
+ channel: channelId,
614
+ ts: threadTs,
615
+ oldest: threadOldest,
616
+ limit: 20,
617
+ });
618
+
619
+ if (!repliesResult.ok) continue;
620
+
621
+ const replies = repliesResult.messages || [];
622
+
623
+ // ── Thread-level cooldown ──────────────────────────────────
624
+ // If the agent posted ANY reply in this thread within the last
625
+ // THREAD_COOLDOWN_MS, skip the entire thread. This prevents
626
+ // re-ingesting human messages that a running session is already
627
+ // handling, which previously caused cascading holding notes.
628
+ const THREAD_COOLDOWN_MS = 10 * 60 * 1000; // 10 minutes
629
+ const cooldownTs = (Date.now() - THREAD_COOLDOWN_MS) / 1000;
630
+ const agentRepliedRecently = replies.some(
631
+ (r) =>
632
+ r.user === AGENT_USER_ID &&
633
+ parseFloat(r.ts) > cooldownTs,
634
+ );
635
+ if (agentRepliedRecently) continue; // skip entire thread
636
+
637
+ for (const reply of replies) {
638
+ // Skip the parent message itself (ts === thread_ts)
639
+ if (reply.ts === threadTs) continue;
640
+ // Skip bot messages and the agent's own messages
641
+ if (reply.bot_id || reply.user === AGENT_USER_ID) continue;
642
+ // Only process replies within our lookback window
643
+ if (parseFloat(reply.ts) <= threadOldestTs) continue;
644
+
645
+ // Skip messages that the agent already replied to in this thread.
646
+ // If the agent posted a message AFTER this human message, a prior
647
+ // session already handled it — don't re-ingest and duplicate.
648
+ const agentRepliedAfter = replies.some(
649
+ (r) => r.user === AGENT_USER_ID && parseFloat(r.ts) > parseFloat(reply.ts)
650
+ );
651
+ if (agentRepliedAfter) continue;
652
+
653
+ const replyText = reply.text || "";
654
+ const isCeo = reply.user === CEO_USER_ID;
655
+ const replyId = `${reply.ts.replace(".", "-")}-thread_reply`;
656
+
657
+ // Dedup 1: check if this reply is already in the current cycle's items array
658
+ const alreadyQueued = items.some(
659
+ (i) => i.raw_ref === `slack:${channelId}:${reply.ts}`,
660
+ );
661
+ if (alreadyQueued) continue;
662
+
663
+ // Dedup 2: check if an inbox file already exists (active or processed)
664
+ // from a previous poll cycle. The 30-min lookback window exceeds the
665
+ // 15-min lock TTL, so without this the daemon's acquireLock() would
666
+ // re-acquire an expired lock and re-process the same reply.
667
+ const replyIdFragment = replyId;
668
+ const alreadyInInbox = existingInboxFiles.some(
669
+ (f) => f.includes(replyIdFragment),
670
+ );
671
+ if (alreadyInInbox) continue;
672
+
673
+ // Build thread context from the replies we already fetched
674
+ const precedingInThread = replies
675
+ .filter((m) => parseFloat(m.ts) < parseFloat(reply.ts))
676
+ .map((m) => {
677
+ const who = m.user === AGENT_USER_ID ? (loadAgent().firstName || "Agent") : resolveName(m.user);
678
+ return `${who}: ${(m.text || "").substring(0, 500)}`;
679
+ })
680
+ .join("\n");
681
+
682
+ // Extract and download attachments for thread replies
683
+ const replyAttachments = extractSlackAttachments(reply.files);
684
+ for (const att of replyAttachments) {
685
+ const file = (reply.files || []).find((f) => f.id === att.id);
686
+ const localPath = await downloadSlackAttachment(file, SLACK_TOKEN);
687
+ if (localPath) att.local_path = localPath;
688
+ }
689
+
690
+ // Determine if this is a DM thread or a channel thread
691
+ const isDmThread = channelId.startsWith("D");
692
+ const chanInfo = !isDmThread
693
+ ? allChannels.find((c) => c.id === channelId)
694
+ : null;
695
+ const channelLabel = isDmThread
696
+ ? `dm/${resolveName(reply.user)}`
697
+ : chanInfo ? chanInfo.name : channelId;
698
+ const subjectLabel = isDmThread
699
+ ? `DM thread reply from ${resolveName(reply.user)}`
700
+ : `#${channelLabel} thread reply from ${resolveName(reply.user)}`;
701
+
702
+ threadItems.push({
703
+ id: replyId,
704
+ service: "slack",
705
+ channel: channelLabel,
706
+ channel_id: channelId,
707
+ sender: resolveName(reply.user),
708
+ sender_privilege: resolvePrivilege(reply.user),
709
+ timestamp: new Date(parseFloat(reply.ts) * 1000).toISOString(),
710
+ subject: subjectLabel,
711
+ content: replyText,
712
+ thread_id: threadTs,
713
+ thread_context: precedingInThread || null,
714
+ is_reply: true,
715
+ event_type: "thread_reply",
716
+ attachments: replyAttachments.length > 0 ? replyAttachments : undefined,
717
+ priority_signals: {
718
+ from_ceo: isCeo,
719
+ tagged_urgent:
720
+ /\b(urgent|emergency|asap|blocker|critical)\b/i.test(replyText),
721
+ contains_deadline: false,
722
+ mentions_agent: isDmThread || (loadAgent().firstName && new RegExp(loadAgent().firstName, "i").test(replyText)),
723
+ },
724
+ raw_ref: `slack:${channelId}:${reply.ts}`,
725
+ });
726
+ }
727
+ } catch (err) {
728
+ errors.push(`Thread replies ${channelId}/${threadTs}: ${err.message}`);
729
+ }
730
+ }
731
+ }
732
+
733
+ // Merge thread reply items
734
+ items.push(...threadItems);
735
+
736
+ // Persist updated active threads
737
+ try {
738
+ writeFileSync(ACTIVE_THREADS_FILE, JSON.stringify(activeThreads));
739
+ } catch (err) {
740
+ errors.push(`Writing active threads: ${err.message}`);
741
+ }
742
+
743
+ // ── Pick up events-server items ──────────────────────────────────────
744
+ // The comms-monitor.sh + Slack Events API write .json files to inbox.
745
+ // These include @mentions, channel thread replies, and reactions that
746
+ // the conversations.history API doesn't return. Scan for unprocessed
747
+ // .json files and convert them into items for the daemon pipeline.
748
+ const SLACK_INBOX_DIR = join(AGENT_REPO_DIR, "state", "inbox", "slack");
749
+ try {
750
+ const inboxFiles = readdirSync(SLACK_INBOX_DIR)
751
+ .filter((f) => f.endsWith(".json") && !f.endsWith(".processed"))
752
+ .slice(0, 10); // Process up to 10 per cycle
753
+
754
+ for (const file of inboxFiles) {
755
+ try {
756
+ const raw = readFileSync(join(SLACK_INBOX_DIR, file), "utf-8");
757
+ const data = JSON.parse(raw);
758
+
759
+ // Dedup: skip if we already have this item from API polling
760
+ // Events-server JSON uses `slack_event` key; also check `event` for compatibility
761
+ const evtPre = data.slack_event || data.event || {};
762
+ const eventTs = data.ts || data.event_ts || data.received_at || evtPre.ts || "";
763
+ const evtChannel = data.channel_id || data.channel || evtPre.channel || "";
764
+ const eventRef = data.raw_ref || `slack:${evtChannel}:${eventTs}`;
765
+ const alreadyHave = items.some((i) => i.raw_ref === eventRef);
766
+ if (alreadyHave) {
767
+ // Mark as processed since the API poll already got it
768
+ try { renameSync(join(SLACK_INBOX_DIR, file), join(SLACK_INBOX_DIR, file + ".processed")); } catch {}
769
+ continue;
770
+ }
771
+
772
+ // Convert events-server JSON to daemon item format
773
+ // Events-server JSON uses `slack_event` key; also check `event` for compatibility
774
+ const evt = data.slack_event || data.event || {};
775
+ const sender = data.sender || resolveName(data.user || data.user_id || evt.user || "unknown");
776
+
777
+ // Skip the agent's own messages — the events server should filter these,
778
+ // but if any slip through (e.g. message_changed edge cases), catch here
779
+ const userId = data.user || data.user_id || evt.user || "";
780
+ const myFullSlug = (loadAgent().fullName || "").toLowerCase().replace(/\s+/g, "-");
781
+ if (userId === AGENT_USER_ID || (myFullSlug && sender === myFullSlug)) {
782
+ try { renameSync(join(SLACK_INBOX_DIR, file), join(SLACK_INBOX_DIR, file + ".processed")); } catch {}
783
+ continue;
784
+ }
785
+
786
+ const privilege = data.sender_privilege || resolvePrivilege(userId);
787
+ const content = data.content || data.text || evt.text || "";
788
+ const channelId = data.channel_id || data.channel || evt.channel || "";
789
+ const threadTs = data.thread_id || data.thread_ts || evt.thread_ts || "";
790
+
791
+ // Fetch thread context if this is a thread reply
792
+ let threadContext = data.thread_context || null;
793
+ if (!threadContext && threadTs && eventTs && threadTs !== eventTs && channelId) {
794
+ threadContext = await fetchThreadContext(channelId, threadTs, eventTs);
795
+ }
796
+
797
+ // Extract and download attachments from events-server items
798
+ const evtFiles = evt.files || data.files || [];
799
+ const evtAttachments = extractSlackAttachments(evtFiles);
800
+ for (const att of evtAttachments) {
801
+ const fileObj = evtFiles.find((f) => f.id === att.id);
802
+ const localPath = await downloadSlackAttachment(fileObj, SLACK_TOKEN);
803
+ if (localPath) att.local_path = localPath;
804
+ }
805
+
806
+ items.push({
807
+ id: data.id || eventTs.replace(".", "-") || file.replace(".json", ""),
808
+ service: "slack",
809
+ channel: data.channel || channelId,
810
+ channel_id: channelId,
811
+ sender,
812
+ sender_privilege: privilege,
813
+ timestamp: data.timestamp || new Date().toISOString(),
814
+ subject: data.subject || `Slack event`,
815
+ content,
816
+ thread_id: threadTs,
817
+ thread_context: threadContext,
818
+ is_reply: data.is_reply || (!!threadTs && threadTs !== eventTs),
819
+ attachments: evtAttachments.length > 0 ? evtAttachments : undefined,
820
+ priority_signals: data.priority_signals || {
821
+ from_ceo: privilege === "ceo",
822
+ tagged_urgent: /\b(urgent|emergency|asap|blocker|critical)\b/i.test(content),
823
+ contains_deadline: false,
824
+ mentions_agent: loadAgent().firstName ? new RegExp(loadAgent().firstName, "i").test(content) : false,
825
+ },
826
+ raw_ref: eventRef,
827
+ _inbox_file: file,
828
+ });
829
+
830
+ // Mark as processed
831
+ try { renameSync(join(SLACK_INBOX_DIR, file), join(SLACK_INBOX_DIR, file + ".processed")); } catch {}
832
+ } catch (err) {
833
+ errors.push(`Inbox file ${file}: ${err.message}`);
834
+ }
835
+ }
836
+ } catch {
837
+ // Inbox dir doesn't exist — fine
838
+ }
839
+
840
+ for (const item of items) {
841
+ writeInboxItem("slack", item);
842
+ }
843
+
844
+ writeCursor("slack", new Date().toISOString());
845
+
846
+ const cycleDurationMs = Date.now() - cycleStartTime;
847
+ const cycleDurationSec = (cycleDurationMs / 1000).toFixed(1);
848
+ console.log(
849
+ `[slack-poller] Cycle ${pollCycleCount} completed in ${cycleDurationSec}s ` +
850
+ `(${channelsThisCycle.length} channels, ${items.length} items, ${errors.length} errors)`,
851
+ );
852
+
853
+ return { items, errors };
854
+ }