@cohortapp/agent-sdk 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/init-agent.md +104 -0
- package/.claude/commands/init-maestro.md +1187 -0
- package/.claude/settings.json +161 -0
- package/.env.example +216 -0
- package/README.md +632 -0
- package/agents/browser-operator/agent.md +52 -0
- package/agents/calendar-ops/agent.md +50 -0
- package/agents/communications/agent.md +96 -0
- package/agents/decision-log/agent.md +65 -0
- package/agents/desktop-operator/agent.md +59 -0
- package/agents/gmail-operator/agent.md +62 -0
- package/agents/inbound-dispatcher/agent.md +66 -0
- package/agents/inbox-processor/agent.md +39 -0
- package/agents/pmo-execution/agent.md +60 -0
- package/agents/session-spawner/agent.md +64 -0
- package/agents/slack-operator/agent.md +60 -0
- package/agents/whatsapp-operator/agent.md +60 -0
- package/agents/workflow-automation/agent.md +61 -0
- package/archetypes/altitudes/c-suite.yaml +58 -0
- package/archetypes/altitudes/founder.yaml +68 -0
- package/archetypes/altitudes/senior-manager.yaml +63 -0
- package/archetypes/altitudes/svp.yaml +60 -0
- package/archetypes/altitudes/vp.yaml +50 -0
- package/archetypes/archetype.schema.json +77 -0
- package/archetypes/base.yaml +47 -0
- package/archetypes/capabilities/commercial-leader.yaml +159 -0
- package/archetypes/capabilities/compliance-officer.yaml +159 -0
- package/archetypes/capabilities/executive-operator.yaml +169 -0
- package/archetypes/capabilities/finance-leader.yaml +162 -0
- package/archetypes/capabilities/operations-leader.yaml +154 -0
- package/archetypes/capabilities/product-leader.yaml +148 -0
- package/archetypes/capabilities/technical-leader.yaml +146 -0
- package/archetypes/functions/commercial-leader.yaml +62 -0
- package/archetypes/functions/compliance-officer.yaml +64 -0
- package/archetypes/functions/executive-operator.yaml +70 -0
- package/archetypes/functions/finance-leader.yaml +70 -0
- package/archetypes/functions/operations-leader.yaml +62 -0
- package/archetypes/functions/product-leader.yaml +61 -0
- package/archetypes/functions/technical-leader.yaml +57 -0
- package/bin/cohort-mcp.mjs +81 -0
- package/bin/maestro.mjs +3516 -0
- package/bin/maestro.test.mjs +1015 -0
- package/desktop-control/README.md +56 -0
- package/desktop-control/app-profiles/gmail.yaml +120 -0
- package/desktop-control/app-profiles/slack.yaml +315 -0
- package/desktop-control/app-profiles/whatsapp.yaml +107 -0
- package/docs/architecture/agent-topology.md +2239 -0
- package/docs/architecture/archetype-agent-factory.md +110 -0
- package/docs/architecture/collective-memory-and-org-mesh.md +115 -0
- package/docs/architecture/continuous-monitoring.md +221 -0
- package/docs/architecture/mcp-capability-map.md +585 -0
- package/docs/architecture/system-architecture.md +1272 -0
- package/docs/company-context/README.md +40 -0
- package/docs/guides/agent-persona-setup.md +600 -0
- package/docs/guides/agents-observe-setup.md +64 -0
- package/docs/guides/billing-console-keys.md +88 -0
- package/docs/guides/ccxray-diagnostics.md +65 -0
- package/docs/guides/channel-bus.md +127 -0
- package/docs/guides/claude-mem-setup.md +79 -0
- package/docs/guides/claude-pace-setup.md +56 -0
- package/docs/guides/claudraband-sessions.md +98 -0
- package/docs/guides/clawteam-swarm.md +116 -0
- package/docs/guides/code-review-graph-setup.md +86 -0
- package/docs/guides/email-setup.md +431 -0
- package/docs/guides/mac-mini.md +119 -0
- package/docs/guides/media-generation-setup.md +349 -0
- package/docs/guides/model-routing.md +162 -0
- package/docs/guides/observability-otel.md +265 -0
- package/docs/guides/org-onboarding.md +132 -0
- package/docs/guides/outbound-governance-setup.md +437 -0
- package/docs/guides/pdf-generation-setup.md +315 -0
- package/docs/guides/poller-daemon-setup.md +563 -0
- package/docs/guides/rag-context-setup.md +459 -0
- package/docs/guides/self-optimization-pattern.md +82 -0
- package/docs/guides/setup-wizard.md +178 -0
- package/docs/guides/slack-setup.md +350 -0
- package/docs/guides/telegram-setup.md +227 -0
- package/docs/guides/twilio-subaccounts-setup.md +223 -0
- package/docs/guides/verification.md +128 -0
- package/docs/guides/voice-mode.md +188 -0
- package/docs/guides/voice-sms-setup.md +698 -0
- package/docs/guides/webhook-relay-setup.md +349 -0
- package/docs/guides/whatsapp-setup.md +288 -0
- package/docs/prompts/board-pack-cover-template.md +36 -0
- package/docs/prompts/decision-recommendation-template.md +88 -0
- package/docs/prompts/followup-message-template.md +141 -0
- package/docs/prompts/investor-letter-template.md +52 -0
- package/docs/prompts/morning-brief-template.md +82 -0
- package/docs/prompts/presentation-template.md +58 -0
- package/docs/prompts/weekly-strategic-memo-template.md +104 -0
- package/docs/research/hallucinated-tool-output-investigation.md +151 -0
- package/docs/runbooks/backup-restore.md +205 -0
- package/docs/runbooks/cohort-cutover.md +129 -0
- package/docs/runbooks/fleet-operations.md +200 -0
- package/docs/runbooks/incident-response.md +226 -0
- package/docs/runbooks/mac-mini-bootstrap.md +431 -0
- package/docs/runbooks/perpetual-operations.md +509 -0
- package/docs/runbooks/recovery-and-failover.md +260 -0
- package/framework-features.json +267 -0
- package/ingest/README.md +87 -0
- package/lib/action-executor.js +689 -0
- package/lib/action-executor.test.mjs +871 -0
- package/lib/agent-root.mjs +37 -0
- package/lib/archetype.mjs +236 -0
- package/lib/archetype.test.mjs +132 -0
- package/lib/autonomy.mjs +114 -0
- package/lib/autonomy.test.mjs +66 -0
- package/lib/backlog.mjs +358 -0
- package/lib/backlog.test.mjs +266 -0
- package/lib/budget-guard.mjs +279 -0
- package/lib/budget-guard.test.mjs +291 -0
- package/lib/cadence-bus-schedule.test.mjs +194 -0
- package/lib/cadence-bus.mjs +1120 -0
- package/lib/cadence-bus.test.mjs +720 -0
- package/lib/cadences.mjs +205 -0
- package/lib/cadences.test.mjs +125 -0
- package/lib/capability.mjs +154 -0
- package/lib/capability.test.mjs +78 -0
- package/lib/channels/base-adapter.mjs +719 -0
- package/lib/channels/base-adapter.test.mjs +590 -0
- package/lib/channels/channel.mjs +128 -0
- package/lib/channels/channels.test.mjs +371 -0
- package/lib/channels/contract.mjs +215 -0
- package/lib/channels/contract.test.mjs +137 -0
- package/lib/channels/conversation-resolver.mjs +95 -0
- package/lib/channels/gmail/adapter.mjs +87 -0
- package/lib/channels/inbox-item.mjs +255 -0
- package/lib/channels/inbox-item.test.mjs +335 -0
- package/lib/channels/index.mjs +94 -0
- package/lib/channels/orgmail/adapter.mjs +353 -0
- package/lib/channels/orgmail/adapter.test.mjs +311 -0
- package/lib/channels/pairing.mjs +363 -0
- package/lib/channels/pairing.test.mjs +270 -0
- package/lib/channels/registry.mjs +164 -0
- package/lib/channels/slack/adapter.mjs +317 -0
- package/lib/channels/slack-adapter.test.mjs +212 -0
- package/lib/channels/sms/adapter.mjs +43 -0
- package/lib/channels/telegram/adapter.mjs +432 -0
- package/lib/channels/telegram-adapter.test.mjs +306 -0
- package/lib/channels/voice/adapter.mjs +301 -0
- package/lib/channels/voice/adapter.test.mjs +278 -0
- package/lib/channels/whatsapp/adapter-baileys.mjs +587 -0
- package/lib/channels/whatsapp/adapter-baileys.test.mjs +359 -0
- package/lib/channels/whatsapp/adapter-twilio.mjs +65 -0
- package/lib/channels/whatsapp/baileys-typing.test.mjs +154 -0
- package/lib/charter.mjs +256 -0
- package/lib/charter.test.mjs +89 -0
- package/lib/claude-bin.mjs +134 -0
- package/lib/claude-bin.test.mjs +75 -0
- package/lib/collective/capture.mjs +185 -0
- package/lib/collective/capture.test.mjs +121 -0
- package/lib/collective/cards.mjs +201 -0
- package/lib/collective/cards.test.mjs +114 -0
- package/lib/collective/config.mjs +186 -0
- package/lib/collective/config.test.mjs +123 -0
- package/lib/collective/global-config.mjs +113 -0
- package/lib/collective/global-config.test.mjs +75 -0
- package/lib/collective/presence.mjs +201 -0
- package/lib/collective/presence.test.mjs +95 -0
- package/lib/collective/recall.mjs +215 -0
- package/lib/collective/recall.test.mjs +116 -0
- package/lib/comms/send-gate.mjs +554 -0
- package/lib/comms/send-gate.test.mjs +577 -0
- package/lib/comms.mjs +67 -0
- package/lib/comms.test.mjs +41 -0
- package/lib/diagnostics/alerts.mjs +424 -0
- package/lib/diagnostics/alerts.test.mjs +318 -0
- package/lib/diagnostics/backup-freshness.mjs +188 -0
- package/lib/diagnostics/backup-freshness.test.mjs +185 -0
- package/lib/diagnostics/counters.mjs +269 -0
- package/lib/diagnostics/counters.test.mjs +206 -0
- package/lib/diagnostics/events.mjs +188 -0
- package/lib/diagnostics/events.test.mjs +290 -0
- package/lib/diagnostics/otel.mjs +237 -0
- package/lib/diagnostics/otel.test.mjs +196 -0
- package/lib/diagnostics/trace.mjs +216 -0
- package/lib/diagnostics/trace.test.mjs +251 -0
- package/lib/env-compat.mjs +74 -0
- package/lib/env-compat.test.mjs +104 -0
- package/lib/feature-init.mjs +331 -0
- package/lib/fs-atomic.mjs +112 -0
- package/lib/fs-atomic.test.mjs +72 -0
- package/lib/fs-ownership.mjs +111 -0
- package/lib/fs-ownership.test.mjs +158 -0
- package/lib/hooks/bus.mjs +347 -0
- package/lib/hooks/bus.test.mjs +387 -0
- package/lib/index.js +16 -0
- package/lib/learning/config.mjs +106 -0
- package/lib/learning/config.test.mjs +75 -0
- package/lib/learning/counters.mjs +156 -0
- package/lib/learning/counters.test.mjs +69 -0
- package/lib/learning/curator-consolidate.test.mjs +238 -0
- package/lib/learning/curator.mjs +453 -0
- package/lib/learning/curator.test.mjs +106 -0
- package/lib/learning/log.mjs +40 -0
- package/lib/learning/reflect.mjs +534 -0
- package/lib/learning/reflect.test.mjs +0 -0
- package/lib/learning/session-index.mjs +352 -0
- package/lib/learning/session-index.test.mjs +125 -0
- package/lib/learning/skill-writer.mjs +474 -0
- package/lib/learning/skill-writer.test.mjs +210 -0
- package/lib/mcp/server.mjs +328 -0
- package/lib/mcp/server.test.mjs +400 -0
- package/lib/model-router/auth-profiles.mjs +758 -0
- package/lib/model-router/auth-profiles.test.mjs +580 -0
- package/lib/model-router/catalog/anthropic.yaml +153 -0
- package/lib/model-router/catalog/deepseek.yaml +86 -0
- package/lib/model-router/catalog/moonshot.yaml +81 -0
- package/lib/model-router/catalog/qwen.yaml +114 -0
- package/lib/model-router/catalog.mjs +925 -0
- package/lib/model-router/catalog.test.mjs +385 -0
- package/lib/model-router/economics.mjs +564 -0
- package/lib/model-router/economics.test.mjs +344 -0
- package/lib/model-router/failover.mjs +298 -0
- package/lib/model-router/failover.test.mjs +439 -0
- package/lib/model-router/health.mjs +453 -0
- package/lib/model-router/health.test.mjs +338 -0
- package/lib/model-router/integration-coverage.test.mjs +829 -0
- package/lib/model-router/integration.test.mjs +564 -0
- package/lib/model-router/ledger.mjs +402 -0
- package/lib/model-router/ledger.test.mjs +382 -0
- package/lib/model-router/llm-task.mjs +515 -0
- package/lib/model-router/llm-task.test.mjs +392 -0
- package/lib/model-router/org-credentials.mjs +260 -0
- package/lib/model-router/org-credentials.test.mjs +265 -0
- package/lib/model-router/pricing-refresh.mjs +463 -0
- package/lib/model-router/pricing-refresh.test.mjs +286 -0
- package/lib/model-router/reconcile.mjs +429 -0
- package/lib/model-router/reconcile.test.mjs +316 -0
- package/lib/model-router/repair.mjs +471 -0
- package/lib/model-router/repair.test.mjs +180 -0
- package/lib/model-router/resolve.mjs +1206 -0
- package/lib/model-router/spawn.mjs +497 -0
- package/lib/model-router/spawn.test.mjs +425 -0
- package/lib/model-router/taxonomy.mjs +893 -0
- package/lib/model-router/taxonomy.test.mjs +410 -0
- package/lib/model-router.mjs +677 -0
- package/lib/model-router.test.mjs +907 -0
- package/lib/org/activity.mjs +211 -0
- package/lib/org/activity.test.mjs +134 -0
- package/lib/org/approvals.mjs +448 -0
- package/lib/org/approvals.test.mjs +216 -0
- package/lib/org/awareness.mjs +222 -0
- package/lib/org/awareness.test.mjs +159 -0
- package/lib/org/board.mjs +229 -0
- package/lib/org/board.test.mjs +177 -0
- package/lib/org/bootstrap-context.mjs +169 -0
- package/lib/org/bootstrap-context.test.mjs +153 -0
- package/lib/org/client.mjs +1628 -0
- package/lib/org/client.test.mjs +1107 -0
- package/lib/org/cohort-client.mjs +67 -0
- package/lib/org/cohort-client.test.mjs +126 -0
- package/lib/org/cost-sync.mjs +227 -0
- package/lib/org/cost-sync.test.mjs +153 -0
- package/lib/org/doctor.mjs +212 -0
- package/lib/org/doctor.test.mjs +212 -0
- package/lib/org/handoff.mjs +293 -0
- package/lib/org/handoff.test.mjs +269 -0
- package/lib/org/integration-tools.mjs +182 -0
- package/lib/org/integration-tools.test.mjs +160 -0
- package/lib/org/keys.mjs +131 -0
- package/lib/org/keys.test.mjs +92 -0
- package/lib/org/knowledge.mjs +463 -0
- package/lib/org/knowledge.test.mjs +319 -0
- package/lib/org/leases.mjs +335 -0
- package/lib/org/leases.test.mjs +235 -0
- package/lib/org/mesh-integration.test.mjs +127 -0
- package/lib/org/mesh.mjs +459 -0
- package/lib/org/mesh.test.mjs +345 -0
- package/lib/org/messaging.mjs +503 -0
- package/lib/org/messaging.test.mjs +238 -0
- package/lib/org/policy.mjs +345 -0
- package/lib/org/policy.test.mjs +237 -0
- package/lib/org/protocol.checksum +1 -0
- package/lib/org/protocol.checksum.test.mjs +90 -0
- package/lib/org/protocol.mjs +967 -0
- package/lib/org/protocol.test.mjs +264 -0
- package/lib/org/registry.mjs +194 -0
- package/lib/org/registry.test.mjs +100 -0
- package/lib/org/tool-surface-integration.test.mjs +120 -0
- package/lib/org/tool-surface.mjs +2535 -0
- package/lib/org/tool-surface.test.mjs +589 -0
- package/lib/org/ui-parity.mjs +3236 -0
- package/lib/org/ui-parity.test.mjs +348 -0
- package/lib/org/verify.mjs +176 -0
- package/lib/org/verify.test.mjs +194 -0
- package/lib/rag/embed.mjs +188 -0
- package/lib/rag/indexer.mjs +425 -0
- package/lib/rag/rag.test.mjs +505 -0
- package/lib/rag/search.mjs +475 -0
- package/lib/rate-guard.mjs +246 -0
- package/lib/rate-guard.test.mjs +201 -0
- package/lib/render.mjs +112 -0
- package/lib/render.test.mjs +68 -0
- package/lib/resource-governor.mjs +297 -0
- package/lib/resource-governor.test.mjs +262 -0
- package/lib/scheduling/dynamic-jobs.mjs +675 -0
- package/lib/scheduling/dynamic-jobs.test.mjs +344 -0
- package/lib/scheduling/jitter.mjs +0 -0
- package/lib/scheduling/jitter.test.mjs +140 -0
- package/lib/secrets/broker.mjs +315 -0
- package/lib/secrets/broker.test.mjs +280 -0
- package/lib/secrets/providers.mjs +461 -0
- package/lib/secrets/providers.test.mjs +274 -0
- package/lib/security/audit-engine.mjs +684 -0
- package/lib/security/audit-engine.test.mjs +389 -0
- package/lib/security/coerce-args.mjs +552 -0
- package/lib/security/coerce-args.test.mjs +281 -0
- package/lib/security/dangerous-tools.mjs +97 -0
- package/lib/security/dangerous-tools.test.mjs +68 -0
- package/lib/security/external-content.mjs +145 -0
- package/lib/security/external-content.test.mjs +67 -0
- package/lib/security/redact.mjs +592 -0
- package/lib/security/redact.test.mjs +441 -0
- package/lib/security/secret-equal.mjs +73 -0
- package/lib/security/secret-equal.test.mjs +55 -0
- package/lib/session-permissions.mjs +101 -0
- package/lib/session-permissions.test.mjs +100 -0
- package/lib/setup/claude-probe.mjs +74 -0
- package/lib/setup/completeness.mjs +175 -0
- package/lib/setup/completeness.test.mjs +110 -0
- package/lib/setup/context-pack.mjs +173 -0
- package/lib/setup/context-pack.test.mjs +89 -0
- package/lib/setup/enrich.mjs +277 -0
- package/lib/setup/enrich.test.mjs +115 -0
- package/lib/setup/enroll-from-cohort.mjs +441 -0
- package/lib/setup/enroll-from-cohort.test.mjs +233 -0
- package/lib/setup/integration.test.mjs +162 -0
- package/lib/setup/io.mjs +360 -0
- package/lib/setup/io.test.mjs +77 -0
- package/lib/setup/run-generator.mjs +81 -0
- package/lib/setup/runner.mjs +244 -0
- package/lib/setup/runner.test.mjs +132 -0
- package/lib/setup/sections/comms.mjs +173 -0
- package/lib/setup/sections/company.mjs +120 -0
- package/lib/setup/sections/enrich.mjs +138 -0
- package/lib/setup/sections/identity.mjs +182 -0
- package/lib/setup/sections/identity.test.mjs +140 -0
- package/lib/setup/sections/learning.mjs +153 -0
- package/lib/setup/sections/learning.test.mjs +81 -0
- package/lib/setup/sections/messaging.mjs +219 -0
- package/lib/setup/sections/messaging.test.mjs +127 -0
- package/lib/setup/sections/model.mjs +102 -0
- package/lib/setup/sections/operating-model.mjs +78 -0
- package/lib/setup/sections/org.mjs +475 -0
- package/lib/setup/sections/org.test.mjs +313 -0
- package/lib/setup/sections/orgmail.mjs +173 -0
- package/lib/setup/sections/orgmail.test.mjs +118 -0
- package/lib/setup/sections/recovery.mjs +159 -0
- package/lib/setup/sections/recovery.test.mjs +98 -0
- package/lib/setup/sections/tools.mjs +132 -0
- package/lib/setup/sections/verify.mjs +97 -0
- package/lib/setup/sot.mjs +205 -0
- package/lib/setup/sot.test.mjs +81 -0
- package/lib/setup/state.mjs +151 -0
- package/lib/setup/state.test.mjs +92 -0
- package/lib/singleton.js +229 -0
- package/lib/singleton.test.mjs +135 -0
- package/lib/telemetry/alerts.mjs +216 -0
- package/lib/telemetry/alerts.test.mjs +109 -0
- package/lib/telemetry/collect.mjs +512 -0
- package/lib/telemetry/collect.test.mjs +202 -0
- package/lib/tool-definitions-integration.test.mjs +83 -0
- package/lib/tool-definitions.js +738 -0
- package/lib/tool-definitions.test.mjs +437 -0
- package/lib/util/fetch-timeout.mjs +136 -0
- package/lib/util/fetch-timeout.test.mjs +202 -0
- package/lib/util/reconnect.mjs +343 -0
- package/lib/util/reconnect.test.mjs +369 -0
- package/lib/util/unhandled.mjs +205 -0
- package/lib/util/unhandled.test.mjs +216 -0
- package/lib/voice/context-loader.mjs +466 -0
- package/lib/voice/index.mjs +100 -0
- package/lib/voice/openai-realtime.mjs +510 -0
- package/lib/voice/outbound.mjs +542 -0
- package/lib/voice/outbound.test.mjs +69 -0
- package/lib/voice/post-call-brief.mjs +428 -0
- package/lib/voice/provider.mjs +52 -0
- package/lib/voice/session-rotation.mjs +257 -0
- package/lib/voice/stt.mjs +161 -0
- package/lib/voice/stt.test.mjs +226 -0
- package/lib/voice/tool-bridge.mjs +370 -0
- package/lib/voice/tts.mjs +104 -0
- package/lib/voice/twilio-sip-bridge.mjs +288 -0
- package/lib/voice/voice.test.mjs +990 -0
- package/mcp/README.md +80 -0
- package/package.json +151 -0
- package/plugins/maestro-skills/plugin.json +139 -0
- package/plugins/maestro-skills/skills/agents-observe.md +110 -0
- package/plugins/maestro-skills/skills/board-deck.md +68 -0
- package/plugins/maestro-skills/skills/books-close.md +77 -0
- package/plugins/maestro-skills/skills/brand-steward.md +121 -0
- package/plugins/maestro-skills/skills/calendar-plan.md +57 -0
- package/plugins/maestro-skills/skills/call-working-sessions.md +124 -0
- package/plugins/maestro-skills/skills/ccxray-diagnostics.md +91 -0
- package/plugins/maestro-skills/skills/claude-pace.md +61 -0
- package/plugins/maestro-skills/skills/code-review-graph.md +99 -0
- package/plugins/maestro-skills/skills/crm-pipeline.md +65 -0
- package/plugins/maestro-skills/skills/decision-brief.md +89 -0
- package/plugins/maestro-skills/skills/directory-hygiene.md +125 -0
- package/plugins/maestro-skills/skills/draft-comms.md +84 -0
- package/plugins/maestro-skills/skills/evening-wrap.md +53 -0
- package/plugins/maestro-skills/skills/files-find.md +65 -0
- package/plugins/maestro-skills/skills/generative-ui.md +228 -0
- package/plugins/maestro-skills/skills/hiring-triage.md +74 -0
- package/plugins/maestro-skills/skills/inbox-triage.md +61 -0
- package/plugins/maestro-skills/skills/mail-triage.md +86 -0
- package/plugins/maestro-skills/skills/morning-brief.md +54 -0
- package/plugins/maestro-skills/skills/native-artifacts.md +157 -0
- package/plugins/maestro-skills/skills/org-board.md +133 -0
- package/plugins/maestro-skills/skills/org-credential.md +68 -0
- package/plugins/maestro-skills/skills/org-recall.md +81 -0
- package/plugins/maestro-skills/skills/pipeline-review.md +76 -0
- package/plugins/maestro-skills/skills/regulatory-status.md +81 -0
- package/plugins/maestro-skills/skills/router-why.md +78 -0
- package/plugins/maestro-skills/skills/schedule-meeting.md +91 -0
- package/plugins/maestro-skills/skills/session-search.md +71 -0
- package/plugins/maestro-skills/skills/set-reminder.md +93 -0
- package/plugins/maestro-skills/skills/slack-followup.md +64 -0
- package/plugins/maestro-skills/skills/team-activity.md +86 -0
- package/plugins/maestro-skills/skills/weekly-memo.md +70 -0
- package/policies/action-classification.yaml +114 -0
- package/policies/ai-disclosure.yaml +294 -0
- package/policies/communication-style.md +139 -0
- package/policies/information-barriers.yaml +118 -0
- package/policies/prompt-injection-defence.yaml +138 -0
- package/public/assets/icon-dark.png +0 -0
- package/public/assets/icon-dark.svg +9 -0
- package/public/assets/icon-light.svg +9 -0
- package/public/assets/logo-dark.svg +15 -0
- package/public/assets/logo-light.svg +15 -0
- package/scaffold/.mcp.json +7 -0
- package/scaffold/CLAUDE.md +368 -0
- package/scaffold/config/agent.json +55 -0
- package/scaffold/config/agent.ts +76 -0
- package/scaffold/config/agent.ts.example +89 -0
- package/scaffold/config/alerts.yaml +23 -0
- package/scaffold/config/allowlist.yaml.example +25 -0
- package/scaffold/config/caller-id-map.yaml +46 -0
- package/scaffold/config/collective.yaml +49 -0
- package/scaffold/config/company.json +20 -0
- package/scaffold/config/known-agents.json +6 -0
- package/scaffold/config/learning.yaml +55 -0
- package/scaffold/config/model-routing.yaml.example +104 -0
- package/scaffold/config/org.yaml +25 -0
- package/scaffold/config/orgmail.yaml.example +19 -0
- package/scaffold/config/recovery.yaml +72 -0
- package/scaffold/config/secrets.yaml +27 -0
- package/scaffold/config/slack.yaml.example +35 -0
- package/scaffold/config/telegram.yaml.example +38 -0
- package/scaffold/config/voice.yaml.example +89 -0
- package/scaffold/config/whatsapp.yaml.example +39 -0
- package/schedules/README.md +49 -0
- package/schedules/triggers/backlog-executor.md +102 -0
- package/schedules/triggers/brand-steward.md +72 -0
- package/schedules/triggers/daily-evening-wrap.md +159 -0
- package/schedules/triggers/daily-midday-sweep.md +58 -0
- package/schedules/triggers/daily-morning-brief.md +55 -0
- package/schedules/triggers/directory-hygiene.md +81 -0
- package/schedules/triggers/dynamic-jobs.md +40 -0
- package/schedules/triggers/inbox-processor.md +115 -0
- package/schedules/triggers/meeting-action-capture.md +60 -0
- package/schedules/triggers/meeting-prep.md +69 -0
- package/schedules/triggers/messaging-inbound.md +50 -0
- package/schedules/triggers/org-pulse.md +24 -0
- package/schedules/triggers/quarterly-self-assessment.md +54 -0
- package/schedules/triggers/weekly-engineering-health.md +37 -0
- package/schedules/triggers/weekly-execution.md +65 -0
- package/schedules/triggers/weekly-hiring.md +53 -0
- package/schedules/triggers/weekly-priorities.md +38 -0
- package/schedules/triggers/weekly-strategic-memo.md +124 -0
- package/scripts/archive-email.sh +55 -0
- package/scripts/cadence/cadence-status.mjs +36 -0
- package/scripts/cadence/enqueue-cadence-tick.mjs +174 -0
- package/scripts/cadence/enqueue-cadence-tick.test.mjs +187 -0
- package/scripts/cadence/launchd-cadence-wrapper.sh +85 -0
- package/scripts/cadence/launchd-cloud-relay-wrapper.sh +95 -0
- package/scripts/cadence/launchd-socket-mode-wrapper.sh +95 -0
- package/scripts/ci/check-docs-accuracy.mjs +493 -0
- package/scripts/ci/check-docs-accuracy.test.mjs +409 -0
- package/scripts/ci/check-exports-exist.mjs +140 -0
- package/scripts/ci/check-files-exist.mjs +107 -0
- package/scripts/ci/check-no-build-artifacts.mjs +111 -0
- package/scripts/ci/check-no-build-artifacts.test.mjs +71 -0
- package/scripts/ci/check-no-confidential.mjs +198 -0
- package/scripts/ci/check-no-conflict-markers.mjs +169 -0
- package/scripts/ci/check-no-residual-identity.mjs +163 -0
- package/scripts/ci/check-no-residual-identity.test.mjs +89 -0
- package/scripts/ci/check-tarball-fidelity.mjs +205 -0
- package/scripts/ci/check-unresolved-tokens.mjs +83 -0
- package/scripts/ci/check.mjs +109 -0
- package/scripts/ci/check.test.mjs +194 -0
- package/scripts/ci/run-coverage.mjs +82 -0
- package/scripts/ci/run-tests.mjs +71 -0
- package/scripts/cloud-relay/README.md +59 -0
- package/scripts/cloud-relay/index.mjs +233 -0
- package/scripts/cloud-relay/package.json +15 -0
- package/scripts/cloud-relay/railway.json +13 -0
- package/scripts/cloud-relay/voice/README.md +94 -0
- package/scripts/cloud-relay/voice/package-lock.json +39 -0
- package/scripts/cloud-relay/voice/package.json +16 -0
- package/scripts/cloud-relay/voice/railway.json +13 -0
- package/scripts/cloud-relay/voice/server.mjs +532 -0
- package/scripts/collective/hook-runner.mjs +211 -0
- package/scripts/collective/hook-runner.test.mjs +90 -0
- package/scripts/collective/org-pulse.mjs +72 -0
- package/scripts/collective/org-sync.mjs +61 -0
- package/scripts/collective/recall.mjs +45 -0
- package/scripts/collective/who.mjs +30 -0
- package/scripts/comms-monitor.sh +288 -0
- package/scripts/configure-whatsapp-sandbox.sh +201 -0
- package/scripts/continuous-monitor.sh +91 -0
- package/scripts/cost/fleet-digest.mjs +407 -0
- package/scripts/cost/fleet-digest.test.mjs +207 -0
- package/scripts/cost/track-claude-usage.mjs +169 -0
- package/scripts/daemon/agent-daemon.mjs +989 -0
- package/scripts/daemon/agent-daemon.test.mjs +525 -0
- package/scripts/daemon/cadence-consumer-governance.test.mjs +220 -0
- package/scripts/daemon/cadence-consumer.mjs +1080 -0
- package/scripts/daemon/cadence-consumer.test.mjs +770 -0
- package/scripts/daemon/cadence-handlers.mjs +1121 -0
- package/scripts/daemon/cadence-handlers.test.mjs +617 -0
- package/scripts/daemon/classifier.mjs +704 -0
- package/scripts/daemon/classifier.test.mjs +238 -0
- package/scripts/daemon/classify-kind.mjs +54 -0
- package/scripts/daemon/classify-kind.test.mjs +40 -0
- package/scripts/daemon/context-compiler.mjs +605 -0
- package/scripts/daemon/context-compiler.test.mjs +300 -0
- package/scripts/daemon/dispatcher-cooldown.test.mjs +122 -0
- package/scripts/daemon/dispatcher-governance.test.mjs +886 -0
- package/scripts/daemon/dispatcher.mjs +1516 -0
- package/scripts/daemon/health.mjs +72 -0
- package/scripts/daemon/inbox-deferral.mjs +210 -0
- package/scripts/daemon/inbox-deferral.test.mjs +242 -0
- package/scripts/daemon/integration.test.mjs +149 -0
- package/scripts/daemon/launchd-wrapper-generic.sh +96 -0
- package/scripts/daemon/launchd-wrapper-slack-events.sh +37 -0
- package/scripts/daemon/launchd-wrapper.sh +91 -0
- package/scripts/daemon/lib/session-router.mjs +274 -0
- package/scripts/daemon/lib/session-router.test.mjs +295 -0
- package/scripts/daemon/maestro-daemon.mjs +275 -0
- package/scripts/daemon/prompt-builder.mjs +685 -0
- package/scripts/daemon/prompt-builder.test.mjs +213 -0
- package/scripts/daemon/responder.mjs +854 -0
- package/scripts/daemon/session-lock.mjs +721 -0
- package/scripts/daemon/session-lock.test.mjs +252 -0
- package/scripts/daemon/session-outcomes.mjs +640 -0
- package/scripts/daemon/session-outcomes.test.mjs +533 -0
- package/scripts/daemon/typing-registry.mjs +90 -0
- package/scripts/daemon/typing-registry.test.mjs +77 -0
- package/scripts/daemon/voice-webhook-server.mjs +804 -0
- package/scripts/decisions/capture-decision.mjs +116 -0
- package/scripts/disclosure_assessment.py +873 -0
- package/scripts/disclosure_boundaries.py +562 -0
- package/scripts/email-signature-principal.html +52 -0
- package/scripts/email-signature.html +60 -0
- package/scripts/email_quote_thread.py +167 -0
- package/scripts/email_thread_dedup.py +362 -0
- package/scripts/emergency-stop.sh +81 -0
- package/scripts/healthcheck.sh +116 -0
- package/scripts/hooks/block-mcp-cohort-send.sh +15 -0
- package/scripts/hooks/block-mcp-slack-send.sh +7 -0
- package/scripts/hooks/post-action-log.sh +126 -0
- package/scripts/hooks/pre-send-audit.sh +174 -0
- package/scripts/hooks/pre-send-audit.test.mjs +215 -0
- package/scripts/hooks/session-end-log.sh +27 -0
- package/scripts/hooks/session-start-banner.sh +115 -0
- package/scripts/huddle/audio-bridge.mjs +664 -0
- package/scripts/huddle/boot-slack-cdp.sh +102 -0
- package/scripts/huddle/huddle-controller.mjs +942 -0
- package/scripts/huddle/huddle-server.mjs +1229 -0
- package/scripts/huddle/launch-slack.sh +232 -0
- package/scripts/huddle/openai-realtime-bridge.mjs +462 -0
- package/scripts/huddle/package-lock.json +62 -0
- package/scripts/huddle/package.json +22 -0
- package/scripts/huddle/setup-audio.sh +239 -0
- package/scripts/huddle/start-call.mjs +318 -0
- package/scripts/huddle/test-pipeline.mjs +263 -0
- package/scripts/learning/consolidate-skills.mjs +72 -0
- package/scripts/learning/session-search.mjs +125 -0
- package/scripts/llm_email_dedup.py +442 -0
- package/scripts/local-triggers/generate-plists.sh +432 -0
- package/scripts/local-triggers/generate-plists.test.mjs +413 -0
- package/scripts/local-triggers/install-all.sh +49 -0
- package/scripts/local-triggers/plists/.gitkeep +0 -0
- package/scripts/local-triggers/run-trigger.sh +63 -0
- package/scripts/local-triggers/templates/rag-reindex.plist.template +47 -0
- package/scripts/local-triggers/templates/voice-relay-poller.plist.template +54 -0
- package/scripts/local-triggers/templates/voice-tunnel.plist.template +55 -0
- package/scripts/local-triggers/templates/voice-webhook.plist.template +51 -0
- package/scripts/maintenance/backup-to-cloud.sh +124 -0
- package/scripts/maintenance/health-check.sh +377 -0
- package/scripts/media-generation/README.md +105 -0
- package/scripts/media-generation/gemini-image-client.mjs +173 -0
- package/scripts/media-generation/generate-assets.mjs +289 -0
- package/scripts/media-generation/veo-video-client.mjs +219 -0
- package/scripts/org/send-orgmail.mjs +227 -0
- package/scripts/outbound-dedup-cleanup.sh +43 -0
- package/scripts/outbound-dedup.sh +477 -0
- package/scripts/outbound_dedup.py +115 -0
- package/scripts/parse-voice-transcript.mjs +481 -0
- package/scripts/pdf-generation/README.md +63 -0
- package/scripts/pdf-generation/build-document.mjs +247 -0
- package/scripts/pdf-generation/templates/board-pack.latex +136 -0
- package/scripts/pdf-generation/templates/corporate-letter.latex +126 -0
- package/scripts/pdf-generation/templates/memo.latex +114 -0
- package/scripts/poll-slack-events.sh +35 -0
- package/scripts/poller/calendar-poller.mjs +12 -0
- package/scripts/poller/gmail-poller.mjs +192 -0
- package/scripts/poller/imap-client.mjs +289 -0
- package/scripts/poller/inbox-scan-poller.mjs +156 -0
- package/scripts/poller/inbox-scan-poller.test.mjs +231 -0
- package/scripts/poller/index.mjs +73 -0
- package/scripts/poller/intra-session-check.mjs +285 -0
- package/scripts/poller/lib/cloud-relay-dedup.mjs +88 -0
- package/scripts/poller/lib/cloud-relay-dedup.test.mjs +133 -0
- package/scripts/poller/lib/slash-command-handlers.mjs +177 -0
- package/scripts/poller/secondary-gmail-poller.mjs +132 -0
- package/scripts/poller/slack-cloud-relay-client.mjs +368 -0
- package/scripts/poller/slack-poller.mjs +854 -0
- package/scripts/poller/slack-socket-mode.mjs +917 -0
- package/scripts/poller/slack-socket-mode.test.mjs +753 -0
- package/scripts/poller/trigger.mjs +75 -0
- package/scripts/poller/utils.mjs +371 -0
- package/scripts/poller/voice-cloud-relay-client.mjs +179 -0
- package/scripts/poller/voice-poller.mjs +236 -0
- package/scripts/poller-launchd/install.sh +66 -0
- package/scripts/poller-launchd/poller.plist.template +40 -0
- package/scripts/poller-launchd/whatsapp-handler.plist.template +39 -0
- package/scripts/post-interaction-indexer.py +1598 -0
- package/scripts/pre-draft-context.py +994 -0
- package/scripts/pre_draft_lookup.py +258 -0
- package/scripts/rag/build-index.mjs +47 -0
- package/scripts/rag/ingest.mjs +111 -0
- package/scripts/rag/search.mjs +119 -0
- package/scripts/rag-indexer.py +629 -0
- package/scripts/restore-from-backup.sh +248 -0
- package/scripts/restore-from-backup.test.mjs +178 -0
- package/scripts/resume-operations.sh +80 -0
- package/scripts/search-secondary-inbox.py +181 -0
- package/scripts/secondary-inbox-poller.py +437 -0
- package/scripts/self-optimization/compute-metrics.py +398 -0
- package/scripts/send-email-as-principal.py +369 -0
- package/scripts/send-email-threaded.py +392 -0
- package/scripts/send-email-with-attachment.py +377 -0
- package/scripts/send-email.sh +131 -0
- package/scripts/send-sms.sh +175 -0
- package/scripts/send-whatsapp.sh +292 -0
- package/scripts/session-start.sh +106 -0
- package/scripts/setup/boot-claude-session.sh +94 -0
- package/scripts/setup/configure-macos.sh +674 -0
- package/scripts/setup/configure-twilio-sip-trunk.mjs +207 -0
- package/scripts/setup/configure-voice-tunnel.mjs +182 -0
- package/scripts/setup/generate-agent-env.mjs +92 -0
- package/scripts/setup/generate-agent-package-json.mjs +222 -0
- package/scripts/setup/generate-agent-package-json.test.mjs +143 -0
- package/scripts/setup/generate-autonomy.mjs +60 -0
- package/scripts/setup/generate-backlog.mjs +101 -0
- package/scripts/setup/generate-cadences.mjs +92 -0
- package/scripts/setup/generate-capability.mjs +162 -0
- package/scripts/setup/generate-charter.mjs +76 -0
- package/scripts/setup/generate-comms.mjs +59 -0
- package/scripts/setup/generate-company.mjs +92 -0
- package/scripts/setup/init-agent.sh +547 -0
- package/scripts/setup/init-agent.test.mjs +151 -0
- package/scripts/setup/init-archetype.mjs +74 -0
- package/scripts/setup/init-backup.mjs +54 -0
- package/scripts/setup/init-cadence-bus.mjs +60 -0
- package/scripts/setup/init-channel-bus.mjs +46 -0
- package/scripts/setup/init-cost-tracking.mjs +45 -0
- package/scripts/setup/init-decision-capture.mjs +66 -0
- package/scripts/setup/init-known-agents.mjs +57 -0
- package/scripts/setup/init-learning.mjs +70 -0
- package/scripts/setup/init-memory-executive.mjs +45 -0
- package/scripts/setup/init-model-router.mjs +124 -0
- package/scripts/setup/init-rag.mjs +174 -0
- package/scripts/setup/init-session-router.mjs +38 -0
- package/scripts/setup/init-slack-socket-mode.mjs +260 -0
- package/scripts/setup/init-telegram.mjs +165 -0
- package/scripts/setup/init-voice-realtime.mjs +204 -0
- package/scripts/setup/init-whatsapp-baileys.mjs +77 -0
- package/scripts/setup/install-dev-tools.sh +150 -0
- package/scripts/setup/lib/install-plist.mjs +95 -0
- package/scripts/setup/migrate-agent-to-sot.mjs +192 -0
- package/scripts/setup/render-environment-yaml.mjs +133 -0
- package/scripts/slack-events-ctl.sh +177 -0
- package/scripts/slack-events-server.mjs +1045 -0
- package/scripts/slack-react.mjs +89 -0
- package/scripts/slack-responded.sh +232 -0
- package/scripts/slack-send.sh +287 -0
- package/scripts/slack-typing.mjs +196 -0
- package/scripts/slack-upload-v2.py +95 -0
- package/scripts/sms-handler.mjs +450 -0
- package/scripts/spawn-session.sh +120 -0
- package/scripts/sync-protocol.mjs +217 -0
- package/scripts/system-verify.sh +184 -0
- package/scripts/test-email-thread-dedup.py +239 -0
- package/scripts/test-information-barriers.py +484 -0
- package/scripts/test-llm-email-dedup.py +251 -0
- package/scripts/test-pre-draft-integration.py +203 -0
- package/scripts/test-rag-phase2.sh +442 -0
- package/scripts/test-rag-search.sh +251 -0
- package/scripts/test-voice-parser.mjs +316 -0
- package/scripts/user-context-search.py +659 -0
- package/scripts/validate_outbound.py +1504 -0
- package/scripts/watchdog/ai.maestro.memory-watchdog.plist +41 -0
- package/scripts/watchdog/force-reboot.sh +157 -0
- package/scripts/watchdog/memory-watchdog.sh +473 -0
- package/scripts/whatsapp-handler.mjs +538 -0
- package/teams/desktop-operations.yaml +34 -0
- package/teams/executive-office.yaml +27 -0
- package/teams/legal-and-regulatory.yaml +24 -0
- package/teams/platform-and-engineering.yaml +23 -0
- package/teams/strategy-and-growth.yaml +29 -0
- package/workflows/continuous/backlog-executor.yaml +141 -0
- package/workflows/continuous/inbound-monitor.yaml +168 -0
- package/workflows/daily/applicant-triage.yaml +197 -0
- package/workflows/daily/comms-triage.yaml +80 -0
- package/workflows/daily/evening-wrap.yaml +105 -0
- package/workflows/daily/morning-brief.yaml +164 -0
- package/workflows/daily/slack-followup-sweep.yaml +87 -0
- package/workflows/event-driven/README.md +50 -0
- package/workflows/event-driven/agent-failure-investigation.yaml +137 -0
- package/workflows/event-driven/pr-review.yaml +107 -0
- package/workflows/monthly/board-readiness.yaml +76 -0
- package/workflows/quarterly/strategic-scenario-analysis.yaml +85 -0
- package/workflows/session-protocol.md +171 -0
- package/workflows/weekly/engineering-health.yaml +154 -0
- package/workflows/weekly/hiring-review.yaml +169 -0
- package/workflows/weekly/rollup-pipeline-review.yaml +76 -0
- package/workflows/weekly/strategic-memo.yaml +79 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/env-compat.mjs — NEOLITH_* ⇄ COHORT_* environment bridge.
|
|
3
|
+
*
|
|
4
|
+
* The Neolith→Cohort rename (f3082db) moved every brand env read site to
|
|
5
|
+
* COHORT_* (~86 sites: COHORT_API_KEY, COHORT_ORG_ID, COHORT_BASE, COHORT_DB,
|
|
6
|
+
* COHORT_ADMIN_TOKEN, …), but the installed fleet of Mac-mini agents still
|
|
7
|
+
* launches maestro binaries with the NEOLITH_* names baked into launchd
|
|
8
|
+
* plists, operator shells, and .env files. This module is the runtime layer a
|
|
9
|
+
* text rename cannot ship: one bidirectional alias pass over the environment,
|
|
10
|
+
* so that
|
|
11
|
+
*
|
|
12
|
+
* - a fleet host exporting only NEOLITH_* keeps working against code that
|
|
13
|
+
* reads COHORT_* (legacy → canonical), and
|
|
14
|
+
* - a freshly-provisioned host exporting only COHORT_* keeps working for
|
|
15
|
+
* any older installed binary or spawned subprocess that still reads
|
|
16
|
+
* NEOLITH_* — children inherit the bridged env (canonical → legacy).
|
|
17
|
+
*
|
|
18
|
+
* Suffixes map 1:1 (the rename was textual): NEOLITH_API_KEY ⇄ COHORT_API_KEY.
|
|
19
|
+
* Never clobbers: a key that is already PRESENT — even set to "" — is left
|
|
20
|
+
* byte-for-byte untouched (`in` presence check, not truthiness), so an
|
|
21
|
+
* operator who deliberately sets both names to different values sees no
|
|
22
|
+
* surprise. Idempotent: a second pass finds every twin present and no-ops.
|
|
23
|
+
*
|
|
24
|
+
* NO import-time side effect — maestro modules are side-effect-free at import
|
|
25
|
+
* by convention. Every process entry (bin/maestro.mjs, bin/cohort-mcp.mjs,
|
|
26
|
+
* services/cohort/bin/cohort.mjs, the env-reading scripts/*.mjs) and the SDK
|
|
27
|
+
* entry lib/org/client.mjs — which agent bins import without ever passing
|
|
28
|
+
* through maestro's own entrypoints — call applyBrandEnvCompat() explicitly
|
|
29
|
+
* at the top, before any env-first config resolution.
|
|
30
|
+
*
|
|
31
|
+
* Zero deps — not even node builtins. ESM.
|
|
32
|
+
*
|
|
33
|
+
* @module lib/env-compat
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
"use strict";
|
|
37
|
+
|
|
38
|
+
/** Legacy brand prefix the installed fleet still exports. */
|
|
39
|
+
export const LEGACY_PREFIX = "NEOLITH_";
|
|
40
|
+
|
|
41
|
+
/** Canonical brand prefix the codebase reads post-rename. */
|
|
42
|
+
export const CANONICAL_PREFIX = "COHORT_";
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Mirror every `NEOLITH_*` key onto its `COHORT_*` twin and vice versa,
|
|
46
|
+
* in place, skipping any twin that already exists (never clobbers; presence
|
|
47
|
+
* is `in`-based so an explicit empty string beats an alias). Safe to call
|
|
48
|
+
* repeatedly — the second call is a no-op.
|
|
49
|
+
*
|
|
50
|
+
* @param {NodeJS.ProcessEnv|Record<string,string|undefined>} [env=process.env]
|
|
51
|
+
* Environment map to bridge in place; injectable for deterministic tests.
|
|
52
|
+
* @returns {{from:string,to:string}[]} the aliases created, in application
|
|
53
|
+
* order — [] when nothing needed bridging (and always [] on a re-run).
|
|
54
|
+
*/
|
|
55
|
+
export function applyBrandEnvCompat(env = process.env) {
|
|
56
|
+
const created = [];
|
|
57
|
+
if (!env || typeof env !== "object") return created;
|
|
58
|
+
for (const key of Object.keys(env)) {
|
|
59
|
+
let twin;
|
|
60
|
+
if (key.startsWith(LEGACY_PREFIX)) {
|
|
61
|
+
twin = CANONICAL_PREFIX + key.slice(LEGACY_PREFIX.length);
|
|
62
|
+
} else if (key.startsWith(CANONICAL_PREFIX)) {
|
|
63
|
+
twin = LEGACY_PREFIX + key.slice(CANONICAL_PREFIX.length);
|
|
64
|
+
} else {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (twin in env) continue; // never clobber — presence wins, even ""
|
|
68
|
+
const value = env[key];
|
|
69
|
+
if (typeof value !== "string") continue; // never mint "undefined" strings
|
|
70
|
+
env[twin] = value;
|
|
71
|
+
created.push({ from: key, to: twin });
|
|
72
|
+
}
|
|
73
|
+
return created;
|
|
74
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* env-compat.test.mjs — NEOLITH_* ⇄ COHORT_* bridge: both directions,
|
|
3
|
+
* no-clobber (presence beats aliasing, even for ""), non-brand keys
|
|
4
|
+
* untouched, idempotency, and the default process.env path. Uses injected
|
|
5
|
+
* plain-object envs everywhere except the one save/restore process.env test.
|
|
6
|
+
* Run: node --test lib/env-compat.test.mjs
|
|
7
|
+
*/
|
|
8
|
+
"use strict";
|
|
9
|
+
|
|
10
|
+
import { test } from "node:test";
|
|
11
|
+
import assert from "node:assert/strict";
|
|
12
|
+
|
|
13
|
+
import { applyBrandEnvCompat, LEGACY_PREFIX, CANONICAL_PREFIX } from "./env-compat.mjs";
|
|
14
|
+
|
|
15
|
+
test("legacy → canonical: NEOLITH_* is mirrored to COHORT_*", () => {
|
|
16
|
+
const env = { NEOLITH_API_KEY: "nlk_123", NEOLITH_ORG_ID: "acme" };
|
|
17
|
+
const created = applyBrandEnvCompat(env);
|
|
18
|
+
assert.equal(env.COHORT_API_KEY, "nlk_123");
|
|
19
|
+
assert.equal(env.COHORT_ORG_ID, "acme");
|
|
20
|
+
assert.equal(env.NEOLITH_API_KEY, "nlk_123"); // source untouched
|
|
21
|
+
assert.deepEqual(
|
|
22
|
+
created.sort((a, b) => a.from.localeCompare(b.from)),
|
|
23
|
+
[
|
|
24
|
+
{ from: "NEOLITH_API_KEY", to: "COHORT_API_KEY" },
|
|
25
|
+
{ from: "NEOLITH_ORG_ID", to: "COHORT_ORG_ID" },
|
|
26
|
+
],
|
|
27
|
+
);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("canonical → legacy: COHORT_* is mirrored to NEOLITH_*", () => {
|
|
31
|
+
const env = { COHORT_DB: "/var/db/cohort.sqlite", COHORT_ADMIN_TOKEN: "adm_1" };
|
|
32
|
+
applyBrandEnvCompat(env);
|
|
33
|
+
assert.equal(env.NEOLITH_DB, "/var/db/cohort.sqlite");
|
|
34
|
+
assert.equal(env.NEOLITH_ADMIN_TOKEN, "adm_1");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("never clobbers: a present twin is left byte-for-byte untouched", () => {
|
|
38
|
+
const env = {
|
|
39
|
+
NEOLITH_API_KEY: "old-value",
|
|
40
|
+
COHORT_API_KEY: "new-value", // both set, deliberately different
|
|
41
|
+
NEOLITH_BASE: "https://legacy.example",
|
|
42
|
+
COHORT_BASE: "", // present-but-empty still wins over the alias
|
|
43
|
+
};
|
|
44
|
+
const created = applyBrandEnvCompat(env);
|
|
45
|
+
assert.equal(env.COHORT_API_KEY, "new-value");
|
|
46
|
+
assert.equal(env.NEOLITH_API_KEY, "old-value");
|
|
47
|
+
assert.equal(env.COHORT_BASE, ""); // NOT overwritten by NEOLITH_BASE
|
|
48
|
+
assert.deepEqual(created, []); // every twin existed — nothing bridged
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("presence propagates: an empty-string source mirrors as empty string", () => {
|
|
52
|
+
const env = { NEOLITH_AGENT_ID: "" };
|
|
53
|
+
applyBrandEnvCompat(env);
|
|
54
|
+
assert.equal(env.COHORT_AGENT_ID, "");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("non-brand keys and non-string values are ignored", () => {
|
|
58
|
+
const env = {
|
|
59
|
+
PATH: "/usr/bin",
|
|
60
|
+
NEOLITHIC_ERA: "not-a-brand-var", // prefix must match exactly
|
|
61
|
+
COHORTS: "plural, not the prefix",
|
|
62
|
+
NEOLITH_BROKEN: undefined, // never minted into "undefined"
|
|
63
|
+
};
|
|
64
|
+
const created = applyBrandEnvCompat(env);
|
|
65
|
+
assert.deepEqual(created, []);
|
|
66
|
+
assert.ok(!("COHORT_BROKEN" in env));
|
|
67
|
+
assert.ok(!("COHORTIC_ERA" in env));
|
|
68
|
+
assert.equal(Object.keys(env).length, 4);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("idempotent: a second pass is a no-op that reports []", () => {
|
|
72
|
+
const env = { NEOLITH_API_TOKEN: "t1", COHORT_REPO: "/path/to/hq" };
|
|
73
|
+
const first = applyBrandEnvCompat(env);
|
|
74
|
+
assert.equal(first.length, 2);
|
|
75
|
+
const snapshot = { ...env };
|
|
76
|
+
const second = applyBrandEnvCompat(env);
|
|
77
|
+
assert.deepEqual(second, []);
|
|
78
|
+
assert.deepEqual(env, snapshot);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("defaults to process.env and bridges the real environment", () => {
|
|
82
|
+
// Unique suffix so a concurrent test file can never collide.
|
|
83
|
+
const suffix = `ENV_COMPAT_TEST_${process.pid}`;
|
|
84
|
+
const legacyKey = `${LEGACY_PREFIX}${suffix}`;
|
|
85
|
+
const canonicalKey = `${CANONICAL_PREFIX}${suffix}`;
|
|
86
|
+
const prevLegacy = process.env[legacyKey];
|
|
87
|
+
const prevCanonical = process.env[canonicalKey];
|
|
88
|
+
try {
|
|
89
|
+
delete process.env[canonicalKey];
|
|
90
|
+
process.env[legacyKey] = "bridged";
|
|
91
|
+
applyBrandEnvCompat();
|
|
92
|
+
assert.equal(process.env[canonicalKey], "bridged");
|
|
93
|
+
} finally {
|
|
94
|
+
if (prevLegacy == null) delete process.env[legacyKey];
|
|
95
|
+
else process.env[legacyKey] = prevLegacy;
|
|
96
|
+
if (prevCanonical == null) delete process.env[canonicalKey];
|
|
97
|
+
else process.env[canonicalKey] = prevCanonical;
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("tolerates a missing/foreign env argument without throwing", () => {
|
|
102
|
+
assert.deepEqual(applyBrandEnvCompat(null), []);
|
|
103
|
+
assert.deepEqual(applyBrandEnvCompat("not-an-object"), []);
|
|
104
|
+
});
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Maestro — Feature Init Tracking
|
|
3
|
+
*
|
|
4
|
+
* Pairs the framework's `framework-features.json` (versioned registry) with
|
|
5
|
+
* each agent's `.maestro/features.json` (per-agent installed state) so
|
|
6
|
+
* `maestro upgrade` and `maestro init` can:
|
|
7
|
+
*
|
|
8
|
+
* 1. Detect new framework features the agent has not yet initialised.
|
|
9
|
+
* 2. Auto-run low-risk init steps (mkdir, scaffold, copy).
|
|
10
|
+
* 3. Defer init steps that need user input (RAG repo selection, backup
|
|
11
|
+
* credentials) onto a `pending` queue.
|
|
12
|
+
* 4. Surface the pending queue via a SessionStart hook banner so the
|
|
13
|
+
* operator (or Claude) sees "run `maestro init` to complete setup"
|
|
14
|
+
* every time a Claude Code session opens in the agent's directory.
|
|
15
|
+
*
|
|
16
|
+
* Storage:
|
|
17
|
+
* ~/maestro/framework-features.json ← framework SOT
|
|
18
|
+
* ~/<agent>/.maestro/features.json ← agent's installed state
|
|
19
|
+
*
|
|
20
|
+
* Agent file shape:
|
|
21
|
+
* {
|
|
22
|
+
* "framework_version": "1.9.0",
|
|
23
|
+
* "schema_version": "1",
|
|
24
|
+
* "last_upgrade": "2026-05-12T10:00:00Z",
|
|
25
|
+
* "initialized": {
|
|
26
|
+
* "<feature-name>": { "version": "1", "initialized_at": "..." }
|
|
27
|
+
* },
|
|
28
|
+
* "pending": [
|
|
29
|
+
* { "feature": "...", "added_at": "...", "version": "1" }
|
|
30
|
+
* ]
|
|
31
|
+
* }
|
|
32
|
+
*
|
|
33
|
+
* The library is dep-free (only node:fs / node:path / node:child_process)
|
|
34
|
+
* and exits cleanly on missing files — fresh agents that haven't been
|
|
35
|
+
* upgraded yet simply have no `.maestro/features.json`.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
import {
|
|
39
|
+
existsSync,
|
|
40
|
+
mkdirSync,
|
|
41
|
+
readFileSync,
|
|
42
|
+
writeFileSync,
|
|
43
|
+
} from "node:fs";
|
|
44
|
+
import { join, resolve, dirname } from "node:path";
|
|
45
|
+
import { spawnSync } from "node:child_process";
|
|
46
|
+
|
|
47
|
+
export const FEATURE_REGISTRY_RELATIVE = "framework-features.json";
|
|
48
|
+
export const AGENT_STATE_RELATIVE = ".maestro/features.json";
|
|
49
|
+
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
// Path resolution
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
export function resolveAgentRoot(agentRoot) {
|
|
55
|
+
return resolve(agentRoot || process.env.AGENT_ROOT || process.env.AGENT_DIR || process.cwd());
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function getStatePath(agentRoot) {
|
|
59
|
+
return join(resolveAgentRoot(agentRoot), AGENT_STATE_RELATIVE);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
// Registry / state loaders
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Load the framework feature registry. Pass the absolute path to
|
|
68
|
+
* `framework-features.json` (typically resolved relative to the maestro
|
|
69
|
+
* CLI entry point). Returns the parsed registry or throws if missing.
|
|
70
|
+
*/
|
|
71
|
+
export function loadRegistry(registryPath) {
|
|
72
|
+
if (!existsSync(registryPath)) {
|
|
73
|
+
throw new Error(`framework-features.json not found at ${registryPath}`);
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
return JSON.parse(readFileSync(registryPath, "utf-8"));
|
|
77
|
+
} catch (err) {
|
|
78
|
+
throw new Error(`failed to parse framework-features.json: ${err.message}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Load the per-agent install state. Returns a fresh empty state if the
|
|
84
|
+
* file doesn't exist yet — callers should treat that as "every feature is
|
|
85
|
+
* pending init for the first time".
|
|
86
|
+
*/
|
|
87
|
+
export function loadAgentState(agentRoot) {
|
|
88
|
+
const statePath = getStatePath(agentRoot);
|
|
89
|
+
if (!existsSync(statePath)) {
|
|
90
|
+
return {
|
|
91
|
+
framework_version: null,
|
|
92
|
+
schema_version: "1",
|
|
93
|
+
last_upgrade: null,
|
|
94
|
+
initialized: {},
|
|
95
|
+
pending: [],
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
try {
|
|
99
|
+
const raw = JSON.parse(readFileSync(statePath, "utf-8"));
|
|
100
|
+
// Defensive defaults so partially-written state still works.
|
|
101
|
+
raw.initialized = raw.initialized || {};
|
|
102
|
+
raw.pending = Array.isArray(raw.pending) ? raw.pending : [];
|
|
103
|
+
raw.schema_version = raw.schema_version || "1";
|
|
104
|
+
return raw;
|
|
105
|
+
} catch {
|
|
106
|
+
return {
|
|
107
|
+
framework_version: null,
|
|
108
|
+
schema_version: "1",
|
|
109
|
+
last_upgrade: null,
|
|
110
|
+
initialized: {},
|
|
111
|
+
pending: [],
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function saveAgentState(agentRoot, state) {
|
|
117
|
+
const statePath = getStatePath(agentRoot);
|
|
118
|
+
mkdirSync(dirname(statePath), { recursive: true });
|
|
119
|
+
writeFileSync(statePath, JSON.stringify(state, null, 2) + "\n");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
// Diff between registry and agent state
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Return a list of features that need initialisation. A feature is "needs
|
|
128
|
+
* init" when the registry's `version` is strictly newer than the agent's
|
|
129
|
+
* recorded version, OR when the agent has no entry at all.
|
|
130
|
+
*
|
|
131
|
+
* Returns: [{ name, version, definition, status: "new" | "upgraded" }]
|
|
132
|
+
*/
|
|
133
|
+
export function diffFeatures(registry, agentState) {
|
|
134
|
+
const out = [];
|
|
135
|
+
const installed = agentState.initialized || {};
|
|
136
|
+
for (const [name, def] of Object.entries(registry.features || {})) {
|
|
137
|
+
const current = installed[name];
|
|
138
|
+
if (!current) {
|
|
139
|
+
out.push({ name, version: def.version, definition: def, status: "new" });
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (String(current.version) !== String(def.version)) {
|
|
143
|
+
out.push({ name, version: def.version, definition: def, status: "upgraded" });
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return out;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
// Init runner
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Reconcile the agent's state with the registry. For each diffed feature:
|
|
155
|
+
* - if `init.auto === true` and `runAuto`, execute init.command (spawn
|
|
156
|
+
* /bin/bash -lc so wrappers / npx scripts resolve correctly).
|
|
157
|
+
* - else, add to pending list.
|
|
158
|
+
*
|
|
159
|
+
* Options:
|
|
160
|
+
* maestroRoot absolute path to ~/maestro (used to resolve init scripts)
|
|
161
|
+
* agentRoot absolute path to the agent's repo
|
|
162
|
+
* registry parsed registry (loaded by loadRegistry)
|
|
163
|
+
* runAuto boolean; default true. Set false during dry-run/plan modes.
|
|
164
|
+
* logger optional fn({ stage, feature, status, message })
|
|
165
|
+
*
|
|
166
|
+
* Returns: { ranAuto: [...], pending: [...], failed: [...] }
|
|
167
|
+
*/
|
|
168
|
+
export function reconcileFeatures({ maestroRoot, agentRoot, registry, runAuto = true, logger } = {}) {
|
|
169
|
+
const root = resolveAgentRoot(agentRoot);
|
|
170
|
+
const state = loadAgentState(root);
|
|
171
|
+
const diff = diffFeatures(registry, state);
|
|
172
|
+
const result = { ranAuto: [], pending: [], failed: [] };
|
|
173
|
+
const log = logger || (() => {});
|
|
174
|
+
const now = new Date().toISOString();
|
|
175
|
+
|
|
176
|
+
for (const item of diff) {
|
|
177
|
+
const init = item.definition.init || {};
|
|
178
|
+
const isAuto = init.auto === true;
|
|
179
|
+
|
|
180
|
+
if (isAuto && runAuto) {
|
|
181
|
+
log({ stage: "running", feature: item.name, message: init.description });
|
|
182
|
+
const ran = runInitCommand({ maestroRoot, agentRoot: root, command: init.command });
|
|
183
|
+
if (ran.ok) {
|
|
184
|
+
state.initialized[item.name] = { version: String(item.version), initialized_at: now };
|
|
185
|
+
result.ranAuto.push(item.name);
|
|
186
|
+
log({ stage: "completed", feature: item.name });
|
|
187
|
+
} else {
|
|
188
|
+
result.failed.push({ name: item.name, error: ran.error });
|
|
189
|
+
// Push to pending so user can retry manually.
|
|
190
|
+
pushPending(state, item, now);
|
|
191
|
+
log({ stage: "failed", feature: item.name, message: ran.error });
|
|
192
|
+
}
|
|
193
|
+
} else {
|
|
194
|
+
pushPending(state, item, now);
|
|
195
|
+
result.pending.push(item.name);
|
|
196
|
+
log({ stage: "pending", feature: item.name, message: init.description });
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// After reconciliation, prune any pending entries that are now
|
|
201
|
+
// initialised (e.g. user ran the init manually).
|
|
202
|
+
state.pending = (state.pending || []).filter((p) => !state.initialized[p.feature]);
|
|
203
|
+
|
|
204
|
+
state.framework_version = registry.framework_version || state.framework_version;
|
|
205
|
+
state.last_upgrade = now;
|
|
206
|
+
saveAgentState(root, state);
|
|
207
|
+
return result;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function pushPending(state, item, now) {
|
|
211
|
+
const existing = (state.pending || []).find((p) => p.feature === item.name);
|
|
212
|
+
if (existing) {
|
|
213
|
+
existing.version = String(item.version);
|
|
214
|
+
existing.added_at = existing.added_at || now;
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
state.pending = state.pending || [];
|
|
218
|
+
state.pending.push({
|
|
219
|
+
feature: item.name,
|
|
220
|
+
version: String(item.version),
|
|
221
|
+
added_at: now,
|
|
222
|
+
title: item.definition.title || item.name,
|
|
223
|
+
description: item.definition.init?.description || "",
|
|
224
|
+
command: item.definition.init?.command || "",
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Run an init command in the agent repo.
|
|
230
|
+
*
|
|
231
|
+
* ──────────────────────────────────────────────────────────────────────────
|
|
232
|
+
* TRUST BOUNDARY (audit L20) — READ BEFORE CHANGING.
|
|
233
|
+
*
|
|
234
|
+
* `command` MUST originate ONLY from the trusted framework feature registry
|
|
235
|
+
* (`framework-features.json`, loaded by {@link loadRegistry}). That file is
|
|
236
|
+
* shipped with maestro and version-controlled; its `init.command` strings are
|
|
237
|
+
* authored by framework maintainers, NOT by end users, inbound messages, or
|
|
238
|
+
* any other runtime input.
|
|
239
|
+
*
|
|
240
|
+
* The generic-shell fallback below executes `command` via `/bin/bash -lc`,
|
|
241
|
+
* which is shell-INTERPRETED (unlike the rest of maestro, which uses
|
|
242
|
+
* execFile/spawn with argv arrays per the project's no-shell convention). That
|
|
243
|
+
* is acceptable ONLY because the input is trusted-by-provenance. If you ever
|
|
244
|
+
* wire a caller that lets untrusted data reach `command`, you MUST instead
|
|
245
|
+
* pass an argv array to spawnSync without a shell (or an allowlist) — do NOT
|
|
246
|
+
* extend the `/bin/bash -lc` path to untrusted input.
|
|
247
|
+
*
|
|
248
|
+
* As defence-in-depth, this function refuses an obviously-untrusted command:
|
|
249
|
+
* a non-string, or a string carrying NUL / newline / carriage-return bytes
|
|
250
|
+
* (which a legitimate single-line registry command never contains and which
|
|
251
|
+
* are classic shell-injection smuggling vectors). This guard does not weaken
|
|
252
|
+
* the trusted path; it only fails closed on input that could not have come
|
|
253
|
+
* from a well-formed registry entry.
|
|
254
|
+
*
|
|
255
|
+
* Accepts simple shell strings so init scripts can rely on PATH having
|
|
256
|
+
* homebrew/nvm bins. Always runs with cwd = agentRoot and inherits the
|
|
257
|
+
* caller's env plus AGENT_ROOT.
|
|
258
|
+
* ──────────────────────────────────────────────────────────────────────────
|
|
259
|
+
*/
|
|
260
|
+
function runInitCommand({ maestroRoot, agentRoot, command }) {
|
|
261
|
+
if (!command || command === "true") return { ok: true };
|
|
262
|
+
// Defence-in-depth guard (audit L20): a well-formed registry init.command is
|
|
263
|
+
// a single-line string. Anything else is treated as untrusted and refused
|
|
264
|
+
// rather than handed to the shell.
|
|
265
|
+
if (typeof command !== "string" || /[\0\r\n]/.test(command)) {
|
|
266
|
+
return { ok: false, error: "refused untrusted init command (must be a single-line string from the framework registry)" };
|
|
267
|
+
}
|
|
268
|
+
// If the command names a path that exists relative to maestroRoot,
|
|
269
|
+
// resolve to its absolute path so the init script doesn't depend on
|
|
270
|
+
// the agent having a copy of the script already.
|
|
271
|
+
const parts = command.split(/\s+/);
|
|
272
|
+
// Support: `node scripts/setup/init-xxx.mjs`
|
|
273
|
+
if (parts[0] === "node" && parts[1]) {
|
|
274
|
+
const candidateAgent = join(agentRoot, parts[1]);
|
|
275
|
+
const candidateFramework = maestroRoot ? join(maestroRoot, parts[1]) : null;
|
|
276
|
+
let scriptPath = null;
|
|
277
|
+
if (existsSync(candidateAgent)) scriptPath = candidateAgent;
|
|
278
|
+
else if (candidateFramework && existsSync(candidateFramework)) scriptPath = candidateFramework;
|
|
279
|
+
if (scriptPath) {
|
|
280
|
+
const args = [scriptPath, ...parts.slice(2)];
|
|
281
|
+
const r = spawnSync(process.execPath, args, {
|
|
282
|
+
cwd: agentRoot,
|
|
283
|
+
env: { ...process.env, AGENT_ROOT: agentRoot, AGENT_DIR: agentRoot, MAESTRO_ROOT: maestroRoot || "" },
|
|
284
|
+
encoding: "utf-8",
|
|
285
|
+
});
|
|
286
|
+
if (r.status === 0) return { ok: true, stdout: r.stdout };
|
|
287
|
+
return { ok: false, error: (r.stderr || `exit ${r.status}`).trim() };
|
|
288
|
+
}
|
|
289
|
+
return { ok: false, error: `init script not found: ${parts[1]}` };
|
|
290
|
+
}
|
|
291
|
+
// Generic shell fallback. SHELL-INTERPRETED — `command` MUST be trusted
|
|
292
|
+
// (framework-registry-sourced) per the TRUST BOUNDARY note above. The guard
|
|
293
|
+
// at the top of this function has already rejected obviously-untrusted input.
|
|
294
|
+
const r = spawnSync("/bin/bash", ["-lc", command], {
|
|
295
|
+
cwd: agentRoot,
|
|
296
|
+
env: { ...process.env, AGENT_ROOT: agentRoot, AGENT_DIR: agentRoot, MAESTRO_ROOT: maestroRoot || "" },
|
|
297
|
+
encoding: "utf-8",
|
|
298
|
+
});
|
|
299
|
+
if (r.status === 0) return { ok: true, stdout: r.stdout };
|
|
300
|
+
return { ok: false, error: (r.stderr || `exit ${r.status}`).trim() };
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// ---------------------------------------------------------------------------
|
|
304
|
+
// Pending queue accessors (used by banner + `maestro init`)
|
|
305
|
+
// ---------------------------------------------------------------------------
|
|
306
|
+
|
|
307
|
+
export function listPending(agentRoot) {
|
|
308
|
+
const state = loadAgentState(agentRoot);
|
|
309
|
+
return state.pending || [];
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export function markInitialised(agentRoot, featureName, version) {
|
|
313
|
+
const state = loadAgentState(agentRoot);
|
|
314
|
+
state.initialized = state.initialized || {};
|
|
315
|
+
state.initialized[featureName] = {
|
|
316
|
+
version: String(version),
|
|
317
|
+
initialized_at: new Date().toISOString(),
|
|
318
|
+
};
|
|
319
|
+
state.pending = (state.pending || []).filter((p) => p.feature !== featureName);
|
|
320
|
+
saveAgentState(agentRoot, state);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export function bannerSnapshot(agentRoot) {
|
|
324
|
+
const state = loadAgentState(agentRoot);
|
|
325
|
+
return {
|
|
326
|
+
pending_count: (state.pending || []).length,
|
|
327
|
+
pending: state.pending || [],
|
|
328
|
+
last_upgrade: state.last_upgrade,
|
|
329
|
+
framework_version: state.framework_version,
|
|
330
|
+
};
|
|
331
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* fs-atomic.mjs — durable, atomic filesystem write primitives.
|
|
3
|
+
*
|
|
4
|
+
* Many maestro writers (self.json, org-state JSONLs, registry/presence files,
|
|
5
|
+
* cost ledgers) currently use a bare `writeFileSync`, which can leave a
|
|
6
|
+
* zero-length or half-written file if the process dies mid-write — exactly the
|
|
7
|
+
* corruption hazard the state-integrity audit flagged. This module is the one
|
|
8
|
+
* shared home for the write-temp → fsync → rename primitive so every durable
|
|
9
|
+
* writer gets crash-safety the same way (SPEC-org-server Phase 0; roadmap 0.15).
|
|
10
|
+
*
|
|
11
|
+
* The pattern (proven in lib/cadence-bus.mjs `writeJsonAtomic`): write a sibling
|
|
12
|
+
* `.tmp` file, fsync its CONTENTS to disk, then `rename(2)` it over the target.
|
|
13
|
+
* The fsync happens BEFORE the rename so the durable bytes are in place once the
|
|
14
|
+
* atomic rename publishes the name — a crash after rename returns cannot surface
|
|
15
|
+
* a partial file. On macOS APFS (and Linux ext4/xfs) rename(2) is atomic for a
|
|
16
|
+
* same-directory source/dest.
|
|
17
|
+
*
|
|
18
|
+
* Node builtins only (maestro is "type":"module", near-zero-dep by design).
|
|
19
|
+
*
|
|
20
|
+
* @module lib/fs-atomic
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import {
|
|
24
|
+
mkdirSync,
|
|
25
|
+
openSync,
|
|
26
|
+
writeSync,
|
|
27
|
+
fsyncSync,
|
|
28
|
+
closeSync,
|
|
29
|
+
renameSync,
|
|
30
|
+
unlinkSync,
|
|
31
|
+
appendFileSync,
|
|
32
|
+
} from "node:fs";
|
|
33
|
+
import { dirname } from "node:path";
|
|
34
|
+
import { randomBytes } from "node:crypto";
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Build a collision-resistant sibling temp path for `targetPath`. Keeping the
|
|
38
|
+
* temp file in the SAME directory guarantees the subsequent rename is
|
|
39
|
+
* same-volume (and therefore atomic), unlike a temp in os.tmpdir().
|
|
40
|
+
* @param {string} targetPath
|
|
41
|
+
* @returns {string}
|
|
42
|
+
*/
|
|
43
|
+
function tmpSibling(targetPath) {
|
|
44
|
+
return `${targetPath}.tmp.${process.pid}.${Date.now()}.${randomBytes(4).toString("hex")}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Atomically write `data` (string or Buffer) to `targetPath`, creating parent
|
|
49
|
+
* dirs as needed. Durable: contents are fsync'd before the rename publishes the
|
|
50
|
+
* name. Throws on failure (after cleaning up the temp file).
|
|
51
|
+
*
|
|
52
|
+
* @param {string} targetPath
|
|
53
|
+
* @param {string|Buffer} data
|
|
54
|
+
* @param {object} [opts]
|
|
55
|
+
* @param {import("node:fs").Mode} [opts.mode] file mode for the created file
|
|
56
|
+
* @returns {void}
|
|
57
|
+
*/
|
|
58
|
+
export function writeFileAtomic(targetPath, data, opts = {}) {
|
|
59
|
+
mkdirSync(dirname(targetPath), { recursive: true });
|
|
60
|
+
const tmp = tmpSibling(targetPath);
|
|
61
|
+
const fd = openSync(tmp, "w", opts.mode);
|
|
62
|
+
try {
|
|
63
|
+
writeSync(fd, data);
|
|
64
|
+
fsyncSync(fd);
|
|
65
|
+
} finally {
|
|
66
|
+
closeSync(fd);
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
renameSync(tmp, targetPath);
|
|
70
|
+
} catch (err) {
|
|
71
|
+
try { unlinkSync(tmp); } catch { /* ignore orphan-cleanup failure */ }
|
|
72
|
+
throw err;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Atomically write `obj` as pretty-printed JSON (+ trailing newline) to
|
|
78
|
+
* `targetPath`. The canonical writer for self.json / config / state snapshots.
|
|
79
|
+
*
|
|
80
|
+
* @param {string} targetPath
|
|
81
|
+
* @param {unknown} obj
|
|
82
|
+
* @param {object} [opts] @param {import("node:fs").Mode} [opts.mode]
|
|
83
|
+
* @returns {void}
|
|
84
|
+
*/
|
|
85
|
+
export function writeJsonAtomic(targetPath, obj, opts = {}) {
|
|
86
|
+
writeFileAtomic(targetPath, JSON.stringify(obj, null, 2) + "\n", opts);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Append one JSON record as a single line to a JSONL file (append-only logs:
|
|
91
|
+
* audit trails, org-event streams, ledgers). Creates parent dirs as needed.
|
|
92
|
+
*
|
|
93
|
+
* A single `appendFileSync` of a sub-PIPE_BUF line is the standard atomic-append
|
|
94
|
+
* idiom for an O_APPEND write on a local filesystem; we serialize the object
|
|
95
|
+
* once and write it in a single call so concurrent appenders interleave whole
|
|
96
|
+
* lines, never partial ones. Returns true on success; on IO failure returns
|
|
97
|
+
* false rather than throwing, so a best-effort audit append can never crash the
|
|
98
|
+
* caller's main path (callers that need hard durability should check the return).
|
|
99
|
+
*
|
|
100
|
+
* @param {string} targetPath
|
|
101
|
+
* @param {unknown} record
|
|
102
|
+
* @returns {boolean} true if the line was written
|
|
103
|
+
*/
|
|
104
|
+
export function appendJsonl(targetPath, record) {
|
|
105
|
+
try {
|
|
106
|
+
mkdirSync(dirname(targetPath), { recursive: true });
|
|
107
|
+
appendFileSync(targetPath, JSON.stringify(record) + "\n");
|
|
108
|
+
return true;
|
|
109
|
+
} catch {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
}
|