@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,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* otel.test.mjs — node:test coverage for the Claude Code OTLP env builder.
|
|
3
|
+
*
|
|
4
|
+
* Pure-function tests: env and identity are passed in, never read from the real
|
|
5
|
+
* process.env, so nothing leaks between cases. We assert the builder emits env
|
|
6
|
+
* ONLY when enabled, stamps service.name + agent id, and never throws.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { test } from "node:test";
|
|
10
|
+
import assert from "node:assert/strict";
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
buildOtelEnv,
|
|
14
|
+
otelEnabled,
|
|
15
|
+
deriveAgentId,
|
|
16
|
+
buildResourceAttributes,
|
|
17
|
+
} from "./otel.mjs";
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// otelEnabled
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
test("otelEnabled is false by default (empty env, no config)", () => {
|
|
24
|
+
assert.equal(otelEnabled({ env: {}, config: {} }), false);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("otelEnabled honours MAESTRO_OTEL truthy values", () => {
|
|
28
|
+
for (const v of ["1", "true", "yes", "on", "TRUE", "On"]) {
|
|
29
|
+
assert.equal(otelEnabled({ env: { MAESTRO_OTEL: v }, config: {} }), true, v);
|
|
30
|
+
}
|
|
31
|
+
for (const v of ["0", "false", "no", "off", "", "nope"]) {
|
|
32
|
+
assert.equal(otelEnabled({ env: { MAESTRO_OTEL: v }, config: {} }), false, v);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("config.enabled boolean overrides the env flag", () => {
|
|
37
|
+
// config wins both ways
|
|
38
|
+
assert.equal(otelEnabled({ env: { MAESTRO_OTEL: "1" }, config: { enabled: false } }), false);
|
|
39
|
+
assert.equal(otelEnabled({ env: { MAESTRO_OTEL: "0" }, config: { enabled: true } }), true);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// buildOtelEnv — disabled => empty
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
test("buildOtelEnv returns an empty map when disabled", () => {
|
|
47
|
+
assert.deepEqual(buildOtelEnv({ env: {}, config: {} }), {});
|
|
48
|
+
assert.deepEqual(
|
|
49
|
+
buildOtelEnv({ env: { MAESTRO_OTEL: "0" }, config: {}, identity: { repoSlug: "alex-ai" } }),
|
|
50
|
+
{}
|
|
51
|
+
);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("the empty map is safe to spread (no telemetry keys leak)", () => {
|
|
55
|
+
const merged = { PATH: "/usr/bin", ...buildOtelEnv({ env: {}, config: {} }) };
|
|
56
|
+
assert.deepEqual(Object.keys(merged), ["PATH"]);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
// buildOtelEnv — enabled => full block with sane defaults
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
test("buildOtelEnv emits the enabling block when MAESTRO_OTEL=1", () => {
|
|
64
|
+
const env = buildOtelEnv({
|
|
65
|
+
env: { MAESTRO_OTEL: "1" },
|
|
66
|
+
config: {},
|
|
67
|
+
identity: { repoSlug: "alex-ai" },
|
|
68
|
+
});
|
|
69
|
+
assert.equal(env.CLAUDE_CODE_ENABLE_TELEMETRY, "1");
|
|
70
|
+
assert.equal(env.OTEL_METRICS_EXPORTER, "otlp");
|
|
71
|
+
assert.equal(env.OTEL_LOGS_EXPORTER, "otlp");
|
|
72
|
+
assert.equal(env.OTEL_TRACES_EXPORTER, "otlp");
|
|
73
|
+
assert.equal(env.OTEL_EXPORTER_OTLP_PROTOCOL, "http/protobuf");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("buildOtelEnv stamps service.name and agent id into resource attributes", () => {
|
|
77
|
+
const env = buildOtelEnv({
|
|
78
|
+
config: { enabled: true },
|
|
79
|
+
identity: { repoSlug: "alex-ai", maestroVersion: "0.15.2" },
|
|
80
|
+
});
|
|
81
|
+
const attrs = env.OTEL_RESOURCE_ATTRIBUTES;
|
|
82
|
+
assert.ok(attrs.includes("service.name=maestro-agent"), attrs);
|
|
83
|
+
assert.ok(attrs.includes("service.version=0.15.2"), attrs);
|
|
84
|
+
assert.ok(attrs.includes("service.instance.id=alex-ai"), attrs);
|
|
85
|
+
assert.ok(attrs.includes("agent.id=alex-ai"), attrs);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("buildOtelEnv lets serviceName be overridden", () => {
|
|
89
|
+
const env = buildOtelEnv({
|
|
90
|
+
config: { enabled: true },
|
|
91
|
+
identity: { repoSlug: "robin-ai" },
|
|
92
|
+
serviceName: "claude-code-robin",
|
|
93
|
+
});
|
|
94
|
+
assert.ok(env.OTEL_RESOURCE_ATTRIBUTES.includes("service.name=claude-code-robin"));
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
// buildOtelEnv — optional knobs only when present
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
|
|
101
|
+
test("endpoint/headers/intervals are emitted only when configured", () => {
|
|
102
|
+
const bare = buildOtelEnv({ config: { enabled: true }, identity: { repoSlug: "a" } });
|
|
103
|
+
assert.ok(!("OTEL_EXPORTER_OTLP_ENDPOINT" in bare));
|
|
104
|
+
assert.ok(!("OTEL_EXPORTER_OTLP_HEADERS" in bare));
|
|
105
|
+
assert.ok(!("OTEL_METRIC_EXPORT_INTERVAL" in bare));
|
|
106
|
+
assert.ok(!("OTEL_LOGS_EXPORT_INTERVAL" in bare));
|
|
107
|
+
|
|
108
|
+
const full = buildOtelEnv({
|
|
109
|
+
config: {
|
|
110
|
+
enabled: true,
|
|
111
|
+
endpoint: "https://otel.fleet.internal:4318",
|
|
112
|
+
headers: "Authorization=Bearer xyz",
|
|
113
|
+
protocol: "grpc",
|
|
114
|
+
metricExportIntervalMs: 60000,
|
|
115
|
+
logsExportIntervalMs: 5000,
|
|
116
|
+
},
|
|
117
|
+
identity: { repoSlug: "a" },
|
|
118
|
+
});
|
|
119
|
+
assert.equal(full.OTEL_EXPORTER_OTLP_ENDPOINT, "https://otel.fleet.internal:4318");
|
|
120
|
+
assert.equal(full.OTEL_EXPORTER_OTLP_HEADERS, "Authorization=Bearer xyz");
|
|
121
|
+
assert.equal(full.OTEL_EXPORTER_OTLP_PROTOCOL, "grpc");
|
|
122
|
+
assert.equal(full.OTEL_METRIC_EXPORT_INTERVAL, "60000");
|
|
123
|
+
assert.equal(full.OTEL_LOGS_EXPORT_INTERVAL, "5000");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("config takes precedence over env for exporter knobs", () => {
|
|
127
|
+
const env = buildOtelEnv({
|
|
128
|
+
env: { MAESTRO_OTEL: "1", OTEL_EXPORTER_OTLP_ENDPOINT: "http://env-endpoint:4318" },
|
|
129
|
+
config: { endpoint: "http://config-endpoint:4318" },
|
|
130
|
+
identity: { repoSlug: "a" },
|
|
131
|
+
});
|
|
132
|
+
assert.equal(env.OTEL_EXPORTER_OTLP_ENDPOINT, "http://config-endpoint:4318");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("env supplies the endpoint when config omits it", () => {
|
|
136
|
+
const env = buildOtelEnv({
|
|
137
|
+
env: { MAESTRO_OTEL: "1", OTEL_EXPORTER_OTLP_ENDPOINT: "http://env-endpoint:4318" },
|
|
138
|
+
config: {},
|
|
139
|
+
identity: { repoSlug: "a" },
|
|
140
|
+
});
|
|
141
|
+
assert.equal(env.OTEL_EXPORTER_OTLP_ENDPOINT, "http://env-endpoint:4318");
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("extra resource attributes from config are appended", () => {
|
|
145
|
+
const env = buildOtelEnv({
|
|
146
|
+
config: { enabled: true, resourceAttributes: { "deployment.environment": "prod" } },
|
|
147
|
+
identity: { repoSlug: "a" },
|
|
148
|
+
});
|
|
149
|
+
assert.ok(env.OTEL_RESOURCE_ATTRIBUTES.includes("deployment.environment=prod"));
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// ---------------------------------------------------------------------------
|
|
153
|
+
// deriveAgentId
|
|
154
|
+
// ---------------------------------------------------------------------------
|
|
155
|
+
|
|
156
|
+
test("deriveAgentId prefers explicit id, then repoSlug, then name", () => {
|
|
157
|
+
assert.equal(deriveAgentId({ agentId: "spiffe-007", repoSlug: "x" }), "spiffe-007");
|
|
158
|
+
assert.equal(deriveAgentId({ repoSlug: "Alex-AI" }), "alex-ai");
|
|
159
|
+
assert.equal(deriveAgentId({ fullName: "Robin Quist" }), "robin-quist");
|
|
160
|
+
assert.equal(deriveAgentId({}), "");
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("deriveAgentId slugifies unsafe characters", () => {
|
|
164
|
+
assert.equal(deriveAgentId({ fullName: "Señor Tester!" }), "se-or-tester");
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
// buildResourceAttributes — formatting + sanitization
|
|
169
|
+
// ---------------------------------------------------------------------------
|
|
170
|
+
|
|
171
|
+
test("buildResourceAttributes skips empty values (no dangling k=)", () => {
|
|
172
|
+
const attrs = buildResourceAttributes({ serviceName: "svc", serviceVersion: "", agentId: "" });
|
|
173
|
+
assert.equal(attrs, "service.name=svc");
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("buildResourceAttributes sanitizes comma/equals inside a value", () => {
|
|
177
|
+
const attrs = buildResourceAttributes({
|
|
178
|
+
serviceName: "svc",
|
|
179
|
+
agentId: "a=b,c",
|
|
180
|
+
});
|
|
181
|
+
// a=b,c would corrupt the k=v,k=v list; commas/equals stripped to spaces.
|
|
182
|
+
assert.ok(!attrs.includes("a=b,c"));
|
|
183
|
+
assert.ok(attrs.includes("agent.id=a b c"));
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// ---------------------------------------------------------------------------
|
|
187
|
+
// never throws
|
|
188
|
+
// ---------------------------------------------------------------------------
|
|
189
|
+
|
|
190
|
+
test("buildOtelEnv never throws on garbage input", () => {
|
|
191
|
+
let out;
|
|
192
|
+
assert.doesNotThrow(() => {
|
|
193
|
+
out = buildOtelEnv({ env: null, config: null, identity: null });
|
|
194
|
+
});
|
|
195
|
+
assert.equal(typeof out, "object");
|
|
196
|
+
});
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/diagnostics/trace.mjs — trace/correlation IDs for the agent event spine.
|
|
3
|
+
*
|
|
4
|
+
* One user interaction touches many hops — poller item (or cadence tick) →
|
|
5
|
+
* classify → dispatch → session → send — but today there is no thread that ties
|
|
6
|
+
* those rows together; reconstructing a single interaction means fuzzy timestamp
|
|
7
|
+
* joins (gaps-enterprise-ops G7). This module mints ONE `trace_id` at the start
|
|
8
|
+
* of an interaction and gives the daemon a way to carry it (plus a span path)
|
|
9
|
+
* through every downstream hop so the diagnostic-event stream (events.mjs) and
|
|
10
|
+
* Claude Code's native OTLP export (otel.mjs) correlate to the same id. That is
|
|
11
|
+
* what makes the <2 min DM SLO measurable: first item_received → last sent,
|
|
12
|
+
* grouped by `trace_id`.
|
|
13
|
+
*
|
|
14
|
+
* The id is ULID-shaped: a 48-bit millisecond timestamp + 80 bits of
|
|
15
|
+
* randomness, Crockford-base32, 26 chars, monotonic-ish and lexicographically
|
|
16
|
+
* sortable by creation time. We do NOT pull in a ULID dependency — maestro is
|
|
17
|
+
* near-zero-dep — this is a self-contained encoder over `node:crypto`.
|
|
18
|
+
*
|
|
19
|
+
* Propagation is via `AsyncLocalStorage`: `withTrace(ctx, fn)` runs `fn` (sync
|
|
20
|
+
* or async) inside a context, and any code beneath — however deep the await
|
|
21
|
+
* chain — reads it with `currentTrace()`. Where an explicit hand-off is easier
|
|
22
|
+
* (a spawned sub-process, a serialized inbox item), `traceHeaders()` /
|
|
23
|
+
* `traceFromHeaders()` move the context across the boundary as plain fields, and
|
|
24
|
+
* `childSpan()` extends the span path one hop.
|
|
25
|
+
*
|
|
26
|
+
* Constraints (CLAUDE.md): ESM, Node built-ins only, pure/injectable (the clock
|
|
27
|
+
* and RNG are overridable so id minting is deterministic under test). Nothing
|
|
28
|
+
* here throws on a bad/missing context — a missing trace returns `null`, never
|
|
29
|
+
* an exception, so an un-instrumented call path degrades to "no correlation"
|
|
30
|
+
* rather than crashing the daemon.
|
|
31
|
+
*
|
|
32
|
+
* @module lib/diagnostics/trace
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
36
|
+
import { randomBytes } from "node:crypto";
|
|
37
|
+
|
|
38
|
+
// Crockford base32 alphabet (ULID spec): no I, L, O, U to avoid ambiguity.
|
|
39
|
+
const CROCKFORD = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
|
|
40
|
+
const TIME_LEN = 10; // 48-bit ms timestamp → 10 base32 chars
|
|
41
|
+
const RAND_LEN = 16; // 80-bit randomness → 16 base32 chars
|
|
42
|
+
const ULID_LEN = TIME_LEN + RAND_LEN; // 26
|
|
43
|
+
const ULID_RE = /^[0-9A-HJKMNP-TV-Z]{26}$/;
|
|
44
|
+
|
|
45
|
+
/** The single ambient store for the current trace context. */
|
|
46
|
+
const STORE = new AsyncLocalStorage();
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Encode `time` (epoch ms) as the 10-char timestamp head of a ULID.
|
|
50
|
+
* @param {number} time
|
|
51
|
+
* @returns {string}
|
|
52
|
+
*/
|
|
53
|
+
function encodeTime(time) {
|
|
54
|
+
let t = Math.floor(time);
|
|
55
|
+
let out = "";
|
|
56
|
+
for (let i = TIME_LEN - 1; i >= 0; i--) {
|
|
57
|
+
const mod = t % 32;
|
|
58
|
+
out = CROCKFORD[mod] + out;
|
|
59
|
+
t = (t - mod) / 32;
|
|
60
|
+
}
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Encode 80 random bits as the 16-char tail of a ULID. We draw 10 bytes and
|
|
66
|
+
* map them 5-bits-at-a-time across the 16 output chars (10 * 8 = 80 = 16 * 5).
|
|
67
|
+
* @param {() => Buffer} rng returns at least 10 random bytes
|
|
68
|
+
* @returns {string}
|
|
69
|
+
*/
|
|
70
|
+
function encodeRandom(rng) {
|
|
71
|
+
const bytes = rng(10);
|
|
72
|
+
// Build the 80-bit value as a BigInt, then peel 5 bits per char.
|
|
73
|
+
let v = 0n;
|
|
74
|
+
for (let i = 0; i < 10; i++) v = (v << 8n) | BigInt(bytes[i] & 0xff);
|
|
75
|
+
let out = "";
|
|
76
|
+
for (let i = 0; i < RAND_LEN; i++) {
|
|
77
|
+
out = CROCKFORD[Number(v & 31n)] + out;
|
|
78
|
+
v >>= 5n;
|
|
79
|
+
}
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Mint a fresh, sortable trace id (ULID-shaped, 26 chars).
|
|
85
|
+
*
|
|
86
|
+
* @param {object} [deps]
|
|
87
|
+
* @param {() => number} [deps.now] clock (default Date.now)
|
|
88
|
+
* @param {(n: number) => Buffer} [deps.rng] RNG (default crypto.randomBytes)
|
|
89
|
+
* @returns {string}
|
|
90
|
+
*/
|
|
91
|
+
export function mintTraceId(deps = {}) {
|
|
92
|
+
const now = typeof deps.now === "function" ? deps.now : Date.now;
|
|
93
|
+
const rng = typeof deps.rng === "function" ? deps.rng : randomBytes;
|
|
94
|
+
return encodeTime(now()) + encodeRandom(rng);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* True if `id` is a syntactically valid ULID-shaped trace id.
|
|
99
|
+
* @param {unknown} id
|
|
100
|
+
* @returns {boolean}
|
|
101
|
+
*/
|
|
102
|
+
export function isTraceId(id) {
|
|
103
|
+
return typeof id === "string" && id.length === ULID_LEN && ULID_RE.test(id);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Normalise a context argument into a `{ trace_id, span }` shape. Accepts a bare
|
|
108
|
+
* id string, a partial context object, or null/undefined (mints a fresh trace).
|
|
109
|
+
* Always returns a valid context with a string `span` (defaults to "root").
|
|
110
|
+
*
|
|
111
|
+
* @param {string|{trace_id?:string, span?:string}|null|undefined} ctx
|
|
112
|
+
* @param {object} [deps] forwarded to mintTraceId when an id must be minted
|
|
113
|
+
* @returns {{trace_id: string, span: string}}
|
|
114
|
+
*/
|
|
115
|
+
export function toContext(ctx, deps = {}) {
|
|
116
|
+
if (typeof ctx === "string") {
|
|
117
|
+
return { trace_id: isTraceId(ctx) ? ctx : ctx, span: "root" };
|
|
118
|
+
}
|
|
119
|
+
if (ctx && typeof ctx === "object") {
|
|
120
|
+
const trace_id =
|
|
121
|
+
typeof ctx.trace_id === "string" && ctx.trace_id
|
|
122
|
+
? ctx.trace_id
|
|
123
|
+
: mintTraceId(deps);
|
|
124
|
+
const span = typeof ctx.span === "string" && ctx.span ? ctx.span : "root";
|
|
125
|
+
return { trace_id, span };
|
|
126
|
+
}
|
|
127
|
+
return { trace_id: mintTraceId(deps), span: "root" };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Run `fn` with `ctx` as the ambient trace context. `ctx` may be a bare
|
|
132
|
+
* trace-id string or a `{trace_id, span}` object; it is normalised first, and
|
|
133
|
+
* the normalised context is passed to `fn` so callers that minted inline still
|
|
134
|
+
* see the canonical shape. Works for sync and async `fn` (AsyncLocalStorage
|
|
135
|
+
* propagates across awaits). The return value of `fn` is returned verbatim.
|
|
136
|
+
*
|
|
137
|
+
* @template T
|
|
138
|
+
* @param {string|{trace_id?:string, span?:string}} ctx
|
|
139
|
+
* @param {(ctx: {trace_id: string, span: string}) => T} fn
|
|
140
|
+
* @param {object} [deps]
|
|
141
|
+
* @returns {T}
|
|
142
|
+
*/
|
|
143
|
+
export function withTrace(ctx, fn, deps = {}) {
|
|
144
|
+
const normalized = toContext(ctx, deps);
|
|
145
|
+
return STORE.run(normalized, () => fn(normalized));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* The current ambient trace context, or null when called outside any
|
|
150
|
+
* `withTrace`. Never throws.
|
|
151
|
+
* @returns {{trace_id: string, span: string}|null}
|
|
152
|
+
*/
|
|
153
|
+
export function currentTrace() {
|
|
154
|
+
return STORE.getStore() || null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* The current ambient `trace_id`, or null outside any `withTrace`.
|
|
159
|
+
* @returns {string|null}
|
|
160
|
+
*/
|
|
161
|
+
export function currentTraceId() {
|
|
162
|
+
const ctx = STORE.getStore();
|
|
163
|
+
return ctx ? ctx.trace_id : null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Derive a child context one hop deeper in the span path, preserving the
|
|
168
|
+
* trace_id. `parent` defaults to the ambient context; if there is none, a fresh
|
|
169
|
+
* trace is minted so the child is still correlatable to itself.
|
|
170
|
+
*
|
|
171
|
+
* Span paths are dotted, e.g. "root" → "root.classify" → "root.classify.send",
|
|
172
|
+
* giving a cheap, human-readable parent/child lattice without an OTel SDK.
|
|
173
|
+
*
|
|
174
|
+
* @param {string} name span segment (e.g. "classify", "dispatch", "send")
|
|
175
|
+
* @param {{trace_id?:string, span?:string}|null} [parent]
|
|
176
|
+
* @param {object} [deps]
|
|
177
|
+
* @returns {{trace_id: string, span: string}}
|
|
178
|
+
*/
|
|
179
|
+
export function childSpan(name, parent = currentTrace(), deps = {}) {
|
|
180
|
+
const base = toContext(parent, deps);
|
|
181
|
+
const seg = String(name || "span").replace(/[^A-Za-z0-9_-]/g, "_");
|
|
182
|
+
return { trace_id: base.trace_id, span: `${base.span}.${seg}` };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Serialize a trace context into plain header fields for crossing a boundary
|
|
187
|
+
* (spawned sub-process env, a queued inbox item, an HTTP relay). Defaults to the
|
|
188
|
+
* ambient context. Returns an empty object when there is no context to carry, so
|
|
189
|
+
* callers can spread it unconditionally.
|
|
190
|
+
*
|
|
191
|
+
* @param {{trace_id?:string, span?:string}|null} [ctx]
|
|
192
|
+
* @returns {{trace_id?: string, span?: string}}
|
|
193
|
+
*/
|
|
194
|
+
export function traceHeaders(ctx = currentTrace()) {
|
|
195
|
+
if (!ctx || typeof ctx !== "object" || !ctx.trace_id) return {};
|
|
196
|
+
return { trace_id: ctx.trace_id, span: ctx.span || "root" };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Reconstruct a trace context from header-ish fields received across a boundary.
|
|
201
|
+
* Returns null when no usable trace id is present (caller may then mint a fresh
|
|
202
|
+
* one), so an un-propagated hop degrades to "new trace" rather than crashing.
|
|
203
|
+
*
|
|
204
|
+
* @param {{trace_id?: string, span?: string}|null|undefined} headers
|
|
205
|
+
* @returns {{trace_id: string, span: string}|null}
|
|
206
|
+
*/
|
|
207
|
+
export function traceFromHeaders(headers) {
|
|
208
|
+
if (!headers || typeof headers !== "object") return null;
|
|
209
|
+
const id = headers.trace_id;
|
|
210
|
+
if (typeof id !== "string" || !id) return null;
|
|
211
|
+
const span =
|
|
212
|
+
typeof headers.span === "string" && headers.span ? headers.span : "root";
|
|
213
|
+
return { trace_id: id, span };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export const TRACE_ID_LENGTH = ULID_LEN;
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* trace.test.mjs — node:test coverage for the trace/correlation spine.
|
|
3
|
+
*
|
|
4
|
+
* Hermetic: the clock and RNG are injected so minted ids are deterministic, and
|
|
5
|
+
* AsyncLocalStorage propagation is exercised across real awaits.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { test } from "node:test";
|
|
9
|
+
import assert from "node:assert/strict";
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
mintTraceId,
|
|
13
|
+
isTraceId,
|
|
14
|
+
toContext,
|
|
15
|
+
withTrace,
|
|
16
|
+
currentTrace,
|
|
17
|
+
currentTraceId,
|
|
18
|
+
childSpan,
|
|
19
|
+
traceHeaders,
|
|
20
|
+
traceFromHeaders,
|
|
21
|
+
TRACE_ID_LENGTH,
|
|
22
|
+
} from "./trace.mjs";
|
|
23
|
+
|
|
24
|
+
// A deterministic 10-byte RNG.
|
|
25
|
+
function fixedRng(byte) {
|
|
26
|
+
return () => Buffer.alloc(10, byte);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// mintTraceId
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
test("mintTraceId returns a 26-char ULID-shaped id", () => {
|
|
34
|
+
const id = mintTraceId();
|
|
35
|
+
assert.equal(id.length, TRACE_ID_LENGTH);
|
|
36
|
+
assert.equal(id.length, 26);
|
|
37
|
+
assert.ok(isTraceId(id), `expected ${id} to be a valid trace id`);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("mintTraceId is deterministic under injected clock + rng", () => {
|
|
41
|
+
const deps = { now: () => 1_700_000_000_000, rng: fixedRng(0x00) };
|
|
42
|
+
const a = mintTraceId(deps);
|
|
43
|
+
const b = mintTraceId(deps);
|
|
44
|
+
assert.equal(a, b, "same clock + rng must yield the same id");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("mintTraceId is time-sortable: later timestamp sorts after earlier", () => {
|
|
48
|
+
const early = mintTraceId({ now: () => 1_000_000_000_000, rng: fixedRng(0xff) });
|
|
49
|
+
const late = mintTraceId({ now: () => 2_000_000_000_000, rng: fixedRng(0x00) });
|
|
50
|
+
assert.ok(early < late, "earlier-minted id should sort lexicographically first");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("mintTraceId randomness varies the tail for the same clock", () => {
|
|
54
|
+
const now = () => 1_700_000_000_000;
|
|
55
|
+
const a = mintTraceId({ now, rng: fixedRng(0x01) });
|
|
56
|
+
const b = mintTraceId({ now, rng: fixedRng(0x02) });
|
|
57
|
+
assert.notEqual(a, b);
|
|
58
|
+
// Same timestamp head, different random tail.
|
|
59
|
+
assert.equal(a.slice(0, 10), b.slice(0, 10));
|
|
60
|
+
assert.notEqual(a.slice(10), b.slice(10));
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("two real mints are unique", () => {
|
|
64
|
+
const ids = new Set();
|
|
65
|
+
for (let i = 0; i < 1000; i++) ids.add(mintTraceId());
|
|
66
|
+
assert.equal(ids.size, 1000);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
// isTraceId
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
test("isTraceId rejects malformed inputs", () => {
|
|
74
|
+
assert.equal(isTraceId(""), false);
|
|
75
|
+
assert.equal(isTraceId(null), false);
|
|
76
|
+
assert.equal(isTraceId(undefined), false);
|
|
77
|
+
assert.equal(isTraceId(123), false);
|
|
78
|
+
assert.equal(isTraceId("short"), false);
|
|
79
|
+
assert.equal(isTraceId("a".repeat(26)), false); // lowercase not in alphabet
|
|
80
|
+
assert.equal(isTraceId("I".repeat(26)), false); // I is excluded from Crockford
|
|
81
|
+
assert.equal(isTraceId(mintTraceId()), true);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
// toContext
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
test("toContext normalizes a bare id string into {trace_id, span}", () => {
|
|
89
|
+
const id = mintTraceId();
|
|
90
|
+
const ctx = toContext(id);
|
|
91
|
+
assert.equal(ctx.trace_id, id);
|
|
92
|
+
assert.equal(ctx.span, "root");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("toContext mints a fresh trace when given null/undefined", () => {
|
|
96
|
+
const a = toContext(null);
|
|
97
|
+
const b = toContext(undefined);
|
|
98
|
+
assert.ok(isTraceId(a.trace_id));
|
|
99
|
+
assert.ok(isTraceId(b.trace_id));
|
|
100
|
+
assert.notEqual(a.trace_id, b.trace_id);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("toContext preserves a provided span", () => {
|
|
104
|
+
const ctx = toContext({ trace_id: "X", span: "root.classify" });
|
|
105
|
+
assert.equal(ctx.trace_id, "X");
|
|
106
|
+
assert.equal(ctx.span, "root.classify");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
// withTrace / currentTrace propagation
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
|
|
113
|
+
test("currentTrace is null outside any withTrace", () => {
|
|
114
|
+
assert.equal(currentTrace(), null);
|
|
115
|
+
assert.equal(currentTraceId(), null);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("withTrace exposes the context to synchronous code", () => {
|
|
119
|
+
const id = mintTraceId();
|
|
120
|
+
const seen = withTrace(id, () => currentTraceId());
|
|
121
|
+
assert.equal(seen, id);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("withTrace passes the normalized context to fn", () => {
|
|
125
|
+
const id = mintTraceId();
|
|
126
|
+
const ctx = withTrace(id, (c) => c);
|
|
127
|
+
assert.deepEqual(ctx, { trace_id: id, span: "root" });
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("withTrace propagates across awaits (async hooks)", async () => {
|
|
131
|
+
const id = mintTraceId();
|
|
132
|
+
const seen = await withTrace(id, async () => {
|
|
133
|
+
await new Promise((r) => setTimeout(r, 1));
|
|
134
|
+
await Promise.resolve();
|
|
135
|
+
return currentTraceId();
|
|
136
|
+
});
|
|
137
|
+
assert.equal(seen, id);
|
|
138
|
+
// Context does not leak after the run.
|
|
139
|
+
assert.equal(currentTrace(), null);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("withTrace nests: inner context restores to outer on exit", () => {
|
|
143
|
+
const outer = mintTraceId();
|
|
144
|
+
const inner = mintTraceId();
|
|
145
|
+
withTrace(outer, () => {
|
|
146
|
+
assert.equal(currentTraceId(), outer);
|
|
147
|
+
withTrace(inner, () => {
|
|
148
|
+
assert.equal(currentTraceId(), inner);
|
|
149
|
+
});
|
|
150
|
+
assert.equal(currentTraceId(), outer);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// ---------------------------------------------------------------------------
|
|
155
|
+
// childSpan
|
|
156
|
+
// ---------------------------------------------------------------------------
|
|
157
|
+
|
|
158
|
+
test("childSpan extends the span path and keeps the trace_id", () => {
|
|
159
|
+
const id = mintTraceId();
|
|
160
|
+
withTrace(id, () => {
|
|
161
|
+
const c = childSpan("classify");
|
|
162
|
+
assert.equal(c.trace_id, id);
|
|
163
|
+
assert.equal(c.span, "root.classify");
|
|
164
|
+
const cc = childSpan("send", c);
|
|
165
|
+
assert.equal(cc.trace_id, id);
|
|
166
|
+
assert.equal(cc.span, "root.classify.send");
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test("childSpan sanitizes unsafe segment chars", () => {
|
|
171
|
+
const c = childSpan("a/b c.d", { trace_id: "T", span: "root" });
|
|
172
|
+
assert.equal(c.span, "root.a_b_c_d");
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test("childSpan with no ambient context mints a fresh trace", () => {
|
|
176
|
+
const c = childSpan("dispatch");
|
|
177
|
+
assert.ok(isTraceId(c.trace_id));
|
|
178
|
+
assert.equal(c.span, "root.dispatch");
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
// traceHeaders / traceFromHeaders — boundary crossing
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
|
|
185
|
+
test("traceHeaders serializes the ambient context", () => {
|
|
186
|
+
const id = mintTraceId();
|
|
187
|
+
const headers = withTrace({ trace_id: id, span: "root.dispatch" }, () =>
|
|
188
|
+
traceHeaders()
|
|
189
|
+
);
|
|
190
|
+
assert.deepEqual(headers, { trace_id: id, span: "root.dispatch" });
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test("traceHeaders returns {} when there is no context", () => {
|
|
194
|
+
assert.deepEqual(traceHeaders(), {});
|
|
195
|
+
assert.deepEqual(traceHeaders(null), {});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test("traceFromHeaders round-trips a serialized context", () => {
|
|
199
|
+
const id = mintTraceId();
|
|
200
|
+
const headers = { trace_id: id, span: "root.session" };
|
|
201
|
+
const ctx = traceFromHeaders(headers);
|
|
202
|
+
assert.deepEqual(ctx, { trace_id: id, span: "root.session" });
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test("traceFromHeaders returns null on missing/garbage input", () => {
|
|
206
|
+
assert.equal(traceFromHeaders(null), null);
|
|
207
|
+
assert.equal(traceFromHeaders(undefined), null);
|
|
208
|
+
assert.equal(traceFromHeaders({}), null);
|
|
209
|
+
assert.equal(traceFromHeaders({ span: "root" }), null);
|
|
210
|
+
assert.equal(traceFromHeaders("nope"), null);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test("traceFromHeaders defaults span to root when absent", () => {
|
|
214
|
+
const id = mintTraceId();
|
|
215
|
+
const ctx = traceFromHeaders({ trace_id: id });
|
|
216
|
+
assert.equal(ctx.span, "root");
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
// ---------------------------------------------------------------------------
|
|
220
|
+
// End-to-end: one trace_id carried item -> classify -> dispatch -> send
|
|
221
|
+
// ---------------------------------------------------------------------------
|
|
222
|
+
|
|
223
|
+
test("a single trace_id correlates a full interaction across hops", async () => {
|
|
224
|
+
const traceId = mintTraceId();
|
|
225
|
+
const spansSeen = [];
|
|
226
|
+
|
|
227
|
+
// item/tick mints the trace and opens the root context.
|
|
228
|
+
await withTrace(traceId, async () => {
|
|
229
|
+
spansSeen.push(currentTrace().span); // root
|
|
230
|
+
|
|
231
|
+
// classify (sync hop)
|
|
232
|
+
const classifyCtx = childSpan("classify");
|
|
233
|
+
await withTrace(classifyCtx, async () => {
|
|
234
|
+
spansSeen.push(currentTrace().span);
|
|
235
|
+
assert.equal(currentTraceId(), traceId);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// dispatch -> serialize across a (simulated) process boundary
|
|
239
|
+
const dispatchCtx = childSpan("dispatch");
|
|
240
|
+
const headers = traceHeaders(dispatchCtx);
|
|
241
|
+
|
|
242
|
+
// session: reconstruct from headers in the "child process"
|
|
243
|
+
const sessionCtx = traceFromHeaders(headers);
|
|
244
|
+
await withTrace(childSpan("send", sessionCtx), async () => {
|
|
245
|
+
spansSeen.push(currentTrace().span);
|
|
246
|
+
assert.equal(currentTraceId(), traceId);
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
assert.deepEqual(spansSeen, ["root", "root.classify", "root.dispatch.send"]);
|
|
251
|
+
});
|