@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,1120 @@
1
+ /**
2
+ * Maestro — Cadence Bus
3
+ *
4
+ * Local, file-backed event queue that decouples scheduled cadence ticks
5
+ * (launchd, manual, daemon, init-maestro, upgrade) from the persistent main
6
+ * Maestro session that actually services them.
7
+ *
8
+ * Why this exists
9
+ * ---------------
10
+ * Before the bus, every scheduled cadence tick (inbox-processor every 5m,
11
+ * backlog-executor every 10m, the daily/weekly/quarterly ones, …) was wired
12
+ * to launch a fresh `claude --print` session via run-trigger.sh. That meant
13
+ * dozens of full Claude Code spawns per day, each paying full auth/context/
14
+ * token overhead even when the tick had nothing to do. It also made
15
+ * emergency-stop, deduplication, rate-limiting, and quota gating ad-hoc.
16
+ *
17
+ * After the bus, launchd just enqueues a tiny JSON event onto a directory.
18
+ * The persistent daemon (maestro-daemon.mjs) consumes events in-process,
19
+ * handles lightweight scan/classify/route work inline, and only spawns a
20
+ * sub-session when the work genuinely warrants it.
21
+ *
22
+ * Storage layout (all paths relative to AGENT_ROOT)
23
+ * --------------------------------------------------
24
+ * state/cadence-bus/
25
+ * inbox/ — newly enqueued events (one JSON per event)
26
+ * claimed/ — events currently being processed
27
+ * processed/<YYYY-MM-DD>/ — successful completions, archived by date
28
+ * failed/ — last-known-bad event payloads (transient errors)
29
+ * dlq/ — events that exceeded retry budget (terminal)
30
+ * queue.jsonl — append-only fallback log (every enqueue lands
31
+ * here too, so no event is lost even if inbox/
32
+ * is unavailable on a given enqueue)
33
+ * health.json — heartbeat written by the consumer
34
+ *
35
+ * Concurrency model
36
+ * -----------------
37
+ * Atomic enqueue: write to a temp file in the same directory, then
38
+ * rename(tmp, final). rename(2) is atomic on macOS APFS
39
+ * when source and destination live on the same volume.
40
+ *
41
+ * Atomic claim: rename(inbox/<id>.json → claimed/<id>.json). If two
42
+ * consumers race for the same file, exactly one rename
43
+ * succeeds; the loser gets ENOENT and skips the event.
44
+ * No locks, no stale-lock pathology.
45
+ *
46
+ * Stale recovery: on consumer startup (and periodically while running),
47
+ * any file in claimed/ older than STALE_CLAIM_MS is
48
+ * either returned to inbox/ (under the retry budget) or
49
+ * moved to dlq/. This handles crashes mid-processing.
50
+ *
51
+ * Event payload schema
52
+ * --------------------
53
+ * {
54
+ * "id": "evt-2026-05-12T15:30:00.123Z-<random>",
55
+ * "type": "cadence_tick" | "manual_tick" | "internal",
56
+ * "source": "launchd" | "manual" | "daemon" | "init-maestro" | "upgrade",
57
+ * "ts": "2026-05-12T15:30:00.123Z",
58
+ * "cadence": "inbox-processor" | "backlog-executor" | "weekly-strategic-memo" | …,
59
+ * "workflow": "continuous/inbox-processor" (optional),
60
+ * "correlation_id": "<external id, optional>",
61
+ * "priority": "high" | "normal" | "low" (default "normal"),
62
+ * "metadata": { … free-form, kept for auditing },
63
+ * "attempts": 0 (auto-managed by consumer)
64
+ * }
65
+ *
66
+ * The bus does not interpret `cadence` itself — it only routes by it.
67
+ * Handler selection and execution live in scripts/daemon/cadence-handlers.mjs
68
+ * and scripts/daemon/cadence-consumer.mjs.
69
+ */
70
+
71
+ import {
72
+ appendFileSync,
73
+ closeSync,
74
+ existsSync,
75
+ fsyncSync,
76
+ mkdirSync,
77
+ openSync,
78
+ readFileSync,
79
+ readdirSync,
80
+ renameSync,
81
+ rmSync,
82
+ statSync,
83
+ truncateSync,
84
+ unlinkSync,
85
+ writeFileSync,
86
+ writeSync,
87
+ } from "node:fs";
88
+ import { join, resolve, dirname } from "node:path";
89
+ import { randomBytes } from "node:crypto";
90
+
91
+ // ---------------------------------------------------------------------------
92
+ // Constants (override-friendly via env for tests)
93
+ // ---------------------------------------------------------------------------
94
+
95
+ export const BUS_RELATIVE = "state/cadence-bus";
96
+ export const LOGS_RELATIVE = "logs/cadence-bus";
97
+
98
+ // Bus schema version. If we change the on-disk layout in a backwards-
99
+ // incompatible way, bump this and write a migration. Doctor reads this.
100
+ export const BUS_VERSION = "1";
101
+
102
+ // Stale claim threshold — claims older than this are recovered to inbox or
103
+ // the dlq. Defaults assume the longest live cadence handler runs well under
104
+ // 30 minutes; tune via env for long-running migrations.
105
+ const DEFAULT_STALE_CLAIM_MS = 30 * 60 * 1000;
106
+
107
+ // Maximum attempts before an event goes to dlq. Cadence ticks are mostly
108
+ // non-essential; we don't want a single stuck tick to clog the bus.
109
+ const DEFAULT_MAX_ATTEMPTS = 5;
110
+
111
+ // Retention defaults (audit M8). On a 24/7 mini nothing prunes processed/,
112
+ // dlq/, failed/, queue.jsonl or the log dir, so they grow without bound.
113
+ // sweepRetention enforces these caps; all overridable via opts / env.
114
+ const DEFAULT_PROCESSED_RETENTION_DAYS = 14;
115
+ const DEFAULT_TERMINAL_KEEP = 500; // newest N files kept in dlq/ & failed/
116
+ const DEFAULT_QUEUE_MAX_BYTES = 5 * 1024 * 1024; // queue.jsonl rotate threshold (~5MB)
117
+ const DEFAULT_LOG_RETENTION_DAYS = 14; // logs/cadence-bus/<date>.jsonl
118
+
119
+ // ---------------------------------------------------------------------------
120
+ // Path resolution
121
+ // ---------------------------------------------------------------------------
122
+
123
+ /**
124
+ * Resolve the agent root. Prefers explicit arg, then AGENT_ROOT env, then
125
+ * AGENT_DIR env, then process.cwd(). Returns an absolute path.
126
+ */
127
+ export function resolveAgentRoot(agentRoot) {
128
+ return resolve(
129
+ agentRoot ||
130
+ process.env.AGENT_ROOT ||
131
+ process.env.AGENT_DIR ||
132
+ process.cwd()
133
+ );
134
+ }
135
+
136
+ /**
137
+ * Return all bus paths derived from an agent root.
138
+ */
139
+ export function getBusPaths(agentRoot) {
140
+ const root = resolveAgentRoot(agentRoot);
141
+ const base = join(root, BUS_RELATIVE);
142
+ return {
143
+ agentRoot: root,
144
+ base,
145
+ inbox: join(base, "inbox"),
146
+ claimed: join(base, "claimed"),
147
+ processed: join(base, "processed"),
148
+ failed: join(base, "failed"),
149
+ dlq: join(base, "dlq"),
150
+ queueJsonl: join(base, "queue.jsonl"),
151
+ health: join(base, "health.json"),
152
+ version: join(base, "VERSION"),
153
+ // WS4 at-most-once cron state: per-cadence {lastRunAt,nextRunAt,intervalMs}.
154
+ schedule: join(base, "schedule.json"),
155
+ scheduleLock: join(base, "schedule.json.lock"),
156
+ logsDir: join(root, LOGS_RELATIVE),
157
+ emergencyStop: join(root, ".emergency-stop"),
158
+ };
159
+ }
160
+
161
+ /**
162
+ * Idempotently create the cadence-bus directory tree. Safe to call on every
163
+ * enqueue/consume; no-op once present.
164
+ */
165
+ export function ensureBusDirs(agentRoot) {
166
+ const paths = getBusPaths(agentRoot);
167
+ for (const dir of [paths.inbox, paths.claimed, paths.processed, paths.failed, paths.dlq, paths.logsDir]) {
168
+ mkdirSync(dir, { recursive: true });
169
+ }
170
+ // Touch a version marker so doctor / upgrade can detect a freshly-created
171
+ // bus and reason about schema migrations later.
172
+ if (!existsSync(paths.version)) {
173
+ writeFileSync(paths.version, `${BUS_VERSION}\n`);
174
+ }
175
+ return paths;
176
+ }
177
+
178
+ // ---------------------------------------------------------------------------
179
+ // Event id
180
+ // ---------------------------------------------------------------------------
181
+
182
+ /**
183
+ * Generate a unique, sortable event id of the form:
184
+ * evt-2026-05-12T15:30:00.123Z-<8 hex>
185
+ * The ISO prefix makes inbox/ directory listings naturally sorted by time.
186
+ * The random suffix avoids collisions for events enqueued in the same ms.
187
+ */
188
+ export function nextEventId(now = new Date()) {
189
+ const iso = now.toISOString();
190
+ const rand = randomBytes(4).toString("hex");
191
+ return `evt-${iso}-${rand}`;
192
+ }
193
+
194
+ // ---------------------------------------------------------------------------
195
+ // Logging
196
+ // ---------------------------------------------------------------------------
197
+
198
+ function todayUtc(now = new Date()) {
199
+ return now.toISOString().slice(0, 10);
200
+ }
201
+
202
+ /**
203
+ * Append a structured log line to logs/cadence-bus/<date>.jsonl. Best-effort;
204
+ * caller never blocks on log failures. Returns true on success, false on a
205
+ * caught I/O error.
206
+ */
207
+ export function logBusEvent(agentRoot, entry) {
208
+ try {
209
+ const paths = getBusPaths(agentRoot);
210
+ mkdirSync(paths.logsDir, { recursive: true });
211
+ const file = join(paths.logsDir, `${todayUtc()}.jsonl`);
212
+ const line = JSON.stringify({ ts: new Date().toISOString(), ...entry }) + "\n";
213
+ appendFileSync(file, line);
214
+ return true;
215
+ } catch {
216
+ return false;
217
+ }
218
+ }
219
+
220
+ // ---------------------------------------------------------------------------
221
+ // Atomic write
222
+ // ---------------------------------------------------------------------------
223
+
224
+ /**
225
+ * Atomically write JSON to `targetPath`. Writes a sibling `.tmp` file, fsyncs
226
+ * the file's contents to disk, then renames it over the target. The fsync
227
+ * happens BEFORE the rename so the durable bytes are in place once the atomic
228
+ * rename publishes the name — a crash after the rename returns can't surface a
229
+ * zero-length or half-written file. (On macOS APFS rename(2) is atomic for
230
+ * same-volume source/dest; fsync flushes the data the rename then exposes.)
231
+ *
232
+ * Exported (audit M6) so the consumer's re-queue paths use the same
233
+ * write-temp-then-rename primitive instead of a bare writeFileSync that can
234
+ * leave a half-written inbox file if the process dies mid-write.
235
+ */
236
+ export function writeJsonAtomic(targetPath, obj) {
237
+ mkdirSync(dirname(targetPath), { recursive: true });
238
+ const tmp = `${targetPath}.tmp.${process.pid}.${Date.now()}.${randomBytes(2).toString("hex")}`;
239
+ const data = JSON.stringify(obj, null, 2) + "\n";
240
+ // Write + fsync via an explicit fd so the contents are durable before the
241
+ // rename publishes the name. closeSync runs whether or not fsync succeeds.
242
+ const fd = openSync(tmp, "w");
243
+ try {
244
+ writeSync(fd, data);
245
+ fsyncSync(fd);
246
+ } finally {
247
+ closeSync(fd);
248
+ }
249
+ try {
250
+ renameSync(tmp, targetPath);
251
+ } catch (err) {
252
+ // Clean up the orphan tmp file if rename failed.
253
+ try { unlinkSync(tmp); } catch { /* ignore */ }
254
+ throw err;
255
+ }
256
+ }
257
+
258
+ // ---------------------------------------------------------------------------
259
+ // Enqueue
260
+ // ---------------------------------------------------------------------------
261
+
262
+ /**
263
+ * Append a one-line JSON record to the fallback queue.jsonl. Used both as a
264
+ * tamper-evident audit log of every enqueue and as a last-resort fallback
265
+ * when writing to inbox/ fails (e.g. disk full, permissions).
266
+ */
267
+ function appendFallbackQueue(paths, event) {
268
+ try {
269
+ mkdirSync(dirname(paths.queueJsonl), { recursive: true });
270
+ appendFileSync(paths.queueJsonl, JSON.stringify(event) + "\n");
271
+ return true;
272
+ } catch {
273
+ return false;
274
+ }
275
+ }
276
+
277
+ // ---------------------------------------------------------------------------
278
+ // At-most-once cron gate (WS4)
279
+ // ---------------------------------------------------------------------------
280
+ //
281
+ // The plist's StartInterval/StartCalendarInterval is the wall-clock trigger,
282
+ // but launchd can double-fire (catch-up after sleep, overlapping loads) and,
283
+ // after a reboot/outage, it would otherwise replay every missed tick at once.
284
+ // When the caller passes `intervalMs` (derived by generate-plists.sh from the
285
+ // cadence's interval), enqueueTick consults a per-cadence schedule under an
286
+ // O_EXCL lock and admits AT MOST ONE tick per interval:
287
+ //
288
+ // • grace — a fire within graceMs of the boundary is the real tick:
289
+ // advance nextRunAt and admit. An early/duplicate fire is
290
+ // suppressed. graceMs = clamp(interval/2, 120s, 2h).
291
+ // • stale fast-fwd — if we're more than one whole interval past nextRunAt
292
+ // (we were asleep / powered off), jump nextRunAt to the
293
+ // NEXT future boundary and admit exactly ONE catch-up
294
+ // tick — never the burst of missed ones.
295
+ //
296
+ // When `intervalMs` is absent the gate is a no-op and enqueueTick behaves
297
+ // exactly as before (full back-compat — every existing caller/test).
298
+
299
+ const SCHEDULE_GRACE_MIN_MS = 120_000; // 2 min
300
+ const SCHEDULE_GRACE_MAX_MS = 2 * 60 * 60_000; // 2 h
301
+ const SCHEDULE_LOCK_STALE_MS = 30_000; // a held lock older than this is stale
302
+
303
+ function clampGrace(intervalMs) {
304
+ return Math.min(SCHEDULE_GRACE_MAX_MS, Math.max(SCHEDULE_GRACE_MIN_MS, Math.floor(intervalMs / 2)));
305
+ }
306
+
307
+ /**
308
+ * Acquire the schedule lock via O_EXCL (same atomic primitive as
309
+ * session-lock.mjs:89). Returns true on success. A stale lock (older than
310
+ * SCHEDULE_LOCK_STALE_MS) is reclaimed so a crash mid-update can't wedge the
311
+ * gate forever.
312
+ */
313
+ function acquireScheduleLock(paths, now) {
314
+ try {
315
+ writeFileSync(paths.scheduleLock, JSON.stringify({ pid: process.pid, at: now }), { flag: "wx" });
316
+ return true;
317
+ } catch (err) {
318
+ if (err.code !== "EEXIST") return false;
319
+ // Reclaim if stale.
320
+ try {
321
+ const st = statSync(paths.scheduleLock);
322
+ if (now - st.mtimeMs >= SCHEDULE_LOCK_STALE_MS) {
323
+ writeFileSync(paths.scheduleLock, JSON.stringify({ pid: process.pid, at: now }));
324
+ return true;
325
+ }
326
+ } catch { /* vanished — try once more */ }
327
+ try {
328
+ writeFileSync(paths.scheduleLock, JSON.stringify({ pid: process.pid, at: now }), { flag: "wx" });
329
+ return true;
330
+ } catch { return false; }
331
+ }
332
+ }
333
+
334
+ function releaseScheduleLock(paths) {
335
+ try { unlinkSync(paths.scheduleLock); } catch { /* */ }
336
+ }
337
+
338
+ function readSchedule(paths) {
339
+ try { return JSON.parse(readFileSync(paths.schedule, "utf-8")) || {}; }
340
+ catch { return {}; }
341
+ }
342
+
343
+ /**
344
+ * Decide whether to admit this cadence tick, mutating the on-disk schedule.
345
+ * Pure-ish: the only side effect is the schedule.json write (under the lock).
346
+ * Returns { admit: boolean, reason, kind }.
347
+ *
348
+ * If the lock can't be taken (rare contention) we FAIL OPEN — admit the tick
349
+ * (Invariant: never drop work; a duplicate is cheaper than a missed cadence).
350
+ *
351
+ * @param {object} paths
352
+ * @param {string} cadence
353
+ * @param {number} intervalMs
354
+ * @param {number} now
355
+ */
356
+ function scheduleGate(paths, cadence, intervalMs, now) {
357
+ if (!(Number.isFinite(intervalMs) && intervalMs > 0)) {
358
+ return { admit: true, reason: "no-interval", kind: "passthrough" };
359
+ }
360
+ mkdirSync(paths.base, { recursive: true });
361
+ if (!acquireScheduleLock(paths, now)) {
362
+ return { admit: true, reason: "lock-contended-fail-open", kind: "admit" };
363
+ }
364
+ try {
365
+ const sched = readSchedule(paths);
366
+ const entry = sched[cadence];
367
+ const grace = clampGrace(intervalMs);
368
+
369
+ let result;
370
+ if (!entry || !Number.isFinite(Number(entry.nextRunAt))) {
371
+ // First time we've seen this cadence: admit now, schedule the next one.
372
+ sched[cadence] = { lastRunAt: now, nextRunAt: now + intervalMs, intervalMs };
373
+ result = { admit: true, reason: "first-tick", kind: "admit" };
374
+ } else {
375
+ const nextRunAt = Number(entry.nextRunAt);
376
+ if (now > nextRunAt + intervalMs) {
377
+ // STALE: we were asleep/off for more than a whole interval. Jump
378
+ // nextRunAt to the next FUTURE boundary and admit exactly one catch-up.
379
+ let boundary = nextRunAt;
380
+ // Advance in whole intervals until strictly in the future.
381
+ const periodsMissed = Math.floor((now - nextRunAt) / intervalMs) + 1;
382
+ boundary = nextRunAt + periodsMissed * intervalMs;
383
+ if (boundary <= now) boundary += intervalMs; // safety: ensure future
384
+ sched[cadence] = { lastRunAt: now, nextRunAt: boundary, intervalMs };
385
+ result = { admit: true, reason: "stale-fast-forward", kind: "catch-up", periodsMissed };
386
+ } else if (now >= nextRunAt - grace) {
387
+ // On-time (within grace of the boundary): the real tick. Advance.
388
+ sched[cadence] = { lastRunAt: now, nextRunAt: nextRunAt + intervalMs, intervalMs };
389
+ result = { admit: true, reason: "on-boundary", kind: "admit" };
390
+ } else {
391
+ // Too early / duplicate launchd fire — suppress.
392
+ result = { admit: false, reason: "before-grace-window", kind: "duplicate", nextRunAt };
393
+ }
394
+ }
395
+
396
+ if (result.admit) {
397
+ try { writeJsonAtomic(paths.schedule, sched); }
398
+ catch { /* best-effort; on write failure we still admit (no work lost) */ }
399
+ }
400
+ return result;
401
+ } finally {
402
+ releaseScheduleLock(paths);
403
+ }
404
+ }
405
+
406
+ /**
407
+ * Enqueue a cadence event onto the bus.
408
+ *
409
+ * @param {object} input
410
+ * @param {string} input.cadence Cadence name (e.g. "inbox-processor"). Required.
411
+ * @param {string} [input.type] Event type, default "cadence_tick".
412
+ * @param {string} [input.source] Originator label, default "manual".
413
+ * @param {string} [input.workflow]
414
+ * @param {string} [input.correlation_id]
415
+ * @param {"high"|"normal"|"low"} [input.priority]
416
+ * @param {object} [input.metadata]
417
+ * @param {number} [input.intervalMs] Cadence interval (ms). When present,
418
+ * enables the at-most-once cron gate
419
+ * (grace + stale fast-forward). Absent →
420
+ * legacy behaviour, every call enqueues.
421
+ * @param {number} [input.now] Injectable clock (ms) for tests.
422
+ * @param {string} [input.agentRoot] Override AGENT_ROOT for tests.
423
+ * @returns {{ id: string, path: string, fallbackOnly: boolean, skipped?: string }}
424
+ */
425
+ export function enqueueTick(input = {}) {
426
+ if (!input || typeof input !== "object") {
427
+ throw new TypeError("enqueueTick: input must be an object");
428
+ }
429
+ const cadence = String(input.cadence || "").trim();
430
+ if (!cadence) {
431
+ throw new Error("enqueueTick: cadence is required");
432
+ }
433
+
434
+ const paths = ensureBusDirs(input.agentRoot);
435
+
436
+ // Emergency-stop short-circuit. The bus still logs the event so we can
437
+ // see what would have run, but no file lands in inbox/.
438
+ if (existsSync(paths.emergencyStop)) {
439
+ const event = buildEvent(input, cadence);
440
+ appendFallbackQueue(paths, { ...event, _suppressed: "emergency-stop" });
441
+ logBusEvent(paths.agentRoot, {
442
+ level: "info",
443
+ stage: "enqueue_suppressed",
444
+ cadence,
445
+ reason: "emergency-stop",
446
+ id: event.id,
447
+ });
448
+ return { id: event.id, path: null, fallbackOnly: true, skipped: "emergency-stop" };
449
+ }
450
+
451
+ const event = buildEvent(input, cadence);
452
+ const target = join(paths.inbox, `${event.id}.json`);
453
+
454
+ // Dedup against replay (audit H12). When a caller supplies an explicit
455
+ // `input.id` (e.g. a poller re-delivering after a reconnect), writing
456
+ // straight to inbox/ would clobber an in-flight event — overwriting its
457
+ // bumped attempt count — or silently re-run a cadence already processed.
458
+ // Auto-generated ids are collision-resistant, so this only guards the
459
+ // explicit-id case.
460
+ if (input.id) {
461
+ const dup =
462
+ existsSync(join(paths.inbox, `${event.id}.json`)) ? "inbox" :
463
+ existsSync(join(paths.claimed, `${event.id}.json`)) ? "claimed" :
464
+ existsSync(join(paths.dlq, `${event.id}.json`)) ? "dlq" :
465
+ isProcessedId(paths.agentRoot, event.id) ? "processed" :
466
+ null;
467
+ if (dup) {
468
+ logBusEvent(paths.agentRoot, {
469
+ level: "info",
470
+ stage: "enqueue_deduped",
471
+ cadence,
472
+ id: event.id,
473
+ existing: dup,
474
+ });
475
+ return { id: event.id, path: null, fallbackOnly: false, deduped: dup };
476
+ }
477
+ }
478
+
479
+ // At-most-once cron gate (WS4). Only engages when intervalMs is supplied by
480
+ // the caller (plist). Suppressed ticks are still audited so we can see what
481
+ // launchd fired, but nothing lands in inbox/.
482
+ if (Number.isFinite(input.intervalMs) && input.intervalMs > 0) {
483
+ const nowMs = typeof input.now === "number" ? input.now : Date.now();
484
+ const gate = scheduleGate(paths, cadence, input.intervalMs, nowMs);
485
+ if (!gate.admit) {
486
+ appendFallbackQueue(paths, { ...event, _suppressed: gate.reason });
487
+ logBusEvent(paths.agentRoot, {
488
+ level: "info",
489
+ stage: "enqueue_suppressed",
490
+ cadence,
491
+ reason: gate.reason,
492
+ kind: gate.kind,
493
+ id: event.id,
494
+ });
495
+ return { id: event.id, path: null, fallbackOnly: true, skipped: gate.reason };
496
+ }
497
+ // Annotate the admitted event so the catch-up case is visible downstream.
498
+ if (gate.kind === "catch-up") {
499
+ event.metadata = { ...(event.metadata || {}), schedule_kind: "catch-up", periods_missed: gate.periodsMissed };
500
+ }
501
+ }
502
+
503
+ // Always write the audit row first. If inbox write fails the event is
504
+ // still recoverable from queue.jsonl.
505
+ appendFallbackQueue(paths, event);
506
+
507
+ let fallbackOnly = false;
508
+ try {
509
+ writeJsonAtomic(target, event);
510
+ } catch (err) {
511
+ fallbackOnly = true;
512
+ logBusEvent(paths.agentRoot, {
513
+ level: "error",
514
+ stage: "enqueue_inbox_failed",
515
+ cadence,
516
+ id: event.id,
517
+ error: err.message,
518
+ });
519
+ }
520
+
521
+ logBusEvent(paths.agentRoot, {
522
+ level: "info",
523
+ stage: fallbackOnly ? "enqueued_fallback" : "enqueued",
524
+ cadence,
525
+ source: event.source,
526
+ priority: event.priority,
527
+ id: event.id,
528
+ });
529
+
530
+ return { id: event.id, path: fallbackOnly ? null : target, fallbackOnly };
531
+ }
532
+
533
+ function buildEvent(input, cadence) {
534
+ return {
535
+ id: input.id || nextEventId(),
536
+ type: input.type || "cadence_tick",
537
+ source: input.source || "manual",
538
+ ts: input.ts || new Date().toISOString(),
539
+ cadence,
540
+ workflow: input.workflow || null,
541
+ correlation_id: input.correlation_id || null,
542
+ priority: ["high", "normal", "low"].includes(input.priority) ? input.priority : "normal",
543
+ metadata: input.metadata && typeof input.metadata === "object" ? input.metadata : {},
544
+ attempts: typeof input.attempts === "number" ? input.attempts : 0,
545
+ };
546
+ }
547
+
548
+ // ---------------------------------------------------------------------------
549
+ // Consume
550
+ // ---------------------------------------------------------------------------
551
+
552
+ /**
553
+ * List inbox event ids in chronological order. Returns ids without `.json`.
554
+ */
555
+ export function listInbox(agentRoot) {
556
+ const paths = getBusPaths(agentRoot);
557
+ if (!existsSync(paths.inbox)) return [];
558
+ return readdirSync(paths.inbox)
559
+ .filter((n) => n.endsWith(".json") && !n.endsWith(".tmp"))
560
+ .sort()
561
+ .map((n) => n.slice(0, -5));
562
+ }
563
+
564
+ /**
565
+ * List currently-claimed event ids. Used by stale-claim recovery.
566
+ */
567
+ export function listClaimed(agentRoot) {
568
+ const paths = getBusPaths(agentRoot);
569
+ if (!existsSync(paths.claimed)) return [];
570
+ return readdirSync(paths.claimed)
571
+ .filter((n) => n.endsWith(".json") && !n.endsWith(".tmp"))
572
+ .sort()
573
+ .map((n) => n.slice(0, -5));
574
+ }
575
+
576
+ /**
577
+ * Atomically claim the oldest inbox event for processing. Returns the parsed
578
+ * event payload (with absolute paths attached), or null if the inbox was
579
+ * empty / all candidates lost the race.
580
+ *
581
+ * Other consumers racing for the same event will see ENOENT on rename and
582
+ * naturally move on.
583
+ */
584
+ export function claimNextTick(agentRoot) {
585
+ const paths = ensureBusDirs(agentRoot);
586
+ const ids = listInbox(paths.agentRoot);
587
+ for (const id of ids) {
588
+ const src = join(paths.inbox, `${id}.json`);
589
+ const dst = join(paths.claimed, `${id}.json`);
590
+ try {
591
+ renameSync(src, dst);
592
+ } catch (err) {
593
+ if (err.code === "ENOENT") continue; // someone else claimed it
594
+ throw err;
595
+ }
596
+ // Load the event from its claimed path. If it's malformed, fail it and
597
+ // try the next one.
598
+ let event;
599
+ try {
600
+ event = JSON.parse(readFileSync(dst, "utf-8"));
601
+ } catch (err) {
602
+ logBusEvent(paths.agentRoot, {
603
+ level: "error",
604
+ stage: "claim_parse_failed",
605
+ id,
606
+ error: err.message,
607
+ });
608
+ failTick(paths.agentRoot, id, `parse-failed: ${err.message}`);
609
+ continue;
610
+ }
611
+ // Attempt accounting is single-sourced in failTick (audit M5). Claiming
612
+ // an event is NOT a failed attempt, so claim must not mutate `attempts`
613
+ // — otherwise a successful first run would have already burned one unit
614
+ // of retry budget, and a crash-then-recover would double-count it.
615
+ // Normalise the field so downstream readers always see a number.
616
+ if (typeof event.attempts !== "number") {
617
+ event.attempts = 0;
618
+ }
619
+ logBusEvent(paths.agentRoot, {
620
+ level: "info",
621
+ stage: "claimed",
622
+ id,
623
+ cadence: event.cadence,
624
+ attempts: event.attempts,
625
+ });
626
+ return { event, claimedPath: dst };
627
+ }
628
+ return null;
629
+ }
630
+
631
+ // ---------------------------------------------------------------------------
632
+ // Lifecycle transitions
633
+ // ---------------------------------------------------------------------------
634
+
635
+ function moveClaimed(paths, id, destDir, suffix = ".json") {
636
+ const src = join(paths.claimed, `${id}.json`);
637
+ mkdirSync(destDir, { recursive: true });
638
+ const dst = join(destDir, `${id}${suffix}`);
639
+ if (!existsSync(src)) return null;
640
+ renameSync(src, dst);
641
+ return dst;
642
+ }
643
+
644
+ /**
645
+ * Has an event id already been archived under processed/<date>/? Used by
646
+ * enqueueTick to suppress re-running a cadence that already completed when a
647
+ * producer replays an explicit id after a reconnect (H12). Scans the dated
648
+ * processed/ subdirectories for <id>.json.
649
+ *
650
+ * @param {string} agentRoot
651
+ * @param {string} id
652
+ * @returns {boolean}
653
+ */
654
+ export function isProcessedId(agentRoot, id) {
655
+ const paths = getBusPaths(agentRoot);
656
+ if (!existsSync(paths.processed)) return false;
657
+ let dateDirs;
658
+ try {
659
+ dateDirs = readdirSync(paths.processed, { withFileTypes: true });
660
+ } catch {
661
+ return false;
662
+ }
663
+ for (const d of dateDirs) {
664
+ if (!d.isDirectory()) continue;
665
+ if (existsSync(join(paths.processed, d.name, `${id}.json`))) return true;
666
+ }
667
+ return false;
668
+ }
669
+
670
+ /**
671
+ * Mark a claimed event as successfully processed. Moves it to
672
+ * processed/<YYYY-MM-DD>/<id>.json and stores the handler result alongside.
673
+ */
674
+ export function completeTick(agentRoot, id, result = {}) {
675
+ const paths = ensureBusDirs(agentRoot);
676
+ const date = todayUtc();
677
+ const dst = moveClaimed(paths, id, join(paths.processed, date));
678
+ if (!dst) {
679
+ logBusEvent(paths.agentRoot, { level: "warn", stage: "complete_missing", id });
680
+ return null;
681
+ }
682
+ // Annotate the archived event with the result. Best-effort, but observable:
683
+ // this write carries the handler result + completed_at audit fields, so a
684
+ // silent failure would leave the processed/ record unannotated — log it.
685
+ try {
686
+ const event = JSON.parse(readFileSync(dst, "utf-8"));
687
+ event.result = result;
688
+ event.completed_at = new Date().toISOString();
689
+ writeJsonAtomic(dst, event);
690
+ } catch (err) {
691
+ logBusEvent(paths.agentRoot, {
692
+ level: "warn",
693
+ stage: "best_effort_write_failed",
694
+ op: "complete_annotate",
695
+ id,
696
+ error: err.message,
697
+ });
698
+ }
699
+ logBusEvent(paths.agentRoot, {
700
+ level: "info",
701
+ stage: "processed",
702
+ id,
703
+ cadence: result.cadence || null,
704
+ decision: result.decision || null,
705
+ duration_ms: result.duration_ms ?? null,
706
+ });
707
+ return dst;
708
+ }
709
+
710
+ /**
711
+ * Mark a claimed event as failed. If under the retry budget, returns it to
712
+ * inbox/ for another attempt; otherwise routes to dlq/.
713
+ */
714
+ export function failTick(agentRoot, id, errorOrReason, opts = {}) {
715
+ const paths = ensureBusDirs(agentRoot);
716
+ const maxAttempts = opts.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
717
+ const srcClaimed = join(paths.claimed, `${id}.json`);
718
+ let event = null;
719
+ if (existsSync(srcClaimed)) {
720
+ try { event = JSON.parse(readFileSync(srcClaimed, "utf-8")); } catch { /* */ }
721
+ }
722
+ if (!event) {
723
+ // Caller already moved or never had a claim; nothing to do.
724
+ logBusEvent(paths.agentRoot, { level: "warn", stage: "fail_missing", id });
725
+ return null;
726
+ }
727
+ event.last_error = typeof errorOrReason === "string"
728
+ ? errorOrReason
729
+ : (errorOrReason?.message || String(errorOrReason));
730
+ event.failed_at = new Date().toISOString();
731
+
732
+ // Single source of truth for attempt accounting (audit M5). A failed
733
+ // attempt is the ONLY thing that increments `attempts`; claim/consumer
734
+ // never touch it. Increment first, then compare against the budget so the
735
+ // count reflects the attempt that just failed.
736
+ const attempts = (typeof event.attempts === "number" ? event.attempts : 0) + 1;
737
+ event.attempts = attempts;
738
+
739
+ if (attempts >= maxAttempts || opts.terminal === true) {
740
+ // Move to dlq/. Persist the bumped count + last_error onto the claimed
741
+ // file first so the archived dlq payload reflects the final attempt.
742
+ // Best-effort, but observable: this write carries the attempt count that
743
+ // justified the DLQ decision, so a silent failure would archive a stale
744
+ // payload — log it instead of swallowing.
745
+ try {
746
+ writeJsonAtomic(srcClaimed, event);
747
+ } catch (err) {
748
+ logBusEvent(paths.agentRoot, {
749
+ level: "warn",
750
+ stage: "best_effort_write_failed",
751
+ op: "dlq_persist_attempts",
752
+ id,
753
+ cadence: event.cadence,
754
+ attempts,
755
+ error: err.message,
756
+ });
757
+ }
758
+ moveClaimed(paths, id, paths.dlq);
759
+ logBusEvent(paths.agentRoot, {
760
+ level: "error",
761
+ stage: "dlq",
762
+ id,
763
+ cadence: event.cadence,
764
+ attempts,
765
+ reason: event.last_error,
766
+ });
767
+ return { destination: "dlq" };
768
+ }
769
+
770
+ // Re-enqueue: write fresh in inbox/, drop the claim. Guarded (audit M7):
771
+ // if the inbox write fails (ENOSPC / EACCES), leave the claim in place so
772
+ // the next stale-claim recovery sweep retries it, rather than throwing out
773
+ // of failTick and losing the event entirely.
774
+ const newTarget = join(paths.inbox, `${id}.json`);
775
+ try {
776
+ writeJsonAtomic(newTarget, event);
777
+ } catch (err) {
778
+ logBusEvent(paths.agentRoot, {
779
+ level: "error",
780
+ stage: "retry_requeue_failed",
781
+ id,
782
+ cadence: event.cadence,
783
+ attempts,
784
+ error: err.message,
785
+ });
786
+ return { destination: "claimed", error: err.message };
787
+ }
788
+ try { unlinkSync(srcClaimed); } catch { /* */ }
789
+ logBusEvent(paths.agentRoot, {
790
+ level: "warn",
791
+ stage: "retry_requeued",
792
+ id,
793
+ cadence: event.cadence,
794
+ attempts,
795
+ reason: event.last_error,
796
+ });
797
+ return { destination: "inbox" };
798
+ }
799
+
800
+ /**
801
+ * Atomically re-enqueue an already-loaded event object back onto inbox/ and
802
+ * drop its claim (audit M6). Used by the consumer's circuit-open / deferred
803
+ * re-queue paths so they go through the same write-temp-then-rename primitive
804
+ * as every other bus write instead of a bare writeFileSync.
805
+ *
806
+ * Does NOT touch `event.attempts` — re-queueing because of an upstream gate
807
+ * (circuit open, concurrency cap) is not a failed attempt, and attempt
808
+ * accounting is single-sourced in failTick (M5). The caller passes the event
809
+ * exactly as it wants it re-queued.
810
+ *
811
+ * Best-effort: on a write failure the claim is left in place so stale-claim
812
+ * recovery can retry it. Returns true if the event landed in inbox/.
813
+ *
814
+ * @param {string} agentRoot
815
+ * @param {object} event Must include `id`.
816
+ * @returns {boolean}
817
+ */
818
+ export function requeueTick(agentRoot, event) {
819
+ if (!event || typeof event !== "object" || !event.id) {
820
+ throw new TypeError("requeueTick: event with an id is required");
821
+ }
822
+ const paths = ensureBusDirs(agentRoot);
823
+ const target = join(paths.inbox, `${event.id}.json`);
824
+ try {
825
+ writeJsonAtomic(target, event);
826
+ } catch (err) {
827
+ logBusEvent(paths.agentRoot, {
828
+ level: "error",
829
+ stage: "requeue_failed",
830
+ id: event.id,
831
+ cadence: event.cadence,
832
+ error: err.message,
833
+ });
834
+ return false;
835
+ }
836
+ try { unlinkSync(join(paths.claimed, `${event.id}.json`)); } catch { /* */ }
837
+ return true;
838
+ }
839
+
840
+ // ---------------------------------------------------------------------------
841
+ // Stale claim recovery
842
+ // ---------------------------------------------------------------------------
843
+
844
+ /**
845
+ * Sweep claimed/ for entries older than `maxAgeMs`. Each stale claim is
846
+ * either returned to inbox/ (under the retry budget) or moved to dlq/.
847
+ * Safe to run on consumer startup *and* periodically while running, because
848
+ * the consumer is the only writer to claimed/.
849
+ *
850
+ * Returns the count of events recovered + count moved to dlq.
851
+ */
852
+ export function recoverStaleClaims(agentRoot, maxAgeMs = DEFAULT_STALE_CLAIM_MS, now = Date.now()) {
853
+ const paths = ensureBusDirs(agentRoot);
854
+ const stats = { recovered: 0, dlq: 0, scanned: 0 };
855
+ if (!existsSync(paths.claimed)) return stats;
856
+ for (const name of readdirSync(paths.claimed)) {
857
+ if (!name.endsWith(".json")) continue;
858
+ stats.scanned++;
859
+ const claimedPath = join(paths.claimed, name);
860
+ let st;
861
+ try { st = statSync(claimedPath); } catch { continue; }
862
+ if (now - st.mtimeMs < maxAgeMs) continue;
863
+
864
+ const id = name.slice(0, -5);
865
+ const outcome = failTick(paths.agentRoot, id, "stale-claim-recovery", {
866
+ maxAttempts: DEFAULT_MAX_ATTEMPTS,
867
+ });
868
+ if (outcome?.destination === "dlq") stats.dlq++;
869
+ else if (outcome?.destination === "inbox") stats.recovered++;
870
+ }
871
+ if (stats.scanned > 0) {
872
+ logBusEvent(paths.agentRoot, {
873
+ level: "info",
874
+ stage: "stale_recovery_complete",
875
+ ...stats,
876
+ });
877
+ }
878
+ return stats;
879
+ }
880
+
881
+ // ---------------------------------------------------------------------------
882
+ // Retention sweep (audit M8)
883
+ // ---------------------------------------------------------------------------
884
+
885
+ /**
886
+ * Best-effort: list directory entries, or [] on any error.
887
+ */
888
+ function safeReaddir(dir, opts) {
889
+ try { return readdirSync(dir, opts); } catch { return []; }
890
+ }
891
+
892
+ /**
893
+ * Prune unbounded bus growth. Designed to run on the same periodic timer as
894
+ * recoverStaleClaims. EVERY step is wrapped so a single failure (permissions,
895
+ * a file vanishing mid-sweep, ENOSPC on a temp write) can never throw out of
896
+ * the sweep and kill the recovery loop.
897
+ *
898
+ * Steps:
899
+ * 1. Delete processed/<YYYY-MM-DD>/ dirs older than `processedDays`
900
+ * (default 14, env MAESTRO_RETENTION_DAYS, opts.processedDays).
901
+ * 2. Cap dlq/ and failed/ to the newest `terminalKeep` files (default 500,
902
+ * opts.terminalKeep). Oldest are unlinked first.
903
+ * 3. Rotate/truncate queue.jsonl if it exceeds `queueMaxBytes` (~5MB),
904
+ * keeping the tail so recent audit rows survive.
905
+ * 4. Delete logs/cadence-bus/<YYYY-MM-DD>.jsonl older than `logDays`.
906
+ *
907
+ * @param {string} agentRoot
908
+ * @param {object} [opts]
909
+ * @param {number} [opts.processedDays]
910
+ * @param {number} [opts.terminalKeep]
911
+ * @param {number} [opts.queueMaxBytes]
912
+ * @param {number} [opts.logDays]
913
+ * @param {number} [opts.now] injectable clock (ms) for tests
914
+ * @returns {{processedDirsDeleted:number, dlqDeleted:number, failedDeleted:number, queueRotated:boolean, logsDeleted:number, errors:number}}
915
+ */
916
+ export function sweepRetention(agentRoot, opts = {}) {
917
+ const stats = {
918
+ processedDirsDeleted: 0,
919
+ dlqDeleted: 0,
920
+ failedDeleted: 0,
921
+ queueRotated: false,
922
+ logsDeleted: 0,
923
+ errors: 0,
924
+ };
925
+
926
+ let paths;
927
+ try {
928
+ paths = getBusPaths(agentRoot);
929
+ } catch {
930
+ stats.errors++;
931
+ return stats;
932
+ }
933
+
934
+ const now = typeof opts.now === "number" ? opts.now : Date.now();
935
+ const envDays = Number(process.env.MAESTRO_RETENTION_DAYS);
936
+ const processedDays =
937
+ Number.isFinite(opts.processedDays) ? opts.processedDays
938
+ : Number.isFinite(envDays) ? envDays
939
+ : DEFAULT_PROCESSED_RETENTION_DAYS;
940
+ const terminalKeep = Number.isFinite(opts.terminalKeep) ? opts.terminalKeep : DEFAULT_TERMINAL_KEEP;
941
+ const queueMaxBytes = Number.isFinite(opts.queueMaxBytes) ? opts.queueMaxBytes : DEFAULT_QUEUE_MAX_BYTES;
942
+ const logDays = Number.isFinite(opts.logDays) ? opts.logDays : DEFAULT_LOG_RETENTION_DAYS;
943
+
944
+ // --- 1. processed/<date>/ dirs older than processedDays --------------------
945
+ try {
946
+ const cutoff = now - processedDays * 24 * 60 * 60 * 1000;
947
+ for (const ent of safeReaddir(paths.processed, { withFileTypes: true })) {
948
+ if (!ent.isDirectory()) continue;
949
+ // Date dirs are named YYYY-MM-DD. Parse to a UTC midnight timestamp;
950
+ // fall back to mtime for anything that doesn't match the pattern.
951
+ let dirMs;
952
+ const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(ent.name);
953
+ if (m) {
954
+ dirMs = Date.parse(`${ent.name}T00:00:00.000Z`);
955
+ }
956
+ const full = join(paths.processed, ent.name);
957
+ if (!Number.isFinite(dirMs)) {
958
+ try { dirMs = statSync(full).mtimeMs; } catch { continue; }
959
+ }
960
+ if (dirMs < cutoff) {
961
+ try {
962
+ rmSync(full, { recursive: true, force: true });
963
+ stats.processedDirsDeleted++;
964
+ } catch { stats.errors++; }
965
+ }
966
+ }
967
+ } catch { stats.errors++; }
968
+
969
+ // --- 2. cap dlq/ and failed/ to newest terminalKeep ------------------------
970
+ for (const [dir, key] of [[paths.dlq, "dlqDeleted"], [paths.failed, "failedDeleted"]]) {
971
+ try {
972
+ const files = safeReaddir(dir).filter((n) => n.endsWith(".json"));
973
+ if (files.length <= terminalKeep) continue;
974
+ // Sort by mtime ascending (oldest first); delete the overflow.
975
+ const withTimes = [];
976
+ for (const n of files) {
977
+ const full = join(dir, n);
978
+ let mtime = 0;
979
+ try { mtime = statSync(full).mtimeMs; } catch { continue; }
980
+ withTimes.push({ full, mtime });
981
+ }
982
+ withTimes.sort((a, b) => a.mtime - b.mtime);
983
+ const toDelete = withTimes.length - terminalKeep;
984
+ for (let i = 0; i < toDelete; i++) {
985
+ try {
986
+ unlinkSync(withTimes[i].full);
987
+ stats[key]++;
988
+ } catch { stats.errors++; }
989
+ }
990
+ } catch { stats.errors++; }
991
+ }
992
+
993
+ // --- 3. rotate/truncate queue.jsonl if oversized ---------------------------
994
+ try {
995
+ if (existsSync(paths.queueJsonl)) {
996
+ let size = 0;
997
+ try { size = statSync(paths.queueJsonl).size; } catch { size = 0; }
998
+ if (size > queueMaxBytes) {
999
+ // Keep the tail (most recent rows). Read the last ~half of the cap so
1000
+ // the file shrinks well below the threshold and recent audit context
1001
+ // survives. Best-effort; on read failure fall back to truncating.
1002
+ let tail = "";
1003
+ try {
1004
+ const buf = readFileSync(paths.queueJsonl, "utf-8");
1005
+ const keepBytes = Math.floor(queueMaxBytes / 2);
1006
+ tail = buf.slice(-keepBytes);
1007
+ // Drop a partial leading line so the file stays valid JSONL.
1008
+ const nl = tail.indexOf("\n");
1009
+ if (nl >= 0) tail = tail.slice(nl + 1);
1010
+ } catch { tail = ""; }
1011
+ try {
1012
+ if (tail) {
1013
+ writeFileSync(paths.queueJsonl, tail);
1014
+ } else {
1015
+ truncateSync(paths.queueJsonl, 0);
1016
+ }
1017
+ stats.queueRotated = true;
1018
+ } catch { stats.errors++; }
1019
+ }
1020
+ }
1021
+ } catch { stats.errors++; }
1022
+
1023
+ // --- 4. logs/cadence-bus/<date>.jsonl older than logDays -------------------
1024
+ try {
1025
+ const cutoff = now - logDays * 24 * 60 * 60 * 1000;
1026
+ for (const n of safeReaddir(paths.logsDir)) {
1027
+ const m = /^(\d{4}-\d{2}-\d{2})\.jsonl$/.exec(n);
1028
+ if (!m) continue;
1029
+ const fileMs = Date.parse(`${m[1]}T00:00:00.000Z`);
1030
+ if (!Number.isFinite(fileMs)) continue;
1031
+ if (fileMs < cutoff) {
1032
+ try {
1033
+ unlinkSync(join(paths.logsDir, n));
1034
+ stats.logsDeleted++;
1035
+ } catch { stats.errors++; }
1036
+ }
1037
+ }
1038
+ } catch { stats.errors++; }
1039
+
1040
+ // Audit row — best-effort, never throws (logBusEvent swallows its own I/O).
1041
+ if (
1042
+ stats.processedDirsDeleted || stats.dlqDeleted || stats.failedDeleted ||
1043
+ stats.queueRotated || stats.logsDeleted || stats.errors
1044
+ ) {
1045
+ logBusEvent(paths.agentRoot, { level: "info", stage: "retention_sweep", ...stats });
1046
+ }
1047
+ return stats;
1048
+ }
1049
+
1050
+ // ---------------------------------------------------------------------------
1051
+ // Heartbeat
1052
+ // ---------------------------------------------------------------------------
1053
+
1054
+ /**
1055
+ * Write the consumer's heartbeat state. Doctor / healthcheck reads this to
1056
+ * confirm the persistent main session is alive.
1057
+ */
1058
+ export function writeHealth(agentRoot, state = {}) {
1059
+ const paths = ensureBusDirs(agentRoot);
1060
+ const payload = {
1061
+ version: BUS_VERSION,
1062
+ ts: new Date().toISOString(),
1063
+ pid: process.pid,
1064
+ ...state,
1065
+ };
1066
+ try {
1067
+ writeJsonAtomic(paths.health, payload);
1068
+ } catch {
1069
+ /* best-effort */
1070
+ }
1071
+ return payload;
1072
+ }
1073
+
1074
+ /**
1075
+ * Read the consumer's heartbeat state. Returns null if missing or unreadable.
1076
+ */
1077
+ export function readHealth(agentRoot) {
1078
+ const paths = getBusPaths(agentRoot);
1079
+ if (!existsSync(paths.health)) return null;
1080
+ try {
1081
+ return JSON.parse(readFileSync(paths.health, "utf-8"));
1082
+ } catch {
1083
+ return null;
1084
+ }
1085
+ }
1086
+
1087
+ /**
1088
+ * Inspect bus depth — counts of events at each stage. Used by doctor and
1089
+ * tests; cheap enough to call frequently.
1090
+ */
1091
+ export function busDepth(agentRoot) {
1092
+ const paths = getBusPaths(agentRoot);
1093
+ const dirCount = (d) => existsSync(d) ? readdirSync(d).filter((n) => n.endsWith(".json")).length : 0;
1094
+ return {
1095
+ inbox: dirCount(paths.inbox),
1096
+ claimed: dirCount(paths.claimed),
1097
+ dlq: dirCount(paths.dlq),
1098
+ failed: dirCount(paths.failed),
1099
+ };
1100
+ }
1101
+
1102
+ // ---------------------------------------------------------------------------
1103
+ // CLI usability
1104
+ // ---------------------------------------------------------------------------
1105
+
1106
+ /**
1107
+ * Touch each bus directory so a fresh checkout/install has the expected
1108
+ * shape on disk. Equivalent to calling ensureBusDirs + readHealth probe
1109
+ * + writing a `.gitkeep` per dir so empty-bus repos still track structure.
1110
+ */
1111
+ export function bootstrapBus(agentRoot) {
1112
+ const paths = ensureBusDirs(agentRoot);
1113
+ for (const dir of [paths.inbox, paths.claimed, paths.processed, paths.failed, paths.dlq, paths.logsDir]) {
1114
+ const keep = join(dir, ".gitkeep");
1115
+ if (!existsSync(keep)) {
1116
+ try { closeSync(openSync(keep, "a")); } catch { /* ignore */ }
1117
+ }
1118
+ }
1119
+ return paths;
1120
+ }