@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,353 @@
1
+ /**
2
+ * Maestro — workspace email channel adapter (`orgmail`)
3
+ *
4
+ * The Cohort-hosted mailbox lane (companion spec 2026-07-21 §9): every AI
5
+ * member gets a real address at the org's verified domain; inbound mail is
6
+ * persisted server-side and delivered here **entirely over org RPC** — no
7
+ * IMAP, no app passwords, no provider creds on the mini. DISTINCT from the
8
+ * legacy personal-Gmail lane (`gmail` — scripts/poller/gmail-poller.mjs),
9
+ * which keeps serving plain Google accounts; the two coexist (separate inbox
10
+ * dirs: state/inbox/orgmail/ vs state/inbox/gmail/).
11
+ *
12
+ * Mechanics (mirrors lib/channels/telegram/adapter.mjs structurally):
13
+ * - `_connect` — poll loop, default 45s (the messaging-inbound cadence):
14
+ * email.inbox {unreadOnly:true, limit:50} → per summary (newest-LAST) →
15
+ * email.message {id} → onRaw(fullDto) → on clean handoff + `mark_read`,
16
+ * email.markRead {messageId}. **NO local cursor file** — markRead IS the
17
+ * cursor, server-side. At-least-once by design: a failed markRead simply
18
+ * re-delivers next tick; duplicates are absorbed downstream (inbox-item id
19
+ * = EmailMessage.id + the agent-daemon's dedup locks and claims).
20
+ * - `_toEvent` — MessageEvent with the subject PREFIXED into `text` (so it
21
+ * survives every downstream shape) AND set as `ev.subject` (eventToInboxItem
22
+ * sanitizes + carries the item-shape subject field).
23
+ * - `_send` — emailSend({threadId, text, idempotencyId}) — deliberately no
24
+ * to/subject: hq's reply rule derives recipients from the thread's newest
25
+ * delivered inbound message and D5(b) covers those senders per-recipient,
26
+ * so the canonical quick-reply needs no approval. An approval_required
27
+ * result maps to {ok:false} pointing at scripts/org/send-orgmail.mjs.
28
+ * Inherited send() supplies hook-bus beforeSend + send-gate + reply-storm
29
+ * perimeter for free.
30
+ * - `pairingMode: "open"` — external senders are inherently unknown; the
31
+ * authorization boundary is Cohort-side (mailbox routing, quarantine
32
+ * caps, auto-submitted loop guards, first-contact send approvals). A
33
+ * deny-gate here would park every customer email in pairing limbo. No
34
+ * `internalDomains` machinery either: workspace-mail senders are genuinely
35
+ * external.
36
+ * - No `_setTyping` / `_react` (email has neither; BaseAdapter no-ops are
37
+ * correct).
38
+ *
39
+ * Credentials come ONLY from config/org.yaml (`org.cohort.*` — the enrollment
40
+ * SoT); the gate file config/orgmail.yaml enables the platform.
41
+ *
42
+ * @module lib/channels/orgmail/adapter
43
+ */
44
+
45
+ "use strict";
46
+
47
+ import { CHANNEL_CAPABILITIES } from "../channel.mjs";
48
+ import { definePlatform } from "../contract.mjs";
49
+ import { BaseAdapter, CHANNEL_COUNTERS } from "../base-adapter.mjs";
50
+ import { randomUUID } from "node:crypto";
51
+ import { loadOrgConfig, configFromAgent } from "../../org/client.mjs";
52
+ import { emailSend, emailMarkRead, emailInbox, emailMessage } from "../../org/ui-parity.mjs";
53
+
54
+ export const ORGMAIL_CAPABILITIES = [
55
+ CHANNEL_CAPABILITIES.SEND,
56
+ CHANNEL_CAPABILITIES.THREAD,
57
+ "attachments:metadata",
58
+ ];
59
+
60
+ /** Default poll cadence — the messaging-inbound rhythm. */
61
+ export const DEFAULT_POLL_MS = 45_000;
62
+
63
+ export class OrgMailAdapter extends BaseAdapter {
64
+ /**
65
+ * @param {object} opts
66
+ * @param {object} [opts.config] parsed config/orgmail.yaml ({orgmail:{...}} or flat)
67
+ * @param {object} [opts.orgConfig] parsed config/org.yaml (else loaded from agentRoot)
68
+ * @param {Function} [opts.fetchImpl] injected fetch (tests)
69
+ * @param {number} [opts.pollMs] poll override (else MAESTRO_ORGMAIL_POLL_MS / config poll_seconds)
70
+ */
71
+ constructor(opts = {}) {
72
+ super({
73
+ name: "orgmail",
74
+ capabilities: ORGMAIL_CAPABILITIES,
75
+ // Companion decision: external senders are inherently unknown — the
76
+ // authorization boundary is server-side. See module doc.
77
+ pairingMode: "open",
78
+ sendGate: true,
79
+ supervise: true,
80
+ ...opts,
81
+ // orgmail's health is the RPC probe below, not a socket; the reconnect
82
+ // supervisor polls _connectionHealthy which we leave state-based.
83
+ });
84
+ this._fetchImpl = opts.fetchImpl;
85
+ this._orgConfig = opts.orgConfig || null;
86
+ const gate = normalizeGateConfig(this.config);
87
+ this._markRead = gate.mark_read !== false;
88
+ this._pollMs = resolvePollMs(opts.pollMs, gate, process.env);
89
+ this._timer = null;
90
+ this._polling = false;
91
+ }
92
+
93
+ /** Resolve {base, token, orgId} from config/org.yaml (the enrollment SoT). */
94
+ _org() {
95
+ const cfg = this._orgConfig || loadOrgConfig(this.agentRoot);
96
+ const c = configFromAgent(cfg);
97
+ return { base: c.base, token: c.token, orgId: c.orgId, fetchImpl: this._fetchImpl };
98
+ }
99
+
100
+ // -- connect: RPC poll loop ------------------------------------------------
101
+
102
+ async _connect({ onRaw, signal }) {
103
+ const tick = async () => {
104
+ if (this._polling || this._stopping) return;
105
+ this._polling = true;
106
+ try {
107
+ await this._pollOnce(onRaw);
108
+ } catch (err) {
109
+ // Poll errors → log + next tick; repeated failure surfaces via the
110
+ // reconnect supervisor's health poll.
111
+ this.log("warn", `orgmail poll failed: ${err && err.message}`);
112
+ } finally {
113
+ this._polling = false;
114
+ }
115
+ };
116
+
117
+ if (signal) {
118
+ signal.addEventListener?.("abort", () => {
119
+ if (this._timer) this._clearInterval(this._timer);
120
+ this._timer = null;
121
+ });
122
+ }
123
+
124
+ await tick(); // first sweep immediately so start() sees a live adapter
125
+ this._timer = this._setInterval(tick, this._pollMs);
126
+ if (this._timer && typeof this._timer.unref === "function") this._timer.unref();
127
+ }
128
+
129
+ /**
130
+ * One inbox sweep: list unread summaries, fetch each full DTO (newest LAST
131
+ * so downstream sees chronological order), hand off, then markRead ONLY on a
132
+ * clean handoff — a throwing onRaw leaves the item unread and it re-delivers
133
+ * next tick (at-least-once).
134
+ */
135
+ async _pollOnce(onRaw) {
136
+ const o = this._org();
137
+ if (!o.base || !o.token) return; // not enrolled — dormant, never throws
138
+ const inbox = await emailInbox({ unreadOnly: true, limit: 50 }, o);
139
+ if (!inbox.ok) {
140
+ this.log("warn", `email.inbox error: ${inbox.error?.code || "?"} ${inbox.error?.message || ""}`);
141
+ return;
142
+ }
143
+ const summaries = Array.isArray(inbox.result?.messages) ? inbox.result.messages : [];
144
+ // Server returns newest-first (keyset createdAt desc); we deliver newest-LAST.
145
+ const ordered = [...summaries].reverse();
146
+ for (const summary of ordered) {
147
+ if (this._stopping) return;
148
+ const id = summary && summary.id;
149
+ if (!id) continue;
150
+ const full = await emailMessage({ id }, o);
151
+ if (!full.ok) {
152
+ this.log("warn", `email.message ${id} error: ${full.error?.code || "?"}`);
153
+ continue;
154
+ }
155
+ let clean = true;
156
+ try {
157
+ onRaw(full.result);
158
+ } catch (err) {
159
+ clean = false; // leave unread → re-delivered next tick
160
+ this.log("error", `orgmail handoff threw (item stays unread): ${err && err.message}`);
161
+ }
162
+ if (clean && this._markRead) {
163
+ const marked = await emailMarkRead({ messageId: id }, { ...o, idempotencyKey: `orgmail-read:${id}` });
164
+ if (!marked.ok) this.log("warn", `email.markRead ${id} failed (will re-deliver): ${marked.error?.code || "?"}`);
165
+ }
166
+ }
167
+ }
168
+
169
+ /**
170
+ * UNLIKE the base (which swallows onInbound errors to protect a live socket
171
+ * loop), the poll loop WANTS the throw: `_pollOnce`'s clean-handoff detection
172
+ * must see a failed handoff so markRead — the server-side cursor — does not
173
+ * advance past an undelivered item (at-least-once). Own/peer drops keep the
174
+ * base semantics (a drop is a CLEAN outcome: the cursor advances). A
175
+ * `_toEvent` translation throw is also clean by construction (the base onRaw
176
+ * catches it before reaching here) — a poison item is marked read rather
177
+ * than re-delivered forever.
178
+ */
179
+ _handleEvent(ev) {
180
+ if (this._isOwnOrPeer(ev)) {
181
+ this._count(CHANNEL_COUNTERS.PEER_AGENT_DROPPED, { from: this._eventSenderId(ev) });
182
+ this.log("debug", "dropping own/peer-agent inbound (no auto-reply)");
183
+ return;
184
+ }
185
+ // pairingMode "open": no pairing gate (authorization is Cohort-side).
186
+ this._onInbound(ev);
187
+ }
188
+
189
+ // -- translate: EmailMessageDTO → MessageEvent -----------------------------
190
+
191
+ _toEvent(raw) {
192
+ if (!raw || typeof raw !== "object" || !raw.id) return null;
193
+ const subject = typeof raw.subject === "string" ? raw.subject : "";
194
+ const bodyText =
195
+ typeof raw.text === "string" && raw.text
196
+ ? raw.text
197
+ : htmlToText(typeof raw.html === "string" ? raw.html : "");
198
+ const from = String(raw.from || "");
199
+ const threadId = String(raw.threadId || "");
200
+ const timestamp = raw.receivedAt || raw.createdAt || new Date().toISOString();
201
+ const attachments = Array.isArray(raw.attachments)
202
+ ? raw.attachments.map((a) => ({
203
+ type: "document",
204
+ mime: a?.contentType || null,
205
+ filename: a?.filename || null,
206
+ size_bytes: a?.size || 0,
207
+ id: a?.id,
208
+ }))
209
+ : undefined;
210
+
211
+ return {
212
+ channel: "orgmail",
213
+ service: "orgmail",
214
+ kind: "message",
215
+ message_id: String(raw.id),
216
+ thread_id: threadId,
217
+ from: { id: from, display: from, name: from },
218
+ to: { id: String(raw.mailbox?.address || "") },
219
+ // Subject prefixed so it survives every downstream shape; ev.subject also
220
+ // set — eventToInboxItem sanitizes and carries the item-shape field.
221
+ text: `Subject: ${subject}\n\n${bodyText}`,
222
+ subject,
223
+ attachments: attachments && attachments.length ? attachments : undefined,
224
+ timestamp,
225
+ in_reply_to: raw.inReplyTo || undefined,
226
+ raw,
227
+ sender: from,
228
+ sender_privilege: "external",
229
+ channel_id: threadId,
230
+ channel_label: `email/${from}`,
231
+ is_reply: !!raw.inReplyTo,
232
+ priority_signals: {
233
+ from_ceo: false,
234
+ tagged_urgent: /\b(urgent|emergency|asap|critical)\b/i.test(subject),
235
+ contains_deadline: false,
236
+ mentions_agent: true, // mail to the agent's own mailbox is always directed
237
+ },
238
+ raw_ref: `orgmail:${threadId}:${raw.id}`,
239
+ ingest_source: "orgmail",
240
+ thread_context: null,
241
+ source: {
242
+ channel: "orgmail",
243
+ chatId: threadId,
244
+ replyToRef: String(raw.id),
245
+ userScope: "dm",
246
+ peerId: from,
247
+ },
248
+ };
249
+ }
250
+
251
+ // -- outbound --------------------------------------------------------------
252
+
253
+ async _send(out) {
254
+ const o = this._org();
255
+ if (!o.base || !o.token) return { ok: false, error: "orgmail: org enrollment missing (config/org.yaml)" };
256
+ const threadId = out?.source?.chatId || out?.thread_ref || out?.to;
257
+ if (!threadId) return { ok: false, error: "orgmail._send: thread id required (out.source.chatId)" };
258
+ const frame = await emailSend(
259
+ { threadId: String(threadId), text: out.text || "", idempotencyId: randomUUID() },
260
+ { ...o, idempotencyKey: randomUUID() },
261
+ );
262
+ if (!frame.ok) {
263
+ return { ok: false, error: `${frame.error?.code || "ERROR"}: ${frame.error?.message || "email.send failed"}` };
264
+ }
265
+ if (frame.result?.status === "approval_required") {
266
+ return {
267
+ ok: false,
268
+ error:
269
+ "first-contact approval required — run scripts/org/send-orgmail.mjs --request-approval " +
270
+ "(the recipient set includes an address this mailbox has never corresponded with)",
271
+ raw: frame.result,
272
+ };
273
+ }
274
+ return { ok: true, message_id: String(frame.result?.messageId || ""), raw: frame.result };
275
+ }
276
+
277
+ // -- health ---------------------------------------------------------------
278
+
279
+ /**
280
+ * One email.inbox {limit:1} round-trip (the same probe the doctor reuses).
281
+ * Family-absent / unreachable / unpaired all surface the error frame's
282
+ * message; the reconnect supervisor handles transient RPC outages.
283
+ */
284
+ async healthCheck() {
285
+ const o = this._org();
286
+ if (!o.base || !o.token) return { ok: false, detail: "orgmail: org enrollment missing (config/org.yaml)" };
287
+ const r = await emailInbox({ limit: 1 }, o);
288
+ if (r.ok) {
289
+ const addr = r.result?.mailbox?.address || "";
290
+ return { ok: true, detail: `orgmail mailbox ${addr || "reachable"}` };
291
+ }
292
+ return { ok: false, detail: `orgmail: ${r.error?.code || "?"} ${r.error?.message || "unreachable"}` };
293
+ }
294
+
295
+ async stop() {
296
+ await super.stop();
297
+ if (this._timer) this._clearInterval(this._timer);
298
+ this._timer = null;
299
+ }
300
+ }
301
+
302
+ // ── Helpers ──────────────────────────────────────────────────────────────
303
+
304
+ /** Accept {orgmail:{...}} (the gate-file shape) or a flat config object. */
305
+ function normalizeGateConfig(config) {
306
+ if (config && typeof config === "object" && config.orgmail && typeof config.orgmail === "object") {
307
+ return config.orgmail;
308
+ }
309
+ return config && typeof config === "object" ? config : {};
310
+ }
311
+
312
+ /** Poll interval: explicit opt → MAESTRO_ORGMAIL_POLL_MS → poll_seconds → 45s. */
313
+ export function resolvePollMs(explicit, gate = {}, env = {}) {
314
+ if (Number.isFinite(explicit) && explicit > 0) return explicit;
315
+ const envMs = Number(env.MAESTRO_ORGMAIL_POLL_MS);
316
+ if (Number.isFinite(envMs) && envMs > 0) return envMs;
317
+ const cfgS = Number(gate.poll_seconds);
318
+ if (Number.isFinite(cfgS) && cfgS > 0) return cfgS * 1000;
319
+ return DEFAULT_POLL_MS;
320
+ }
321
+
322
+ /** Minimal HTML → text (same approach as the IMAP poller's stripHtml). */
323
+ export function htmlToText(html) {
324
+ if (typeof html !== "string" || !html) return "";
325
+ let text = html.replace(/<(style|script)[^>]*>[\s\S]*?<\/\1>/gi, "");
326
+ text = text.replace(/<br\s*\/?>/gi, "\n");
327
+ text = text.replace(/<\/(p|div|tr|li|h[1-6])>/gi, "\n");
328
+ text = text.replace(/<[^>]+>/g, "");
329
+ text = text.replace(/&nbsp;/g, " ").replace(/&amp;/g, "&");
330
+ text = text.replace(/&lt;/g, "<").replace(/&gt;/g, ">");
331
+ text = text.replace(/&quot;/g, '"').replace(/&#39;/g, "'");
332
+ text = text.replace(/&\w+;/g, "");
333
+ text = text.replace(/[ \t]+/g, " ");
334
+ text = text.replace(/\n{3,}/g, "\n\n");
335
+ return text.trim();
336
+ }
337
+
338
+ /** Factory matching the channel/platform contract. */
339
+ export function createOrgMailChannel(opts = {}) {
340
+ return new OrgMailAdapter(opts);
341
+ }
342
+
343
+ // Self-register so the daemon's generic platform loop discovers orgmail by the
344
+ // presence of config/orgmail.yaml (no core edit). Daemon-managed — NOT in
345
+ // DEDICATED_PLIST_PLATFORMS.
346
+ definePlatform({
347
+ name: "orgmail",
348
+ gateFile: "config/orgmail.yaml",
349
+ factory: createOrgMailChannel,
350
+ capabilities: ORGMAIL_CAPABILITIES,
351
+ });
352
+
353
+ export default createOrgMailChannel;
@@ -0,0 +1,311 @@
1
+ /**
2
+ * orgmail adapter tests — the workspace email channel over org RPC.
3
+ *
4
+ * The hq side is MOCKED the way the other adapter tests mock their provider
5
+ * (telegram injects a fake grammY bot; here we inject a fake fetchImpl that
6
+ * routes /api/v1/email.* to canned res frames) — no network, no timers left
7
+ * running (supervise:false + injected interval fns; ticks driven by hand).
8
+ *
9
+ * Run: node --test lib/channels/orgmail/adapter.test.mjs
10
+ */
11
+
12
+ "use strict";
13
+
14
+ import { test } from "node:test";
15
+ import assert from "node:assert/strict";
16
+
17
+ import {
18
+ OrgMailAdapter,
19
+ createOrgMailChannel,
20
+ resolvePollMs,
21
+ htmlToText,
22
+ DEFAULT_POLL_MS,
23
+ ORGMAIL_CAPABILITIES,
24
+ } from "./adapter.mjs";
25
+ import { getPlatform, isDaemonManagedPlatform } from "../contract.mjs";
26
+
27
+ const ORG_CFG = { org: { cohort: { enabled: true, base: "https://org.example", orgId: "acme", token: "tok-1" } } };
28
+
29
+ /**
30
+ * A fake hq: routes the five email.* endpoints. `inboxPages` is a queue of
31
+ * summary arrays (one per poll tick); `messages` maps id → full DTO.
32
+ */
33
+ function hqMock({ inboxPages = [[]], messages = {}, sendBody, markReadFails = false } = {}) {
34
+ const pages = [...inboxPages];
35
+ const calls = [];
36
+ const fn = async (url, init) => {
37
+ const u = String(url);
38
+ const body = init && init.body ? JSON.parse(init.body) : {};
39
+ calls.push({ url: u, body, headers: (init && init.headers) || {} });
40
+ const respond = (payload, status = 200, ok = true) => ({ ok, status, json: async () => payload, headers: { get: () => undefined } });
41
+ if (u.endsWith("/api/v1/email.inbox")) {
42
+ const page = pages.length ? pages.shift() : [];
43
+ return respond({ ok: true, result: { mailbox: { address: "astra@agents.example", status: "ACTIVE" }, messages: page, nextCursor: null } });
44
+ }
45
+ if (u.endsWith("/api/v1/email.message")) {
46
+ const dto = messages[body.id];
47
+ return dto ? respond({ ok: true, result: dto }) : respond({ ok: false, error: { code: "NOT_FOUND", message: "no row" } }, 404, false);
48
+ }
49
+ if (u.endsWith("/api/v1/email.markRead")) {
50
+ if (markReadFails) return respond({ ok: false, error: { code: "INTERNAL", message: "boom" } }, 500, false);
51
+ return respond({ ok: true, result: { updated: 1 } });
52
+ }
53
+ if (u.endsWith("/api/v1/email.send")) {
54
+ return respond(sendBody || { ok: true, result: { status: "queued", messageId: "out-1", threadId: body.threadId } });
55
+ }
56
+ return respond({ ok: false, error: { code: "NOT_FOUND", message: `unknown ${u}` } }, 404, false);
57
+ };
58
+ fn.calls = calls;
59
+ fn.callsTo = (suffix) => calls.filter((c) => c.url.endsWith(suffix));
60
+ return fn;
61
+ }
62
+
63
+ const DTO = (over = {}) => ({
64
+ id: "em-1",
65
+ threadId: "th-1",
66
+ direction: "INBOUND",
67
+ status: "RECEIVED",
68
+ from: "casey@customer.com",
69
+ to: ["astra@agents.example"],
70
+ subject: "Quick pricing question",
71
+ text: "How much for 50 seats?",
72
+ html: null,
73
+ receivedAt: "2026-07-21T01:02:03.000Z",
74
+ createdAt: "2026-07-21T01:02:04.000Z",
75
+ inReplyTo: null,
76
+ attachments: [{ id: "att-1", filename: "specs.pdf", contentType: "application/pdf", size: 1234 }],
77
+ ...over,
78
+ });
79
+
80
+ function makeAdapter(fetchImpl, over = {}) {
81
+ const intervals = [];
82
+ const a = new OrgMailAdapter({
83
+ agentRoot: "/tmp/orgmail-test",
84
+ orgConfig: ORG_CFG,
85
+ config: { orgmail: { enabled: true, poll_seconds: 45, mark_read: true, ...(over.gate || {}) } },
86
+ fetchImpl,
87
+ supervise: false,
88
+ sendGate: over.sendGate ?? false,
89
+ screenOutbound: over.screenOutbound,
90
+ setInterval: (fn, ms) => { intervals.push({ fn, ms }); return { unref() {} }; },
91
+ clearInterval: () => {},
92
+ log: () => {},
93
+ ...over.opts,
94
+ });
95
+ return { a, intervals };
96
+ }
97
+
98
+ const settle = () => new Promise((r) => setTimeout(r, 5));
99
+
100
+ // ── registration ───────────────────────────────────────────────────────────
101
+
102
+ test("platform self-registration: orgmail gated on config/orgmail.yaml, daemon-managed", () => {
103
+ const def = getPlatform("orgmail");
104
+ assert.ok(def, "definePlatform ran at import");
105
+ assert.equal(def.gateFile, "config/orgmail.yaml");
106
+ assert.deepEqual(def.capabilities, ORGMAIL_CAPABILITIES);
107
+ assert.equal(typeof def.factory, "function");
108
+ assert.equal(isDaemonManagedPlatform("orgmail"), true, "NOT in DEDICATED_PLIST_PLATFORMS");
109
+ const inst = createOrgMailChannel({ agentRoot: "/tmp", orgConfig: ORG_CFG, supervise: false, log: () => {} });
110
+ assert.equal(inst.name, "orgmail");
111
+ });
112
+
113
+ // ── config helpers ─────────────────────────────────────────────────────────
114
+
115
+ test("resolvePollMs: explicit > env > poll_seconds > 45s default", () => {
116
+ assert.equal(resolvePollMs(1000, { poll_seconds: 10 }, { MAESTRO_ORGMAIL_POLL_MS: "2000" }), 1000);
117
+ assert.equal(resolvePollMs(undefined, { poll_seconds: 10 }, { MAESTRO_ORGMAIL_POLL_MS: "2000" }), 2000);
118
+ assert.equal(resolvePollMs(undefined, { poll_seconds: 10 }, {}), 10_000);
119
+ assert.equal(resolvePollMs(undefined, {}, {}), DEFAULT_POLL_MS);
120
+ });
121
+
122
+ test("htmlToText strips markup, decodes entities, preserves paragraph breaks", () => {
123
+ const text = htmlToText("<div><p>Hello &amp; hi</p><br><script>evil()</script><p>Second&nbsp;line</p></div>");
124
+ assert.match(text, /Hello & hi/);
125
+ assert.match(text, /Second line/);
126
+ assert.ok(!text.includes("evil"), "script bodies stripped");
127
+ });
128
+
129
+ // ── poll loop ──────────────────────────────────────────────────────────────
130
+
131
+ test("poll: inbox summaries → per-item email.message → onRaw → markRead on clean handoff", async () => {
132
+ const fetchImpl = hqMock({
133
+ inboxPages: [[{ id: "em-2" }, { id: "em-1" }]], // server newest-first
134
+ messages: { "em-1": DTO({ id: "em-1" }), "em-2": DTO({ id: "em-2", subject: "Follow-up" }) },
135
+ });
136
+ const { a } = makeAdapter(fetchImpl);
137
+ const inbound = [];
138
+ await a.start({ onInbound: (ev) => inbound.push(ev) });
139
+ await settle();
140
+
141
+ assert.equal(fetchImpl.callsTo("/api/v1/email.inbox").length, 1);
142
+ assert.deepEqual(fetchImpl.callsTo("/api/v1/email.inbox")[0].body, { unreadOnly: true, limit: 50 });
143
+ assert.equal(fetchImpl.callsTo("/api/v1/email.message").length, 2, "one body fetch per summary");
144
+ assert.equal(inbound.length, 2, "both events delivered (pairingMode open)");
145
+ assert.deepEqual(inbound.map((e) => e.message_id), ["em-1", "em-2"], "newest LAST (chronological)");
146
+ const marked = fetchImpl.callsTo("/api/v1/email.markRead");
147
+ assert.deepEqual(marked.map((c) => c.body), [{ messageId: "em-1" }, { messageId: "em-2" }], "markRead per clean handoff — the server-side cursor");
148
+ await a.stop();
149
+ });
150
+
151
+ test("poll: a throwing handoff leaves the item UNREAD → re-delivered next tick (at-least-once)", async () => {
152
+ const fetchImpl = hqMock({
153
+ inboxPages: [[{ id: "em-1" }], [{ id: "em-1" }]], // still unread on tick 2
154
+ messages: { "em-1": DTO() },
155
+ });
156
+ const { a, intervals } = makeAdapter(fetchImpl);
157
+ let calls = 0;
158
+ await a.start({
159
+ onInbound: () => {
160
+ calls += 1;
161
+ if (calls === 1) throw new Error("classifier down");
162
+ },
163
+ });
164
+ await settle();
165
+ assert.equal(fetchImpl.callsTo("/api/v1/email.markRead").length, 0, "no markRead after a throwing handoff");
166
+ await intervals[0].fn(); // drive tick 2 by hand
167
+ await settle();
168
+ assert.equal(calls, 2, "item re-delivered");
169
+ assert.equal(fetchImpl.callsTo("/api/v1/email.markRead").length, 1, "clean second handoff marks read");
170
+ await a.stop();
171
+ });
172
+
173
+ test("poll: mark_read:false leaves items unread", async () => {
174
+ const fetchImpl = hqMock({ inboxPages: [[{ id: "em-1" }]], messages: { "em-1": DTO() } });
175
+ const { a } = makeAdapter(fetchImpl, { gate: { mark_read: false } });
176
+ const inbound = [];
177
+ await a.start({ onInbound: (ev) => inbound.push(ev) });
178
+ await settle();
179
+ assert.equal(inbound.length, 1);
180
+ assert.equal(fetchImpl.callsTo("/api/v1/email.markRead").length, 0);
181
+ await a.stop();
182
+ });
183
+
184
+ test("poll: a failed markRead is logged, not thrown (re-delivery next tick)", async () => {
185
+ const fetchImpl = hqMock({ inboxPages: [[{ id: "em-1" }]], messages: { "em-1": DTO() }, markReadFails: true });
186
+ const { a } = makeAdapter(fetchImpl);
187
+ const inbound = [];
188
+ await a.start({ onInbound: (ev) => inbound.push(ev) });
189
+ await settle();
190
+ assert.equal(inbound.length, 1, "delivery still happened");
191
+ await a.stop();
192
+ });
193
+
194
+ test("poll: unenrolled org (no token) is dormant — zero network, zero throw", async () => {
195
+ const fetchImpl = hqMock();
196
+ const a = new OrgMailAdapter({
197
+ agentRoot: "/tmp/orgmail-none", orgConfig: { org: {} }, fetchImpl,
198
+ supervise: false, sendGate: false, log: () => {},
199
+ setInterval: () => ({ unref() {} }), clearInterval: () => {},
200
+ });
201
+ await a.start({ onInbound: () => {} });
202
+ await settle();
203
+ assert.equal(fetchImpl.calls.length, 0);
204
+ await a.stop();
205
+ });
206
+
207
+ // ── _toEvent ───────────────────────────────────────────────────────────────
208
+
209
+ test("_toEvent: subject-prefixed text, subject field, ids, source, attachments metadata", () => {
210
+ const { a } = makeAdapter(hqMock());
211
+ const ev = a._toEvent(DTO());
212
+ assert.equal(ev.channel, "orgmail");
213
+ assert.equal(ev.kind, "message");
214
+ assert.equal(ev.message_id, "em-1");
215
+ assert.equal(ev.thread_id, "th-1");
216
+ assert.equal(ev.text, "Subject: Quick pricing question\n\nHow much for 50 seats?");
217
+ assert.equal(ev.subject, "Quick pricing question", "item-shape subject carried");
218
+ assert.equal(ev.from.id, "casey@customer.com");
219
+ assert.equal(ev.timestamp, "2026-07-21T01:02:03.000Z", "receivedAt preferred");
220
+ assert.deepEqual(ev.source, {
221
+ channel: "orgmail", chatId: "th-1", replyToRef: "em-1", userScope: "dm", peerId: "casey@customer.com",
222
+ });
223
+ assert.equal(ev.attachments.length, 1);
224
+ assert.equal(ev.attachments[0].filename, "specs.pdf");
225
+ assert.equal(ev.raw_ref, "orgmail:th-1:em-1");
226
+ });
227
+
228
+ test("_toEvent: html-only body converts to text; in_reply_to mapped", () => {
229
+ const { a } = makeAdapter(hqMock());
230
+ const ev = a._toEvent(DTO({ text: null, html: "<p>Hello <b>there</b></p>", inReplyTo: "<rfc-1@x>" }));
231
+ assert.match(ev.text, /Hello there/);
232
+ assert.equal(ev.in_reply_to, "<rfc-1@x>");
233
+ assert.equal(ev.is_reply, true);
234
+ });
235
+
236
+ test("_toEvent: junk frames drop (null)", () => {
237
+ const { a } = makeAdapter(hqMock());
238
+ assert.equal(a._toEvent(null), null);
239
+ assert.equal(a._toEvent({}), null);
240
+ });
241
+
242
+ // ── _send ──────────────────────────────────────────────────────────────────
243
+
244
+ test("_send: thread reply rides email.send {threadId, text, idempotencyId} — no to/subject (server reply rule)", async () => {
245
+ const fetchImpl = hqMock();
246
+ const { a } = makeAdapter(fetchImpl);
247
+ const res = await a._send({ text: "On it — reply drafted.", source: { channel: "orgmail", chatId: "th-9" } });
248
+ assert.equal(res.ok, true);
249
+ assert.equal(res.message_id, "out-1");
250
+ const sent = fetchImpl.callsTo("/api/v1/email.send");
251
+ assert.equal(sent.length, 1);
252
+ assert.equal(sent[0].body.threadId, "th-9");
253
+ assert.equal(sent[0].body.text, "On it — reply drafted.");
254
+ assert.ok(sent[0].body.idempotencyId && sent[0].body.idempotencyId.length >= 8, "business dedup id minted");
255
+ assert.equal(sent[0].body.to, undefined, "recipients derived server-side");
256
+ assert.equal(sent[0].body.subject, undefined, "subject inherited from the thread");
257
+ assert.ok(sent[0].headers["x-idempotency-key"], "fresh dispatcher key per invocation");
258
+ });
259
+
260
+ test("_send: approval_required maps to {ok:false} pointing at send-orgmail.mjs", async () => {
261
+ const fetchImpl = hqMock({
262
+ sendBody: { ok: true, result: { status: "approval_required", approval: { actionClass: "external_comms", payloadHash: "h", intent: {} }, firstContactRecipients: ["new@x.com"] } },
263
+ });
264
+ const { a } = makeAdapter(fetchImpl);
265
+ const res = await a._send({ text: "hi", source: { chatId: "th-9" } });
266
+ assert.equal(res.ok, false);
267
+ assert.match(res.error, /send-orgmail\.mjs --request-approval/);
268
+ assert.equal(res.raw.status, "approval_required");
269
+ });
270
+
271
+ test("send(): the inherited BaseAdapter perimeter screens outbound content", async () => {
272
+ const screened = [];
273
+ const fetchImpl = hqMock();
274
+ const { a } = makeAdapter(fetchImpl, {
275
+ sendGate: true,
276
+ screenOutbound: async (o) => { screened.push(o); return { allow: true }; },
277
+ });
278
+ const res = await a.send({ text: "gated reply", source: { channel: "orgmail", chatId: "th-2" } });
279
+ assert.equal(res.ok, true);
280
+ assert.equal(screened.length, 1, "screenOutbound ran before _send");
281
+ // and a veto blocks with a uniform SendResult
282
+ const { a: blocked } = makeAdapter(hqMock(), { sendGate: true, screenOutbound: async () => ({ allow: false, reason: "policy" }) });
283
+ const b = await blocked.send({ text: "no", source: { chatId: "th-2" } });
284
+ assert.deepEqual(b, { ok: false, blocked: true, reason: "policy" });
285
+ });
286
+
287
+ // ── health ─────────────────────────────────────────────────────────────────
288
+
289
+ test("healthCheck: one email.inbox {limit:1} probe; surfaces the mailbox address", async () => {
290
+ const fetchImpl = hqMock();
291
+ const { a } = makeAdapter(fetchImpl);
292
+ const h = await a.healthCheck();
293
+ assert.equal(h.ok, true);
294
+ assert.match(h.detail, /astra@agents\.example/);
295
+ assert.deepEqual(fetchImpl.callsTo("/api/v1/email.inbox")[0].body, { limit: 1 });
296
+ });
297
+
298
+ test("healthCheck: server predating the email family (404 frame) degrades honestly", async () => {
299
+ const fetchImpl = async (url, init) => ({ ok: false, status: 404, json: async () => ({ ok: false, error: { code: "NOT_FOUND", message: "unknown method email.inbox" } }), headers: { get: () => undefined } });
300
+ const { a } = makeAdapter(fetchImpl);
301
+ const h = await a.healthCheck();
302
+ assert.equal(h.ok, false);
303
+ assert.match(h.detail, /NOT_FOUND/);
304
+ });
305
+
306
+ test("healthCheck: missing enrollment is an honest unhealthy, not a throw", async () => {
307
+ const a = new OrgMailAdapter({ agentRoot: "/tmp/none", orgConfig: { org: {} }, supervise: false, log: () => {}, setInterval: () => ({ unref() {} }), clearInterval: () => {} });
308
+ const h = await a.healthCheck();
309
+ assert.equal(h.ok, false);
310
+ assert.match(h.detail, /enrollment missing/);
311
+ });