@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.
- package/.claude/commands/init-agent.md +104 -0
- package/.claude/commands/init-maestro.md +1187 -0
- package/.claude/settings.json +161 -0
- package/.env.example +216 -0
- package/README.md +632 -0
- package/agents/browser-operator/agent.md +52 -0
- package/agents/calendar-ops/agent.md +50 -0
- package/agents/communications/agent.md +96 -0
- package/agents/decision-log/agent.md +65 -0
- package/agents/desktop-operator/agent.md +59 -0
- package/agents/gmail-operator/agent.md +62 -0
- package/agents/inbound-dispatcher/agent.md +66 -0
- package/agents/inbox-processor/agent.md +39 -0
- package/agents/pmo-execution/agent.md +60 -0
- package/agents/session-spawner/agent.md +64 -0
- package/agents/slack-operator/agent.md +60 -0
- package/agents/whatsapp-operator/agent.md +60 -0
- package/agents/workflow-automation/agent.md +61 -0
- package/archetypes/altitudes/c-suite.yaml +58 -0
- package/archetypes/altitudes/founder.yaml +68 -0
- package/archetypes/altitudes/senior-manager.yaml +63 -0
- package/archetypes/altitudes/svp.yaml +60 -0
- package/archetypes/altitudes/vp.yaml +50 -0
- package/archetypes/archetype.schema.json +77 -0
- package/archetypes/base.yaml +47 -0
- package/archetypes/capabilities/commercial-leader.yaml +159 -0
- package/archetypes/capabilities/compliance-officer.yaml +159 -0
- package/archetypes/capabilities/executive-operator.yaml +169 -0
- package/archetypes/capabilities/finance-leader.yaml +162 -0
- package/archetypes/capabilities/operations-leader.yaml +154 -0
- package/archetypes/capabilities/product-leader.yaml +148 -0
- package/archetypes/capabilities/technical-leader.yaml +146 -0
- package/archetypes/functions/commercial-leader.yaml +62 -0
- package/archetypes/functions/compliance-officer.yaml +64 -0
- package/archetypes/functions/executive-operator.yaml +70 -0
- package/archetypes/functions/finance-leader.yaml +70 -0
- package/archetypes/functions/operations-leader.yaml +62 -0
- package/archetypes/functions/product-leader.yaml +61 -0
- package/archetypes/functions/technical-leader.yaml +57 -0
- package/bin/cohort-mcp.mjs +81 -0
- package/bin/maestro.mjs +3516 -0
- package/bin/maestro.test.mjs +1015 -0
- package/desktop-control/README.md +56 -0
- package/desktop-control/app-profiles/gmail.yaml +120 -0
- package/desktop-control/app-profiles/slack.yaml +315 -0
- package/desktop-control/app-profiles/whatsapp.yaml +107 -0
- package/docs/architecture/agent-topology.md +2239 -0
- package/docs/architecture/archetype-agent-factory.md +110 -0
- package/docs/architecture/collective-memory-and-org-mesh.md +115 -0
- package/docs/architecture/continuous-monitoring.md +221 -0
- package/docs/architecture/mcp-capability-map.md +585 -0
- package/docs/architecture/system-architecture.md +1272 -0
- package/docs/company-context/README.md +40 -0
- package/docs/guides/agent-persona-setup.md +600 -0
- package/docs/guides/agents-observe-setup.md +64 -0
- package/docs/guides/billing-console-keys.md +88 -0
- package/docs/guides/ccxray-diagnostics.md +65 -0
- package/docs/guides/channel-bus.md +127 -0
- package/docs/guides/claude-mem-setup.md +79 -0
- package/docs/guides/claude-pace-setup.md +56 -0
- package/docs/guides/claudraband-sessions.md +98 -0
- package/docs/guides/clawteam-swarm.md +116 -0
- package/docs/guides/code-review-graph-setup.md +86 -0
- package/docs/guides/email-setup.md +431 -0
- package/docs/guides/mac-mini.md +119 -0
- package/docs/guides/media-generation-setup.md +349 -0
- package/docs/guides/model-routing.md +162 -0
- package/docs/guides/observability-otel.md +265 -0
- package/docs/guides/org-onboarding.md +132 -0
- package/docs/guides/outbound-governance-setup.md +437 -0
- package/docs/guides/pdf-generation-setup.md +315 -0
- package/docs/guides/poller-daemon-setup.md +563 -0
- package/docs/guides/rag-context-setup.md +459 -0
- package/docs/guides/self-optimization-pattern.md +82 -0
- package/docs/guides/setup-wizard.md +178 -0
- package/docs/guides/slack-setup.md +350 -0
- package/docs/guides/telegram-setup.md +227 -0
- package/docs/guides/twilio-subaccounts-setup.md +223 -0
- package/docs/guides/verification.md +128 -0
- package/docs/guides/voice-mode.md +188 -0
- package/docs/guides/voice-sms-setup.md +698 -0
- package/docs/guides/webhook-relay-setup.md +349 -0
- package/docs/guides/whatsapp-setup.md +288 -0
- package/docs/prompts/board-pack-cover-template.md +36 -0
- package/docs/prompts/decision-recommendation-template.md +88 -0
- package/docs/prompts/followup-message-template.md +141 -0
- package/docs/prompts/investor-letter-template.md +52 -0
- package/docs/prompts/morning-brief-template.md +82 -0
- package/docs/prompts/presentation-template.md +58 -0
- package/docs/prompts/weekly-strategic-memo-template.md +104 -0
- package/docs/research/hallucinated-tool-output-investigation.md +151 -0
- package/docs/runbooks/backup-restore.md +205 -0
- package/docs/runbooks/cohort-cutover.md +129 -0
- package/docs/runbooks/fleet-operations.md +200 -0
- package/docs/runbooks/incident-response.md +226 -0
- package/docs/runbooks/mac-mini-bootstrap.md +431 -0
- package/docs/runbooks/perpetual-operations.md +509 -0
- package/docs/runbooks/recovery-and-failover.md +260 -0
- package/framework-features.json +267 -0
- package/ingest/README.md +87 -0
- package/lib/action-executor.js +689 -0
- package/lib/action-executor.test.mjs +871 -0
- package/lib/agent-root.mjs +37 -0
- package/lib/archetype.mjs +236 -0
- package/lib/archetype.test.mjs +132 -0
- package/lib/autonomy.mjs +114 -0
- package/lib/autonomy.test.mjs +66 -0
- package/lib/backlog.mjs +358 -0
- package/lib/backlog.test.mjs +266 -0
- package/lib/budget-guard.mjs +279 -0
- package/lib/budget-guard.test.mjs +291 -0
- package/lib/cadence-bus-schedule.test.mjs +194 -0
- package/lib/cadence-bus.mjs +1120 -0
- package/lib/cadence-bus.test.mjs +720 -0
- package/lib/cadences.mjs +205 -0
- package/lib/cadences.test.mjs +125 -0
- package/lib/capability.mjs +154 -0
- package/lib/capability.test.mjs +78 -0
- package/lib/channels/base-adapter.mjs +719 -0
- package/lib/channels/base-adapter.test.mjs +590 -0
- package/lib/channels/channel.mjs +128 -0
- package/lib/channels/channels.test.mjs +371 -0
- package/lib/channels/contract.mjs +215 -0
- package/lib/channels/contract.test.mjs +137 -0
- package/lib/channels/conversation-resolver.mjs +95 -0
- package/lib/channels/gmail/adapter.mjs +87 -0
- package/lib/channels/inbox-item.mjs +255 -0
- package/lib/channels/inbox-item.test.mjs +335 -0
- package/lib/channels/index.mjs +94 -0
- package/lib/channels/orgmail/adapter.mjs +353 -0
- package/lib/channels/orgmail/adapter.test.mjs +311 -0
- package/lib/channels/pairing.mjs +363 -0
- package/lib/channels/pairing.test.mjs +270 -0
- package/lib/channels/registry.mjs +164 -0
- package/lib/channels/slack/adapter.mjs +317 -0
- package/lib/channels/slack-adapter.test.mjs +212 -0
- package/lib/channels/sms/adapter.mjs +43 -0
- package/lib/channels/telegram/adapter.mjs +432 -0
- package/lib/channels/telegram-adapter.test.mjs +306 -0
- package/lib/channels/voice/adapter.mjs +301 -0
- package/lib/channels/voice/adapter.test.mjs +278 -0
- package/lib/channels/whatsapp/adapter-baileys.mjs +587 -0
- package/lib/channels/whatsapp/adapter-baileys.test.mjs +359 -0
- package/lib/channels/whatsapp/adapter-twilio.mjs +65 -0
- package/lib/channels/whatsapp/baileys-typing.test.mjs +154 -0
- package/lib/charter.mjs +256 -0
- package/lib/charter.test.mjs +89 -0
- package/lib/claude-bin.mjs +134 -0
- package/lib/claude-bin.test.mjs +75 -0
- package/lib/collective/capture.mjs +185 -0
- package/lib/collective/capture.test.mjs +121 -0
- package/lib/collective/cards.mjs +201 -0
- package/lib/collective/cards.test.mjs +114 -0
- package/lib/collective/config.mjs +186 -0
- package/lib/collective/config.test.mjs +123 -0
- package/lib/collective/global-config.mjs +113 -0
- package/lib/collective/global-config.test.mjs +75 -0
- package/lib/collective/presence.mjs +201 -0
- package/lib/collective/presence.test.mjs +95 -0
- package/lib/collective/recall.mjs +215 -0
- package/lib/collective/recall.test.mjs +116 -0
- package/lib/comms/send-gate.mjs +554 -0
- package/lib/comms/send-gate.test.mjs +577 -0
- package/lib/comms.mjs +67 -0
- package/lib/comms.test.mjs +41 -0
- package/lib/diagnostics/alerts.mjs +424 -0
- package/lib/diagnostics/alerts.test.mjs +318 -0
- package/lib/diagnostics/backup-freshness.mjs +188 -0
- package/lib/diagnostics/backup-freshness.test.mjs +185 -0
- package/lib/diagnostics/counters.mjs +269 -0
- package/lib/diagnostics/counters.test.mjs +206 -0
- package/lib/diagnostics/events.mjs +188 -0
- package/lib/diagnostics/events.test.mjs +290 -0
- package/lib/diagnostics/otel.mjs +237 -0
- package/lib/diagnostics/otel.test.mjs +196 -0
- package/lib/diagnostics/trace.mjs +216 -0
- package/lib/diagnostics/trace.test.mjs +251 -0
- package/lib/env-compat.mjs +74 -0
- package/lib/env-compat.test.mjs +104 -0
- package/lib/feature-init.mjs +331 -0
- package/lib/fs-atomic.mjs +112 -0
- package/lib/fs-atomic.test.mjs +72 -0
- package/lib/fs-ownership.mjs +111 -0
- package/lib/fs-ownership.test.mjs +158 -0
- package/lib/hooks/bus.mjs +347 -0
- package/lib/hooks/bus.test.mjs +387 -0
- package/lib/index.js +16 -0
- package/lib/learning/config.mjs +106 -0
- package/lib/learning/config.test.mjs +75 -0
- package/lib/learning/counters.mjs +156 -0
- package/lib/learning/counters.test.mjs +69 -0
- package/lib/learning/curator-consolidate.test.mjs +238 -0
- package/lib/learning/curator.mjs +453 -0
- package/lib/learning/curator.test.mjs +106 -0
- package/lib/learning/log.mjs +40 -0
- package/lib/learning/reflect.mjs +534 -0
- package/lib/learning/reflect.test.mjs +0 -0
- package/lib/learning/session-index.mjs +352 -0
- package/lib/learning/session-index.test.mjs +125 -0
- package/lib/learning/skill-writer.mjs +474 -0
- package/lib/learning/skill-writer.test.mjs +210 -0
- package/lib/mcp/server.mjs +328 -0
- package/lib/mcp/server.test.mjs +400 -0
- package/lib/model-router/auth-profiles.mjs +758 -0
- package/lib/model-router/auth-profiles.test.mjs +580 -0
- package/lib/model-router/catalog/anthropic.yaml +153 -0
- package/lib/model-router/catalog/deepseek.yaml +86 -0
- package/lib/model-router/catalog/moonshot.yaml +81 -0
- package/lib/model-router/catalog/qwen.yaml +114 -0
- package/lib/model-router/catalog.mjs +925 -0
- package/lib/model-router/catalog.test.mjs +385 -0
- package/lib/model-router/economics.mjs +564 -0
- package/lib/model-router/economics.test.mjs +344 -0
- package/lib/model-router/failover.mjs +298 -0
- package/lib/model-router/failover.test.mjs +439 -0
- package/lib/model-router/health.mjs +453 -0
- package/lib/model-router/health.test.mjs +338 -0
- package/lib/model-router/integration-coverage.test.mjs +829 -0
- package/lib/model-router/integration.test.mjs +564 -0
- package/lib/model-router/ledger.mjs +402 -0
- package/lib/model-router/ledger.test.mjs +382 -0
- package/lib/model-router/llm-task.mjs +515 -0
- package/lib/model-router/llm-task.test.mjs +392 -0
- package/lib/model-router/org-credentials.mjs +260 -0
- package/lib/model-router/org-credentials.test.mjs +265 -0
- package/lib/model-router/pricing-refresh.mjs +463 -0
- package/lib/model-router/pricing-refresh.test.mjs +286 -0
- package/lib/model-router/reconcile.mjs +429 -0
- package/lib/model-router/reconcile.test.mjs +316 -0
- package/lib/model-router/repair.mjs +471 -0
- package/lib/model-router/repair.test.mjs +180 -0
- package/lib/model-router/resolve.mjs +1206 -0
- package/lib/model-router/spawn.mjs +497 -0
- package/lib/model-router/spawn.test.mjs +425 -0
- package/lib/model-router/taxonomy.mjs +893 -0
- package/lib/model-router/taxonomy.test.mjs +410 -0
- package/lib/model-router.mjs +677 -0
- package/lib/model-router.test.mjs +907 -0
- package/lib/org/activity.mjs +211 -0
- package/lib/org/activity.test.mjs +134 -0
- package/lib/org/approvals.mjs +448 -0
- package/lib/org/approvals.test.mjs +216 -0
- package/lib/org/awareness.mjs +222 -0
- package/lib/org/awareness.test.mjs +159 -0
- package/lib/org/board.mjs +229 -0
- package/lib/org/board.test.mjs +177 -0
- package/lib/org/bootstrap-context.mjs +169 -0
- package/lib/org/bootstrap-context.test.mjs +153 -0
- package/lib/org/client.mjs +1628 -0
- package/lib/org/client.test.mjs +1107 -0
- package/lib/org/cohort-client.mjs +67 -0
- package/lib/org/cohort-client.test.mjs +126 -0
- package/lib/org/cost-sync.mjs +227 -0
- package/lib/org/cost-sync.test.mjs +153 -0
- package/lib/org/doctor.mjs +212 -0
- package/lib/org/doctor.test.mjs +212 -0
- package/lib/org/handoff.mjs +293 -0
- package/lib/org/handoff.test.mjs +269 -0
- package/lib/org/integration-tools.mjs +182 -0
- package/lib/org/integration-tools.test.mjs +160 -0
- package/lib/org/keys.mjs +131 -0
- package/lib/org/keys.test.mjs +92 -0
- package/lib/org/knowledge.mjs +463 -0
- package/lib/org/knowledge.test.mjs +319 -0
- package/lib/org/leases.mjs +335 -0
- package/lib/org/leases.test.mjs +235 -0
- package/lib/org/mesh-integration.test.mjs +127 -0
- package/lib/org/mesh.mjs +459 -0
- package/lib/org/mesh.test.mjs +345 -0
- package/lib/org/messaging.mjs +503 -0
- package/lib/org/messaging.test.mjs +238 -0
- package/lib/org/policy.mjs +345 -0
- package/lib/org/policy.test.mjs +237 -0
- package/lib/org/protocol.checksum +1 -0
- package/lib/org/protocol.checksum.test.mjs +90 -0
- package/lib/org/protocol.mjs +967 -0
- package/lib/org/protocol.test.mjs +264 -0
- package/lib/org/registry.mjs +194 -0
- package/lib/org/registry.test.mjs +100 -0
- package/lib/org/tool-surface-integration.test.mjs +120 -0
- package/lib/org/tool-surface.mjs +2535 -0
- package/lib/org/tool-surface.test.mjs +589 -0
- package/lib/org/ui-parity.mjs +3236 -0
- package/lib/org/ui-parity.test.mjs +348 -0
- package/lib/org/verify.mjs +176 -0
- package/lib/org/verify.test.mjs +194 -0
- package/lib/rag/embed.mjs +188 -0
- package/lib/rag/indexer.mjs +425 -0
- package/lib/rag/rag.test.mjs +505 -0
- package/lib/rag/search.mjs +475 -0
- package/lib/rate-guard.mjs +246 -0
- package/lib/rate-guard.test.mjs +201 -0
- package/lib/render.mjs +112 -0
- package/lib/render.test.mjs +68 -0
- package/lib/resource-governor.mjs +297 -0
- package/lib/resource-governor.test.mjs +262 -0
- package/lib/scheduling/dynamic-jobs.mjs +675 -0
- package/lib/scheduling/dynamic-jobs.test.mjs +344 -0
- package/lib/scheduling/jitter.mjs +0 -0
- package/lib/scheduling/jitter.test.mjs +140 -0
- package/lib/secrets/broker.mjs +315 -0
- package/lib/secrets/broker.test.mjs +280 -0
- package/lib/secrets/providers.mjs +461 -0
- package/lib/secrets/providers.test.mjs +274 -0
- package/lib/security/audit-engine.mjs +684 -0
- package/lib/security/audit-engine.test.mjs +389 -0
- package/lib/security/coerce-args.mjs +552 -0
- package/lib/security/coerce-args.test.mjs +281 -0
- package/lib/security/dangerous-tools.mjs +97 -0
- package/lib/security/dangerous-tools.test.mjs +68 -0
- package/lib/security/external-content.mjs +145 -0
- package/lib/security/external-content.test.mjs +67 -0
- package/lib/security/redact.mjs +592 -0
- package/lib/security/redact.test.mjs +441 -0
- package/lib/security/secret-equal.mjs +73 -0
- package/lib/security/secret-equal.test.mjs +55 -0
- package/lib/session-permissions.mjs +101 -0
- package/lib/session-permissions.test.mjs +100 -0
- package/lib/setup/claude-probe.mjs +74 -0
- package/lib/setup/completeness.mjs +175 -0
- package/lib/setup/completeness.test.mjs +110 -0
- package/lib/setup/context-pack.mjs +173 -0
- package/lib/setup/context-pack.test.mjs +89 -0
- package/lib/setup/enrich.mjs +277 -0
- package/lib/setup/enrich.test.mjs +115 -0
- package/lib/setup/enroll-from-cohort.mjs +441 -0
- package/lib/setup/enroll-from-cohort.test.mjs +233 -0
- package/lib/setup/integration.test.mjs +162 -0
- package/lib/setup/io.mjs +360 -0
- package/lib/setup/io.test.mjs +77 -0
- package/lib/setup/run-generator.mjs +81 -0
- package/lib/setup/runner.mjs +244 -0
- package/lib/setup/runner.test.mjs +132 -0
- package/lib/setup/sections/comms.mjs +173 -0
- package/lib/setup/sections/company.mjs +120 -0
- package/lib/setup/sections/enrich.mjs +138 -0
- package/lib/setup/sections/identity.mjs +182 -0
- package/lib/setup/sections/identity.test.mjs +140 -0
- package/lib/setup/sections/learning.mjs +153 -0
- package/lib/setup/sections/learning.test.mjs +81 -0
- package/lib/setup/sections/messaging.mjs +219 -0
- package/lib/setup/sections/messaging.test.mjs +127 -0
- package/lib/setup/sections/model.mjs +102 -0
- package/lib/setup/sections/operating-model.mjs +78 -0
- package/lib/setup/sections/org.mjs +475 -0
- package/lib/setup/sections/org.test.mjs +313 -0
- package/lib/setup/sections/orgmail.mjs +173 -0
- package/lib/setup/sections/orgmail.test.mjs +118 -0
- package/lib/setup/sections/recovery.mjs +159 -0
- package/lib/setup/sections/recovery.test.mjs +98 -0
- package/lib/setup/sections/tools.mjs +132 -0
- package/lib/setup/sections/verify.mjs +97 -0
- package/lib/setup/sot.mjs +205 -0
- package/lib/setup/sot.test.mjs +81 -0
- package/lib/setup/state.mjs +151 -0
- package/lib/setup/state.test.mjs +92 -0
- package/lib/singleton.js +229 -0
- package/lib/singleton.test.mjs +135 -0
- package/lib/telemetry/alerts.mjs +216 -0
- package/lib/telemetry/alerts.test.mjs +109 -0
- package/lib/telemetry/collect.mjs +512 -0
- package/lib/telemetry/collect.test.mjs +202 -0
- package/lib/tool-definitions-integration.test.mjs +83 -0
- package/lib/tool-definitions.js +738 -0
- package/lib/tool-definitions.test.mjs +437 -0
- package/lib/util/fetch-timeout.mjs +136 -0
- package/lib/util/fetch-timeout.test.mjs +202 -0
- package/lib/util/reconnect.mjs +343 -0
- package/lib/util/reconnect.test.mjs +369 -0
- package/lib/util/unhandled.mjs +205 -0
- package/lib/util/unhandled.test.mjs +216 -0
- package/lib/voice/context-loader.mjs +466 -0
- package/lib/voice/index.mjs +100 -0
- package/lib/voice/openai-realtime.mjs +510 -0
- package/lib/voice/outbound.mjs +542 -0
- package/lib/voice/outbound.test.mjs +69 -0
- package/lib/voice/post-call-brief.mjs +428 -0
- package/lib/voice/provider.mjs +52 -0
- package/lib/voice/session-rotation.mjs +257 -0
- package/lib/voice/stt.mjs +161 -0
- package/lib/voice/stt.test.mjs +226 -0
- package/lib/voice/tool-bridge.mjs +370 -0
- package/lib/voice/tts.mjs +104 -0
- package/lib/voice/twilio-sip-bridge.mjs +288 -0
- package/lib/voice/voice.test.mjs +990 -0
- package/mcp/README.md +80 -0
- package/package.json +151 -0
- package/plugins/maestro-skills/plugin.json +139 -0
- package/plugins/maestro-skills/skills/agents-observe.md +110 -0
- package/plugins/maestro-skills/skills/board-deck.md +68 -0
- package/plugins/maestro-skills/skills/books-close.md +77 -0
- package/plugins/maestro-skills/skills/brand-steward.md +121 -0
- package/plugins/maestro-skills/skills/calendar-plan.md +57 -0
- package/plugins/maestro-skills/skills/call-working-sessions.md +124 -0
- package/plugins/maestro-skills/skills/ccxray-diagnostics.md +91 -0
- package/plugins/maestro-skills/skills/claude-pace.md +61 -0
- package/plugins/maestro-skills/skills/code-review-graph.md +99 -0
- package/plugins/maestro-skills/skills/crm-pipeline.md +65 -0
- package/plugins/maestro-skills/skills/decision-brief.md +89 -0
- package/plugins/maestro-skills/skills/directory-hygiene.md +125 -0
- package/plugins/maestro-skills/skills/draft-comms.md +84 -0
- package/plugins/maestro-skills/skills/evening-wrap.md +53 -0
- package/plugins/maestro-skills/skills/files-find.md +65 -0
- package/plugins/maestro-skills/skills/generative-ui.md +228 -0
- package/plugins/maestro-skills/skills/hiring-triage.md +74 -0
- package/plugins/maestro-skills/skills/inbox-triage.md +61 -0
- package/plugins/maestro-skills/skills/mail-triage.md +86 -0
- package/plugins/maestro-skills/skills/morning-brief.md +54 -0
- package/plugins/maestro-skills/skills/native-artifacts.md +157 -0
- package/plugins/maestro-skills/skills/org-board.md +133 -0
- package/plugins/maestro-skills/skills/org-credential.md +68 -0
- package/plugins/maestro-skills/skills/org-recall.md +81 -0
- package/plugins/maestro-skills/skills/pipeline-review.md +76 -0
- package/plugins/maestro-skills/skills/regulatory-status.md +81 -0
- package/plugins/maestro-skills/skills/router-why.md +78 -0
- package/plugins/maestro-skills/skills/schedule-meeting.md +91 -0
- package/plugins/maestro-skills/skills/session-search.md +71 -0
- package/plugins/maestro-skills/skills/set-reminder.md +93 -0
- package/plugins/maestro-skills/skills/slack-followup.md +64 -0
- package/plugins/maestro-skills/skills/team-activity.md +86 -0
- package/plugins/maestro-skills/skills/weekly-memo.md +70 -0
- package/policies/action-classification.yaml +114 -0
- package/policies/ai-disclosure.yaml +294 -0
- package/policies/communication-style.md +139 -0
- package/policies/information-barriers.yaml +118 -0
- package/policies/prompt-injection-defence.yaml +138 -0
- package/public/assets/icon-dark.png +0 -0
- package/public/assets/icon-dark.svg +9 -0
- package/public/assets/icon-light.svg +9 -0
- package/public/assets/logo-dark.svg +15 -0
- package/public/assets/logo-light.svg +15 -0
- package/scaffold/.mcp.json +7 -0
- package/scaffold/CLAUDE.md +368 -0
- package/scaffold/config/agent.json +55 -0
- package/scaffold/config/agent.ts +76 -0
- package/scaffold/config/agent.ts.example +89 -0
- package/scaffold/config/alerts.yaml +23 -0
- package/scaffold/config/allowlist.yaml.example +25 -0
- package/scaffold/config/caller-id-map.yaml +46 -0
- package/scaffold/config/collective.yaml +49 -0
- package/scaffold/config/company.json +20 -0
- package/scaffold/config/known-agents.json +6 -0
- package/scaffold/config/learning.yaml +55 -0
- package/scaffold/config/model-routing.yaml.example +104 -0
- package/scaffold/config/org.yaml +25 -0
- package/scaffold/config/orgmail.yaml.example +19 -0
- package/scaffold/config/recovery.yaml +72 -0
- package/scaffold/config/secrets.yaml +27 -0
- package/scaffold/config/slack.yaml.example +35 -0
- package/scaffold/config/telegram.yaml.example +38 -0
- package/scaffold/config/voice.yaml.example +89 -0
- package/scaffold/config/whatsapp.yaml.example +39 -0
- package/schedules/README.md +49 -0
- package/schedules/triggers/backlog-executor.md +102 -0
- package/schedules/triggers/brand-steward.md +72 -0
- package/schedules/triggers/daily-evening-wrap.md +159 -0
- package/schedules/triggers/daily-midday-sweep.md +58 -0
- package/schedules/triggers/daily-morning-brief.md +55 -0
- package/schedules/triggers/directory-hygiene.md +81 -0
- package/schedules/triggers/dynamic-jobs.md +40 -0
- package/schedules/triggers/inbox-processor.md +115 -0
- package/schedules/triggers/meeting-action-capture.md +60 -0
- package/schedules/triggers/meeting-prep.md +69 -0
- package/schedules/triggers/messaging-inbound.md +50 -0
- package/schedules/triggers/org-pulse.md +24 -0
- package/schedules/triggers/quarterly-self-assessment.md +54 -0
- package/schedules/triggers/weekly-engineering-health.md +37 -0
- package/schedules/triggers/weekly-execution.md +65 -0
- package/schedules/triggers/weekly-hiring.md +53 -0
- package/schedules/triggers/weekly-priorities.md +38 -0
- package/schedules/triggers/weekly-strategic-memo.md +124 -0
- package/scripts/archive-email.sh +55 -0
- package/scripts/cadence/cadence-status.mjs +36 -0
- package/scripts/cadence/enqueue-cadence-tick.mjs +174 -0
- package/scripts/cadence/enqueue-cadence-tick.test.mjs +187 -0
- package/scripts/cadence/launchd-cadence-wrapper.sh +85 -0
- package/scripts/cadence/launchd-cloud-relay-wrapper.sh +95 -0
- package/scripts/cadence/launchd-socket-mode-wrapper.sh +95 -0
- package/scripts/ci/check-docs-accuracy.mjs +493 -0
- package/scripts/ci/check-docs-accuracy.test.mjs +409 -0
- package/scripts/ci/check-exports-exist.mjs +140 -0
- package/scripts/ci/check-files-exist.mjs +107 -0
- package/scripts/ci/check-no-build-artifacts.mjs +111 -0
- package/scripts/ci/check-no-build-artifacts.test.mjs +71 -0
- package/scripts/ci/check-no-confidential.mjs +198 -0
- package/scripts/ci/check-no-conflict-markers.mjs +169 -0
- package/scripts/ci/check-no-residual-identity.mjs +163 -0
- package/scripts/ci/check-no-residual-identity.test.mjs +89 -0
- package/scripts/ci/check-tarball-fidelity.mjs +205 -0
- package/scripts/ci/check-unresolved-tokens.mjs +83 -0
- package/scripts/ci/check.mjs +109 -0
- package/scripts/ci/check.test.mjs +194 -0
- package/scripts/ci/run-coverage.mjs +82 -0
- package/scripts/ci/run-tests.mjs +71 -0
- package/scripts/cloud-relay/README.md +59 -0
- package/scripts/cloud-relay/index.mjs +233 -0
- package/scripts/cloud-relay/package.json +15 -0
- package/scripts/cloud-relay/railway.json +13 -0
- package/scripts/cloud-relay/voice/README.md +94 -0
- package/scripts/cloud-relay/voice/package-lock.json +39 -0
- package/scripts/cloud-relay/voice/package.json +16 -0
- package/scripts/cloud-relay/voice/railway.json +13 -0
- package/scripts/cloud-relay/voice/server.mjs +532 -0
- package/scripts/collective/hook-runner.mjs +211 -0
- package/scripts/collective/hook-runner.test.mjs +90 -0
- package/scripts/collective/org-pulse.mjs +72 -0
- package/scripts/collective/org-sync.mjs +61 -0
- package/scripts/collective/recall.mjs +45 -0
- package/scripts/collective/who.mjs +30 -0
- package/scripts/comms-monitor.sh +288 -0
- package/scripts/configure-whatsapp-sandbox.sh +201 -0
- package/scripts/continuous-monitor.sh +91 -0
- package/scripts/cost/fleet-digest.mjs +407 -0
- package/scripts/cost/fleet-digest.test.mjs +207 -0
- package/scripts/cost/track-claude-usage.mjs +169 -0
- package/scripts/daemon/agent-daemon.mjs +989 -0
- package/scripts/daemon/agent-daemon.test.mjs +525 -0
- package/scripts/daemon/cadence-consumer-governance.test.mjs +220 -0
- package/scripts/daemon/cadence-consumer.mjs +1080 -0
- package/scripts/daemon/cadence-consumer.test.mjs +770 -0
- package/scripts/daemon/cadence-handlers.mjs +1121 -0
- package/scripts/daemon/cadence-handlers.test.mjs +617 -0
- package/scripts/daemon/classifier.mjs +704 -0
- package/scripts/daemon/classifier.test.mjs +238 -0
- package/scripts/daemon/classify-kind.mjs +54 -0
- package/scripts/daemon/classify-kind.test.mjs +40 -0
- package/scripts/daemon/context-compiler.mjs +605 -0
- package/scripts/daemon/context-compiler.test.mjs +300 -0
- package/scripts/daemon/dispatcher-cooldown.test.mjs +122 -0
- package/scripts/daemon/dispatcher-governance.test.mjs +886 -0
- package/scripts/daemon/dispatcher.mjs +1516 -0
- package/scripts/daemon/health.mjs +72 -0
- package/scripts/daemon/inbox-deferral.mjs +210 -0
- package/scripts/daemon/inbox-deferral.test.mjs +242 -0
- package/scripts/daemon/integration.test.mjs +149 -0
- package/scripts/daemon/launchd-wrapper-generic.sh +96 -0
- package/scripts/daemon/launchd-wrapper-slack-events.sh +37 -0
- package/scripts/daemon/launchd-wrapper.sh +91 -0
- package/scripts/daemon/lib/session-router.mjs +274 -0
- package/scripts/daemon/lib/session-router.test.mjs +295 -0
- package/scripts/daemon/maestro-daemon.mjs +275 -0
- package/scripts/daemon/prompt-builder.mjs +685 -0
- package/scripts/daemon/prompt-builder.test.mjs +213 -0
- package/scripts/daemon/responder.mjs +854 -0
- package/scripts/daemon/session-lock.mjs +721 -0
- package/scripts/daemon/session-lock.test.mjs +252 -0
- package/scripts/daemon/session-outcomes.mjs +640 -0
- package/scripts/daemon/session-outcomes.test.mjs +533 -0
- package/scripts/daemon/typing-registry.mjs +90 -0
- package/scripts/daemon/typing-registry.test.mjs +77 -0
- package/scripts/daemon/voice-webhook-server.mjs +804 -0
- package/scripts/decisions/capture-decision.mjs +116 -0
- package/scripts/disclosure_assessment.py +873 -0
- package/scripts/disclosure_boundaries.py +562 -0
- package/scripts/email-signature-principal.html +52 -0
- package/scripts/email-signature.html +60 -0
- package/scripts/email_quote_thread.py +167 -0
- package/scripts/email_thread_dedup.py +362 -0
- package/scripts/emergency-stop.sh +81 -0
- package/scripts/healthcheck.sh +116 -0
- package/scripts/hooks/block-mcp-cohort-send.sh +15 -0
- package/scripts/hooks/block-mcp-slack-send.sh +7 -0
- package/scripts/hooks/post-action-log.sh +126 -0
- package/scripts/hooks/pre-send-audit.sh +174 -0
- package/scripts/hooks/pre-send-audit.test.mjs +215 -0
- package/scripts/hooks/session-end-log.sh +27 -0
- package/scripts/hooks/session-start-banner.sh +115 -0
- package/scripts/huddle/audio-bridge.mjs +664 -0
- package/scripts/huddle/boot-slack-cdp.sh +102 -0
- package/scripts/huddle/huddle-controller.mjs +942 -0
- package/scripts/huddle/huddle-server.mjs +1229 -0
- package/scripts/huddle/launch-slack.sh +232 -0
- package/scripts/huddle/openai-realtime-bridge.mjs +462 -0
- package/scripts/huddle/package-lock.json +62 -0
- package/scripts/huddle/package.json +22 -0
- package/scripts/huddle/setup-audio.sh +239 -0
- package/scripts/huddle/start-call.mjs +318 -0
- package/scripts/huddle/test-pipeline.mjs +263 -0
- package/scripts/learning/consolidate-skills.mjs +72 -0
- package/scripts/learning/session-search.mjs +125 -0
- package/scripts/llm_email_dedup.py +442 -0
- package/scripts/local-triggers/generate-plists.sh +432 -0
- package/scripts/local-triggers/generate-plists.test.mjs +413 -0
- package/scripts/local-triggers/install-all.sh +49 -0
- package/scripts/local-triggers/plists/.gitkeep +0 -0
- package/scripts/local-triggers/run-trigger.sh +63 -0
- package/scripts/local-triggers/templates/rag-reindex.plist.template +47 -0
- package/scripts/local-triggers/templates/voice-relay-poller.plist.template +54 -0
- package/scripts/local-triggers/templates/voice-tunnel.plist.template +55 -0
- package/scripts/local-triggers/templates/voice-webhook.plist.template +51 -0
- package/scripts/maintenance/backup-to-cloud.sh +124 -0
- package/scripts/maintenance/health-check.sh +377 -0
- package/scripts/media-generation/README.md +105 -0
- package/scripts/media-generation/gemini-image-client.mjs +173 -0
- package/scripts/media-generation/generate-assets.mjs +289 -0
- package/scripts/media-generation/veo-video-client.mjs +219 -0
- package/scripts/org/send-orgmail.mjs +227 -0
- package/scripts/outbound-dedup-cleanup.sh +43 -0
- package/scripts/outbound-dedup.sh +477 -0
- package/scripts/outbound_dedup.py +115 -0
- package/scripts/parse-voice-transcript.mjs +481 -0
- package/scripts/pdf-generation/README.md +63 -0
- package/scripts/pdf-generation/build-document.mjs +247 -0
- package/scripts/pdf-generation/templates/board-pack.latex +136 -0
- package/scripts/pdf-generation/templates/corporate-letter.latex +126 -0
- package/scripts/pdf-generation/templates/memo.latex +114 -0
- package/scripts/poll-slack-events.sh +35 -0
- package/scripts/poller/calendar-poller.mjs +12 -0
- package/scripts/poller/gmail-poller.mjs +192 -0
- package/scripts/poller/imap-client.mjs +289 -0
- package/scripts/poller/inbox-scan-poller.mjs +156 -0
- package/scripts/poller/inbox-scan-poller.test.mjs +231 -0
- package/scripts/poller/index.mjs +73 -0
- package/scripts/poller/intra-session-check.mjs +285 -0
- package/scripts/poller/lib/cloud-relay-dedup.mjs +88 -0
- package/scripts/poller/lib/cloud-relay-dedup.test.mjs +133 -0
- package/scripts/poller/lib/slash-command-handlers.mjs +177 -0
- package/scripts/poller/secondary-gmail-poller.mjs +132 -0
- package/scripts/poller/slack-cloud-relay-client.mjs +368 -0
- package/scripts/poller/slack-poller.mjs +854 -0
- package/scripts/poller/slack-socket-mode.mjs +917 -0
- package/scripts/poller/slack-socket-mode.test.mjs +753 -0
- package/scripts/poller/trigger.mjs +75 -0
- package/scripts/poller/utils.mjs +371 -0
- package/scripts/poller/voice-cloud-relay-client.mjs +179 -0
- package/scripts/poller/voice-poller.mjs +236 -0
- package/scripts/poller-launchd/install.sh +66 -0
- package/scripts/poller-launchd/poller.plist.template +40 -0
- package/scripts/poller-launchd/whatsapp-handler.plist.template +39 -0
- package/scripts/post-interaction-indexer.py +1598 -0
- package/scripts/pre-draft-context.py +994 -0
- package/scripts/pre_draft_lookup.py +258 -0
- package/scripts/rag/build-index.mjs +47 -0
- package/scripts/rag/ingest.mjs +111 -0
- package/scripts/rag/search.mjs +119 -0
- package/scripts/rag-indexer.py +629 -0
- package/scripts/restore-from-backup.sh +248 -0
- package/scripts/restore-from-backup.test.mjs +178 -0
- package/scripts/resume-operations.sh +80 -0
- package/scripts/search-secondary-inbox.py +181 -0
- package/scripts/secondary-inbox-poller.py +437 -0
- package/scripts/self-optimization/compute-metrics.py +398 -0
- package/scripts/send-email-as-principal.py +369 -0
- package/scripts/send-email-threaded.py +392 -0
- package/scripts/send-email-with-attachment.py +377 -0
- package/scripts/send-email.sh +131 -0
- package/scripts/send-sms.sh +175 -0
- package/scripts/send-whatsapp.sh +292 -0
- package/scripts/session-start.sh +106 -0
- package/scripts/setup/boot-claude-session.sh +94 -0
- package/scripts/setup/configure-macos.sh +674 -0
- package/scripts/setup/configure-twilio-sip-trunk.mjs +207 -0
- package/scripts/setup/configure-voice-tunnel.mjs +182 -0
- package/scripts/setup/generate-agent-env.mjs +92 -0
- package/scripts/setup/generate-agent-package-json.mjs +222 -0
- package/scripts/setup/generate-agent-package-json.test.mjs +143 -0
- package/scripts/setup/generate-autonomy.mjs +60 -0
- package/scripts/setup/generate-backlog.mjs +101 -0
- package/scripts/setup/generate-cadences.mjs +92 -0
- package/scripts/setup/generate-capability.mjs +162 -0
- package/scripts/setup/generate-charter.mjs +76 -0
- package/scripts/setup/generate-comms.mjs +59 -0
- package/scripts/setup/generate-company.mjs +92 -0
- package/scripts/setup/init-agent.sh +547 -0
- package/scripts/setup/init-agent.test.mjs +151 -0
- package/scripts/setup/init-archetype.mjs +74 -0
- package/scripts/setup/init-backup.mjs +54 -0
- package/scripts/setup/init-cadence-bus.mjs +60 -0
- package/scripts/setup/init-channel-bus.mjs +46 -0
- package/scripts/setup/init-cost-tracking.mjs +45 -0
- package/scripts/setup/init-decision-capture.mjs +66 -0
- package/scripts/setup/init-known-agents.mjs +57 -0
- package/scripts/setup/init-learning.mjs +70 -0
- package/scripts/setup/init-memory-executive.mjs +45 -0
- package/scripts/setup/init-model-router.mjs +124 -0
- package/scripts/setup/init-rag.mjs +174 -0
- package/scripts/setup/init-session-router.mjs +38 -0
- package/scripts/setup/init-slack-socket-mode.mjs +260 -0
- package/scripts/setup/init-telegram.mjs +165 -0
- package/scripts/setup/init-voice-realtime.mjs +204 -0
- package/scripts/setup/init-whatsapp-baileys.mjs +77 -0
- package/scripts/setup/install-dev-tools.sh +150 -0
- package/scripts/setup/lib/install-plist.mjs +95 -0
- package/scripts/setup/migrate-agent-to-sot.mjs +192 -0
- package/scripts/setup/render-environment-yaml.mjs +133 -0
- package/scripts/slack-events-ctl.sh +177 -0
- package/scripts/slack-events-server.mjs +1045 -0
- package/scripts/slack-react.mjs +89 -0
- package/scripts/slack-responded.sh +232 -0
- package/scripts/slack-send.sh +287 -0
- package/scripts/slack-typing.mjs +196 -0
- package/scripts/slack-upload-v2.py +95 -0
- package/scripts/sms-handler.mjs +450 -0
- package/scripts/spawn-session.sh +120 -0
- package/scripts/sync-protocol.mjs +217 -0
- package/scripts/system-verify.sh +184 -0
- package/scripts/test-email-thread-dedup.py +239 -0
- package/scripts/test-information-barriers.py +484 -0
- package/scripts/test-llm-email-dedup.py +251 -0
- package/scripts/test-pre-draft-integration.py +203 -0
- package/scripts/test-rag-phase2.sh +442 -0
- package/scripts/test-rag-search.sh +251 -0
- package/scripts/test-voice-parser.mjs +316 -0
- package/scripts/user-context-search.py +659 -0
- package/scripts/validate_outbound.py +1504 -0
- package/scripts/watchdog/ai.maestro.memory-watchdog.plist +41 -0
- package/scripts/watchdog/force-reboot.sh +157 -0
- package/scripts/watchdog/memory-watchdog.sh +473 -0
- package/scripts/whatsapp-handler.mjs +538 -0
- package/teams/desktop-operations.yaml +34 -0
- package/teams/executive-office.yaml +27 -0
- package/teams/legal-and-regulatory.yaml +24 -0
- package/teams/platform-and-engineering.yaml +23 -0
- package/teams/strategy-and-growth.yaml +29 -0
- package/workflows/continuous/backlog-executor.yaml +141 -0
- package/workflows/continuous/inbound-monitor.yaml +168 -0
- package/workflows/daily/applicant-triage.yaml +197 -0
- package/workflows/daily/comms-triage.yaml +80 -0
- package/workflows/daily/evening-wrap.yaml +105 -0
- package/workflows/daily/morning-brief.yaml +164 -0
- package/workflows/daily/slack-followup-sweep.yaml +87 -0
- package/workflows/event-driven/README.md +50 -0
- package/workflows/event-driven/agent-failure-investigation.yaml +137 -0
- package/workflows/event-driven/pr-review.yaml +107 -0
- package/workflows/monthly/board-readiness.yaml +76 -0
- package/workflows/quarterly/strategic-scenario-analysis.yaml +85 -0
- package/workflows/session-protocol.md +171 -0
- package/workflows/weekly/engineering-health.yaml +154 -0
- package/workflows/weekly/hiring-review.yaml +169 -0
- package/workflows/weekly/rollup-pipeline-review.yaml +76 -0
- package/workflows/weekly/strategic-memo.yaml +79 -0
|
@@ -0,0 +1,917 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// =============================================================================
|
|
3
|
+
// Slack Socket Mode Listener — realtime DM + @mention ingestion
|
|
4
|
+
// =============================================================================
|
|
5
|
+
//
|
|
6
|
+
// Replaces the 60s `slack-poller.mjs` and the 5s Railway webhook relay polling
|
|
7
|
+
// for DM / @mention ingestion. Holds a persistent WebSocket to Slack's
|
|
8
|
+
// Socket Mode endpoint and translates inbound message events to the same
|
|
9
|
+
// inbox-item shape that `slack-poller.mjs` produces, then hands them to
|
|
10
|
+
// `writeInboxItem` from `utils.mjs` (preserving the existing contract).
|
|
11
|
+
//
|
|
12
|
+
// Why Socket Mode over polling
|
|
13
|
+
// ----------------------------
|
|
14
|
+
// 1. Realtime — events arrive in <500ms instead of the 60s poll cycle.
|
|
15
|
+
// 2. No rate-limit pressure — one persistent socket vs. ~25 API calls/cycle.
|
|
16
|
+
// 3. No public webhook endpoint required — Slack pushes via outbound WSS.
|
|
17
|
+
// 4. Aligns with Slack's recommended pattern for self-hosted agents.
|
|
18
|
+
//
|
|
19
|
+
// Requirements
|
|
20
|
+
// ------------
|
|
21
|
+
// - SLACK_APP_LEVEL_TOKEN (xapp-…) with `connections:write` scope
|
|
22
|
+
// - SLACK_BOT_TOKEN (xoxb-…) for the optional API enrichment fallbacks
|
|
23
|
+
// - Socket Mode enabled in the Slack app config
|
|
24
|
+
// - Event subscriptions enabled with: message.channels, message.groups,
|
|
25
|
+
// message.im, message.mpim, app_mention
|
|
26
|
+
//
|
|
27
|
+
// Run: node scripts/poller/slack-socket-mode.mjs
|
|
28
|
+
// Install: launchd plist with KeepAlive (SuccessfulExit:false, Crashed:true).
|
|
29
|
+
//
|
|
30
|
+
// Module exports a programmable surface (createSocketModeClient,
|
|
31
|
+
// eventToInboxItem, etc.) so the test file can exercise translation +
|
|
32
|
+
// reconnect logic without touching the network. The CLI invocation
|
|
33
|
+
// (process.argv[1]) starts the daemon directly.
|
|
34
|
+
// =============================================================================
|
|
35
|
+
|
|
36
|
+
import { existsSync, readFileSync, mkdirSync, appendFileSync } from "node:fs";
|
|
37
|
+
import { dirname, join, resolve } from "node:path";
|
|
38
|
+
import { fileURLToPath } from "node:url";
|
|
39
|
+
|
|
40
|
+
import { writeInboxItem, resolvePrivilege, resolveName, AGENT_REPO_DIR } from "./utils.mjs";
|
|
41
|
+
import { handleSlashCommand, postSlashResponse } from "./lib/slash-command-handlers.mjs";
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Process a slash_commands envelope: invoke the handler, then POST the
|
|
45
|
+
* formatted response back to Slack via the payload's response_url. Errors are
|
|
46
|
+
* caught and logged — Slack times out at 3s, so we keep the work short.
|
|
47
|
+
*/
|
|
48
|
+
async function handleSlashCommandEnvelope(envelope, agentRoot, identity) {
|
|
49
|
+
const payload = envelope.payload;
|
|
50
|
+
const result = await handleSlashCommand(payload, { agentRoot, identity });
|
|
51
|
+
if (!result) {
|
|
52
|
+
// Unknown command — let Slack show its default "command not configured"
|
|
53
|
+
// error to the user. Don't POST to response_url at all.
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
await postSlashResponse(payload.response_url, result);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
// Constants
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
64
|
+
const __dirname = dirname(__filename);
|
|
65
|
+
|
|
66
|
+
// Slack endpoint for opening a Socket Mode WSS connection.
|
|
67
|
+
const APPS_CONNECTIONS_OPEN_URL = "https://slack.com/api/apps.connections.open";
|
|
68
|
+
|
|
69
|
+
// Backoff defaults — exponential with cap. The first retry waits 1s, then
|
|
70
|
+
// doubles each failure up to 60s. Reset on a clean `hello`.
|
|
71
|
+
export const RECONNECT_INITIAL_MS = 1_000;
|
|
72
|
+
export const RECONNECT_MAX_MS = 60_000;
|
|
73
|
+
|
|
74
|
+
// How often we check .emergency-stop and refresh keep-alive state. Slack
|
|
75
|
+
// sends `disconnect` (type: "disconnect") ~30 min into a session asking
|
|
76
|
+
// us to reconnect; we just close + reconnect on the next tick.
|
|
77
|
+
export const SUPERVISOR_INTERVAL_MS = 5_000;
|
|
78
|
+
|
|
79
|
+
// Slack's recommended ping interval. The WS server sends `pings`; we
|
|
80
|
+
// respond with `pong`. Most node ws implementations handle this for us,
|
|
81
|
+
// but we also enqueue a heartbeat to surface stalls.
|
|
82
|
+
export const HEARTBEAT_INTERVAL_MS = 30_000;
|
|
83
|
+
export const HEARTBEAT_TIMEOUT_MS = 90_000;
|
|
84
|
+
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
// Env loader (mirrors slack-events-server.mjs — no dotenv dep)
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Read .env from the agent root and merge into process.env (existing keys
|
|
91
|
+
* win — we don't clobber values the operator deliberately set in their
|
|
92
|
+
* shell). No-op when .env is missing.
|
|
93
|
+
*/
|
|
94
|
+
export function loadEnvFromFile(agentRoot) {
|
|
95
|
+
const envPath = join(agentRoot, ".env");
|
|
96
|
+
if (!existsSync(envPath)) return;
|
|
97
|
+
const body = readFileSync(envPath, "utf-8");
|
|
98
|
+
for (const line of body.split("\n")) {
|
|
99
|
+
const m = line.match(/^([A-Z_][A-Z0-9_]*)=(.*)$/);
|
|
100
|
+
if (!m) continue;
|
|
101
|
+
let val = m[2].trim();
|
|
102
|
+
if ((val.startsWith('"') && val.endsWith('"')) ||
|
|
103
|
+
(val.startsWith("'") && val.endsWith("'"))) {
|
|
104
|
+
val = val.slice(1, -1);
|
|
105
|
+
}
|
|
106
|
+
if (process.env[m[1]] === undefined || process.env[m[1]] === "") {
|
|
107
|
+
process.env[m[1]] = val;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
// Logging — JSONL to logs/polling/<date>-slack-socket.jsonl
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Append a structured log line. Best-effort; logging failures must never
|
|
118
|
+
* take the listener down. The log path mirrors the existing poller layout
|
|
119
|
+
* so downstream tooling (doctor, log rotation) keeps working unchanged.
|
|
120
|
+
*/
|
|
121
|
+
export function logLine(agentRoot, entry) {
|
|
122
|
+
try {
|
|
123
|
+
const dir = join(agentRoot, "logs", "polling");
|
|
124
|
+
mkdirSync(dir, { recursive: true });
|
|
125
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
126
|
+
const file = join(dir, `${today}-slack-socket.jsonl`);
|
|
127
|
+
const line = JSON.stringify({ ts: new Date().toISOString(), ...entry }) + "\n";
|
|
128
|
+
appendFileSync(file, line);
|
|
129
|
+
} catch {
|
|
130
|
+
/* logging must never propagate */
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function info(agentRoot, message, data = {}) {
|
|
135
|
+
console.log(`[slack-socket] ${message}`);
|
|
136
|
+
logLine(agentRoot, { level: "info", message, ...data });
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function warn(agentRoot, message, data = {}) {
|
|
140
|
+
console.warn(`[slack-socket] ${message}`);
|
|
141
|
+
logLine(agentRoot, { level: "warn", message, ...data });
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function error(agentRoot, message, data = {}) {
|
|
145
|
+
console.error(`[slack-socket] ${message}`);
|
|
146
|
+
logLine(agentRoot, { level: "error", message, ...data });
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
// Agent identity + known-agents registry
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Load `config/agent.json` and return a canonical identity object with safe
|
|
155
|
+
* defaults so callers never need to null-check.
|
|
156
|
+
*/
|
|
157
|
+
export function loadAgentIdentity(agentRoot) {
|
|
158
|
+
try {
|
|
159
|
+
const agent = JSON.parse(readFileSync(join(agentRoot, "config/agent.json"), "utf-8"));
|
|
160
|
+
return {
|
|
161
|
+
firstName: agent.firstName || "Agent",
|
|
162
|
+
fullName: agent.fullName || "Agent",
|
|
163
|
+
slackMemberId: agent.slackMemberId || "",
|
|
164
|
+
principalSlackId: agent.principal?.slackMemberId || "",
|
|
165
|
+
};
|
|
166
|
+
} catch {
|
|
167
|
+
return { firstName: "Agent", fullName: "Agent", slackMemberId: "", principalSlackId: "" };
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Load peer agent Slack IDs from `config/known-agents.json`. Excludes the
|
|
173
|
+
* running agent itself. Used by the filter that drops messages from other
|
|
174
|
+
* agents — so e.g. Robin's Slack traffic doesn't trigger Ravi's daemon.
|
|
175
|
+
*
|
|
176
|
+
* Reads only from the agent's own config — each agent maintains its own
|
|
177
|
+
* peer registry (shipped by `npx @cohortapp/agent-sdk upgrade`).
|
|
178
|
+
*/
|
|
179
|
+
export function loadPeerSlackIds(agentRoot, ownSlackId) {
|
|
180
|
+
const candidates = [
|
|
181
|
+
join(agentRoot, "config/known-agents.json"),
|
|
182
|
+
];
|
|
183
|
+
for (const path of candidates) {
|
|
184
|
+
try {
|
|
185
|
+
const raw = readFileSync(path, "utf-8");
|
|
186
|
+
const reg = JSON.parse(raw);
|
|
187
|
+
const ids = new Set();
|
|
188
|
+
for (const a of reg.agents || []) {
|
|
189
|
+
if (a?.slackId && a.slackId !== ownSlackId) ids.add(a.slackId);
|
|
190
|
+
}
|
|
191
|
+
if (ids.size > 0) return ids;
|
|
192
|
+
} catch {
|
|
193
|
+
/* try next */
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return new Set();
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ---------------------------------------------------------------------------
|
|
200
|
+
// Slack envelope filtering — DM, @mention, thread reply
|
|
201
|
+
// ---------------------------------------------------------------------------
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Decide whether a Slack event envelope is interesting to this agent.
|
|
205
|
+
*
|
|
206
|
+
* Returns { keep: boolean, reason: string }. The reason exists so the
|
|
207
|
+
* test suite (and ops triage) can assert exactly *why* an event was
|
|
208
|
+
* dropped or kept. Keep this function pure — no I/O, no globals beyond
|
|
209
|
+
* the identity/peers arguments.
|
|
210
|
+
*/
|
|
211
|
+
export function shouldKeepEvent(envelope, { ownSlackId, peerSlackIds, reactions, ccChannels } = {}) {
|
|
212
|
+
if (!envelope || typeof envelope !== "object") {
|
|
213
|
+
return { keep: false, reason: "envelope-missing" };
|
|
214
|
+
}
|
|
215
|
+
if (envelope.type !== "events_api") {
|
|
216
|
+
return { keep: false, reason: `envelope-type-${envelope.type}` };
|
|
217
|
+
}
|
|
218
|
+
const event = envelope.payload?.event;
|
|
219
|
+
if (!event) return { keep: false, reason: "no-event" };
|
|
220
|
+
|
|
221
|
+
// WS2 gap (a): reaction events. Opt-in via `reactions` so the historical
|
|
222
|
+
// default (drop reactions) is preserved for callers that don't enable it.
|
|
223
|
+
// Drop the agent's own reactions + peer-agent reactions (same loop-guard).
|
|
224
|
+
if (event.type === "reaction_added" || event.type === "reaction_removed") {
|
|
225
|
+
if (!reactions) return { keep: false, reason: `event-type-${event.type}` };
|
|
226
|
+
const reactor = event.user || "";
|
|
227
|
+
if (ownSlackId && reactor === ownSlackId) return { keep: false, reason: "self-reaction" };
|
|
228
|
+
if (peerSlackIds && peerSlackIds.has(reactor)) return { keep: false, reason: "peer-agent" };
|
|
229
|
+
return { keep: true, reason: "reaction" };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Only message events. app_mention events come as `type: app_mention`
|
|
233
|
+
// but Slack also delivers a parallel `type: message` for the same text,
|
|
234
|
+
// so we normalise on the message handler and detect mentions inline.
|
|
235
|
+
// Allow `thread_broadcast` (a thread reply also broadcast to channel)
|
|
236
|
+
// through as a normal message; drop everything else.
|
|
237
|
+
if (event.type !== "message") return { keep: false, reason: `event-type-${event.type}` };
|
|
238
|
+
if (event.subtype && event.subtype !== "thread_broadcast" && event.subtype !== "file_share") {
|
|
239
|
+
return { keep: false, reason: `event-subtype-${event.subtype}` };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Drop bot messages and the agent's own messages.
|
|
243
|
+
if (event.bot_id) return { keep: false, reason: "bot-message" };
|
|
244
|
+
if (event.user && ownSlackId && event.user === ownSlackId) {
|
|
245
|
+
return { keep: false, reason: "self-message" };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Drop messages from peer agents — they're not customers and replying
|
|
249
|
+
// would loop two daemons forever.
|
|
250
|
+
if (event.user && peerSlackIds && peerSlackIds.has(event.user)) {
|
|
251
|
+
return { keep: false, reason: "peer-agent" };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// DM channels start with "D"; keep all of them.
|
|
255
|
+
const isDm = typeof event.channel === "string" && event.channel.startsWith("D");
|
|
256
|
+
if (isDm) return { keep: true, reason: "dm" };
|
|
257
|
+
|
|
258
|
+
// Channel messages — only keep if the agent is @-mentioned.
|
|
259
|
+
const text = typeof event.text === "string" ? event.text : "";
|
|
260
|
+
const mentionPattern = ownSlackId ? new RegExp(`<@${ownSlackId}(?:\\|[^>]*)?>`) : null;
|
|
261
|
+
if (mentionPattern && mentionPattern.test(text)) {
|
|
262
|
+
return { keep: true, reason: "mention" };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// WS2 gap (c): opt-in CC channels. A non-@mention message in a watched
|
|
266
|
+
// channel is kept as a "channel_cc" so the agent can observe (not reply).
|
|
267
|
+
// Routed to observe-not-reply by the downstream directed-gate.
|
|
268
|
+
if (ccChannels && typeof event.channel === "string" && ccChannels.has(event.channel)) {
|
|
269
|
+
return { keep: true, reason: "channel_cc" };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return { keep: false, reason: "no-mention-no-dm" };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// ---------------------------------------------------------------------------
|
|
276
|
+
// Translation — Slack envelope → poller-compatible inbox item
|
|
277
|
+
// ---------------------------------------------------------------------------
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Convert a Slack Socket Mode envelope to the same inbox-item shape that
|
|
281
|
+
* `slack-poller.mjs` produces. Pure function; all I/O happens at the caller.
|
|
282
|
+
*
|
|
283
|
+
* The output schema mirrors `slack-poller.mjs` exactly so `writeInboxItem`
|
|
284
|
+
* + the downstream daemon pipeline keep working without changes:
|
|
285
|
+
* - `id` = ts with the dot replaced by a dash (matches poller convention)
|
|
286
|
+
* - `service` = "slack"
|
|
287
|
+
* - `channel` = friendly channel label ("dm/<sender-name>" for DMs,
|
|
288
|
+
* channel id for channels — channel name resolution requires an API
|
|
289
|
+
* call we'd rather avoid on the hot path; the daemon downstream can
|
|
290
|
+
* fill in if needed)
|
|
291
|
+
* - `priority_signals.mentions_agent` = true for DMs (1:1 context), true
|
|
292
|
+
* when the regex matched, false otherwise.
|
|
293
|
+
*/
|
|
294
|
+
export function eventToInboxItem(envelope, { ownSlackId, principalSlackId, agentFirstName, threadContext, ccChannels } = {}) {
|
|
295
|
+
const event = envelope.payload.event;
|
|
296
|
+
|
|
297
|
+
// WS2 gap (a): reaction events have a different shape (item.ts is the
|
|
298
|
+
// *target* message). Produce a reaction-kinded item. `threadContext` (when
|
|
299
|
+
// the adapter resolved the target message text via conversations.history)
|
|
300
|
+
// becomes the content so the classifier has something to reason over.
|
|
301
|
+
if (event.type === "reaction_added" || event.type === "reaction_removed") {
|
|
302
|
+
const reactor = event.user || "";
|
|
303
|
+
const targetTs = event.item?.ts || "";
|
|
304
|
+
const channelId = event.item?.channel || "";
|
|
305
|
+
const isDm = typeof channelId === "string" && channelId.startsWith("D");
|
|
306
|
+
const senderName = resolveName(reactor);
|
|
307
|
+
const channelLabel = isDm ? `dm/${senderName}` : channelId;
|
|
308
|
+
const verb = event.type === "reaction_added" ? "added" : "removed";
|
|
309
|
+
const targetText = typeof threadContext === "string" ? threadContext : "";
|
|
310
|
+
return {
|
|
311
|
+
id: `${targetTs.replace(".", "-")}-reaction-${reactor}`,
|
|
312
|
+
service: "slack",
|
|
313
|
+
channel: channelLabel,
|
|
314
|
+
channel_id: channelId,
|
|
315
|
+
sender: senderName,
|
|
316
|
+
sender_privilege: resolvePrivilege(reactor),
|
|
317
|
+
timestamp: new Date((event.event_ts ? parseFloat(event.event_ts) : Date.now() / 1000) * 1000).toISOString(),
|
|
318
|
+
subject: `:${event.reaction}: ${verb} on a message`,
|
|
319
|
+
content: targetText ? `[${senderName} ${verb} :${event.reaction}:]\n${targetText}` : `${senderName} ${verb} :${event.reaction}:`,
|
|
320
|
+
thread_id: "",
|
|
321
|
+
thread_context: targetText || null,
|
|
322
|
+
is_reply: false,
|
|
323
|
+
priority_signals: {
|
|
324
|
+
from_ceo: !!principalSlackId && reactor === principalSlackId,
|
|
325
|
+
tagged_urgent: false,
|
|
326
|
+
contains_deadline: false,
|
|
327
|
+
mentions_agent: isDm,
|
|
328
|
+
},
|
|
329
|
+
raw_ref: `slack:${channelId}:${targetTs}:reaction`,
|
|
330
|
+
source: "socket-mode",
|
|
331
|
+
kind: "reaction",
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const userId = event.user || "";
|
|
336
|
+
const text = event.text || "";
|
|
337
|
+
const ts = event.ts || "";
|
|
338
|
+
const channelId = event.channel || "";
|
|
339
|
+
const threadTs = event.thread_ts || "";
|
|
340
|
+
const isReply = !!threadTs && threadTs !== ts;
|
|
341
|
+
const isDm = typeof channelId === "string" && channelId.startsWith("D");
|
|
342
|
+
|
|
343
|
+
// Friendly labels. We can't resolve a channel name without an API call
|
|
344
|
+
// we'd rather not make per event; the agent's downstream tooling already
|
|
345
|
+
// handles channel-id → name resolution where needed.
|
|
346
|
+
const senderName = resolveName(userId);
|
|
347
|
+
const channelLabel = isDm ? `dm/${senderName}` : channelId;
|
|
348
|
+
const subject = isDm ? `DM from ${senderName}` : `#${channelLabel}`;
|
|
349
|
+
|
|
350
|
+
// Priority signal detection mirrors slack-poller.mjs.
|
|
351
|
+
const isCeo = !!principalSlackId && userId === principalSlackId;
|
|
352
|
+
const isUrgent = /\b(urgent|emergency|asap|blocker|critical)\b/i.test(text);
|
|
353
|
+
const mentionPattern = ownSlackId ? new RegExp(`<@${ownSlackId}(?:\\|[^>]*)?>`) : null;
|
|
354
|
+
const explicitlyMentioned = mentionPattern ? mentionPattern.test(text) : false;
|
|
355
|
+
const nameMention = !!agentFirstName && text.toLowerCase().includes(agentFirstName.toLowerCase());
|
|
356
|
+
const mentionsAgent = isDm || explicitlyMentioned || nameMention;
|
|
357
|
+
|
|
358
|
+
// WS2 gap (c): non-@mention message in a watched CC channel → channel_cc.
|
|
359
|
+
const isCc = !isDm && !explicitlyMentioned && !nameMention &&
|
|
360
|
+
ccChannels && typeof channelId === "string" && ccChannels.has(channelId);
|
|
361
|
+
|
|
362
|
+
const item = {
|
|
363
|
+
id: ts.replace(".", "-"),
|
|
364
|
+
service: "slack",
|
|
365
|
+
channel: channelLabel,
|
|
366
|
+
channel_id: channelId,
|
|
367
|
+
sender: senderName,
|
|
368
|
+
sender_privilege: resolvePrivilege(userId),
|
|
369
|
+
timestamp: new Date(parseFloat(ts) * 1000).toISOString(),
|
|
370
|
+
subject,
|
|
371
|
+
content: text,
|
|
372
|
+
thread_id: threadTs || "",
|
|
373
|
+
// WS2 gap (b): thread replies get their prior-thread history filled by the
|
|
374
|
+
// adapter (conversations.replies). Default null preserves the legacy
|
|
375
|
+
// Socket Mode behaviour (no thread history) byte-for-byte.
|
|
376
|
+
thread_context: typeof threadContext === "string" ? threadContext : null,
|
|
377
|
+
is_reply: isReply,
|
|
378
|
+
priority_signals: {
|
|
379
|
+
from_ceo: isCeo,
|
|
380
|
+
tagged_urgent: isUrgent,
|
|
381
|
+
contains_deadline: false,
|
|
382
|
+
mentions_agent: mentionsAgent,
|
|
383
|
+
},
|
|
384
|
+
raw_ref: `slack:${channelId}:${ts}`,
|
|
385
|
+
source: "socket-mode",
|
|
386
|
+
};
|
|
387
|
+
if (isCc) item.kind = "channel_cc";
|
|
388
|
+
return item;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// ---------------------------------------------------------------------------
|
|
392
|
+
// Slack `apps.connections.open` — get a fresh WSS URL
|
|
393
|
+
// ---------------------------------------------------------------------------
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Call apps.connections.open with the app-level token to obtain a WSS URL.
|
|
397
|
+
* Returns `{ url }` on success or `{ error }` on failure. The URL is
|
|
398
|
+
* single-use and expires quickly — call this every time we open a socket.
|
|
399
|
+
*/
|
|
400
|
+
export async function openSocketConnection({ appToken, fetchFn = fetch }) {
|
|
401
|
+
if (!appToken) {
|
|
402
|
+
return { error: "missing-app-token" };
|
|
403
|
+
}
|
|
404
|
+
let response;
|
|
405
|
+
try {
|
|
406
|
+
response = await fetchFn(APPS_CONNECTIONS_OPEN_URL, {
|
|
407
|
+
method: "POST",
|
|
408
|
+
headers: {
|
|
409
|
+
Authorization: `Bearer ${appToken}`,
|
|
410
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
411
|
+
},
|
|
412
|
+
});
|
|
413
|
+
} catch (err) {
|
|
414
|
+
return { error: `network: ${err.message}` };
|
|
415
|
+
}
|
|
416
|
+
let body;
|
|
417
|
+
try {
|
|
418
|
+
body = await response.json();
|
|
419
|
+
} catch (err) {
|
|
420
|
+
return { error: `parse: ${err.message}` };
|
|
421
|
+
}
|
|
422
|
+
if (!body.ok) {
|
|
423
|
+
return { error: body.error || "unknown-error" };
|
|
424
|
+
}
|
|
425
|
+
if (!body.url) {
|
|
426
|
+
return { error: "no-url-in-response" };
|
|
427
|
+
}
|
|
428
|
+
return { url: body.url };
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// ---------------------------------------------------------------------------
|
|
432
|
+
// Reconnect supervisor — exponential back-off with jitter
|
|
433
|
+
// ---------------------------------------------------------------------------
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Compute the next reconnect delay using exponential back-off plus a small
|
|
437
|
+
* (0-25%) random jitter to avoid thundering-herd if multiple agents
|
|
438
|
+
* reconnect simultaneously.
|
|
439
|
+
*/
|
|
440
|
+
export function nextBackoffMs(currentMs, max = RECONNECT_MAX_MS) {
|
|
441
|
+
const doubled = Math.min(max, Math.max(RECONNECT_INITIAL_MS, currentMs * 2));
|
|
442
|
+
const jitter = Math.floor(Math.random() * 0.25 * doubled);
|
|
443
|
+
return Math.min(max, doubled + jitter);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// ---------------------------------------------------------------------------
|
|
447
|
+
// Socket Mode client — the main orchestration loop
|
|
448
|
+
// ---------------------------------------------------------------------------
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Create a Socket Mode client. The client is *programmable*: every external
|
|
452
|
+
* dependency (fetch, WebSocket constructor, clock, write-inbox-item) is
|
|
453
|
+
* injectable so the test suite can run end-to-end without ever touching
|
|
454
|
+
* the network or the file system.
|
|
455
|
+
*
|
|
456
|
+
* @param {object} opts
|
|
457
|
+
* @param {string} opts.agentRoot
|
|
458
|
+
* @param {string} opts.appToken xapp-… app-level token
|
|
459
|
+
* @param {object} opts.identity loadAgentIdentity() result
|
|
460
|
+
* @param {Set<string>} opts.peerSlackIds
|
|
461
|
+
* @param {Function} [opts.fetchFn] defaults to global fetch
|
|
462
|
+
* @param {Function} [opts.WebSocketCtor] defaults to global WebSocket
|
|
463
|
+
* @param {Function} [opts.writeInbox] defaults to writeInboxItem
|
|
464
|
+
* @param {Function} [opts.now] defaults to Date.now
|
|
465
|
+
* @param {Function} [opts.setTimeout] defaults to globalThis.setTimeout
|
|
466
|
+
* @param {Function} [opts.clearTimeout] defaults to globalThis.clearTimeout
|
|
467
|
+
* @param {boolean} [opts.reactions] WS2: keep reaction_added/removed events
|
|
468
|
+
* @param {Set<string>} [opts.ccChannels] WS2: channel ids to observe as channel_cc
|
|
469
|
+
* @param {(envelope:object)=>Promise<{threadContext?:string}>|undefined} [opts.enrich]
|
|
470
|
+
* WS2: async hook called for kept events before translation; returns
|
|
471
|
+
* e.g. `{ threadContext }` resolved via conversations.replies/history.
|
|
472
|
+
*/
|
|
473
|
+
export function createSocketModeClient(opts) {
|
|
474
|
+
const {
|
|
475
|
+
agentRoot,
|
|
476
|
+
appToken,
|
|
477
|
+
identity,
|
|
478
|
+
peerSlackIds,
|
|
479
|
+
fetchFn = fetch,
|
|
480
|
+
WebSocketCtor = globalThis.WebSocket,
|
|
481
|
+
writeInbox = writeInboxItem,
|
|
482
|
+
now = Date.now,
|
|
483
|
+
setTimeout: setTimeoutFn = globalThis.setTimeout,
|
|
484
|
+
clearTimeout: clearTimeoutFn = globalThis.clearTimeout,
|
|
485
|
+
reactions = false,
|
|
486
|
+
ccChannels = null,
|
|
487
|
+
enrich = null,
|
|
488
|
+
} = opts;
|
|
489
|
+
|
|
490
|
+
if (!WebSocketCtor) {
|
|
491
|
+
throw new Error(
|
|
492
|
+
"Socket Mode requires a WebSocket implementation. Use Node 22+ (global WebSocket) or pass WebSocketCtor.",
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const state = {
|
|
497
|
+
socket: null,
|
|
498
|
+
backoffMs: RECONNECT_INITIAL_MS,
|
|
499
|
+
stopped: false,
|
|
500
|
+
reconnectTimer: null,
|
|
501
|
+
heartbeatTimer: null,
|
|
502
|
+
lastPingAt: 0,
|
|
503
|
+
stats: {
|
|
504
|
+
connects: 0,
|
|
505
|
+
reconnects: 0,
|
|
506
|
+
eventsReceived: 0,
|
|
507
|
+
eventsKept: 0,
|
|
508
|
+
eventsDropped: 0,
|
|
509
|
+
acksSent: 0,
|
|
510
|
+
lastDisconnectReason: null,
|
|
511
|
+
},
|
|
512
|
+
};
|
|
513
|
+
|
|
514
|
+
// -------------------------------------------------------------------------
|
|
515
|
+
// ack — Slack requires every envelope to be acked back over the same socket.
|
|
516
|
+
// -------------------------------------------------------------------------
|
|
517
|
+
function ack(envelopeId, ws) {
|
|
518
|
+
if (!envelopeId || !ws || ws.readyState !== 1 /* WebSocket.OPEN */) return;
|
|
519
|
+
try {
|
|
520
|
+
ws.send(JSON.stringify({ envelope_id: envelopeId }));
|
|
521
|
+
state.stats.acksSent++;
|
|
522
|
+
} catch (err) {
|
|
523
|
+
warn(agentRoot, `ack send failed: ${err.message}`, { envelopeId });
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// -------------------------------------------------------------------------
|
|
528
|
+
// handleEnvelope — translate + persist via writeInbox.
|
|
529
|
+
// -------------------------------------------------------------------------
|
|
530
|
+
function handleEnvelope(envelope, ws) {
|
|
531
|
+
state.stats.eventsReceived++;
|
|
532
|
+
|
|
533
|
+
// Slack sends multiple envelope types over the same socket: hello,
|
|
534
|
+
// disconnect, events_api, slash_commands, interactive, etc. We only
|
|
535
|
+
// care about the events_api stream for inbox ingestion.
|
|
536
|
+
if (envelope.type === "hello") {
|
|
537
|
+
info(agentRoot, "hello — connected to Slack Socket Mode", {
|
|
538
|
+
num_connections: envelope.num_connections,
|
|
539
|
+
debug_info: envelope.debug_info,
|
|
540
|
+
});
|
|
541
|
+
state.backoffMs = RECONNECT_INITIAL_MS; // reset on a clean hello
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
if (envelope.type === "disconnect") {
|
|
545
|
+
info(agentRoot, "disconnect requested by Slack — will reconnect", {
|
|
546
|
+
reason: envelope.reason,
|
|
547
|
+
});
|
|
548
|
+
state.stats.lastDisconnectReason = envelope.reason || "slack-requested";
|
|
549
|
+
// Slack always wants an ack on this, but most importantly we should
|
|
550
|
+
// close + reconnect to pick up the new server-side endpoint.
|
|
551
|
+
try { ws.close(); } catch { /* */ }
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// Every other envelope type needs an ack — do this BEFORE any
|
|
556
|
+
// potentially-slow processing so Slack doesn't re-deliver.
|
|
557
|
+
if (envelope.envelope_id) ack(envelope.envelope_id, ws);
|
|
558
|
+
|
|
559
|
+
// Slash commands take a different path: they don't go to the inbox,
|
|
560
|
+
// they get dispatched directly to a handler that responds via the
|
|
561
|
+
// payload's response_url. This is how /voice (Slack-initiated outbound
|
|
562
|
+
// call) works — our practical equivalent of huddle integration since
|
|
563
|
+
// Slack doesn't expose a programmatic huddle-join API.
|
|
564
|
+
if (envelope.type === "slash_commands" && envelope.payload?.command) {
|
|
565
|
+
handleSlashCommandEnvelope(envelope, agentRoot, identity).catch((err) => {
|
|
566
|
+
warn(agentRoot, `slash command handler failed: ${err.message}`, {
|
|
567
|
+
command: envelope.payload?.command,
|
|
568
|
+
envelopeId: envelope.envelope_id,
|
|
569
|
+
});
|
|
570
|
+
});
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
const decision = shouldKeepEvent(envelope, {
|
|
575
|
+
ownSlackId: identity.slackMemberId,
|
|
576
|
+
peerSlackIds,
|
|
577
|
+
reactions,
|
|
578
|
+
ccChannels,
|
|
579
|
+
});
|
|
580
|
+
if (!decision.keep) {
|
|
581
|
+
state.stats.eventsDropped++;
|
|
582
|
+
logLine(agentRoot, { level: "debug", message: "event-dropped", reason: decision.reason });
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// WS2: when an enrich hook is configured (the channel adapter wires one to
|
|
587
|
+
// resolve thread_context / reaction target text via the Slack Web API), run
|
|
588
|
+
// the async path. Without a hook we take the historical *synchronous* path
|
|
589
|
+
// so byte-for-byte behaviour and the existing test timing are preserved.
|
|
590
|
+
if (typeof enrich === "function") {
|
|
591
|
+
Promise.resolve()
|
|
592
|
+
.then(() => enrich(envelope))
|
|
593
|
+
.catch((err) => {
|
|
594
|
+
warn(agentRoot, `enrich failed (fail-open): ${err.message}`);
|
|
595
|
+
return {};
|
|
596
|
+
})
|
|
597
|
+
.then((extra) => translateAndWrite(envelope, extra || {}));
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
translateAndWrite(envelope, {});
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function translateAndWrite(envelope, extra) {
|
|
604
|
+
let item;
|
|
605
|
+
try {
|
|
606
|
+
item = eventToInboxItem(envelope, {
|
|
607
|
+
ownSlackId: identity.slackMemberId,
|
|
608
|
+
principalSlackId: identity.principalSlackId,
|
|
609
|
+
agentFirstName: identity.firstName,
|
|
610
|
+
ccChannels,
|
|
611
|
+
threadContext: extra && typeof extra.threadContext === "string" ? extra.threadContext : undefined,
|
|
612
|
+
});
|
|
613
|
+
} catch (err) {
|
|
614
|
+
error(agentRoot, `translation failed: ${err.message}`, { stack: err.stack });
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
try {
|
|
619
|
+
writeInbox("slack", item);
|
|
620
|
+
state.stats.eventsKept++;
|
|
621
|
+
info(agentRoot, "inbox-item-written", {
|
|
622
|
+
id: item.id,
|
|
623
|
+
channel: item.channel,
|
|
624
|
+
sender: item.sender,
|
|
625
|
+
is_reply: item.is_reply,
|
|
626
|
+
kind: item.kind || "message",
|
|
627
|
+
mentions_agent: item.priority_signals.mentions_agent,
|
|
628
|
+
});
|
|
629
|
+
} catch (err) {
|
|
630
|
+
error(agentRoot, `writeInbox failed: ${err.message}`, { stack: err.stack, id: item.id });
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// -------------------------------------------------------------------------
|
|
635
|
+
// connect — single attempt; on close, schedule a reconnect.
|
|
636
|
+
// -------------------------------------------------------------------------
|
|
637
|
+
async function connect() {
|
|
638
|
+
if (state.stopped) return;
|
|
639
|
+
if (existsSync(join(agentRoot, ".emergency-stop"))) {
|
|
640
|
+
info(agentRoot, ".emergency-stop present — refusing to connect");
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
const open = await openSocketConnection({ appToken, fetchFn });
|
|
645
|
+
if (open.error) {
|
|
646
|
+
state.stats.lastDisconnectReason = `open-failed:${open.error}`;
|
|
647
|
+
error(agentRoot, `apps.connections.open failed: ${open.error}`);
|
|
648
|
+
scheduleReconnect();
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
info(agentRoot, "opening WebSocket", { url_prefix: open.url.slice(0, 64) });
|
|
652
|
+
|
|
653
|
+
let ws;
|
|
654
|
+
try {
|
|
655
|
+
ws = new WebSocketCtor(open.url);
|
|
656
|
+
} catch (err) {
|
|
657
|
+
error(agentRoot, `WebSocket construction failed: ${err.message}`);
|
|
658
|
+
scheduleReconnect();
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
state.socket = ws;
|
|
662
|
+
state.stats.connects++;
|
|
663
|
+
if (state.stats.connects > 1) state.stats.reconnects++;
|
|
664
|
+
|
|
665
|
+
// Both event-emitter and addEventListener APIs work on global WebSocket;
|
|
666
|
+
// use addEventListener so a `ws` polyfill from a future test injection
|
|
667
|
+
// doesn't bite us.
|
|
668
|
+
const onOpen = () => {
|
|
669
|
+
state.lastPingAt = now();
|
|
670
|
+
};
|
|
671
|
+
const onMessage = (event) => {
|
|
672
|
+
const raw = typeof event === "string" ? event : (event?.data ?? "");
|
|
673
|
+
if (!raw) return;
|
|
674
|
+
let envelope;
|
|
675
|
+
try {
|
|
676
|
+
envelope = JSON.parse(typeof raw === "string" ? raw : raw.toString());
|
|
677
|
+
} catch (err) {
|
|
678
|
+
warn(agentRoot, `non-JSON frame: ${err.message}`, { raw_snippet: String(raw).slice(0, 200) });
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
try {
|
|
682
|
+
handleEnvelope(envelope, ws);
|
|
683
|
+
} catch (err) {
|
|
684
|
+
error(agentRoot, `handleEnvelope threw: ${err.message}`, { stack: err.stack });
|
|
685
|
+
}
|
|
686
|
+
};
|
|
687
|
+
const onError = (event) => {
|
|
688
|
+
warn(agentRoot, `socket error: ${event?.message || "unknown"}`);
|
|
689
|
+
};
|
|
690
|
+
const onClose = (event) => {
|
|
691
|
+
const code = event?.code ?? 0;
|
|
692
|
+
const reason = event?.reason ?? "";
|
|
693
|
+
info(agentRoot, "socket closed", { code, reason });
|
|
694
|
+
state.socket = null;
|
|
695
|
+
scheduleReconnect();
|
|
696
|
+
};
|
|
697
|
+
|
|
698
|
+
ws.addEventListener?.("open", onOpen);
|
|
699
|
+
ws.addEventListener?.("message", onMessage);
|
|
700
|
+
ws.addEventListener?.("error", onError);
|
|
701
|
+
ws.addEventListener?.("close", onClose);
|
|
702
|
+
// The `ws` npm package uses Node's EventEmitter; addEventListener may
|
|
703
|
+
// be missing. Fall back to `on` if present.
|
|
704
|
+
if (typeof ws.addEventListener !== "function" && typeof ws.on === "function") {
|
|
705
|
+
ws.on("open", onOpen);
|
|
706
|
+
ws.on("message", (data) => onMessage({ data }));
|
|
707
|
+
ws.on("error", (err) => onError({ message: err?.message }));
|
|
708
|
+
ws.on("close", (code, reason) => onClose({ code, reason: String(reason ?? "") }));
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// -------------------------------------------------------------------------
|
|
713
|
+
// scheduleReconnect — exponential back-off; resets on each hello.
|
|
714
|
+
// -------------------------------------------------------------------------
|
|
715
|
+
function scheduleReconnect() {
|
|
716
|
+
if (state.stopped) return;
|
|
717
|
+
if (state.reconnectTimer) return; // already scheduled
|
|
718
|
+
state.backoffMs = nextBackoffMs(state.backoffMs);
|
|
719
|
+
info(agentRoot, `reconnect in ${state.backoffMs}ms`, { backoff_ms: state.backoffMs });
|
|
720
|
+
state.reconnectTimer = setTimeoutFn(() => {
|
|
721
|
+
state.reconnectTimer = null;
|
|
722
|
+
connect().catch((err) => {
|
|
723
|
+
error(agentRoot, `connect rejected: ${err.message}`, { stack: err.stack });
|
|
724
|
+
scheduleReconnect();
|
|
725
|
+
});
|
|
726
|
+
}, state.backoffMs);
|
|
727
|
+
if (state.reconnectTimer && typeof state.reconnectTimer.unref === "function") {
|
|
728
|
+
state.reconnectTimer.unref();
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// -------------------------------------------------------------------------
|
|
733
|
+
// supervisor — checks .emergency-stop, prunes stale sockets.
|
|
734
|
+
//
|
|
735
|
+
// Timers are unref()-ed so they don't pin the Node event loop alive on
|
|
736
|
+
// test exit. In production the WebSocket itself holds the loop open,
|
|
737
|
+
// and the supervisor's job is purely to detect .emergency-stop while
|
|
738
|
+
// the listener is otherwise idle.
|
|
739
|
+
// -------------------------------------------------------------------------
|
|
740
|
+
function startSupervisor() {
|
|
741
|
+
state.heartbeatTimer = setTimeoutFn(function tick() {
|
|
742
|
+
if (state.stopped) return;
|
|
743
|
+
// Emergency-stop short-circuit. Exit 0 so KeepAlive(SuccessfulExit:false)
|
|
744
|
+
// refuses to restart us.
|
|
745
|
+
if (existsSync(join(agentRoot, ".emergency-stop"))) {
|
|
746
|
+
info(agentRoot, ".emergency-stop detected — exiting 0");
|
|
747
|
+
stop();
|
|
748
|
+
process.exit(0);
|
|
749
|
+
}
|
|
750
|
+
state.heartbeatTimer = setTimeoutFn(tick, SUPERVISOR_INTERVAL_MS);
|
|
751
|
+
if (state.heartbeatTimer && typeof state.heartbeatTimer.unref === "function") {
|
|
752
|
+
state.heartbeatTimer.unref();
|
|
753
|
+
}
|
|
754
|
+
}, SUPERVISOR_INTERVAL_MS);
|
|
755
|
+
if (state.heartbeatTimer && typeof state.heartbeatTimer.unref === "function") {
|
|
756
|
+
state.heartbeatTimer.unref();
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
// -------------------------------------------------------------------------
|
|
761
|
+
// start / stop lifecycle
|
|
762
|
+
// -------------------------------------------------------------------------
|
|
763
|
+
async function start() {
|
|
764
|
+
state.stopped = false;
|
|
765
|
+
startSupervisor();
|
|
766
|
+
await connect();
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
function stop() {
|
|
770
|
+
state.stopped = true;
|
|
771
|
+
if (state.reconnectTimer) {
|
|
772
|
+
clearTimeoutFn(state.reconnectTimer);
|
|
773
|
+
state.reconnectTimer = null;
|
|
774
|
+
}
|
|
775
|
+
if (state.heartbeatTimer) {
|
|
776
|
+
clearTimeoutFn(state.heartbeatTimer);
|
|
777
|
+
state.heartbeatTimer = null;
|
|
778
|
+
}
|
|
779
|
+
if (state.socket) {
|
|
780
|
+
try { state.socket.close(); } catch { /* */ }
|
|
781
|
+
state.socket = null;
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
function getStats() {
|
|
786
|
+
return { ...state.stats };
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
function getState() {
|
|
790
|
+
return {
|
|
791
|
+
connected: !!state.socket && state.socket.readyState === 1,
|
|
792
|
+
backoffMs: state.backoffMs,
|
|
793
|
+
stopped: state.stopped,
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
return { start, stop, getStats, getState, _internals: { handleEnvelope, scheduleReconnect, state } };
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
// ---------------------------------------------------------------------------
|
|
801
|
+
// config/slack.yaml enhancements (single-owner rule, H2)
|
|
802
|
+
// ---------------------------------------------------------------------------
|
|
803
|
+
|
|
804
|
+
/**
|
|
805
|
+
* Resolve the reactions / cc-channels / enrich opts this standalone listener
|
|
806
|
+
* should pass to createSocketModeClient, from config/slack.yaml. This is the
|
|
807
|
+
* mechanism by which enabling config/slack.yaml ENHANCES the single Slack
|
|
808
|
+
* connection (rather than the daemon spawning a second one — which it no longer
|
|
809
|
+
* does, see maestro-daemon.mjs). Exported so the wiring is unit-testable.
|
|
810
|
+
*
|
|
811
|
+
* The enrich hook reuses SlackAdapter's blessed Web-API enrichment via a
|
|
812
|
+
* no-socket adapter instance (constructing it opens no connection — the socket
|
|
813
|
+
* only opens in _connect, which we never call). Dynamic import avoids an
|
|
814
|
+
* eval-time cycle (adapter.mjs imports from this module). Fail-open: any error
|
|
815
|
+
* yields the historical no-enrich defaults so the listener still runs.
|
|
816
|
+
*
|
|
817
|
+
* @returns {Promise<{reactions:boolean, ccChannels:Set<string>|null, enrich:Function|null}>}
|
|
818
|
+
*/
|
|
819
|
+
export async function resolveSocketModeEnhancements(agentRoot, identity, { botToken = process.env.SLACK_BOT_TOKEN || "" } = {}) {
|
|
820
|
+
let slackConfig = {};
|
|
821
|
+
let enrich = null;
|
|
822
|
+
try {
|
|
823
|
+
const adapterMod = await import("../../lib/channels/slack/adapter.mjs");
|
|
824
|
+
slackConfig = adapterMod.loadSlackConfig(agentRoot) || {};
|
|
825
|
+
const enrichAdapter = adapterMod.createSlackChannel({ agentRoot, botToken, slackConfig });
|
|
826
|
+
enrichAdapter._identity = identity;
|
|
827
|
+
enrich = (envelope) => enrichAdapter._enrich(envelope);
|
|
828
|
+
} catch (err) {
|
|
829
|
+
warn(agentRoot, `slack.yaml enrichment unavailable (fail-open): ${err.message}`);
|
|
830
|
+
}
|
|
831
|
+
const reactions = slackConfig.reactions !== false; // default on, mirrors the adapter
|
|
832
|
+
const ccChannels = Array.isArray(slackConfig.cc_channels) && slackConfig.cc_channels.length
|
|
833
|
+
? new Set(slackConfig.cc_channels)
|
|
834
|
+
: null;
|
|
835
|
+
return { reactions, ccChannels, enrich };
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
// ---------------------------------------------------------------------------
|
|
839
|
+
// CLI entry point
|
|
840
|
+
// ---------------------------------------------------------------------------
|
|
841
|
+
|
|
842
|
+
/**
|
|
843
|
+
* Boot the daemon when invoked as a script (not when imported by tests).
|
|
844
|
+
*/
|
|
845
|
+
async function main() {
|
|
846
|
+
const agentRoot = process.env.AGENT_ROOT || process.env.AGENT_DIR || AGENT_REPO_DIR || resolve(__dirname, "..", "..");
|
|
847
|
+
|
|
848
|
+
// Emergency-stop check before anything else (mirror maestro-daemon.mjs).
|
|
849
|
+
if (existsSync(join(agentRoot, ".emergency-stop"))) {
|
|
850
|
+
console.error("[slack-socket] .emergency-stop flag present — refusing to start.");
|
|
851
|
+
console.error("[slack-socket] Lift with: scripts/resume-operations.sh");
|
|
852
|
+
// Hold for a moment so KeepAlive's throttle has something to throttle.
|
|
853
|
+
await new Promise((r) => setTimeout(r, 30_000));
|
|
854
|
+
process.exit(0);
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
loadEnvFromFile(agentRoot);
|
|
858
|
+
|
|
859
|
+
const appToken = process.env.SLACK_APP_LEVEL_TOKEN || "";
|
|
860
|
+
if (!appToken || !appToken.startsWith("xapp-")) {
|
|
861
|
+
error(agentRoot, "SLACK_APP_LEVEL_TOKEN missing or malformed (must start with xapp-). " +
|
|
862
|
+
"Run: node scripts/setup/init-slack-socket-mode.mjs");
|
|
863
|
+
process.exit(78); // EX_CONFIG
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
const identity = loadAgentIdentity(agentRoot);
|
|
867
|
+
const peerSlackIds = loadPeerSlackIds(agentRoot, identity.slackMemberId);
|
|
868
|
+
|
|
869
|
+
// SINGLE-OWNER RULE (H2): this standalone process is the ONE owner of the
|
|
870
|
+
// Slack real-time connection (the maestro daemon's generic channel loop
|
|
871
|
+
// deliberately skips Slack — see maestro-daemon.mjs). Enabling config/slack.yaml
|
|
872
|
+
// must therefore ENHANCE this single connection (reactions / thread context /
|
|
873
|
+
// cc channels), never spawn a second one — so we feed reactions/ccChannels/
|
|
874
|
+
// enrich straight into the one client.
|
|
875
|
+
const { reactions, ccChannels, enrich } = await resolveSocketModeEnhancements(agentRoot, identity);
|
|
876
|
+
|
|
877
|
+
info(agentRoot, "starting Socket Mode listener", {
|
|
878
|
+
agent: identity.firstName,
|
|
879
|
+
slack_id: identity.slackMemberId || "(unset)",
|
|
880
|
+
peers: peerSlackIds.size,
|
|
881
|
+
reactions,
|
|
882
|
+
cc_channels: ccChannels ? ccChannels.size : 0,
|
|
883
|
+
});
|
|
884
|
+
|
|
885
|
+
const client = createSocketModeClient({ agentRoot, appToken, identity, peerSlackIds, reactions, ccChannels, enrich });
|
|
886
|
+
const shutdown = (signal) => {
|
|
887
|
+
info(agentRoot, `received ${signal}, shutting down`);
|
|
888
|
+
client.stop();
|
|
889
|
+
setTimeout(() => process.exit(0), 250);
|
|
890
|
+
};
|
|
891
|
+
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
892
|
+
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
893
|
+
process.on("unhandledRejection", (reason) => {
|
|
894
|
+
error(agentRoot, `unhandled rejection: ${reason?.message || reason}`, {
|
|
895
|
+
stack: reason?.stack,
|
|
896
|
+
});
|
|
897
|
+
});
|
|
898
|
+
process.on("uncaughtException", (err) => {
|
|
899
|
+
error(agentRoot, `uncaught exception: ${err.message}`, { stack: err.stack });
|
|
900
|
+
// Let launchd restart us — non-zero exit triggers KeepAlive.Crashed.
|
|
901
|
+
setTimeout(() => process.exit(1), 250);
|
|
902
|
+
});
|
|
903
|
+
|
|
904
|
+
await client.start();
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
// Only run main() when this file is executed directly (not when imported
|
|
908
|
+
// as a module by the test suite). The `process.argv[1]` check matches the
|
|
909
|
+
// pattern used elsewhere in the framework.
|
|
910
|
+
const isDirectInvocation = process.argv[1] && resolve(process.argv[1]) === __filename;
|
|
911
|
+
if (isDirectInvocation) {
|
|
912
|
+
main().catch((err) => {
|
|
913
|
+
console.error(`[slack-socket] fatal: ${err.message}`);
|
|
914
|
+
console.error(err.stack);
|
|
915
|
+
process.exit(1);
|
|
916
|
+
});
|
|
917
|
+
}
|