@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,1121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Maestro — Cadence Handler Registry
|
|
3
|
+
*
|
|
4
|
+
* Per-cadence policy for how the persistent main session services an event
|
|
5
|
+
* delivered onto the cadence bus.
|
|
6
|
+
*
|
|
7
|
+
* Three handler modes:
|
|
8
|
+
*
|
|
9
|
+
* "inline" — Handler runs entirely inside the persistent main Node
|
|
10
|
+
* process. No Claude Code spawned. Use for housekeeping
|
|
11
|
+
* (dashboard refresh, log rotation, queue sweeps that need
|
|
12
|
+
* no reasoning).
|
|
13
|
+
*
|
|
14
|
+
* "guarded" — Persistent process runs a lightweight pre-check (e.g.
|
|
15
|
+
* "is there anything in the inbox?") and only escalates to
|
|
16
|
+
* a sub-session if substantive work is required. If the
|
|
17
|
+
* pre-check says "no work", the tick completes inline.
|
|
18
|
+
*
|
|
19
|
+
* "escalate" — The cadence always needs Claude-grade reasoning or
|
|
20
|
+
* substantive drafting; the consumer spawns a sub-session
|
|
21
|
+
* with the trigger prompt under schedules/triggers/<name>.md.
|
|
22
|
+
* Reserved for substantive drafting, multi-step outreach,
|
|
23
|
+
* large audits, research, and explicit isolated audit
|
|
24
|
+
* boundaries.
|
|
25
|
+
*
|
|
26
|
+
* Handlers / guards receive ({ event, agentRoot, log }) and return a result
|
|
27
|
+
* object that becomes part of the processed event's archive record. They
|
|
28
|
+
* must not throw — return { ok:false, error } so the consumer can decide to
|
|
29
|
+
* retry or DLQ via its policy.
|
|
30
|
+
*
|
|
31
|
+
* Adding a new cadence: add an entry below. If you skip an entry the
|
|
32
|
+
* consumer falls back to escalate behaviour as long as a trigger prompt
|
|
33
|
+
* exists at schedules/triggers/<cadence>.md; otherwise it DLQ's the event
|
|
34
|
+
* with a clear error.
|
|
35
|
+
*
|
|
36
|
+
* Optional per-entry field `allowedTools` (audit H1): a string[] of the tool
|
|
37
|
+
* names the cadence's sub-session is permitted to use. This is consumed ONLY
|
|
38
|
+
* when the operator opts into scoped permissions
|
|
39
|
+
* (MAESTRO_SCOPED_PERMISSIONS=1) via lib/session-permissions.mjs; it is
|
|
40
|
+
* ignored otherwise (default behaviour stays --dangerously-skip-permissions).
|
|
41
|
+
* Cadences without the field fall back to a conservative read-only allowlist
|
|
42
|
+
* in scoped mode. Keep these lists as tight as the cadence genuinely needs.
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync, renameSync } from "node:fs";
|
|
46
|
+
import { join, dirname } from "node:path";
|
|
47
|
+
import { fileURLToPath } from "node:url";
|
|
48
|
+
import { spawn } from "node:child_process";
|
|
49
|
+
import { recoverStaleClaims } from "../../lib/cadence-bus.mjs";
|
|
50
|
+
import { isQuarterStartMonth } from "../../lib/cadences.mjs";
|
|
51
|
+
|
|
52
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
53
|
+
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
// Pre-check helpers (shared across guarded handlers)
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Count files in a directory matching an optional regex. Returns 0 if the
|
|
60
|
+
* directory doesn't exist (idiomatic for fresh agent repos).
|
|
61
|
+
*/
|
|
62
|
+
function countFiles(dir, re) {
|
|
63
|
+
if (!existsSync(dir)) return 0;
|
|
64
|
+
try {
|
|
65
|
+
const all = readdirSync(dir);
|
|
66
|
+
return re ? all.filter((n) => re.test(n)).length : all.length;
|
|
67
|
+
} catch {
|
|
68
|
+
return 0;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Return queue file names that contain at least one "- " entry. Avoids a
|
|
74
|
+
* YAML parser dep: cheap line-scan, plenty good enough for a guard.
|
|
75
|
+
*/
|
|
76
|
+
function queuesWithWork(agentRoot) {
|
|
77
|
+
const dir = join(agentRoot, "state", "queues");
|
|
78
|
+
if (!existsSync(dir)) return [];
|
|
79
|
+
const out = [];
|
|
80
|
+
for (const name of readdirSync(dir)) {
|
|
81
|
+
if (!name.endsWith(".yaml")) continue;
|
|
82
|
+
try {
|
|
83
|
+
const body = readFileSync(join(dir, name), "utf-8");
|
|
84
|
+
if (/^- /m.test(body)) out.push(name);
|
|
85
|
+
} catch { /* ignore */ }
|
|
86
|
+
}
|
|
87
|
+
return out;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
// Inline handlers
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Heartbeat tick — purely internal. The consumer writes health.json on
|
|
96
|
+
* every cycle anyway; this handler exists so external schedulers (init,
|
|
97
|
+
* upgrade, doctor) can drop a heartbeat into the bus to verify end-to-end
|
|
98
|
+
* delivery.
|
|
99
|
+
*/
|
|
100
|
+
async function handleHeartbeat({ event }) {
|
|
101
|
+
return {
|
|
102
|
+
ok: true,
|
|
103
|
+
decision: "inline",
|
|
104
|
+
cadence: event.cadence,
|
|
105
|
+
note: "heartbeat acknowledged",
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Stale-claim recovery — the consumer already does this on startup and
|
|
111
|
+
* periodically; an explicit cadence event lets ops force a sweep without
|
|
112
|
+
* restarting the daemon.
|
|
113
|
+
*/
|
|
114
|
+
async function handleStaleRecovery({ event, agentRoot }) {
|
|
115
|
+
const stats = recoverStaleClaims(agentRoot);
|
|
116
|
+
return { ok: true, decision: "inline", cadence: event.cadence, stats };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
// Guarded handlers — cheap pre-check, escalate only when there's work
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* inbox-processor guard:
|
|
125
|
+
* The reactive daemon (agent-daemon.mjs) continuously polls inbound
|
|
126
|
+
* services + drains state/inbox/ + dispatches sessions via classifier
|
|
127
|
+
* and dispatcher. Running the cadence-bus inbox-processor cadence on
|
|
128
|
+
* top causes DOUBLE responses to the same message — observed in
|
|
129
|
+
* ravi-ai when a CEO DM got TWO substantive replies (one from each
|
|
130
|
+
* path) on 2026-05-12.
|
|
131
|
+
*
|
|
132
|
+
* So this guard now always returns inline. The reactive daemon owns
|
|
133
|
+
* inbox dispatch. Operators who want the cadence-only mode (no reactive
|
|
134
|
+
* daemon) can set INBOX_CADENCE_ESCALATE=1 in .env to flip behaviour.
|
|
135
|
+
*
|
|
136
|
+
* Pre-check is still done (count pending items) so the inline result
|
|
137
|
+
* carries useful telemetry, but escalation is suppressed.
|
|
138
|
+
*/
|
|
139
|
+
async function guardInboxProcessor({ agentRoot }) {
|
|
140
|
+
const inboxRoot = join(agentRoot, "state", "inbox");
|
|
141
|
+
const sources = ["slack", "gmail", "calendar", "internal", "sms", "whatsapp"];
|
|
142
|
+
let pending = 0;
|
|
143
|
+
for (const src of sources) {
|
|
144
|
+
pending += countFiles(join(inboxRoot, src), /\.(json|ya?ml)$/i);
|
|
145
|
+
}
|
|
146
|
+
if (process.env.INBOX_CADENCE_ESCALATE === "1") {
|
|
147
|
+
if (pending === 0) {
|
|
148
|
+
return { ok: true, decision: "inline", reason: "inbox empty", pending: 0 };
|
|
149
|
+
}
|
|
150
|
+
return { ok: true, decision: "escalate", pending };
|
|
151
|
+
}
|
|
152
|
+
return {
|
|
153
|
+
ok: true,
|
|
154
|
+
decision: "inline",
|
|
155
|
+
reason: "reactive-daemon-owns-inbox",
|
|
156
|
+
pending,
|
|
157
|
+
note: "Set INBOX_CADENCE_ESCALATE=1 to override.",
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* backlog-executor guard:
|
|
163
|
+
* The reactive daemon (agent-daemon.mjs) already runs an internal
|
|
164
|
+
* backlog sweep every BACKLOG_INTERVAL (default 10min) which dispatches
|
|
165
|
+
* one session per top-priority queue item with proper post-completion
|
|
166
|
+
* cooldowns. Running the cadence-bus backlog-executor on top of that
|
|
167
|
+
* spawns DUPLICATE sessions for the same items — observed as 159
|
|
168
|
+
* redundant spawns / day in ravi-ai's logs.
|
|
169
|
+
*
|
|
170
|
+
* So this guard now always returns inline. The reactive daemon owns
|
|
171
|
+
* the backlog. Operators who prefer the cadence-only mode (no reactive
|
|
172
|
+
* daemon) can set BACKLOG_CADENCE_ESCALATE=1 in .env to flip behaviour.
|
|
173
|
+
*/
|
|
174
|
+
async function guardBacklogExecutor({ agentRoot }) {
|
|
175
|
+
if (process.env.BACKLOG_CADENCE_ESCALATE === "1") {
|
|
176
|
+
const withWork = queuesWithWork(agentRoot);
|
|
177
|
+
if (withWork.length === 0) {
|
|
178
|
+
return { ok: true, decision: "inline", reason: "all queues empty" };
|
|
179
|
+
}
|
|
180
|
+
return { ok: true, decision: "escalate", queues_with_work: withWork };
|
|
181
|
+
}
|
|
182
|
+
return {
|
|
183
|
+
ok: true,
|
|
184
|
+
decision: "inline",
|
|
185
|
+
reason: "reactive-daemon-owns-backlog",
|
|
186
|
+
note: "Set BACKLOG_CADENCE_ESCALATE=1 to override.",
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// --- skill-curator consolidation due-check (phase 2) -----------------------
|
|
191
|
+
|
|
192
|
+
const CONSOLIDATION_STATE_REL = join("state", "learning", "consolidation.json");
|
|
193
|
+
const CONSOLIDATE_WORKER_REL = join(__dirname, "..", "learning", "consolidate-skills.mjs");
|
|
194
|
+
|
|
195
|
+
/** Read the last consolidation timestamp (ms). Fail-open → 0 (never run). */
|
|
196
|
+
function readLastConsolidationAt(agentRoot) {
|
|
197
|
+
try {
|
|
198
|
+
const p = join(agentRoot, CONSOLIDATION_STATE_REL);
|
|
199
|
+
if (!existsSync(p)) return 0;
|
|
200
|
+
const obj = JSON.parse(readFileSync(p, "utf8"));
|
|
201
|
+
const t = Date.parse(obj && obj.lastConsolidationAt);
|
|
202
|
+
return Number.isFinite(t) ? t : 0;
|
|
203
|
+
} catch {
|
|
204
|
+
return 0;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Persist the last consolidation timestamp atomically. Fail-open. */
|
|
209
|
+
function writeLastConsolidationAt(agentRoot, nowMs) {
|
|
210
|
+
try {
|
|
211
|
+
const p = join(agentRoot, CONSOLIDATION_STATE_REL);
|
|
212
|
+
mkdirSync(dirname(p), { recursive: true });
|
|
213
|
+
const tmp = `${p}.${process.pid}.${Date.now()}.tmp`;
|
|
214
|
+
writeFileSync(tmp, JSON.stringify({ lastConsolidationAt: new Date(nowMs).toISOString() }, null, 2) + "\n");
|
|
215
|
+
renameSync(tmp, p);
|
|
216
|
+
} catch { /* fail-open */ }
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** Spawn the consolidation worker DETACHED + unref'd (mirrors hook-runner). Fail-open. */
|
|
220
|
+
function spawnConsolidationWorker(agentRoot) {
|
|
221
|
+
try {
|
|
222
|
+
const child = spawn(process.execPath, [CONSOLIDATE_WORKER_REL], {
|
|
223
|
+
detached: true,
|
|
224
|
+
stdio: "ignore",
|
|
225
|
+
env: { ...process.env, MAESTRO_CONSOLIDATE_ROOT: agentRoot },
|
|
226
|
+
});
|
|
227
|
+
child.unref();
|
|
228
|
+
return true;
|
|
229
|
+
} catch {
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Decide whether an LLM umbrella-consolidation pass is DUE and, if so, spawn it
|
|
236
|
+
* DETACHED (never inline, never a nested sub-session). Gated on: config enabled,
|
|
237
|
+
* candidate count ≥ minSkills, and intervalDays since the last run. Returns a
|
|
238
|
+
* small descriptor for the guard's result. Injectable seams (tests):
|
|
239
|
+
* o.now — clock (ms)
|
|
240
|
+
* o.spawnFn — replace the detached spawner
|
|
241
|
+
* o.candidateCountFn — replace the candidate counter (avoids touching disk)
|
|
242
|
+
* Fail-open: any throw → { spawned:false, reason:"error" }.
|
|
243
|
+
*/
|
|
244
|
+
async function maybeSpawnConsolidation(agentRoot, o = {}) {
|
|
245
|
+
try {
|
|
246
|
+
const nowMs = Number.isFinite(o.now) ? o.now : Date.now();
|
|
247
|
+
let minSkills = 5, intervalDays = 14, enabled = true;
|
|
248
|
+
try {
|
|
249
|
+
const cfgMod = await import("../../lib/learning/config.mjs");
|
|
250
|
+
const collMod = await import("../../lib/collective/config.mjs");
|
|
251
|
+
const coll = await collMod.loadConfig(agentRoot);
|
|
252
|
+
const cfg = await cfgMod.loadLearningConfig(agentRoot, coll);
|
|
253
|
+
const c = cfg && cfg.consolidation ? cfg.consolidation : {};
|
|
254
|
+
enabled = c.enabled !== false && cfg.enabled !== false;
|
|
255
|
+
if (Number.isFinite(c.minSkills)) minSkills = c.minSkills;
|
|
256
|
+
if (Number.isFinite(c.intervalDays)) intervalDays = c.intervalDays;
|
|
257
|
+
} catch { /* defaults */ }
|
|
258
|
+
|
|
259
|
+
if (!enabled) return { spawned: false, reason: "disabled" };
|
|
260
|
+
|
|
261
|
+
// (b) enough agent-authored active skills to be worth a clustering pass.
|
|
262
|
+
let candidateCount;
|
|
263
|
+
if (typeof o.candidateCountFn === "function") {
|
|
264
|
+
candidateCount = o.candidateCountFn(agentRoot);
|
|
265
|
+
} else {
|
|
266
|
+
const curator = await import("../../lib/learning/curator.mjs");
|
|
267
|
+
candidateCount = curator.selectConsolidationCandidates(agentRoot).length;
|
|
268
|
+
}
|
|
269
|
+
if (candidateCount < minSkills) return { spawned: false, reason: "below-threshold", candidateCount };
|
|
270
|
+
|
|
271
|
+
// (c) at least intervalDays since the last consolidation.
|
|
272
|
+
const last = readLastConsolidationAt(agentRoot);
|
|
273
|
+
const dueAt = last + intervalDays * 86400_000;
|
|
274
|
+
if (last && nowMs < dueAt) return { spawned: false, reason: "not-due", candidateCount };
|
|
275
|
+
|
|
276
|
+
const spawnFn = o.spawnFn || spawnConsolidationWorker;
|
|
277
|
+
const ok = spawnFn(agentRoot);
|
|
278
|
+
if (ok) writeLastConsolidationAt(agentRoot, nowMs); // mark even if the worker later no-ops (don't re-spawn each tick)
|
|
279
|
+
return { spawned: !!ok, reason: ok ? "spawned" : "spawn-failed", candidateCount };
|
|
280
|
+
} catch (err) {
|
|
281
|
+
return { spawned: false, reason: "error", error: err && err.message ? err.message : String(err) };
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* skill-curator guard (WS3): deterministic lifecycle housekeeping for the
|
|
287
|
+
* agent's self-authored skills, PLUS a periodic LLM umbrella-consolidation pass.
|
|
288
|
+
*
|
|
289
|
+
* Phase 1 (inline, no Claude): the cheap pre-check asks the sidecar whether ANY
|
|
290
|
+
* non-pinned skill has crossed a stale (30d) or archive (90d) boundary; if so we
|
|
291
|
+
* run the pure curator transitions INLINE (moving a .md into _archive/ and
|
|
292
|
+
* dropping it from plugin.json needs no reasoning).
|
|
293
|
+
*
|
|
294
|
+
* Phase 2 (detached worker, NOT a sub-session): when consolidation is DUE
|
|
295
|
+
* (enabled + ≥ minSkills agent-authored active skills + ≥ intervalDays since the
|
|
296
|
+
* last pass, tracked in state/learning/consolidation.json) we spawn
|
|
297
|
+
* scripts/learning/consolidate-skills.mjs as a DETACHED, unref'd child — the same
|
|
298
|
+
* fire-and-forget pattern as the collective capture worker. We do NOT block the
|
|
299
|
+
* consumer and do NOT escalate to a managed sub-session; the guard still returns
|
|
300
|
+
* `inline`. The LLM call lives entirely inside that detached worker.
|
|
301
|
+
*/
|
|
302
|
+
async function guardSkillCurator({ agentRoot, log }, opts = {}) {
|
|
303
|
+
try {
|
|
304
|
+
const curator = await import("../../lib/learning/curator.mjs");
|
|
305
|
+
let staleDays, archiveDays;
|
|
306
|
+
try {
|
|
307
|
+
const cfgMod = await import("../../lib/learning/config.mjs");
|
|
308
|
+
const collMod = await import("../../lib/collective/config.mjs");
|
|
309
|
+
const coll = await collMod.loadConfig(agentRoot);
|
|
310
|
+
const cfg = await cfgMod.loadLearningConfig(agentRoot, coll);
|
|
311
|
+
staleDays = cfg.staleDays; archiveDays = cfg.archiveDays;
|
|
312
|
+
} catch { /* fall back to curator defaults */ }
|
|
313
|
+
|
|
314
|
+
// Daily catch-up for the verbatim transcript store (the Stop-hook ingests
|
|
315
|
+
// per-session; this sweeps anything that machine missed). Best-effort.
|
|
316
|
+
try {
|
|
317
|
+
const sidx = await import("../../lib/learning/session-index.mjs");
|
|
318
|
+
await sidx.ingestAll({ agentRoot });
|
|
319
|
+
} catch { /* on-demand store; a missed sweep is harmless */ }
|
|
320
|
+
|
|
321
|
+
// Phase 1 — deterministic lifecycle transitions (inline).
|
|
322
|
+
let transitioned = [], archived = [];
|
|
323
|
+
if (curator.hasPendingTransitions(agentRoot, { staleDays, archiveDays })) {
|
|
324
|
+
const res = curator.runCurator({ agentRoot, staleDays, archiveDays });
|
|
325
|
+
transitioned = res.transitioned; archived = res.archived;
|
|
326
|
+
if (typeof log === "function" && (transitioned.length || archived.length)) {
|
|
327
|
+
log("info", `[skill-curator] transitioned ${transitioned.length}, archived ${archived.length}`);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Phase 2 — LLM umbrella consolidation, spawned DETACHED if due. Never blocks
|
|
332
|
+
// the consumer, never escalates to a sub-session.
|
|
333
|
+
const consolidation = await maybeSpawnConsolidation(agentRoot, opts.consolidation || {});
|
|
334
|
+
if (consolidation.spawned && typeof log === "function") {
|
|
335
|
+
log("info", `[skill-curator] umbrella consolidation spawned (detached); ${consolidation.candidateCount} candidate(s)`);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return {
|
|
339
|
+
ok: true,
|
|
340
|
+
decision: "inline",
|
|
341
|
+
transitioned,
|
|
342
|
+
archived,
|
|
343
|
+
consolidation: consolidation.spawned ? "spawned" : consolidation.reason,
|
|
344
|
+
};
|
|
345
|
+
} catch (err) {
|
|
346
|
+
// Fail-open: housekeeping must never DLQ.
|
|
347
|
+
return { ok: true, decision: "inline", reason: "curator-error", error: err && err.message ? err.message : String(err) };
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* meeting-prep guard: only escalate if today's calendar inbox has any
|
|
353
|
+
* unprocessed event files. The cheap signal is just checking
|
|
354
|
+
* state/inbox/calendar/ for unprocessed entries.
|
|
355
|
+
*/
|
|
356
|
+
async function guardMeetingPrep({ agentRoot }) {
|
|
357
|
+
const dir = join(agentRoot, "state", "inbox", "calendar");
|
|
358
|
+
const pending = countFiles(dir, /\.(json|ya?ml)$/i);
|
|
359
|
+
if (pending === 0) {
|
|
360
|
+
return { ok: true, decision: "inline", reason: "no pending calendar events" };
|
|
361
|
+
}
|
|
362
|
+
return { ok: true, decision: "escalate", pending };
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* quarterly-self-assessment guard (Phase 0 item 0.14): the plist fires on the
|
|
367
|
+
* 1st of EVERY month, but the report is QUARTERLY. Escalate to the Claude
|
|
368
|
+
* sub-session only in a quarter-start month (Jan/Apr/Jul/Oct); otherwise the
|
|
369
|
+
* tick completes inline with no spawn — preventing the 12×/year (3× cost)
|
|
370
|
+
* misfire confirmed by the audit. Pure delegation to isQuarterStartMonth so the
|
|
371
|
+
* month logic stays unit-tested in lib/cadences.mjs.
|
|
372
|
+
*/
|
|
373
|
+
async function guardQuarterlySelfAssessment({ now } = {}) {
|
|
374
|
+
const when = now instanceof Date ? now : new Date();
|
|
375
|
+
if (!isQuarterStartMonth(when)) {
|
|
376
|
+
return { ok: true, decision: "inline", reason: "not a quarter-start month" };
|
|
377
|
+
}
|
|
378
|
+
return { ok: true, decision: "escalate", reason: "quarter start" };
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// ---------------------------------------------------------------------------
|
|
382
|
+
// Economics / scheduling cadences (WS-economics)
|
|
383
|
+
//
|
|
384
|
+
// These wire already-built deterministic economics + scheduling functions onto
|
|
385
|
+
// the cadence bus. They run INLINE in the persistent daemon (no LLM spawn) —
|
|
386
|
+
// except dynamic-jobs, which is a GUARDED escalate-when-due primitive. Every one
|
|
387
|
+
// is fail-open: a throw never DLQs the tick, it returns { ok:false, error }.
|
|
388
|
+
// ---------------------------------------------------------------------------
|
|
389
|
+
|
|
390
|
+
/** Read config/agent.json fail-open (for the org legs). Returns null on any error. */
|
|
391
|
+
function loadAgentCfg(agentRoot) {
|
|
392
|
+
try {
|
|
393
|
+
const p = join(agentRoot, "config", "agent.json");
|
|
394
|
+
if (!existsSync(p)) return null;
|
|
395
|
+
return JSON.parse(readFileSync(p, "utf8"));
|
|
396
|
+
} catch {
|
|
397
|
+
return null;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* nightly-cost-reconcile (inline): three-way spend reconcile of today's local
|
|
403
|
+
* router ledger against the Anthropic Admin Cost API. We pass NO fetchImpl, so
|
|
404
|
+
* the provider leg is skipped and reconcile fails open (driftPct null, never an
|
|
405
|
+
* error). A real deployment injects the Admin API fetch in prod. Drift > 10%
|
|
406
|
+
* (`flagged`) logs a warning. The result (drift %) goes in the inline result.
|
|
407
|
+
* The lib fn is injectable via `opts.reconcileImpl` for tests.
|
|
408
|
+
*/
|
|
409
|
+
async function handleNightlyCostReconcile({ event, agentRoot, log }, opts = {}) {
|
|
410
|
+
try {
|
|
411
|
+
const reconcile =
|
|
412
|
+
opts.reconcileImpl ||
|
|
413
|
+
(await import("../../lib/model-router/reconcile.mjs")).reconcileDaily;
|
|
414
|
+
// fetchImpl intentionally undefined → reconcile fails open (provider null).
|
|
415
|
+
const res = await reconcile({ agentRoot, fetchImpl: opts.fetchImpl });
|
|
416
|
+
if (res && res.flagged && typeof log === "function") {
|
|
417
|
+
log("warn", `[nightly-cost-reconcile] drift ${res.driftPct}% exceeds ${res.thresholdPct}% (local $${res.localUSD} vs provider $${res.providerUSD})`);
|
|
418
|
+
}
|
|
419
|
+
return {
|
|
420
|
+
ok: true,
|
|
421
|
+
decision: "inline",
|
|
422
|
+
cadence: event.cadence,
|
|
423
|
+
day: res?.day ?? null,
|
|
424
|
+
driftPct: res?.driftPct ?? null,
|
|
425
|
+
flagged: !!(res && res.flagged),
|
|
426
|
+
localUSD: res?.localUSD ?? null,
|
|
427
|
+
providerUSD: res?.providerUSD ?? null,
|
|
428
|
+
};
|
|
429
|
+
} catch (err) {
|
|
430
|
+
return { ok: false, decision: "inline", cadence: event.cadence, error: err && err.message ? err.message : String(err) };
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* fleet-cost-digest (inline): build the daily fleet LLM-spend digest from the
|
|
436
|
+
* local router ledger (org rollup leg fails open when Cohort is disabled). The
|
|
437
|
+
* digest summary (total / sessions / cache-read KPI / source) goes in the inline
|
|
438
|
+
* result. Injectable via `opts.digestImpl` for tests.
|
|
439
|
+
*/
|
|
440
|
+
async function handleFleetCostDigest({ event, agentRoot, log }, opts = {}) {
|
|
441
|
+
try {
|
|
442
|
+
const digestFn =
|
|
443
|
+
opts.digestImpl ||
|
|
444
|
+
(await import("../../scripts/cost/fleet-digest.mjs")).fleetDigest;
|
|
445
|
+
const cfg = opts.cfg !== undefined ? opts.cfg : loadAgentCfg(agentRoot);
|
|
446
|
+
const d = await digestFn({ agentRoot, cfg });
|
|
447
|
+
if (d && d.cacheReadKpiOk === false && typeof log === "function") {
|
|
448
|
+
log("warn", `[fleet-cost-digest] cache-read ratio ${(d.cacheReadRatio * 100).toFixed(1)}% below 60% target`);
|
|
449
|
+
}
|
|
450
|
+
return {
|
|
451
|
+
ok: true,
|
|
452
|
+
decision: "inline",
|
|
453
|
+
cadence: event.cadence,
|
|
454
|
+
day: d?.day ?? null,
|
|
455
|
+
totalUSD: d?.totalUSD ?? null,
|
|
456
|
+
sessions: d?.sessions ?? null,
|
|
457
|
+
cacheReadRatio: d?.cacheReadRatio ?? null,
|
|
458
|
+
cacheReadKpiOk: d?.cacheReadKpiOk ?? null,
|
|
459
|
+
source: d?.source ?? null,
|
|
460
|
+
summary: d ? `Fleet LLM spend ${d.day} (${d.source}): $${d.totalUSD} over ${d.sessions} session(s), cache-read ${(d.cacheReadRatio * 100).toFixed(1)}%` : null,
|
|
461
|
+
};
|
|
462
|
+
} catch (err) {
|
|
463
|
+
return { ok: false, decision: "inline", cadence: event.cadence, error: err && err.message ? err.message : String(err) };
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* org-cost-sync (inline): push today's router ledger rows to the Cohort org
|
|
469
|
+
* rollup via cost.report, batched + idempotent. No-op / fail-open when org
|
|
470
|
+
* (Cohort) is disabled (enabled:false in the result). Injectable via
|
|
471
|
+
* `opts.reportImpl` for tests.
|
|
472
|
+
*/
|
|
473
|
+
async function handleOrgCostSync({ event, agentRoot }, opts = {}) {
|
|
474
|
+
try {
|
|
475
|
+
const reportFn =
|
|
476
|
+
opts.reportImpl ||
|
|
477
|
+
(await import("../../lib/org/cost-sync.mjs")).reportLedger;
|
|
478
|
+
const cfg = opts.cfg !== undefined ? opts.cfg : loadAgentCfg(agentRoot);
|
|
479
|
+
const res = await reportFn({ agentRoot, cfg, fetchImpl: opts.fetchImpl });
|
|
480
|
+
return {
|
|
481
|
+
ok: true,
|
|
482
|
+
decision: "inline",
|
|
483
|
+
cadence: event.cadence,
|
|
484
|
+
enabled: !!(res && res.enabled),
|
|
485
|
+
rowsRead: res?.rowsRead ?? 0,
|
|
486
|
+
reported: res?.reported ?? 0,
|
|
487
|
+
failed: res?.failed ?? 0,
|
|
488
|
+
localUSD: res?.localUSD ?? 0,
|
|
489
|
+
};
|
|
490
|
+
} catch (err) {
|
|
491
|
+
return { ok: false, decision: "inline", cadence: event.cadence, error: err && err.message ? err.message : String(err) };
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* monitoring-alerts (inline): gather the diagnostics counters, today's cost
|
|
497
|
+
* ledger, peer presence, daemon health + backup freshness, evaluate the
|
|
498
|
+
* behavioral alert rules, and emit any firing alerts to the configured webhook
|
|
499
|
+
* (config/alerts.yaml `alerts.webhook`) — else log them. Deterministic, no LLM.
|
|
500
|
+
* Fail-open + fully injectable for tests. This is what makes lib/diagnostics/
|
|
501
|
+
* alerts.mjs actually fire on a schedule.
|
|
502
|
+
*/
|
|
503
|
+
async function handleMonitoringAlerts({ event, agentRoot, log }, opts = {}) {
|
|
504
|
+
try {
|
|
505
|
+
const A = opts.alertsImpl || (await import("../../lib/diagnostics/alerts.mjs"));
|
|
506
|
+
const snapshot = opts.snapshot !== undefined
|
|
507
|
+
? opts.snapshot
|
|
508
|
+
: (await import("../../lib/diagnostics/counters.mjs")).snapshot({ agentRoot });
|
|
509
|
+
const ledger = opts.ledger !== undefined
|
|
510
|
+
? opts.ledger
|
|
511
|
+
: (await import("../../lib/model-router/ledger.mjs")).sumLedgerToday({ agentRoot });
|
|
512
|
+
const presence = opts.presence !== undefined
|
|
513
|
+
? opts.presence
|
|
514
|
+
: (await import("../../lib/collective/presence.mjs")).list(agentRoot);
|
|
515
|
+
const health = opts.health !== undefined ? opts.health : recoverStaleClaims && readDaemonHealth(agentRoot);
|
|
516
|
+
const cfg = opts.cfg !== undefined ? opts.cfg : loadAgentCfg(agentRoot);
|
|
517
|
+
const now = opts.now || Date.now();
|
|
518
|
+
|
|
519
|
+
const alerts = A.evaluateAlerts({ snapshot, ledger, presence, health, thresholds: opts.thresholds, now }) || [];
|
|
520
|
+
|
|
521
|
+
const sinkUrl = cfg && cfg.alerts && cfg.alerts.webhook;
|
|
522
|
+
const sink = opts.sink || (async (alert) => {
|
|
523
|
+
if (sinkUrl) {
|
|
524
|
+
const { fetchWithTimeout } = await import("../../lib/util/fetch-timeout.mjs");
|
|
525
|
+
await fetchWithTimeout(sinkUrl, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(alert) }, 8000);
|
|
526
|
+
} else if (typeof log === "function") {
|
|
527
|
+
log("warn", `[alert:${alert.severity}] ${alert.id} — ${alert.detail}`);
|
|
528
|
+
}
|
|
529
|
+
});
|
|
530
|
+
const res = alerts.length ? await A.emitAlerts(alerts, { sink }) : { attempted: 0, delivered: 0, failed: 0 };
|
|
531
|
+
return { ok: true, decision: "inline", cadence: event.cadence, alerts: alerts.length, delivered: res.delivered ?? 0, severities: alerts.map((a) => a.severity) };
|
|
532
|
+
} catch (err) {
|
|
533
|
+
return { ok: false, decision: "inline", cadence: event.cadence, error: err && err.message ? err.message : String(err) };
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/** Best-effort daemon-health freshness for the alert rules (never throws). */
|
|
538
|
+
function readDaemonHealth(agentRoot) {
|
|
539
|
+
try {
|
|
540
|
+
const p = join(agentRoot, "state", "cadence-bus", "health.json");
|
|
541
|
+
if (!existsSync(p)) return { daemonDown: true };
|
|
542
|
+
const h = JSON.parse(readFileSync(p, "utf-8"));
|
|
543
|
+
return { ts: h.ts || h.updatedAt || null, daemonDown: false };
|
|
544
|
+
} catch { return { daemonDown: true }; }
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* dynamic-jobs guard (guarded): ask dueJobs() which agent-created jobs are due
|
|
549
|
+
* right now. None due → inline (cheap, write-free). Some due → escalate, with
|
|
550
|
+
* the due jobs carried in the result so the spawned session can action each
|
|
551
|
+
* reminder/job payload. dueJobs ADVANCES the store (coalesces recurring, expires
|
|
552
|
+
* stale one-offs) as a side effect — that is the intended consume. Injectable via
|
|
553
|
+
* `opts.dueJobsImpl` + `opts.now` for tests. Fail-open: a throw stays inline.
|
|
554
|
+
*/
|
|
555
|
+
async function guardDynamicJobs({ event, agentRoot }, opts = {}) {
|
|
556
|
+
try {
|
|
557
|
+
const due =
|
|
558
|
+
opts.dueJobsImpl ||
|
|
559
|
+
(await import("../../lib/scheduling/dynamic-jobs.mjs")).dueJobs;
|
|
560
|
+
const jobs = due(agentRoot, { now: opts.now }) || [];
|
|
561
|
+
if (jobs.length === 0) {
|
|
562
|
+
return { ok: true, decision: "inline", cadence: event.cadence, reason: "no jobs due", due: 0 };
|
|
563
|
+
}
|
|
564
|
+
return { ok: true, decision: "escalate", cadence: event.cadence, reason: `${jobs.length} job(s) due`, due: jobs.length, jobs };
|
|
565
|
+
} catch (err) {
|
|
566
|
+
return { ok: false, decision: "inline", cadence: event.cadence, error: err && err.message ? err.message : String(err) };
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// ---------------------------------------------------------------------------
|
|
571
|
+
// Org messaging inbound (SP10) — pull Cohort messages/@mentions/call-invites
|
|
572
|
+
// ---------------------------------------------------------------------------
|
|
573
|
+
|
|
574
|
+
const MESSAGING_CURSOR_REL = join("state", "messaging", "inbound-cursor.json");
|
|
575
|
+
|
|
576
|
+
/** Read the org-messaging inbound cursor (seq). Fail-open → 0 (from genesis). */
|
|
577
|
+
function readMessagingCursor(agentRoot) {
|
|
578
|
+
try {
|
|
579
|
+
const p = join(agentRoot, MESSAGING_CURSOR_REL);
|
|
580
|
+
if (!existsSync(p)) return 0;
|
|
581
|
+
const obj = JSON.parse(readFileSync(p, "utf8"));
|
|
582
|
+
const c = Number(obj && obj.cursor);
|
|
583
|
+
return Number.isFinite(c) ? c : 0;
|
|
584
|
+
} catch {
|
|
585
|
+
return 0;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/** Persist the org-messaging inbound cursor atomically (temp → rename). Fail-open. */
|
|
590
|
+
function writeMessagingCursor(agentRoot, cursor) {
|
|
591
|
+
try {
|
|
592
|
+
const p = join(agentRoot, MESSAGING_CURSOR_REL);
|
|
593
|
+
mkdirSync(dirname(p), { recursive: true });
|
|
594
|
+
const tmp = `${p}.${process.pid}.${Date.now()}.tmp`;
|
|
595
|
+
writeFileSync(tmp, JSON.stringify({ cursor, updatedAt: new Date().toISOString() }, null, 2) + "\n");
|
|
596
|
+
renameSync(tmp, p);
|
|
597
|
+
} catch { /* fail-open */ }
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* messaging-inbound guard (guarded): poll the Cohort messaging/calling event
|
|
602
|
+
* stream for new messages / @mentions / call-invites DIRECTED AT this
|
|
603
|
+
* agent (lib/org/messaging.pullInbound), and ROUTE each one into the existing
|
|
604
|
+
* daemon inbox pipeline by writing a legacy inbox item under
|
|
605
|
+
* state/inbox/cohort/ — so the agent reacts to a Cohort @mention exactly like
|
|
606
|
+
* a Slack/Telegram one (classifier → directed-gate → responder → dispatcher).
|
|
607
|
+
*
|
|
608
|
+
* The reactive daemon OWNS inbox dispatch, so the default decision is `inline`
|
|
609
|
+
* (we route the items but don't escalate — escalating would double-respond, the
|
|
610
|
+
* same hazard guardInboxProcessor documents). Operators running cadence-only mode
|
|
611
|
+
* (no reactive daemon) can set MESSAGING_INBOUND_ESCALATE=1 to escalate to the
|
|
612
|
+
* trigger when new directed items arrive.
|
|
613
|
+
*
|
|
614
|
+
* Cursor is advanced + persisted atomically AFTER the items are written, so a
|
|
615
|
+
* crash re-pulls (at-least-once into the dedup-guarded writeInboxItem) rather
|
|
616
|
+
* than dropping. Fail-open: a disabled/unreachable org or any throw stays inline
|
|
617
|
+
* with 0 routed (org messaging is fail-open at runtime).
|
|
618
|
+
*
|
|
619
|
+
* Injectable seams (tests): o.pullImpl, o.writeImpl, o.toInboxImpl, o.cfg,
|
|
620
|
+
* o.fetchImpl, o.now — so tests never touch the network or real config.
|
|
621
|
+
*/
|
|
622
|
+
async function guardMessagingInbound({ event, agentRoot, log }, opts = {}) {
|
|
623
|
+
try {
|
|
624
|
+
const cfg = opts.cfg !== undefined ? opts.cfg : loadAgentCfg(agentRoot);
|
|
625
|
+
// No org config at all → nothing to pull. Stay inline (fail-open, no spawn).
|
|
626
|
+
if (!cfg || !cfg.org || !cfg.org.cohort) {
|
|
627
|
+
return { ok: true, decision: "inline", cadence: event.cadence, reason: "org messaging not configured", routed: 0 };
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
const pull = opts.pullImpl || (await import("../../lib/org/messaging.mjs")).pullInbound;
|
|
631
|
+
const eventToInboxItem =
|
|
632
|
+
opts.toInboxImpl || (await import("../../lib/channels/inbox-item.mjs")).eventToInboxItem;
|
|
633
|
+
const writeInboxItem = opts.writeImpl || (await import("../poller/utils.mjs")).writeInboxItem;
|
|
634
|
+
|
|
635
|
+
const cursor = readMessagingCursor(agentRoot);
|
|
636
|
+
const res = await pull({ cfg, cursor, fetchImpl: opts.fetchImpl });
|
|
637
|
+
const events = (res && Array.isArray(res.events)) ? res.events : [];
|
|
638
|
+
|
|
639
|
+
let routed = 0;
|
|
640
|
+
for (const ev of events) {
|
|
641
|
+
try {
|
|
642
|
+
const item = eventToInboxItem(ev);
|
|
643
|
+
writeInboxItem("cohort", item, agentRoot);
|
|
644
|
+
routed += 1;
|
|
645
|
+
} catch { /* one bad event must not drop the rest */ }
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// Advance the cursor AFTER writing (at-least-once; writeInboxItem dedupes).
|
|
649
|
+
const next = res && res.nextCursor != null ? res.nextCursor : cursor;
|
|
650
|
+
if (next !== cursor) writeMessagingCursor(agentRoot, next);
|
|
651
|
+
|
|
652
|
+
if (routed && typeof log === "function") {
|
|
653
|
+
log("info", `[messaging-inbound] routed ${routed} Cohort item(s) into the inbox pipeline`);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
if (process.env.MESSAGING_INBOUND_ESCALATE === "1" && routed > 0) {
|
|
657
|
+
return { ok: true, decision: "escalate", cadence: event.cadence, reason: `${routed} directed item(s)`, routed };
|
|
658
|
+
}
|
|
659
|
+
return {
|
|
660
|
+
ok: true,
|
|
661
|
+
decision: "inline",
|
|
662
|
+
cadence: event.cadence,
|
|
663
|
+
reason: routed ? "routed to inbox (reactive-daemon-owns-dispatch)" : "no new directed items",
|
|
664
|
+
routed,
|
|
665
|
+
note: routed ? "Set MESSAGING_INBOUND_ESCALATE=1 to escalate instead of routing to the inbox." : undefined,
|
|
666
|
+
};
|
|
667
|
+
} catch (err) {
|
|
668
|
+
return { ok: true, decision: "inline", cadence: event.cadence, reason: "messaging-inbound-error", routed: 0, error: err && err.message ? err.message : String(err) };
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// ---------------------------------------------------------------------------
|
|
673
|
+
// Directory hygiene + brand steward (2026-07 Directory & Design)
|
|
674
|
+
// ---------------------------------------------------------------------------
|
|
675
|
+
|
|
676
|
+
/**
|
|
677
|
+
* Resolve { base, token } for an org call from an agent cfg, or null when the
|
|
678
|
+
* integration is off/unconfigured. Mirrors lib/org/messaging.resolveOrg — the
|
|
679
|
+
* SAME isEnabled + configFromAgent chain, so a disabled agent is inert here for
|
|
680
|
+
* exactly the reason it is inert everywhere else. Never throws.
|
|
681
|
+
*/
|
|
682
|
+
async function resolveOrgCall(cfg) {
|
|
683
|
+
try {
|
|
684
|
+
const { isEnabled, configFromAgent } = await import("../../lib/org/client.mjs");
|
|
685
|
+
if (!isEnabled(cfg)) return null;
|
|
686
|
+
const c = configFromAgent(cfg);
|
|
687
|
+
return c.base ? { base: c.base, token: c.token, orgId: c.orgId } : null;
|
|
688
|
+
} catch {
|
|
689
|
+
return null;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/**
|
|
694
|
+
* directory-hygiene guard (guarded): a CHEAP pre-check over the Directory's two
|
|
695
|
+
* human review queues — pending captures and pending duplicate candidates. If
|
|
696
|
+
* both are empty the tick completes inline with no Claude spawn; if either has
|
|
697
|
+
* work, escalate to schedules/triggers/directory-hygiene.md, where the session
|
|
698
|
+
* proposes captures from signatures/rosters/bank feeds, enriches channels,
|
|
699
|
+
* re-verifies anything unseen for >90d, and flags duplicates.
|
|
700
|
+
*
|
|
701
|
+
* The guard itself only ever READS. Everything the escalated session does is a
|
|
702
|
+
* PROPOSAL: merges, awareness changes, capture resolution and archives are
|
|
703
|
+
* human-gated server-side (DECISIONS D-7), so this cadence cannot mutate a
|
|
704
|
+
* record even if the model tried.
|
|
705
|
+
*
|
|
706
|
+
* Awareness note: the queue counts below are awareness-scoped like every other
|
|
707
|
+
* Directory read. A zero count means "nothing this seat can see", never "nothing
|
|
708
|
+
* exists" — which is why a zero-count tick stays inline rather than reporting
|
|
709
|
+
* a clean queue.
|
|
710
|
+
*
|
|
711
|
+
* Injectable seams (tests): o.cfg, o.callImpl, o.fetchImpl — so tests never
|
|
712
|
+
* touch the network or real config. Fail-open: an unreachable/disabled org, a
|
|
713
|
+
* transport error, or ANY throw stays inline with 0 pending.
|
|
714
|
+
*/
|
|
715
|
+
async function guardDirectoryHygiene({ event, agentRoot, log }, opts = {}) {
|
|
716
|
+
const cadence = event && event.cadence ? event.cadence : "directory-hygiene";
|
|
717
|
+
try {
|
|
718
|
+
const cfg = opts.cfg !== undefined ? opts.cfg : loadAgentCfg(agentRoot);
|
|
719
|
+
const org = await resolveOrgCall(cfg);
|
|
720
|
+
if (!org) {
|
|
721
|
+
return { ok: true, decision: "inline", cadence, reason: "org directory not configured", captures: 0, duplicates: 0 };
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
const call = opts.callImpl || (await import("../../lib/org/client.mjs")).call;
|
|
725
|
+
const o = { base: org.base, token: org.token, orgId: org.orgId, fetchImpl: opts.fetchImpl };
|
|
726
|
+
const [capFrame, dupFrame] = await Promise.all([
|
|
727
|
+
call("directory.listCaptures", { status: "PENDING", limit: 50 }, o),
|
|
728
|
+
call("directory.listMerges", { status: "PENDING", limit: 50 }, o),
|
|
729
|
+
]);
|
|
730
|
+
|
|
731
|
+
const rows = (frame, ...keys) => {
|
|
732
|
+
if (!frame || !frame.ok || !frame.result) return [];
|
|
733
|
+
const r = frame.result;
|
|
734
|
+
if (Array.isArray(r)) return r;
|
|
735
|
+
for (const k of keys) if (Array.isArray(r[k])) return r[k];
|
|
736
|
+
return [];
|
|
737
|
+
};
|
|
738
|
+
const captures = rows(capFrame, "captures", "items").length;
|
|
739
|
+
const duplicates = rows(dupFrame, "candidates", "merges", "items").length;
|
|
740
|
+
const pending = captures + duplicates;
|
|
741
|
+
|
|
742
|
+
if (pending === 0) {
|
|
743
|
+
return { ok: true, decision: "inline", cadence, reason: "review queues clear", captures: 0, duplicates: 0 };
|
|
744
|
+
}
|
|
745
|
+
if (typeof log === "function") {
|
|
746
|
+
log("info", `[directory-hygiene] ${captures} capture(s) + ${duplicates} duplicate(s) pending`);
|
|
747
|
+
}
|
|
748
|
+
return {
|
|
749
|
+
ok: true,
|
|
750
|
+
decision: "escalate",
|
|
751
|
+
cadence,
|
|
752
|
+
reason: `${captures} capture(s), ${duplicates} duplicate(s) pending`,
|
|
753
|
+
captures,
|
|
754
|
+
duplicates,
|
|
755
|
+
pending,
|
|
756
|
+
};
|
|
757
|
+
} catch (err) {
|
|
758
|
+
return {
|
|
759
|
+
ok: true,
|
|
760
|
+
decision: "inline",
|
|
761
|
+
cadence,
|
|
762
|
+
reason: "directory-hygiene-error",
|
|
763
|
+
captures: 0,
|
|
764
|
+
duplicates: 0,
|
|
765
|
+
error: err && err.message ? err.message : String(err),
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
const BRAND_CURSOR_REL = join("state", "org", "brand-steward-cursor.json");
|
|
771
|
+
|
|
772
|
+
/** Read the brand-steward event cursor (seq). Fail-open → 0 (from genesis). */
|
|
773
|
+
function readBrandCursor(agentRoot) {
|
|
774
|
+
try {
|
|
775
|
+
const p = join(agentRoot, BRAND_CURSOR_REL);
|
|
776
|
+
if (!existsSync(p)) return 0;
|
|
777
|
+
const c = Number(JSON.parse(readFileSync(p, "utf8")).cursor);
|
|
778
|
+
return Number.isFinite(c) ? c : 0;
|
|
779
|
+
} catch {
|
|
780
|
+
return 0;
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
/** Persist the brand-steward cursor atomically (temp → rename). Fail-open. */
|
|
785
|
+
function writeBrandCursor(agentRoot, cursor) {
|
|
786
|
+
try {
|
|
787
|
+
const p = join(agentRoot, BRAND_CURSOR_REL);
|
|
788
|
+
mkdirSync(dirname(p), { recursive: true });
|
|
789
|
+
const tmp = `${p}.${process.pid}.${Date.now()}.tmp`;
|
|
790
|
+
writeFileSync(tmp, JSON.stringify({ cursor, updatedAt: new Date().toISOString() }, null, 2) + "\n");
|
|
791
|
+
renameSync(tmp, p);
|
|
792
|
+
} catch { /* fail-open */ }
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
/** Brand events that mean "the foundation moved; downstream assets are stale". */
|
|
796
|
+
const BRAND_SAVE_KINDS = new Set(["version.saved", "foundation.updated", "version.head_moved"]);
|
|
797
|
+
|
|
798
|
+
/**
|
|
799
|
+
* brand-steward guard (guarded): tail the org event chain for a brand
|
|
800
|
+
* foundation change (`branding/version.saved` and its siblings). A foundation
|
|
801
|
+
* save is what makes rendered imagery and templates STALE, so this is
|
|
802
|
+
* event-driven rather than time-driven — it escalates ONLY on a real save, and
|
|
803
|
+
* otherwise costs one cheap GET.
|
|
804
|
+
*
|
|
805
|
+
* On escalation the session runs schedules/triggers/brand-steward.md: quote the
|
|
806
|
+
* stale slots (`design_generate_image` with `budgetCents: 0`), re-render what is
|
|
807
|
+
* worth re-rendering inside the budget, and post a working-now status line
|
|
808
|
+
* carrying the actual spend.
|
|
809
|
+
*
|
|
810
|
+
* The cursor advances AFTER the decision, so a crash re-reads the same window
|
|
811
|
+
* (at-least-once) rather than silently skipping a brand save. `hq` ignores a
|
|
812
|
+
* `family=` query param on the events read, so the family/kind filter is applied
|
|
813
|
+
* client-side here — the same posture as lib/org/messaging.pullInbound.
|
|
814
|
+
*
|
|
815
|
+
* Injectable seams (tests): o.cfg, o.readImpl, o.fetchImpl. Fail-open in every
|
|
816
|
+
* direction; never throws.
|
|
817
|
+
*/
|
|
818
|
+
async function guardBrandSteward({ event, agentRoot, log }, opts = {}) {
|
|
819
|
+
const cadence = event && event.cadence ? event.cadence : "brand-steward";
|
|
820
|
+
try {
|
|
821
|
+
const cfg = opts.cfg !== undefined ? opts.cfg : loadAgentCfg(agentRoot);
|
|
822
|
+
const org = await resolveOrgCall(cfg);
|
|
823
|
+
if (!org) {
|
|
824
|
+
return { ok: true, decision: "inline", cadence, reason: "org design not configured", saves: 0 };
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
const read = opts.readImpl || (await import("../../lib/org/client.mjs")).read;
|
|
828
|
+
const cursor = readBrandCursor(agentRoot);
|
|
829
|
+
const r = await read(
|
|
830
|
+
`events?cursor=${encodeURIComponent(cursor)}&limit=200&family=branding`,
|
|
831
|
+
{ base: org.base, token: org.token, orgId: org.orgId, fetchImpl: opts.fetchImpl },
|
|
832
|
+
);
|
|
833
|
+
if (!r || !r.ok || !r.payload) {
|
|
834
|
+
return { ok: true, decision: "inline", cadence, reason: "events unreadable", saves: 0 };
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
const raw = Array.isArray(r.payload)
|
|
838
|
+
? r.payload
|
|
839
|
+
: Array.isArray(r.payload.events)
|
|
840
|
+
? r.payload.events
|
|
841
|
+
: [];
|
|
842
|
+
let maxSeq = cursor;
|
|
843
|
+
const saves = [];
|
|
844
|
+
for (const ev of raw) {
|
|
845
|
+
const seq = Number(ev && ev.seq);
|
|
846
|
+
if (Number.isFinite(seq)) maxSeq = Math.max(Number(maxSeq) || 0, seq);
|
|
847
|
+
// hq ignores family= on the events read, so filter here.
|
|
848
|
+
if (!ev || ev.family !== "branding") continue;
|
|
849
|
+
if (!BRAND_SAVE_KINDS.has(String(ev.kind || ""))) continue;
|
|
850
|
+
saves.push({ seq, kind: ev.kind, at: ev.at ?? null });
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
if (raw.length && maxSeq !== cursor) writeBrandCursor(agentRoot, maxSeq);
|
|
854
|
+
|
|
855
|
+
if (saves.length === 0) {
|
|
856
|
+
return { ok: true, decision: "inline", cadence, reason: "no brand save since last tick", saves: 0, cursor: maxSeq };
|
|
857
|
+
}
|
|
858
|
+
if (typeof log === "function") {
|
|
859
|
+
log("info", `[brand-steward] ${saves.length} brand foundation change(s) — imagery/templates may be stale`);
|
|
860
|
+
}
|
|
861
|
+
return {
|
|
862
|
+
ok: true,
|
|
863
|
+
decision: "escalate",
|
|
864
|
+
cadence,
|
|
865
|
+
reason: `${saves.length} brand foundation change(s) since seq ${cursor}`,
|
|
866
|
+
saves: saves.length,
|
|
867
|
+
latestKind: saves[saves.length - 1].kind,
|
|
868
|
+
cursor: maxSeq,
|
|
869
|
+
};
|
|
870
|
+
} catch (err) {
|
|
871
|
+
return {
|
|
872
|
+
ok: true,
|
|
873
|
+
decision: "inline",
|
|
874
|
+
cadence,
|
|
875
|
+
reason: "brand-steward-error",
|
|
876
|
+
saves: 0,
|
|
877
|
+
error: err && err.message ? err.message : String(err),
|
|
878
|
+
};
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// ---------------------------------------------------------------------------
|
|
883
|
+
// Registry
|
|
884
|
+
// ---------------------------------------------------------------------------
|
|
885
|
+
|
|
886
|
+
export const CADENCE_REGISTRY = {
|
|
887
|
+
// Internal housekeeping (always inline)
|
|
888
|
+
"cadence-bus-heartbeat": {
|
|
889
|
+
mode: "inline",
|
|
890
|
+
handler: handleHeartbeat,
|
|
891
|
+
description: "Probe tick used by doctor / upgrade to verify end-to-end delivery.",
|
|
892
|
+
},
|
|
893
|
+
"cadence-bus-recovery": {
|
|
894
|
+
mode: "inline",
|
|
895
|
+
handler: handleStaleRecovery,
|
|
896
|
+
description: "On-demand stale-claim recovery.",
|
|
897
|
+
},
|
|
898
|
+
|
|
899
|
+
// Operational cadences with cheap pre-checks
|
|
900
|
+
"inbox-processor": {
|
|
901
|
+
mode: "guarded",
|
|
902
|
+
guard: guardInboxProcessor,
|
|
903
|
+
prompt: "schedules/triggers/inbox-processor.md",
|
|
904
|
+
description: "Classify / route inbound items if any are waiting.",
|
|
905
|
+
},
|
|
906
|
+
"backlog-executor": {
|
|
907
|
+
mode: "guarded",
|
|
908
|
+
guard: guardBacklogExecutor,
|
|
909
|
+
prompt: "schedules/triggers/backlog-executor.md",
|
|
910
|
+
description: "Execute the next slice of the proactive backlog if queues are non-empty.",
|
|
911
|
+
},
|
|
912
|
+
"skill-curator": {
|
|
913
|
+
mode: "guarded",
|
|
914
|
+
guard: guardSkillCurator,
|
|
915
|
+
// No prompt file and never escalates to a sub-session. Phase 1 (lifecycle
|
|
916
|
+
// transitions) runs inline; phase 2 (LLM umbrella consolidation) runs as a
|
|
917
|
+
// DETACHED worker (scripts/learning/consolidate-skills.mjs) spawned from the
|
|
918
|
+
// guard when due — the guard still returns `inline`, so the consumer is
|
|
919
|
+
// never blocked and no managed sub-session is created.
|
|
920
|
+
description: "Lifecycle housekeeping (active→stale→archived) inline + periodic LLM umbrella consolidation via a detached worker. Never escalates.",
|
|
921
|
+
},
|
|
922
|
+
"meeting-prep": {
|
|
923
|
+
mode: "guarded",
|
|
924
|
+
guard: guardMeetingPrep,
|
|
925
|
+
prompt: "schedules/triggers/meeting-prep.md",
|
|
926
|
+
description: "Prepare for upcoming meetings when there are unprocessed calendar items.",
|
|
927
|
+
// Scoped-permissions allowlist (audit H1). Meeting prep reads the calendar
|
|
928
|
+
// + recent context and drafts a brief; it does not need shell/write/send
|
|
929
|
+
// primitives. Only consulted when MAESTRO_SCOPED_PERMISSIONS=1.
|
|
930
|
+
allowedTools: ["Read", "Grep", "Glob", "LS", "search_calendar", "search_meetings"],
|
|
931
|
+
},
|
|
932
|
+
|
|
933
|
+
// Always-escalate (substantive reasoning / drafting required)
|
|
934
|
+
"meeting-action-capture": {
|
|
935
|
+
mode: "escalate",
|
|
936
|
+
prompt: "schedules/triggers/meeting-action-capture.md",
|
|
937
|
+
description: "Capture meeting action items into queues.",
|
|
938
|
+
},
|
|
939
|
+
"daily-morning-brief": {
|
|
940
|
+
mode: "escalate",
|
|
941
|
+
prompt: "schedules/triggers/daily-morning-brief.md",
|
|
942
|
+
description: "Daily morning brief.",
|
|
943
|
+
},
|
|
944
|
+
"daily-midday-sweep": {
|
|
945
|
+
mode: "escalate",
|
|
946
|
+
prompt: "schedules/triggers/daily-midday-sweep.md",
|
|
947
|
+
description: "Midday comms triage and queue sweep.",
|
|
948
|
+
},
|
|
949
|
+
"daily-evening-wrap": {
|
|
950
|
+
mode: "escalate",
|
|
951
|
+
prompt: "schedules/triggers/daily-evening-wrap.md",
|
|
952
|
+
description: "Evening wrap, log review, follow-up close.",
|
|
953
|
+
},
|
|
954
|
+
"weekly-hiring": {
|
|
955
|
+
mode: "escalate",
|
|
956
|
+
prompt: "schedules/triggers/weekly-hiring.md",
|
|
957
|
+
description: "Weekly hiring review.",
|
|
958
|
+
},
|
|
959
|
+
"weekly-priorities": {
|
|
960
|
+
mode: "escalate",
|
|
961
|
+
prompt: "schedules/triggers/weekly-priorities.md",
|
|
962
|
+
description: "Weekly priorities review.",
|
|
963
|
+
},
|
|
964
|
+
"weekly-engineering-health": {
|
|
965
|
+
mode: "escalate",
|
|
966
|
+
prompt: "schedules/triggers/weekly-engineering-health.md",
|
|
967
|
+
description: "Weekly engineering health.",
|
|
968
|
+
},
|
|
969
|
+
"weekly-execution": {
|
|
970
|
+
mode: "escalate",
|
|
971
|
+
prompt: "schedules/triggers/weekly-execution.md",
|
|
972
|
+
description: "Weekly execution review.",
|
|
973
|
+
},
|
|
974
|
+
"weekly-strategic-memo": {
|
|
975
|
+
mode: "escalate",
|
|
976
|
+
prompt: "schedules/triggers/weekly-strategic-memo.md",
|
|
977
|
+
description: "Weekly strategic memo drafting.",
|
|
978
|
+
},
|
|
979
|
+
"quarterly-self-assessment": {
|
|
980
|
+
mode: "guarded",
|
|
981
|
+
guard: guardQuarterlySelfAssessment,
|
|
982
|
+
prompt: "schedules/triggers/quarterly-self-assessment.md",
|
|
983
|
+
description: "Quarterly self-assessment (guard escalates only in a quarter-start month).",
|
|
984
|
+
},
|
|
985
|
+
|
|
986
|
+
// Economics / scheduling cadences (WS-economics). Deterministic → inline,
|
|
987
|
+
// except dynamic-jobs (guarded escalate-when-due).
|
|
988
|
+
"nightly-cost-reconcile": {
|
|
989
|
+
mode: "inline",
|
|
990
|
+
handler: handleNightlyCostReconcile,
|
|
991
|
+
description: "Nightly three-way spend reconcile vs the Admin Cost API; flags drift > 10%. Inline (no LLM); fails open with no fetchImpl.",
|
|
992
|
+
},
|
|
993
|
+
"fleet-cost-digest": {
|
|
994
|
+
mode: "inline",
|
|
995
|
+
handler: handleFleetCostDigest,
|
|
996
|
+
description: "Daily fleet LLM-spend digest from the local router ledger (org rollup leg fails open). Inline.",
|
|
997
|
+
},
|
|
998
|
+
"org-cost-sync": {
|
|
999
|
+
mode: "inline",
|
|
1000
|
+
handler: handleOrgCostSync,
|
|
1001
|
+
description: "Push today's router ledger rows to Cohort cost.report (no-op/fail-open when org disabled). Inline.",
|
|
1002
|
+
},
|
|
1003
|
+
"monitoring-alerts": {
|
|
1004
|
+
mode: "inline",
|
|
1005
|
+
handler: handleMonitoringAlerts,
|
|
1006
|
+
description: "Evaluate behavioral alert rules (guard-fail/refusal spikes, cost p99, absent-agent, daemon-down) over the diagnostics counters + ledger + presence; emit to config/alerts.yaml webhook else log. Inline (no LLM); fail-open.",
|
|
1007
|
+
},
|
|
1008
|
+
"dynamic-jobs": {
|
|
1009
|
+
mode: "guarded",
|
|
1010
|
+
guard: guardDynamicJobs,
|
|
1011
|
+
prompt: "schedules/triggers/dynamic-jobs.md",
|
|
1012
|
+
description: "Action agent-created reminders/jobs. Guard escalates only when dueJobs() returns due work; otherwise inline.",
|
|
1013
|
+
},
|
|
1014
|
+
|
|
1015
|
+
// Org messaging inbound (SP10). Guarded — the guard pulls new Cohort
|
|
1016
|
+
// messages/@mentions/call-invites directed at this agent and routes them into
|
|
1017
|
+
// the inbox pipeline (the reactive daemon owns dispatch, so it stays inline);
|
|
1018
|
+
// MESSAGING_INBOUND_ESCALATE=1 flips it to escalate when items arrive.
|
|
1019
|
+
"messaging-inbound": {
|
|
1020
|
+
mode: "guarded",
|
|
1021
|
+
guard: guardMessagingInbound,
|
|
1022
|
+
prompt: "schedules/triggers/messaging-inbound.md",
|
|
1023
|
+
description: "Pull Cohort org messages/@mentions/call-invites directed at this agent into the inbox pipeline. Guarded — inline routing by default; escalate-when-items with MESSAGING_INBOUND_ESCALATE=1. Fail-open when org disabled.",
|
|
1024
|
+
// Scoped-permissions allowlist (audit H1): when it escalates, the sub-session
|
|
1025
|
+
// reads the org context and drafts a reply — no shell/write primitives.
|
|
1026
|
+
allowedTools: ["Read", "Grep", "Glob", "LS", "messaging_history", "messaging_send"],
|
|
1027
|
+
},
|
|
1028
|
+
|
|
1029
|
+
// Directory & Design stewards (2026-07). Both guarded, both cheap when idle.
|
|
1030
|
+
"directory-hygiene": {
|
|
1031
|
+
mode: "guarded",
|
|
1032
|
+
guard: guardDirectoryHygiene,
|
|
1033
|
+
prompt: "schedules/triggers/directory-hygiene.md",
|
|
1034
|
+
description:
|
|
1035
|
+
"Keep the Directory clean: propose captures from signatures/rosters/bank feeds, enrich channels, re-verify anything unseen for >90d, flag duplicates. Guarded — escalates only when the capture/duplicate queues have work. PROPOSE-ONLY: merges, awareness changes and archives are human-gated server-side. Fail-open when org disabled.",
|
|
1036
|
+
// Scoped-permissions allowlist (audit H1). Every Directory verb here is a
|
|
1037
|
+
// read or a PROPOSAL; the human-gated verbs are deliberately absent, so even
|
|
1038
|
+
// in scoped mode the cadence physically cannot reach for them.
|
|
1039
|
+
allowedTools: [
|
|
1040
|
+
"Read", "Grep", "Glob", "LS",
|
|
1041
|
+
"directory_search", "directory_list_people", "directory_list_orgs",
|
|
1042
|
+
"directory_get_person", "directory_get_org",
|
|
1043
|
+
"directory_list_captures", "directory_list_merges",
|
|
1044
|
+
"directory_propose_capture", "directory_propose_merge",
|
|
1045
|
+
"directory_add_channel", "directory_update_person",
|
|
1046
|
+
],
|
|
1047
|
+
},
|
|
1048
|
+
"brand-steward": {
|
|
1049
|
+
mode: "guarded",
|
|
1050
|
+
guard: guardBrandSteward,
|
|
1051
|
+
prompt: "schedules/triggers/brand-steward.md",
|
|
1052
|
+
description:
|
|
1053
|
+
"React to a brand foundation save: quote the stale image slots and templates, re-render what is worth re-rendering inside the budget, and post a working-now status carrying the real spend. Guarded — escalates only on a branding version.saved/foundation.updated event. Fail-open when org disabled.",
|
|
1054
|
+
// Reads + the costed producers. branding.saveFoundation/restoreFoundation are
|
|
1055
|
+
// admin-scope server-side and have no tool here — the cadence proposes.
|
|
1056
|
+
allowedTools: [
|
|
1057
|
+
"Read", "Grep", "Glob", "LS",
|
|
1058
|
+
"design_foundation", "design_voice", "design_list_templates",
|
|
1059
|
+
"design_render_template", "design_export_kit", "design_generate_image",
|
|
1060
|
+
"design_propose_change", "messaging_send",
|
|
1061
|
+
],
|
|
1062
|
+
},
|
|
1063
|
+
};
|
|
1064
|
+
|
|
1065
|
+
// Optional config-driven cadence registry (config/.cadence-registry.json),
|
|
1066
|
+
// written by scripts/setup/generate-cadences.mjs from the agent's function ×
|
|
1067
|
+
// altitude. Lets archetype-specific cadences route without editing this file.
|
|
1068
|
+
// Cached after first read.
|
|
1069
|
+
let _configRegistry = null;
|
|
1070
|
+
function loadConfigRegistry() {
|
|
1071
|
+
if (_configRegistry !== null) return _configRegistry;
|
|
1072
|
+
_configRegistry = {};
|
|
1073
|
+
try {
|
|
1074
|
+
const root = process.env.AGENT_ROOT || process.env.AGENT_DIR || process.cwd();
|
|
1075
|
+
const p = join(root, "config", ".cadence-registry.json");
|
|
1076
|
+
if (existsSync(p)) _configRegistry = JSON.parse(readFileSync(p, "utf-8")) || {};
|
|
1077
|
+
} catch { _configRegistry = {}; }
|
|
1078
|
+
return _configRegistry;
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
/**
|
|
1082
|
+
* Look up a cadence definition. Returns null for unknown cadences so the
|
|
1083
|
+
* consumer can decide between inferred-escalate and DLQ based on prompt file
|
|
1084
|
+
* availability. The hardcoded CADENCE_REGISTRY wins; the config-driven archetype
|
|
1085
|
+
* cadences (config/.cadence-registry.json) fill in the rest.
|
|
1086
|
+
*/
|
|
1087
|
+
export function getCadenceDef(name) {
|
|
1088
|
+
if (CADENCE_REGISTRY[name]) return CADENCE_REGISTRY[name];
|
|
1089
|
+
const cfg = loadConfigRegistry()[name];
|
|
1090
|
+
if (cfg && cfg.prompt) {
|
|
1091
|
+
return { mode: cfg.mode || "escalate", prompt: cfg.prompt, description: "archetype cadence (config-driven)" };
|
|
1092
|
+
}
|
|
1093
|
+
return null;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
/**
|
|
1097
|
+
* Convenience: list all registered cadence names. Used by doctor to confirm
|
|
1098
|
+
* the consumer can route every cadence that the plist generator emits.
|
|
1099
|
+
*/
|
|
1100
|
+
export function listCadenceNames() {
|
|
1101
|
+
return Object.keys(CADENCE_REGISTRY);
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
// Exported for tests (and for the consumer, which already reaches the guard via
|
|
1105
|
+
// CADENCE_REGISTRY). The optional second arg carries injectable seams for the
|
|
1106
|
+
// phase-2 consolidation due-check (clock + fake spawner) without touching the
|
|
1107
|
+
// production path.
|
|
1108
|
+
export { guardSkillCurator, maybeSpawnConsolidation };
|
|
1109
|
+
|
|
1110
|
+
// Economics / scheduling handlers — exported for tests (the consumer reaches
|
|
1111
|
+
// them via CADENCE_REGISTRY). Each takes an optional second arg of injectable
|
|
1112
|
+
// seams (lib fn / clock / cfg) so tests never touch the network or real config.
|
|
1113
|
+
export {
|
|
1114
|
+
handleNightlyCostReconcile,
|
|
1115
|
+
handleFleetCostDigest,
|
|
1116
|
+
handleOrgCostSync,
|
|
1117
|
+
guardDynamicJobs,
|
|
1118
|
+
guardMessagingInbound,
|
|
1119
|
+
guardDirectoryHygiene,
|
|
1120
|
+
guardBrandSteward,
|
|
1121
|
+
};
|