@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,297 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/resource-governor.mjs — one admission gate before every spawn (WS4).
|
|
3
|
+
*
|
|
4
|
+
* `admit({source, priority, mode}, deps?)` decides, deterministically given
|
|
5
|
+
* its inputs, whether a new `claude --print` sub-session may launch:
|
|
6
|
+
*
|
|
7
|
+
* ADMIT — launch now.
|
|
8
|
+
* QUEUE — keep the work in the in-memory dispatcher queue (re-derivable
|
|
9
|
+
* from durable state/queues/*.yaml); it spawns when a slot frees.
|
|
10
|
+
* DEFER — leave the work on disk untouched (backlog: the 10-min sweep
|
|
11
|
+
* retries; cadence: requeueTick). DEFER must NEVER burn retry
|
|
12
|
+
* budget (the callers honour this — they do not call failTick on a
|
|
13
|
+
* governor deferral).
|
|
14
|
+
*
|
|
15
|
+
* The whole point (Invariants): under pressure work is QUEUED or DEFERRED —
|
|
16
|
+
* never dropped — and the agent throttles instead of bricking.
|
|
17
|
+
*
|
|
18
|
+
* Dynamic ceiling (replaces the hardcoded MAX_CONCURRENT=10):
|
|
19
|
+
* dynamicMax = clamp(floor(totalmem * 0.60 / 750MB), 1, min(10, cpus))
|
|
20
|
+
* effectiveMax = min(dynamicMax, throttleCeiling)
|
|
21
|
+
* where throttleCeiling comes from state/throttle.json (written by the memory
|
|
22
|
+
* watchdog's SOFT tier; auto-expiring). The governor only READS that file —
|
|
23
|
+
* it never writes it.
|
|
24
|
+
*
|
|
25
|
+
* Thresholds (env GOV_* overridable; tests inject `deps` instead):
|
|
26
|
+
* liveCount >= effectiveMax → QUEUE
|
|
27
|
+
* freemem < totalmem*0.15 OR < 2*750MB headroom → DEFER
|
|
28
|
+
* loadavg1 > cpus*1.5 → DEFER
|
|
29
|
+
* liveRSS + 750MB > 0.60*totalmem → QUEUE
|
|
30
|
+
* else → ADMIT
|
|
31
|
+
*
|
|
32
|
+
* Plus a budget `mode`: when essential-only (budget at 100%),
|
|
33
|
+
* source==="backlog"/"cadence" → DEFER; inbox → still goes through the normal
|
|
34
|
+
* ADMIT/QUEUE path (never go dark on the user).
|
|
35
|
+
*
|
|
36
|
+
* Purity: `admit` is a pure function of its injected `deps` snapshot. The
|
|
37
|
+
* default `deps` reads real os.* + a 2s-cached `liveClaudeStats()` (via ps) +
|
|
38
|
+
* state/throttle.json, but tests pass an explicit `deps` and get fully
|
|
39
|
+
* deterministic decisions.
|
|
40
|
+
*
|
|
41
|
+
* Constraints (CLAUDE.md): ESM, execFile (not exec), Node built-ins only.
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
import os from "node:os";
|
|
45
|
+
import { execFileSync } from "node:child_process";
|
|
46
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
47
|
+
import { join, resolve } from "node:path";
|
|
48
|
+
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
// Constants
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
/** Per-session RAM budget heuristic (Claude Code sub-session working set). */
|
|
54
|
+
export const PER_SESSION_MB = num(process.env.GOV_PER_SESSION_MB, 750);
|
|
55
|
+
const MB = 1024 * 1024;
|
|
56
|
+
|
|
57
|
+
/** Fraction of total RAM the governor is willing to hand to claude sessions. */
|
|
58
|
+
export const RAM_FRACTION = frac(process.env.GOV_RAM_FRACTION, 0.6);
|
|
59
|
+
/** Free-memory floor as a fraction of total RAM. */
|
|
60
|
+
export const FREEMEM_FLOOR_FRACTION = frac(process.env.GOV_FREEMEM_FLOOR, 0.15);
|
|
61
|
+
/** Load-average multiplier over cpu count that triggers a DEFER. */
|
|
62
|
+
export const LOAD_FACTOR = num(process.env.GOV_LOAD_FACTOR, 1.5);
|
|
63
|
+
/** Hard ceiling on dynamicMax regardless of RAM. */
|
|
64
|
+
export const HARD_MAX = num(process.env.GOV_HARD_MAX, 10);
|
|
65
|
+
/** Minimum dynamicMax (always allow at least one session). */
|
|
66
|
+
const MIN_MAX = 1;
|
|
67
|
+
|
|
68
|
+
function num(v, dflt) {
|
|
69
|
+
const n = Number(v);
|
|
70
|
+
return Number.isFinite(n) && n > 0 ? n : dflt;
|
|
71
|
+
}
|
|
72
|
+
function frac(v, dflt) {
|
|
73
|
+
const n = Number(v);
|
|
74
|
+
return Number.isFinite(n) && n > 0 && n < 1 ? n : dflt;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
// throttle.json (written by the watchdog SOFT tier; we only read it)
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
function throttlePath(deps) {
|
|
82
|
+
if (deps && deps.throttlePath) return resolve(deps.throttlePath);
|
|
83
|
+
const root =
|
|
84
|
+
(deps && deps.agentRoot) ||
|
|
85
|
+
process.env.AGENT_ROOT ||
|
|
86
|
+
process.env.AGENT_DIR ||
|
|
87
|
+
process.cwd();
|
|
88
|
+
return join(resolve(root), "state", "throttle.json");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Read the soft-throttle ceiling. Returns Infinity when no (unexpired)
|
|
93
|
+
* throttle is in force, so `min(dynamicMax, ceiling)` is a no-op by default.
|
|
94
|
+
* Never throws.
|
|
95
|
+
*
|
|
96
|
+
* @returns {{ ceiling:number, reason?:string, expiresAt?:number }}
|
|
97
|
+
*/
|
|
98
|
+
export function readThrottle(deps) {
|
|
99
|
+
const now = (deps && typeof deps.now === "function" ? deps.now : Date.now)();
|
|
100
|
+
const p = throttlePath(deps);
|
|
101
|
+
if (!existsSync(p)) return { ceiling: Infinity };
|
|
102
|
+
try {
|
|
103
|
+
const raw = JSON.parse(readFileSync(p, "utf-8"));
|
|
104
|
+
const expiresAt = Number(raw.expires_at ?? raw.expiresAt);
|
|
105
|
+
// Auto-expire: a stale throttle file (watchdog died before clearing it)
|
|
106
|
+
// must not pin the ceiling forever.
|
|
107
|
+
if (Number.isFinite(expiresAt) && now >= expiresAt) return { ceiling: Infinity };
|
|
108
|
+
const ceiling = Number(raw.ceiling);
|
|
109
|
+
if (!Number.isFinite(ceiling) || ceiling < 0) return { ceiling: Infinity };
|
|
110
|
+
return { ceiling, reason: raw.reason, expiresAt: Number.isFinite(expiresAt) ? expiresAt : undefined };
|
|
111
|
+
} catch {
|
|
112
|
+
return { ceiling: Infinity };
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
// Live claude process stats (cached 2s) via ps
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
|
|
120
|
+
let _liveCache = { at: 0, value: null };
|
|
121
|
+
const LIVE_CACHE_MS = 2_000;
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Count live `claude` CLI processes + their total RSS (MB) via `ps`. Cached
|
|
125
|
+
* for 2s so a burst of admit() calls doesn't fork ps repeatedly. Best-effort:
|
|
126
|
+
* on any failure returns { count:0, rssMB:0 } (fail-open-for-work).
|
|
127
|
+
*
|
|
128
|
+
* @returns {{ count:number, rssMB:number }}
|
|
129
|
+
*/
|
|
130
|
+
export function liveClaudeStats(opts = {}) {
|
|
131
|
+
const now = opts.now || Date.now;
|
|
132
|
+
const t = now();
|
|
133
|
+
if (!opts.force && _liveCache.value && t - _liveCache.at < LIVE_CACHE_MS) {
|
|
134
|
+
return _liveCache.value;
|
|
135
|
+
}
|
|
136
|
+
let value = { count: 0, rssMB: 0 };
|
|
137
|
+
try {
|
|
138
|
+
// pid, rss(KB), comm — mirror the watchdog's filter set so the governor's
|
|
139
|
+
// view of "live claude" matches what the watchdog would act on.
|
|
140
|
+
const out = execFileSync("/bin/ps", ["-eo", "pid,rss,comm"], { encoding: "utf-8", timeout: 4000 });
|
|
141
|
+
let count = 0;
|
|
142
|
+
let rssKb = 0;
|
|
143
|
+
for (const line of out.split("\n")) {
|
|
144
|
+
const l = line.trim();
|
|
145
|
+
if (!l) continue;
|
|
146
|
+
if (!/claude/i.test(l)) continue;
|
|
147
|
+
if (/Claude\.app|Claude Helper|watchdog|grep|ps -eo/.test(l)) continue;
|
|
148
|
+
const parts = l.split(/\s+/);
|
|
149
|
+
const rss = Number(parts[1]);
|
|
150
|
+
if (Number.isFinite(rss)) rssKb += rss;
|
|
151
|
+
count += 1;
|
|
152
|
+
}
|
|
153
|
+
value = { count, rssMB: Math.round(rssKb / 1024) };
|
|
154
|
+
} catch {
|
|
155
|
+
value = { count: 0, rssMB: 0 };
|
|
156
|
+
}
|
|
157
|
+
_liveCache = { at: t, value };
|
|
158
|
+
return value;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Reset the live-stats cache (tests). */
|
|
162
|
+
export function _resetLiveCache() { _liveCache = { at: 0, value: null }; }
|
|
163
|
+
|
|
164
|
+
// ---------------------------------------------------------------------------
|
|
165
|
+
// Default deps snapshot
|
|
166
|
+
// ---------------------------------------------------------------------------
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Build the real-world deps snapshot the governor reasons over. Tests bypass
|
|
170
|
+
* this entirely by passing their own `deps`.
|
|
171
|
+
*/
|
|
172
|
+
export function defaultDeps(extra = {}) {
|
|
173
|
+
const live = liveClaudeStats();
|
|
174
|
+
const throttle = readThrottle(extra);
|
|
175
|
+
return {
|
|
176
|
+
freemem: os.freemem(),
|
|
177
|
+
totalmem: os.totalmem(),
|
|
178
|
+
loadavg: os.loadavg(),
|
|
179
|
+
cpus: os.cpus().length || 1,
|
|
180
|
+
liveClaude: live,
|
|
181
|
+
throttleCeiling: throttle.ceiling,
|
|
182
|
+
throttleReason: throttle.reason,
|
|
183
|
+
...extra,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ---------------------------------------------------------------------------
|
|
188
|
+
// Dynamic ceiling
|
|
189
|
+
// ---------------------------------------------------------------------------
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* dynamicMax = clamp(floor(totalmem*RAM_FRACTION / PER_SESSION_MB), 1, min(HARD_MAX, cpus))
|
|
193
|
+
*/
|
|
194
|
+
export function dynamicMax(deps) {
|
|
195
|
+
const totalmem = numField(deps && deps.totalmem, os.totalmem());
|
|
196
|
+
const cpus = Math.max(1, numField(deps && deps.cpus, os.cpus().length || 1));
|
|
197
|
+
const byRam = Math.floor((totalmem * RAM_FRACTION) / (PER_SESSION_MB * MB));
|
|
198
|
+
const upper = Math.min(HARD_MAX, cpus);
|
|
199
|
+
return clamp(byRam, MIN_MAX, Math.max(MIN_MAX, upper));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function clamp(v, lo, hi) { return Math.min(hi, Math.max(lo, v)); }
|
|
203
|
+
function numField(v, dflt) { const n = Number(v); return Number.isFinite(n) ? n : dflt; }
|
|
204
|
+
|
|
205
|
+
// ---------------------------------------------------------------------------
|
|
206
|
+
// admit()
|
|
207
|
+
// ---------------------------------------------------------------------------
|
|
208
|
+
|
|
209
|
+
const ADMIT = "ADMIT";
|
|
210
|
+
const QUEUE = "QUEUE";
|
|
211
|
+
const DEFER = "DEFER";
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* The admission decision.
|
|
215
|
+
*
|
|
216
|
+
* @param {object} req { source, priority, mode }
|
|
217
|
+
* source: "inbox" | "backlog" | "cadence" (default "backlog")
|
|
218
|
+
* priority: "critical"|"high"|"normal"|"low" (default "normal")
|
|
219
|
+
* mode: "essential-only" flips budget gating (else normal)
|
|
220
|
+
* @param {object} [deps] injected snapshot; defaults to defaultDeps()
|
|
221
|
+
* { freemem, totalmem, loadavg, cpus, liveClaude:{count,rssMB}, throttleCeiling }
|
|
222
|
+
* @returns {{ decision:"ADMIT"|"QUEUE"|"DEFER", reason:string, snapshot:object }}
|
|
223
|
+
*/
|
|
224
|
+
export function admit(req = {}, deps) {
|
|
225
|
+
const d = deps || defaultDeps();
|
|
226
|
+
const source = req.source || "backlog";
|
|
227
|
+
const mode = req.mode || (req.essentialOnly ? "essential-only" : null);
|
|
228
|
+
|
|
229
|
+
const totalmem = numField(d.totalmem, os.totalmem());
|
|
230
|
+
const freemem = numField(d.freemem, os.freemem());
|
|
231
|
+
const cpus = Math.max(1, numField(d.cpus, os.cpus().length || 1));
|
|
232
|
+
const load1 = Array.isArray(d.loadavg) ? numField(d.loadavg[0], 0) : numField(d.loadavg, 0);
|
|
233
|
+
const live = d.liveClaude || { count: 0, rssMB: 0 };
|
|
234
|
+
const liveCount = numField(live.count, 0);
|
|
235
|
+
const liveRssMB = numField(live.rssMB, 0);
|
|
236
|
+
const throttleCeiling = numField(d.throttleCeiling, Infinity);
|
|
237
|
+
|
|
238
|
+
const dMax = dynamicMax(d);
|
|
239
|
+
const effectiveMax = Math.max(1, Math.min(dMax, throttleCeiling));
|
|
240
|
+
|
|
241
|
+
const snapshot = {
|
|
242
|
+
source,
|
|
243
|
+
mode: mode || "normal",
|
|
244
|
+
dynamicMax: dMax,
|
|
245
|
+
effectiveMax,
|
|
246
|
+
throttleCeiling: throttleCeiling === Infinity ? null : throttleCeiling,
|
|
247
|
+
liveCount,
|
|
248
|
+
liveRssMB,
|
|
249
|
+
freememMB: Math.round(freemem / MB),
|
|
250
|
+
totalmemMB: Math.round(totalmem / MB),
|
|
251
|
+
load1,
|
|
252
|
+
cpus,
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
const decide = (decision, reason) => ({ decision, reason, snapshot });
|
|
256
|
+
|
|
257
|
+
// (0) Budget essential-only: non-inbox work is deferred entirely. Inbox
|
|
258
|
+
// falls through to the normal pressure gating below (we keep replying).
|
|
259
|
+
if (mode === "essential-only" && (source === "backlog" || source === "cadence")) {
|
|
260
|
+
return decide(DEFER, "essential-only: daily budget cap reached; deferring non-inbox work");
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// (1) Concurrency ceiling → QUEUE (work re-derivable from the queue).
|
|
264
|
+
if (liveCount >= effectiveMax) {
|
|
265
|
+
const why = throttleCeiling < dMax
|
|
266
|
+
? `at throttled ceiling (${liveCount}/${effectiveMax}; soft-throttle active)`
|
|
267
|
+
: `at concurrency ceiling (${liveCount}/${effectiveMax})`;
|
|
268
|
+
return decide(QUEUE, why);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// (2) Free-memory floor → DEFER. Two guards: a percentage floor AND an
|
|
272
|
+
// absolute headroom of at least 2 sessions' worth of RAM.
|
|
273
|
+
const freememFloor = totalmem * FREEMEM_FLOOR_FRACTION;
|
|
274
|
+
const headroomBytes = 2 * PER_SESSION_MB * MB;
|
|
275
|
+
if (freemem < freememFloor || freemem < headroomBytes) {
|
|
276
|
+
return decide(DEFER, `low free memory (${snapshot.freememMB}MB free; need >=${Math.round(Math.max(freememFloor, headroomBytes) / MB)}MB)`);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// (3) Load average → DEFER. A box already thrashing shouldn't take more on.
|
|
280
|
+
if (load1 > cpus * LOAD_FACTOR) {
|
|
281
|
+
return decide(DEFER, `load too high (1m load ${load1.toFixed(2)} > ${(cpus * LOAD_FACTOR).toFixed(1)} = cpus*${LOAD_FACTOR})`);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// (4) Projected RSS → QUEUE. If admitting one more session would push the
|
|
285
|
+
// claude working set past the RAM fraction, wait for a slot rather than
|
|
286
|
+
// risk an OOM kill.
|
|
287
|
+
const projectedMB = liveRssMB + PER_SESSION_MB;
|
|
288
|
+
const rssCapMB = (totalmem * RAM_FRACTION) / MB;
|
|
289
|
+
if (projectedMB > rssCapMB) {
|
|
290
|
+
return decide(QUEUE, `projected RSS ${projectedMB}MB would exceed ${Math.round(rssCapMB)}MB (${Math.round(RAM_FRACTION * 100)}% of RAM)`);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// (5) Healthy.
|
|
294
|
+
return decide(ADMIT, "resources healthy");
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export const DECISIONS = { ADMIT, QUEUE, DEFER };
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* resource-governor.test.mjs — node:test coverage for the admission gate.
|
|
3
|
+
*
|
|
4
|
+
* Every test injects a full `deps` snapshot so admit() is a pure, deterministic
|
|
5
|
+
* function — no real os.* reads, no ps fork. We exercise each decision branch
|
|
6
|
+
* (ADMIT / QUEUE / DEFER), the dynamicMax sizing across 8/16/32GB boxes, the
|
|
7
|
+
* throttle-ceiling clamp, and essential-only mode.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { test } from "node:test";
|
|
11
|
+
import assert from "node:assert/strict";
|
|
12
|
+
import { promises as fsp } from "node:fs";
|
|
13
|
+
import { writeFileSync } from "node:fs";
|
|
14
|
+
import { tmpdir } from "node:os";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
admit,
|
|
19
|
+
dynamicMax,
|
|
20
|
+
readThrottle,
|
|
21
|
+
PER_SESSION_MB,
|
|
22
|
+
RAM_FRACTION,
|
|
23
|
+
DECISIONS,
|
|
24
|
+
} from "./resource-governor.mjs";
|
|
25
|
+
|
|
26
|
+
const GB = 1024 * 1024 * 1024;
|
|
27
|
+
const MB = 1024 * 1024;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Build a healthy baseline deps snapshot for an N-GB / C-cpu box with `live`
|
|
31
|
+
* sessions already running and `liveRssMB` total. Override any field.
|
|
32
|
+
*/
|
|
33
|
+
function deps(over = {}) {
|
|
34
|
+
const totalmem = over.totalmem ?? 16 * GB;
|
|
35
|
+
return {
|
|
36
|
+
totalmem,
|
|
37
|
+
freemem: over.freemem ?? totalmem * 0.5, // plenty free
|
|
38
|
+
loadavg: over.loadavg ?? [0.5, 0.5, 0.5],
|
|
39
|
+
cpus: over.cpus ?? 8,
|
|
40
|
+
liveClaude: over.liveClaude ?? { count: 0, rssMB: 0 },
|
|
41
|
+
throttleCeiling: over.throttleCeiling ?? Infinity,
|
|
42
|
+
...over,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// dynamicMax sizing
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
test("dynamicMax sizes by RAM, clamped to min(HARD_MAX, cpus)", () => {
|
|
51
|
+
// 8GB: floor(8*0.6/0.75) = floor(6.4) = 6, capped by cpus too.
|
|
52
|
+
assert.equal(dynamicMax({ totalmem: 8 * GB, cpus: 8 }), 6);
|
|
53
|
+
// 16GB: floor(16*0.6/0.75)=12 → clamp to min(10, cpus). cpus=8 → 8.
|
|
54
|
+
assert.equal(dynamicMax({ totalmem: 16 * GB, cpus: 8 }), 8);
|
|
55
|
+
// 16GB with 16 cpus → min(10,16)=10, byRam=12 → clamp to 10.
|
|
56
|
+
assert.equal(dynamicMax({ totalmem: 16 * GB, cpus: 16 }), 10);
|
|
57
|
+
// 32GB with 16 cpus: byRam=floor(32*0.6/0.75)=25 → clamp to min(10,16)=10.
|
|
58
|
+
assert.equal(dynamicMax({ totalmem: 32 * GB, cpus: 16 }), 10);
|
|
59
|
+
// Tiny box always yields at least 1.
|
|
60
|
+
assert.equal(dynamicMax({ totalmem: 1 * GB, cpus: 1 }), 1);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("dynamicMax never returns 0 even on a starved box", () => {
|
|
64
|
+
assert.ok(dynamicMax({ totalmem: 512 * MB, cpus: 2 }) >= 1);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
// ADMIT
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
test("admit: healthy box with spare capacity → ADMIT", () => {
|
|
72
|
+
const r = admit({ source: "backlog" }, deps());
|
|
73
|
+
assert.equal(r.decision, DECISIONS.ADMIT);
|
|
74
|
+
assert.equal(r.snapshot.dynamicMax, 8);
|
|
75
|
+
assert.equal(r.snapshot.effectiveMax, 8);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("admit: inbox on a healthy box → ADMIT", () => {
|
|
79
|
+
const r = admit({ source: "inbox", priority: "high" }, deps());
|
|
80
|
+
assert.equal(r.decision, DECISIONS.ADMIT);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
// QUEUE — concurrency ceiling
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
test("admit: at the concurrency ceiling → QUEUE", () => {
|
|
88
|
+
// effectiveMax = 8; 8 already live → QUEUE.
|
|
89
|
+
const r = admit({ source: "backlog" }, deps({ liveClaude: { count: 8, rssMB: 2000 } }));
|
|
90
|
+
assert.equal(r.decision, DECISIONS.QUEUE);
|
|
91
|
+
assert.match(r.reason, /concurrency ceiling/);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("admit: throttle ceiling clamps effectiveMax below dynamicMax → QUEUE earlier", () => {
|
|
95
|
+
// dynamicMax 8 but soft-throttle pinned the ceiling to 3; 3 live → QUEUE.
|
|
96
|
+
const r = admit({ source: "backlog" }, deps({ throttleCeiling: 3, liveClaude: { count: 3, rssMB: 1000 } }));
|
|
97
|
+
assert.equal(r.decision, DECISIONS.QUEUE);
|
|
98
|
+
assert.equal(r.snapshot.effectiveMax, 3);
|
|
99
|
+
assert.match(r.reason, /soft-throttle/);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
// QUEUE — projected RSS
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
|
|
106
|
+
test("admit: projected RSS over the RAM fraction → QUEUE", () => {
|
|
107
|
+
// 16GB → RAM fraction cap = 16*0.6*1024 ≈ 9830MB. Live RSS 9500MB + 750 > cap.
|
|
108
|
+
// Keep count below ceiling and memory/load healthy so RSS is the trigger.
|
|
109
|
+
const r = admit({ source: "backlog" }, deps({
|
|
110
|
+
liveClaude: { count: 2, rssMB: 9500 },
|
|
111
|
+
freemem: 16 * GB * 0.5,
|
|
112
|
+
}));
|
|
113
|
+
assert.equal(r.decision, DECISIONS.QUEUE);
|
|
114
|
+
assert.match(r.reason, /projected RSS/);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
// DEFER — free memory
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
|
|
121
|
+
test("admit: free memory below the 15% floor → DEFER", () => {
|
|
122
|
+
const r = admit({ source: "backlog" }, deps({ freemem: 16 * GB * 0.10 }));
|
|
123
|
+
assert.equal(r.decision, DECISIONS.DEFER);
|
|
124
|
+
assert.match(r.reason, /low free memory/);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("admit: free memory below the 2-session absolute headroom → DEFER", () => {
|
|
128
|
+
// Big box where 15% is generous, but absolute free < 2*750MB = 1500MB.
|
|
129
|
+
const r = admit({ source: "backlog" }, deps({ totalmem: 64 * GB, freemem: 1000 * MB }));
|
|
130
|
+
assert.equal(r.decision, DECISIONS.DEFER);
|
|
131
|
+
assert.match(r.reason, /low free memory/);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
// DEFER — load average
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
|
|
138
|
+
test("admit: 1-minute load above cpus*1.5 → DEFER", () => {
|
|
139
|
+
// cpus=8 → threshold 12. load 13 → DEFER. Keep memory healthy.
|
|
140
|
+
const r = admit({ source: "backlog" }, deps({ loadavg: [13, 5, 5] }));
|
|
141
|
+
assert.equal(r.decision, DECISIONS.DEFER);
|
|
142
|
+
assert.match(r.reason, /load too high/);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("admit: load exactly at the threshold is still ADMIT (strict >)", () => {
|
|
146
|
+
const r = admit({ source: "backlog" }, deps({ loadavg: [12, 1, 1], cpus: 8 }));
|
|
147
|
+
assert.equal(r.decision, DECISIONS.ADMIT);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// ---------------------------------------------------------------------------
|
|
151
|
+
// essential-only mode
|
|
152
|
+
// ---------------------------------------------------------------------------
|
|
153
|
+
|
|
154
|
+
test("essential-only: backlog → DEFER without burning resources", () => {
|
|
155
|
+
const r = admit({ source: "backlog", mode: "essential-only" }, deps());
|
|
156
|
+
assert.equal(r.decision, DECISIONS.DEFER);
|
|
157
|
+
assert.match(r.reason, /essential-only/);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("essential-only: cadence → DEFER", () => {
|
|
161
|
+
const r = admit({ source: "cadence", mode: "essential-only" }, deps());
|
|
162
|
+
assert.equal(r.decision, DECISIONS.DEFER);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("essential-only: inbox still flows through normal gating (ADMIT when healthy)", () => {
|
|
166
|
+
const r = admit({ source: "inbox", mode: "essential-only" }, deps());
|
|
167
|
+
assert.equal(r.decision, DECISIONS.ADMIT, "inbox must keep replying even at budget cap");
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test("essential-only: inbox still QUEUEs (not DEFERs) under concurrency pressure", () => {
|
|
171
|
+
const r = admit({ source: "inbox", mode: "essential-only" }, deps({ liveClaude: { count: 8, rssMB: 2000 } }));
|
|
172
|
+
assert.equal(r.decision, DECISIONS.QUEUE);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// ---------------------------------------------------------------------------
|
|
176
|
+
// precedence — ceiling QUEUE beats memory DEFER? No: budget→ceiling→mem→load→rss.
|
|
177
|
+
// Verify the documented order is honoured.
|
|
178
|
+
// ---------------------------------------------------------------------------
|
|
179
|
+
|
|
180
|
+
test("decision precedence: essential-only beats everything for backlog", () => {
|
|
181
|
+
// Even with a healthy box, essential-only short-circuits to DEFER.
|
|
182
|
+
const r = admit({ source: "backlog", mode: "essential-only" }, deps({ liveClaude: { count: 0, rssMB: 0 } }));
|
|
183
|
+
assert.equal(r.decision, DECISIONS.DEFER);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test("decision precedence: concurrency QUEUE is checked before memory DEFER", () => {
|
|
187
|
+
// At ceiling AND low memory → spec orders ceiling first → QUEUE.
|
|
188
|
+
const r = admit({ source: "backlog" }, deps({
|
|
189
|
+
liveClaude: { count: 8, rssMB: 1000 },
|
|
190
|
+
freemem: 16 * GB * 0.05,
|
|
191
|
+
}));
|
|
192
|
+
assert.equal(r.decision, DECISIONS.QUEUE);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// ---------------------------------------------------------------------------
|
|
196
|
+
// readThrottle — auto-expiry
|
|
197
|
+
// ---------------------------------------------------------------------------
|
|
198
|
+
|
|
199
|
+
test("readThrottle returns Infinity when no file present", async () => {
|
|
200
|
+
const dir = join(tmpdir(), `gov-throttle-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`);
|
|
201
|
+
await fsp.mkdir(join(dir, "state"), { recursive: true });
|
|
202
|
+
try {
|
|
203
|
+
assert.equal(readThrottle({ agentRoot: dir }).ceiling, Infinity);
|
|
204
|
+
} finally { await fsp.rm(dir, { recursive: true, force: true }); }
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test("readThrottle honours ceiling while unexpired, ignores once expired", async () => {
|
|
208
|
+
const dir = join(tmpdir(), `gov-throttle2-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`);
|
|
209
|
+
await fsp.mkdir(join(dir, "state"), { recursive: true });
|
|
210
|
+
try {
|
|
211
|
+
const now = 1_000_000;
|
|
212
|
+
writeFileSync(join(dir, "state", "throttle.json"), JSON.stringify({
|
|
213
|
+
ceiling: 2, reason: "SOFT", set_at: now - 1000, expires_at: now + 60_000,
|
|
214
|
+
}));
|
|
215
|
+
// Unexpired → ceiling honoured.
|
|
216
|
+
assert.equal(readThrottle({ agentRoot: dir, now: () => now }).ceiling, 2);
|
|
217
|
+
// After expiry → ignored.
|
|
218
|
+
assert.equal(readThrottle({ agentRoot: dir, now: () => now + 120_000 }).ceiling, Infinity);
|
|
219
|
+
} finally { await fsp.rm(dir, { recursive: true, force: true }); }
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test("governor reads the EXACT throttle.json byte-shape the watchdog SOFT tier writes", async () => {
|
|
223
|
+
// This is the contract between scripts/watchdog/memory-watchdog.sh (writer)
|
|
224
|
+
// and the governor (reader). The watchdog writes ceiling + reason + EPOCH-MS
|
|
225
|
+
// set_at/expires_at. If either side drifts, throttling silently no-ops — so
|
|
226
|
+
// pin the shape here.
|
|
227
|
+
const dir = join(tmpdir(), `gov-wd-shape-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`);
|
|
228
|
+
await fsp.mkdir(join(dir, "state"), { recursive: true });
|
|
229
|
+
try {
|
|
230
|
+
const nowSec = 1_700_000_000; // arbitrary epoch seconds
|
|
231
|
+
const setMs = nowSec * 1000;
|
|
232
|
+
const expMs = (nowSec + 300) * 1000; // THROTTLE_TTL_SECONDS = 300
|
|
233
|
+
// Byte-for-byte the printf in write_throttle():
|
|
234
|
+
writeFileSync(join(dir, "state", "throttle.json"),
|
|
235
|
+
`{"ceiling":3,"reason":"soft-tier","set_at":${setMs},"expires_at":${expMs}}\n`);
|
|
236
|
+
|
|
237
|
+
// While unexpired → ceiling honoured.
|
|
238
|
+
const t1 = readThrottle({ agentRoot: dir, now: () => setMs + 1000 });
|
|
239
|
+
assert.equal(t1.ceiling, 3);
|
|
240
|
+
assert.equal(t1.reason, "soft-tier");
|
|
241
|
+
// After TTL → ignored (governor auto-expires even if the watchdog died).
|
|
242
|
+
const t2 = readThrottle({ agentRoot: dir, now: () => expMs + 1 });
|
|
243
|
+
assert.equal(t2.ceiling, Infinity);
|
|
244
|
+
} finally { await fsp.rm(dir, { recursive: true, force: true }); }
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
test("admit reads a real throttle.json from agentRoot and clamps", async () => {
|
|
248
|
+
const dir = join(tmpdir(), `gov-throttle3-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`);
|
|
249
|
+
await fsp.mkdir(join(dir, "state"), { recursive: true });
|
|
250
|
+
try {
|
|
251
|
+
const now = 2_000_000;
|
|
252
|
+
writeFileSync(join(dir, "state", "throttle.json"), JSON.stringify({
|
|
253
|
+
ceiling: 1, reason: "SOFT", expires_at: now + 60_000,
|
|
254
|
+
}));
|
|
255
|
+
// Build a deps snapshot that reads the throttle file: pass throttleCeiling
|
|
256
|
+
// from readThrottle so the integration is exercised end-to-end.
|
|
257
|
+
const t = readThrottle({ agentRoot: dir, now: () => now });
|
|
258
|
+
const r = admit({ source: "backlog" }, deps({ throttleCeiling: t.ceiling, liveClaude: { count: 1, rssMB: 500 } }));
|
|
259
|
+
assert.equal(r.snapshot.effectiveMax, 1);
|
|
260
|
+
assert.equal(r.decision, DECISIONS.QUEUE);
|
|
261
|
+
} finally { await fsp.rm(dir, { recursive: true, force: true }); }
|
|
262
|
+
});
|