@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,201 @@
1
+ /**
2
+ * lib/collective/presence.mjs — concurrent-session presence registry.
3
+ *
4
+ * Gives every running Claude Code session situational awareness of the OTHER
5
+ * sessions active on the machine right now. Each session is one JSON file under
6
+ * `state/collective/presence/<sessionId>.json`, written with the same atomic-`wx`
7
+ * + fail-open idiom as scripts/daemon/session-lock.mjs. Stale entries (no
8
+ * heartbeat past `staleSec`, or a dead PID) are reaped on read.
9
+ *
10
+ * Anti-bloat: presence surfaces as ONE summary line in the SessionStart digest,
11
+ * never a full dump. Everything is fail-open — presence never blocks a session.
12
+ *
13
+ * Record shape:
14
+ * { sessionId, pid, cwd, repo, agentName, startedAt, lastBeat,
15
+ * intent, model, status, kind }
16
+ *
17
+ * @module lib/collective/presence
18
+ */
19
+
20
+ "use strict";
21
+
22
+ import {
23
+ writeFileSync, readFileSync, unlinkSync, readdirSync, mkdirSync, existsSync,
24
+ } from "node:fs";
25
+ import { join, basename } from "node:path";
26
+
27
+ const PRESENCE_REL = "state/collective/presence";
28
+ const DEFAULT_STALE_SEC = 300;
29
+
30
+ function presenceDir(agentRoot) {
31
+ return join(agentRoot, PRESENCE_REL);
32
+ }
33
+
34
+ function safeId(sessionId) {
35
+ return String(sessionId || "unknown").replace(/[^a-zA-Z0-9_-]/g, "-").slice(0, 120);
36
+ }
37
+
38
+ /** Liveness probe — signal 0 checks existence without sending a signal. */
39
+ function isProcessRunning(pid) {
40
+ if (!pid || !Number.isFinite(pid)) return true; // unknown pid → don't reap on this basis
41
+ try {
42
+ process.kill(pid, 0);
43
+ return true;
44
+ } catch (err) {
45
+ return err.code === "EPERM"; // exists but not ours
46
+ }
47
+ }
48
+
49
+ function nowIso(nowMs) {
50
+ return new Date(Number.isFinite(nowMs) ? nowMs : Date.now()).toISOString();
51
+ }
52
+
53
+ /**
54
+ * Register (or refresh) a session in the presence registry. Idempotent: a
55
+ * second call for the same sessionId overwrites, preserving startedAt.
56
+ *
57
+ * @param {string} agentRoot
58
+ * @param {object} info - { sessionId, pid?, cwd?, repo?, agentName?, intent?, model?, status?, kind?, nowMs? }
59
+ * @returns {boolean} success
60
+ */
61
+ export function register(agentRoot, info = {}) {
62
+ if (!agentRoot || !info.sessionId) return false;
63
+ const dir = presenceDir(agentRoot);
64
+ const path = join(dir, `${safeId(info.sessionId)}.json`);
65
+ try {
66
+ mkdirSync(dir, { recursive: true });
67
+ let startedAt = nowIso(info.nowMs);
68
+ try {
69
+ if (existsSync(path)) {
70
+ const prev = JSON.parse(readFileSync(path, "utf8"));
71
+ if (prev && prev.startedAt) startedAt = prev.startedAt;
72
+ }
73
+ } catch { /* ignore prior */ }
74
+ const rec = {
75
+ sessionId: String(info.sessionId),
76
+ pid: Number.isFinite(info.pid) ? info.pid : process.pid,
77
+ cwd: info.cwd || "",
78
+ repo: info.repo || (info.cwd ? basename(info.cwd) : ""),
79
+ agentName: info.agentName || "",
80
+ startedAt,
81
+ lastBeat: nowIso(info.nowMs),
82
+ intent: String(info.intent || "").slice(0, 200),
83
+ model: info.model || "",
84
+ status: info.status || "active",
85
+ kind: info.kind || "interactive",
86
+ };
87
+ writeFileSync(path, JSON.stringify(rec, null, 2));
88
+ return true;
89
+ } catch {
90
+ return false;
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Refresh a session's heartbeat (and optionally intent/status). No-op if the
96
+ * record is gone. Fail-open.
97
+ * @param {string} agentRoot @param {string} sessionId @param {object} [updates]
98
+ */
99
+ export function heartbeat(agentRoot, sessionId, updates = {}) {
100
+ if (!agentRoot || !sessionId) return false;
101
+ const path = join(presenceDir(agentRoot), `${safeId(sessionId)}.json`);
102
+ try {
103
+ const rec = JSON.parse(readFileSync(path, "utf8"));
104
+ rec.lastBeat = nowIso(updates.nowMs);
105
+ if (updates.intent !== undefined) rec.intent = String(updates.intent).slice(0, 200);
106
+ if (updates.status !== undefined) rec.status = updates.status;
107
+ writeFileSync(path, JSON.stringify(rec, null, 2));
108
+ return true;
109
+ } catch {
110
+ return false;
111
+ }
112
+ }
113
+
114
+ /** Remove a session from the registry (called on Stop / SessionEnd). */
115
+ export function deregister(agentRoot, sessionId) {
116
+ if (!agentRoot || !sessionId) return;
117
+ try {
118
+ unlinkSync(join(presenceDir(agentRoot), `${safeId(sessionId)}.json`));
119
+ } catch { /* already gone — fine */ }
120
+ }
121
+
122
+ /**
123
+ * List live sessions, reaping stale (no heartbeat past staleSec) or dead-PID
124
+ * entries as a side effect. Newest-heartbeat first.
125
+ *
126
+ * @param {string} agentRoot
127
+ * @param {object} [opts] - { staleSec, nowMs, reap=true }
128
+ * @returns {object[]}
129
+ */
130
+ export function list(agentRoot, opts = {}) {
131
+ if (!agentRoot) return [];
132
+ const dir = presenceDir(agentRoot);
133
+ if (!existsSync(dir)) return [];
134
+ const staleMs = (Number.isFinite(opts.staleSec) ? opts.staleSec : DEFAULT_STALE_SEC) * 1000;
135
+ const nowMs = Number.isFinite(opts.nowMs) ? opts.nowMs : Date.now();
136
+ const reap = opts.reap !== false;
137
+ let files;
138
+ try {
139
+ files = readdirSync(dir).filter((f) => f.endsWith(".json"));
140
+ } catch {
141
+ return [];
142
+ }
143
+ const live = [];
144
+ for (const f of files) {
145
+ const path = join(dir, f);
146
+ let rec;
147
+ try {
148
+ rec = JSON.parse(readFileSync(path, "utf8"));
149
+ } catch {
150
+ if (reap) { try { unlinkSync(path); } catch { /* */ } }
151
+ continue;
152
+ }
153
+ const age = nowMs - Date.parse(rec.lastBeat || rec.startedAt || 0);
154
+ const stale = !Number.isFinite(age) || age > staleMs;
155
+ const dead = !isProcessRunning(rec.pid);
156
+ if (stale || dead) {
157
+ if (reap) { try { unlinkSync(path); } catch { /* */ } }
158
+ continue;
159
+ }
160
+ live.push(rec);
161
+ }
162
+ live.sort((a, b) => Date.parse(b.lastBeat || 0) - Date.parse(a.lastBeat || 0));
163
+ return live;
164
+ }
165
+
166
+ /** Human "Xm"/"Xs"/"Xh" age from a lastBeat ISO, relative to nowMs. */
167
+ function ageLabel(iso, nowMs) {
168
+ const ms = nowMs - Date.parse(iso);
169
+ if (!Number.isFinite(ms) || ms < 0) return "now";
170
+ const s = Math.round(ms / 1000);
171
+ if (s < 45) return "just now";
172
+ const m = Math.round(s / 60);
173
+ if (m < 60) return `${m}m`;
174
+ const h = Math.round(m / 60);
175
+ return `${h}h`;
176
+ }
177
+
178
+ /**
179
+ * Build the one-line presence summary for the SessionStart digest. Excludes the
180
+ * current session. Returns "" when no other sessions are active (so the digest
181
+ * can omit the line entirely).
182
+ *
183
+ * @param {string} agentRoot
184
+ * @param {object} [opts] - { excludeSessionId, staleSec, nowMs, max=5 }
185
+ * @returns {string}
186
+ */
187
+ export function summarize(agentRoot, opts = {}) {
188
+ const nowMs = Number.isFinite(opts.nowMs) ? opts.nowMs : Date.now();
189
+ const max = Number.isFinite(opts.max) ? opts.max : 5;
190
+ const others = list(agentRoot, { staleSec: opts.staleSec, nowMs, reap: opts.reap })
191
+ .filter((r) => r.sessionId !== opts.excludeSessionId);
192
+ if (others.length === 0) return "";
193
+ const parts = others.slice(0, max).map((r) => {
194
+ const where = r.kind === "daemon" ? "daemon" : (r.repo || "session");
195
+ const what = r.intent ? `: ${r.intent}` : "";
196
+ return `${where}${what} (${ageLabel(r.lastBeat, nowMs)})`;
197
+ });
198
+ const extra = others.length > max ? ` +${others.length - max} more` : "";
199
+ const noun = others.length === 1 ? "session" : "sessions";
200
+ return `${others.length} other ${noun} active — ${parts.join("; ")}${extra}`;
201
+ }
@@ -0,0 +1,95 @@
1
+ /**
2
+ * presence.test.mjs — tests for the concurrent-session presence registry.
3
+ * Run: node --test lib/collective/presence.test.mjs
4
+ */
5
+ "use strict";
6
+
7
+ import { test } from "node:test";
8
+ import assert from "node:assert/strict";
9
+ import { mkdtempSync, rmSync } from "node:fs";
10
+ import { tmpdir } from "node:os";
11
+ import { join } from "node:path";
12
+
13
+ import { register, heartbeat, deregister, list, summarize } from "./presence.mjs";
14
+
15
+ function tmp() {
16
+ return mkdtempSync(join(tmpdir(), "collective-presence-"));
17
+ }
18
+ const T0 = Date.parse("2026-06-03T12:00:00Z");
19
+
20
+ test("register + list: round-trips a live session", () => {
21
+ const root = tmp();
22
+ try {
23
+ assert.equal(register(root, { sessionId: "s1", cwd: "/Users/x/maestro", intent: "triage inbox", nowMs: T0 }), true);
24
+ const live = list(root, { nowMs: T0 + 1000, staleSec: 300 });
25
+ assert.equal(live.length, 1);
26
+ assert.equal(live[0].sessionId, "s1");
27
+ assert.equal(live[0].repo, "maestro", "repo derived from cwd basename");
28
+ assert.equal(live[0].intent, "triage inbox");
29
+ } finally {
30
+ rmSync(root, { recursive: true, force: true });
31
+ }
32
+ });
33
+
34
+ test("heartbeat: refreshes lastBeat and intent", () => {
35
+ const root = tmp();
36
+ try {
37
+ register(root, { sessionId: "s1", nowMs: T0 });
38
+ heartbeat(root, "s1", { nowMs: T0 + 60_000, intent: "drafting reply" });
39
+ const live = list(root, { nowMs: T0 + 61_000, staleSec: 300 });
40
+ assert.equal(live[0].intent, "drafting reply");
41
+ assert.equal(Date.parse(live[0].lastBeat), T0 + 60_000);
42
+ } finally {
43
+ rmSync(root, { recursive: true, force: true });
44
+ }
45
+ });
46
+
47
+ test("deregister: removes the session", () => {
48
+ const root = tmp();
49
+ try {
50
+ register(root, { sessionId: "s1", nowMs: T0 });
51
+ deregister(root, "s1");
52
+ assert.equal(list(root, { nowMs: T0 + 1000 }).length, 0);
53
+ } finally {
54
+ rmSync(root, { recursive: true, force: true });
55
+ }
56
+ });
57
+
58
+ test("list: reaps stale sessions (no heartbeat past staleSec)", () => {
59
+ const root = tmp();
60
+ try {
61
+ register(root, { sessionId: "stale", nowMs: T0 });
62
+ // 10 min later with a 5-min stale window → reaped.
63
+ const live = list(root, { nowMs: T0 + 10 * 60_000, staleSec: 300 });
64
+ assert.equal(live.length, 0);
65
+ // Confirm it was actually deleted, not just filtered.
66
+ assert.equal(list(root, { nowMs: T0 + 10 * 60_000 + 1, staleSec: 999999 }).length, 0);
67
+ } finally {
68
+ rmSync(root, { recursive: true, force: true });
69
+ }
70
+ });
71
+
72
+ test("list: reaps dead-PID sessions", () => {
73
+ const root = tmp();
74
+ try {
75
+ register(root, { sessionId: "dead", pid: 2_000_000_000, nowMs: T0 });
76
+ const live = list(root, { nowMs: T0 + 1000, staleSec: 300 });
77
+ assert.equal(live.length, 0, "a session whose PID is not running is reaped");
78
+ } finally {
79
+ rmSync(root, { recursive: true, force: true });
80
+ }
81
+ });
82
+
83
+ test("summarize: excludes self; empty when alone; formats others", () => {
84
+ const root = tmp();
85
+ try {
86
+ register(root, { sessionId: "me", cwd: "/x/maestro", nowMs: T0 });
87
+ assert.equal(summarize(root, { excludeSessionId: "me", nowMs: T0 + 1000 }), "", "alone → empty");
88
+ register(root, { sessionId: "other", cwd: "/x/openclaw", intent: "research", kind: "interactive", nowMs: T0 });
89
+ const line = summarize(root, { excludeSessionId: "me", nowMs: T0 + 1000, staleSec: 300 });
90
+ assert.match(line, /1 other session active/);
91
+ assert.match(line, /openclaw: research/);
92
+ } finally {
93
+ rmSync(root, { recursive: true, force: true });
94
+ }
95
+ });
@@ -0,0 +1,215 @@
1
+ /**
2
+ * lib/collective/recall.mjs — budgeted recall for the SessionStart digest.
3
+ *
4
+ * THE anti-bloat module. Turns the agent's memory cards (+ live presence) into a
5
+ * small, hard-token-budgeted context block injected at session start. Ranking is
6
+ * relevance × recency-decay × salience; near-duplicates are removed with MMR;
7
+ * the result is cut to `budgetTokens`.
8
+ *
9
+ * The ranking/budget/MMR core is pure and unit-tested. `buildDigest()` wires it
10
+ * to the card store + presence; `recallForQuery()` wraps the existing lib/rag
11
+ * search for on-demand, query-driven recall (the `collective recall` CLI).
12
+ *
13
+ * @module lib/collective/recall
14
+ */
15
+
16
+ "use strict";
17
+
18
+ import { readRecentCards, cardToText } from "./cards.mjs";
19
+ import { summarize as summarizePresence } from "./presence.mjs";
20
+
21
+ const DAY_MS = 86400_000;
22
+
23
+ /** ~4 chars/token heuristic; good enough for a budget guard. */
24
+ export function estimateTokens(text) {
25
+ if (!text) return 0;
26
+ return Math.ceil(String(text).length / 4);
27
+ }
28
+
29
+ /** Exponential recency decay: 1.0 now → 0.5 at one half-life → … */
30
+ export function decayWeight(ageMs, halfLifeDays) {
31
+ const hl = (Number.isFinite(halfLifeDays) && halfLifeDays > 0 ? halfLifeDays : 30) * DAY_MS;
32
+ const age = Number.isFinite(ageMs) && ageMs > 0 ? ageMs : 0;
33
+ return Math.pow(2, -age / hl);
34
+ }
35
+
36
+ /** Salience boost in [0.4, 1.0] so low-salience cards rank lower but never vanish. */
37
+ function salienceBoost(salience) {
38
+ const s = Number.isFinite(salience) ? Math.max(0, Math.min(1, salience)) : 0.5;
39
+ return 0.4 + 0.6 * s;
40
+ }
41
+
42
+ /**
43
+ * Score + sort cards by relevance × recency-decay × salience. Pure.
44
+ *
45
+ * @param {object[]} cards
46
+ * @param {object} [opts] - { relevanceById?, nowMs, halfLifeDays }
47
+ * relevanceById: optional map cardId → relevance (0..1); defaults to 1 (i.e.
48
+ * pure recency×salience, the cold-start case).
49
+ * @returns {Array<{card, score}>}
50
+ */
51
+ export function rankCards(cards, opts = {}) {
52
+ const nowMs = Number.isFinite(opts.nowMs) ? opts.nowMs : Date.now();
53
+ const rel = opts.relevanceById || null;
54
+ const scored = (Array.isArray(cards) ? cards : []).map((card) => {
55
+ const ageMs = nowMs - Date.parse(card.ts || 0);
56
+ const relevance = rel && rel[card.id] != null ? rel[card.id] : 1;
57
+ const score = relevance * decayWeight(ageMs, opts.halfLifeDays) * salienceBoost(card.salience);
58
+ return { card, score };
59
+ });
60
+ scored.sort((a, b) => b.score - a.score);
61
+ return scored;
62
+ }
63
+
64
+ /**
65
+ * Lowercase token set for similarity (drops sub-3-char tokens + punctuation).
66
+ * Exported so the learning loop (lib/learning/reflect) reuses the SAME tokenizer
67
+ * the digest's MMR uses — one definition of "similar".
68
+ */
69
+ export function tokenSet(text) {
70
+ return new Set(
71
+ String(text || "").toLowerCase().replace(/[^a-z0-9 ]/g, " ").split(/\s+/).filter((t) => t.length > 2),
72
+ );
73
+ }
74
+
75
+ /**
76
+ * Jaccard similarity of two token sets. Exported for reuse by the learning loop
77
+ * (procedure-recurrence detection) — see lib/learning/reflect.
78
+ */
79
+ export function jaccard(a, b) {
80
+ if (a.size === 0 || b.size === 0) return 0;
81
+ let inter = 0;
82
+ for (const t of a) if (b.has(t)) inter++;
83
+ return inter / (a.size + b.size - inter);
84
+ }
85
+
86
+ /**
87
+ * Maximal Marginal Relevance selection — greedily pick high-score items while
88
+ * penalizing similarity to already-selected items, so the digest is diverse
89
+ * rather than five near-duplicate cards. Pure.
90
+ *
91
+ * @param {Array<{card, score}>} scored - sorted desc by score
92
+ * @param {object} [opts] - { limit=12, lambda=0.7 }
93
+ * @returns {Array<{card, score}>}
94
+ */
95
+ export function mmrSelect(scored, opts = {}) {
96
+ const limit = Number.isFinite(opts.limit) ? opts.limit : 12;
97
+ const lambda = Number.isFinite(opts.lambda) ? opts.lambda : 0.7;
98
+ const pool = scored.slice();
99
+ const selected = [];
100
+ const selSets = [];
101
+ while (pool.length && selected.length < limit) {
102
+ let bestIdx = 0;
103
+ let bestVal = -Infinity;
104
+ for (let i = 0; i < pool.length; i++) {
105
+ const set = tokenSet(cardToText(pool[i].card));
106
+ let maxSim = 0;
107
+ for (const s of selSets) maxSim = Math.max(maxSim, jaccard(set, s));
108
+ const mmr = lambda * pool[i].score - (1 - lambda) * maxSim;
109
+ if (mmr > bestVal) { bestVal = mmr; bestIdx = i; }
110
+ }
111
+ const [chosen] = pool.splice(bestIdx, 1);
112
+ selected.push(chosen);
113
+ selSets.push(tokenSet(cardToText(chosen.card)));
114
+ }
115
+ return selected;
116
+ }
117
+
118
+ /**
119
+ * Take items (in order) until the cumulative token cost of their rendered lines
120
+ * would exceed `budgetTokens` (after reserving `reservedTokens` for the header /
121
+ * presence line). Pure.
122
+ *
123
+ * @param {Array<{card, score}>} items
124
+ * @param {object} [opts] - { budgetTokens=1500, reservedTokens=0 }
125
+ * @returns {Array<{card, score}>}
126
+ */
127
+ export function selectWithinBudget(items, opts = {}) {
128
+ const budget = Number.isFinite(opts.budgetTokens) ? opts.budgetTokens : 1500;
129
+ let used = Number.isFinite(opts.reservedTokens) ? opts.reservedTokens : 0;
130
+ const out = [];
131
+ for (const it of items) {
132
+ const cost = estimateTokens("- " + cardToText(it.card)) + 1;
133
+ if (used + cost > budget) break;
134
+ used += cost;
135
+ out.push(it);
136
+ }
137
+ return out;
138
+ }
139
+
140
+ /**
141
+ * Render the digest block. Returns "" when there is nothing to say (no presence,
142
+ * no cards) so the hook can stay silent.
143
+ */
144
+ export function formatDigest({ presenceLine = "", cards = [], totalCards = 0 } = {}) {
145
+ if (!presenceLine && cards.length === 0) return "";
146
+ const lines = ["<!-- maestro:collective-digest -->"];
147
+ if (presenceLine) lines.push(`**Active now:** ${presenceLine}`);
148
+ if (cards.length) {
149
+ const more = totalCards > cards.length ? ` of ${totalCards}; \`rag_search\` or \`collective recall\` for more` : "";
150
+ lines.push(`**Recent collective memory** (${cards.length}${more}):`);
151
+ for (const c of cards) lines.push(`- ${cardToText(c)}`);
152
+ }
153
+ return lines.join("\n");
154
+ }
155
+
156
+ /**
157
+ * Build the full SessionStart digest for an agent root. Pulls recent cards +
158
+ * live presence, ranks, MMR-dedupes, and cuts to budget. Fail-open → "".
159
+ *
160
+ * @param {string} agentRoot
161
+ * @param {object} [opts] - {
162
+ * budgetTokens, halfLifeDays, nowMs, excludeSessionId, staleSec, maxCards,
163
+ * relevanceById, presenceLineOverride, cardsOverride
164
+ * }
165
+ * @returns {string}
166
+ */
167
+ export function buildDigest(agentRoot, opts = {}) {
168
+ try {
169
+ const nowMs = Number.isFinite(opts.nowMs) ? opts.nowMs : Date.now();
170
+ const budgetTokens = Number.isFinite(opts.budgetTokens) ? opts.budgetTokens : 1500;
171
+ const presenceLine = opts.presenceLineOverride != null
172
+ ? opts.presenceLineOverride
173
+ : summarizePresence(agentRoot, { excludeSessionId: opts.excludeSessionId, staleSec: opts.staleSec, nowMs });
174
+ const cards = opts.cardsOverride != null
175
+ ? opts.cardsOverride
176
+ : readRecentCards(agentRoot, { nowMs, days: Math.max(45, (opts.halfLifeDays || 30) * 2) });
177
+ const scored = rankCards(cards, { relevanceById: opts.relevanceById, nowMs, halfLifeDays: opts.halfLifeDays });
178
+ const diversified = mmrSelect(scored, { limit: Number.isFinite(opts.maxCards) ? opts.maxCards : 12 });
179
+ const reserved = estimateTokens(presenceLine) + 24; // header + presence
180
+ const selected = selectWithinBudget(diversified, { budgetTokens, reservedTokens: reserved });
181
+ return formatDigest({
182
+ presenceLine,
183
+ cards: selected.map((s) => s.card),
184
+ totalCards: cards.length,
185
+ });
186
+ } catch {
187
+ return "";
188
+ }
189
+ }
190
+
191
+ /**
192
+ * On-demand, query-driven recall over the full indexed corpus (incl. collective
193
+ * cards). Thin wrapper over lib/rag search; `searchFn` is injectable for tests.
194
+ *
195
+ * @param {object} opts - { agentRoot, query, topN=8, searchFn? }
196
+ * @returns {Promise<Array>}
197
+ */
198
+ export async function recallForQuery(opts = {}) {
199
+ const { agentRoot, query, topN = 8 } = opts;
200
+ if (!agentRoot || !query) return [];
201
+ let searchFn = opts.searchFn;
202
+ if (!searchFn) {
203
+ try {
204
+ const mod = await import("../rag/search.mjs");
205
+ searchFn = mod.search;
206
+ } catch {
207
+ return [];
208
+ }
209
+ }
210
+ try {
211
+ return await searchFn({ query, topN, agentRoot });
212
+ } catch {
213
+ return [];
214
+ }
215
+ }
@@ -0,0 +1,116 @@
1
+ /**
2
+ * recall.test.mjs — tests for the budgeted recall core.
3
+ * Run: node --test lib/collective/recall.test.mjs
4
+ */
5
+ "use strict";
6
+
7
+ import { test } from "node:test";
8
+ import assert from "node:assert/strict";
9
+
10
+ import {
11
+ estimateTokens,
12
+ decayWeight,
13
+ rankCards,
14
+ mmrSelect,
15
+ selectWithinBudget,
16
+ formatDigest,
17
+ buildDigest,
18
+ recallForQuery,
19
+ } from "./recall.mjs";
20
+
21
+ const T = Date.parse("2026-06-03T12:00:00Z");
22
+ const card = (over) => ({
23
+ id: over.id || over.title, ts: over.ts, kind: over.kind || "fact",
24
+ title: over.title, body: over.body || "", tags: over.tags || [],
25
+ salience: over.salience ?? 0.5, sessionId: "s", repo: "r", sourceRefs: [],
26
+ });
27
+
28
+ test("estimateTokens: ~chars/4", () => {
29
+ assert.equal(estimateTokens(""), 0);
30
+ assert.equal(estimateTokens("abcd"), 1);
31
+ assert.equal(estimateTokens("a".repeat(40)), 10);
32
+ });
33
+
34
+ test("decayWeight: 1.0 at age 0, ~0.5 at one half-life", () => {
35
+ assert.equal(decayWeight(0, 30), 1);
36
+ assert.ok(Math.abs(decayWeight(30 * 86400_000, 30) - 0.5) < 1e-9);
37
+ assert.ok(decayWeight(60 * 86400_000, 30) < 0.26);
38
+ });
39
+
40
+ test("rankCards: recency + salience order; relevanceById applies", () => {
41
+ const cards = [
42
+ card({ title: "old-hi-sal", ts: "2026-05-01T00:00:00Z", salience: 0.9 }),
43
+ card({ title: "new-lo-sal", ts: "2026-06-03T00:00:00Z", salience: 0.2 }),
44
+ ];
45
+ const ranked = rankCards(cards, { nowMs: T, halfLifeDays: 30 });
46
+ assert.equal(ranked[0].card.title, "new-lo-sal", "recent beats month-old even at lower salience");
47
+ // Relevance map can flip it.
48
+ const ranked2 = rankCards(cards, {
49
+ nowMs: T, halfLifeDays: 30,
50
+ relevanceById: { "old-hi-sal": 1, "new-lo-sal": 0.05 },
51
+ });
52
+ assert.equal(ranked2[0].card.title, "old-hi-sal", "strong relevance dominates");
53
+ });
54
+
55
+ test("mmrSelect: dedupes near-duplicates and respects limit", () => {
56
+ const scored = [
57
+ { card: card({ title: "Shipped the billing migration to prod" }), score: 1.0 },
58
+ { card: card({ title: "Shipped the billing migration to production" }), score: 0.99 },
59
+ { card: card({ title: "Hired a new compliance analyst" }), score: 0.8 },
60
+ ];
61
+ const picked = mmrSelect(scored, { limit: 2, lambda: 0.7 });
62
+ assert.equal(picked.length, 2);
63
+ const titles = picked.map((p) => p.card.title);
64
+ assert.ok(titles.includes("Shipped the billing migration to prod"));
65
+ assert.ok(
66
+ titles.includes("Hired a new compliance analyst"),
67
+ "MMR should prefer the diverse card over the near-duplicate",
68
+ );
69
+ });
70
+
71
+ test("selectWithinBudget: stops before exceeding the budget", () => {
72
+ const items = Array.from({ length: 50 }, (_, i) => ({ card: card({ title: `card number ${i} with some words` }), score: 1 - i / 100 }));
73
+ const tiny = selectWithinBudget(items, { budgetTokens: 20 });
74
+ const big = selectWithinBudget(items, { budgetTokens: 100000 });
75
+ assert.ok(tiny.length < big.length, "tighter budget selects fewer");
76
+ assert.ok(tiny.length >= 1);
77
+ });
78
+
79
+ test("formatDigest: empty when nothing; includes presence + cards", () => {
80
+ assert.equal(formatDigest({ presenceLine: "", cards: [] }), "");
81
+ const out = formatDigest({
82
+ presenceLine: "1 other session active — maestro: triage (2m)",
83
+ cards: [card({ title: "Decided to ship v2", kind: "decision" })],
84
+ totalCards: 3,
85
+ });
86
+ assert.match(out, /maestro:collective-digest/);
87
+ assert.match(out, /Active now:/);
88
+ assert.match(out, /Decided to ship v2/);
89
+ assert.match(out, /of 3/, "indicates more available");
90
+ });
91
+
92
+ test("buildDigest: budgeted, uses overrides, fail-open", () => {
93
+ const cards = Array.from({ length: 30 }, (_, i) =>
94
+ card({ title: `Event ${i} happened in the system today`, ts: "2026-06-03T00:00:00Z", salience: 0.5 }));
95
+ const full = buildDigest("/nonexistent", {
96
+ nowMs: T, budgetTokens: 100000, presenceLineOverride: "2 sessions active", cardsOverride: cards,
97
+ });
98
+ assert.match(full, /Active now:/);
99
+ const tight = buildDigest("/nonexistent", {
100
+ nowMs: T, budgetTokens: 60, presenceLineOverride: "2 sessions active", cardsOverride: cards,
101
+ });
102
+ // Tight budget yields a much shorter block.
103
+ assert.ok(tight.length < full.length);
104
+ // Fail-open: bad input never throws.
105
+ assert.equal(typeof buildDigest(null, { cardsOverride: null, presenceLineOverride: "" }), "string");
106
+ });
107
+
108
+ test("recallForQuery: injected search; empty without query", async () => {
109
+ assert.deepEqual(await recallForQuery({ agentRoot: "/x" }), []);
110
+ const hits = await recallForQuery({
111
+ agentRoot: "/x", query: "billing",
112
+ searchFn: async ({ query }) => [{ source_path: "memory/collective/cards/x.jsonl", relevance: 1, snippet: query }],
113
+ });
114
+ assert.equal(hits.length, 1);
115
+ assert.equal(hits[0].relevance, 1);
116
+ });