@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,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sot.test.mjs — single-source-of-truth helper tests (lib/setup/sot.mjs).
|
|
3
|
+
* Covers wrapper generation idempotency + agent.json merge keeping unrelated keys.
|
|
4
|
+
* Run: `node --test lib/setup/sot.test.mjs`
|
|
5
|
+
* @module lib/setup/sot.test
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
"use strict";
|
|
9
|
+
|
|
10
|
+
import { test } from "node:test";
|
|
11
|
+
import assert from "node:assert/strict";
|
|
12
|
+
import { mkdtempSync, writeFileSync, mkdirSync, readFileSync, existsSync } from "node:fs";
|
|
13
|
+
import { tmpdir } from "node:os";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import {
|
|
16
|
+
readAgentJson, writeAgentJson, mergeAgentJson, deepMergeByKey,
|
|
17
|
+
ensureSotWrapper, isWrapper, agentTsPath, WRAPPER_TEMPLATE,
|
|
18
|
+
} from "./sot.mjs";
|
|
19
|
+
|
|
20
|
+
function tmpRoot() {
|
|
21
|
+
const r = mkdtempSync(join(tmpdir(), "maestro-sot-"));
|
|
22
|
+
mkdirSync(join(r, "config"), { recursive: true });
|
|
23
|
+
return r;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
test("readAgentJson returns null when missing, object when present", () => {
|
|
27
|
+
const root = tmpRoot();
|
|
28
|
+
assert.equal(readAgentJson(root), null);
|
|
29
|
+
writeAgentJson(root, { firstName: "Jo" });
|
|
30
|
+
assert.deepEqual(readAgentJson(root), { firstName: "Jo" });
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("deepMergeByKey preserves unrelated keys and deep-merges objects", () => {
|
|
34
|
+
const base = { a: 1, nested: { x: 1, y: 2 }, arr: [1, 2] };
|
|
35
|
+
const patch = { nested: { y: 9, z: 3 }, arr: [3] };
|
|
36
|
+
const out = deepMergeByKey(base, patch);
|
|
37
|
+
assert.deepEqual(out, { a: 1, nested: { x: 1, y: 9, z: 3 }, arr: [3] });
|
|
38
|
+
// inputs not mutated
|
|
39
|
+
assert.deepEqual(base.nested, { x: 1, y: 2 });
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("mergeAgentJson keeps unrelated fields (split-brain guard)", () => {
|
|
43
|
+
const root = tmpRoot();
|
|
44
|
+
writeAgentJson(root, {
|
|
45
|
+
firstName: "OLD", title: "Role",
|
|
46
|
+
principal: { fullName: "P One", email: "p@x.com" },
|
|
47
|
+
responsibilities: ["a"],
|
|
48
|
+
});
|
|
49
|
+
mergeAgentJson(root, { firstName: "NEW", principal: { email: "p2@x.com" } });
|
|
50
|
+
const back = readAgentJson(root);
|
|
51
|
+
assert.equal(back.firstName, "NEW");
|
|
52
|
+
assert.equal(back.title, "Role", "unrelated key preserved");
|
|
53
|
+
assert.equal(back.principal.fullName, "P One", "nested unrelated key preserved");
|
|
54
|
+
assert.equal(back.principal.email, "p2@x.com", "nested key updated");
|
|
55
|
+
assert.deepEqual(back.responsibilities, ["a"], "array preserved");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("isWrapper distinguishes wrapper from standalone literal", () => {
|
|
59
|
+
assert.equal(isWrapper(WRAPPER_TEMPLATE), true);
|
|
60
|
+
const standalone = `export const agent = {\n firstName: 'Ravi',\n} as const;\n`;
|
|
61
|
+
assert.equal(isWrapper(standalone), false);
|
|
62
|
+
assert.equal(isWrapper("nonsense"), false);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("ensureSotWrapper writes wrapper when absent and is idempotent", () => {
|
|
66
|
+
const root = tmpRoot();
|
|
67
|
+
const first = ensureSotWrapper(root);
|
|
68
|
+
assert.equal(first.wrote, true);
|
|
69
|
+
assert.ok(existsSync(agentTsPath(root)));
|
|
70
|
+
assert.ok(isWrapper(readFileSync(agentTsPath(root), "utf-8")));
|
|
71
|
+
const second = ensureSotWrapper(root);
|
|
72
|
+
assert.equal(second.wrote, false, "second call is a no-op");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("ensureSotWrapper replaces a standalone literal .ts", () => {
|
|
76
|
+
const root = tmpRoot();
|
|
77
|
+
writeFileSync(agentTsPath(root), `export const agent = { firstName: 'Ravi' } as const;\n`);
|
|
78
|
+
const r = ensureSotWrapper(root);
|
|
79
|
+
assert.equal(r.wrote, true);
|
|
80
|
+
assert.ok(isWrapper(readFileSync(agentTsPath(root), "utf-8")));
|
|
81
|
+
});
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/setup/state.mjs — durable checkpoint store for the `maestro setup` wizard.
|
|
3
|
+
*
|
|
4
|
+
* Persists to `state/setup/progress.json`. The checkpoint is a *hint*: on resume
|
|
5
|
+
* the runner re-`detect`s every section (the on-disk SoT is authoritative), but
|
|
6
|
+
* the checkpoint records what was applied, when, the verify result, and a hash
|
|
7
|
+
* of the answers that produced it (so a changed answer invalidates the skip).
|
|
8
|
+
*
|
|
9
|
+
* Schema (per §1 wizard / §4 design):
|
|
10
|
+
* {
|
|
11
|
+
* version: 1,
|
|
12
|
+
* updatedAt: ISO,
|
|
13
|
+
* answers: { ...durable answers across sections },
|
|
14
|
+
* sections: {
|
|
15
|
+
* <id>: { status, applied_at, verify, answersHash }
|
|
16
|
+
* },
|
|
17
|
+
* enrich: { <fieldUnit>: { status, at, reason? } } // enrichment ledger
|
|
18
|
+
* }
|
|
19
|
+
*
|
|
20
|
+
* Atomic writes (tmp + rename). Fail-soft on read (bad/missing file → fresh
|
|
21
|
+
* empty progress) so a corrupt checkpoint never blocks setup.
|
|
22
|
+
*
|
|
23
|
+
* @module lib/setup/state
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
"use strict";
|
|
27
|
+
|
|
28
|
+
import { existsSync, readFileSync, writeFileSync, renameSync, mkdirSync } from "node:fs";
|
|
29
|
+
import { join, dirname } from "node:path";
|
|
30
|
+
import { createHash } from "node:crypto";
|
|
31
|
+
|
|
32
|
+
export const PROGRESS_REL = "state/setup/progress.json";
|
|
33
|
+
export const SCHEMA_VERSION = 1;
|
|
34
|
+
|
|
35
|
+
/** Absolute path to the progress file for an agent root. */
|
|
36
|
+
export function progressPath(agentRoot) {
|
|
37
|
+
return join(agentRoot, PROGRESS_REL);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** A fresh, empty progress object. */
|
|
41
|
+
export function emptyProgress() {
|
|
42
|
+
return {
|
|
43
|
+
version: SCHEMA_VERSION,
|
|
44
|
+
updatedAt: null,
|
|
45
|
+
answers: {},
|
|
46
|
+
sections: {},
|
|
47
|
+
enrich: {},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Stable hash of an answers object (order-independent). Used to detect when a
|
|
53
|
+
* section's inputs changed so a previously-complete section is re-run.
|
|
54
|
+
* @param {object} answers
|
|
55
|
+
* @returns {string} 16-char hex digest
|
|
56
|
+
*/
|
|
57
|
+
export function hashAnswers(answers) {
|
|
58
|
+
const canonical = canonicalize(answers == null ? {} : answers);
|
|
59
|
+
return createHash("sha256").update(canonical).digest("hex").slice(0, 16);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function canonicalize(value) {
|
|
63
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
64
|
+
if (Array.isArray(value)) return `[${value.map(canonicalize).join(",")}]`;
|
|
65
|
+
const keys = Object.keys(value).sort();
|
|
66
|
+
return `{${keys.map((k) => `${JSON.stringify(k)}:${canonicalize(value[k])}`).join(",")}}`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Load the checkpoint for an agent root, returning a fresh empty progress when
|
|
71
|
+
* the file is missing or unreadable/corrupt (never throws).
|
|
72
|
+
* @param {string} agentRoot
|
|
73
|
+
* @returns {object}
|
|
74
|
+
*/
|
|
75
|
+
export function loadProgress(agentRoot) {
|
|
76
|
+
const p = progressPath(agentRoot);
|
|
77
|
+
if (!existsSync(p)) return emptyProgress();
|
|
78
|
+
try {
|
|
79
|
+
const raw = JSON.parse(readFileSync(p, "utf-8"));
|
|
80
|
+
if (!raw || typeof raw !== "object") return emptyProgress();
|
|
81
|
+
return {
|
|
82
|
+
version: raw.version || SCHEMA_VERSION,
|
|
83
|
+
updatedAt: raw.updatedAt || null,
|
|
84
|
+
answers: raw.answers && typeof raw.answers === "object" ? raw.answers : {},
|
|
85
|
+
sections: raw.sections && typeof raw.sections === "object" ? raw.sections : {},
|
|
86
|
+
enrich: raw.enrich && typeof raw.enrich === "object" ? raw.enrich : {},
|
|
87
|
+
};
|
|
88
|
+
} catch {
|
|
89
|
+
return emptyProgress();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Atomically persist a progress object. Stamps `updatedAt`.
|
|
95
|
+
* @param {string} agentRoot
|
|
96
|
+
* @param {object} progress
|
|
97
|
+
*/
|
|
98
|
+
export function saveProgress(agentRoot, progress) {
|
|
99
|
+
const p = progressPath(agentRoot);
|
|
100
|
+
mkdirSync(dirname(p), { recursive: true });
|
|
101
|
+
const body = { ...emptyProgress(), ...progress, version: SCHEMA_VERSION, updatedAt: new Date().toISOString() };
|
|
102
|
+
const tmp = `${p}.${process.pid}.${Date.now()}.tmp`;
|
|
103
|
+
writeFileSync(tmp, JSON.stringify(body, null, 2) + "\n", "utf-8");
|
|
104
|
+
renameSync(tmp, p);
|
|
105
|
+
return body;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Record a section's status into the checkpoint and persist atomically.
|
|
110
|
+
* Returns the updated progress object.
|
|
111
|
+
* @param {string} agentRoot
|
|
112
|
+
* @param {string} sectionId
|
|
113
|
+
* @param {{status:string, verify?:object, answersHash?:string, answers?:object}} patch
|
|
114
|
+
*/
|
|
115
|
+
export function markSection(agentRoot, sectionId, patch = {}) {
|
|
116
|
+
const progress = loadProgress(agentRoot);
|
|
117
|
+
const prev = progress.sections[sectionId] || {};
|
|
118
|
+
progress.sections[sectionId] = {
|
|
119
|
+
status: patch.status || prev.status || "unknown",
|
|
120
|
+
applied_at: patch.status ? new Date().toISOString() : prev.applied_at || null,
|
|
121
|
+
verify: patch.verify !== undefined ? patch.verify : prev.verify || null,
|
|
122
|
+
answersHash: patch.answersHash !== undefined ? patch.answersHash : prev.answersHash || null,
|
|
123
|
+
};
|
|
124
|
+
if (patch.answers && typeof patch.answers === "object") {
|
|
125
|
+
progress.answers = { ...progress.answers, ...patch.answers };
|
|
126
|
+
}
|
|
127
|
+
return saveProgress(agentRoot, progress);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Record an enrichment unit outcome in the ledger.
|
|
132
|
+
* @param {string} agentRoot
|
|
133
|
+
* @param {string} unit enrichable field id (e.g. "agent.persona")
|
|
134
|
+
* @param {{status:'enriched'|'pending'|'skipped', reason?:string}} patch
|
|
135
|
+
*/
|
|
136
|
+
export function markEnrich(agentRoot, unit, patch = {}) {
|
|
137
|
+
const progress = loadProgress(agentRoot);
|
|
138
|
+
progress.enrich[unit] = {
|
|
139
|
+
status: patch.status || "pending",
|
|
140
|
+
at: new Date().toISOString(),
|
|
141
|
+
...(patch.reason ? { reason: patch.reason } : {}),
|
|
142
|
+
};
|
|
143
|
+
return saveProgress(agentRoot, progress);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Merge durable answers into the checkpoint (no section change). */
|
|
147
|
+
export function mergeAnswers(agentRoot, answers) {
|
|
148
|
+
const progress = loadProgress(agentRoot);
|
|
149
|
+
progress.answers = { ...progress.answers, ...(answers || {}) };
|
|
150
|
+
return saveProgress(agentRoot, progress);
|
|
151
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* state.test.mjs — checkpoint round-trip / resume tests for lib/setup/state.mjs.
|
|
3
|
+
* Run: `node --test lib/setup/state.test.mjs`
|
|
4
|
+
* @module lib/setup/state.test
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
"use strict";
|
|
8
|
+
|
|
9
|
+
import { test } from "node:test";
|
|
10
|
+
import assert from "node:assert/strict";
|
|
11
|
+
import { mkdtempSync, writeFileSync, mkdirSync, existsSync, readFileSync } from "node:fs";
|
|
12
|
+
import { tmpdir } from "node:os";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import {
|
|
15
|
+
loadProgress, saveProgress, markSection, markEnrich, mergeAnswers,
|
|
16
|
+
hashAnswers, progressPath, emptyProgress,
|
|
17
|
+
} from "./state.mjs";
|
|
18
|
+
|
|
19
|
+
function tmpRoot() {
|
|
20
|
+
return mkdtempSync(join(tmpdir(), "maestro-state-"));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
test("loadProgress returns empty progress for a fresh root", () => {
|
|
24
|
+
const root = tmpRoot();
|
|
25
|
+
const p = loadProgress(root);
|
|
26
|
+
assert.equal(p.version, 1);
|
|
27
|
+
assert.deepEqual(p.sections, {});
|
|
28
|
+
assert.deepEqual(p.answers, {});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("saveProgress + loadProgress round-trip with atomic write", () => {
|
|
32
|
+
const root = tmpRoot();
|
|
33
|
+
const p = emptyProgress();
|
|
34
|
+
p.answers.firstName = "Jacob";
|
|
35
|
+
saveProgress(root, p);
|
|
36
|
+
assert.ok(existsSync(progressPath(root)), "progress.json must exist");
|
|
37
|
+
const back = loadProgress(root);
|
|
38
|
+
assert.equal(back.answers.firstName, "Jacob");
|
|
39
|
+
assert.ok(back.updatedAt, "updatedAt stamped");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("markSection records status + applied_at and persists", () => {
|
|
43
|
+
const root = tmpRoot();
|
|
44
|
+
markSection(root, "identity", { status: "complete", answersHash: "abc123", verify: { ok: true } });
|
|
45
|
+
const p = loadProgress(root);
|
|
46
|
+
assert.equal(p.sections.identity.status, "complete");
|
|
47
|
+
assert.equal(p.sections.identity.answersHash, "abc123");
|
|
48
|
+
assert.deepEqual(p.sections.identity.verify, { ok: true });
|
|
49
|
+
assert.ok(p.sections.identity.applied_at);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("markSection merges durable answers", () => {
|
|
53
|
+
const root = tmpRoot();
|
|
54
|
+
markSection(root, "identity", { status: "complete", answers: { firstName: "Mara" } });
|
|
55
|
+
markSection(root, "company", { status: "complete", answers: { company: "Acme" } });
|
|
56
|
+
const p = loadProgress(root);
|
|
57
|
+
assert.equal(p.answers.firstName, "Mara");
|
|
58
|
+
assert.equal(p.answers.company, "Acme");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("hashAnswers is order-independent and changes with content", () => {
|
|
62
|
+
const a = hashAnswers({ x: 1, y: 2 });
|
|
63
|
+
const b = hashAnswers({ y: 2, x: 1 });
|
|
64
|
+
const c = hashAnswers({ x: 1, y: 3 });
|
|
65
|
+
assert.equal(a, b, "key order must not matter");
|
|
66
|
+
assert.notEqual(a, c, "content change must change hash");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("markEnrich records ledger entries", () => {
|
|
70
|
+
const root = tmpRoot();
|
|
71
|
+
markEnrich(root, "agent.persona", { status: "pending", reason: "llm failed" });
|
|
72
|
+
const p = loadProgress(root);
|
|
73
|
+
assert.equal(p.enrich["agent.persona"].status, "pending");
|
|
74
|
+
assert.equal(p.enrich["agent.persona"].reason, "llm failed");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("corrupt progress file falls back to empty (never throws)", () => {
|
|
78
|
+
const root = tmpRoot();
|
|
79
|
+
mkdirSync(join(root, "state/setup"), { recursive: true });
|
|
80
|
+
writeFileSync(progressPath(root), "{ not json ");
|
|
81
|
+
const p = loadProgress(root);
|
|
82
|
+
assert.deepEqual(p.sections, {});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("mergeAnswers updates without disturbing sections", () => {
|
|
86
|
+
const root = tmpRoot();
|
|
87
|
+
markSection(root, "identity", { status: "complete" });
|
|
88
|
+
mergeAnswers(root, { tone: "warm" });
|
|
89
|
+
const p = loadProgress(root);
|
|
90
|
+
assert.equal(p.answers.tone, "warm");
|
|
91
|
+
assert.equal(p.sections.identity.status, "complete");
|
|
92
|
+
});
|
package/lib/singleton.js
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Maestro — Process Singleton Guard
|
|
3
|
+
*
|
|
4
|
+
* Prevents duplicate instances of long-running services (daemon, events
|
|
5
|
+
* server, huddle server, etc.). Uses PID files with stale detection.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* import { acquireLock, releaseLock } from "@cohortapp/agent-sdk/lib/singleton.js";
|
|
9
|
+
*
|
|
10
|
+
* // At startup:
|
|
11
|
+
* if (!acquireLock("slack-events")) {
|
|
12
|
+
* console.log("Already running — exiting");
|
|
13
|
+
* process.exit(0);
|
|
14
|
+
* }
|
|
15
|
+
*
|
|
16
|
+
* // On shutdown:
|
|
17
|
+
* process.on("SIGTERM", () => { releaseLock("slack-events"); process.exit(0); });
|
|
18
|
+
*
|
|
19
|
+
* @module singleton
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { readFileSync, unlinkSync, existsSync, mkdirSync, openSync, writeSync, closeSync } from "node:fs";
|
|
23
|
+
import { join } from "node:path";
|
|
24
|
+
import { execFileSync } from "node:child_process";
|
|
25
|
+
import { resolveAgentRoot } from "./agent-root.mjs";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Resolve the process-locks directory.
|
|
29
|
+
*
|
|
30
|
+
* Resolved per call (not memoised at import) via the shared {@link resolveAgentRoot}
|
|
31
|
+
* so singleton honours the same AGENT_ROOT → AGENT_DIR → cwd precedence as the
|
|
32
|
+
* cadence bus, and so callers/tests that set the env after import are respected.
|
|
33
|
+
* Previously this read AGENT_ROOT once at import with no AGENT_DIR fallback,
|
|
34
|
+
* diverging from the bus and pinning the path to import-time cwd. (L5/L23)
|
|
35
|
+
*
|
|
36
|
+
* @returns {string} Absolute path to the process locks directory.
|
|
37
|
+
*/
|
|
38
|
+
function locksDir() {
|
|
39
|
+
return join(resolveAgentRoot(), "state", "locks", "process");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Attempt to acquire a singleton lock for a named service.
|
|
44
|
+
* Returns true if the lock was acquired (no other instance running).
|
|
45
|
+
* Returns false if another instance is already running.
|
|
46
|
+
*
|
|
47
|
+
* Handles stale PID files (process died without cleanup).
|
|
48
|
+
*
|
|
49
|
+
* @param {string} name - Service name (e.g., "slack-events", "daemon", "huddle-server")
|
|
50
|
+
* @returns {boolean}
|
|
51
|
+
*/
|
|
52
|
+
export function acquireLock(name) {
|
|
53
|
+
const dir = locksDir();
|
|
54
|
+
mkdirSync(dir, { recursive: true });
|
|
55
|
+
const pidFile = join(dir, `${name}.pid`);
|
|
56
|
+
|
|
57
|
+
// Atomic create-or-fail with O_EXCL ("wx"). The previous
|
|
58
|
+
// existsSync()-then-writeFileSync() was a check-then-act TOCTOU race: two
|
|
59
|
+
// services starting at the same instant could both pass the existsSync
|
|
60
|
+
// check and both "acquire" the lock, ending up with two daemons draining
|
|
61
|
+
// the same bus (double escalation, double side effects). With O_EXCL the
|
|
62
|
+
// kernel guarantees exactly one winner; the loser inspects the holder and
|
|
63
|
+
// reclaims only a genuinely stale lock, then retries once. (M9)
|
|
64
|
+
let acquired = false;
|
|
65
|
+
for (let attempt = 0; attempt < 2 && !acquired; attempt++) {
|
|
66
|
+
try {
|
|
67
|
+
const fd = openSync(pidFile, "wx");
|
|
68
|
+
// Store an identity token (process start time) alongside the PID so a
|
|
69
|
+
// future liveness check can distinguish "same process still alive" from
|
|
70
|
+
// "a different process happened to be assigned the same PID" — the
|
|
71
|
+
// classic PID-reuse-after-reboot false positive that would otherwise
|
|
72
|
+
// make a stale lock look alive and block startup forever. (L2)
|
|
73
|
+
writeSync(fd, serialiseHolder(process.pid, processIdentity(process.pid)));
|
|
74
|
+
closeSync(fd);
|
|
75
|
+
acquired = true;
|
|
76
|
+
} catch (err) {
|
|
77
|
+
if (err.code !== "EEXIST") throw err;
|
|
78
|
+
let holder = { pid: 0, identity: null };
|
|
79
|
+
try {
|
|
80
|
+
holder = parseHolder(readFileSync(pidFile, "utf8"));
|
|
81
|
+
} catch {
|
|
82
|
+
// Corrupt/unreadable PID file — treat as stale and reclaim below.
|
|
83
|
+
}
|
|
84
|
+
if (holder.pid && isHolderAlive(holder)) {
|
|
85
|
+
// Another instance is genuinely running.
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
// Stale (dead PID, recycled PID, or corrupt lock) — remove and retry once.
|
|
89
|
+
try { unlinkSync(pidFile); } catch { /* another start may have reclaimed it */ }
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (!acquired) return false;
|
|
93
|
+
|
|
94
|
+
// Register cleanup on exit
|
|
95
|
+
const cleanup = () => {
|
|
96
|
+
try { unlinkSync(pidFile); } catch { /* ignore */ }
|
|
97
|
+
};
|
|
98
|
+
process.on("exit", cleanup);
|
|
99
|
+
process.on("SIGINT", () => { cleanup(); process.exit(0); });
|
|
100
|
+
process.on("SIGTERM", () => { cleanup(); process.exit(0); });
|
|
101
|
+
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Manually release a lock.
|
|
107
|
+
* @param {string} name
|
|
108
|
+
*/
|
|
109
|
+
export function releaseLock(name) {
|
|
110
|
+
const pidFile = join(locksDir(), `${name}.pid`);
|
|
111
|
+
try { unlinkSync(pidFile); } catch { /* ignore */ }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Check if a service is currently running.
|
|
116
|
+
* @param {string} name
|
|
117
|
+
* @returns {{ running: boolean, pid: number|null }}
|
|
118
|
+
*/
|
|
119
|
+
export function checkLock(name) {
|
|
120
|
+
const pidFile = join(locksDir(), `${name}.pid`);
|
|
121
|
+
if (!existsSync(pidFile)) return { running: false, pid: null };
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
const holder = parseHolder(readFileSync(pidFile, "utf8"));
|
|
125
|
+
if (holder.pid && isHolderAlive(holder)) {
|
|
126
|
+
return { running: true, pid: holder.pid };
|
|
127
|
+
}
|
|
128
|
+
// Stale (dead PID, recycled PID, or corrupt lock) — clean up
|
|
129
|
+
try { unlinkSync(pidFile); } catch { /* ignore */ }
|
|
130
|
+
return { running: false, pid: null };
|
|
131
|
+
} catch {
|
|
132
|
+
return { running: false, pid: null };
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Best-effort process identity token: the process start time as reported by
|
|
138
|
+
* `ps`. Two different processes that happen to share a PID (e.g. PID reuse
|
|
139
|
+
* after a reboot) will almost never share a start time, so comparing this on
|
|
140
|
+
* the liveness check turns a PID-reuse false positive into a correctly-detected
|
|
141
|
+
* stale lock. Best-effort by design: if `ps` is unavailable or the PID is gone,
|
|
142
|
+
* returns null and the caller falls back to a plain existence check. (L2)
|
|
143
|
+
*
|
|
144
|
+
* @param {number} pid
|
|
145
|
+
* @returns {string|null}
|
|
146
|
+
*/
|
|
147
|
+
function processIdentity(pid) {
|
|
148
|
+
if (!pid || pid <= 0) return null;
|
|
149
|
+
try {
|
|
150
|
+
// `ps -o lstart=` prints the process start time with no header. Stable for
|
|
151
|
+
// the life of the process; differs across PID reuse. execFileSync (not
|
|
152
|
+
// exec) avoids any shell. A non-existent PID exits non-zero -> caught.
|
|
153
|
+
const out = execFileSync("ps", ["-o", "lstart=", "-p", String(pid)], {
|
|
154
|
+
encoding: "utf8",
|
|
155
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
156
|
+
}).trim();
|
|
157
|
+
return out || null;
|
|
158
|
+
} catch {
|
|
159
|
+
// ps unavailable (or PID gone) — best effort, no identity captured.
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Serialise a lock holder to the on-disk format. Backwards compatible: a bare
|
|
166
|
+
* PID (no identity) is still a valid first line, so old lock files written by
|
|
167
|
+
* earlier versions parse fine.
|
|
168
|
+
*
|
|
169
|
+
* Format: line 1 = PID, line 2 (optional) = identity token.
|
|
170
|
+
*
|
|
171
|
+
* @param {number} pid
|
|
172
|
+
* @param {string|null} identity
|
|
173
|
+
* @returns {string}
|
|
174
|
+
*/
|
|
175
|
+
function serialiseHolder(pid, identity) {
|
|
176
|
+
return identity ? `${pid}\n${identity}` : String(pid);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Parse a lock file's contents into a holder record. Tolerates the legacy
|
|
181
|
+
* single-line (PID only) format as well as the PID + identity format.
|
|
182
|
+
*
|
|
183
|
+
* @param {string} raw
|
|
184
|
+
* @returns {{ pid: number, identity: string|null }}
|
|
185
|
+
*/
|
|
186
|
+
function parseHolder(raw) {
|
|
187
|
+
const lines = String(raw).split("\n");
|
|
188
|
+
const pid = parseInt((lines[0] || "").trim(), 10) || 0;
|
|
189
|
+
const identity = lines.length > 1 ? (lines.slice(1).join("\n").trim() || null) : null;
|
|
190
|
+
return { pid, identity };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Liveness check for a recorded lock holder, identity-aware.
|
|
195
|
+
*
|
|
196
|
+
* 1. If the PID is not alive at all -> not alive (stale).
|
|
197
|
+
* 2. If the lock recorded no identity (legacy file or ps was unavailable when
|
|
198
|
+
* it was written) -> fall back to the plain PID existence result (no worse
|
|
199
|
+
* than the historical behaviour).
|
|
200
|
+
* 3. If an identity was recorded, recompute the current identity for that PID
|
|
201
|
+
* and require a match. A mismatch means the PID was recycled by a different
|
|
202
|
+
* process, so the lock is stale and reclaimable. If we cannot recompute the
|
|
203
|
+
* current identity (ps unavailable now), we err on the side of "alive" to
|
|
204
|
+
* avoid evicting a real holder.
|
|
205
|
+
*
|
|
206
|
+
* @param {{ pid: number, identity: string|null }} holder
|
|
207
|
+
* @returns {boolean}
|
|
208
|
+
*/
|
|
209
|
+
function isHolderAlive(holder) {
|
|
210
|
+
if (!holder.pid || !isProcessRunning(holder.pid)) return false;
|
|
211
|
+
if (!holder.identity) return true; // legacy / no token recorded — best effort
|
|
212
|
+
const current = processIdentity(holder.pid);
|
|
213
|
+
if (current === null) return true; // can't verify now — don't evict a live holder
|
|
214
|
+
return current === holder.identity;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Check if a PID is still alive.
|
|
219
|
+
* @param {number} pid
|
|
220
|
+
* @returns {boolean}
|
|
221
|
+
*/
|
|
222
|
+
function isProcessRunning(pid) {
|
|
223
|
+
try {
|
|
224
|
+
process.kill(pid, 0); // Signal 0 = existence check, no actual signal
|
|
225
|
+
return true;
|
|
226
|
+
} catch {
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { mkdtempSync, writeFileSync, existsSync, rmSync, readFileSync, mkdirSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
|
|
7
|
+
// singleton.js now resolves the locks dir per call from AGENT_ROOT (then
|
|
8
|
+
// AGENT_DIR, then cwd). Set AGENT_ROOT to a throwaway dir before importing so
|
|
9
|
+
// the default suite operates inside it; individual tests that exercise the
|
|
10
|
+
// AGENT_DIR fallback save/restore the environment themselves.
|
|
11
|
+
const root = mkdtempSync(join(tmpdir(), "maestro-singleton-"));
|
|
12
|
+
process.env.AGENT_ROOT = root;
|
|
13
|
+
delete process.env.AGENT_DIR;
|
|
14
|
+
const { acquireLock, releaseLock, checkLock } = await import("./singleton.js");
|
|
15
|
+
|
|
16
|
+
const LOCKS = join(root, "state", "locks", "process");
|
|
17
|
+
|
|
18
|
+
test("first acquire succeeds; second (live holder) is refused", () => {
|
|
19
|
+
assert.equal(acquireLock("a"), true);
|
|
20
|
+
// Our own PID is the holder and is alive, so re-acquire must fail.
|
|
21
|
+
assert.equal(acquireLock("a"), false);
|
|
22
|
+
releaseLock("a");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("release frees the lock so it can be re-acquired", () => {
|
|
26
|
+
assert.equal(acquireLock("b"), true);
|
|
27
|
+
releaseLock("b");
|
|
28
|
+
assert.equal(acquireLock("b"), true);
|
|
29
|
+
releaseLock("b");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("a stale lock (dead PID) is reclaimed atomically", () => {
|
|
33
|
+
const pidFile = join(LOCKS, "c.pid");
|
|
34
|
+
writeFileSync(pidFile, "2147483646"); // effectively never a live PID
|
|
35
|
+
assert.equal(acquireLock("c"), true);
|
|
36
|
+
// The reclaimed lock now records our PID on line 1 (identity may follow).
|
|
37
|
+
assert.equal(readFileSync(pidFile, "utf8").split("\n")[0].trim(), String(process.pid));
|
|
38
|
+
releaseLock("c");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("a corrupt lock file is reclaimed", () => {
|
|
42
|
+
const pidFile = join(LOCKS, "d.pid");
|
|
43
|
+
writeFileSync(pidFile, "not-a-pid");
|
|
44
|
+
assert.equal(acquireLock("d"), true);
|
|
45
|
+
releaseLock("d");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("acquire writes our own pid into the lock file", () => {
|
|
49
|
+
assert.equal(acquireLock("e"), true);
|
|
50
|
+
assert.equal(readFileSync(join(LOCKS, "e.pid"), "utf8").split("\n")[0].trim(), String(process.pid));
|
|
51
|
+
releaseLock("e");
|
|
52
|
+
assert.ok(!existsSync(join(LOCKS, "e.pid")));
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("checkLock reports running state for a held lock", () => {
|
|
56
|
+
assert.equal(acquireLock("f"), true);
|
|
57
|
+
const status = checkLock("f");
|
|
58
|
+
assert.equal(status.running, true);
|
|
59
|
+
assert.equal(status.pid, process.pid);
|
|
60
|
+
releaseLock("f");
|
|
61
|
+
assert.equal(checkLock("f").running, false);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// L5/L23: locks dir resolution honours AGENT_DIR when AGENT_ROOT is unset, and
|
|
65
|
+
// is computed per call rather than memoised at import.
|
|
66
|
+
test("locks dir honours AGENT_DIR when AGENT_ROOT is unset", () => {
|
|
67
|
+
const savedRoot = process.env.AGENT_ROOT;
|
|
68
|
+
const savedDir = process.env.AGENT_DIR;
|
|
69
|
+
const altRoot = mkdtempSync(join(tmpdir(), "maestro-singleton-agentdir-"));
|
|
70
|
+
try {
|
|
71
|
+
delete process.env.AGENT_ROOT;
|
|
72
|
+
process.env.AGENT_DIR = altRoot;
|
|
73
|
+
const altLocks = join(altRoot, "state", "locks", "process");
|
|
74
|
+
|
|
75
|
+
// Acquire under AGENT_DIR — the lock file must land under altRoot, proving
|
|
76
|
+
// per-call resolution picked up AGENT_DIR (not the import-time AGENT_ROOT).
|
|
77
|
+
assert.equal(acquireLock("agentdir"), true);
|
|
78
|
+
assert.ok(existsSync(join(altLocks, "agentdir.pid")));
|
|
79
|
+
// And it must NOT have been written under the original AGENT_ROOT tree.
|
|
80
|
+
assert.ok(!existsSync(join(LOCKS, "agentdir.pid")));
|
|
81
|
+
releaseLock("agentdir");
|
|
82
|
+
assert.ok(!existsSync(join(altLocks, "agentdir.pid")));
|
|
83
|
+
} finally {
|
|
84
|
+
if (savedRoot === undefined) delete process.env.AGENT_ROOT;
|
|
85
|
+
else process.env.AGENT_ROOT = savedRoot;
|
|
86
|
+
if (savedDir === undefined) delete process.env.AGENT_DIR;
|
|
87
|
+
else process.env.AGENT_DIR = savedDir;
|
|
88
|
+
rmSync(altRoot, { recursive: true, force: true });
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// L2: PID-reuse false positive. A lock whose PID is alive (our own PID) but
|
|
93
|
+
// whose recorded identity token does NOT match the real process identity is a
|
|
94
|
+
// recycled PID, and must be reclaimed rather than treated as a live holder.
|
|
95
|
+
test("a stale lock with alive-but-different identity is reclaimed", () => {
|
|
96
|
+
const pidFile = join(LOCKS, "g.pid");
|
|
97
|
+
mkdirSync(LOCKS, { recursive: true });
|
|
98
|
+
// Our own PID (alive) + a bogus identity token that cannot match the real
|
|
99
|
+
// `ps -o lstart=` start time for this process.
|
|
100
|
+
writeFileSync(pidFile, `${process.pid}\nBOGUS-IDENTITY-TOKEN-NEVER-MATCHES`);
|
|
101
|
+
|
|
102
|
+
// checkLock must see this as not running (identity mismatch -> stale).
|
|
103
|
+
assert.equal(checkLock("g").running, false);
|
|
104
|
+
|
|
105
|
+
// And acquire must succeed by reclaiming the stale lock.
|
|
106
|
+
writeFileSync(pidFile, `${process.pid}\nBOGUS-IDENTITY-TOKEN-NEVER-MATCHES`);
|
|
107
|
+
assert.equal(acquireLock("g"), true);
|
|
108
|
+
// The reclaimed lock now records our PID on line 1.
|
|
109
|
+
assert.equal(readFileSync(pidFile, "utf8").split("\n")[0].trim(), String(process.pid));
|
|
110
|
+
releaseLock("g");
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Identity match path: a lock written by acquire (real identity recorded) for
|
|
114
|
+
// our own live PID is correctly treated as a live holder.
|
|
115
|
+
test("a lock with matching identity for a live PID is treated as held", () => {
|
|
116
|
+
assert.equal(acquireLock("h"), true);
|
|
117
|
+
// Re-acquire must fail: same PID, identity recomputed and matches.
|
|
118
|
+
assert.equal(acquireLock("h"), false);
|
|
119
|
+
assert.equal(checkLock("h").running, true);
|
|
120
|
+
releaseLock("h");
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// Legacy compatibility: an old single-line (PID-only) lock for a live PID is
|
|
124
|
+
// still honoured as a live holder (no worse than historical behaviour).
|
|
125
|
+
test("a legacy pid-only lock for a live PID is honoured", () => {
|
|
126
|
+
const pidFile = join(LOCKS, "i.pid");
|
|
127
|
+
mkdirSync(LOCKS, { recursive: true });
|
|
128
|
+
writeFileSync(pidFile, String(process.pid)); // legacy format, no identity
|
|
129
|
+
assert.equal(checkLock("i").running, true);
|
|
130
|
+
// Re-acquire must be refused since the PID is alive and no identity was set.
|
|
131
|
+
assert.equal(acquireLock("i"), false);
|
|
132
|
+
rmSync(pidFile, { force: true });
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test.after(() => rmSync(root, { recursive: true, force: true }));
|