@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,854 @@
1
+ /**
2
+ * responder.mjs — Quick response layer
3
+ *
4
+ * Handles two scenarios with a single short-lived `claude --print` call:
5
+ *
6
+ * 1. SIMPLE REPLIES: Invokes the Claude Code CLI to generate a response,
7
+ * then posts directly via Slack/Gmail API.
8
+ *
9
+ * 2. HOLDING MESSAGES: For complex items that need a full session,
10
+ * generates and sends an immediate acknowledgment so the sender
11
+ * knows it's being worked on.
12
+ *
13
+ * Migrated off `@anthropic-ai/sdk` per CEO directive (Slack DM
14
+ * D099N1JGKRQ, 2026-04-27 09:38Z + 11:33Z): all agent daemon model
15
+ * calls must funnel through Claude Code CLI sessions (Max
16
+ * subscription), not the Anthropic API.
17
+ */
18
+
19
+ import { readFileSync, writeFileSync, readdirSync, appendFileSync, mkdirSync } from "fs";
20
+ import { execFileSync, spawn } from "child_process";
21
+ import { join } from "path";
22
+ import { randomUUID } from "crypto";
23
+ import { checkRecentlySent, registerSent } from "./session-lock.mjs";
24
+ import { routingKey as deriveRoutingKey, createRouter } from "./lib/session-router.mjs";
25
+ // Permission scoping (security CRITICAL / audit H1). sessionPermissionArgs()
26
+ // preserves the historical "--dangerously-skip-permissions" by default and only
27
+ // scopes tools when the operator opts in (MAESTRO_SCOPED_PERMISSIONS=1), so the
28
+ // quick-reply / holding spawn path honours the same gate as every other spawn.
29
+ import { sessionPermissionArgs } from "../../lib/session-permissions.mjs";
30
+ import { screenOutbound } from "../../lib/comms/send-gate.mjs";
31
+ // Observability spine (WS — diagnostics). emitEvent("sent") marks a successful
32
+ // quick-reply / holding send (interaction-end on the quick path); counters.bump
33
+ // ("send.blocked") tallies a guard / send-gate / dedup block. When the send-gate
34
+ // itself fails OPEN (infra error on an internal Slack channel) we emit
35
+ // "onGuardFail" on the lifecycle bus so the daemon's real subscriber counts it —
36
+ // proving the bus is live with a real consumer. All fail-open (never throw).
37
+ import { emitEvent, EVENT_TYPES } from "../../lib/diagnostics/events.mjs";
38
+ import * as counters from "../../lib/diagnostics/counters.mjs";
39
+ import { getHookBus } from "../../lib/hooks/bus.mjs";
40
+
41
+ const AGENT_REPO_DIR = process.env.AGENT_DIR || join(new URL(".", import.meta.url).pathname, "../..");
42
+ const SONNET_MODEL = "claude-sonnet-4-6";
43
+ // Resolve claude against the agent's PATH (not launchd's bare env).
44
+ import { resolveClaudeBin, augmentedPath, daemonClaudeArgs } from "../../lib/claude-bin.mjs";
45
+ const CLAUDE_BIN = resolveClaudeBin();
46
+ const CLAUDE_CLI_TIMEOUT_MS = 60_000;
47
+ const SESSION_REGISTRY_PATH = join(AGENT_REPO_DIR, "state", "daemon", "session-router-registry.json");
48
+
49
+ // Identity loaded from config/agent.json (canonical SOT). Fall back to a
50
+ // neutral placeholder if the file isn't present — the daemon must still
51
+ // run even on a partially-configured repo.
52
+ let _agent = null;
53
+ function loadAgent() {
54
+ if (_agent) return _agent;
55
+ try {
56
+ _agent = JSON.parse(readFileSync(join(AGENT_REPO_DIR, "config/agent.json"), "utf-8"));
57
+ } catch {
58
+ _agent = { firstName: "Agent", fullName: "Agent", title: "agent", principal: { fullName: "the principal" }, company: "the company" };
59
+ }
60
+ return _agent;
61
+ }
62
+
63
+ // Singleton router — lazily created on first generateResponse() call. The
64
+ // scaffold's createRouter is async (eager registry read), so we cache the
65
+ // promise and await it inside generateResponse.
66
+ let routerPromise = null;
67
+ function getRouter() {
68
+ if (!routerPromise) {
69
+ routerPromise = createRouter({ registryPath: SESSION_REGISTRY_PATH });
70
+ }
71
+ return routerPromise;
72
+ }
73
+
74
+ /**
75
+ * Translate a daemon-shaped item into the source/channel/thread_ts shape
76
+ * that session-router's routingKey() expects.
77
+ *
78
+ * Daemon items use {service, channel, thread_id, sender_email, ...}; the
79
+ * router's pure key fn was specced against {source, channel, thread_ts,
80
+ * thread_id, ...} per memo §4.2. This adapter is the seam between them.
81
+ *
82
+ * Returns null for items the router can't key (e.g. service we don't yet
83
+ * support, or missing required fields). Caller falls back to a fresh
84
+ * pre-minted UUID + EPHEMERAL semantics.
85
+ */
86
+ function deriveRouterItem(item) {
87
+ if (!item || typeof item !== "object") return null;
88
+
89
+ if (item.service === "slack") {
90
+ const channel = item.channel || item.channel_id;
91
+ if (!channel) return null;
92
+ return {
93
+ source: "slack",
94
+ channel,
95
+ thread_ts: item.thread_id || item.thread_ts || null,
96
+ ts: item.ts || item.timestamp || null,
97
+ };
98
+ }
99
+
100
+ if (item.service === "gmail") {
101
+ const tid = item.thread_id || item.threadId;
102
+ if (!tid) return null;
103
+ return { source: "gmail", thread_id: tid };
104
+ }
105
+
106
+ if (item.service === "calendar") {
107
+ const eid = item.event_id || item.eventId;
108
+ if (!eid) return null;
109
+ return { source: "calendar", event_id: eid };
110
+ }
111
+
112
+ return null;
113
+ }
114
+
115
+ // Lazy token access — dotenv loads in daemon main before these are called
116
+ function getSlackToken() {
117
+ return process.env.SLACK_USER_TOKEN || process.env.SLACK_BOT_TOKEN;
118
+ }
119
+
120
+ let cachedPreamble = null;
121
+
122
+ // Spawn `claude --print` with the supplied system + user prompts and model.
123
+ // Mirrors the pattern used in classifier.mjs:
124
+ // • child_process.spawn (not exec) — avoids shell-escape injection on
125
+ // potentially-hostile sender content.
126
+ // • System prompt rides on --append-system-prompt; user prompt is written
127
+ // to stdin and the pipe is closed.
128
+ // • Non-zero exit, timeout, spawn error or stdin write error all reject
129
+ // so the caller can surface the failure.
130
+ //
131
+ // Session-router wire-up (b2-b4, cycle 474): when the caller supplies a
132
+ // `sessionId` (pre-minted UUID) and a `router` + `routingKey`, the spawn
133
+ // adds `--session-id <uuid> --output-format json`, parses the one-line JSON
134
+ // stdout into {session_id, result, is_error}, calls router.touch on success
135
+ // and router.recordExit on close. Per b1 flag-verification report, NEVER
136
+ // combine `--resume` with `--session-id` (not needed: pre-minting + reusing
137
+ // the same UUID across spawns is the resume mechanism).
138
+ //
139
+ // @returns {Promise<{ text: string, jsonResult: object|null, exitCode: number }>}
140
+ function runClaudeCLI(systemPrompt, userPrompt, model, opts = {}) {
141
+ const { sessionId = null, router = null, routingKey = null } = opts;
142
+
143
+ return new Promise((resolvePromise, rejectPromise) => {
144
+ const args = [
145
+ "--print",
146
+ ...sessionPermissionArgs({ source: "responder" }),
147
+ ...daemonClaudeArgs(),
148
+ "--model", model,
149
+ "--append-system-prompt", systemPrompt,
150
+ ];
151
+ if (sessionId) {
152
+ // --output-format json is only valid in combination with --print (per
153
+ // b1 report). We always pass --print above, so this is safe.
154
+ args.push("--session-id", sessionId, "--output-format", "json");
155
+ }
156
+
157
+ const proc = spawn(CLAUDE_BIN, args, {
158
+ stdio: ["pipe", "pipe", "pipe"],
159
+ // Force claude CLI onto keychain OAuth (Max subscription); strip any
160
+ // stale ANTHROPIC_API_KEY/AUTH_TOKEN inherited from the daemon env.
161
+ // Augment PATH so the subsession finds homebrew/nvm tools.
162
+ env: { ...process.env, PATH: augmentedPath(), ANTHROPIC_API_KEY: "", ANTHROPIC_AUTH_TOKEN: "" },
163
+ });
164
+
165
+ let stdout = "";
166
+ let stderr = "";
167
+ let settled = false;
168
+
169
+ const timer = setTimeout(() => {
170
+ if (settled) return;
171
+ settled = true;
172
+ try { proc.kill("SIGTERM"); } catch (_) { /* noop */ }
173
+ setTimeout(() => { try { if (!proc.killed) proc.kill("SIGKILL"); } catch (_) { /* noop */ } }, 2000);
174
+ rejectPromise(new Error(`claude CLI timed out after ${CLAUDE_CLI_TIMEOUT_MS}ms`));
175
+ }, CLAUDE_CLI_TIMEOUT_MS);
176
+
177
+ proc.stdout.on("data", (chunk) => { stdout += chunk.toString(); });
178
+ proc.stderr.on("data", (chunk) => { stderr += chunk.toString(); });
179
+
180
+ proc.on("error", (err) => {
181
+ if (settled) return;
182
+ settled = true;
183
+ clearTimeout(timer);
184
+ rejectPromise(new Error(`claude CLI spawn error: ${err.message}`));
185
+ });
186
+
187
+ proc.on("close", (code) => {
188
+ // (b4) Always notify the router of the exit, regardless of whether
189
+ // the promise has already settled (timeout path) or not. recordExit
190
+ // is a no-op for keys the router has never touched, so the only
191
+ // cost is a registry write — which we want for non-zero exits so
192
+ // the next route() returns EPHEMERAL_REPLACE.
193
+ if (router && routingKey) {
194
+ // Fire-and-forget; recordExit is async but failure here must not
195
+ // mask the real result. Errors swallowed because the router has
196
+ // its own atomic-write semantics and bubbling here would crash
197
+ // the daemon over a non-critical bookkeeping write.
198
+ Promise.resolve(router.recordExit(routingKey, code)).catch((err) => {
199
+ console.warn(`[responder] router.recordExit failed for ${routingKey}: ${err.message}`);
200
+ });
201
+ }
202
+
203
+ if (settled) return;
204
+ settled = true;
205
+ clearTimeout(timer);
206
+ if (code !== 0) {
207
+ const tail = (stderr || "").trim().slice(-500);
208
+ rejectPromise(new Error(`claude CLI exited ${code}: ${tail || "no stderr"}`));
209
+ return;
210
+ }
211
+
212
+ // (b3) If we asked for JSON, parse it. Otherwise return raw text.
213
+ if (sessionId) {
214
+ const trimmed = (stdout || "").trim();
215
+ try {
216
+ const parsed = JSON.parse(trimmed);
217
+ // Per b1 report: top-level `session_id` (snake_case UUID), `result`
218
+ // (text), `is_error` (bool). Top-level `uuid` is the message UUID,
219
+ // NOT the session id — do NOT use it.
220
+ resolvePromise({ text: parsed.result ?? "", jsonResult: parsed, exitCode: code });
221
+ } catch (parseErr) {
222
+ // Legacy fallback (rollout-safety): older CLIs or unexpected output
223
+ // shapes shouldn't crash the daemon. Log a warning, surface the raw
224
+ // text, and let the caller decide whether to call router.touch.
225
+ console.warn(`[responder] claude CLI JSON parse failed (sessionId=${sessionId}): ${parseErr.message} — falling back to raw stdout`);
226
+ resolvePromise({ text: trimmed, jsonResult: null, exitCode: code });
227
+ }
228
+ } else {
229
+ resolvePromise({ text: stdout, jsonResult: null, exitCode: code });
230
+ }
231
+ });
232
+
233
+ try {
234
+ proc.stdin.end(userPrompt, "utf8");
235
+ } catch (err) {
236
+ if (settled) return;
237
+ settled = true;
238
+ clearTimeout(timer);
239
+ rejectPromise(new Error(`claude CLI stdin write error: ${err.message}`));
240
+ }
241
+ });
242
+ }
243
+
244
+ function today() {
245
+ return new Date().toISOString().split("T")[0];
246
+ }
247
+
248
+ function logDir() {
249
+ const dir = join(AGENT_REPO_DIR, "logs", "daemon");
250
+ mkdirSync(dir, { recursive: true });
251
+ return dir;
252
+ }
253
+
254
+ function logResponse(entry) {
255
+ const path = join(logDir(), `${today()}-responses.jsonl`);
256
+ appendFileSync(path, JSON.stringify({ timestamp: new Date().toISOString(), ...entry }) + "\n");
257
+ }
258
+
259
+ /**
260
+ * Write an interaction record so future sessions can see this exchange.
261
+ * Both the incoming message and the agent's reply are logged.
262
+ */
263
+ function logInteraction(item, responseText) {
264
+ if (!item || !item.sender || item.service !== "slack") return;
265
+ const senderSlug = item.sender.replace(/\s+/g, "-").toLowerCase();
266
+ const channelId = resolveSlackChannel(item);
267
+
268
+ // Write to both channel-ID and sender-slug directories
269
+ const dirs = [];
270
+ if (channelId) dirs.push(join(AGENT_REPO_DIR, "memory", "interactions", "slack", channelId));
271
+ dirs.push(join(AGENT_REPO_DIR, "memory", "interactions", "slack", `dm-${senderSlug}`));
272
+
273
+ const incomingEntry = {
274
+ ts: item.ts || item.timestamp || new Date().toISOString(),
275
+ from: item.sender,
276
+ channel: channelId || item.channel,
277
+ type: item.is_reply ? "thread_reply" : "dm",
278
+ content: item.content || "",
279
+ received_at: new Date().toISOString(),
280
+ classification: "quick_reply",
281
+ response_sent: true,
282
+ };
283
+
284
+ const responseEntry = {
285
+ ts: new Date().toISOString(),
286
+ from: loadAgent().firstName.toLowerCase(),
287
+ channel: channelId || item.channel,
288
+ type: "reply",
289
+ content: responseText,
290
+ sent_at: new Date().toISOString(),
291
+ via: "quick_reply_responder",
292
+ };
293
+
294
+ for (const dir of dirs) {
295
+ try {
296
+ mkdirSync(dir, { recursive: true });
297
+ const logPath = join(dir, `${today()}.jsonl`);
298
+ appendFileSync(logPath, JSON.stringify(incomingEntry) + "\n");
299
+ appendFileSync(logPath, JSON.stringify(responseEntry) + "\n");
300
+ } catch (err) {
301
+ console.warn(`[responder] Failed to log interaction to ${dir}: ${err.message}`);
302
+ }
303
+ }
304
+ }
305
+
306
+ // ---------------------------------------------------------------------------
307
+ // Identity preamble (cached)
308
+ // ---------------------------------------------------------------------------
309
+
310
+ function loadPreamble() {
311
+ if (cachedPreamble) return cachedPreamble;
312
+ try {
313
+ const raw = readFileSync(join(AGENT_REPO_DIR, "CLAUDE.md"), "utf-8");
314
+ // Extract just the identity and communication rules
315
+ const lines = raw.split("\n");
316
+ const sections = [];
317
+ let capturing = false;
318
+ const targets = ["## Identity", "## Company Context", "## Communication Rules"];
319
+ for (const line of lines) {
320
+ if (targets.some((h) => line.startsWith(h))) { capturing = true; sections.push(line); continue; }
321
+ if (capturing && /^## [A-Z]/.test(line) && !targets.some((h) => line.startsWith(h))) { capturing = false; continue; }
322
+ if (capturing) sections.push(line);
323
+ }
324
+ cachedPreamble = sections.join("\n").trim();
325
+ if (cachedPreamble.length < 100) cachedPreamble = FALLBACK_PREAMBLE;
326
+ } catch {
327
+ cachedPreamble = FALLBACK_PREAMBLE;
328
+ }
329
+ return cachedPreamble;
330
+ }
331
+
332
+ function buildFallbackPreamble() {
333
+ const a = loadAgent();
334
+ const principalName = a.principal?.fullName || "the principal";
335
+ const principalTitle = a.principal?.title || "principal";
336
+ return `You are ${a.fullName}, ${a.title} to ${principalName}, ${principalTitle} of ${a.company}.
337
+ You operate as the autonomous executive command layer. Be concise, direct, and professional.
338
+ For ${a.principal?.firstName || "principal"} messages: prioritise speed and directness. For others: be warm but efficient.`;
339
+ }
340
+ const FALLBACK_PREAMBLE = buildFallbackPreamble();
341
+
342
+ // ---------------------------------------------------------------------------
343
+ // Load user profile for context
344
+ // ---------------------------------------------------------------------------
345
+
346
+ function loadUserProfile(sender) {
347
+ try {
348
+ const profileName = sender.replace(/\s+/g, "-").toLowerCase();
349
+ const path = join(AGENT_REPO_DIR, "memory", "profiles", "users", `${profileName}.yaml`);
350
+ return readFileSync(path, "utf-8");
351
+ } catch {
352
+ return null;
353
+ }
354
+ }
355
+
356
+ // ---------------------------------------------------------------------------
357
+ // Load recent conversation history for context
358
+ // ---------------------------------------------------------------------------
359
+
360
+ function loadConversationHistory(item) {
361
+ if (!item || !item.sender) return null;
362
+
363
+ const senderSlug = item.sender.replace(/\s+/g, "-").toLowerCase();
364
+ const channelId = resolveSlackChannel(item);
365
+
366
+ const candidateDirs = [];
367
+ if (channelId) candidateDirs.push(join(AGENT_REPO_DIR, "memory", "interactions", "slack", channelId));
368
+ candidateDirs.push(join(AGENT_REPO_DIR, "memory", "interactions", "slack", `dm-${senderSlug}`));
369
+
370
+ const entries = [];
371
+ for (const dir of candidateDirs) {
372
+ try {
373
+ const files = readdirSync(dir)
374
+ .filter((f) => f.endsWith(".jsonl"))
375
+ .sort().reverse().slice(0, 2);
376
+ for (const file of files) {
377
+ try {
378
+ const content = readFileSync(join(dir, file), "utf-8").trim();
379
+ if (!content) continue;
380
+ for (const line of content.split("\n").filter(Boolean)) {
381
+ try { entries.push(JSON.parse(line)); } catch {}
382
+ }
383
+ } catch {}
384
+ }
385
+ } catch {}
386
+ }
387
+
388
+ if (entries.length === 0) return null;
389
+
390
+ entries.sort((a, b) => (a.ts || a.received_at || "").localeCompare(b.ts || b.received_at || ""));
391
+ const recent = entries.slice(-15);
392
+
393
+ return recent.map((e) => {
394
+ const from = e.from || e.user_name || e.sender || "unknown";
395
+ const text = e.content || e.text || "";
396
+ return `[${e.received_at || e.ts || ""}] ${from}: ${text.substring(0, 200)}`;
397
+ }).join("\n");
398
+ }
399
+
400
+ // ---------------------------------------------------------------------------
401
+ // Generate response text via `claude --print` CLI
402
+ // ---------------------------------------------------------------------------
403
+ //
404
+ // Indirection so a test can supply deterministic reply text WITHOUT spawning a
405
+ // `claude` child (mirrors the dispatcher's _spawn seam). Production default is
406
+ // the real generator, so behaviour is unchanged. The public senders call
407
+ // generateResponse() through this slot.
408
+ let _generateResponse;
409
+ /**
410
+ * Test seam: replace the response generator. Pass a function `(item,
411
+ * classResult, isHolding) => Promise<string>` to force reply text; pass nothing
412
+ * to restore the real CLI-backed generator. Returns a restore fn.
413
+ */
414
+ export function setGenerateResponseForTests(fn) {
415
+ const prev = _generateResponse;
416
+ _generateResponse = typeof fn === "function" ? fn : realGenerateResponse;
417
+ return () => { _generateResponse = prev; };
418
+ }
419
+
420
+ async function realGenerateResponse(item, classResult, isHolding = false) {
421
+ const preamble = loadPreamble();
422
+ const profile = loadUserProfile(item.sender);
423
+ // Use Sonnet for holding messages (speed matters), but the classifier's
424
+ // recommended model for actual replies (Opus for CEO, Sonnet for routine)
425
+ const model = isHolding ? SONNET_MODEL : (classResult.model === "opus" ? "claude-opus-4-6" : SONNET_MODEL);
426
+
427
+ let systemPrompt;
428
+ if (isHolding) {
429
+ systemPrompt = `${preamble}
430
+
431
+ You are generating a brief acknowledgment/holding message. The sender has made a request that requires complex work. You need to:
432
+ 1. Acknowledge receipt of their message
433
+ 2. Briefly confirm what you understand they need
434
+ 3. Let them know you're working on it
435
+ 4. Give a rough timeframe if appropriate (e.g., "I'll have this ready within the hour")
436
+
437
+ Keep it to 2-3 sentences max. Be warm but efficient. Match the sender's communication style.
438
+ Do NOT use phrases like "I'm on it!" or overly casual language. Be professional.
439
+ ${profile ? `\nSender profile:\n${profile}` : ""}`;
440
+ } else {
441
+ systemPrompt = `${preamble}
442
+
443
+ You are generating a direct response to this message. Be concise and actionable.
444
+ If it's a question, answer it. If it's a request, confirm and describe what you'll do or have done.
445
+ If it's informational, acknowledge appropriately.
446
+
447
+ Keep responses focused — 1-4 sentences for simple items, up to a short paragraph for more nuanced ones.
448
+ Match the sender's tone and urgency level.
449
+ ${profile ? `\nSender profile:\n${profile}` : ""}`;
450
+ }
451
+
452
+ const conversationHistory = loadConversationHistory(item);
453
+
454
+ const userContent = [
455
+ `From: ${item.sender} (${item.sender_privilege || "unknown"})`,
456
+ `Via: ${item.service} / ${item.channel}`,
457
+ item.subject ? `Subject: ${item.subject}` : null,
458
+ conversationHistory ? `\nRecent conversation history:\n${conversationHistory}` : null,
459
+ `\nCurrent message:\n${item.content || "(empty)"}`,
460
+ item.thread_context ? `\nThread context:\n${item.thread_context}` : null,
461
+ `\nClassification: ${classResult.summary}`,
462
+ ].filter(Boolean).join("\n");
463
+
464
+ // (b2) Session-router decision. Compute the routing key from a daemon→router
465
+ // adapter view of the item. If the item can't be keyed (unknown service,
466
+ // missing channel/thread), fall back to pure ephemeral with no router calls.
467
+ const router = await getRouter();
468
+ const routerItem = deriveRouterItem(item);
469
+ let key = null;
470
+ let sessionId = null;
471
+ if (routerItem) {
472
+ try {
473
+ key = deriveRoutingKey(routerItem);
474
+ const decision = router.route(key);
475
+ if (decision.decision === "RESUME" && decision.resumeId) {
476
+ sessionId = decision.resumeId;
477
+ } else {
478
+ // EPHEMERAL or EPHEMERAL_REPLACE — pre-mint a fresh UUID. Reusing
479
+ // the same key on next call (with a different sessionId) is fine;
480
+ // touch() will overwrite the registry entry.
481
+ sessionId = randomUUID();
482
+ }
483
+ } catch (err) {
484
+ // routingKey() throws on malformed items. Don't crash — fall back to
485
+ // pure ephemeral with no session-router participation.
486
+ console.warn(`[responder] routingKey derivation failed: ${err.message} — falling back to non-routed ephemeral`);
487
+ key = null;
488
+ sessionId = null;
489
+ }
490
+ }
491
+
492
+ const cliResult = await runClaudeCLI(systemPrompt, userContent, model, {
493
+ sessionId,
494
+ router: key ? router : null,
495
+ routingKey: key,
496
+ });
497
+
498
+ const text = (cliResult.text || "").trim();
499
+ if (!text) {
500
+ throw new Error("claude CLI returned empty result text in generateResponse");
501
+ }
502
+
503
+ // (b3) On success, touch the registry with the CLI-resolved session_id so
504
+ // the next call routed to this key gets a RESUME decision. Skip touch on
505
+ // is_error responses or when JSON parsing failed (legacy fallback path).
506
+ if (key && cliResult.jsonResult && cliResult.jsonResult.is_error !== true) {
507
+ const claudeSessionId = cliResult.jsonResult.session_id || sessionId;
508
+ try {
509
+ await router.touch(key, { claudeSessionId, model });
510
+ } catch (err) {
511
+ console.warn(`[responder] router.touch failed for ${key}: ${err.message}`);
512
+ }
513
+ }
514
+
515
+ return text;
516
+ }
517
+
518
+ // Default the generator slot to the real CLI-backed implementation. (Declared
519
+ // after the function so the binding exists; setGenerateResponseForTests swaps it.)
520
+ _generateResponse = realGenerateResponse;
521
+
522
+ // ---------------------------------------------------------------------------
523
+ // Lightweight outbound validation (inline — no Python dependency)
524
+ // Catches the most critical issues before sending quick replies
525
+ // ---------------------------------------------------------------------------
526
+
527
+ function validateQuickReply(text) {
528
+ const issues = [];
529
+ const lower = text.toLowerCase();
530
+
531
+ // Rule 1: AI self-disclosure — the agent must never reveal she/he is AI
532
+ const aiPatterns = [
533
+ /\bi am an? ai\b/i, /\bi'?m an? ai\b/i, /\bas an ai\b/i,
534
+ /\blanguage model\b/i, /\bartificial intelligence\b/i,
535
+ /\bi am a? ?virtual\b/i, /\bi don'?t have (?:feelings|emotions|a body)\b/i,
536
+ /\bclaude\b/i, /\banthropic\b/i, /\bchatbot\b/i,
537
+ ];
538
+ for (const p of aiPatterns) {
539
+ if (p.test(text)) {
540
+ issues.push({ rule: "ai_disclosure", severity: "block", detail: `Matched: ${p}` });
541
+ break;
542
+ }
543
+ }
544
+
545
+ // Rule 2: No local file paths in outbound messages
546
+ if (/(?:outputs|docs|knowledge|memory|state|scripts|config)\/[^\s]+/.test(text)) {
547
+ issues.push({ rule: "local_path_reference", severity: "block", detail: "Message references a local file path" });
548
+ }
549
+
550
+ // Rule 3: the agent should not schedule themselves for in-person meetings
551
+ const meetingPatterns = [
552
+ /\bi(?:'ll| will) (?:come|attend|be there|join (?:you |the meeting )?in person)\b/i,
553
+ /\blet'?s meet (?:at|in)\b/i,
554
+ /\bi(?:'ll| will) (?:swing by|pop by|stop by|come by)\b/i,
555
+ ];
556
+ for (const p of meetingPatterns) {
557
+ if (p.test(text)) {
558
+ issues.push({ rule: "agent_inperson", severity: "block", detail: `Matched: ${p}` });
559
+ break;
560
+ }
561
+ }
562
+
563
+ return issues;
564
+ }
565
+
566
+ // ---------------------------------------------------------------------------
567
+ // Send via Slack API
568
+ // ---------------------------------------------------------------------------
569
+
570
+ async function sendSlackMessage(channel, text, threadTs = null) {
571
+ // Unified send-gate (P0-3): the daemon quick-reply/holding path must pass the
572
+ // same chokepoint as the shell senders + BaseAdapter — banned-phrase, AI
573
+ // disclosure, and information-barrier screening at one place. Block on deny.
574
+ try {
575
+ const gate = await screenOutbound({ channel: "slack", recipient: channel, text, agentRoot: AGENT_REPO_DIR });
576
+ if (gate && gate.allow === false) {
577
+ throw new Error(`send-gate blocked Slack reply: ${gate.reason}`);
578
+ }
579
+ } catch (err) {
580
+ if (/send-gate blocked/.test(err.message)) throw err; // a real block propagates
581
+ // gate infra error (module/policy unreadable): fail-open for internal Slack
582
+ // channels (matches send-gate's internal posture) — never silently drop.
583
+ console.warn(`[responder] send-gate check errored (allowing internal Slack): ${err.message}`);
584
+ // Observability: the send-gate just FAILED OPEN — surface it on the lifecycle
585
+ // bus so the daemon's onGuardFail subscriber counts it (the bus's one real
586
+ // consumer). Fire-and-forget; emit is fully isolated and never throws.
587
+ try {
588
+ Promise.resolve(getHookBus().emit("onGuardFail", { guard: "send_gate", channel: "slack", recipient: channel, reason: err.message, failed_open: true }))
589
+ .catch(() => { /* bus emit is isolated; never propagate */ });
590
+ } catch { /* never let telemetry break the send path */ }
591
+ }
592
+
593
+ // Always use user token — bot tokens can't access DM channels
594
+ const token = getSlackToken();
595
+ if (!token) throw new Error("No Slack token available (set SLACK_USER_TOKEN in .env)");
596
+
597
+ const body = {
598
+ channel,
599
+ text,
600
+ ...(threadTs ? { thread_ts: threadTs } : {}),
601
+ };
602
+
603
+ const res = await fetch("https://slack.com/api/chat.postMessage", {
604
+ method: "POST",
605
+ headers: {
606
+ "Authorization": `Bearer ${token}`,
607
+ "Content-Type": "application/json",
608
+ },
609
+ body: JSON.stringify(body),
610
+ });
611
+
612
+ const data = await res.json();
613
+ if (!data.ok) {
614
+ throw new Error(`Slack API error: ${data.error}`);
615
+ }
616
+
617
+ return data;
618
+ }
619
+
620
+ // ---------------------------------------------------------------------------
621
+ // Send via Gmail API (draft or send)
622
+ // Currently logs the response — full Gmail send integration can be added
623
+ // ---------------------------------------------------------------------------
624
+
625
+ async function sendGmailResponse(item, text) {
626
+ const to = item.sender_email || item.sender;
627
+ const subject = `Re: ${item.subject || "(no subject)"}`;
628
+ const sendScript = join(AGENT_REPO_DIR, "scripts", "send-email-threaded.py");
629
+
630
+ try {
631
+ const args = [sendScript, to, subject, text];
632
+ if (item.subject) {
633
+ args.push("--reply-to-subject", item.subject);
634
+ }
635
+
636
+ execFileSync("python3", args, {
637
+ cwd: AGENT_REPO_DIR,
638
+ timeout: 30000,
639
+ encoding: "utf-8",
640
+ env: { ...process.env },
641
+ });
642
+
643
+ return { sent: true, via: "smtp", to };
644
+ } catch (err) {
645
+ console.error(`[responder] Gmail send failed for ${to}: ${err.message}`);
646
+ // Fall back to draft file so the response is not lost
647
+ const draftPath = join(AGENT_REPO_DIR, "outputs", "drafts",
648
+ `${today()}-quick-reply-${item.sender.replace(/[^a-z0-9]/gi, "-")}.md`);
649
+ mkdirSync(join(AGENT_REPO_DIR, "outputs", "drafts"), { recursive: true });
650
+ const content = `# Quick Reply Draft (SEND FAILED)\n\nTo: ${to}\nSubject: ${subject}\nGenerated: ${new Date().toISOString()}\nError: ${err.message}\n\n---\n\n${text}\n`;
651
+ writeFileSync(draftPath, content);
652
+ return { sent: false, via: "draft_fallback", draft_path: draftPath, error: err.message };
653
+ }
654
+ }
655
+
656
+ // ---------------------------------------------------------------------------
657
+ // Resolve Slack channel ID from item
658
+ // ---------------------------------------------------------------------------
659
+
660
+ function resolveSlackChannel(item) {
661
+ // Direct channel ID (starts with D for DM, C for channel)
662
+ if (item.channel && /^[DC][A-Z0-9]{8,}$/.test(item.channel)) return item.channel;
663
+ if (item.channel_id) return item.channel_id;
664
+
665
+ // Extract from raw_ref format: "slack:D099N1JGKRQ:1775331885.690669"
666
+ if (item.raw_ref) {
667
+ const match = item.raw_ref.match(/slack:([DC][A-Z0-9]+):/);
668
+ if (match) return match[1];
669
+ // Also try bare channel ID in raw_ref
670
+ const bare = item.raw_ref.match(/^([DC][A-Z0-9]{8,})$/);
671
+ if (bare) return bare[1];
672
+ }
673
+
674
+ return null;
675
+ }
676
+
677
+ // ---------------------------------------------------------------------------
678
+ // Public API
679
+ // ---------------------------------------------------------------------------
680
+
681
+ /**
682
+ * Send a quick response to a simple item. No claude --print session needed.
683
+ * Uses Sonnet API to generate text, then posts directly via Slack/Gmail.
684
+ *
685
+ * @returns {{ sent: boolean, text: string, channel: string }} result
686
+ */
687
+ export async function sendQuickResponse(item, classResult) {
688
+ const startTime = Date.now();
689
+
690
+ try {
691
+ const text = await _generateResponse(item, classResult, false);
692
+
693
+ // Validate before sending — block replies that violate critical rules
694
+ const validationIssues = validateQuickReply(text);
695
+ const blocked = validationIssues.filter((i) => i.severity === "block");
696
+ if (blocked.length > 0) {
697
+ console.warn(`[responder] Quick reply BLOCKED for ${item.sender}: ${blocked.map((b) => b.rule).join(", ")}`);
698
+ logResponse({
699
+ type: "quick_response_blocked",
700
+ sender: item.sender,
701
+ service: item.service,
702
+ issues: blocked,
703
+ text_preview: text.substring(0, 100),
704
+ });
705
+ // Observability: a send-gate / validation block — count it (never throws).
706
+ counters.bump("send.blocked", { stage: "validation", service: item.service, rules: blocked.map((b) => b.rule) });
707
+ // Fall through to full session which has richer validation
708
+ return { sent: false, text: null, blocked: true, issues: blocked };
709
+ }
710
+
711
+ let sendResult = { sent: false };
712
+
713
+ if (item.service === "slack") {
714
+ const channel = resolveSlackChannel(item);
715
+ if (channel) {
716
+ // Check sent-message registry for duplicates
717
+ const threadTs = item.thread_id || null;
718
+ const dupCheck = checkRecentlySent(channel, threadTs, "quick_reply");
719
+ if (!dupCheck.allowed) {
720
+ console.log(`[responder] Quick reply blocked by sent-registry: ${dupCheck.reason}`);
721
+ logResponse({ type: "quick_response_dedup_blocked", sender: item.sender, reason: dupCheck.reason });
722
+ // Observability: a send-gate (dedup registry) block — count it.
723
+ counters.bump("send.blocked", { stage: "dedup", service: "slack", reason: dupCheck.reason });
724
+ return { sent: false, text: null, blocked: true, reason: dupCheck.reason };
725
+ }
726
+
727
+ await sendSlackMessage(channel, text, threadTs);
728
+ registerSent(channel, threadTs, "quick_reply", "quick-responder", text.substring(0, 100));
729
+ sendResult = { sent: true, channel, via: "slack_api" };
730
+ // Observability: a quick reply actually left the agent — interaction-end
731
+ // on the quick path. Ambient trace_id (set by the daemon at item_received)
732
+ // correlates it; item.trace_id is carried explicitly as a belt-and-braces
733
+ // hand-off. Never throws.
734
+ emitEvent({ type: EVENT_TYPES.SENT, trace_id: item.trace_id || undefined, attrs: { service: "slack", channel, via: "quick_reply" } });
735
+ // Log interaction to memory so future sessions have context
736
+ logInteraction(item, text);
737
+ } else {
738
+ console.warn(`[responder] Could not resolve Slack channel for ${item.sender} (channel: ${item.channel}, raw_ref: ${item.raw_ref})`);
739
+ sendResult = { sent: false, reason: "no_channel_id" };
740
+ }
741
+ } else if (item.service === "gmail") {
742
+ const gmailResult = await sendGmailResponse(item, text);
743
+ sendResult = { sent: gmailResult.sent, via: gmailResult.via, ...(gmailResult.draft_path ? { draft_path: gmailResult.draft_path } : {}), ...(gmailResult.to ? { to: gmailResult.to } : {}) };
744
+ if (gmailResult.sent) {
745
+ emitEvent({ type: EVENT_TYPES.SENT, trace_id: item.trace_id || undefined, attrs: { service: "gmail", via: gmailResult.via } });
746
+ logInteraction(item, text);
747
+ }
748
+ }
749
+
750
+ const duration = Date.now() - startTime;
751
+ logResponse({
752
+ type: "quick_response",
753
+ sender: item.sender,
754
+ service: item.service,
755
+ duration_ms: duration,
756
+ text_length: text.length,
757
+ ...sendResult,
758
+ });
759
+
760
+ console.log(`[responder] Quick reply to ${item.sender} (${duration}ms) — ${sendResult.via || "not_sent"}`);
761
+ return { sent: sendResult.sent, text, ...sendResult };
762
+
763
+ } catch (err) {
764
+ console.error(`[responder] Quick response failed for ${item.sender}:`, err.message);
765
+ logResponse({ type: "quick_response_error", sender: item.sender, error: err.message });
766
+ return { sent: false, text: null, error: err.message };
767
+ }
768
+ }
769
+
770
+ /**
771
+ * Send a holding message for a complex item, then return the message text
772
+ * so it can be included in the dispatched session's prompt.
773
+ *
774
+ * @returns {{ sent: boolean, holdingText: string }} result
775
+ */
776
+ export async function sendHoldingMessage(item, classResult) {
777
+ const startTime = Date.now();
778
+
779
+ try {
780
+ const text = await _generateResponse(item, classResult, true);
781
+ let sendResult = { sent: false };
782
+
783
+ if (item.service === "slack") {
784
+ const channel = resolveSlackChannel(item);
785
+ if (channel) {
786
+ // Check sent-message registry for duplicates
787
+ const threadTs = item.thread_id || null;
788
+ const dupCheck = checkRecentlySent(channel, threadTs, "holding");
789
+ if (!dupCheck.allowed) {
790
+ console.log(`[responder] Holding message blocked by sent-registry: ${dupCheck.reason}`);
791
+ logResponse({ type: "holding_dedup_blocked", sender: item.sender, reason: dupCheck.reason });
792
+ counters.bump("send.blocked", { stage: "dedup", service: "slack", kind: "holding", reason: dupCheck.reason });
793
+ return { sent: false, holdingText: null, blocked: true, reason: dupCheck.reason };
794
+ }
795
+
796
+ await sendSlackMessage(channel, text, threadTs);
797
+ registerSent(channel, threadTs, "holding", "quick-responder", text.substring(0, 100));
798
+ sendResult = { sent: true, channel, via: "slack_api" };
799
+ } else {
800
+ console.warn(`[responder] Could not resolve Slack channel for holding msg to ${item.sender} (channel: ${item.channel}, raw_ref: ${item.raw_ref})`);
801
+ }
802
+ } else if (item.service === "gmail") {
803
+ const gmailResult = await sendGmailResponse(item, text);
804
+ sendResult = { sent: gmailResult.sent, via: gmailResult.via, ...(gmailResult.draft_path ? { draft_path: gmailResult.draft_path } : {}), ...(gmailResult.to ? { to: gmailResult.to } : {}) };
805
+ }
806
+
807
+ const duration = Date.now() - startTime;
808
+ logResponse({
809
+ type: "holding_message",
810
+ sender: item.sender,
811
+ service: item.service,
812
+ duration_ms: duration,
813
+ text_length: text.length,
814
+ ...sendResult,
815
+ });
816
+
817
+ console.log(`[responder] Holding msg to ${item.sender} (${duration}ms) — ${sendResult.via || "not_sent"}`);
818
+ return { sent: sendResult.sent, holdingText: text, ...sendResult };
819
+
820
+ } catch (err) {
821
+ console.error(`[responder] Holding message failed for ${item.sender}:`, err.message);
822
+ logResponse({ type: "holding_message_error", sender: item.sender, error: err.message });
823
+ return { sent: false, holdingText: null, error: err.message };
824
+ }
825
+ }
826
+
827
+ /**
828
+ * Determine if an item can be handled with a quick response
829
+ * (no session needed) vs requiring a full claude --print session.
830
+ *
831
+ * Critical/high-priority messages and CEO DMs MUST go through full sessions
832
+ * so they get tool access, conversation history, and memory lookup.
833
+ * Quick replies are only for truly simple, low-stakes messages.
834
+ */
835
+ export function isQuickReply(classResult) {
836
+ // These always need full sessions — never quick reply
837
+ if (classResult.action === "research") return false;
838
+ if (classResult.action === "draft") return false;
839
+ if (classResult.action === "queue") return false;
840
+
841
+ // Critical and high-priority items need full sessions for proper context
842
+ if (classResult.priority === "critical") return false;
843
+ if (classResult.priority === "high") return false;
844
+
845
+ // Opus-routed items are complex by definition — need full sessions
846
+ if (classResult.model === "opus") return false;
847
+
848
+ // Only normal/low priority simple responses get quick reply path
849
+ if (classResult.action === "respond") return true;
850
+ if (classResult.action === "archive") return true;
851
+ if (classResult.action === "ignore") return true;
852
+
853
+ return false;
854
+ }