@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,453 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/model-router/health.mjs — per-key circuit breakers for the model router.
|
|
3
|
+
*
|
|
4
|
+
* WRAPS (does not replace) `lib/rate-guard.mjs`. rate-guard remains the single
|
|
5
|
+
* shared 429/overload breaker per *provider* with decorrelated-jitter backoff;
|
|
6
|
+
* health.mjs adds the router's finer-grained per-KEY breaker, where a key is
|
|
7
|
+
* either a backend (`anthropic`) or a backend:model-class (`anthropic:sonnet`).
|
|
8
|
+
* Model-scoped keys are the load-bearing primitive behind the disposition
|
|
9
|
+
* table's `backend:model` cooldown scope — Haiku keeps flowing while the Sonnet
|
|
10
|
+
* bucket is dry (SPEC §4.5 / §4.6).
|
|
11
|
+
*
|
|
12
|
+
* Breaker file shape (one JSON per key under state/rate-limits/, same dir/
|
|
13
|
+
* pattern as rate-guard so doctor dumps both):
|
|
14
|
+
* { key, openedAt, cooldownUntil, reason, strikes, lastProbeAt, halfOpen }
|
|
15
|
+
*
|
|
16
|
+
* Two correctness properties over the audited rate-guard race (M4):
|
|
17
|
+
* 1. Writes go through an O_EXCL lock so two concurrent `open()` calls on the
|
|
18
|
+
* same key converge to ONE coherent file instead of clobbering each other
|
|
19
|
+
* mid read-modify-write. The loser of the lock retries-then-merges.
|
|
20
|
+
* 2. `strikes` is monotonic per key across the lock so the stepped rate_limit
|
|
21
|
+
* ladder (30s→1m→5m) advances correctly even under concurrency.
|
|
22
|
+
*
|
|
23
|
+
* Half-open probing: at most ONE probe per backend per 30s, only within 2 min
|
|
24
|
+
* of cooldown expiry — lets a single in-flight failover run a cheap canary
|
|
25
|
+
* before the chain commits, without a probe storm. Counters reset on expiry.
|
|
26
|
+
*
|
|
27
|
+
* House style: ESM, Node built-ins only, never throws on I/O failure
|
|
28
|
+
* (fail-open-for-work — a breaker we can't read allows the spawn), clock + dir
|
|
29
|
+
* injectable so tests are hermetic and never touch the real fs/clock.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import {
|
|
33
|
+
existsSync,
|
|
34
|
+
mkdirSync,
|
|
35
|
+
readFileSync,
|
|
36
|
+
writeFileSync,
|
|
37
|
+
renameSync,
|
|
38
|
+
unlinkSync,
|
|
39
|
+
openSync,
|
|
40
|
+
closeSync,
|
|
41
|
+
readdirSync,
|
|
42
|
+
} from "node:fs";
|
|
43
|
+
import { join, resolve, dirname } from "node:path";
|
|
44
|
+
import { randomBytes } from "node:crypto";
|
|
45
|
+
|
|
46
|
+
import { sanitizeProvider } from "../rate-guard.mjs";
|
|
47
|
+
import { DISPOSITION, cooldownMsFor } from "./taxonomy.mjs";
|
|
48
|
+
|
|
49
|
+
const SECOND = 1000;
|
|
50
|
+
const MINUTE = 60 * SECOND;
|
|
51
|
+
|
|
52
|
+
/** Half-open: at most one probe per backend per this window. */
|
|
53
|
+
export const HALF_OPEN_PROBE_INTERVAL_MS = 30 * SECOND;
|
|
54
|
+
/** Half-open is eligible only within this window before cooldown expiry. */
|
|
55
|
+
export const HALF_OPEN_WINDOW_MS = 2 * MINUTE;
|
|
56
|
+
/** How long we busy-spin trying to grab the O_EXCL lock before giving up. */
|
|
57
|
+
const LOCK_TIMEOUT_MS = 2 * SECOND;
|
|
58
|
+
const LOCK_SPIN_MS = 5;
|
|
59
|
+
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
// Injected primitives + paths
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
function clock(deps) {
|
|
65
|
+
return deps && typeof deps.now === "function" ? deps.now : Date.now;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function resolveStateDir(deps) {
|
|
69
|
+
if (deps && deps.stateDir) return resolve(deps.stateDir);
|
|
70
|
+
const root =
|
|
71
|
+
(deps && deps.agentRoot) ||
|
|
72
|
+
process.env.AGENT_ROOT ||
|
|
73
|
+
process.env.AGENT_DIR ||
|
|
74
|
+
process.cwd();
|
|
75
|
+
return join(resolve(root), "state", "rate-limits");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Canonical breaker key. Accepts either:
|
|
80
|
+
* keyFor("anthropic") → "anthropic"
|
|
81
|
+
* keyFor("anthropic", "sonnet") → "anthropic:sonnet"
|
|
82
|
+
* and sanitizes both segments the same way rate-guard sanitizes providers.
|
|
83
|
+
*
|
|
84
|
+
* @param {string} backend
|
|
85
|
+
* @param {string} [model]
|
|
86
|
+
* @returns {string}
|
|
87
|
+
*/
|
|
88
|
+
export function keyFor(backend, model) {
|
|
89
|
+
const b = sanitizeProvider(backend);
|
|
90
|
+
if (model == null || model === "") return b;
|
|
91
|
+
// sanitizeProvider lowercases + strips unsafe chars; reuse it for the model
|
|
92
|
+
// class so the composite key stays filesystem-safe and collision-resistant.
|
|
93
|
+
const m = sanitizeProvider(model);
|
|
94
|
+
return `${b}:${m}`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** The backend segment of a key ("anthropic:sonnet" → "anthropic"). */
|
|
98
|
+
export function backendOfKey(key) {
|
|
99
|
+
const i = String(key).indexOf(":");
|
|
100
|
+
return i === -1 ? String(key) : String(key).slice(0, i);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function keyPath(key, deps) {
|
|
104
|
+
// The composite "backend:model" must stay one file; ":" is fs-legal on the
|
|
105
|
+
// mac/linux targets but we normalize to "__" to avoid surprises and to keep
|
|
106
|
+
// doctor globbing simple.
|
|
107
|
+
const safe = String(key).replace(/:/g, "__");
|
|
108
|
+
return join(resolveStateDir(deps), `breaker-${safe}.json`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
// State (de)serialization
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
|
|
115
|
+
function emptyState(key) {
|
|
116
|
+
return {
|
|
117
|
+
key,
|
|
118
|
+
openedAt: 0,
|
|
119
|
+
cooldownUntil: 0,
|
|
120
|
+
reason: null,
|
|
121
|
+
strikes: 0,
|
|
122
|
+
lastProbeAt: 0,
|
|
123
|
+
halfOpen: false,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function numOr0(v) {
|
|
128
|
+
const n = Number(v);
|
|
129
|
+
return Number.isFinite(n) ? n : 0;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// JSON cannot represent Infinity (JSON.stringify emits null), but an
|
|
133
|
+
// operator-held breaker (auth_permanent / model_not_found) MUST persist an
|
|
134
|
+
// open-until-operator cooldown. Round-trip it through a string sentinel so a
|
|
135
|
+
// reboot doesn't silently re-open a permanently-cooled backend.
|
|
136
|
+
const INF_SENTINEL = "never";
|
|
137
|
+
|
|
138
|
+
/** Parse a persisted cooldownUntil, honoring the Infinity sentinel. */
|
|
139
|
+
function parseCooldown(v) {
|
|
140
|
+
if (v === INF_SENTINEL || v === "Infinity") return Infinity;
|
|
141
|
+
return numOr0(v);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Serialize a state for disk, encoding Infinity as the sentinel string. */
|
|
145
|
+
function serializeState(state) {
|
|
146
|
+
return {
|
|
147
|
+
...state,
|
|
148
|
+
cooldownUntil: state.cooldownUntil === Infinity ? INF_SENTINEL : state.cooldownUntil,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Read a breaker's state. Missing/corrupt → closed (empty) state. Never throws.
|
|
154
|
+
*
|
|
155
|
+
* @param {string} key
|
|
156
|
+
* @param {object} [deps]
|
|
157
|
+
* @returns {{key,openedAt,cooldownUntil,reason,strikes,lastProbeAt,halfOpen}}
|
|
158
|
+
*/
|
|
159
|
+
export function readBreaker(key, deps) {
|
|
160
|
+
const p = keyPath(key, deps);
|
|
161
|
+
if (!existsSync(p)) return emptyState(key);
|
|
162
|
+
try {
|
|
163
|
+
const raw = JSON.parse(readFileSync(p, "utf-8"));
|
|
164
|
+
return {
|
|
165
|
+
key,
|
|
166
|
+
openedAt: numOr0(raw.openedAt),
|
|
167
|
+
cooldownUntil: parseCooldown(raw.cooldownUntil),
|
|
168
|
+
reason: typeof raw.reason === "string" ? raw.reason : null,
|
|
169
|
+
strikes: numOr0(raw.strikes),
|
|
170
|
+
lastProbeAt: numOr0(raw.lastProbeAt),
|
|
171
|
+
halfOpen: raw.halfOpen === true,
|
|
172
|
+
};
|
|
173
|
+
} catch {
|
|
174
|
+
return emptyState(key);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ---------------------------------------------------------------------------
|
|
179
|
+
// O_EXCL lock — the fix for the unlocked read-modify-write race (M4).
|
|
180
|
+
//
|
|
181
|
+
// withLock(key, deps, mutator) acquires `${file}.lock` via openSync(..,"wx")
|
|
182
|
+
// (O_CREAT|O_EXCL), runs mutator(currentState) → nextState under the lock,
|
|
183
|
+
// atomically writes nextState, releases the lock. Two concurrent opens on the
|
|
184
|
+
// same key serialize: the loser spins on EEXIST until the winner releases,
|
|
185
|
+
// then re-reads the now-updated state and applies its own mutation on top.
|
|
186
|
+
// Result: monotonic `strikes`, no clobbered cooldownUntil.
|
|
187
|
+
// ---------------------------------------------------------------------------
|
|
188
|
+
|
|
189
|
+
function acquireLock(lockPath, deps) {
|
|
190
|
+
const now = clock(deps);
|
|
191
|
+
const deadline = now() + LOCK_TIMEOUT_MS;
|
|
192
|
+
// Synchronous spin — these critical sections are sub-millisecond writes.
|
|
193
|
+
for (;;) {
|
|
194
|
+
try {
|
|
195
|
+
const fd = openSync(lockPath, "wx"); // O_CREAT | O_EXCL | O_WRONLY
|
|
196
|
+
return fd;
|
|
197
|
+
} catch (err) {
|
|
198
|
+
if (err && err.code === "EEXIST") {
|
|
199
|
+
// Stale-lock break: if the lockfile is older than the timeout window,
|
|
200
|
+
// assume the holder died and steal it. Best-effort; never throw.
|
|
201
|
+
try {
|
|
202
|
+
const raw = readFileSync(lockPath, "utf-8");
|
|
203
|
+
const heldAt = Number(JSON.parse(raw).at);
|
|
204
|
+
if (Number.isFinite(heldAt) && now() - heldAt > LOCK_TIMEOUT_MS) {
|
|
205
|
+
try { unlinkSync(lockPath); } catch { /* */ }
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
} catch { /* unreadable lock — fall through to spin/timeout */ }
|
|
209
|
+
if (now() >= deadline) return null; // give up; caller degrades
|
|
210
|
+
busyWaitMs(LOCK_SPIN_MS);
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
// Any other error (e.g. ENOENT on a missing dir) — let caller mkdir+retry
|
|
214
|
+
return undefined;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function busyWaitMs(ms) {
|
|
220
|
+
const end = Date.now() + ms;
|
|
221
|
+
while (Date.now() < end) { /* spin */ }
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Run `mutator(currentState) → nextState` under an exclusive per-key lock and
|
|
226
|
+
* atomically persist the result. Returns the persisted next state. On any
|
|
227
|
+
* unrecoverable I/O failure, applies the mutation in-memory and returns it
|
|
228
|
+
* (fail-open: a breaker we can't persist degrades to in-memory, never blocks).
|
|
229
|
+
*/
|
|
230
|
+
function withLock(key, deps, mutator) {
|
|
231
|
+
const file = keyPath(key, deps);
|
|
232
|
+
const lockPath = `${file}.lock`;
|
|
233
|
+
const now = clock(deps);
|
|
234
|
+
try {
|
|
235
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
236
|
+
} catch { /* */ }
|
|
237
|
+
|
|
238
|
+
let fd = acquireLock(lockPath, deps);
|
|
239
|
+
if (fd === undefined) {
|
|
240
|
+
// Dir may not have existed; retry once after mkdir.
|
|
241
|
+
try { mkdirSync(dirname(file), { recursive: true }); } catch { /* */ }
|
|
242
|
+
fd = acquireLock(lockPath, deps);
|
|
243
|
+
}
|
|
244
|
+
if (fd == null) {
|
|
245
|
+
// Couldn't lock — degrade to a best-effort unlocked write so we still
|
|
246
|
+
// record the breaker (better a slightly racy file than no breaker at all).
|
|
247
|
+
const cur = readBreaker(key, deps);
|
|
248
|
+
const next = mutator(cur) || cur;
|
|
249
|
+
bestEffortWrite(file, next, deps);
|
|
250
|
+
return next;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
try {
|
|
254
|
+
// Stamp the lockfile so a crashed holder can be detected + stolen.
|
|
255
|
+
try { writeFileSync(fd, JSON.stringify({ at: now(), pid: process.pid })); } catch { /* */ }
|
|
256
|
+
const cur = readBreaker(key, deps);
|
|
257
|
+
const next = mutator(cur) || cur;
|
|
258
|
+
bestEffortWrite(file, next, deps);
|
|
259
|
+
return next;
|
|
260
|
+
} finally {
|
|
261
|
+
try { closeSync(fd); } catch { /* */ }
|
|
262
|
+
try { unlinkSync(lockPath); } catch { /* */ }
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function bestEffortWrite(file, state, deps) {
|
|
267
|
+
try {
|
|
268
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
269
|
+
const tmp = `${file}.tmp.${process.pid}.${Date.now()}.${randomBytes(2).toString("hex")}`;
|
|
270
|
+
writeFileSync(tmp, JSON.stringify(serializeState(state), null, 2) + "\n");
|
|
271
|
+
try {
|
|
272
|
+
renameSync(tmp, file);
|
|
273
|
+
} catch (err) {
|
|
274
|
+
try { unlinkSync(tmp); } catch { /* */ }
|
|
275
|
+
throw err;
|
|
276
|
+
}
|
|
277
|
+
} catch {
|
|
278
|
+
/* fail-open: in-memory only */
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// ---------------------------------------------------------------------------
|
|
283
|
+
// Public API
|
|
284
|
+
// ---------------------------------------------------------------------------
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Is the breaker for `key` currently open (blocking work) at time `now`?
|
|
288
|
+
* A breaker is open while `cooldownUntil > now`. cooldownUntil === Infinity
|
|
289
|
+
* (auth_permanent / model_not_found) means open-until-operator. Never throws.
|
|
290
|
+
*
|
|
291
|
+
* @param {string} key
|
|
292
|
+
* @param {number} [now]
|
|
293
|
+
* @param {object} [deps]
|
|
294
|
+
* @returns {{open:boolean, until:number, reason:string|null, strikes:number}}
|
|
295
|
+
*/
|
|
296
|
+
export function isOpen(key, now, deps) {
|
|
297
|
+
const t = Number.isFinite(now) ? now : clock(deps)();
|
|
298
|
+
const s = readBreaker(key, deps);
|
|
299
|
+
const open = s.cooldownUntil > t;
|
|
300
|
+
return { open, until: s.cooldownUntil, reason: s.reason, strikes: s.strikes };
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Open (or advance) the breaker for `key` with `reason`. If `cooldownMs` is
|
|
305
|
+
* omitted, it is derived from the disposition table using the post-increment
|
|
306
|
+
* strike count (so the stepped rate_limit ladder advances on each consecutive
|
|
307
|
+
* hit). cooldownMs === Infinity ⇒ cooldownUntil = Infinity (operator-cleared).
|
|
308
|
+
*
|
|
309
|
+
* Locked read-modify-write: concurrent opens converge — `strikes` is the prior
|
|
310
|
+
* value + 1 (monotonic), and `cooldownUntil` is the max of the existing and the
|
|
311
|
+
* newly computed window so a longer cooldown is never shortened by a racing
|
|
312
|
+
* shorter one.
|
|
313
|
+
*
|
|
314
|
+
* @param {string} key
|
|
315
|
+
* @param {string} reason FailoverReason
|
|
316
|
+
* @param {number|null} [cooldownMs] explicit ms; null/undefined → from table
|
|
317
|
+
* @param {number} [now]
|
|
318
|
+
* @param {object} [deps]
|
|
319
|
+
* @returns {{key,openedAt,cooldownUntil,reason,strikes,...}}
|
|
320
|
+
*/
|
|
321
|
+
export function open(key, reason, cooldownMs, now, deps) {
|
|
322
|
+
const t = Number.isFinite(now) ? now : clock(deps)();
|
|
323
|
+
return withLock(key, deps, (cur) => {
|
|
324
|
+
const strikes = numOr0(cur.strikes) + 1;
|
|
325
|
+
let ms = cooldownMs;
|
|
326
|
+
if (ms == null) {
|
|
327
|
+
// strike-1 indexes the ladder from 0 (first hit → 30s).
|
|
328
|
+
ms = cooldownMsFor(reason, strikes - 1);
|
|
329
|
+
}
|
|
330
|
+
const until = ms === Infinity ? Infinity : t + Math.max(0, ms);
|
|
331
|
+
const cooldownUntil =
|
|
332
|
+
cur.cooldownUntil === Infinity
|
|
333
|
+
? Infinity
|
|
334
|
+
: Math.max(numOr0(cur.cooldownUntil), until);
|
|
335
|
+
return {
|
|
336
|
+
key,
|
|
337
|
+
openedAt: cur.openedAt > 0 && cur.cooldownUntil > t ? cur.openedAt : t,
|
|
338
|
+
cooldownUntil,
|
|
339
|
+
reason,
|
|
340
|
+
strikes,
|
|
341
|
+
lastProbeAt: numOr0(cur.lastProbeAt),
|
|
342
|
+
halfOpen: false, // a fresh open clears any half-open probe state
|
|
343
|
+
};
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Record a clean success for `key` — closes the breaker and resets strikes so
|
|
349
|
+
* the next incident starts from the base step of the ladder. Idempotent: a
|
|
350
|
+
* no-op (no write) when already closed and unstruck, to avoid churning the file
|
|
351
|
+
* on every successful spawn.
|
|
352
|
+
*
|
|
353
|
+
* @returns {object} the (closed) state
|
|
354
|
+
*/
|
|
355
|
+
export function close(key, now, deps) {
|
|
356
|
+
const cur = readBreaker(key, deps);
|
|
357
|
+
if (cur.cooldownUntil === 0 && cur.strikes === 0 && cur.openedAt === 0 && !cur.halfOpen) {
|
|
358
|
+
return cur; // already closed — don't churn
|
|
359
|
+
}
|
|
360
|
+
return withLock(key, deps, () => emptyState(key));
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/** Alias matching the rate-guard vocabulary. */
|
|
364
|
+
export const recordSuccess = close;
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Operator clear — force a breaker closed regardless of cooldownUntil
|
|
368
|
+
* (including auth_permanent / model_not_found Infinity holds). For the
|
|
369
|
+
* `until operator` and `permanent` dispositions.
|
|
370
|
+
*/
|
|
371
|
+
export function clear(key, deps) {
|
|
372
|
+
return withLock(key, deps, () => emptyState(key));
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Half-open probe gate. Returns true (and stamps `lastProbeAt`) iff a single
|
|
377
|
+
* canary probe is permitted right now for `key`'s backend:
|
|
378
|
+
* - the breaker is currently open, AND
|
|
379
|
+
* - we are within HALF_OPEN_WINDOW_MS of cooldown expiry (so a probe is
|
|
380
|
+
* worthwhile — we're about to retry anyway), AND
|
|
381
|
+
* - no probe has run for this BACKEND within HALF_OPEN_PROBE_INTERVAL_MS
|
|
382
|
+
* (1 probe/backend/30s — across all its model-class keys).
|
|
383
|
+
* Infinity cooldowns (auth_permanent / model_not_found) are never probed —
|
|
384
|
+
* they require an operator, not a canary.
|
|
385
|
+
*
|
|
386
|
+
* The stamp is written under the lock so two concurrent failover runs don't
|
|
387
|
+
* both think they "own" the probe.
|
|
388
|
+
*
|
|
389
|
+
* @param {string} key
|
|
390
|
+
* @param {number} [now]
|
|
391
|
+
* @param {object} [deps]
|
|
392
|
+
* @returns {boolean} true ⇒ caller may run exactly one probe
|
|
393
|
+
*/
|
|
394
|
+
export function halfOpenProbe(key, now, deps) {
|
|
395
|
+
const t = Number.isFinite(now) ? now : clock(deps)();
|
|
396
|
+
const s = readBreaker(key, deps);
|
|
397
|
+
// Closed, or not within the pre-expiry window, or operator-held → no probe.
|
|
398
|
+
if (s.cooldownUntil <= t) return false; // already closed; just spawn normally
|
|
399
|
+
if (s.cooldownUntil === Infinity) return false; // operator-held
|
|
400
|
+
if (s.cooldownUntil - t > HALF_OPEN_WINDOW_MS) return false; // too early
|
|
401
|
+
|
|
402
|
+
// Backend-level probe rate limit: scan this key's own lastProbeAt (and, for a
|
|
403
|
+
// backend:model key, the backend key too) so it's truly 1/backend/30s.
|
|
404
|
+
const backend = backendOfKey(key);
|
|
405
|
+
const recent = (st) => st.lastProbeAt && t - st.lastProbeAt < HALF_OPEN_PROBE_INTERVAL_MS;
|
|
406
|
+
if (recent(s)) return false;
|
|
407
|
+
if (key !== backend) {
|
|
408
|
+
const backendState = readBreaker(backend, deps);
|
|
409
|
+
if (recent(backendState)) return false;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Stamp under the lock and mark half-open.
|
|
413
|
+
withLock(key, deps, (cur) => ({
|
|
414
|
+
...cur,
|
|
415
|
+
key,
|
|
416
|
+
lastProbeAt: t,
|
|
417
|
+
halfOpen: true,
|
|
418
|
+
}));
|
|
419
|
+
return true;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Snapshot every breaker file under the state dir (for `maestro doctor`'s
|
|
424
|
+
* breaker dump). Best-effort, never throws; returns [] when the dir is absent.
|
|
425
|
+
*/
|
|
426
|
+
export function listBreakers(now, deps) {
|
|
427
|
+
const dir = resolveStateDir(deps);
|
|
428
|
+
if (!existsSync(dir)) return [];
|
|
429
|
+
let names = [];
|
|
430
|
+
try {
|
|
431
|
+
names = readdirSync(dir);
|
|
432
|
+
} catch {
|
|
433
|
+
return [];
|
|
434
|
+
}
|
|
435
|
+
const t = Number.isFinite(now) ? now : clock(deps)();
|
|
436
|
+
const out = [];
|
|
437
|
+
for (const name of names) {
|
|
438
|
+
if (!name.startsWith("breaker-") || !name.endsWith(".json")) continue;
|
|
439
|
+
const key = name.slice("breaker-".length, -".json".length).replace(/__/g, ":");
|
|
440
|
+
const s = readBreaker(key, deps);
|
|
441
|
+
out.push({ ...s, open: s.cooldownUntil > t });
|
|
442
|
+
}
|
|
443
|
+
return out;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
export const _internals = {
|
|
447
|
+
emptyState,
|
|
448
|
+
keyPath,
|
|
449
|
+
withLock,
|
|
450
|
+
HALF_OPEN_PROBE_INTERVAL_MS,
|
|
451
|
+
HALF_OPEN_WINDOW_MS,
|
|
452
|
+
DISPOSITION,
|
|
453
|
+
};
|