@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,677 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Maestro — Model router
|
|
3
|
+
*
|
|
4
|
+
* Routes each `claude --print` spawn (and any other Anthropic-Messages
|
|
5
|
+
* caller) to the cheapest backend that satisfies the request's declared
|
|
6
|
+
* capability needs. Backends are configured via `config/model-routing.yaml`
|
|
7
|
+
* in the agent repo; if no config is present, the router is inert and the
|
|
8
|
+
* existing Claude-CLI-on-Max-subscription behaviour is preserved verbatim.
|
|
9
|
+
*
|
|
10
|
+
* Why this exists:
|
|
11
|
+
* - Anthropic Sonnet 4.6 / Opus 4.7 are still the quality ceiling for
|
|
12
|
+
* agent loops, but background work (classification, log triage,
|
|
13
|
+
* scheduled summaries) does NOT need them.
|
|
14
|
+
* - Moonshot exposes a native `/anthropic` Messages endpoint, so
|
|
15
|
+
* Kimi K2.6 is a literal drop-in for Claude Code — no router,
|
|
16
|
+
* no translation, no tool-use degradation.
|
|
17
|
+
* - Qwen3-Coder via OpenRouter is one router-hop away and significantly
|
|
18
|
+
* cheaper; usable for agents that don't need extended-thinking or
|
|
19
|
+
* parallel tool calls.
|
|
20
|
+
*
|
|
21
|
+
* What this is NOT:
|
|
22
|
+
* - Not a wire-format translator. We rely on the backend speaking
|
|
23
|
+
* Anthropic Messages directly (Moonshot, Anthropic, NIM) OR a thin
|
|
24
|
+
* OpenAI-compat shim that Claude Code's existing
|
|
25
|
+
* ANTHROPIC_BASE_URL/AUTH_TOKEN/MODEL knobs already handle.
|
|
26
|
+
* - Not a model wrapper. We only emit env vars for the spawned child;
|
|
27
|
+
* the existing `claude` binary does the actual HTTP I/O.
|
|
28
|
+
*
|
|
29
|
+
* Public API:
|
|
30
|
+
* loadRoutingConfig(agentRoot) → parsed YAML or null if absent
|
|
31
|
+
* resolveBackend(request, opts) → { name, envForSpawn, model, … }
|
|
32
|
+
* describeBackend(name, opts) → human-readable backend summary
|
|
33
|
+
* listBackends(opts) → array of backend names
|
|
34
|
+
* estimateCost(request, opts) → rough $ for a given req
|
|
35
|
+
*
|
|
36
|
+
* Caller integration: in dispatcher.mjs / responder.mjs / classifier.mjs,
|
|
37
|
+
* compute a small `AgentRequest` based on the item being processed and
|
|
38
|
+
* merge the returned `envForSpawn` into the child env. Returning a null
|
|
39
|
+
* backend (no config) means "use whatever you used before".
|
|
40
|
+
*
|
|
41
|
+
* Kill switch:
|
|
42
|
+
* - MAESTRO_ROUTER_FORCE_ANTHROPIC=1 (or "true") short-circuits ALL
|
|
43
|
+
* resolution to the stock Anthropic-CLI behaviour — the same path as
|
|
44
|
+
* when no routing config is present (resolveBackend → null, callers
|
|
45
|
+
* fall through to keychain-OAuth `claude --print`). This is the operator
|
|
46
|
+
* escape hatch when a routed backend misbehaves fleet-wide; it overrides
|
|
47
|
+
* any policy without editing config.
|
|
48
|
+
*
|
|
49
|
+
* Spec / risk notes (May 2026):
|
|
50
|
+
* - ANTHROPIC_API_KEY MUST be set to the empty string when routing to a
|
|
51
|
+
* non-Anthropic base URL; merely unsetting it makes Claude Code fall
|
|
52
|
+
* back to its keychain OAuth and authenticate against api.anthropic.com.
|
|
53
|
+
* - CLAUDE_CODE_ATTRIBUTION_HEADER=0 strips the system-prompt attribution
|
|
54
|
+
* block so a third-party gateway's prompt-cache key isn't poisoned.
|
|
55
|
+
* - Some OpenRouter providers return empty tool_use.input for Qwen3-Coder
|
|
56
|
+
* (musistudio/claude-code-router#409). We classify those as
|
|
57
|
+
* `degraded_tool_use: true` and route agents needing reliable tool use
|
|
58
|
+
* away from them.
|
|
59
|
+
* - A backend whose required `auth_env` is unset/empty is SKIPPED at
|
|
60
|
+
* resolve time (treated as unavailable, chain continues) so we never
|
|
61
|
+
* spawn a non-Anthropic backend with an empty credential.
|
|
62
|
+
*/
|
|
63
|
+
|
|
64
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
65
|
+
import { join, resolve } from "node:path";
|
|
66
|
+
|
|
67
|
+
// v2 surface — the resolveChain "brain" + strict config validation. Imported as
|
|
68
|
+
// a sibling module so the v1 API above stays byte-compatible; resolveChain is
|
|
69
|
+
// purely additive (SPEC §3). See lib/model-router/resolve.mjs.
|
|
70
|
+
import { resolveChain, validateRoutingConfig } from "./model-router/resolve.mjs";
|
|
71
|
+
// Execution lane (additive, SPEC §6.2/§6.3): the session spawn primitive,
|
|
72
|
+
// the governed cheap one-shot, and the cheap-model tool-call repair. Imported as
|
|
73
|
+
// sibling modules so the v1 + resolveChain surfaces stay byte-compatible.
|
|
74
|
+
import { spawnRouted } from "./model-router/spawn.mjs";
|
|
75
|
+
import { llmTask } from "./model-router/llm-task.mjs";
|
|
76
|
+
import { repairToolCalls } from "./model-router/repair.mjs";
|
|
77
|
+
|
|
78
|
+
// We accept either YAML or JSON. js-yaml is optional; the loader gracefully
|
|
79
|
+
// degrades to JSON-only if it isn't installed.
|
|
80
|
+
let yamlParse = null;
|
|
81
|
+
try {
|
|
82
|
+
const y = await import("js-yaml");
|
|
83
|
+
yamlParse = (text) => y.load(text);
|
|
84
|
+
} catch {
|
|
85
|
+
yamlParse = null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
// Defaults — used when no config exists OR when a config has gaps.
|
|
90
|
+
// Anthropic-only fallback so existing agents keep working unchanged.
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
|
|
93
|
+
const ANTHROPIC_DEFAULT = Object.freeze({
|
|
94
|
+
transport: "anthropic-cli", // special: do nothing (current behaviour)
|
|
95
|
+
base_url: "https://api.anthropic.com",
|
|
96
|
+
auth_env: null, // null → use the keychain-resident OAuth token
|
|
97
|
+
capabilities: [
|
|
98
|
+
"thinking",
|
|
99
|
+
"vision",
|
|
100
|
+
"prompt_cache_1h",
|
|
101
|
+
"tool_use",
|
|
102
|
+
"parallel_tools",
|
|
103
|
+
"long_context_1m",
|
|
104
|
+
],
|
|
105
|
+
pricing: { input_per_m: 3.0, output_per_m: 15.0 }, // Sonnet 4.6 ballpark
|
|
106
|
+
models: {
|
|
107
|
+
classifier: "claude-haiku-4-5-20251001",
|
|
108
|
+
default: "claude-sonnet-4-6",
|
|
109
|
+
premium: "claude-opus-4-7",
|
|
110
|
+
fast: "claude-haiku-4-5-20251001",
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const DEFAULT_CONFIG = Object.freeze({
|
|
115
|
+
backends: { anthropic: ANTHROPIC_DEFAULT },
|
|
116
|
+
routing_policy: [{ default: true, backend: "anthropic" }],
|
|
117
|
+
fallback_to_anthropic: true,
|
|
118
|
+
strip_attribution_header: true,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* The Anthropic-only default routing config (audit L21).
|
|
123
|
+
*
|
|
124
|
+
* Returns a normalised, deep-cloned copy of {@link DEFAULT_CONFIG} so callers
|
|
125
|
+
* that explicitly want "the built-in Anthropic-CLI default" — rather than the
|
|
126
|
+
* `null` that `loadRoutingConfig` returns when no config file is present — have
|
|
127
|
+
* a concrete config object to pass into `resolveBackend({}, { config })`.
|
|
128
|
+
*
|
|
129
|
+
* This is intentionally an OPT-IN accessor. The default search path
|
|
130
|
+
* (`loadRoutingConfig` returning `null` when no file exists) is preserved
|
|
131
|
+
* verbatim so existing callers keep falling through to "current Claude CLI
|
|
132
|
+
* behaviour"; nothing about that contract changes.
|
|
133
|
+
*
|
|
134
|
+
* @returns {object} A fresh, normalised default config (safe to mutate).
|
|
135
|
+
*/
|
|
136
|
+
export function defaultRoutingConfig() {
|
|
137
|
+
// Normalise through the same path as a file-loaded config so the returned
|
|
138
|
+
// shape (clones of ANTHROPIC_DEFAULT, derived booleans, etc.) is identical.
|
|
139
|
+
return normaliseConfig(
|
|
140
|
+
{
|
|
141
|
+
backends: { anthropic: cloneBackend(ANTHROPIC_DEFAULT) },
|
|
142
|
+
routing_policy: [{ default: true, backend: "anthropic" }],
|
|
143
|
+
fallback_to_anthropic: DEFAULT_CONFIG.fallback_to_anthropic,
|
|
144
|
+
strip_attribution_header: DEFAULT_CONFIG.strip_attribution_header,
|
|
145
|
+
},
|
|
146
|
+
{ sourcePath: "<default>" }
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ---------------------------------------------------------------------------
|
|
151
|
+
// Config loader
|
|
152
|
+
// ---------------------------------------------------------------------------
|
|
153
|
+
|
|
154
|
+
export const CONFIG_RELATIVE_PATH = "config/model-routing.yaml";
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Resolve the active routing config. Search order:
|
|
158
|
+
* 1. opts.configPath (explicit)
|
|
159
|
+
* 2. $MAESTRO_ROUTING_CONFIG env var
|
|
160
|
+
* 3. <agentRoot>/config/model-routing.yaml
|
|
161
|
+
* 4. <agentRoot>/config/model-routing.json
|
|
162
|
+
*
|
|
163
|
+
* Returns null when no config exists so callers can fall through to
|
|
164
|
+
* "current behaviour". Throws on parse error so misconfiguration is loud.
|
|
165
|
+
*
|
|
166
|
+
* @param {string} [agentRoot]
|
|
167
|
+
* @param {{ configPath?: string, useDefault?: boolean }} [opts]
|
|
168
|
+
* When `useDefault` is true, returns {@link defaultRoutingConfig} instead of
|
|
169
|
+
* null if no config file is found (opt-in; default false preserves the
|
|
170
|
+
* null-when-absent contract).
|
|
171
|
+
*/
|
|
172
|
+
export function loadRoutingConfig(agentRoot, opts = {}) {
|
|
173
|
+
const root = resolve(
|
|
174
|
+
agentRoot ||
|
|
175
|
+
process.env.AGENT_ROOT ||
|
|
176
|
+
process.env.AGENT_DIR ||
|
|
177
|
+
process.cwd()
|
|
178
|
+
);
|
|
179
|
+
const candidates = [
|
|
180
|
+
opts.configPath,
|
|
181
|
+
process.env.MAESTRO_ROUTING_CONFIG,
|
|
182
|
+
join(root, CONFIG_RELATIVE_PATH),
|
|
183
|
+
join(root, "config/model-routing.json"),
|
|
184
|
+
].filter(Boolean);
|
|
185
|
+
|
|
186
|
+
for (const path of candidates) {
|
|
187
|
+
if (!existsSync(path)) continue;
|
|
188
|
+
const raw = readFileSync(path, "utf-8");
|
|
189
|
+
const parsed = path.endsWith(".json")
|
|
190
|
+
? JSON.parse(raw)
|
|
191
|
+
: parseYaml(raw, path);
|
|
192
|
+
return normaliseConfig(parsed, { sourcePath: path });
|
|
193
|
+
}
|
|
194
|
+
// Opt-in: callers that want the built-in Anthropic default rather than null.
|
|
195
|
+
if (opts.useDefault) return defaultRoutingConfig();
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function parseYaml(text, sourcePath) {
|
|
200
|
+
if (!yamlParse) {
|
|
201
|
+
throw new Error(
|
|
202
|
+
`[model-router] ${sourcePath} is YAML but js-yaml is not installed. ` +
|
|
203
|
+
"Install it (`npm i js-yaml`) or convert the config to JSON."
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
try {
|
|
207
|
+
return yamlParse(text);
|
|
208
|
+
} catch (err) {
|
|
209
|
+
throw new Error(`[model-router] failed to parse ${sourcePath}: ${err.message}`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function normaliseConfig(raw, { sourcePath } = {}) {
|
|
214
|
+
if (!raw || typeof raw !== "object") {
|
|
215
|
+
throw new Error(`[model-router] config at ${sourcePath} is empty or not an object`);
|
|
216
|
+
}
|
|
217
|
+
// ── Policy v2 ─────────────────────────────────────────────────────────────
|
|
218
|
+
// A `schema_version: 2` config drives the new resolveChain brain (catalog
|
|
219
|
+
// rows + chains + task/data classes + lanes), NOT the v1 backend resolver.
|
|
220
|
+
// We pass it through largely verbatim (resolveChain validates it strictly via
|
|
221
|
+
// validateRoutingConfig); we only fill in the few defaults the brain reads.
|
|
222
|
+
if (raw.schema_version === 2) {
|
|
223
|
+
return normaliseConfigV2(raw, sourcePath);
|
|
224
|
+
}
|
|
225
|
+
const backends = { ...(raw.backends || {}) };
|
|
226
|
+
if (!Object.keys(backends).length) {
|
|
227
|
+
throw new Error(`[model-router] config at ${sourcePath} declares no backends`);
|
|
228
|
+
}
|
|
229
|
+
// Ensure an anthropic backend always exists as the safety net unless the
|
|
230
|
+
// operator has explicitly opted out via fallback_to_anthropic: false.
|
|
231
|
+
if (!backends.anthropic && raw.fallback_to_anthropic !== false) {
|
|
232
|
+
backends.anthropic = cloneBackend(ANTHROPIC_DEFAULT);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Defensive normalisation of each backend.
|
|
236
|
+
for (const [name, b] of Object.entries(backends)) {
|
|
237
|
+
if (!b.transport) {
|
|
238
|
+
throw new Error(`[model-router] backend "${name}" missing transport`);
|
|
239
|
+
}
|
|
240
|
+
b.capabilities = Array.isArray(b.capabilities) ? b.capabilities : [];
|
|
241
|
+
b.models = b.models || {};
|
|
242
|
+
b.pricing = b.pricing || {};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const policy = Array.isArray(raw.routing_policy) && raw.routing_policy.length
|
|
246
|
+
? raw.routing_policy
|
|
247
|
+
: [{ default: true, backend: Object.keys(backends)[0] }];
|
|
248
|
+
|
|
249
|
+
return {
|
|
250
|
+
backends,
|
|
251
|
+
routing_policy: policy,
|
|
252
|
+
fallback_to_anthropic: raw.fallback_to_anthropic !== false,
|
|
253
|
+
strip_attribution_header: raw.strip_attribution_header !== false,
|
|
254
|
+
disable_experimental_betas: raw.disable_experimental_betas !== false,
|
|
255
|
+
source_path: sourcePath,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Normalise a policy-v2 config. The v2 brain (resolveChain) consults the catalog
|
|
261
|
+
* for model rows; the config only carries policy: aliases, data-class backends,
|
|
262
|
+
* the ordered routing_policy chain rules, the budget ladder, and defaults. We
|
|
263
|
+
* keep the raw shape and only stamp defaults the brain relies on so a sparse
|
|
264
|
+
* config still resolves. Strict validation is the job of validateRoutingConfig.
|
|
265
|
+
*/
|
|
266
|
+
function normaliseConfigV2(raw, sourcePath) {
|
|
267
|
+
const defaults = raw.defaults && typeof raw.defaults === "object" ? { ...raw.defaults } : {};
|
|
268
|
+
if (defaults.needs_tool_use_for_sessions === undefined) defaults.needs_tool_use_for_sessions = true;
|
|
269
|
+
if (defaults.data_class === undefined) defaults.data_class = "sensitive";
|
|
270
|
+
if (defaults.cache_ttl === undefined) defaults.cache_ttl = "1h";
|
|
271
|
+
return {
|
|
272
|
+
schema_version: 2,
|
|
273
|
+
aliases: raw.aliases && typeof raw.aliases === "object" ? { ...raw.aliases } : {},
|
|
274
|
+
defaults,
|
|
275
|
+
backends: raw.backends && typeof raw.backends === "object" ? { ...raw.backends } : {},
|
|
276
|
+
routing_policy: Array.isArray(raw.routing_policy) ? raw.routing_policy : [],
|
|
277
|
+
budget_ladder: raw.budget_ladder && typeof raw.budget_ladder === "object" ? { ...raw.budget_ladder } : {},
|
|
278
|
+
fallback_to_anthropic: raw.fallback_to_anthropic !== false,
|
|
279
|
+
strip_attribution_header: raw.strip_attribution_header !== false,
|
|
280
|
+
disable_experimental_betas: raw.disable_experimental_betas !== false,
|
|
281
|
+
source_path: sourcePath,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// ---------------------------------------------------------------------------
|
|
286
|
+
// Resolver
|
|
287
|
+
// ---------------------------------------------------------------------------
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* @typedef {Object} AgentRequest
|
|
291
|
+
* @property {string} [agent_role] Maestro action role tag, e.g. "classifier", "responder", "ceo_pre_pass"
|
|
292
|
+
* @property {boolean} [needs_thinking] Extended-thinking required
|
|
293
|
+
* @property {boolean} [needs_vision] Vision input required
|
|
294
|
+
* @property {boolean} [needs_long_context] >200k token context window required
|
|
295
|
+
* @property {boolean} [needs_tool_use] Tool/function calling required
|
|
296
|
+
* @property {boolean} [needs_parallel_tools] Multi-tool-per-turn required
|
|
297
|
+
* @property {string} [tier] "classifier" | "default" | "premium" | "fast"
|
|
298
|
+
* @property {string} [model_hint] "haiku" | "sonnet" | "opus" — coarse legacy tier
|
|
299
|
+
* @property {number} [token_estimate] Approximate input token count
|
|
300
|
+
* @property {string} [source] "inbox" | "backlog" | "cadence" | "voice" | etc
|
|
301
|
+
* @property {string} [priority] "critical" | "high" | "normal"
|
|
302
|
+
*/
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Resolve a backend for the given request. Returns null when no config is
|
|
306
|
+
* loaded so callers can fall through to "use current Claude CLI behaviour".
|
|
307
|
+
*
|
|
308
|
+
* @param {AgentRequest} request
|
|
309
|
+
* @param {{ config?: object, agentRoot?: string, env?: object }} opts
|
|
310
|
+
* @returns {null | ResolvedBackend}
|
|
311
|
+
*/
|
|
312
|
+
export function resolveBackend(request = {}, opts = {}) {
|
|
313
|
+
const env = opts.env || process.env;
|
|
314
|
+
// Kill switch: force stock Anthropic-CLI behaviour regardless of policy.
|
|
315
|
+
// Returning null is the same fall-through path callers take when no config
|
|
316
|
+
// is present, so this is a true escape hatch — no env injection, no routing.
|
|
317
|
+
if (forceAnthropic(env)) return null;
|
|
318
|
+
|
|
319
|
+
const config = opts.config || loadRoutingConfig(opts.agentRoot);
|
|
320
|
+
if (!config) return null;
|
|
321
|
+
|
|
322
|
+
const tried = [];
|
|
323
|
+
const chosen = pickPolicyRule(config.routing_policy, request);
|
|
324
|
+
const backendOrder = [chosen.backend].concat(
|
|
325
|
+
Array.isArray(chosen.fallback) ? chosen.fallback : []
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
for (const backendName of backendOrder) {
|
|
329
|
+
const backend = config.backends[backendName];
|
|
330
|
+
if (!backend) {
|
|
331
|
+
tried.push({ backend: backendName, ok: false, reason: "unknown_backend" });
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
const supports = backendSupports(backend, request);
|
|
335
|
+
if (!supports.ok) {
|
|
336
|
+
tried.push({ backend: backendName, ok: false, reason: supports.reason });
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
// A backend with a configured-but-absent credential is unavailable, not a
|
|
340
|
+
// candidate to spawn with an empty token (which would silently fail auth
|
|
341
|
+
// at runtime, burn retries, and poison the breaker). Skip and continue.
|
|
342
|
+
if (!hasAuth(backend, env)) {
|
|
343
|
+
tried.push({ backend: backendName, ok: false, reason: "missing_auth_env" });
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
return buildResolved({
|
|
347
|
+
name: backendName,
|
|
348
|
+
backend,
|
|
349
|
+
request,
|
|
350
|
+
config,
|
|
351
|
+
ruleMeta: chosen,
|
|
352
|
+
env,
|
|
353
|
+
tried,
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Safety net: anthropic-cli needs no auth_env (keychain OAuth), so the
|
|
358
|
+
// hasAuth gate above never excludes it here.
|
|
359
|
+
if (config.fallback_to_anthropic && config.backends.anthropic) {
|
|
360
|
+
return buildResolved({
|
|
361
|
+
name: "anthropic",
|
|
362
|
+
backend: config.backends.anthropic,
|
|
363
|
+
request,
|
|
364
|
+
config,
|
|
365
|
+
ruleMeta: { backend: "anthropic", fallback_reason: "no_compatible_backend" },
|
|
366
|
+
env,
|
|
367
|
+
tried,
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// No fallback declared and nothing satisfied the request.
|
|
372
|
+
return null;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* @typedef {Object} ResolvedBackend
|
|
377
|
+
* @property {string} name Backend key
|
|
378
|
+
* @property {string} transport "anthropic-cli" | "anthropic-native" | "openai-compat"
|
|
379
|
+
* @property {string} model Concrete model string for ANTHROPIC_MODEL
|
|
380
|
+
* @property {string} fastModel Concrete model for ANTHROPIC_SMALL_FAST_MODEL
|
|
381
|
+
* @property {Object<string,string>} envForSpawn Env vars to merge into the child env
|
|
382
|
+
* @property {Array<object>} tried Diagnostic record of attempts
|
|
383
|
+
* @property {object} capabilities Capability flags actually granted
|
|
384
|
+
* @property {object} pricing Pricing info for cost tracking
|
|
385
|
+
* @property {string} [rule] Matched policy rule (debug)
|
|
386
|
+
*/
|
|
387
|
+
|
|
388
|
+
function buildResolved({ name, backend, request, config, ruleMeta, env, tried }) {
|
|
389
|
+
const tier = request.tier || mapHintToTier(request.model_hint) || "default";
|
|
390
|
+
const model = backend.models[tier] || backend.models.default;
|
|
391
|
+
const fastModel = backend.models.fast || backend.models.classifier || model;
|
|
392
|
+
|
|
393
|
+
const envForSpawn = {};
|
|
394
|
+
|
|
395
|
+
if (backend.transport === "anthropic-cli") {
|
|
396
|
+
// Special case: preserve current `claude --print` behaviour exactly.
|
|
397
|
+
// We don't touch ANTHROPIC_BASE_URL/AUTH_TOKEN/API_KEY; Claude Code
|
|
398
|
+
// will use its keychain-resident OAuth token (Max subscription).
|
|
399
|
+
// We DO honour the request's tier via --model (caller passes it on
|
|
400
|
+
// the CLI), but we don't need to inject anything into the env.
|
|
401
|
+
} else {
|
|
402
|
+
// For any non-Anthropic backend, we must:
|
|
403
|
+
// 1. Point Claude Code at the new base URL
|
|
404
|
+
// 2. Provide the auth token
|
|
405
|
+
// 3. Set ANTHROPIC_API_KEY to the empty string (NOT unset) so
|
|
406
|
+
// Claude Code skips its OAuth-with-Anthropic flow.
|
|
407
|
+
// 4. Optionally strip the attribution header so the backend's
|
|
408
|
+
// prompt cache isn't poisoned.
|
|
409
|
+
envForSpawn.ANTHROPIC_BASE_URL = backend.base_url;
|
|
410
|
+
if (backend.auth_env) {
|
|
411
|
+
const token = env[backend.auth_env] || "";
|
|
412
|
+
envForSpawn.ANTHROPIC_AUTH_TOKEN = token;
|
|
413
|
+
}
|
|
414
|
+
envForSpawn.ANTHROPIC_API_KEY = "";
|
|
415
|
+
if (model) envForSpawn.ANTHROPIC_MODEL = model;
|
|
416
|
+
if (fastModel) envForSpawn.ANTHROPIC_SMALL_FAST_MODEL = fastModel;
|
|
417
|
+
if (config.strip_attribution_header) {
|
|
418
|
+
envForSpawn.CLAUDE_CODE_ATTRIBUTION_HEADER = "0";
|
|
419
|
+
}
|
|
420
|
+
if (config.disable_experimental_betas) {
|
|
421
|
+
envForSpawn.CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS = "1";
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
return {
|
|
426
|
+
name,
|
|
427
|
+
transport: backend.transport,
|
|
428
|
+
model,
|
|
429
|
+
fastModel,
|
|
430
|
+
envForSpawn,
|
|
431
|
+
tried,
|
|
432
|
+
capabilities: indexCapabilities(backend.capabilities),
|
|
433
|
+
pricing: backend.pricing || {},
|
|
434
|
+
rule: ruleMeta?.match ? JSON.stringify(ruleMeta.match) : "default",
|
|
435
|
+
fallback_reason: ruleMeta?.fallback_reason || null,
|
|
436
|
+
config_source: config.source_path || null,
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// ---------------------------------------------------------------------------
|
|
441
|
+
// Policy + capability matching
|
|
442
|
+
// ---------------------------------------------------------------------------
|
|
443
|
+
|
|
444
|
+
function pickPolicyRule(rules, request) {
|
|
445
|
+
// Rules are evaluated top-to-bottom; first match wins. A rule with
|
|
446
|
+
// `default: true` always matches and acts as the terminal fallback.
|
|
447
|
+
for (const rule of rules) {
|
|
448
|
+
if (rule.default) return rule;
|
|
449
|
+
if (matchesRule(rule.match || {}, request)) return rule;
|
|
450
|
+
}
|
|
451
|
+
// No rule matched — synthesize a default pointing to the first backend.
|
|
452
|
+
return { default: true, backend: rules[0]?.backend, synthesized: true };
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function matchesRule(match, req) {
|
|
456
|
+
for (const [key, expected] of Object.entries(match)) {
|
|
457
|
+
if (key === "token_estimate_gte") {
|
|
458
|
+
if ((req.token_estimate ?? 0) < expected) return false;
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
461
|
+
if (key === "token_estimate_lt") {
|
|
462
|
+
if ((req.token_estimate ?? 0) >= expected) return false;
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
if (key === "needs_any") {
|
|
466
|
+
const needs = Array.isArray(expected) ? expected : [expected];
|
|
467
|
+
if (!needs.some((n) => req[n])) return false;
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
470
|
+
if (key === "agent_role_in") {
|
|
471
|
+
if (!Array.isArray(expected) || !expected.includes(req.agent_role)) return false;
|
|
472
|
+
continue;
|
|
473
|
+
}
|
|
474
|
+
if (req[key] !== expected) return false;
|
|
475
|
+
}
|
|
476
|
+
return true;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function backendSupports(backend, req) {
|
|
480
|
+
const caps = indexCapabilities(backend.capabilities);
|
|
481
|
+
if (req.needs_thinking && !caps.thinking) return { ok: false, reason: "no_thinking" };
|
|
482
|
+
if (req.needs_vision && !caps.vision) return { ok: false, reason: "no_vision" };
|
|
483
|
+
if (req.needs_tool_use && !caps.tool_use) return { ok: false, reason: "no_tool_use" };
|
|
484
|
+
if (req.needs_parallel_tools && !caps.parallel_tools)
|
|
485
|
+
return { ok: false, reason: "no_parallel_tools" };
|
|
486
|
+
if (req.needs_long_context) {
|
|
487
|
+
if (!caps.long_context_1m && !caps.long_context_262k && !caps.long_context_200k) {
|
|
488
|
+
return { ok: false, reason: "no_long_context" };
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
// Reject backends explicitly marked degraded for the requested capability.
|
|
492
|
+
if (req.needs_tool_use && caps.degraded_tool_use) {
|
|
493
|
+
return { ok: false, reason: "degraded_tool_use" };
|
|
494
|
+
}
|
|
495
|
+
return { ok: true };
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function indexCapabilities(arr) {
|
|
499
|
+
const out = {};
|
|
500
|
+
for (const c of arr || []) out[c] = true;
|
|
501
|
+
return out;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* True when the operator kill switch is set. Accepts "1" or "true" so it
|
|
506
|
+
* matches the other CLAUDE_CODE_* / GOV_* boolean env conventions in maestro.
|
|
507
|
+
*/
|
|
508
|
+
function forceAnthropic(env) {
|
|
509
|
+
const v = env?.MAESTRO_ROUTER_FORCE_ANTHROPIC;
|
|
510
|
+
return v === "1" || v === "true";
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* A backend is auth-available when it either needs no token (anthropic-cli
|
|
515
|
+
* keychain OAuth, or an explicit `auth_env: null` local server) or its named
|
|
516
|
+
* auth env var is present and non-empty. An empty/whitespace token counts as
|
|
517
|
+
* absent — spawning with it would just fail auth at runtime.
|
|
518
|
+
*/
|
|
519
|
+
function hasAuth(backend, env) {
|
|
520
|
+
if (!backend.auth_env) return true; // null/absent → no token required
|
|
521
|
+
const token = env?.[backend.auth_env];
|
|
522
|
+
return typeof token === "string" && token.trim() !== "";
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function cloneBackend(b) {
|
|
526
|
+
return {
|
|
527
|
+
...b,
|
|
528
|
+
capabilities: Array.isArray(b.capabilities) ? [...b.capabilities] : [],
|
|
529
|
+
models: { ...(b.models || {}) },
|
|
530
|
+
pricing: { ...(b.pricing || {}) },
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function mapHintToTier(hint) {
|
|
535
|
+
if (!hint) return null;
|
|
536
|
+
const h = String(hint).toLowerCase();
|
|
537
|
+
if (h === "haiku") return "fast";
|
|
538
|
+
if (h === "sonnet") return "default";
|
|
539
|
+
if (h === "opus") return "premium";
|
|
540
|
+
if (h === "classifier") return "fast";
|
|
541
|
+
if (h === "premium" || h === "default" || h === "fast" || h === "classifier") return h;
|
|
542
|
+
return null;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// ---------------------------------------------------------------------------
|
|
546
|
+
// Cost estimation (rough — pricing differs by region and version)
|
|
547
|
+
// ---------------------------------------------------------------------------
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* Estimate the dollar cost for a request given the resolved backend.
|
|
551
|
+
* Treats pricing as $ per million tokens. Returns null if we lack data.
|
|
552
|
+
*/
|
|
553
|
+
export function estimateCost(request, resolved) {
|
|
554
|
+
if (!resolved || !resolved.pricing) return null;
|
|
555
|
+
const inTokens = request.token_estimate ?? 0;
|
|
556
|
+
const outTokens = request.token_estimate_out ?? Math.floor(inTokens * 0.5);
|
|
557
|
+
const { input_per_m, output_per_m, cache_hit_per_m } = resolved.pricing;
|
|
558
|
+
if (input_per_m == null || output_per_m == null) return null;
|
|
559
|
+
const cacheHits = request.cache_hit_ratio ?? 0;
|
|
560
|
+
const inputUsd =
|
|
561
|
+
((inTokens * (1 - cacheHits)) / 1e6) * input_per_m +
|
|
562
|
+
(cache_hit_per_m
|
|
563
|
+
? ((inTokens * cacheHits) / 1e6) * cache_hit_per_m
|
|
564
|
+
: 0);
|
|
565
|
+
const outputUsd = (outTokens / 1e6) * output_per_m;
|
|
566
|
+
return Number((inputUsd + outputUsd).toFixed(6));
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// ---------------------------------------------------------------------------
|
|
570
|
+
// Introspection helpers
|
|
571
|
+
// ---------------------------------------------------------------------------
|
|
572
|
+
|
|
573
|
+
export function listBackends(opts = {}) {
|
|
574
|
+
const config = opts.config || loadRoutingConfig(opts.agentRoot);
|
|
575
|
+
if (!config) return ["anthropic"];
|
|
576
|
+
return Object.keys(config.backends);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
export function describeBackend(name, opts = {}) {
|
|
580
|
+
const config = opts.config || loadRoutingConfig(opts.agentRoot);
|
|
581
|
+
const backend = config?.backends?.[name];
|
|
582
|
+
if (!backend) return null;
|
|
583
|
+
return {
|
|
584
|
+
name,
|
|
585
|
+
transport: backend.transport,
|
|
586
|
+
base_url: backend.base_url || null,
|
|
587
|
+
capabilities: indexCapabilities(backend.capabilities),
|
|
588
|
+
models: { ...backend.models },
|
|
589
|
+
pricing: { ...backend.pricing },
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* Build the CLI `--model` flag value for a resolved backend. For
|
|
595
|
+
* anthropic-cli transport this is the bare model string (haiku/sonnet/opus
|
|
596
|
+
* shorthand or a fully-qualified id). For non-Anthropic transports we pass
|
|
597
|
+
* the concrete model string because Claude Code propagates it directly.
|
|
598
|
+
*/
|
|
599
|
+
export function modelFlagFor(resolved, request = {}) {
|
|
600
|
+
if (!resolved) return modelFlagFromHint(request);
|
|
601
|
+
const tier = request.tier || mapHintToTier(request.model_hint) || "default";
|
|
602
|
+
// For anthropic-cli, prefer the human-readable shorthand because the CLI
|
|
603
|
+
// accepts "sonnet" / "opus" / "haiku" and Anthropic resolves them to the
|
|
604
|
+
// current default version, which is what existing maestro code relies on.
|
|
605
|
+
if (resolved.transport === "anthropic-cli") {
|
|
606
|
+
if (tier === "fast" || tier === "classifier") return "haiku";
|
|
607
|
+
if (tier === "premium") return "opus";
|
|
608
|
+
if (tier === "default") return "sonnet";
|
|
609
|
+
return resolved.model;
|
|
610
|
+
}
|
|
611
|
+
return resolved.model;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function modelFlagFromHint(req) {
|
|
615
|
+
const tier = req?.tier || mapHintToTier(req?.model_hint) || "default";
|
|
616
|
+
if (tier === "fast" || tier === "classifier") return "haiku";
|
|
617
|
+
if (tier === "premium") return "opus";
|
|
618
|
+
return "sonnet";
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// ---------------------------------------------------------------------------
|
|
622
|
+
// Convenience: derive an AgentRequest from a maestro classifier result.
|
|
623
|
+
// Keeps the dispatcher's call-site one-liner-clean.
|
|
624
|
+
// ---------------------------------------------------------------------------
|
|
625
|
+
|
|
626
|
+
export function requestFromClassifierResult(classResult = {}, opts = {}) {
|
|
627
|
+
const req = {
|
|
628
|
+
agent_role: opts.role || "responder",
|
|
629
|
+
source: opts.source || "inbox",
|
|
630
|
+
priority: classResult.priority || "normal",
|
|
631
|
+
model_hint: classResult.model || null, // "opus" | "sonnet"
|
|
632
|
+
};
|
|
633
|
+
// Dispatcher spawns are full Claude Code agent loops with the complete tool
|
|
634
|
+
// surface, so tool use is required by construction. Default needs_tool_use
|
|
635
|
+
// to TRUE for session work and let one-shot lookups opt out explicitly
|
|
636
|
+
// (opts.oneShot: true) — otherwise a normal-priority item could match a
|
|
637
|
+
// `degraded_tool_use` backend and the capability gate would never fire.
|
|
638
|
+
req.needs_tool_use = opts.oneShot ? false : true;
|
|
639
|
+
// Critical / opus → also needs extended thinking.
|
|
640
|
+
if (classResult.priority === "critical" || classResult.model === "opus") {
|
|
641
|
+
req.needs_thinking = true;
|
|
642
|
+
req.needs_tool_use = true;
|
|
643
|
+
}
|
|
644
|
+
if (opts.token_estimate) req.token_estimate = opts.token_estimate;
|
|
645
|
+
return req;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// ---------------------------------------------------------------------------
|
|
649
|
+
// v2 surface (additive) — re-export the resolveChain brain + validator so
|
|
650
|
+
// callers import everything from "@cohortapp/agent-sdk/model-router".
|
|
651
|
+
// ---------------------------------------------------------------------------
|
|
652
|
+
|
|
653
|
+
export { resolveChain, validateRoutingConfig };
|
|
654
|
+
// Execution lane re-exports (additive). spawnRouted is the daemon's session
|
|
655
|
+
// primitive; llmTask the sanctioned cheap one-shot; repairToolCalls promotes
|
|
656
|
+
// cheap-model text/JSON tool-call leakage into native tool_use blocks.
|
|
657
|
+
export { spawnRouted, llmTask, repairToolCalls };
|
|
658
|
+
|
|
659
|
+
// Default export: the resolver itself, for ergonomic `import router from`.
|
|
660
|
+
export default {
|
|
661
|
+
loadRoutingConfig,
|
|
662
|
+
defaultRoutingConfig,
|
|
663
|
+
resolveBackend,
|
|
664
|
+
estimateCost,
|
|
665
|
+
listBackends,
|
|
666
|
+
describeBackend,
|
|
667
|
+
modelFlagFor,
|
|
668
|
+
requestFromClassifierResult,
|
|
669
|
+
CONFIG_RELATIVE_PATH,
|
|
670
|
+
// v2:
|
|
671
|
+
resolveChain,
|
|
672
|
+
validateRoutingConfig,
|
|
673
|
+
// execution lane:
|
|
674
|
+
spawnRouted,
|
|
675
|
+
llmTask,
|
|
676
|
+
repairToolCalls,
|
|
677
|
+
};
|