@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,989 @@
1
+ #!/usr/bin/env node
2
+ // =============================================================================
3
+ // Agent Daemon — Reactive event-driven message processor
4
+ // =============================================================================
5
+ //
6
+ // Persistent Node.js process that:
7
+ // 1. Polls Slack/Gmail/Calendar every 30s
8
+ // 2. Classifies each item via Haiku API (~0.5-1s)
9
+ // 3. Spawns dedicated claude --print sessions per item (up to 10 parallel)
10
+ // 4. Sweeps backlog queues every 2 min for stale items
11
+ //
12
+ // Replaces: poller + inbox-processor + backlog-executor (3-stage pipeline)
13
+ // Target: CEO DM → response in under 2 minutes
14
+ //
15
+ // Run: node scripts/daemon/<agent>-daemon.mjs (e.g. ravi-daemon.mjs)
16
+ // Install: launchd plist with KeepAlive: true
17
+ // =============================================================================
18
+
19
+ import { resolve, join } from "path";
20
+ import { readdirSync, readFileSync, renameSync, mkdirSync, appendFileSync, writeFileSync, unlinkSync } from "fs";
21
+
22
+ // Load .env before anything else. dotenv is present on every agent machine but
23
+ // is not a packaged dependency, so we import it dynamically + best-effort: this
24
+ // keeps production behaviour identical (the .env is loaded exactly as before)
25
+ // while letting the module be imported in CI/dev (no dotenv) for unit testing.
26
+ const AGENT_REPO_DIR = process.env.AGENT_DIR || resolve(new URL(".", import.meta.url).pathname, "../..");
27
+ try {
28
+ const { config } = await import("dotenv");
29
+ config({ path: join(AGENT_REPO_DIR, ".env") });
30
+ } catch { /* dotenv unavailable (CI/dev) — env already provided by the parent */ }
31
+
32
+ // Load agent identity (canonical SOT) so filters can match the running
33
+ // agent's own name/slack-id rather than a hardcoded one.
34
+ let _agent = null;
35
+ function loadAgent() {
36
+ if (_agent) return _agent;
37
+ try {
38
+ _agent = JSON.parse(readFileSync(join(AGENT_REPO_DIR, "config/agent.json"), "utf-8"));
39
+ } catch {
40
+ _agent = { firstName: "Agent", lastName: "", slackMemberId: "" };
41
+ }
42
+ return _agent;
43
+ }
44
+
45
+ import { pollSlack } from "../poller/slack-poller.mjs";
46
+ import { pollGmail } from "../poller/gmail-poller.mjs";
47
+ import { pollCalendar } from "../poller/calendar-poller.mjs";
48
+ import { pollVoice } from "../poller/voice-poller.mjs";
49
+ import { pollSecondaryGmail } from "../poller/secondary-gmail-poller.mjs";
50
+ import { makeInboxScanPoller } from "../poller/inbox-scan-poller.mjs";
51
+ import { existsSync as _existsSync } from "fs";
52
+ import { isPriorityItem } from "../poller/utils.mjs";
53
+ import { classifyItem, isDirectedAtAgent } from "./classifier.mjs";
54
+ import { dispatch, getStatus, availableSlots, canDispatchBacklog, resetActiveSessions } from "./dispatcher.mjs";
55
+ import { buildPrompt } from "./prompt-builder.mjs";
56
+ import { sendQuickResponse, sendHoldingMessage, isQuickReply } from "./responder.mjs";
57
+ import { recordPoll, recordClassification, recordSession, writeHealthDashboard } from "./health.mjs";
58
+ import { acquireLock, releaseLock, updateLock, scanStaleLocks, acquireThreadLock, claimRequest, hasActiveClaim, sweepStaleItemClaims, sanitiseItemId } from "./session-lock.mjs";
59
+ import { markDeferred } from "./inbox-deferral.mjs";
60
+ import { parseQueueItems } from "../../lib/backlog.mjs";
61
+ // Org shared-memory write-back (central store via memory.author / knowledge.append
62
+ // RPC). After a daemon turn completes cleanly we distil a one-line record of the
63
+ // work and land it in the org's shared, ACL'd store so the fleet's memory
64
+ // compounds. Fail-open + best-effort — never blocks or throws into the hot path;
65
+ // local SQLite/interaction logs remain the fail-open cache.
66
+ import { isEnabled as orgEnabled, loadOrgConfig } from "../../lib/org/client.mjs";
67
+ import { remember as orgRemember } from "../../lib/org/knowledge.mjs";
68
+ import { sweepSessionOutcomes } from "./session-outcomes.mjs";
69
+ // Observability spine (WS — diagnostics). mintTraceId + withTrace give each
70
+ // inbound item ONE trace_id that deep callees inherit via AsyncLocalStorage;
71
+ // emitEvent appends the canonical interaction hops (item_received → … → sent /
72
+ // error) to logs/diagnostics. counters.bump tallies fail-opens. getHookBus()
73
+ // is the in-process lifecycle bus we wire ONE real onGuardFail subscriber onto
74
+ // at init so the bus is live, not dead. ALL of these are fail-open by design
75
+ // (emitEvent/bump never throw), so this instrumentation can never take down the
76
+ // hot inbound path it observes.
77
+ import { mintTraceId, withTrace } from "../../lib/diagnostics/trace.mjs";
78
+ import { emitEvent, EVENT_TYPES } from "../../lib/diagnostics/events.mjs";
79
+ import * as counters from "../../lib/diagnostics/counters.mjs";
80
+ import { getHookBus } from "../../lib/hooks/bus.mjs";
81
+
82
+ // ---------------------------------------------------------------------------
83
+ // Configuration
84
+ // ---------------------------------------------------------------------------
85
+
86
+ const POLL_INTERVAL = parseInt(process.env.DAEMON_POLL_INTERVAL || "60000", 10); // 60s (up from 30s to avoid Slack rate limits)
87
+ // Backlog sweep cadence — bumped 2min → 10min in 1.10. The dispatcher
88
+ // applies a per-item post-completion cooldown (4h success, 30min failure)
89
+ // so we no longer need a tight sweep loop. Operators with high-throughput
90
+ // queues can override via DAEMON_BACKLOG_INTERVAL.
91
+ const BACKLOG_INTERVAL = parseInt(process.env.DAEMON_BACKLOG_INTERVAL || "600000", 10); // 10 min
92
+ const HEALTH_INTERVAL = 60000; // 1 min
93
+ // Note: dedup is now handled by file-based locks in session-lock.mjs
94
+
95
+ // ---------------------------------------------------------------------------
96
+ // Logging
97
+ // ---------------------------------------------------------------------------
98
+
99
+ function logDir() {
100
+ const dir = join(AGENT_REPO_DIR, "logs", "daemon");
101
+ mkdirSync(dir, { recursive: true });
102
+ return dir;
103
+ }
104
+
105
+ function today() {
106
+ return new Date().toISOString().split("T")[0];
107
+ }
108
+
109
+ function logEvent(type, entry) {
110
+ const path = join(logDir(), `${today()}-${type}.jsonl`);
111
+ appendFileSync(path, JSON.stringify({ timestamp: new Date().toISOString(), ...entry }) + "\n");
112
+ }
113
+
114
+ // ---------------------------------------------------------------------------
115
+ // Deduplication — now handled by file-based locks in session-lock.mjs
116
+ // ---------------------------------------------------------------------------
117
+
118
+ // ---------------------------------------------------------------------------
119
+ // Poll Loop
120
+ // ---------------------------------------------------------------------------
121
+
122
+ async function poll() {
123
+ const services = [
124
+ { name: "slack", fn: pollSlack },
125
+ { name: "gmail", fn: pollGmail },
126
+ { name: "alex-gmail", fn: pollSecondaryGmail },
127
+ { name: "calendar", fn: pollCalendar },
128
+ { name: "voice", fn: pollVoice },
129
+ ];
130
+
131
+ // WS2: channel-bus platforms (Telegram, Baileys WhatsApp) deliver events as
132
+ // inbox YAML via the daemon's channel loop. They have no API poller, so a
133
+ // generic inbox scanner drains their items into the same pipeline. Gate on
134
+ // the platform's config file so we don't scan dirs for disabled channels.
135
+ for (const [name, gate] of [["telegram", "config/telegram.yaml"], ["whatsapp", "config/whatsapp.yaml"], ["orgmail", "config/orgmail.yaml"]]) {
136
+ if (_existsSync(join(AGENT_REPO_DIR, gate))) {
137
+ services.push({ name, fn: makeInboxScanPoller(name, { agentRoot: AGENT_REPO_DIR }) });
138
+ }
139
+ }
140
+
141
+ let totalItems = 0;
142
+
143
+ for (const svc of services) {
144
+ try {
145
+ const result = await svc.fn();
146
+ // Dedup by raw_ref first: the same Slack message arrives via
147
+ // multiple paths (channel scan, thread scan, events-server JSON)
148
+ // with different item IDs. Using raw_ref as the lock key ensures
149
+ // a single message is only processed once regardless of path.
150
+ const seenRefs = new Set();
151
+ const newItems = result.items.filter((item) => {
152
+ // Skip the agent's own messages — defensive check in case intake filters miss them
153
+ const sender = (item.sender || "").toLowerCase();
154
+ const me = loadAgent();
155
+ const myFirst = (me.firstName || "").toLowerCase();
156
+ const myFull = (me.fullName || "").toLowerCase().replace(/\s+/g, "-");
157
+ if (myFirst && (sender === myFirst || sender === myFull)) return false;
158
+
159
+ const lockKey = item.raw_ref || item.id || `${svc.name}-${Date.now()}`;
160
+ if (seenRefs.has(lockKey)) return false;
161
+ seenRefs.add(lockKey);
162
+ const lock = acquireLock(lockKey, {
163
+ sender: item.sender || "unknown",
164
+ channel: item.channel || item.channel_id || "unknown",
165
+ });
166
+ return lock.acquired;
167
+ });
168
+ totalItems += newItems.length;
169
+
170
+ for (const item of newItems) {
171
+ await processItem(item, svc.name);
172
+ }
173
+
174
+ if (result.errors.length > 0) {
175
+ console.warn(`[daemon] ${svc.name} errors:`, result.errors.slice(0, 3));
176
+ }
177
+ } catch (err) {
178
+ console.error(`[daemon] ${svc.name} poll failed:`, err.message);
179
+ }
180
+ }
181
+
182
+ recordPoll(totalItems);
183
+ if (totalItems > 0) {
184
+ console.log(`[daemon] Poll found ${totalItems} new items`);
185
+ }
186
+
187
+ }
188
+
189
+ // ---------------------------------------------------------------------------
190
+ // Process a single item
191
+ // ---------------------------------------------------------------------------
192
+
193
+ /**
194
+ * Process a single inbound item end-to-end (classify → quick-reply or
195
+ * holding+session → finalize).
196
+ *
197
+ * @param {object} item the poller inbox item
198
+ * @param {string} service the source service ("slack" | "gmail" | ...)
199
+ * @param {object} [deps] test-only injectable collaborators. PRODUCTION PASSES
200
+ * NOTHING — the defaults are the real imports, so behaviour is byte-identical.
201
+ * Mirrors the existing seam idiom (setSpawnForTests / opts.runClaudeCLI). This
202
+ * exists ONLY so a test can drive the REAL instrumented body (trace minting,
203
+ * item_received → … → sent emission) without spawning a `claude` child.
204
+ * @param {Function} [deps.classify] override classifyItem
205
+ * @param {Function} [deps.dispatch] override dispatch
206
+ * @param {Function} [deps.sendQuickResponse] override sendQuickResponse
207
+ * @param {Function} [deps.sendHoldingMessage] override sendHoldingMessage
208
+ * @param {Function} [deps.isQuickReply] override isQuickReply
209
+ */
210
+ export async function processItem(item, service, deps = {}) {
211
+ const itemId = item.raw_ref || item.id || `${service}-${Date.now()}`;
212
+
213
+ // Observability: mint ONE trace_id per inbound item and run the whole
214
+ // processing body inside withTrace so every deep callee (classifier, dispatch,
215
+ // responder, the spawned session's close hook) inherits the same id via
216
+ // AsyncLocalStorage. The id is also carried explicitly onto the item so it
217
+ // survives the serialized dispatch + in-flight admission (a boundary that
218
+ // AsyncLocalStorage cannot cross). emitEvent is fail-open — it never throws.
219
+ const trace_id = mintTraceId();
220
+ item.trace_id = trace_id;
221
+ return withTrace({ trace_id, span: "root" }, () => processItemTraced(item, service, itemId, trace_id, deps));
222
+ }
223
+
224
+ async function processItemTraced(item, service, itemId, trace_id, deps = {}) {
225
+ // Resolve collaborators: real imports by default (production), injected fakes
226
+ // only when a test supplies them. Names match the imported symbols 1:1.
227
+ const _classify = deps.classify || classifyItem;
228
+ const _dispatch = deps.dispatch || dispatch;
229
+ const _sendQuickResponse = deps.sendQuickResponse || sendQuickResponse;
230
+ const _sendHoldingMessage = deps.sendHoldingMessage || sendHoldingMessage;
231
+ const _isQuickReply = deps.isQuickReply || isQuickReply;
232
+
233
+ emitEvent({
234
+ type: EVENT_TYPES.ITEM_RECEIVED,
235
+ trace_id,
236
+ attrs: { item_id: itemId, service, sender: item.sender || null, kind: item.kind || "message" },
237
+ });
238
+
239
+ try {
240
+ // Enrich item with channel-type metadata for directed-message detection
241
+ const channelStr = (item.channel || "").toLowerCase();
242
+ const channelId = item.channel_id || "";
243
+ const isDm = channelStr.startsWith("dm/") || channelId.startsWith("D");
244
+ const myFirstName = loadAgent().firstName || "Agent";
245
+ const agentThreadRegex = new RegExp(`^${myFirstName}:`, "m");
246
+ const agentInThread = !!(item.thread_context && agentThreadRegex.test(item.thread_context));
247
+ item.is_dm = isDm;
248
+ item.agent_in_thread = agentInThread;
249
+
250
+ // Classify via Haiku API
251
+ const classResult = await _classify({
252
+ sender: item.sender || "unknown",
253
+ sender_privilege: item.sender_privilege || item.priority_signals?.from_ceo ? "ceo" : "unknown",
254
+ channel: item.channel || "unknown",
255
+ service,
256
+ content: item.content || item.subject || "",
257
+ is_reply: item.is_reply || false,
258
+ thread_context: item.thread_context || null,
259
+ subject: item.subject || "",
260
+ is_dm: isDm,
261
+ is_group: !isDm && service === "slack",
262
+ agent_in_thread: agentInThread,
263
+ kind: item.kind || "message",
264
+ });
265
+
266
+ recordClassification(true);
267
+
268
+ logEvent("classifications", {
269
+ item_id: itemId,
270
+ sender: item.sender,
271
+ service,
272
+ ...classResult,
273
+ });
274
+
275
+ console.log(`[daemon] Classified: [${classResult.priority}] ${classResult.summary} → ${classResult.model} (${classResult.action})`);
276
+
277
+ // Skip ignored items
278
+ if (classResult.category === "ignore" || classResult.action === "ignore") {
279
+ markProcessed(item, service);
280
+ return;
281
+ }
282
+
283
+ // DIRECTED-MESSAGE GATE: In channels and group chats, only respond to
284
+ // messages that are clearly directed at the agent. DMs always pass.
285
+ // This prevents the agent from inserting itself into every conversation.
286
+ //
287
+ // Two-layer check:
288
+ // 1. If LLM says NOT directed → verify with rules (catch missed @mentions, CEO, DMs)
289
+ // 2. If LLM says directed BUT it's a non-DM channel → verify with rules (catch over-eager LLM)
290
+ if (service === "slack") {
291
+ const isDm = item.is_dm || (item.channel || "").startsWith("dm/") || (item.channel_id || "").startsWith("D");
292
+
293
+ if (!classResult.directed_at_agent) {
294
+ // LLM says not directed — double-check with rule-based heuristics
295
+ // to catch clear signals the LLM may have missed (DM, CEO, @mention)
296
+ const ruleCheck = isDirectedAtAgent(item);
297
+ if (!ruleCheck) {
298
+ console.log(`[daemon] Directed-message filter: skipping non-directed message from ${item.sender} in ${item.channel}`);
299
+ logEvent("classifications", {
300
+ item_id: itemId,
301
+ sender: item.sender,
302
+ service,
303
+ skipped: true,
304
+ reason: "not_directed_at_agent",
305
+ classifier_directed: false,
306
+ rule_directed: false,
307
+ summary: classResult.summary,
308
+ });
309
+ markProcessed(item, service);
310
+ return;
311
+ }
312
+ // Rule-based says directed — override LLM
313
+ console.log(`[daemon] Directed-message override: LLM said not directed but rule-based detected direction (${item.sender} in ${item.channel})`);
314
+ } else if (!isDm) {
315
+ // LLM says directed in a channel/group — sanity-check with rules.
316
+ // If rule-based also agrees, proceed. If rules say no AND the message
317
+ // doesn't contain the agent's name, the LLM was probably over-eager.
318
+ const ruleCheck = isDirectedAtAgent(item);
319
+ const content = (item.content || "").toLowerCase();
320
+ const me = loadAgent();
321
+ const myFirst = (me.firstName || "").toLowerCase();
322
+ const mySlackPrefix = (me.slackMemberId || "").slice(0, 3); // e.g. "U09"
323
+ const mentionsAgent =
324
+ (myFirst && content.includes(myFirst)) ||
325
+ (mySlackPrefix && content.includes(`<@${mySlackPrefix}`));
326
+ if (!ruleCheck && !mentionsAgent) {
327
+ console.log(`[daemon] Directed-message filter (LLM override): LLM said directed but rules disagree for ${item.sender} in ${item.channel} — skipping`);
328
+ logEvent("classifications", {
329
+ item_id: itemId,
330
+ sender: item.sender,
331
+ service,
332
+ skipped: true,
333
+ reason: "llm_over_eager_directed",
334
+ classifier_directed: true,
335
+ rule_directed: false,
336
+ summary: classResult.summary,
337
+ });
338
+ markProcessed(item, service);
339
+ return;
340
+ }
341
+ }
342
+ }
343
+
344
+ // THREAD / CHANNEL DEDUP: Prevent multiple sessions from responding to the
345
+ // same conversation. For thread replies, lock by thread_ts. For non-threaded
346
+ // DMs, lock by channel_id (so rapid-fire DMs don't each spawn a session).
347
+ // This check runs BEFORE quick reply to prevent ALL duplicate responses.
348
+ {
349
+ const channel = item.channel_id || (item.raw_ref ? item.raw_ref.match(/slack:([^:]+):/)?.[1] : null) || item.channel;
350
+ const threadTs = item.thread_id || (isDm ? `dm-channel` : null);
351
+ if (threadTs && channel) {
352
+ const threadCheck = acquireThreadLock(channel, threadTs);
353
+ if (!threadCheck.allowed) {
354
+ // Defer instead of dropping: rename to .deferred so the file
355
+ // is preserved and re-promoted to the live inbox when the
356
+ // active session for this channel releases its lock. The
357
+ // dispatcher calls promoteDeferred() from its session-close
358
+ // path; the next poll cycle then picks the latest deferred
359
+ // item up and dispatches a single session that sees the full
360
+ // thread context (older bursts are bundled).
361
+ //
362
+ // We also release the per-item lock that the poll filter
363
+ // acquired at the top of the cycle — otherwise the next
364
+ // poll's acquireLock would block re-processing of this
365
+ // exact item after we promote it back to live.
366
+ const deferred = markDeferred(item, service, AGENT_REPO_DIR);
367
+ releaseLock(itemId);
368
+ console.log(`[daemon] Thread/channel dedup: deferring item from ${item.sender} — ${threadCheck.reason}${deferred ? "" : " (no inbox file to defer)"}`);
369
+ logEvent("classifications", {
370
+ item_id: itemId,
371
+ sender: item.sender,
372
+ service,
373
+ deferred: deferred > 0,
374
+ reason: `thread_dedup: ${threadCheck.reason}`,
375
+ });
376
+ return;
377
+ }
378
+ }
379
+ }
380
+
381
+ // REQUEST CLAIMING: For action_required items, claim the request so
382
+ // parallel sessions (from overlapping poll cycles or backlog sweep)
383
+ // don't independently process the same "email me the brief" request.
384
+ // Claim key: action_type + recipient channel + summary (normalized).
385
+ if (classResult.category === "action_required" && (classResult.action === "respond" || classResult.action === "draft" || classResult.action === "research")) {
386
+ const claimAttrs = {
387
+ recipient: item.channel_id || item.channel || item.sender || "unknown",
388
+ subject: classResult.summary || item.subject || "",
389
+ action_type: classResult.action,
390
+ };
391
+ const claim = claimRequest(claimAttrs);
392
+ if (!claim.claimed) {
393
+ console.log(`[daemon] Request claim denied for ${item.sender}: ${claim.reason}`);
394
+ logEvent("classifications", {
395
+ item_id: itemId,
396
+ sender: item.sender,
397
+ service,
398
+ skipped: true,
399
+ reason: `request_claim_denied: ${claim.reason}`,
400
+ summary: classResult.summary,
401
+ });
402
+ markProcessed(item, service);
403
+ return;
404
+ }
405
+ }
406
+
407
+ // QUICK REPLY PATH: Direct responses via API + Slack/Gmail posting.
408
+ // No claude --print session needed. ~4-8 seconds total.
409
+ if (_isQuickReply(classResult)) {
410
+ console.log(`[daemon] Quick reply path for ${item.sender} (${classResult.model})`);
411
+ const result = await _sendQuickResponse(item, classResult);
412
+ if (result.sent) {
413
+ markProcessed(item, service);
414
+ return;
415
+ }
416
+ // If quick reply failed to send or was blocked by validation, fall through to dispatch a full session
417
+ const reason = result.blocked ? `validation blocked: ${result.issues?.map(i => i.rule).join(", ")}` : "send failed";
418
+ console.warn(`[daemon] Quick reply not sent (${reason}), falling through to session dispatch`);
419
+ }
420
+
421
+ // COMPLEX WORK PATH: Send immediate holding message, then dispatch full session
422
+ let holdingText = null;
423
+ try {
424
+ if (classResult.action === "respond" || classResult.action === "draft" || classResult.action === "research") {
425
+ const holdResult = await _sendHoldingMessage(item, classResult);
426
+ holdingText = holdResult.sent ? holdResult.holdingText : null;
427
+ if (holdResult.sent) {
428
+ updateLock(itemId, { holdingSent: true });
429
+ }
430
+ }
431
+ } catch (err) {
432
+ console.error(`[daemon] Holding message failed (non-fatal): ${err.message}`);
433
+ }
434
+
435
+ // Build prompt with holding message context and dispatch
436
+ const prompt = await buildPrompt(item, classResult, {
437
+ type: "inbox",
438
+ holdingMessage: holdingText,
439
+ });
440
+ // F1/H2: record a DURABLE in-flight admission and DEFER markProcessed() to
441
+ // the dispatch onClose SUCCESS path. The previous code marked the item
442
+ // `.processed` immediately after this fire-and-forget dispatch — a crash
443
+ // before the spawned session closed lost the user's message. Now a clean
444
+ // close finalizes; any failure (non-zero close / spawn error) clears the
445
+ // admission and releases the per-item lock so the un-`.processed` item is
446
+ // re-delivered on the next poll; a crash mid-flight leaves the admission for
447
+ // recoverInFlight() to re-deliver on restart.
448
+ recordInFlight(item, service);
449
+ _dispatch(prompt, item, classResult, "inbox", {
450
+ onClose: ({ ok, code, sessionId, stdout }) => {
451
+ if (ok) {
452
+ markProcessed(item, service);
453
+ clearInFlight(item);
454
+ // Post-session write-back: land a one-line distillation of the work
455
+ // into the org's shared store so the fleet's memory compounds.
456
+ // Best-effort + fail-open — fire-and-forget, never blocks finalize.
457
+ Promise.resolve(writeBackSession(item, classResult, { sessionId }))
458
+ .catch(() => { /* fail-open */ });
459
+ // Post-session OUTCOME DISCIPLINE (parity with hq's meeting chair): a
460
+ // turn that committed to next steps must not leave them in a session
461
+ // log. Capture them off the turn's final text and land them through the
462
+ // org protocol (board.create + knowledge.append). OFF unless
463
+ // DAEMON_SESSION_OUTCOMES=1; idempotent + fail-open; fire-and-forget.
464
+ Promise.resolve(sweepOutcomesForSession({ sessionId, stdout, item, classResult }))
465
+ .catch(() => { /* fail-open */ });
466
+ // Finalize (interaction end): emit `sent` carrying the SAME trace_id
467
+ // minted at item_received, so the full hop chain (item_received → … →
468
+ // sent) groups by one stable id. The session itself posts the actual
469
+ // user-facing reply via the Slack/Gmail APIs; this row marks the clean
470
+ // close of the work the user's message kicked off.
471
+ emitEvent({ type: EVENT_TYPES.SENT, trace_id, attrs: { item_id: itemId, service, exit_code: code } });
472
+ } else {
473
+ // Leave the inbox file un-`.processed` (re-deliverable). Drop the
474
+ // admission + release the per-item lock so the next poll re-delivers
475
+ // immediately rather than waiting out the stale-lock TTL.
476
+ clearInFlight(item);
477
+ const key = inflightKey(item);
478
+ if (key) releaseLock(key);
479
+ emitEvent({ type: EVENT_TYPES.ERROR, trace_id, attrs: { item_id: itemId, service, exit_code: code, stage: "session_close" } });
480
+ }
481
+ },
482
+ });
483
+ recordSession(false); // spawned
484
+
485
+ } catch (err) {
486
+ console.error(`[daemon] Failed to process item ${itemId}:`, err.message);
487
+ recordClassification(false);
488
+ emitEvent({ type: EVENT_TYPES.ERROR, trace_id, attrs: { item_id: itemId, service, stage: "process_item", error: err.message } });
489
+ }
490
+ }
491
+
492
+ // ---------------------------------------------------------------------------
493
+ // Hook-bus wiring — register the ONE real lifecycle subscriber.
494
+ // ---------------------------------------------------------------------------
495
+ //
496
+ // The bus (lib/hooks/bus.mjs) exists so cross-cutting controls become SUBSCRIBERS
497
+ // to well-known lifecycle events instead of bespoke wiring. We register a real
498
+ // onGuardFail subscriber here so the bus is live with at least one consumer: each
499
+ // emit("onGuardFail", ctx) — fired wherever a guard fails open — bumps the
500
+ // durable `guard.fail` counter doctor reads. Idempotent: a module-level flag
501
+ // prevents double-subscription if init runs twice (re-import in tests, etc.).
502
+
503
+ let _hookSubscribersRegistered = false;
504
+
505
+ /**
506
+ * Wire the daemon's lifecycle-bus subscribers. Exported + idempotent so a test
507
+ * can drive it directly and assert the subscriber fires, without spinning the
508
+ * poll loops. Best-effort: a subscribe failure is swallowed (the bus is
509
+ * fail-open; a missing subscriber degrades to "uncounted", never a crash).
510
+ *
511
+ * @returns {boolean} true if the subscriber is now registered
512
+ */
513
+ export function registerHookSubscribers() {
514
+ if (_hookSubscribersRegistered) return true;
515
+ try {
516
+ getHookBus().subscribe("onGuardFail", (ctx) => counters.bump("guard.fail", ctx));
517
+ _hookSubscribersRegistered = true;
518
+ return true;
519
+ } catch (err) {
520
+ console.warn(`[daemon] Failed to register hook subscribers: ${err.message}`);
521
+ return false;
522
+ }
523
+ }
524
+
525
+ // ---------------------------------------------------------------------------
526
+ // Post-session org-memory write-back — land a short distillation of the work
527
+ // this turn did into the org's shared, ACL'd store (central store via the
528
+ // existing memory.author / knowledge.append RPC). Fail-open + best-effort:
529
+ // disabled/unreachable ⇒ a benign no-op; never throws into the dispatch path.
530
+ // Local interaction logs / SQLite remain the fail-open cache.
531
+ // ---------------------------------------------------------------------------
532
+
533
+ // Cached org config (config/org.yaml) — read once per process. undefined until loaded.
534
+ let _cachedDaemonOrgCfg;
535
+
536
+ function daemonOrgCfg() {
537
+ if (_cachedDaemonOrgCfg !== undefined) return _cachedDaemonOrgCfg;
538
+ try {
539
+ _cachedDaemonOrgCfg = loadOrgConfig(AGENT_REPO_DIR) || {};
540
+ } catch {
541
+ _cachedDaemonOrgCfg = {};
542
+ }
543
+ return _cachedDaemonOrgCfg;
544
+ }
545
+
546
+ /** For tests: reset the cached org config so a fresh config/org.yaml is re-read. */
547
+ export function _resetDaemonOrgCfgCache() {
548
+ _cachedDaemonOrgCfg = undefined;
549
+ }
550
+
551
+ // A distillation longer than this is truncated before it lands in the store —
552
+ // the shared memory holds a salient one-liner, never a transcript.
553
+ const SESSION_DISTILL_MAX_CHARS = 400;
554
+
555
+ /**
556
+ * Distil a completed turn into one salient line for the shared store. Pure +
557
+ * bounded; draws only on the item + classification the onClose closure holds
558
+ * (the session's own actions are audited separately). Returns null when there is
559
+ * nothing worth remembering.
560
+ *
561
+ * @param {object} item inbox item that was processed (may be null for backlog)
562
+ * @param {object} classResult classifier output
563
+ * @param {object} [opts] - { agent?, queueItem? } — injectable for tests
564
+ * @returns {string|null}
565
+ */
566
+ export function sessionDistillation(item, classResult, opts = {}) {
567
+ const agent = opts.agent || loadAgent();
568
+ const who = (agent && agent.firstName) ? agent.firstName : "The agent";
569
+ const action = (classResult && classResult.action) || "handled";
570
+ const sender = (item && item.sender) ? String(item.sender) : null;
571
+ const queueItem = opts.queueItem;
572
+ const topic =
573
+ (classResult && classResult.summary) ||
574
+ (item && item.subject) ||
575
+ (queueItem && queueItem.title) ||
576
+ (item && item.content ? String(item.content).slice(0, 160) : null);
577
+ if (!topic && !sender) return null;
578
+
579
+ const parts = [`${who} ${action}`];
580
+ if (sender) parts.push(`a request from ${sender}`);
581
+ if (topic) parts.push(`re: ${String(topic).trim()}`);
582
+ let text = `${parts.join(" ")} (${today()}).`;
583
+ if (text.length > SESSION_DISTILL_MAX_CHARS) {
584
+ text = text.slice(0, SESSION_DISTILL_MAX_CHARS - 1) + "…";
585
+ }
586
+ return text;
587
+ }
588
+
589
+ /**
590
+ * Write a completed turn's distillation to the org's shared store. Fail-open:
591
+ * disabled/unreachable/empty ⇒ `{ok:false, skipped:true}`; never throws. The
592
+ * server assigns the fact's group/ACL from the caller's verified identity — we
593
+ * pass no group hint, so it lands in the caller's default org scope.
594
+ *
595
+ * @param {object} item
596
+ * @param {object} classResult
597
+ * @param {object} [opts] - { cfg?, rememberImpl?, agent?, queueItem?, sessionId? } — injectable for tests
598
+ * @returns {Promise<{ok:boolean, skipped?:boolean}>}
599
+ */
600
+ export async function writeBackSession(item, classResult, opts = {}) {
601
+ try {
602
+ const cfg = opts.cfg !== undefined ? opts.cfg : daemonOrgCfg();
603
+ if (!orgEnabled(cfg)) return { ok: false, skipped: true };
604
+ const text = sessionDistillation(item, classResult, opts);
605
+ if (!text) return { ok: false, skipped: true };
606
+ const rememberImpl = opts.rememberImpl || orgRemember;
607
+ return await rememberImpl(text, {
608
+ cfg,
609
+ kind: "episode",
610
+ provenance: { source: "daemon-session", sessionId: opts.sessionId || null },
611
+ });
612
+ } catch {
613
+ // Fail-open: the shared-memory write must never take down the dispatch path.
614
+ return { ok: false, skipped: true };
615
+ }
616
+ }
617
+
618
+ /**
619
+ * Post-session OUTCOME DISCIPLINE — the daemon's parity with hq's meeting chair.
620
+ *
621
+ * hq guarantees that a live call's action points become real board rows, with a
622
+ * teardown sweep for the ones the chair captured but never wrote. A daemon turn
623
+ * had no such guarantee: it could commit to next steps that then existed only in
624
+ * `state/sessions/logs/<id>.log`.
625
+ *
626
+ * This is the DRIVEN half — the dispatcher's close path calls it, so the
627
+ * guarantee does not depend on the model remembering to write anything down. The
628
+ * work itself (capture → board.create + knowledge.append) lives in
629
+ * ./session-outcomes.mjs; this wrapper only supplies the daemon's own collaborators
630
+ * (org config + agent identity).
631
+ *
632
+ * OFF unless `DAEMON_SESSION_OUTCOMES=1`. Idempotent + fail-open; never throws.
633
+ *
634
+ * @param {object} a - { sessionId, stdout?, item?, classResult? } plus test seams
635
+ * ({ cfg?, enabled?, agent?, createItemImpl?, rememberImpl? })
636
+ * @returns {Promise<{created:number, captured?:number, skipped?:boolean, reason?:string}>}
637
+ */
638
+ export async function sweepOutcomesForSession(a = {}) {
639
+ try {
640
+ const cfg = a.cfg !== undefined ? a.cfg : daemonOrgCfg();
641
+ let agentName = a.agentName;
642
+ if (!agentName) {
643
+ const agent = a.agent || loadAgent();
644
+ agentName = (agent && agent.firstName) || "The agent";
645
+ }
646
+ return await sweepSessionOutcomes({ ...a, cfg, agentName });
647
+ } catch {
648
+ // Fail-open: outcome landing must never take down the dispatch path.
649
+ return { created: 0, skipped: true, reason: "error" };
650
+ }
651
+ }
652
+
653
+ // ---------------------------------------------------------------------------
654
+ // Mark inbox item as processed
655
+ // ---------------------------------------------------------------------------
656
+
657
+ export function markProcessed(item, service) {
658
+ // The pollers write files to state/inbox/{service}/
659
+ // We mark them by renaming to .processed
660
+ try {
661
+ const inboxDir = join(AGENT_REPO_DIR, "state", "inbox", service);
662
+ const files = readdirSync(inboxDir).filter(
663
+ (f) => !f.endsWith(".processed") && (f.includes(item.id) || f.includes(item.raw_ref))
664
+ );
665
+ for (const file of files) {
666
+ const src = join(inboxDir, file);
667
+ const dst = join(inboxDir, file + ".processed");
668
+ renameSync(src, dst);
669
+ }
670
+ } catch {
671
+ // Not all items have inbox files (e.g. backlog items)
672
+ }
673
+ }
674
+
675
+ // ---------------------------------------------------------------------------
676
+ // In-flight admission — durable record of an inbox item handed to a spawned
677
+ // session, so the F1/H2 message-loss window is closed.
678
+ // ---------------------------------------------------------------------------
679
+ //
680
+ // Previously processItem() called markProcessed() (rename → .processed)
681
+ // IMMEDIATELY after the fire-and-forget dispatch(). A crash before the spawned
682
+ // session closed left the item `.processed` with redelivery blocked → the user's
683
+ // message was lost.
684
+ //
685
+ // Now: after dispatching a session we write a durable in-flight admission file
686
+ // and DEFER markProcessed() to the dispatch onClose success path. On a clean
687
+ // close we finalize (markProcessed + clear the admission). On a non-zero close
688
+ // or spawn error we clear the admission and release the per-item lock, leaving
689
+ // the un-`.processed` inbox file re-deliverable on the next poll. On a crash
690
+ // mid-flight the admission file survives; recoverInFlight() (startup) releases
691
+ // the held per-item lock so the still-un-`.processed` item is re-polled.
692
+ //
693
+ // Idempotency: a re-delivered item that actually completed before the crash is
694
+ // caught by the existing dedup layers (per-item lock on re-poll, thread/channel
695
+ // lock, request-claim, and the sent-message registry) — so redelivery never
696
+ // double-acts.
697
+
698
+ function inflightDir() {
699
+ const dir = join(AGENT_REPO_DIR, "state", "inbox-inflight");
700
+ mkdirSync(dir, { recursive: true });
701
+ return dir;
702
+ }
703
+
704
+ /** Stable per-item key: same precedence as the daemon's acquireLock + the
705
+ * dispatcher's lock release (raw_ref first, then id), so recovery can release
706
+ * the exact lock that is gating redelivery. */
707
+ function inflightKey(item) {
708
+ return item.raw_ref || item.id || null;
709
+ }
710
+
711
+ /**
712
+ * Record a durable in-flight admission for an inbox item whose session was just
713
+ * dispatched. Best-effort: a write failure only means this item won't be
714
+ * auto-recovered after a crash (it still finalizes normally on close) — never
715
+ * throws into the dispatch path.
716
+ */
717
+ export function recordInFlight(item, service) {
718
+ const key = inflightKey(item);
719
+ if (!key) return;
720
+ try {
721
+ const path = join(inflightDir(), `${sanitiseItemId(key)}.json`);
722
+ const tmp = path + ".tmp";
723
+ writeFileSync(tmp, JSON.stringify({
724
+ lockKey: key,
725
+ itemId: item.id || null,
726
+ raw_ref: item.raw_ref || null,
727
+ service,
728
+ sender: item.sender || null,
729
+ channel: item.channel || item.channel_id || null,
730
+ // Carry the interaction's trace_id onto the durable admission so a
731
+ // post-crash recovery (or an audit join) can correlate the re-delivered
732
+ // item back to its original interaction. Cheap + additive.
733
+ trace_id: item.trace_id || null,
734
+ admittedAt: new Date().toISOString(),
735
+ }, null, 2));
736
+ renameSync(tmp, path);
737
+ } catch (err) {
738
+ console.warn(`[daemon] Failed to record in-flight admission for ${key}: ${err.message}`);
739
+ }
740
+ }
741
+
742
+ /** Clear the in-flight admission once a dispatched session reaches a terminal
743
+ * state (success → finalized; failure → left re-deliverable). Best-effort. */
744
+ export function clearInFlight(item) {
745
+ const key = inflightKey(item);
746
+ if (!key) return;
747
+ try { unlinkSync(join(inflightDir(), `${sanitiseItemId(key)}.json`)); }
748
+ catch { /* already gone */ }
749
+ }
750
+
751
+ /**
752
+ * On startup, recover inbox items that were mid-flight when the daemon
753
+ * crashed/rebooted. For each surviving admission file we release the per-item
754
+ * lock that would otherwise gate redelivery for the full stale-lock TTL, then
755
+ * delete the admission file. The inbox file itself was never marked `.processed`
756
+ * (markProcessed is deferred to clean close), so the next poll re-delivers it
757
+ * through the normal pipeline — where the dedup layers prevent any double-act if
758
+ * the work had in fact completed before the crash.
759
+ *
760
+ * @returns {number} count of in-flight admissions recovered
761
+ */
762
+ export function recoverInFlight() {
763
+ let recovered = 0;
764
+ let files = [];
765
+ try {
766
+ files = readdirSync(inflightDir()).filter((f) => f.endsWith(".json") && !f.endsWith(".tmp"));
767
+ } catch { return 0; }
768
+ for (const file of files) {
769
+ const path = join(inflightDir(), file);
770
+ let rec = null;
771
+ try { rec = JSON.parse(readFileSync(path, "utf-8")); } catch { /* corrupt */ }
772
+ try {
773
+ if (rec && rec.lockKey) releaseLock(rec.lockKey);
774
+ unlinkSync(path);
775
+ recovered++;
776
+ } catch { /* best-effort */ }
777
+ }
778
+ if (recovered > 0) {
779
+ console.log(`[daemon] Recovered ${recovered} in-flight inbox item(s) for redelivery`);
780
+ }
781
+ return recovered;
782
+ }
783
+
784
+ // ---------------------------------------------------------------------------
785
+ // Backlog Sweep
786
+ // ---------------------------------------------------------------------------
787
+
788
+ async function sweepBacklog() {
789
+ const slots = availableSlots();
790
+ if (slots <= 0) return; // No capacity
791
+
792
+ try {
793
+ const queueDir = join(AGENT_REPO_DIR, "state", "queues");
794
+ const files = readdirSync(queueDir).filter((f) => f.endsWith(".yaml"));
795
+ const actionableItems = [];
796
+
797
+ for (const file of files) {
798
+ try {
799
+ const content = readFileSync(join(queueDir, file), "utf-8");
800
+ // Parse via the shared, tested parser (lib/backlog.mjs). The previous
801
+ // inline parser never extracted the item `id` (and split only on
802
+ // column-0 dashes, folding js-yaml's indented ` - id:` items into one
803
+ // block) — so qi.id was always undefined and the entire file-based
804
+ // item-claim subsystem (hasActiveClaim/claimItem) was dead. (0.8 / F2)
805
+ actionableItems.push(...parseQueueItems(content, file));
806
+ } catch {
807
+ // Skip unparseable queue files
808
+ }
809
+ }
810
+
811
+ // Sort: critical first, then high, then normal
812
+ const priorityOrder = { critical: 0, high: 1, normal: 2, low: 3 };
813
+ actionableItems.sort((a, b) => (priorityOrder[a.priority] || 3) - (priorityOrder[b.priority] || 3));
814
+
815
+ // Filter out items that already have active sessions or exceeded retries
816
+ const dispatchable = actionableItems.filter((qi) => {
817
+ const check = canDispatchBacklog(qi);
818
+ if (!check.allowed) {
819
+ if (check.reason === "max_retries_exceeded") {
820
+ console.log(`[daemon] Backlog skip: "${qi.title}" — retries exhausted`);
821
+ }
822
+ return false;
823
+ }
824
+
825
+ // File-based item claim check — survives daemon restart and is visible
826
+ // to concurrent launchd triggers. Complements in-memory activeBacklogKeys.
827
+ // (ib-20260407-001b: concurrent session coordination)
828
+ if (qi.id && hasActiveClaim(qi.id)) {
829
+ console.log(`[daemon] Backlog skip: "${qi.title}" — item claimed by another session`);
830
+ return false;
831
+ }
832
+
833
+ return true;
834
+ });
835
+
836
+ // Dispatch top items up to available slots
837
+ const toDispatch = dispatchable.slice(0, Math.min(slots, 5));
838
+ let dispatched = 0;
839
+
840
+ for (const queueItem of toDispatch) {
841
+ const classResult = {
842
+ priority: queueItem.priority,
843
+ action: "queue",
844
+ model: queueItem.priority === "critical" ? "opus" : "sonnet",
845
+ summary: queueItem.title,
846
+ category: "action_required",
847
+ };
848
+
849
+ const prompt = await buildPrompt(null, classResult, {
850
+ type: "backlog",
851
+ queueItem,
852
+ });
853
+
854
+ // Backlog turns commit to next steps just as readily as inbox turns, so
855
+ // they get the same outcome discipline. This onClose does NOT touch the
856
+ // backlog finalize path (the session updates its own queue item) — it is
857
+ // purely the outcome sweep, gated off by default.
858
+ dispatch(prompt, queueItem, classResult, "backlog", {
859
+ onClose: ({ ok, sessionId, stdout }) => {
860
+ if (!ok) return;
861
+ Promise.resolve(sweepOutcomesForSession({ sessionId, stdout, item: queueItem, classResult }))
862
+ .catch(() => { /* fail-open */ });
863
+ },
864
+ });
865
+ recordSession(false);
866
+ dispatched++;
867
+ }
868
+
869
+ if (dispatched > 0) {
870
+ console.log(`[daemon] Backlog sweep: dispatched ${dispatched} items (${actionableItems.length - dispatchable.length} skipped: in-flight or exhausted)`);
871
+ }
872
+ } catch (err) {
873
+ console.error(`[daemon] Backlog sweep error:`, err.message);
874
+ }
875
+ }
876
+
877
+ // ---------------------------------------------------------------------------
878
+ // Main
879
+ // ---------------------------------------------------------------------------
880
+
881
+ async function main() {
882
+ console.log("╔══════════════════════════════════════════════════════════╗");
883
+ console.log(`║ ${(loadAgent().firstName || "Agent").padEnd(8)} Daemon — Reactive Event Processor ║`);
884
+ console.log("╠══════════════════════════════════════════════════════════╣");
885
+ console.log(`║ Directory: ${AGENT_REPO_DIR}`);
886
+ console.log(`║ Poll: every ${POLL_INTERVAL / 1000}s`);
887
+ console.log(`║ Backlog: every ${BACKLOG_INTERVAL / 1000}s`);
888
+ console.log(`║ Concurrency: up to ${process.env.DAEMON_MAX_CONCURRENT || 10} parallel sessions`);
889
+ console.log("╚══════════════════════════════════════════════════════════╝");
890
+
891
+ // Observability: register the ONE real hook-bus subscriber so the lifecycle
892
+ // bus is LIVE (not a dead seam). Every onGuardFail emit (where a guard fails
893
+ // open elsewhere in the pipeline) increments the durable `guard.fail` counter
894
+ // that doctor/operators read. subscribe + bump are both fail-open; an init
895
+ // failure here can never stop the daemon coming up.
896
+ registerHookSubscribers();
897
+
898
+ // Check for emergency stop
899
+ try {
900
+ readFileSync(join(AGENT_REPO_DIR, ".emergency-stop"));
901
+ console.error("[daemon] Emergency stop active — exiting");
902
+ process.exit(0);
903
+ } catch {
904
+ // No emergency stop — continue
905
+ }
906
+
907
+ // Clear orphaned state from prior daemon instances
908
+ const staleCleared = scanStaleLocks();
909
+ if (staleCleared > 0) {
910
+ console.log(`[daemon] Cleared ${staleCleared} stale session locks`);
911
+ }
912
+ // F1/H2: re-deliver inbox items that were mid-flight when this box crashed.
913
+ // Each surviving in-flight admission's per-item lock is released so the
914
+ // still-un-`.processed` file is re-polled (dedup layers prevent double-acting
915
+ // if the work had completed). Runs BEFORE the first poll so recovery wins the
916
+ // race against re-acquisition.
917
+ recoverInFlight();
918
+ // WS4: on startup, reconcile in-flight session resume markers — re-dispatch
919
+ // `claude --print --resume` for any work that was mid-flight when the box
920
+ // rebooted/lost power (3-strike cap → status:blocked). Shutdown paths below
921
+ // call resetActiveSessions() WITHOUT reconcile so we never re-spawn work we
922
+ // are deliberately stopping.
923
+ resetActiveSessions({ reconcile: true });
924
+
925
+ // Initial poll immediately
926
+ await poll();
927
+
928
+ // Poll loop
929
+ setInterval(async () => {
930
+ try {
931
+ // Check emergency stop
932
+ try { readFileSync(join(AGENT_REPO_DIR, ".emergency-stop")); process.exit(0); } catch {}
933
+ await poll();
934
+ } catch (err) {
935
+ console.error("[daemon] Poll loop error:", err.message);
936
+ }
937
+ }, POLL_INTERVAL);
938
+
939
+ // Backlog sweep
940
+ setInterval(async () => {
941
+ try {
942
+ await sweepBacklog();
943
+ } catch (err) {
944
+ console.error("[daemon] Backlog sweep error:", err.message);
945
+ }
946
+ }, BACKLOG_INTERVAL);
947
+
948
+ // Health dashboard + stale claim sweep
949
+ setInterval(() => {
950
+ try {
951
+ writeHealthDashboard();
952
+ // Sweep stale item claims (ib-20260407-001b: concurrent session coordination)
953
+ const claimsSwept = sweepStaleItemClaims();
954
+ if (claimsSwept > 0) {
955
+ console.log(`[daemon] Swept ${claimsSwept} stale item claims`);
956
+ }
957
+ } catch (err) {
958
+ console.error("[daemon] Health write error:", err.message);
959
+ }
960
+ }, HEALTH_INTERVAL);
961
+
962
+ // Graceful shutdown — clean up active.json so next startup doesn't see stale sessions
963
+ process.on("SIGTERM", () => {
964
+ console.log("[daemon] SIGTERM received, shutting down gracefully");
965
+ resetActiveSessions();
966
+ writeHealthDashboard();
967
+ process.exit(0);
968
+ });
969
+
970
+ process.on("SIGINT", () => {
971
+ console.log("[daemon] SIGINT received, shutting down");
972
+ resetActiveSessions();
973
+ writeHealthDashboard();
974
+ process.exit(0);
975
+ });
976
+
977
+ console.log("[daemon] Running. Ctrl+C to stop.");
978
+ }
979
+
980
+ // Autostart on import (production: launched directly via `node agent-daemon.mjs`
981
+ // or imported by maestro-daemon.mjs — both expect main() to run). A test may set
982
+ // MAESTRO_DAEMON_NO_AUTOSTART before importing to exercise the exported helpers
983
+ // (recoverInFlight, etc.) in isolation without spinning up the poll loops.
984
+ if (!process.env.MAESTRO_DAEMON_NO_AUTOSTART) {
985
+ main().catch((err) => {
986
+ console.error("[daemon] Fatal:", err);
987
+ process.exit(1);
988
+ });
989
+ }