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