@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,1229 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Agent Huddle — Main Server
4
+ *
5
+ * Orchestrates Slack huddle voice participation by coordinating:
6
+ * - HuddleController (CDP-based Slack UI automation)
7
+ * - AudioBridge (BlackHole capture → Deepgram STT → ElevenLabs TTS → BlackHole playback)
8
+ * - Claude API (conversation intelligence, reused from voice-ai)
9
+ *
10
+ * Lifecycle:
11
+ * 1. Connects to Slack desktop app via CDP
12
+ * 2. Listens for huddle invitations (Events API + DOM polling)
13
+ * 3. On invitation or initiate command → joins huddle
14
+ * 4. Captures audio → transcribes → Claude generates response → TTS → speaks
15
+ * 5. On huddle end → saves transcript, routes action items
16
+ *
17
+ * Usage:
18
+ * node huddle-server.mjs # Start and listen for huddles
19
+ * node huddle-server.mjs --join #general # Join huddle in #general
20
+ * node huddle-server.mjs --call principal # Initiate huddle with the agent's principal
21
+ */
22
+
23
+ import dotenv from "dotenv";
24
+ import { fileURLToPath as _fu } from "node:url";
25
+ import { dirname as _dn, join as _jn } from "node:path";
26
+
27
+ // Load .env from agent repo root (where all API keys live), then huddle-specific .env
28
+ const _d = _dn(_fu(import.meta.url));
29
+ dotenv.config({ path: _jn(_d, "../..", ".env") });
30
+ dotenv.config({ path: _jn(_d, ".env"), override: true });
31
+ import { EventEmitter } from "node:events";
32
+ import { readFileSync, appendFileSync, mkdirSync, existsSync } from "node:fs";
33
+ import { join, dirname } from "node:path";
34
+ import { fileURLToPath } from "node:url";
35
+ import { createServer } from "node:http";
36
+ import { execFile as execFileCb } from "node:child_process";
37
+ import { promisify } from "node:util";
38
+ import Anthropic from "@anthropic-ai/sdk";
39
+
40
+ const execFileAsync = promisify(execFileCb);
41
+ import { HuddleController } from "./huddle-controller.mjs";
42
+ import { AudioBridge } from "./audio-bridge.mjs";
43
+ import { OpenAIRealtimeHuddleBridge } from "./openai-realtime-bridge.mjs";
44
+
45
+ // Brain selection. Default is "openai-realtime" — the same single-model
46
+ // speech-to-speech path that drives PSTN. The legacy Deepgram+Claude+11Labs
47
+ // pipeline is still selectable via HUDDLE_BRAIN=legacy for fallback.
48
+ const HUDDLE_BRAIN = (process.env.HUDDLE_BRAIN || "openai-realtime").toLowerCase();
49
+
50
+ const __dirname = dirname(fileURLToPath(import.meta.url));
51
+ const AGENT_REPO_DIR = process.env.AGENT_DIR || join(__dirname, "../..");
52
+ const VOICE_AI_ROOT = process.env.VOICE_AI_ROOT || join(process.env.HOME || "", "voice-ai");
53
+
54
+ // Load agent identity for transcript speaker tags + display names.
55
+ let _agent = null;
56
+ function loadAgent() {
57
+ if (_agent) return _agent;
58
+ try {
59
+ _agent = JSON.parse(readFileSync(join(AGENT_REPO_DIR, "config/agent.json"), "utf-8"));
60
+ } catch {
61
+ _agent = { firstName: "Agent" };
62
+ }
63
+ return _agent;
64
+ }
65
+ const AGENT_SLUG = (loadAgent().firstName || "agent").toLowerCase();
66
+
67
+ // ---------------------------------------------------------------------------
68
+ // Configuration
69
+ // ---------------------------------------------------------------------------
70
+
71
+ const MAX_TOKENS = 300;
72
+ const MAX_TOOL_ROUNDS = 3;
73
+ const HUDDLE_EVENTS_PORT = parseInt(process.env.HUDDLE_EVENTS_PORT || "3200", 10);
74
+
75
+ // Use Claude Code CLI (Max subscription) instead of API calls.
76
+ // Falls back to API if claude CLI is not available.
77
+ const USE_CLAUDE_CLI = process.env.HUDDLE_USE_API !== "1";
78
+ const anthropic = USE_CLAUDE_CLI ? null : new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
79
+
80
+ // ---------------------------------------------------------------------------
81
+ // Maestro action executor + tool definitions (colocated under lib/ since
82
+ // 1.12.19). Falls back to the legacy ~/voice-ai install for old setups.
83
+ // ---------------------------------------------------------------------------
84
+
85
+ let executeAction = null;
86
+ let getToolsForAccessLevel = null;
87
+ let getToolNamesForAccessLevel = null;
88
+
89
+ const _maestroExecutorCandidates = [
90
+ join(__dirname, "../../lib/action-executor.js"),
91
+ join(__dirname, "../../lib/action-executor.mjs"),
92
+ join(VOICE_AI_ROOT, "lib/action-executor.js"),
93
+ ];
94
+ const _maestroToolsCandidates = [
95
+ join(__dirname, "../../lib/tool-definitions.js"),
96
+ join(__dirname, "../../lib/tool-definitions.mjs"),
97
+ join(VOICE_AI_ROOT, "lib/tool-definitions.js"),
98
+ ];
99
+
100
+ for (const candidate of _maestroExecutorCandidates) {
101
+ if (!existsSync(candidate)) continue;
102
+ try {
103
+ const mod = await import(candidate);
104
+ executeAction = mod.executeAction;
105
+ if (executeAction) break;
106
+ } catch { /* try next */ }
107
+ }
108
+ for (const candidate of _maestroToolsCandidates) {
109
+ if (!existsSync(candidate)) continue;
110
+ try {
111
+ const mod = await import(candidate);
112
+ getToolsForAccessLevel = mod.getToolsForAccessLevel;
113
+ getToolNamesForAccessLevel = mod.getToolNamesForAccessLevel;
114
+ if (getToolsForAccessLevel) break;
115
+ } catch { /* try next */ }
116
+ }
117
+ if (executeAction && getToolsForAccessLevel) {
118
+ console.log("[INIT] action executor + tool definitions loaded");
119
+ } else {
120
+ console.warn("[INIT] action executor / tool definitions not available — huddle will be read-only");
121
+ }
122
+
123
+ // ---------------------------------------------------------------------------
124
+ // Huddle Session State
125
+ // ---------------------------------------------------------------------------
126
+
127
+ /** @enum {string} */
128
+ const HuddleState = {
129
+ IDLE: "idle",
130
+ JOINING: "joining",
131
+ ACTIVE: "active",
132
+ LEAVING: "leaving",
133
+ };
134
+
135
+ /**
136
+ * HuddleServer is the top-level orchestrator for the agent's huddle participation.
137
+ */
138
+ class HuddleServer extends EventEmitter {
139
+ constructor() {
140
+ super();
141
+ this.controller = new HuddleController();
142
+ // Brain selection: openai-realtime (default) is the single-model
143
+ // speech-to-speech path identical to PSTN inbound; legacy retains the
144
+ // older Deepgram+Claude+ElevenLabs three-stage pipeline.
145
+ if (HUDDLE_BRAIN === "openai-realtime") {
146
+ this.bridge = new OpenAIRealtimeHuddleBridge({
147
+ apiKey: process.env.OPENAI_API_KEY,
148
+ agentRoot: AGENT_REPO_DIR,
149
+ // tools + executor wired lazily in start() so we don't pull
150
+ // action-executor at import time.
151
+ });
152
+ this._brainKind = "openai-realtime";
153
+ } else {
154
+ // Pass API keys explicitly because ESM import hoisting evaluates
155
+ // audio-bridge.mjs before dotenv.config() runs in this file.
156
+ this.bridge = new AudioBridge({
157
+ deepgramKey: process.env.DEEPGRAM_API_KEY,
158
+ elevenLabsKey: process.env.ELEVENLABS_API_KEY,
159
+ });
160
+ this._brainKind = "legacy";
161
+ }
162
+
163
+ this.state = HuddleState.IDLE;
164
+ this.session = null; // Active huddle session
165
+ this.autoJoin = true; // Auto-join incoming huddle invitations
166
+ }
167
+
168
+ // -------------------------------------------------------------------------
169
+ // Startup
170
+ // -------------------------------------------------------------------------
171
+
172
+ async start() {
173
+ console.log("\n==========================================");
174
+ console.log(` ${loadAgent().firstName || "Agent"} Huddle Server`);
175
+ console.log(" " + new Date().toISOString());
176
+ console.log("==========================================\n");
177
+
178
+ // Verify audio devices
179
+ const devices = await this.bridge.checkDevices();
180
+ if (!devices.capture || !devices.playback) {
181
+ console.error("[INIT] Audio devices missing:");
182
+ console.error(` Capture (BlackHole 2ch): ${devices.capture ? "OK" : "MISSING"}`);
183
+ console.error(` Playback (BlackHole 16ch): ${devices.playback ? "OK" : "MISSING"}`);
184
+ console.error("Run: ./setup-audio.sh to install");
185
+ process.exit(1);
186
+ }
187
+ console.log("[INIT] Audio devices verified");
188
+ console.log(`[INIT] LLM mode: ${USE_CLAUDE_CLI ? "Claude Code CLI (Max subscription)" : "Anthropic API (pay-per-token)"}`);
189
+
190
+ // Connect to Slack via CDP
191
+ this._wireControllerEvents();
192
+ const connected = await this.controller.connect();
193
+ if (!connected) {
194
+ console.error("[INIT] Failed to connect to Slack via CDP");
195
+ console.error("Run: ./launch-slack.sh to start Slack with CDP enabled");
196
+ process.exit(1);
197
+ }
198
+ console.log("[INIT] Connected to Slack via CDP");
199
+
200
+ // Wire audio bridge events
201
+ this._wireBridgeEvents();
202
+
203
+ // Start Events API listener for reliable huddle detection
204
+ this._startEventsListener();
205
+
206
+ console.log("[INIT] Huddle server ready — listening for invitations\n");
207
+
208
+ // Handle CLI arguments
209
+ const args = process.argv.slice(2);
210
+ if (args[0] === "--join" && args[1]) {
211
+ await this.joinHuddle(args[1]);
212
+ } else if (args[0] === "--call" && args[1]) {
213
+ await this.initiateHuddle(args[1]);
214
+ }
215
+ }
216
+
217
+ // -------------------------------------------------------------------------
218
+ // Huddle lifecycle
219
+ // -------------------------------------------------------------------------
220
+
221
+ /**
222
+ * Join an existing huddle in a channel or DM.
223
+ * @param {string} channel - Channel name or person name
224
+ */
225
+ async joinHuddle(channel) {
226
+ if (this.state !== HuddleState.IDLE) {
227
+ console.log(`[HUDDLE] Cannot join — currently ${this.state}`);
228
+ return;
229
+ }
230
+
231
+ this.state = HuddleState.JOINING;
232
+ console.log(`[HUDDLE] Joining huddle in: ${channel}`);
233
+
234
+ try {
235
+ const joined = await this.controller.joinHuddle(channel);
236
+ if (!joined) {
237
+ console.error("[HUDDLE] Failed to join huddle");
238
+ this.state = HuddleState.IDLE;
239
+ return;
240
+ }
241
+
242
+ await this._startSession(channel, "joined");
243
+ } catch (err) {
244
+ console.error(`[HUDDLE] Join error: ${err.message}`);
245
+ this.state = HuddleState.IDLE;
246
+ }
247
+ }
248
+
249
+ /**
250
+ * Initiate a new huddle with a person or in a channel.
251
+ * @param {string} target - Channel name or person name
252
+ */
253
+ async initiateHuddle(target) {
254
+ if (this.state !== HuddleState.IDLE) {
255
+ console.log(`[HUDDLE] Cannot initiate — currently ${this.state}`);
256
+ return;
257
+ }
258
+
259
+ this.state = HuddleState.JOINING;
260
+ console.log(`[HUDDLE] Initiating huddle with: ${target}`);
261
+
262
+ try {
263
+ const started = await this.controller.initiateHuddle(target);
264
+ if (!started) {
265
+ console.error("[HUDDLE] Failed to start huddle");
266
+ this.state = HuddleState.IDLE;
267
+ return;
268
+ }
269
+
270
+ await this._startSession(target, "initiated");
271
+ } catch (err) {
272
+ console.error(`[HUDDLE] Initiate error: ${err.message}`);
273
+ this.state = HuddleState.IDLE;
274
+ }
275
+ }
276
+
277
+ /**
278
+ * Leave the current huddle.
279
+ */
280
+ async leaveHuddle() {
281
+ if (this.state !== HuddleState.ACTIVE) return;
282
+
283
+ this.state = HuddleState.LEAVING;
284
+ console.log("[HUDDLE] Leaving huddle...");
285
+
286
+ try {
287
+ await this.controller.leaveHuddle();
288
+ } catch (err) {
289
+ console.error(`[HUDDLE] Leave error: ${err.message}`);
290
+ }
291
+
292
+ await this._endSession();
293
+ this.state = HuddleState.IDLE;
294
+ }
295
+
296
+ // -------------------------------------------------------------------------
297
+ // Session management
298
+ // -------------------------------------------------------------------------
299
+
300
+ async _startSession(channel, mode, initiator = null) {
301
+ // Guard: don't start a duplicate session
302
+ if (this.state === HuddleState.ACTIVE && this.session) {
303
+ console.log(`[HUDDLE] Session already active — ignoring duplicate start`);
304
+ return;
305
+ }
306
+ this.state = HuddleState.ACTIVE;
307
+
308
+ // Resolve who initiated the huddle for access control
309
+ const callerInfo = this._resolveHuddleParticipant(initiator || channel);
310
+
311
+ this.session = {
312
+ channel,
313
+ mode, // "joined" or "initiated"
314
+ startedAt: new Date().toISOString(),
315
+ messages: [],
316
+ transcript: [],
317
+ participants: [],
318
+ accessLevel: callerInfo.accessLevel,
319
+ callerSlug: callerInfo.slug,
320
+ callerName: callerInfo.name,
321
+ threadPollTimer: null, // Huddle thread message polling
322
+ lastThreadTs: null, // Last seen thread message timestamp
323
+ };
324
+
325
+ // Load live context from the agent repo for Claude
326
+ this.session.liveContext = this._loadLiveContext();
327
+
328
+ // In webaudio capture mode (legacy bridge only), set Slack's speaker to
329
+ // physical output (eliminates AEC echo). Audio is captured directly from
330
+ // the WebRTC stream via CDP, so BlackHole 2ch is not needed for the
331
+ // speaker path.
332
+ if (this.bridge.captureMode === "webaudio") {
333
+ await this._setSlackSpeakerToDefault();
334
+ }
335
+
336
+ // Wire the openai-realtime brain with tools + executor + per-call
337
+ // identity (channel, peer, brief). Done here rather than at bridge
338
+ // construction so each new huddle session can pull fresh context.
339
+ if (this._brainKind === "openai-realtime") {
340
+ await this._configureRealtimeBrain(channel, mode);
341
+ }
342
+
343
+ // Start audio bridge
344
+ await this.bridge.start();
345
+
346
+ console.log(`[HUDDLE] Session active in ${channel} (${mode})`);
347
+ this._logEvent("session-started", { channel, mode });
348
+
349
+ // Speak a greeting
350
+ const greeting = mode === "initiated"
351
+ ? `Hey, do you have a moment?`
352
+ : `Hey, what's up?`;
353
+
354
+ await this.bridge.speak(greeting);
355
+ if (this.session) {
356
+ this.session.messages.push({ role: "assistant", content: greeting });
357
+ this.session.transcript.push({ speaker: AGENT_SLUG, text: greeting, ts: Date.now() });
358
+ }
359
+
360
+ // Start polling the huddle thread for text messages
361
+ this._startThreadPolling();
362
+ }
363
+
364
+ async _endSession() {
365
+ if (!this.session) return;
366
+
367
+ // Stop thread polling
368
+ if (this.session.threadPollTimer) {
369
+ clearInterval(this.session.threadPollTimer);
370
+ this.session.threadPollTimer = null;
371
+ }
372
+
373
+ // Stop audio bridge
374
+ await this.bridge.stop();
375
+
376
+ // Save transcript
377
+ this._saveTranscript();
378
+
379
+ // Log session end
380
+ const duration = Math.round((Date.now() - new Date(this.session.startedAt).getTime()) / 1000);
381
+ const turns = this.session.messages.length;
382
+ console.log(`[HUDDLE] Session ended — ${duration}s, ${turns} turns`);
383
+ this._logEvent("session-ended", {
384
+ channel: this.session.channel,
385
+ duration,
386
+ turns,
387
+ });
388
+
389
+ this.session = null;
390
+ }
391
+
392
+ // -------------------------------------------------------------------------
393
+ // Conversation loop (adapted from voice-ai/lib/claude.js)
394
+ // -------------------------------------------------------------------------
395
+
396
+ /**
397
+ * Configure the OpenAI Realtime bridge before start() — loads tools
398
+ * appropriate for voice + wires the action executor so the model can
399
+ * place follow-ups, send Slack messages, query RAG, etc. mid-huddle.
400
+ * The bridge itself loads identity + queues + team + RAG via
401
+ * lib/voice/context-loader.mjs at start().
402
+ */
403
+ async _configureRealtimeBrain(channel, mode) {
404
+ try {
405
+ const [{ actionsToRealtimeTools }, { executeAction }] = await Promise.all([
406
+ import("../../lib/voice/tool-bridge.mjs"),
407
+ import("../../lib/action-executor.js"),
408
+ ]);
409
+ const tools = await actionsToRealtimeTools({
410
+ accessLevel: "voice",
411
+ agentRoot: AGENT_REPO_DIR,
412
+ });
413
+ this.bridge.tools = tools;
414
+ this.bridge.peer = channel || null;
415
+ this.bridge.brief = mode === "initiated"
416
+ ? "You're initiating a casual huddle. Open with a quick check-in."
417
+ : `You've been invited to a huddle in ${channel}. Listen, then respond naturally.`;
418
+ this.bridge.executeTool = async ({ name, arguments: args, call_id }) => {
419
+ try {
420
+ const result = await executeAction(
421
+ name,
422
+ args,
423
+ { accessLevel: "voice", source: "huddle", channelId: channel },
424
+ `huddle-${this.session?.id || Date.now()}`,
425
+ { agentRoot: AGENT_REPO_DIR },
426
+ );
427
+ this._logEvent("tool-executed", { name, call_id, success: result?.success });
428
+ return typeof result === "string" ? result : JSON.stringify(result || {});
429
+ } catch (err) {
430
+ this._logEvent("tool-failed", { name, call_id, error: err.message });
431
+ return JSON.stringify({ ok: false, error: err.message });
432
+ }
433
+ };
434
+ } catch (err) {
435
+ console.warn(`[HUDDLE] failed to wire realtime tools: ${err.message}`);
436
+ }
437
+ }
438
+
439
+ /**
440
+ * Process a transcript from the huddle and generate the agent's response.
441
+ *
442
+ * Only used by the legacy Deepgram+Claude+11Labs brain. The openai-realtime
443
+ * brain handles speech-in → speech-out internally; this handler is a no-op
444
+ * for it (the bridge emits 'transcript' events for logging only).
445
+ *
446
+ * @param {string} userText - What someone said in the huddle
447
+ */
448
+ async _handleTranscript(userText) {
449
+ if (!this.session || this.state !== HuddleState.ACTIVE) return;
450
+ if (!userText || !userText.trim()) return;
451
+
452
+ // openai-realtime: just append to transcript, no Claude round-trip
453
+ if (this._brainKind === "openai-realtime") {
454
+ this.session.transcript.push({ speaker: "participant", text: userText, ts: Date.now() });
455
+ return;
456
+ }
457
+
458
+ console.log(`[SPEECH] "${userText}"`);
459
+
460
+ // Add to conversation history
461
+ this.session.messages.push({ role: "user", content: userText });
462
+ this.session.transcript.push({ speaker: "participant", text: userText, ts: Date.now() });
463
+
464
+ try {
465
+ // Get Claude's response
466
+ const responseText = await this._getClaudeResponse(this.session.messages);
467
+
468
+ if (!responseText || !responseText.trim()) return;
469
+
470
+ console.log(`[AGENT] "${responseText}"`);
471
+
472
+ // Check for end signal
473
+ const shouldEnd = responseText.includes("END_HUDDLE");
474
+ const cleanResponse = responseText.replace(/\s*END_HUDDLE\s*/g, "").trim();
475
+
476
+ // Add to history
477
+ this.session.messages.push({ role: "assistant", content: cleanResponse });
478
+ this.session.transcript.push({ speaker: AGENT_SLUG, text: cleanResponse, ts: Date.now() });
479
+
480
+ // Speak the response via TTS → BlackHole 16ch → Slack mic
481
+ await this.bridge.speak(cleanResponse);
482
+
483
+ if (shouldEnd) {
484
+ // Wait for TTS to finish, then leave
485
+ setTimeout(() => this.leaveHuddle(), 2000);
486
+ }
487
+ } catch (err) {
488
+ console.error(`[CLAUDE] Error: ${err.message}`);
489
+
490
+ const fallback = "Sorry, give me one second — I'm having a brief technical hiccup.";
491
+ await this.bridge.speak(fallback);
492
+ }
493
+ }
494
+
495
+ // -------------------------------------------------------------------------
496
+ // Huddle thread text message monitoring
497
+ // -------------------------------------------------------------------------
498
+
499
+ /**
500
+ * Start polling the huddle thread for text messages via the Slack API.
501
+ * Text messages in the huddle thread are injected into the conversation
502
+ * so the agent can respond to both voice and text.
503
+ */
504
+ _startThreadPolling() {
505
+ const slackToken = process.env.SLACK_USER_TOKEN || process.env.SLACK_BOT_TOKEN;
506
+ if (!slackToken) {
507
+ console.log("[THREAD] No Slack token — thread monitoring disabled");
508
+ return;
509
+ }
510
+
511
+ // Determine the huddle channel ID from the current Slack page
512
+ this._resolveHuddleChannelId().then((channelId) => {
513
+ if (!channelId) {
514
+ console.log("[THREAD] Could not resolve huddle channel — thread monitoring disabled");
515
+ return;
516
+ }
517
+
518
+ console.log(`[THREAD] Monitoring huddle thread in ${channelId}`);
519
+ this.session.lastThreadTs = String(Date.now() / 1000); // Start from now
520
+
521
+ this.session.threadPollTimer = setInterval(async () => {
522
+ try {
523
+ await this._pollThreadMessages(channelId, slackToken);
524
+ } catch (err) {
525
+ console.error(`[THREAD] Poll error: ${err.message}`);
526
+ }
527
+ }, 5000); // Poll every 5 seconds
528
+ });
529
+ }
530
+
531
+ /**
532
+ * Resolve the current huddle's channel ID from the Slack page via CDP.
533
+ * @returns {Promise<string|null>}
534
+ */
535
+ async _resolveHuddleChannelId() {
536
+ try {
537
+ const targets = await fetch("http://localhost:9222/json").then((r) => r.json());
538
+ const mainTarget = targets.find(
539
+ (t) => t.type === "page" && t.url?.includes("app.slack.com/client")
540
+ );
541
+ if (!mainTarget) return null;
542
+
543
+ // Extract channel ID from the URL: /client/TEAM_ID/CHANNEL_ID
544
+ const match = mainTarget.url.match(/\/client\/[A-Z0-9]+\/([A-Z0-9]+)/);
545
+ return match ? match[1] : null;
546
+ } catch {
547
+ return null;
548
+ }
549
+ }
550
+
551
+ /**
552
+ * Poll the Slack channel for recent messages in the huddle thread.
553
+ * Only processes text messages from other users (not the agent's bot).
554
+ */
555
+ async _pollThreadMessages(channelId, token) {
556
+ if (!this.session) return;
557
+
558
+ const oldest = this.session.lastThreadTs || "0";
559
+ const url = `https://slack.com/api/conversations.history?channel=${channelId}&oldest=${oldest}&limit=10`;
560
+
561
+ const resp = await fetch(url, {
562
+ headers: { Authorization: `Bearer ${token}` },
563
+ });
564
+ const data = await resp.json();
565
+ if (!data.ok || !data.messages || data.messages.length === 0) return;
566
+
567
+ // The agent's Slack user ID (skip its own messages). Hardcoded fallback
568
+ // removed — agents must set SLACK_BOT_USER_ID in .env if they want this
569
+ // filter active, otherwise the agent's slackMemberId from agent.json is used.
570
+ const agentUserId = process.env.SLACK_BOT_USER_ID || loadAgent().slackMemberId || "";
571
+
572
+ for (const msg of data.messages.reverse()) { // oldest first
573
+ // Skip bot messages, the agent's own messages, and non-text messages
574
+ if (agentUserId && msg.user === agentUserId) continue;
575
+ if (msg.subtype) continue; // Skip system messages
576
+ if (!msg.text || !msg.text.trim()) continue;
577
+
578
+ // Update last seen timestamp
579
+ this.session.lastThreadTs = msg.ts;
580
+
581
+ // Handle the text message like a voice transcript
582
+ const senderName = msg.user_profile?.display_name || msg.user || "someone";
583
+ const textContent = `[Text message from ${senderName}]: ${msg.text}`;
584
+
585
+ console.log(`[THREAD] ${textContent}`);
586
+ this.session.transcript.push({ speaker: "text", text: textContent, ts: Date.now() });
587
+
588
+ // Add to conversation and generate response
589
+ await this._handleTranscript(textContent);
590
+ }
591
+ }
592
+
593
+ /**
594
+ * Get a response from Claude given the conversation history.
595
+ * Uses Claude Code CLI (Max subscription) by default, falling back
596
+ * to the Anthropic API if CLI is unavailable or HUDDLE_USE_API=1.
597
+ *
598
+ * @param {Array} messages - Conversation history [{role, content}]
599
+ * @returns {Promise<string>} Response text
600
+ */
601
+ async _getClaudeResponse(messages) {
602
+ if (USE_CLAUDE_CLI) {
603
+ return this._getClaudeResponseCLI(messages);
604
+ }
605
+ return this._getClaudeResponseAPI(messages);
606
+ }
607
+
608
+ /**
609
+ * Get a response via Claude Code CLI (uses Max subscription).
610
+ * Constructs a prompt from system + conversation history and pipes
611
+ * it through `claude --print`.
612
+ */
613
+ async _getClaudeResponseCLI(messages) {
614
+ const systemPrompt = this._buildSystemPrompt();
615
+
616
+ // Build a single prompt from system + conversation history
617
+ const parts = [`[System]\n${systemPrompt}\n`];
618
+ for (const msg of messages) {
619
+ const role = msg.role === "assistant" ? (loadAgent().firstName || "Agent") : "Participant";
620
+ const content = typeof msg.content === "string"
621
+ ? msg.content
622
+ : msg.content?.map((b) => b.text || "").join("") || "";
623
+ if (content.trim()) {
624
+ parts.push(`[${role}]\n${content}`);
625
+ }
626
+ }
627
+ parts.push(`[${loadAgent().firstName || "Agent"}]`);
628
+
629
+ const prompt = parts.join("\n\n");
630
+
631
+ const { stdout } = await execFileAsync("claude", [
632
+ "--print",
633
+ "--model", "sonnet",
634
+ "-p", prompt,
635
+ ], {
636
+ timeout: 30000,
637
+ maxBuffer: 1024 * 1024,
638
+ env: { ...process.env, TERM: "dumb" },
639
+ });
640
+
641
+ return (stdout || "").trim();
642
+ }
643
+
644
+ /**
645
+ * Get a response via Anthropic API (pay-per-token fallback).
646
+ * Supports tool use with multi-round execution.
647
+ */
648
+ async _getClaudeResponseAPI(messages) {
649
+ const systemPrompt = this._buildSystemPrompt();
650
+ const accessLevel = this.session?.accessLevel || "default";
651
+ const tools = getToolsForAccessLevel ? getToolsForAccessLevel(accessLevel) : [];
652
+
653
+ const apiParams = {
654
+ model: "claude-sonnet-4-20250514",
655
+ max_tokens: MAX_TOKENS,
656
+ system: systemPrompt,
657
+ messages: [...messages],
658
+ };
659
+
660
+ if (tools.length > 0) {
661
+ apiParams.tools = tools;
662
+ }
663
+
664
+ let fullResponse = "";
665
+ let round = 0;
666
+
667
+ while (round < MAX_TOOL_ROUNDS) {
668
+ round++;
669
+ const response = await anthropic.messages.create(apiParams);
670
+
671
+ const textParts = [];
672
+ const toolUseBlocks = [];
673
+
674
+ for (const block of response.content) {
675
+ if (block.type === "text") {
676
+ textParts.push(block.text);
677
+ } else if (block.type === "tool_use") {
678
+ toolUseBlocks.push(block);
679
+ }
680
+ }
681
+
682
+ const roundText = textParts.join("");
683
+ fullResponse += roundText;
684
+
685
+ if (toolUseBlocks.length === 0) break;
686
+
687
+ if (roundText.trim()) {
688
+ const filler = roundText.replace(/\s*END_HUDDLE\s*/g, "").trim();
689
+ if (filler) {
690
+ await this.bridge.speak(filler);
691
+ if (this.session) {
692
+ this.session.transcript.push({ speaker: AGENT_SLUG, text: filler, ts: Date.now() });
693
+ }
694
+ }
695
+ }
696
+
697
+ const assistantContent = [];
698
+ if (roundText.trim()) {
699
+ assistantContent.push({ type: "text", text: roundText });
700
+ }
701
+ for (const tb of toolUseBlocks) {
702
+ assistantContent.push({ type: "tool_use", id: tb.id, name: tb.name, input: tb.input });
703
+ }
704
+ apiParams.messages.push({ role: "assistant", content: assistantContent });
705
+
706
+ const toolResults = [];
707
+ for (const tb of toolUseBlocks) {
708
+ console.log(`[TOOL] Executing: ${tb.name}`);
709
+ this._logEvent("tool-call", { tool: tb.name, input: tb.input });
710
+
711
+ if (executeAction) {
712
+ const callerInfo = {
713
+ slug: this.session?.callerSlug || "huddle-participant",
714
+ name: this.session?.callerName || "Huddle Participant",
715
+ accessLevel,
716
+ };
717
+ const result = await executeAction(tb.name, tb.input, callerInfo, `huddle-${Date.now()}`);
718
+ toolResults.push({ type: "tool_result", tool_use_id: tb.id, content: result.result });
719
+ console.log(`[TOOL] ${tb.name}: ${result.success ? "OK" : "FAILED"}`);
720
+ } else {
721
+ toolResults.push({
722
+ type: "tool_result",
723
+ tool_use_id: tb.id,
724
+ content: "Tool execution not available in this session.",
725
+ });
726
+ }
727
+ }
728
+
729
+ apiParams.messages.push({ role: "user", content: toolResults });
730
+ }
731
+
732
+ return fullResponse.trim();
733
+ }
734
+
735
+ // -------------------------------------------------------------------------
736
+ // Participant resolution (for access control + personalization)
737
+ // -------------------------------------------------------------------------
738
+
739
+ /**
740
+ * Resolve a huddle participant's identity from the agent repo contacts/profiles.
741
+ * Used for access control (CEO gets write tools) and personalized greetings.
742
+ */
743
+ _resolveHuddleParticipant(identifier) {
744
+ // Seed `knownParticipants` with the agent's PRINCIPAL from
745
+ // config/agent.json (SOT) — replaces the previously-hardcoded "alex"
746
+ // entry. Falls back to common operational handles for back-compat.
747
+ const knownParticipants = {
748
+ "#ceo-office": { slug: "ceo-office", name: "CEO Office Channel", accessLevel: "ceo" },
749
+ };
750
+ try {
751
+ const agentPath = join(AGENT_REPO_DIR, "config/agent.json");
752
+ const agentJson = JSON.parse(readFileSync(agentPath, "utf-8"));
753
+ const principal = agentJson?.principal;
754
+ if (principal?.firstName) {
755
+ const slug = `${principal.firstName}-${principal.lastName || ""}`.toLowerCase().replace(/\s+/g, "-").replace(/-+$/, "");
756
+ const entry = {
757
+ slug,
758
+ name: principal.fullName || `${principal.firstName} ${principal.lastName || ""}`.trim(),
759
+ accessLevel: "ceo",
760
+ };
761
+ knownParticipants[principal.firstName.toLowerCase()] = entry;
762
+ knownParticipants[slug] = entry;
763
+ knownParticipants["principal"] = entry;
764
+ }
765
+ } catch { /* agent.json unavailable — only #ceo-office remains */ }
766
+
767
+ const key = (identifier || "").toLowerCase().replace(/^#/, "");
768
+ const match = knownParticipants[key];
769
+
770
+ if (match) return match;
771
+
772
+ // Try loading from contacts.yaml
773
+ try {
774
+ const contactsPath = join(AGENT_REPO_DIR, "config/contacts.yaml");
775
+ const contacts = readFileSync(contactsPath, "utf8");
776
+ // Simple search — look for the identifier in the contacts file
777
+ if (contacts.toLowerCase().includes(key)) {
778
+ // If found in contacts, give default access
779
+ return { slug: key, name: identifier, accessLevel: "default" };
780
+ }
781
+ } catch { /* optional */ }
782
+
783
+ return { slug: "unknown", name: identifier || "Unknown", accessLevel: "default" };
784
+ }
785
+
786
+ // -------------------------------------------------------------------------
787
+ // Slack Events API listener (huddle detection)
788
+ // -------------------------------------------------------------------------
789
+
790
+ /**
791
+ * Start a lightweight HTTP server that receives forwarded huddle events
792
+ * from the main slack-events-server.mjs. This provides a more reliable
793
+ * huddle detection path than DOM polling alone.
794
+ */
795
+ _startEventsListener() {
796
+ this.eventsServer = createServer((req, res) => {
797
+ if (req.method !== "POST" || req.url !== "/huddle-event") {
798
+ res.writeHead(404);
799
+ res.end();
800
+ return;
801
+ }
802
+
803
+ let body = "";
804
+ req.on("data", (chunk) => { body += chunk; });
805
+ req.on("end", () => {
806
+ res.writeHead(200, { "Content-Type": "application/json" });
807
+ res.end('{"ok":true}');
808
+
809
+ try {
810
+ const event = JSON.parse(body);
811
+ this._handleHuddleEvent(event);
812
+ } catch (err) {
813
+ console.error(`[EVENTS] Parse error: ${err.message}`);
814
+ }
815
+ });
816
+ });
817
+
818
+ this.eventsServer.listen(HUDDLE_EVENTS_PORT, "127.0.0.1", () => {
819
+ console.log(`[EVENTS] Huddle event listener on port ${HUDDLE_EVENTS_PORT}`);
820
+ });
821
+ }
822
+
823
+ /**
824
+ * Handle a huddle event forwarded from the Slack Events API.
825
+ * Event shape: { user, huddle_state, channel, channel_type }
826
+ */
827
+ _handleHuddleEvent(event) {
828
+ const { user, user_name, huddle_state, channel, channel_type } = event;
829
+
830
+ console.log(`[EVENTS] Huddle event: ${user_name || user} → ${huddle_state} in ${channel || "unknown"}`);
831
+ this._logEvent("events-api-huddle", event);
832
+
833
+ // Someone started a huddle and the agent isn't in one yet
834
+ if (huddle_state === "in_a_huddle" && this.state === HuddleState.IDLE && this.autoJoin) {
835
+ // Determine the channel to join
836
+ const targetChannel = channel || user_name;
837
+ if (targetChannel) {
838
+ console.log(`[EVENTS] Huddle detected — auto-joining ${targetChannel}`);
839
+ this.joinHuddle(targetChannel);
840
+ }
841
+ }
842
+ }
843
+
844
+ // -------------------------------------------------------------------------
845
+ // System prompt (adapted for huddle context)
846
+ // -------------------------------------------------------------------------
847
+
848
+ _buildSystemPrompt() {
849
+ const liveContextBlock = this.session?.liveContext
850
+ ? `\n\nLIVE CONTEXT:\n${this.session.liveContext}`
851
+ : "";
852
+
853
+ return `${HUDDLE_SYSTEM_PROMPT}${liveContextBlock}`;
854
+ }
855
+
856
+ // -------------------------------------------------------------------------
857
+ // Live context loader (reads from the agent repo state files)
858
+ // -------------------------------------------------------------------------
859
+
860
+ _loadLiveContext() {
861
+ const contextParts = [];
862
+
863
+ // Load priorities
864
+ try {
865
+ const priorities = readFileSync(join(AGENT_REPO_DIR, "config/priorities.yaml"), "utf8");
866
+ contextParts.push(`PRIORITIES:\n${priorities.slice(0, 2000)}`);
867
+ } catch { /* optional */ }
868
+
869
+ // Load executive summary
870
+ try {
871
+ const summary = readFileSync(join(AGENT_REPO_DIR, "state/dashboards/executive-summary.yaml"), "utf8");
872
+ contextParts.push(`EXECUTIVE SUMMARY:\n${summary.slice(0, 3000)}`);
873
+ } catch { /* optional */ }
874
+
875
+ return contextParts.join("\n\n");
876
+ }
877
+
878
+ // -------------------------------------------------------------------------
879
+ // Event wiring
880
+ // -------------------------------------------------------------------------
881
+
882
+ _wireControllerEvents() {
883
+ this.controller.on("connected", () => {
884
+ console.log("[CDP] Connected to Slack");
885
+ });
886
+
887
+ this.controller.on("disconnected", () => {
888
+ console.log("[CDP] Disconnected from Slack — reconnecting...");
889
+ });
890
+
891
+ this.controller.on("huddle:invitation", async (data) => {
892
+ console.log(`[HUDDLE] Incoming invitation from ${data.from} in ${data.channel}`);
893
+ this._logEvent("invitation-received", data);
894
+
895
+ if (this.autoJoin && this.state === HuddleState.IDLE) {
896
+ console.log("[HUDDLE] Auto-joining...");
897
+ await this.controller.acceptInvitation();
898
+ await this._startSession(data.channel, "joined");
899
+ }
900
+ });
901
+
902
+ this.controller.on("huddle:joined", async (data) => {
903
+ console.log(`[HUDDLE] Active huddle detected: ${data.channel}`);
904
+ if (this.state === HuddleState.IDLE) {
905
+ await this._startSession(data.channel, "joined");
906
+ }
907
+ });
908
+
909
+ this.controller.on("huddle:left", async () => {
910
+ if (this.state === HuddleState.ACTIVE) {
911
+ // Debounce: the polling can falsely detect "left" when the huddle
912
+ // runs in a separate window and the main window DOM is temporarily
913
+ // empty. Wait 5 seconds and re-check before ending the session.
914
+ console.log("[HUDDLE] Huddle may have ended — verifying in 5s...");
915
+ await new Promise((r) => setTimeout(r, 5000));
916
+ const stillActive = await this.controller.isInHuddle();
917
+ if (!stillActive && this.state === HuddleState.ACTIVE) {
918
+ console.log("[HUDDLE] Confirmed: huddle ended");
919
+ await this._endSession();
920
+ this.state = HuddleState.IDLE;
921
+ } else {
922
+ console.log("[HUDDLE] False alarm — huddle is still active");
923
+ }
924
+ }
925
+ });
926
+
927
+ this.controller.on("error", (err) => {
928
+ console.error(`[CDP] Error: ${err.message} — ${err.detail || ""}`);
929
+ });
930
+ }
931
+
932
+ _wireBridgeEvents() {
933
+ this.bridge.on("transcript", (data) => {
934
+ // openai-realtime emits {role, text} — split user vs assistant
935
+ // legacy bridge emits {text} for user-side only
936
+ if (this._brainKind === "openai-realtime") {
937
+ if (!this.session) return;
938
+ const speaker = data.role === "assistant" ? AGENT_SLUG : "participant";
939
+ this.session.transcript.push({ speaker, text: data.text, ts: Date.now() });
940
+ if (data.role === "assistant") {
941
+ console.log(`[${AGENT_SLUG.toUpperCase()}] "${data.text}"`);
942
+ } else {
943
+ console.log(`[SPEECH] "${data.text}"`);
944
+ }
945
+ return;
946
+ }
947
+ // Legacy: drive the Claude conversation loop
948
+ this._handleTranscript(data.text);
949
+ });
950
+
951
+ this.bridge.on("transcript:interim", (data) => {
952
+ // Interim transcript — show for debugging
953
+ process.stdout.write(`\r[INTERIM] ${data.text} `);
954
+ });
955
+
956
+ this.bridge.on("tool:call", (data) => {
957
+ console.log(`[TOOL] ${data.name}(${(data.arguments || "").slice(0, 80)})`);
958
+ });
959
+
960
+ this.bridge.on("speech:start", () => {
961
+ // Someone started talking
962
+ });
963
+
964
+ this.bridge.on("speech:end", () => {
965
+ process.stdout.write("\r" + " ".repeat(80) + "\r"); // Clear interim line
966
+ });
967
+
968
+ this.bridge.on("tts:start", async (data) => {
969
+ // Legacy bridge emits {text: ...} (a discrete utterance about to play);
970
+ // openai-realtime emits empty (continuous audio stream just opened).
971
+ const preview = data?.text ? `"${data.text.substring(0, 60)}…"` : "(audio stream live)";
972
+ console.log(`[TTS] ${preview}`);
973
+ // Unmute Slack mic so TTS audio reaches the huddle
974
+ await this._setSlackMicMute(false);
975
+ });
976
+
977
+ this.bridge.on("tts:end", async () => {
978
+ // Re-mute Slack mic after TTS to prevent echo
979
+ // Small delay to ensure the last audio samples are transmitted
980
+ setTimeout(() => this._setSlackMicMute(true), 300);
981
+ });
982
+
983
+ this.bridge.on("error", (err) => {
984
+ console.error(`[BRIDGE] ${err.component} error: ${err.message}`);
985
+ });
986
+ }
987
+
988
+ // -------------------------------------------------------------------------
989
+ // Slack mic mute control (half-duplex echo prevention)
990
+ // -------------------------------------------------------------------------
991
+
992
+ /**
993
+ * Toggle the agent's Slack mic mute via CDP on the huddle window.
994
+ * Keeps mic muted between TTS outputs to prevent echo from the
995
+ * virtual audio loopback.
996
+ * @param {boolean} muted - true to mute, false to unmute
997
+ */
998
+ async _setSlackMicMute(muted) {
999
+ // In webaudio mode, mic muting for echo prevention is unnecessary —
1000
+ // the echo is eliminated at the capture level, not the mic level.
1001
+ if (this.bridge.captureMode === "webaudio") return;
1002
+ try {
1003
+ const targets = await fetch("http://localhost:9222/json").then((r) => r.json());
1004
+ const huddleTarget = targets.find(
1005
+ (t) => t.type === "page" && (t.title?.includes("Huddle:") || t.title?.includes("🎤"))
1006
+ );
1007
+ if (!huddleTarget) return;
1008
+
1009
+ const ws = new (await import("ws")).WebSocket(huddleTarget.webSocketDebuggerUrl);
1010
+ await new Promise((resolve, reject) => {
1011
+ ws.on("error", reject);
1012
+ ws.on("open", () => {
1013
+ ws.send(JSON.stringify({
1014
+ id: 1,
1015
+ method: "Runtime.evaluate",
1016
+ params: {
1017
+ expression: `(() => {
1018
+ const muteBtn = document.querySelector('[data-qa="segmented-mute-button-main"]');
1019
+ if (!muteBtn) return "no-mute-btn";
1020
+ const label = (muteBtn.getAttribute("aria-label") || "").toLowerCase();
1021
+ const currentlyMuted = label.includes("unmute");
1022
+ if (currentlyMuted === ${muted}) return "already-${muted ? "muted" : "unmuted"}";
1023
+ muteBtn.click();
1024
+ return "${muted ? "muted" : "unmuted"}";
1025
+ })()`,
1026
+ returnByValue: true,
1027
+ },
1028
+ }));
1029
+ });
1030
+ ws.on("message", (data) => {
1031
+ const msg = JSON.parse(data.toString());
1032
+ if (msg.id === 1) {
1033
+ const result = msg.result?.result?.value || "unknown";
1034
+ if (result !== "already-muted" && result !== "already-unmuted") {
1035
+ console.log(`[MIC] ${result}`);
1036
+ }
1037
+ ws.close();
1038
+ resolve();
1039
+ }
1040
+ });
1041
+ });
1042
+ } catch {
1043
+ // Non-fatal — mute toggle failed silently
1044
+ }
1045
+ }
1046
+
1047
+ /**
1048
+ * Set Slack's speaker output to "default" (physical speakers) via CDP.
1049
+ * Used in webaudio capture mode to eliminate AEC echo — audio is captured
1050
+ * directly from the WebRTC stream, so BlackHole 2ch isn't needed.
1051
+ */
1052
+ async _setSlackSpeakerToDefault() {
1053
+ try {
1054
+ const targets = await fetch("http://localhost:9222/json").then((r) => r.json());
1055
+ const mainTarget = targets.find(
1056
+ (t) => t.type === "page" && t.url?.includes("app.slack.com/client")
1057
+ );
1058
+ if (!mainTarget) return;
1059
+
1060
+ const ws = new (await import("ws")).WebSocket(mainTarget.webSocketDebuggerUrl);
1061
+ await new Promise((resolve) => {
1062
+ ws.on("error", () => resolve());
1063
+ ws.on("open", () => {
1064
+ ws.send(JSON.stringify({
1065
+ id: 1,
1066
+ method: "Runtime.evaluate",
1067
+ params: {
1068
+ expression: `(async () => {
1069
+ // Set speaker preference to default (physical speakers)
1070
+ const inputId = "9f391a3f2efb9f50f9dc8dea1534700bcc5d4e0478b56438930a2071401b25bb";
1071
+ localStorage.setItem("preferred-media-devices",
1072
+ JSON.stringify({ audioinput: inputId, audiooutput: "default" }));
1073
+
1074
+ // Also change the active audio element's output immediately
1075
+ for (const audio of document.querySelectorAll("audio")) {
1076
+ try { await audio.setSinkId("default"); } catch {}
1077
+ }
1078
+ return "speaker set to default";
1079
+ })()`,
1080
+ returnByValue: true,
1081
+ awaitPromise: true,
1082
+ },
1083
+ }));
1084
+ });
1085
+ ws.on("message", (data) => {
1086
+ const msg = JSON.parse(data.toString());
1087
+ if (msg.id === 1) {
1088
+ console.log(`[AUDIO] Slack speaker → physical (${msg.result?.result?.value})`);
1089
+ ws.close();
1090
+ resolve();
1091
+ }
1092
+ });
1093
+ });
1094
+ } catch {
1095
+ // Non-fatal
1096
+ }
1097
+ }
1098
+
1099
+ // -------------------------------------------------------------------------
1100
+ // Logging & transcript persistence
1101
+ // -------------------------------------------------------------------------
1102
+
1103
+ _logEvent(event, data = {}) {
1104
+ const logDir = join(AGENT_REPO_DIR, "logs/huddle");
1105
+ mkdirSync(logDir, { recursive: true });
1106
+
1107
+ const date = new Date().toISOString().split("T")[0];
1108
+ const logFile = join(logDir, `${date}-huddle-events.jsonl`);
1109
+
1110
+ const entry = {
1111
+ timestamp: new Date().toISOString(),
1112
+ event,
1113
+ ...data,
1114
+ };
1115
+
1116
+ appendFileSync(logFile, JSON.stringify(entry) + "\n");
1117
+ }
1118
+
1119
+ _saveTranscript() {
1120
+ if (!this.session || this.session.transcript.length === 0) return;
1121
+
1122
+ const logDir = join(AGENT_REPO_DIR, "logs/huddle");
1123
+ mkdirSync(logDir, { recursive: true });
1124
+
1125
+ const date = new Date().toISOString().split("T")[0];
1126
+ const ts = Date.now();
1127
+ const channel = (this.session.channel || "unknown").replace(/[^a-zA-Z0-9-]/g, "_");
1128
+ const transcriptFile = join(logDir, `${date}-transcript-${channel}-${ts}.jsonl`);
1129
+
1130
+ for (const entry of this.session.transcript) {
1131
+ appendFileSync(transcriptFile, JSON.stringify(entry) + "\n");
1132
+ }
1133
+
1134
+ console.log(`[TRANSCRIPT] Saved to ${transcriptFile}`);
1135
+ }
1136
+ }
1137
+
1138
+ // ---------------------------------------------------------------------------
1139
+ // Huddle-specific system prompt
1140
+ // ---------------------------------------------------------------------------
1141
+
1142
+ function buildHuddleSystemPrompt() {
1143
+ const a = loadAgent();
1144
+ const principal = a.principal || {};
1145
+ return `You are ${a.fullName}, ${a.title} to ${principal.fullName || "the principal"}, ${principal.title || "the principal"} of ${a.company}.
1146
+ You are participating in a Slack huddle — a real-time voice conversation.
1147
+
1148
+ PERSONALITY:
1149
+ - Sound like a thoughtful colleague, not an assistant. Full guide: policies/communication-style.md
1150
+ - Natural, conversational, warm — this is a casual voice huddle, not a formal call
1151
+ - Concise responses — one or two sentences per turn, then pause
1152
+ - Use contractions, casual phrasing, natural filler words ("yeah", "right", "got it")
1153
+ - Match the energy of the other participants — terse if they're terse, warm if they're warm
1154
+ - If they are discussing something serious, be appropriately focused
1155
+
1156
+ BANNED OPENERS AND PHRASES (do not use):
1157
+ - "Certainly!" / "Absolutely!" / "Great question!" as openers
1158
+ - "I'd be happy to…" / "I'm here to help" / "Let me know if you need anything else"
1159
+ - "As an AI" / "As a language model" / "I should clarify that…"
1160
+ - "I hope this helps" / "Feel free to ask if you have any questions"
1161
+ - "Here's a comprehensive overview of…"
1162
+ - Don't re-introduce yourself by listing your role at length. "Hi, this is ${a.firstName || "the agent"}" is enough.
1163
+
1164
+ ABOUT ADAPTIC:
1165
+ - Northwind is a global AI-native institutional asset management group
1166
+ - Headquartered in DIFC, Dubai with entities across seven plus jurisdictions
1167
+ - CEO and founder: the principal (resolved from config/agent.json)
1168
+ - Building Northwind OS, an algorithmic trading platform
1169
+ - Currently in regulatory licensing phase
1170
+
1171
+ HUDDLE RULES:
1172
+ - Huddles are informal — keep responses short and natural
1173
+ - Listen more than you talk — do not dominate the conversation
1174
+ - If asked a direct question, answer it concisely
1175
+ - If you hear a request or action item, confirm it: "Got it, I will take care of that"
1176
+ - If you cannot help with something, say so briefly
1177
+ - If the conversation naturally ends or someone says goodbye, respond with a brief farewell and end your response with END_HUDDLE on its own line
1178
+
1179
+ MULTI-SPEAKER AWARENESS:
1180
+ - Multiple people may be in the huddle
1181
+ - You may not always know who is speaking — that is okay
1182
+ - If someone introduces themselves, use their name
1183
+ - Do not assume all speech is from one person
1184
+
1185
+ SPEECH RULES:
1186
+ - Never use special characters, markdown, bullet points, or formatting
1187
+ - Spell out numbers in words
1188
+ - Keep it conversational — say "yeah" not "yes indeed"
1189
+ - Do not use emojis
1190
+ - When saying email addresses, use "at" and "dot" instead of symbols
1191
+
1192
+ CRITICAL — DO NOT REPEAT YOURSELF:
1193
+ - Never re-introduce yourself after the initial greeting
1194
+ - Each response should add new information or move the conversation forward
1195
+ - If you already answered a question, do not repeat the answer`;
1196
+ }
1197
+
1198
+ const HUDDLE_SYSTEM_PROMPT = buildHuddleSystemPrompt();
1199
+
1200
+ // ---------------------------------------------------------------------------
1201
+ // Main
1202
+ // ---------------------------------------------------------------------------
1203
+
1204
+ const server = new HuddleServer();
1205
+
1206
+ // Graceful shutdown
1207
+ process.on("SIGINT", async () => {
1208
+ console.log("\n[SHUTDOWN] Shutting down huddle server...");
1209
+ if (server.state === HuddleState.ACTIVE) {
1210
+ await server.bridge.speak("I need to go — talk soon.");
1211
+ await server.leaveHuddle();
1212
+ }
1213
+ await server.controller.disconnect();
1214
+ process.exit(0);
1215
+ });
1216
+
1217
+ process.on("SIGTERM", async () => {
1218
+ if (server.state === HuddleState.ACTIVE) {
1219
+ await server.leaveHuddle();
1220
+ }
1221
+ await server.controller.disconnect();
1222
+ process.exit(0);
1223
+ });
1224
+
1225
+ // Start
1226
+ server.start().catch((err) => {
1227
+ console.error(`[FATAL] ${err.message}`);
1228
+ process.exit(1);
1229
+ });