@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,1015 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* maestro.test.mjs — CLI tests for the bin/maestro.mjs commands.
|
|
3
|
+
*
|
|
4
|
+
* Covers:
|
|
5
|
+
* • `create` scaffolds the cadence-bus directory tree, the enqueue
|
|
6
|
+
* script, the consumer, and the cadence handlers under the new agent.
|
|
7
|
+
* • `upgrade` migrates a legacy-style agent repo: backs up old plists,
|
|
8
|
+
* bootstraps state/cadence-bus/, regenerates plists.
|
|
9
|
+
* • `upgrade` preserves agent-specific files (CLAUDE.md, config/,
|
|
10
|
+
* knowledge/, memory/, state/, logs/, outputs/, .env) untouched.
|
|
11
|
+
* • `upgrade` is idempotent when run twice on the same repo.
|
|
12
|
+
* • `doctor` flags legacy plists with actionable remediation.
|
|
13
|
+
*
|
|
14
|
+
* Note: `create` runs `npm install` at the end, which is heavyweight. To
|
|
15
|
+
* keep test runtime reasonable we skip the npm install step by passing
|
|
16
|
+
* an alternate npm shim on PATH that is a no-op. Same trick for git.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { test } from "node:test";
|
|
20
|
+
import assert from "node:assert/strict";
|
|
21
|
+
import { promises as fsp } from "fs";
|
|
22
|
+
import { writeFileSync, readFileSync, existsSync, mkdirSync, readdirSync } from "node:fs";
|
|
23
|
+
import { tmpdir } from "os";
|
|
24
|
+
import { join, resolve, dirname } from "path";
|
|
25
|
+
import { fileURLToPath } from "node:url";
|
|
26
|
+
import { spawnSync, execFileSync } from "node:child_process";
|
|
27
|
+
import { chmodSync, statSync } from "node:fs";
|
|
28
|
+
|
|
29
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
30
|
+
const MAESTRO_ROOT = resolve(__dirname, "..");
|
|
31
|
+
const MAESTRO_CLI = resolve(__dirname, "maestro.mjs");
|
|
32
|
+
|
|
33
|
+
// Pure predicates exported from the CLI module (importing does not dispatch).
|
|
34
|
+
const { evaluateCostTripwire, summariseLedgerRows, evaluateStaleness } = await import(MAESTRO_CLI);
|
|
35
|
+
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// Helpers
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
async function tmpRoot(prefix = "maestro-cli-test") {
|
|
41
|
+
const path = join(
|
|
42
|
+
tmpdir(),
|
|
43
|
+
`${prefix}-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
|
|
44
|
+
);
|
|
45
|
+
await fsp.mkdir(path, { recursive: true });
|
|
46
|
+
return path;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function rmRoot(path) {
|
|
50
|
+
try { await fsp.rm(path, { recursive: true, force: true }); } catch { /* */ }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Build a PATH that shadows `npm` (and optionally `git`) with no-op shims
|
|
55
|
+
* so `create` doesn't actually try to install dependencies. Returns the new
|
|
56
|
+
* env block plus the shim dir (for cleanup).
|
|
57
|
+
*/
|
|
58
|
+
async function withShim() {
|
|
59
|
+
const dir = await tmpRoot("maestro-shim");
|
|
60
|
+
const npm = join(dir, "npm");
|
|
61
|
+
writeFileSync(npm, "#!/bin/bash\necho '[shim npm]' \"$@\" >&2\nexit 0\n");
|
|
62
|
+
await fsp.chmod(npm, 0o755);
|
|
63
|
+
// Keep real git (we want git init to actually init).
|
|
64
|
+
const env = { ...process.env, PATH: `${dir}:${process.env.PATH || ""}` };
|
|
65
|
+
return { env, dir };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function runCli(args, cwd, env) {
|
|
69
|
+
return spawnSync(process.execPath, [MAESTRO_CLI, ...args], {
|
|
70
|
+
cwd: cwd || process.cwd(),
|
|
71
|
+
env: env || process.env,
|
|
72
|
+
encoding: "utf-8",
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Like runCli but pipes `input` to the child's stdin (for `secrets rotate`). */
|
|
77
|
+
function runCliStdin(args, cwd, input, env) {
|
|
78
|
+
return spawnSync(process.execPath, [MAESTRO_CLI, ...args], {
|
|
79
|
+
cwd: cwd || process.cwd(),
|
|
80
|
+
env: env || process.env,
|
|
81
|
+
input,
|
|
82
|
+
encoding: "utf-8",
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
// create
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
|
|
90
|
+
test("create scaffolds cadence-bus dirs and enqueue/consumer/handlers", async () => {
|
|
91
|
+
const parent = await tmpRoot("maestro-create");
|
|
92
|
+
const { env, dir: shim } = await withShim();
|
|
93
|
+
try {
|
|
94
|
+
const r = runCli(["create", "test-agent"], parent, env);
|
|
95
|
+
assert.equal(r.status, 0, r.stderr || r.stdout);
|
|
96
|
+
const agent = join(parent, "test-agent");
|
|
97
|
+
|
|
98
|
+
// Cadence bus directory tree
|
|
99
|
+
for (const d of [
|
|
100
|
+
"state/cadence-bus",
|
|
101
|
+
"state/cadence-bus/inbox",
|
|
102
|
+
"state/cadence-bus/claimed",
|
|
103
|
+
"state/cadence-bus/processed",
|
|
104
|
+
"state/cadence-bus/failed",
|
|
105
|
+
"state/cadence-bus/dlq",
|
|
106
|
+
"logs/cadence-bus",
|
|
107
|
+
]) {
|
|
108
|
+
assert.ok(existsSync(join(agent, d)), `missing ${d}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// New framework files copied into the agent repo.
|
|
112
|
+
for (const f of [
|
|
113
|
+
"lib/cadence-bus.mjs",
|
|
114
|
+
"scripts/cadence/enqueue-cadence-tick.mjs",
|
|
115
|
+
"scripts/cadence/launchd-cadence-wrapper.sh",
|
|
116
|
+
"scripts/cadence/cadence-status.mjs",
|
|
117
|
+
"scripts/daemon/cadence-consumer.mjs",
|
|
118
|
+
"scripts/daemon/cadence-handlers.mjs",
|
|
119
|
+
]) {
|
|
120
|
+
assert.ok(existsSync(join(agent, f)), `missing ${f}`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Agent package.json has the cadence:* scripts.
|
|
124
|
+
const pkg = JSON.parse(readFileSync(join(agent, "package.json"), "utf-8"));
|
|
125
|
+
assert.ok(pkg.scripts["cadence:enqueue"]);
|
|
126
|
+
assert.ok(pkg.scripts["cadence:consume"]);
|
|
127
|
+
assert.ok(pkg.scripts["cadence:status"]);
|
|
128
|
+
} finally {
|
|
129
|
+
await rmRoot(parent);
|
|
130
|
+
await rmRoot(shim);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
// upgrade — migration
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
|
|
138
|
+
async function makeLegacyAgent() {
|
|
139
|
+
const root = await tmpRoot("maestro-legacy");
|
|
140
|
+
// Minimal identity so `upgrade` recognises this as a Maestro repo.
|
|
141
|
+
mkdirSync(join(root, "config"), { recursive: true });
|
|
142
|
+
writeFileSync(join(root, "CLAUDE.md"), "# legacy agent\n");
|
|
143
|
+
writeFileSync(join(root, "config/agent.ts"), "export const AGENT = { firstName: 'legacy' };\n");
|
|
144
|
+
writeFileSync(join(root, "package.json"), '{"name":"legacy-agent","version":"1.0.0"}\n');
|
|
145
|
+
// Personal/local agent state that must NOT be touched.
|
|
146
|
+
mkdirSync(join(root, "knowledge/decisions"), { recursive: true });
|
|
147
|
+
mkdirSync(join(root, "memory/interactions"), { recursive: true });
|
|
148
|
+
mkdirSync(join(root, "state/queues"), { recursive: true });
|
|
149
|
+
mkdirSync(join(root, "outputs"), { recursive: true });
|
|
150
|
+
mkdirSync(join(root, "logs"), { recursive: true });
|
|
151
|
+
writeFileSync(join(root, "knowledge/decisions/local-decision.md"), "# do not touch");
|
|
152
|
+
writeFileSync(join(root, "memory/interactions/local-memory.yaml"), "# do not touch");
|
|
153
|
+
writeFileSync(join(root, "state/queues/action-stack.yaml"), "items:\n - id: P1\n title: local item\n");
|
|
154
|
+
writeFileSync(join(root, "outputs/local-output.md"), "# my draft");
|
|
155
|
+
writeFileSync(join(root, "logs/local.log"), "audit data");
|
|
156
|
+
writeFileSync(join(root, ".env"), "ANTHROPIC_API_KEY=sk-x\n");
|
|
157
|
+
|
|
158
|
+
// Legacy plist that calls run-trigger.sh directly.
|
|
159
|
+
mkdirSync(join(root, "scripts/local-triggers/plists"), { recursive: true });
|
|
160
|
+
writeFileSync(
|
|
161
|
+
join(root, "scripts/local-triggers/plists/ai.adaptic.legacy-inbox-processor.plist"),
|
|
162
|
+
`<?xml version="1.0"?><plist><dict>
|
|
163
|
+
<key>ProgramArguments</key><array>
|
|
164
|
+
<string>/foo/scripts/local-triggers/run-trigger.sh</string>
|
|
165
|
+
<string>inbox-processor</string>
|
|
166
|
+
</array></dict></plist>`
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
// Init git so smart-merge sees the working tree as clean.
|
|
170
|
+
execFileSync("git", ["init", "-q"], { cwd: root });
|
|
171
|
+
execFileSync("git", ["add", "-A"], { cwd: root });
|
|
172
|
+
execFileSync(
|
|
173
|
+
"git",
|
|
174
|
+
["-c", "user.email=test@test", "-c", "user.name=test", "commit", "-q", "-m", "init"],
|
|
175
|
+
{ cwd: root },
|
|
176
|
+
);
|
|
177
|
+
return root;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
test("upgrade backs up legacy plists and regenerates with cadence-bus marker", async () => {
|
|
181
|
+
const root = await makeLegacyAgent();
|
|
182
|
+
try {
|
|
183
|
+
const r = runCli(["upgrade"], root);
|
|
184
|
+
assert.equal(r.status, 0, r.stderr);
|
|
185
|
+
// Backup directory exists.
|
|
186
|
+
const backupRoot = join(root, ".maestro/backup/plists");
|
|
187
|
+
assert.ok(existsSync(backupRoot), "backup root must exist");
|
|
188
|
+
const stamps = readdirSync(backupRoot);
|
|
189
|
+
assert.ok(stamps.length >= 1);
|
|
190
|
+
const backedUp = readdirSync(join(backupRoot, stamps[0]));
|
|
191
|
+
assert.ok(backedUp.some((n) => /legacy-inbox-processor\.plist$/.test(n)));
|
|
192
|
+
|
|
193
|
+
// After regeneration, plists carry the cadence-bus marker and none
|
|
194
|
+
// reference run-trigger.sh.
|
|
195
|
+
const plistDir = join(root, "scripts/local-triggers/plists");
|
|
196
|
+
const plists = readdirSync(plistDir).filter((n) => n.endsWith(".plist"));
|
|
197
|
+
assert.ok(plists.length >= 1);
|
|
198
|
+
let legacy = 0;
|
|
199
|
+
let modern = 0;
|
|
200
|
+
for (const name of plists) {
|
|
201
|
+
const body = readFileSync(join(plistDir, name), "utf-8");
|
|
202
|
+
if (body.includes("run-trigger.sh")) legacy++;
|
|
203
|
+
if (body.includes("maestro-plist-arch: cadence-bus")) modern++;
|
|
204
|
+
}
|
|
205
|
+
assert.equal(legacy, 0, "no legacy plists should remain post-upgrade");
|
|
206
|
+
assert.equal(modern, plists.length, "every plist must carry the cadence-bus marker");
|
|
207
|
+
} finally { await rmRoot(root); }
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test("upgrade preserves agent-specific files untouched", async () => {
|
|
211
|
+
const root = await makeLegacyAgent();
|
|
212
|
+
try {
|
|
213
|
+
const before = {
|
|
214
|
+
claudemd: readFileSync(join(root, "CLAUDE.md"), "utf-8"),
|
|
215
|
+
agentTs: readFileSync(join(root, "config/agent.ts"), "utf-8"),
|
|
216
|
+
decision: readFileSync(join(root, "knowledge/decisions/local-decision.md"), "utf-8"),
|
|
217
|
+
memory: readFileSync(join(root, "memory/interactions/local-memory.yaml"), "utf-8"),
|
|
218
|
+
queue: readFileSync(join(root, "state/queues/action-stack.yaml"), "utf-8"),
|
|
219
|
+
output: readFileSync(join(root, "outputs/local-output.md"), "utf-8"),
|
|
220
|
+
log: readFileSync(join(root, "logs/local.log"), "utf-8"),
|
|
221
|
+
env: readFileSync(join(root, ".env"), "utf-8"),
|
|
222
|
+
};
|
|
223
|
+
const r = runCli(["upgrade"], root);
|
|
224
|
+
assert.equal(r.status, 0, r.stderr);
|
|
225
|
+
for (const [k, v] of Object.entries(before)) {
|
|
226
|
+
const path = ({
|
|
227
|
+
claudemd: "CLAUDE.md",
|
|
228
|
+
agentTs: "config/agent.ts",
|
|
229
|
+
decision: "knowledge/decisions/local-decision.md",
|
|
230
|
+
memory: "memory/interactions/local-memory.yaml",
|
|
231
|
+
queue: "state/queues/action-stack.yaml",
|
|
232
|
+
output: "outputs/local-output.md",
|
|
233
|
+
log: "logs/local.log",
|
|
234
|
+
env: ".env",
|
|
235
|
+
})[k];
|
|
236
|
+
assert.equal(
|
|
237
|
+
readFileSync(join(root, path), "utf-8"),
|
|
238
|
+
v,
|
|
239
|
+
`${path} was modified by upgrade`,
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
} finally { await rmRoot(root); }
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test("upgrade is idempotent (second run is a clean no-op for plists)", async () => {
|
|
246
|
+
const root = await makeLegacyAgent();
|
|
247
|
+
try {
|
|
248
|
+
const first = runCli(["upgrade"], root);
|
|
249
|
+
assert.equal(first.status, 0);
|
|
250
|
+
// Snapshot the regenerated plist contents.
|
|
251
|
+
const plistDir = join(root, "scripts/local-triggers/plists");
|
|
252
|
+
const before = {};
|
|
253
|
+
for (const name of readdirSync(plistDir)) {
|
|
254
|
+
before[name] = readFileSync(join(plistDir, name), "utf-8");
|
|
255
|
+
}
|
|
256
|
+
const second = runCli(["upgrade"], root);
|
|
257
|
+
assert.equal(second.status, 0);
|
|
258
|
+
// The second run should NOT introduce any new backup directory beyond
|
|
259
|
+
// what the first run created, because there are no legacy plists left.
|
|
260
|
+
const backups = readdirSync(join(root, ".maestro/backup/plists"));
|
|
261
|
+
assert.equal(backups.length, 1, "no new backup directory on idempotent run");
|
|
262
|
+
// Plist contents byte-identical.
|
|
263
|
+
for (const [name, body] of Object.entries(before)) {
|
|
264
|
+
assert.equal(readFileSync(join(plistDir, name), "utf-8"), body, `${name} changed`);
|
|
265
|
+
}
|
|
266
|
+
} finally { await rmRoot(root); }
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
test("upgrade --dry-run reports legacy without modifying anything", async () => {
|
|
270
|
+
const root = await makeLegacyAgent();
|
|
271
|
+
try {
|
|
272
|
+
// Snapshot a known sentinel file.
|
|
273
|
+
const sentinel = readFileSync(join(root, "scripts/local-triggers/plists/ai.adaptic.legacy-inbox-processor.plist"), "utf-8");
|
|
274
|
+
const r = runCli(["upgrade", "--dry-run"], root);
|
|
275
|
+
assert.equal(r.status, 0);
|
|
276
|
+
assert.match(r.stdout, /still call run-trigger\.sh/);
|
|
277
|
+
// Sentinel must not have changed.
|
|
278
|
+
assert.equal(
|
|
279
|
+
readFileSync(join(root, "scripts/local-triggers/plists/ai.adaptic.legacy-inbox-processor.plist"), "utf-8"),
|
|
280
|
+
sentinel,
|
|
281
|
+
);
|
|
282
|
+
// No backup directory created.
|
|
283
|
+
assert.ok(!existsSync(join(root, ".maestro/backup")), "dry-run must not create backups");
|
|
284
|
+
} finally { await rmRoot(root); }
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// ---------------------------------------------------------------------------
|
|
288
|
+
// doctor
|
|
289
|
+
// ---------------------------------------------------------------------------
|
|
290
|
+
|
|
291
|
+
test("doctor flags legacy plists with actionable remediation", async () => {
|
|
292
|
+
const root = await makeLegacyAgent();
|
|
293
|
+
try {
|
|
294
|
+
const r = runCli(["doctor"], root);
|
|
295
|
+
// doctor exits 1 when issues are found; that's expected here.
|
|
296
|
+
assert.ok(r.status >= 0);
|
|
297
|
+
const combined = (r.stdout || "") + (r.stderr || "");
|
|
298
|
+
assert.match(combined, /run-trigger\.sh/);
|
|
299
|
+
assert.match(combined, /maestro upgrade/);
|
|
300
|
+
} finally { await rmRoot(root); }
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
test("doctor recognises new cadence-bus architecture after upgrade", async () => {
|
|
304
|
+
const root = await makeLegacyAgent();
|
|
305
|
+
try {
|
|
306
|
+
runCli(["upgrade"], root);
|
|
307
|
+
const r = runCli(["doctor"], root);
|
|
308
|
+
const combined = (r.stdout || "") + (r.stderr || "");
|
|
309
|
+
assert.match(combined, /cadence-bus architecture/);
|
|
310
|
+
// 'plist(s) still call run-trigger.sh' must NOT appear.
|
|
311
|
+
assert.ok(!combined.includes("plist(s) still call run-trigger.sh"));
|
|
312
|
+
} finally { await rmRoot(root); }
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
// ---------------------------------------------------------------------------
|
|
316
|
+
// setup (WS1 wizard) — CLI surface
|
|
317
|
+
// ---------------------------------------------------------------------------
|
|
318
|
+
|
|
319
|
+
test("setup --help runs in a non-agent dir without throwing (exit 0)", async () => {
|
|
320
|
+
const dir = await tmpRoot("maestro-setup-help");
|
|
321
|
+
try {
|
|
322
|
+
const r = runCli(["setup", "--help"], dir);
|
|
323
|
+
assert.equal(r.status, 0, r.stderr);
|
|
324
|
+
assert.match(r.stdout, /maestro setup/);
|
|
325
|
+
assert.match(r.stdout, /--headless/);
|
|
326
|
+
} finally { await rmRoot(dir); }
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
test("setup --status in a non-agent dir prints a graceful message (exit 0)", async () => {
|
|
330
|
+
const dir = await tmpRoot("maestro-setup-status");
|
|
331
|
+
try {
|
|
332
|
+
const r = runCli(["setup", "--status"], dir);
|
|
333
|
+
assert.equal(r.status, 0, r.stderr);
|
|
334
|
+
const out = (r.stdout || "") + (r.stderr || "");
|
|
335
|
+
assert.match(out, /Not a Maestro agent directory/);
|
|
336
|
+
} finally { await rmRoot(dir); }
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
test("setup --status in an agent dir lists section detect() statuses", async () => {
|
|
340
|
+
const dir = await tmpRoot("maestro-setup-agent");
|
|
341
|
+
try {
|
|
342
|
+
mkdirSync(join(dir, "config"), { recursive: true });
|
|
343
|
+
// Minimal SoT so the agent-repo guard passes + sections can detect.
|
|
344
|
+
writeFileSync(join(dir, "config/agent.json"), JSON.stringify({
|
|
345
|
+
firstName: "UNCONFIGURED", lastName: "AGENT", title: "Unconfigured Role", email: "",
|
|
346
|
+
function: "executive-operator", altitude: "c-suite",
|
|
347
|
+
}, null, 2));
|
|
348
|
+
const r = runCli(["setup", "--status"], dir);
|
|
349
|
+
assert.equal(r.status, 0, r.stderr);
|
|
350
|
+
const out = (r.stdout || "") + (r.stderr || "");
|
|
351
|
+
assert.match(out, /identity/);
|
|
352
|
+
assert.match(out, /operating-model/);
|
|
353
|
+
assert.match(out, /verify/);
|
|
354
|
+
} finally { await rmRoot(dir); }
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
// ---------------------------------------------------------------------------
|
|
358
|
+
// doctor cost-telemetry tripwire — pure predicate (observability F2)
|
|
359
|
+
// ---------------------------------------------------------------------------
|
|
360
|
+
|
|
361
|
+
test("summariseLedgerRows sums estimated_usd and counts rows; tolerates junk", () => {
|
|
362
|
+
const s = summariseLedgerRows([
|
|
363
|
+
{ estimated_usd: 0.5 },
|
|
364
|
+
{ estimated_usd: 1.25 },
|
|
365
|
+
{ estimated_usd: "0.25" }, // numeric string coerced
|
|
366
|
+
{ estimated_usd: NaN }, // ignored as 0
|
|
367
|
+
{}, // counts as a row, $0
|
|
368
|
+
null, // skipped entirely
|
|
369
|
+
"garbage", // skipped entirely
|
|
370
|
+
]);
|
|
371
|
+
assert.equal(s.rowCount, 5, "only object rows count");
|
|
372
|
+
assert.equal(s.totalUsd, 2, "0.5 + 1.25 + 0.25 = 2.00");
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
test("summariseLedgerRows on empty input is zeroed", () => {
|
|
376
|
+
assert.deepEqual(summariseLedgerRows([]), { rowCount: 0, totalUsd: 0 });
|
|
377
|
+
assert.deepEqual(summariseLedgerRows(undefined), { rowCount: 0, totalUsd: 0 });
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
test("evaluateCostTripwire: rows exist but spend is $0 → RED (telemetry blind)", () => {
|
|
381
|
+
const t = evaluateCostTripwire({
|
|
382
|
+
rows: [{ estimated_usd: 0 }, { estimated_usd: 0 }, { estimated_usd: 0 }],
|
|
383
|
+
sessionCount: 0,
|
|
384
|
+
});
|
|
385
|
+
assert.equal(t.red, true);
|
|
386
|
+
assert.equal(t.status, "red");
|
|
387
|
+
assert.equal(t.rowCount, 3);
|
|
388
|
+
assert.equal(t.totalUsd, 0);
|
|
389
|
+
assert.match(t.reason, /blind/i);
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
test("evaluateCostTripwire: no rows but heartbeat-active session → RED", () => {
|
|
393
|
+
// Daemon is live (heartbeat) but the ledger is empty → still blind.
|
|
394
|
+
const t = evaluateCostTripwire({ rows: [], sessionCount: 1 });
|
|
395
|
+
assert.equal(t.red, true);
|
|
396
|
+
assert.equal(t.status, "red");
|
|
397
|
+
assert.match(t.reason, /blind/i);
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
test("evaluateCostTripwire: no sessions and no rows → GREEN (nothing to measure)", () => {
|
|
401
|
+
const t = evaluateCostTripwire({ rows: [], sessionCount: 0 });
|
|
402
|
+
assert.equal(t.red, false);
|
|
403
|
+
assert.equal(t.status, "green");
|
|
404
|
+
assert.match(t.reason, /nothing to measure/i);
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
test("evaluateCostTripwire: spend > 0 → GREEN even with sessions", () => {
|
|
408
|
+
const t = evaluateCostTripwire({
|
|
409
|
+
rows: [{ estimated_usd: 0 }, { estimated_usd: 0.42 }],
|
|
410
|
+
sessionCount: 1,
|
|
411
|
+
});
|
|
412
|
+
assert.equal(t.red, false);
|
|
413
|
+
assert.equal(t.status, "green");
|
|
414
|
+
assert.equal(t.totalUsd, 0.42);
|
|
415
|
+
assert.match(t.reason, /live/i);
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
test("evaluateCostTripwire: defaults (no args) are GREEN, not a false alarm", () => {
|
|
419
|
+
const t = evaluateCostTripwire();
|
|
420
|
+
assert.equal(t.red, false);
|
|
421
|
+
assert.equal(t.status, "green");
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
// ---------------------------------------------------------------------------
|
|
425
|
+
// doctor cost-telemetry tripwire — end-to-end against a real ledger file
|
|
426
|
+
// ---------------------------------------------------------------------------
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Minimal agent repo that doctor can run against without exploding before our
|
|
430
|
+
* checks. doctor exits 1 on any issue (a bare repo has many), so these tests
|
|
431
|
+
* assert on the specific lines our checks emit rather than the exit code.
|
|
432
|
+
*/
|
|
433
|
+
async function makeBareAgent() {
|
|
434
|
+
const root = await tmpRoot("maestro-doctor");
|
|
435
|
+
mkdirSync(join(root, "config"), { recursive: true });
|
|
436
|
+
writeFileSync(join(root, "config/agent.json"), JSON.stringify({
|
|
437
|
+
firstName: "test", function: "executive-operator", altitude: "c-suite",
|
|
438
|
+
}, null, 2));
|
|
439
|
+
writeFileSync(join(root, "CLAUDE.md"), "# test agent\n");
|
|
440
|
+
return root;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function utcDate() { return new Date().toISOString().slice(0, 10); }
|
|
444
|
+
|
|
445
|
+
test("doctor RED when today's ledger has rows but $0 spend", async () => {
|
|
446
|
+
const root = await makeBareAgent();
|
|
447
|
+
try {
|
|
448
|
+
mkdirSync(join(root, "state/cost-tracking"), { recursive: true });
|
|
449
|
+
const ledger = join(root, "state/cost-tracking", `${utcDate()}.jsonl`);
|
|
450
|
+
writeFileSync(ledger,
|
|
451
|
+
JSON.stringify({ ts: new Date().toISOString(), cadence: "inbox-processor", model: "sonnet", input_tokens: 0, output_tokens: 0, estimated_usd: 0 }) + "\n" +
|
|
452
|
+
JSON.stringify({ ts: new Date().toISOString(), cadence: "backlog-executor", model: "opus", input_tokens: 0, output_tokens: 0, estimated_usd: 0 }) + "\n"
|
|
453
|
+
);
|
|
454
|
+
const r = runCli(["doctor"], root);
|
|
455
|
+
const out = (r.stdout || "") + (r.stderr || "");
|
|
456
|
+
assert.match(out, /Cost telemetry BLIND/);
|
|
457
|
+
assert.match(out, /track-claude-usage\.mjs/);
|
|
458
|
+
} finally { await rmRoot(root); }
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
test("doctor GREEN when today's ledger records real spend", async () => {
|
|
462
|
+
const root = await makeBareAgent();
|
|
463
|
+
try {
|
|
464
|
+
mkdirSync(join(root, "state/cost-tracking"), { recursive: true });
|
|
465
|
+
const ledger = join(root, "state/cost-tracking", `${utcDate()}.jsonl`);
|
|
466
|
+
writeFileSync(ledger,
|
|
467
|
+
JSON.stringify({ ts: new Date().toISOString(), cadence: "inbox-processor", model: "sonnet", input_tokens: 1500, output_tokens: 320, estimated_usd: 0.0093 }) + "\n"
|
|
468
|
+
);
|
|
469
|
+
const r = runCli(["doctor"], root);
|
|
470
|
+
const out = (r.stdout || "") + (r.stderr || "");
|
|
471
|
+
assert.match(out, /Cost telemetry live/);
|
|
472
|
+
assert.ok(!out.includes("Cost telemetry BLIND"), "must not flag blind when spend>0");
|
|
473
|
+
} finally { await rmRoot(root); }
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
test("doctor GREEN (nothing to measure) when no ledger exists today", async () => {
|
|
477
|
+
const root = await makeBareAgent();
|
|
478
|
+
try {
|
|
479
|
+
const r = runCli(["doctor"], root);
|
|
480
|
+
const out = (r.stdout || "") + (r.stderr || "");
|
|
481
|
+
assert.match(out, /no sessions recorded today/);
|
|
482
|
+
} finally { await rmRoot(root); }
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
// ---------------------------------------------------------------------------
|
|
486
|
+
// doctor .env permission check (setup-onboarding W4)
|
|
487
|
+
// ---------------------------------------------------------------------------
|
|
488
|
+
|
|
489
|
+
test("doctor flags a group/world-readable .env with the chmod fix", { skip: typeof process.getuid !== "function" }, async () => {
|
|
490
|
+
const root = await makeBareAgent();
|
|
491
|
+
try {
|
|
492
|
+
const envPath = join(root, ".env");
|
|
493
|
+
writeFileSync(envPath, "ANTHROPIC_API_KEY=sk-secret\n");
|
|
494
|
+
chmodSync(envPath, 0o644);
|
|
495
|
+
const r = runCli(["doctor"], root);
|
|
496
|
+
const out = (r.stdout || "") + (r.stderr || "");
|
|
497
|
+
assert.match(out, /\.env is mode 644/);
|
|
498
|
+
assert.match(out, /chmod 600 \.env/);
|
|
499
|
+
} finally { await rmRoot(root); }
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
test("doctor passes a chmod-600 .env", { skip: typeof process.getuid !== "function" }, async () => {
|
|
503
|
+
const root = await makeBareAgent();
|
|
504
|
+
try {
|
|
505
|
+
const envPath = join(root, ".env");
|
|
506
|
+
writeFileSync(envPath, "ANTHROPIC_API_KEY=sk-secret\n");
|
|
507
|
+
chmodSync(envPath, 0o600);
|
|
508
|
+
const r = runCli(["doctor"], root);
|
|
509
|
+
const out = (r.stdout || "") + (r.stderr || "");
|
|
510
|
+
assert.match(out, /\.env permissions are owner-only/);
|
|
511
|
+
assert.ok(!out.includes(".env is mode"), "must not flag a 600 .env");
|
|
512
|
+
} finally { await rmRoot(root); }
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
// ---------------------------------------------------------------------------
|
|
516
|
+
// evaluateStaleness (pure) — the router data-file staleness ladder (graft #18)
|
|
517
|
+
// ---------------------------------------------------------------------------
|
|
518
|
+
|
|
519
|
+
test("evaluateStaleness: fresh data is green", () => {
|
|
520
|
+
const now = Date.UTC(2026, 5, 11);
|
|
521
|
+
const v = evaluateStaleness({ fetched: "2026-06-10", now });
|
|
522
|
+
assert.equal(v.status, "green");
|
|
523
|
+
assert.equal(v.ageDays, 1);
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
test("evaluateStaleness: >7d but <30d is a WARN", () => {
|
|
527
|
+
const now = Date.UTC(2026, 5, 30);
|
|
528
|
+
const v = evaluateStaleness({ fetched: "2026-06-10", now });
|
|
529
|
+
assert.equal(v.status, "warn");
|
|
530
|
+
assert.ok(v.ageDays >= 7 && v.ageDays < 30);
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
test("evaluateStaleness: >=30d is a FAIL", () => {
|
|
534
|
+
const now = Date.UTC(2026, 6, 15);
|
|
535
|
+
const v = evaluateStaleness({ fetched: "2026-06-10", now });
|
|
536
|
+
assert.equal(v.status, "fail");
|
|
537
|
+
assert.ok(v.ageDays >= 30);
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
test("evaluateStaleness: a stale volatile catalog escalates the FAIL reason", () => {
|
|
541
|
+
const now = Date.UTC(2026, 6, 15);
|
|
542
|
+
const v = evaluateStaleness({ fetched: "2026-06-10", now, hasVolatile: true, label: "catalog" });
|
|
543
|
+
assert.equal(v.status, "fail");
|
|
544
|
+
assert.match(v.reason, /volatile/);
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
test("evaluateStaleness: a missing fetch date is a WARN (provenance unknown)", () => {
|
|
548
|
+
const v = evaluateStaleness({ fetched: null });
|
|
549
|
+
assert.equal(v.status, "warn");
|
|
550
|
+
assert.match(v.reason, /no fetch date/);
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
// ---------------------------------------------------------------------------
|
|
554
|
+
// router CLI — `maestro router why` / `maestro router validate`
|
|
555
|
+
// ---------------------------------------------------------------------------
|
|
556
|
+
|
|
557
|
+
const V2_ROUTER_YAML = `
|
|
558
|
+
schema_version: 2
|
|
559
|
+
aliases:
|
|
560
|
+
frontier: anthropic/claude-opus-4-8
|
|
561
|
+
default: anthropic/claude-sonnet-4-6
|
|
562
|
+
fast: anthropic/claude-haiku-4-5
|
|
563
|
+
cheap: deepseek/deepseek-v4-flash
|
|
564
|
+
defaults:
|
|
565
|
+
data_class: sensitive
|
|
566
|
+
backends:
|
|
567
|
+
anthropic: { allowed_data_classes: [public, internal, sensitive] }
|
|
568
|
+
deepseek: { allowed_data_classes: [public] }
|
|
569
|
+
routing_policy:
|
|
570
|
+
- match: { task_class: classify.inbox }
|
|
571
|
+
harness: direct
|
|
572
|
+
chain: [fast, cheap, rules]
|
|
573
|
+
needs_tool_use: false
|
|
574
|
+
- default: true
|
|
575
|
+
chain: [default, fast]
|
|
576
|
+
fallback_to_anthropic: true
|
|
577
|
+
`;
|
|
578
|
+
|
|
579
|
+
async function makeV2RouterAgent() {
|
|
580
|
+
const root = await makeBareAgent();
|
|
581
|
+
writeFileSync(join(root, "config/model-routing.yaml"), V2_ROUTER_YAML);
|
|
582
|
+
return root;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
test("router why: prints chosen + explain + chain for a session request", async () => {
|
|
586
|
+
const root = await makeV2RouterAgent();
|
|
587
|
+
try {
|
|
588
|
+
const r = runCli(["router", "why", "session.responder"], root);
|
|
589
|
+
const out = (r.stdout || "") + (r.stderr || "");
|
|
590
|
+
assert.match(out, /chosen/);
|
|
591
|
+
assert.match(out, /anthropic\/claude-sonnet-4-6/);
|
|
592
|
+
assert.match(out, /explain/);
|
|
593
|
+
assert.match(out, /chain/);
|
|
594
|
+
} finally { await rmRoot(root); }
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
test("router why: accepts inline request JSON and reports tried[] skips", async () => {
|
|
598
|
+
const root = await makeV2RouterAgent();
|
|
599
|
+
try {
|
|
600
|
+
const r = runCli(
|
|
601
|
+
["router", "why", '{"task_class":"classify.inbox","data_class":"public"}'],
|
|
602
|
+
root
|
|
603
|
+
);
|
|
604
|
+
const out = (r.stdout || "") + (r.stderr || "");
|
|
605
|
+
// No keys present → cheap deepseek + anthropic haiku-direct are unreachable;
|
|
606
|
+
// the deterministic "rules" fallback wins and the skips are listed.
|
|
607
|
+
assert.match(out, /tried/);
|
|
608
|
+
assert.match(out, /missing_credential/);
|
|
609
|
+
} finally { await rmRoot(root); }
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
test("router validate: a clean v2 config validates (exit 0)", async () => {
|
|
613
|
+
const root = await makeV2RouterAgent();
|
|
614
|
+
try {
|
|
615
|
+
const r = runCli(["router", "validate"], root);
|
|
616
|
+
const out = (r.stdout || "") + (r.stderr || "");
|
|
617
|
+
assert.equal(r.status, 0, out);
|
|
618
|
+
assert.match(out, /valid/);
|
|
619
|
+
} finally { await rmRoot(root); }
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
test("router validate: an unknown match key fails validation (exit 1)", async () => {
|
|
623
|
+
const root = await makeBareAgent();
|
|
624
|
+
try {
|
|
625
|
+
writeFileSync(
|
|
626
|
+
join(root, "config/model-routing.yaml"),
|
|
627
|
+
"schema_version: 2\naliases:\n default: anthropic/claude-sonnet-4-6\nrouting_policy:\n - match: { taks_class: x }\n chain: [default]\n - default: true\n chain: [default]\n"
|
|
628
|
+
);
|
|
629
|
+
const r = runCli(["router", "validate"], root);
|
|
630
|
+
const out = (r.stdout || "") + (r.stderr || "");
|
|
631
|
+
assert.equal(r.status, 1, out);
|
|
632
|
+
assert.match(out, /unknown match key/);
|
|
633
|
+
} finally { await rmRoot(root); }
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
test("router help: bare `router` prints usage", async () => {
|
|
637
|
+
const root = await makeBareAgent();
|
|
638
|
+
try {
|
|
639
|
+
const r = runCli(["router"], root);
|
|
640
|
+
const out = (r.stdout || "") + (r.stderr || "");
|
|
641
|
+
assert.match(out, /router why/);
|
|
642
|
+
assert.match(out, /router validate/);
|
|
643
|
+
} finally { await rmRoot(root); }
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
// ---------------------------------------------------------------------------
|
|
647
|
+
// audit — security-posture audit + attestation (G15)
|
|
648
|
+
// ---------------------------------------------------------------------------
|
|
649
|
+
|
|
650
|
+
const GOOD_AUDIT_SETTINGS = JSON.stringify({
|
|
651
|
+
hooks: {
|
|
652
|
+
PreToolUse: [
|
|
653
|
+
{ matcher: "mcp__claude_ai_Slack__slack_send_message", hooks: [{ type: "command", command: "./scripts/hooks/block-mcp-slack-send.sh" }] },
|
|
654
|
+
{ matcher: "mcp__claude_ai_Gmail__gmail_create_draft", hooks: [{ type: "command", command: "./scripts/hooks/pre-send-audit.sh gmail" }] },
|
|
655
|
+
],
|
|
656
|
+
PostToolUse: [{ matcher: "", hooks: [{ type: "command", command: "./scripts/hooks/post-action-log.sh" }] }],
|
|
657
|
+
Stop: [{ matcher: "", hooks: [{ type: "command", command: "./scripts/hooks/session-end-log.sh" }] }],
|
|
658
|
+
},
|
|
659
|
+
}, null, 2);
|
|
660
|
+
|
|
661
|
+
/** A conformant agent repo (every audit check passes when scoped-perms is set). */
|
|
662
|
+
async function makeConformantAgent() {
|
|
663
|
+
const root = await makeBareAgent();
|
|
664
|
+
mkdirSync(join(root, ".claude"), { recursive: true });
|
|
665
|
+
mkdirSync(join(root, "scripts/hooks"), { recursive: true });
|
|
666
|
+
mkdirSync(join(root, ".maestro"), { recursive: true });
|
|
667
|
+
mkdirSync(join(root, "policies"), { recursive: true });
|
|
668
|
+
// config/agent.json already written by makeBareAgent.
|
|
669
|
+
const envPath = join(root, ".env");
|
|
670
|
+
writeFileSync(envPath, "ANTHROPIC_API_KEY=sk-test\nMAESTRO_SCOPED_PERMISSIONS=1\n");
|
|
671
|
+
chmodSync(envPath, 0o600);
|
|
672
|
+
writeFileSync(join(root, ".claude/settings.json"), GOOD_AUDIT_SETTINGS);
|
|
673
|
+
for (const s of ["block-mcp-slack-send.sh", "pre-send-audit.sh", "post-action-log.sh", "session-end-log.sh"]) {
|
|
674
|
+
writeFileSync(join(root, "scripts/hooks", s), "#!/bin/bash\nexit 0\n");
|
|
675
|
+
}
|
|
676
|
+
writeFileSync(join(root, ".maestro/backup-config.yaml"), "enabled: true\nprovider: s3\nbucket: b\n");
|
|
677
|
+
writeFileSync(join(root, "config/validation-rules.yaml"), "ai_disclosure:\n enabled: true\n severity: block\n");
|
|
678
|
+
writeFileSync(join(root, "policies/action-classification.yaml"), "classes: []\n");
|
|
679
|
+
return root;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
test("audit: a conformant agent exits 0 and reports posture conformant", async () => {
|
|
683
|
+
const root = await makeConformantAgent();
|
|
684
|
+
try {
|
|
685
|
+
const r = runCli(["audit"], root);
|
|
686
|
+
const out = (r.stdout || "") + (r.stderr || "");
|
|
687
|
+
assert.equal(r.status, 0, out);
|
|
688
|
+
assert.match(out, /checks passed/);
|
|
689
|
+
assert.match(out, /attestation:/);
|
|
690
|
+
assert.match(out, /Posture conformant/);
|
|
691
|
+
} finally { await rmRoot(root); }
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
test("audit: a group-readable .env makes the run exit non-zero with the fix", async () => {
|
|
695
|
+
const root = await makeConformantAgent();
|
|
696
|
+
try {
|
|
697
|
+
chmodSync(join(root, ".env"), 0o644);
|
|
698
|
+
const r = runCli(["audit"], root);
|
|
699
|
+
const out = (r.stdout || "") + (r.stderr || "");
|
|
700
|
+
assert.equal(r.status, 1, out);
|
|
701
|
+
assert.match(out, /env-mode-600/);
|
|
702
|
+
assert.match(out, /chmod 600/);
|
|
703
|
+
assert.match(out, /unsuppressed failure/);
|
|
704
|
+
} finally { await rmRoot(root); }
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
test("audit --fix: repairs a group-readable .env and then exits 0", async () => {
|
|
708
|
+
const root = await makeConformantAgent();
|
|
709
|
+
try {
|
|
710
|
+
chmodSync(join(root, ".env"), 0o644);
|
|
711
|
+
const r = runCli(["audit", "--fix"], root);
|
|
712
|
+
const out = (r.stdout || "") + (r.stderr || "");
|
|
713
|
+
assert.equal(r.status, 0, out);
|
|
714
|
+
assert.match(out, /applied 1 safe fix/);
|
|
715
|
+
assert.equal(statSync(join(root, ".env")).mode & 0o777, 0o600);
|
|
716
|
+
} finally { await rmRoot(root); }
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
test("audit: a suppressed-with-reason failure exits 0", async () => {
|
|
720
|
+
const root = await makeConformantAgent();
|
|
721
|
+
try {
|
|
722
|
+
chmodSync(join(root, ".env"), 0o644); // would fail env-mode-600
|
|
723
|
+
writeFileSync(join(root, "config/audit-suppressions.yaml"),
|
|
724
|
+
"- id: env-mode-600\n reason: ephemeral CI runner\n");
|
|
725
|
+
const r = runCli(["audit"], root);
|
|
726
|
+
const out = (r.stdout || "") + (r.stderr || "");
|
|
727
|
+
assert.equal(r.status, 0, out);
|
|
728
|
+
assert.match(out, /SUPPRESSED/);
|
|
729
|
+
assert.match(out, /ephemeral CI runner/);
|
|
730
|
+
} finally { await rmRoot(root); }
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
test("audit --json: emits structured results + a hash-locked attestation", async () => {
|
|
734
|
+
const root = await makeConformantAgent();
|
|
735
|
+
try {
|
|
736
|
+
const r = runCli(["audit", "--json"], root);
|
|
737
|
+
assert.equal(r.status, 0, r.stderr || r.stdout);
|
|
738
|
+
const doc = JSON.parse(r.stdout);
|
|
739
|
+
assert.equal(doc.ok, true);
|
|
740
|
+
assert.ok(Array.isArray(doc.results) && doc.results.length >= 8);
|
|
741
|
+
assert.equal(typeof doc.attestation.hash, "string");
|
|
742
|
+
assert.equal(doc.attestation.schema, "maestro.audit.attestation.v1");
|
|
743
|
+
assert.equal(doc.attestation.identity.function, "executive-operator");
|
|
744
|
+
} finally { await rmRoot(root); }
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
test("audit --json: missing scoped-permissions makes ok:false and exit 1", async () => {
|
|
748
|
+
const root = await makeConformantAgent();
|
|
749
|
+
try {
|
|
750
|
+
writeFileSync(join(root, ".env"), "ANTHROPIC_API_KEY=sk-test\n"); // drop opt-in
|
|
751
|
+
chmodSync(join(root, ".env"), 0o600);
|
|
752
|
+
const r = runCli(["audit", "--json"], root);
|
|
753
|
+
assert.equal(r.status, 1);
|
|
754
|
+
const doc = JSON.parse(r.stdout);
|
|
755
|
+
assert.equal(doc.ok, false);
|
|
756
|
+
const sp = doc.results.find((x) => x.id === "scoped-permissions-configured");
|
|
757
|
+
assert.equal(sp.ok, false);
|
|
758
|
+
assert.equal(sp.severity, "critical");
|
|
759
|
+
} finally { await rmRoot(root); }
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
test("audit: exits 1 with a clear message outside a Maestro agent dir", async () => {
|
|
763
|
+
const dir = await tmpRoot("maestro-audit-nonagent");
|
|
764
|
+
try {
|
|
765
|
+
const r = runCli(["audit"], dir);
|
|
766
|
+
const out = (r.stdout || "") + (r.stderr || "");
|
|
767
|
+
assert.equal(r.status, 1);
|
|
768
|
+
assert.match(out, /Not a Maestro agent directory/);
|
|
769
|
+
} finally { await rmRoot(dir); }
|
|
770
|
+
});
|
|
771
|
+
|
|
772
|
+
test("audit --help: prints usage and exits 0", async () => {
|
|
773
|
+
const dir = await tmpRoot("maestro-audit-help");
|
|
774
|
+
try {
|
|
775
|
+
const r = runCli(["audit", "--help"], dir);
|
|
776
|
+
assert.equal(r.status, 0, r.stderr);
|
|
777
|
+
assert.match(r.stdout, /maestro audit/);
|
|
778
|
+
assert.match(r.stdout, /--fix/);
|
|
779
|
+
} finally { await rmRoot(dir); }
|
|
780
|
+
});
|
|
781
|
+
|
|
782
|
+
// ---------------------------------------------------------------------------
|
|
783
|
+
// secrets — list / rotate (subcommand, gaps-enterprise-ops G12)
|
|
784
|
+
// ---------------------------------------------------------------------------
|
|
785
|
+
|
|
786
|
+
test("secrets --help prints usage and exits 0", async () => {
|
|
787
|
+
const dir = await tmpRoot("maestro-secrets-help");
|
|
788
|
+
try {
|
|
789
|
+
const r = runCli(["secrets", "--help"], dir);
|
|
790
|
+
assert.equal(r.status, 0, r.stderr);
|
|
791
|
+
assert.match(r.stdout, /maestro secrets/);
|
|
792
|
+
assert.match(r.stdout, /names only/);
|
|
793
|
+
} finally { await rmRoot(dir); }
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
test("secrets: exits 1 outside a Maestro agent dir", async () => {
|
|
797
|
+
const dir = await tmpRoot("maestro-secrets-nonagent");
|
|
798
|
+
try {
|
|
799
|
+
const r = runCli(["secrets", "list"], dir);
|
|
800
|
+
const out = (r.stdout || "") + (r.stderr || "");
|
|
801
|
+
assert.equal(r.status, 1);
|
|
802
|
+
assert.match(out, /Not a Maestro agent directory/);
|
|
803
|
+
} finally { await rmRoot(dir); }
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
test("secrets rotate then list shows the NAME but never the value", async () => {
|
|
807
|
+
const root = await makeBareAgent();
|
|
808
|
+
try {
|
|
809
|
+
// config/secrets.yaml selects the local file provider.
|
|
810
|
+
writeFileSync(join(root, "config/secrets.yaml"), "provider: file\n");
|
|
811
|
+
|
|
812
|
+
// Rotate a secret with the value supplied on stdin (never in argv).
|
|
813
|
+
const rot = runCliStdin(["secrets", "rotate", "--name", "DB_PASSWORD"], root, "p@ss-SECRET-123");
|
|
814
|
+
const rotOut = (rot.stdout || "") + (rot.stderr || "");
|
|
815
|
+
assert.equal(rot.status, 0, rotOut);
|
|
816
|
+
assert.match(rotOut, /Rotated "DB_PASSWORD"/);
|
|
817
|
+
|
|
818
|
+
// list shows the name...
|
|
819
|
+
const ls = runCli(["secrets", "list"], root);
|
|
820
|
+
const lsOut = (ls.stdout || "") + (ls.stderr || "");
|
|
821
|
+
assert.equal(ls.status, 0, lsOut);
|
|
822
|
+
assert.match(lsOut, /DB_PASSWORD/);
|
|
823
|
+
// ...but never the value, in either command's output.
|
|
824
|
+
assert.ok(!lsOut.includes("p@ss-SECRET-123"), "list must not print the value");
|
|
825
|
+
assert.ok(!rotOut.includes("p@ss-SECRET-123"), "rotate must not echo the value");
|
|
826
|
+
|
|
827
|
+
// The on-disk store is sealed (ciphertext != plaintext).
|
|
828
|
+
const store = readFileSync(join(root, "state/secrets/.secrets.json"), "utf-8");
|
|
829
|
+
assert.ok(!store.includes("p@ss-SECRET-123"), "value sealed at rest");
|
|
830
|
+
|
|
831
|
+
// The audit log records the name, not the value.
|
|
832
|
+
const audit = readFileSync(join(root, "logs/audit/secrets.jsonl"), "utf-8");
|
|
833
|
+
assert.match(audit, /DB_PASSWORD/);
|
|
834
|
+
assert.ok(!audit.includes("p@ss-SECRET-123"), "audit is names-only");
|
|
835
|
+
} finally { await rmRoot(root); }
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
test("secrets rotate without --name exits 1 with usage", async () => {
|
|
839
|
+
const root = await makeBareAgent();
|
|
840
|
+
try {
|
|
841
|
+
const r = runCliStdin(["secrets", "rotate"], root, "v");
|
|
842
|
+
const out = (r.stdout || "") + (r.stderr || "");
|
|
843
|
+
assert.equal(r.status, 1);
|
|
844
|
+
assert.match(out, /--name/);
|
|
845
|
+
} finally { await rmRoot(root); }
|
|
846
|
+
});
|
|
847
|
+
|
|
848
|
+
test("secrets sync without a configured broker exits 1", async () => {
|
|
849
|
+
const root = await makeBareAgent();
|
|
850
|
+
try {
|
|
851
|
+
writeFileSync(join(root, "config/secrets.yaml"), "provider: file\n");
|
|
852
|
+
const r = runCli(["secrets", "sync"], root);
|
|
853
|
+
const out = (r.stdout || "") + (r.stderr || "");
|
|
854
|
+
assert.equal(r.status, 1);
|
|
855
|
+
assert.match(out, /No broker configured/);
|
|
856
|
+
} finally { await rmRoot(root); }
|
|
857
|
+
});
|
|
858
|
+
|
|
859
|
+
// ---------------------------------------------------------------------------
|
|
860
|
+
// doctor — backup-config DR check (gaps-enterprise-ops G16)
|
|
861
|
+
// ---------------------------------------------------------------------------
|
|
862
|
+
|
|
863
|
+
test("doctor RED when no backup target is configured", async () => {
|
|
864
|
+
const root = await makeBareAgent();
|
|
865
|
+
try {
|
|
866
|
+
const r = runCli(["doctor"], root);
|
|
867
|
+
const out = (r.stdout || "") + (r.stderr || "");
|
|
868
|
+
assert.match(out, /No backup target configured/);
|
|
869
|
+
assert.match(out, /backup-config\.yaml/);
|
|
870
|
+
// doctor exits 1 whenever any issue is found; the missing backup is one.
|
|
871
|
+
assert.equal(r.status, 1, out);
|
|
872
|
+
} finally { await rmRoot(root); }
|
|
873
|
+
});
|
|
874
|
+
|
|
875
|
+
test("doctor GREEN for backup when configured + a fresh successful run is recorded", async () => {
|
|
876
|
+
const root = await makeBareAgent();
|
|
877
|
+
try {
|
|
878
|
+
mkdirSync(join(root, ".maestro"), { recursive: true });
|
|
879
|
+
writeFileSync(join(root, ".maestro/backup-config.yaml"), "enabled: true\nprovider: s3\nbucket: agent-backups\n");
|
|
880
|
+
writeFileSync(join(root, ".maestro/last-backup.json"), JSON.stringify({
|
|
881
|
+
completed_at: new Date().toISOString(), provider: "s3", bucket: "agent-backups",
|
|
882
|
+
}));
|
|
883
|
+
const r = runCli(["doctor"], root);
|
|
884
|
+
const out = (r.stdout || "") + (r.stderr || "");
|
|
885
|
+
assert.match(out, /Backup configured \+ fresh/);
|
|
886
|
+
assert.ok(!out.includes("No backup target configured"), "must not flag missing backup when fresh");
|
|
887
|
+
} finally { await rmRoot(root); }
|
|
888
|
+
});
|
|
889
|
+
|
|
890
|
+
// ---------------------------------------------------------------------------
|
|
891
|
+
// framework-features.json — registry validity + new-system enrollment
|
|
892
|
+
// (registry-scaffold-exports integration lane)
|
|
893
|
+
// ---------------------------------------------------------------------------
|
|
894
|
+
|
|
895
|
+
test("framework-features.json is valid JSON and registers the new systems", async () => {
|
|
896
|
+
const raw = readFileSync(join(MAESTRO_ROOT, "framework-features.json"), "utf-8");
|
|
897
|
+
const reg = JSON.parse(raw); // throws if invalid → test fails
|
|
898
|
+
assert.ok(reg.features && typeof reg.features === "object", "features map present");
|
|
899
|
+
const ids = Object.keys(reg.features);
|
|
900
|
+
// Every new system added by this lane must be enrolled in the registry.
|
|
901
|
+
for (const id of [
|
|
902
|
+
"org-cohort", "org-credential-broker", "secrets-lifecycle",
|
|
903
|
+
"diagnostics-spine", "alerting", "hook-bus", "dynamic-scheduling",
|
|
904
|
+
"send-gate", "knowledge-plane",
|
|
905
|
+
]) {
|
|
906
|
+
assert.ok(ids.includes(id), `framework-features.json missing feature id: ${id}`);
|
|
907
|
+
}
|
|
908
|
+
// Pre-existing core systems still enrolled (no regression).
|
|
909
|
+
for (const id of ["model-router", "cadence-bus"]) {
|
|
910
|
+
assert.ok(ids.includes(id), `framework-features.json lost feature id: ${id}`);
|
|
911
|
+
}
|
|
912
|
+
// Schema fidelity: each new entry matches the existing feature schema.
|
|
913
|
+
for (const id of ["org-cohort", "secrets-lifecycle", "diagnostics-spine"]) {
|
|
914
|
+
const f = reg.features[id];
|
|
915
|
+
assert.equal(typeof f.version, "string", `${id}.version`);
|
|
916
|
+
assert.equal(typeof f.since, "string", `${id}.since`);
|
|
917
|
+
assert.equal(typeof f.title, "string", `${id}.title`);
|
|
918
|
+
assert.ok(f.init && typeof f.init === "object", `${id}.init`);
|
|
919
|
+
assert.equal(typeof f.init.auto, "boolean", `${id}.init.auto`);
|
|
920
|
+
assert.equal(typeof f.init.command, "string", `${id}.init.command`);
|
|
921
|
+
assert.equal(typeof f.init.description, "string", `${id}.init.description`);
|
|
922
|
+
// init.command must be a single-line string (feature-init trust-boundary guard).
|
|
923
|
+
assert.ok(!/[\0\r\n]/.test(f.init.command), `${id}.init.command must be single-line`);
|
|
924
|
+
}
|
|
925
|
+
// Honest enablement: org/cohort + its broker default OFF until configured.
|
|
926
|
+
assert.equal(reg.features["org-cohort"].init.auto, false, "org-cohort must not auto-enroll");
|
|
927
|
+
assert.equal(reg.features["org-credential-broker"].init.auto, false, "broker must not auto-enable");
|
|
928
|
+
});
|
|
929
|
+
|
|
930
|
+
// ---------------------------------------------------------------------------
|
|
931
|
+
// create — ships the new disabled-by-default scaffold configs
|
|
932
|
+
// ---------------------------------------------------------------------------
|
|
933
|
+
|
|
934
|
+
test("create ships disabled-by-default org.yaml + secrets.yaml templates", async () => {
|
|
935
|
+
const parent = await tmpRoot("maestro-create-cfg");
|
|
936
|
+
const { env, dir: shim } = await withShim();
|
|
937
|
+
try {
|
|
938
|
+
const r = runCli(["create", "cfg-agent"], parent, env);
|
|
939
|
+
assert.equal(r.status, 0, r.stderr || r.stdout);
|
|
940
|
+
const agent = join(parent, "cfg-agent");
|
|
941
|
+
|
|
942
|
+
const orgYaml = join(agent, "config/org.yaml");
|
|
943
|
+
const secretsYaml = join(agent, "config/secrets.yaml");
|
|
944
|
+
assert.ok(existsSync(orgYaml), "config/org.yaml must be scaffolded");
|
|
945
|
+
assert.ok(existsSync(secretsYaml), "config/secrets.yaml must be scaffolded");
|
|
946
|
+
|
|
947
|
+
// org.yaml is disabled by default (the agent runs standalone).
|
|
948
|
+
const orgBody = readFileSync(orgYaml, "utf-8");
|
|
949
|
+
assert.match(orgBody, /enabled:\s*false/, "org enrollment must default OFF");
|
|
950
|
+
assert.match(orgBody, /cohort/);
|
|
951
|
+
|
|
952
|
+
// secrets.yaml defaults to the local file provider.
|
|
953
|
+
const secBody = readFileSync(secretsYaml, "utf-8");
|
|
954
|
+
assert.match(secBody, /provider:\s*file/, "secrets must default to the local file provider");
|
|
955
|
+
} finally {
|
|
956
|
+
await rmRoot(parent);
|
|
957
|
+
await rmRoot(shim);
|
|
958
|
+
}
|
|
959
|
+
});
|
|
960
|
+
|
|
961
|
+
// ---------------------------------------------------------------------------
|
|
962
|
+
// upgrade — propagates new config templates to existing agents (add-only)
|
|
963
|
+
// ---------------------------------------------------------------------------
|
|
964
|
+
|
|
965
|
+
test("upgrade adds new scaffold config templates without clobbering existing ones", async () => {
|
|
966
|
+
const root = await makeLegacyAgent();
|
|
967
|
+
try {
|
|
968
|
+
// Seed an operator-customised alerts.yaml that upgrade must NOT overwrite.
|
|
969
|
+
mkdirSync(join(root, "config"), { recursive: true });
|
|
970
|
+
const customAlerts = "alerts:\n webhook: https://hooks.example/custom\n";
|
|
971
|
+
writeFileSync(join(root, "config/alerts.yaml"), customAlerts);
|
|
972
|
+
execFileSync("git", ["add", "-A"], { cwd: root });
|
|
973
|
+
execFileSync("git", ["-c", "user.email=t@t", "-c", "user.name=t", "commit", "-q", "-m", "seed alerts"], { cwd: root });
|
|
974
|
+
|
|
975
|
+
const r = runCli(["upgrade"], root);
|
|
976
|
+
assert.equal(r.status, 0, r.stderr);
|
|
977
|
+
|
|
978
|
+
// New templates landed.
|
|
979
|
+
assert.ok(existsSync(join(root, "config/org.yaml")), "org.yaml must be delivered on upgrade");
|
|
980
|
+
assert.ok(existsSync(join(root, "config/secrets.yaml")), "secrets.yaml must be delivered on upgrade");
|
|
981
|
+
assert.match(readFileSync(join(root, "config/org.yaml"), "utf-8"), /enabled:\s*false/);
|
|
982
|
+
|
|
983
|
+
// Existing operator config preserved byte-for-byte.
|
|
984
|
+
assert.equal(readFileSync(join(root, "config/alerts.yaml"), "utf-8"), customAlerts,
|
|
985
|
+
"upgrade must not clobber an existing operator config");
|
|
986
|
+
} finally { await rmRoot(root); }
|
|
987
|
+
});
|
|
988
|
+
|
|
989
|
+
// ---------------------------------------------------------------------------
|
|
990
|
+
// doctor — surfaces the new framework systems
|
|
991
|
+
// ---------------------------------------------------------------------------
|
|
992
|
+
|
|
993
|
+
test("doctor surfaces the new framework systems (org / secrets / diagnostics)", async () => {
|
|
994
|
+
// Use a freshly-created agent so lib/* + the scaffold configs are present.
|
|
995
|
+
const parent = await tmpRoot("maestro-doctor-systems");
|
|
996
|
+
const { env, dir: shim } = await withShim();
|
|
997
|
+
try {
|
|
998
|
+
const cr = runCli(["create", "doc-agent"], parent, env);
|
|
999
|
+
assert.equal(cr.status, 0, cr.stderr || cr.stdout);
|
|
1000
|
+
const agent = join(parent, "doc-agent");
|
|
1001
|
+
const r = runCli(["doctor"], agent);
|
|
1002
|
+
const out = (r.stdout || "") + (r.stderr || "");
|
|
1003
|
+
// 2.0: the presence-only org line became real connectivity probes; a fresh
|
|
1004
|
+
// (unenrolled) create short-circuits on the single "not enrolled" line.
|
|
1005
|
+
assert.match(out, /Org plane: not enrolled|Org enrollment resolved/);
|
|
1006
|
+
assert.match(out, /Secret lifecycle/);
|
|
1007
|
+
assert.match(out, /Diagnostics spine present/);
|
|
1008
|
+
assert.match(out, /Alerting configured/);
|
|
1009
|
+
// None of the new lib modules should be flagged missing in a fresh create.
|
|
1010
|
+
assert.ok(!/module missing/.test(out), "fresh create must ship every new lib module");
|
|
1011
|
+
} finally {
|
|
1012
|
+
await rmRoot(parent);
|
|
1013
|
+
await rmRoot(shim);
|
|
1014
|
+
}
|
|
1015
|
+
});
|