@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,72 @@
1
+ /**
2
+ * Tests for lib/fs-atomic.mjs — durable atomic write primitives.
3
+ * @module lib/fs-atomic.test
4
+ */
5
+
6
+ import { describe, it, beforeEach, afterEach } from "node:test";
7
+ import assert from "node:assert/strict";
8
+ import { mkdtempSync, rmSync, readFileSync, writeFileSync, existsSync, readdirSync } from "node:fs";
9
+ import { join } from "node:path";
10
+ import { tmpdir } from "node:os";
11
+ import { writeFileAtomic, writeJsonAtomic, appendJsonl } from "./fs-atomic.mjs";
12
+
13
+ let dir;
14
+ beforeEach(() => { dir = mkdtempSync(join(tmpdir(), "fs-atomic-")); });
15
+ afterEach(() => { rmSync(dir, { recursive: true, force: true }); });
16
+
17
+ describe("writeFileAtomic", () => {
18
+ it("writes contents and creates missing parent dirs", () => {
19
+ const p = join(dir, "a", "b", "c.txt");
20
+ writeFileAtomic(p, "hello");
21
+ assert.equal(readFileSync(p, "utf8"), "hello");
22
+ });
23
+
24
+ it("overwrites an existing file atomically (final contents only)", () => {
25
+ const p = join(dir, "x.txt");
26
+ writeFileAtomic(p, "first");
27
+ writeFileAtomic(p, "second");
28
+ assert.equal(readFileSync(p, "utf8"), "second");
29
+ });
30
+
31
+ it("leaves no .tmp sibling behind on success", () => {
32
+ const p = join(dir, "y.txt");
33
+ writeFileAtomic(p, "data");
34
+ const strays = readdirSync(dir).filter((f) => f.includes(".tmp."));
35
+ assert.equal(strays.length, 0);
36
+ });
37
+
38
+ it("accepts a Buffer payload", () => {
39
+ const p = join(dir, "buf.bin");
40
+ writeFileAtomic(p, Buffer.from([1, 2, 3]));
41
+ assert.deepEqual([...readFileSync(p)], [1, 2, 3]);
42
+ });
43
+ });
44
+
45
+ describe("writeJsonAtomic", () => {
46
+ it("round-trips an object as pretty JSON with trailing newline", () => {
47
+ const p = join(dir, "obj.json");
48
+ writeJsonAtomic(p, { a: 1, b: [2, 3] });
49
+ const raw = readFileSync(p, "utf8");
50
+ assert.ok(raw.endsWith("\n"));
51
+ assert.deepEqual(JSON.parse(raw), { a: 1, b: [2, 3] });
52
+ });
53
+ });
54
+
55
+ describe("appendJsonl", () => {
56
+ it("appends whole JSON lines, preserving prior content", () => {
57
+ const p = join(dir, "log.jsonl");
58
+ assert.equal(appendJsonl(p, { n: 1 }), true);
59
+ assert.equal(appendJsonl(p, { n: 2 }), true);
60
+ const lines = readFileSync(p, "utf8").trim().split("\n");
61
+ assert.equal(lines.length, 2);
62
+ assert.deepEqual(JSON.parse(lines[0]), { n: 1 });
63
+ assert.deepEqual(JSON.parse(lines[1]), { n: 2 });
64
+ });
65
+
66
+ it("returns false instead of throwing when the path is unwritable", () => {
67
+ // A path whose parent is an existing *file* cannot be mkdir'd → IO error.
68
+ const blocker = join(dir, "blocker");
69
+ writeFileSync(blocker, "x");
70
+ assert.equal(appendJsonl(join(blocker, "nope.jsonl"), { n: 1 }), false);
71
+ });
72
+ });
@@ -0,0 +1,111 @@
1
+ /**
2
+ * lib/fs-ownership.mjs — Pure ownership / permission hygiene checks.
3
+ *
4
+ * Audit M3: the framework ships `state/` and `logs/` and then runs as the
5
+ * unprivileged user. If those trees were created (or chown'd) root — a common
6
+ * side effect of running an installer step under `sudo` — the daemon's writes
7
+ * fail silently, or someone "fixes" it with a 0777 chmod that leaves the agent's
8
+ * private state world-writable. Neither is acceptable.
9
+ *
10
+ * This module is intentionally side-effect free and dependency free so it can be
11
+ * unit-tested against synthetic stat-like inputs without needing real
12
+ * root-owned directories. The `maestro doctor` command wires it up against the
13
+ * live filesystem; nothing here ever performs a chown/chmod — fixing ownership
14
+ * is an explicit operator action.
15
+ */
16
+
17
+ /**
18
+ * Evaluate a single path's ownership + permission posture.
19
+ *
20
+ * @param {object} input
21
+ * @param {string} input.path Display path (used only in messages).
22
+ * @param {number} input.mode st_mode (as from fs.stat().mode).
23
+ * @param {number} input.uid Owning uid of the path (st_uid).
24
+ * @param {number} input.runtimeUid uid the framework runs as (process.getuid()).
25
+ * @param {boolean} [input.exists=true] Whether the path exists. Missing paths
26
+ * are reported as ok:true, exists:false so
27
+ * the caller can decide (a missing optional
28
+ * subdir is not an ownership violation).
29
+ * @returns {{ ok: boolean, exists: boolean, path: string,
30
+ * ownedByRuntime: boolean, groupWritable: boolean,
31
+ * worldWritable: boolean, reasons: string[] }}
32
+ */
33
+ export function checkPathOwnership({ path, mode, uid, runtimeUid, exists = true }) {
34
+ if (!exists) {
35
+ return {
36
+ ok: true,
37
+ exists: false,
38
+ path,
39
+ ownedByRuntime: false,
40
+ groupWritable: false,
41
+ worldWritable: false,
42
+ reasons: [],
43
+ };
44
+ }
45
+
46
+ const reasons = [];
47
+
48
+ // Ownership: the runtime uid must own the path so it can always write,
49
+ // rename, and chmod within it. root-owned (uid 0) state is the classic
50
+ // failure mode this guards against.
51
+ const ownedByRuntime = uid === runtimeUid;
52
+ if (!ownedByRuntime) {
53
+ reasons.push(
54
+ `owned by uid ${uid}, but the framework runs as uid ${runtimeUid}`,
55
+ );
56
+ }
57
+
58
+ // Permission hygiene: agent state is private. Group- or world-writable
59
+ // directories let any local account tamper with queues, dashboards, and
60
+ // logs, and are usually the residue of a `chmod -R 0777` workaround.
61
+ const groupWritable = (mode & 0o020) !== 0;
62
+ const worldWritable = (mode & 0o002) !== 0;
63
+ if (groupWritable) reasons.push("group-writable (g+w)");
64
+ if (worldWritable) reasons.push("world-writable (o+w)");
65
+
66
+ return {
67
+ ok: reasons.length === 0,
68
+ exists: true,
69
+ path,
70
+ ownedByRuntime,
71
+ groupWritable,
72
+ worldWritable,
73
+ reasons,
74
+ };
75
+ }
76
+
77
+ /**
78
+ * Evaluate a set of paths and produce an aggregate verdict plus a single,
79
+ * copy-pasteable remediation command rooted at the agent directory.
80
+ *
81
+ * @param {Array<{path:string, mode:number, uid:number, exists?:boolean}>} entries
82
+ * @param {object} opts
83
+ * @param {number} opts.runtimeUid process.getuid().
84
+ * @param {string} [opts.whoami="$(whoami)"] Username for the chown hint.
85
+ * @param {string[]} [opts.roots=["state","logs"]] Top-level dirs for the fix cmd.
86
+ * @returns {{ ok: boolean, violations: object[], fixCommand: string|null }}
87
+ */
88
+ export function checkOwnershipSet(entries, { runtimeUid, whoami = "$(whoami)", roots = ["state", "logs"] } = {}) {
89
+ const results = entries.map((e) =>
90
+ checkPathOwnership({ ...e, runtimeUid }),
91
+ );
92
+ const violations = results.filter((r) => r.exists && !r.ok);
93
+ const fixCommand =
94
+ violations.length > 0 ? buildFixCommand(roots, whoami) : null;
95
+ return { ok: violations.length === 0, violations, fixCommand };
96
+ }
97
+
98
+ /**
99
+ * The exact remediation the operator should run. chown restores ownership to
100
+ * the runtime user; `chmod -R u+rwX,go-w` restores write access for the owner
101
+ * and strips group/world write — without clobbering the execute bit on dirs
102
+ * (capital X only sets +x where it already applies or on directories).
103
+ *
104
+ * @param {string[]} roots
105
+ * @param {string} whoami
106
+ * @returns {string}
107
+ */
108
+ export function buildFixCommand(roots = ["state", "logs"], whoami = "$(whoami)") {
109
+ const joined = roots.join(" ");
110
+ return `sudo chown -R ${whoami} ${joined} && chmod -R u+rwX,go-w ${joined}`;
111
+ }
@@ -0,0 +1,158 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import {
4
+ checkPathOwnership,
5
+ checkOwnershipSet,
6
+ buildFixCommand,
7
+ } from "./fs-ownership.mjs";
8
+
9
+ // Octal modes are easier to read for these cases. We pass the directory mode
10
+ // bits the way fs.stat().mode reports them (S_IFDIR | perms).
11
+ const RUNTIME_UID = 501;
12
+
13
+ test("checkPathOwnership: owned by runtime, 0700 -> ok", () => {
14
+ const r = checkPathOwnership({
15
+ path: "state",
16
+ mode: 0o40700,
17
+ uid: RUNTIME_UID,
18
+ runtimeUid: RUNTIME_UID,
19
+ });
20
+ assert.equal(r.ok, true);
21
+ assert.equal(r.exists, true);
22
+ assert.equal(r.ownedByRuntime, true);
23
+ assert.equal(r.groupWritable, false);
24
+ assert.equal(r.worldWritable, false);
25
+ assert.deepEqual(r.reasons, []);
26
+ });
27
+
28
+ test("checkPathOwnership: owned by runtime, 0755 -> ok (not group/world writable)", () => {
29
+ const r = checkPathOwnership({
30
+ path: "logs",
31
+ mode: 0o40755,
32
+ uid: RUNTIME_UID,
33
+ runtimeUid: RUNTIME_UID,
34
+ });
35
+ assert.equal(r.ok, true);
36
+ assert.deepEqual(r.reasons, []);
37
+ });
38
+
39
+ test("checkPathOwnership: root-owned -> violation with uid in reason", () => {
40
+ const r = checkPathOwnership({
41
+ path: "state",
42
+ mode: 0o40700,
43
+ uid: 0,
44
+ runtimeUid: RUNTIME_UID,
45
+ });
46
+ assert.equal(r.ok, false);
47
+ assert.equal(r.ownedByRuntime, false);
48
+ assert.equal(r.reasons.length, 1);
49
+ assert.match(r.reasons[0], /uid 0/);
50
+ assert.match(r.reasons[0], /uid 501/);
51
+ });
52
+
53
+ test("checkPathOwnership: group-writable -> violation", () => {
54
+ const r = checkPathOwnership({
55
+ path: "logs",
56
+ mode: 0o40770,
57
+ uid: RUNTIME_UID,
58
+ runtimeUid: RUNTIME_UID,
59
+ });
60
+ assert.equal(r.ok, false);
61
+ assert.equal(r.groupWritable, true);
62
+ assert.ok(r.reasons.some((x) => /group-writable/.test(x)));
63
+ });
64
+
65
+ test("checkPathOwnership: world-writable (0777 workaround) -> violation", () => {
66
+ const r = checkPathOwnership({
67
+ path: "state",
68
+ mode: 0o40777,
69
+ uid: RUNTIME_UID,
70
+ runtimeUid: RUNTIME_UID,
71
+ });
72
+ assert.equal(r.ok, false);
73
+ assert.equal(r.worldWritable, true);
74
+ assert.equal(r.groupWritable, true);
75
+ assert.ok(r.reasons.some((x) => /world-writable/.test(x)));
76
+ assert.ok(r.reasons.some((x) => /group-writable/.test(x)));
77
+ });
78
+
79
+ test("checkPathOwnership: root-owned AND world-writable -> three reasons", () => {
80
+ const r = checkPathOwnership({
81
+ path: "state",
82
+ mode: 0o40777,
83
+ uid: 0,
84
+ runtimeUid: RUNTIME_UID,
85
+ });
86
+ assert.equal(r.ok, false);
87
+ // ownership + group-writable + world-writable
88
+ assert.equal(r.reasons.length, 3);
89
+ });
90
+
91
+ test("checkPathOwnership: missing path -> ok:true exists:false, no reasons", () => {
92
+ const r = checkPathOwnership({
93
+ path: "state/cadence-bus",
94
+ mode: 0,
95
+ uid: 0,
96
+ runtimeUid: RUNTIME_UID,
97
+ exists: false,
98
+ });
99
+ assert.equal(r.ok, true);
100
+ assert.equal(r.exists, false);
101
+ assert.deepEqual(r.reasons, []);
102
+ });
103
+
104
+ test("checkOwnershipSet: all clean -> ok, no fixCommand", () => {
105
+ const res = checkOwnershipSet(
106
+ [
107
+ { path: "state", mode: 0o40700, uid: RUNTIME_UID },
108
+ { path: "logs", mode: 0o40755, uid: RUNTIME_UID },
109
+ ],
110
+ { runtimeUid: RUNTIME_UID },
111
+ );
112
+ assert.equal(res.ok, true);
113
+ assert.equal(res.violations.length, 0);
114
+ assert.equal(res.fixCommand, null);
115
+ });
116
+
117
+ test("checkOwnershipSet: one root-owned -> not ok, fixCommand present", () => {
118
+ const res = checkOwnershipSet(
119
+ [
120
+ { path: "state", mode: 0o40700, uid: 0 },
121
+ { path: "logs", mode: 0o40700, uid: RUNTIME_UID },
122
+ ],
123
+ { runtimeUid: RUNTIME_UID, whoami: "wong" },
124
+ );
125
+ assert.equal(res.ok, false);
126
+ assert.equal(res.violations.length, 1);
127
+ assert.equal(res.violations[0].path, "state");
128
+ assert.equal(
129
+ res.fixCommand,
130
+ "sudo chown -R wong state logs && chmod -R u+rwX,go-w state logs",
131
+ );
132
+ });
133
+
134
+ test("checkOwnershipSet: missing entries are ignored (not violations)", () => {
135
+ const res = checkOwnershipSet(
136
+ [
137
+ { path: "state", mode: 0o40700, uid: RUNTIME_UID },
138
+ { path: "state/cadence-bus", mode: 0, uid: 0, exists: false },
139
+ ],
140
+ { runtimeUid: RUNTIME_UID },
141
+ );
142
+ assert.equal(res.ok, true);
143
+ assert.equal(res.fixCommand, null);
144
+ });
145
+
146
+ test("buildFixCommand: default roots + $(whoami)", () => {
147
+ assert.equal(
148
+ buildFixCommand(),
149
+ "sudo chown -R $(whoami) state logs && chmod -R u+rwX,go-w state logs",
150
+ );
151
+ });
152
+
153
+ test("buildFixCommand: custom roots", () => {
154
+ assert.equal(
155
+ buildFixCommand(["state", "logs", "knowledge"], "alice"),
156
+ "sudo chown -R alice state logs knowledge && chmod -R u+rwX,go-w state logs knowledge",
157
+ );
158
+ });
@@ -0,0 +1,347 @@
1
+ /**
2
+ * lib/hooks/bus.mjs — typed lifecycle hook bus.
3
+ *
4
+ * Why this exists
5
+ * ---------------
6
+ * Maestro grew a handful of cross-cutting controls that each wired themselves
7
+ * directly into the paths they guard: the pre-send audit gates exactly one
8
+ * tool, guard telemetry is bespoke per call site, audit logging is scattered,
9
+ * and the learning loop's governance is hardwired. The audit's single
10
+ * highest-leverage P1 enabler (gaps-product-quality P1-8) is a *typed lifecycle
11
+ * hook bus* — once it exists, the send-gate, ownership backstop, disclosure
12
+ * assessment, guard telemetry, audit logging and learning governance all become
13
+ * SUBSCRIBERS to well-known lifecycle events instead of bespoke wiring scattered
14
+ * across the daemon, the channel adapters and the senders.
15
+ *
16
+ * Design
17
+ * ------
18
+ * The bus is purely in-process (no filesystem — that's what the underlying
19
+ * audit logs are for). It exposes a CLOSED set of typed lifecycle events; an
20
+ * unknown event name is rejected at subscribe/emit time so a typo can never
21
+ * silently register a dead subscriber. Subscribers run in priority order
22
+ * (higher priority first; ties broken by registration order for determinism).
23
+ *
24
+ * Cancellable vs observe-only
25
+ * ---------------------------
26
+ * `beforeSend` is the one CANCELLABLE chain: any subscriber may veto the send
27
+ * by returning (or resolving to) `{ cancel: true, reason }`. The first veto
28
+ * short-circuits the remaining subscribers and `emit` resolves with the
29
+ * decision so the caller can refuse to send and surface the reason. This is the
30
+ * seam the send-gate, ownership backstop and disclosure assessment plug into.
31
+ * Every other event is OBSERVE-ONLY: subscriber return values are ignored, the
32
+ * chain always runs to completion, and `emit` resolves to the standard
33
+ * not-cancelled decision.
34
+ *
35
+ * Fail-open isolation (house idiom)
36
+ * ---------------------------------
37
+ * One throwing (or rejecting) subscriber is caught, reported, and SKIPPED — it
38
+ * never breaks the chain or starves the subscribers after it. A thrown error in
39
+ * a cancellable chain is treated as "no opinion" (it does NOT cancel the send),
40
+ * so a buggy gate can never silently drop traffic. Errors are surfaced two ways:
41
+ * via the pluggable `logger` (defaults to the audit JSONL) and by re-emitting an
42
+ * `onError` event — but the `onError` chain is itself isolated and never
43
+ * re-enters recursively, so a throwing error-handler can't loop the bus.
44
+ *
45
+ * Sync + async
46
+ * ------------
47
+ * Subscribers may be synchronous or return a promise; `emit` awaits each one in
48
+ * priority order before moving to the next, so ordering holds across async work.
49
+ *
50
+ * @module lib/hooks/bus
51
+ */
52
+
53
+ import { appendJsonl } from "../fs-atomic.mjs";
54
+ import { resolveAgentRoot } from "../agent-root.mjs";
55
+ import { join } from "node:path";
56
+
57
+ // ---------------------------------------------------------------------------
58
+ // Closed event set
59
+ // ---------------------------------------------------------------------------
60
+
61
+ /**
62
+ * The CLOSED set of typed lifecycle events. Cancellable events let a subscriber
63
+ * veto the operation; observe-only events ignore return values. Adding a new
64
+ * event is a deliberate edit here — the closed set is what makes a mistyped
65
+ * event name a loud error instead of a dead subscriber.
66
+ */
67
+ export const HOOK_EVENTS = Object.freeze({
68
+ /** Before an outbound message is sent. CANCELLABLE — send-gate / ownership / disclosure. */
69
+ beforeSend: { cancellable: true },
70
+ /** After an outbound message is sent (delivery receipt / audit). Observe-only. */
71
+ afterSend: { cancellable: false },
72
+ /** An inbound message has been classified (kind/priority/privilege). Observe-only. */
73
+ onClassify: { cancellable: false },
74
+ /** An inbound event has been dispatched to a session. Observe-only. */
75
+ onDispatch: { cancellable: false },
76
+ /** A (sub-)session has closed (learning capture / reflect). Observe-only. */
77
+ onSessionClose: { cancellable: false },
78
+ /** A guard (budget/rate/governor) failed open or tripped. Observe-only telemetry. */
79
+ onGuardFail: { cancellable: false },
80
+ /** The learning loop authored or landed a skill. Observe-only governance. */
81
+ onSkillAuthored: { cancellable: false },
82
+ /** A subscriber threw, or a caller reports an error onto the bus. Observe-only. */
83
+ onError: { cancellable: false },
84
+ });
85
+
86
+ /** Names of the closed event set, for validation + iteration. */
87
+ export const HOOK_EVENT_NAMES = Object.freeze(Object.keys(HOOK_EVENTS));
88
+
89
+ /** @returns {boolean} whether `event` is a known lifecycle event. */
90
+ export function isHookEvent(event) {
91
+ return Object.prototype.hasOwnProperty.call(HOOK_EVENTS, event);
92
+ }
93
+
94
+ /** @returns {boolean} whether `event` is a cancellable chain. */
95
+ export function isCancellableEvent(event) {
96
+ return isHookEvent(event) && HOOK_EVENTS[event].cancellable === true;
97
+ }
98
+
99
+ function assertKnownEvent(event, op) {
100
+ if (!isHookEvent(event)) {
101
+ throw new Error(
102
+ `HookBus.${op}: unknown event "${String(event)}". ` +
103
+ `Known events: ${HOOK_EVENT_NAMES.join(", ")}.`
104
+ );
105
+ }
106
+ }
107
+
108
+ // ---------------------------------------------------------------------------
109
+ // Default logger
110
+ // ---------------------------------------------------------------------------
111
+
112
+ /**
113
+ * Default subscriber-error logger. Best-effort append to logs/audit/hooks.jsonl
114
+ * via the shared atomic JSONL appender (which never throws). Returns nothing.
115
+ * Callers can inject their own logger via the factory for tests / alt sinks.
116
+ *
117
+ * @param {object} entry
118
+ */
119
+ function defaultLogger(entry) {
120
+ try {
121
+ const file = join(resolveAgentRoot(), "logs", "audit", "hooks.jsonl");
122
+ appendJsonl(file, { ts: new Date().toISOString(), ...entry });
123
+ } catch {
124
+ /* fail-open: a logger failure must never break the bus */
125
+ }
126
+ }
127
+
128
+ // ---------------------------------------------------------------------------
129
+ // HookBus
130
+ // ---------------------------------------------------------------------------
131
+
132
+ /**
133
+ * A typed lifecycle hook bus. Construct via {@link createHookBus} (tests) or
134
+ * read the process-wide instance via {@link getHookBus} (production wiring).
135
+ */
136
+ export class HookBus {
137
+ /**
138
+ * @param {object} [opts]
139
+ * @param {(entry: object) => void} [opts.logger] sink for subscriber errors
140
+ * and dropped-cancellation diagnostics. Defaults to the audit JSONL.
141
+ * @param {number} [opts.defaultPriority] priority for subscribers that don't
142
+ * specify one (default 0). Higher runs first.
143
+ */
144
+ constructor(opts = {}) {
145
+ /** @type {Map<string, Array<{fn: Function, priority: number, seq: number}>>} */
146
+ this._subs = new Map();
147
+ for (const name of HOOK_EVENT_NAMES) this._subs.set(name, []);
148
+ this._logger = typeof opts.logger === "function" ? opts.logger : defaultLogger;
149
+ this._defaultPriority = Number.isFinite(opts.defaultPriority) ? opts.defaultPriority : 0;
150
+ this._seq = 0; // monotonic registration counter for stable tie-breaking
151
+ this._inError = false; // re-entrancy guard for the onError chain
152
+ }
153
+
154
+ /**
155
+ * Register a subscriber for a lifecycle event.
156
+ *
157
+ * Subscribers run in DESCENDING priority order; subscribers registered with
158
+ * the same priority run in registration order (FIFO). For a cancellable
159
+ * event a subscriber may return (or resolve to) `{ cancel: true, reason }`
160
+ * to veto the operation and short-circuit the chain.
161
+ *
162
+ * @param {string} event one of {@link HOOK_EVENT_NAMES}
163
+ * @param {(ctx: object) => (void | object | Promise<void|object>)} fn
164
+ * @param {object} [opts]
165
+ * @param {number} [opts.priority] higher runs first (default 0)
166
+ * @returns {() => void} an unsubscribe handle (idempotent)
167
+ */
168
+ subscribe(event, fn, opts = {}) {
169
+ assertKnownEvent(event, "subscribe");
170
+ if (typeof fn !== "function") {
171
+ throw new TypeError(`HookBus.subscribe: handler for "${event}" must be a function`);
172
+ }
173
+ const priority = Number.isFinite(opts.priority) ? opts.priority : this._defaultPriority;
174
+ const entry = { fn, priority, seq: this._seq++ };
175
+ const list = this._subs.get(event);
176
+ list.push(entry);
177
+ // Keep the list sorted so emit() is a straight walk. Descending priority,
178
+ // ascending seq for stable FIFO within a priority band.
179
+ list.sort((a, b) => (b.priority - a.priority) || (a.seq - b.seq));
180
+ let active = true;
181
+ return () => {
182
+ if (!active) return;
183
+ active = false;
184
+ this._removeEntry(event, entry);
185
+ };
186
+ }
187
+
188
+ /**
189
+ * Remove a previously-registered subscriber by reference. Removes the FIRST
190
+ * matching (event, fn) registration. Returns true if one was removed.
191
+ *
192
+ * @param {string} event
193
+ * @param {Function} fn
194
+ * @returns {boolean}
195
+ */
196
+ unsubscribe(event, fn) {
197
+ assertKnownEvent(event, "unsubscribe");
198
+ const list = this._subs.get(event);
199
+ const i = list.findIndex((e) => e.fn === fn);
200
+ if (i === -1) return false;
201
+ list.splice(i, 1);
202
+ return true;
203
+ }
204
+
205
+ _removeEntry(event, entry) {
206
+ const list = this._subs.get(event);
207
+ if (!list) return;
208
+ const i = list.indexOf(entry);
209
+ if (i !== -1) list.splice(i, 1);
210
+ }
211
+
212
+ /** @returns {number} live subscriber count for an event. */
213
+ count(event) {
214
+ assertKnownEvent(event, "count");
215
+ return this._subs.get(event).length;
216
+ }
217
+
218
+ /** Remove all subscribers (all events, or just one). Mostly for tests. */
219
+ clear(event) {
220
+ if (event === undefined) {
221
+ for (const name of HOOK_EVENT_NAMES) this._subs.set(name, []);
222
+ return;
223
+ }
224
+ assertKnownEvent(event, "clear");
225
+ this._subs.set(event, []);
226
+ }
227
+
228
+ /**
229
+ * Emit a lifecycle event. Runs every live subscriber in priority order,
230
+ * awaiting each (sync or async) before the next, so ordering holds across
231
+ * async work.
232
+ *
233
+ * Isolation: a throwing/rejecting subscriber is caught, reported (via the
234
+ * injected logger and a non-recursive `onError` re-emit) and SKIPPED — it
235
+ * never aborts the chain or starves later subscribers.
236
+ *
237
+ * Cancellation (cancellable events only): if a subscriber returns
238
+ * `{ cancel: true, reason }` the chain short-circuits and the returned
239
+ * decision carries `{ cancelled: true, reason, by }`. A subscriber that
240
+ * throws is treated as "no opinion" — it does NOT cancel.
241
+ *
242
+ * @param {string} event one of {@link HOOK_EVENT_NAMES}
243
+ * @param {object} [ctx] context object passed to every subscriber
244
+ * @returns {Promise<{cancelled: boolean, reason: (string|null), by: (number|null), errors: number}>}
245
+ */
246
+ async emit(event, ctx = {}) {
247
+ assertKnownEvent(event, "emit");
248
+ const cancellable = HOOK_EVENTS[event].cancellable === true;
249
+ // Snapshot the subscriber list so subscribe/unsubscribe during emit does
250
+ // not mutate the in-flight walk (and a subscriber added mid-emit doesn't
251
+ // run for the event already in flight).
252
+ const list = this._subs.get(event).slice();
253
+ let errors = 0;
254
+
255
+ for (let i = 0; i < list.length; i++) {
256
+ const { fn } = list[i];
257
+ let outcome;
258
+ try {
259
+ outcome = await fn(ctx);
260
+ } catch (err) {
261
+ errors++;
262
+ this._reportSubscriberError(event, i, err);
263
+ continue; // isolate: a throw is "no opinion", never cancels
264
+ }
265
+ if (cancellable && outcome && typeof outcome === "object" && outcome.cancel === true) {
266
+ return {
267
+ cancelled: true,
268
+ reason: typeof outcome.reason === "string" ? outcome.reason : "cancelled",
269
+ by: i,
270
+ errors,
271
+ };
272
+ }
273
+ }
274
+ return { cancelled: false, reason: null, by: null, errors };
275
+ }
276
+
277
+ /**
278
+ * Report a subscriber error to the logger and (unless we're already inside
279
+ * the onError chain) re-emit `onError`. The re-entrancy guard makes a
280
+ * throwing onError subscriber safe — its own throw is logged but does not
281
+ * recurse.
282
+ */
283
+ _reportSubscriberError(event, index, err) {
284
+ const entry = {
285
+ level: "error",
286
+ stage: "hook_subscriber_error",
287
+ event,
288
+ index,
289
+ error: err && err.message ? err.message : String(err),
290
+ };
291
+ try {
292
+ this._logger(entry);
293
+ } catch {
294
+ /* a broken logger must never break the bus */
295
+ }
296
+ if (event === "onError" || this._inError) return;
297
+ this._inError = true;
298
+ // Fire-and-forget: surface the error onto the bus for any onError
299
+ // subscribers, but never await it from inside another emit's hot loop and
300
+ // never let it reject upward. emit() already isolates each subscriber.
301
+ Promise.resolve()
302
+ .then(() => this.emit("onError", { sourceEvent: event, index, error: entry.error }))
303
+ .catch(() => { /* fully isolated */ })
304
+ .finally(() => { this._inError = false; });
305
+ }
306
+ }
307
+
308
+ // ---------------------------------------------------------------------------
309
+ // Factory + singleton accessor
310
+ // ---------------------------------------------------------------------------
311
+
312
+ /**
313
+ * Create a fresh, isolated HookBus. Tests should use this so registrations
314
+ * don't leak between cases. Production wiring uses {@link getHookBus}.
315
+ *
316
+ * @param {object} [opts] see {@link HookBus}
317
+ * @returns {HookBus}
318
+ */
319
+ export function createHookBus(opts) {
320
+ return new HookBus(opts);
321
+ }
322
+
323
+ /** @type {HookBus|null} */
324
+ let _singleton = null;
325
+
326
+ /**
327
+ * Return the process-wide HookBus, creating it on first use. This is the
328
+ * instance the send scripts, BaseAdapter, daemon and learning loop subscribe
329
+ * to so they all observe the same lifecycle.
330
+ *
331
+ * @param {object} [opts] applied ONLY when the singleton is first created
332
+ * @returns {HookBus}
333
+ */
334
+ export function getHookBus(opts) {
335
+ if (_singleton === null) _singleton = new HookBus(opts);
336
+ return _singleton;
337
+ }
338
+
339
+ /**
340
+ * Reset the process-wide singleton. Test-only escape hatch so a suite that
341
+ * touches the global bus can isolate itself.
342
+ */
343
+ export function resetHookBus() {
344
+ _singleton = null;
345
+ }
346
+
347
+ export default { HookBus, createHookBus, getHookBus, resetHookBus, HOOK_EVENTS, HOOK_EVENT_NAMES };