@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,217 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * sync-protocol.mjs — THE way to evolve the shared Cohort wire contract.
4
+ *
5
+ * `lib/org/protocol.mjs` is the single CANONICAL source of the org-protocol wire
6
+ * contract (method names, scopes, mutating-method set, event families, board
7
+ * status vocabulary, error codes, and the `computeRowHash` hash-chain function).
8
+ * maestro (ESM .mjs / Node) and Cohort (TS / Next) must agree on it
9
+ * BYTE-FOR-BYTE. The cross-repo parity guard (`lib/org/protocol.checksum.test.mjs`
10
+ * here, plus the mirror test in Cohort) fails the moment the two drift.
11
+ *
12
+ * Workflow to change the protocol:
13
+ * 1. Edit `lib/org/protocol.mjs` (maestro — the canonical copy).
14
+ * 2. Run `node scripts/sync-protocol.mjs` (this script):
15
+ * - recomputes the sha256 of `protocol.mjs`;
16
+ * - copies it to `<cohort>/packages/protocol/index.mjs` (re-vendor);
17
+ * - writes the new checksum to `<cohort>/packages/protocol/protocol.checksum`
18
+ * AND to the maestro checksum reference (`lib/org/protocol.checksum`).
19
+ * - prints the new checksum and the exact files to commit.
20
+ * 3. Commit BOTH repos.
21
+ *
22
+ * No checksum literal lives in any test file. Both parity guards READ the
23
+ * `protocol.checksum` reference at run time:
24
+ * - maestro `lib/org/protocol.checksum.test.mjs` reads `lib/org/protocol.checksum`;
25
+ * - Cohort `packages/protocol/protocol.test.ts` reads `packages/protocol/protocol.checksum`.
26
+ * That is the whole point — there is no frozen `EXPECTED`/`CANONICAL_CHECKSUM`
27
+ * constant for a re-vendor to leave stale; this script only has to keep the two
28
+ * `protocol.checksum` files (and the vendored `index.mjs`) in lock-step with
29
+ * `protocol.mjs`.
30
+ *
31
+ * Idempotent: re-running with an unchanged protocol re-writes the same bytes and
32
+ * the same checksum (no spurious diffs). Fail-soft on a missing Cohort checkout:
33
+ * it still updates + prints the maestro-side checksum so the local guard is never
34
+ * left stale, and it tells you which Cohort files to sync once the repo is present.
35
+ *
36
+ * Usage (the Cohort checkout path is REQUIRED — arg or COHORT_REPO env; there
37
+ * is deliberately no default: a silently-wrong hardcoded path is worse than a
38
+ * hard stop):
39
+ * node scripts/sync-protocol.mjs <cohort-path> # explicit checkout path
40
+ * COHORT_REPO=/path/to/hq node scripts/sync-protocol.mjs
41
+ * node scripts/sync-protocol.mjs <cohort-path> --dry-run # compute + print only
42
+ * node scripts/sync-protocol.mjs <cohort-path> --check # exit non-zero on drift
43
+ *
44
+ * Node builtins only (maestro is "type":"module", near-zero-dep by design).
45
+ *
46
+ * @module scripts/sync-protocol
47
+ */
48
+ "use strict";
49
+
50
+ import { createHash } from "node:crypto";
51
+ import {
52
+ readFileSync,
53
+ writeFileSync,
54
+ mkdirSync,
55
+ existsSync,
56
+ } from "node:fs";
57
+ import { dirname, join, resolve } from "node:path";
58
+ import { fileURLToPath } from "node:url";
59
+ import { applyBrandEnvCompat } from "../lib/env-compat.mjs";
60
+
61
+ // Fleet back-compat FIRST: bridge NEOLITH_* ⇄ COHORT_* env names so a host
62
+ // still exporting NEOLITH_REPO resolves the COHORT_REPO checkout path below.
63
+ applyBrandEnvCompat();
64
+
65
+ const HERE = dirname(fileURLToPath(import.meta.url));
66
+ const REPO_ROOT = resolve(HERE, "..");
67
+
68
+ const PROTOCOL_PATH = join(REPO_ROOT, "lib", "org", "protocol.mjs");
69
+ const MAESTRO_CHECKSUM_PATH = join(REPO_ROOT, "lib", "org", "protocol.checksum");
70
+
71
+ /** sha256 over a file's exact bytes (byte-for-byte — no normalization). */
72
+ function sha256File(path) {
73
+ return createHash("sha256").update(readFileSync(path)).digest("hex");
74
+ }
75
+
76
+ /** Parse argv: first non-flag positional is the Cohort path; collect flags. */
77
+ function parseArgs(argv) {
78
+ const flags = new Set();
79
+ let cohortArg = null;
80
+ for (const a of argv) {
81
+ if (a.startsWith("--")) flags.add(a);
82
+ else if (cohortArg === null) cohortArg = a;
83
+ }
84
+ return { cohortArg, flags };
85
+ }
86
+
87
+ /**
88
+ * Resolve the Cohort checkout root from arg > env. Returns `{ root, source }`
89
+ * (source describes where the path came from for logging), or null when
90
+ * NEITHER is provided — the caller hard-stops rather than writing into a
91
+ * guessed path.
92
+ */
93
+ function resolveCohortRoot(cohortArg) {
94
+ if (cohortArg) return { root: resolve(cohortArg), source: "arg" };
95
+ if (process.env.COHORT_REPO)
96
+ return { root: resolve(process.env.COHORT_REPO), source: "env COHORT_REPO" };
97
+ return null;
98
+ }
99
+
100
+ function main() {
101
+ const { cohortArg, flags } = parseArgs(process.argv.slice(2));
102
+ const dryRun = flags.has("--dry-run") || flags.has("--dry");
103
+ const checkOnly = flags.has("--check");
104
+
105
+ if (!existsSync(PROTOCOL_PATH)) {
106
+ console.error(`sync-protocol: canonical protocol missing at ${PROTOCOL_PATH}`);
107
+ process.exit(2);
108
+ }
109
+
110
+ if (!resolveCohortRoot(cohortArg)) {
111
+ console.error(
112
+ "sync-protocol: no Cohort checkout named — pass the path as the first argument " +
113
+ "(node scripts/sync-protocol.mjs /path/to/hq) or set COHORT_REPO. " +
114
+ "There is deliberately no default: a silently-wrong path would sync the protocol nowhere.",
115
+ );
116
+ process.exit(2);
117
+ }
118
+
119
+ const digest = sha256File(PROTOCOL_PATH);
120
+ console.log(`sync-protocol: canonical lib/org/protocol.mjs`);
121
+ console.log(`sync-protocol: sha256 = ${digest}`);
122
+ if (dryRun) console.log("sync-protocol: --dry-run — no files will be written.");
123
+ if (checkOnly) console.log("sync-protocol: --check — verifying sync, no files will be written.");
124
+
125
+ const committed = new Set();
126
+ let drift = false;
127
+
128
+ // ---- maestro side: checksum reference + frozen test constant ----
129
+ const curChecksum = existsSync(MAESTRO_CHECKSUM_PATH)
130
+ ? readFileSync(MAESTRO_CHECKSUM_PATH, "utf8").trim()
131
+ : null;
132
+ if (curChecksum !== digest) {
133
+ drift = true;
134
+ if (!dryRun && !checkOnly) {
135
+ writeFileSync(MAESTRO_CHECKSUM_PATH, digest + "\n");
136
+ console.log(`sync-protocol: wrote ${MAESTRO_CHECKSUM_PATH}`);
137
+ } else {
138
+ console.log(
139
+ `sync-protocol: OUT OF SYNC ${MAESTRO_CHECKSUM_PATH} (${curChecksum ?? "missing"} != ${digest})`,
140
+ );
141
+ }
142
+ } else {
143
+ console.log(`sync-protocol: up-to-date ${MAESTRO_CHECKSUM_PATH}`);
144
+ }
145
+ committed.add(MAESTRO_CHECKSUM_PATH);
146
+
147
+ // The maestro + Cohort parity guards READ their `protocol.checksum` at run
148
+ // time, so there is no frozen test constant to rewrite here — keeping the two
149
+ // checksum files in sync (below) is sufficient.
150
+
151
+ // ---- Cohort side: re-vendor @cohort/protocol + write its checksum ----
152
+ const { root: cohortRoot, source } = resolveCohortRoot(cohortArg);
153
+ const pkgDir = join(cohortRoot, "packages", "protocol");
154
+ const vendoredIndex = join(pkgDir, "index.mjs");
155
+ const cohortChecksum = join(pkgDir, "protocol.checksum");
156
+
157
+ if (!existsSync(cohortRoot)) {
158
+ console.warn(
159
+ `sync-protocol: WARNING — Cohort checkout not found at ${cohortRoot} (${source}).`,
160
+ );
161
+ console.warn(
162
+ "sync-protocol: skipped re-vendoring @cohort/protocol. Once the repo is present, re-run to sync:",
163
+ );
164
+ console.warn(`sync-protocol: ${vendoredIndex}`);
165
+ console.warn(`sync-protocol: ${cohortChecksum}`);
166
+ } else {
167
+ const protocolBytes = readFileSync(PROTOCOL_PATH);
168
+ const curVendored =
169
+ existsSync(vendoredIndex) ? readFileSync(vendoredIndex) : null;
170
+ const curCohortChecksum =
171
+ existsSync(cohortChecksum)
172
+ ? readFileSync(cohortChecksum, "utf8").trim()
173
+ : null;
174
+ const vendoredInSync =
175
+ curVendored !== null && Buffer.compare(curVendored, protocolBytes) === 0;
176
+ const cohortChecksumInSync = curCohortChecksum === digest;
177
+
178
+ if (!vendoredInSync || !cohortChecksumInSync) {
179
+ drift = true;
180
+ if (!dryRun && !checkOnly) {
181
+ mkdirSync(pkgDir, { recursive: true });
182
+ writeFileSync(vendoredIndex, protocolBytes);
183
+ writeFileSync(cohortChecksum, digest + "\n");
184
+ console.log(`sync-protocol: re-vendored ${vendoredIndex}`);
185
+ console.log(`sync-protocol: wrote ${cohortChecksum}`);
186
+ } else {
187
+ console.log(`sync-protocol: OUT OF SYNC ${vendoredIndex}`);
188
+ console.log(
189
+ `sync-protocol: OUT OF SYNC ${cohortChecksum} (${curCohortChecksum ?? "missing"} != ${digest})`,
190
+ );
191
+ }
192
+ } else {
193
+ console.log(`sync-protocol: up-to-date ${vendoredIndex}`);
194
+ console.log(`sync-protocol: up-to-date ${cohortChecksum}`);
195
+ }
196
+ committed.add(vendoredIndex);
197
+ committed.add(cohortChecksum);
198
+ }
199
+
200
+ // ---- summary ----
201
+ console.log("");
202
+ console.log(`sync-protocol: checksum = ${digest}`);
203
+ console.log("sync-protocol: files to commit (both repos):");
204
+ for (const f of committed) console.log(` - ${f}`);
205
+
206
+ if (checkOnly) {
207
+ if (drift) {
208
+ console.error("sync-protocol: --check FAILED — protocol artifacts are out of sync. Run `node scripts/sync-protocol.mjs`.");
209
+ process.exit(1);
210
+ }
211
+ console.log("sync-protocol: --check OK — everything is in sync.");
212
+ }
213
+
214
+ process.exit(0);
215
+ }
216
+
217
+ main();
@@ -0,0 +1,184 @@
1
+ #!/bin/bash
2
+ # Agent System Verification — checks all components are operational
3
+ # Usage: ./scripts/system-verify.sh
4
+ #
5
+ # Run this anytime to verify the full system is working.
6
+
7
+ set -e
8
+ AGENT_REPO_DIR="${AGENT_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}"
9
+ cd "$AGENT_REPO_DIR"
10
+
11
+ # Load identity (AGENT_LAUNCHD_PREFIX in particular) — the verification
12
+ # checks read launchd labels with the agent-specific prefix.
13
+ if [ -f "$AGENT_REPO_DIR/config/agent.env" ]; then
14
+ # shellcheck disable=SC1091
15
+ source "$AGENT_REPO_DIR/config/agent.env"
16
+ fi
17
+ LAUNCHD_PREFIX="${AGENT_LAUNCHD_PREFIX:-ai.maestro.agent}"
18
+
19
+ RED='\033[0;31m'
20
+ GREEN='\033[0;32m'
21
+ YELLOW='\033[1;33m'
22
+ NC='\033[0m'
23
+
24
+ pass=0
25
+ warn=0
26
+ fail=0
27
+
28
+ check() {
29
+ if eval "$2" > /dev/null 2>&1; then
30
+ echo -e " ${GREEN}✓${NC} $1"
31
+ ((pass++))
32
+ else
33
+ echo -e " ${RED}✗${NC} $1"
34
+ ((fail++))
35
+ fi
36
+ }
37
+
38
+ warn_check() {
39
+ if eval "$2" > /dev/null 2>&1; then
40
+ echo -e " ${GREEN}✓${NC} $1"
41
+ ((pass++))
42
+ else
43
+ echo -e " ${YELLOW}⚠${NC} $1"
44
+ ((warn++))
45
+ fi
46
+ }
47
+
48
+ echo "╔══════════════════════════════════════════════╗"
49
+ printf "║ %-44s║\n" "$(echo "${AGENT_FULL_NAME:-AGENT}" | tr '[:lower:]' '[:upper:]') SYSTEM VERIFICATION"
50
+ echo "║ $(date '+%Y-%m-%d %H:%M %Z') ║"
51
+ echo "╚══════════════════════════════════════════════╝"
52
+ echo ""
53
+
54
+ echo "=== DAEMONS ==="
55
+ # Launchd labels start with AGENT_LAUNCHD_PREFIX (e.g. ai.maestro.ravi).
56
+ # The legacy code searched for "robin-poller" etc.; we now derive the
57
+ # agent-specific component from the prefix's last segment so the check
58
+ # works for any agent.
59
+ LAUNCHD_AGENT_SLUG="$(basename "$LAUNCHD_PREFIX" | tr -d ' ')" # e.g. "ravi" from "ai.maestro.ravi"
60
+ check "Poller daemon loaded" "launchctl list | grep -q ${LAUNCHD_AGENT_SLUG}-poller"
61
+ check "Inbox processor loaded" "launchctl list | grep -q ${LAUNCHD_AGENT_SLUG}-inbox-processor"
62
+ check "Meeting prep loaded" "launchctl list | grep -q ${LAUNCHD_AGENT_SLUG}-meeting-prep"
63
+ check "Meeting capture loaded" "launchctl list | grep -q ${LAUNCHD_AGENT_SLUG}-meeting-action-capture"
64
+ check "Midday sweep loaded" "launchctl list | grep -q ${LAUNCHD_AGENT_SLUG}-midday-sweep"
65
+ check "Weekly hiring loaded" "launchctl list | grep -q ${LAUNCHD_AGENT_SLUG}-weekly-hiring"
66
+ check "Weekly priorities loaded" "launchctl list | grep -q ${LAUNCHD_AGENT_SLUG}-weekly-priorities"
67
+ check "Weekly eng health loaded" "launchctl list | grep -q ${LAUNCHD_AGENT_SLUG}-weekly-engineering-health"
68
+ check "Weekly execution loaded" "launchctl list | grep -q ${LAUNCHD_AGENT_SLUG}-weekly-execution"
69
+ check "Weekly strategic memo loaded" "launchctl list | grep -q ${LAUNCHD_AGENT_SLUG}-weekly-strategic-memo"
70
+ check "Quarterly self-assessment loaded" "launchctl list | grep -q ${LAUNCHD_AGENT_SLUG}-quarterly"
71
+ echo ""
72
+
73
+ echo "=== REMOTE TRIGGERS ==="
74
+ warn_check "Git pushed to origin" "test $(git rev-list --count origin/main..HEAD) -eq 0"
75
+ echo " Remote triggers read from GitHub — must be pushed to work"
76
+ echo ""
77
+
78
+ echo "=== POLLER ACTIVITY ==="
79
+ TODAY=$(date -u +%Y-%m-%d)
80
+ YESTERDAY=$(date -u -v-1d +%Y-%m-%d 2>/dev/null || date -u -d yesterday +%Y-%m-%d 2>/dev/null || echo "unknown")
81
+ POLLER_LOG="logs/polling/${TODAY}-poller.jsonl"
82
+ POLLER_LOG_YESTERDAY="logs/polling/${YESTERDAY}-poller.jsonl"
83
+ if [ -f "$POLLER_LOG" ]; then
84
+ LAST_POLL=$(tail -1 "$POLLER_LOG" | python3 -c "import sys,json; print(json.loads(sys.stdin.read()).get('timestamp','unknown'))" 2>/dev/null || echo "unknown")
85
+ POLL_COUNT=$(wc -l < "$POLLER_LOG" | tr -d ' ')
86
+ echo -e " ${GREEN}✓${NC} Poller active today: $POLL_COUNT polls, last at $LAST_POLL"
87
+ ((pass++))
88
+ elif [ -f "$POLLER_LOG_YESTERDAY" ]; then
89
+ LAST_POLL=$(tail -1 "$POLLER_LOG_YESTERDAY" | python3 -c "import sys,json; print(json.loads(sys.stdin.read()).get('timestamp','unknown'))" 2>/dev/null || echo "unknown")
90
+ echo -e " ${YELLOW}⚠${NC} No log today, yesterday's last poll: $LAST_POLL"
91
+ ((warn++))
92
+ else
93
+ echo -e " ${RED}✗${NC} No poller logs found"
94
+ ((fail++))
95
+ fi
96
+ echo ""
97
+
98
+ echo "=== WORKFLOW EXECUTION ==="
99
+ for workflow in inbox-processor meeting-prep meeting-action-capture; do
100
+ LOG="logs/workflows/${TODAY}-${workflow}.jsonl"
101
+ LOG_Y="logs/workflows/${YESTERDAY}-${workflow}.jsonl"
102
+ if [ -f "$LOG" ]; then
103
+ ENTRIES=$(wc -l < "$LOG" | tr -d ' ')
104
+ echo -e " ${GREEN}✓${NC} $workflow: $ENTRIES log entries today"
105
+ ((pass++))
106
+ elif [ -f "$LOG_Y" ]; then
107
+ echo -e " ${YELLOW}⚠${NC} $workflow: ran yesterday, not yet today"
108
+ ((warn++))
109
+ else
110
+ echo -e " ${RED}✗${NC} $workflow: no recent logs"
111
+ ((fail++))
112
+ fi
113
+ done
114
+ echo ""
115
+
116
+ echo "=== QUEUE HEALTH ==="
117
+ TOTAL=0
118
+ for q in state/queues/*.yaml; do
119
+ COUNT=$(grep -c "^ - id:" "$q" 2>/dev/null || echo 0)
120
+ TOTAL=$((TOTAL + COUNT))
121
+ done
122
+ echo " Total items across all queues: $TOTAL"
123
+ if [ "$TOTAL" -gt 0 ]; then
124
+ echo -e " ${GREEN}✓${NC} Queues populated"
125
+ ((pass++))
126
+ else
127
+ echo -e " ${RED}✗${NC} All queues empty — backlog not loaded"
128
+ ((fail++))
129
+ fi
130
+
131
+ CRITICAL=$(grep -r "priority: critical" state/queues/ 2>/dev/null | wc -l | tr -d ' ')
132
+ echo " Critical items: $CRITICAL"
133
+ echo ""
134
+
135
+ echo "=== DASHBOARDS ==="
136
+ for dash in executive-summary strategy-scorecard org-health leadership-accountability engineering-health relationship-pipeline capital-readiness risk-register system-health; do
137
+ check "Dashboard: $dash" "test -f state/dashboards/${dash}.yaml"
138
+ done
139
+ echo ""
140
+
141
+ echo "=== STATE FILES ==="
142
+ check "15+ queue files" "test $(ls state/queues/*.yaml | wc -l) -ge 15"
143
+ check "5 user profiles" "test $(ls memory/profiles/users/*.yaml | wc -l) -ge 5"
144
+ check "10 channel profiles" "test $(ls memory/profiles/channels/*.yaml | wc -l) -ge 10"
145
+ check "Decision library" "test -f knowledge/decisions/index.yaml"
146
+ check "Strategy state" "test -f knowledge/syntheses/strategy-state.yaml"
147
+ check "Blocker playbooks" "test -f knowledge/sources/blocker-playbooks.yaml"
148
+ check "SLA defaults" "test -f config/sla-defaults.yaml"
149
+ check "Defence policy" "test -f policies/prompt-injection-defence.yaml"
150
+ echo ""
151
+
152
+ echo "=== HOOKS ==="
153
+ check "Pre-send audit hook" "test -x scripts/hooks/pre-send-audit.sh"
154
+ check "Post-action log hook" "test -x scripts/hooks/post-action-log.sh"
155
+ check "Session-end log hook" "test -x scripts/hooks/session-end-log.sh"
156
+ check "Hooks configured in settings" "grep -q 'pre-send-audit' .claude/settings.json"
157
+ echo ""
158
+
159
+ echo "=== TRIGGER PROMPTS ==="
160
+ TRIGGER_COUNT=$(ls schedules/triggers/*.md | wc -l | tr -d ' ')
161
+ check "12+ trigger prompts" "test $TRIGGER_COUNT -ge 12"
162
+ echo " Trigger prompts: $TRIGGER_COUNT"
163
+ echo ""
164
+
165
+ echo "=== CLAUDE.MD ==="
166
+ CT_COUNT=$(grep -c "Control Tower" CLAUDE.md)
167
+ check "12 control towers documented" "test $CT_COUNT -ge 12"
168
+ check "Agent teams enabled" "grep -q 'AGENT_TEAMS' .claude/settings.json"
169
+ echo ""
170
+
171
+ echo "══════════════════════════════════════════════"
172
+ echo -e " ${GREEN}PASS: $pass${NC} ${YELLOW}WARN: $warn${NC} ${RED}FAIL: $fail${NC}"
173
+ echo "══════════════════════════════════════════════"
174
+
175
+ if [ "$fail" -gt 0 ]; then
176
+ echo -e " ${RED}System has failures — investigate above${NC}"
177
+ exit 1
178
+ elif [ "$warn" -gt 0 ]; then
179
+ echo -e " ${YELLOW}System operational with warnings${NC}"
180
+ exit 0
181
+ else
182
+ echo -e " ${GREEN}System fully operational${NC}"
183
+ exit 0
184
+ fi
@@ -0,0 +1,239 @@
1
+ #!/usr/bin/env python3
2
+ """Tests for email-thread-dedup.py — thread-level email deduplication.
3
+
4
+ Run: python3 scripts/test-email-thread-dedup.py
5
+ """
6
+ import json
7
+ import os
8
+ import sys
9
+ import time
10
+
11
+ # Add scripts dir to path
12
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
13
+ from email_thread_dedup import (
14
+ _normalize_subject,
15
+ _normalize_recipient,
16
+ _thread_key,
17
+ check_thread_lock,
18
+ acquire_thread_lock,
19
+ confirm_thread_lock,
20
+ release_thread_lock,
21
+ clean_expired_locks,
22
+ LOCK_DIR,
23
+ TTL_SECONDS,
24
+ _lock_path,
25
+ )
26
+
27
+ PASS = 0
28
+ FAIL = 0
29
+
30
+
31
+ def assert_eq(label, actual, expected):
32
+ global PASS, FAIL
33
+ if actual == expected:
34
+ PASS += 1
35
+ print(f" PASS: {label}")
36
+ else:
37
+ FAIL += 1
38
+ print(f" FAIL: {label}")
39
+ print(f" expected: {expected!r}")
40
+ print(f" actual: {actual!r}")
41
+
42
+
43
+ def assert_true(label, value):
44
+ assert_eq(label, bool(value), True)
45
+
46
+
47
+ def assert_false(label, value):
48
+ assert_eq(label, bool(value), False)
49
+
50
+
51
+ # --- Test subject normalization ---
52
+
53
+ print("\n=== Subject normalization ===")
54
+ assert_eq("strips Re:", _normalize_subject("Re: Hello"), "Hello")
55
+ assert_eq("strips RE:", _normalize_subject("RE: Hello"), "Hello")
56
+ assert_eq("strips Fwd:", _normalize_subject("Fwd: Hello"), "Hello")
57
+ assert_eq("strips FW:", _normalize_subject("FW: Hello"), "Hello")
58
+ assert_eq("strips nested Re: Re:", _normalize_subject("Re: Re: Hello"), "Hello")
59
+ assert_eq("strips mixed Re: Fwd:", _normalize_subject("Re: Fwd: Hello"), "Hello")
60
+ assert_eq("normalizes whitespace", _normalize_subject(" Re: Hello World "), "Hello World")
61
+ assert_eq("preserves clean subject", _normalize_subject("Hello World"), "Hello World")
62
+
63
+ # --- Test recipient normalization ---
64
+
65
+ print("\n=== Recipient normalization ===")
66
+ assert_eq("lowercases", _normalize_recipient("John@Example.COM"), "john@example.com")
67
+ assert_eq("strips display name", _normalize_recipient("John Doe <john@example.com>"), "john@example.com")
68
+ assert_eq("takes first of multiple", _normalize_recipient("john@a.com, jane@b.com"), "john@a.com")
69
+ assert_eq("strips whitespace", _normalize_recipient(" john@example.com "), "john@example.com")
70
+
71
+ # --- Test thread key consistency ---
72
+
73
+ print("\n=== Thread key consistency ===")
74
+ key1 = _thread_key("john@example.com", "Re: Hello World")
75
+ key2 = _thread_key("john@example.com", "Hello World")
76
+ key3 = _thread_key("john@example.com", "RE: Fwd: Hello World")
77
+ key4 = _thread_key("JOHN@EXAMPLE.COM", "Re: Hello World")
78
+ key5 = _thread_key("john@example.com", "Different Subject")
79
+
80
+ assert_eq("Re: stripped matches base", key1, key2)
81
+ assert_eq("RE: Fwd: stripped matches base", key3, key2)
82
+ assert_eq("case-insensitive recipient", key4, key1)
83
+ assert_true("different subject = different key", key5 != key1)
84
+
85
+ # --- Test lock lifecycle ---
86
+
87
+ print("\n=== Lock lifecycle ===")
88
+
89
+ # Use unique test subjects to avoid interference
90
+ TEST_TO = "test-dedup@example.com"
91
+ TEST_SUBJECT = f"Test Subject {time.time()}"
92
+
93
+ # Clean start
94
+ release_thread_lock(TEST_TO, TEST_SUBJECT)
95
+
96
+ # 1. Check — should be available
97
+ result = check_thread_lock(TEST_TO, TEST_SUBJECT)
98
+ assert_false("initially unlocked", result["locked"])
99
+
100
+ # 2. Acquire — should succeed
101
+ acq = acquire_thread_lock(TEST_TO, TEST_SUBJECT, session_id="test-session-1", preview="Hello this is a test")
102
+ assert_true("first acquire succeeds", acq["acquired"])
103
+ assert_true("thread key returned", len(acq["thread_key"]) > 0)
104
+
105
+ # 3. Check again — should now be locked
106
+ result = check_thread_lock(TEST_TO, TEST_SUBJECT)
107
+ assert_true("now locked", result["locked"])
108
+ assert_eq("locked by correct session", result["lock_data"]["session_id"], "test-session-1")
109
+
110
+ # 4. Second acquire — should be BLOCKED
111
+ acq2 = acquire_thread_lock(TEST_TO, TEST_SUBJECT, session_id="test-session-2", preview="Different wording")
112
+ assert_false("second acquire blocked", acq2["acquired"])
113
+ assert_true("existing lock returned", acq2["existing_lock"] is not None)
114
+ assert_eq("existing lock session", acq2["existing_lock"]["session_id"], "test-session-1")
115
+
116
+ # 5. Confirm — should update status
117
+ confirm_thread_lock(TEST_TO, TEST_SUBJECT, preview="Confirmed preview text")
118
+ result = check_thread_lock(TEST_TO, TEST_SUBJECT)
119
+ assert_true("still locked after confirm", result["locked"])
120
+ assert_eq("status is confirmed", result["lock_data"]["status"], "confirmed")
121
+
122
+ # 6. Release — should unlock
123
+ release_thread_lock(TEST_TO, TEST_SUBJECT)
124
+ result = check_thread_lock(TEST_TO, TEST_SUBJECT)
125
+ assert_false("unlocked after release", result["locked"])
126
+
127
+ # --- Test expired lock handling ---
128
+
129
+ print("\n=== Expired lock handling ===")
130
+ TEST_SUBJECT_EXP = f"Expired Subject {time.time()}"
131
+
132
+ # Create a lock with a timestamp in the past (>24h ago)
133
+ key = _thread_key(TEST_TO, TEST_SUBJECT_EXP)
134
+ path = _lock_path(key)
135
+ os.makedirs(LOCK_DIR, exist_ok=True)
136
+ expired_data = {
137
+ "thread_key": key,
138
+ "timestamp": "2026-01-01T00:00:00Z",
139
+ "timestamp_epoch": time.time() - (TTL_SECONDS + 3600), # 25 hours ago
140
+ "session_id": "old-session",
141
+ "subject_normalized": "Expired Subject",
142
+ "recipient_normalized": TEST_TO,
143
+ "preview": "old message",
144
+ "status": "confirmed",
145
+ }
146
+ with open(path, "w") as f:
147
+ json.dump(expired_data, f)
148
+
149
+ # Check — expired lock should NOT block
150
+ result = check_thread_lock(TEST_TO, TEST_SUBJECT_EXP)
151
+ assert_false("expired lock does not block", result["locked"])
152
+
153
+ # Acquire over expired lock — should succeed
154
+ acq = acquire_thread_lock(TEST_TO, TEST_SUBJECT_EXP, session_id="new-session")
155
+ assert_true("acquire over expired lock succeeds", acq["acquired"])
156
+
157
+ # Clean up
158
+ release_thread_lock(TEST_TO, TEST_SUBJECT_EXP)
159
+
160
+ # --- Test Re:/Fwd: variant blocking ---
161
+
162
+ print("\n=== Subject variant blocking ===")
163
+ TEST_SUBJECT_VAR = f"Important Meeting Notes {time.time()}"
164
+ release_thread_lock(TEST_TO, TEST_SUBJECT_VAR) # clean start
165
+
166
+ # Acquire with base subject
167
+ acq = acquire_thread_lock(TEST_TO, TEST_SUBJECT_VAR, session_id="session-a")
168
+ assert_true("acquire base subject", acq["acquired"])
169
+
170
+ # Attempt with Re: variant — should be blocked (same thread)
171
+ acq2 = acquire_thread_lock(TEST_TO, f"Re: {TEST_SUBJECT_VAR}", session_id="session-b")
172
+ assert_false("Re: variant blocked", acq2["acquired"])
173
+
174
+ # Attempt with RE: Fwd: variant — should also be blocked
175
+ acq3 = acquire_thread_lock(TEST_TO, f"RE: Fwd: {TEST_SUBJECT_VAR}", session_id="session-c")
176
+ assert_false("RE: Fwd: variant blocked", acq3["acquired"])
177
+
178
+ # Clean up
179
+ release_thread_lock(TEST_TO, TEST_SUBJECT_VAR)
180
+
181
+ # --- Test clean_expired_locks ---
182
+
183
+ print("\n=== Cleanup function ===")
184
+ TEST_SUBJECT_CLN = f"Cleanup Test {time.time()}"
185
+
186
+ # Create an expired lock
187
+ key = _thread_key(TEST_TO, TEST_SUBJECT_CLN)
188
+ path = _lock_path(key)
189
+ expired_data = {
190
+ "thread_key": key,
191
+ "timestamp": "2026-01-01T00:00:00Z",
192
+ "timestamp_epoch": time.time() - (TTL_SECONDS + 3600),
193
+ "session_id": "cleanup-test",
194
+ "subject_normalized": "Cleanup Test",
195
+ "recipient_normalized": TEST_TO,
196
+ "status": "confirmed",
197
+ }
198
+ with open(path, "w") as f:
199
+ json.dump(expired_data, f)
200
+
201
+ assert_true("expired lock file exists before clean", os.path.exists(path))
202
+ removed = clean_expired_locks()
203
+ assert_true("cleanup removed at least 1", removed >= 1)
204
+ assert_false("expired lock file gone after clean", os.path.exists(path))
205
+
206
+ # --- Test lock file contents ---
207
+
208
+ print("\n=== Lock file contents ===")
209
+ TEST_SUBJECT_META = f"Metadata Check {time.time()}"
210
+ release_thread_lock(TEST_TO, TEST_SUBJECT_META)
211
+
212
+ acquire_thread_lock(TEST_TO, TEST_SUBJECT_META, session_id="meta-session", preview="Hello, I wanted to follow up on our conversation")
213
+ key = _thread_key(TEST_TO, TEST_SUBJECT_META)
214
+ path = _lock_path(key)
215
+
216
+ with open(path, "r") as f:
217
+ data = json.load(f)
218
+
219
+ assert_eq("has thread_key", data.get("thread_key"), key)
220
+ assert_true("has timestamp", "timestamp" in data)
221
+ assert_true("has timestamp_epoch", "timestamp_epoch" in data)
222
+ assert_eq("has session_id", data.get("session_id"), "meta-session")
223
+ assert_true("has subject_normalized", "subject_normalized" in data)
224
+ assert_true("has recipient_normalized", "recipient_normalized" in data)
225
+ assert_true("has preview", len(data.get("preview", "")) > 0)
226
+ assert_eq("status is acquired", data.get("status"), "acquired")
227
+
228
+ release_thread_lock(TEST_TO, TEST_SUBJECT_META)
229
+
230
+ # --- Summary ---
231
+
232
+ print(f"\n{'='*50}")
233
+ print(f"Results: {PASS} passed, {FAIL} failed, {PASS + FAIL} total")
234
+ if FAIL > 0:
235
+ print("SOME TESTS FAILED")
236
+ sys.exit(1)
237
+ else:
238
+ print("ALL TESTS PASSED")
239
+ sys.exit(0)