@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,1121 @@
1
+ /**
2
+ * Maestro — Cadence Handler Registry
3
+ *
4
+ * Per-cadence policy for how the persistent main session services an event
5
+ * delivered onto the cadence bus.
6
+ *
7
+ * Three handler modes:
8
+ *
9
+ * "inline" — Handler runs entirely inside the persistent main Node
10
+ * process. No Claude Code spawned. Use for housekeeping
11
+ * (dashboard refresh, log rotation, queue sweeps that need
12
+ * no reasoning).
13
+ *
14
+ * "guarded" — Persistent process runs a lightweight pre-check (e.g.
15
+ * "is there anything in the inbox?") and only escalates to
16
+ * a sub-session if substantive work is required. If the
17
+ * pre-check says "no work", the tick completes inline.
18
+ *
19
+ * "escalate" — The cadence always needs Claude-grade reasoning or
20
+ * substantive drafting; the consumer spawns a sub-session
21
+ * with the trigger prompt under schedules/triggers/<name>.md.
22
+ * Reserved for substantive drafting, multi-step outreach,
23
+ * large audits, research, and explicit isolated audit
24
+ * boundaries.
25
+ *
26
+ * Handlers / guards receive ({ event, agentRoot, log }) and return a result
27
+ * object that becomes part of the processed event's archive record. They
28
+ * must not throw — return { ok:false, error } so the consumer can decide to
29
+ * retry or DLQ via its policy.
30
+ *
31
+ * Adding a new cadence: add an entry below. If you skip an entry the
32
+ * consumer falls back to escalate behaviour as long as a trigger prompt
33
+ * exists at schedules/triggers/<cadence>.md; otherwise it DLQ's the event
34
+ * with a clear error.
35
+ *
36
+ * Optional per-entry field `allowedTools` (audit H1): a string[] of the tool
37
+ * names the cadence's sub-session is permitted to use. This is consumed ONLY
38
+ * when the operator opts into scoped permissions
39
+ * (MAESTRO_SCOPED_PERMISSIONS=1) via lib/session-permissions.mjs; it is
40
+ * ignored otherwise (default behaviour stays --dangerously-skip-permissions).
41
+ * Cadences without the field fall back to a conservative read-only allowlist
42
+ * in scoped mode. Keep these lists as tight as the cadence genuinely needs.
43
+ */
44
+
45
+ import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync, renameSync } from "node:fs";
46
+ import { join, dirname } from "node:path";
47
+ import { fileURLToPath } from "node:url";
48
+ import { spawn } from "node:child_process";
49
+ import { recoverStaleClaims } from "../../lib/cadence-bus.mjs";
50
+ import { isQuarterStartMonth } from "../../lib/cadences.mjs";
51
+
52
+ const __dirname = dirname(fileURLToPath(import.meta.url));
53
+
54
+ // ---------------------------------------------------------------------------
55
+ // Pre-check helpers (shared across guarded handlers)
56
+ // ---------------------------------------------------------------------------
57
+
58
+ /**
59
+ * Count files in a directory matching an optional regex. Returns 0 if the
60
+ * directory doesn't exist (idiomatic for fresh agent repos).
61
+ */
62
+ function countFiles(dir, re) {
63
+ if (!existsSync(dir)) return 0;
64
+ try {
65
+ const all = readdirSync(dir);
66
+ return re ? all.filter((n) => re.test(n)).length : all.length;
67
+ } catch {
68
+ return 0;
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Return queue file names that contain at least one "- " entry. Avoids a
74
+ * YAML parser dep: cheap line-scan, plenty good enough for a guard.
75
+ */
76
+ function queuesWithWork(agentRoot) {
77
+ const dir = join(agentRoot, "state", "queues");
78
+ if (!existsSync(dir)) return [];
79
+ const out = [];
80
+ for (const name of readdirSync(dir)) {
81
+ if (!name.endsWith(".yaml")) continue;
82
+ try {
83
+ const body = readFileSync(join(dir, name), "utf-8");
84
+ if (/^- /m.test(body)) out.push(name);
85
+ } catch { /* ignore */ }
86
+ }
87
+ return out;
88
+ }
89
+
90
+ // ---------------------------------------------------------------------------
91
+ // Inline handlers
92
+ // ---------------------------------------------------------------------------
93
+
94
+ /**
95
+ * Heartbeat tick — purely internal. The consumer writes health.json on
96
+ * every cycle anyway; this handler exists so external schedulers (init,
97
+ * upgrade, doctor) can drop a heartbeat into the bus to verify end-to-end
98
+ * delivery.
99
+ */
100
+ async function handleHeartbeat({ event }) {
101
+ return {
102
+ ok: true,
103
+ decision: "inline",
104
+ cadence: event.cadence,
105
+ note: "heartbeat acknowledged",
106
+ };
107
+ }
108
+
109
+ /**
110
+ * Stale-claim recovery — the consumer already does this on startup and
111
+ * periodically; an explicit cadence event lets ops force a sweep without
112
+ * restarting the daemon.
113
+ */
114
+ async function handleStaleRecovery({ event, agentRoot }) {
115
+ const stats = recoverStaleClaims(agentRoot);
116
+ return { ok: true, decision: "inline", cadence: event.cadence, stats };
117
+ }
118
+
119
+ // ---------------------------------------------------------------------------
120
+ // Guarded handlers — cheap pre-check, escalate only when there's work
121
+ // ---------------------------------------------------------------------------
122
+
123
+ /**
124
+ * inbox-processor guard:
125
+ * The reactive daemon (agent-daemon.mjs) continuously polls inbound
126
+ * services + drains state/inbox/ + dispatches sessions via classifier
127
+ * and dispatcher. Running the cadence-bus inbox-processor cadence on
128
+ * top causes DOUBLE responses to the same message — observed in
129
+ * ravi-ai when a CEO DM got TWO substantive replies (one from each
130
+ * path) on 2026-05-12.
131
+ *
132
+ * So this guard now always returns inline. The reactive daemon owns
133
+ * inbox dispatch. Operators who want the cadence-only mode (no reactive
134
+ * daemon) can set INBOX_CADENCE_ESCALATE=1 in .env to flip behaviour.
135
+ *
136
+ * Pre-check is still done (count pending items) so the inline result
137
+ * carries useful telemetry, but escalation is suppressed.
138
+ */
139
+ async function guardInboxProcessor({ agentRoot }) {
140
+ const inboxRoot = join(agentRoot, "state", "inbox");
141
+ const sources = ["slack", "gmail", "calendar", "internal", "sms", "whatsapp"];
142
+ let pending = 0;
143
+ for (const src of sources) {
144
+ pending += countFiles(join(inboxRoot, src), /\.(json|ya?ml)$/i);
145
+ }
146
+ if (process.env.INBOX_CADENCE_ESCALATE === "1") {
147
+ if (pending === 0) {
148
+ return { ok: true, decision: "inline", reason: "inbox empty", pending: 0 };
149
+ }
150
+ return { ok: true, decision: "escalate", pending };
151
+ }
152
+ return {
153
+ ok: true,
154
+ decision: "inline",
155
+ reason: "reactive-daemon-owns-inbox",
156
+ pending,
157
+ note: "Set INBOX_CADENCE_ESCALATE=1 to override.",
158
+ };
159
+ }
160
+
161
+ /**
162
+ * backlog-executor guard:
163
+ * The reactive daemon (agent-daemon.mjs) already runs an internal
164
+ * backlog sweep every BACKLOG_INTERVAL (default 10min) which dispatches
165
+ * one session per top-priority queue item with proper post-completion
166
+ * cooldowns. Running the cadence-bus backlog-executor on top of that
167
+ * spawns DUPLICATE sessions for the same items — observed as 159
168
+ * redundant spawns / day in ravi-ai's logs.
169
+ *
170
+ * So this guard now always returns inline. The reactive daemon owns
171
+ * the backlog. Operators who prefer the cadence-only mode (no reactive
172
+ * daemon) can set BACKLOG_CADENCE_ESCALATE=1 in .env to flip behaviour.
173
+ */
174
+ async function guardBacklogExecutor({ agentRoot }) {
175
+ if (process.env.BACKLOG_CADENCE_ESCALATE === "1") {
176
+ const withWork = queuesWithWork(agentRoot);
177
+ if (withWork.length === 0) {
178
+ return { ok: true, decision: "inline", reason: "all queues empty" };
179
+ }
180
+ return { ok: true, decision: "escalate", queues_with_work: withWork };
181
+ }
182
+ return {
183
+ ok: true,
184
+ decision: "inline",
185
+ reason: "reactive-daemon-owns-backlog",
186
+ note: "Set BACKLOG_CADENCE_ESCALATE=1 to override.",
187
+ };
188
+ }
189
+
190
+ // --- skill-curator consolidation due-check (phase 2) -----------------------
191
+
192
+ const CONSOLIDATION_STATE_REL = join("state", "learning", "consolidation.json");
193
+ const CONSOLIDATE_WORKER_REL = join(__dirname, "..", "learning", "consolidate-skills.mjs");
194
+
195
+ /** Read the last consolidation timestamp (ms). Fail-open → 0 (never run). */
196
+ function readLastConsolidationAt(agentRoot) {
197
+ try {
198
+ const p = join(agentRoot, CONSOLIDATION_STATE_REL);
199
+ if (!existsSync(p)) return 0;
200
+ const obj = JSON.parse(readFileSync(p, "utf8"));
201
+ const t = Date.parse(obj && obj.lastConsolidationAt);
202
+ return Number.isFinite(t) ? t : 0;
203
+ } catch {
204
+ return 0;
205
+ }
206
+ }
207
+
208
+ /** Persist the last consolidation timestamp atomically. Fail-open. */
209
+ function writeLastConsolidationAt(agentRoot, nowMs) {
210
+ try {
211
+ const p = join(agentRoot, CONSOLIDATION_STATE_REL);
212
+ mkdirSync(dirname(p), { recursive: true });
213
+ const tmp = `${p}.${process.pid}.${Date.now()}.tmp`;
214
+ writeFileSync(tmp, JSON.stringify({ lastConsolidationAt: new Date(nowMs).toISOString() }, null, 2) + "\n");
215
+ renameSync(tmp, p);
216
+ } catch { /* fail-open */ }
217
+ }
218
+
219
+ /** Spawn the consolidation worker DETACHED + unref'd (mirrors hook-runner). Fail-open. */
220
+ function spawnConsolidationWorker(agentRoot) {
221
+ try {
222
+ const child = spawn(process.execPath, [CONSOLIDATE_WORKER_REL], {
223
+ detached: true,
224
+ stdio: "ignore",
225
+ env: { ...process.env, MAESTRO_CONSOLIDATE_ROOT: agentRoot },
226
+ });
227
+ child.unref();
228
+ return true;
229
+ } catch {
230
+ return false;
231
+ }
232
+ }
233
+
234
+ /**
235
+ * Decide whether an LLM umbrella-consolidation pass is DUE and, if so, spawn it
236
+ * DETACHED (never inline, never a nested sub-session). Gated on: config enabled,
237
+ * candidate count ≥ minSkills, and intervalDays since the last run. Returns a
238
+ * small descriptor for the guard's result. Injectable seams (tests):
239
+ * o.now — clock (ms)
240
+ * o.spawnFn — replace the detached spawner
241
+ * o.candidateCountFn — replace the candidate counter (avoids touching disk)
242
+ * Fail-open: any throw → { spawned:false, reason:"error" }.
243
+ */
244
+ async function maybeSpawnConsolidation(agentRoot, o = {}) {
245
+ try {
246
+ const nowMs = Number.isFinite(o.now) ? o.now : Date.now();
247
+ let minSkills = 5, intervalDays = 14, enabled = true;
248
+ try {
249
+ const cfgMod = await import("../../lib/learning/config.mjs");
250
+ const collMod = await import("../../lib/collective/config.mjs");
251
+ const coll = await collMod.loadConfig(agentRoot);
252
+ const cfg = await cfgMod.loadLearningConfig(agentRoot, coll);
253
+ const c = cfg && cfg.consolidation ? cfg.consolidation : {};
254
+ enabled = c.enabled !== false && cfg.enabled !== false;
255
+ if (Number.isFinite(c.minSkills)) minSkills = c.minSkills;
256
+ if (Number.isFinite(c.intervalDays)) intervalDays = c.intervalDays;
257
+ } catch { /* defaults */ }
258
+
259
+ if (!enabled) return { spawned: false, reason: "disabled" };
260
+
261
+ // (b) enough agent-authored active skills to be worth a clustering pass.
262
+ let candidateCount;
263
+ if (typeof o.candidateCountFn === "function") {
264
+ candidateCount = o.candidateCountFn(agentRoot);
265
+ } else {
266
+ const curator = await import("../../lib/learning/curator.mjs");
267
+ candidateCount = curator.selectConsolidationCandidates(agentRoot).length;
268
+ }
269
+ if (candidateCount < minSkills) return { spawned: false, reason: "below-threshold", candidateCount };
270
+
271
+ // (c) at least intervalDays since the last consolidation.
272
+ const last = readLastConsolidationAt(agentRoot);
273
+ const dueAt = last + intervalDays * 86400_000;
274
+ if (last && nowMs < dueAt) return { spawned: false, reason: "not-due", candidateCount };
275
+
276
+ const spawnFn = o.spawnFn || spawnConsolidationWorker;
277
+ const ok = spawnFn(agentRoot);
278
+ if (ok) writeLastConsolidationAt(agentRoot, nowMs); // mark even if the worker later no-ops (don't re-spawn each tick)
279
+ return { spawned: !!ok, reason: ok ? "spawned" : "spawn-failed", candidateCount };
280
+ } catch (err) {
281
+ return { spawned: false, reason: "error", error: err && err.message ? err.message : String(err) };
282
+ }
283
+ }
284
+
285
+ /**
286
+ * skill-curator guard (WS3): deterministic lifecycle housekeeping for the
287
+ * agent's self-authored skills, PLUS a periodic LLM umbrella-consolidation pass.
288
+ *
289
+ * Phase 1 (inline, no Claude): the cheap pre-check asks the sidecar whether ANY
290
+ * non-pinned skill has crossed a stale (30d) or archive (90d) boundary; if so we
291
+ * run the pure curator transitions INLINE (moving a .md into _archive/ and
292
+ * dropping it from plugin.json needs no reasoning).
293
+ *
294
+ * Phase 2 (detached worker, NOT a sub-session): when consolidation is DUE
295
+ * (enabled + ≥ minSkills agent-authored active skills + ≥ intervalDays since the
296
+ * last pass, tracked in state/learning/consolidation.json) we spawn
297
+ * scripts/learning/consolidate-skills.mjs as a DETACHED, unref'd child — the same
298
+ * fire-and-forget pattern as the collective capture worker. We do NOT block the
299
+ * consumer and do NOT escalate to a managed sub-session; the guard still returns
300
+ * `inline`. The LLM call lives entirely inside that detached worker.
301
+ */
302
+ async function guardSkillCurator({ agentRoot, log }, opts = {}) {
303
+ try {
304
+ const curator = await import("../../lib/learning/curator.mjs");
305
+ let staleDays, archiveDays;
306
+ try {
307
+ const cfgMod = await import("../../lib/learning/config.mjs");
308
+ const collMod = await import("../../lib/collective/config.mjs");
309
+ const coll = await collMod.loadConfig(agentRoot);
310
+ const cfg = await cfgMod.loadLearningConfig(agentRoot, coll);
311
+ staleDays = cfg.staleDays; archiveDays = cfg.archiveDays;
312
+ } catch { /* fall back to curator defaults */ }
313
+
314
+ // Daily catch-up for the verbatim transcript store (the Stop-hook ingests
315
+ // per-session; this sweeps anything that machine missed). Best-effort.
316
+ try {
317
+ const sidx = await import("../../lib/learning/session-index.mjs");
318
+ await sidx.ingestAll({ agentRoot });
319
+ } catch { /* on-demand store; a missed sweep is harmless */ }
320
+
321
+ // Phase 1 — deterministic lifecycle transitions (inline).
322
+ let transitioned = [], archived = [];
323
+ if (curator.hasPendingTransitions(agentRoot, { staleDays, archiveDays })) {
324
+ const res = curator.runCurator({ agentRoot, staleDays, archiveDays });
325
+ transitioned = res.transitioned; archived = res.archived;
326
+ if (typeof log === "function" && (transitioned.length || archived.length)) {
327
+ log("info", `[skill-curator] transitioned ${transitioned.length}, archived ${archived.length}`);
328
+ }
329
+ }
330
+
331
+ // Phase 2 — LLM umbrella consolidation, spawned DETACHED if due. Never blocks
332
+ // the consumer, never escalates to a sub-session.
333
+ const consolidation = await maybeSpawnConsolidation(agentRoot, opts.consolidation || {});
334
+ if (consolidation.spawned && typeof log === "function") {
335
+ log("info", `[skill-curator] umbrella consolidation spawned (detached); ${consolidation.candidateCount} candidate(s)`);
336
+ }
337
+
338
+ return {
339
+ ok: true,
340
+ decision: "inline",
341
+ transitioned,
342
+ archived,
343
+ consolidation: consolidation.spawned ? "spawned" : consolidation.reason,
344
+ };
345
+ } catch (err) {
346
+ // Fail-open: housekeeping must never DLQ.
347
+ return { ok: true, decision: "inline", reason: "curator-error", error: err && err.message ? err.message : String(err) };
348
+ }
349
+ }
350
+
351
+ /**
352
+ * meeting-prep guard: only escalate if today's calendar inbox has any
353
+ * unprocessed event files. The cheap signal is just checking
354
+ * state/inbox/calendar/ for unprocessed entries.
355
+ */
356
+ async function guardMeetingPrep({ agentRoot }) {
357
+ const dir = join(agentRoot, "state", "inbox", "calendar");
358
+ const pending = countFiles(dir, /\.(json|ya?ml)$/i);
359
+ if (pending === 0) {
360
+ return { ok: true, decision: "inline", reason: "no pending calendar events" };
361
+ }
362
+ return { ok: true, decision: "escalate", pending };
363
+ }
364
+
365
+ /**
366
+ * quarterly-self-assessment guard (Phase 0 item 0.14): the plist fires on the
367
+ * 1st of EVERY month, but the report is QUARTERLY. Escalate to the Claude
368
+ * sub-session only in a quarter-start month (Jan/Apr/Jul/Oct); otherwise the
369
+ * tick completes inline with no spawn — preventing the 12×/year (3× cost)
370
+ * misfire confirmed by the audit. Pure delegation to isQuarterStartMonth so the
371
+ * month logic stays unit-tested in lib/cadences.mjs.
372
+ */
373
+ async function guardQuarterlySelfAssessment({ now } = {}) {
374
+ const when = now instanceof Date ? now : new Date();
375
+ if (!isQuarterStartMonth(when)) {
376
+ return { ok: true, decision: "inline", reason: "not a quarter-start month" };
377
+ }
378
+ return { ok: true, decision: "escalate", reason: "quarter start" };
379
+ }
380
+
381
+ // ---------------------------------------------------------------------------
382
+ // Economics / scheduling cadences (WS-economics)
383
+ //
384
+ // These wire already-built deterministic economics + scheduling functions onto
385
+ // the cadence bus. They run INLINE in the persistent daemon (no LLM spawn) —
386
+ // except dynamic-jobs, which is a GUARDED escalate-when-due primitive. Every one
387
+ // is fail-open: a throw never DLQs the tick, it returns { ok:false, error }.
388
+ // ---------------------------------------------------------------------------
389
+
390
+ /** Read config/agent.json fail-open (for the org legs). Returns null on any error. */
391
+ function loadAgentCfg(agentRoot) {
392
+ try {
393
+ const p = join(agentRoot, "config", "agent.json");
394
+ if (!existsSync(p)) return null;
395
+ return JSON.parse(readFileSync(p, "utf8"));
396
+ } catch {
397
+ return null;
398
+ }
399
+ }
400
+
401
+ /**
402
+ * nightly-cost-reconcile (inline): three-way spend reconcile of today's local
403
+ * router ledger against the Anthropic Admin Cost API. We pass NO fetchImpl, so
404
+ * the provider leg is skipped and reconcile fails open (driftPct null, never an
405
+ * error). A real deployment injects the Admin API fetch in prod. Drift > 10%
406
+ * (`flagged`) logs a warning. The result (drift %) goes in the inline result.
407
+ * The lib fn is injectable via `opts.reconcileImpl` for tests.
408
+ */
409
+ async function handleNightlyCostReconcile({ event, agentRoot, log }, opts = {}) {
410
+ try {
411
+ const reconcile =
412
+ opts.reconcileImpl ||
413
+ (await import("../../lib/model-router/reconcile.mjs")).reconcileDaily;
414
+ // fetchImpl intentionally undefined → reconcile fails open (provider null).
415
+ const res = await reconcile({ agentRoot, fetchImpl: opts.fetchImpl });
416
+ if (res && res.flagged && typeof log === "function") {
417
+ log("warn", `[nightly-cost-reconcile] drift ${res.driftPct}% exceeds ${res.thresholdPct}% (local $${res.localUSD} vs provider $${res.providerUSD})`);
418
+ }
419
+ return {
420
+ ok: true,
421
+ decision: "inline",
422
+ cadence: event.cadence,
423
+ day: res?.day ?? null,
424
+ driftPct: res?.driftPct ?? null,
425
+ flagged: !!(res && res.flagged),
426
+ localUSD: res?.localUSD ?? null,
427
+ providerUSD: res?.providerUSD ?? null,
428
+ };
429
+ } catch (err) {
430
+ return { ok: false, decision: "inline", cadence: event.cadence, error: err && err.message ? err.message : String(err) };
431
+ }
432
+ }
433
+
434
+ /**
435
+ * fleet-cost-digest (inline): build the daily fleet LLM-spend digest from the
436
+ * local router ledger (org rollup leg fails open when Cohort is disabled). The
437
+ * digest summary (total / sessions / cache-read KPI / source) goes in the inline
438
+ * result. Injectable via `opts.digestImpl` for tests.
439
+ */
440
+ async function handleFleetCostDigest({ event, agentRoot, log }, opts = {}) {
441
+ try {
442
+ const digestFn =
443
+ opts.digestImpl ||
444
+ (await import("../../scripts/cost/fleet-digest.mjs")).fleetDigest;
445
+ const cfg = opts.cfg !== undefined ? opts.cfg : loadAgentCfg(agentRoot);
446
+ const d = await digestFn({ agentRoot, cfg });
447
+ if (d && d.cacheReadKpiOk === false && typeof log === "function") {
448
+ log("warn", `[fleet-cost-digest] cache-read ratio ${(d.cacheReadRatio * 100).toFixed(1)}% below 60% target`);
449
+ }
450
+ return {
451
+ ok: true,
452
+ decision: "inline",
453
+ cadence: event.cadence,
454
+ day: d?.day ?? null,
455
+ totalUSD: d?.totalUSD ?? null,
456
+ sessions: d?.sessions ?? null,
457
+ cacheReadRatio: d?.cacheReadRatio ?? null,
458
+ cacheReadKpiOk: d?.cacheReadKpiOk ?? null,
459
+ source: d?.source ?? null,
460
+ summary: d ? `Fleet LLM spend ${d.day} (${d.source}): $${d.totalUSD} over ${d.sessions} session(s), cache-read ${(d.cacheReadRatio * 100).toFixed(1)}%` : null,
461
+ };
462
+ } catch (err) {
463
+ return { ok: false, decision: "inline", cadence: event.cadence, error: err && err.message ? err.message : String(err) };
464
+ }
465
+ }
466
+
467
+ /**
468
+ * org-cost-sync (inline): push today's router ledger rows to the Cohort org
469
+ * rollup via cost.report, batched + idempotent. No-op / fail-open when org
470
+ * (Cohort) is disabled (enabled:false in the result). Injectable via
471
+ * `opts.reportImpl` for tests.
472
+ */
473
+ async function handleOrgCostSync({ event, agentRoot }, opts = {}) {
474
+ try {
475
+ const reportFn =
476
+ opts.reportImpl ||
477
+ (await import("../../lib/org/cost-sync.mjs")).reportLedger;
478
+ const cfg = opts.cfg !== undefined ? opts.cfg : loadAgentCfg(agentRoot);
479
+ const res = await reportFn({ agentRoot, cfg, fetchImpl: opts.fetchImpl });
480
+ return {
481
+ ok: true,
482
+ decision: "inline",
483
+ cadence: event.cadence,
484
+ enabled: !!(res && res.enabled),
485
+ rowsRead: res?.rowsRead ?? 0,
486
+ reported: res?.reported ?? 0,
487
+ failed: res?.failed ?? 0,
488
+ localUSD: res?.localUSD ?? 0,
489
+ };
490
+ } catch (err) {
491
+ return { ok: false, decision: "inline", cadence: event.cadence, error: err && err.message ? err.message : String(err) };
492
+ }
493
+ }
494
+
495
+ /**
496
+ * monitoring-alerts (inline): gather the diagnostics counters, today's cost
497
+ * ledger, peer presence, daemon health + backup freshness, evaluate the
498
+ * behavioral alert rules, and emit any firing alerts to the configured webhook
499
+ * (config/alerts.yaml `alerts.webhook`) — else log them. Deterministic, no LLM.
500
+ * Fail-open + fully injectable for tests. This is what makes lib/diagnostics/
501
+ * alerts.mjs actually fire on a schedule.
502
+ */
503
+ async function handleMonitoringAlerts({ event, agentRoot, log }, opts = {}) {
504
+ try {
505
+ const A = opts.alertsImpl || (await import("../../lib/diagnostics/alerts.mjs"));
506
+ const snapshot = opts.snapshot !== undefined
507
+ ? opts.snapshot
508
+ : (await import("../../lib/diagnostics/counters.mjs")).snapshot({ agentRoot });
509
+ const ledger = opts.ledger !== undefined
510
+ ? opts.ledger
511
+ : (await import("../../lib/model-router/ledger.mjs")).sumLedgerToday({ agentRoot });
512
+ const presence = opts.presence !== undefined
513
+ ? opts.presence
514
+ : (await import("../../lib/collective/presence.mjs")).list(agentRoot);
515
+ const health = opts.health !== undefined ? opts.health : recoverStaleClaims && readDaemonHealth(agentRoot);
516
+ const cfg = opts.cfg !== undefined ? opts.cfg : loadAgentCfg(agentRoot);
517
+ const now = opts.now || Date.now();
518
+
519
+ const alerts = A.evaluateAlerts({ snapshot, ledger, presence, health, thresholds: opts.thresholds, now }) || [];
520
+
521
+ const sinkUrl = cfg && cfg.alerts && cfg.alerts.webhook;
522
+ const sink = opts.sink || (async (alert) => {
523
+ if (sinkUrl) {
524
+ const { fetchWithTimeout } = await import("../../lib/util/fetch-timeout.mjs");
525
+ await fetchWithTimeout(sinkUrl, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(alert) }, 8000);
526
+ } else if (typeof log === "function") {
527
+ log("warn", `[alert:${alert.severity}] ${alert.id} — ${alert.detail}`);
528
+ }
529
+ });
530
+ const res = alerts.length ? await A.emitAlerts(alerts, { sink }) : { attempted: 0, delivered: 0, failed: 0 };
531
+ return { ok: true, decision: "inline", cadence: event.cadence, alerts: alerts.length, delivered: res.delivered ?? 0, severities: alerts.map((a) => a.severity) };
532
+ } catch (err) {
533
+ return { ok: false, decision: "inline", cadence: event.cadence, error: err && err.message ? err.message : String(err) };
534
+ }
535
+ }
536
+
537
+ /** Best-effort daemon-health freshness for the alert rules (never throws). */
538
+ function readDaemonHealth(agentRoot) {
539
+ try {
540
+ const p = join(agentRoot, "state", "cadence-bus", "health.json");
541
+ if (!existsSync(p)) return { daemonDown: true };
542
+ const h = JSON.parse(readFileSync(p, "utf-8"));
543
+ return { ts: h.ts || h.updatedAt || null, daemonDown: false };
544
+ } catch { return { daemonDown: true }; }
545
+ }
546
+
547
+ /**
548
+ * dynamic-jobs guard (guarded): ask dueJobs() which agent-created jobs are due
549
+ * right now. None due → inline (cheap, write-free). Some due → escalate, with
550
+ * the due jobs carried in the result so the spawned session can action each
551
+ * reminder/job payload. dueJobs ADVANCES the store (coalesces recurring, expires
552
+ * stale one-offs) as a side effect — that is the intended consume. Injectable via
553
+ * `opts.dueJobsImpl` + `opts.now` for tests. Fail-open: a throw stays inline.
554
+ */
555
+ async function guardDynamicJobs({ event, agentRoot }, opts = {}) {
556
+ try {
557
+ const due =
558
+ opts.dueJobsImpl ||
559
+ (await import("../../lib/scheduling/dynamic-jobs.mjs")).dueJobs;
560
+ const jobs = due(agentRoot, { now: opts.now }) || [];
561
+ if (jobs.length === 0) {
562
+ return { ok: true, decision: "inline", cadence: event.cadence, reason: "no jobs due", due: 0 };
563
+ }
564
+ return { ok: true, decision: "escalate", cadence: event.cadence, reason: `${jobs.length} job(s) due`, due: jobs.length, jobs };
565
+ } catch (err) {
566
+ return { ok: false, decision: "inline", cadence: event.cadence, error: err && err.message ? err.message : String(err) };
567
+ }
568
+ }
569
+
570
+ // ---------------------------------------------------------------------------
571
+ // Org messaging inbound (SP10) — pull Cohort messages/@mentions/call-invites
572
+ // ---------------------------------------------------------------------------
573
+
574
+ const MESSAGING_CURSOR_REL = join("state", "messaging", "inbound-cursor.json");
575
+
576
+ /** Read the org-messaging inbound cursor (seq). Fail-open → 0 (from genesis). */
577
+ function readMessagingCursor(agentRoot) {
578
+ try {
579
+ const p = join(agentRoot, MESSAGING_CURSOR_REL);
580
+ if (!existsSync(p)) return 0;
581
+ const obj = JSON.parse(readFileSync(p, "utf8"));
582
+ const c = Number(obj && obj.cursor);
583
+ return Number.isFinite(c) ? c : 0;
584
+ } catch {
585
+ return 0;
586
+ }
587
+ }
588
+
589
+ /** Persist the org-messaging inbound cursor atomically (temp → rename). Fail-open. */
590
+ function writeMessagingCursor(agentRoot, cursor) {
591
+ try {
592
+ const p = join(agentRoot, MESSAGING_CURSOR_REL);
593
+ mkdirSync(dirname(p), { recursive: true });
594
+ const tmp = `${p}.${process.pid}.${Date.now()}.tmp`;
595
+ writeFileSync(tmp, JSON.stringify({ cursor, updatedAt: new Date().toISOString() }, null, 2) + "\n");
596
+ renameSync(tmp, p);
597
+ } catch { /* fail-open */ }
598
+ }
599
+
600
+ /**
601
+ * messaging-inbound guard (guarded): poll the Cohort messaging/calling event
602
+ * stream for new messages / @mentions / call-invites DIRECTED AT this
603
+ * agent (lib/org/messaging.pullInbound), and ROUTE each one into the existing
604
+ * daemon inbox pipeline by writing a legacy inbox item under
605
+ * state/inbox/cohort/ — so the agent reacts to a Cohort @mention exactly like
606
+ * a Slack/Telegram one (classifier → directed-gate → responder → dispatcher).
607
+ *
608
+ * The reactive daemon OWNS inbox dispatch, so the default decision is `inline`
609
+ * (we route the items but don't escalate — escalating would double-respond, the
610
+ * same hazard guardInboxProcessor documents). Operators running cadence-only mode
611
+ * (no reactive daemon) can set MESSAGING_INBOUND_ESCALATE=1 to escalate to the
612
+ * trigger when new directed items arrive.
613
+ *
614
+ * Cursor is advanced + persisted atomically AFTER the items are written, so a
615
+ * crash re-pulls (at-least-once into the dedup-guarded writeInboxItem) rather
616
+ * than dropping. Fail-open: a disabled/unreachable org or any throw stays inline
617
+ * with 0 routed (org messaging is fail-open at runtime).
618
+ *
619
+ * Injectable seams (tests): o.pullImpl, o.writeImpl, o.toInboxImpl, o.cfg,
620
+ * o.fetchImpl, o.now — so tests never touch the network or real config.
621
+ */
622
+ async function guardMessagingInbound({ event, agentRoot, log }, opts = {}) {
623
+ try {
624
+ const cfg = opts.cfg !== undefined ? opts.cfg : loadAgentCfg(agentRoot);
625
+ // No org config at all → nothing to pull. Stay inline (fail-open, no spawn).
626
+ if (!cfg || !cfg.org || !cfg.org.cohort) {
627
+ return { ok: true, decision: "inline", cadence: event.cadence, reason: "org messaging not configured", routed: 0 };
628
+ }
629
+
630
+ const pull = opts.pullImpl || (await import("../../lib/org/messaging.mjs")).pullInbound;
631
+ const eventToInboxItem =
632
+ opts.toInboxImpl || (await import("../../lib/channels/inbox-item.mjs")).eventToInboxItem;
633
+ const writeInboxItem = opts.writeImpl || (await import("../poller/utils.mjs")).writeInboxItem;
634
+
635
+ const cursor = readMessagingCursor(agentRoot);
636
+ const res = await pull({ cfg, cursor, fetchImpl: opts.fetchImpl });
637
+ const events = (res && Array.isArray(res.events)) ? res.events : [];
638
+
639
+ let routed = 0;
640
+ for (const ev of events) {
641
+ try {
642
+ const item = eventToInboxItem(ev);
643
+ writeInboxItem("cohort", item, agentRoot);
644
+ routed += 1;
645
+ } catch { /* one bad event must not drop the rest */ }
646
+ }
647
+
648
+ // Advance the cursor AFTER writing (at-least-once; writeInboxItem dedupes).
649
+ const next = res && res.nextCursor != null ? res.nextCursor : cursor;
650
+ if (next !== cursor) writeMessagingCursor(agentRoot, next);
651
+
652
+ if (routed && typeof log === "function") {
653
+ log("info", `[messaging-inbound] routed ${routed} Cohort item(s) into the inbox pipeline`);
654
+ }
655
+
656
+ if (process.env.MESSAGING_INBOUND_ESCALATE === "1" && routed > 0) {
657
+ return { ok: true, decision: "escalate", cadence: event.cadence, reason: `${routed} directed item(s)`, routed };
658
+ }
659
+ return {
660
+ ok: true,
661
+ decision: "inline",
662
+ cadence: event.cadence,
663
+ reason: routed ? "routed to inbox (reactive-daemon-owns-dispatch)" : "no new directed items",
664
+ routed,
665
+ note: routed ? "Set MESSAGING_INBOUND_ESCALATE=1 to escalate instead of routing to the inbox." : undefined,
666
+ };
667
+ } catch (err) {
668
+ return { ok: true, decision: "inline", cadence: event.cadence, reason: "messaging-inbound-error", routed: 0, error: err && err.message ? err.message : String(err) };
669
+ }
670
+ }
671
+
672
+ // ---------------------------------------------------------------------------
673
+ // Directory hygiene + brand steward (2026-07 Directory & Design)
674
+ // ---------------------------------------------------------------------------
675
+
676
+ /**
677
+ * Resolve { base, token } for an org call from an agent cfg, or null when the
678
+ * integration is off/unconfigured. Mirrors lib/org/messaging.resolveOrg — the
679
+ * SAME isEnabled + configFromAgent chain, so a disabled agent is inert here for
680
+ * exactly the reason it is inert everywhere else. Never throws.
681
+ */
682
+ async function resolveOrgCall(cfg) {
683
+ try {
684
+ const { isEnabled, configFromAgent } = await import("../../lib/org/client.mjs");
685
+ if (!isEnabled(cfg)) return null;
686
+ const c = configFromAgent(cfg);
687
+ return c.base ? { base: c.base, token: c.token, orgId: c.orgId } : null;
688
+ } catch {
689
+ return null;
690
+ }
691
+ }
692
+
693
+ /**
694
+ * directory-hygiene guard (guarded): a CHEAP pre-check over the Directory's two
695
+ * human review queues — pending captures and pending duplicate candidates. If
696
+ * both are empty the tick completes inline with no Claude spawn; if either has
697
+ * work, escalate to schedules/triggers/directory-hygiene.md, where the session
698
+ * proposes captures from signatures/rosters/bank feeds, enriches channels,
699
+ * re-verifies anything unseen for >90d, and flags duplicates.
700
+ *
701
+ * The guard itself only ever READS. Everything the escalated session does is a
702
+ * PROPOSAL: merges, awareness changes, capture resolution and archives are
703
+ * human-gated server-side (DECISIONS D-7), so this cadence cannot mutate a
704
+ * record even if the model tried.
705
+ *
706
+ * Awareness note: the queue counts below are awareness-scoped like every other
707
+ * Directory read. A zero count means "nothing this seat can see", never "nothing
708
+ * exists" — which is why a zero-count tick stays inline rather than reporting
709
+ * a clean queue.
710
+ *
711
+ * Injectable seams (tests): o.cfg, o.callImpl, o.fetchImpl — so tests never
712
+ * touch the network or real config. Fail-open: an unreachable/disabled org, a
713
+ * transport error, or ANY throw stays inline with 0 pending.
714
+ */
715
+ async function guardDirectoryHygiene({ event, agentRoot, log }, opts = {}) {
716
+ const cadence = event && event.cadence ? event.cadence : "directory-hygiene";
717
+ try {
718
+ const cfg = opts.cfg !== undefined ? opts.cfg : loadAgentCfg(agentRoot);
719
+ const org = await resolveOrgCall(cfg);
720
+ if (!org) {
721
+ return { ok: true, decision: "inline", cadence, reason: "org directory not configured", captures: 0, duplicates: 0 };
722
+ }
723
+
724
+ const call = opts.callImpl || (await import("../../lib/org/client.mjs")).call;
725
+ const o = { base: org.base, token: org.token, orgId: org.orgId, fetchImpl: opts.fetchImpl };
726
+ const [capFrame, dupFrame] = await Promise.all([
727
+ call("directory.listCaptures", { status: "PENDING", limit: 50 }, o),
728
+ call("directory.listMerges", { status: "PENDING", limit: 50 }, o),
729
+ ]);
730
+
731
+ const rows = (frame, ...keys) => {
732
+ if (!frame || !frame.ok || !frame.result) return [];
733
+ const r = frame.result;
734
+ if (Array.isArray(r)) return r;
735
+ for (const k of keys) if (Array.isArray(r[k])) return r[k];
736
+ return [];
737
+ };
738
+ const captures = rows(capFrame, "captures", "items").length;
739
+ const duplicates = rows(dupFrame, "candidates", "merges", "items").length;
740
+ const pending = captures + duplicates;
741
+
742
+ if (pending === 0) {
743
+ return { ok: true, decision: "inline", cadence, reason: "review queues clear", captures: 0, duplicates: 0 };
744
+ }
745
+ if (typeof log === "function") {
746
+ log("info", `[directory-hygiene] ${captures} capture(s) + ${duplicates} duplicate(s) pending`);
747
+ }
748
+ return {
749
+ ok: true,
750
+ decision: "escalate",
751
+ cadence,
752
+ reason: `${captures} capture(s), ${duplicates} duplicate(s) pending`,
753
+ captures,
754
+ duplicates,
755
+ pending,
756
+ };
757
+ } catch (err) {
758
+ return {
759
+ ok: true,
760
+ decision: "inline",
761
+ cadence,
762
+ reason: "directory-hygiene-error",
763
+ captures: 0,
764
+ duplicates: 0,
765
+ error: err && err.message ? err.message : String(err),
766
+ };
767
+ }
768
+ }
769
+
770
+ const BRAND_CURSOR_REL = join("state", "org", "brand-steward-cursor.json");
771
+
772
+ /** Read the brand-steward event cursor (seq). Fail-open → 0 (from genesis). */
773
+ function readBrandCursor(agentRoot) {
774
+ try {
775
+ const p = join(agentRoot, BRAND_CURSOR_REL);
776
+ if (!existsSync(p)) return 0;
777
+ const c = Number(JSON.parse(readFileSync(p, "utf8")).cursor);
778
+ return Number.isFinite(c) ? c : 0;
779
+ } catch {
780
+ return 0;
781
+ }
782
+ }
783
+
784
+ /** Persist the brand-steward cursor atomically (temp → rename). Fail-open. */
785
+ function writeBrandCursor(agentRoot, cursor) {
786
+ try {
787
+ const p = join(agentRoot, BRAND_CURSOR_REL);
788
+ mkdirSync(dirname(p), { recursive: true });
789
+ const tmp = `${p}.${process.pid}.${Date.now()}.tmp`;
790
+ writeFileSync(tmp, JSON.stringify({ cursor, updatedAt: new Date().toISOString() }, null, 2) + "\n");
791
+ renameSync(tmp, p);
792
+ } catch { /* fail-open */ }
793
+ }
794
+
795
+ /** Brand events that mean "the foundation moved; downstream assets are stale". */
796
+ const BRAND_SAVE_KINDS = new Set(["version.saved", "foundation.updated", "version.head_moved"]);
797
+
798
+ /**
799
+ * brand-steward guard (guarded): tail the org event chain for a brand
800
+ * foundation change (`branding/version.saved` and its siblings). A foundation
801
+ * save is what makes rendered imagery and templates STALE, so this is
802
+ * event-driven rather than time-driven — it escalates ONLY on a real save, and
803
+ * otherwise costs one cheap GET.
804
+ *
805
+ * On escalation the session runs schedules/triggers/brand-steward.md: quote the
806
+ * stale slots (`design_generate_image` with `budgetCents: 0`), re-render what is
807
+ * worth re-rendering inside the budget, and post a working-now status line
808
+ * carrying the actual spend.
809
+ *
810
+ * The cursor advances AFTER the decision, so a crash re-reads the same window
811
+ * (at-least-once) rather than silently skipping a brand save. `hq` ignores a
812
+ * `family=` query param on the events read, so the family/kind filter is applied
813
+ * client-side here — the same posture as lib/org/messaging.pullInbound.
814
+ *
815
+ * Injectable seams (tests): o.cfg, o.readImpl, o.fetchImpl. Fail-open in every
816
+ * direction; never throws.
817
+ */
818
+ async function guardBrandSteward({ event, agentRoot, log }, opts = {}) {
819
+ const cadence = event && event.cadence ? event.cadence : "brand-steward";
820
+ try {
821
+ const cfg = opts.cfg !== undefined ? opts.cfg : loadAgentCfg(agentRoot);
822
+ const org = await resolveOrgCall(cfg);
823
+ if (!org) {
824
+ return { ok: true, decision: "inline", cadence, reason: "org design not configured", saves: 0 };
825
+ }
826
+
827
+ const read = opts.readImpl || (await import("../../lib/org/client.mjs")).read;
828
+ const cursor = readBrandCursor(agentRoot);
829
+ const r = await read(
830
+ `events?cursor=${encodeURIComponent(cursor)}&limit=200&family=branding`,
831
+ { base: org.base, token: org.token, orgId: org.orgId, fetchImpl: opts.fetchImpl },
832
+ );
833
+ if (!r || !r.ok || !r.payload) {
834
+ return { ok: true, decision: "inline", cadence, reason: "events unreadable", saves: 0 };
835
+ }
836
+
837
+ const raw = Array.isArray(r.payload)
838
+ ? r.payload
839
+ : Array.isArray(r.payload.events)
840
+ ? r.payload.events
841
+ : [];
842
+ let maxSeq = cursor;
843
+ const saves = [];
844
+ for (const ev of raw) {
845
+ const seq = Number(ev && ev.seq);
846
+ if (Number.isFinite(seq)) maxSeq = Math.max(Number(maxSeq) || 0, seq);
847
+ // hq ignores family= on the events read, so filter here.
848
+ if (!ev || ev.family !== "branding") continue;
849
+ if (!BRAND_SAVE_KINDS.has(String(ev.kind || ""))) continue;
850
+ saves.push({ seq, kind: ev.kind, at: ev.at ?? null });
851
+ }
852
+
853
+ if (raw.length && maxSeq !== cursor) writeBrandCursor(agentRoot, maxSeq);
854
+
855
+ if (saves.length === 0) {
856
+ return { ok: true, decision: "inline", cadence, reason: "no brand save since last tick", saves: 0, cursor: maxSeq };
857
+ }
858
+ if (typeof log === "function") {
859
+ log("info", `[brand-steward] ${saves.length} brand foundation change(s) — imagery/templates may be stale`);
860
+ }
861
+ return {
862
+ ok: true,
863
+ decision: "escalate",
864
+ cadence,
865
+ reason: `${saves.length} brand foundation change(s) since seq ${cursor}`,
866
+ saves: saves.length,
867
+ latestKind: saves[saves.length - 1].kind,
868
+ cursor: maxSeq,
869
+ };
870
+ } catch (err) {
871
+ return {
872
+ ok: true,
873
+ decision: "inline",
874
+ cadence,
875
+ reason: "brand-steward-error",
876
+ saves: 0,
877
+ error: err && err.message ? err.message : String(err),
878
+ };
879
+ }
880
+ }
881
+
882
+ // ---------------------------------------------------------------------------
883
+ // Registry
884
+ // ---------------------------------------------------------------------------
885
+
886
+ export const CADENCE_REGISTRY = {
887
+ // Internal housekeeping (always inline)
888
+ "cadence-bus-heartbeat": {
889
+ mode: "inline",
890
+ handler: handleHeartbeat,
891
+ description: "Probe tick used by doctor / upgrade to verify end-to-end delivery.",
892
+ },
893
+ "cadence-bus-recovery": {
894
+ mode: "inline",
895
+ handler: handleStaleRecovery,
896
+ description: "On-demand stale-claim recovery.",
897
+ },
898
+
899
+ // Operational cadences with cheap pre-checks
900
+ "inbox-processor": {
901
+ mode: "guarded",
902
+ guard: guardInboxProcessor,
903
+ prompt: "schedules/triggers/inbox-processor.md",
904
+ description: "Classify / route inbound items if any are waiting.",
905
+ },
906
+ "backlog-executor": {
907
+ mode: "guarded",
908
+ guard: guardBacklogExecutor,
909
+ prompt: "schedules/triggers/backlog-executor.md",
910
+ description: "Execute the next slice of the proactive backlog if queues are non-empty.",
911
+ },
912
+ "skill-curator": {
913
+ mode: "guarded",
914
+ guard: guardSkillCurator,
915
+ // No prompt file and never escalates to a sub-session. Phase 1 (lifecycle
916
+ // transitions) runs inline; phase 2 (LLM umbrella consolidation) runs as a
917
+ // DETACHED worker (scripts/learning/consolidate-skills.mjs) spawned from the
918
+ // guard when due — the guard still returns `inline`, so the consumer is
919
+ // never blocked and no managed sub-session is created.
920
+ description: "Lifecycle housekeeping (active→stale→archived) inline + periodic LLM umbrella consolidation via a detached worker. Never escalates.",
921
+ },
922
+ "meeting-prep": {
923
+ mode: "guarded",
924
+ guard: guardMeetingPrep,
925
+ prompt: "schedules/triggers/meeting-prep.md",
926
+ description: "Prepare for upcoming meetings when there are unprocessed calendar items.",
927
+ // Scoped-permissions allowlist (audit H1). Meeting prep reads the calendar
928
+ // + recent context and drafts a brief; it does not need shell/write/send
929
+ // primitives. Only consulted when MAESTRO_SCOPED_PERMISSIONS=1.
930
+ allowedTools: ["Read", "Grep", "Glob", "LS", "search_calendar", "search_meetings"],
931
+ },
932
+
933
+ // Always-escalate (substantive reasoning / drafting required)
934
+ "meeting-action-capture": {
935
+ mode: "escalate",
936
+ prompt: "schedules/triggers/meeting-action-capture.md",
937
+ description: "Capture meeting action items into queues.",
938
+ },
939
+ "daily-morning-brief": {
940
+ mode: "escalate",
941
+ prompt: "schedules/triggers/daily-morning-brief.md",
942
+ description: "Daily morning brief.",
943
+ },
944
+ "daily-midday-sweep": {
945
+ mode: "escalate",
946
+ prompt: "schedules/triggers/daily-midday-sweep.md",
947
+ description: "Midday comms triage and queue sweep.",
948
+ },
949
+ "daily-evening-wrap": {
950
+ mode: "escalate",
951
+ prompt: "schedules/triggers/daily-evening-wrap.md",
952
+ description: "Evening wrap, log review, follow-up close.",
953
+ },
954
+ "weekly-hiring": {
955
+ mode: "escalate",
956
+ prompt: "schedules/triggers/weekly-hiring.md",
957
+ description: "Weekly hiring review.",
958
+ },
959
+ "weekly-priorities": {
960
+ mode: "escalate",
961
+ prompt: "schedules/triggers/weekly-priorities.md",
962
+ description: "Weekly priorities review.",
963
+ },
964
+ "weekly-engineering-health": {
965
+ mode: "escalate",
966
+ prompt: "schedules/triggers/weekly-engineering-health.md",
967
+ description: "Weekly engineering health.",
968
+ },
969
+ "weekly-execution": {
970
+ mode: "escalate",
971
+ prompt: "schedules/triggers/weekly-execution.md",
972
+ description: "Weekly execution review.",
973
+ },
974
+ "weekly-strategic-memo": {
975
+ mode: "escalate",
976
+ prompt: "schedules/triggers/weekly-strategic-memo.md",
977
+ description: "Weekly strategic memo drafting.",
978
+ },
979
+ "quarterly-self-assessment": {
980
+ mode: "guarded",
981
+ guard: guardQuarterlySelfAssessment,
982
+ prompt: "schedules/triggers/quarterly-self-assessment.md",
983
+ description: "Quarterly self-assessment (guard escalates only in a quarter-start month).",
984
+ },
985
+
986
+ // Economics / scheduling cadences (WS-economics). Deterministic → inline,
987
+ // except dynamic-jobs (guarded escalate-when-due).
988
+ "nightly-cost-reconcile": {
989
+ mode: "inline",
990
+ handler: handleNightlyCostReconcile,
991
+ description: "Nightly three-way spend reconcile vs the Admin Cost API; flags drift > 10%. Inline (no LLM); fails open with no fetchImpl.",
992
+ },
993
+ "fleet-cost-digest": {
994
+ mode: "inline",
995
+ handler: handleFleetCostDigest,
996
+ description: "Daily fleet LLM-spend digest from the local router ledger (org rollup leg fails open). Inline.",
997
+ },
998
+ "org-cost-sync": {
999
+ mode: "inline",
1000
+ handler: handleOrgCostSync,
1001
+ description: "Push today's router ledger rows to Cohort cost.report (no-op/fail-open when org disabled). Inline.",
1002
+ },
1003
+ "monitoring-alerts": {
1004
+ mode: "inline",
1005
+ handler: handleMonitoringAlerts,
1006
+ description: "Evaluate behavioral alert rules (guard-fail/refusal spikes, cost p99, absent-agent, daemon-down) over the diagnostics counters + ledger + presence; emit to config/alerts.yaml webhook else log. Inline (no LLM); fail-open.",
1007
+ },
1008
+ "dynamic-jobs": {
1009
+ mode: "guarded",
1010
+ guard: guardDynamicJobs,
1011
+ prompt: "schedules/triggers/dynamic-jobs.md",
1012
+ description: "Action agent-created reminders/jobs. Guard escalates only when dueJobs() returns due work; otherwise inline.",
1013
+ },
1014
+
1015
+ // Org messaging inbound (SP10). Guarded — the guard pulls new Cohort
1016
+ // messages/@mentions/call-invites directed at this agent and routes them into
1017
+ // the inbox pipeline (the reactive daemon owns dispatch, so it stays inline);
1018
+ // MESSAGING_INBOUND_ESCALATE=1 flips it to escalate when items arrive.
1019
+ "messaging-inbound": {
1020
+ mode: "guarded",
1021
+ guard: guardMessagingInbound,
1022
+ prompt: "schedules/triggers/messaging-inbound.md",
1023
+ description: "Pull Cohort org messages/@mentions/call-invites directed at this agent into the inbox pipeline. Guarded — inline routing by default; escalate-when-items with MESSAGING_INBOUND_ESCALATE=1. Fail-open when org disabled.",
1024
+ // Scoped-permissions allowlist (audit H1): when it escalates, the sub-session
1025
+ // reads the org context and drafts a reply — no shell/write primitives.
1026
+ allowedTools: ["Read", "Grep", "Glob", "LS", "messaging_history", "messaging_send"],
1027
+ },
1028
+
1029
+ // Directory & Design stewards (2026-07). Both guarded, both cheap when idle.
1030
+ "directory-hygiene": {
1031
+ mode: "guarded",
1032
+ guard: guardDirectoryHygiene,
1033
+ prompt: "schedules/triggers/directory-hygiene.md",
1034
+ description:
1035
+ "Keep the Directory clean: propose captures from signatures/rosters/bank feeds, enrich channels, re-verify anything unseen for >90d, flag duplicates. Guarded — escalates only when the capture/duplicate queues have work. PROPOSE-ONLY: merges, awareness changes and archives are human-gated server-side. Fail-open when org disabled.",
1036
+ // Scoped-permissions allowlist (audit H1). Every Directory verb here is a
1037
+ // read or a PROPOSAL; the human-gated verbs are deliberately absent, so even
1038
+ // in scoped mode the cadence physically cannot reach for them.
1039
+ allowedTools: [
1040
+ "Read", "Grep", "Glob", "LS",
1041
+ "directory_search", "directory_list_people", "directory_list_orgs",
1042
+ "directory_get_person", "directory_get_org",
1043
+ "directory_list_captures", "directory_list_merges",
1044
+ "directory_propose_capture", "directory_propose_merge",
1045
+ "directory_add_channel", "directory_update_person",
1046
+ ],
1047
+ },
1048
+ "brand-steward": {
1049
+ mode: "guarded",
1050
+ guard: guardBrandSteward,
1051
+ prompt: "schedules/triggers/brand-steward.md",
1052
+ description:
1053
+ "React to a brand foundation save: quote the stale image slots and templates, re-render what is worth re-rendering inside the budget, and post a working-now status carrying the real spend. Guarded — escalates only on a branding version.saved/foundation.updated event. Fail-open when org disabled.",
1054
+ // Reads + the costed producers. branding.saveFoundation/restoreFoundation are
1055
+ // admin-scope server-side and have no tool here — the cadence proposes.
1056
+ allowedTools: [
1057
+ "Read", "Grep", "Glob", "LS",
1058
+ "design_foundation", "design_voice", "design_list_templates",
1059
+ "design_render_template", "design_export_kit", "design_generate_image",
1060
+ "design_propose_change", "messaging_send",
1061
+ ],
1062
+ },
1063
+ };
1064
+
1065
+ // Optional config-driven cadence registry (config/.cadence-registry.json),
1066
+ // written by scripts/setup/generate-cadences.mjs from the agent's function ×
1067
+ // altitude. Lets archetype-specific cadences route without editing this file.
1068
+ // Cached after first read.
1069
+ let _configRegistry = null;
1070
+ function loadConfigRegistry() {
1071
+ if (_configRegistry !== null) return _configRegistry;
1072
+ _configRegistry = {};
1073
+ try {
1074
+ const root = process.env.AGENT_ROOT || process.env.AGENT_DIR || process.cwd();
1075
+ const p = join(root, "config", ".cadence-registry.json");
1076
+ if (existsSync(p)) _configRegistry = JSON.parse(readFileSync(p, "utf-8")) || {};
1077
+ } catch { _configRegistry = {}; }
1078
+ return _configRegistry;
1079
+ }
1080
+
1081
+ /**
1082
+ * Look up a cadence definition. Returns null for unknown cadences so the
1083
+ * consumer can decide between inferred-escalate and DLQ based on prompt file
1084
+ * availability. The hardcoded CADENCE_REGISTRY wins; the config-driven archetype
1085
+ * cadences (config/.cadence-registry.json) fill in the rest.
1086
+ */
1087
+ export function getCadenceDef(name) {
1088
+ if (CADENCE_REGISTRY[name]) return CADENCE_REGISTRY[name];
1089
+ const cfg = loadConfigRegistry()[name];
1090
+ if (cfg && cfg.prompt) {
1091
+ return { mode: cfg.mode || "escalate", prompt: cfg.prompt, description: "archetype cadence (config-driven)" };
1092
+ }
1093
+ return null;
1094
+ }
1095
+
1096
+ /**
1097
+ * Convenience: list all registered cadence names. Used by doctor to confirm
1098
+ * the consumer can route every cadence that the plist generator emits.
1099
+ */
1100
+ export function listCadenceNames() {
1101
+ return Object.keys(CADENCE_REGISTRY);
1102
+ }
1103
+
1104
+ // Exported for tests (and for the consumer, which already reaches the guard via
1105
+ // CADENCE_REGISTRY). The optional second arg carries injectable seams for the
1106
+ // phase-2 consolidation due-check (clock + fake spawner) without touching the
1107
+ // production path.
1108
+ export { guardSkillCurator, maybeSpawnConsolidation };
1109
+
1110
+ // Economics / scheduling handlers — exported for tests (the consumer reaches
1111
+ // them via CADENCE_REGISTRY). Each takes an optional second arg of injectable
1112
+ // seams (lib fn / clock / cfg) so tests never touch the network or real config.
1113
+ export {
1114
+ handleNightlyCostReconcile,
1115
+ handleFleetCostDigest,
1116
+ handleOrgCostSync,
1117
+ guardDynamicJobs,
1118
+ guardMessagingInbound,
1119
+ guardDirectoryHygiene,
1120
+ guardBrandSteward,
1121
+ };