@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,185 @@
1
+ /**
2
+ * lib/collective/capture.mjs — distil a finished session into memory cards.
3
+ *
4
+ * The write path of collective memory. A global Stop/PreCompact hook hands the
5
+ * session transcript here; we run a CHEAP model to distil it into a handful of
6
+ * durable memory cards (decisions/facts/artifacts/open-loops/entities) and append
7
+ * them to the card store. NEVER stores raw transcripts.
8
+ *
9
+ * Hard guarantees (this runs on every session machine-wide):
10
+ * - FAIL-OPEN: any error → 0 cards written, never throws, never blocks.
11
+ * - SCOPED: honors the repo denylist + the `enabled` flag.
12
+ * - BOUNDED: skips trivial sessions, caps cards/session, time-boxes the model.
13
+ *
14
+ * The distiller is injectable (`distillFn`) so tests never spawn a model; the
15
+ * default shells a `claude --print --model <cheap>` via lib/claude-bin.
16
+ *
17
+ * @module lib/collective/capture
18
+ */
19
+
20
+ "use strict";
21
+
22
+ import { spawnSync } from "node:child_process";
23
+ import { normalizeCard, appendCards, repoLabel } from "./cards.mjs";
24
+ import { isRepoDenied } from "./config.mjs";
25
+ import { resolveClaudeBin, daemonClaudeArgs, augmentedPath } from "../claude-bin.mjs";
26
+
27
+ const DISTILL_TIMEOUT_MS = 60_000;
28
+ const MAX_TRANSCRIPT_CHARS = 16_000;
29
+
30
+ /** Keep the last `n` chars of a string (the tail is the most relevant). */
31
+ function tail(s, n) {
32
+ return s.length <= n ? s : s.slice(s.length - n);
33
+ }
34
+
35
+ function extractContent(obj) {
36
+ const c = obj.content ?? (obj.message && obj.message.content);
37
+ if (typeof c === "string") return c;
38
+ if (Array.isArray(c)) {
39
+ return c.map((part) => (typeof part === "string" ? part : part && part.text ? part.text : "")).filter(Boolean).join(" ");
40
+ }
41
+ if (typeof obj.text === "string") return obj.text;
42
+ return "";
43
+ }
44
+
45
+ /**
46
+ * Flatten a Claude Code transcript (JSONL of message objects) into role-tagged
47
+ * text, tail-capped. Plain (non-JSONL) input is passed through tail-capped.
48
+ * @param {string} jsonl
49
+ * @returns {string}
50
+ */
51
+ export function transcriptToText(jsonl) {
52
+ if (!jsonl) return "";
53
+ const lines = String(jsonl).split("\n").map((s) => s.trim()).filter(Boolean);
54
+ const out = [];
55
+ let looksJsonl = false;
56
+ for (const line of lines) {
57
+ if (!line.startsWith("{")) continue;
58
+ looksJsonl = true;
59
+ try {
60
+ const obj = JSON.parse(line);
61
+ const role = obj.role || (obj.message && obj.message.role) || obj.type || "";
62
+ const content = extractContent(obj);
63
+ if (content) out.push(`${role ? role + ": " : ""}${content}`);
64
+ } catch { /* skip bad line */ }
65
+ }
66
+ if (!looksJsonl) return tail(String(jsonl), MAX_TRANSCRIPT_CHARS);
67
+ return tail(out.join("\n"), MAX_TRANSCRIPT_CHARS);
68
+ }
69
+
70
+ /** Count user turns in role-tagged transcript text. */
71
+ export function countUserTurns(text) {
72
+ if (!text) return 0;
73
+ const m = String(text).match(/(^|\n)\s*(user|human)\s*:/gi);
74
+ return m ? m.length : 0;
75
+ }
76
+
77
+ /** The distillation instruction handed to the cheap model. */
78
+ export function buildDistillPrompt(transcriptText, maxCards = 12) {
79
+ return [
80
+ "You distil a finished work session into durable MEMORY CARDS for an AI agent's",
81
+ "long-term memory. Output ONLY a JSON array (no prose, no code fence).",
82
+ "",
83
+ "Each card: {\"kind\",\"title\",\"body\",\"tags\",\"salience\",\"sourceRefs\"}.",
84
+ " kind: one of decision|fact|artifact|openloop|entity|procedure",
85
+ " procedure: a repeatable multi-step method a future session could re-run",
86
+ " (a named how-to with concrete steps) — NOT a one-off action.",
87
+ " title: <=160 chars, the durable takeaway (not a play-by-play)",
88
+ " body: <=600 chars of supporting detail",
89
+ " tags: 0-6 short topic tags",
90
+ " salience: 0..1 — how useful this will be to a FUTURE session (be selective)",
91
+ " sourceRefs: file paths / urls / ids touched (optional)",
92
+ "",
93
+ "Rules: capture only what a future session would genuinely benefit from —",
94
+ "decisions made, facts learned, artifacts produced, open loops left, key",
95
+ `entities. Skip chit-chat, transient debugging, and secrets. At most ${maxCards} cards.`,
96
+ "If nothing is worth remembering, output [].",
97
+ "",
98
+ "=== SESSION TRANSCRIPT (tail) ===",
99
+ transcriptText,
100
+ "=== END ===",
101
+ ].join("\n");
102
+ }
103
+
104
+ /** Extract a JSON array of raw cards from model output. Tolerant; [] on failure. */
105
+ export function extractCardsFromModelOutput(text) {
106
+ if (!text || typeof text !== "string") return [];
107
+ const start = text.indexOf("[");
108
+ const end = text.lastIndexOf("]");
109
+ if (start === -1 || end === -1 || end <= start) return [];
110
+ try {
111
+ const arr = JSON.parse(text.slice(start, end + 1));
112
+ return Array.isArray(arr) ? arr : [];
113
+ } catch {
114
+ return [];
115
+ }
116
+ }
117
+
118
+ /** Default distiller: spawn a cheap `claude --print` pass. Fail-open → "". */
119
+ export function defaultDistiller(prompt, opts = {}) {
120
+ try {
121
+ const model = opts.model || "haiku";
122
+ const args = ["--print", "--model", model, ...daemonClaudeArgs()];
123
+ const res = spawnSync(resolveClaudeBin(), args, {
124
+ input: prompt,
125
+ encoding: "utf8",
126
+ timeout: DISTILL_TIMEOUT_MS,
127
+ env: { ...process.env, PATH: augmentedPath() },
128
+ maxBuffer: 8 * 1024 * 1024,
129
+ });
130
+ if (res.status === 0 && res.stdout) return res.stdout;
131
+ return "";
132
+ } catch {
133
+ return "";
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Capture one finished session into cards. Orchestrates the guards, distillation,
139
+ * normalization, and append. Always resolves (never rejects).
140
+ *
141
+ * @param {object} o - {
142
+ * agentRoot, sessionId, cwd, transcriptText, cfg,
143
+ * distillFn?, nowMs?, model?, maxCards?, minTurns?
144
+ * }
145
+ * @returns {Promise<{written:number, skipped:boolean, reason:string}>}
146
+ */
147
+ export async function captureSession(o = {}) {
148
+ const { agentRoot, sessionId, cwd, transcriptText = "", cfg = {} } = o;
149
+ if (!agentRoot) return { written: 0, skipped: true, reason: "no-agent-root" };
150
+ if (cfg.enabled === false) return { written: 0, skipped: true, reason: "disabled" };
151
+ if (isRepoDenied(cwd, cfg)) return { written: 0, skipped: true, reason: "denylisted" };
152
+
153
+ const minTurns = Number.isFinite(o.minTurns) ? o.minTurns : (Number.isFinite(cfg.minTurnsToCapture) ? cfg.minTurnsToCapture : 2);
154
+ if (countUserTurns(transcriptText) < minTurns) {
155
+ return { written: 0, skipped: true, reason: "too-short" };
156
+ }
157
+
158
+ const maxCards = Number.isFinite(o.maxCards) ? o.maxCards : (Number.isFinite(cfg.maxCardsPerSession) ? cfg.maxCardsPerSession : 12);
159
+ const model = o.model || cfg.captureModel || "haiku";
160
+ const distill = o.distillFn || defaultDistiller;
161
+
162
+ let raw;
163
+ try {
164
+ raw = await distill(buildDistillPrompt(transcriptText, maxCards), { model, agentRoot });
165
+ } catch {
166
+ return { written: 0, skipped: true, reason: "distill-failed" };
167
+ }
168
+
169
+ const rawCards = extractCardsFromModelOutput(raw).slice(0, maxCards);
170
+ if (rawCards.length === 0) return { written: 0, skipped: false, reason: "no-cards" };
171
+
172
+ const ctx = {
173
+ sessionId: sessionId || "unknown",
174
+ repo: repoLabel(cwd),
175
+ ts: Number.isFinite(o.nowMs) ? new Date(o.nowMs).toISOString() : undefined,
176
+ };
177
+ // Pre-normalize so a single bad card doesn't poison the batch.
178
+ const cards = rawCards.map((c) => normalizeCard(c, ctx)).filter(Boolean);
179
+ const written = appendCards(agentRoot, cards, ctx);
180
+ // Surface the normalized cards (additive) so a second pass — e.g. the WS3
181
+ // skill-promoter — can inspect what was just distilled without re-reading the
182
+ // store. Optional `_onCards` callback mirrors it for streaming callers.
183
+ if (typeof o._onCards === "function") { try { o._onCards(cards); } catch { /* */ } }
184
+ return { written, skipped: false, reason: written ? "ok" : "no-cards", cards };
185
+ }
@@ -0,0 +1,121 @@
1
+ /**
2
+ * capture.test.mjs — tests for session → memory-card distillation.
3
+ * Run: node --test lib/collective/capture.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 {
14
+ transcriptToText,
15
+ countUserTurns,
16
+ buildDistillPrompt,
17
+ extractCardsFromModelOutput,
18
+ captureSession,
19
+ } from "./capture.mjs";
20
+ import { readRecentCards } from "./cards.mjs";
21
+
22
+ function tmp() {
23
+ return mkdtempSync(join(tmpdir(), "collective-capture-"));
24
+ }
25
+
26
+ test("transcriptToText: flattens JSONL; passes plain text through", () => {
27
+ const jsonl = [
28
+ JSON.stringify({ role: "user", content: "do the thing" }),
29
+ JSON.stringify({ role: "assistant", content: [{ type: "text", text: "done" }] }),
30
+ ].join("\n");
31
+ const out = transcriptToText(jsonl);
32
+ assert.match(out, /user: do the thing/);
33
+ assert.match(out, /assistant: done/);
34
+ assert.equal(transcriptToText("just plain text"), "just plain text");
35
+ });
36
+
37
+ test("countUserTurns: counts user/human turns", () => {
38
+ assert.equal(countUserTurns("user: a\nassistant: b\nuser: c"), 2);
39
+ assert.equal(countUserTurns("human: hi\nassistant: yo"), 1);
40
+ assert.equal(countUserTurns(""), 0);
41
+ });
42
+
43
+ test("buildDistillPrompt: instructs JSON-only output", () => {
44
+ const p = buildDistillPrompt("user: x", 5);
45
+ assert.match(p, /ONLY a JSON array/);
46
+ assert.match(p, /at most 5 cards/i);
47
+ assert.match(p, /SESSION TRANSCRIPT/);
48
+ });
49
+
50
+ test("extractCardsFromModelOutput: fenced, plain, and garbage", () => {
51
+ assert.equal(extractCardsFromModelOutput("```json\n[{\"title\":\"a\"}]\n```").length, 1);
52
+ assert.equal(extractCardsFromModelOutput('prose [{"title":"a"},{"title":"b"}] more').length, 2);
53
+ assert.deepEqual(extractCardsFromModelOutput("no json here"), []);
54
+ assert.deepEqual(extractCardsFromModelOutput(""), []);
55
+ });
56
+
57
+ const longTranscript = "user: please ship billing\nassistant: shipping\nuser: confirm done\nassistant: done";
58
+
59
+ test("captureSession: writes distilled cards (injected distiller)", async () => {
60
+ const root = tmp();
61
+ try {
62
+ const distillFn = async () => JSON.stringify([
63
+ { kind: "decision", title: "Shipped billing migration", salience: 0.9, tags: ["billing"] },
64
+ { kind: "openloop", title: "Verify invoices next run", salience: 0.6 },
65
+ ]);
66
+ const res = await captureSession({
67
+ agentRoot: root, sessionId: "s1", cwd: "/x/maestro",
68
+ transcriptText: longTranscript, cfg: { enabled: true }, distillFn,
69
+ nowMs: Date.parse("2026-06-03T12:00:00Z"),
70
+ });
71
+ assert.equal(res.written, 2);
72
+ assert.equal(res.reason, "ok");
73
+ const cards = readRecentCards(root, { nowMs: Date.parse("2026-06-03T13:00:00Z") });
74
+ assert.equal(cards.length, 2);
75
+ assert.equal(cards.find((c) => c.kind === "decision").repo, "maestro");
76
+ } finally {
77
+ rmSync(root, { recursive: true, force: true });
78
+ }
79
+ });
80
+
81
+ test("captureSession: skips when disabled / denylisted / too short", async () => {
82
+ const root = tmp();
83
+ const distillFn = async () => "[{\"title\":\"x\"}]";
84
+ try {
85
+ assert.equal((await captureSession({ agentRoot: root, transcriptText: longTranscript, cfg: { enabled: false }, distillFn })).reason, "disabled");
86
+ assert.equal((await captureSession({ agentRoot: root, cwd: "/x/secret", transcriptText: longTranscript, cfg: { repoDenylist: ["secret"] }, distillFn })).reason, "denylisted");
87
+ assert.equal((await captureSession({ agentRoot: root, transcriptText: "user: hi", cfg: {}, distillFn })).reason, "too-short");
88
+ assert.equal((await captureSession({ transcriptText: longTranscript, cfg: {}, distillFn })).reason, "no-agent-root");
89
+ } finally {
90
+ rmSync(root, { recursive: true, force: true });
91
+ }
92
+ });
93
+
94
+ test("captureSession: fail-open on bad distiller output / throw", async () => {
95
+ const root = tmp();
96
+ try {
97
+ const garbage = await captureSession({ agentRoot: root, transcriptText: longTranscript, cfg: {}, distillFn: async () => "not json" });
98
+ assert.equal(garbage.written, 0);
99
+ assert.equal(garbage.reason, "no-cards");
100
+ const thrower = await captureSession({ agentRoot: root, transcriptText: longTranscript, cfg: {}, distillFn: async () => { throw new Error("boom"); } });
101
+ assert.equal(thrower.written, 0);
102
+ assert.equal(thrower.skipped, true);
103
+ assert.equal(thrower.reason, "distill-failed");
104
+ } finally {
105
+ rmSync(root, { recursive: true, force: true });
106
+ }
107
+ });
108
+
109
+ test("captureSession: caps cards per session", async () => {
110
+ const root = tmp();
111
+ try {
112
+ const many = Array.from({ length: 20 }, (_, i) => ({ kind: "fact", title: `fact ${i}`, salience: 0.5 }));
113
+ const res = await captureSession({
114
+ agentRoot: root, transcriptText: longTranscript, cfg: {}, maxCards: 5,
115
+ distillFn: async () => JSON.stringify(many), nowMs: Date.parse("2026-06-03T12:00:00Z"),
116
+ });
117
+ assert.equal(res.written, 5);
118
+ } finally {
119
+ rmSync(root, { recursive: true, force: true });
120
+ }
121
+ });
@@ -0,0 +1,201 @@
1
+ /**
2
+ * lib/collective/cards.mjs — the memory-card store.
3
+ *
4
+ * A "memory card" is a small, structured, durable unit of cross-session memory
5
+ * distilled from a finished Claude Code session — NEVER a raw transcript. Cards
6
+ * are append-only JSONL under `memory/collective/cards/<YYYY-MM-DD>.jsonl`, which
7
+ * the existing lib/rag indexer picks up as a normal source (so recall reuses the
8
+ * FTS5 + embedding index — no separate store).
9
+ *
10
+ * Card shape:
11
+ * {
12
+ * id, // stable hash of (sessionId|kind|title)
13
+ * ts, // ISO 8601 creation time
14
+ * sessionId, // originating Claude Code session
15
+ * repo, // basename of the originating cwd (provenance, not a path)
16
+ * kind, // decision | fact | artifact | openloop | entity
17
+ * title, // one-line summary
18
+ * body, // <= BODY_MAX chars of detail
19
+ * tags, // string[]
20
+ * salience, // 0..1 importance (drives recall ranking + retention)
21
+ * sourceRefs // string[] — file paths / urls / ids touched (optional)
22
+ * }
23
+ *
24
+ * Pure + fail-open: append/read never throw on a bad card or unreadable file.
25
+ *
26
+ * @module lib/collective/cards
27
+ */
28
+
29
+ "use strict";
30
+
31
+ import { existsSync, readFileSync, appendFileSync, mkdirSync, readdirSync } from "node:fs";
32
+ import { join, basename } from "node:path";
33
+ import { createHash } from "node:crypto";
34
+
35
+ export const KINDS = Object.freeze(["decision", "fact", "artifact", "openloop", "entity", "procedure"]);
36
+ const BODY_MAX = 600;
37
+ const TITLE_MAX = 160;
38
+ const CARDS_REL = "memory/collective/cards";
39
+
40
+ /** Directory holding the per-day card JSONL files for an agent root. */
41
+ export function cardsDir(agentRoot) {
42
+ return join(agentRoot, CARDS_REL);
43
+ }
44
+
45
+ function clamp01(n) {
46
+ const x = typeof n === "number" && Number.isFinite(n) ? n : 0.5;
47
+ return x < 0 ? 0 : x > 1 ? 1 : x;
48
+ }
49
+
50
+ function shortHash(s) {
51
+ return createHash("sha1").update(String(s)).digest("hex").slice(0, 12);
52
+ }
53
+
54
+ /** UTC date stamp (YYYY-MM-DD) for an ISO timestamp. */
55
+ function dayStamp(iso) {
56
+ // Slice is safe for ISO strings; fall back to the raw first 10 chars.
57
+ return String(iso).slice(0, 10);
58
+ }
59
+
60
+ /**
61
+ * Validate + normalize a raw distilled card into the canonical shape. Returns
62
+ * null if the card has no usable title (the one hard requirement).
63
+ *
64
+ * @param {object} raw
65
+ * @param {object} [ctx] - { sessionId, repo, ts } defaults applied when absent
66
+ * @returns {object|null}
67
+ */
68
+ export function normalizeCard(raw, ctx = {}) {
69
+ if (!raw || typeof raw !== "object") return null;
70
+ const title = String(raw.title || "").trim().slice(0, TITLE_MAX);
71
+ if (!title) return null;
72
+ const kind = KINDS.includes(raw.kind) ? raw.kind : "fact";
73
+ const ts = String(raw.ts || ctx.ts || "").trim() || isoNow(ctx);
74
+ const sessionId = String(raw.sessionId || ctx.sessionId || "unknown");
75
+ const repo = String(raw.repo || ctx.repo || "").trim();
76
+ const body = String(raw.body || "").trim().slice(0, BODY_MAX);
77
+ const tags = Array.isArray(raw.tags)
78
+ ? raw.tags.map((t) => String(t).trim()).filter(Boolean).slice(0, 12)
79
+ : [];
80
+ const sourceRefs = Array.isArray(raw.sourceRefs)
81
+ ? raw.sourceRefs.map((r) => String(r).trim()).filter(Boolean).slice(0, 20)
82
+ : [];
83
+ const salience = clamp01(raw.salience);
84
+ const id = String(raw.id || `card-${shortHash(`${sessionId}|${kind}|${title}`)}`);
85
+ return { id, ts, sessionId, repo, kind, title, body, tags, salience, sourceRefs };
86
+ }
87
+
88
+ // `ctx.ts` may be injected for deterministic tests; otherwise use wall clock.
89
+ function isoNow(ctx) {
90
+ if (ctx && ctx.ts) return ctx.ts;
91
+ return new Date().toISOString();
92
+ }
93
+
94
+ /**
95
+ * Append normalized cards to today's (or each card's own date's) JSONL file.
96
+ * De-duplicates within the call by id. Fail-open: returns the count written, 0
97
+ * on any error.
98
+ *
99
+ * @param {string} agentRoot
100
+ * @param {object[]} cards - raw or normalized cards
101
+ * @param {object} [ctx] - normalization context (sessionId, repo, ts)
102
+ * @returns {number} number of cards written
103
+ */
104
+ export function appendCards(agentRoot, cards, ctx = {}) {
105
+ if (!agentRoot || !Array.isArray(cards) || cards.length === 0) return 0;
106
+ const dir = cardsDir(agentRoot);
107
+ let written = 0;
108
+ try {
109
+ mkdirSync(dir, { recursive: true });
110
+ const seen = new Set();
111
+ // Group lines by day file so a batch spanning midnight lands correctly.
112
+ const byDay = new Map();
113
+ for (const raw of cards) {
114
+ const card = normalizeCard(raw, ctx);
115
+ if (!card) continue;
116
+ if (seen.has(card.id)) continue;
117
+ seen.add(card.id);
118
+ const day = dayStamp(card.ts);
119
+ if (!byDay.has(day)) byDay.set(day, []);
120
+ byDay.get(day).push(JSON.stringify(card));
121
+ }
122
+ for (const [day, lines] of byDay) {
123
+ appendFileSync(join(dir, `${day}.jsonl`), lines.join("\n") + "\n");
124
+ written += lines.length;
125
+ }
126
+ } catch {
127
+ return written;
128
+ }
129
+ return written;
130
+ }
131
+
132
+ /** Parse a JSONL file into card objects, skipping bad lines. Fail-open. */
133
+ function readJsonl(path) {
134
+ try {
135
+ const raw = readFileSync(path, "utf8");
136
+ const out = [];
137
+ for (const line of raw.split("\n")) {
138
+ const t = line.trim();
139
+ if (!t) continue;
140
+ try {
141
+ const obj = JSON.parse(t);
142
+ if (obj && obj.title) out.push(obj);
143
+ } catch { /* skip bad line */ }
144
+ }
145
+ return out;
146
+ } catch {
147
+ return [];
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Read recent cards across the last `days` day-files, newest first.
153
+ * Bounded by filename-date filtering first (no full-corpus scan), then `limit`.
154
+ *
155
+ * @param {string} agentRoot
156
+ * @param {object} [opts] - { days=45, limit=500, nowMs }
157
+ * @returns {object[]}
158
+ */
159
+ export function readRecentCards(agentRoot, opts = {}) {
160
+ if (!agentRoot) return [];
161
+ const days = Number.isFinite(opts.days) ? opts.days : 45;
162
+ const limit = Number.isFinite(opts.limit) ? opts.limit : 500;
163
+ const nowMs = Number.isFinite(opts.nowMs) ? opts.nowMs : Date.now();
164
+ const cutoffMs = nowMs - days * 86400_000;
165
+ const dir = cardsDir(agentRoot);
166
+ if (!existsSync(dir)) return [];
167
+ let files;
168
+ try {
169
+ files = readdirSync(dir).filter((f) => /^\d{4}-\d{2}-\d{2}\.jsonl$/.test(f));
170
+ } catch {
171
+ return [];
172
+ }
173
+ // Keep only day-files within the window (by filename date), newest first.
174
+ files = files
175
+ .filter((f) => {
176
+ const d = Date.parse(f.slice(0, 10) + "T23:59:59Z");
177
+ return Number.isFinite(d) ? d >= cutoffMs : true;
178
+ })
179
+ .sort()
180
+ .reverse();
181
+ const all = [];
182
+ for (const f of files) {
183
+ for (const c of readJsonl(join(dir, f))) all.push(c);
184
+ if (all.length >= limit * 2) break; // soft cap before sort
185
+ }
186
+ all.sort((a, b) => String(b.ts).localeCompare(String(a.ts)));
187
+ return all.slice(0, limit);
188
+ }
189
+
190
+ /** Compact one-line-ish text form of a card (for digests + embedding). */
191
+ export function cardToText(card) {
192
+ const tags = card.tags && card.tags.length ? ` [${card.tags.join(", ")}]` : "";
193
+ const body = card.body ? ` — ${card.body}` : "";
194
+ return `(${card.kind}) ${card.title}${body}${tags}`;
195
+ }
196
+
197
+ /** Provenance-safe repo label from a cwd (basename only, never a full path). */
198
+ export function repoLabel(cwd) {
199
+ if (!cwd) return "";
200
+ try { return basename(cwd); } catch { return ""; }
201
+ }
@@ -0,0 +1,114 @@
1
+ /**
2
+ * cards.test.mjs — tests for the memory-card store.
3
+ * Run: node --test lib/collective/cards.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 {
14
+ KINDS,
15
+ normalizeCard,
16
+ appendCards,
17
+ readRecentCards,
18
+ cardToText,
19
+ repoLabel,
20
+ } from "./cards.mjs";
21
+
22
+ function tmp() {
23
+ return mkdtempSync(join(tmpdir(), "collective-cards-"));
24
+ }
25
+
26
+ test("normalizeCard: requires a title", () => {
27
+ assert.equal(normalizeCard({ kind: "fact" }), null);
28
+ assert.equal(normalizeCard(null), null);
29
+ assert.ok(normalizeCard({ title: "x" }));
30
+ });
31
+
32
+ test("normalizeCard: kind fallback, salience clamp, stable id, ctx defaults", () => {
33
+ const c = normalizeCard(
34
+ { title: "Decided to ship", kind: "bogus", salience: 5 },
35
+ { sessionId: "s1", repo: "maestro", ts: "2026-06-01T00:00:00Z" },
36
+ );
37
+ assert.equal(c.kind, "fact", "unknown kind falls back to fact");
38
+ assert.equal(c.salience, 1, "salience clamped to [0,1]");
39
+ assert.equal(c.sessionId, "s1");
40
+ assert.equal(c.repo, "maestro");
41
+ assert.equal(c.ts, "2026-06-01T00:00:00Z");
42
+ // id is a stable hash of sessionId|kind|title
43
+ const c2 = normalizeCard({ title: "Decided to ship", kind: "bogus" }, { sessionId: "s1" });
44
+ assert.equal(c.id, c2.id, "same (session,kind,title) → same id");
45
+ });
46
+
47
+ test("normalizeCard: all KINDS preserved; body/tags truncated/cleaned", () => {
48
+ for (const k of KINDS) {
49
+ assert.equal(normalizeCard({ title: "t", kind: k }).kind, k);
50
+ }
51
+ const c = normalizeCard({
52
+ title: "t",
53
+ body: "x".repeat(5000),
54
+ tags: [" a ", "", "b", 3, "c", "d", "e", "f", "g", "h", "i", "j", "k", "l"],
55
+ });
56
+ assert.ok(c.body.length <= 600);
57
+ assert.ok(c.tags.length <= 12);
58
+ assert.deepEqual(c.tags.slice(0, 4), ["a", "b", "3", "c"], "trimmed, empties dropped, non-strings coerced");
59
+ });
60
+
61
+ test("appendCards + readRecentCards: round-trip, dedup, newest-first", () => {
62
+ const root = tmp();
63
+ try {
64
+ const n = appendCards(
65
+ root,
66
+ [
67
+ { title: "older", kind: "fact", ts: "2026-06-01T10:00:00Z" },
68
+ { title: "newer", kind: "decision", ts: "2026-06-02T10:00:00Z" },
69
+ { title: "older", kind: "fact", ts: "2026-06-01T10:00:00Z" }, // dup id
70
+ ],
71
+ { sessionId: "s1", repo: "r" },
72
+ );
73
+ assert.equal(n, 2, "duplicate id collapsed");
74
+ const cards = readRecentCards(root, { nowMs: Date.parse("2026-06-02T12:00:00Z") });
75
+ assert.equal(cards.length, 2);
76
+ assert.equal(cards[0].title, "newer", "newest first");
77
+ assert.equal(cards[1].title, "older");
78
+ } finally {
79
+ rmSync(root, { recursive: true, force: true });
80
+ }
81
+ });
82
+
83
+ test("readRecentCards: day-window filtering + limit", () => {
84
+ const root = tmp();
85
+ try {
86
+ appendCards(root, [{ title: "ancient", ts: "2026-01-01T00:00:00Z" }], { sessionId: "s" });
87
+ appendCards(root, [{ title: "recent", ts: "2026-06-02T00:00:00Z" }], { sessionId: "s" });
88
+ const recent = readRecentCards(root, { days: 30, nowMs: Date.parse("2026-06-03T00:00:00Z") });
89
+ assert.equal(recent.length, 1, "ancient card outside 30-day window excluded");
90
+ assert.equal(recent[0].title, "recent");
91
+ const limited = readRecentCards(root, { days: 9999, limit: 1, nowMs: Date.parse("2026-06-03T00:00:00Z") });
92
+ assert.equal(limited.length, 1, "limit honored");
93
+ } finally {
94
+ rmSync(root, { recursive: true, force: true });
95
+ }
96
+ });
97
+
98
+ test("appendCards: empty/!agentRoot is a safe no-op", () => {
99
+ assert.equal(appendCards(null, [{ title: "x" }]), 0);
100
+ assert.equal(appendCards(tmp(), []), 0);
101
+ });
102
+
103
+ test("cardToText: includes kind, title, body, tags", () => {
104
+ const txt = cardToText({ kind: "decision", title: "Ship v2", body: "after review", tags: ["release"] });
105
+ assert.match(txt, /decision/);
106
+ assert.match(txt, /Ship v2/);
107
+ assert.match(txt, /after review/);
108
+ assert.match(txt, /release/);
109
+ });
110
+
111
+ test("repoLabel: basename only, never a full path", () => {
112
+ assert.equal(repoLabel("/Users/x/maestro"), "maestro");
113
+ assert.equal(repoLabel(""), "");
114
+ });