@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,116 @@
1
+ #!/bin/bash
2
+ # Health Check — Verifies all Maestro agent subsystems are operational
3
+ # Usage: ./scripts/healthcheck.sh
4
+ # Exit codes: 0 = healthy, 1 = degraded, 2 = critical
5
+
6
+ set -e
7
+
8
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
9
+ SOPHIE_AI_DIR="$(dirname "$SCRIPT_DIR")"
10
+ TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
11
+ HEALTHY=0
12
+ WARNINGS=0
13
+ ERRORS=0
14
+
15
+ echo "Maestro Health Check — $TIMESTAMP"
16
+ echo "======================================="
17
+
18
+ # Check 1: Emergency stop flag
19
+ if [ -f "$SOPHIE_AI_DIR/.emergency-stop" ]; then
20
+ echo "[STOP] Emergency stop flag is active"
21
+ ERRORS=$((ERRORS + 1))
22
+ else
23
+ echo "[OK] No emergency stop flag"
24
+ fi
25
+
26
+ # Check 2: Anthropic auth — either ANTHROPIC_API_KEY set OR subscription
27
+ # auth enabled. Daemons launched under launchd don't inherit the agent's
28
+ # shell env, so a "missing" ANTHROPIC_API_KEY is normal when the operator
29
+ # opted into Claude Code subscription auth (MAESTRO_PREFER_SUBSCRIPTION_AUTH=1
30
+ # in .env strips the var and falls back to Keychain OAuth).
31
+ if [ -n "${ANTHROPIC_API_KEY:-}" ]; then
32
+ echo "[OK] Environment variable: ANTHROPIC_API_KEY"
33
+ elif [ -f "$SOPHIE_AI_DIR/.env" ] && grep -qE "^MAESTRO_PREFER_SUBSCRIPTION_AUTH=(1|true|yes)$" "$SOPHIE_AI_DIR/.env"; then
34
+ echo "[OK] Anthropic auth: subscription mode (.env: MAESTRO_PREFER_SUBSCRIPTION_AUTH=1)"
35
+ elif [ -f "$SOPHIE_AI_DIR/.env" ] && grep -qE "^ANTHROPIC_API_KEY=." "$SOPHIE_AI_DIR/.env"; then
36
+ echo "[OK] Anthropic auth: ANTHROPIC_API_KEY in .env"
37
+ else
38
+ echo "[FAIL] Anthropic auth not configured (.env needs either ANTHROPIC_API_KEY or MAESTRO_PREFER_SUBSCRIPTION_AUTH=1)"
39
+ ERRORS=$((ERRORS + 1))
40
+ fi
41
+
42
+ # Check 3: Key directories exist and are writable
43
+ for dir in logs outputs knowledge/memory config; do
44
+ if [ -d "$SOPHIE_AI_DIR/$dir" ] && [ -w "$SOPHIE_AI_DIR/$dir" ]; then
45
+ echo "[OK] Directory writable: $dir"
46
+ else
47
+ echo "[FAIL] Directory missing or not writable: $dir"
48
+ ERRORS=$((ERRORS + 1))
49
+ fi
50
+ done
51
+
52
+ # Check 4: Source repositories accessible
53
+ for repo in company-web innovation-lab partner-framework company-legal regulatory-submission strategic-initiative; do
54
+ if [ -d "$HOME/$repo" ]; then
55
+ echo "[OK] Source repo accessible: ~/$repo"
56
+ else
57
+ echo "[WARN] Source repo not found: ~/$repo"
58
+ WARNINGS=$((WARNINGS + 1))
59
+ fi
60
+ done
61
+
62
+ # Check 5: Key applications available
63
+ for app in Slack WhatsApp Safari; do
64
+ if [ -d "/Applications/$app.app" ] || [ -d "$HOME/Applications/$app.app" ]; then
65
+ echo "[OK] Application installed: $app"
66
+ else
67
+ echo "[WARN] Application not found: $app"
68
+ WARNINGS=$((WARNINGS + 1))
69
+ fi
70
+ done
71
+
72
+ # Check 6: Network connectivity
73
+ if curl -s --max-time 5 https://api.anthropic.com > /dev/null 2>&1; then
74
+ echo "[OK] Network: Anthropic API reachable"
75
+ else
76
+ echo "[FAIL] Network: Cannot reach Anthropic API"
77
+ ERRORS=$((ERRORS + 1))
78
+ fi
79
+
80
+ # Check 7: Disk space
81
+ DISK_FREE=$(df -h "$SOPHIE_AI_DIR" | awk 'NR==2{print $5}' | tr -d '%')
82
+ if [ "$DISK_FREE" -gt 90 ]; then
83
+ echo "[WARN] Disk usage: ${DISK_FREE}% — consider cleanup"
84
+ WARNINGS=$((WARNINGS + 1))
85
+ elif [ "$DISK_FREE" -gt 95 ]; then
86
+ echo "[FAIL] Disk usage: ${DISK_FREE}% — critically low"
87
+ ERRORS=$((ERRORS + 1))
88
+ else
89
+ echo "[OK] Disk usage: ${DISK_FREE}%"
90
+ fi
91
+
92
+ # Check 8: Config files present
93
+ for config in priorities.yaml environment.yaml contacts.yaml; do
94
+ if [ -f "$SOPHIE_AI_DIR/config/$config" ]; then
95
+ echo "[OK] Config present: $config"
96
+ else
97
+ echo "[FAIL] Config missing: $config"
98
+ ERRORS=$((ERRORS + 1))
99
+ fi
100
+ done
101
+
102
+ # Summary
103
+ echo ""
104
+ echo "======================================="
105
+ echo "Results: $ERRORS errors, $WARNINGS warnings"
106
+
107
+ if [ "$ERRORS" -gt 0 ]; then
108
+ echo "Status: CRITICAL — fix errors before operating"
109
+ exit 2
110
+ elif [ "$WARNINGS" -gt 0 ]; then
111
+ echo "Status: DEGRADED — operational with limitations"
112
+ exit 1
113
+ else
114
+ echo "Status: HEALTHY — all systems operational"
115
+ exit 0
116
+ fi
@@ -0,0 +1,15 @@
1
+ #!/bin/bash
2
+ # Block org-MCP outbound writes in agent-repo sessions.
3
+ #
4
+ # The cohort MCP server (scaffold/.mcp.json) exposes the org tool surface to
5
+ # interactive Claude sessions. READS are welcome; the outbound WRITES
6
+ # (messaging_send, email_send, email_draft_send) and the org_rpc escape hatch
7
+ # must go through the
8
+ # sanctioned CLI lane instead — scripts/slack-send.sh / scripts/org/send-orgmail.mjs
9
+ # — which carries the full dedup + reply-storm + governance perimeter that the
10
+ # MCP tool's content screen alone does not. Same pattern as
11
+ # block-mcp-slack-send.sh (CEO directive si-010).
12
+ #
13
+ # Exit 2 = blocked (message shown to Claude as rejection)
14
+ echo "BLOCKED: org-MCP outbound writes are session-blocked. Reply via the sanctioned CLI lane instead: org messaging → the daemon's messaging path / scripts/slack-send.sh for Slack; workspace email → node scripts/org/send-orgmail.mjs --thread \"<thread_id>\" --body-file <path>. Read/board/decision org tools remain available." >&2
15
+ exit 2
@@ -0,0 +1,7 @@
1
+ #!/bin/bash
2
+ # Block MCP Slack sends — per CEO directive si-010
3
+ # All Slack sending must go through User OAuth Token (xoxp-) via slack-send.sh
4
+ # MCP Slack adds "Sent using @Claude" label — unacceptable for the agent's identity
5
+ # Exit 2 = blocked (message shown to Claude as rejection)
6
+ echo "BLOCKED: Per CEO directive si-010 — do NOT use MCP Slack for sending. Use scripts/slack-send.sh with SLACK_USER_TOKEN instead." >&2
7
+ exit 2
@@ -0,0 +1,126 @@
1
+ #!/bin/bash
2
+ # Post-action log hook — logs every tool completion to the audit trail.
3
+ # Called by Claude Code's PostToolUse hook after every tool execution.
4
+ #
5
+ # Stdin: Claude Code PostToolUse JSON payload (same contract the sibling
6
+ # pre-send-audit.sh relies on):
7
+ # { "tool_name": "...", "tool_input": { ... }, "tool_response": ...,
8
+ # "session_id": "..." }
9
+ #
10
+ # We parse that payload (jq when available, grep/sed fallback otherwise) and
11
+ # append one row:
12
+ # {"timestamp":ISO8601Z,"tool":<tool_name>,"status":<"error"|"completed">,
13
+ # "target":<best-effort recipient/path/url/command>,"session_id":<id|null>}
14
+ #
15
+ # Design note: PostToolUse runs AFTER the tool has executed, so this hook can
16
+ # never block or undo anything. Every path therefore fails OPEN and the script
17
+ # always exits 0 — a parse error must still produce a row (with whatever is
18
+ # parseable and status "unknown-parse"), never crash the session.
19
+
20
+ AGENT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
21
+ ACTION_LOG="$AGENT_DIR/logs/audit/$(date +%Y-%m-%d)-actions.jsonl"
22
+ TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
23
+
24
+ # Read the full hook payload from stdin (do NOT discard it — it carries the
25
+ # real tool name, inputs and response that the old hook threw away).
26
+ PAYLOAD=$(cat 2>/dev/null)
27
+
28
+ mkdir -p "$(dirname "$ACTION_LOG")" 2>/dev/null
29
+
30
+ # Emit a row and exit. Args: tool status target session_id (each already a
31
+ # bare string; jq -n quotes/escapes them safely; nulls passed as empty string
32
+ # become JSON null).
33
+ emit_row() {
34
+ local tool="$1" status="$2" target="$3" sid="$4"
35
+ if command -v jq >/dev/null 2>&1; then
36
+ jq -cn \
37
+ --arg ts "$TIMESTAMP" \
38
+ --arg tool "$tool" \
39
+ --arg status "$status" \
40
+ --arg target "$target" \
41
+ --arg sid "$sid" \
42
+ '{timestamp:$ts, tool:$tool, status:$status,
43
+ target:(if $target=="" then null else $target end),
44
+ session_id:(if $sid=="" then null else $sid end)}' \
45
+ >> "$ACTION_LOG" 2>/dev/null
46
+ else
47
+ # jq missing: hand-escape the few fields we control (backslash first,
48
+ # then double-quote, then strip control chars/newlines) so the row is
49
+ # still valid JSONL.
50
+ local esc_tool esc_status esc_target target_json sid_json
51
+ esc_tool=$(printf '%s' "$tool" | sed 's/\\/\\\\/g; s/"/\\"/g' | tr -d '\000-\037')
52
+ esc_status=$(printf '%s' "$status" | sed 's/\\/\\\\/g; s/"/\\"/g' | tr -d '\000-\037')
53
+ if [ -n "$target" ]; then
54
+ esc_target=$(printf '%s' "$target" | sed 's/\\/\\\\/g; s/"/\\"/g' | tr -d '\000-\037')
55
+ target_json="\"$esc_target\""
56
+ else
57
+ target_json="null"
58
+ fi
59
+ if [ -n "$sid" ]; then
60
+ sid_json="\"$(printf '%s' "$sid" | sed 's/\\/\\\\/g; s/"/\\"/g' | tr -d '\000-\037')\""
61
+ else
62
+ sid_json="null"
63
+ fi
64
+ printf '{"timestamp":"%s","tool":"%s","status":"%s","target":%s,"session_id":%s}\n' \
65
+ "$TIMESTAMP" "$esc_tool" "$esc_status" "$target_json" "$sid_json" \
66
+ >> "$ACTION_LOG" 2>/dev/null
67
+ fi
68
+ exit 0
69
+ }
70
+
71
+ # --- jq path (preferred) ----------------------------------------------------
72
+ if command -v jq >/dev/null 2>&1 && printf '%s' "$PAYLOAD" | jq -e . >/dev/null 2>&1; then
73
+ TOOL_NAME=$(printf '%s' "$PAYLOAD" | jq -r '.tool_name // "unknown"' 2>/dev/null)
74
+ SESSION_ID=$(printf '%s' "$PAYLOAD" | jq -r '.session_id // empty' 2>/dev/null)
75
+
76
+ # Status: "error" when the tool response signals failure in any of the
77
+ # shapes Claude Code uses (string "error", {error:...}, {is_error:true},
78
+ # {success:false}, or a non-zero numeric exit/code); else "completed".
79
+ STATUS=$(printf '%s' "$PAYLOAD" | jq -r '
80
+ .tool_response as $r
81
+ | if ($r == null) then "completed"
82
+ elif ($r | type) == "string" then
83
+ (if ($r | ascii_downcase | test("error")) then "error" else "completed" end)
84
+ elif ($r | type) == "object" then
85
+ (if ($r.is_error == true)
86
+ or ($r.error != null and $r.error != false)
87
+ or ($r.success == false)
88
+ or (($r.exit_code // $r.exitCode // $r.code // 0) | tonumber? // 0) != 0
89
+ then "error" else "completed" end)
90
+ else "completed" end
91
+ ' 2>/dev/null)
92
+ [ -n "$STATUS" ] || STATUS="completed"
93
+
94
+ # Target: best-effort, first present of file_path / command / url / to /
95
+ # channel / recipient from tool_input. Command is truncated to 200 chars.
96
+ TARGET=$(printf '%s' "$PAYLOAD" | jq -r '
97
+ (.tool_input // {}) as $ti
98
+ | ( $ti.file_path?
99
+ // ( $ti.command? | if . == null then null else (.[0:200]) end )
100
+ // $ti.url?
101
+ // $ti.to?
102
+ // $ti.channel?
103
+ // $ti.recipient?
104
+ // empty )
105
+ | if . == null then "" else (. | tostring) end
106
+ ' 2>/dev/null)
107
+
108
+ emit_row "$TOOL_NAME" "$STATUS" "$TARGET" "$SESSION_ID"
109
+ fi
110
+
111
+ # --- fallback path: jq missing or payload not valid JSON --------------------
112
+ # Pull the tool name out of the raw payload with a minimal grep/sed extraction
113
+ # rather than writing "unknown". If we get a name we still record a useful row;
114
+ # status is "unknown-parse" because we cannot reliably inspect the response.
115
+ TOOL_NAME=$(printf '%s' "$PAYLOAD" \
116
+ | grep -o '"tool_name"[[:space:]]*:[[:space:]]*"[^"]*"' \
117
+ | head -1 \
118
+ | sed 's/.*"tool_name"[[:space:]]*:[[:space:]]*"//; s/"$//')
119
+ [ -n "$TOOL_NAME" ] || TOOL_NAME="unknown"
120
+
121
+ SESSION_ID=$(printf '%s' "$PAYLOAD" \
122
+ | grep -o '"session_id"[[:space:]]*:[[:space:]]*"[^"]*"' \
123
+ | head -1 \
124
+ | sed 's/.*"session_id"[[:space:]]*:[[:space:]]*"//; s/"$//')
125
+
126
+ emit_row "$TOOL_NAME" "unknown-parse" "" "$SESSION_ID"
@@ -0,0 +1,174 @@
1
+ #!/bin/bash
2
+ # Pre-send audit hook — gates outbound communications
3
+ # Called by Claude Code PreToolUse hook before Slack/Gmail/WhatsApp sends.
4
+ # Reads the PreToolUse payload from stdin, screens the outbound CONTENT
5
+ # against the banned-phrase list (policies/communication-style.md), runs a
6
+ # recipient allowlist sanity check, and enforces hourly + daily rate caps.
7
+ #
8
+ # Usage: pre-send-audit.sh <channel_type>
9
+ # channel_type: slack | slack-draft | gmail | whatsapp
10
+ #
11
+ # Stdin: Claude Code PreToolUse JSON payload:
12
+ # { "tool_name": "...", "tool_input": { "text"|"message"|"body": "...",
13
+ # "to"|"recipient"|"channel": "..." }, ... }
14
+ #
15
+ # Exit codes:
16
+ # 0 = allowed (stdout message shown to Claude)
17
+ # 2 = blocked / any parse|IO error (stderr reason shown as rejection).
18
+ # Claude Code blocks the tool call on any non-zero exit, so EVERY
19
+ # failure path below intentionally fails CLOSED with exit 2.
20
+ #
21
+ # Design note: this script does NOT use `set -e`. A non-zero from a grep
22
+ # (no match) or an arithmetic expansion must not silently abort the script
23
+ # into an implicit success/fail-open state. Instead every gate is checked
24
+ # explicitly and the script ends with `exit 0` only after all checks pass.
25
+
26
+ set -uo pipefail
27
+
28
+ # --- fail-closed helper -----------------------------------------------------
29
+ deny() {
30
+ # $1 = human-readable reason
31
+ echo "BLOCKED: ${1:-pre-send audit failed}" >&2
32
+ exit 2
33
+ }
34
+
35
+ CHANNEL_TYPE="${1:-}"
36
+ [ -n "$CHANNEL_TYPE" ] || deny "Channel type required (slack|slack-draft|gmail|whatsapp)"
37
+
38
+ # jq is mandatory — without it we cannot inspect content, so refuse to send.
39
+ command -v jq >/dev/null 2>&1 || deny "jq not available — cannot screen outbound content (fail closed)"
40
+
41
+ # --- read stdin payload -----------------------------------------------------
42
+ TOOL_INPUT=$(cat)
43
+ [ -n "$TOOL_INPUT" ] || deny "Empty hook payload — nothing to audit (fail closed)"
44
+
45
+ # Validate that the payload is well-formed JSON before doing anything else.
46
+ if ! printf '%s' "$TOOL_INPUT" | jq -e . >/dev/null 2>&1; then
47
+ deny "Malformed hook payload (not valid JSON) — fail closed"
48
+ fi
49
+
50
+ # Extract the outbound text. Claude Code wraps the tool args under
51
+ # tool_input; for robustness we also look at the top level. We concatenate
52
+ # every plausible content field so a banned phrase anywhere is caught.
53
+ CONTENT=$(printf '%s' "$TOOL_INPUT" | jq -r '
54
+ [ .tool_input // {} , . ]
55
+ | map(
56
+ ( .text? // empty ),
57
+ ( .message? // empty ),
58
+ ( .body? // empty ),
59
+ ( .content? // empty ),
60
+ ( .blocks? | if type=="array" or type=="object" then tojson else empty end )
61
+ )
62
+ | flatten
63
+ | map(select(. != null and . != ""))
64
+ | join("\n")
65
+ ' 2>/dev/null) || deny "Failed to parse outbound content (fail closed)"
66
+
67
+ # Extract the recipient for the allowlist sanity check (optional field).
68
+ RECIPIENT=$(printf '%s' "$TOOL_INPUT" | jq -r '
69
+ ( .tool_input // {} ) as $ti
70
+ | ( $ti.to? // $ti.recipient? // $ti.channel? // $ti.email? // $ti.thread_ts? // .to? // .recipient? // empty )
71
+ | if . == null then "" else (. | tostring) end
72
+ ' 2>/dev/null) || deny "Failed to parse recipient (fail closed)"
73
+
74
+ # --- (a) banned-phrase screen ----------------------------------------------
75
+ # Source of truth: policies/communication-style.md (kept in sync manually).
76
+ # Matching is case-insensitive and substring-based. Opener phrases
77
+ # ("Certainly!" / "Absolutely!") are common in legitimate prose mid-sentence,
78
+ # so we anchor those to the start of the content / a line.
79
+ BANNED_SUBSTRINGS=(
80
+ "I'm here to help"
81
+ "I am here to help"
82
+ "Let me know if you need anything else"
83
+ "I hope this helps"
84
+ "Feel free to ask if you have any questions"
85
+ "As an AI"
86
+ "As a language model"
87
+ "I should clarify that I am"
88
+ "Great question!"
89
+ "Here's a comprehensive overview of"
90
+ "Here is a comprehensive overview of"
91
+ "I'd be happy to"
92
+ "I would be happy to"
93
+ )
94
+
95
+ # Lowercased copy for case-insensitive substring matching.
96
+ CONTENT_LC=$(printf '%s' "$CONTENT" | tr '[:upper:]' '[:lower:]')
97
+
98
+ for phrase in "${BANNED_SUBSTRINGS[@]}"; do
99
+ phrase_lc=$(printf '%s' "$phrase" | tr '[:upper:]' '[:lower:]')
100
+ case "$CONTENT_LC" in
101
+ *"$phrase_lc"*)
102
+ deny "Banned AI-tell phrase in outbound message: \"$phrase\" (see policies/communication-style.md)"
103
+ ;;
104
+ esac
105
+ done
106
+
107
+ # Opener-only phrases: block when they begin the message or any line.
108
+ # (Mid-sentence "absolutely" is fine; "Absolutely!" as a line opener is not.)
109
+ if printf '%s' "$CONTENT" | grep -Eiq '(^|[[:cntrl:]])[[:space:]]*(Certainly!|Absolutely!|Great question!)'; then
110
+ deny "Banned filler opener (Certainly!/Absolutely!/Great question!) — see policies/communication-style.md"
111
+ fi
112
+
113
+ # --- (b) recipient allowlist sanity check ----------------------------------
114
+ # Only enforced when a recipient is present AND an allowlist file exists.
115
+ # Allowlist file: one entry (channel id, email, handle) per line, '#' comments.
116
+ ALLOWLIST_FILE="${MAESTRO_SEND_ALLOWLIST:-}"
117
+ if [ -n "$RECIPIENT" ] && [ -n "$ALLOWLIST_FILE" ] && [ -f "$ALLOWLIST_FILE" ]; then
118
+ if ! grep -Eq -- "^[[:space:]]*$(printf '%s' "$RECIPIENT" | sed 's/[.[\*^$()+?{|]/\\&/g')[[:space:]]*(#.*)?$" "$ALLOWLIST_FILE" 2>/dev/null; then
119
+ deny "Recipient \"$RECIPIENT\" not on send allowlist ($ALLOWLIST_FILE)"
120
+ fi
121
+ fi
122
+
123
+ # --- (c) rate caps (hourly + daily) ----------------------------------------
124
+ RUNTIME_DIR="${MAESTRO_RUNTIME_DIR:-$HOME/.maestro/runtime}/audit"
125
+ if ! mkdir -p "$RUNTIME_DIR" 2>/dev/null; then
126
+ deny "Cannot create audit runtime dir $RUNTIME_DIR (fail closed)"
127
+ fi
128
+
129
+ COUNTER_FILE="$RUNTIME_DIR/send-counter.tsv"
130
+ TODAY=$(date +%Y-%m-%d)
131
+ HOUR=$(date +%H)
132
+ TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
133
+ SEND_LOG="$RUNTIME_DIR/$TODAY-sends.jsonl"
134
+
135
+ PER_HOUR_CAP="${MAESTRO_SEND_PER_HOUR:-3000}"
136
+ PER_DAY_CAP="${MAESTRO_SEND_PER_DAY:-20000}"
137
+
138
+ # Counter format (TSV): date<TAB>hour<TAB>hour_count<TAB>day_count
139
+ # Reset whenever the date rolls over; reset the hour bucket when hour rolls.
140
+ PREV_DATE=""; PREV_HOUR=""; HOUR_COUNT=0; DAY_COUNT=0
141
+ if [ -f "$COUNTER_FILE" ]; then
142
+ IFS=$'\t' read -r PREV_DATE PREV_HOUR HOUR_COUNT DAY_COUNT < "$COUNTER_FILE" 2>/dev/null || true
143
+ fi
144
+ # Sanitise to integers (fail closed to 0 rather than aborting).
145
+ case "$HOUR_COUNT" in (''|*[!0-9]*) HOUR_COUNT=0 ;; esac
146
+ case "$DAY_COUNT" in (''|*[!0-9]*) DAY_COUNT=0 ;; esac
147
+
148
+ if [ "$PREV_DATE" != "$TODAY" ]; then
149
+ DAY_COUNT=0
150
+ HOUR_COUNT=0
151
+ elif [ "$PREV_HOUR" != "$HOUR" ]; then
152
+ HOUR_COUNT=0
153
+ fi
154
+
155
+ if [ "$DAY_COUNT" -ge "$PER_DAY_CAP" ]; then
156
+ deny "Daily send limit ($PER_DAY_CAP) reached. Sends resume tomorrow."
157
+ fi
158
+ if [ "$HOUR_COUNT" -ge "$PER_HOUR_CAP" ]; then
159
+ deny "Hourly send limit ($PER_HOUR_CAP) reached. Sends resume next hour."
160
+ fi
161
+
162
+ # --- commit: increment counters + log allowed send -------------------------
163
+ HOUR_COUNT=$((HOUR_COUNT + 1))
164
+ DAY_COUNT=$((DAY_COUNT + 1))
165
+ if ! printf '%s\t%s\t%s\t%s\n' "$TODAY" "$HOUR" "$HOUR_COUNT" "$DAY_COUNT" > "$COUNTER_FILE" 2>/dev/null; then
166
+ deny "Cannot write send counter (fail closed)"
167
+ fi
168
+
169
+ RECIPIENT_JSON=$(printf '%s' "$RECIPIENT" | jq -Rs . 2>/dev/null || printf '""')
170
+ printf '{"timestamp":"%s","channel":"%s","recipient":%s,"status":"allowed"}\n' \
171
+ "$TIMESTAMP" "$CHANNEL_TYPE" "$RECIPIENT_JSON" >> "$SEND_LOG" 2>/dev/null || true
172
+
173
+ echo "Audit: $CHANNEL_TYPE send screened and logged at $TIMESTAMP (hour: $HOUR_COUNT/$PER_HOUR_CAP, day: $DAY_COUNT/$PER_DAY_CAP)"
174
+ exit 0
@@ -0,0 +1,215 @@
1
+ // node:test for scripts/hooks/pre-send-audit.sh (audit M1)
2
+ //
3
+ // The hook is a Claude Code PreToolUse gate that must SCREEN outbound
4
+ // content and FAIL CLOSED (exit 2) on any banned phrase, allowlist miss,
5
+ // rate-cap breach, or parse/IO error. These tests execFile the real shell
6
+ // script with crafted stdin JSON and assert the exit code.
7
+ //
8
+ // jq is required by the hook; if it is not on PATH we skip rather than
9
+ // report a false failure (but jq is available in CI/dev here).
10
+
11
+ import { test } from "node:test";
12
+ import assert from "node:assert/strict";
13
+ import { execFile, execFileSync } from "node:child_process";
14
+ import { fileURLToPath } from "node:url";
15
+ import { dirname, join } from "node:path";
16
+ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
17
+ import { tmpdir } from "node:os";
18
+
19
+ const __dirname = dirname(fileURLToPath(import.meta.url));
20
+ const HOOK = join(__dirname, "pre-send-audit.sh");
21
+
22
+ function jqAvailable() {
23
+ try {
24
+ execFileSync("jq", ["--version"], { stdio: "ignore" });
25
+ return true;
26
+ } catch {
27
+ return false;
28
+ }
29
+ }
30
+ const JQ = jqAvailable();
31
+
32
+ // Run the hook with the given stdin, returning { code, stdout, stderr }.
33
+ // Each call gets its own runtime dir so the rate counters never leak
34
+ // between cases. The shell env is reset to a known baseline so a stray
35
+ // MAESTRO_SEND_* in the test runner's environment cannot skew caps.
36
+ function runHook(stdin, { channel = "slack", env = {} } = {}) {
37
+ const runtimeDir = mkdtempSync(join(tmpdir(), "presend-"));
38
+ return new Promise((resolve) => {
39
+ const child = execFile(
40
+ "bash",
41
+ [HOOK, channel],
42
+ {
43
+ env: {
44
+ PATH: process.env.PATH,
45
+ HOME: runtimeDir,
46
+ MAESTRO_RUNTIME_DIR: runtimeDir,
47
+ ...env,
48
+ },
49
+ },
50
+ (err, stdout, stderr) => {
51
+ const code = err && typeof err.code === "number" ? err.code : 0;
52
+ rmSync(runtimeDir, { recursive: true, force: true });
53
+ resolve({ code, stdout, stderr });
54
+ },
55
+ );
56
+ child.stdin.end(stdin);
57
+ });
58
+ }
59
+
60
+ test("clean message passes (exit 0)", { skip: JQ ? false : "jq not on PATH" }, async () => {
61
+ const payload = JSON.stringify({
62
+ tool_name: "send",
63
+ tool_input: { text: "In progress — only blocker is DBA approval, expected EOD.", to: "C123" },
64
+ });
65
+ const { code, stdout } = await runHook(payload);
66
+ assert.equal(code, 0, `expected exit 0, got ${code}`);
67
+ assert.match(stdout, /screened and logged/);
68
+ });
69
+
70
+ test("banned phrase 'Let me know if you need anything else' blocks (exit 2)", { skip: JQ ? false : "jq not on PATH" }, async () => {
71
+ const payload = JSON.stringify({ tool_input: { text: "Done. Let me know if you need anything else!" } });
72
+ const { code, stderr } = await runHook(payload);
73
+ assert.equal(code, 2, `expected exit 2, got ${code}`);
74
+ assert.match(stderr, /BLOCKED/);
75
+ });
76
+
77
+ test("banned phrase 'As an AI' blocks (exit 2)", { skip: JQ ? false : "jq not on PATH" }, async () => {
78
+ const payload = JSON.stringify({ tool_input: { body: "As an AI, I cannot make that commitment." } });
79
+ const { code } = await runHook(payload, { channel: "gmail" });
80
+ assert.equal(code, 2, `expected exit 2, got ${code}`);
81
+ });
82
+
83
+ test("banned phrase \"I'd be happy to\" blocks (exit 2)", { skip: JQ ? false : "jq not on PATH" }, async () => {
84
+ const payload = JSON.stringify({ tool_input: { message: "I'd be happy to set that up for you." } });
85
+ const { code } = await runHook(payload);
86
+ assert.equal(code, 2, `expected exit 2, got ${code}`);
87
+ });
88
+
89
+ test("filler opener 'Absolutely!' blocks (exit 2)", { skip: JQ ? false : "jq not on PATH" }, async () => {
90
+ const payload = JSON.stringify({ tool_input: { text: "Absolutely! I can pull that together." } });
91
+ const { code } = await runHook(payload);
92
+ assert.equal(code, 2, `expected exit 2, got ${code}`);
93
+ });
94
+
95
+ test("mid-sentence 'absolutely' (no opener) passes (exit 0)", { skip: JQ ? false : "jq not on PATH" }, async () => {
96
+ const payload = JSON.stringify({ tool_input: { text: "I am absolutely certain the deploy is fine." } });
97
+ const { code } = await runHook(payload);
98
+ assert.equal(code, 0, `expected exit 0, got ${code}`);
99
+ });
100
+
101
+ test("banned phrase inside Slack blocks array blocks (exit 2)", { skip: JQ ? false : "jq not on PATH" }, async () => {
102
+ const payload = JSON.stringify({
103
+ tool_input: { blocks: [{ type: "section", text: { type: "mrkdwn", text: "As an AI I will help with that." } }] },
104
+ });
105
+ const { code } = await runHook(payload);
106
+ assert.equal(code, 2, `expected exit 2, got ${code}`);
107
+ });
108
+
109
+ test("malformed JSON fails closed (exit 2)", { skip: JQ ? false : "jq not on PATH" }, async () => {
110
+ const { code, stderr } = await runHook("not json {{{");
111
+ assert.equal(code, 2, `expected exit 2, got ${code}`);
112
+ assert.match(stderr, /Malformed|fail closed/i);
113
+ });
114
+
115
+ test("empty stdin fails closed (exit 2)", { skip: JQ ? false : "jq not on PATH" }, async () => {
116
+ const { code } = await runHook("");
117
+ assert.equal(code, 2, `expected exit 2, got ${code}`);
118
+ });
119
+
120
+ test("missing channel arg fails closed (exit 2)", { skip: JQ ? false : "jq not on PATH" }, async () => {
121
+ // Invoke with no channel argument.
122
+ const runtimeDir = mkdtempSync(join(tmpdir(), "presend-"));
123
+ const code = await new Promise((resolve) => {
124
+ const child = execFile(
125
+ "bash",
126
+ [HOOK],
127
+ { env: { PATH: process.env.PATH, HOME: runtimeDir, MAESTRO_RUNTIME_DIR: runtimeDir } },
128
+ (err) => {
129
+ rmSync(runtimeDir, { recursive: true, force: true });
130
+ resolve(err && typeof err.code === "number" ? err.code : 0);
131
+ },
132
+ );
133
+ child.stdin.end(JSON.stringify({ tool_input: { text: "hi" } }));
134
+ });
135
+ assert.equal(code, 2, `expected exit 2, got ${code}`);
136
+ });
137
+
138
+ test("recipient on allowlist passes (exit 0)", { skip: JQ ? false : "jq not on PATH" }, async () => {
139
+ const dir = mkdtempSync(join(tmpdir(), "presend-allow-"));
140
+ const allow = join(dir, "allow.txt");
141
+ writeFileSync(allow, "C123\ngood@example.com\n# a comment\n");
142
+ const payload = JSON.stringify({ tool_input: { text: "hello", to: "C123" } });
143
+ const { code } = await runHook(payload, { env: { MAESTRO_SEND_ALLOWLIST: allow } });
144
+ rmSync(dir, { recursive: true, force: true });
145
+ assert.equal(code, 0, `expected exit 0, got ${code}`);
146
+ });
147
+
148
+ test("recipient NOT on allowlist fails closed (exit 2)", { skip: JQ ? false : "jq not on PATH" }, async () => {
149
+ const dir = mkdtempSync(join(tmpdir(), "presend-allow-"));
150
+ const allow = join(dir, "allow.txt");
151
+ writeFileSync(allow, "C123\ngood@example.com\n");
152
+ const payload = JSON.stringify({ tool_input: { text: "hello", to: "C999" } });
153
+ const { code, stderr } = await runHook(payload, { env: { MAESTRO_SEND_ALLOWLIST: allow } });
154
+ rmSync(dir, { recursive: true, force: true });
155
+ assert.equal(code, 2, `expected exit 2, got ${code}`);
156
+ assert.match(stderr, /allowlist/i);
157
+ });
158
+
159
+ test("hourly cap blocks once exceeded (exit 2)", { skip: JQ ? false : "jq not on PATH" }, async () => {
160
+ // Share one runtime dir across three sends with PER_HOUR=2.
161
+ const runtimeDir = mkdtempSync(join(tmpdir(), "presend-hour-"));
162
+ const payload = JSON.stringify({ tool_input: { text: "hi" } });
163
+ const send = () =>
164
+ new Promise((resolve) => {
165
+ const child = execFile(
166
+ "bash",
167
+ [HOOK, "slack"],
168
+ {
169
+ env: {
170
+ PATH: process.env.PATH,
171
+ HOME: runtimeDir,
172
+ MAESTRO_RUNTIME_DIR: runtimeDir,
173
+ MAESTRO_SEND_PER_HOUR: "2",
174
+ },
175
+ },
176
+ (err) => resolve(err && typeof err.code === "number" ? err.code : 0),
177
+ );
178
+ child.stdin.end(payload);
179
+ });
180
+ const c1 = await send();
181
+ const c2 = await send();
182
+ const c3 = await send();
183
+ rmSync(runtimeDir, { recursive: true, force: true });
184
+ assert.equal(c1, 0, `send 1 expected 0, got ${c1}`);
185
+ assert.equal(c2, 0, `send 2 expected 0, got ${c2}`);
186
+ assert.equal(c3, 2, `send 3 expected 2 (hourly cap), got ${c3}`);
187
+ });
188
+
189
+ test("daily cap blocks once exceeded (exit 2)", { skip: JQ ? false : "jq not on PATH" }, async () => {
190
+ const runtimeDir = mkdtempSync(join(tmpdir(), "presend-day-"));
191
+ const payload = JSON.stringify({ tool_input: { text: "hi" } });
192
+ const send = () =>
193
+ new Promise((resolve) => {
194
+ const child = execFile(
195
+ "bash",
196
+ [HOOK, "slack"],
197
+ {
198
+ env: {
199
+ PATH: process.env.PATH,
200
+ HOME: runtimeDir,
201
+ MAESTRO_RUNTIME_DIR: runtimeDir,
202
+ MAESTRO_SEND_PER_DAY: "1",
203
+ },
204
+ },
205
+ (err, stdout, stderr) => resolve({ code: err && typeof err.code === "number" ? err.code : 0, stderr }),
206
+ );
207
+ child.stdin.end(payload);
208
+ });
209
+ const r1 = await send();
210
+ const r2 = await send();
211
+ rmSync(runtimeDir, { recursive: true, force: true });
212
+ assert.equal(r1.code, 0, `send 1 expected 0, got ${r1.code}`);
213
+ assert.equal(r2.code, 2, `send 2 expected 2 (daily cap), got ${r2.code}`);
214
+ assert.match(r2.stderr, /Daily send limit/);
215
+ });