@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.
Files changed (731) hide show
  1. package/.claude/commands/init-agent.md +104 -0
  2. package/.claude/commands/init-maestro.md +1187 -0
  3. package/.claude/settings.json +161 -0
  4. package/.env.example +216 -0
  5. package/README.md +632 -0
  6. package/agents/browser-operator/agent.md +52 -0
  7. package/agents/calendar-ops/agent.md +50 -0
  8. package/agents/communications/agent.md +96 -0
  9. package/agents/decision-log/agent.md +65 -0
  10. package/agents/desktop-operator/agent.md +59 -0
  11. package/agents/gmail-operator/agent.md +62 -0
  12. package/agents/inbound-dispatcher/agent.md +66 -0
  13. package/agents/inbox-processor/agent.md +39 -0
  14. package/agents/pmo-execution/agent.md +60 -0
  15. package/agents/session-spawner/agent.md +64 -0
  16. package/agents/slack-operator/agent.md +60 -0
  17. package/agents/whatsapp-operator/agent.md +60 -0
  18. package/agents/workflow-automation/agent.md +61 -0
  19. package/archetypes/altitudes/c-suite.yaml +58 -0
  20. package/archetypes/altitudes/founder.yaml +68 -0
  21. package/archetypes/altitudes/senior-manager.yaml +63 -0
  22. package/archetypes/altitudes/svp.yaml +60 -0
  23. package/archetypes/altitudes/vp.yaml +50 -0
  24. package/archetypes/archetype.schema.json +77 -0
  25. package/archetypes/base.yaml +47 -0
  26. package/archetypes/capabilities/commercial-leader.yaml +159 -0
  27. package/archetypes/capabilities/compliance-officer.yaml +159 -0
  28. package/archetypes/capabilities/executive-operator.yaml +169 -0
  29. package/archetypes/capabilities/finance-leader.yaml +162 -0
  30. package/archetypes/capabilities/operations-leader.yaml +154 -0
  31. package/archetypes/capabilities/product-leader.yaml +148 -0
  32. package/archetypes/capabilities/technical-leader.yaml +146 -0
  33. package/archetypes/functions/commercial-leader.yaml +62 -0
  34. package/archetypes/functions/compliance-officer.yaml +64 -0
  35. package/archetypes/functions/executive-operator.yaml +70 -0
  36. package/archetypes/functions/finance-leader.yaml +70 -0
  37. package/archetypes/functions/operations-leader.yaml +62 -0
  38. package/archetypes/functions/product-leader.yaml +61 -0
  39. package/archetypes/functions/technical-leader.yaml +57 -0
  40. package/bin/cohort-mcp.mjs +81 -0
  41. package/bin/maestro.mjs +3516 -0
  42. package/bin/maestro.test.mjs +1015 -0
  43. package/desktop-control/README.md +56 -0
  44. package/desktop-control/app-profiles/gmail.yaml +120 -0
  45. package/desktop-control/app-profiles/slack.yaml +315 -0
  46. package/desktop-control/app-profiles/whatsapp.yaml +107 -0
  47. package/docs/architecture/agent-topology.md +2239 -0
  48. package/docs/architecture/archetype-agent-factory.md +110 -0
  49. package/docs/architecture/collective-memory-and-org-mesh.md +115 -0
  50. package/docs/architecture/continuous-monitoring.md +221 -0
  51. package/docs/architecture/mcp-capability-map.md +585 -0
  52. package/docs/architecture/system-architecture.md +1272 -0
  53. package/docs/company-context/README.md +40 -0
  54. package/docs/guides/agent-persona-setup.md +600 -0
  55. package/docs/guides/agents-observe-setup.md +64 -0
  56. package/docs/guides/billing-console-keys.md +88 -0
  57. package/docs/guides/ccxray-diagnostics.md +65 -0
  58. package/docs/guides/channel-bus.md +127 -0
  59. package/docs/guides/claude-mem-setup.md +79 -0
  60. package/docs/guides/claude-pace-setup.md +56 -0
  61. package/docs/guides/claudraband-sessions.md +98 -0
  62. package/docs/guides/clawteam-swarm.md +116 -0
  63. package/docs/guides/code-review-graph-setup.md +86 -0
  64. package/docs/guides/email-setup.md +431 -0
  65. package/docs/guides/mac-mini.md +119 -0
  66. package/docs/guides/media-generation-setup.md +349 -0
  67. package/docs/guides/model-routing.md +162 -0
  68. package/docs/guides/observability-otel.md +265 -0
  69. package/docs/guides/org-onboarding.md +132 -0
  70. package/docs/guides/outbound-governance-setup.md +437 -0
  71. package/docs/guides/pdf-generation-setup.md +315 -0
  72. package/docs/guides/poller-daemon-setup.md +563 -0
  73. package/docs/guides/rag-context-setup.md +459 -0
  74. package/docs/guides/self-optimization-pattern.md +82 -0
  75. package/docs/guides/setup-wizard.md +178 -0
  76. package/docs/guides/slack-setup.md +350 -0
  77. package/docs/guides/telegram-setup.md +227 -0
  78. package/docs/guides/twilio-subaccounts-setup.md +223 -0
  79. package/docs/guides/verification.md +128 -0
  80. package/docs/guides/voice-mode.md +188 -0
  81. package/docs/guides/voice-sms-setup.md +698 -0
  82. package/docs/guides/webhook-relay-setup.md +349 -0
  83. package/docs/guides/whatsapp-setup.md +288 -0
  84. package/docs/prompts/board-pack-cover-template.md +36 -0
  85. package/docs/prompts/decision-recommendation-template.md +88 -0
  86. package/docs/prompts/followup-message-template.md +141 -0
  87. package/docs/prompts/investor-letter-template.md +52 -0
  88. package/docs/prompts/morning-brief-template.md +82 -0
  89. package/docs/prompts/presentation-template.md +58 -0
  90. package/docs/prompts/weekly-strategic-memo-template.md +104 -0
  91. package/docs/research/hallucinated-tool-output-investigation.md +151 -0
  92. package/docs/runbooks/backup-restore.md +205 -0
  93. package/docs/runbooks/cohort-cutover.md +129 -0
  94. package/docs/runbooks/fleet-operations.md +200 -0
  95. package/docs/runbooks/incident-response.md +226 -0
  96. package/docs/runbooks/mac-mini-bootstrap.md +431 -0
  97. package/docs/runbooks/perpetual-operations.md +509 -0
  98. package/docs/runbooks/recovery-and-failover.md +260 -0
  99. package/framework-features.json +267 -0
  100. package/ingest/README.md +87 -0
  101. package/lib/action-executor.js +689 -0
  102. package/lib/action-executor.test.mjs +871 -0
  103. package/lib/agent-root.mjs +37 -0
  104. package/lib/archetype.mjs +236 -0
  105. package/lib/archetype.test.mjs +132 -0
  106. package/lib/autonomy.mjs +114 -0
  107. package/lib/autonomy.test.mjs +66 -0
  108. package/lib/backlog.mjs +358 -0
  109. package/lib/backlog.test.mjs +266 -0
  110. package/lib/budget-guard.mjs +279 -0
  111. package/lib/budget-guard.test.mjs +291 -0
  112. package/lib/cadence-bus-schedule.test.mjs +194 -0
  113. package/lib/cadence-bus.mjs +1120 -0
  114. package/lib/cadence-bus.test.mjs +720 -0
  115. package/lib/cadences.mjs +205 -0
  116. package/lib/cadences.test.mjs +125 -0
  117. package/lib/capability.mjs +154 -0
  118. package/lib/capability.test.mjs +78 -0
  119. package/lib/channels/base-adapter.mjs +719 -0
  120. package/lib/channels/base-adapter.test.mjs +590 -0
  121. package/lib/channels/channel.mjs +128 -0
  122. package/lib/channels/channels.test.mjs +371 -0
  123. package/lib/channels/contract.mjs +215 -0
  124. package/lib/channels/contract.test.mjs +137 -0
  125. package/lib/channels/conversation-resolver.mjs +95 -0
  126. package/lib/channels/gmail/adapter.mjs +87 -0
  127. package/lib/channels/inbox-item.mjs +255 -0
  128. package/lib/channels/inbox-item.test.mjs +335 -0
  129. package/lib/channels/index.mjs +94 -0
  130. package/lib/channels/orgmail/adapter.mjs +353 -0
  131. package/lib/channels/orgmail/adapter.test.mjs +311 -0
  132. package/lib/channels/pairing.mjs +363 -0
  133. package/lib/channels/pairing.test.mjs +270 -0
  134. package/lib/channels/registry.mjs +164 -0
  135. package/lib/channels/slack/adapter.mjs +317 -0
  136. package/lib/channels/slack-adapter.test.mjs +212 -0
  137. package/lib/channels/sms/adapter.mjs +43 -0
  138. package/lib/channels/telegram/adapter.mjs +432 -0
  139. package/lib/channels/telegram-adapter.test.mjs +306 -0
  140. package/lib/channels/voice/adapter.mjs +301 -0
  141. package/lib/channels/voice/adapter.test.mjs +278 -0
  142. package/lib/channels/whatsapp/adapter-baileys.mjs +587 -0
  143. package/lib/channels/whatsapp/adapter-baileys.test.mjs +359 -0
  144. package/lib/channels/whatsapp/adapter-twilio.mjs +65 -0
  145. package/lib/channels/whatsapp/baileys-typing.test.mjs +154 -0
  146. package/lib/charter.mjs +256 -0
  147. package/lib/charter.test.mjs +89 -0
  148. package/lib/claude-bin.mjs +134 -0
  149. package/lib/claude-bin.test.mjs +75 -0
  150. package/lib/collective/capture.mjs +185 -0
  151. package/lib/collective/capture.test.mjs +121 -0
  152. package/lib/collective/cards.mjs +201 -0
  153. package/lib/collective/cards.test.mjs +114 -0
  154. package/lib/collective/config.mjs +186 -0
  155. package/lib/collective/config.test.mjs +123 -0
  156. package/lib/collective/global-config.mjs +113 -0
  157. package/lib/collective/global-config.test.mjs +75 -0
  158. package/lib/collective/presence.mjs +201 -0
  159. package/lib/collective/presence.test.mjs +95 -0
  160. package/lib/collective/recall.mjs +215 -0
  161. package/lib/collective/recall.test.mjs +116 -0
  162. package/lib/comms/send-gate.mjs +554 -0
  163. package/lib/comms/send-gate.test.mjs +577 -0
  164. package/lib/comms.mjs +67 -0
  165. package/lib/comms.test.mjs +41 -0
  166. package/lib/diagnostics/alerts.mjs +424 -0
  167. package/lib/diagnostics/alerts.test.mjs +318 -0
  168. package/lib/diagnostics/backup-freshness.mjs +188 -0
  169. package/lib/diagnostics/backup-freshness.test.mjs +185 -0
  170. package/lib/diagnostics/counters.mjs +269 -0
  171. package/lib/diagnostics/counters.test.mjs +206 -0
  172. package/lib/diagnostics/events.mjs +188 -0
  173. package/lib/diagnostics/events.test.mjs +290 -0
  174. package/lib/diagnostics/otel.mjs +237 -0
  175. package/lib/diagnostics/otel.test.mjs +196 -0
  176. package/lib/diagnostics/trace.mjs +216 -0
  177. package/lib/diagnostics/trace.test.mjs +251 -0
  178. package/lib/env-compat.mjs +74 -0
  179. package/lib/env-compat.test.mjs +104 -0
  180. package/lib/feature-init.mjs +331 -0
  181. package/lib/fs-atomic.mjs +112 -0
  182. package/lib/fs-atomic.test.mjs +72 -0
  183. package/lib/fs-ownership.mjs +111 -0
  184. package/lib/fs-ownership.test.mjs +158 -0
  185. package/lib/hooks/bus.mjs +347 -0
  186. package/lib/hooks/bus.test.mjs +387 -0
  187. package/lib/index.js +16 -0
  188. package/lib/learning/config.mjs +106 -0
  189. package/lib/learning/config.test.mjs +75 -0
  190. package/lib/learning/counters.mjs +156 -0
  191. package/lib/learning/counters.test.mjs +69 -0
  192. package/lib/learning/curator-consolidate.test.mjs +238 -0
  193. package/lib/learning/curator.mjs +453 -0
  194. package/lib/learning/curator.test.mjs +106 -0
  195. package/lib/learning/log.mjs +40 -0
  196. package/lib/learning/reflect.mjs +534 -0
  197. package/lib/learning/reflect.test.mjs +0 -0
  198. package/lib/learning/session-index.mjs +352 -0
  199. package/lib/learning/session-index.test.mjs +125 -0
  200. package/lib/learning/skill-writer.mjs +474 -0
  201. package/lib/learning/skill-writer.test.mjs +210 -0
  202. package/lib/mcp/server.mjs +328 -0
  203. package/lib/mcp/server.test.mjs +400 -0
  204. package/lib/model-router/auth-profiles.mjs +758 -0
  205. package/lib/model-router/auth-profiles.test.mjs +580 -0
  206. package/lib/model-router/catalog/anthropic.yaml +153 -0
  207. package/lib/model-router/catalog/deepseek.yaml +86 -0
  208. package/lib/model-router/catalog/moonshot.yaml +81 -0
  209. package/lib/model-router/catalog/qwen.yaml +114 -0
  210. package/lib/model-router/catalog.mjs +925 -0
  211. package/lib/model-router/catalog.test.mjs +385 -0
  212. package/lib/model-router/economics.mjs +564 -0
  213. package/lib/model-router/economics.test.mjs +344 -0
  214. package/lib/model-router/failover.mjs +298 -0
  215. package/lib/model-router/failover.test.mjs +439 -0
  216. package/lib/model-router/health.mjs +453 -0
  217. package/lib/model-router/health.test.mjs +338 -0
  218. package/lib/model-router/integration-coverage.test.mjs +829 -0
  219. package/lib/model-router/integration.test.mjs +564 -0
  220. package/lib/model-router/ledger.mjs +402 -0
  221. package/lib/model-router/ledger.test.mjs +382 -0
  222. package/lib/model-router/llm-task.mjs +515 -0
  223. package/lib/model-router/llm-task.test.mjs +392 -0
  224. package/lib/model-router/org-credentials.mjs +260 -0
  225. package/lib/model-router/org-credentials.test.mjs +265 -0
  226. package/lib/model-router/pricing-refresh.mjs +463 -0
  227. package/lib/model-router/pricing-refresh.test.mjs +286 -0
  228. package/lib/model-router/reconcile.mjs +429 -0
  229. package/lib/model-router/reconcile.test.mjs +316 -0
  230. package/lib/model-router/repair.mjs +471 -0
  231. package/lib/model-router/repair.test.mjs +180 -0
  232. package/lib/model-router/resolve.mjs +1206 -0
  233. package/lib/model-router/spawn.mjs +497 -0
  234. package/lib/model-router/spawn.test.mjs +425 -0
  235. package/lib/model-router/taxonomy.mjs +893 -0
  236. package/lib/model-router/taxonomy.test.mjs +410 -0
  237. package/lib/model-router.mjs +677 -0
  238. package/lib/model-router.test.mjs +907 -0
  239. package/lib/org/activity.mjs +211 -0
  240. package/lib/org/activity.test.mjs +134 -0
  241. package/lib/org/approvals.mjs +448 -0
  242. package/lib/org/approvals.test.mjs +216 -0
  243. package/lib/org/awareness.mjs +222 -0
  244. package/lib/org/awareness.test.mjs +159 -0
  245. package/lib/org/board.mjs +229 -0
  246. package/lib/org/board.test.mjs +177 -0
  247. package/lib/org/bootstrap-context.mjs +169 -0
  248. package/lib/org/bootstrap-context.test.mjs +153 -0
  249. package/lib/org/client.mjs +1628 -0
  250. package/lib/org/client.test.mjs +1107 -0
  251. package/lib/org/cohort-client.mjs +67 -0
  252. package/lib/org/cohort-client.test.mjs +126 -0
  253. package/lib/org/cost-sync.mjs +227 -0
  254. package/lib/org/cost-sync.test.mjs +153 -0
  255. package/lib/org/doctor.mjs +212 -0
  256. package/lib/org/doctor.test.mjs +212 -0
  257. package/lib/org/handoff.mjs +293 -0
  258. package/lib/org/handoff.test.mjs +269 -0
  259. package/lib/org/integration-tools.mjs +182 -0
  260. package/lib/org/integration-tools.test.mjs +160 -0
  261. package/lib/org/keys.mjs +131 -0
  262. package/lib/org/keys.test.mjs +92 -0
  263. package/lib/org/knowledge.mjs +463 -0
  264. package/lib/org/knowledge.test.mjs +319 -0
  265. package/lib/org/leases.mjs +335 -0
  266. package/lib/org/leases.test.mjs +235 -0
  267. package/lib/org/mesh-integration.test.mjs +127 -0
  268. package/lib/org/mesh.mjs +459 -0
  269. package/lib/org/mesh.test.mjs +345 -0
  270. package/lib/org/messaging.mjs +503 -0
  271. package/lib/org/messaging.test.mjs +238 -0
  272. package/lib/org/policy.mjs +345 -0
  273. package/lib/org/policy.test.mjs +237 -0
  274. package/lib/org/protocol.checksum +1 -0
  275. package/lib/org/protocol.checksum.test.mjs +90 -0
  276. package/lib/org/protocol.mjs +967 -0
  277. package/lib/org/protocol.test.mjs +264 -0
  278. package/lib/org/registry.mjs +194 -0
  279. package/lib/org/registry.test.mjs +100 -0
  280. package/lib/org/tool-surface-integration.test.mjs +120 -0
  281. package/lib/org/tool-surface.mjs +2535 -0
  282. package/lib/org/tool-surface.test.mjs +589 -0
  283. package/lib/org/ui-parity.mjs +3236 -0
  284. package/lib/org/ui-parity.test.mjs +348 -0
  285. package/lib/org/verify.mjs +176 -0
  286. package/lib/org/verify.test.mjs +194 -0
  287. package/lib/rag/embed.mjs +188 -0
  288. package/lib/rag/indexer.mjs +425 -0
  289. package/lib/rag/rag.test.mjs +505 -0
  290. package/lib/rag/search.mjs +475 -0
  291. package/lib/rate-guard.mjs +246 -0
  292. package/lib/rate-guard.test.mjs +201 -0
  293. package/lib/render.mjs +112 -0
  294. package/lib/render.test.mjs +68 -0
  295. package/lib/resource-governor.mjs +297 -0
  296. package/lib/resource-governor.test.mjs +262 -0
  297. package/lib/scheduling/dynamic-jobs.mjs +675 -0
  298. package/lib/scheduling/dynamic-jobs.test.mjs +344 -0
  299. package/lib/scheduling/jitter.mjs +0 -0
  300. package/lib/scheduling/jitter.test.mjs +140 -0
  301. package/lib/secrets/broker.mjs +315 -0
  302. package/lib/secrets/broker.test.mjs +280 -0
  303. package/lib/secrets/providers.mjs +461 -0
  304. package/lib/secrets/providers.test.mjs +274 -0
  305. package/lib/security/audit-engine.mjs +684 -0
  306. package/lib/security/audit-engine.test.mjs +389 -0
  307. package/lib/security/coerce-args.mjs +552 -0
  308. package/lib/security/coerce-args.test.mjs +281 -0
  309. package/lib/security/dangerous-tools.mjs +97 -0
  310. package/lib/security/dangerous-tools.test.mjs +68 -0
  311. package/lib/security/external-content.mjs +145 -0
  312. package/lib/security/external-content.test.mjs +67 -0
  313. package/lib/security/redact.mjs +592 -0
  314. package/lib/security/redact.test.mjs +441 -0
  315. package/lib/security/secret-equal.mjs +73 -0
  316. package/lib/security/secret-equal.test.mjs +55 -0
  317. package/lib/session-permissions.mjs +101 -0
  318. package/lib/session-permissions.test.mjs +100 -0
  319. package/lib/setup/claude-probe.mjs +74 -0
  320. package/lib/setup/completeness.mjs +175 -0
  321. package/lib/setup/completeness.test.mjs +110 -0
  322. package/lib/setup/context-pack.mjs +173 -0
  323. package/lib/setup/context-pack.test.mjs +89 -0
  324. package/lib/setup/enrich.mjs +277 -0
  325. package/lib/setup/enrich.test.mjs +115 -0
  326. package/lib/setup/enroll-from-cohort.mjs +441 -0
  327. package/lib/setup/enroll-from-cohort.test.mjs +233 -0
  328. package/lib/setup/integration.test.mjs +162 -0
  329. package/lib/setup/io.mjs +360 -0
  330. package/lib/setup/io.test.mjs +77 -0
  331. package/lib/setup/run-generator.mjs +81 -0
  332. package/lib/setup/runner.mjs +244 -0
  333. package/lib/setup/runner.test.mjs +132 -0
  334. package/lib/setup/sections/comms.mjs +173 -0
  335. package/lib/setup/sections/company.mjs +120 -0
  336. package/lib/setup/sections/enrich.mjs +138 -0
  337. package/lib/setup/sections/identity.mjs +182 -0
  338. package/lib/setup/sections/identity.test.mjs +140 -0
  339. package/lib/setup/sections/learning.mjs +153 -0
  340. package/lib/setup/sections/learning.test.mjs +81 -0
  341. package/lib/setup/sections/messaging.mjs +219 -0
  342. package/lib/setup/sections/messaging.test.mjs +127 -0
  343. package/lib/setup/sections/model.mjs +102 -0
  344. package/lib/setup/sections/operating-model.mjs +78 -0
  345. package/lib/setup/sections/org.mjs +475 -0
  346. package/lib/setup/sections/org.test.mjs +313 -0
  347. package/lib/setup/sections/orgmail.mjs +173 -0
  348. package/lib/setup/sections/orgmail.test.mjs +118 -0
  349. package/lib/setup/sections/recovery.mjs +159 -0
  350. package/lib/setup/sections/recovery.test.mjs +98 -0
  351. package/lib/setup/sections/tools.mjs +132 -0
  352. package/lib/setup/sections/verify.mjs +97 -0
  353. package/lib/setup/sot.mjs +205 -0
  354. package/lib/setup/sot.test.mjs +81 -0
  355. package/lib/setup/state.mjs +151 -0
  356. package/lib/setup/state.test.mjs +92 -0
  357. package/lib/singleton.js +229 -0
  358. package/lib/singleton.test.mjs +135 -0
  359. package/lib/telemetry/alerts.mjs +216 -0
  360. package/lib/telemetry/alerts.test.mjs +109 -0
  361. package/lib/telemetry/collect.mjs +512 -0
  362. package/lib/telemetry/collect.test.mjs +202 -0
  363. package/lib/tool-definitions-integration.test.mjs +83 -0
  364. package/lib/tool-definitions.js +738 -0
  365. package/lib/tool-definitions.test.mjs +437 -0
  366. package/lib/util/fetch-timeout.mjs +136 -0
  367. package/lib/util/fetch-timeout.test.mjs +202 -0
  368. package/lib/util/reconnect.mjs +343 -0
  369. package/lib/util/reconnect.test.mjs +369 -0
  370. package/lib/util/unhandled.mjs +205 -0
  371. package/lib/util/unhandled.test.mjs +216 -0
  372. package/lib/voice/context-loader.mjs +466 -0
  373. package/lib/voice/index.mjs +100 -0
  374. package/lib/voice/openai-realtime.mjs +510 -0
  375. package/lib/voice/outbound.mjs +542 -0
  376. package/lib/voice/outbound.test.mjs +69 -0
  377. package/lib/voice/post-call-brief.mjs +428 -0
  378. package/lib/voice/provider.mjs +52 -0
  379. package/lib/voice/session-rotation.mjs +257 -0
  380. package/lib/voice/stt.mjs +161 -0
  381. package/lib/voice/stt.test.mjs +226 -0
  382. package/lib/voice/tool-bridge.mjs +370 -0
  383. package/lib/voice/tts.mjs +104 -0
  384. package/lib/voice/twilio-sip-bridge.mjs +288 -0
  385. package/lib/voice/voice.test.mjs +990 -0
  386. package/mcp/README.md +80 -0
  387. package/package.json +151 -0
  388. package/plugins/maestro-skills/plugin.json +139 -0
  389. package/plugins/maestro-skills/skills/agents-observe.md +110 -0
  390. package/plugins/maestro-skills/skills/board-deck.md +68 -0
  391. package/plugins/maestro-skills/skills/books-close.md +77 -0
  392. package/plugins/maestro-skills/skills/brand-steward.md +121 -0
  393. package/plugins/maestro-skills/skills/calendar-plan.md +57 -0
  394. package/plugins/maestro-skills/skills/call-working-sessions.md +124 -0
  395. package/plugins/maestro-skills/skills/ccxray-diagnostics.md +91 -0
  396. package/plugins/maestro-skills/skills/claude-pace.md +61 -0
  397. package/plugins/maestro-skills/skills/code-review-graph.md +99 -0
  398. package/plugins/maestro-skills/skills/crm-pipeline.md +65 -0
  399. package/plugins/maestro-skills/skills/decision-brief.md +89 -0
  400. package/plugins/maestro-skills/skills/directory-hygiene.md +125 -0
  401. package/plugins/maestro-skills/skills/draft-comms.md +84 -0
  402. package/plugins/maestro-skills/skills/evening-wrap.md +53 -0
  403. package/plugins/maestro-skills/skills/files-find.md +65 -0
  404. package/plugins/maestro-skills/skills/generative-ui.md +228 -0
  405. package/plugins/maestro-skills/skills/hiring-triage.md +74 -0
  406. package/plugins/maestro-skills/skills/inbox-triage.md +61 -0
  407. package/plugins/maestro-skills/skills/mail-triage.md +86 -0
  408. package/plugins/maestro-skills/skills/morning-brief.md +54 -0
  409. package/plugins/maestro-skills/skills/native-artifacts.md +157 -0
  410. package/plugins/maestro-skills/skills/org-board.md +133 -0
  411. package/plugins/maestro-skills/skills/org-credential.md +68 -0
  412. package/plugins/maestro-skills/skills/org-recall.md +81 -0
  413. package/plugins/maestro-skills/skills/pipeline-review.md +76 -0
  414. package/plugins/maestro-skills/skills/regulatory-status.md +81 -0
  415. package/plugins/maestro-skills/skills/router-why.md +78 -0
  416. package/plugins/maestro-skills/skills/schedule-meeting.md +91 -0
  417. package/plugins/maestro-skills/skills/session-search.md +71 -0
  418. package/plugins/maestro-skills/skills/set-reminder.md +93 -0
  419. package/plugins/maestro-skills/skills/slack-followup.md +64 -0
  420. package/plugins/maestro-skills/skills/team-activity.md +86 -0
  421. package/plugins/maestro-skills/skills/weekly-memo.md +70 -0
  422. package/policies/action-classification.yaml +114 -0
  423. package/policies/ai-disclosure.yaml +294 -0
  424. package/policies/communication-style.md +139 -0
  425. package/policies/information-barriers.yaml +118 -0
  426. package/policies/prompt-injection-defence.yaml +138 -0
  427. package/public/assets/icon-dark.png +0 -0
  428. package/public/assets/icon-dark.svg +9 -0
  429. package/public/assets/icon-light.svg +9 -0
  430. package/public/assets/logo-dark.svg +15 -0
  431. package/public/assets/logo-light.svg +15 -0
  432. package/scaffold/.mcp.json +7 -0
  433. package/scaffold/CLAUDE.md +368 -0
  434. package/scaffold/config/agent.json +55 -0
  435. package/scaffold/config/agent.ts +76 -0
  436. package/scaffold/config/agent.ts.example +89 -0
  437. package/scaffold/config/alerts.yaml +23 -0
  438. package/scaffold/config/allowlist.yaml.example +25 -0
  439. package/scaffold/config/caller-id-map.yaml +46 -0
  440. package/scaffold/config/collective.yaml +49 -0
  441. package/scaffold/config/company.json +20 -0
  442. package/scaffold/config/known-agents.json +6 -0
  443. package/scaffold/config/learning.yaml +55 -0
  444. package/scaffold/config/model-routing.yaml.example +104 -0
  445. package/scaffold/config/org.yaml +25 -0
  446. package/scaffold/config/orgmail.yaml.example +19 -0
  447. package/scaffold/config/recovery.yaml +72 -0
  448. package/scaffold/config/secrets.yaml +27 -0
  449. package/scaffold/config/slack.yaml.example +35 -0
  450. package/scaffold/config/telegram.yaml.example +38 -0
  451. package/scaffold/config/voice.yaml.example +89 -0
  452. package/scaffold/config/whatsapp.yaml.example +39 -0
  453. package/schedules/README.md +49 -0
  454. package/schedules/triggers/backlog-executor.md +102 -0
  455. package/schedules/triggers/brand-steward.md +72 -0
  456. package/schedules/triggers/daily-evening-wrap.md +159 -0
  457. package/schedules/triggers/daily-midday-sweep.md +58 -0
  458. package/schedules/triggers/daily-morning-brief.md +55 -0
  459. package/schedules/triggers/directory-hygiene.md +81 -0
  460. package/schedules/triggers/dynamic-jobs.md +40 -0
  461. package/schedules/triggers/inbox-processor.md +115 -0
  462. package/schedules/triggers/meeting-action-capture.md +60 -0
  463. package/schedules/triggers/meeting-prep.md +69 -0
  464. package/schedules/triggers/messaging-inbound.md +50 -0
  465. package/schedules/triggers/org-pulse.md +24 -0
  466. package/schedules/triggers/quarterly-self-assessment.md +54 -0
  467. package/schedules/triggers/weekly-engineering-health.md +37 -0
  468. package/schedules/triggers/weekly-execution.md +65 -0
  469. package/schedules/triggers/weekly-hiring.md +53 -0
  470. package/schedules/triggers/weekly-priorities.md +38 -0
  471. package/schedules/triggers/weekly-strategic-memo.md +124 -0
  472. package/scripts/archive-email.sh +55 -0
  473. package/scripts/cadence/cadence-status.mjs +36 -0
  474. package/scripts/cadence/enqueue-cadence-tick.mjs +174 -0
  475. package/scripts/cadence/enqueue-cadence-tick.test.mjs +187 -0
  476. package/scripts/cadence/launchd-cadence-wrapper.sh +85 -0
  477. package/scripts/cadence/launchd-cloud-relay-wrapper.sh +95 -0
  478. package/scripts/cadence/launchd-socket-mode-wrapper.sh +95 -0
  479. package/scripts/ci/check-docs-accuracy.mjs +493 -0
  480. package/scripts/ci/check-docs-accuracy.test.mjs +409 -0
  481. package/scripts/ci/check-exports-exist.mjs +140 -0
  482. package/scripts/ci/check-files-exist.mjs +107 -0
  483. package/scripts/ci/check-no-build-artifacts.mjs +111 -0
  484. package/scripts/ci/check-no-build-artifacts.test.mjs +71 -0
  485. package/scripts/ci/check-no-confidential.mjs +198 -0
  486. package/scripts/ci/check-no-conflict-markers.mjs +169 -0
  487. package/scripts/ci/check-no-residual-identity.mjs +163 -0
  488. package/scripts/ci/check-no-residual-identity.test.mjs +89 -0
  489. package/scripts/ci/check-tarball-fidelity.mjs +205 -0
  490. package/scripts/ci/check-unresolved-tokens.mjs +83 -0
  491. package/scripts/ci/check.mjs +109 -0
  492. package/scripts/ci/check.test.mjs +194 -0
  493. package/scripts/ci/run-coverage.mjs +82 -0
  494. package/scripts/ci/run-tests.mjs +71 -0
  495. package/scripts/cloud-relay/README.md +59 -0
  496. package/scripts/cloud-relay/index.mjs +233 -0
  497. package/scripts/cloud-relay/package.json +15 -0
  498. package/scripts/cloud-relay/railway.json +13 -0
  499. package/scripts/cloud-relay/voice/README.md +94 -0
  500. package/scripts/cloud-relay/voice/package-lock.json +39 -0
  501. package/scripts/cloud-relay/voice/package.json +16 -0
  502. package/scripts/cloud-relay/voice/railway.json +13 -0
  503. package/scripts/cloud-relay/voice/server.mjs +532 -0
  504. package/scripts/collective/hook-runner.mjs +211 -0
  505. package/scripts/collective/hook-runner.test.mjs +90 -0
  506. package/scripts/collective/org-pulse.mjs +72 -0
  507. package/scripts/collective/org-sync.mjs +61 -0
  508. package/scripts/collective/recall.mjs +45 -0
  509. package/scripts/collective/who.mjs +30 -0
  510. package/scripts/comms-monitor.sh +288 -0
  511. package/scripts/configure-whatsapp-sandbox.sh +201 -0
  512. package/scripts/continuous-monitor.sh +91 -0
  513. package/scripts/cost/fleet-digest.mjs +407 -0
  514. package/scripts/cost/fleet-digest.test.mjs +207 -0
  515. package/scripts/cost/track-claude-usage.mjs +169 -0
  516. package/scripts/daemon/agent-daemon.mjs +989 -0
  517. package/scripts/daemon/agent-daemon.test.mjs +525 -0
  518. package/scripts/daemon/cadence-consumer-governance.test.mjs +220 -0
  519. package/scripts/daemon/cadence-consumer.mjs +1080 -0
  520. package/scripts/daemon/cadence-consumer.test.mjs +770 -0
  521. package/scripts/daemon/cadence-handlers.mjs +1121 -0
  522. package/scripts/daemon/cadence-handlers.test.mjs +617 -0
  523. package/scripts/daemon/classifier.mjs +704 -0
  524. package/scripts/daemon/classifier.test.mjs +238 -0
  525. package/scripts/daemon/classify-kind.mjs +54 -0
  526. package/scripts/daemon/classify-kind.test.mjs +40 -0
  527. package/scripts/daemon/context-compiler.mjs +605 -0
  528. package/scripts/daemon/context-compiler.test.mjs +300 -0
  529. package/scripts/daemon/dispatcher-cooldown.test.mjs +122 -0
  530. package/scripts/daemon/dispatcher-governance.test.mjs +886 -0
  531. package/scripts/daemon/dispatcher.mjs +1516 -0
  532. package/scripts/daemon/health.mjs +72 -0
  533. package/scripts/daemon/inbox-deferral.mjs +210 -0
  534. package/scripts/daemon/inbox-deferral.test.mjs +242 -0
  535. package/scripts/daemon/integration.test.mjs +149 -0
  536. package/scripts/daemon/launchd-wrapper-generic.sh +96 -0
  537. package/scripts/daemon/launchd-wrapper-slack-events.sh +37 -0
  538. package/scripts/daemon/launchd-wrapper.sh +91 -0
  539. package/scripts/daemon/lib/session-router.mjs +274 -0
  540. package/scripts/daemon/lib/session-router.test.mjs +295 -0
  541. package/scripts/daemon/maestro-daemon.mjs +275 -0
  542. package/scripts/daemon/prompt-builder.mjs +685 -0
  543. package/scripts/daemon/prompt-builder.test.mjs +213 -0
  544. package/scripts/daemon/responder.mjs +854 -0
  545. package/scripts/daemon/session-lock.mjs +721 -0
  546. package/scripts/daemon/session-lock.test.mjs +252 -0
  547. package/scripts/daemon/session-outcomes.mjs +640 -0
  548. package/scripts/daemon/session-outcomes.test.mjs +533 -0
  549. package/scripts/daemon/typing-registry.mjs +90 -0
  550. package/scripts/daemon/typing-registry.test.mjs +77 -0
  551. package/scripts/daemon/voice-webhook-server.mjs +804 -0
  552. package/scripts/decisions/capture-decision.mjs +116 -0
  553. package/scripts/disclosure_assessment.py +873 -0
  554. package/scripts/disclosure_boundaries.py +562 -0
  555. package/scripts/email-signature-principal.html +52 -0
  556. package/scripts/email-signature.html +60 -0
  557. package/scripts/email_quote_thread.py +167 -0
  558. package/scripts/email_thread_dedup.py +362 -0
  559. package/scripts/emergency-stop.sh +81 -0
  560. package/scripts/healthcheck.sh +116 -0
  561. package/scripts/hooks/block-mcp-cohort-send.sh +15 -0
  562. package/scripts/hooks/block-mcp-slack-send.sh +7 -0
  563. package/scripts/hooks/post-action-log.sh +126 -0
  564. package/scripts/hooks/pre-send-audit.sh +174 -0
  565. package/scripts/hooks/pre-send-audit.test.mjs +215 -0
  566. package/scripts/hooks/session-end-log.sh +27 -0
  567. package/scripts/hooks/session-start-banner.sh +115 -0
  568. package/scripts/huddle/audio-bridge.mjs +664 -0
  569. package/scripts/huddle/boot-slack-cdp.sh +102 -0
  570. package/scripts/huddle/huddle-controller.mjs +942 -0
  571. package/scripts/huddle/huddle-server.mjs +1229 -0
  572. package/scripts/huddle/launch-slack.sh +232 -0
  573. package/scripts/huddle/openai-realtime-bridge.mjs +462 -0
  574. package/scripts/huddle/package-lock.json +62 -0
  575. package/scripts/huddle/package.json +22 -0
  576. package/scripts/huddle/setup-audio.sh +239 -0
  577. package/scripts/huddle/start-call.mjs +318 -0
  578. package/scripts/huddle/test-pipeline.mjs +263 -0
  579. package/scripts/learning/consolidate-skills.mjs +72 -0
  580. package/scripts/learning/session-search.mjs +125 -0
  581. package/scripts/llm_email_dedup.py +442 -0
  582. package/scripts/local-triggers/generate-plists.sh +432 -0
  583. package/scripts/local-triggers/generate-plists.test.mjs +413 -0
  584. package/scripts/local-triggers/install-all.sh +49 -0
  585. package/scripts/local-triggers/plists/.gitkeep +0 -0
  586. package/scripts/local-triggers/run-trigger.sh +63 -0
  587. package/scripts/local-triggers/templates/rag-reindex.plist.template +47 -0
  588. package/scripts/local-triggers/templates/voice-relay-poller.plist.template +54 -0
  589. package/scripts/local-triggers/templates/voice-tunnel.plist.template +55 -0
  590. package/scripts/local-triggers/templates/voice-webhook.plist.template +51 -0
  591. package/scripts/maintenance/backup-to-cloud.sh +124 -0
  592. package/scripts/maintenance/health-check.sh +377 -0
  593. package/scripts/media-generation/README.md +105 -0
  594. package/scripts/media-generation/gemini-image-client.mjs +173 -0
  595. package/scripts/media-generation/generate-assets.mjs +289 -0
  596. package/scripts/media-generation/veo-video-client.mjs +219 -0
  597. package/scripts/org/send-orgmail.mjs +227 -0
  598. package/scripts/outbound-dedup-cleanup.sh +43 -0
  599. package/scripts/outbound-dedup.sh +477 -0
  600. package/scripts/outbound_dedup.py +115 -0
  601. package/scripts/parse-voice-transcript.mjs +481 -0
  602. package/scripts/pdf-generation/README.md +63 -0
  603. package/scripts/pdf-generation/build-document.mjs +247 -0
  604. package/scripts/pdf-generation/templates/board-pack.latex +136 -0
  605. package/scripts/pdf-generation/templates/corporate-letter.latex +126 -0
  606. package/scripts/pdf-generation/templates/memo.latex +114 -0
  607. package/scripts/poll-slack-events.sh +35 -0
  608. package/scripts/poller/calendar-poller.mjs +12 -0
  609. package/scripts/poller/gmail-poller.mjs +192 -0
  610. package/scripts/poller/imap-client.mjs +289 -0
  611. package/scripts/poller/inbox-scan-poller.mjs +156 -0
  612. package/scripts/poller/inbox-scan-poller.test.mjs +231 -0
  613. package/scripts/poller/index.mjs +73 -0
  614. package/scripts/poller/intra-session-check.mjs +285 -0
  615. package/scripts/poller/lib/cloud-relay-dedup.mjs +88 -0
  616. package/scripts/poller/lib/cloud-relay-dedup.test.mjs +133 -0
  617. package/scripts/poller/lib/slash-command-handlers.mjs +177 -0
  618. package/scripts/poller/secondary-gmail-poller.mjs +132 -0
  619. package/scripts/poller/slack-cloud-relay-client.mjs +368 -0
  620. package/scripts/poller/slack-poller.mjs +854 -0
  621. package/scripts/poller/slack-socket-mode.mjs +917 -0
  622. package/scripts/poller/slack-socket-mode.test.mjs +753 -0
  623. package/scripts/poller/trigger.mjs +75 -0
  624. package/scripts/poller/utils.mjs +371 -0
  625. package/scripts/poller/voice-cloud-relay-client.mjs +179 -0
  626. package/scripts/poller/voice-poller.mjs +236 -0
  627. package/scripts/poller-launchd/install.sh +66 -0
  628. package/scripts/poller-launchd/poller.plist.template +40 -0
  629. package/scripts/poller-launchd/whatsapp-handler.plist.template +39 -0
  630. package/scripts/post-interaction-indexer.py +1598 -0
  631. package/scripts/pre-draft-context.py +994 -0
  632. package/scripts/pre_draft_lookup.py +258 -0
  633. package/scripts/rag/build-index.mjs +47 -0
  634. package/scripts/rag/ingest.mjs +111 -0
  635. package/scripts/rag/search.mjs +119 -0
  636. package/scripts/rag-indexer.py +629 -0
  637. package/scripts/restore-from-backup.sh +248 -0
  638. package/scripts/restore-from-backup.test.mjs +178 -0
  639. package/scripts/resume-operations.sh +80 -0
  640. package/scripts/search-secondary-inbox.py +181 -0
  641. package/scripts/secondary-inbox-poller.py +437 -0
  642. package/scripts/self-optimization/compute-metrics.py +398 -0
  643. package/scripts/send-email-as-principal.py +369 -0
  644. package/scripts/send-email-threaded.py +392 -0
  645. package/scripts/send-email-with-attachment.py +377 -0
  646. package/scripts/send-email.sh +131 -0
  647. package/scripts/send-sms.sh +175 -0
  648. package/scripts/send-whatsapp.sh +292 -0
  649. package/scripts/session-start.sh +106 -0
  650. package/scripts/setup/boot-claude-session.sh +94 -0
  651. package/scripts/setup/configure-macos.sh +674 -0
  652. package/scripts/setup/configure-twilio-sip-trunk.mjs +207 -0
  653. package/scripts/setup/configure-voice-tunnel.mjs +182 -0
  654. package/scripts/setup/generate-agent-env.mjs +92 -0
  655. package/scripts/setup/generate-agent-package-json.mjs +222 -0
  656. package/scripts/setup/generate-agent-package-json.test.mjs +143 -0
  657. package/scripts/setup/generate-autonomy.mjs +60 -0
  658. package/scripts/setup/generate-backlog.mjs +101 -0
  659. package/scripts/setup/generate-cadences.mjs +92 -0
  660. package/scripts/setup/generate-capability.mjs +162 -0
  661. package/scripts/setup/generate-charter.mjs +76 -0
  662. package/scripts/setup/generate-comms.mjs +59 -0
  663. package/scripts/setup/generate-company.mjs +92 -0
  664. package/scripts/setup/init-agent.sh +547 -0
  665. package/scripts/setup/init-agent.test.mjs +151 -0
  666. package/scripts/setup/init-archetype.mjs +74 -0
  667. package/scripts/setup/init-backup.mjs +54 -0
  668. package/scripts/setup/init-cadence-bus.mjs +60 -0
  669. package/scripts/setup/init-channel-bus.mjs +46 -0
  670. package/scripts/setup/init-cost-tracking.mjs +45 -0
  671. package/scripts/setup/init-decision-capture.mjs +66 -0
  672. package/scripts/setup/init-known-agents.mjs +57 -0
  673. package/scripts/setup/init-learning.mjs +70 -0
  674. package/scripts/setup/init-memory-executive.mjs +45 -0
  675. package/scripts/setup/init-model-router.mjs +124 -0
  676. package/scripts/setup/init-rag.mjs +174 -0
  677. package/scripts/setup/init-session-router.mjs +38 -0
  678. package/scripts/setup/init-slack-socket-mode.mjs +260 -0
  679. package/scripts/setup/init-telegram.mjs +165 -0
  680. package/scripts/setup/init-voice-realtime.mjs +204 -0
  681. package/scripts/setup/init-whatsapp-baileys.mjs +77 -0
  682. package/scripts/setup/install-dev-tools.sh +150 -0
  683. package/scripts/setup/lib/install-plist.mjs +95 -0
  684. package/scripts/setup/migrate-agent-to-sot.mjs +192 -0
  685. package/scripts/setup/render-environment-yaml.mjs +133 -0
  686. package/scripts/slack-events-ctl.sh +177 -0
  687. package/scripts/slack-events-server.mjs +1045 -0
  688. package/scripts/slack-react.mjs +89 -0
  689. package/scripts/slack-responded.sh +232 -0
  690. package/scripts/slack-send.sh +287 -0
  691. package/scripts/slack-typing.mjs +196 -0
  692. package/scripts/slack-upload-v2.py +95 -0
  693. package/scripts/sms-handler.mjs +450 -0
  694. package/scripts/spawn-session.sh +120 -0
  695. package/scripts/sync-protocol.mjs +217 -0
  696. package/scripts/system-verify.sh +184 -0
  697. package/scripts/test-email-thread-dedup.py +239 -0
  698. package/scripts/test-information-barriers.py +484 -0
  699. package/scripts/test-llm-email-dedup.py +251 -0
  700. package/scripts/test-pre-draft-integration.py +203 -0
  701. package/scripts/test-rag-phase2.sh +442 -0
  702. package/scripts/test-rag-search.sh +251 -0
  703. package/scripts/test-voice-parser.mjs +316 -0
  704. package/scripts/user-context-search.py +659 -0
  705. package/scripts/validate_outbound.py +1504 -0
  706. package/scripts/watchdog/ai.maestro.memory-watchdog.plist +41 -0
  707. package/scripts/watchdog/force-reboot.sh +157 -0
  708. package/scripts/watchdog/memory-watchdog.sh +473 -0
  709. package/scripts/whatsapp-handler.mjs +538 -0
  710. package/teams/desktop-operations.yaml +34 -0
  711. package/teams/executive-office.yaml +27 -0
  712. package/teams/legal-and-regulatory.yaml +24 -0
  713. package/teams/platform-and-engineering.yaml +23 -0
  714. package/teams/strategy-and-growth.yaml +29 -0
  715. package/workflows/continuous/backlog-executor.yaml +141 -0
  716. package/workflows/continuous/inbound-monitor.yaml +168 -0
  717. package/workflows/daily/applicant-triage.yaml +197 -0
  718. package/workflows/daily/comms-triage.yaml +80 -0
  719. package/workflows/daily/evening-wrap.yaml +105 -0
  720. package/workflows/daily/morning-brief.yaml +164 -0
  721. package/workflows/daily/slack-followup-sweep.yaml +87 -0
  722. package/workflows/event-driven/README.md +50 -0
  723. package/workflows/event-driven/agent-failure-investigation.yaml +137 -0
  724. package/workflows/event-driven/pr-review.yaml +107 -0
  725. package/workflows/monthly/board-readiness.yaml +76 -0
  726. package/workflows/quarterly/strategic-scenario-analysis.yaml +85 -0
  727. package/workflows/session-protocol.md +171 -0
  728. package/workflows/weekly/engineering-health.yaml +154 -0
  729. package/workflows/weekly/hiring-review.yaml +169 -0
  730. package/workflows/weekly/rollup-pipeline-review.yaml +76 -0
  731. 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
+ });