@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,167 @@
1
+ #!/usr/bin/env python3
2
+ """Fetch and format the most recent message in an email thread for quoting in replies.
3
+
4
+ Used by send-email-threaded.py and send-email-as-principal.py to include the
5
+ original email chain in reply bodies — matching standard email client behaviour.
6
+ """
7
+ import imaplib
8
+ import email
9
+ from email.utils import parsedate_to_datetime, formatdate
10
+ from datetime import datetime
11
+ import re
12
+
13
+
14
+ def fetch_thread_body(imap_user, imap_password, subject_search, exclude_from=None):
15
+ """Fetch the most recent message body from a thread for quoting.
16
+
17
+ Args:
18
+ imap_user: Gmail address for IMAP login
19
+ imap_password: App password for IMAP login
20
+ subject_search: Subject line to search for (Re:/Fwd: prefixes stripped)
21
+ exclude_from: Email address to exclude (avoid quoting our own messages)
22
+
23
+ Returns:
24
+ dict with keys: from_header, date_header, to_header, body_html, body_text
25
+ or None if no matching message found.
26
+ """
27
+ try:
28
+ mail = imaplib.IMAP4_SSL("imap.gmail.com")
29
+ mail.login(imap_user, imap_password)
30
+
31
+ clean_subject = subject_search.replace("Re: ", "").replace("RE: ", "").replace("Fwd: ", "").strip()
32
+ # Sanitise for IMAP search
33
+ ascii_subject = (
34
+ clean_subject
35
+ .replace('\u2014', '-').replace('\u2013', '-')
36
+ .replace('\u2018', "'").replace('\u2019', "'")
37
+ .replace('\u201c', '"').replace('\u201d', '"')
38
+ )
39
+
40
+ best = None # (date, result_dict, raw_msg)
41
+
42
+ for folder in ["INBOX", "[Gmail]/Sent Mail"]:
43
+ try:
44
+ mail.select(folder, readonly=True)
45
+ status, messages = mail.search(None, f'(SUBJECT "{ascii_subject}")')
46
+ if status != "OK" or not messages[0]:
47
+ continue
48
+
49
+ for num in messages[0].split():
50
+ status, data = mail.fetch(num, "(BODY[])")
51
+ if status != "OK":
52
+ continue
53
+ raw = data[0][1]
54
+ msg = email.message_from_bytes(raw)
55
+
56
+ # Skip our own messages so we quote the other party
57
+ from_hdr = msg.get("From", "")
58
+ if exclude_from and exclude_from.lower() in from_hdr.lower():
59
+ continue
60
+
61
+ # Parse date for recency comparison
62
+ date_str = msg.get("Date", "")
63
+ try:
64
+ dt = parsedate_to_datetime(date_str)
65
+ except Exception:
66
+ dt = datetime.min
67
+
68
+ if best is None or dt > best[0]:
69
+ body_html, body_text = _extract_body(msg)
70
+ best = (dt, {
71
+ "from_header": from_hdr,
72
+ "date_header": date_str,
73
+ "to_header": msg.get("To", ""),
74
+ "cc_header": msg.get("Cc", ""),
75
+ "subject": msg.get("Subject", ""),
76
+ "body_html": body_html,
77
+ "body_text": body_text,
78
+ }, None)
79
+ except Exception:
80
+ continue
81
+
82
+ mail.logout()
83
+ return best[1] if best else None
84
+
85
+ except Exception as e:
86
+ import sys
87
+ print(f"QUOTE_FETCH_WARN: {e}", file=sys.stderr)
88
+ return None
89
+
90
+
91
+ def _extract_body(msg):
92
+ """Extract HTML and plain-text body from an email.message.Message."""
93
+ html_body = None
94
+ text_body = None
95
+
96
+ if msg.is_multipart():
97
+ for part in msg.walk():
98
+ ct = part.get_content_type()
99
+ disp = str(part.get("Content-Disposition", ""))
100
+ if "attachment" in disp:
101
+ continue
102
+ try:
103
+ payload = part.get_payload(decode=True)
104
+ if payload is None:
105
+ continue
106
+ charset = part.get_content_charset() or "utf-8"
107
+ decoded = payload.decode(charset, errors="replace")
108
+ except Exception:
109
+ continue
110
+
111
+ if ct == "text/html" and html_body is None:
112
+ html_body = decoded
113
+ elif ct == "text/plain" and text_body is None:
114
+ text_body = decoded
115
+ else:
116
+ try:
117
+ payload = msg.get_payload(decode=True)
118
+ charset = msg.get_content_charset() or "utf-8"
119
+ decoded = payload.decode(charset, errors="replace")
120
+ if msg.get_content_type() == "text/html":
121
+ html_body = decoded
122
+ else:
123
+ text_body = decoded
124
+ except Exception:
125
+ pass
126
+
127
+ return html_body, text_body
128
+
129
+
130
+ def format_quoted_html(thread_msg):
131
+ """Format a fetched thread message as a Gmail-style quoted block.
132
+
133
+ Args:
134
+ thread_msg: dict returned by fetch_thread_body()
135
+
136
+ Returns:
137
+ HTML string with the quoted content, or empty string if no content.
138
+ """
139
+ if not thread_msg:
140
+ return ""
141
+
142
+ # Build the "On <date>, <from> wrote:" line
143
+ from_display = thread_msg["from_header"]
144
+ date_display = thread_msg["date_header"]
145
+ wrote_line = f"On {date_display}, {from_display} wrote:"
146
+
147
+ # Prefer HTML body; fall back to plain text converted to HTML
148
+ if thread_msg["body_html"]:
149
+ # Strip outer <html>/<body> tags to embed cleanly
150
+ quoted_content = thread_msg["body_html"]
151
+ quoted_content = re.sub(r'</?html[^>]*>', '', quoted_content, flags=re.IGNORECASE)
152
+ quoted_content = re.sub(r'</?body[^>]*>', '', quoted_content, flags=re.IGNORECASE)
153
+ quoted_content = re.sub(r'<!DOCTYPE[^>]*>', '', quoted_content, flags=re.IGNORECASE)
154
+ quoted_content = re.sub(r'<head>.*?</head>', '', quoted_content, flags=re.IGNORECASE | re.DOTALL)
155
+ elif thread_msg["body_text"]:
156
+ quoted_content = thread_msg["body_text"].replace('\n', '<br>')
157
+ else:
158
+ return ""
159
+
160
+ return f"""<br>
161
+ <div class="gmail_quote">
162
+ <div style="margin:0 0 0 .8ex;border-left:1px solid #ccc;padding-left:1ex;">
163
+ <div style="font-size:small;color:#888;">{wrote_line}</div>
164
+ <br>
165
+ {quoted_content}
166
+ </div>
167
+ </div>"""
@@ -0,0 +1,362 @@
1
+ #!/usr/bin/env python3
2
+ """Email thread-level deduplication — prevents replying twice to the same thread.
3
+
4
+ Unlike the content-hash dedup in outbound_dedup.py (which catches identical sends),
5
+ this module keys on the *thread identity* (normalized subject + primary recipient).
6
+ Two replies with completely different wording to the same thread will be caught.
7
+
8
+ Lock dir: state/locks/outbound/email-threads/
9
+ Lock file: {thread_key}.lock (JSON with metadata)
10
+ TTL: 24 hours
11
+
12
+ Usage (standalone):
13
+ python3 email-thread-dedup.py check <to> <subject>
14
+ python3 email-thread-dedup.py acquire <to> <subject> [--session-id ID] [--preview TEXT]
15
+ python3 email-thread-dedup.py release <to> <subject>
16
+ python3 email-thread-dedup.py clean
17
+
18
+ Usage (as module):
19
+ from email_thread_dedup import check_thread_lock, acquire_thread_lock
20
+
21
+ result = check_thread_lock(to, subject)
22
+ if result["locked"]:
23
+ print(f"DEDUP_SKIP: Thread already replied to at {result['timestamp']}")
24
+ sys.exit(2)
25
+
26
+ acquire_thread_lock(to, subject, session_id, preview)
27
+ # ... send the email ...
28
+ """
29
+ import hashlib
30
+ import json
31
+ import os
32
+ import re
33
+ import sys
34
+ import time
35
+ from datetime import datetime, timezone
36
+
37
+ from pathlib import Path
38
+ AGENT_REPO_DIR = os.environ.get("AGENT_DIR", str(Path(__file__).resolve().parent.parent))
39
+ LOCK_DIR = os.path.join(AGENT_REPO_DIR, "state", "locks", "outbound", "email-threads")
40
+ AUDIT_DIR = os.path.join(AGENT_REPO_DIR, "logs", "audit")
41
+ TTL_SECONDS = 24 * 60 * 60 # 24 hours
42
+
43
+
44
+ def _normalize_subject(subject: str) -> str:
45
+ """Normalize subject for thread matching.
46
+
47
+ Strips Re:/Fwd: prefixes, dates, times, timezones, day names, and
48
+ normalizes punctuation/whitespace. This ensures subjects like:
49
+ "Weekly Strategic Review Brief — 7 April 2026"
50
+ "Weekly Strategic Review Brief — Tue 7 April 2026, 14:00 GST"
51
+ "Re: Weekly Strategic Review Brief — 7 April 2026"
52
+ all hash to the same key.
53
+ """
54
+ s = subject.strip()
55
+ # Iteratively strip reply/forward prefixes (case-insensitive)
56
+ while True:
57
+ new_s = re.sub(r'^(Re|RE|Fwd|FWD|Fw|FW)\s*:\s*', '', s).strip()
58
+ if new_s == s:
59
+ break
60
+ s = new_s
61
+ # Lowercase for normalization
62
+ s = s.lower()
63
+ # Strip day names (Mon, Monday, etc.)
64
+ s = re.sub(r'\b(mon|tue|wed|thu|fri|sat|sun)(day|nesday|rsday|urday)?\b', '', s, flags=re.IGNORECASE)
65
+ # Strip times (HH:MM, HH:MM:SS)
66
+ s = re.sub(r'\d{1,2}:\d{2}(:\d{2})?', '', s)
67
+ # Strip timezone abbreviations
68
+ s = re.sub(r'\b(gst|utc|est|pst|cst|mst|gmt|bst|cet|aest|aedt|ast|ict|jst|ist)\b', '', s, flags=re.IGNORECASE)
69
+ # Strip full dates: "7 April 2026", "7 Apr 2026"
70
+ s = re.sub(r',?\s*\d{1,2}\s+(january|february|march|april|may|june|july|august|september|october|november|december)\s+\d{4}', '', s, flags=re.IGNORECASE)
71
+ s = re.sub(r',?\s*\d{1,2}\s+(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)\s+\d{4}', '', s, flags=re.IGNORECASE)
72
+ # Strip ISO dates: 2026-04-07
73
+ s = re.sub(r'\d{4}-\d{2}-\d{2}', '', s)
74
+ # Collapse repeated em-dashes and commas
75
+ s = re.sub(r',[\s,]*', ' ', s)
76
+ s = re.sub(r'\s*[—–-]+\s*[—–-]+\s*', ' — ', s)
77
+ s = re.sub(r'\s*[—–-]+\s*$', '', s)
78
+ # Normalize whitespace
79
+ s = re.sub(r'\s+', ' ', s).strip()
80
+ return s
81
+
82
+
83
+ def _normalize_recipient(to: str) -> str:
84
+ """Normalize recipient: lowercase, strip whitespace, take first address if multiple."""
85
+ to = to.strip().lower()
86
+ # If comma-separated, take the first address
87
+ if ',' in to:
88
+ to = to.split(',')[0].strip()
89
+ # Strip display name if present: "Name <email>" -> "email"
90
+ match = re.search(r'<([^>]+)>', to)
91
+ if match:
92
+ to = match.group(1)
93
+ return to
94
+
95
+
96
+ def _thread_key(to: str, subject: str) -> str:
97
+ """Generate a deterministic key for a thread: hash of normalized subject + primary recipient."""
98
+ norm_subject = _normalize_subject(subject)
99
+ norm_recipient = _normalize_recipient(to)
100
+ raw = f"{norm_subject}|{norm_recipient}"
101
+ return hashlib.sha256(raw.encode('utf-8')).hexdigest()[:24]
102
+
103
+
104
+ def _lock_path(thread_key: str) -> str:
105
+ """Return the full path to a thread lock file."""
106
+ return os.path.join(LOCK_DIR, f"{thread_key}.lock")
107
+
108
+
109
+ def _now_iso() -> str:
110
+ return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
111
+
112
+
113
+ def _log_audit(action: str, thread_key: str, to: str, subject: str, detail: str = ""):
114
+ """Append a dedup decision to the daily audit log."""
115
+ os.makedirs(AUDIT_DIR, exist_ok=True)
116
+ date_str = datetime.now(timezone.utc).strftime("%Y-%m-%d")
117
+ audit_file = os.path.join(AUDIT_DIR, f"{date_str}-actions.jsonl")
118
+ session_id = os.environ.get("SOPHIE_SESSION_ID", str(os.getpid()))
119
+ entry = {
120
+ "timestamp": _now_iso(),
121
+ "agent": "email-thread-dedup",
122
+ "action": action,
123
+ "thread_key": thread_key,
124
+ "to": to,
125
+ "subject": subject,
126
+ "detail": detail[:200],
127
+ "session": session_id,
128
+ }
129
+ try:
130
+ with open(audit_file, "a") as f:
131
+ f.write(json.dumps(entry) + "\n")
132
+ except Exception:
133
+ pass # Never let audit logging block sends
134
+
135
+
136
+ def check_thread_lock(to: str, subject: str) -> dict:
137
+ """Check if a thread lock exists and is still valid (within TTL).
138
+
139
+ Returns dict with:
140
+ locked: bool — True if a valid lock exists (should BLOCK send)
141
+ thread_key: str — the computed thread key
142
+ lock_data: dict|None — the lock metadata if locked
143
+ """
144
+ os.makedirs(LOCK_DIR, exist_ok=True)
145
+ key = _thread_key(to, subject)
146
+ path = _lock_path(key)
147
+
148
+ if not os.path.exists(path):
149
+ return {"locked": False, "thread_key": key, "lock_data": None}
150
+
151
+ try:
152
+ with open(path, "r") as f:
153
+ data = json.load(f)
154
+ except (json.JSONDecodeError, IOError):
155
+ # Corrupt lock file — treat as unlocked
156
+ return {"locked": False, "thread_key": key, "lock_data": None}
157
+
158
+ # Check TTL
159
+ lock_ts = data.get("timestamp_epoch", 0)
160
+ age_seconds = time.time() - lock_ts
161
+ if age_seconds >= TTL_SECONDS:
162
+ # Lock expired — treat as unlocked
163
+ return {"locked": False, "thread_key": key, "lock_data": data}
164
+
165
+ # Valid lock exists
166
+ return {"locked": True, "thread_key": key, "lock_data": data}
167
+
168
+
169
+ def acquire_thread_lock(to: str, subject: str, session_id: str = "", preview: str = "") -> dict:
170
+ """Attempt to acquire a thread lock.
171
+
172
+ Returns dict with:
173
+ acquired: bool — True if lock was acquired (send may proceed)
174
+ thread_key: str
175
+ existing_lock: dict|None — if not acquired, the existing lock data
176
+ """
177
+ os.makedirs(LOCK_DIR, exist_ok=True)
178
+ if not session_id:
179
+ session_id = os.environ.get("SOPHIE_SESSION_ID", str(os.getpid()))
180
+
181
+ key = _thread_key(to, subject)
182
+ path = _lock_path(key)
183
+ norm_subject = _normalize_subject(subject)
184
+ norm_recipient = _normalize_recipient(to)
185
+
186
+ # Check existing lock
187
+ check = check_thread_lock(to, subject)
188
+ if check["locked"]:
189
+ _log_audit(
190
+ "DEDUP_SKIP",
191
+ key, to, subject,
192
+ f"Thread already replied to by session {check['lock_data'].get('session_id', '?')} at {check['lock_data'].get('timestamp', '?')}"
193
+ )
194
+ return {"acquired": False, "thread_key": key, "existing_lock": check["lock_data"]}
195
+
196
+ # Write lock file (atomic via write-to-temp-then-rename)
197
+ lock_data = {
198
+ "thread_key": key,
199
+ "timestamp": _now_iso(),
200
+ "timestamp_epoch": time.time(),
201
+ "session_id": session_id,
202
+ "subject_normalized": norm_subject,
203
+ "recipient_normalized": norm_recipient,
204
+ "subject_original": subject[:200],
205
+ "recipient_original": to[:200],
206
+ "preview": (preview or "")[:100],
207
+ "status": "acquired",
208
+ }
209
+
210
+ tmp_path = path + f".tmp.{os.getpid()}"
211
+ try:
212
+ with open(tmp_path, "w") as f:
213
+ json.dump(lock_data, f, indent=2)
214
+ os.rename(tmp_path, path) # atomic on POSIX
215
+ except Exception as e:
216
+ # Clean up temp file on failure
217
+ try:
218
+ os.unlink(tmp_path)
219
+ except OSError:
220
+ pass
221
+ # Fail-open: if we can't write the lock, allow the send
222
+ _log_audit("LOCK_WRITE_ERROR", key, to, subject, str(e))
223
+ return {"acquired": True, "thread_key": key, "existing_lock": None}
224
+
225
+ _log_audit("SEND", key, to, subject, f"Lock acquired by session {session_id}")
226
+ return {"acquired": True, "thread_key": key, "existing_lock": None}
227
+
228
+
229
+ def confirm_thread_lock(to: str, subject: str, preview: str = ""):
230
+ """Update a lock file after successful send with confirmation details."""
231
+ key = _thread_key(to, subject)
232
+ path = _lock_path(key)
233
+
234
+ if not os.path.exists(path):
235
+ return
236
+
237
+ try:
238
+ with open(path, "r") as f:
239
+ data = json.load(f)
240
+ except (json.JSONDecodeError, IOError):
241
+ return
242
+
243
+ data["status"] = "confirmed"
244
+ data["confirmed_at"] = _now_iso()
245
+ if preview:
246
+ data["preview"] = preview[:100]
247
+
248
+ try:
249
+ with open(path, "w") as f:
250
+ json.dump(data, f, indent=2)
251
+ except Exception:
252
+ pass
253
+
254
+
255
+ def release_thread_lock(to: str, subject: str):
256
+ """Explicitly remove a thread lock (for testing or correction)."""
257
+ key = _thread_key(to, subject)
258
+ path = _lock_path(key)
259
+ try:
260
+ os.unlink(path)
261
+ except OSError:
262
+ pass
263
+
264
+
265
+ def clean_expired_locks():
266
+ """Purge all lock files older than TTL. Returns count of removed locks."""
267
+ os.makedirs(LOCK_DIR, exist_ok=True)
268
+ removed = 0
269
+ now = time.time()
270
+ for filename in os.listdir(LOCK_DIR):
271
+ if not filename.endswith(".lock"):
272
+ continue
273
+ path = os.path.join(LOCK_DIR, filename)
274
+ try:
275
+ with open(path, "r") as f:
276
+ data = json.load(f)
277
+ lock_ts = data.get("timestamp_epoch", 0)
278
+ if now - lock_ts >= TTL_SECONDS:
279
+ os.unlink(path)
280
+ removed += 1
281
+ except (json.JSONDecodeError, IOError, OSError):
282
+ # Remove corrupt files
283
+ try:
284
+ os.unlink(path)
285
+ removed += 1
286
+ except OSError:
287
+ pass
288
+ return removed
289
+
290
+
291
+ # --- CLI Interface ---
292
+
293
+ def main():
294
+ import argparse
295
+
296
+ parser = argparse.ArgumentParser(
297
+ description="Email thread-level deduplication. Prevents replying twice to the same thread within 24h."
298
+ )
299
+ subparsers = parser.add_subparsers(dest="command", help="Command")
300
+
301
+ # check
302
+ check_p = subparsers.add_parser("check", help="Check if a thread lock exists")
303
+ check_p.add_argument("to", help="Recipient email address")
304
+ check_p.add_argument("subject", help="Email subject line")
305
+
306
+ # acquire
307
+ acq_p = subparsers.add_parser("acquire", help="Acquire a thread lock before sending")
308
+ acq_p.add_argument("to", help="Recipient email address")
309
+ acq_p.add_argument("subject", help="Email subject line")
310
+ acq_p.add_argument("--session-id", default="", help="Session ID for audit trail")
311
+ acq_p.add_argument("--preview", default="", help="First ~100 chars of email body")
312
+
313
+ # release
314
+ rel_p = subparsers.add_parser("release", help="Explicitly remove a thread lock")
315
+ rel_p.add_argument("to", help="Recipient email address")
316
+ rel_p.add_argument("subject", help="Email subject line")
317
+
318
+ # clean
319
+ subparsers.add_parser("clean", help="Purge expired locks (>24h)")
320
+
321
+ args = parser.parse_args()
322
+
323
+ if args.command == "check":
324
+ result = check_thread_lock(args.to, args.subject)
325
+ if result["locked"]:
326
+ lock = result["lock_data"]
327
+ print(f"LOCKED: Thread already replied to at {lock.get('timestamp', '?')} by session {lock.get('session_id', '?')}")
328
+ print(f" Subject: {lock.get('subject_normalized', '?')}")
329
+ print(f" Recipient: {lock.get('recipient_normalized', '?')}")
330
+ print(f" Preview: {lock.get('preview', '')[:80]}")
331
+ sys.exit(2)
332
+ else:
333
+ print("AVAILABLE: No active thread lock")
334
+ sys.exit(0)
335
+
336
+ elif args.command == "acquire":
337
+ result = acquire_thread_lock(args.to, args.subject, args.session_id, args.preview)
338
+ if result["acquired"]:
339
+ print(f"ACQUIRED: Thread lock created (key={result['thread_key']})")
340
+ sys.exit(0)
341
+ else:
342
+ lock = result["existing_lock"]
343
+ print(f"DEDUP_SKIP: Thread already replied to at {lock.get('timestamp', '?')} by session {lock.get('session_id', '?')}")
344
+ sys.exit(2)
345
+
346
+ elif args.command == "release":
347
+ release_thread_lock(args.to, args.subject)
348
+ print("RELEASED")
349
+ sys.exit(0)
350
+
351
+ elif args.command == "clean":
352
+ removed = clean_expired_locks()
353
+ print(f"CLEANED: Removed {removed} expired lock(s)")
354
+ sys.exit(0)
355
+
356
+ else:
357
+ parser.print_help()
358
+ sys.exit(1)
359
+
360
+
361
+ if __name__ == "__main__":
362
+ main()
@@ -0,0 +1,81 @@
1
+ #!/bin/bash
2
+ # Emergency Stop — Immediately halts all Maestro agent operations.
3
+ # Usage: ./scripts/emergency-stop.sh
4
+ #
5
+ # This script is the kill switch for all autonomous operations:
6
+ # 1. Drops .emergency-stop flag (every workflow / cadence consumer / enqueue
7
+ # script honours this on the next tick).
8
+ # 2. Unloads every installed `ai.maestro.<agent>-*` (and legacy
9
+ # `ai.adaptic.<agent>-*`) launchd job.
10
+ # 3. Kills running Claude Code subagent processes.
11
+ #
12
+ # Plist resolution: the agent's first-name slug is read from config/agent.json
13
+ # (SOT) so unload targets the correct labels; falls back to the directory
14
+ # basename if agent.json isn't readable.
15
+
16
+ set -e
17
+
18
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
19
+ AGENT_DIR="$(dirname "$SCRIPT_DIR")"
20
+ LOG_FILE="$AGENT_DIR/logs/emergency-stop.log"
21
+ TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
22
+ mkdir -p "$(dirname "$LOG_FILE")" 2>/dev/null || true
23
+
24
+ # Resolve agent first-name slug from SOT (config/agent.json) so the unload
25
+ # loop targets the right launchd labels. Falls back to the basename of the
26
+ # agent directory (stripping -ai suffix).
27
+ AGENT_FIRST=""
28
+ if [ -f "$AGENT_DIR/config/agent.json" ]; then
29
+ if command -v jq >/dev/null 2>&1; then
30
+ AGENT_FIRST=$(jq -r '.firstName // empty' "$AGENT_DIR/config/agent.json" | tr '[:upper:]' '[:lower:]')
31
+ else
32
+ AGENT_FIRST=$(awk -F'"' '/"firstName"[[:space:]]*:/ { print tolower($4); exit }' "$AGENT_DIR/config/agent.json")
33
+ fi
34
+ fi
35
+ [ -z "$AGENT_FIRST" ] && AGENT_FIRST=$(basename "$AGENT_DIR" | sed 's/-ai$//')
36
+
37
+ LAUNCH_AGENTS_DIR="$HOME/Library/LaunchAgents"
38
+ # Match the current ai.maestro.* namespace AND the legacy ai.adaptic.* one
39
+ # (deployed agents whose plists predate the rename). The loops guard with [ -f ].
40
+ PLIST_GLOB="$LAUNCH_AGENTS_DIR/ai.maestro.${AGENT_FIRST}-*.plist $LAUNCH_AGENTS_DIR/ai.adaptic.${AGENT_FIRST}-*.plist"
41
+
42
+ echo "[$TIMESTAMP] EMERGENCY STOP INITIATED (agent=$AGENT_FIRST)" | tee -a "$LOG_FILE"
43
+
44
+ # 1. Drop the stop flag FIRST so any in-flight work sees it on next tick.
45
+ echo "$TIMESTAMP" > "$AGENT_DIR/.emergency-stop"
46
+ echo "[$TIMESTAMP] Stop flag created" >> "$LOG_FILE"
47
+
48
+ # 2. Unload installed launchd jobs.
49
+ echo "Unloading installed launchd jobs ($PLIST_GLOB)..."
50
+ unloaded=0
51
+ for plist in $PLIST_GLOB; do
52
+ if [ -f "$plist" ]; then
53
+ launchctl unload "$plist" 2>/dev/null || true
54
+ echo "[$TIMESTAMP] Unloaded: $(basename "$plist")" >> "$LOG_FILE"
55
+ unloaded=$((unloaded + 1))
56
+ fi
57
+ done
58
+ echo "[$TIMESTAMP] Unloaded $unloaded launchd job(s)" >> "$LOG_FILE"
59
+
60
+ # 3. Kill running Claude Code subagent processes. Filter to processes that
61
+ # have AGENT_ROOT or this agent's directory in their cwd to avoid
62
+ # killing unrelated claude sessions the operator may have running.
63
+ echo "Stopping Claude Code agent processes..."
64
+ # pgrep -lf is more selective than pkill -f
65
+ pids=$(pgrep -f "claude" 2>/dev/null | tr '\n' ' ' || true)
66
+ if [ -n "$pids" ]; then
67
+ # Send SIGTERM first, give 3s, then SIGKILL stragglers.
68
+ kill -TERM $pids 2>/dev/null || true
69
+ sleep 3
70
+ still=$(pgrep -f "claude" 2>/dev/null | tr '\n' ' ' || true)
71
+ [ -n "$still" ] && kill -KILL $still 2>/dev/null || true
72
+ echo "[$TIMESTAMP] Claude processes terminated ($pids)" >> "$LOG_FILE"
73
+ fi
74
+
75
+ # 4. Log completion.
76
+ echo "[$TIMESTAMP] EMERGENCY STOP COMPLETE — All operations halted" | tee -a "$LOG_FILE"
77
+ echo ""
78
+ echo "To resume operations:"
79
+ echo " ./scripts/resume-operations.sh"
80
+ echo ""
81
+ echo "All actions have been logged to: $LOG_FILE"