@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
package/bin/maestro.mjs
ADDED
|
@@ -0,0 +1,3516 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Maestro CLI — Create and manage autonomous AI agent deployments.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* npx @cohortapp/agent-sdk create <dirname> # Scaffold a new agent repo
|
|
7
|
+
* npx @cohortapp/agent-sdk upgrade # Update framework files in current agent repo
|
|
8
|
+
* npx @cohortapp/agent-sdk doctor # Verify installation and configuration
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { resolve, join, dirname, relative, sep, basename } from "node:path";
|
|
12
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
13
|
+
import {
|
|
14
|
+
mkdirSync,
|
|
15
|
+
cpSync,
|
|
16
|
+
copyFileSync,
|
|
17
|
+
existsSync,
|
|
18
|
+
readFileSync,
|
|
19
|
+
writeFileSync,
|
|
20
|
+
readdirSync,
|
|
21
|
+
statSync,
|
|
22
|
+
lstatSync,
|
|
23
|
+
openSync,
|
|
24
|
+
readSync,
|
|
25
|
+
closeSync,
|
|
26
|
+
} from "node:fs";
|
|
27
|
+
import { execFileSync, spawnSync } from "node:child_process";
|
|
28
|
+
import { createHash } from "node:crypto";
|
|
29
|
+
import { homedir } from "node:os";
|
|
30
|
+
import { checkOwnershipSet } from "../lib/fs-ownership.mjs";
|
|
31
|
+
import { resolveArchetype, migrateLegacyArchetype } from "../lib/archetype.mjs";
|
|
32
|
+
import { runAudit, applyFixes, buildAttestation } from "../lib/security/audit-engine.mjs";
|
|
33
|
+
import { selectProvider } from "../lib/secrets/providers.mjs";
|
|
34
|
+
import { syncSecrets, rotateSecret, makeBroker, auditLogPath } from "../lib/secrets/broker.mjs";
|
|
35
|
+
import { applyBrandEnvCompat } from "../lib/env-compat.mjs";
|
|
36
|
+
|
|
37
|
+
// Fleet back-compat FIRST: bridge NEOLITH_* ⇄ COHORT_* env names before any
|
|
38
|
+
// env-first resolution below — installed hosts still export the legacy names.
|
|
39
|
+
applyBrandEnvCompat();
|
|
40
|
+
|
|
41
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
42
|
+
const MAESTRO_ROOT = resolve(__dirname, "..");
|
|
43
|
+
const SCAFFOLD_DIR = join(MAESTRO_ROOT, "scaffold");
|
|
44
|
+
const FRAMEWORK_FEATURES_PATH = join(MAESTRO_ROOT, "framework-features.json");
|
|
45
|
+
|
|
46
|
+
// Plist architecture marker — must match the value emitted by
|
|
47
|
+
// scripts/local-triggers/generate-plists.sh. Used by doctor + upgrade
|
|
48
|
+
// migration to distinguish cadence-bus plists from the legacy
|
|
49
|
+
// spawn-per-tick pattern (which invoked run-trigger.sh directly).
|
|
50
|
+
const PLIST_ARCH_MARKER = "maestro-plist-arch: cadence-bus";
|
|
51
|
+
const LEGACY_PLIST_INDICATOR = "run-trigger.sh";
|
|
52
|
+
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
// Colours
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
const C = {
|
|
58
|
+
reset: "\x1b[0m",
|
|
59
|
+
bold: "\x1b[1m",
|
|
60
|
+
red: "\x1b[31m",
|
|
61
|
+
green: "\x1b[32m",
|
|
62
|
+
yellow: "\x1b[33m",
|
|
63
|
+
blue: "\x1b[34m",
|
|
64
|
+
cyan: "\x1b[36m",
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const log = (msg) => console.log(`${C.blue}[maestro]${C.reset} ${msg}`);
|
|
68
|
+
const ok = (msg) => console.log(`${C.green} ✓${C.reset} ${msg}`);
|
|
69
|
+
const warn = (msg) => console.log(`${C.yellow} ⚠${C.reset} ${msg}`);
|
|
70
|
+
const fail = (msg) => console.error(`${C.red} ✗${C.reset} ${msg}`);
|
|
71
|
+
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
// CREATE — scaffold a new agent repo
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
function create(targetName) {
|
|
77
|
+
if (!targetName) {
|
|
78
|
+
fail("Usage: maestro create <agent-directory-name>");
|
|
79
|
+
fail("Example: maestro create acme-ai");
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const targetDir = resolve(process.cwd(), targetName);
|
|
84
|
+
|
|
85
|
+
if (existsSync(targetDir)) {
|
|
86
|
+
fail(`Directory already exists: ${targetDir}`);
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
console.log();
|
|
91
|
+
log(`Creating agent: ${targetName}`);
|
|
92
|
+
mkdirSync(targetDir, { recursive: true });
|
|
93
|
+
|
|
94
|
+
// ── Step 1: Copy framework files ────────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
log("Copying framework files...");
|
|
97
|
+
|
|
98
|
+
const frameworkDirs = [
|
|
99
|
+
"scripts",
|
|
100
|
+
"agents",
|
|
101
|
+
"workflows",
|
|
102
|
+
"policies",
|
|
103
|
+
"teams",
|
|
104
|
+
"docs",
|
|
105
|
+
"public",
|
|
106
|
+
"plugins",
|
|
107
|
+
"schedules",
|
|
108
|
+
"desktop-control",
|
|
109
|
+
"ingest",
|
|
110
|
+
"mcp",
|
|
111
|
+
"services",
|
|
112
|
+
// lib/ holds shared framework primitives — singleton lock, cadence bus,
|
|
113
|
+
// action executor, tool definitions — that scripts/ imports via relative
|
|
114
|
+
// paths. Must travel with each agent repo so the relative imports work
|
|
115
|
+
// without depending on a checkout of @cohortapp/agent-sdk on the same host.
|
|
116
|
+
"lib",
|
|
117
|
+
];
|
|
118
|
+
|
|
119
|
+
for (const dir of frameworkDirs) {
|
|
120
|
+
const src = join(MAESTRO_ROOT, dir);
|
|
121
|
+
if (existsSync(src)) {
|
|
122
|
+
cpSync(src, join(targetDir, dir), { recursive: true });
|
|
123
|
+
ok(dir);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ── Step 2: Copy scaffold templates ─────────────────────────────────────
|
|
128
|
+
|
|
129
|
+
log("Copying agent templates...");
|
|
130
|
+
|
|
131
|
+
if (existsSync(SCAFFOLD_DIR)) {
|
|
132
|
+
cpSync(SCAFFOLD_DIR, targetDir, { recursive: true });
|
|
133
|
+
ok("scaffold templates");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Copy .claude/ directory
|
|
137
|
+
const claudeDir = join(MAESTRO_ROOT, ".claude");
|
|
138
|
+
if (existsSync(claudeDir)) {
|
|
139
|
+
cpSync(claudeDir, join(targetDir, ".claude"), { recursive: true });
|
|
140
|
+
ok(".claude/ (settings, commands)");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ── Step 3: Create agent-specific directories ───────────────────────────
|
|
144
|
+
|
|
145
|
+
log("Creating agent directories...");
|
|
146
|
+
|
|
147
|
+
const agentDirs = [
|
|
148
|
+
"config",
|
|
149
|
+
"knowledge/decisions",
|
|
150
|
+
"knowledge/decisions/archive",
|
|
151
|
+
"knowledge/entities",
|
|
152
|
+
"knowledge/memory",
|
|
153
|
+
"knowledge/sources",
|
|
154
|
+
"knowledge/syntheses",
|
|
155
|
+
"memory/interactions",
|
|
156
|
+
"memory/profiles/users",
|
|
157
|
+
"memory/profiles/channels",
|
|
158
|
+
"memory/precedents",
|
|
159
|
+
"memory/precedents/market-signals",
|
|
160
|
+
"memory/indexes",
|
|
161
|
+
"memory/templates",
|
|
162
|
+
"state/inbox/slack",
|
|
163
|
+
"state/inbox/gmail",
|
|
164
|
+
"state/inbox/calendar",
|
|
165
|
+
"state/inbox/sms",
|
|
166
|
+
"state/inbox/whatsapp",
|
|
167
|
+
"state/inbox/internal",
|
|
168
|
+
"state/inbox/attachments",
|
|
169
|
+
"state/inbox/processed",
|
|
170
|
+
"state/queues",
|
|
171
|
+
"state/dashboards",
|
|
172
|
+
"state/polling",
|
|
173
|
+
"state/locks/outbound",
|
|
174
|
+
"state/triggers/priority",
|
|
175
|
+
"state/handoffs",
|
|
176
|
+
"state/huddle",
|
|
177
|
+
"state/indexes",
|
|
178
|
+
"state/rag",
|
|
179
|
+
"state/sessions",
|
|
180
|
+
"state/slack-responded",
|
|
181
|
+
"state/slack-thread-tracker",
|
|
182
|
+
"state/tmp",
|
|
183
|
+
"state/cadence-bus",
|
|
184
|
+
"state/cadence-bus/inbox",
|
|
185
|
+
"state/cadence-bus/claimed",
|
|
186
|
+
"state/cadence-bus/processed",
|
|
187
|
+
"state/cadence-bus/failed",
|
|
188
|
+
"state/cadence-bus/dlq",
|
|
189
|
+
// 1.12.0 — channel bus + voice + WhatsApp (Baileys) state dirs.
|
|
190
|
+
// Pre-created at scaffold time so daemon startups don't race with
|
|
191
|
+
// first-run mkdir calls. Each feature's init wizard re-creates these
|
|
192
|
+
// idempotently as a safety net.
|
|
193
|
+
"state/channels",
|
|
194
|
+
"state/voice",
|
|
195
|
+
"state/whatsapp/auth",
|
|
196
|
+
"state/whatsapp/audio",
|
|
197
|
+
"state/whatsapp/outbound",
|
|
198
|
+
"outputs/briefs",
|
|
199
|
+
"outputs/drafts",
|
|
200
|
+
"outputs/memos",
|
|
201
|
+
"outputs/research",
|
|
202
|
+
"outputs/tasks",
|
|
203
|
+
"outputs/sessions",
|
|
204
|
+
"logs/polling",
|
|
205
|
+
"logs/workflows",
|
|
206
|
+
"logs/sessions",
|
|
207
|
+
"logs/audit",
|
|
208
|
+
"logs/security",
|
|
209
|
+
"logs/evolution",
|
|
210
|
+
"logs/huddle",
|
|
211
|
+
"logs/daemon",
|
|
212
|
+
"logs/cadence-bus",
|
|
213
|
+
"logs/infra",
|
|
214
|
+
"logs/monitor",
|
|
215
|
+
"logs/phone",
|
|
216
|
+
"logs/sms",
|
|
217
|
+
"logs/whatsapp",
|
|
218
|
+
"logs/email",
|
|
219
|
+
// 1.12.0
|
|
220
|
+
"logs/channels",
|
|
221
|
+
"logs/voice",
|
|
222
|
+
"logs/voice/sessions",
|
|
223
|
+
"self-optimization/scenarios",
|
|
224
|
+
"tests",
|
|
225
|
+
];
|
|
226
|
+
|
|
227
|
+
for (const dir of agentDirs) {
|
|
228
|
+
mkdirSync(join(targetDir, dir), { recursive: true });
|
|
229
|
+
}
|
|
230
|
+
ok(`${agentDirs.length} agent directories`);
|
|
231
|
+
|
|
232
|
+
// ── Step 3b: Generate template files ───────────────────────────────────
|
|
233
|
+
// These are empty-but-structured templates that define the operational
|
|
234
|
+
// schema for queues, dashboards, knowledge, and config. Without them,
|
|
235
|
+
// workflows and triggers have nothing to read/write.
|
|
236
|
+
|
|
237
|
+
log("Generating operational templates...");
|
|
238
|
+
|
|
239
|
+
// Queue templates — 16 operational queues
|
|
240
|
+
const queueTemplates = {
|
|
241
|
+
"action-stack": "Active action items requiring attention or follow-through",
|
|
242
|
+
"decision-queue": "Key decisions awaiting principal input or closure",
|
|
243
|
+
"follow-ups": "Commitments to external parties with due dates",
|
|
244
|
+
"awaiting-reply": "Messages sent that are awaiting responses",
|
|
245
|
+
"awaiting-approval": "Items pending principal or stakeholder approval",
|
|
246
|
+
"blocked-initiatives": "Strategic initiatives blocked by dependencies",
|
|
247
|
+
"high-risk-items": "Items with elevated risk requiring close monitoring",
|
|
248
|
+
"upcoming-deadlines": "Time-sensitive items with approaching deadlines",
|
|
249
|
+
"relationship-health": "Relationship maintenance and engagement tracking",
|
|
250
|
+
"hiring-critical-path": "Critical hiring actions and pipeline blockers",
|
|
251
|
+
"platform-blockers": "Technical and platform issues blocking delivery",
|
|
252
|
+
"capital-watchpoints": "Financial and fundraising items requiring attention",
|
|
253
|
+
"legal-obligations": "Regulatory submissions and legal compliance deadlines",
|
|
254
|
+
"narrative-opportunities": "Strategic narrative and thought leadership opportunities",
|
|
255
|
+
"tech-debt": "Technical debt with strategic impact",
|
|
256
|
+
"improvement-backlog": "Self-improvement and operational enhancement candidates",
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
for (const [name, desc] of Object.entries(queueTemplates)) {
|
|
260
|
+
const content = `queue_name: ${name}\ndescription: ${desc}\nitems: []\n`;
|
|
261
|
+
writeFileSync(join(targetDir, `state/queues/${name}.yaml`), content);
|
|
262
|
+
}
|
|
263
|
+
ok(`${Object.keys(queueTemplates).length} queue templates`);
|
|
264
|
+
|
|
265
|
+
// Dashboard templates — 10 operational dashboards
|
|
266
|
+
const dashboardTemplates = {
|
|
267
|
+
"executive-summary": {
|
|
268
|
+
description: "Primary situational awareness — read at every session start",
|
|
269
|
+
fields: "generated: null\nsummary:\n total_open_items: 0\n critical_items: 0\n overdue_items: 0\n sla_breaches: 0\n items_resolved_today: 0\n items_created_today: 0\nqueue_health: {}\ntop_priorities: []\ntop_blockers: []\n",
|
|
270
|
+
},
|
|
271
|
+
"daemon-health": {
|
|
272
|
+
description: "System health metrics for the daemon and triggers",
|
|
273
|
+
fields: "status: initialising\nlast_check: null\ntrigger_health: {}\npoller_health: {}\n",
|
|
274
|
+
},
|
|
275
|
+
"strategy-scorecard": {
|
|
276
|
+
description: "Progress on strategic priorities and quarterly OKRs",
|
|
277
|
+
fields: "generated: null\npriorities: []\ninitiatives: []\n",
|
|
278
|
+
},
|
|
279
|
+
"risk-register": {
|
|
280
|
+
description: "Enterprise risk register with severity and mitigation",
|
|
281
|
+
fields: "generated: null\nrisks: []\n",
|
|
282
|
+
},
|
|
283
|
+
"engineering-health": {
|
|
284
|
+
description: "Per-repo engineering activity, PRs, delivery confidence",
|
|
285
|
+
fields: "generated: null\nrepositories: []\ndelivery_confidence: null\n",
|
|
286
|
+
},
|
|
287
|
+
"org-health": {
|
|
288
|
+
description: "Hiring pipeline, capability gaps, structural risks",
|
|
289
|
+
fields: "generated: null\nhiring_pipeline: {}\ncapability_gaps: []\n",
|
|
290
|
+
},
|
|
291
|
+
"leadership-accountability": {
|
|
292
|
+
description: "Per-leader commitment follow-through tracking",
|
|
293
|
+
fields: "generated: null\nleaders: []\n",
|
|
294
|
+
},
|
|
295
|
+
"relationship-pipeline": {
|
|
296
|
+
description: "Strategic relationships with stage and warmth tracking",
|
|
297
|
+
fields: "generated: null\nrelationships: []\n",
|
|
298
|
+
},
|
|
299
|
+
"capital-readiness": {
|
|
300
|
+
description: "Runway, fundraising readiness, investor pipeline",
|
|
301
|
+
fields: "generated: null\nrunway_months: null\nreadiness_score: null\npipeline: []\n",
|
|
302
|
+
},
|
|
303
|
+
"system-health": {
|
|
304
|
+
description: "Self-governance metrics — evolution, coverage, queue health",
|
|
305
|
+
fields: "generated: null\nevolution_changes: 0\nknowledge_coverage: null\ntrigger_reliability: null\n",
|
|
306
|
+
},
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
for (const [name, { description, fields }] of Object.entries(dashboardTemplates)) {
|
|
310
|
+
const content = `# ${name.replace(/-/g, " ").replace(/\b\w/g, c => c.toUpperCase())} Dashboard\n# ${description}\n\n${fields}`;
|
|
311
|
+
writeFileSync(join(targetDir, `state/dashboards/${name}.yaml`), content);
|
|
312
|
+
}
|
|
313
|
+
ok(`${Object.keys(dashboardTemplates).length} dashboard templates`);
|
|
314
|
+
|
|
315
|
+
// Config templates — operational config files
|
|
316
|
+
log("Generating config templates...");
|
|
317
|
+
|
|
318
|
+
const configTemplates = {
|
|
319
|
+
"environment.yaml": `# Environment Configuration
|
|
320
|
+
# All environment-specific values — actual secrets are in .env
|
|
321
|
+
# Agent identity is defined in config/agent.ts
|
|
322
|
+
|
|
323
|
+
system:
|
|
324
|
+
name: maestro
|
|
325
|
+
version: 1.0.0
|
|
326
|
+
operator_persona: "UNCONFIGURED"
|
|
327
|
+
operator_role: "UNCONFIGURED"
|
|
328
|
+
company: ""
|
|
329
|
+
ceo: ""
|
|
330
|
+
timezone: UTC
|
|
331
|
+
|
|
332
|
+
machine:
|
|
333
|
+
type: mac-mini
|
|
334
|
+
os: macOS
|
|
335
|
+
hostname: \${HOSTNAME}
|
|
336
|
+
purpose: Autonomous agent operations node
|
|
337
|
+
|
|
338
|
+
paths:
|
|
339
|
+
agent_home: ~/${targetName}
|
|
340
|
+
logs: ~/${targetName}/logs
|
|
341
|
+
outputs: ~/${targetName}/outputs
|
|
342
|
+
`,
|
|
343
|
+
"contacts.yaml": `# Key Contacts
|
|
344
|
+
# Communication classifications and relationship context
|
|
345
|
+
# Updated by: init-maestro wizard, agent interactions
|
|
346
|
+
|
|
347
|
+
contacts: []
|
|
348
|
+
|
|
349
|
+
# Classification levels:
|
|
350
|
+
# - internal: team members, direct reports
|
|
351
|
+
# - external-trusted: partners, advisors, board members
|
|
352
|
+
# - external-standard: vendors, candidates, general contacts
|
|
353
|
+
# - regulatory: regulators, legal counterparties
|
|
354
|
+
`,
|
|
355
|
+
"priorities.yaml": `# Active Strategic Priorities
|
|
356
|
+
# Updated by: weekly-priorities workflow, principal directive
|
|
357
|
+
# Read by: morning-brief, backlog-executor, strategy-scorecard
|
|
358
|
+
|
|
359
|
+
priorities: []
|
|
360
|
+
|
|
361
|
+
# Priority format:
|
|
362
|
+
# - id: P1
|
|
363
|
+
# title: Priority title
|
|
364
|
+
# status: active | paused | completed
|
|
365
|
+
# owner: principal | agent
|
|
366
|
+
# milestones: []
|
|
367
|
+
# deadline: null
|
|
368
|
+
`,
|
|
369
|
+
"sla-defaults.yaml": `# SLA Defaults — response time expectations by item type
|
|
370
|
+
# Used by: inbox-processor, meeting-action-capture, midday-sweep
|
|
371
|
+
|
|
372
|
+
sla_defaults:
|
|
373
|
+
principal_request: 4
|
|
374
|
+
leadership_request: 24
|
|
375
|
+
external_partner: 48
|
|
376
|
+
regulatory: 24
|
|
377
|
+
candidate: 24
|
|
378
|
+
board_member: 12
|
|
379
|
+
investor: 24
|
|
380
|
+
general: 72
|
|
381
|
+
|
|
382
|
+
thresholds:
|
|
383
|
+
at_risk_percent: 75
|
|
384
|
+
breach_percent: 100
|
|
385
|
+
|
|
386
|
+
drift_thresholds:
|
|
387
|
+
critical_priority: 7
|
|
388
|
+
medium_priority: 14
|
|
389
|
+
low_priority: 30
|
|
390
|
+
`,
|
|
391
|
+
"brand-assets.yaml": `# Brand Assets Configuration
|
|
392
|
+
# Official brand assets, design system, and usage rules
|
|
393
|
+
# Assets directory: public/assets/
|
|
394
|
+
|
|
395
|
+
asset_inventory:
|
|
396
|
+
icon_dark: public/assets/icon-dark.svg
|
|
397
|
+
icon_light: public/assets/icon-light.svg
|
|
398
|
+
logo_dark: public/assets/logo-dark.svg
|
|
399
|
+
logo_light: public/assets/logo-light.svg
|
|
400
|
+
|
|
401
|
+
usage_rules:
|
|
402
|
+
dark_variants: "Use on light/white backgrounds"
|
|
403
|
+
light_variants: "Use on dark backgrounds"
|
|
404
|
+
full_logo: "Cover pages, document headers, title slides"
|
|
405
|
+
icon_only: "Footers, watermarks, compact placements"
|
|
406
|
+
never: "stretch, rotate, recolour, or modify the assets"
|
|
407
|
+
`,
|
|
408
|
+
"repo-registry.yaml": `# Source Repository Registry
|
|
409
|
+
# Cross-repository map for codebase awareness
|
|
410
|
+
# Updated by: init-maestro wizard
|
|
411
|
+
|
|
412
|
+
repositories: []
|
|
413
|
+
|
|
414
|
+
# Format:
|
|
415
|
+
# - name: repo-name
|
|
416
|
+
# path: ~/repo-name
|
|
417
|
+
# purpose: What this repo contains
|
|
418
|
+
# domains: [domain1, domain2]
|
|
419
|
+
`,
|
|
420
|
+
"slack-channels.yaml": `# Slack Channel Configuration
|
|
421
|
+
# Which channels the agent monitors and their purpose
|
|
422
|
+
# Updated by: init-maestro wizard
|
|
423
|
+
|
|
424
|
+
channels: []
|
|
425
|
+
|
|
426
|
+
# Format:
|
|
427
|
+
# - id: C0XXXXX
|
|
428
|
+
# name: channel-name
|
|
429
|
+
# purpose: What this channel is for
|
|
430
|
+
# monitor: true | false
|
|
431
|
+
# respond: true | false
|
|
432
|
+
`,
|
|
433
|
+
"tooling-map.yaml": `# Tooling Map
|
|
434
|
+
# Maps responsibilities to available tools and scripts
|
|
435
|
+
# Generated by: init-maestro sub-agent 8
|
|
436
|
+
|
|
437
|
+
tools: []
|
|
438
|
+
`,
|
|
439
|
+
"validation-rules.yaml": `# Outbound Correspondence Validation Rules
|
|
440
|
+
# Used by: scripts/validate-outbound.py
|
|
441
|
+
# Purpose: Pre-send gate to prevent factual inaccuracies and protocol violations
|
|
442
|
+
|
|
443
|
+
relationship_claims:
|
|
444
|
+
enabled: true
|
|
445
|
+
severity: block
|
|
446
|
+
patterns: []
|
|
447
|
+
|
|
448
|
+
ai_disclosure:
|
|
449
|
+
enabled: true
|
|
450
|
+
severity: block
|
|
451
|
+
blocked_phrases:
|
|
452
|
+
- "I am an AI"
|
|
453
|
+
- "as an AI"
|
|
454
|
+
- "I'm an artificial intelligence"
|
|
455
|
+
- "language model"
|
|
456
|
+
|
|
457
|
+
tone_violations:
|
|
458
|
+
enabled: true
|
|
459
|
+
severity: warn
|
|
460
|
+
rules: []
|
|
461
|
+
`,
|
|
462
|
+
"mcp-integrations.yaml": `# MCP Server Integration Configuration
|
|
463
|
+
# Which MCP services this agent connects to
|
|
464
|
+
# Updated by: init-maestro wizard
|
|
465
|
+
|
|
466
|
+
integrations:
|
|
467
|
+
slack: { enabled: false, notes: "Configure via .env" }
|
|
468
|
+
gmail: { enabled: false, notes: "Configure via .env" }
|
|
469
|
+
calendar: { enabled: false, notes: "Configure via .env" }
|
|
470
|
+
`,
|
|
471
|
+
"mcp-servers.json": `{}`,
|
|
472
|
+
"caller-id-map.yaml": `# Caller ID Map
|
|
473
|
+
# Maps phone numbers to known contacts for SMS/voice identification
|
|
474
|
+
# Updated by: agent interactions
|
|
475
|
+
|
|
476
|
+
contacts: {}
|
|
477
|
+
`,
|
|
478
|
+
"hiring.yaml": `# Hiring Configuration
|
|
479
|
+
# Pipeline settings, screening rules, and template references
|
|
480
|
+
# Updated by: init-maestro wizard (for executive-operator / operations-leader archetypes)
|
|
481
|
+
|
|
482
|
+
enabled: false
|
|
483
|
+
roles: []
|
|
484
|
+
screening:
|
|
485
|
+
auto_advance_threshold: 75
|
|
486
|
+
auto_reject_threshold: 40
|
|
487
|
+
review_band: [40, 75]
|
|
488
|
+
`,
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
for (const [filename, content] of Object.entries(configTemplates)) {
|
|
492
|
+
const dest = join(targetDir, `config/${filename}`);
|
|
493
|
+
if (!existsSync(dest)) {
|
|
494
|
+
writeFileSync(dest, content);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
ok(`${Object.keys(configTemplates).length} config templates`);
|
|
498
|
+
|
|
499
|
+
// Knowledge templates — schemas and empty indexes
|
|
500
|
+
log("Generating knowledge templates...");
|
|
501
|
+
|
|
502
|
+
const knowledgeTemplates = {
|
|
503
|
+
"decisions/decision-schema.yaml": `# Decision Record Schema
|
|
504
|
+
# Format: DEC-YYYY-MM-DD-NNN
|
|
505
|
+
# See docs for full schema documentation
|
|
506
|
+
|
|
507
|
+
schema:
|
|
508
|
+
version: "1.0"
|
|
509
|
+
fields:
|
|
510
|
+
id: { type: string, format: "DEC-YYYY-MM-DD-NNN", required: true }
|
|
511
|
+
date: { type: date, format: "YYYY-MM-DD", required: true }
|
|
512
|
+
title: { type: string, required: true }
|
|
513
|
+
domain: { type: string, required: true }
|
|
514
|
+
decision_maker: { type: string, required: true }
|
|
515
|
+
decision_text: { type: string, required: true }
|
|
516
|
+
context: { type: string, required: true }
|
|
517
|
+
rationale: { type: string, required: true }
|
|
518
|
+
status: { type: string, enum: [active, superseded, reversed, completed], default: active }
|
|
519
|
+
`,
|
|
520
|
+
"decisions/index.yaml": `# Decision Index
|
|
521
|
+
# Auto-maintained by decision-log agent
|
|
522
|
+
|
|
523
|
+
decisions: []
|
|
524
|
+
`,
|
|
525
|
+
"entities/entity-map.yaml": `# Entity Map
|
|
526
|
+
# All contacts, companies, regulators, advisors
|
|
527
|
+
# De-duplication rules and relationship mapping
|
|
528
|
+
|
|
529
|
+
entities: []
|
|
530
|
+
`,
|
|
531
|
+
"entities/org-chart.md": `# Organisation Chart
|
|
532
|
+
|
|
533
|
+
*Configure via /init-maestro*
|
|
534
|
+
`,
|
|
535
|
+
"sources/source-registry.yaml": `# Authoritative Source Registry
|
|
536
|
+
# Central registry of all information sources
|
|
537
|
+
|
|
538
|
+
sources: {}
|
|
539
|
+
`,
|
|
540
|
+
"sources/blocker-playbooks.yaml": `# Blocker Resolution Playbooks
|
|
541
|
+
# Common blocker patterns and resolution strategies
|
|
542
|
+
|
|
543
|
+
playbooks: []
|
|
544
|
+
`,
|
|
545
|
+
"syntheses/strategy-state.yaml": `# Living Strategy State
|
|
546
|
+
# Evolves weekly with strategic position, assumptions, risks, signals
|
|
547
|
+
|
|
548
|
+
last_updated: null
|
|
549
|
+
strategic_position: ""
|
|
550
|
+
key_assumptions: []
|
|
551
|
+
active_risks: []
|
|
552
|
+
market_signals: []
|
|
553
|
+
`,
|
|
554
|
+
"syntheses/README.md": `# Knowledge Syntheses
|
|
555
|
+
|
|
556
|
+
Strategic syntheses generated by domain agents. Updated weekly.
|
|
557
|
+
`,
|
|
558
|
+
"memory/executive-memory.yaml": `# Executive Memory
|
|
559
|
+
# Institutional memory for strategic context
|
|
560
|
+
|
|
561
|
+
entries: []
|
|
562
|
+
`,
|
|
563
|
+
};
|
|
564
|
+
|
|
565
|
+
for (const [path, content] of Object.entries(knowledgeTemplates)) {
|
|
566
|
+
writeFileSync(join(targetDir, `knowledge/${path}`), content);
|
|
567
|
+
}
|
|
568
|
+
ok(`${Object.keys(knowledgeTemplates).length} knowledge templates`);
|
|
569
|
+
|
|
570
|
+
// Self-optimization templates
|
|
571
|
+
const selfOptTemplates = {
|
|
572
|
+
"program.md": `# Self-Optimization Program
|
|
573
|
+
|
|
574
|
+
This agent evolves its own operating setup through structured experimentation.
|
|
575
|
+
Changes are classified by risk: low (auto-apply), medium (apply + notify), high (escalate).
|
|
576
|
+
`,
|
|
577
|
+
"experiment-log.yaml": `# Experiment Log
|
|
578
|
+
experiments: []
|
|
579
|
+
`,
|
|
580
|
+
"failure-patterns.yaml": `# Failure Patterns
|
|
581
|
+
# Recurring failure modes and mitigations
|
|
582
|
+
patterns: []
|
|
583
|
+
`,
|
|
584
|
+
"metric-snapshots.yaml": `# Metric Snapshots
|
|
585
|
+
# Periodic captures of operational metrics
|
|
586
|
+
snapshots: []
|
|
587
|
+
`,
|
|
588
|
+
"scoring-rubrics.yaml": `# Scoring Rubrics
|
|
589
|
+
# Evaluation criteria for self-assessment
|
|
590
|
+
rubrics: []
|
|
591
|
+
`,
|
|
592
|
+
};
|
|
593
|
+
|
|
594
|
+
for (const [filename, content] of Object.entries(selfOptTemplates)) {
|
|
595
|
+
writeFileSync(join(targetDir, `self-optimization/${filename}`), content);
|
|
596
|
+
}
|
|
597
|
+
ok(`${Object.keys(selfOptTemplates).length} self-optimization templates`);
|
|
598
|
+
|
|
599
|
+
// .gitkeep files for empty directories that git would otherwise ignore
|
|
600
|
+
const gitkeepDirs = [
|
|
601
|
+
"state/handoffs", "state/huddle", "state/indexes", "state/rag",
|
|
602
|
+
"state/sessions", "state/slack-responded", "state/slack-thread-tracker",
|
|
603
|
+
"state/tmp", "state/inbox/attachments", "state/inbox/processed",
|
|
604
|
+
"state/inbox/whatsapp", "memory/precedents/market-signals",
|
|
605
|
+
"knowledge/decisions/archive", "tests",
|
|
606
|
+
"logs/infra", "logs/monitor", "logs/phone", "logs/sms",
|
|
607
|
+
"logs/whatsapp", "logs/email", "self-optimization/scenarios",
|
|
608
|
+
];
|
|
609
|
+
|
|
610
|
+
for (const dir of gitkeepDirs) {
|
|
611
|
+
const keepFile = join(targetDir, dir, ".gitkeep");
|
|
612
|
+
if (!existsSync(keepFile)) {
|
|
613
|
+
writeFileSync(keepFile, "");
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
ok(".gitkeep files for empty directories");
|
|
617
|
+
|
|
618
|
+
// ── Step 4: Generate package.json ───────────────────────────────────────
|
|
619
|
+
|
|
620
|
+
log("Generating package.json...");
|
|
621
|
+
|
|
622
|
+
const agentPackage = {
|
|
623
|
+
name: targetName,
|
|
624
|
+
version: "1.0.0",
|
|
625
|
+
description: "Maestro AI agent — configure with: maestro setup",
|
|
626
|
+
type: "module",
|
|
627
|
+
private: true,
|
|
628
|
+
scripts: {
|
|
629
|
+
"init-agent": "./scripts/setup/init-agent.sh",
|
|
630
|
+
"configure-macos": "sudo ./scripts/setup/configure-macos.sh",
|
|
631
|
+
"configure-macos:check": "./scripts/setup/configure-macos.sh --check",
|
|
632
|
+
daemon: "node scripts/daemon/maestro-daemon.mjs",
|
|
633
|
+
"cadence:enqueue": "node scripts/cadence/enqueue-cadence-tick.mjs",
|
|
634
|
+
"cadence:consume": "node scripts/daemon/cadence-consumer.mjs",
|
|
635
|
+
"cadence:status": "node scripts/cadence/cadence-status.mjs",
|
|
636
|
+
healthcheck: "./scripts/healthcheck.sh",
|
|
637
|
+
"emergency-stop": "./scripts/emergency-stop.sh",
|
|
638
|
+
resume: "./scripts/resume-operations.sh",
|
|
639
|
+
"pdf:memo": "node scripts/pdf-generation/build-document.mjs --template memo",
|
|
640
|
+
"pdf:board-pack": "node scripts/pdf-generation/build-document.mjs --template board-pack",
|
|
641
|
+
"pdf:letter": "node scripts/pdf-generation/build-document.mjs --template corporate-letter",
|
|
642
|
+
"pdf:investor": "node scripts/pdf-generation/build-document.mjs --template investor-letter",
|
|
643
|
+
"gen:illustration": "node scripts/media-generation/generate-assets.mjs --illustration",
|
|
644
|
+
"gen:video": "node scripts/media-generation/generate-assets.mjs --video",
|
|
645
|
+
"gen:all-missing": "node scripts/media-generation/generate-assets.mjs --all-missing",
|
|
646
|
+
"gen:list": "node scripts/media-generation/generate-assets.mjs --list",
|
|
647
|
+
"slack:events": "node scripts/slack-events-server.mjs",
|
|
648
|
+
"slack:events:start": "./scripts/slack-events-ctl.sh start",
|
|
649
|
+
"slack:events:stop": "./scripts/slack-events-ctl.sh stop",
|
|
650
|
+
"slack:events:status": "./scripts/slack-events-ctl.sh status",
|
|
651
|
+
upgrade: "npx @cohortapp/agent-sdk upgrade",
|
|
652
|
+
init: "npx @cohortapp/agent-sdk init",
|
|
653
|
+
doctor: "npx @cohortapp/agent-sdk doctor",
|
|
654
|
+
},
|
|
655
|
+
dependencies: {
|
|
656
|
+
"@google/genai": "^1.42.0",
|
|
657
|
+
dotenv: "^16.4.5",
|
|
658
|
+
execa: "^9.6.1",
|
|
659
|
+
imapflow: "^1.2.18",
|
|
660
|
+
mailparser: "^3.9.6",
|
|
661
|
+
openai: "^6.33.0",
|
|
662
|
+
},
|
|
663
|
+
devDependencies: {
|
|
664
|
+
sharp: "^0.34.5",
|
|
665
|
+
tsx: "^4.20.6",
|
|
666
|
+
},
|
|
667
|
+
};
|
|
668
|
+
|
|
669
|
+
writeFileSync(
|
|
670
|
+
join(targetDir, "package.json"),
|
|
671
|
+
JSON.stringify(agentPackage, null, 2) + "\n"
|
|
672
|
+
);
|
|
673
|
+
ok("package.json");
|
|
674
|
+
|
|
675
|
+
// ── Step 5: Copy .env.example and .gitignore ────────────────────────────
|
|
676
|
+
|
|
677
|
+
for (const file of [".env.example", ".gitignore"]) {
|
|
678
|
+
const src = join(MAESTRO_ROOT, file);
|
|
679
|
+
if (existsSync(src)) {
|
|
680
|
+
cpSync(src, join(targetDir, file));
|
|
681
|
+
ok(file);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// ── Step 6: Initialize git ──────────────────────────────────────────────
|
|
686
|
+
|
|
687
|
+
log("Initializing git repository...");
|
|
688
|
+
try {
|
|
689
|
+
execFileSync("git", ["init"], { cwd: targetDir, stdio: "pipe" });
|
|
690
|
+
ok("git init");
|
|
691
|
+
} catch {
|
|
692
|
+
warn("git init failed — initialize manually");
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// ── Step 7: Install dependencies ────────────────────────────────────────
|
|
696
|
+
|
|
697
|
+
log("Installing dependencies...");
|
|
698
|
+
try {
|
|
699
|
+
execFileSync("npm", ["install"], { cwd: targetDir, stdio: "pipe" });
|
|
700
|
+
ok("npm install");
|
|
701
|
+
} catch {
|
|
702
|
+
warn("npm install failed — run manually: cd " + targetDir + " && npm install");
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// ── Done ────────────────────────────────────────────────────────────────
|
|
706
|
+
|
|
707
|
+
console.log();
|
|
708
|
+
console.log("╔══════════════════════════════════════════════════════════════╗");
|
|
709
|
+
console.log("║ Agent created successfully! ║");
|
|
710
|
+
console.log("╠══════════════════════════════════════════════════════════════╣");
|
|
711
|
+
console.log("║ ║");
|
|
712
|
+
console.log("║ Next steps: ║");
|
|
713
|
+
console.log(`║ 1. cd ${targetName.padEnd(51)}║`);
|
|
714
|
+
console.log('║ 2. maestro setup ║');
|
|
715
|
+
console.log("║ ║");
|
|
716
|
+
console.log("║ The wizard handles everything: identity, company context, ║");
|
|
717
|
+
console.log("║ model/provider, channels, the operating model, and ║");
|
|
718
|
+
console.log("║ self-verification. ║");
|
|
719
|
+
console.log("║ ║");
|
|
720
|
+
console.log("╚══════════════════════════════════════════════════════════════╝");
|
|
721
|
+
console.log();
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// ---------------------------------------------------------------------------
|
|
725
|
+
// UPGRADE — update framework files in current agent repo (smart merge)
|
|
726
|
+
// ---------------------------------------------------------------------------
|
|
727
|
+
//
|
|
728
|
+
// Upgrade philosophy: by default, never silently overwrite a file the user has
|
|
729
|
+
// modified locally. Detection uses git as the source of truth:
|
|
730
|
+
//
|
|
731
|
+
// • untracked or has-uncommitted-changes in agent repo ⇒ "locally modified"
|
|
732
|
+
// • tracked and clean against HEAD ⇒ "vendored, safe to overwrite"
|
|
733
|
+
//
|
|
734
|
+
// For every framework file we'd otherwise overwrite, we classify into:
|
|
735
|
+
//
|
|
736
|
+
// added — file doesn't exist in agent repo → copy
|
|
737
|
+
// updated — file exists, agent's copy matches HEAD (no local edits) → overwrite
|
|
738
|
+
// same — file exists, content already byte-identical to upstream → skip
|
|
739
|
+
// preserved — file exists with local edits → keep agent's copy, write the
|
|
740
|
+
// upstream version to .maestro/incoming/<path> for manual review
|
|
741
|
+
// (unless --force-overwrite is passed)
|
|
742
|
+
//
|
|
743
|
+
// Flags:
|
|
744
|
+
// --dry-run preview only; no files written
|
|
745
|
+
// --force-overwrite overwrite even locally-modified files (with backup)
|
|
746
|
+
// --no-incoming skip writing .maestro/incoming/ shadows
|
|
747
|
+
// --verbose list every file's classification
|
|
748
|
+
|
|
749
|
+
// Paths that get upgraded. Each entry is { path, mode } where mode is:
|
|
750
|
+
// "smart" — per-file classification described above
|
|
751
|
+
// "merge" — add new files only; never overwrite existing (used for agents/)
|
|
752
|
+
const UPGRADE_PATHS = [
|
|
753
|
+
{ path: "scripts", mode: "smart" },
|
|
754
|
+
{ path: "policies", mode: "smart" },
|
|
755
|
+
{ path: "docs", mode: "smart" },
|
|
756
|
+
{ path: "public/assets", mode: "smart" },
|
|
757
|
+
{ path: "workflows", mode: "smart" },
|
|
758
|
+
{ path: "schedules", mode: "smart" },
|
|
759
|
+
{ path: "desktop-control", mode: "smart" },
|
|
760
|
+
{ path: "ingest", mode: "smart" },
|
|
761
|
+
{ path: "mcp", mode: "smart" },
|
|
762
|
+
{ path: ".claude/commands", mode: "smart" },
|
|
763
|
+
{ path: "plugins/maestro-skills", mode: "smart" },
|
|
764
|
+
{ path: "teams", mode: "smart" },
|
|
765
|
+
{ path: "agents", mode: "merge" },
|
|
766
|
+
// Framework primitives — colocated with scripts/ so relative imports
|
|
767
|
+
// (e.g. ../../lib/cadence-bus.mjs from scripts/cadence/*) resolve in
|
|
768
|
+
// every agent repo without needing @cohortapp/agent-sdk in node_modules.
|
|
769
|
+
{ path: "lib", mode: "smart" },
|
|
770
|
+
];
|
|
771
|
+
|
|
772
|
+
function sha256File(p) {
|
|
773
|
+
return createHash("sha256").update(readFileSync(p)).digest("hex");
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
function walkFiles(root) {
|
|
777
|
+
const out = [];
|
|
778
|
+
if (!existsSync(root)) return out;
|
|
779
|
+
const stack = [root];
|
|
780
|
+
while (stack.length) {
|
|
781
|
+
const dir = stack.pop();
|
|
782
|
+
for (const name of readdirSync(dir)) {
|
|
783
|
+
const full = join(dir, name);
|
|
784
|
+
// Use lstat first so we can detect (and skip) symlinks — particularly
|
|
785
|
+
// broken ones, which are common in shared scaffolds.
|
|
786
|
+
let lst;
|
|
787
|
+
try { lst = lstatSync(full); }
|
|
788
|
+
catch { continue; }
|
|
789
|
+
if (lst.isSymbolicLink()) {
|
|
790
|
+
// Skip symlinks entirely — agents shouldn't inherit links into paths
|
|
791
|
+
// that may not exist on the target machine.
|
|
792
|
+
continue;
|
|
793
|
+
}
|
|
794
|
+
if (lst.isDirectory()) stack.push(full);
|
|
795
|
+
else if (lst.isFile()) out.push(full);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
return out;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// .maestroignore — gitignore-style allowlist of paths the upgrade must NOT
|
|
802
|
+
// touch. Supports comments (#), directory prefixes (trailing /), exact paths,
|
|
803
|
+
// simple globs (* matches a single path segment, ** matches recursively), and
|
|
804
|
+
// negation (! prefix to un-ignore). Match order is top-to-bottom; last match
|
|
805
|
+
// wins, like .gitignore.
|
|
806
|
+
function loadMaestroignore(cwd) {
|
|
807
|
+
const file = join(cwd, ".maestroignore");
|
|
808
|
+
if (!existsSync(file)) return null;
|
|
809
|
+
const patterns = [];
|
|
810
|
+
for (const raw of readFileSync(file, "utf-8").split(/\r?\n/)) {
|
|
811
|
+
const stripped = raw.replace(/^\s+|\s+$/g, "");
|
|
812
|
+
if (!stripped || stripped.startsWith("#")) continue;
|
|
813
|
+
const negate = stripped.startsWith("!");
|
|
814
|
+
const pat = negate ? stripped.slice(1).trim() : stripped;
|
|
815
|
+
if (!pat) continue;
|
|
816
|
+
patterns.push({ pat, negate });
|
|
817
|
+
}
|
|
818
|
+
return patterns.length ? patterns : null;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
function patternToRegex(pat) {
|
|
822
|
+
// Helper: convert a glob-aware pattern to its regex source.
|
|
823
|
+
// ** must be substituted before single * so we don't double-rewrite it.
|
|
824
|
+
// * is intentionally outside the regex-escape char class so the
|
|
825
|
+
// glob substitution can find unescaped `*` characters afterward.
|
|
826
|
+
const globToRe = (p) =>
|
|
827
|
+
p.replace(/[.+^${}()|[\]\\]/g, "\\$&")
|
|
828
|
+
.replace(/\*\*/g, "<DBL>")
|
|
829
|
+
.replace(/\*/g, "[^/]*")
|
|
830
|
+
.replace(/<DBL>/g, ".*");
|
|
831
|
+
|
|
832
|
+
// Directory prefix: `scripts/daemon/` or `agents/role-*/` match
|
|
833
|
+
// anything under that path (recursive).
|
|
834
|
+
if (pat.endsWith("/")) {
|
|
835
|
+
return new RegExp("^" + globToRe(pat));
|
|
836
|
+
}
|
|
837
|
+
// Single-line glob: must match in full (no trailing slash).
|
|
838
|
+
if (pat.includes("*")) {
|
|
839
|
+
return new RegExp("^" + globToRe(pat) + "$");
|
|
840
|
+
}
|
|
841
|
+
// Exact: also matches anything underneath (gitignore semantics — a
|
|
842
|
+
// pattern without trailing slash still protects a directory if it
|
|
843
|
+
// resolves to one).
|
|
844
|
+
const escaped = pat.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
845
|
+
return new RegExp("^" + escaped + "(/|$)");
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
function matchesIgnore(repoRel, patterns) {
|
|
849
|
+
if (!patterns) return false;
|
|
850
|
+
let ignored = false;
|
|
851
|
+
for (const { pat, negate } of patterns) {
|
|
852
|
+
if (patternToRegex(pat).test(repoRel)) {
|
|
853
|
+
ignored = !negate;
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
return ignored;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
function isGitRepo(cwd) {
|
|
860
|
+
try {
|
|
861
|
+
execFileSync("git", ["rev-parse", "--is-inside-work-tree"], { cwd, stdio: "pipe" });
|
|
862
|
+
return true;
|
|
863
|
+
} catch { return false; }
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
// Build a Set of repo-relative paths that are dirty (modified, added, untracked).
|
|
867
|
+
// Single git invocation is much faster than per-file checks.
|
|
868
|
+
function dirtyPathSet(cwd) {
|
|
869
|
+
const set = new Set();
|
|
870
|
+
try {
|
|
871
|
+
const out = execFileSync(
|
|
872
|
+
"git",
|
|
873
|
+
["status", "--porcelain=v1", "-z", "--untracked-files=all"],
|
|
874
|
+
{ cwd, encoding: "utf-8" }
|
|
875
|
+
);
|
|
876
|
+
// -z separates records with NUL; each record is "XY path" (no quoting).
|
|
877
|
+
// For renames (R/C) the format is "R newpath\0oldpath\0" — we only care
|
|
878
|
+
// about destination, but for safety we treat both as dirty.
|
|
879
|
+
for (const rec of out.split("\0")) {
|
|
880
|
+
if (!rec) continue;
|
|
881
|
+
const status = rec.slice(0, 2);
|
|
882
|
+
const path = rec.slice(3);
|
|
883
|
+
// Anything in porcelain output is non-clean by definition.
|
|
884
|
+
if (status !== " ") set.add(path);
|
|
885
|
+
}
|
|
886
|
+
} catch {
|
|
887
|
+
// Not a git repo or git failed — caller will fall back to caution mode.
|
|
888
|
+
}
|
|
889
|
+
return set;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
// ---------------------------------------------------------------------------
|
|
893
|
+
// Cadence-bus migration (called from upgrade)
|
|
894
|
+
// ---------------------------------------------------------------------------
|
|
895
|
+
//
|
|
896
|
+
// As of maestro 1.8 scheduled cadence ticks no longer spawn a fresh Claude
|
|
897
|
+
// Code session per tick. launchd plists now invoke a lightweight Node
|
|
898
|
+
// enqueue script that drops a JSON event onto state/cadence-bus/; the
|
|
899
|
+
// persistent maestro-daemon.mjs consumes the bus.
|
|
900
|
+
//
|
|
901
|
+
// This migration runs idempotently on every upgrade:
|
|
902
|
+
// 1. Detect generated plists at scripts/local-triggers/plists/ that still
|
|
903
|
+
// reference run-trigger.sh (the legacy per-tick spawn path).
|
|
904
|
+
// 2. Back them up to .maestro/backup/plists/<utc-timestamp>/.
|
|
905
|
+
// 3. Regenerate plists via scripts/local-triggers/generate-plists.sh so
|
|
906
|
+
// the on-disk files match the cadence-bus architecture.
|
|
907
|
+
// 4. Surface installed plists at ~/Library/LaunchAgents/ that still match
|
|
908
|
+
// the legacy pattern, with exact launchctl unload/load commands the
|
|
909
|
+
// operator can run.
|
|
910
|
+
//
|
|
911
|
+
// Returns a summary object the caller uses to print user-visible output.
|
|
912
|
+
|
|
913
|
+
function plistRefersToLegacy(path) {
|
|
914
|
+
try {
|
|
915
|
+
const body = readFileSync(path, "utf-8");
|
|
916
|
+
return body.includes(LEGACY_PLIST_INDICATOR);
|
|
917
|
+
} catch {
|
|
918
|
+
return false;
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
function plistMatchesArch(path) {
|
|
923
|
+
try {
|
|
924
|
+
const body = readFileSync(path, "utf-8");
|
|
925
|
+
return body.includes(PLIST_ARCH_MARKER);
|
|
926
|
+
} catch {
|
|
927
|
+
return false;
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
function listPlists(dir) {
|
|
932
|
+
if (!existsSync(dir)) return [];
|
|
933
|
+
try {
|
|
934
|
+
return readdirSync(dir).filter((n) => n.endsWith(".plist")).map((n) => join(dir, n));
|
|
935
|
+
} catch {
|
|
936
|
+
return [];
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
function migrateCadenceBus(cwd, flags) {
|
|
941
|
+
const summary = {
|
|
942
|
+
generated_legacy: [],
|
|
943
|
+
generated_modern: [],
|
|
944
|
+
installed_legacy: [],
|
|
945
|
+
backed_up: [],
|
|
946
|
+
regenerated: false,
|
|
947
|
+
skipped_regen_reason: null,
|
|
948
|
+
bus_dir: null,
|
|
949
|
+
notes: [],
|
|
950
|
+
};
|
|
951
|
+
|
|
952
|
+
// Bootstrap the cadence-bus directory tree even when nothing else is
|
|
953
|
+
// moving — needed for fresh installs that upgrade through this path.
|
|
954
|
+
const busBase = join(cwd, "state", "cadence-bus");
|
|
955
|
+
summary.bus_dir = busBase;
|
|
956
|
+
for (const d of ["inbox", "claimed", "processed", "failed", "dlq"]) {
|
|
957
|
+
const full = join(busBase, d);
|
|
958
|
+
if (!existsSync(full) && !flags.dryRun) {
|
|
959
|
+
mkdirSync(full, { recursive: true });
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
const versionFile = join(busBase, "VERSION");
|
|
963
|
+
if (!existsSync(versionFile) && !flags.dryRun) {
|
|
964
|
+
writeFileSync(versionFile, "1\n");
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
// Inspect generated plists.
|
|
968
|
+
const generatedDir = join(cwd, "scripts/local-triggers/plists");
|
|
969
|
+
const generated = listPlists(generatedDir);
|
|
970
|
+
for (const p of generated) {
|
|
971
|
+
const rel = relative(cwd, p);
|
|
972
|
+
if (plistRefersToLegacy(p)) summary.generated_legacy.push(rel);
|
|
973
|
+
else if (plistMatchesArch(p)) summary.generated_modern.push(rel);
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
// Inspect installed plists.
|
|
977
|
+
const installedDir = join(process.env.HOME || "/", "Library/LaunchAgents");
|
|
978
|
+
for (const p of listPlists(installedDir)) {
|
|
979
|
+
const name = basename(p);
|
|
980
|
+
// Only consider plists owned by Maestro — labels start with ai.maestro.
|
|
981
|
+
// (or legacy ai.adaptic. for agents installed before the rename).
|
|
982
|
+
if (!name.startsWith("ai.maestro.") && !name.startsWith("ai.adaptic.")) continue;
|
|
983
|
+
if (plistRefersToLegacy(p)) summary.installed_legacy.push(p);
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
// Back up any legacy generated plists.
|
|
987
|
+
if (summary.generated_legacy.length > 0) {
|
|
988
|
+
const ts = new Date().toISOString().replace(/[:.]/g, "-");
|
|
989
|
+
const backupDir = join(cwd, ".maestro/backup/plists", ts);
|
|
990
|
+
if (!flags.dryRun) {
|
|
991
|
+
mkdirSync(backupDir, { recursive: true });
|
|
992
|
+
for (const rel of summary.generated_legacy) {
|
|
993
|
+
const src = join(cwd, rel);
|
|
994
|
+
const dst = join(backupDir, basename(src));
|
|
995
|
+
try {
|
|
996
|
+
copyFileSync(src, dst);
|
|
997
|
+
summary.backed_up.push(relative(cwd, dst));
|
|
998
|
+
} catch (err) {
|
|
999
|
+
summary.notes.push(`backup failed for ${rel}: ${err.message}`);
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
} else {
|
|
1003
|
+
// Dry-run: still report what would be backed up.
|
|
1004
|
+
summary.notes.push(`would back up ${summary.generated_legacy.length} legacy plist(s) to .maestro/backup/plists/${ts}/`);
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
// Regenerate via the bundled script. Soft failure — operator can rerun.
|
|
1008
|
+
const generator = join(cwd, "scripts/local-triggers/generate-plists.sh");
|
|
1009
|
+
if (existsSync(generator)) {
|
|
1010
|
+
if (!flags.dryRun) {
|
|
1011
|
+
try {
|
|
1012
|
+
execFileSync("/bin/bash", [generator], { cwd, stdio: "pipe" });
|
|
1013
|
+
summary.regenerated = true;
|
|
1014
|
+
} catch (err) {
|
|
1015
|
+
summary.skipped_regen_reason = `generate-plists.sh failed: ${err.message.split("\n")[0]}`;
|
|
1016
|
+
}
|
|
1017
|
+
} else {
|
|
1018
|
+
summary.notes.push("dry-run: would regenerate plists via scripts/local-triggers/generate-plists.sh");
|
|
1019
|
+
}
|
|
1020
|
+
} else {
|
|
1021
|
+
summary.skipped_regen_reason = "scripts/local-triggers/generate-plists.sh missing";
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
return summary;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
function printCadenceBusSummary(summary, flags) {
|
|
1029
|
+
console.log();
|
|
1030
|
+
console.log(`${C.bold}Cadence bus migration${C.reset}${flags.dryRun ? " (dry run)" : ""}`);
|
|
1031
|
+
if (summary.generated_legacy.length === 0 && summary.installed_legacy.length === 0) {
|
|
1032
|
+
ok("Already on cadence-bus architecture (no legacy plists found).");
|
|
1033
|
+
if (summary.generated_modern.length > 0) {
|
|
1034
|
+
ok(`${summary.generated_modern.length} generated plist(s) carry the cadence-bus marker.`);
|
|
1035
|
+
}
|
|
1036
|
+
return;
|
|
1037
|
+
}
|
|
1038
|
+
if (summary.generated_legacy.length > 0) {
|
|
1039
|
+
warn(`${summary.generated_legacy.length} generated plist(s) still call run-trigger.sh directly:`);
|
|
1040
|
+
for (const p of summary.generated_legacy.slice(0, 8)) console.log(` ${p}`);
|
|
1041
|
+
if (summary.generated_legacy.length > 8) {
|
|
1042
|
+
console.log(` … and ${summary.generated_legacy.length - 8} more`);
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
if (summary.backed_up.length > 0) {
|
|
1046
|
+
ok(`Backed up ${summary.backed_up.length} legacy plist(s) under ${dirname(summary.backed_up[0])}`);
|
|
1047
|
+
}
|
|
1048
|
+
if (summary.regenerated) {
|
|
1049
|
+
ok("Regenerated plists with the cadence-bus architecture.");
|
|
1050
|
+
} else if (summary.skipped_regen_reason) {
|
|
1051
|
+
warn(`Skipped plist regeneration: ${summary.skipped_regen_reason}`);
|
|
1052
|
+
warn("Run manually: ./scripts/local-triggers/generate-plists.sh");
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
if (summary.installed_legacy.length > 0) {
|
|
1056
|
+
console.log();
|
|
1057
|
+
warn(`${summary.installed_legacy.length} installed launchd plist(s) at ~/Library/LaunchAgents/ still use the legacy spawn-per-tick pattern.`);
|
|
1058
|
+
warn("Reload them so launchctl picks up the cadence-bus versions:");
|
|
1059
|
+
for (const p of summary.installed_legacy.slice(0, 8)) {
|
|
1060
|
+
const label = basename(p, ".plist");
|
|
1061
|
+
console.log(` launchctl unload "${p}" && launchctl load "${p}"`);
|
|
1062
|
+
// Cosmetic alias for label clarity.
|
|
1063
|
+
void label;
|
|
1064
|
+
}
|
|
1065
|
+
if (summary.installed_legacy.length > 8) {
|
|
1066
|
+
console.log(` … and ${summary.installed_legacy.length - 8} more`);
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
for (const note of summary.notes) console.log(` note: ${note}`);
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
function parseUpgradeFlags(args) {
|
|
1074
|
+
const flags = {
|
|
1075
|
+
dryRun: false,
|
|
1076
|
+
forceOverwrite: false,
|
|
1077
|
+
noIncoming: false,
|
|
1078
|
+
verbose: false,
|
|
1079
|
+
};
|
|
1080
|
+
for (const a of args) {
|
|
1081
|
+
if (a === "--dry-run" || a === "-n") flags.dryRun = true;
|
|
1082
|
+
else if (a === "--force-overwrite" || a === "--force") flags.forceOverwrite = true;
|
|
1083
|
+
else if (a === "--no-incoming") flags.noIncoming = true;
|
|
1084
|
+
else if (a === "--verbose" || a === "-v") flags.verbose = true;
|
|
1085
|
+
else if (a === "--help" || a === "-h") return null;
|
|
1086
|
+
else { fail(`Unknown flag: ${a}`); process.exit(1); }
|
|
1087
|
+
}
|
|
1088
|
+
return flags;
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
async function upgrade(args = []) {
|
|
1092
|
+
const flags = parseUpgradeFlags(args);
|
|
1093
|
+
if (flags === null) {
|
|
1094
|
+
console.log(`
|
|
1095
|
+
Usage: maestro upgrade [flags]
|
|
1096
|
+
|
|
1097
|
+
Flags:
|
|
1098
|
+
--dry-run, -n Preview changes without writing
|
|
1099
|
+
--force-overwrite Overwrite even locally-modified files (backs them up)
|
|
1100
|
+
--no-incoming Don't write .maestro/incoming/ shadows for preserved files
|
|
1101
|
+
--verbose, -v Print classification for every file
|
|
1102
|
+
--help, -h Show this help
|
|
1103
|
+
|
|
1104
|
+
Per-file behaviour:
|
|
1105
|
+
added — new upstream file → copy
|
|
1106
|
+
updated — existed, byte-equal to your committed copy → safe overwrite
|
|
1107
|
+
same — existed and already byte-identical to upstream → skip
|
|
1108
|
+
ignored — matches a pattern in .maestroignore → never touched (any state)
|
|
1109
|
+
preserved — has uncommitted local edits → keep yours, upstream lands at
|
|
1110
|
+
.maestro/incoming/<path> for manual diff
|
|
1111
|
+
mergeKept — under agents/ → never overwrite (custom agents preserved)
|
|
1112
|
+
forced — overwritten by --force-overwrite; backup at .maestro/backup/<path>
|
|
1113
|
+
|
|
1114
|
+
.maestroignore format (gitignore-style, top-down, last match wins):
|
|
1115
|
+
scripts/slack-send.sh exact file
|
|
1116
|
+
scripts/daemon/ directory and everything beneath
|
|
1117
|
+
scripts/send-email-*.py single-segment glob
|
|
1118
|
+
workflows/** recursive glob
|
|
1119
|
+
!workflows/quarterly/ un-ignore (force overwrite)
|
|
1120
|
+
`);
|
|
1121
|
+
return;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
const cwd = process.cwd();
|
|
1125
|
+
|
|
1126
|
+
// config/agent.json is the single source of truth (config/agent.ts is the
|
|
1127
|
+
// derived wrapper). Accept either as the agent-repo signal — but agent.json is
|
|
1128
|
+
// primary; a CLAUDE.md alone (framework checkout) still qualifies for upgrade.
|
|
1129
|
+
if (!existsSync(join(cwd, "config/agent.json")) && !existsSync(join(cwd, "config/agent.ts")) && !existsSync(join(cwd, "CLAUDE.md"))) {
|
|
1130
|
+
fail("Not a Maestro agent directory (config/agent.json not found)");
|
|
1131
|
+
process.exit(1);
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
const inGit = isGitRepo(cwd);
|
|
1135
|
+
if (!inGit) {
|
|
1136
|
+
warn("Not in a git repo — cannot detect local modifications.");
|
|
1137
|
+
warn("All existing files will be treated as locally modified and preserved.");
|
|
1138
|
+
warn("Use --force-overwrite to override.");
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
const dirty = inGit ? dirtyPathSet(cwd) : null;
|
|
1142
|
+
const ignorePatterns = loadMaestroignore(cwd);
|
|
1143
|
+
if (ignorePatterns) {
|
|
1144
|
+
log(`.maestroignore loaded (${ignorePatterns.length} pattern${ignorePatterns.length === 1 ? "" : "s"})`);
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
const banner = flags.dryRun ? "DRY RUN — " : "";
|
|
1148
|
+
log(`${banner}Upgrading framework files from @cohortapp/agent-sdk...`);
|
|
1149
|
+
|
|
1150
|
+
const counts = { added: 0, updated: 0, same: 0, ignored: 0, preserved: 0, mergeKept: 0, forced: 0 };
|
|
1151
|
+
const preservedFiles = [];
|
|
1152
|
+
const ignoredFiles = [];
|
|
1153
|
+
|
|
1154
|
+
for (const { path: relRoot, mode } of UPGRADE_PATHS) {
|
|
1155
|
+
const srcRoot = join(MAESTRO_ROOT, relRoot);
|
|
1156
|
+
const dstRoot = join(cwd, relRoot);
|
|
1157
|
+
if (!existsSync(srcRoot)) continue;
|
|
1158
|
+
|
|
1159
|
+
const srcFiles = walkFiles(srcRoot);
|
|
1160
|
+
for (const srcFile of srcFiles) {
|
|
1161
|
+
const relFromRoot = relative(srcRoot, srcFile);
|
|
1162
|
+
const dstFile = join(dstRoot, relFromRoot);
|
|
1163
|
+
const repoRel = relative(cwd, dstFile).split(sep).join("/");
|
|
1164
|
+
|
|
1165
|
+
const isIgnored = matchesIgnore(repoRel, ignorePatterns);
|
|
1166
|
+
|
|
1167
|
+
// Case 0: .maestroignore match — never touch, regardless of state.
|
|
1168
|
+
// Checked BEFORE existsSync so that protected new-files-from-upstream
|
|
1169
|
+
// are never silently introduced into the agent repo either.
|
|
1170
|
+
if (isIgnored) {
|
|
1171
|
+
counts.ignored++;
|
|
1172
|
+
ignoredFiles.push(repoRel);
|
|
1173
|
+
if (flags.verbose) console.log(` · ${repoRel} (ignored via .maestroignore)`);
|
|
1174
|
+
continue;
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
// Case 1: new file (doesn't exist in agent) — always copy.
|
|
1178
|
+
if (!existsSync(dstFile)) {
|
|
1179
|
+
if (!flags.dryRun) {
|
|
1180
|
+
mkdirSync(dirname(dstFile), { recursive: true });
|
|
1181
|
+
copyFileSync(srcFile, dstFile);
|
|
1182
|
+
}
|
|
1183
|
+
counts.added++;
|
|
1184
|
+
if (flags.verbose) ok(`+ ${repoRel}`);
|
|
1185
|
+
continue;
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
// Case 2: byte-identical — nothing to do.
|
|
1189
|
+
const srcHash = sha256File(srcFile);
|
|
1190
|
+
const dstHash = sha256File(dstFile);
|
|
1191
|
+
if (srcHash === dstHash) {
|
|
1192
|
+
counts.same++;
|
|
1193
|
+
if (flags.verbose) console.log(` = ${repoRel}`);
|
|
1194
|
+
continue;
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
// Case 3: merge-mode never overwrites existing files.
|
|
1198
|
+
if (mode === "merge") {
|
|
1199
|
+
counts.mergeKept++;
|
|
1200
|
+
if (flags.verbose) console.log(` ~ ${repoRel} (merge-mode, kept)`);
|
|
1201
|
+
continue;
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
// Case 4: locally modified? Determine via git dirty set.
|
|
1205
|
+
const locallyModified = inGit ? dirty.has(repoRel) : true;
|
|
1206
|
+
|
|
1207
|
+
if (locallyModified && !flags.forceOverwrite) {
|
|
1208
|
+
counts.preserved++;
|
|
1209
|
+
preservedFiles.push(repoRel);
|
|
1210
|
+
if (!flags.dryRun && !flags.noIncoming) {
|
|
1211
|
+
const shadow = join(cwd, ".maestro", "incoming", repoRel);
|
|
1212
|
+
mkdirSync(dirname(shadow), { recursive: true });
|
|
1213
|
+
copyFileSync(srcFile, shadow);
|
|
1214
|
+
}
|
|
1215
|
+
if (flags.verbose) console.log(` ~ ${repoRel} (local edits, preserved)`);
|
|
1216
|
+
continue;
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
// Case 5: overwrite (clean tracked file, or --force).
|
|
1220
|
+
if (locallyModified && flags.forceOverwrite) {
|
|
1221
|
+
// Back up the about-to-be-clobbered file.
|
|
1222
|
+
if (!flags.dryRun) {
|
|
1223
|
+
const backup = join(cwd, ".maestro", "backup", repoRel);
|
|
1224
|
+
mkdirSync(dirname(backup), { recursive: true });
|
|
1225
|
+
copyFileSync(dstFile, backup);
|
|
1226
|
+
}
|
|
1227
|
+
counts.forced++;
|
|
1228
|
+
if (flags.verbose) warn(`! ${repoRel} (forced; backup written)`);
|
|
1229
|
+
} else {
|
|
1230
|
+
counts.updated++;
|
|
1231
|
+
if (flags.verbose) console.log(` > ${repoRel}`);
|
|
1232
|
+
}
|
|
1233
|
+
if (!flags.dryRun) {
|
|
1234
|
+
mkdirSync(dirname(dstFile), { recursive: true });
|
|
1235
|
+
copyFileSync(srcFile, dstFile);
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
// Ensure standard runtime directories exist.
|
|
1241
|
+
const ensureDirs = [
|
|
1242
|
+
"state/handoffs", "state/huddle", "state/indexes", "state/rag",
|
|
1243
|
+
"state/sessions", "state/slack-responded", "state/slack-thread-tracker",
|
|
1244
|
+
"state/tmp", "state/inbox/whatsapp", "state/inbox/attachments",
|
|
1245
|
+
"state/inbox/processed", "memory/precedents/market-signals",
|
|
1246
|
+
"knowledge/decisions/archive", "self-optimization/scenarios", "tests",
|
|
1247
|
+
"logs/infra", "logs/monitor", "logs/phone", "logs/sms",
|
|
1248
|
+
"logs/whatsapp", "logs/email",
|
|
1249
|
+
// Cadence bus (architecture introduced in maestro 1.8). Idempotent.
|
|
1250
|
+
"state/cadence-bus", "state/cadence-bus/inbox", "state/cadence-bus/claimed",
|
|
1251
|
+
"state/cadence-bus/processed", "state/cadence-bus/failed", "state/cadence-bus/dlq",
|
|
1252
|
+
"logs/cadence-bus",
|
|
1253
|
+
// Org/secrets/diagnostics/scheduling/hook spine (maestro 1.18+). Runtime
|
|
1254
|
+
// dirs for lib/{secrets,diagnostics,scheduling,hooks,org}; all fail-open if
|
|
1255
|
+
// absent, but pre-creating them avoids first-write races. Idempotent.
|
|
1256
|
+
"state/secrets", "state/scheduling", "state/scheduling/jobs",
|
|
1257
|
+
"logs/diagnostics", "logs/diagnostics/counters", "logs/traces",
|
|
1258
|
+
"logs/events", "logs/audit/hooks",
|
|
1259
|
+
];
|
|
1260
|
+
let newDirs = 0;
|
|
1261
|
+
for (const dir of ensureDirs) {
|
|
1262
|
+
const dirPath = join(cwd, dir);
|
|
1263
|
+
if (!existsSync(dirPath)) {
|
|
1264
|
+
if (!flags.dryRun) mkdirSync(dirPath, { recursive: true });
|
|
1265
|
+
newDirs++;
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
// ── Scaffold config templates (add-only) ────────────────────────────────
|
|
1270
|
+
// scaffold/config/* are disabled-by-default templates a new agent gets at
|
|
1271
|
+
// `create` time (cpSync flattens scaffold/ over the repo root). UPGRADE_PATHS
|
|
1272
|
+
// deliberately does NOT include config/ (it is operator-owned, never smart-
|
|
1273
|
+
// merged), so existing agents would otherwise never receive NEW config
|
|
1274
|
+
// templates introduced by a framework rev — e.g. org.yaml / secrets.yaml /
|
|
1275
|
+
// alerts.yaml shipped with the org/secrets/diagnostics systems. Copy any
|
|
1276
|
+
// scaffold/config file that the agent does NOT already have. We NEVER
|
|
1277
|
+
// overwrite an existing config (operator-owned, may be customised or enabled);
|
|
1278
|
+
// .maestroignore and --dry-run are honoured. The `.example` companions land
|
|
1279
|
+
// too so the agent has the documented full form alongside the live default.
|
|
1280
|
+
const scaffoldConfigDir = join(SCAFFOLD_DIR, "config");
|
|
1281
|
+
let newConfigs = 0;
|
|
1282
|
+
if (existsSync(scaffoldConfigDir)) {
|
|
1283
|
+
for (const name of readdirSync(scaffoldConfigDir)) {
|
|
1284
|
+
const src = join(scaffoldConfigDir, name);
|
|
1285
|
+
let st;
|
|
1286
|
+
try { st = lstatSync(src); } catch { continue; }
|
|
1287
|
+
if (!st.isFile()) continue; // configs are flat files
|
|
1288
|
+
const dst = join(cwd, "config", name);
|
|
1289
|
+
const repoRel = relative(cwd, dst).split(sep).join("/");
|
|
1290
|
+
if (matchesIgnore(repoRel, ignorePatterns)) {
|
|
1291
|
+
counts.ignored++;
|
|
1292
|
+
ignoredFiles.push(repoRel);
|
|
1293
|
+
if (flags.verbose) console.log(` · ${repoRel} (ignored via .maestroignore)`);
|
|
1294
|
+
continue;
|
|
1295
|
+
}
|
|
1296
|
+
if (existsSync(dst)) {
|
|
1297
|
+
// Operator owns config/ — never clobber an existing template.
|
|
1298
|
+
if (flags.verbose) console.log(` ~ ${repoRel} (config exists, kept)`);
|
|
1299
|
+
continue;
|
|
1300
|
+
}
|
|
1301
|
+
if (!flags.dryRun) {
|
|
1302
|
+
mkdirSync(dirname(dst), { recursive: true });
|
|
1303
|
+
copyFileSync(src, dst);
|
|
1304
|
+
}
|
|
1305
|
+
counts.added++;
|
|
1306
|
+
newConfigs++;
|
|
1307
|
+
if (flags.verbose) ok(`+ ${repoRel}`);
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
// ── SoT self-heal: kill the agent.ts/agent.json split-brain ─────────────
|
|
1312
|
+
// The single source of truth is config/agent.json with config/agent.ts as a
|
|
1313
|
+
// thin re-export wrapper. Legacy repos carry a standalone literal agent.ts
|
|
1314
|
+
// (and may lack agent.json). Auto-migrate so they self-heal on upgrade.
|
|
1315
|
+
if (!flags.dryRun) {
|
|
1316
|
+
try {
|
|
1317
|
+
const tsPath = join(cwd, "config/agent.ts");
|
|
1318
|
+
const jsonPath = join(cwd, "config/agent.json");
|
|
1319
|
+
const tsSrc = existsSync(tsPath) ? readFileSync(tsPath, "utf-8") : "";
|
|
1320
|
+
const tsIsWrapper = /import\s+\w+\s+from\s+['"]\.\/agent\.json['"]/.test(tsSrc);
|
|
1321
|
+
const tsIsStandalone = existsSync(tsPath) && /export\s+const\s+agent\s*(?::[^=]+)?=\s*\{/.test(tsSrc) && !tsIsWrapper;
|
|
1322
|
+
|
|
1323
|
+
if (!existsSync(jsonPath) && tsIsStandalone) {
|
|
1324
|
+
// No SoT yet — run the full migration (extracts agent.json + writes wrapper).
|
|
1325
|
+
try {
|
|
1326
|
+
execFileSync("node", [join(MAESTRO_ROOT, "scripts/setup/migrate-agent-to-sot.mjs")], {
|
|
1327
|
+
cwd, env: { ...process.env, AGENT_DIR: cwd, AGENT_ROOT: cwd, MAESTRO_ROOT }, stdio: "pipe",
|
|
1328
|
+
});
|
|
1329
|
+
ok("migrated config/agent.ts → config/agent.json + thin wrapper (SoT self-heal)");
|
|
1330
|
+
} catch (e) {
|
|
1331
|
+
warn(`SoT migration could not run automatically: ${e && e.message ? e.message : e}`);
|
|
1332
|
+
warn(" Run manually: npx tsx scripts/setup/migrate-agent-to-sot.mjs");
|
|
1333
|
+
}
|
|
1334
|
+
} else if (existsSync(jsonPath) && tsIsStandalone) {
|
|
1335
|
+
// SoT exists but the .ts is still a standalone literal — just re-wrap it.
|
|
1336
|
+
const { ensureSotWrapper } = await import(join(MAESTRO_ROOT, "lib", "setup", "sot.mjs"));
|
|
1337
|
+
const r = ensureSotWrapper(cwd);
|
|
1338
|
+
if (r.wrote) ok("rewrote config/agent.ts as the SoT wrapper (re-exports agent.json)");
|
|
1339
|
+
}
|
|
1340
|
+
} catch (e) {
|
|
1341
|
+
warn(`SoT self-heal skipped: ${e && e.message ? e.message : e}`);
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
// Cadence-bus migration: detect legacy spawn-per-tick plists and rewrite
|
|
1346
|
+
// them so launchd enqueues cadence events instead of spawning Claude.
|
|
1347
|
+
const cadenceMigration = migrateCadenceBus(cwd, flags);
|
|
1348
|
+
|
|
1349
|
+
// Regenerate derived config from config/agent.json (the SOT).
|
|
1350
|
+
// Soft failures — older agents that haven't migrated to the SOT layout
|
|
1351
|
+
// yet don't have agent.json and that's fine.
|
|
1352
|
+
if (!flags.dryRun && existsSync(join(cwd, "config", "agent.json"))) {
|
|
1353
|
+
// config/agent.env — env-var file consumed by shell scripts.
|
|
1354
|
+
try {
|
|
1355
|
+
execFileSync(
|
|
1356
|
+
"node",
|
|
1357
|
+
[join(MAESTRO_ROOT, "scripts/setup/generate-agent-env.mjs")],
|
|
1358
|
+
{ cwd, env: { ...process.env, AGENT_DIR: cwd }, stdio: "pipe" }
|
|
1359
|
+
);
|
|
1360
|
+
ok("regenerated config/agent.env");
|
|
1361
|
+
} catch (e) {
|
|
1362
|
+
warn(`could not regenerate config/agent.env: ${e.message}`);
|
|
1363
|
+
}
|
|
1364
|
+
// config/environment.yaml — operational config consumed by workflows.
|
|
1365
|
+
// Replaces the previously-static hardcoded identity fields.
|
|
1366
|
+
try {
|
|
1367
|
+
execFileSync(
|
|
1368
|
+
"node",
|
|
1369
|
+
[join(MAESTRO_ROOT, "scripts/setup/render-environment-yaml.mjs")],
|
|
1370
|
+
{ cwd, env: { ...process.env, AGENT_DIR: cwd }, stdio: "pipe" }
|
|
1371
|
+
);
|
|
1372
|
+
ok("regenerated config/environment.yaml");
|
|
1373
|
+
} catch (e) {
|
|
1374
|
+
warn(`could not regenerate config/environment.yaml: ${e.message}`);
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
// Summary
|
|
1379
|
+
console.log();
|
|
1380
|
+
console.log(`${C.bold}Upgrade summary${C.reset}${flags.dryRun ? " (dry run, nothing written)" : ""}`);
|
|
1381
|
+
if (counts.added) ok(`${counts.added} added (new files)`);
|
|
1382
|
+
if (counts.updated) ok(`${counts.updated} updated (vendored, no local edits)`);
|
|
1383
|
+
if (counts.same) console.log(` = ${counts.same} already up-to-date`);
|
|
1384
|
+
if (counts.ignored) console.log(` · ${counts.ignored} ignored (.maestroignore protected)`);
|
|
1385
|
+
if (counts.mergeKept) console.log(` ~ ${counts.mergeKept} merge-mode kept (agents/ custom files preserved)`);
|
|
1386
|
+
if (counts.preserved) warn(`${counts.preserved} preserved (local edits — kept your version)`);
|
|
1387
|
+
if (counts.forced) warn(`${counts.forced} force-overwritten (backups in .maestro/backup/)`);
|
|
1388
|
+
if (newDirs) ok(`${newDirs} new directories created`);
|
|
1389
|
+
|
|
1390
|
+
if (preservedFiles.length && !flags.noIncoming && !flags.dryRun) {
|
|
1391
|
+
console.log();
|
|
1392
|
+
log("Upstream versions of your locally-modified files saved to .maestro/incoming/");
|
|
1393
|
+
log("Review with: diff <path> .maestro/incoming/<path>");
|
|
1394
|
+
for (const p of preservedFiles.slice(0, 5)) console.log(` ${p}`);
|
|
1395
|
+
if (preservedFiles.length > 5) console.log(` … and ${preservedFiles.length - 5} more`);
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
// Surface the cadence-bus migration summary AFTER the file-by-file
|
|
1399
|
+
// upgrade summary so the operator sees both layers of change.
|
|
1400
|
+
printCadenceBusSummary(cadenceMigration, flags);
|
|
1401
|
+
|
|
1402
|
+
// Reconcile framework features — run auto-init steps, queue manual ones.
|
|
1403
|
+
if (!flags.dryRun && existsSync(FRAMEWORK_FEATURES_PATH)) {
|
|
1404
|
+
try {
|
|
1405
|
+
const { loadRegistry, reconcileFeatures } = await import("../lib/feature-init.mjs");
|
|
1406
|
+
const registry = loadRegistry(FRAMEWORK_FEATURES_PATH);
|
|
1407
|
+
console.log();
|
|
1408
|
+
log("Reconciling framework features...");
|
|
1409
|
+
const result = reconcileFeatures({
|
|
1410
|
+
maestroRoot: MAESTRO_ROOT,
|
|
1411
|
+
agentRoot: cwd,
|
|
1412
|
+
registry,
|
|
1413
|
+
runAuto: true,
|
|
1414
|
+
logger: (entry) => {
|
|
1415
|
+
if (entry.stage === "completed") ok(`init: ${entry.feature}`);
|
|
1416
|
+
else if (entry.stage === "failed") warn(`init failed: ${entry.feature} — ${entry.message}`);
|
|
1417
|
+
else if (entry.stage === "pending") console.log(` ~ pending: ${entry.feature} (run \`maestro init ${entry.feature} --apply\`)`);
|
|
1418
|
+
},
|
|
1419
|
+
});
|
|
1420
|
+
if (result.pending.length) {
|
|
1421
|
+
warn(`${result.pending.length} feature(s) still pending — run: maestro init`);
|
|
1422
|
+
} else if (result.ranAuto.length) {
|
|
1423
|
+
ok(`${result.ranAuto.length} feature(s) initialised this run`);
|
|
1424
|
+
} else {
|
|
1425
|
+
ok("All framework features up to date");
|
|
1426
|
+
}
|
|
1427
|
+
} catch (e) {
|
|
1428
|
+
warn(`feature reconciliation skipped: ${e.message}`);
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
console.log();
|
|
1433
|
+
log("Agent-specific paths (config/, CLAUDE.md, knowledge/, memory/, state/, outputs/, logs/, .env) were NOT touched.");
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
// ---------------------------------------------------------------------------
|
|
1437
|
+
// DOCTOR — cost-telemetry tripwire (pure predicate, unit-tested)
|
|
1438
|
+
// ---------------------------------------------------------------------------
|
|
1439
|
+
//
|
|
1440
|
+
// Observability audit F2: the daily-budget governor sums estimated_usd across
|
|
1441
|
+
// today's cost ledger rows. If callers spawn sessions but record $0 (the
|
|
1442
|
+
// historic --input-tokens 0 --output-tokens 0 bug), the ledger fills with rows
|
|
1443
|
+
// whose estimated_usd is 0, the spend band is permanently 0%, and the budget
|
|
1444
|
+
// cap silently can never trip. Doctor surfaces that "blind" state in RED.
|
|
1445
|
+
//
|
|
1446
|
+
// The ledger lives at <agentRoot>/state/cost-tracking/<UTC-YYYY-MM-DD>.jsonl
|
|
1447
|
+
// and is written by scripts/cost/track-claude-usage.mjs (record subcommand).
|
|
1448
|
+
// Each row carries estimated_usd (the tracker computes it from token counts).
|
|
1449
|
+
//
|
|
1450
|
+
// evaluateCostTripwire is a pure function so the red/green decision is testable
|
|
1451
|
+
// in isolation from disk + the doctor's side-effecting ok/warn/fail output.
|
|
1452
|
+
|
|
1453
|
+
/**
|
|
1454
|
+
* Sum estimated_usd and count rows from already-parsed ledger rows.
|
|
1455
|
+
* Tolerant of missing/NaN estimated_usd (treated as 0) and ignores non-objects.
|
|
1456
|
+
* @param {Array<object>} rows
|
|
1457
|
+
* @returns {{ rowCount: number, totalUsd: number }}
|
|
1458
|
+
*/
|
|
1459
|
+
function summariseLedgerRows(rows) {
|
|
1460
|
+
let rowCount = 0;
|
|
1461
|
+
let totalUsd = 0;
|
|
1462
|
+
for (const row of rows || []) {
|
|
1463
|
+
if (!row || typeof row !== "object") continue;
|
|
1464
|
+
rowCount++;
|
|
1465
|
+
const usd = Number(row.estimated_usd);
|
|
1466
|
+
if (Number.isFinite(usd)) totalUsd += usd;
|
|
1467
|
+
}
|
|
1468
|
+
return { rowCount, totalUsd: +totalUsd.toFixed(6) };
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
/**
|
|
1472
|
+
* Decide the cost-telemetry posture from today's ledger rows and a count of
|
|
1473
|
+
* sessions known to have run today (rows themselves OR a daemon heartbeat that
|
|
1474
|
+
* proves activity). The contract the spec pins down:
|
|
1475
|
+
*
|
|
1476
|
+
* RED — sessions ran today (rows exist OR heartbeat active) but summed
|
|
1477
|
+
* estimated_usd === 0 → "spend telemetry is blind".
|
|
1478
|
+
* GREEN — no sessions ran (nothing to measure) OR spend > 0.
|
|
1479
|
+
*
|
|
1480
|
+
* @param {{ rows?: Array<object>, sessionCount?: number }} input
|
|
1481
|
+
* rows — parsed ledger rows for today (may be empty)
|
|
1482
|
+
* sessionCount — sessions evidenced today from outside the ledger (e.g. a
|
|
1483
|
+
* fresh daemon heartbeat). Activity = (rows.length>0) || (sessionCount>0).
|
|
1484
|
+
* @returns {{ red: boolean, status: "red"|"green", reason: string, rowCount: number, totalUsd: number, sessionCount: number }}
|
|
1485
|
+
*/
|
|
1486
|
+
function evaluateCostTripwire({ rows = [], sessionCount = 0 } = {}) {
|
|
1487
|
+
const { rowCount, totalUsd } = summariseLedgerRows(rows);
|
|
1488
|
+
const sessions = Math.max(0, Number(sessionCount) || 0);
|
|
1489
|
+
const hadActivity = rowCount > 0 || sessions > 0;
|
|
1490
|
+
|
|
1491
|
+
if (!hadActivity) {
|
|
1492
|
+
return {
|
|
1493
|
+
red: false,
|
|
1494
|
+
status: "green",
|
|
1495
|
+
reason: "no sessions ran today — nothing to measure",
|
|
1496
|
+
rowCount,
|
|
1497
|
+
totalUsd,
|
|
1498
|
+
sessionCount: sessions,
|
|
1499
|
+
};
|
|
1500
|
+
}
|
|
1501
|
+
if (totalUsd > 0) {
|
|
1502
|
+
return {
|
|
1503
|
+
red: false,
|
|
1504
|
+
status: "green",
|
|
1505
|
+
reason: `spend telemetry live ($${totalUsd} across ${rowCount} ledger row(s))`,
|
|
1506
|
+
rowCount,
|
|
1507
|
+
totalUsd,
|
|
1508
|
+
sessionCount: sessions,
|
|
1509
|
+
};
|
|
1510
|
+
}
|
|
1511
|
+
return {
|
|
1512
|
+
red: true,
|
|
1513
|
+
status: "red",
|
|
1514
|
+
reason: `spend telemetry is blind — ${hadActivity ? "sessions ran today" : ""} but ledger sums $0 (${rowCount} row(s)${sessions ? `, heartbeat-active` : ""}). Callers are recording 0 tokens; budget governor can never trip on spend.`,
|
|
1515
|
+
rowCount,
|
|
1516
|
+
totalUsd,
|
|
1517
|
+
sessionCount: sessions,
|
|
1518
|
+
};
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
/**
|
|
1522
|
+
* Staleness ladder for the router's data files (pricing cache & catalog cost
|
|
1523
|
+
* provenance, and the policy overlay): a freshly-fetched figure is GREEN; a
|
|
1524
|
+
* `fetched`/`issued_at` date older than 7 days is WARN; older than 30 days is
|
|
1525
|
+
* FAIL. Volatile-priced rows whose provenance is stale are the dangerous case
|
|
1526
|
+
* (a promo expiry silently busting a budget), so a stale catalog with any
|
|
1527
|
+
* volatile row escalates straight to FAIL at 30 d. Pure + testable; the doctor
|
|
1528
|
+
* maps the verdict onto ok/warn/fail.
|
|
1529
|
+
*
|
|
1530
|
+
* @param {{ fetched?: string|null, now?: number, hasVolatile?: boolean, label?: string }} input
|
|
1531
|
+
* fetched — ISO date (YYYY-MM-DD or full ISO) the data was last refreshed
|
|
1532
|
+
* now — epoch ms (injectable for tests; default Date.now)
|
|
1533
|
+
* hasVolatile — true if any priced row is volatile (escalates the FAIL case)
|
|
1534
|
+
* @returns {{ status: "green"|"warn"|"fail", ageDays: number|null, reason: string }}
|
|
1535
|
+
*/
|
|
1536
|
+
function evaluateStaleness({ fetched, now = Date.now(), hasVolatile = false, label = "data" } = {}) {
|
|
1537
|
+
if (!fetched) {
|
|
1538
|
+
return { status: "warn", ageDays: null, reason: `${label} has no fetch date — provenance unknown` };
|
|
1539
|
+
}
|
|
1540
|
+
const t = new Date(fetched).getTime();
|
|
1541
|
+
if (!Number.isFinite(t)) {
|
|
1542
|
+
return { status: "warn", ageDays: null, reason: `${label} fetch date "${fetched}" is unparseable` };
|
|
1543
|
+
}
|
|
1544
|
+
const ageDays = Math.floor((now - t) / 86_400_000);
|
|
1545
|
+
if (ageDays >= 30) {
|
|
1546
|
+
return {
|
|
1547
|
+
status: "fail",
|
|
1548
|
+
ageDays,
|
|
1549
|
+
reason: `${label} is ${ageDays}d old (>30d)${hasVolatile ? " and prices volatile — a promo expiry may be silently busting the budget" : ""}`,
|
|
1550
|
+
};
|
|
1551
|
+
}
|
|
1552
|
+
if (ageDays >= 7) {
|
|
1553
|
+
return { status: "warn", ageDays, reason: `${label} is ${ageDays}d old (>7d) — refresh the pricing cache` };
|
|
1554
|
+
}
|
|
1555
|
+
return { status: "green", ageDays, reason: `${label} fresh (${ageDays}d old)` };
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
// ---------------------------------------------------------------------------
|
|
1559
|
+
// ROUTER — `maestro router why <item>` / `maestro router validate`
|
|
1560
|
+
// ---------------------------------------------------------------------------
|
|
1561
|
+
|
|
1562
|
+
const ROUTER_HELP = `
|
|
1563
|
+
${C.bold}maestro router${C.reset} — inspect the v2 model router.
|
|
1564
|
+
|
|
1565
|
+
Usage:
|
|
1566
|
+
maestro router why <request-json|task_class> Resolve a RouteRequest and print
|
|
1567
|
+
the decision: chain, chosen,
|
|
1568
|
+
explain, tried[], audit.
|
|
1569
|
+
maestro router validate Strict-validate config/model-routing.yaml
|
|
1570
|
+
against the catalog (+ overlay).
|
|
1571
|
+
|
|
1572
|
+
Examples:
|
|
1573
|
+
maestro router why '{"task_class":"classify.inbox","data_class":"public"}'
|
|
1574
|
+
maestro router why session.responder
|
|
1575
|
+
maestro router validate
|
|
1576
|
+
`;
|
|
1577
|
+
|
|
1578
|
+
/**
|
|
1579
|
+
* Load the v2 routing config + bundled catalog for the current agent repo.
|
|
1580
|
+
* Returns { config, catalog } or null with an explanation logged.
|
|
1581
|
+
*/
|
|
1582
|
+
async function loadRouterContext(cwd) {
|
|
1583
|
+
const routerLib = join(MAESTRO_ROOT, "lib", "model-router.mjs");
|
|
1584
|
+
const catalogLib = join(MAESTRO_ROOT, "lib", "model-router", "catalog.mjs");
|
|
1585
|
+
if (!existsSync(routerLib) || !existsSync(catalogLib)) {
|
|
1586
|
+
fail("model-router library not reachable — run `npm run upgrade` in this agent");
|
|
1587
|
+
return null;
|
|
1588
|
+
}
|
|
1589
|
+
const { loadRoutingConfig } = await import(`file://${routerLib}`);
|
|
1590
|
+
const { loadCatalog } = await import(`file://${catalogLib}`);
|
|
1591
|
+
let config = null;
|
|
1592
|
+
try {
|
|
1593
|
+
config = loadRoutingConfig(cwd);
|
|
1594
|
+
} catch (err) {
|
|
1595
|
+
fail(`config/model-routing.yaml failed to load: ${err.message}`);
|
|
1596
|
+
return null;
|
|
1597
|
+
}
|
|
1598
|
+
const catalog = loadCatalog(cwd, {});
|
|
1599
|
+
return { config, catalog, routerLib };
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
async function routerCmd(args = []) {
|
|
1603
|
+
const sub = args[0];
|
|
1604
|
+
if (!sub || sub === "--help" || sub === "-h") {
|
|
1605
|
+
console.log(ROUTER_HELP);
|
|
1606
|
+
return;
|
|
1607
|
+
}
|
|
1608
|
+
const cwd = process.cwd();
|
|
1609
|
+
const ctx = await loadRouterContext(cwd);
|
|
1610
|
+
if (!ctx) {
|
|
1611
|
+
process.exitCode = 1;
|
|
1612
|
+
return;
|
|
1613
|
+
}
|
|
1614
|
+
const { config, catalog, routerLib } = ctx;
|
|
1615
|
+
|
|
1616
|
+
if (sub === "validate") {
|
|
1617
|
+
const { validateRoutingConfig } = await import(`file://${routerLib}`);
|
|
1618
|
+
if (!config) {
|
|
1619
|
+
warn("No config/model-routing.yaml present — router is inert (Anthropic-only).");
|
|
1620
|
+
return;
|
|
1621
|
+
}
|
|
1622
|
+
if (config.schema_version !== 2) {
|
|
1623
|
+
ok("config/model-routing.yaml is a v1 config (legacy resolver path; not v2-validated).");
|
|
1624
|
+
return;
|
|
1625
|
+
}
|
|
1626
|
+
const { errors, warnings } = validateRoutingConfig(config, { catalog, env: process.env });
|
|
1627
|
+
for (const w of warnings) warn(`${w.where}: ${w.warning}`);
|
|
1628
|
+
for (const e of errors) fail(`${e.where}: ${e.error}`);
|
|
1629
|
+
if (errors.length === 0) ok(`config/model-routing.yaml valid (${warnings.length} warning(s)).`);
|
|
1630
|
+
process.exitCode = errors.length ? 1 : 0;
|
|
1631
|
+
return;
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
if (sub === "why") {
|
|
1635
|
+
const { resolveChain } = await import(`file://${routerLib}`);
|
|
1636
|
+
const raw = args.slice(1).join(" ").trim();
|
|
1637
|
+
if (!raw) {
|
|
1638
|
+
fail('usage: maestro router why <request-json|task_class>');
|
|
1639
|
+
process.exitCode = 1;
|
|
1640
|
+
return;
|
|
1641
|
+
}
|
|
1642
|
+
let request;
|
|
1643
|
+
if (raw.startsWith("{")) {
|
|
1644
|
+
try {
|
|
1645
|
+
request = JSON.parse(raw);
|
|
1646
|
+
} catch (err) {
|
|
1647
|
+
fail(`could not parse request JSON: ${err.message}`);
|
|
1648
|
+
process.exitCode = 1;
|
|
1649
|
+
return;
|
|
1650
|
+
}
|
|
1651
|
+
} else {
|
|
1652
|
+
// Bare token → treat as a task_class.
|
|
1653
|
+
request = { task_class: raw };
|
|
1654
|
+
}
|
|
1655
|
+
const decision = resolveChain(request, { agentRoot: cwd, config, catalog, env: process.env });
|
|
1656
|
+
printRouteDecision(decision);
|
|
1657
|
+
return;
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
fail(`Unknown router subcommand: ${sub}`);
|
|
1661
|
+
console.log(ROUTER_HELP);
|
|
1662
|
+
process.exitCode = 1;
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
/** Pretty-print a RouteDecision for `maestro router why`. */
|
|
1666
|
+
function printRouteDecision(d) {
|
|
1667
|
+
if (!d) {
|
|
1668
|
+
fail("resolveChain returned nothing");
|
|
1669
|
+
return;
|
|
1670
|
+
}
|
|
1671
|
+
console.log();
|
|
1672
|
+
console.log(`${C.bold}decision${C.reset} ${d.decision_id}`);
|
|
1673
|
+
if (d.chosen) {
|
|
1674
|
+
console.log(`${C.bold}chosen${C.reset} ${C.green}${d.chosen.provider}/${d.chosen.model}${C.reset} harness=${d.chosen.harness} transport=${d.chosen.transport} wire=${d.chosen.wire}`);
|
|
1675
|
+
} else {
|
|
1676
|
+
console.log(`${C.bold}chosen${C.reset} ${C.red}<no route>${C.reset}`);
|
|
1677
|
+
}
|
|
1678
|
+
console.log(`${C.bold}explain${C.reset} ${d.explain}`);
|
|
1679
|
+
console.log(`${C.bold}lane${C.reset} ${d.lane} cache_ttl=${d.cache_ttl} estCostUSD=${d.estCostUSD}`);
|
|
1680
|
+
console.log(`${C.bold}chain${C.reset} ${(d.chain || []).map((c) => `${c.ref}(${c.harness}/${c.transport})`).join(" → ") || "(empty)"}`);
|
|
1681
|
+
if (d.tried && d.tried.length) {
|
|
1682
|
+
console.log(`${C.bold}tried${C.reset}`);
|
|
1683
|
+
for (const t of d.tried) console.log(` ${C.yellow}✗${C.reset} ${t.ref} — ${t.reason}`);
|
|
1684
|
+
}
|
|
1685
|
+
if (Object.keys(d.envForSpawn || {}).length) {
|
|
1686
|
+
console.log(`${C.bold}envForSpawn${C.reset}`);
|
|
1687
|
+
for (const [k, v] of Object.entries(d.envForSpawn)) {
|
|
1688
|
+
const shown = /TOKEN|KEY/.test(k) && v ? "***" : v;
|
|
1689
|
+
console.log(` ${k}=${shown}`);
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
console.log(`${C.bold}spawnArgs${C.reset} model=${d.spawnArgs.modelFlag} maxTurns=${d.spawnArgs.maxTurns} bare=${d.spawnArgs.bare}`);
|
|
1693
|
+
console.log(`${C.bold}audit${C.reset} rule=${d.audit.rule} fallback=${d.audit.fallback_reason || "—"} config=${d.audit.config_source || "—"} overlay=${d.audit.overlay_version || "—"}`);
|
|
1694
|
+
console.log();
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
// ---------------------------------------------------------------------------
|
|
1698
|
+
// DOCTOR — verify installation
|
|
1699
|
+
// ---------------------------------------------------------------------------
|
|
1700
|
+
|
|
1701
|
+
async function doctor() {
|
|
1702
|
+
const cwd = process.cwd();
|
|
1703
|
+
console.log();
|
|
1704
|
+
log("Running Maestro diagnostics...");
|
|
1705
|
+
console.log();
|
|
1706
|
+
|
|
1707
|
+
let issues = 0;
|
|
1708
|
+
|
|
1709
|
+
// ── Framework files ─────────────────────────────────────────────────────
|
|
1710
|
+
// config/agent.json is the single source of truth; config/agent.ts is the
|
|
1711
|
+
// derived thin wrapper (checked separately below as a secondary signal).
|
|
1712
|
+
const essentialFiles = [
|
|
1713
|
+
"config/agent.json", "CLAUDE.md", ".claude/settings.json",
|
|
1714
|
+
".claude/commands/init-maestro.md", "package.json",
|
|
1715
|
+
"scripts/setup/init-agent.sh", "scripts/healthcheck.sh",
|
|
1716
|
+
"config/environment.yaml", "config/contacts.yaml",
|
|
1717
|
+
"config/priorities.yaml", "config/sla-defaults.yaml",
|
|
1718
|
+
"scripts/daemon/maestro-daemon.mjs",
|
|
1719
|
+
"scripts/local-triggers/generate-plists.sh",
|
|
1720
|
+
"state/dashboards/executive-summary.yaml",
|
|
1721
|
+
"state/queues/action-stack.yaml",
|
|
1722
|
+
"knowledge/decisions/decision-schema.yaml",
|
|
1723
|
+
// Cadence-bus architecture (maestro 1.8+)
|
|
1724
|
+
"lib/cadence-bus.mjs",
|
|
1725
|
+
"scripts/cadence/enqueue-cadence-tick.mjs",
|
|
1726
|
+
"scripts/cadence/launchd-cadence-wrapper.sh",
|
|
1727
|
+
"scripts/cadence/cadence-status.mjs",
|
|
1728
|
+
"scripts/daemon/cadence-consumer.mjs",
|
|
1729
|
+
"scripts/daemon/cadence-handlers.mjs",
|
|
1730
|
+
];
|
|
1731
|
+
|
|
1732
|
+
for (const file of essentialFiles) {
|
|
1733
|
+
if (existsSync(join(cwd, file))) ok(file);
|
|
1734
|
+
else { fail(`Missing: ${file}`); issues++; }
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
// ── config/agent.ts is the derived SoT wrapper (secondary signal) ───────
|
|
1738
|
+
// A standalone literal .ts (legacy split-brain) is a WARN, fixed by `upgrade`.
|
|
1739
|
+
{
|
|
1740
|
+
const tsPath = join(cwd, "config/agent.ts");
|
|
1741
|
+
if (existsSync(tsPath) && existsSync(join(cwd, "config/agent.json"))) {
|
|
1742
|
+
const src = readFileSync(tsPath, "utf-8");
|
|
1743
|
+
if (/import\s+\w+\s+from\s+['"]\.\/agent\.json['"]/.test(src)) ok("config/agent.ts is the SoT wrapper (re-exports agent.json)");
|
|
1744
|
+
else { warn("config/agent.ts is a standalone literal (legacy split-brain) — run: npx @cohortapp/agent-sdk upgrade"); issues++; }
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
// ── Archetype profile (function × altitude) ─────────────────────────────
|
|
1749
|
+
// Resolve the agent's operating model from the framework archetype library.
|
|
1750
|
+
// A freshly-created framework checkout has no config/agent.json; skip there.
|
|
1751
|
+
try {
|
|
1752
|
+
const agentJsonPath = join(cwd, "config/agent.json");
|
|
1753
|
+
if (existsSync(agentJsonPath)) {
|
|
1754
|
+
const cfg = JSON.parse(readFileSync(agentJsonPath, "utf-8"));
|
|
1755
|
+
const sel = cfg.function && cfg.altitude
|
|
1756
|
+
? { function: cfg.function, altitude: cfg.altitude }
|
|
1757
|
+
: migrateLegacyArchetype(cfg.archetype);
|
|
1758
|
+
if (!sel) {
|
|
1759
|
+
warn("config/agent.json has no { function, altitude } archetype — run maestro setup");
|
|
1760
|
+
issues++;
|
|
1761
|
+
} else {
|
|
1762
|
+
const profile = await resolveArchetype(sel);
|
|
1763
|
+
ok(`Archetype resolves: ${profile.label} (${profile.function} × ${profile.altitude})`);
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
} catch (err) {
|
|
1767
|
+
fail(`Archetype profile invalid: ${err && err.message ? err.message : err}`);
|
|
1768
|
+
issues++;
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
// ── Operating model: completeness gate (shared with `maestro setup`) ─────
|
|
1772
|
+
// The single source of truth for "fully set up" lives in
|
|
1773
|
+
// lib/setup/completeness.mjs so setup and doctor never drift. A freshly-created
|
|
1774
|
+
// framework checkout (no config/agent.json) is tolerated — skip the gate there.
|
|
1775
|
+
if (existsSync(join(cwd, "config/agent.json"))) {
|
|
1776
|
+
try {
|
|
1777
|
+
const { assertComplete } = await import(join(MAESTRO_ROOT, "lib", "setup", "completeness.mjs"));
|
|
1778
|
+
const res = await assertComplete(cwd);
|
|
1779
|
+
if (res.ok) {
|
|
1780
|
+
ok("Operating model complete (charter, priorities, backlog≥5, roster, cadences, policies — placeholder-free)");
|
|
1781
|
+
} else {
|
|
1782
|
+
// Identity gaps are reported by the archetype check above; surface the
|
|
1783
|
+
// operating-model artifacts here with their remedy.
|
|
1784
|
+
for (const m of res.missing) {
|
|
1785
|
+
if (m.artifact.startsWith("identity") || m.artifact === "config/agent.json") continue;
|
|
1786
|
+
warn(`Operating model: ${m.artifact} — ${m.reason}`);
|
|
1787
|
+
issues++;
|
|
1788
|
+
}
|
|
1789
|
+
warn(" Finish setup: maestro setup (resumes from the checkpoint)");
|
|
1790
|
+
}
|
|
1791
|
+
} catch (err) {
|
|
1792
|
+
warn(`Operating-model gate skipped: ${err && err.message ? err.message : err}`);
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
// ── Cadence bus state directories ───────────────────────────────────────
|
|
1797
|
+
const busDirs = [
|
|
1798
|
+
"state/cadence-bus",
|
|
1799
|
+
"state/cadence-bus/inbox",
|
|
1800
|
+
"state/cadence-bus/claimed",
|
|
1801
|
+
"state/cadence-bus/processed",
|
|
1802
|
+
"state/cadence-bus/failed",
|
|
1803
|
+
"state/cadence-bus/dlq",
|
|
1804
|
+
"logs/cadence-bus",
|
|
1805
|
+
];
|
|
1806
|
+
for (const d of busDirs) {
|
|
1807
|
+
if (existsSync(join(cwd, d))) ok(d);
|
|
1808
|
+
else { warn(`Missing: ${d} — run: npx @cohortapp/agent-sdk upgrade`); issues++; }
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
// ── Launchd plist architecture ──────────────────────────────────────────
|
|
1812
|
+
const plistDir = join(cwd, "scripts/local-triggers/plists");
|
|
1813
|
+
if (existsSync(plistDir)) {
|
|
1814
|
+
const plists = readdirSync(plistDir).filter((n) => n.endsWith(".plist"));
|
|
1815
|
+
let legacy = 0;
|
|
1816
|
+
let modern = 0;
|
|
1817
|
+
for (const name of plists) {
|
|
1818
|
+
const body = readFileSync(join(plistDir, name), "utf-8");
|
|
1819
|
+
if (body.includes(LEGACY_PLIST_INDICATOR)) legacy++;
|
|
1820
|
+
else if (body.includes(PLIST_ARCH_MARKER)) modern++;
|
|
1821
|
+
}
|
|
1822
|
+
if (plists.length === 0) {
|
|
1823
|
+
warn("scripts/local-triggers/plists/ is empty — run: scripts/local-triggers/generate-plists.sh");
|
|
1824
|
+
issues++;
|
|
1825
|
+
} else if (legacy > 0) {
|
|
1826
|
+
// Legacy plists are a migration warning rather than a fatal — the
|
|
1827
|
+
// operator can fix them with `maestro upgrade`. Use warn() so the
|
|
1828
|
+
// message lands on stdout where scripted callers will see it.
|
|
1829
|
+
warn(`${legacy} plist(s) still call run-trigger.sh directly (legacy spawn-per-tick).`);
|
|
1830
|
+
warn(` Fix: npx @cohortapp/agent-sdk upgrade (will back up + regenerate)`);
|
|
1831
|
+
issues++;
|
|
1832
|
+
} else {
|
|
1833
|
+
ok(`${modern}/${plists.length} plist(s) use the cadence-bus architecture`);
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
// Spot-check installed plists at ~/Library/LaunchAgents/.
|
|
1837
|
+
const installedDir = join(process.env.HOME || "/", "Library/LaunchAgents");
|
|
1838
|
+
if (existsSync(installedDir)) {
|
|
1839
|
+
let installedLegacy = 0;
|
|
1840
|
+
for (const name of readdirSync(installedDir)) {
|
|
1841
|
+
if (!name.startsWith("ai.maestro.") && !name.startsWith("ai.adaptic.")) continue;
|
|
1842
|
+
if (!name.endsWith(".plist")) continue;
|
|
1843
|
+
const body = readFileSync(join(installedDir, name), "utf-8");
|
|
1844
|
+
if (body.includes(LEGACY_PLIST_INDICATOR)) installedLegacy++;
|
|
1845
|
+
}
|
|
1846
|
+
if (installedLegacy > 0) {
|
|
1847
|
+
warn(`${installedLegacy} installed launchd plist(s) still use the legacy pattern.`);
|
|
1848
|
+
warn(" Fix: npx @cohortapp/agent-sdk upgrade then follow the printed launchctl commands.");
|
|
1849
|
+
issues++;
|
|
1850
|
+
} else {
|
|
1851
|
+
ok("Installed launchd plists (ai.maestro.*/legacy ai.adaptic.*) are on cadence-bus or absent");
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
} else {
|
|
1855
|
+
warn("scripts/local-triggers/plists/ does not exist yet — run scripts/setup/init-agent.sh");
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1858
|
+
// ── Cadence consumer heartbeat ──────────────────────────────────────────
|
|
1859
|
+
try {
|
|
1860
|
+
// Read via the bundled lib so we don't reinvent path resolution.
|
|
1861
|
+
const mod = readFileSync(join(cwd, "state/cadence-bus/health.json"), "utf-8");
|
|
1862
|
+
const health = JSON.parse(mod);
|
|
1863
|
+
const ageMs = Date.now() - new Date(health.ts).getTime();
|
|
1864
|
+
if (ageMs < 60_000) ok(`Cadence consumer heartbeat fresh (${Math.round(ageMs / 1000)}s)`);
|
|
1865
|
+
else if (ageMs < 5 * 60_000) warn(`Cadence consumer heartbeat stale (${Math.round(ageMs / 1000)}s) — daemon may be slow.`);
|
|
1866
|
+
else {
|
|
1867
|
+
warn(`Cadence consumer heartbeat very stale (${Math.round(ageMs / 1000)}s) — start the daemon: npm run daemon`);
|
|
1868
|
+
issues++;
|
|
1869
|
+
}
|
|
1870
|
+
} catch {
|
|
1871
|
+
warn("No cadence consumer heartbeat yet — start the daemon: npm run daemon");
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1874
|
+
// ── Manual tick smoke test ──────────────────────────────────────────────
|
|
1875
|
+
// Enqueue a heartbeat tick if cadence files are in place. The bus
|
|
1876
|
+
// accepts the event even when the consumer isn't running; the file lands
|
|
1877
|
+
// on disk and is recoverable. This proves the producer side end-to-end.
|
|
1878
|
+
const enqueueScript = join(cwd, "scripts/cadence/enqueue-cadence-tick.mjs");
|
|
1879
|
+
if (existsSync(enqueueScript)) {
|
|
1880
|
+
const result = spawnSync(process.execPath, [
|
|
1881
|
+
enqueueScript, "cadence-bus-heartbeat",
|
|
1882
|
+
"--source=daemon", "--metadata=note=doctor-smoke", "--quiet",
|
|
1883
|
+
], { cwd, encoding: "utf-8", env: { ...process.env, AGENT_ROOT: cwd } });
|
|
1884
|
+
if (result.status === 0) ok("Cadence enqueue smoke test passed");
|
|
1885
|
+
else {
|
|
1886
|
+
fail(`Cadence enqueue smoke test failed (exit ${result.status}): ${(result.stderr || "").trim()}`);
|
|
1887
|
+
issues++;
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
// ── WS4: recovery + resource governance ─────────────────────────────────
|
|
1892
|
+
// The kill switch is HUMAN-ONLY now; the watchdog self-heals (soft→hard→
|
|
1893
|
+
// restart→graceful-reboot) and never writes .emergency-stop on OOM. Surface
|
|
1894
|
+
// the live governance posture so an operator can see throttle/breaker/budget
|
|
1895
|
+
// state and the reboot wiring at a glance.
|
|
1896
|
+
{
|
|
1897
|
+
// .emergency-stop present → the agent is intentionally stopped by a human.
|
|
1898
|
+
if (existsSync(join(cwd, ".emergency-stop"))) {
|
|
1899
|
+
let reason = "";
|
|
1900
|
+
try { reason = readFileSync(join(cwd, ".emergency-stop"), "utf-8").trim().split("\n")[0]; } catch { /* */ }
|
|
1901
|
+
fail(`.emergency-stop present${reason ? ` ("${reason}")` : ""} — human-only kill switch is set; agent will not run. Clear with: scripts/resume-operations.sh`);
|
|
1902
|
+
issues++;
|
|
1903
|
+
} else {
|
|
1904
|
+
ok(".emergency-stop absent (agent not human-halted)");
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1907
|
+
// Watchdog heartbeat freshness (<90s).
|
|
1908
|
+
try {
|
|
1909
|
+
const hbRaw = readFileSync(join(cwd, "state/heartbeat"), "utf-8").trim();
|
|
1910
|
+
const hbAgeMs = Date.now() - new Date(hbRaw).getTime();
|
|
1911
|
+
if (Number.isFinite(hbAgeMs) && hbAgeMs < 90_000) ok(`Memory watchdog heartbeat fresh (${Math.round(hbAgeMs / 1000)}s)`);
|
|
1912
|
+
else { warn(`Memory watchdog heartbeat stale (${Math.round(hbAgeMs / 1000)}s) — check ai.maestro.*-memory-watchdog`); issues++; }
|
|
1913
|
+
} catch {
|
|
1914
|
+
warn("No memory watchdog heartbeat (state/heartbeat) — is the watchdog plist loaded?");
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
// Soft-throttle ceiling (state/throttle.json) — WARN while unexpired.
|
|
1918
|
+
try {
|
|
1919
|
+
const { readThrottle, dynamicMax, defaultDeps } = await import(join(MAESTRO_ROOT, "lib", "resource-governor.mjs"));
|
|
1920
|
+
const thr = readThrottle({ agentRoot: cwd });
|
|
1921
|
+
if (thr.ceiling !== Infinity) {
|
|
1922
|
+
warn(`Resource governor throttled: ceiling=${thr.ceiling}${thr.reason ? ` (${thr.reason})` : ""} — watchdog SOFT/HARD tier active; auto-clears on TTL`);
|
|
1923
|
+
} else {
|
|
1924
|
+
ok("No active soft-throttle (state/throttle.json clear/expired)");
|
|
1925
|
+
}
|
|
1926
|
+
// Computed dynamicMax vs RAM (sizing sanity).
|
|
1927
|
+
try {
|
|
1928
|
+
const deps = defaultDeps({ agentRoot: cwd });
|
|
1929
|
+
const dMax = dynamicMax(deps);
|
|
1930
|
+
ok(`Governor dynamicMax=${dMax} (RAM ${Math.round(deps.totalmem / (1024 * 1024 * 1024))}GB, ${deps.cpus} cpus)`);
|
|
1931
|
+
} catch { /* os read best-effort */ }
|
|
1932
|
+
} catch (err) {
|
|
1933
|
+
warn(`Resource governor check skipped: ${err && err.message ? err.message : err}`);
|
|
1934
|
+
}
|
|
1935
|
+
|
|
1936
|
+
// Shared 429 breaker — WARN if open.
|
|
1937
|
+
try {
|
|
1938
|
+
const { checkRateLimit } = await import(join(MAESTRO_ROOT, "lib", "rate-guard.mjs"));
|
|
1939
|
+
const rb = checkRateLimit(process.env.MAESTRO_RATE_PROVIDER || "anthropic", { agentRoot: cwd });
|
|
1940
|
+
if (!rb.allowed) {
|
|
1941
|
+
const mins = Math.max(0, Math.round((rb.retryAt - Date.now()) / 60_000));
|
|
1942
|
+
warn(`429 rate breaker OPEN — spawns gated for ~${mins}m (state/rate-limits/)`); issues++;
|
|
1943
|
+
} else {
|
|
1944
|
+
ok("429 rate breaker closed (spawns allowed)");
|
|
1945
|
+
}
|
|
1946
|
+
} catch { /* breaker read best-effort */ }
|
|
1947
|
+
|
|
1948
|
+
// Today's budget band.
|
|
1949
|
+
try {
|
|
1950
|
+
const { dailyStatus } = await import(join(MAESTRO_ROOT, "lib", "budget-guard.mjs"));
|
|
1951
|
+
const b = dailyStatus({ agentRoot: cwd });
|
|
1952
|
+
if (b.essentialOnly) { warn(`Daily budget cap reached ($${b.spentUSD}/$${b.capUSD}) — essential-only (inbox replies continue; backlog+cadence deferred)`); issues++; }
|
|
1953
|
+
else if (b.band >= 75) { warn(`Daily budget at ${b.band}% ($${b.spentUSD}/$${b.capUSD})`); }
|
|
1954
|
+
else ok(`Daily budget band ${b.band}% ($${b.spentUSD}/$${b.capUSD})`);
|
|
1955
|
+
} catch { /* ledger read best-effort */ }
|
|
1956
|
+
|
|
1957
|
+
// Cost-telemetry tripwire (observability F2). Read today's cost ledger and
|
|
1958
|
+
// cross-check against evidence that sessions actually ran (ledger rows OR a
|
|
1959
|
+
// fresh cadence-consumer heartbeat). If sessions ran but summed
|
|
1960
|
+
// estimated_usd is $0, spend telemetry is blind and the budget governor
|
|
1961
|
+
// can never trip — RED. No sessions, or spend>0 → GREEN.
|
|
1962
|
+
try {
|
|
1963
|
+
const ledgerDate = new Date().toISOString().slice(0, 10);
|
|
1964
|
+
const ledgerPath = join(cwd, "state/cost-tracking", `${ledgerDate}.jsonl`);
|
|
1965
|
+
const rows = [];
|
|
1966
|
+
if (existsSync(ledgerPath)) {
|
|
1967
|
+
const body = readFileSync(ledgerPath, "utf-8");
|
|
1968
|
+
for (const line of body.split("\n")) {
|
|
1969
|
+
if (!line.trim()) continue;
|
|
1970
|
+
try { rows.push(JSON.parse(line)); } catch { /* skip malformed row */ }
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1973
|
+
// Heartbeat as out-of-ledger session evidence: a fresh consumer heartbeat
|
|
1974
|
+
// (<5m) means the daemon is live and spawning, so $0 spend is suspicious
|
|
1975
|
+
// even before the first ledger row lands.
|
|
1976
|
+
let heartbeatActive = 0;
|
|
1977
|
+
try {
|
|
1978
|
+
const health = JSON.parse(readFileSync(join(cwd, "state/cadence-bus/health.json"), "utf-8"));
|
|
1979
|
+
const ageMs = Date.now() - new Date(health.ts).getTime();
|
|
1980
|
+
if (Number.isFinite(ageMs) && ageMs >= 0 && ageMs < 5 * 60_000) heartbeatActive = 1;
|
|
1981
|
+
} catch { /* no heartbeat — rely on ledger rows alone */ }
|
|
1982
|
+
|
|
1983
|
+
const tripwire = evaluateCostTripwire({ rows, sessionCount: heartbeatActive });
|
|
1984
|
+
if (tripwire.red) {
|
|
1985
|
+
fail(`Cost telemetry BLIND — ${tripwire.reason}`);
|
|
1986
|
+
fail(` Fix: callers must pass real --input-tokens/--output-tokens to scripts/cost/track-claude-usage.mjs (lift usage from the claude --output-format json envelope).`);
|
|
1987
|
+
issues++;
|
|
1988
|
+
} else if (tripwire.rowCount === 0 && heartbeatActive === 0) {
|
|
1989
|
+
ok("Cost telemetry: no sessions recorded today (nothing to measure)");
|
|
1990
|
+
} else {
|
|
1991
|
+
ok(`Cost telemetry live ($${tripwire.totalUsd} across ${tripwire.rowCount} ledger row(s) today)`);
|
|
1992
|
+
}
|
|
1993
|
+
} catch { /* cost ledger read best-effort */ }
|
|
1994
|
+
|
|
1995
|
+
// Resume-pending markers stuck at the strike cap.
|
|
1996
|
+
try {
|
|
1997
|
+
const rpDir = join(cwd, "state/sessions/resume-pending");
|
|
1998
|
+
if (existsSync(rpDir)) {
|
|
1999
|
+
let stuck = 0, total = 0;
|
|
2000
|
+
for (const f of readdirSync(rpDir)) {
|
|
2001
|
+
if (!f.endsWith(".json")) continue;
|
|
2002
|
+
total++;
|
|
2003
|
+
try {
|
|
2004
|
+
const m = JSON.parse(readFileSync(join(rpDir, f), "utf-8"));
|
|
2005
|
+
if ((m.recoveryAttempts || 0) >= 3) stuck++;
|
|
2006
|
+
} catch { /* skip */ }
|
|
2007
|
+
}
|
|
2008
|
+
if (stuck > 0) { warn(`${stuck}/${total} resume-pending marker(s) hit the 3-strike cap — items were marked blocked; review state/queues/`); issues++; }
|
|
2009
|
+
else if (total > 0) ok(`${total} in-flight resume marker(s), none stuck`);
|
|
2010
|
+
else ok("No in-flight resume markers");
|
|
2011
|
+
}
|
|
2012
|
+
} catch { /* */ }
|
|
2013
|
+
|
|
2014
|
+
// pmset autorestart — the box must power back on after an outage.
|
|
2015
|
+
try {
|
|
2016
|
+
const ps = spawnSync("pmset", ["-g"], { encoding: "utf-8" });
|
|
2017
|
+
if (ps.status === 0) {
|
|
2018
|
+
const m = (ps.stdout || "").match(/\bautorestart\s+(\d)/);
|
|
2019
|
+
if (m && m[1] === "1") ok("pmset autorestart=1 (box reboots after power loss)");
|
|
2020
|
+
else { warn("pmset autorestart is not 1 — set: sudo pmset -a autorestart 1 (so the agent recovers from power loss)"); issues++; }
|
|
2021
|
+
}
|
|
2022
|
+
} catch { /* pmset unavailable (non-mac / sandbox) — skip */ }
|
|
2023
|
+
}
|
|
2024
|
+
|
|
2025
|
+
// ── .env ────────────────────────────────────────────────────────────────
|
|
2026
|
+
if (existsSync(join(cwd, ".env"))) {
|
|
2027
|
+
// .env permission hygiene (setup-onboarding W4): every credential — Slack,
|
|
2028
|
+
// Gmail, Twilio, Anthropic — lives here in plaintext. It must be chmod 600
|
|
2029
|
+
// (owner-only); a group/world-readable .env on a shared or stolen Mac mini
|
|
2030
|
+
// leaks the principal's full credential set. The framework never chmods it
|
|
2031
|
+
// for the operator — surface the exact fix instead. POSIX-only (mode bits).
|
|
2032
|
+
if (typeof process.getuid === "function") {
|
|
2033
|
+
try {
|
|
2034
|
+
const mode = statSync(join(cwd, ".env")).mode & 0o777;
|
|
2035
|
+
if ((mode & 0o077) !== 0) {
|
|
2036
|
+
fail(`.env is mode ${mode.toString(8).padStart(3, "0")} — group/world can read plaintext secrets.`);
|
|
2037
|
+
fail(` Fix: chmod 600 .env`);
|
|
2038
|
+
issues++;
|
|
2039
|
+
} else {
|
|
2040
|
+
ok(".env permissions are owner-only (chmod 600)");
|
|
2041
|
+
}
|
|
2042
|
+
} catch { /* stat failed — skip */ }
|
|
2043
|
+
}
|
|
2044
|
+
const env = readFileSync(join(cwd, ".env"), "utf-8");
|
|
2045
|
+
const check = (key, required) => {
|
|
2046
|
+
const re = new RegExp(`^${key}=.+`, "m");
|
|
2047
|
+
if (re.test(env)) ok(`${key} configured`);
|
|
2048
|
+
else if (required) { warn(`${key} not set`); issues++; }
|
|
2049
|
+
else warn(`${key} not set (optional)`);
|
|
2050
|
+
};
|
|
2051
|
+
check("ANTHROPIC_API_KEY", true);
|
|
2052
|
+
check("SLACK_USER_TOKEN", false);
|
|
2053
|
+
check("GMAIL_APP_PASSWORD", false);
|
|
2054
|
+
|
|
2055
|
+
// Auth validity: if ANTHROPIC_API_KEY is set, ping the API to
|
|
2056
|
+
// verify it works. An invalid key in .env will silently be sent
|
|
2057
|
+
// to every `claude --print` sub-session and cause cascading 401s
|
|
2058
|
+
// (exactly the ravi-ai inbox-processor runaway). Better to catch
|
|
2059
|
+
// it here. Skips the check if the user opted out via
|
|
2060
|
+
// MAESTRO_PREFER_SUBSCRIPTION_AUTH=1 (subscription wins).
|
|
2061
|
+
const keyMatch = env.match(/^ANTHROPIC_API_KEY=(.+)$/m);
|
|
2062
|
+
const preferSubsMatch = env.match(/^MAESTRO_PREFER_SUBSCRIPTION_AUTH=(.+)$/m);
|
|
2063
|
+
const preferSubs = preferSubsMatch && /^1|true|yes$/i.test(preferSubsMatch[1].trim());
|
|
2064
|
+
if (keyMatch && !preferSubs) {
|
|
2065
|
+
const key = keyMatch[1].trim().replace(/^"|"$/g, "");
|
|
2066
|
+
try {
|
|
2067
|
+
const result = spawnSync("curl", [
|
|
2068
|
+
"-s", "-o", "/dev/null", "-w", "%{http_code}",
|
|
2069
|
+
"-X", "POST",
|
|
2070
|
+
"-H", `x-api-key: ${key}`,
|
|
2071
|
+
"-H", "anthropic-version: 2023-06-01",
|
|
2072
|
+
"-H", "content-type: application/json",
|
|
2073
|
+
"--max-time", "8",
|
|
2074
|
+
"https://api.anthropic.com/v1/messages",
|
|
2075
|
+
"-d", JSON.stringify({ model: "claude-haiku-4-5", max_tokens: 5, messages: [{ role: "user", content: "ping" }] }),
|
|
2076
|
+
], { encoding: "utf-8" });
|
|
2077
|
+
const code = (result.stdout || "").trim();
|
|
2078
|
+
if (code === "200") ok("ANTHROPIC_API_KEY validated against api.anthropic.com");
|
|
2079
|
+
else if (code === "401") {
|
|
2080
|
+
warn(`ANTHROPIC_API_KEY is INVALID (HTTP 401 from api.anthropic.com).`);
|
|
2081
|
+
warn(` This will cause every sub-session spawn to fail. Either:`);
|
|
2082
|
+
warn(` 1. Replace the key in .env with a valid one, OR`);
|
|
2083
|
+
warn(` 2. Set MAESTRO_PREFER_SUBSCRIPTION_AUTH=1 in .env to use Claude Code subscription auth.`);
|
|
2084
|
+
issues++;
|
|
2085
|
+
} else if (code) warn(`ANTHROPIC_API_KEY check returned HTTP ${code} (expected 200)`);
|
|
2086
|
+
else warn(`ANTHROPIC_API_KEY check skipped (no network / curl missing)`);
|
|
2087
|
+
} catch { warn("ANTHROPIC_API_KEY check failed (curl error)"); }
|
|
2088
|
+
} else if (preferSubs) {
|
|
2089
|
+
ok("MAESTRO_PREFER_SUBSCRIPTION_AUTH=1 — using Claude Code subscription (Keychain OAuth)");
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2092
|
+
// ── Slack Socket Mode ────────────────────────────────────────────────
|
|
2093
|
+
// When SLACK_APP_LEVEL_TOKEN is set, verify the launchd job is loaded
|
|
2094
|
+
// and the listener has connected recently. The check is fully optional:
|
|
2095
|
+
// the framework still works with the legacy 60s poller if the operator
|
|
2096
|
+
// hasn't enabled Socket Mode yet.
|
|
2097
|
+
const socketTokenMatch = env.match(/^SLACK_APP_LEVEL_TOKEN=(.+)$/m);
|
|
2098
|
+
const socketToken = socketTokenMatch ? socketTokenMatch[1].trim().replace(/^["']|["']$/g, "") : "";
|
|
2099
|
+
if (socketToken && socketToken.startsWith("xapp-")) {
|
|
2100
|
+
ok("SLACK_APP_LEVEL_TOKEN configured (Socket Mode candidate)");
|
|
2101
|
+
|
|
2102
|
+
// Resolve the agent's first name the same way generate-plists.sh does
|
|
2103
|
+
// so the label matches whatever is installed in ~/Library/LaunchAgents/.
|
|
2104
|
+
let firstName = "";
|
|
2105
|
+
try {
|
|
2106
|
+
const aj = JSON.parse(readFileSync(join(cwd, "config/agent.json"), "utf-8"));
|
|
2107
|
+
if (aj.firstName) firstName = String(aj.firstName).toLowerCase();
|
|
2108
|
+
} catch { /* fall back */ }
|
|
2109
|
+
if (!firstName) firstName = cwd.split("/").pop().replace(/-ai$/, "").toLowerCase();
|
|
2110
|
+
// Is the plist installed at ~/Library/LaunchAgents/? Prefer the current
|
|
2111
|
+
// ai.maestro.* namespace; fall back to legacy ai.adaptic.* for agents
|
|
2112
|
+
// installed before the rename.
|
|
2113
|
+
const installedDir = join(process.env.HOME || "/", "Library/LaunchAgents");
|
|
2114
|
+
const socketCandidates = [`ai.maestro.${firstName}-slack-socket`, `ai.adaptic.${firstName}-slack-socket`];
|
|
2115
|
+
const socketLabel = socketCandidates.find((l) => existsSync(join(installedDir, `${l}.plist`))) || socketCandidates[0];
|
|
2116
|
+
const installedPath = join(installedDir, `${socketLabel}.plist`);
|
|
2117
|
+
if (existsSync(installedPath)) {
|
|
2118
|
+
// Is it actually loaded? launchctl list returns 0 when the label exists.
|
|
2119
|
+
const listRes = spawnSync("launchctl", ["list", socketLabel], { encoding: "utf-8" });
|
|
2120
|
+
if (listRes.status === 0) ok(`launchd job loaded: ${socketLabel}`);
|
|
2121
|
+
else {
|
|
2122
|
+
warn(`Plist installed but launchd job not loaded: ${socketLabel}`);
|
|
2123
|
+
warn(` Fix: launchctl load ${installedPath}`);
|
|
2124
|
+
issues++;
|
|
2125
|
+
}
|
|
2126
|
+
} else {
|
|
2127
|
+
warn(`SLACK_APP_LEVEL_TOKEN set but ${installedPath} missing.`);
|
|
2128
|
+
warn(" Run: node scripts/setup/init-slack-socket-mode.mjs");
|
|
2129
|
+
issues++;
|
|
2130
|
+
}
|
|
2131
|
+
|
|
2132
|
+
// Did the listener log a `hello` envelope recently? The log lives at
|
|
2133
|
+
// logs/polling/<today>-slack-socket.jsonl — scan the last few lines
|
|
2134
|
+
// for a fresh "hello" or "inbox-item-written" entry.
|
|
2135
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
2136
|
+
const yesterday = new Date(Date.now() - 86400000).toISOString().slice(0, 10);
|
|
2137
|
+
let foundRecent = false;
|
|
2138
|
+
let latestTs = null;
|
|
2139
|
+
for (const day of [today, yesterday]) {
|
|
2140
|
+
const logPath = join(cwd, "logs/polling", `${day}-slack-socket.jsonl`);
|
|
2141
|
+
if (!existsSync(logPath)) continue;
|
|
2142
|
+
// Read the tail (last 50KB) to avoid loading huge files.
|
|
2143
|
+
try {
|
|
2144
|
+
const stat = statSync(logPath);
|
|
2145
|
+
const buf = Buffer.alloc(Math.min(stat.size, 50_000));
|
|
2146
|
+
const fd = openSync(logPath, "r");
|
|
2147
|
+
try {
|
|
2148
|
+
const start = Math.max(0, stat.size - buf.length);
|
|
2149
|
+
readSync(fd, buf, 0, buf.length, start);
|
|
2150
|
+
} finally { closeSync(fd); }
|
|
2151
|
+
const tail = buf.toString("utf-8");
|
|
2152
|
+
for (const line of tail.split("\n").reverse()) {
|
|
2153
|
+
if (!line) continue;
|
|
2154
|
+
try {
|
|
2155
|
+
const entry = JSON.parse(line);
|
|
2156
|
+
if (entry.message === "hello — connected to Slack Socket Mode" ||
|
|
2157
|
+
entry.message === "inbox-item-written" ||
|
|
2158
|
+
entry.message === "starting Socket Mode listener") {
|
|
2159
|
+
latestTs = entry.ts;
|
|
2160
|
+
const ageMs = Date.now() - new Date(entry.ts).getTime();
|
|
2161
|
+
if (ageMs >= 0 && ageMs < 60 * 60_000) foundRecent = true;
|
|
2162
|
+
break;
|
|
2163
|
+
}
|
|
2164
|
+
} catch { /* malformed line — skip */ }
|
|
2165
|
+
}
|
|
2166
|
+
if (latestTs) break;
|
|
2167
|
+
} catch { /* unreadable log — skip */ }
|
|
2168
|
+
}
|
|
2169
|
+
if (foundRecent) {
|
|
2170
|
+
const ageMin = Math.round((Date.now() - new Date(latestTs).getTime()) / 60_000);
|
|
2171
|
+
ok(`Socket Mode listener was active ${ageMin}m ago`);
|
|
2172
|
+
} else if (latestTs) {
|
|
2173
|
+
const ageMin = Math.round((Date.now() - new Date(latestTs).getTime()) / 60_000);
|
|
2174
|
+
warn(`Socket Mode last connect ${ageMin}m ago — may be stalled`);
|
|
2175
|
+
issues++;
|
|
2176
|
+
} else {
|
|
2177
|
+
warn("Socket Mode listener has not produced a connect log yet.");
|
|
2178
|
+
warn(" Check: tail -f logs/polling/$(date +%Y-%m-%d)-slack-socket.jsonl");
|
|
2179
|
+
issues++;
|
|
2180
|
+
}
|
|
2181
|
+
}
|
|
2182
|
+
} else {
|
|
2183
|
+
fail(".env file not found — copy from .env.example");
|
|
2184
|
+
issues++;
|
|
2185
|
+
}
|
|
2186
|
+
|
|
2187
|
+
// ── Dependencies ────────────────────────────────────────────────────────
|
|
2188
|
+
if (existsSync(join(cwd, "node_modules"))) ok("node_modules installed");
|
|
2189
|
+
else { fail("node_modules not found — run: npm install"); issues++; }
|
|
2190
|
+
|
|
2191
|
+
try {
|
|
2192
|
+
execFileSync("which", ["claude"], { stdio: "pipe" });
|
|
2193
|
+
ok("Claude CLI installed");
|
|
2194
|
+
} catch {
|
|
2195
|
+
fail("Claude CLI not found — install: npm install -g @anthropic-ai/claude-code");
|
|
2196
|
+
issues++;
|
|
2197
|
+
}
|
|
2198
|
+
|
|
2199
|
+
try {
|
|
2200
|
+
execFileSync("which", ["jq"], { stdio: "pipe" });
|
|
2201
|
+
ok("jq installed");
|
|
2202
|
+
} catch {
|
|
2203
|
+
warn("jq not found — install: brew install jq (used by various shell helpers)");
|
|
2204
|
+
}
|
|
2205
|
+
|
|
2206
|
+
// ── Emergency-stop wiring ───────────────────────────────────────────────
|
|
2207
|
+
const stopScript = join(cwd, "scripts/emergency-stop.sh");
|
|
2208
|
+
if (existsSync(stopScript)) {
|
|
2209
|
+
const body = readFileSync(stopScript, "utf-8");
|
|
2210
|
+
if (body.includes("$AGENT_DIR/schedules") || body.includes("$SOPHIE_AI_DIR/schedules")) {
|
|
2211
|
+
warn("scripts/emergency-stop.sh still looks in $REPO/schedules/ (legacy path bug). Run: maestro upgrade");
|
|
2212
|
+
issues++;
|
|
2213
|
+
} else if (!body.includes("Library/LaunchAgents")) {
|
|
2214
|
+
warn("scripts/emergency-stop.sh doesn't reference ~/Library/LaunchAgents/ — kill-switch may be ineffective.");
|
|
2215
|
+
issues++;
|
|
2216
|
+
} else {
|
|
2217
|
+
ok("scripts/emergency-stop.sh present (uses ~/Library/LaunchAgents/)");
|
|
2218
|
+
}
|
|
2219
|
+
} else { warn("scripts/emergency-stop.sh missing — kill-switch unavailable"); issues++; }
|
|
2220
|
+
|
|
2221
|
+
// ── Feature-init state ──────────────────────────────────────────────────
|
|
2222
|
+
const featureState = join(cwd, ".maestro/features.json");
|
|
2223
|
+
if (existsSync(featureState)) {
|
|
2224
|
+
try {
|
|
2225
|
+
const s = JSON.parse(readFileSync(featureState, "utf-8"));
|
|
2226
|
+
const pending = (s.pending || []).length;
|
|
2227
|
+
const initialised = Object.keys(s.initialized || {}).length;
|
|
2228
|
+
if (pending > 0) {
|
|
2229
|
+
warn(`${pending} framework feature(s) pending init — run: maestro init`);
|
|
2230
|
+
issues++;
|
|
2231
|
+
} else {
|
|
2232
|
+
ok(`${initialised} framework feature(s) initialised`);
|
|
2233
|
+
}
|
|
2234
|
+
} catch {
|
|
2235
|
+
warn(".maestro/features.json is malformed — run: maestro upgrade");
|
|
2236
|
+
issues++;
|
|
2237
|
+
}
|
|
2238
|
+
} else {
|
|
2239
|
+
warn(".maestro/features.json not found — run: maestro upgrade");
|
|
2240
|
+
issues++;
|
|
2241
|
+
}
|
|
2242
|
+
|
|
2243
|
+
// ── Workflow / agent / skill cross-references ──────────────────────────
|
|
2244
|
+
// Scan workflows/ for `agent:` and `skill:` references and verify each
|
|
2245
|
+
// exists. Catches the "morning-brief references agent: foo that doesn't
|
|
2246
|
+
// exist" class of integration drift.
|
|
2247
|
+
const workflowsDir = join(cwd, "workflows");
|
|
2248
|
+
const agentsDir = join(cwd, "agents");
|
|
2249
|
+
const skillsDir = join(cwd, "plugins/maestro-skills/skills");
|
|
2250
|
+
if (existsSync(workflowsDir)) {
|
|
2251
|
+
const missingAgents = new Set();
|
|
2252
|
+
const missingSkills = new Set();
|
|
2253
|
+
const walkYaml = (dir) => {
|
|
2254
|
+
for (const name of readdirSync(dir)) {
|
|
2255
|
+
const full = join(dir, name);
|
|
2256
|
+
const st = statSync(full);
|
|
2257
|
+
if (st.isDirectory()) walkYaml(full);
|
|
2258
|
+
else if (st.isFile() && /\.ya?ml$/.test(name)) {
|
|
2259
|
+
const body = readFileSync(full, "utf-8");
|
|
2260
|
+
for (const m of body.matchAll(/^\s+agent:\s*([\w-]+)\b/gm)) {
|
|
2261
|
+
const a = m[1];
|
|
2262
|
+
if (a === "agent-self" || a === "agent_self") continue;
|
|
2263
|
+
if (!existsSync(join(agentsDir, a))) missingAgents.add(a);
|
|
2264
|
+
}
|
|
2265
|
+
for (const m of body.matchAll(/^\s+skill:\s*([\w-]+)\b/gm)) {
|
|
2266
|
+
const s = m[1];
|
|
2267
|
+
if (!existsSync(join(skillsDir, `${s}.md`))) missingSkills.add(s);
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2270
|
+
}
|
|
2271
|
+
};
|
|
2272
|
+
try { walkYaml(workflowsDir); }
|
|
2273
|
+
catch (err) { warn(`workflow scan failed: ${err.message}`); }
|
|
2274
|
+
if (missingAgents.size === 0 && missingSkills.size === 0) {
|
|
2275
|
+
ok("All workflow agent/skill references resolved");
|
|
2276
|
+
} else {
|
|
2277
|
+
if (missingAgents.size) { warn(`Workflows reference ${missingAgents.size} missing agent(s): ${[...missingAgents].slice(0,5).join(", ")}${missingAgents.size > 5 ? "…" : ""}`); issues++; }
|
|
2278
|
+
if (missingSkills.size) { warn(`Workflows reference ${missingSkills.size} missing skill(s): ${[...missingSkills].slice(0,5).join(", ")}${missingSkills.size > 5 ? "…" : ""}`); issues++; }
|
|
2279
|
+
}
|
|
2280
|
+
}
|
|
2281
|
+
|
|
2282
|
+
// ── state/ + logs/ ownership & permission hygiene (audit M3) ─────────────
|
|
2283
|
+
// The framework runs as the unprivileged user. If state/ or logs/ were
|
|
2284
|
+
// created/chown'd root (e.g. an installer step run under sudo) the daemon's
|
|
2285
|
+
// writes fail silently; a 0777 chmod "fix" then leaves private state
|
|
2286
|
+
// world-writable. Verify ownership + perms here and tell the operator the
|
|
2287
|
+
// exact command to run. We never chown/chmod automatically.
|
|
2288
|
+
if (typeof process.getuid === "function") {
|
|
2289
|
+
const ownershipPaths = [
|
|
2290
|
+
"state", "logs",
|
|
2291
|
+
"state/queues", "state/dashboards",
|
|
2292
|
+
"state/cadence-bus",
|
|
2293
|
+
"state/cadence-bus/inbox", "state/cadence-bus/claimed",
|
|
2294
|
+
"state/cadence-bus/processed", "state/cadence-bus/failed",
|
|
2295
|
+
"state/cadence-bus/dlq",
|
|
2296
|
+
"logs/cadence-bus",
|
|
2297
|
+
];
|
|
2298
|
+
const entries = ownershipPaths.map((rel) => {
|
|
2299
|
+
const full = join(cwd, rel);
|
|
2300
|
+
try {
|
|
2301
|
+
const st = statSync(full);
|
|
2302
|
+
return { path: rel, mode: st.mode, uid: st.uid, exists: true };
|
|
2303
|
+
} catch {
|
|
2304
|
+
return { path: rel, exists: false };
|
|
2305
|
+
}
|
|
2306
|
+
});
|
|
2307
|
+
const ownership = checkOwnershipSet(entries, { runtimeUid: process.getuid() });
|
|
2308
|
+
if (ownership.ok) {
|
|
2309
|
+
ok("state/ and logs/ owned by runtime uid, not group/world-writable");
|
|
2310
|
+
} else {
|
|
2311
|
+
for (const v of ownership.violations) {
|
|
2312
|
+
fail(`${v.path}: ${v.reasons.join("; ")}`);
|
|
2313
|
+
}
|
|
2314
|
+
fail(` Fix: ${ownership.fixCommand}`);
|
|
2315
|
+
issues += ownership.violations.length;
|
|
2316
|
+
}
|
|
2317
|
+
}
|
|
2318
|
+
|
|
2319
|
+
// ── Cadence-bus DLQ depth ───────────────────────────────────────────────
|
|
2320
|
+
const dlqDir = join(cwd, "state/cadence-bus/dlq");
|
|
2321
|
+
if (existsSync(dlqDir)) {
|
|
2322
|
+
const dlqFiles = readdirSync(dlqDir).filter((n) => n.endsWith(".json"));
|
|
2323
|
+
if (dlqFiles.length === 0) ok("Cadence-bus DLQ is empty");
|
|
2324
|
+
else if (dlqFiles.length < 5) warn(`Cadence-bus DLQ: ${dlqFiles.length} event(s). Review: state/cadence-bus/dlq/`);
|
|
2325
|
+
else {
|
|
2326
|
+
fail(`Cadence-bus DLQ: ${dlqFiles.length} event(s) — investigate before clearing.`);
|
|
2327
|
+
issues++;
|
|
2328
|
+
}
|
|
2329
|
+
}
|
|
2330
|
+
|
|
2331
|
+
// ── Backup configured + fresh (DR-config, gaps-enterprise-ops G16) ──────
|
|
2332
|
+
// RED when backup is unconfigured: at fleet scale a machine with no DR target
|
|
2333
|
+
// is a silent data-loss risk. Previously doctor exited 0 on a missing backup
|
|
2334
|
+
// target; now it FAILs so the gap surfaces. GREEN only when a target is
|
|
2335
|
+
// configured AND a recent (<25h) successful run is recorded.
|
|
2336
|
+
const backupCfg = join(cwd, ".maestro/backup-config.yaml");
|
|
2337
|
+
if (existsSync(backupCfg)) {
|
|
2338
|
+
const cfg = readFileSync(backupCfg, "utf-8");
|
|
2339
|
+
if (/enabled:\s*true/.test(cfg)) {
|
|
2340
|
+
const lastBackup = join(cwd, ".maestro/last-backup.json");
|
|
2341
|
+
if (existsSync(lastBackup)) {
|
|
2342
|
+
try {
|
|
2343
|
+
const lb = JSON.parse(readFileSync(lastBackup, "utf-8"));
|
|
2344
|
+
const age = Date.now() - new Date(lb.completed_at).getTime();
|
|
2345
|
+
if (age < 25 * 60 * 60 * 1000) ok(`Backup configured + fresh: last run ${Math.round(age / 3600000)}h ago (${lb.provider}://${lb.bucket})`);
|
|
2346
|
+
else { warn(`Last backup ${Math.round(age / 3600000)}h ago — investigate scripts/maintenance/backup-to-cloud.sh`); issues++; }
|
|
2347
|
+
} catch { warn("last-backup.json is malformed"); }
|
|
2348
|
+
} else {
|
|
2349
|
+
warn("Backup enabled but no successful run yet — schedule scripts/maintenance/backup-to-cloud.sh");
|
|
2350
|
+
issues++;
|
|
2351
|
+
}
|
|
2352
|
+
} else {
|
|
2353
|
+
fail("Backup target present but disabled (.maestro/backup-config.yaml: enabled is not true).");
|
|
2354
|
+
console.log(" fix: set enabled: true and schedule scripts/maintenance/backup-to-cloud.sh");
|
|
2355
|
+
issues++;
|
|
2356
|
+
}
|
|
2357
|
+
} else {
|
|
2358
|
+
fail("No backup target configured — fleet machines need a DR backup (.maestro/backup-config.yaml missing).");
|
|
2359
|
+
console.log(" fix: create .maestro/backup-config.yaml (enabled: true, provider, bucket) and schedule scripts/maintenance/backup-to-cloud.sh");
|
|
2360
|
+
issues++;
|
|
2361
|
+
}
|
|
2362
|
+
|
|
2363
|
+
// ── Cross-agent registry ────────────────────────────────────────────────
|
|
2364
|
+
const knownAgents = join(cwd, "config/known-agents.json");
|
|
2365
|
+
if (existsSync(knownAgents)) {
|
|
2366
|
+
try {
|
|
2367
|
+
const reg = JSON.parse(readFileSync(knownAgents, "utf-8"));
|
|
2368
|
+
const count = Array.isArray(reg.agents) ? reg.agents.length : 0;
|
|
2369
|
+
if (count > 0) ok(`Cross-agent registry: ${count} peer agent(s)`);
|
|
2370
|
+
else warn("config/known-agents.json present but empty — peer-agent detection won't work");
|
|
2371
|
+
} catch {
|
|
2372
|
+
warn("config/known-agents.json is malformed");
|
|
2373
|
+
issues++;
|
|
2374
|
+
}
|
|
2375
|
+
} else {
|
|
2376
|
+
warn("config/known-agents.json missing — run: maestro init known-agents-registry --apply");
|
|
2377
|
+
issues++;
|
|
2378
|
+
}
|
|
2379
|
+
|
|
2380
|
+
// ── Model router (1.12.0+) ─────────────────────────────────────────────
|
|
2381
|
+
// Disabled-but-present is the normal "ready to opt in" state. Active is
|
|
2382
|
+
// verified by loading the config and checking each backend's auth env var.
|
|
2383
|
+
const routerActive = join(cwd, "config/model-routing.yaml");
|
|
2384
|
+
const routerDisabled = join(cwd, "config/model-routing.yaml.disabled");
|
|
2385
|
+
if (existsSync(routerActive)) {
|
|
2386
|
+
try {
|
|
2387
|
+
// Use the framework's loader so we get the same parser as the daemon.
|
|
2388
|
+
const routerPath = process.env.HOME
|
|
2389
|
+
? join(process.env.HOME, "maestro/lib/model-router.mjs")
|
|
2390
|
+
: null;
|
|
2391
|
+
if (routerPath && existsSync(routerPath)) {
|
|
2392
|
+
const { loadRoutingConfig } = await import(`file://${routerPath}`);
|
|
2393
|
+
const cfg = loadRoutingConfig(cwd);
|
|
2394
|
+
if (!cfg) {
|
|
2395
|
+
warn("config/model-routing.yaml present but loader returned null — check syntax");
|
|
2396
|
+
issues++;
|
|
2397
|
+
} else if (cfg.schema_version === 2) {
|
|
2398
|
+
// ── Policy v2 path: strict-validate + staleness ladder ──────────
|
|
2399
|
+
ok(`Model router active (policy v2, ${Object.keys(cfg.aliases || {}).length} alias(es))`);
|
|
2400
|
+
try {
|
|
2401
|
+
const { validateRoutingConfig } = await import(`file://${routerPath}`);
|
|
2402
|
+
const catalogLib = join(process.env.HOME, "maestro/lib/model-router/catalog.mjs");
|
|
2403
|
+
const { loadCatalog } = await import(`file://${catalogLib}`);
|
|
2404
|
+
const catalog = loadCatalog(cwd, {});
|
|
2405
|
+
const { errors, warnings } = validateRoutingConfig(cfg, { catalog, env: process.env });
|
|
2406
|
+
for (const w of warnings) warn(` router config: ${w.where} — ${w.warning}`);
|
|
2407
|
+
for (const e of errors) { fail(` router config: ${e.where} — ${e.error}`); issues++; }
|
|
2408
|
+
if (errors.length === 0) ok(" config strict-validation: passed");
|
|
2409
|
+
|
|
2410
|
+
// Staleness ladder: catalog cost provenance + pricing cache.
|
|
2411
|
+
// WARN @7d, FAIL @30d (graft #18). Volatile rows escalate the FAIL.
|
|
2412
|
+
const models = catalog.models || [];
|
|
2413
|
+
let oldest = null;
|
|
2414
|
+
let anyVolatile = false;
|
|
2415
|
+
for (const r of models) {
|
|
2416
|
+
const p = r.cost_provenance || {};
|
|
2417
|
+
if (p.volatile) anyVolatile = true;
|
|
2418
|
+
if (p.fetched && (oldest == null || p.fetched < oldest)) oldest = p.fetched;
|
|
2419
|
+
}
|
|
2420
|
+
const catStale = evaluateStaleness({ fetched: oldest, hasVolatile: anyVolatile, label: "catalog cost provenance" });
|
|
2421
|
+
if (catStale.status === "fail") { fail(` ${catStale.reason}`); issues++; }
|
|
2422
|
+
else if (catStale.status === "warn") { warn(` ${catStale.reason}`); issues++; }
|
|
2423
|
+
else ok(` ${catStale.reason}`);
|
|
2424
|
+
|
|
2425
|
+
// Pricing cache freshness (cost fields refresh out of band).
|
|
2426
|
+
const priceCachePath = join(cwd, "state/model-router/pricing-cache.json");
|
|
2427
|
+
if (existsSync(priceCachePath)) {
|
|
2428
|
+
let fetched = null;
|
|
2429
|
+
try {
|
|
2430
|
+
const pc = JSON.parse(readFileSync(priceCachePath, "utf-8"));
|
|
2431
|
+
const entries = pc.models && typeof pc.models === "object" ? pc.models : pc;
|
|
2432
|
+
for (const v of Object.values(entries)) {
|
|
2433
|
+
const f = v && v.provenance && v.provenance.fetched;
|
|
2434
|
+
if (f && (fetched == null || f < fetched)) fetched = f;
|
|
2435
|
+
}
|
|
2436
|
+
if (!fetched && pc.fetched) fetched = pc.fetched;
|
|
2437
|
+
} catch { /* */ }
|
|
2438
|
+
const pcStale = evaluateStaleness({ fetched, hasVolatile: anyVolatile, label: "pricing cache" });
|
|
2439
|
+
if (pcStale.status === "fail") { fail(` ${pcStale.reason}`); issues++; }
|
|
2440
|
+
else if (pcStale.status === "warn") { warn(` ${pcStale.reason}`); issues++; }
|
|
2441
|
+
else ok(` ${pcStale.reason}`);
|
|
2442
|
+
} else {
|
|
2443
|
+
ok(" pricing cache absent — using bundled catalog prices (provenance above governs)");
|
|
2444
|
+
}
|
|
2445
|
+
} catch (verr) {
|
|
2446
|
+
warn(` router v2 checks skipped: ${verr.message}`);
|
|
2447
|
+
}
|
|
2448
|
+
} else {
|
|
2449
|
+
const names = Object.keys(cfg.backends);
|
|
2450
|
+
ok(`Model router active (${names.length} backend${names.length === 1 ? "" : "s"}: ${names.join(", ")})`);
|
|
2451
|
+
// Verify auth env for each backend that needs one. We check .env
|
|
2452
|
+
// on disk (not process.env) because doctor doesn't load dotenv;
|
|
2453
|
+
// the daemon does that at spawn time, so a key present in .env is
|
|
2454
|
+
// effectively configured even if process.env doesn't show it.
|
|
2455
|
+
let envBody = "";
|
|
2456
|
+
try { envBody = readFileSync(join(cwd, ".env"), "utf-8"); } catch { /* */ }
|
|
2457
|
+
for (const [name, b] of Object.entries(cfg.backends)) {
|
|
2458
|
+
if (!b.auth_env) continue;
|
|
2459
|
+
const re = new RegExp(`^${b.auth_env}=.+`, "m");
|
|
2460
|
+
const hasInEnvFile = re.test(envBody);
|
|
2461
|
+
const hasInProcEnv = !!process.env[b.auth_env];
|
|
2462
|
+
if (!hasInEnvFile && !hasInProcEnv) {
|
|
2463
|
+
warn(`Model router backend "${name}" needs ${b.auth_env} — not set in .env or process env`);
|
|
2464
|
+
issues++;
|
|
2465
|
+
} else {
|
|
2466
|
+
ok(` ${name}: ${b.auth_env} configured`);
|
|
2467
|
+
}
|
|
2468
|
+
}
|
|
2469
|
+
}
|
|
2470
|
+
} else {
|
|
2471
|
+
warn("model-router config present but lib/model-router.mjs not reachable — `npm run upgrade` in agent");
|
|
2472
|
+
}
|
|
2473
|
+
} catch (err) {
|
|
2474
|
+
fail(`Model router config failed to load: ${err.message}`);
|
|
2475
|
+
issues++;
|
|
2476
|
+
}
|
|
2477
|
+
} else if (existsSync(routerDisabled)) {
|
|
2478
|
+
ok("Model router available (config/model-routing.yaml.disabled — rename to activate)");
|
|
2479
|
+
}
|
|
2480
|
+
|
|
2481
|
+
// ── Voice realtime (1.12.0+) ───────────────────────────────────────────
|
|
2482
|
+
const voiceActive = join(cwd, "config/voice.yaml");
|
|
2483
|
+
const voiceDisabled = join(cwd, "config/voice.yaml.disabled");
|
|
2484
|
+
if (existsSync(voiceActive)) {
|
|
2485
|
+
ok("Voice channel active (config/voice.yaml)");
|
|
2486
|
+
if (existsSync(join(cwd, ".env"))) {
|
|
2487
|
+
const env = readFileSync(join(cwd, ".env"), "utf-8");
|
|
2488
|
+
if (/^OPENAI_API_KEY=.+/m.test(env)) ok(" OPENAI_API_KEY configured");
|
|
2489
|
+
else { warn(" OPENAI_API_KEY missing — voice channel will not connect"); issues++; }
|
|
2490
|
+
if (/^TWILIO_ACCOUNT_SID=.+/m.test(env) && /^TWILIO_AUTH_TOKEN=.+/m.test(env)) ok(" Twilio credentials configured");
|
|
2491
|
+
else warn(" Twilio creds missing (only required for SIP inbound mode)");
|
|
2492
|
+
}
|
|
2493
|
+
if (!existsSync(join(cwd, "logs/voice/sessions"))) warn(" logs/voice/sessions/ missing — run: maestro init voice-realtime --apply");
|
|
2494
|
+
} else if (existsSync(voiceDisabled)) {
|
|
2495
|
+
ok("Voice channel available (config/voice.yaml.disabled — rename to activate)");
|
|
2496
|
+
}
|
|
2497
|
+
|
|
2498
|
+
// ── Channel bus (1.12.0+) ──────────────────────────────────────────────
|
|
2499
|
+
const channelStateDir = join(cwd, "state/channels");
|
|
2500
|
+
if (existsSync(channelStateDir)) ok("Channel bus state dir present (state/channels/)");
|
|
2501
|
+
else warn("state/channels/ missing — run: maestro init channel-bus --apply");
|
|
2502
|
+
|
|
2503
|
+
// ── WhatsApp transport (1.12.0+) ───────────────────────────────────────
|
|
2504
|
+
const waActive = join(cwd, "config/whatsapp.yaml");
|
|
2505
|
+
const waDisabled = join(cwd, "config/whatsapp.yaml.disabled");
|
|
2506
|
+
if (existsSync(waActive)) {
|
|
2507
|
+
try {
|
|
2508
|
+
const body = readFileSync(waActive, "utf-8");
|
|
2509
|
+
const isBaileys = /^transport:\s*baileys\b/m.test(body);
|
|
2510
|
+
const isTwilio = /^transport:\s*twilio\b/m.test(body);
|
|
2511
|
+
if (isBaileys) {
|
|
2512
|
+
ok("WhatsApp channel active (transport: baileys)");
|
|
2513
|
+
const pkgA = join(cwd, "node_modules/baileys/package.json");
|
|
2514
|
+
const pkgB = join(cwd, "node_modules/@whiskeysockets/baileys/package.json");
|
|
2515
|
+
if (existsSync(pkgA) || existsSync(pkgB)) ok(" baileys npm package installed");
|
|
2516
|
+
else { warn(" baileys not installed — run: npm i baileys"); issues++; }
|
|
2517
|
+
const authDir = join(cwd, "state/whatsapp/auth");
|
|
2518
|
+
if (!existsSync(authDir)) { warn(" state/whatsapp/auth/ missing — run: maestro init whatsapp-baileys --apply"); issues++; }
|
|
2519
|
+
} else if (isTwilio) {
|
|
2520
|
+
ok("WhatsApp channel active (transport: twilio)");
|
|
2521
|
+
if (existsSync(join(cwd, ".env"))) {
|
|
2522
|
+
const env = readFileSync(join(cwd, ".env"), "utf-8");
|
|
2523
|
+
if (/^TWILIO_ACCOUNT_SID=.+/m.test(env)) ok(" TWILIO_ACCOUNT_SID configured");
|
|
2524
|
+
else { warn(" TWILIO_ACCOUNT_SID missing"); issues++; }
|
|
2525
|
+
}
|
|
2526
|
+
} else {
|
|
2527
|
+
warn("config/whatsapp.yaml present but transport is neither 'twilio' nor 'baileys'");
|
|
2528
|
+
issues++;
|
|
2529
|
+
}
|
|
2530
|
+
} catch (err) {
|
|
2531
|
+
warn(`config/whatsapp.yaml unreadable: ${err.message}`);
|
|
2532
|
+
issues++;
|
|
2533
|
+
}
|
|
2534
|
+
} else if (existsSync(waDisabled)) {
|
|
2535
|
+
ok("WhatsApp channel available (config/whatsapp.yaml.disabled — rename to activate)");
|
|
2536
|
+
}
|
|
2537
|
+
|
|
2538
|
+
// ── Collective memory & org mesh ─────────────────────────────────────────
|
|
2539
|
+
if (existsSync(join(cwd, "scripts/collective/hook-runner.mjs"))) {
|
|
2540
|
+
const pointer = join(homedir(), ".claude", "maestro-agent.json");
|
|
2541
|
+
const globalSettings = join(homedir(), ".claude", "settings.json");
|
|
2542
|
+
let wired = false;
|
|
2543
|
+
if (existsSync(globalSettings)) {
|
|
2544
|
+
try {
|
|
2545
|
+
const gs = JSON.parse(readFileSync(globalSettings, "utf-8"));
|
|
2546
|
+
const cmds = Object.values(gs.hooks || {}).flat().flatMap((g) => (g && g.hooks ? g.hooks : [])).map((h) => h && h.command).filter(Boolean);
|
|
2547
|
+
wired = cmds.some((c) => c.includes("scripts/collective/hook-runner.mjs"));
|
|
2548
|
+
} catch { /* */ }
|
|
2549
|
+
}
|
|
2550
|
+
if (wired && existsSync(pointer)) {
|
|
2551
|
+
ok("Collective memory wired into global Claude config (~/.claude)");
|
|
2552
|
+
} else {
|
|
2553
|
+
warn("Collective memory not wired into global Claude config.");
|
|
2554
|
+
warn(" Fix: npx @cohortapp/agent-sdk global-setup");
|
|
2555
|
+
issues++;
|
|
2556
|
+
}
|
|
2557
|
+
}
|
|
2558
|
+
|
|
2559
|
+
// ── New framework systems (maestro 1.18+) ───────────────────────────────
|
|
2560
|
+
// One-line presence check per system. The lib/* modules are delivered by
|
|
2561
|
+
// `maestro upgrade` (UPGRADE_PATHS includes lib/); a missing module means an
|
|
2562
|
+
// upgrade is overdue (WARN, not FAIL — these systems are additive + fail-open).
|
|
2563
|
+
// The configs are disabled-by-default templates, so "available" is the healthy
|
|
2564
|
+
// resting state until the operator opts in.
|
|
2565
|
+
{
|
|
2566
|
+
const libPresent = (rel) => existsSync(join(cwd, rel));
|
|
2567
|
+
const needsUpgrade = (rel, label) => {
|
|
2568
|
+
if (libPresent(rel)) return true;
|
|
2569
|
+
warn(`${label} module missing (${rel}) — run: npx @cohortapp/agent-sdk upgrade`);
|
|
2570
|
+
issues++;
|
|
2571
|
+
return false;
|
|
2572
|
+
};
|
|
2573
|
+
|
|
2574
|
+
// Org / Cohort plane — REAL connectivity probes (2.0 §5), not presence
|
|
2575
|
+
// checks: enrollment resolution, directory reachability + latency, the
|
|
2576
|
+
// x-org-protocol drift header, the messaging.channels pairing gate, and
|
|
2577
|
+
// the workspace-mailbox probe when config/orgmail.yaml exists. `fail`
|
|
2578
|
+
// counts toward the issues total; `warn` lines are advisory (protocol
|
|
2579
|
+
// drift / servers predating the phase-1 header).
|
|
2580
|
+
if (needsUpgrade("lib/org/client.mjs", "Org client")) {
|
|
2581
|
+
try {
|
|
2582
|
+
const { checkOrgConnectivity } = await import("../lib/org/doctor.mjs");
|
|
2583
|
+
for (const r of await checkOrgConnectivity({ agentRoot: cwd })) {
|
|
2584
|
+
if (r.level === "ok") ok(r.msg);
|
|
2585
|
+
else if (r.level === "warn") warn(r.msg);
|
|
2586
|
+
else { fail(r.msg); issues++; }
|
|
2587
|
+
}
|
|
2588
|
+
} catch (err) {
|
|
2589
|
+
warn(`Org connectivity probes unavailable: ${err && err.message ? err.message : err}`);
|
|
2590
|
+
}
|
|
2591
|
+
}
|
|
2592
|
+
|
|
2593
|
+
// Secret lifecycle (local sealed store by default; broker optional).
|
|
2594
|
+
if (needsUpgrade("lib/secrets/broker.mjs", "Secrets broker")) {
|
|
2595
|
+
if (existsSync(join(cwd, "config/secrets.yaml"))) ok("Secret lifecycle configured (config/secrets.yaml — `maestro secrets list`)");
|
|
2596
|
+
else ok("Secret lifecycle available (config/secrets.yaml absent — `maestro upgrade` ships the template)");
|
|
2597
|
+
}
|
|
2598
|
+
|
|
2599
|
+
// Diagnostics / observability spine.
|
|
2600
|
+
needsUpgrade("lib/diagnostics/trace.mjs", "Diagnostics trace");
|
|
2601
|
+
if (libPresent("lib/diagnostics/counters.mjs")) ok("Diagnostics spine present (trace/events/counters)");
|
|
2602
|
+
|
|
2603
|
+
// Behavioral alerting (thresholds + webhook sink).
|
|
2604
|
+
if (existsSync(join(cwd, "config/alerts.yaml"))) ok("Alerting configured (config/alerts.yaml)");
|
|
2605
|
+
else warn("config/alerts.yaml absent — run: npx @cohortapp/agent-sdk upgrade");
|
|
2606
|
+
|
|
2607
|
+
// Hook bus, dynamic scheduling, send-gate — additive lib spine.
|
|
2608
|
+
needsUpgrade("lib/hooks/bus.mjs", "Hook bus");
|
|
2609
|
+
if (libPresent("lib/scheduling/dynamic-jobs.mjs")) ok("Dynamic scheduling present (lib/scheduling/)");
|
|
2610
|
+
needsUpgrade("lib/comms/send-gate.mjs", "Send-gate");
|
|
2611
|
+
}
|
|
2612
|
+
|
|
2613
|
+
console.log();
|
|
2614
|
+
if (issues === 0) ok("All checks passed.");
|
|
2615
|
+
else warn(`${issues} issue(s) found.`);
|
|
2616
|
+
process.exitCode = issues === 0 ? 0 : 1;
|
|
2617
|
+
}
|
|
2618
|
+
|
|
2619
|
+
// ---------------------------------------------------------------------------
|
|
2620
|
+
// SETUP — the deterministic, resumable, self-verifying wizard (WS1)
|
|
2621
|
+
// ---------------------------------------------------------------------------
|
|
2622
|
+
|
|
2623
|
+
const SETUP_HELP = `
|
|
2624
|
+
${C.bold}maestro setup${C.reset} — configure this agent (deterministic, resumable, self-verifying).
|
|
2625
|
+
|
|
2626
|
+
Usage:
|
|
2627
|
+
maestro setup [section] [flags]
|
|
2628
|
+
|
|
2629
|
+
Sections (run in order): identity company model comms tools operating-model enrich verify
|
|
2630
|
+
|
|
2631
|
+
Flags:
|
|
2632
|
+
--resume Resume from the checkpoint (default when one exists)
|
|
2633
|
+
--from <id> Start at section <id> and continue
|
|
2634
|
+
--only <id,id,...> Run only these sections (forced, even if complete)
|
|
2635
|
+
--status Print each section's detect() status and exit
|
|
2636
|
+
--headless Never prompt; use --answers / env / defaults
|
|
2637
|
+
--answers <file.json> Pre-supplied answers for headless / scripted runs
|
|
2638
|
+
--no-enrich Skip LLM enrichment (leave deterministic skeletons)
|
|
2639
|
+
--dry-run Print what would run; write nothing
|
|
2640
|
+
--verbose Print generator output as sections apply
|
|
2641
|
+
--help, -h Show this help
|
|
2642
|
+
|
|
2643
|
+
Run from inside an agent repo (one with config/agent.json). After
|
|
2644
|
+
\`maestro create <dir>\`, do: cd <dir> && maestro setup
|
|
2645
|
+
`;
|
|
2646
|
+
|
|
2647
|
+
/**
|
|
2648
|
+
* `maestro pair <code>` — open a pre-auth pairing handshake with the org.
|
|
2649
|
+
*
|
|
2650
|
+
* The operator (or an org admin acting on the agent's behalf) picks a `code`,
|
|
2651
|
+
* runs this to POST pairing.request (UNAUTHENTICATED — the agent has no key
|
|
2652
|
+
* yet), and shares the code with an org admin who approves it via
|
|
2653
|
+
* pairing.approve. The device keypair is materialised on first use and its
|
|
2654
|
+
* public key rides in the handshake. Enablement of the knowledge plane still
|
|
2655
|
+
* waits on the admin issuing a Bearer key (paste it into config/org.yaml or
|
|
2656
|
+
* re-run `maestro setup --only org`). Complements the admin-API-key lane; it
|
|
2657
|
+
* does not replace it.
|
|
2658
|
+
*/
|
|
2659
|
+
async function pairCmd(args = []) {
|
|
2660
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
2661
|
+
log("Usage: maestro pair <code>");
|
|
2662
|
+
log(" Opens a pre-auth pairing handshake; an org admin approves it (pairing.approve).");
|
|
2663
|
+
log(" Resolves the Cohort base from COHORT_BASE or config/org.yaml.");
|
|
2664
|
+
return;
|
|
2665
|
+
}
|
|
2666
|
+
const cwd = process.cwd();
|
|
2667
|
+
const isAgentRepo = existsSync(join(cwd, "config/agent.json")) || existsSync(join(cwd, "config/agent.ts"));
|
|
2668
|
+
if (!isAgentRepo) {
|
|
2669
|
+
warn("Not a Maestro agent directory (no config/agent.json here).");
|
|
2670
|
+
log("Run this from an agent repo (create one with `maestro create <dirname>`).");
|
|
2671
|
+
process.exitCode = 1;
|
|
2672
|
+
return;
|
|
2673
|
+
}
|
|
2674
|
+
|
|
2675
|
+
let orgMod, clientMod;
|
|
2676
|
+
try {
|
|
2677
|
+
orgMod = await import(pathToFileURL(join(MAESTRO_ROOT, "lib", "setup", "sections", "org.mjs")).href);
|
|
2678
|
+
clientMod = await import(pathToFileURL(join(MAESTRO_ROOT, "lib", "org", "client.mjs")).href);
|
|
2679
|
+
} catch (err) {
|
|
2680
|
+
fail(`could not load the org client: ${err && err.message ? err.message : err}`);
|
|
2681
|
+
process.exitCode = 1;
|
|
2682
|
+
return;
|
|
2683
|
+
}
|
|
2684
|
+
|
|
2685
|
+
const cfg = await orgMod.readOrgConfig(cwd);
|
|
2686
|
+
const c = clientMod.configFromAgent(cfg);
|
|
2687
|
+
const base = (process.env.COHORT_BASE || c.base || "").trim();
|
|
2688
|
+
const code = (args.find((a) => a && !a.startsWith("-")) || process.env.COHORT_PAIRING_CODE || "").trim();
|
|
2689
|
+
|
|
2690
|
+
if (!base) {
|
|
2691
|
+
fail("no Cohort base URL — set COHORT_BASE or run `maestro setup --only org` first.");
|
|
2692
|
+
process.exitCode = 1;
|
|
2693
|
+
return;
|
|
2694
|
+
}
|
|
2695
|
+
if (!code || code.length < 4) {
|
|
2696
|
+
fail("usage: maestro pair <code> (4+ chars; share it with your org admin to approve).");
|
|
2697
|
+
process.exitCode = 1;
|
|
2698
|
+
return;
|
|
2699
|
+
}
|
|
2700
|
+
|
|
2701
|
+
log(`Requesting pairing with ${base} …`);
|
|
2702
|
+
const res = await orgMod.requestPairing(cwd, { base, code, orgId: c.orgId });
|
|
2703
|
+
if (res.ok) {
|
|
2704
|
+
ok(`Pairing requested — code ${res.code}, agent ${res.agentId}, status ${res.status}.`);
|
|
2705
|
+
log("Ask an org admin to approve it (pairing.approve), then set org.cohort.token to the issued key");
|
|
2706
|
+
log("(or re-run `maestro setup --only org`). config/org.yaml records the outstanding handshake.");
|
|
2707
|
+
} else {
|
|
2708
|
+
fail(`Pairing failed: ${res.error}`);
|
|
2709
|
+
process.exitCode = 1;
|
|
2710
|
+
}
|
|
2711
|
+
}
|
|
2712
|
+
|
|
2713
|
+
async function setupCmd(args = []) {
|
|
2714
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
2715
|
+
console.log(SETUP_HELP);
|
|
2716
|
+
return;
|
|
2717
|
+
}
|
|
2718
|
+
const cwd = process.cwd();
|
|
2719
|
+
const isAgentRepo = existsSync(join(cwd, "config/agent.json")) || existsSync(join(cwd, "config/agent.ts"));
|
|
2720
|
+
const wantStatus = args.includes("--status");
|
|
2721
|
+
|
|
2722
|
+
if (!isAgentRepo) {
|
|
2723
|
+
// Graceful, non-throwing message (the done-bar requires --help / --status to
|
|
2724
|
+
// run cleanly in a non-agent dir).
|
|
2725
|
+
warn("Not a Maestro agent directory (no config/agent.json here).");
|
|
2726
|
+
log("Create one first: npx @cohortapp/agent-sdk create <dirname>");
|
|
2727
|
+
log("Then: cd <dirname> && maestro setup");
|
|
2728
|
+
if (!wantStatus) process.exitCode = 1;
|
|
2729
|
+
return;
|
|
2730
|
+
}
|
|
2731
|
+
|
|
2732
|
+
let runner;
|
|
2733
|
+
try {
|
|
2734
|
+
runner = await import(join(MAESTRO_ROOT, "lib", "setup", "runner.mjs"));
|
|
2735
|
+
} catch (err) {
|
|
2736
|
+
fail(`could not load the setup runner: ${err && err.message ? err.message : err}`);
|
|
2737
|
+
process.exitCode = 1;
|
|
2738
|
+
return;
|
|
2739
|
+
}
|
|
2740
|
+
|
|
2741
|
+
try {
|
|
2742
|
+
const parsed = runner.parseFlags(args);
|
|
2743
|
+
const res = await runner.runSetup({
|
|
2744
|
+
agentRoot: cwd,
|
|
2745
|
+
maestroRoot: MAESTRO_ROOT,
|
|
2746
|
+
flags: parsed.flags,
|
|
2747
|
+
section: parsed.section,
|
|
2748
|
+
});
|
|
2749
|
+
if (res && res.status) return; // --status already printed
|
|
2750
|
+
console.log();
|
|
2751
|
+
const gateGreen = res && res.verify && res.verify.complete && res.verify.complete.ok;
|
|
2752
|
+
if (res && res.ok && gateGreen) {
|
|
2753
|
+
ok("Setup complete — full operating model + a green completeness gate.");
|
|
2754
|
+
} else if (res && res.ok) {
|
|
2755
|
+
// No hard failures (identity/archetype/Claude all OK), but the completeness
|
|
2756
|
+
// gate isn't fully green — usually un-enriched prose slots (e.g. --no-enrich).
|
|
2757
|
+
ok("Setup ran — identity, model, and the operating model are in place.");
|
|
2758
|
+
warn("Some company-specific slots are still skeletons (run without --no-enrich to fill them).");
|
|
2759
|
+
warn("Re-run any time: maestro setup (resumes from the checkpoint)");
|
|
2760
|
+
} else {
|
|
2761
|
+
warn("Setup finished with hard failures — see the capability summary above.");
|
|
2762
|
+
warn("Re-run after fixing: maestro setup (resumes from the checkpoint)");
|
|
2763
|
+
process.exitCode = 1;
|
|
2764
|
+
}
|
|
2765
|
+
} catch (err) {
|
|
2766
|
+
fail(`setup failed: ${err && err.message ? err.message : err}`);
|
|
2767
|
+
process.exitCode = 1;
|
|
2768
|
+
}
|
|
2769
|
+
}
|
|
2770
|
+
|
|
2771
|
+
// ---------------------------------------------------------------------------
|
|
2772
|
+
// INIT — run pending feature-init steps
|
|
2773
|
+
// ---------------------------------------------------------------------------
|
|
2774
|
+
|
|
2775
|
+
async function initCmd(args) {
|
|
2776
|
+
const cwd = process.cwd();
|
|
2777
|
+
const help = args.includes("--help") || args.includes("-h");
|
|
2778
|
+
const apply = args.includes("--apply") || args.includes("-y") || args.includes("--yes");
|
|
2779
|
+
const featureArg = args.find((a) => !a.startsWith("-"));
|
|
2780
|
+
|
|
2781
|
+
if (help) {
|
|
2782
|
+
console.log(`
|
|
2783
|
+
Usage: maestro init [feature-name] [--apply]
|
|
2784
|
+
|
|
2785
|
+
Reconciles this agent's .maestro/features.json against the framework
|
|
2786
|
+
feature registry (framework-features.json shipped with @cohortapp/agent-sdk).
|
|
2787
|
+
Auto-runnable features are executed directly; features requiring user
|
|
2788
|
+
input are listed and can be opted into individually with their name as
|
|
2789
|
+
the first argument.
|
|
2790
|
+
|
|
2791
|
+
maestro init list pending features (dry-run)
|
|
2792
|
+
maestro init --apply run every auto-runnable pending step
|
|
2793
|
+
maestro init <feature> --apply run only the named feature's init step
|
|
2794
|
+
maestro init --help this help
|
|
2795
|
+
|
|
2796
|
+
After init, run \`maestro doctor\` to confirm everything is in place.
|
|
2797
|
+
`);
|
|
2798
|
+
return;
|
|
2799
|
+
}
|
|
2800
|
+
|
|
2801
|
+
const { loadRegistry, loadAgentState, reconcileFeatures, listPending, markInitialised, diffFeatures } = await import("../lib/feature-init.mjs");
|
|
2802
|
+
let registry;
|
|
2803
|
+
try { registry = loadRegistry(FRAMEWORK_FEATURES_PATH); }
|
|
2804
|
+
catch (err) { fail(err.message); process.exit(1); }
|
|
2805
|
+
|
|
2806
|
+
if (featureArg) {
|
|
2807
|
+
// Targeted init — run just one feature's command.
|
|
2808
|
+
const def = registry.features?.[featureArg];
|
|
2809
|
+
if (!def) { fail(`unknown feature: ${featureArg}`); process.exit(1); }
|
|
2810
|
+
log(`Running init for ${featureArg}: ${def.init?.description || ""}`);
|
|
2811
|
+
if (!apply) { warn("dry-run — pass --apply to actually run."); return; }
|
|
2812
|
+
const r = await runFeatureInit({ name: featureArg, def, cwd });
|
|
2813
|
+
if (r.ok) {
|
|
2814
|
+
markInitialised(cwd, featureArg, def.version);
|
|
2815
|
+
ok(`initialised: ${featureArg} v${def.version}`);
|
|
2816
|
+
} else {
|
|
2817
|
+
fail(`init failed: ${r.error}`);
|
|
2818
|
+
process.exit(1);
|
|
2819
|
+
}
|
|
2820
|
+
return;
|
|
2821
|
+
}
|
|
2822
|
+
|
|
2823
|
+
// Reconcile + list pending.
|
|
2824
|
+
const state = loadAgentState(cwd);
|
|
2825
|
+
const diff = diffFeatures(registry, state);
|
|
2826
|
+
if (diff.length === 0 && (state.pending || []).length === 0) {
|
|
2827
|
+
ok("All framework features are initialised.");
|
|
2828
|
+
return;
|
|
2829
|
+
}
|
|
2830
|
+
|
|
2831
|
+
// Show what's pending before doing anything.
|
|
2832
|
+
if (!apply) {
|
|
2833
|
+
log(`${diff.length} feature(s) need initialisation:`);
|
|
2834
|
+
for (const item of diff) {
|
|
2835
|
+
const auto = item.definition.init?.auto;
|
|
2836
|
+
console.log(` ${auto ? "•" : "?"} ${item.definition.title || item.name} (v${item.version}) — ${auto ? "auto" : "needs --apply"}`);
|
|
2837
|
+
if (item.definition.init?.description) console.log(` ${item.definition.init.description}`);
|
|
2838
|
+
}
|
|
2839
|
+
console.log();
|
|
2840
|
+
log("Run `maestro init --apply` to execute every auto-runnable step.");
|
|
2841
|
+
return;
|
|
2842
|
+
}
|
|
2843
|
+
|
|
2844
|
+
// --apply: run auto features now.
|
|
2845
|
+
log("Running auto-init steps...");
|
|
2846
|
+
const result = reconcileFeatures({
|
|
2847
|
+
maestroRoot: MAESTRO_ROOT,
|
|
2848
|
+
agentRoot: cwd,
|
|
2849
|
+
registry,
|
|
2850
|
+
runAuto: true,
|
|
2851
|
+
logger: (entry) => {
|
|
2852
|
+
const sym = entry.stage === "completed" ? `${C.green}✓${C.reset}` : entry.stage === "failed" ? `${C.red}✗${C.reset}` : entry.stage === "pending" ? `${C.yellow}~${C.reset}` : `${C.cyan}…${C.reset}`;
|
|
2853
|
+
console.log(` ${sym} ${entry.feature}${entry.message ? ` — ${entry.message}` : ""}`);
|
|
2854
|
+
},
|
|
2855
|
+
});
|
|
2856
|
+
|
|
2857
|
+
console.log();
|
|
2858
|
+
if (result.ranAuto.length) ok(`${result.ranAuto.length} feature(s) initialised: ${result.ranAuto.join(", ")}`);
|
|
2859
|
+
if (result.pending.length) warn(`${result.pending.length} feature(s) still pending (need manual decision): ${result.pending.join(", ")}`);
|
|
2860
|
+
if (result.failed.length) {
|
|
2861
|
+
fail(`${result.failed.length} feature(s) failed during init:`);
|
|
2862
|
+
for (const f of result.failed) console.log(` ✗ ${f.name}: ${f.error}`);
|
|
2863
|
+
process.exit(1);
|
|
2864
|
+
}
|
|
2865
|
+
if (result.pending.length) {
|
|
2866
|
+
console.log();
|
|
2867
|
+
const pendingList = listPending(cwd);
|
|
2868
|
+
for (const p of pendingList) {
|
|
2869
|
+
console.log(` To initialise ${p.feature}: maestro init ${p.feature} --apply`);
|
|
2870
|
+
}
|
|
2871
|
+
}
|
|
2872
|
+
}
|
|
2873
|
+
|
|
2874
|
+
async function runFeatureInit({ name, def, cwd }) {
|
|
2875
|
+
const { execFileSync } = await import("node:child_process");
|
|
2876
|
+
const cmd = def.init?.command;
|
|
2877
|
+
if (!cmd || cmd === "true") return { ok: true };
|
|
2878
|
+
const parts = cmd.split(/\s+/);
|
|
2879
|
+
try {
|
|
2880
|
+
if (parts[0] === "node" && parts[1]) {
|
|
2881
|
+
const script = join(cwd, parts[1]);
|
|
2882
|
+
const fallback = join(MAESTRO_ROOT, parts[1]);
|
|
2883
|
+
const scriptPath = existsSync(script) ? script : (existsSync(fallback) ? fallback : null);
|
|
2884
|
+
if (!scriptPath) return { ok: false, error: `script not found: ${parts[1]}` };
|
|
2885
|
+
execFileSync(process.execPath, [scriptPath, ...parts.slice(2)], {
|
|
2886
|
+
cwd,
|
|
2887
|
+
env: { ...process.env, AGENT_ROOT: cwd, AGENT_DIR: cwd, MAESTRO_ROOT },
|
|
2888
|
+
stdio: "inherit",
|
|
2889
|
+
});
|
|
2890
|
+
return { ok: true };
|
|
2891
|
+
}
|
|
2892
|
+
execFileSync("/bin/bash", ["-lc", cmd], {
|
|
2893
|
+
cwd,
|
|
2894
|
+
env: { ...process.env, AGENT_ROOT: cwd, AGENT_DIR: cwd, MAESTRO_ROOT },
|
|
2895
|
+
stdio: "inherit",
|
|
2896
|
+
});
|
|
2897
|
+
return { ok: true };
|
|
2898
|
+
} catch (err) {
|
|
2899
|
+
return { ok: false, error: err.message };
|
|
2900
|
+
}
|
|
2901
|
+
}
|
|
2902
|
+
|
|
2903
|
+
// ---------------------------------------------------------------------------
|
|
2904
|
+
// MAIN
|
|
2905
|
+
// ---------------------------------------------------------------------------
|
|
2906
|
+
|
|
2907
|
+
/** Timestamp suffix for config backups. */
|
|
2908
|
+
function tsStamp() {
|
|
2909
|
+
return new Date().toISOString().replace(/[:.]/g, "-");
|
|
2910
|
+
}
|
|
2911
|
+
|
|
2912
|
+
/** Does `dir` look like a collective-capable agent repo? */
|
|
2913
|
+
function isAgentRepo(dir) {
|
|
2914
|
+
return existsSync(join(dir, "config", "agent.json")) &&
|
|
2915
|
+
existsSync(join(dir, "scripts", "collective", "hook-runner.mjs"));
|
|
2916
|
+
}
|
|
2917
|
+
|
|
2918
|
+
/** Scan the home dir (depth 1) for collective-capable agent repos. */
|
|
2919
|
+
function discoverAgentRepos(home) {
|
|
2920
|
+
const out = [];
|
|
2921
|
+
let entries;
|
|
2922
|
+
try { entries = readdirSync(home, { withFileTypes: true }); } catch { return out; }
|
|
2923
|
+
for (const e of entries) {
|
|
2924
|
+
if (!e.isDirectory() || e.name.startsWith(".")) continue;
|
|
2925
|
+
const dir = join(home, e.name);
|
|
2926
|
+
if (isAgentRepo(dir)) out.push(dir);
|
|
2927
|
+
}
|
|
2928
|
+
return out;
|
|
2929
|
+
}
|
|
2930
|
+
|
|
2931
|
+
/**
|
|
2932
|
+
* Resolve the agent repo global-setup should wire. Order: explicit path arg →
|
|
2933
|
+
* cwd → resolveAgentRoot (env/walk/pointer) → unique home-dir scan. Returns the
|
|
2934
|
+
* path, or null if none / ambiguous (caller reports).
|
|
2935
|
+
*/
|
|
2936
|
+
async function resolveSetupRepo(args) {
|
|
2937
|
+
const argPath = args.find((a) => !a.startsWith("-"));
|
|
2938
|
+
if (argPath) { const p = resolve(argPath); return isAgentRepo(p) ? p : null; }
|
|
2939
|
+
if (existsSync(join(process.cwd(), "scripts/collective/hook-runner.mjs"))) return process.cwd();
|
|
2940
|
+
try {
|
|
2941
|
+
const { resolveAgentRoot } = await import("../lib/collective/config.mjs");
|
|
2942
|
+
const r = resolveAgentRoot(process.cwd());
|
|
2943
|
+
if (r && isAgentRepo(r)) return r;
|
|
2944
|
+
} catch { /* */ }
|
|
2945
|
+
const found = discoverAgentRepos(homedir());
|
|
2946
|
+
if (found.length === 1) return found[0];
|
|
2947
|
+
if (found.length > 1) { fail(`Multiple agent repos found: ${found.join(", ")}. Pass one: maestro global-setup <repo>`); return null; }
|
|
2948
|
+
return null;
|
|
2949
|
+
}
|
|
2950
|
+
|
|
2951
|
+
/**
|
|
2952
|
+
* global-setup — wire the Collective Memory & Org Mesh layer into the GLOBAL
|
|
2953
|
+
* Claude Code config (~/.claude) so EVERY session on this machine contributes to
|
|
2954
|
+
* and recalls from this agent's long-term memory, and is aware of concurrent
|
|
2955
|
+
* sessions. Idempotent + additive (never clobbers existing hooks/plugins/config;
|
|
2956
|
+
* backs up settings.json before writing). Run by init-agent.sh, by `wundr
|
|
2957
|
+
* install` (which defers to maestro), and standalone. See
|
|
2958
|
+
* docs/architecture/collective-memory-and-org-mesh.md.
|
|
2959
|
+
*/
|
|
2960
|
+
async function globalSetup(args = []) {
|
|
2961
|
+
const dryRun = args.includes("--dry-run") || args.includes("-n");
|
|
2962
|
+
console.log();
|
|
2963
|
+
log("Wiring collective memory into the global Claude Code config...");
|
|
2964
|
+
const cwd = await resolveSetupRepo(args);
|
|
2965
|
+
if (!cwd) {
|
|
2966
|
+
fail("Could not locate a Maestro agent repo to wire.");
|
|
2967
|
+
fail("Run from your agent repo, pass it explicitly (`maestro global-setup <repo>`),");
|
|
2968
|
+
fail("or run `npx @cohortapp/agent-sdk upgrade` in the agent repo first.");
|
|
2969
|
+
process.exit(1);
|
|
2970
|
+
}
|
|
2971
|
+
log(`Agent repo: ${cwd}`);
|
|
2972
|
+
const gc = await import("../lib/collective/global-config.mjs");
|
|
2973
|
+
const claudeDir = join(homedir(), ".claude");
|
|
2974
|
+
const settingsPath = join(claudeDir, "settings.json");
|
|
2975
|
+
const claudeMdPath = join(claudeDir, "CLAUDE.md");
|
|
2976
|
+
const pointerFile = join(claudeDir, "maestro-agent.json");
|
|
2977
|
+
if (!dryRun) mkdirSync(claudeDir, { recursive: true });
|
|
2978
|
+
|
|
2979
|
+
let agentName = "";
|
|
2980
|
+
try {
|
|
2981
|
+
const aj = JSON.parse(readFileSync(join(cwd, "config/agent.json"), "utf-8"));
|
|
2982
|
+
agentName = aj.firstName || aj.fullName || "";
|
|
2983
|
+
} catch { /* unconfigured is fine */ }
|
|
2984
|
+
|
|
2985
|
+
// 1) settings.json hooks (deep-merged, idempotent, backed up before write).
|
|
2986
|
+
let settings = {};
|
|
2987
|
+
if (existsSync(settingsPath)) {
|
|
2988
|
+
try { settings = JSON.parse(readFileSync(settingsPath, "utf-8")); } catch { settings = {}; }
|
|
2989
|
+
}
|
|
2990
|
+
const { hooks, added } = gc.mergeCollectiveHooks(settings.hooks || {}, cwd, process.execPath);
|
|
2991
|
+
if (added.length > 0) {
|
|
2992
|
+
settings.hooks = hooks;
|
|
2993
|
+
if (!dryRun) {
|
|
2994
|
+
if (existsSync(settingsPath)) {
|
|
2995
|
+
try { copyFileSync(settingsPath, `${settingsPath}.backup.${tsStamp()}`); } catch { /* */ }
|
|
2996
|
+
}
|
|
2997
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
2998
|
+
}
|
|
2999
|
+
ok(`${dryRun ? "[dry-run] " : ""}global hooks added: ${added.join(", ")}`);
|
|
3000
|
+
} else {
|
|
3001
|
+
ok("global hooks already present (no change)");
|
|
3002
|
+
}
|
|
3003
|
+
|
|
3004
|
+
// 2) global CLAUDE.md directive (only our delimited block is touched).
|
|
3005
|
+
let mdContent = "";
|
|
3006
|
+
if (existsSync(claudeMdPath)) { try { mdContent = readFileSync(claudeMdPath, "utf-8"); } catch { /* */ } }
|
|
3007
|
+
const md = gc.upsertClaudeMdBlock(mdContent, gc.buildCollectiveClaudeMd(cwd));
|
|
3008
|
+
if (md.changed) {
|
|
3009
|
+
if (!dryRun) writeFileSync(claudeMdPath, md.content);
|
|
3010
|
+
ok(`${dryRun ? "[dry-run] " : ""}global CLAUDE.md collective block written`);
|
|
3011
|
+
} else {
|
|
3012
|
+
ok("global CLAUDE.md collective block up to date");
|
|
3013
|
+
}
|
|
3014
|
+
|
|
3015
|
+
// 3) machine pointer (locates the agent for sessions in any repo).
|
|
3016
|
+
if (!dryRun) writeFileSync(pointerFile, JSON.stringify(gc.buildPointer(cwd, agentName), null, 2));
|
|
3017
|
+
ok(`${dryRun ? "[dry-run] " : ""}machine pointer → ${pointerFile}`);
|
|
3018
|
+
|
|
3019
|
+
console.log();
|
|
3020
|
+
if (dryRun) {
|
|
3021
|
+
log("Dry run complete — no files written.");
|
|
3022
|
+
} else {
|
|
3023
|
+
log("Collective memory wired. New Claude Code sessions on this machine now");
|
|
3024
|
+
log("contribute to + recall from this agent, and see concurrent sessions.");
|
|
3025
|
+
if (added.length > 0) log("Restart running interactive sessions to pick up the new hooks.");
|
|
3026
|
+
}
|
|
3027
|
+
}
|
|
3028
|
+
|
|
3029
|
+
// ---------------------------------------------------------------------------
|
|
3030
|
+
// AUDIT — registry-driven security-posture audit + attestation (G15)
|
|
3031
|
+
// ---------------------------------------------------------------------------
|
|
3032
|
+
//
|
|
3033
|
+
// `maestro audit` runs lib/security/audit-engine.mjs against the current agent
|
|
3034
|
+
// repo and prints a one-line-per-check summary (or JSON). It exits non-zero on
|
|
3035
|
+
// any unsuppressed failure, so it's CI-usable. `--fix` applies the safe,
|
|
3036
|
+
// declared auto-fixes first, then re-audits. `--attest` (implied by --json)
|
|
3037
|
+
// emits the hash-locked conformance artifact the org server aggregates.
|
|
3038
|
+
|
|
3039
|
+
async function auditCmd(args = []) {
|
|
3040
|
+
const flags = { json: false, fix: false };
|
|
3041
|
+
for (const a of args) {
|
|
3042
|
+
if (a === "--json") flags.json = true;
|
|
3043
|
+
else if (a === "--fix") flags.fix = true;
|
|
3044
|
+
else if (a === "--help" || a === "-h") {
|
|
3045
|
+
console.log(`
|
|
3046
|
+
Usage: maestro audit [--json] [--fix]
|
|
3047
|
+
|
|
3048
|
+
Runs the registry-driven security-posture audit (lib/security/audit-engine.mjs)
|
|
3049
|
+
against this agent repo. Exits non-zero on any unsuppressed failure.
|
|
3050
|
+
|
|
3051
|
+
Flags:
|
|
3052
|
+
--json Emit machine-readable JSON (results + hash-locked attestation)
|
|
3053
|
+
--fix Apply safe auto-fixes (e.g. chmod 600 .env, remove tracked .pyc),
|
|
3054
|
+
then re-audit. Non-fixable findings are left for you to resolve.
|
|
3055
|
+
|
|
3056
|
+
Suppressions: config/audit-suppressions.yaml — a list of { id, reason, until }.
|
|
3057
|
+
A suppression silences one check id, requires a reason, and expires at 'until'.
|
|
3058
|
+
`.trim());
|
|
3059
|
+
return;
|
|
3060
|
+
} else { fail(`Unknown flag: ${a}`); process.exit(1); }
|
|
3061
|
+
}
|
|
3062
|
+
|
|
3063
|
+
const cwd = process.cwd();
|
|
3064
|
+
if (!existsSync(join(cwd, "config/agent.json")) && !existsSync(join(cwd, "config/agent.ts")) && !existsSync(join(cwd, "CLAUDE.md"))) {
|
|
3065
|
+
if (flags.json) { console.log(JSON.stringify({ ok: false, error: "not a Maestro agent directory" })); }
|
|
3066
|
+
else { fail("Not a Maestro agent directory (config/agent.json not found)"); }
|
|
3067
|
+
process.exit(1);
|
|
3068
|
+
}
|
|
3069
|
+
|
|
3070
|
+
let audit;
|
|
3071
|
+
let fixed = [];
|
|
3072
|
+
if (flags.fix) {
|
|
3073
|
+
const res = await applyFixes(cwd);
|
|
3074
|
+
fixed = res.fixed;
|
|
3075
|
+
audit = res.audit;
|
|
3076
|
+
} else {
|
|
3077
|
+
audit = await runAudit(cwd);
|
|
3078
|
+
}
|
|
3079
|
+
const attestation = buildAttestation(audit, cwd);
|
|
3080
|
+
|
|
3081
|
+
if (flags.json) {
|
|
3082
|
+
console.log(JSON.stringify({ ok: audit.ok, summary: audit.summary, results: audit.results, suppressed: audit.suppressed, fixed, attestation }, null, 2));
|
|
3083
|
+
process.exit(audit.ok ? 0 : 1);
|
|
3084
|
+
}
|
|
3085
|
+
|
|
3086
|
+
console.log();
|
|
3087
|
+
console.log(`${C.bold}Security posture audit${C.reset}`);
|
|
3088
|
+
if (fixed.length) ok(`applied ${fixed.length} safe fix(es): ${fixed.join(", ")}`);
|
|
3089
|
+
for (const r of audit.results) {
|
|
3090
|
+
if (r.ok) { ok(`[${r.severity}] ${r.id} — ${r.detail}`); continue; }
|
|
3091
|
+
if (r.suppressed) {
|
|
3092
|
+
warn(`[${r.severity}] ${r.id} — SUPPRESSED (${r.suppressionReason}${r.suppressionUntil ? `, until ${r.suppressionUntil}` : ""})`);
|
|
3093
|
+
continue;
|
|
3094
|
+
}
|
|
3095
|
+
fail(`[${r.severity}] ${r.id} — ${r.detail}`);
|
|
3096
|
+
if (r.fix) console.log(` fix: ${r.fix}${r.fixable ? " (or: maestro audit --fix)" : ""}`);
|
|
3097
|
+
}
|
|
3098
|
+
console.log();
|
|
3099
|
+
const s = audit.summary;
|
|
3100
|
+
console.log(`=== ${s.passed}/${s.total} checks passed${s.suppressed ? `, ${s.suppressed} suppressed` : ""}${s.failed ? `, ${s.failed} FAILED` : ""} ===`);
|
|
3101
|
+
console.log(`attestation: ${attestation.hash.slice(0, 16)}… (archetype: ${attestation.identity.function || "?"}/${attestation.identity.altitude || "?"})`);
|
|
3102
|
+
if (audit.ok) ok("Posture conformant — no unsuppressed failures.");
|
|
3103
|
+
else fail(`Posture has ${s.failed} unsuppressed failure(s)${s.worstSeverity ? ` (worst: ${s.worstSeverity})` : ""}.`);
|
|
3104
|
+
process.exit(audit.ok ? 0 : 1);
|
|
3105
|
+
}
|
|
3106
|
+
|
|
3107
|
+
// ---------------------------------------------------------------------------
|
|
3108
|
+
// SECRETS — secret lifecycle (gaps-enterprise-ops G12)
|
|
3109
|
+
// ---------------------------------------------------------------------------
|
|
3110
|
+
//
|
|
3111
|
+
// `maestro secrets sync` Pull org secrets from the broker into the
|
|
3112
|
+
// local encrypted store (cached, offline).
|
|
3113
|
+
// `maestro secrets list` Print secret NAMES only (never values).
|
|
3114
|
+
// `maestro secrets rotate --name X` Rotate X; the new value is read from stdin
|
|
3115
|
+
// so it never lands in shell history / argv.
|
|
3116
|
+
//
|
|
3117
|
+
// Config lives in config/secrets.yaml. The broker fetch is the runtime global
|
|
3118
|
+
// (real network) here — tests exercise lib/secrets directly with an injected
|
|
3119
|
+
// fetch, never through this command.
|
|
3120
|
+
|
|
3121
|
+
const SECRETS_HELP = `
|
|
3122
|
+
Usage: maestro secrets <list|sync|rotate> [flags]
|
|
3123
|
+
|
|
3124
|
+
list Print secret names in the local store (names only)
|
|
3125
|
+
sync [--name X ...] Pull org secrets from the broker into the local store
|
|
3126
|
+
(no --name → pull every name the broker lists)
|
|
3127
|
+
rotate --name X Rotate secret X; the new value is read from stdin
|
|
3128
|
+
|
|
3129
|
+
Config: config/secrets.yaml (provider: file|env|keychain, broker: { base_url, token_env })
|
|
3130
|
+
Audit: logs/audit/secrets.jsonl (names only — values are never logged)
|
|
3131
|
+
`.trim();
|
|
3132
|
+
|
|
3133
|
+
/** Load + parse config/secrets.yaml (YAML or JSON), or null when absent. */
|
|
3134
|
+
async function loadSecretsConfig(cwd) {
|
|
3135
|
+
const path = join(cwd, "config/secrets.yaml");
|
|
3136
|
+
if (!existsSync(path)) return null;
|
|
3137
|
+
const text = readFileSync(path, "utf-8");
|
|
3138
|
+
try {
|
|
3139
|
+
const y = await import("js-yaml");
|
|
3140
|
+
return y.load(text) || null;
|
|
3141
|
+
} catch {
|
|
3142
|
+
// js-yaml unavailable — accept a JSON config as a graceful fallback.
|
|
3143
|
+
try { return JSON.parse(text); } catch { return null; }
|
|
3144
|
+
}
|
|
3145
|
+
}
|
|
3146
|
+
|
|
3147
|
+
/** Read a value from stdin (for `rotate`, so secrets never touch argv). */
|
|
3148
|
+
function readStdin() {
|
|
3149
|
+
try {
|
|
3150
|
+
// 1 MiB cap is plenty for a secret; read the whole fd 0.
|
|
3151
|
+
const buf = readFileSync(0, "utf-8");
|
|
3152
|
+
return buf.replace(/\r?\n$/, "");
|
|
3153
|
+
} catch {
|
|
3154
|
+
return "";
|
|
3155
|
+
}
|
|
3156
|
+
}
|
|
3157
|
+
|
|
3158
|
+
async function secretsCmd(args = []) {
|
|
3159
|
+
const sub = args[0];
|
|
3160
|
+
if (!sub || sub === "--help" || sub === "-h") {
|
|
3161
|
+
console.log(SECRETS_HELP);
|
|
3162
|
+
return;
|
|
3163
|
+
}
|
|
3164
|
+
const cwd = process.cwd();
|
|
3165
|
+
if (!existsSync(join(cwd, "config/agent.json")) && !existsSync(join(cwd, "config/agent.ts")) && !existsSync(join(cwd, "CLAUDE.md"))) {
|
|
3166
|
+
fail("Not a Maestro agent directory (config/agent.json not found)");
|
|
3167
|
+
process.exit(1);
|
|
3168
|
+
}
|
|
3169
|
+
|
|
3170
|
+
// Parse --name (repeatable) for sync/rotate.
|
|
3171
|
+
const names = [];
|
|
3172
|
+
for (let i = 1; i < args.length; i++) {
|
|
3173
|
+
if (args[i] === "--name" || args[i] === "-n") { names.push(args[++i]); }
|
|
3174
|
+
}
|
|
3175
|
+
|
|
3176
|
+
const cfg = await loadSecretsConfig(cwd);
|
|
3177
|
+
|
|
3178
|
+
if (sub === "list") {
|
|
3179
|
+
const provider = selectProvider(cfg, { agentRoot: cwd, env: process.env });
|
|
3180
|
+
const list = await provider.list();
|
|
3181
|
+
if (list.length === 0) {
|
|
3182
|
+
log(`No secrets in the local store (provider: ${provider.name}).`);
|
|
3183
|
+
return;
|
|
3184
|
+
}
|
|
3185
|
+
log(`Secrets in the local store (provider: ${provider.name}) — names only:`);
|
|
3186
|
+
for (const n of list) console.log(` ${n}`);
|
|
3187
|
+
return;
|
|
3188
|
+
}
|
|
3189
|
+
|
|
3190
|
+
if (sub === "sync") {
|
|
3191
|
+
const broker = makeBroker(cfg, { fetch: globalThis.fetch });
|
|
3192
|
+
if (!broker) {
|
|
3193
|
+
fail("No broker configured — set config/secrets.yaml: broker.base_url + token_env.");
|
|
3194
|
+
process.exitCode = 1;
|
|
3195
|
+
return;
|
|
3196
|
+
}
|
|
3197
|
+
let toPull = names.filter(Boolean);
|
|
3198
|
+
if (toPull.length === 0) {
|
|
3199
|
+
// No explicit names → pull everything the broker advertises.
|
|
3200
|
+
toPull = await broker.list();
|
|
3201
|
+
if (toPull.length === 0) {
|
|
3202
|
+
warn("Broker returned no secret names to sync.");
|
|
3203
|
+
return;
|
|
3204
|
+
}
|
|
3205
|
+
}
|
|
3206
|
+
let res;
|
|
3207
|
+
try {
|
|
3208
|
+
res = await syncSecrets({ cfg, agentRoot: cwd, env: process.env, fetch: globalThis.fetch, names: toPull });
|
|
3209
|
+
} catch (err) {
|
|
3210
|
+
fail(`secrets sync failed: ${err.message}`);
|
|
3211
|
+
process.exitCode = 1;
|
|
3212
|
+
return;
|
|
3213
|
+
}
|
|
3214
|
+
if (res.synced.length) ok(`Synced ${res.synced.length} secret(s): ${res.synced.join(", ")}`);
|
|
3215
|
+
if (res.missing.length) warn(`Broker had no value for: ${res.missing.join(", ")}`);
|
|
3216
|
+
for (const f of res.failed) fail(`Failed to store ${f.name}: ${f.error}`);
|
|
3217
|
+
process.exitCode = res.failed.length ? 1 : 0;
|
|
3218
|
+
return;
|
|
3219
|
+
}
|
|
3220
|
+
|
|
3221
|
+
if (sub === "rotate") {
|
|
3222
|
+
const name = names.filter(Boolean)[0];
|
|
3223
|
+
if (!name) {
|
|
3224
|
+
fail("usage: maestro secrets rotate --name <SECRET_NAME> (new value on stdin)");
|
|
3225
|
+
process.exitCode = 1;
|
|
3226
|
+
return;
|
|
3227
|
+
}
|
|
3228
|
+
const value = readStdin();
|
|
3229
|
+
if (!value) {
|
|
3230
|
+
fail("No value on stdin. Pipe the new value: echo -n 'newval' | maestro secrets rotate --name X");
|
|
3231
|
+
process.exitCode = 1;
|
|
3232
|
+
return;
|
|
3233
|
+
}
|
|
3234
|
+
try {
|
|
3235
|
+
const res = await rotateSecret(name, value, { cfg, agentRoot: cwd, env: process.env, fetch: globalThis.fetch });
|
|
3236
|
+
ok(`Rotated "${res.name}" on ${res.provider}${res.mirrored ? " (mirrored locally)" : ""}.`);
|
|
3237
|
+
log(`Audit: ${relative(cwd, auditLogPath(cwd))} (names only)`);
|
|
3238
|
+
} catch (err) {
|
|
3239
|
+
fail(`Rotation failed: ${err.message}`);
|
|
3240
|
+
process.exitCode = 1;
|
|
3241
|
+
return;
|
|
3242
|
+
}
|
|
3243
|
+
return;
|
|
3244
|
+
}
|
|
3245
|
+
|
|
3246
|
+
fail(`Unknown secrets subcommand: ${sub}`);
|
|
3247
|
+
console.log(SECRETS_HELP);
|
|
3248
|
+
process.exitCode = 1;
|
|
3249
|
+
}
|
|
3250
|
+
|
|
3251
|
+
// ---------------------------------------------------------------------------
|
|
3252
|
+
// SYNC — re-pull this agent's record from Cohort and update config/agent.json
|
|
3253
|
+
// ---------------------------------------------------------------------------
|
|
3254
|
+
//
|
|
3255
|
+
// The seamless-UPDATE path that mirrors the Cohort-driven setup pull: when
|
|
3256
|
+
// COHORT_API_KEY + COHORT_ORG_ID (optionally COHORT_AGENT_ID) are present it
|
|
3257
|
+
// re-pulls the MemberProfileDTO, maps it to the config/agent.json subset, merges
|
|
3258
|
+
// it (never clobbering a locally-customised value unless --force), regenerates
|
|
3259
|
+
// the derived artefacts, and prints a diff of what changed. Fail-open + idempotent.
|
|
3260
|
+
|
|
3261
|
+
const SYNC_HELP = `
|
|
3262
|
+
Usage: maestro sync [flags]
|
|
3263
|
+
|
|
3264
|
+
Re-pull this agent's record from Cohort (the org single source of truth) and
|
|
3265
|
+
update config/agent.json. Requires COHORT_API_KEY + COHORT_ORG_ID in the env
|
|
3266
|
+
(optionally COHORT_AGENT_ID to select a specific member; otherwise whoami).
|
|
3267
|
+
|
|
3268
|
+
Flags:
|
|
3269
|
+
--force Overwrite locally-customised values too (default: protect them)
|
|
3270
|
+
--dry-run Show the diff without writing config/agent.json
|
|
3271
|
+
--help, -h Show this help
|
|
3272
|
+
`;
|
|
3273
|
+
|
|
3274
|
+
function parseSyncFlags(args) {
|
|
3275
|
+
const flags = { force: false, dryRun: false };
|
|
3276
|
+
for (const a of args) {
|
|
3277
|
+
if (a === "--force") flags.force = true;
|
|
3278
|
+
else if (a === "--dry-run" || a === "-n") flags.dryRun = true;
|
|
3279
|
+
else if (a === "--help" || a === "-h") return null;
|
|
3280
|
+
else { fail(`Unknown sync flag: ${a}`); process.exit(1); }
|
|
3281
|
+
}
|
|
3282
|
+
return flags;
|
|
3283
|
+
}
|
|
3284
|
+
|
|
3285
|
+
/** Render one diff row's value compactly for the CLI report. */
|
|
3286
|
+
function fmtSyncVal(v) {
|
|
3287
|
+
if (v === undefined) return "(unset)";
|
|
3288
|
+
if (Array.isArray(v)) return `[${v.length} item${v.length === 1 ? "" : "s"}]`;
|
|
3289
|
+
if (v && typeof v === "object") return JSON.stringify(v);
|
|
3290
|
+
const s = String(v);
|
|
3291
|
+
return s.length > 60 ? `${s.slice(0, 57)}…` : s;
|
|
3292
|
+
}
|
|
3293
|
+
|
|
3294
|
+
async function syncCmd(args = []) {
|
|
3295
|
+
const flags = parseSyncFlags(args);
|
|
3296
|
+
if (flags === null) { console.log(SYNC_HELP); return; }
|
|
3297
|
+
const cwd = process.cwd();
|
|
3298
|
+
|
|
3299
|
+
if (!existsSync(join(cwd, "config/agent.json")) && !existsSync(join(cwd, "config/agent.ts"))) {
|
|
3300
|
+
fail("Not a Maestro agent directory (config/agent.json not found)");
|
|
3301
|
+
process.exit(1);
|
|
3302
|
+
}
|
|
3303
|
+
|
|
3304
|
+
const { cohortPullEnv, pullAndPlan } = await import(join(MAESTRO_ROOT, "lib", "setup", "enroll-from-cohort.mjs"));
|
|
3305
|
+
const { fetchSelfProfile } = await import(join(MAESTRO_ROOT, "lib", "org", "client.mjs"));
|
|
3306
|
+
|
|
3307
|
+
const e = cohortPullEnv();
|
|
3308
|
+
if (!e.enabled) {
|
|
3309
|
+
fail("Cohort pull not configured — set COHORT_API_KEY and COHORT_ORG_ID.");
|
|
3310
|
+
process.exit(1);
|
|
3311
|
+
}
|
|
3312
|
+
|
|
3313
|
+
// Test seam (sandbox/offline): MAESTRO_SYNC_FETCH_SELF_PROFILE points at an ESM
|
|
3314
|
+
// module default-exporting a fetchSelfProfile-shaped fn, so the sync path can be
|
|
3315
|
+
// exercised end-to-end without a network round-trip. Never used in production.
|
|
3316
|
+
let fetchSelf = fetchSelfProfile;
|
|
3317
|
+
if (process.env.MAESTRO_SYNC_FETCH_SELF_PROFILE) {
|
|
3318
|
+
try {
|
|
3319
|
+
const mod = await import(pathToFileURL(process.env.MAESTRO_SYNC_FETCH_SELF_PROFILE).href);
|
|
3320
|
+
if (typeof (mod.default || mod.fetchSelfProfile) === "function") fetchSelf = mod.default || mod.fetchSelfProfile;
|
|
3321
|
+
} catch { /* fall back to the real client */ }
|
|
3322
|
+
}
|
|
3323
|
+
|
|
3324
|
+
log(`Re-pulling profile from Cohort (${e.agentId ? `member ${e.agentId}` : "whoami"})…`);
|
|
3325
|
+
const plan = await pullAndPlan({ agentRoot: cwd, force: flags.force, fetchSelfProfileImpl: fetchSelf });
|
|
3326
|
+
if (!plan.pulled) {
|
|
3327
|
+
warn(`No profile pulled (${plan.reason || "unknown"}). config/agent.json unchanged.`);
|
|
3328
|
+
return;
|
|
3329
|
+
}
|
|
3330
|
+
|
|
3331
|
+
if (!plan.changes.length) {
|
|
3332
|
+
ok("Already in sync with Cohort — no changes.");
|
|
3333
|
+
return;
|
|
3334
|
+
}
|
|
3335
|
+
|
|
3336
|
+
console.log();
|
|
3337
|
+
console.log(`${C.bold}Changes from Cohort${flags.dryRun ? " (dry run)" : ""}:${C.reset}`);
|
|
3338
|
+
for (const ch of plan.changes) {
|
|
3339
|
+
console.log(` ${ch.path}: ${C.yellow}${fmtSyncVal(ch.from)}${C.reset} → ${C.green}${fmtSyncVal(ch.to)}${C.reset}`);
|
|
3340
|
+
}
|
|
3341
|
+
console.log();
|
|
3342
|
+
|
|
3343
|
+
if (flags.dryRun) {
|
|
3344
|
+
log("Dry run — config/agent.json not modified. Re-run without --dry-run to apply.");
|
|
3345
|
+
return;
|
|
3346
|
+
}
|
|
3347
|
+
|
|
3348
|
+
const { mergeAgentJson, ensureSotWrapper } = await import(join(MAESTRO_ROOT, "lib", "setup", "sot.mjs"));
|
|
3349
|
+
mergeAgentJson(cwd, plan.patch);
|
|
3350
|
+
ok(`Updated config/agent.json (${plan.changes.length} field${plan.changes.length === 1 ? "" : "s"}).`);
|
|
3351
|
+
|
|
3352
|
+
// Regenerate the derived artefacts so the archetype/wrapper/env reflect the pull.
|
|
3353
|
+
try {
|
|
3354
|
+
const { runGenerator } = await import(join(MAESTRO_ROOT, "lib", "setup", "run-generator.mjs"));
|
|
3355
|
+
runGenerator("init-archetype.mjs", cwd, { maestroRoot: MAESTRO_ROOT });
|
|
3356
|
+
ensureSotWrapper(cwd);
|
|
3357
|
+
runGenerator("generate-agent-env.mjs", cwd, { maestroRoot: MAESTRO_ROOT });
|
|
3358
|
+
ok("Regenerated archetype, agent.ts wrapper, and agent.env.");
|
|
3359
|
+
} catch (err) {
|
|
3360
|
+
warn(`Derived-artefact regeneration skipped: ${err && err.message ? err.message : err}`);
|
|
3361
|
+
}
|
|
3362
|
+
|
|
3363
|
+
// Refresh the authoritative org context cache (config/org-context.json) so the
|
|
3364
|
+
// agent's situational awareness tracks the org. Fail-open + non-blocking.
|
|
3365
|
+
try {
|
|
3366
|
+
const { bootstrapOrgContextInto } = await import(join(MAESTRO_ROOT, "lib", "setup", "sections", "org.mjs"));
|
|
3367
|
+
const res = await bootstrapOrgContextInto(cwd, {});
|
|
3368
|
+
if (res && res.path) ok("Refreshed config/org-context.json from Cohort.");
|
|
3369
|
+
} catch { /* fail-open — local model stands */ }
|
|
3370
|
+
}
|
|
3371
|
+
|
|
3372
|
+
// ---------------------------------------------------------------------------
|
|
3373
|
+
// WHO-OWNS — situational awareness over the cached org context
|
|
3374
|
+
// ---------------------------------------------------------------------------
|
|
3375
|
+
//
|
|
3376
|
+
// The runtime consumer of lib/org/awareness.mjs: deterministic, ZERO-LLM answers
|
|
3377
|
+
// drawn from config/org-context.json (the Cohort bootstrap cache) — "who owns
|
|
3378
|
+
// this scope?", "who do I escalate to?", "who reports to me?", "which adopted
|
|
3379
|
+
// decisions touch this topic?". Fail-open: an un-enrolled agent (no cache) prints
|
|
3380
|
+
// a clear hint instead of erroring.
|
|
3381
|
+
|
|
3382
|
+
/**
|
|
3383
|
+
* Render the awareness answers for the current agent to stdout.
|
|
3384
|
+
* @param {string[]} args - [scope?] plus flags (--escalate, --reports, --decisions, --json)
|
|
3385
|
+
*/
|
|
3386
|
+
async function whoOwnsCmd(args = []) {
|
|
3387
|
+
const cwd = process.cwd();
|
|
3388
|
+
const flags = new Set(args.filter((a) => a.startsWith("--")));
|
|
3389
|
+
const positional = args.filter((a) => !a.startsWith("--"));
|
|
3390
|
+
const scope = positional.join(" ").trim();
|
|
3391
|
+
const asJson = flags.has("--json");
|
|
3392
|
+
|
|
3393
|
+
const { loadOrgContext } = await import(join(MAESTRO_ROOT, "lib", "org", "bootstrap-context.mjs"));
|
|
3394
|
+
const ctx = loadOrgContext(cwd);
|
|
3395
|
+
if (!ctx) {
|
|
3396
|
+
if (asJson) { console.log(JSON.stringify({ error: "no-org-context" })); return; }
|
|
3397
|
+
warn("No cached org context (config/org-context.json). Enroll + sync first:");
|
|
3398
|
+
warn(" maestro setup --only org # then maestro sync");
|
|
3399
|
+
return;
|
|
3400
|
+
}
|
|
3401
|
+
|
|
3402
|
+
const aw = await import(join(MAESTRO_ROOT, "lib", "org", "awareness.mjs"));
|
|
3403
|
+
const wantEscalate = flags.has("--escalate");
|
|
3404
|
+
const wantReports = flags.has("--reports");
|
|
3405
|
+
const wantDecisions = flags.has("--decisions");
|
|
3406
|
+
const wantOwner = !!scope || (!wantEscalate && !wantReports && !wantDecisions);
|
|
3407
|
+
|
|
3408
|
+
const result = {};
|
|
3409
|
+
if (wantOwner && scope) result.owner = aw.ownerOf(ctx, scope);
|
|
3410
|
+
if (wantEscalate || (!scope && !wantReports && !wantDecisions)) result.escalateTo = aw.escalateTo(ctx);
|
|
3411
|
+
if (wantReports) result.reports = aw.whoReportsToMe(ctx, ctx.profile && ctx.profile.slug);
|
|
3412
|
+
if (wantDecisions) result.decisions = aw.relevantDecisions(ctx, scope);
|
|
3413
|
+
|
|
3414
|
+
if (asJson) { console.log(JSON.stringify(result, null, 2)); return; }
|
|
3415
|
+
|
|
3416
|
+
console.log();
|
|
3417
|
+
if (scope && "owner" in result) {
|
|
3418
|
+
const o = result.owner;
|
|
3419
|
+
if (o) log(`Owner of "${scope}": ${C.bold}${o.owner.displayName || o.owner.slug}${C.reset} (${o.role || "assigned"}) — stream ${o.stream.name || o.stream.code || "?"}`);
|
|
3420
|
+
else warn(`No accountable owner found for "${scope}" in the org strategy.`);
|
|
3421
|
+
}
|
|
3422
|
+
if ("escalateTo" in result) {
|
|
3423
|
+
const e = result.escalateTo;
|
|
3424
|
+
if (e) log(`Escalate to: ${e.manager ? (e.manager.displayName || e.manager.slug) : "(no manager)"}${e.escalationPath ? ` — ${e.escalationPath}` : ""}`);
|
|
3425
|
+
else warn("No escalation target recorded in the org context.");
|
|
3426
|
+
}
|
|
3427
|
+
if (wantReports) {
|
|
3428
|
+
const rs = result.reports || [];
|
|
3429
|
+
if (rs.length) { log(`Reports (${rs.length}):`); for (const r of rs) ok(`${r.displayName || r.slug}${r.style ? ` [${r.style}]` : ""}`); }
|
|
3430
|
+
else warn("No direct reports recorded in the org context.");
|
|
3431
|
+
}
|
|
3432
|
+
if (wantDecisions) {
|
|
3433
|
+
const ds = result.decisions || [];
|
|
3434
|
+
if (ds.length) { log(`Adopted decisions${scope ? ` touching "${scope}"` : ""} (${ds.length}):`); for (const d of ds.slice(0, 12)) ok(d.title || d.summary || d.id || "(untitled)"); }
|
|
3435
|
+
else warn(`No adopted decisions${scope ? ` touching "${scope}"` : ""}.`);
|
|
3436
|
+
}
|
|
3437
|
+
}
|
|
3438
|
+
|
|
3439
|
+
// Pure, side-effect-free helpers exported for unit tests (bin/maestro.test.mjs).
|
|
3440
|
+
// They carry no I/O so importing this module to test them is safe; the CLI
|
|
3441
|
+
// dispatch below only runs when the file is the process entrypoint.
|
|
3442
|
+
export { evaluateCostTripwire, summariseLedgerRows, evaluateStaleness, parseSyncFlags, fmtSyncVal };
|
|
3443
|
+
|
|
3444
|
+
async function main() {
|
|
3445
|
+
const [, , command, ...args] = process.argv;
|
|
3446
|
+
|
|
3447
|
+
switch (command) {
|
|
3448
|
+
case "create": create(args[0]); break;
|
|
3449
|
+
case "setup": await setupCmd(args); break;
|
|
3450
|
+
case "pair": await pairCmd(args); break;
|
|
3451
|
+
case "sync": await syncCmd(args); break;
|
|
3452
|
+
case "upgrade": await upgrade(args); break;
|
|
3453
|
+
case "global-setup": await globalSetup(args); break;
|
|
3454
|
+
case "doctor": await doctor(); break;
|
|
3455
|
+
case "audit": await auditCmd(args); break;
|
|
3456
|
+
case "router": await routerCmd(args); break;
|
|
3457
|
+
case "secrets": await secretsCmd(args); break;
|
|
3458
|
+
case "who-owns": case "who": await whoOwnsCmd(args); break;
|
|
3459
|
+
case "init":
|
|
3460
|
+
case "update-init":
|
|
3461
|
+
case "init-update":
|
|
3462
|
+
await initCmd(args); break;
|
|
3463
|
+
case "--help": case "-h": case undefined:
|
|
3464
|
+
console.log(`
|
|
3465
|
+
Maestro — Autonomous AI Agent Operating System
|
|
3466
|
+
|
|
3467
|
+
Usage:
|
|
3468
|
+
npx @cohortapp/agent-sdk create <dirname> Create a new agent repo
|
|
3469
|
+
npx @cohortapp/agent-sdk setup [section] [flags] Configure this agent (deterministic, resumable wizard)
|
|
3470
|
+
npx @cohortapp/agent-sdk pair <code> Open a pre-auth pairing handshake (org admin approves it)
|
|
3471
|
+
npx @cohortapp/agent-sdk sync [--force] [--dry-run] Re-pull this agent's record from Cohort into config/agent.json
|
|
3472
|
+
npx @cohortapp/agent-sdk upgrade [--dry-run] Update framework files
|
|
3473
|
+
npx @cohortapp/agent-sdk init [feature] [--apply] Run pending feature-init steps
|
|
3474
|
+
npx @cohortapp/agent-sdk global-setup [--dry-run] Wire collective memory into ~/.claude (machine-wide)
|
|
3475
|
+
npx @cohortapp/agent-sdk doctor Verify installation
|
|
3476
|
+
npx @cohortapp/agent-sdk audit [--json] [--fix] Security-posture audit + hash-locked attestation
|
|
3477
|
+
npx @cohortapp/agent-sdk router why <item> Explain a routing decision (chain + tried + explain)
|
|
3478
|
+
npx @cohortapp/agent-sdk router validate Strict-validate config/model-routing.yaml
|
|
3479
|
+
npx @cohortapp/agent-sdk secrets list List local secret names (names only)
|
|
3480
|
+
npx @cohortapp/agent-sdk secrets sync [--name X] Pull org secrets from the broker into the local store
|
|
3481
|
+
npx @cohortapp/agent-sdk secrets rotate --name X Rotate a secret (new value read from stdin)
|
|
3482
|
+
npx @cohortapp/agent-sdk who-owns <scope> Who owns a scope / escalate-to / reports (org context, zero-LLM)
|
|
3483
|
+
|
|
3484
|
+
Upgrade flags:
|
|
3485
|
+
--dry-run, -n Preview changes without writing
|
|
3486
|
+
--force-overwrite Overwrite even locally-modified files (backs them up)
|
|
3487
|
+
--no-incoming Don't write .maestro/incoming/ shadows
|
|
3488
|
+
--verbose, -v List classification for every file
|
|
3489
|
+
|
|
3490
|
+
Init flags:
|
|
3491
|
+
(no args) List pending features (dry-run)
|
|
3492
|
+
--apply, -y Run every auto-runnable pending step
|
|
3493
|
+
<feature> --apply Run only the named feature's init step
|
|
3494
|
+
|
|
3495
|
+
Workflow:
|
|
3496
|
+
1. npx @cohortapp/agent-sdk create my-agent
|
|
3497
|
+
2. cd my-agent
|
|
3498
|
+
3. maestro setup
|
|
3499
|
+
|
|
3500
|
+
Upgrade workflow (existing agents):
|
|
3501
|
+
1. npm update @cohortapp/agent-sdk
|
|
3502
|
+
2. npm run upgrade (runs maestro upgrade + auto-init)
|
|
3503
|
+
3. maestro init (run any deferred init steps)
|
|
3504
|
+
`);
|
|
3505
|
+
break;
|
|
3506
|
+
default:
|
|
3507
|
+
fail(`Unknown command: ${command}`);
|
|
3508
|
+
process.exit(1);
|
|
3509
|
+
}
|
|
3510
|
+
}
|
|
3511
|
+
|
|
3512
|
+
// Dispatch only when invoked as the CLI entrypoint — importing for tests must
|
|
3513
|
+
// not run any command (process.argv would be the test runner's argv).
|
|
3514
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
3515
|
+
await main();
|
|
3516
|
+
}
|