@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,319 @@
1
+ /**
2
+ * knowledge.test.mjs — agent-facing shared-memory API over the Cohort knowledge
3
+ * plane. Verifies the verb surface (remember/recall/forget/rewrite + contacts/
4
+ * meetings) routes to the right methods, that the client NEVER filters recall
5
+ * results (the server is authoritative for ACL), that forget invalidates (never
6
+ * deletes), and the fail-open contract when the integration is disabled.
7
+ * Uses an injected fetchImpl — no network.
8
+ * Run: node --test lib/org/knowledge.test.mjs
9
+ */
10
+ "use strict";
11
+
12
+ import { test } from "node:test";
13
+ import assert from "node:assert/strict";
14
+
15
+ import {
16
+ remember,
17
+ rewrite,
18
+ recall,
19
+ recallDetailed,
20
+ forget,
21
+ upsertContact,
22
+ listContacts,
23
+ recordMeeting,
24
+ listMeetings,
25
+ normalizeGroup,
26
+ _internals,
27
+ } from "./knowledge.mjs";
28
+
29
+ const BASE = "https://org.cohortapp.com";
30
+ const CFG = { org: { cohort: { enabled: true, base: BASE, token: "tok" } } };
31
+ const OFF = { org: { cohort: { enabled: false } } };
32
+
33
+ delete process.env.COHORT_API_TOKEN;
34
+ delete process.env.COHORT_TOKEN;
35
+
36
+ /** A fake fetch that records calls and returns a canned res frame / payload. */
37
+ function fakeFetch(responder) {
38
+ const calls = [];
39
+ const impl = async (url, init) => {
40
+ calls.push({
41
+ url,
42
+ init,
43
+ method: init && init.method,
44
+ headers: (init && init.headers) || {},
45
+ body: init && init.body ? JSON.parse(init.body) : null,
46
+ });
47
+ const r = responder ? responder(url, init, calls.length - 1) : null;
48
+ const status = r && typeof r.status === "number" ? r.status : 200;
49
+ const ok = r && typeof r.ok === "boolean" ? r.ok : status >= 200 && status < 300;
50
+ const body = r ? r.body : { ok: true, result: null };
51
+ return { ok, status, headers: { get: () => undefined }, json: async () => body };
52
+ };
53
+ impl.calls = calls;
54
+ return impl;
55
+ }
56
+
57
+ // --- normalizeGroup (pure shape helper; NOT an ACL decision) ----------------
58
+
59
+ test("normalizeGroup: org / unit:<id> / barrier:<cell>; rejects unknown kinds", () => {
60
+ assert.equal(normalizeGroup("org"), "org");
61
+ assert.equal(normalizeGroup({ kind: "org" }), "org");
62
+ assert.equal(normalizeGroup({ kind: "unit", id: "eng" }), "unit:eng");
63
+ assert.equal(normalizeGroup({ kind: "barrier", cell: "deals-2026" }), "barrier:deals-2026");
64
+ assert.equal(normalizeGroup("unit:eng"), "unit:eng");
65
+ assert.equal(normalizeGroup("barrier:deals"), "barrier:deals");
66
+ // unknown kinds / bare ids fall through to "let the server decide"
67
+ assert.equal(normalizeGroup("bogus:x"), undefined);
68
+ assert.equal(normalizeGroup("eng"), undefined);
69
+ assert.equal(normalizeGroup(""), undefined);
70
+ assert.equal(normalizeGroup(undefined), undefined);
71
+ });
72
+
73
+ // --- remember → knowledge.append (default) / knowledge.replace (CAS) ---------
74
+
75
+ test("remember(string): appends an episode (the sole shared-write primitive)", async () => {
76
+ const f = fakeFetch(() => ({ body: { ok: true, result: { id: "k-1", version: 1 } } }));
77
+ const r = await remember("we closed the Q3 deal", { cfg: CFG, group: "org", fetchImpl: f });
78
+ assert.equal(r.ok, true);
79
+ assert.equal(r.mode, "append");
80
+ assert.equal(r.id, "k-1");
81
+ assert.equal(f.calls[0].url, `${BASE}/api/v1/knowledge.append`);
82
+ assert.equal(f.calls[0].method, "POST");
83
+ assert.equal(f.calls[0].headers.authorization, "Bearer tok");
84
+ assert.deepEqual(f.calls[0].body, { text: "we closed the Q3 deal", group: "org" });
85
+ });
86
+
87
+ test("remember(fact object): pulls through opts (kind/participants/provenance), normalises group", async () => {
88
+ const f = fakeFetch(() => ({ body: { ok: true, result: { id: "k-2" } } }));
89
+ await remember(
90
+ { text: "intro call notes" },
91
+ { cfg: CFG, group: { kind: "unit", id: "sales" }, kind: "note", participants: ["a", "b"], provenance: { src: "call" }, fetchImpl: f },
92
+ );
93
+ assert.deepEqual(f.calls[0].body, {
94
+ text: "intro call notes",
95
+ kind: "note",
96
+ participants: ["a", "b"],
97
+ provenance: { src: "call" },
98
+ group: "unit:sales",
99
+ });
100
+ });
101
+
102
+ test("remember with {id, expectedVersion}: routes to knowledge.replace (CAS)", async () => {
103
+ const f = fakeFetch(() => ({ body: { ok: true, result: { id: "k-9", version: 4 } } }));
104
+ const r = await remember({ text: "corrected figure" }, { cfg: CFG, id: "k-9", expectedVersion: 3, fetchImpl: f });
105
+ assert.equal(r.mode, "replace");
106
+ assert.equal(f.calls[0].url, `${BASE}/api/v1/knowledge.replace`);
107
+ assert.equal(f.calls[0].body.id, "k-9");
108
+ assert.equal(f.calls[0].body.expectedVersion, 3);
109
+ });
110
+
111
+ test("remember: idempotency key auto-derived from id on append", async () => {
112
+ const f = fakeFetch(() => ({ body: { ok: true, result: { id: "k-5" } } }));
113
+ await remember({ id: "k-5", text: "x" }, { cfg: CFG, fetchImpl: f });
114
+ assert.equal(f.calls[0].headers["x-idempotency-key"], "knowledge.append:k-5");
115
+ });
116
+
117
+ test("remember: disabled → fail-open skipped, no fetch", async () => {
118
+ const f = fakeFetch();
119
+ const r = await remember("x", { cfg: OFF, fetchImpl: f });
120
+ assert.deepEqual(r, { ok: false, skipped: true });
121
+ assert.equal(f.calls.length, 0);
122
+ });
123
+
124
+ test("remember: empty fact → BAD_REQUEST, no fetch", async () => {
125
+ const f = fakeFetch();
126
+ const r = await remember("", { cfg: CFG, fetchImpl: f });
127
+ assert.equal(r.ok, false);
128
+ assert.equal(r.error.code, "BAD_REQUEST");
129
+ assert.equal(f.calls.length, 0);
130
+ });
131
+
132
+ // --- rewrite → knowledge.rewrite (designated-owner-only) --------------------
133
+
134
+ test("rewrite: routes to knowledge.rewrite with the id + text", async () => {
135
+ const f = fakeFetch(() => ({ body: { ok: true, result: { id: "k-1" } } }));
136
+ const r = await rewrite("k-1", "owner-asserted text", { cfg: CFG, fetchImpl: f });
137
+ assert.equal(r.mode, "rewrite");
138
+ assert.equal(f.calls[0].url, `${BASE}/api/v1/knowledge.rewrite`);
139
+ assert.equal(f.calls[0].body.id, "k-1");
140
+ assert.equal(f.calls[0].body.text, "owner-asserted text");
141
+ });
142
+
143
+ test("rewrite: non-owner FORBIDDEN_SCOPE surfaces in error (server is authoritative)", async () => {
144
+ const f = fakeFetch(() => ({ ok: false, status: 403, body: { ok: false, error: { code: "FORBIDDEN_SCOPE", message: "owner only" } } }));
145
+ const r = await rewrite("k-1", "x", { cfg: CFG, fetchImpl: f });
146
+ assert.equal(r.ok, false);
147
+ assert.equal(r.error.code, "FORBIDDEN_SCOPE");
148
+ });
149
+
150
+ // --- recall → knowledge.search; CLIENT NEVER FILTERS ------------------------
151
+
152
+ test("remember → recall round-trip: recall returns exactly the server's facts", async () => {
153
+ // The server is the ONLY ACL authority. recall renders whatever it returns —
154
+ // here a fact the agent appended comes back from search.
155
+ const serverFacts = [{ id: "k-1", text: "we closed the Q3 deal", group: "org" }];
156
+ let appended = false;
157
+ const f = fakeFetch((url) => {
158
+ if (url.endsWith("/v1/knowledge.append")) { appended = true; return { body: { ok: true, result: { id: "k-1" } } }; }
159
+ if (url.endsWith("/v1/knowledge.search")) return { body: { ok: true, result: { facts: serverFacts, returned_ids: ["k-1"], groups_considered: ["org"] } } };
160
+ return { body: { ok: true, result: null } };
161
+ });
162
+ await remember("we closed the Q3 deal", { cfg: CFG, group: "org", fetchImpl: f });
163
+ assert.equal(appended, true);
164
+ const facts = await recall("Q3 deal", { cfg: CFG, fetchImpl: f });
165
+ assert.deepEqual(facts, serverFacts);
166
+ });
167
+
168
+ test("recall: passes the query (+ group hint, limit) to the server; does NOT filter results", async () => {
169
+ // Server returns a barrier fact AND an org fact. The client must NOT drop the
170
+ // barrier one — if the server returned it, the agent was granted that cell.
171
+ const returned = [
172
+ { id: "k-1", group: "org", text: "public" },
173
+ { id: "k-2", group: "barrier:deals-2026", text: "walled-but-granted" },
174
+ ];
175
+ const f = fakeFetch(() => ({ body: { ok: true, result: { facts: returned, returned_ids: ["k-1", "k-2"], groups_considered: ["org", "barrier:deals-2026"] } } }));
176
+ const facts = await recall("deal terms", { cfg: CFG, group: { kind: "unit", id: "eng" }, limit: 3, fetchImpl: f });
177
+ // client renders EVERYTHING the server returned, including the barrier fact
178
+ assert.deepEqual(facts, returned);
179
+ // and it forwarded the query + the normalised group hint + limit
180
+ assert.equal(f.calls[0].body.query, "deal terms");
181
+ assert.equal(f.calls[0].body.group, "unit:eng");
182
+ assert.equal(f.calls[0].body.limit, 3);
183
+ });
184
+
185
+ test("recall: when the server returns FEWER facts (RLS floored), the client returns exactly those", async () => {
186
+ // Agent asks broadly; server's RLS floor excludes a barrier fact it isn't
187
+ // granted. The client has no idea a fact was withheld — it just renders [k-1].
188
+ const f = fakeFetch(() => ({ body: { ok: true, result: { facts: [{ id: "k-1", group: "org" }], returned_ids: ["k-1"], groups_considered: ["org", "unit:eng"] } } }));
189
+ const facts = await recall("everything", { cfg: CFG, fetchImpl: f });
190
+ assert.deepEqual(facts, [{ id: "k-1", group: "org" }]);
191
+ });
192
+
193
+ test("recallDetailed: surfaces returnedIds + groupsConsidered from the read-audit envelope", async () => {
194
+ const f = fakeFetch(() => ({ body: { ok: true, result: { facts: [{ id: "k-1" }, { id: "k-2" }], returned_ids: ["k-1", "k-2"], groups_considered: ["org", "unit:eng"] } } }));
195
+ const r = await recallDetailed("q", { cfg: CFG, fetchImpl: f });
196
+ assert.equal(r.ok, true);
197
+ assert.deepEqual(r.returnedIds, ["k-1", "k-2"]);
198
+ assert.deepEqual(r.groupsConsidered, ["org", "unit:eng"]);
199
+ });
200
+
201
+ test("recallDetailed: derives returnedIds from facts when the server omits returned_ids", async () => {
202
+ const f = fakeFetch(() => ({ body: { ok: true, result: { facts: [{ id: "k-3" }, { id: "k-4" }] } } }));
203
+ const r = await recallDetailed("q", { cfg: CFG, fetchImpl: f });
204
+ assert.deepEqual(r.returnedIds, ["k-3", "k-4"]);
205
+ assert.deepEqual(r.groupsConsidered, []);
206
+ });
207
+
208
+ test("recall: lexical flag forwarded (keyword stand-in for vector search)", async () => {
209
+ const f = fakeFetch(() => ({ body: { ok: true, result: { facts: [] } } }));
210
+ await recall("keyword", { cfg: CFG, lexical: true, fetchImpl: f });
211
+ assert.equal(f.calls[0].body.lexical, true);
212
+ });
213
+
214
+ test("recall: disabled or empty query → [] (fail-open), no fetch", async () => {
215
+ const f = fakeFetch();
216
+ assert.deepEqual(await recall("q", { cfg: OFF, fetchImpl: f }), []);
217
+ assert.deepEqual(await recall(" ", { cfg: CFG, fetchImpl: f }), []);
218
+ assert.equal(f.calls.length, 0);
219
+ });
220
+
221
+ test("recall: server error → [] (fail-open); recallDetailed reports the error", async () => {
222
+ const f = fakeFetch(() => ({ ok: false, status: 403, body: { ok: false, error: { code: "FORBIDDEN_SCOPE", message: "no knowledge.read" } } }));
223
+ assert.deepEqual(await recall("q", { cfg: CFG, fetchImpl: f }), []);
224
+ const f2 = fakeFetch(() => ({ ok: false, status: 403, body: { ok: false, error: { code: "FORBIDDEN_SCOPE", message: "no knowledge.read" } } }));
225
+ const r = await recallDetailed("q", { cfg: CFG, fetchImpl: f2 });
226
+ assert.equal(r.ok, false);
227
+ assert.deepEqual(r.facts, []);
228
+ assert.equal(r.error.code, "FORBIDDEN_SCOPE");
229
+ });
230
+
231
+ // --- forget → knowledge.invalidate (tombstone, NEVER hard-delete) -----------
232
+
233
+ test("forget: calls knowledge.invalidate (not a delete); carries reason + idempotency", async () => {
234
+ const f = fakeFetch(() => ({ body: { ok: true, result: { id: "k-1", invalid: true } } }));
235
+ const r = await forget("k-1", { cfg: CFG, reason: "superseded by k-2", supersededBy: "k-2", fetchImpl: f });
236
+ assert.equal(r.ok, true);
237
+ assert.equal(r.mode, "invalidate");
238
+ assert.equal(f.calls[0].url, `${BASE}/api/v1/knowledge.invalidate`);
239
+ assert.equal(f.calls[0].body.id, "k-1");
240
+ assert.equal(f.calls[0].body.reason, "superseded by k-2");
241
+ assert.equal(f.calls[0].body.supersededBy, "k-2");
242
+ assert.equal(f.calls[0].headers["x-idempotency-key"], "knowledge.invalidate:k-1");
243
+ });
244
+
245
+ test("forget: disabled → skipped; missing id → BAD_REQUEST; no fetch", async () => {
246
+ const f = fakeFetch();
247
+ assert.deepEqual(await forget("k-1", { cfg: OFF, fetchImpl: f }), { ok: false, skipped: true });
248
+ const bad = await forget("", { cfg: CFG, fetchImpl: f });
249
+ assert.equal(bad.error.code, "BAD_REQUEST");
250
+ assert.equal(f.calls.length, 0);
251
+ });
252
+
253
+ // --- contacts -------------------------------------------------------------
254
+
255
+ test("upsertContact: posts the right shape to contacts.upsert (idempotent on id)", async () => {
256
+ const f = fakeFetch(() => ({ body: { ok: true, result: { id: "c-1" } } }));
257
+ const r = await upsertContact({ id: "c-1", name: "Jordan", emails: ["j@x.com"], group: { kind: "unit", id: "sales" } }, { cfg: CFG, fetchImpl: f });
258
+ assert.equal(r.ok, true);
259
+ assert.equal(f.calls[0].url, `${BASE}/api/v1/contacts.upsert`);
260
+ assert.deepEqual(f.calls[0].body, { id: "c-1", name: "Jordan", emails: ["j@x.com"], group: "unit:sales" });
261
+ assert.equal(f.calls[0].headers["x-idempotency-key"], "contacts.upsert:c-1");
262
+ });
263
+
264
+ test("listContacts: returns the server's ACL-filtered list as-is; [] when disabled", async () => {
265
+ const f = fakeFetch(() => ({ body: { contacts: [{ id: "c-1" }, { id: "c-2" }] } }));
266
+ assert.deepEqual(await listContacts({ cfg: CFG, q: "jord", fetchImpl: f }), [{ id: "c-1" }, { id: "c-2" }]);
267
+ assert.match(f.calls[0].url, /\/v1\/contacts\.list\?q=jord$/);
268
+ assert.deepEqual(await listContacts({ cfg: OFF }), []);
269
+ });
270
+
271
+ test("upsertContact: server NOT_FOUND (handler lane unbuilt) surfaces unimplemented + warns, never a silent swallow", async () => {
272
+ const f = fakeFetch(() => ({ body: { ok: false, error: { code: "NOT_FOUND", message: "unknown method contacts.upsert" } } }));
273
+ const warnings = [];
274
+ const orig = console.warn;
275
+ console.warn = (m) => warnings.push(String(m));
276
+ let r;
277
+ try {
278
+ r = await upsertContact({ id: "c-9", name: "Sam" }, { cfg: CFG, fetchImpl: f });
279
+ } finally {
280
+ console.warn = orig;
281
+ }
282
+ assert.equal(r.ok, false);
283
+ assert.equal(r.unimplemented, true, "NOT_FOUND is flagged as an unimplemented capability, not a plain failure");
284
+ assert.equal(r.error.code, "NOT_FOUND");
285
+ assert.ok(warnings.some((w) => w.includes("contacts.upsert")), "emits a loud, actionable warning naming the missing method");
286
+ });
287
+
288
+ // --- meetings -------------------------------------------------------------
289
+
290
+ test("recordMeeting: posts the right shape to meetings.record (idempotent on id)", async () => {
291
+ const f = fakeFetch(() => ({ body: { ok: true, result: { id: "m-1" } } }));
292
+ const r = await recordMeeting({ id: "m-1", title: "board sync", at: "2026-06-11T10:00:00Z", participants: ["a", "b"] }, { cfg: CFG, fetchImpl: f });
293
+ assert.equal(r.ok, true);
294
+ assert.equal(f.calls[0].url, `${BASE}/api/v1/meetings.record`);
295
+ assert.deepEqual(f.calls[0].body, { id: "m-1", title: "board sync", at: "2026-06-11T10:00:00Z", participants: ["a", "b"] });
296
+ assert.equal(f.calls[0].headers["x-idempotency-key"], "meetings.record:m-1");
297
+ });
298
+
299
+ test("listMeetings: returns the server's participant-ACL list as-is; [] when disabled", async () => {
300
+ const f = fakeFetch(() => ({ body: { meetings: [{ id: "m-1" }] } }));
301
+ assert.deepEqual(await listMeetings({ cfg: CFG, since: "2026-06-01", fetchImpl: f }), [{ id: "m-1" }]);
302
+ assert.match(f.calls[0].url, /\/v1\/meetings\.list\?since=2026-06-01$/);
303
+ assert.deepEqual(await listMeetings({ cfg: OFF }), []);
304
+ });
305
+
306
+ // --- pure internals --------------------------------------------------------
307
+
308
+ test("_internals.pickArray: finds the first array key, [] otherwise", () => {
309
+ assert.deepEqual(_internals.pickArray({ facts: [1, 2] }, ["facts", "results"]), [1, 2]);
310
+ assert.deepEqual(_internals.pickArray({ results: [3] }, ["facts", "results"]), [3]);
311
+ assert.deepEqual(_internals.pickArray([9], ["facts"]), [9]);
312
+ assert.deepEqual(_internals.pickArray({ nope: 1 }, ["facts"]), []);
313
+ assert.deepEqual(_internals.pickArray(null, ["facts"]), []);
314
+ });
315
+
316
+ test("_internals.factToParams: string→{text}; opts pull-through; group normalised; id stringified", () => {
317
+ const p = _internals.factToParams("hi", { kind: "note", id: 42, group: { kind: "unit", id: "eng" } });
318
+ assert.deepEqual(p, { text: "hi", kind: "note", id: "42", group: "unit:eng" });
319
+ });
@@ -0,0 +1,335 @@
1
+ /**
2
+ * lib/org/leases.mjs — agent-side lease helpers over the Cohort lease primitive.
3
+ *
4
+ * The Cohort server exposes one generic lease primitive (lib/org/protocol.mjs
5
+ * LEASE_SCOPES: work-claim / thread-ownership / session-leadership /
6
+ * singleton-cadence). Exactly one holder per (scope, resourceId) until
7
+ * expiry/release — the server resolves the race atomically (single-conditional
8
+ * CAS), the loser gets a CONFLICT/LEASE_HELD error frame.
9
+ *
10
+ * This module is the agent-facing convenience layer over that primitive:
11
+ *
12
+ * - claimLease(scope, resourceId, opts) — generic claim, 3s fail-open.
13
+ * - claimThread(channel, thread, opts) — thread-ownership claim for
14
+ * shared-channel SEND ARBITRATION (§5.4): when ≥2 agents see the same channel
15
+ * message, exactly one wins a `thread-ownership` lease on resource_id
16
+ * `channel:<thread>` and replies; the others stand down.
17
+ * - releaseThread / releaseLease — free a claimed lease.
18
+ * - preSendArbitration(...) — the cancellable pre-send check the
19
+ * channel send path calls; returns {mayReply, leaseToken, ...}. Exactly one
20
+ * agent gets mayReply:true for a given thread.
21
+ *
22
+ * FAIL-OPEN DISCIPLINE (§5.4 rule 4 / design rule 3 "coordination fails open,
23
+ * counted"): a lease call that does not resolve within `failOpenTimeoutMs`
24
+ * (default 3000) is treated as a WIN — the agent proceeds rather than going
25
+ * silent when the coordination plane is down. Every fail-open is surfaced
26
+ * (`failedOpen:true`) so the caller can bump `failOpenSends` in
27
+ * state/org/metrics.json and report it on reconnect.
28
+ *
29
+ * Channels NOT listed `claim-on-receive` in config/ownership.yaml are not
30
+ * arbitrated: preSendArbitration short-circuits to {mayReply:true} for them, so
31
+ * the send path pays no lease round-trip on un-shared channels.
32
+ *
33
+ * Pure-core + injectable IO: the Cohort client is injected as `opts.client`
34
+ * (defaults to the real lib/org/client.mjs helpers) and the ownership config is
35
+ * injected as `opts.ownership` (else read from config/ownership.yaml). Node
36
+ * builtins only. ESM.
37
+ *
38
+ * @module lib/org/leases
39
+ */
40
+
41
+ "use strict";
42
+
43
+ import { readFileSync, existsSync } from "node:fs";
44
+ import { join } from "node:path";
45
+
46
+ import { LEASE_SCOPES } from "./protocol.mjs";
47
+ import * as client from "./client.mjs";
48
+
49
+ /** Default fail-open ceiling for any single lease round-trip (§5.4). */
50
+ export const DEFAULT_FAIL_OPEN_MS = 3000;
51
+
52
+ /** The thread-ownership lease scope + default TTL (10m, per §5.4 rule 1). */
53
+ export const THREAD_OWNERSHIP_SCOPE = "thread-ownership";
54
+ const DEFAULT_THREAD_TTL_MS = 10 * 60 * 1000;
55
+
56
+ // ---------------------------------------------------------------------------
57
+ // ownership.yaml — which channels are arbitrated
58
+ // ---------------------------------------------------------------------------
59
+
60
+ /**
61
+ * Load + parse config/ownership.yaml. Returns the parsed doc, or a benign empty
62
+ * shape (`{channels:[]}`) when the file is absent/unreadable — so an agent
63
+ * without the config simply arbitrates nothing (no channel is claim-on-receive).
64
+ * Fail-open. Injectable root for tests.
65
+ * @param {string} [agentRoot="."]
66
+ * @returns {Promise<{schemaVersion?:number, channels:object[], mentionTtlSec?:number, arbitration?:object}>}
67
+ */
68
+ export async function loadOwnership(agentRoot = ".") {
69
+ const path = join(String(agentRoot || "."), "config", "ownership.yaml");
70
+ if (!existsSync(path)) return { channels: [] };
71
+ try {
72
+ const m = await import("js-yaml");
73
+ const yaml = m.default || m;
74
+ const doc = yaml.load(readFileSync(path, "utf8"));
75
+ if (doc && typeof doc === "object") {
76
+ return { channels: Array.isArray(doc.channels) ? doc.channels : [], ...doc };
77
+ }
78
+ return { channels: [] };
79
+ } catch {
80
+ return { channels: [] };
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Find the ownership rule for a (platform, channel/thread-id) pair, if any.
86
+ * Matches on `channel` or `chat` (telegram) keys, case-insensitively for ids.
87
+ * Pure.
88
+ * @param {{channels:object[]}} ownership
89
+ * @param {string} platform
90
+ * @param {string} channelId
91
+ * @returns {object|null} the matching channel rule, or null
92
+ */
93
+ export function ownershipRuleFor(ownership, platform, channelId) {
94
+ const chans = (ownership && Array.isArray(ownership.channels)) ? ownership.channels : [];
95
+ const plat = String(platform || "").toLowerCase();
96
+ const id = String(channelId || "");
97
+ for (const c of chans) {
98
+ if (!c || typeof c !== "object") continue;
99
+ if (String(c.platform || "").toLowerCase() !== plat) continue;
100
+ const ruleId = c.channel != null ? String(c.channel) : (c.chat != null ? String(c.chat) : "");
101
+ if (ruleId && (ruleId === id || ruleId.toLowerCase() === id.toLowerCase())) return c;
102
+ }
103
+ return null;
104
+ }
105
+
106
+ /**
107
+ * Is this channel in `claim-on-receive` arbitration mode? Pure. Channels with no
108
+ * rule (or `send-time-only` / any other mode) are NOT claim-on-receive.
109
+ * @param {{channels:object[]}} ownership @param {string} platform @param {string} channelId
110
+ * @returns {boolean}
111
+ */
112
+ export function isClaimOnReceive(ownership, platform, channelId) {
113
+ const rule = ownershipRuleFor(ownership, platform, channelId);
114
+ return !!(rule && rule.mode === "claim-on-receive");
115
+ }
116
+
117
+ // ---------------------------------------------------------------------------
118
+ // resource-key canonicalisation
119
+ // ---------------------------------------------------------------------------
120
+
121
+ /**
122
+ * Canonical thread-ownership resource id. Both racing agents must derive the
123
+ * SAME key for the server to adjudicate a single winner, so we normalise: lower
124
+ * the platform, keep the channel id as-is (Slack ids are already uppercase by the
125
+ * conversation-resolver convention), and join with the thread/ts. Pure.
126
+ * @param {string} channel the channel/chat id (already canonicalised upstream)
127
+ * @param {string} thread the thread root ts / message id (falls back to channel)
128
+ * @returns {string} e.g. "channel:C0123ABC:1700000000.0001"
129
+ */
130
+ export function threadResourceId(channel, thread) {
131
+ const ch = String(channel || "").trim();
132
+ const th = String(thread || "").trim();
133
+ return th ? `channel:${ch}:${th}` : `channel:${ch}`;
134
+ }
135
+
136
+ // ---------------------------------------------------------------------------
137
+ // fail-open lease round-trip
138
+ // ---------------------------------------------------------------------------
139
+
140
+ /**
141
+ * Resolve a value, but never wait longer than `ms`; on timeout resolve with
142
+ * `onTimeout`. Used to bound every lease round-trip at the fail-open ceiling so
143
+ * a hung server can never stall a send. Pure over its inputs.
144
+ * @template T
145
+ * @param {Promise<T>} promise
146
+ * @param {number} ms
147
+ * @param {() => T} onTimeout
148
+ * @returns {Promise<T>}
149
+ */
150
+ function withTimeout(promise, ms, onTimeout) {
151
+ if (!(ms > 0)) return promise;
152
+ return new Promise((resolve) => {
153
+ let settled = false;
154
+ const timer = setTimeout(() => {
155
+ if (settled) return;
156
+ settled = true;
157
+ resolve(onTimeout());
158
+ }, ms);
159
+ if (timer && typeof timer.unref === "function") timer.unref();
160
+ Promise.resolve(promise).then(
161
+ (v) => { if (!settled) { settled = true; clearTimeout(timer); resolve(v); } },
162
+ () => { if (!settled) { settled = true; clearTimeout(timer); resolve(onTimeout()); } },
163
+ );
164
+ });
165
+ }
166
+
167
+ /** The Cohort client surface this module needs (injectable for tests). */
168
+ function resolveClient(opts) {
169
+ return (opts && opts.client) || client;
170
+ }
171
+
172
+ /** Connection options for a client RPC, distilled from opts. */
173
+ function rpcOpts(opts) {
174
+ return {
175
+ base: opts && opts.base,
176
+ token: opts && opts.token,
177
+ fetchImpl: opts && opts.fetchImpl,
178
+ idempotencyKey: opts && opts.idempotencyKey,
179
+ };
180
+ }
181
+
182
+ /**
183
+ * Claim a generic lease over the Cohort lease primitive. 3s fail-open: if the
184
+ * server does not adjudicate within `failOpenTimeoutMs`, the caller WINS (the
185
+ * coordination plane fails open, counted).
186
+ *
187
+ * @param {string} scope one of LEASE_SCOPES
188
+ * @param {string} resourceId the resource the lease guards (e.g. a thread key)
189
+ * @param {object} [opts] - {
190
+ * client?, base, token, fetchImpl?, idempotencyKey?, holder?, ttlMs?,
191
+ * failOpenTimeoutMs?
192
+ * }
193
+ * @returns {Promise<{held:boolean, leaseToken:string|null, failedOpen:boolean, reason:string, frame?:object}>}
194
+ */
195
+ export async function claimLease(scope, resourceId, opts = {}) {
196
+ if (!LEASE_SCOPES.includes(scope)) {
197
+ return { held: false, leaseToken: null, failedOpen: false, reason: `bad-scope:${scope}` };
198
+ }
199
+ if (!resourceId) {
200
+ return { held: false, leaseToken: null, failedOpen: false, reason: "no-resource" };
201
+ }
202
+ const c = resolveClient(opts);
203
+ const ms = Number.isFinite(opts.failOpenTimeoutMs) ? opts.failOpenTimeoutMs : DEFAULT_FAIL_OPEN_MS;
204
+ const params = {
205
+ scope,
206
+ resourceId,
207
+ holder: opts.holder || undefined,
208
+ ttlMs: Number.isFinite(opts.ttlMs) ? opts.ttlMs : undefined,
209
+ };
210
+ const frame = await withTimeout(
211
+ Promise.resolve(c.leaseClaim(params, rpcOpts(opts))),
212
+ ms,
213
+ () => ({ __failOpen: true }),
214
+ );
215
+ if (frame && frame.__failOpen) {
216
+ // Server didn't answer in time → fail OPEN: proceed as if we hold it, but
217
+ // with no real lease token (so we can't heartbeat/release a phantom lease).
218
+ return { held: true, leaseToken: null, failedOpen: true, reason: "fail-open-timeout" };
219
+ }
220
+ if (frame && frame.ok) {
221
+ const res = frame.result || {};
222
+ const leaseToken = res.token || res.leaseToken || null;
223
+ return { held: true, leaseToken, failedOpen: false, reason: "claimed", frame };
224
+ }
225
+ // Adjudicated loss (CONFLICT/LEASE_HELD) or any other error frame → stand down.
226
+ const code = frame && frame.error && frame.error.code;
227
+ return { held: false, leaseToken: null, failedOpen: false, reason: code || "denied", frame };
228
+ }
229
+
230
+ /**
231
+ * Release a generic lease. Best-effort + bounded fail-open; a fail-open claim
232
+ * (no real token) is a no-op release. Never throws.
233
+ * @param {string} scope @param {string} resourceId
234
+ * @param {object} [opts] - { client?, base, token, fetchImpl?, leaseToken, failOpenTimeoutMs? }
235
+ * @returns {Promise<{released:boolean, reason:string}>}
236
+ */
237
+ export async function releaseLease(scope, resourceId, opts = {}) {
238
+ if (!opts.leaseToken) return { released: false, reason: "no-token" };
239
+ const c = resolveClient(opts);
240
+ const ms = Number.isFinite(opts.failOpenTimeoutMs) ? opts.failOpenTimeoutMs : DEFAULT_FAIL_OPEN_MS;
241
+ const frame = await withTimeout(
242
+ Promise.resolve(c.leaseRelease({ scope, resourceId, leaseToken: opts.leaseToken }, rpcOpts(opts))),
243
+ ms,
244
+ () => ({ __failOpen: true }),
245
+ );
246
+ if (frame && frame.__failOpen) return { released: false, reason: "fail-open-timeout" };
247
+ return { released: !!(frame && frame.ok), reason: frame && frame.ok ? "released" : "error" };
248
+ }
249
+
250
+ /**
251
+ * Heartbeat a held lease (slide its expiry). Best-effort, bounded fail-open.
252
+ * @param {string} scope @param {string} resourceId
253
+ * @param {object} [opts] - { client?, base, token, fetchImpl?, leaseToken, failOpenTimeoutMs? }
254
+ * @returns {Promise<{ok:boolean, reason:string}>}
255
+ */
256
+ export async function heartbeatLease(scope, resourceId, opts = {}) {
257
+ if (!opts.leaseToken) return { ok: false, reason: "no-token" };
258
+ const c = resolveClient(opts);
259
+ const ms = Number.isFinite(opts.failOpenTimeoutMs) ? opts.failOpenTimeoutMs : DEFAULT_FAIL_OPEN_MS;
260
+ const frame = await withTimeout(
261
+ Promise.resolve(c.leaseHeartbeat({ scope, resourceId, leaseToken: opts.leaseToken }, rpcOpts(opts))),
262
+ ms,
263
+ () => ({ __failOpen: true }),
264
+ );
265
+ if (frame && frame.__failOpen) return { ok: false, reason: "fail-open-timeout" };
266
+ return { ok: !!(frame && frame.ok), reason: frame && frame.ok ? "ok" : "error" };
267
+ }
268
+
269
+ // ---------------------------------------------------------------------------
270
+ // thread-ownership (send arbitration §5.4)
271
+ // ---------------------------------------------------------------------------
272
+
273
+ /**
274
+ * Claim the right to reply in a shared channel thread (thread-ownership lease).
275
+ * Exactly one racing agent wins; the others get held:false. 3s fail-open.
276
+ *
277
+ * @param {string} channel the (canonicalised) channel/chat id
278
+ * @param {string} thread the thread root id (or "" for a top-level message)
279
+ * @param {object} [opts] - { client?, base, token, fetchImpl?, holder?, ttlMs?, failOpenTimeoutMs? }
280
+ * @returns {Promise<{held:boolean, leaseToken:string|null, failedOpen:boolean, reason:string, resourceId:string}>}
281
+ */
282
+ export async function claimThread(channel, thread, opts = {}) {
283
+ const resourceId = threadResourceId(channel, thread);
284
+ const r = await claimLease(THREAD_OWNERSHIP_SCOPE, resourceId, {
285
+ ttlMs: Number.isFinite(opts.ttlMs) ? opts.ttlMs : DEFAULT_THREAD_TTL_MS,
286
+ ...opts,
287
+ });
288
+ return { ...r, resourceId };
289
+ }
290
+
291
+ /**
292
+ * Release a thread-ownership lease (e.g. after the reply is sent, or on stand-down).
293
+ * @param {string} channel @param {string} thread
294
+ * @param {object} [opts] - { client?, base, token, fetchImpl?, leaseToken, failOpenTimeoutMs? }
295
+ * @returns {Promise<{released:boolean, reason:string}>}
296
+ */
297
+ export function releaseThread(channel, thread, opts = {}) {
298
+ return releaseLease(THREAD_OWNERSHIP_SCOPE, threadResourceId(channel, thread), opts);
299
+ }
300
+
301
+ /**
302
+ * The cancellable PRE-SEND arbitration check the channel send path calls before
303
+ * replying in a shared channel (§5.4 rule 3, the send-time backstop). Returns
304
+ * `{mayReply, leaseToken}`: exactly one agent gets mayReply:true for a given
305
+ * thread (the server adjudicates the single winner). Channels that are NOT
306
+ * claim-on-receive short-circuit to mayReply:true with no lease round-trip.
307
+ *
308
+ * On a 3s server timeout it FAILS OPEN (mayReply:true, failedOpen:true) so a
309
+ * down coordination plane never silences the agent — the caller increments the
310
+ * fail-open counter and reports on reconnect.
311
+ *
312
+ * @param {object} args - {
313
+ * platform, channel, thread?, ownership?, agentRoot?,
314
+ * client?, base, token, fetchImpl?, holder?, ttlMs?, failOpenTimeoutMs?
315
+ * }
316
+ * @returns {Promise<{mayReply:boolean, leaseToken:string|null, failedOpen:boolean, arbitrated:boolean, reason:string, resourceId?:string}>}
317
+ */
318
+ export async function preSendArbitration(args = {}) {
319
+ const { platform, channel, thread } = args;
320
+ // Resolve the ownership config (injected or read from disk).
321
+ const ownership = args.ownership || (await loadOwnership(args.agentRoot || "."));
322
+ if (!isClaimOnReceive(ownership, platform, channel)) {
323
+ // Not an arbitrated channel — reply freely, no lease spent.
324
+ return { mayReply: true, leaseToken: null, failedOpen: false, arbitrated: false, reason: "not-arbitrated" };
325
+ }
326
+ const r = await claimThread(channel, thread || "", args);
327
+ return {
328
+ mayReply: r.held,
329
+ leaseToken: r.leaseToken,
330
+ failedOpen: r.failedOpen,
331
+ arbitrated: true,
332
+ reason: r.reason,
333
+ resourceId: r.resourceId,
334
+ };
335
+ }