@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,231 @@
1
+ /**
2
+ * inbox-scan-poller.test.mjs — node:test coverage for inbox-scan-poller.mjs
3
+ *
4
+ * Verifies the scanner is the faithful inverse of the inbox YAML writer for the
5
+ * fields the daemon needs, and that it skips already-processed items. Drives
6
+ * the real writer (writeInboxItem) under an injected AGENT_DIR so the round-trip
7
+ * is end-to-end. Hermetic.
8
+ */
9
+
10
+ import { test } from "node:test";
11
+ import assert from "node:assert/strict";
12
+ import { promises as fsp } from "node:fs";
13
+ import { tmpdir } from "node:os";
14
+ import { join } from "node:path";
15
+
16
+ import { parseInboxItemYaml, scanInbox } from "./inbox-scan-poller.mjs";
17
+ import { markDispatched, markInboxProcessed, writeInboxItem } from "./utils.mjs";
18
+ import { eventToInboxItem } from "../../lib/channels/inbox-item.mjs";
19
+
20
+ async function makeRoot() {
21
+ const p = join(tmpdir(), `inbox-scan-test-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`);
22
+ await fsp.mkdir(p, { recursive: true });
23
+ return p;
24
+ }
25
+ async function rmRoot(p) { try { await fsp.rm(p, { recursive: true, force: true }); } catch { /* */ } }
26
+
27
+ test("parseInboxItemYaml recovers the daemon-relevant fields", () => {
28
+ const item = eventToInboxItem({
29
+ channel: "telegram",
30
+ message_id: "4242",
31
+ from: { id: "987", name: "casey" },
32
+ text: "ping the build please",
33
+ timestamp: "2026-06-09T10:00:00.000Z",
34
+ kind: "message",
35
+ sender: "casey",
36
+ sender_privilege: "unknown",
37
+ subject: "DM from casey",
38
+ channel_id: "987",
39
+ channel_label: "dm/casey",
40
+ is_reply: false,
41
+ priority_signals: { from_ceo: false, tagged_urgent: false, contains_deadline: false, mentions_agent: true },
42
+ raw_ref: "telegram:987:4242",
43
+ ingest_source: "telegram",
44
+ });
45
+
46
+ const yaml = renderViaWriter(item);
47
+ const parsed = parseInboxItemYaml(yaml);
48
+ assert.equal(parsed.id, item.id);
49
+ assert.equal(parsed.service, "telegram");
50
+ assert.equal(parsed.channel, "dm/casey");
51
+ assert.equal(parsed.channel_id, "987");
52
+ assert.equal(parsed.sender, "casey");
53
+ assert.equal(parsed.content, "ping the build please");
54
+ assert.equal(parsed.raw_ref, "telegram:987:4242");
55
+ assert.equal(parsed.priority_signals.mentions_agent, true);
56
+ });
57
+
58
+ test("scanInbox reads written items and skips processed siblings", async () => {
59
+ const root = await makeRoot();
60
+ try {
61
+ // Use the real writer against this tmp root via AGENT_DIR.
62
+ const prev = process.env.AGENT_DIR;
63
+ process.env.AGENT_DIR = root;
64
+ // Fresh import so utils.mjs picks up AGENT_DIR=root.
65
+ const utils = await import(`./utils.mjs?scan=${Date.now()}`);
66
+ const mkItem = (id, text) => eventToInboxItem({
67
+ channel: "telegram", message_id: id, from: { id: "987", name: "casey" }, text,
68
+ timestamp: `2026-06-09T10:00:0${id}.000Z`, sender: "casey", channel_id: "987",
69
+ channel_label: "dm/casey", raw_ref: `telegram:987:${id}`, ingest_source: "telegram",
70
+ priority_signals: { mentions_agent: true },
71
+ });
72
+ utils.writeInboxItem("telegram", mkItem("1", "first"));
73
+ utils.writeInboxItem("telegram", mkItem("2", "second"));
74
+
75
+ let res = scanInbox("telegram", { agentRoot: root });
76
+ assert.equal(res.items.length, 2);
77
+ const texts = res.items.map((i) => i.content).sort();
78
+ assert.deepEqual(texts, ["first", "second"]);
79
+
80
+ // Mark one processed (rename) — scanner must skip it.
81
+ const dir = join(root, "state/inbox/telegram");
82
+ const files = (await fsp.readdir(dir)).filter((f) => f.endsWith(".yaml"));
83
+ await fsp.rename(join(dir, files[0]), join(dir, files[0] + ".processed"));
84
+ res = scanInbox("telegram", { agentRoot: root });
85
+ assert.equal(res.items.length, 1, "processed item must not be re-emitted");
86
+
87
+ if (prev === undefined) delete process.env.AGENT_DIR; else process.env.AGENT_DIR = prev;
88
+ } finally { await rmRoot(root); }
89
+ });
90
+
91
+ test("scanInbox returns empty for a missing inbox dir", () => {
92
+ const res = scanInbox("nope", { agentRoot: "/no/such/root" });
93
+ assert.deepEqual(res, { items: [], errors: [] });
94
+ });
95
+
96
+ test("parseInboxItemYaml preserves multi-line content + thread_context", () => {
97
+ const item = eventToInboxItem({
98
+ channel: "telegram", message_id: "9", from: { id: "1", name: "x" },
99
+ text: "line one\nline two", timestamp: "2026-06-09T10:00:00.000Z",
100
+ sender: "x", channel_id: "1", channel_label: "dm/x", raw_ref: "telegram:1:9",
101
+ thread_context: "prev: hi there", ingest_source: "telegram",
102
+ priority_signals: { mentions_agent: true },
103
+ });
104
+ const yaml = renderViaWriter(item);
105
+ const parsed = parseInboxItemYaml(yaml);
106
+ assert.match(parsed.content, /line one/);
107
+ assert.match(parsed.content, /line two/);
108
+ assert.match(parsed.thread_context, /prev: hi there/);
109
+ });
110
+
111
+ // ── F1/H2: durable admission — crash mid-flight must leave the item re-deliverable ──
112
+
113
+ function mkItem(id, text) {
114
+ return eventToInboxItem({
115
+ channel: "telegram", message_id: id, from: { id: "987", name: "casey" }, text,
116
+ timestamp: `2026-06-09T10:00:0${id}.000Z`, sender: "casey", channel_id: "987",
117
+ channel_label: "dm/casey", raw_ref: `telegram:987:${id}`, ingest_source: "telegram",
118
+ priority_signals: { mentions_agent: true },
119
+ });
120
+ }
121
+
122
+ test("markDispatched parks an admitted item as .dispatched so a fresh scan does NOT re-deliver it", async () => {
123
+ const root = await makeRoot();
124
+ try {
125
+ writeInboxItem("telegram", mkItem("1", "hello"), root);
126
+ const renamed = markDispatched(mkItem("1", "hello"), "telegram", root);
127
+ assert.equal(renamed, 1, "the live .yaml must be renamed to .dispatched");
128
+
129
+ // Happy path: a freshly-dispatched item is in-flight; the scanner must NOT
130
+ // re-emit it (otherwise we double-deliver the same message every poll).
131
+ const res = scanInbox("telegram", { agentRoot: root });
132
+ assert.equal(res.items.length, 0, "fresh .dispatched item must not be re-emitted");
133
+ } finally { await rmRoot(root); }
134
+ });
135
+
136
+ test("scanInbox RECLAIMS a stale .dispatched item (crash mid-flight) so it is re-delivered", async () => {
137
+ const root = await makeRoot();
138
+ try {
139
+ writeInboxItem("telegram", mkItem("1", "did you get this?"), root);
140
+ markDispatched(mkItem("1", "did you get this?"), "telegram", root);
141
+
142
+ // Simulate a daemon crash AFTER admission but BEFORE session close: the
143
+ // in-memory queue is gone, the file sits at .dispatched. Advancing the clock
144
+ // past the reclaim window must make the scanner treat it as stale and
145
+ // re-deliver it. (`now` injected to avoid mtime/clock-granularity flake.)
146
+ const res = scanInbox("telegram", { agentRoot: root, reclaimMs: 1000, now: Date.now() + 5000 });
147
+ assert.equal(res.items.length, 1, "stale .dispatched item must be reclaimed for redelivery");
148
+ assert.equal(res.items[0].content, "did you get this?");
149
+
150
+ // And within the window it must NOT be reclaimed (no double-delivery).
151
+ const fresh = scanInbox("telegram", { agentRoot: root, reclaimMs: 60_000, now: Date.now() });
152
+ assert.equal(fresh.items.length, 0, "an in-flight .dispatched item within the window must not be re-delivered");
153
+ } finally { await rmRoot(root); }
154
+ });
155
+
156
+ test("markInboxProcessed retires a .dispatched item to .processed (no redelivery on success)", async () => {
157
+ const root = await makeRoot();
158
+ try {
159
+ writeInboxItem("telegram", mkItem("1", "all done"), root);
160
+ markDispatched(mkItem("1", "all done"), "telegram", root);
161
+ const retired = markInboxProcessed(mkItem("1", "all done"), "telegram", root);
162
+ assert.equal(retired, 1, "the .dispatched file must be renamed to .processed on session close");
163
+
164
+ // Even with a 0ms reclaim window, a .processed item is terminal — never re-emitted.
165
+ const res = scanInbox("telegram", { agentRoot: root, reclaimMs: 0 });
166
+ assert.equal(res.items.length, 0, "processed item is terminal and must not be reclaimed");
167
+ } finally { await rmRoot(root); }
168
+ });
169
+
170
+ test("markInboxProcessed also retires a live .yaml (item never reached dispatch)", async () => {
171
+ const root = await makeRoot();
172
+ try {
173
+ writeInboxItem("telegram", mkItem("1", "ignored item"), root);
174
+ const retired = markInboxProcessed(mkItem("1", "ignored item"), "telegram", root);
175
+ assert.equal(retired, 1, "a live .yaml must also be retirable directly to .processed");
176
+ const res = scanInbox("telegram", { agentRoot: root, reclaimMs: 0 });
177
+ assert.equal(res.items.length, 0);
178
+ } finally { await rmRoot(root); }
179
+ });
180
+
181
+ test("writeInboxItem dedupe treats a .dispatched sibling as already-present (no re-create on thread lookback)", async () => {
182
+ const root = await makeRoot();
183
+ try {
184
+ writeInboxItem("telegram", mkItem("1", "v1"), root);
185
+ markDispatched(mkItem("1", "v1"), "telegram", root);
186
+ // The 30-min thread lookback would re-write the same message; dedupe must
187
+ // suppress it because a .dispatched sibling exists (else the in-flight item
188
+ // gets re-created as a fresh .yaml and double-delivered).
189
+ writeInboxItem("telegram", mkItem("1", "v1"), root);
190
+ const dir = join(root, "state/inbox/telegram");
191
+ const files = await fsp.readdir(dir);
192
+ const live = files.filter((f) => f.endsWith(".yaml"));
193
+ assert.equal(live.length, 0, "no fresh .yaml may be created while a .dispatched sibling exists");
194
+ assert.equal(files.filter((f) => f.endsWith(".dispatched")).length, 1);
195
+ } finally { await rmRoot(root); }
196
+ });
197
+
198
+ // Render an item to YAML exactly as writeInboxItem would, without touching disk
199
+ // for the field-parse tests. We re-implement the tiny call by writing to a temp
200
+ // dir via the real writer and reading it back.
201
+ function renderViaWriter(item) {
202
+ // Inline minimal renderer mirroring scripts/poller/utils.mjs inboxItemToYaml
203
+ // for the fields under test (the real writer is exercised in scanInbox test).
204
+ const content = (item.content || "").replace(/\n/g, "\n ");
205
+ let yaml = `id: "${item.id}"
206
+ service: "${item.service}"
207
+ channel: "${item.channel}"
208
+ sender: "${item.sender}"
209
+ sender_privilege: "${item.sender_privilege}"
210
+ timestamp: "${item.timestamp}"
211
+ subject: "${item.subject || ""}"
212
+ content: |
213
+ ${content}
214
+ thread_id: "${item.thread_id || ""}"
215
+ is_reply: ${item.is_reply || false}
216
+ is_voice_transcript: false
217
+ priority_signals:
218
+ from_ceo: ${item.priority_signals?.from_ceo || false}
219
+ tagged_urgent: ${item.priority_signals?.tagged_urgent || false}
220
+ contains_deadline: ${item.priority_signals?.contains_deadline || false}
221
+ mentions_agent: ${item.priority_signals?.mentions_agent || false}
222
+ raw_ref: "${item.raw_ref || ""}"
223
+ `;
224
+ if (item.thread_context) {
225
+ const tc = item.thread_context.replace(/\n/g, "\n ");
226
+ yaml += `thread_context: |\n ${tc}\n`;
227
+ }
228
+ if (item.channel_id) yaml += `channel_id: "${item.channel_id}"\n`;
229
+ if (item.kind) yaml += `kind: "${item.kind}"\n`;
230
+ return yaml;
231
+ }
@@ -0,0 +1,73 @@
1
+ #!/usr/bin/env node
2
+ // Agent Poller — Lightweight event detection daemon
3
+ // Runs every 60 seconds via macOS launchd
4
+ // Polls Slack, Gmail, Calendar for new items
5
+ // Writes to state/inbox/ for the agent daemon to process
6
+
7
+ import { pollSlack } from "./slack-poller.mjs";
8
+ import { pollGmail } from "./gmail-poller.mjs";
9
+ import { pollCalendar } from "./calendar-poller.mjs";
10
+ import { pollSecondaryGmail } from "./secondary-gmail-poller.mjs";
11
+ import { triggerAgent } from "./trigger.mjs";
12
+ import { isPriorityItem, AGENT_REPO_DIR } from "./utils.mjs";
13
+ import { appendFileSync, mkdirSync } from "fs";
14
+ import { join } from "path";
15
+
16
+ async function main() {
17
+ const timestamp = new Date().toISOString();
18
+ const logDir = join(AGENT_REPO_DIR, "logs", "polling");
19
+ mkdirSync(logDir, { recursive: true });
20
+ const logFile = join(logDir, `${timestamp.split("T")[0]}-poller.jsonl`);
21
+
22
+ let totalItems = 0;
23
+ let totalErrors = 0;
24
+ let priorityTriggered = false;
25
+
26
+ const services = [
27
+ { name: "slack", fn: pollSlack },
28
+ { name: "gmail", fn: pollGmail },
29
+ { name: "calendar", fn: pollCalendar },
30
+ { name: "alex-gmail", fn: pollSecondaryGmail },
31
+ ];
32
+
33
+ for (const svc of services) {
34
+ try {
35
+ const result = await svc.fn();
36
+ totalItems += result.items.length;
37
+ totalErrors += result.errors.length;
38
+
39
+ for (const item of result.items) {
40
+ if (isPriorityItem(item) && !priorityTriggered) {
41
+ triggerAgent(item);
42
+ priorityTriggered = true;
43
+ }
44
+ }
45
+
46
+ if (result.errors.length > 0) {
47
+ console.error(`[${svc.name}] Errors:`, result.errors);
48
+ }
49
+ } catch (err) {
50
+ totalErrors++;
51
+ console.error(`[${svc.name}] Poll failed:`, err.message);
52
+ }
53
+ }
54
+
55
+ appendFileSync(
56
+ logFile,
57
+ JSON.stringify({
58
+ timestamp,
59
+ items_found: totalItems,
60
+ errors: totalErrors,
61
+ priority_triggered: priorityTriggered,
62
+ }) + "\n",
63
+ );
64
+
65
+ if (totalItems > 0) {
66
+ console.log(`[poller] Found ${totalItems} new items`);
67
+ }
68
+ }
69
+
70
+ main().catch((err) => {
71
+ console.error("[poller] Fatal error:", err);
72
+ process.exit(1);
73
+ });
@@ -0,0 +1,285 @@
1
+ #!/usr/bin/env node
2
+ // Intra-Session Inbox Check — Lightweight poll for use within active Claude Code sessions
3
+ // Called every 2 minutes via CronCreate during a session
4
+ // Focuses on:
5
+ // 1. CEO DM channel (D099N1JGKRQ) for immediate-priority messages
6
+ // 2. Critical channels (ceo-office, engineering)
7
+ // 3. Unprocessed inbox items
8
+ // 4. Writing priority triggers when CEO messages detected
9
+ //
10
+ // This complements the 60s launchd poller — it ensures the agent doesn't
11
+ // miss messages while deep in tasks within a session.
12
+
13
+ import { readdirSync, writeFileSync, mkdirSync, appendFileSync, readFileSync } from "fs";
14
+ import { join, dirname } from "path";
15
+ import { fileURLToPath } from "url";
16
+
17
+ const __dirname = dirname(fileURLToPath(import.meta.url));
18
+ const AGENT_REPO_DIR = process.env.AGENT_DIR || join(__dirname, "../..");
19
+
20
+ const SLACK_TOKEN = process.env.SLACK_USER_TOKEN || process.env.SLACK_TOKEN;
21
+
22
+ // Identity loaded from canonical SOT so principal/agent user IDs aren't
23
+ // hardcoded. The CEO_DM_CHANNEL is detected dynamically (channels starting
24
+ // with "D" are IMs); the previous hardcoded value worked only for Robin.
25
+ function loadAgentIdentity() {
26
+ try {
27
+ return JSON.parse(readFileSync(join(AGENT_REPO_DIR, "config/agent.json"), "utf-8"));
28
+ } catch {
29
+ return { slackMemberId: "", firstName: "Agent", principal: {} };
30
+ }
31
+ }
32
+ const _agent = loadAgentIdentity();
33
+ const AGENT_USER_ID = _agent.slackMemberId || "";
34
+ const CEO_USER_ID = _agent.principal?.slackMemberId || "";
35
+ // CEO_DM_CHANNEL is agent-specific (Slack assigns IM channel IDs dynamically).
36
+ // Provide it via env var, otherwise the CEO-DM hot path is skipped on this
37
+ // poll cycle and we rely on the normal channel scan + Slack Events API to
38
+ // catch principal messages.
39
+ const CEO_DM_CHANNEL = process.env.CEO_DM_CHANNEL || "";
40
+
41
+ // Critical channels to check during intra-session polls
42
+ const CRITICAL_CHANNELS = [
43
+ { id: "C0ARASMQE1W", name: "ceo-office" },
44
+ { id: "C099NT6NKK7", name: "engineering" },
45
+ { id: "C097N5R8HUJ", name: "all-company" },
46
+ ];
47
+
48
+ const TRIGGER_DIR = join(AGENT_REPO_DIR, "state", "triggers", "priority");
49
+ const INBOX_DIRS = ["slack", "gmail", "calendar", "sms", "internal"].map((s) =>
50
+ join(AGENT_REPO_DIR, "state", "inbox", s),
51
+ );
52
+ const LOG_DIR = join(AGENT_REPO_DIR, "logs", "polling");
53
+
54
+ async function slackApi(method, params = {}) {
55
+ const url = new URL(`https://slack.com/api/${method}`);
56
+ Object.entries(params).forEach(([k, v]) =>
57
+ url.searchParams.set(k, String(v)),
58
+ );
59
+ const res = await fetch(url, {
60
+ headers: { Authorization: `Bearer ${SLACK_TOKEN}` },
61
+ });
62
+ return res.json();
63
+ }
64
+
65
+ function countFiles(dir, ext = ".yaml") {
66
+ try {
67
+ return readdirSync(dir).filter((f) => f.endsWith(ext)).length;
68
+ } catch {
69
+ return 0;
70
+ }
71
+ }
72
+
73
+ function writePriorityTrigger(source, sender, channel, content, reason) {
74
+ mkdirSync(TRIGGER_DIR, { recursive: true });
75
+ const ts = Date.now();
76
+ const triggerFile = join(TRIGGER_DIR, `ceo-dm-${ts}.yaml`);
77
+ const triggerContent = `type: priority_trigger
78
+ reason: "${reason}"
79
+ source: "${source}"
80
+ sender: "${sender}"
81
+ channel: "${channel}"
82
+ timestamp: "${new Date().toISOString()}"
83
+ detected_by: intra-session-check
84
+ content: |
85
+ ${(content || "").replace(/\n/g, "\n ")}
86
+ `;
87
+ writeFileSync(triggerFile, triggerContent);
88
+ return triggerFile;
89
+ }
90
+
91
+ async function checkCeoDm() {
92
+ if (!SLACK_TOKEN) return { found: false, error: "SLACK_TOKEN not set" };
93
+ if (!CEO_DM_CHANNEL) return { found: false, error: "CEO_DM_CHANNEL not configured" };
94
+
95
+ // Check last 5 minutes of CEO DM channel
96
+ const oldest = String((Date.now() - 5 * 60 * 1000) / 1000);
97
+ try {
98
+ const result = await slackApi("conversations.history", {
99
+ channel: CEO_DM_CHANNEL,
100
+ oldest,
101
+ limit: 10,
102
+ });
103
+
104
+ if (!result.ok) {
105
+ return { found: false, error: result.error };
106
+ }
107
+
108
+ const ceoMessages = (result.messages || []).filter(
109
+ (m) => m.user === CEO_USER_ID && !m.bot_id,
110
+ );
111
+
112
+ if (ceoMessages.length > 0) {
113
+ const latest = ceoMessages[0];
114
+ const principalSlug = (_agent.principal?.fullName || "principal").toLowerCase().replace(/\s+/g, "-");
115
+ const triggerFile = writePriorityTrigger(
116
+ `slack:${CEO_DM_CHANNEL}:${latest.ts}`,
117
+ principalSlug,
118
+ `dm/${principalSlug}`,
119
+ latest.text,
120
+ "CEO DM detected during intra-session poll",
121
+ );
122
+ return {
123
+ found: true,
124
+ count: ceoMessages.length,
125
+ triggerFile,
126
+ latestText: (latest.text || "").substring(0, 100),
127
+ };
128
+ }
129
+
130
+ return { found: false };
131
+ } catch (err) {
132
+ return { found: false, error: err.message };
133
+ }
134
+ }
135
+
136
+ async function checkCriticalChannels() {
137
+ if (!SLACK_TOKEN) return [];
138
+
139
+ const oldest = String((Date.now() - 3 * 60 * 1000) / 1000);
140
+ const newMessages = [];
141
+
142
+ for (const ch of CRITICAL_CHANNELS) {
143
+ try {
144
+ const result = await slackApi("conversations.history", {
145
+ channel: ch.id,
146
+ oldest,
147
+ limit: 5,
148
+ });
149
+
150
+ if (!result.ok) continue;
151
+
152
+ const relevant = (result.messages || []).filter(
153
+ (m) => !m.bot_id && m.user !== AGENT_USER_ID,
154
+ );
155
+
156
+ const agentNameLower = (_agent.firstName || "").toLowerCase();
157
+ if (relevant.length > 0) {
158
+ newMessages.push({
159
+ channel: ch.name,
160
+ count: relevant.length,
161
+ hasCeo: relevant.some((m) => m.user === CEO_USER_ID),
162
+ hasUrgent: relevant.some((m) =>
163
+ /\b(urgent|emergency|asap|blocker|critical)\b/i.test(m.text || ""),
164
+ ),
165
+ hasAgentMention: agentNameLower
166
+ ? relevant.some((m) => (m.text || "").toLowerCase().includes(agentNameLower))
167
+ : false,
168
+ });
169
+
170
+ // If the principal posted in a critical channel, also create a trigger.
171
+ const ceoMsgs = relevant.filter((m) => m.user === CEO_USER_ID);
172
+ if (ceoMsgs.length > 0) {
173
+ const principalSlug = (_agent.principal?.fullName || "principal").toLowerCase().replace(/\s+/g, "-");
174
+ writePriorityTrigger(
175
+ `slack:${ch.id}:${ceoMsgs[0].ts}`,
176
+ principalSlug,
177
+ ch.name,
178
+ ceoMsgs[0].text,
179
+ `Principal message in #${ch.name} during intra-session poll`,
180
+ );
181
+ }
182
+ }
183
+ } catch {
184
+ // Non-fatal — skip channel
185
+ }
186
+ }
187
+
188
+ return newMessages;
189
+ }
190
+
191
+ function checkInbox() {
192
+ let total = 0;
193
+ const breakdown = {};
194
+ for (const dir of INBOX_DIRS) {
195
+ const count = countFiles(dir);
196
+ if (count > 0) {
197
+ const svc = dir.split("/").pop();
198
+ breakdown[svc] = count;
199
+ total += count;
200
+ }
201
+ }
202
+ return { total, breakdown };
203
+ }
204
+
205
+ function checkTriggers() {
206
+ return countFiles(TRIGGER_DIR);
207
+ }
208
+
209
+ async function main() {
210
+ const timestamp = new Date().toISOString();
211
+ const results = {
212
+ timestamp,
213
+ type: "intra_session_check",
214
+ ceo_dm: null,
215
+ critical_channels: [],
216
+ inbox: null,
217
+ triggers: 0,
218
+ };
219
+
220
+ // 1. Check CEO DM — highest priority
221
+ results.ceo_dm = await checkCeoDm();
222
+
223
+ // 2. Check critical channels
224
+ results.critical_channels = await checkCriticalChannels();
225
+
226
+ // 3. Check unprocessed inbox
227
+ results.inbox = checkInbox();
228
+
229
+ // 4. Check existing triggers
230
+ results.triggers = checkTriggers();
231
+
232
+ // Log the check
233
+ mkdirSync(LOG_DIR, { recursive: true });
234
+ const logFile = join(LOG_DIR, `${timestamp.split("T")[0]}-poller.jsonl`);
235
+ appendFileSync(logFile, JSON.stringify(results) + "\n");
236
+
237
+ // Output summary for Claude Code session
238
+ const parts = [];
239
+
240
+ if (results.ceo_dm?.found) {
241
+ parts.push(
242
+ `CEO DM: ${results.ceo_dm.count} new message(s) — PRIORITY TRIGGER CREATED`,
243
+ );
244
+ }
245
+
246
+ if (results.critical_channels.length > 0) {
247
+ const chSummary = results.critical_channels
248
+ .map((c) => {
249
+ const flags = [];
250
+ if (c.hasCeo) flags.push("CEO");
251
+ if (c.hasUrgent) flags.push("URGENT");
252
+ if (c.hasAgentMention) flags.push(`@${_agent.firstName || "Agent"}`);
253
+ const flagStr = flags.length > 0 ? ` [${flags.join(",")}]` : "";
254
+ return `#${c.channel}(${c.count})${flagStr}`;
255
+ })
256
+ .join(", ");
257
+ parts.push(`Channels: ${chSummary}`);
258
+ }
259
+
260
+ if (results.inbox.total > 0) {
261
+ parts.push(`Inbox: ${results.inbox.total} unprocessed`);
262
+ }
263
+
264
+ if (results.triggers > 0) {
265
+ parts.push(`Triggers: ${results.triggers} pending`);
266
+ }
267
+
268
+ if (parts.length === 0) {
269
+ console.log(
270
+ `[inbox-poll] ${timestamp.split("T")[1].split(".")[0]} — Clear`,
271
+ );
272
+ } else {
273
+ console.log(`[inbox-poll] ${parts.join(" | ")}`);
274
+ }
275
+
276
+ // Exit with code 2 if CEO message found (allows caller to detect priority)
277
+ if (results.ceo_dm?.found) {
278
+ process.exit(2);
279
+ }
280
+ }
281
+
282
+ main().catch((err) => {
283
+ console.error("[inbox-poll] Error:", err.message);
284
+ process.exit(1);
285
+ });
@@ -0,0 +1,88 @@
1
+ /**
2
+ * cloud-relay-dedup.mjs — pure payload-parsing + dedup helpers for the Slack
3
+ * cloud-relay client (audit L30).
4
+ *
5
+ * Extracted from scripts/poller/slack-cloud-relay-client.mjs so the parse/dedup
6
+ * logic can be unit-tested in isolation. The client module imports `dotenv` and
7
+ * opens a live SSE connection at load, neither of which a test wants; these two
8
+ * functions are pure (no I/O, no env, no network), so they live here and the
9
+ * client imports them back. The reconnect-replay filesystem dedup this guards
10
+ * was a production fix (commit ccfbaed) — see isReplayInSiblings.
11
+ *
12
+ * Node builtins only (maestro is "type":"module", near-zero-dep by design).
13
+ *
14
+ * @module scripts/poller/lib/cloud-relay-dedup
15
+ */
16
+
17
+ /**
18
+ * Create a stateful Server-Sent-Events parser. Push raw bytes from the relay
19
+ * into `push(chunk)`; it returns an array of complete event payloads (the JSON
20
+ * text from each `data:` block) discovered in that chunk. Per the SSE spec:
21
+ * - an event is terminated by a blank line,
22
+ * - `data:` lines may repeat and are concatenated with newlines,
23
+ * - a single leading space after the colon is stripped,
24
+ * - lines beginning with `:` are comments (the relay uses these as
25
+ * heartbeats) and are ignored,
26
+ * - `event:` / `id:` / `retry:` lines are ignored (the relay never sends
27
+ * them).
28
+ *
29
+ * The parser buffers across chunk boundaries, so an event split mid-line over
30
+ * two `push` calls is still parsed correctly.
31
+ *
32
+ * @returns {{ push(chunk: string): string[] }}
33
+ */
34
+ export function createSseParser() {
35
+ let buffer = "";
36
+ let dataLines = [];
37
+ return {
38
+ push(chunk) {
39
+ buffer += chunk;
40
+ const out = [];
41
+ let nlIdx;
42
+ while ((nlIdx = buffer.indexOf("\n")) >= 0) {
43
+ const line = buffer.slice(0, nlIdx).replace(/\r$/, "");
44
+ buffer = buffer.slice(nlIdx + 1);
45
+ if (line === "") {
46
+ // End of event — flush
47
+ if (dataLines.length > 0) {
48
+ out.push(dataLines.join("\n"));
49
+ dataLines = [];
50
+ }
51
+ continue;
52
+ }
53
+ if (line.startsWith(":")) continue; // comment (heartbeat)
54
+ if (line.startsWith("data:")) {
55
+ dataLines.push(line.slice(5).replace(/^ /, ""));
56
+ }
57
+ // We ignore "event:" / "id:" / "retry:" lines — the relay
58
+ // doesn't emit them.
59
+ }
60
+ return out;
61
+ },
62
+ };
63
+ }
64
+
65
+ /**
66
+ * Decide whether an event with the given Slack timestamp key has already been
67
+ * written to the inbox, based on the existing inbox filenames.
68
+ *
69
+ * Cloud-relay reconnects replay the same event after a process restart (the
70
+ * in-memory seenEventIds Set is reset on restart), so we gate on filesystem
71
+ * state: if any sibling file for this (ts, cloud-relay) tuple already exists —
72
+ * pending OR `.processed` — the event is a replay and must be discarded. This
73
+ * pins the regression fixed in commit ccfbaed (event_id=Ev0B4MAFCX62 replayed
74
+ * 13x across restart cycles).
75
+ *
76
+ * Pure: takes the directory listing as input so it can be tested without I/O.
77
+ *
78
+ * @param {string[]} siblings filenames currently in the inbox directory
79
+ * @param {string} tsKey Slack ts with "." replaced by "-"
80
+ * (e.g. "1700000000-001000")
81
+ * @returns {boolean} true if a matching sibling (pending or processed) exists
82
+ */
83
+ export function isReplayInSiblings(siblings, tsKey) {
84
+ const replaySuffix = `-${tsKey}-cloud-relay.json`;
85
+ return siblings.some(
86
+ (f) => f.endsWith(replaySuffix) || f.endsWith(`${replaySuffix}.processed`),
87
+ );
88
+ }