@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,377 @@
1
+ #!/usr/bin/env python3
2
+ """Send an HTML email with file attachment via Gmail SMTP.
3
+
4
+ Usage: python3 send-email-with-attachment.py <to> <subject> <body> [--cc email] [--attachment path] [--reply-to-subject "original subject"] [--in-reply-to "<message-id>"]
5
+
6
+ Sends as the running agent (identity from config/agent.json) with branded signature block.
7
+ Supports PNG, PDF, and other file attachments via MIME.
8
+ """
9
+ import sys
10
+ import os
11
+ import imaplib
12
+ import email
13
+ import smtplib
14
+ import mimetypes
15
+ from email.mime.multipart import MIMEMultipart
16
+ from email.mime.text import MIMEText
17
+ from email.mime.base import MIMEBase
18
+ from email.mime.image import MIMEImage
19
+ from email.mime.application import MIMEApplication
20
+ from email import encoders
21
+ from email.utils import formatdate
22
+ from pathlib import Path
23
+
24
+ # Outbound dedup — fail-open if module not available
25
+ try:
26
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
27
+ from outbound_dedup import acquire_email_lock, confirm_email_lock
28
+ _DEDUP_AVAILABLE = True
29
+ except ImportError:
30
+ _DEDUP_AVAILABLE = False
31
+
32
+ # LLM-based dedup — replaces old subject+recipient lock per CEO directive ib-20260406-001
33
+ try:
34
+ from llm_email_dedup import llm_dedup_check
35
+ _LLM_DEDUP_AVAILABLE = True
36
+ except ImportError:
37
+ _LLM_DEDUP_AVAILABLE = False
38
+
39
+ # Pre-draft context enrichment (Phase 7) — advisory, never blocks — fail-open
40
+ try:
41
+ from pre_draft_lookup import pre_draft_lookup
42
+ _PRE_DRAFT_AVAILABLE = True
43
+ except ImportError:
44
+ _PRE_DRAFT_AVAILABLE = False
45
+
46
+ import json
47
+ from pathlib import Path
48
+ _AGENT_REPO_DIR = Path(os.environ.get("AGENT_DIR", str(Path(__file__).resolve().parent.parent)))
49
+ try:
50
+ _AGENT = json.loads((_AGENT_REPO_DIR / "config" / "agent.json").read_text())
51
+ except Exception:
52
+ _AGENT = {"firstName": "Agent", "fullName": "Agent", "email": "agent@example.com", "title": "Agent", "phone": ""}
53
+
54
+ USER = _AGENT.get("email", "agent@example.com")
55
+ LOGO_URL = os.environ.get(
56
+ "EMAIL_SIGNATURE_LOGO_URL",
57
+ "https://ci3.googleusercontent.com/mail-sig/AIorK4xNDnPN6a0MjSHV5urb9g11u2CzHIzwdJbwN21quGjzQFdbOdegCq1Wp6lq3NCqsARbi-ooqPfE9E1D",
58
+ )
59
+
60
+
61
+ def load_password():
62
+ """Load Gmail app password from environment or .env file."""
63
+ pw = os.environ.get("GMAIL_APP_PASSWORD", "")
64
+ if pw:
65
+ return pw
66
+ # Try loading from .env file
67
+ env_path = Path(__file__).parent.parent / ".env"
68
+ if env_path.exists():
69
+ with open(env_path) as f:
70
+ for line in f:
71
+ line = line.strip()
72
+ if line.startswith("GMAIL_APP_PASSWORD="):
73
+ return line.split("=", 1)[1].strip().strip('"').strip("'")
74
+ return ""
75
+
76
+
77
+ PASSWORD = load_password()
78
+ if not PASSWORD:
79
+ print("Error: GMAIL_APP_PASSWORD not set in environment or .env file.")
80
+ sys.exit(1)
81
+
82
+
83
+ def get_thread_message_ids(subject_search):
84
+ """Fetch real Message-IDs from Gmail via IMAP for threading.
85
+
86
+ Searches BOTH Inbox and Sent Mail to find the full thread chain.
87
+ Gmail threads by In-Reply-To/References headers, so we need all
88
+ Message-IDs from the conversation regardless of folder.
89
+ """
90
+ try:
91
+ mail = imaplib.IMAP4_SSL("imap.gmail.com")
92
+ mail.login(USER, PASSWORD)
93
+
94
+ clean_subject = subject_search.replace("Re: ", "").replace("Fwd: ", "").strip()
95
+ msg_ids = []
96
+ seen = set()
97
+
98
+ # Search across both INBOX and Sent Mail
99
+ for folder in ["INBOX", "[Gmail]/Sent Mail"]:
100
+ try:
101
+ mail.select(folder)
102
+ status, messages = mail.search(None, f'(SUBJECT "{clean_subject}")')
103
+ if status == "OK" and messages[0]:
104
+ for num in messages[0].split():
105
+ status, data = mail.fetch(num, '(RFC822.HEADER)')
106
+ if status == "OK":
107
+ hdr = email.message_from_string(data[0][1].decode('utf-8', errors='replace'))
108
+ mid = hdr.get('Message-ID', '')
109
+ if mid and mid not in seen:
110
+ msg_ids.append(mid)
111
+ seen.add(mid)
112
+ except Exception:
113
+ continue
114
+
115
+ mail.logout()
116
+ return msg_ids
117
+ except Exception as e:
118
+ print(f"Warning: Could not fetch thread IDs: {e}")
119
+ return []
120
+
121
+
122
+ def mark_as_read(sender):
123
+ """Mark emails from sender as read."""
124
+ try:
125
+ mail = imaplib.IMAP4_SSL("imap.gmail.com")
126
+ mail.login(USER, PASSWORD)
127
+ mail.select("INBOX")
128
+ status, messages = mail.search(None, f'(UNSEEN FROM "{sender}")')
129
+ if status == "OK" and messages[0]:
130
+ for num in messages[0].split():
131
+ mail.store(num, '+FLAGS', '\\Seen')
132
+ mail.logout()
133
+ except Exception as e:
134
+ print(f"Warning: Could not mark emails as read: {e}")
135
+
136
+
137
+ def build_signature_html():
138
+ full_name = _AGENT.get("fullName", "Agent")
139
+ title = _AGENT.get("title", "")
140
+ phone = _AGENT.get("phone", "")
141
+ phone_line = f'<div style="font-size:x-small;color:#000;">{phone}</div>\n<div><br></div>\n' if phone else ""
142
+ address = os.environ.get(
143
+ "EMAIL_SIGNATURE_ADDRESS",
144
+ "Level 1, Innovation One, Dubai International Financial Centre, Dubai, UAE",
145
+ )
146
+ return f"""<div>
147
+ <div style="font-size:small;"><b>{full_name}</b></div>
148
+ <div style="font-size:small;">{title}</div>
149
+ <div><br></div>
150
+ <div><img src="{LOGO_URL}" width="125"></div>
151
+ <div><br></div>
152
+ {phone_line}<div style="font-size:x-small;color:#000;">{address}</div>
153
+ <div style="font-size:x-small;color:#666;">_________________________________________</div>
154
+ <div style="font-size:x-small;color:#999;">This email and any files transmitted with it are confidential and intended solely for the use of the individual or entity to whom they are addressed. If you have received this email in error please notify the sender. This message contains confidential information and is intended only for the individual named. If you are not the named addressee you should not disseminate, distribute or copy this email. Please notify the sender immediately by email if you have received this email by mistake and delete this email from your system. If you are not the intended recipient you are notified that disclosing, copying, distributing, or taking any action in reliance on the contents of this information is strictly prohibited.</div>
155
+ </div>"""
156
+
157
+
158
+ def attach_file(msg, filepath):
159
+ """Attach a file to the email message."""
160
+ filepath = Path(filepath)
161
+ if not filepath.exists():
162
+ print(f"Error: Attachment file not found: {filepath}")
163
+ sys.exit(1)
164
+
165
+ filename = filepath.name
166
+ content_type, encoding = mimetypes.guess_type(str(filepath))
167
+
168
+ if content_type is None:
169
+ content_type = 'application/octet-stream'
170
+
171
+ maintype, subtype = content_type.split('/', 1)
172
+
173
+ with open(filepath, 'rb') as f:
174
+ file_data = f.read()
175
+
176
+ if maintype == 'image':
177
+ attachment = MIMEImage(file_data, _subtype=subtype)
178
+ elif maintype == 'application':
179
+ attachment = MIMEApplication(file_data, _subtype=subtype)
180
+ else:
181
+ attachment = MIMEBase(maintype, subtype)
182
+ attachment.set_payload(file_data)
183
+ encoders.encode_base64(attachment)
184
+
185
+ attachment.add_header('Content-Disposition', 'attachment', filename=filename)
186
+ msg.attach(attachment)
187
+ print(f"Attached: {filename} ({len(file_data):,} bytes, {content_type})")
188
+
189
+
190
+ def check_already_sent(to, subject):
191
+ """Check Gmail Sent Mail for a recent email to the same recipient with same subject.
192
+ Returns True if a matching email was sent in the last 24 hours."""
193
+ try:
194
+ mail = imaplib.IMAP4_SSL("imap.gmail.com")
195
+ mail.login(USER, PASSWORD)
196
+ mail.select('"[Gmail]/Sent Mail"')
197
+ clean_subject = subject.replace("RE: ", "").replace("Re: ", "").strip()
198
+ from datetime import datetime, timedelta
199
+ since_date = (datetime.now() - timedelta(hours=24)).strftime("%d-%b-%Y")
200
+ status, messages = mail.search(None, f'(SINCE "{since_date}" TO "{to}" SUBJECT "{clean_subject}")')
201
+ found = status == "OK" and messages[0] and len(messages[0].split()) > 0
202
+ count = len(messages[0].split()) if found else 0
203
+ mail.logout()
204
+ if found:
205
+ print(f"SENT_MAIL_DEDUP: Found {count} matching email(s) to {to} with subject containing '{clean_subject}' in last 24h", file=sys.stderr)
206
+ return found
207
+ except Exception as e:
208
+ print(f"SENT_MAIL_CHECK_ERROR: {e}", file=sys.stderr)
209
+ return False # fail-open
210
+
211
+
212
+ def send_email(to, subject, body, cc=None, attachment=None, reply_subject=None, in_reply_to=None):
213
+ # ── Pre-draft context enrichment (Phase 7) ─────────────────────
214
+ # Look up recipient in entity index. Advisory only — never blocks sending.
215
+ # Logs context to logs/audit/{date}-pre-draft-lookups.jsonl
216
+ if _PRE_DRAFT_AVAILABLE:
217
+ try:
218
+ _ctx = pre_draft_lookup(to, message_type="email")
219
+ if _ctx and _ctx.get("communication", {}).get("standing_instructions"):
220
+ print(f"PRE_DRAFT: {len(_ctx['communication']['standing_instructions'])} standing instruction(s) for {to}", file=sys.stderr)
221
+ except Exception as e:
222
+ print(f"PRE_DRAFT_WARN (fail-open): {e}", file=sys.stderr)
223
+
224
+ # ── Layer 1: LLM-based dedup ──────────────────────────────────────
225
+ # Reads recent conversation history with the recipient, asks Claude Haiku
226
+ # whether this topic has already been addressed. Replaces the old
227
+ # subject+recipient lock per CEO directive ib-20260406-001.
228
+ if _LLM_DEDUP_AVAILABLE:
229
+ try:
230
+ llm_result = llm_dedup_check(to, subject, body)
231
+ if llm_result["skip"]:
232
+ print(f"DEDUP_SKIP (LLM): {llm_result['reason']} [confidence={llm_result['confidence']}, msgs_checked={llm_result['recent_messages']}]")
233
+ return
234
+ else:
235
+ print(f"LLM_DEDUP_OK: {llm_result['reason'][:100]} [msgs_checked={llm_result['recent_messages']}]", file=sys.stderr)
236
+ except Exception as e:
237
+ print(f"LLM_DEDUP_WARN (fail-open): {e}", file=sys.stderr)
238
+
239
+ # ── Layer 2: Gmail Sent folder check ─────────────────────────────
240
+ # Double-check against actual sent mail via IMAP
241
+ if check_already_sent(to, subject):
242
+ print(f"DEDUP_SKIP (sent mail check): Already sent to {to} with subject '{subject}' in last 24h")
243
+ return
244
+
245
+ # ── Layer 3: Content-hash dedup ──────────────────────────────────
246
+ # Outbound dedup: prevent concurrent sessions from sending the same email
247
+ _dedup_key = None
248
+ if _DEDUP_AVAILABLE:
249
+ try:
250
+ _dedup_key, _dedup_result = acquire_email_lock(to, subject, body)
251
+ if _dedup_result == "DEDUP_SKIP":
252
+ print("DEDUP_SKIP (content hash)")
253
+ return
254
+ except Exception:
255
+ pass # fail-open
256
+
257
+ msg = MIMEMultipart('mixed')
258
+ msg['From'] = f"{_AGENT.get('fullName', 'Agent')} <{USER}>"
259
+ msg['To'] = to
260
+ msg['Subject'] = subject
261
+ msg['Date'] = formatdate(localtime=True)
262
+
263
+ if cc:
264
+ msg['Cc'] = cc
265
+
266
+ # Add threading headers if replying
267
+ # Priority: direct --in-reply-to > subject-based search
268
+ if in_reply_to:
269
+ msg['In-Reply-To'] = in_reply_to
270
+ msg['References'] = in_reply_to
271
+ # Also search for full chain to build References
272
+ if reply_subject:
273
+ thread_ids = get_thread_message_ids(reply_subject)
274
+ if thread_ids:
275
+ all_refs = thread_ids + [in_reply_to]
276
+ seen = []
277
+ for r in all_refs:
278
+ if r not in seen:
279
+ seen.append(r)
280
+ msg.replace_header('References', ' '.join(seen))
281
+ elif reply_subject:
282
+ thread_ids = get_thread_message_ids(reply_subject)
283
+ if thread_ids:
284
+ msg['In-Reply-To'] = thread_ids[-1]
285
+ msg['References'] = ' '.join(thread_ids)
286
+
287
+ # Build HTML body with signature
288
+ body_html = body.replace('\n', '<br>')
289
+ html = f"""<html><body style="font-family:Arial,Helvetica,sans-serif;font-size:small;color:#000;">
290
+ <div>{body_html}</div>
291
+ <br>
292
+ {build_signature_html()}
293
+ </body></html>"""
294
+
295
+ # Create the alternative part for HTML body
296
+ body_part = MIMEMultipart('alternative')
297
+ body_part.attach(MIMEText(html, 'html'))
298
+ msg.attach(body_part)
299
+
300
+ # Attach file if provided
301
+ if attachment:
302
+ attach_file(msg, attachment)
303
+
304
+ # Build recipient list for sending
305
+ recipients = [to]
306
+ if cc:
307
+ recipients.extend([addr.strip() for addr in cc.split(',')])
308
+
309
+ # Send
310
+ with smtplib.SMTP('smtp.gmail.com', 587) as server:
311
+ server.starttls()
312
+ server.login(USER, PASSWORD)
313
+ server.send_message(msg, to_addrs=recipients)
314
+
315
+ # Mark sender's emails as read
316
+ sender_domain = to.split('@')[-1] if '@' in to else ''
317
+ if sender_domain:
318
+ mark_as_read(to)
319
+
320
+ cc_note = f" (CC: {cc})" if cc else ""
321
+ attachment_note = f" [Attachment: {Path(attachment).name}]" if attachment else ""
322
+ print(f"Sent to {to}{cc_note}, subject: {subject}{attachment_note}")
323
+
324
+ # Post-send: confirm content-hash lock (audit trail)
325
+ if _DEDUP_AVAILABLE:
326
+ try:
327
+ confirm_email_lock(_dedup_key, f"To:{to} Subj:{subject}", to=to, subject=subject)
328
+ except Exception:
329
+ pass
330
+
331
+
332
+ def parse_args():
333
+ """Parse command-line arguments."""
334
+ if len(sys.argv) < 4:
335
+ print("Usage: send-email-with-attachment.py <to> <subject> <body> [--cc email] [--attachment path] [--reply-to-subject 'subject']")
336
+ print()
337
+ print("Arguments:")
338
+ print(" to Recipient email address")
339
+ print(" subject Email subject line")
340
+ print(" body Email body (plain text, newlines converted to <br>)")
341
+ print(" --cc CC email address(es), comma-separated")
342
+ print(" --attachment Path to file to attach (PNG, PDF, etc.)")
343
+ print(" --reply-to-subject Subject of original thread (for threading)")
344
+ sys.exit(1)
345
+
346
+ to = sys.argv[1]
347
+ subject = sys.argv[2]
348
+ body = sys.argv[3]
349
+ cc = None
350
+ attachment = None
351
+ reply_subject = None
352
+ in_reply_to = None
353
+
354
+ i = 4
355
+ while i < len(sys.argv):
356
+ if sys.argv[i] == '--cc' and i + 1 < len(sys.argv):
357
+ cc = sys.argv[i + 1]
358
+ i += 2
359
+ elif sys.argv[i] == '--attachment' and i + 1 < len(sys.argv):
360
+ attachment = sys.argv[i + 1]
361
+ i += 2
362
+ elif sys.argv[i] == '--reply-to-subject' and i + 1 < len(sys.argv):
363
+ reply_subject = sys.argv[i + 1]
364
+ i += 2
365
+ elif sys.argv[i] == '--in-reply-to' and i + 1 < len(sys.argv):
366
+ in_reply_to = sys.argv[i + 1]
367
+ i += 2
368
+ else:
369
+ print(f"Unknown argument: {sys.argv[i]}")
370
+ sys.exit(1)
371
+
372
+ return to, subject, body, cc, attachment, reply_subject, in_reply_to
373
+
374
+
375
+ if __name__ == '__main__':
376
+ to, subject, body, cc, attachment, reply_subject, in_reply_to = parse_args()
377
+ send_email(to, subject, body, cc, attachment, reply_subject, in_reply_to)
@@ -0,0 +1,131 @@
1
+ #!/bin/bash
2
+ # send-email.sh — Send HTML email via SMTP as the running agent
3
+ # Usage: ./scripts/send-email.sh <to> <subject> <body> [cc] [in-reply-to] [references]
4
+ set -e
5
+
6
+ SCRIPT_DIR_INIT="$(cd "$(dirname "$0")" && pwd)"
7
+ AGENT_REPO_DIR="${AGENT_DIR:-$(cd "$SCRIPT_DIR_INIT/.." && pwd)}"
8
+ if [ -f "$AGENT_REPO_DIR/config/agent.env" ]; then
9
+ # shellcheck disable=SC1091
10
+ source "$AGENT_REPO_DIR/config/agent.env"
11
+ fi
12
+
13
+ TO="$1"
14
+ SUBJECT="$2"
15
+ BODY="$3"
16
+ CC="$4"
17
+ IN_REPLY_TO="$5"
18
+ REFERENCES="$6"
19
+
20
+ if [ -z "$TO" ] || [ -z "$SUBJECT" ] || [ -z "$BODY" ]; then
21
+ echo "Usage: send-email.sh <to> <subject> <body> [cc] [in-reply-to] [references]"
22
+ exit 1
23
+ fi
24
+
25
+ # --- Outbound dedup: content-hash only ---
26
+ # Subject+recipient lock removed per CEO directive ib-20260406-001.
27
+ # LLM-based dedup is now handled in the Python send scripts.
28
+ # This shell script uses content-hash dedup only (catches identical body sends).
29
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
30
+ DEDUP_ACQUIRED=""
31
+ if [ -x "$SCRIPT_DIR/outbound-dedup.sh" ]; then
32
+ # Generate content-hash key and acquire lock
33
+ CONTENT_KEY=$("$SCRIPT_DIR/outbound-dedup.sh" generate-key email "$TO" "$SUBJECT" "$BODY" 2>/dev/null) || true
34
+ if [ -n "$CONTENT_KEY" ]; then
35
+ ACQUIRE_RESULT=$("$SCRIPT_DIR/outbound-dedup.sh" acquire email "$CONTENT_KEY" "${AGENT_SESSION_ID:-${SOPHIE_SESSION_ID:-${RAVI_SESSION_ID:-$$}}}" 2>/dev/null) || true
36
+ if [ "$ACQUIRE_RESULT" = "DEDUP_SKIP" ]; then
37
+ echo "DEDUP_SKIP"
38
+ exit 0
39
+ fi
40
+ if [ "$ACQUIRE_RESULT" = "CLAIMED" ]; then
41
+ DEDUP_ACQUIRED="1"
42
+ fi
43
+ fi
44
+ # If ERROR or empty, fall through (fail-open) — send anyway
45
+ fi
46
+
47
+ # Build headers
48
+ HEADERS="To: ${TO}
49
+ From: ${AGENT_FULL_NAME:-Agent} <${AGENT_EMAIL:-agent@example.com}>
50
+ Subject: ${SUBJECT}
51
+ MIME-Version: 1.0
52
+ Content-Type: text/html; charset=UTF-8"
53
+
54
+ if [ -n "$CC" ]; then
55
+ HEADERS="${HEADERS}
56
+ Cc: ${CC}"
57
+ fi
58
+
59
+ if [ -n "$IN_REPLY_TO" ]; then
60
+ HEADERS="${HEADERS}
61
+ In-Reply-To: <${IN_REPLY_TO}>"
62
+ if [ -n "$REFERENCES" ]; then
63
+ HEADERS="${HEADERS}
64
+ References: ${REFERENCES}"
65
+ else
66
+ HEADERS="${HEADERS}
67
+ References: <${IN_REPLY_TO}>"
68
+ fi
69
+ fi
70
+
71
+ LOGO_URL="${EMAIL_SIGNATURE_LOGO_URL:-https://ci3.googleusercontent.com/mail-sig/AIorK4xNDnPN6a0MjSHV5urb9g11u2CzHIzwdJbwN21quGjzQFdbOdegCq1Wp6lq3NCqsARbi-ooqPfE9E1D}"
72
+ SIG_ADDRESS="${EMAIL_SIGNATURE_ADDRESS:-Level 1, Innovation One, Dubai International Financial Centre, Dubai, UAE}"
73
+ SIG_PHONE_HTML=""
74
+ if [ -n "${AGENT_PHONE:-}" ]; then
75
+ PHONE_TEL=$(echo "$AGENT_PHONE" | tr -d ' ')
76
+ SIG_PHONE_HTML="<div style=\"font-size:small;\"><a href=\"tel:${PHONE_TEL}\" style=\"color:#1155cc;\">${AGENT_PHONE}</a></div>
77
+ <div><br></div>
78
+ "
79
+ fi
80
+
81
+ # Build HTML body
82
+ HTML_BODY="<html><body>
83
+ <div style=\"font-family:Arial,Helvetica,sans-serif;font-size:small;color:#000;\">
84
+ $(echo "$BODY" | sed 's/$/<br>/g')
85
+ </div>
86
+ <br>
87
+ <div style=\"font-family:Arial,Helvetica,sans-serif;color:#000;\">
88
+ <div style=\"font-size:small;\"><b>${AGENT_FULL_NAME:-Agent}</b></div>
89
+ <div style=\"font-size:small;\">${AGENT_TITLE:-}</div>
90
+ <div><br></div>
91
+ <div><img src=\"${LOGO_URL}\" width=\"125\"></div>
92
+ <div><br></div>
93
+ ${SIG_PHONE_HTML}<div style=\"font-size:x-small;color:#000;\">${SIG_ADDRESS}</div>
94
+ <div style=\"font-size:x-small;color:#666;\">_________________________________________</div>
95
+ <div><br></div>
96
+ <div style=\"font-size:x-small;color:#999;\">This email and any files transmitted with it are confidential and intended solely for the use of the individual or entity to whom they are addressed. If you have received this email in error please notify the sender. This message contains confidential information and is intended only for the individual named. If you are not the named addressee you should not disseminate, distribute or copy this email. Please notify the sender immediately by email if you have received this email by mistake and delete this email from your system. If you are not the intended recipient you are notified that disclosing, copying, distributing, or taking any action in reliance on the contents of this information is strictly prohibited.</div>
97
+ </div>
98
+ </body></html>"
99
+
100
+ # ── Pre-send policy gate (shared send-gate chokepoint) ──────────────
101
+ # Unified policy layer ABOVE the Python Layer-0 validator: banned phrases,
102
+ # AI-disclosure, information barriers, recipient allowlist — all in
103
+ # lib/comms/send-gate.mjs. Email recipients are external by default, so an
104
+ # unreadable policy fails CLOSED here (mirrors validate_outbound semantics).
105
+ SEND_GATE="$AGENT_REPO_DIR/lib/comms/send-gate.mjs"
106
+ if [ ! -f "$SEND_GATE" ]; then
107
+ SEND_GATE="$AGENT_REPO_DIR/node_modules/@cohortapp/agent-sdk/lib/comms/send-gate.mjs"
108
+ fi
109
+ if [ -f "$SEND_GATE" ]; then
110
+ NODE_BIN="${MAESTRO_NODE_BIN:-/opt/homebrew/bin/node}"
111
+ command -v "$NODE_BIN" >/dev/null 2>&1 || NODE_BIN="node"
112
+ # Replies by default; MAESTRO_SEND_FIRST_CONTACT=true for first contact. The
113
+ # truthfulness invariant (never deny being an AI) applies regardless.
114
+ GATE_REASON=$(printf '%s' "$BODY" | AGENT_DIR="$AGENT_REPO_DIR" "$NODE_BIN" "$SEND_GATE" \
115
+ --channel email --recipient "$TO" --agent-root "$AGENT_REPO_DIR" \
116
+ --first-contact "${MAESTRO_SEND_FIRST_CONTACT:-false}" 2>&1) || GATE_EXIT=$?
117
+ GATE_EXIT=${GATE_EXIT:-0}
118
+ if [ "$GATE_EXIT" != "0" ]; then
119
+ echo "SEND_GATE_BLOCK: Email to $TO blocked by policy gate" >&2
120
+ echo "$GATE_REASON" >&2
121
+ exit 1
122
+ fi
123
+ fi
124
+
125
+ printf "%s\n\n%s" "$HEADERS" "$HTML_BODY" | msmtp -a northwind "$TO"
126
+
127
+ # Post-send: confirm content-hash dedup lock
128
+ if [ -n "$DEDUP_ACQUIRED" ] && [ -x "$SCRIPT_DIR/outbound-dedup.sh" ] && [ -n "$CONTENT_KEY" ]; then
129
+ PREVIEW=$(printf '%s' "$BODY" | head -c 200)
130
+ "$SCRIPT_DIR/outbound-dedup.sh" confirm email "$CONTENT_KEY" "To:$TO Subj:$SUBJECT $PREVIEW" 2>/dev/null || true
131
+ fi
@@ -0,0 +1,175 @@
1
+ #!/bin/bash
2
+ # send-sms.sh — Send outbound SMS via Twilio REST API as the running agent
3
+ # Usage: ./scripts/send-sms.sh --to "+1234567890" --body "message text"
4
+ #
5
+ # Environment variables (from .env):
6
+ # TWILIO_ACCOUNT_SID — Twilio account SID
7
+ # TWILIO_AUTH_TOKEN — Twilio auth token
8
+ # TWILIO_PHONE_NUMBER — The agent's Twilio phone number
9
+ set -e
10
+
11
+ # Load environment
12
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
13
+ AGENT_REPO_DIR="${AGENT_DIR:-$(dirname "$SCRIPT_DIR")}"
14
+
15
+ if [ -f "$AGENT_REPO_DIR/.env" ]; then
16
+ source "$AGENT_REPO_DIR/.env"
17
+ fi
18
+
19
+ # Defaults
20
+ FROM_NUMBER="${TWILIO_PHONE_NUMBER:-+16282656712}"
21
+ TO=""
22
+ BODY=""
23
+
24
+ # Parse arguments
25
+ while [[ $# -gt 0 ]]; do
26
+ case "$1" in
27
+ --to)
28
+ TO="$2"
29
+ shift 2
30
+ ;;
31
+ --body)
32
+ BODY="$2"
33
+ shift 2
34
+ ;;
35
+ --from)
36
+ FROM_NUMBER="$2"
37
+ shift 2
38
+ ;;
39
+ -h|--help)
40
+ echo "Usage: send-sms.sh --to \"+1234567890\" --body \"message text\""
41
+ echo ""
42
+ echo "Options:"
43
+ echo " --to Recipient phone number (E.164 format, required)"
44
+ echo " --body Message text (required)"
45
+ echo " --from Sender phone number (default: \$TWILIO_PHONE_NUMBER or +16282656712)"
46
+ echo " -h Show this help"
47
+ exit 0
48
+ ;;
49
+ *)
50
+ echo "ERROR: Unknown argument: $1" >&2
51
+ echo "Usage: send-sms.sh --to \"+1234567890\" --body \"message text\"" >&2
52
+ exit 1
53
+ ;;
54
+ esac
55
+ done
56
+
57
+ # Validate required arguments
58
+ if [ -z "$TO" ]; then
59
+ echo "ERROR: --to is required" >&2
60
+ exit 1
61
+ fi
62
+
63
+ if [ -z "$BODY" ]; then
64
+ echo "ERROR: --body is required" >&2
65
+ exit 1
66
+ fi
67
+
68
+ # --- Outbound dedup: prevent concurrent sessions from sending the same SMS ---
69
+ DEDUP_ACQUIRED=""
70
+ if [ -x "$SCRIPT_DIR/outbound-dedup.sh" ]; then
71
+ DEDUP_KEY=$("$SCRIPT_DIR/outbound-dedup.sh" generate-key sms "$TO" "" "$BODY" 2>/dev/null) || true
72
+ if [ -n "$DEDUP_KEY" ]; then
73
+ ACQUIRE_RESULT=$("$SCRIPT_DIR/outbound-dedup.sh" acquire sms "$DEDUP_KEY" "${SOPHIE_SESSION_ID:-$$}" 2>/dev/null) || true
74
+ if [ "$ACQUIRE_RESULT" = "DEDUP_SKIP" ]; then
75
+ echo "DEDUP_SKIP: Another session is already sending this SMS"
76
+ exit 0
77
+ fi
78
+ if [ "$ACQUIRE_RESULT" = "CLAIMED" ]; then
79
+ DEDUP_ACQUIRED="1"
80
+ fi
81
+ # If ERROR or empty, fall through (fail-open) — send anyway
82
+ fi
83
+ fi
84
+
85
+ # Validate Twilio credentials
86
+ if [ -z "$TWILIO_ACCOUNT_SID" ]; then
87
+ echo "ERROR: TWILIO_ACCOUNT_SID not set" >&2
88
+ exit 1
89
+ fi
90
+
91
+ if [ -z "$TWILIO_AUTH_TOKEN" ]; then
92
+ echo "ERROR: TWILIO_AUTH_TOKEN not set" >&2
93
+ exit 1
94
+ fi
95
+
96
+ # ── Pre-send policy gate (shared send-gate chokepoint) ──────────────
97
+ # Unified policy layer: banned phrases, AI-disclosure, information barriers,
98
+ # recipient allowlist — all in lib/comms/send-gate.mjs. SMS recipients are phone
99
+ # numbers (external), so an unreadable policy fails CLOSED here.
100
+ SEND_GATE="$AGENT_REPO_DIR/lib/comms/send-gate.mjs"
101
+ if [ ! -f "$SEND_GATE" ]; then
102
+ SEND_GATE="$AGENT_REPO_DIR/node_modules/@cohortapp/agent-sdk/lib/comms/send-gate.mjs"
103
+ fi
104
+ if [ -f "$SEND_GATE" ]; then
105
+ NODE_BIN="${MAESTRO_NODE_BIN:-/opt/homebrew/bin/node}"
106
+ command -v "$NODE_BIN" >/dev/null 2>&1 || NODE_BIN="node"
107
+ # Replies by default; MAESTRO_SEND_FIRST_CONTACT=true for first contact. The
108
+ # truthfulness invariant (never deny being an AI) applies regardless.
109
+ GATE_REASON=$(printf '%s' "$BODY" | AGENT_DIR="$AGENT_REPO_DIR" "$NODE_BIN" "$SEND_GATE" \
110
+ --channel sms --recipient "$TO" --agent-root "$AGENT_REPO_DIR" \
111
+ --first-contact "${MAESTRO_SEND_FIRST_CONTACT:-false}" 2>&1) || GATE_EXIT=$?
112
+ GATE_EXIT=${GATE_EXIT:-0}
113
+ if [ "$GATE_EXIT" != "0" ]; then
114
+ echo "SEND_GATE_BLOCK: SMS to $TO blocked by policy gate" >&2
115
+ echo "$GATE_REASON" >&2
116
+ exit 1
117
+ fi
118
+ fi
119
+
120
+ # Send SMS via Twilio REST API
121
+ RESPONSE=$(curl -s -w "\n%{http_code}" \
122
+ -X POST "https://api.twilio.com/2010-04-01/Accounts/${TWILIO_ACCOUNT_SID}/Messages.json" \
123
+ -u "${TWILIO_ACCOUNT_SID}:${TWILIO_AUTH_TOKEN}" \
124
+ --data-urlencode "To=${TO}" \
125
+ --data-urlencode "From=${FROM_NUMBER}" \
126
+ --data-urlencode "Body=${BODY}")
127
+
128
+ # Split response body and HTTP status code
129
+ HTTP_CODE=$(echo "$RESPONSE" | tail -1)
130
+ RESPONSE_BODY=$(echo "$RESPONSE" | sed '$d')
131
+
132
+ # Extract message SID from response
133
+ MESSAGE_SID=$(echo "$RESPONSE_BODY" | grep -o '"sid": *"[^"]*"' | head -1 | sed 's/"sid": *"//' | sed 's/"//')
134
+ STATUS=$(echo "$RESPONSE_BODY" | grep -o '"status": *"[^"]*"' | head -1 | sed 's/"status": *"//' | sed 's/"//')
135
+
136
+ # Log to SMS log
137
+ LOG_DIR="$AGENT_REPO_DIR/logs/sms/$(date +%Y-%m-%d)"
138
+ mkdir -p "$LOG_DIR"
139
+ LOG_FILE="$LOG_DIR/$(date +%Y-%m-%d)-sms.jsonl"
140
+
141
+ LOG_ENTRY=$(cat <<LOGEOF
142
+ {"timestamp":"$(date -u +%Y-%m-%dT%H:%M:%S.000Z)","direction":"outbound","messageSid":"${MESSAGE_SID}","from":"${FROM_NUMBER}","to":"${TO}","body":"$(echo "$BODY" | head -c 200 | sed 's/"/\\"/g' | tr '\n' ' ')","http_code":"${HTTP_CODE}","status":"${STATUS}","logged_at":"$(date -u +%Y-%m-%dT%H:%M:%S.000Z)"}
143
+ LOGEOF
144
+ )
145
+ echo "$LOG_ENTRY" >> "$LOG_FILE"
146
+
147
+ # Log to audit trail
148
+ AUDIT_DIR="$AGENT_REPO_DIR/logs/audit"
149
+ mkdir -p "$AUDIT_DIR"
150
+ AUDIT_FILE="$AUDIT_DIR/$(date +%Y-%m-%d)-actions.jsonl"
151
+
152
+ AUDIT_ENTRY=$(cat <<AUDITEOF
153
+ {"timestamp":"$(date -u +%Y-%m-%dT%H:%M:%S.000Z)","agent":"send-sms","action":"sms_sent","to":"${TO}","from":"${FROM_NUMBER}","body_preview":"$(echo "$BODY" | head -c 100 | sed 's/"/\\"/g' | tr '\n' ' ')","messageSid":"${MESSAGE_SID}","http_code":"${HTTP_CODE}","status":"${STATUS}"}
154
+ AUDITEOF
155
+ )
156
+ echo "$AUDIT_ENTRY" >> "$AUDIT_FILE"
157
+
158
+ # Output result
159
+ if [ "$HTTP_CODE" = "201" ] || [ "$HTTP_CODE" = "200" ]; then
160
+ # Post-send: confirm the dedup lock (audit trail)
161
+ if [ -n "$DEDUP_ACQUIRED" ] && [ -n "$DEDUP_KEY" ] && [ -x "$SCRIPT_DIR/outbound-dedup.sh" ]; then
162
+ PREVIEW=$(printf '%s' "$BODY" | head -c 200)
163
+ "$SCRIPT_DIR/outbound-dedup.sh" confirm sms "$DEDUP_KEY" "To:$TO SID:$MESSAGE_SID $PREVIEW" 2>/dev/null || true
164
+ fi
165
+ echo "SMS sent successfully"
166
+ echo " To: $TO"
167
+ echo " From: $FROM_NUMBER"
168
+ echo " SID: $MESSAGE_SID"
169
+ echo " Status: $STATUS"
170
+ exit 0
171
+ else
172
+ echo "ERROR: SMS send failed (HTTP $HTTP_CODE)" >&2
173
+ echo "$RESPONSE_BODY" >&2
174
+ exit 1
175
+ fi