@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,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/org/cohort-client.mjs — RETIRED shim.
|
|
3
|
+
*
|
|
4
|
+
* This module USED to speak a defunct contract (PUT `${apiUrl}/{orgId}/agents/{id}`,
|
|
5
|
+
* POST `${base}/agents/{id}/status`, GET `${base}/agents`) that hq's /api/v1 RPC
|
|
6
|
+
* surface NEVER exposed. It is now a thin re-export of the aligned client in
|
|
7
|
+
* lib/org/client.mjs so the published export `@cohortapp/agent-sdk/org/cohort-client`
|
|
8
|
+
* keeps resolving for any external importer, while the wire behaviour is the
|
|
9
|
+
* correct one:
|
|
10
|
+
* - publishEntry → registry.register (POST /api/v1/registry.register)
|
|
11
|
+
* - fetchDirectory → GET /api/v1/directory
|
|
12
|
+
* - isEnabled → the client's enablement check
|
|
13
|
+
* - heartbeat → presence.beat liveness (the old per-agent status POST is
|
|
14
|
+
* fully superseded; presence.beat rides the status snapshot,
|
|
15
|
+
* driven by lib/org/mesh.mjs through the same client).
|
|
16
|
+
*
|
|
17
|
+
* Fail-open is preserved end-to-end by client.mjs (benign value on error/disabled,
|
|
18
|
+
* never a throw). `fetchImpl` stays injectable for tests. New code SHOULD import
|
|
19
|
+
* from "./client.mjs" directly; this shim exists only for back-compat.
|
|
20
|
+
*
|
|
21
|
+
* @module lib/org/cohort-client
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
"use strict";
|
|
25
|
+
|
|
26
|
+
export { isEnabled, publishEntry, fetchDirectory } from "./client.mjs";
|
|
27
|
+
|
|
28
|
+
import { configFromAgent, isEnabled, presenceBeat } from "./client.mjs";
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Heartbeat this agent's liveness. The legacy per-agent status POST
|
|
32
|
+
* (`${base}/agents/{id}/status`) has NO equivalent on hq; liveness is the
|
|
33
|
+
* presence.beat snapshot. This shim maps the old `{cfg,id,status,focus,presence}`
|
|
34
|
+
* call onto presence.beat (strict status snapshot subset) so external callers of
|
|
35
|
+
* the old export keep working. New code should use lib/org/mesh.mjs (which beats
|
|
36
|
+
* the full telemetry snapshot) or client.presenceBeat directly. Fail-open.
|
|
37
|
+
*
|
|
38
|
+
* @param {object} o - { cfg, id?, status?, focus?, presence?, fetchImpl? }
|
|
39
|
+
* @returns {Promise<{ok:boolean, status?:number, skipped?:boolean, reason?:string}>}
|
|
40
|
+
*/
|
|
41
|
+
export async function heartbeat(o = {}) {
|
|
42
|
+
const { cfg } = o;
|
|
43
|
+
if (!isEnabled(cfg)) return { ok: false, skipped: true };
|
|
44
|
+
const c = configFromAgent(cfg);
|
|
45
|
+
// Build a minimal, strict-schema-safe status snapshot. presence.beat's status
|
|
46
|
+
// is z.object().strict() over {state,activity,subAgentsRunning,machine,
|
|
47
|
+
// claudeAuth,alerts,...}; keep this a conservative subset.
|
|
48
|
+
const state = typeof o.status === "string" && o.status ? o.status : "active";
|
|
49
|
+
const params = {
|
|
50
|
+
focus: o.focus || [],
|
|
51
|
+
status: {
|
|
52
|
+
state,
|
|
53
|
+
activity: "",
|
|
54
|
+
subAgentsRunning: 0,
|
|
55
|
+
machine: "",
|
|
56
|
+
claudeAuth: "unknown",
|
|
57
|
+
alerts: [],
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
const frame = await presenceBeat(params, {
|
|
61
|
+
base: c.base,
|
|
62
|
+
token: c.token,
|
|
63
|
+
orgId: c.orgId,
|
|
64
|
+
fetchImpl: o.fetchImpl,
|
|
65
|
+
});
|
|
66
|
+
return { ok: !!(frame && frame.ok), status: frame && frame.ok ? 200 : 0 };
|
|
67
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cohort-client.test.mjs — RETIRED shim re-exporting the aligned client.mjs.
|
|
3
|
+
*
|
|
4
|
+
* The old module spoke a defunct contract (PUT `${apiUrl}/{orgId}/agents/{id}`,
|
|
5
|
+
* POST `${base}/agents/{id}/status`, GET `${base}/agents`). It is now a thin
|
|
6
|
+
* re-export of lib/org/client.mjs, so these tests assert the NEW wire shape:
|
|
7
|
+
* - publishEntry → POST /v1/registry.register (Bearer + x-idempotency-key)
|
|
8
|
+
* - fetchDirectory → GET /v1/directory (parses members/agents/array shapes)
|
|
9
|
+
* - heartbeat → POST /v1/presence.beat (liveness snapshot)
|
|
10
|
+
* and that the OLD `/{orgId}/agents` endpoints are NEVER hit.
|
|
11
|
+
* Run: node --test lib/org/cohort-client.test.mjs
|
|
12
|
+
*/
|
|
13
|
+
"use strict";
|
|
14
|
+
|
|
15
|
+
import { test } from "node:test";
|
|
16
|
+
import assert from "node:assert/strict";
|
|
17
|
+
|
|
18
|
+
import { isEnabled, publishEntry, heartbeat, fetchDirectory } from "./cohort-client.mjs";
|
|
19
|
+
import { PROTOCOL_VERSION } from "./protocol.mjs";
|
|
20
|
+
|
|
21
|
+
// Ambient COHORT_* creds would leak into header assertions; clear them so the
|
|
22
|
+
// tests are hermetic regardless of host env.
|
|
23
|
+
delete process.env.COHORT_API_TOKEN;
|
|
24
|
+
delete process.env.COHORT_TOKEN;
|
|
25
|
+
delete process.env.COHORT_API_KEY;
|
|
26
|
+
delete process.env.COHORT_ORG_ID;
|
|
27
|
+
|
|
28
|
+
const BASE = "https://org.cohortapp.com";
|
|
29
|
+
// New enablement contract: enabled + (base || apiUrl+orgId).
|
|
30
|
+
const ON = { org: { cohort: { enabled: true, base: BASE, token: "tok-xyz" } } };
|
|
31
|
+
const OFF = { org: { cohort: { enabled: false, base: BASE } } };
|
|
32
|
+
|
|
33
|
+
/** A fake fetch that records calls and returns a canned res frame. */
|
|
34
|
+
function fakeFetch(responder) {
|
|
35
|
+
const calls = [];
|
|
36
|
+
const impl = async (url, init) => {
|
|
37
|
+
calls.push({ url, init, headers: (init && init.headers) || {} });
|
|
38
|
+
const r = responder ? responder(url, init, calls.length - 1) : null;
|
|
39
|
+
const status = r && typeof r.status === "number" ? r.status : 200;
|
|
40
|
+
const ok = r && typeof r.ok === "boolean" ? r.ok : status >= 200 && status < 300;
|
|
41
|
+
const body = r ? r.body : { ok: true, result: null };
|
|
42
|
+
return { ok, status, headers: { get: () => undefined }, json: async () => body };
|
|
43
|
+
};
|
|
44
|
+
impl.calls = calls;
|
|
45
|
+
return impl;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
test("isEnabled: requires enabled + (base or apiUrl+orgId)", () => {
|
|
49
|
+
assert.equal(isEnabled(ON), true);
|
|
50
|
+
assert.equal(isEnabled(OFF), false);
|
|
51
|
+
assert.equal(isEnabled({ org: { cohort: { enabled: true, apiUrl: "x", orgId: "o" } } }), true);
|
|
52
|
+
assert.equal(isEnabled(null), false);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("publishEntry: no-op disabled; POSTs registry.register (NOT /agents) when enabled", async () => {
|
|
56
|
+
assert.deepEqual(await publishEntry({ cfg: OFF, entry: { id: "a" } }), { ok: false, skipped: true });
|
|
57
|
+
const f = fakeFetch(() => ({ body: { ok: true, result: null } }));
|
|
58
|
+
const r = await publishEntry({ cfg: ON, entry: { id: "alex-ai", name: "Alex" }, fetchImpl: f });
|
|
59
|
+
assert.equal(r.ok, true);
|
|
60
|
+
assert.equal(f.calls[0].url, `${BASE}/api/v1/registry.register`);
|
|
61
|
+
assert.equal(f.calls[0].init.method, "POST");
|
|
62
|
+
assert.equal(f.calls[0].headers.authorization, "Bearer tok-xyz");
|
|
63
|
+
assert.equal(f.calls[0].headers["x-org-protocol"], String(PROTOCOL_VERSION));
|
|
64
|
+
assert.equal(f.calls[0].headers["x-idempotency-key"], "registry.register:alex-ai");
|
|
65
|
+
// the OLD /{orgId}/agents endpoint must NEVER be hit
|
|
66
|
+
assert.doesNotMatch(f.calls[0].url, /\/agents\//);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("publishEntry: trims a rich entry to the strict registerSchema (archetype object → string)", async () => {
|
|
70
|
+
const f = fakeFetch(() => ({ body: { ok: true, result: null } }));
|
|
71
|
+
const entry = {
|
|
72
|
+
id: "robin-ai",
|
|
73
|
+
name: "Robin",
|
|
74
|
+
fullName: "Robin Vega",
|
|
75
|
+
archetype: { function: "engineering", altitude: "vp", label: "VP Eng" },
|
|
76
|
+
principal: { fullName: "Sam Human", title: "CEO" },
|
|
77
|
+
towers: [{ id: "t1", name: "Platform" }],
|
|
78
|
+
focus: [{ title: "ship the thing" }],
|
|
79
|
+
};
|
|
80
|
+
await publishEntry({ cfg: ON, entry, fetchImpl: f });
|
|
81
|
+
const sent = JSON.parse(f.calls[0].init.body);
|
|
82
|
+
assert.equal(sent.displayName, "Robin Vega");
|
|
83
|
+
assert.equal(sent.archetype, "VP Eng");
|
|
84
|
+
assert.equal(sent.humanSponsor, "Sam Human");
|
|
85
|
+
// the full rich entry rides under `card` (server chain-redacts to hasCard)
|
|
86
|
+
assert.equal(sent.card.id, "robin-ai");
|
|
87
|
+
assert.deepEqual(sent.card.archetype, entry.archetype);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("publishEntry: server error frame → {ok:false} (fail-open shape preserved)", async () => {
|
|
91
|
+
const f = fakeFetch(() => ({ ok: false, status: 500, body: { ok: false, error: { code: "INTERNAL", message: "boom" } } }));
|
|
92
|
+
const r = await publishEntry({ cfg: ON, entry: { id: "x" }, fetchImpl: f });
|
|
93
|
+
assert.equal(r.ok, false);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("heartbeat: no-op disabled; POSTs presence.beat (NOT /agents/{id}/status) when enabled", async () => {
|
|
97
|
+
assert.deepEqual(await heartbeat({ cfg: OFF, id: "a" }), { ok: false, skipped: true });
|
|
98
|
+
const f = fakeFetch(() => ({ body: { ok: true, result: { directives: [] } } }));
|
|
99
|
+
const r = await heartbeat({ cfg: ON, id: "alex-ai", status: "active", fetchImpl: f });
|
|
100
|
+
assert.equal(r.ok, true);
|
|
101
|
+
assert.equal(f.calls[0].url, `${BASE}/api/v1/presence.beat`);
|
|
102
|
+
assert.equal(f.calls[0].init.method, "POST");
|
|
103
|
+
assert.doesNotMatch(f.calls[0].url, /\/agents\//);
|
|
104
|
+
const sent = JSON.parse(f.calls[0].init.body);
|
|
105
|
+
assert.equal(sent.status.state, "active");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("fetchDirectory: [] disabled; parses array + {members:[]} + {agents:[]} shapes", async () => {
|
|
109
|
+
assert.deepEqual(await fetchDirectory({ cfg: OFF }), []);
|
|
110
|
+
const arr = await fetchDirectory({ cfg: ON, fetchImpl: fakeFetch(() => ({ body: [{ id: "x" }] })) });
|
|
111
|
+
assert.equal(arr.length, 1);
|
|
112
|
+
const members = await fetchDirectory({ cfg: ON, fetchImpl: fakeFetch(() => ({ body: { members: [{ id: "m1" }, { id: "m2" }] } })) });
|
|
113
|
+
assert.equal(members.length, 2);
|
|
114
|
+
const agents = await fetchDirectory({ cfg: ON, fetchImpl: fakeFetch(() => ({ body: { agents: [{ id: "y" }] } })) });
|
|
115
|
+
assert.equal(agents.length, 1);
|
|
116
|
+
// GET /v1/directory, never the old /{orgId}/agents
|
|
117
|
+
const f = fakeFetch(() => ({ body: { members: [] } }));
|
|
118
|
+
await fetchDirectory({ cfg: ON, fetchImpl: f });
|
|
119
|
+
assert.equal(f.calls[0].url, `${BASE}/api/v1/directory`);
|
|
120
|
+
assert.equal(f.calls[0].init.method, "GET");
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("fetchDirectory: network error → [] (fail-open)", async () => {
|
|
124
|
+
const arr = await fetchDirectory({ cfg: ON, fetchImpl: async () => { throw new Error("network down"); } });
|
|
125
|
+
assert.deepEqual(arr, []);
|
|
126
|
+
});
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/org/cost-sync.mjs — ship the maestro router ledger to Cohort cost.report.
|
|
3
|
+
*
|
|
4
|
+
* The agent reports its per-session model spend into the org-wide rollup (SPEC
|
|
5
|
+
* §4.8 cost telemetry). The source of truth is the local router ledger written by
|
|
6
|
+
* lib/model-router/ledger.mjs to `state/cost-tracking/<date>.jsonl` (one JSON row
|
|
7
|
+
* per session: agent, model, input_tokens, output_tokens, estimated_usd,
|
|
8
|
+
* decision_id, cadence/source). This module reads today's rows and POSTs them to
|
|
9
|
+
* `cost.report`, batched and IDEMPOTENT on each row's id so a re-run never
|
|
10
|
+
* double-counts — the cadence (`org-sync`) can fire this every tick safely.
|
|
11
|
+
*
|
|
12
|
+
* Reconciliation: the ledger is local-first and remains the budget-guard SoT
|
|
13
|
+
* (budget-guard sums `estimated_usd` from the SAME file). cost.report only
|
|
14
|
+
* MIRRORS those rows into the org rollup; it never mutates the local ledger, so
|
|
15
|
+
* the local and org views always reconcile by construction. `reportLedger`
|
|
16
|
+
* returns the local total alongside the per-row push results so a caller can
|
|
17
|
+
* assert ledger.localUSD == rollup contribution.
|
|
18
|
+
*
|
|
19
|
+
* G16 (zero-LLM-in-the-coordination-loop): this is a pure file-read + HTTP push.
|
|
20
|
+
* It makes ZERO LLM calls. Critically, it preserves each row's `source`/`cadence`
|
|
21
|
+
* field on the wire so the org rollup can attribute LLM spend to a coordination
|
|
22
|
+
* cadence — that attribution is exactly what makes a G16 violation VISIBLE in
|
|
23
|
+
* `cost.rollup`. Do not strip `source`/`cadence` from the reported row.
|
|
24
|
+
*
|
|
25
|
+
* Idempotency key: derived per row from a stable identity (row.id ||
|
|
26
|
+
* `${decision_id}:${session_id}:${ts}` || a content hash). The server is
|
|
27
|
+
* idempotent on cost.report, so the same row reported twice is a no-op replay.
|
|
28
|
+
*
|
|
29
|
+
* Node builtins only. Fail-open (a down/disabled server reports nothing rather
|
|
30
|
+
* than throwing). `fetchImpl`, `now`, and `ledgerDir`/`agentRoot` are injectable
|
|
31
|
+
* for deterministic tests. ESM.
|
|
32
|
+
*
|
|
33
|
+
* @module lib/org/cost-sync
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
"use strict";
|
|
37
|
+
|
|
38
|
+
import { createHash } from "node:crypto";
|
|
39
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
40
|
+
import { join, resolve } from "node:path";
|
|
41
|
+
|
|
42
|
+
import { isEnabled, configFromAgent, costReport } from "./client.mjs";
|
|
43
|
+
|
|
44
|
+
const REPORT_BATCH = 100;
|
|
45
|
+
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// Ledger location (mirror of lib/model-router/ledger.mjs)
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
function ledgerDir(deps = {}) {
|
|
51
|
+
if (deps.ledgerDir) return resolve(deps.ledgerDir);
|
|
52
|
+
const root = deps.agentRoot || process.env.AGENT_ROOT || process.env.AGENT_DIR || process.cwd();
|
|
53
|
+
return join(resolve(root), "state", "cost-tracking");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function dayStr(deps = {}) {
|
|
57
|
+
if (deps.day) return String(deps.day);
|
|
58
|
+
const now = (typeof deps.now === "function" ? deps.now : Date.now)();
|
|
59
|
+
return new Date(now).toISOString().slice(0, 10);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function ledgerFile(deps = {}) {
|
|
63
|
+
return join(ledgerDir(deps), `${dayStr(deps)}.jsonl`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
// Read + normalise ledger rows
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Read the JSONL ledger rows for a day. NEVER throws — a missing/corrupt file or
|
|
72
|
+
* an unparseable line yields fewer rows, never an error. Pure over the fs.
|
|
73
|
+
*
|
|
74
|
+
* @param {object} deps - { ledgerDir?, agentRoot?, day?, now? }
|
|
75
|
+
* @returns {object[]} parsed ledger rows (in file order)
|
|
76
|
+
*/
|
|
77
|
+
export function readLedgerRows(deps = {}) {
|
|
78
|
+
const file = ledgerFile(deps);
|
|
79
|
+
if (!existsSync(file)) return [];
|
|
80
|
+
let body;
|
|
81
|
+
try { body = readFileSync(file, "utf-8"); } catch { return []; }
|
|
82
|
+
const rows = [];
|
|
83
|
+
for (const line of body.split("\n")) {
|
|
84
|
+
if (!line.trim()) continue;
|
|
85
|
+
try { rows.push(JSON.parse(line)); } catch { /* skip a corrupt line */ }
|
|
86
|
+
}
|
|
87
|
+
return rows;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Derive a stable idempotency identity for a ledger row: row.id when present,
|
|
92
|
+
* else a composite of the natural keys, else a content hash. Two reports of the
|
|
93
|
+
* SAME logical session collapse to one row in the rollup. Pure.
|
|
94
|
+
*
|
|
95
|
+
* @param {object} row
|
|
96
|
+
* @returns {string}
|
|
97
|
+
*/
|
|
98
|
+
export function rowId(row) {
|
|
99
|
+
if (!row || typeof row !== "object") return "";
|
|
100
|
+
if (row.id) return String(row.id);
|
|
101
|
+
if (row.row_id) return String(row.row_id);
|
|
102
|
+
const natural = [row.decision_id, row.session_id, row.ts].filter(Boolean).join(":");
|
|
103
|
+
if (natural) return natural;
|
|
104
|
+
// Last resort: hash the content so identical rows dedupe and distinct rows don't.
|
|
105
|
+
return "h:" + createHash("sha256").update(stableJson(row)).digest("hex").slice(0, 24);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function stableJson(value) {
|
|
109
|
+
const sortDeep = (v) => {
|
|
110
|
+
if (Array.isArray(v)) return v.map(sortDeep);
|
|
111
|
+
if (v && typeof v === "object") {
|
|
112
|
+
const out = {};
|
|
113
|
+
for (const k of Object.keys(v).sort()) out[k] = sortDeep(v[k]);
|
|
114
|
+
return out;
|
|
115
|
+
}
|
|
116
|
+
return v;
|
|
117
|
+
};
|
|
118
|
+
return JSON.stringify(sortDeep(value));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Project a ledger row onto the cost.report wire shape. Preserves the
|
|
123
|
+
* router-ledger fields the rollup aggregates by (agent / model / cadence|source /
|
|
124
|
+
* day) and the budget figure (estimated_usd). KEEPS `source`/`cadence` for the
|
|
125
|
+
* G16 attribution line. Pure.
|
|
126
|
+
*
|
|
127
|
+
* @param {object} row
|
|
128
|
+
* @param {object} deps - { day? }
|
|
129
|
+
* @returns {object} the wire row (with a stable `id`)
|
|
130
|
+
*/
|
|
131
|
+
export function toReportRow(row, deps = {}) {
|
|
132
|
+
const id = rowId(row);
|
|
133
|
+
const day = (row && row.ts && String(row.ts).slice(0, 10)) || dayStr(deps);
|
|
134
|
+
return {
|
|
135
|
+
id,
|
|
136
|
+
agent: row.agent ?? null,
|
|
137
|
+
model: row.model ?? null,
|
|
138
|
+
backend: row.backend ?? null,
|
|
139
|
+
input_tokens: numOr0(row.input_tokens),
|
|
140
|
+
output_tokens: numOr0(row.output_tokens),
|
|
141
|
+
cache_read_tokens: numOr0(row.cache_read_tokens),
|
|
142
|
+
cache_creation_tokens: numOr0(row.cache_creation_tokens),
|
|
143
|
+
estimated_usd: numOr0(row.estimated_usd),
|
|
144
|
+
total_cost_usd: row.total_cost_usd ?? null,
|
|
145
|
+
decision_id: row.decision_id ?? null,
|
|
146
|
+
session_id: row.session_id ?? null,
|
|
147
|
+
// G16: attribution fields — DO NOT STRIP. cadence/source surface coordination
|
|
148
|
+
// spend in the rollup so an LLM-in-the-loop violation is visible.
|
|
149
|
+
source: row.source ?? null,
|
|
150
|
+
cadence: row.cadence ?? row.lane ?? null,
|
|
151
|
+
task_class: row.task_class ?? null,
|
|
152
|
+
ts: row.ts ?? null,
|
|
153
|
+
day,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function numOr0(v) {
|
|
158
|
+
const n = Number(v);
|
|
159
|
+
return Number.isFinite(n) ? n : 0;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ---------------------------------------------------------------------------
|
|
163
|
+
// Report — batched, idempotent push to cost.report
|
|
164
|
+
// ---------------------------------------------------------------------------
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Read today's router ledger and report its rows to Cohort via cost.report,
|
|
168
|
+
* batched and idempotent. Cadence-friendly: safe to call every org-sync tick —
|
|
169
|
+
* the server's cost.report idempotency on each row's id collapses replays.
|
|
170
|
+
*
|
|
171
|
+
* Reconciles by returning the LOCAL ledger total alongside the push outcome so a
|
|
172
|
+
* caller can assert the org rollup contribution matches the local budget figure.
|
|
173
|
+
*
|
|
174
|
+
* Always resolves (fail-open): a disabled/unreachable server reports 0 rows.
|
|
175
|
+
*
|
|
176
|
+
* @param {object} o - {
|
|
177
|
+
* cfg, agentRoot?, ledgerDir?, day?, now?, batchSize?, fetchImpl?
|
|
178
|
+
* }
|
|
179
|
+
* @returns {Promise<{
|
|
180
|
+
* enabled:boolean, rowsRead:number, reported:number, failed:number,
|
|
181
|
+
* localUSD:number, batches:Array<{count:number, ok:boolean, code?:string}>
|
|
182
|
+
* }>}
|
|
183
|
+
*/
|
|
184
|
+
export async function reportLedger(o = {}) {
|
|
185
|
+
const rows = readLedgerRows(o);
|
|
186
|
+
const localUSD = round6(rows.reduce((s, r) => s + numOr0(r.estimated_usd), 0));
|
|
187
|
+
|
|
188
|
+
if (!isEnabled(o.cfg)) {
|
|
189
|
+
return { enabled: false, rowsRead: rows.length, reported: 0, failed: 0, localUSD, batches: [] };
|
|
190
|
+
}
|
|
191
|
+
if (rows.length === 0) {
|
|
192
|
+
return { enabled: true, rowsRead: 0, reported: 0, failed: 0, localUSD, batches: [] };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const c = configFromAgent(o.cfg);
|
|
196
|
+
const batchSize = Number.isFinite(o.batchSize) && o.batchSize > 0 ? Math.floor(o.batchSize) : REPORT_BATCH;
|
|
197
|
+
const wireRows = rows.map((r) => toReportRow(r, o));
|
|
198
|
+
|
|
199
|
+
let reported = 0;
|
|
200
|
+
let failed = 0;
|
|
201
|
+
const batches = [];
|
|
202
|
+
for (let i = 0; i < wireRows.length; i += batchSize) {
|
|
203
|
+
const slice = wireRows.slice(i, i + batchSize);
|
|
204
|
+
// Stable idempotency key over the batch's row ids so a re-pushed identical
|
|
205
|
+
// batch is a no-op replay (the server is also per-row idempotent on id).
|
|
206
|
+
const key = "cost.report:" + createHash("sha256").update(slice.map((r) => r.id).join("|")).digest("hex").slice(0, 24);
|
|
207
|
+
const frame = await costReport({ rows: slice }, {
|
|
208
|
+
base: c.base,
|
|
209
|
+
token: c.token,
|
|
210
|
+
idempotencyKey: key,
|
|
211
|
+
fetchImpl: o.fetchImpl,
|
|
212
|
+
});
|
|
213
|
+
const ok = !!(frame && frame.ok);
|
|
214
|
+
if (ok) reported += slice.length; else failed += slice.length;
|
|
215
|
+
batches.push({ count: slice.length, ok, code: ok ? undefined : (frame && frame.error && frame.error.code) || "error" });
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return { enabled: true, rowsRead: rows.length, reported, failed, localUSD, batches };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function round6(n) {
|
|
222
|
+
return Number(Number(n).toFixed(6));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export const _internals = { ledgerFile, ledgerDir, dayStr, stableJson, REPORT_BATCH };
|
|
226
|
+
|
|
227
|
+
export default { readLedgerRows, rowId, toReportRow, reportLedger };
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cost-sync.test.mjs — read today's router ledger and report rows to Cohort via
|
|
3
|
+
* cost.report, idempotently and batched, preserving the G16 attribution fields,
|
|
4
|
+
* reconciling with the local budget total. Tmp ledger dir + injected fetchImpl.
|
|
5
|
+
* Run: node --test lib/org/cost-sync.test.mjs
|
|
6
|
+
*/
|
|
7
|
+
"use strict";
|
|
8
|
+
|
|
9
|
+
import { test } from "node:test";
|
|
10
|
+
import assert from "node:assert/strict";
|
|
11
|
+
import { mkdtempSync, mkdirSync, writeFileSync } from "node:fs";
|
|
12
|
+
import { tmpdir } from "node:os";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
|
|
15
|
+
import { readLedgerRows, rowId, toReportRow, reportLedger } from "./cost-sync.mjs";
|
|
16
|
+
|
|
17
|
+
const BASE = "https://org.cohortapp.com";
|
|
18
|
+
const CFG = { org: { cohort: { enabled: true, base: BASE, token: "tok", orgId: "alex-ai" } } };
|
|
19
|
+
const OFF = { org: { cohort: { enabled: false } } };
|
|
20
|
+
const DAY = "2026-06-11";
|
|
21
|
+
|
|
22
|
+
delete process.env.COHORT_API_TOKEN;
|
|
23
|
+
delete process.env.COHORT_TOKEN;
|
|
24
|
+
|
|
25
|
+
/** Write a JSONL ledger file for a day under a fresh tmp ledger dir. Returns dir. */
|
|
26
|
+
function tmpLedger(rows, day = DAY) {
|
|
27
|
+
const dir = mkdtempSync(join(tmpdir(), "cost-sync-"));
|
|
28
|
+
mkdirSync(dir, { recursive: true });
|
|
29
|
+
const body = rows.map((r) => JSON.stringify(r)).join("\n") + "\n";
|
|
30
|
+
writeFileSync(join(dir, `${day}.jsonl`), body);
|
|
31
|
+
return dir;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function fakeFetch(responder) {
|
|
35
|
+
const calls = [];
|
|
36
|
+
const impl = async (url, init) => {
|
|
37
|
+
calls.push({ url, init, headers: (init && init.headers) || {}, body: init && init.body ? JSON.parse(init.body) : null });
|
|
38
|
+
const r = responder ? responder(url, init, calls.length - 1) : null;
|
|
39
|
+
const status = r && typeof r.status === "number" ? r.status : 200;
|
|
40
|
+
const ok = r && typeof r.ok === "boolean" ? r.ok : status >= 200 && status < 300;
|
|
41
|
+
const body = r ? r.body : { ok: true, result: null };
|
|
42
|
+
return { ok, status, headers: { get: () => undefined }, json: async () => body };
|
|
43
|
+
};
|
|
44
|
+
impl.calls = calls;
|
|
45
|
+
return impl;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const SAMPLE_ROWS = [
|
|
49
|
+
{ ts: `${DAY}T01:00:00.000Z`, agent: "alex", model: "claude-opus-4", input_tokens: 1000, output_tokens: 200, estimated_usd: 0.05, decision_id: "d-1", session_id: "s-1", source: "router", cadence: null },
|
|
50
|
+
{ ts: `${DAY}T02:00:00.000Z`, agent: "alex", model: "claude-haiku", input_tokens: 500, output_tokens: 50, estimated_usd: 0.001, session_id: "s-2", source: "cadence", lane: "org-sync" },
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
// --- read + normalise ------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
test("readLedgerRows: parses JSONL, skips blank/corrupt lines, [] when missing", () => {
|
|
56
|
+
const dir = tmpLedger(SAMPLE_ROWS);
|
|
57
|
+
const rows = readLedgerRows({ ledgerDir: dir, day: DAY });
|
|
58
|
+
assert.equal(rows.length, 2);
|
|
59
|
+
assert.equal(rows[0].decision_id, "d-1");
|
|
60
|
+
// missing file → []
|
|
61
|
+
assert.deepEqual(readLedgerRows({ ledgerDir: dir, day: "1999-01-01" }), []);
|
|
62
|
+
// corrupt line is skipped
|
|
63
|
+
const dir2 = mkdtempSync(join(tmpdir(), "cost-sync-corrupt-"));
|
|
64
|
+
writeFileSync(join(dir2, `${DAY}.jsonl`), '{"agent":"a","estimated_usd":1}\nnot json\n\n{"agent":"b","estimated_usd":2}\n');
|
|
65
|
+
assert.equal(readLedgerRows({ ledgerDir: dir2, day: DAY }).length, 2);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("rowId: stable identity (id > natural keys > content hash)", () => {
|
|
69
|
+
assert.equal(rowId({ id: "explicit" }), "explicit");
|
|
70
|
+
assert.equal(rowId({ decision_id: "d", session_id: "s", ts: "t" }), "d:s:t");
|
|
71
|
+
const h1 = rowId({ agent: "a", estimated_usd: 1 });
|
|
72
|
+
const h2 = rowId({ estimated_usd: 1, agent: "a" }); // key order independent
|
|
73
|
+
assert.equal(h1, h2);
|
|
74
|
+
assert.notEqual(rowId({ agent: "a" }), rowId({ agent: "b" }));
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("toReportRow: preserves the G16 attribution fields (source/cadence) + budget figure", () => {
|
|
78
|
+
const wire = toReportRow(SAMPLE_ROWS[1], { day: DAY });
|
|
79
|
+
assert.equal(wire.source, "cadence");
|
|
80
|
+
assert.equal(wire.cadence, "org-sync"); // falls back to `lane`
|
|
81
|
+
assert.equal(wire.estimated_usd, 0.001);
|
|
82
|
+
assert.equal(wire.day, DAY);
|
|
83
|
+
assert.ok(wire.id);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// --- reportLedger: idempotent batched push ---------------------------------
|
|
87
|
+
|
|
88
|
+
test("reportLedger: posts rows to cost.report, reconciles local USD total", async () => {
|
|
89
|
+
const dir = tmpLedger(SAMPLE_ROWS);
|
|
90
|
+
const f = fakeFetch((url) => (url.endsWith("/v1/cost.report") ? { body: { ok: true, result: { ingested: 2 } } } : null));
|
|
91
|
+
const out = await reportLedger({ cfg: CFG, ledgerDir: dir, day: DAY, fetchImpl: f });
|
|
92
|
+
assert.equal(out.enabled, true);
|
|
93
|
+
assert.equal(out.rowsRead, 2);
|
|
94
|
+
assert.equal(out.reported, 2);
|
|
95
|
+
assert.equal(out.failed, 0);
|
|
96
|
+
// reconciles with budget-guard's view (sum of estimated_usd)
|
|
97
|
+
assert.equal(out.localUSD, 0.051);
|
|
98
|
+
// one batch (default batch size), routed to cost.report
|
|
99
|
+
const reportCalls = f.calls.filter((c) => c.url.endsWith("/v1/cost.report"));
|
|
100
|
+
assert.equal(reportCalls.length, 1);
|
|
101
|
+
assert.equal(reportCalls[0].body.rows.length, 2);
|
|
102
|
+
// each wire row carried its stable id + attribution
|
|
103
|
+
assert.ok(reportCalls[0].body.rows[0].id);
|
|
104
|
+
assert.equal(reportCalls[0].body.rows[1].source, "cadence");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("reportLedger: idempotency key is stable across re-runs of the same rows", async () => {
|
|
108
|
+
const dir = tmpLedger(SAMPLE_ROWS);
|
|
109
|
+
const f1 = fakeFetch(() => ({ body: { ok: true, result: null } }));
|
|
110
|
+
const f2 = fakeFetch(() => ({ body: { ok: true, result: null } }));
|
|
111
|
+
await reportLedger({ cfg: CFG, ledgerDir: dir, day: DAY, fetchImpl: f1 });
|
|
112
|
+
await reportLedger({ cfg: CFG, ledgerDir: dir, day: DAY, fetchImpl: f2 });
|
|
113
|
+
const k1 = f1.calls.find((c) => c.url.endsWith("/v1/cost.report")).headers["x-idempotency-key"];
|
|
114
|
+
const k2 = f2.calls.find((c) => c.url.endsWith("/v1/cost.report")).headers["x-idempotency-key"];
|
|
115
|
+
assert.ok(k1);
|
|
116
|
+
assert.equal(k1, k2); // same rows → same key → server collapses the replay
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("reportLedger: batches by batchSize", async () => {
|
|
120
|
+
const many = Array.from({ length: 5 }, (_, i) => ({ id: `r-${i}`, agent: "a", estimated_usd: 0.01 }));
|
|
121
|
+
const dir = tmpLedger(many);
|
|
122
|
+
const f = fakeFetch(() => ({ body: { ok: true, result: null } }));
|
|
123
|
+
const out = await reportLedger({ cfg: CFG, ledgerDir: dir, day: DAY, batchSize: 2, fetchImpl: f });
|
|
124
|
+
assert.equal(out.reported, 5);
|
|
125
|
+
assert.equal(out.batches.length, 3); // 2 + 2 + 1
|
|
126
|
+
assert.deepEqual(out.batches.map((b) => b.count), [2, 2, 1]);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("reportLedger: disabled org → reports nothing but still reconciles local total", async () => {
|
|
130
|
+
const dir = tmpLedger(SAMPLE_ROWS);
|
|
131
|
+
const out = await reportLedger({ cfg: OFF, ledgerDir: dir, day: DAY });
|
|
132
|
+
assert.equal(out.enabled, false);
|
|
133
|
+
assert.equal(out.reported, 0);
|
|
134
|
+
assert.equal(out.localUSD, 0.051);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("reportLedger: server error → rows counted as failed, never throws", async () => {
|
|
138
|
+
const dir = tmpLedger(SAMPLE_ROWS);
|
|
139
|
+
const f = fakeFetch(() => ({ ok: false, status: 500, body: { ok: false, error: { code: "INTERNAL", message: "boom" } } }));
|
|
140
|
+
const out = await reportLedger({ cfg: CFG, ledgerDir: dir, day: DAY, fetchImpl: f });
|
|
141
|
+
assert.equal(out.reported, 0);
|
|
142
|
+
assert.equal(out.failed, 2);
|
|
143
|
+
assert.equal(out.batches[0].code, "INTERNAL");
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("reportLedger: empty ledger → no call, zero totals", async () => {
|
|
147
|
+
const dir = mkdtempSync(join(tmpdir(), "cost-sync-empty-"));
|
|
148
|
+
const f = fakeFetch(() => ({ body: { ok: true, result: null } }));
|
|
149
|
+
const out = await reportLedger({ cfg: CFG, ledgerDir: dir, day: DAY, fetchImpl: f });
|
|
150
|
+
assert.equal(out.rowsRead, 0);
|
|
151
|
+
assert.equal(out.reported, 0);
|
|
152
|
+
assert.equal(f.calls.length, 0);
|
|
153
|
+
});
|