@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,441 @@
1
+ /**
2
+ * lib/setup/enroll-from-cohort.mjs — pull-and-map an agent's record from the
3
+ * Cohort org-data API into the config/agent.json field subset.
4
+ *
5
+ * The org single-SoT payoff for ONBOARDING: an admin describes the agent ONCE in
6
+ * Cohort (its MemberProfileDTO — slug/title/team/role/agentEmail/profileSections/
7
+ * supervisor/…); when `COHORT_API_KEY` + `COHORT_ORG_ID` are present (optionally
8
+ * `COHORT_AGENT_ID`), `maestro setup` PULLS that record and auto-populates
9
+ * config/agent.json instead of prompting the operator field-by-field. The wizard
10
+ * then CONFIRMS the pulled values; a re-pull (`maestro sync`) keeps the agent in
11
+ * step with the org over time.
12
+ *
13
+ * Two pure, side-effect-free helpers live here so they are trivially testable
14
+ * (the network call lives in lib/org/client.fetchSelfProfile, injected for tests):
15
+ *
16
+ * mapProfileToAgentConfig(profile) → the config/agent.json subset the
17
+ * profile AUTHORITATIVELY provides
18
+ * mergeProfilePatch(current, patch, {force}) → the patch with locally-customised
19
+ * leaves dropped (unless force)
20
+ *
21
+ * Mapping policy — map ONLY fields the profile authoritatively provides; never
22
+ * invent a value. Function/altitude are derived from the member's role+team via a
23
+ * bounded keyword map and ALWAYS fall back to a safe default the wizard can change
24
+ * (never fail the pull on an unmappable role). Local customisation is sacred: a
25
+ * value the operator already set in config/agent.json is NOT clobbered by a pull
26
+ * unless `--force` is passed (the `maestro sync --force` path).
27
+ *
28
+ * Fail-open by contract: every helper tolerates a null/partial profile and
29
+ * returns a (possibly empty) plain object — never throws — so a degraded Cohort
30
+ * record can never block setup; the wizard simply falls back to manual prompts.
31
+ *
32
+ * @module lib/setup/enroll-from-cohort
33
+ */
34
+
35
+ "use strict";
36
+
37
+ import { readAgentJson } from "./sot.mjs";
38
+ import { isPlaceholder } from "./completeness.mjs";
39
+
40
+ // Archetype axes the identity section also offers. Keep these in sync with
41
+ // lib/setup/sections/identity.mjs FUNCTIONS/ALTITUDES — a derived value MUST be
42
+ // one the wizard's select() can render and the archetype resolver accepts.
43
+ const FUNCTIONS = Object.freeze([
44
+ "executive-operator",
45
+ "technical-leader",
46
+ "commercial-leader",
47
+ "people-leader",
48
+ "finance-leader",
49
+ "compliance-leader",
50
+ ]);
51
+ const ALTITUDES = Object.freeze(["c-suite", "svp", "vp", "senior-manager"]);
52
+
53
+ const DEFAULT_FUNCTION = "executive-operator";
54
+ const DEFAULT_ALTITUDE = "c-suite";
55
+
56
+ /**
57
+ * Are the Cohort pull env vars present? The PULL path is gated on an API key +
58
+ * an org id; the agent id is optional (its absence selects the whoami endpoint).
59
+ * Pure read of process.env (injectable for tests).
60
+ * @param {NodeJS.ProcessEnv} [env]
61
+ * @returns {{ enabled:boolean, apiKey:string, orgId:string, agentId:string, base:string }}
62
+ */
63
+ export function cohortPullEnv(env = process.env) {
64
+ const apiKey = str(env.COHORT_API_KEY) || str(env.COHORT_API_TOKEN) || str(env.COHORT_TOKEN);
65
+ const orgId = str(env.COHORT_ORG_ID);
66
+ const agentId = str(env.COHORT_AGENT_ID);
67
+ const base = str(env.COHORT_BASE) || str(env.COHORT_API_URL);
68
+ return { enabled: !!(apiKey && orgId), apiKey, orgId, agentId, base };
69
+ }
70
+
71
+ function str(v) {
72
+ return v == null ? "" : String(v).trim();
73
+ }
74
+
75
+ /**
76
+ * Map a Cohort MemberProfileDTO to the config/agent.json field subset it
77
+ * authoritatively provides. Only sets a field when the profile carries a real
78
+ * value (an empty/placeholder profile yields an empty patch). Never throws.
79
+ *
80
+ * Authoritatively mapped:
81
+ * firstName/lastName/fullName ← firstName/lastName/displayName
82
+ * title ← title
83
+ * email ← agentEmail
84
+ * function/altitude ← derived from role+team (bounded keyword map)
85
+ * archetype ← `${function} ${altitude}`-derived (left to the
86
+ * identity generator; we set function/altitude)
87
+ * principal ← supervisor (fullName/title/email split)
88
+ * operatingPrinciples ← profileSections (operating-principles section)
89
+ * communication.defaultTone ← profileSections (communication/tone section)
90
+ * responsibilities ← profileSections (responsibilities section)
91
+ *
92
+ * @param {object|null} profile MemberProfileDTO
93
+ * @returns {object} the config/agent.json patch (deep-mergeable)
94
+ */
95
+ export function mapProfileToAgentConfig(profile) {
96
+ const p = profile && typeof profile === "object" ? profile : {};
97
+ const patch = {};
98
+
99
+ const first = str(p.firstName);
100
+ const last = str(p.lastName);
101
+ const display = str(p.displayName);
102
+ if (first) patch.firstName = first;
103
+ if (last) patch.lastName = last;
104
+ const fullName = display || [first, last].filter(Boolean).join(" ").trim();
105
+ if (fullName) patch.fullName = fullName;
106
+
107
+ const title = str(p.title);
108
+ if (title) patch.title = title;
109
+
110
+ const email = str(p.agentEmail) || str(p.email);
111
+ if (email) patch.email = email;
112
+
113
+ // function/altitude — derive from role + team. Only emitted when the profile
114
+ // carries a role/team/title signal (a genuinely empty profile yields no axis,
115
+ // leaving the wizard to prompt). When signal IS present, derivation always
116
+ // resolves to a VALID axis (safe default when unmappable) so the wizard renders
117
+ // + confirms it rather than failing the pull.
118
+ const hasAxisSignal = !!(str(p.role) || str(p.team) || str(p.title));
119
+ if (hasAxisSignal) {
120
+ patch.function = deriveFunction(p);
121
+ patch.altitude = deriveAltitude(p);
122
+ }
123
+
124
+ // principal — the supervisor the agent reports to.
125
+ const principal = mapSupervisor(p.supervisor);
126
+ if (principal) patch.principal = principal;
127
+
128
+ // profileSections — authored prose the org maintains. Mapped conservatively:
129
+ // only when a recognisable section exists, and only into fields the profile is
130
+ // the SoT for.
131
+ const sections = Array.isArray(p.profileSections) ? p.profileSections : [];
132
+ const principles = sectionLines(sections, ["operating-principles", "operating principles", "principles"]);
133
+ if (principles.length) patch.operatingPrinciples = principles;
134
+ const responsibilities = sectionLines(sections, ["responsibilities", "responsibility", "mandate"]);
135
+ if (responsibilities.length) patch.responsibilities = responsibilities;
136
+ const tone = sectionText(sections, ["communication", "tone", "communication-style"]);
137
+ if (tone) patch.communication = { defaultTone: tone };
138
+
139
+ // charter — the Cohort-authored operating charter (CharterDTO:
140
+ // approvedByLine/gatesText/sections). When present this is the SOURCE OF TRUTH
141
+ // the charter generator renders FROM (scripts/setup/generate-charter.mjs),
142
+ // overriding the archetype skeleton. We persist it verbatim so the generator
143
+ // and the doctor read the same authored charter.
144
+ const charter = mapCharter(p.charter);
145
+ if (charter) patch.charter = charter;
146
+
147
+ // org-authoritative operating fields the enrollment mapper previously DROPPED.
148
+ // Persisted under `orgProfile` so they are available to autonomy generation
149
+ // (autonomyLevel/escalationPath) and to any caller that needs the org's view of
150
+ // this member's reports/governance/skills/operating channel.
151
+ const orgProfile = mapOrgProfile(p);
152
+ if (orgProfile) patch.orgProfile = orgProfile;
153
+
154
+ return patch;
155
+ }
156
+
157
+ /**
158
+ * Map a CharterDTO ({ approvedByLine, gatesText, sections:[{kind,order,title,body}] })
159
+ * into the persisted charter subset. Returns null when there is no real charter
160
+ * (no sections AND no approvedBy/gates text) so an empty patch stays empty.
161
+ * @param {object|null} charter
162
+ * @returns {object|null}
163
+ */
164
+ function mapCharter(charter) {
165
+ if (!charter || typeof charter !== "object") return null;
166
+ const sectionsIn = Array.isArray(charter.sections) ? charter.sections : [];
167
+ const sections = sectionsIn
168
+ .filter((s) => s && typeof s === "object")
169
+ .map((s) => ({
170
+ kind: str(s.kind),
171
+ order: Number.isFinite(s.order) ? s.order : 0,
172
+ title: str(s.title) || null,
173
+ body: str(s.body),
174
+ }))
175
+ .filter((s) => s.body || s.title);
176
+ const approvedByLine = str(charter.approvedByLine);
177
+ const gatesText = str(charter.gatesText);
178
+ if (!sections.length && !approvedByLine && !gatesText) return null;
179
+ const out = { sections };
180
+ if (approvedByLine) out.approvedByLine = approvedByLine;
181
+ if (gatesText) out.gatesText = gatesText;
182
+ return out;
183
+ }
184
+
185
+ /**
186
+ * Collect the org-authoritative operating fields the wizard should NOT drop:
187
+ * autonomyLevel, escalationPath, skills, operatingChannel, reports, governance.
188
+ * Only includes a field when the profile carries a real value. Returns null when
189
+ * the profile carries none of them (keeps an empty patch empty).
190
+ * @param {object} p MemberProfileDTO
191
+ * @returns {object|null}
192
+ */
193
+ function mapOrgProfile(p) {
194
+ const out = {};
195
+ const autonomyLevel = str(p.autonomyLevel);
196
+ if (autonomyLevel) out.autonomyLevel = autonomyLevel;
197
+ const escalationPath = str(p.escalationPath);
198
+ if (escalationPath) out.escalationPath = escalationPath;
199
+ const operatingChannel = str(p.operatingChannel);
200
+ if (operatingChannel) out.operatingChannel = operatingChannel;
201
+ const skills = Array.isArray(p.skills) ? p.skills.map(str).filter(Boolean) : [];
202
+ if (skills.length) out.skills = skills;
203
+ const reports = Array.isArray(p.reports) ? p.reports.filter((r) => r && typeof r === "object") : [];
204
+ if (reports.length) out.reports = reports;
205
+ const governance = Array.isArray(p.governance) ? p.governance.filter((g) => g && typeof g === "object") : [];
206
+ if (governance.length) out.governance = governance;
207
+ return Object.keys(out).length ? out : null;
208
+ }
209
+
210
+ /** Build the principal sub-object from a supervisor descriptor (string or DTO). */
211
+ function mapSupervisor(sup) {
212
+ if (!sup) return null;
213
+ if (typeof sup === "string") {
214
+ const fullName = str(sup);
215
+ if (!fullName) return null;
216
+ const [pf, ...pr] = fullName.split(/\s+/);
217
+ return { firstName: pf || "", lastName: pr.join(" "), fullName, title: "", email: "" };
218
+ }
219
+ if (typeof sup === "object") {
220
+ const fullName = str(sup.displayName) || str(sup.fullName) || [str(sup.firstName), str(sup.lastName)].filter(Boolean).join(" ").trim();
221
+ if (!fullName && !str(sup.email) && !str(sup.title)) return null;
222
+ const [pf, ...pr] = fullName.split(/\s+/);
223
+ return {
224
+ firstName: str(sup.firstName) || pf || "",
225
+ lastName: str(sup.lastName) || pr.join(" "),
226
+ fullName,
227
+ title: str(sup.title),
228
+ email: str(sup.email) || str(sup.agentEmail),
229
+ };
230
+ }
231
+ return null;
232
+ }
233
+
234
+ /**
235
+ * Derive the archetype `function` axis from the member's role + team using a
236
+ * bounded keyword map. Returns DEFAULT_FUNCTION when nothing matches — the pull
237
+ * never fails on an unmappable role; the wizard confirms/changes it.
238
+ * @param {object} p
239
+ * @returns {string} one of FUNCTIONS
240
+ */
241
+ export function deriveFunction(p) {
242
+ const hay = `${str(p.role)} ${str(p.team)} ${str(p.title)}`.toLowerCase();
243
+ const rules = [
244
+ [/\b(eng|engineer|platform|infra|technical|cto|devops|software|sre)\b/, "technical-leader"],
245
+ [/\b(sales|revenue|gtm|growth|commercial|marketing|bd|biz dev|partnerships)\b/, "commercial-leader"],
246
+ [/\b(people|hr|talent|recruit|org design|human resources)\b/, "people-leader"],
247
+ [/\b(finance|fp&a|accounting|treasury|capital|corp dev|cfo)\b/, "finance-leader"],
248
+ [/\b(compliance|legal|risk|regulatory|counsel|audit|dfsa)\b/, "compliance-leader"],
249
+ [/\b(ops|operations|chief of staff|coo|executive|operator)\b/, "executive-operator"],
250
+ ];
251
+ for (const [re, fn] of rules) if (re.test(hay)) return fn;
252
+ return DEFAULT_FUNCTION;
253
+ }
254
+
255
+ /**
256
+ * Derive the `altitude` axis from role/title seniority cues. Returns
257
+ * DEFAULT_ALTITUDE when nothing matches.
258
+ * @param {object} p
259
+ * @returns {string} one of ALTITUDES
260
+ */
261
+ export function deriveAltitude(p) {
262
+ const hay = `${str(p.role)} ${str(p.title)}`.toLowerCase();
263
+ // Order matters: most-senior cue wins (check c-suite/SVP before VP/manager).
264
+ if (/\b(c-?suite|chief|ceo|cto|cfo|coo|cmo|cpo|founder)\b/.test(hay)) return "c-suite";
265
+ if (/\bsvp\b|senior vice president/.test(hay)) return "svp";
266
+ if (/\bvp\b|vice president|head of|director/.test(hay)) return "vp";
267
+ if (/\b(senior manager|sr\.? manager|manager|lead)\b/.test(hay)) return "senior-manager";
268
+ return DEFAULT_ALTITUDE;
269
+ }
270
+
271
+ /** Find a profile section by case-insensitive id/title match. */
272
+ function findSection(sections, names) {
273
+ const wanted = names.map((n) => n.toLowerCase());
274
+ return (
275
+ sections.find((s) => {
276
+ if (!s || typeof s !== "object") return false;
277
+ const id = str(s.id).toLowerCase();
278
+ const title = str(s.title).toLowerCase();
279
+ const heading = str(s.heading).toLowerCase();
280
+ return wanted.includes(id) || wanted.includes(title) || wanted.includes(heading);
281
+ }) || null
282
+ );
283
+ }
284
+
285
+ /** Pull a section's content as a list of trimmed non-empty lines. */
286
+ function sectionLines(sections, names) {
287
+ const s = findSection(sections, names);
288
+ if (!s) return [];
289
+ if (Array.isArray(s.items)) return s.items.map(str).filter(Boolean);
290
+ if (Array.isArray(s.lines)) return s.lines.map(str).filter(Boolean);
291
+ const body = str(s.content) || str(s.body) || str(s.text);
292
+ if (!body) return [];
293
+ return body
294
+ .split(/\r?\n/)
295
+ .map((l) => l.replace(/^\s*[-*•]\s*/, "").trim())
296
+ .filter(Boolean);
297
+ }
298
+
299
+ /** Pull a section's content as a single trimmed string (first non-empty leaf). */
300
+ function sectionText(sections, names) {
301
+ const s = findSection(sections, names);
302
+ if (!s) return "";
303
+ const body = str(s.content) || str(s.body) || str(s.text);
304
+ if (body) return body.split(/\r?\n/).map((l) => l.trim()).filter(Boolean)[0] || "";
305
+ if (Array.isArray(s.items) && s.items.length) return str(s.items[0]);
306
+ return "";
307
+ }
308
+
309
+ /**
310
+ * Reduce a pull patch so it never clobbers a locally-customised value. A leaf is
311
+ * dropped from the patch when the CURRENT config already has a real (non-empty,
312
+ * non-placeholder) value for it — UNLESS `force` is set. This is the seam that
313
+ * lets `maestro setup`/`sync` re-pull freely while protecting operator edits.
314
+ *
315
+ * Nested objects (principal, communication) are reduced leaf-by-leaf; an entirely
316
+ * dropped object is omitted. Arrays (responsibilities/operatingPrinciples) are
317
+ * treated as a single leaf (kept only if the current array is empty or force).
318
+ *
319
+ * @param {object|null} current the existing config/agent.json ({} when none)
320
+ * @param {object} patch the patch from mapProfileToAgentConfig
321
+ * @param {{force?:boolean}} [opts]
322
+ * @returns {object} the reduced patch (safe to deep-merge)
323
+ */
324
+ export function mergeProfilePatch(current, patch, opts = {}) {
325
+ const force = !!opts.force;
326
+ const cur = current && typeof current === "object" ? current : {};
327
+ const out = {};
328
+ for (const [k, v] of Object.entries(patch || {})) {
329
+ if (v && typeof v === "object" && !Array.isArray(v)) {
330
+ const sub = reduceObject((cur[k] && typeof cur[k] === "object" && cur[k]) || {}, v, force);
331
+ if (sub && Object.keys(sub).length) out[k] = sub;
332
+ continue;
333
+ }
334
+ if (force || isUnset(cur[k])) out[k] = v;
335
+ }
336
+ return out;
337
+ }
338
+
339
+ function reduceObject(curObj, patchObj, force) {
340
+ const out = {};
341
+ for (const [k, v] of Object.entries(patchObj)) {
342
+ if (v && typeof v === "object" && !Array.isArray(v)) {
343
+ const sub = reduceObject((curObj[k] && typeof curObj[k] === "object" && curObj[k]) || {}, v, force);
344
+ if (sub && Object.keys(sub).length) out[k] = sub;
345
+ continue;
346
+ }
347
+ if (force || isUnset(curObj[k])) out[k] = v;
348
+ }
349
+ return out;
350
+ }
351
+
352
+ /** A leaf is "unset" when missing, empty, an empty array, or a placeholder. */
353
+ function isUnset(v) {
354
+ if (v === undefined || v === null) return true;
355
+ if (typeof v === "string") return v.trim() === "" || isPlaceholder(v);
356
+ if (Array.isArray(v)) return v.length === 0;
357
+ return false;
358
+ }
359
+
360
+ /**
361
+ * Compute the leaf-level diff between two config subsets — the human-readable
362
+ * "what changed" report `maestro sync` prints. Returns an array of
363
+ * `{ path, from, to }`. Pure; only inspects the keys present in `patch`.
364
+ * @param {object} before
365
+ * @param {object} patch the reduced patch about to be merged
366
+ * @returns {Array<{path:string, from:*, to:*}>}
367
+ */
368
+ export function diffPatch(before, patch) {
369
+ const b = before && typeof before === "object" ? before : {};
370
+ const changes = [];
371
+ walkDiff(b, patch || {}, "", changes);
372
+ return changes;
373
+ }
374
+
375
+ function walkDiff(before, patch, prefix, changes) {
376
+ for (const [k, v] of Object.entries(patch || {})) {
377
+ const path = prefix ? `${prefix}.${k}` : k;
378
+ const prev = before ? before[k] : undefined;
379
+ if (v && typeof v === "object" && !Array.isArray(v)) {
380
+ walkDiff(prev && typeof prev === "object" ? prev : {}, v, path, changes);
381
+ continue;
382
+ }
383
+ if (!sameLeaf(prev, v)) changes.push({ path, from: prev, to: v });
384
+ }
385
+ }
386
+
387
+ function sameLeaf(a, b) {
388
+ if (Array.isArray(a) && Array.isArray(b)) return JSON.stringify(a) === JSON.stringify(b);
389
+ return a === b;
390
+ }
391
+
392
+ /**
393
+ * High-level orchestration used by both the org/identity sections and
394
+ * `maestro sync`: pull the self profile, map it, reduce against the current
395
+ * config, and (optionally) return the diff. Network call is injected via
396
+ * `fetchSelfProfileImpl` (the lib/org/client export) so this is unit-testable.
397
+ *
398
+ * Fail-open: a missing key, an unreachable server, or a profile-less response
399
+ * yields `{ pulled:false, patch:{}, changes:[] }` — the caller falls back to the
400
+ * manual prompts exactly as today.
401
+ *
402
+ * @param {object} o
403
+ * @param {string} o.agentRoot
404
+ * @param {NodeJS.ProcessEnv} [o.env]
405
+ * @param {boolean} [o.force]
406
+ * @param {Function} o.fetchSelfProfileImpl client.fetchSelfProfile (injected)
407
+ * @returns {Promise<{ pulled:boolean, reason?:string, profile?:object, patch:object, changes:Array }>}
408
+ */
409
+ export async function pullAndPlan(o = {}) {
410
+ const env = o.env || process.env;
411
+ const e = cohortPullEnv(env);
412
+ if (!e.enabled) return { pulled: false, reason: "cohort-pull env not set", patch: {}, changes: [] };
413
+ if (typeof o.fetchSelfProfileImpl !== "function") {
414
+ return { pulled: false, reason: "no fetch impl", patch: {}, changes: [] };
415
+ }
416
+
417
+ let frame;
418
+ try {
419
+ frame = await o.fetchSelfProfileImpl({
420
+ base: e.base,
421
+ apiKey: e.apiKey,
422
+ orgId: e.orgId,
423
+ agentId: e.agentId || undefined,
424
+ emailHint: str(env.COHORT_AGENT_EMAIL) || undefined,
425
+ });
426
+ } catch {
427
+ return { pulled: false, reason: "fetch threw", patch: {}, changes: [] };
428
+ }
429
+ if (!frame || !frame.ok || !frame.result) {
430
+ return { pulled: false, reason: (frame && frame.error && frame.error.message) || "no profile", patch: {}, changes: [] };
431
+ }
432
+
433
+ const profile = frame.result;
434
+ const mapped = mapProfileToAgentConfig(profile);
435
+ const current = readAgentJson(o.agentRoot) || {};
436
+ const patch = mergeProfilePatch(current, mapped, { force: !!o.force });
437
+ const changes = diffPatch(current, patch);
438
+ return { pulled: true, profile, patch, changes };
439
+ }
440
+
441
+ export { FUNCTIONS, ALTITUDES, DEFAULT_FUNCTION, DEFAULT_ALTITUDE };
@@ -0,0 +1,233 @@
1
+ /**
2
+ * enroll-from-cohort.test.mjs — pull-and-map a Cohort MemberProfileDTO into
3
+ * the config/agent.json subset, with local-customisation protection + diff.
4
+ * Run: node --test lib/setup/enroll-from-cohort.test.mjs
5
+ */
6
+ "use strict";
7
+
8
+ import { test } from "node:test";
9
+ import assert from "node:assert/strict";
10
+ import { mkdtempSync, mkdirSync, writeFileSync } from "node:fs";
11
+ import { tmpdir } from "node:os";
12
+ import { join } from "node:path";
13
+
14
+ import {
15
+ mapProfileToAgentConfig,
16
+ mergeProfilePatch,
17
+ diffPatch,
18
+ deriveFunction,
19
+ deriveAltitude,
20
+ cohortPullEnv,
21
+ pullAndPlan,
22
+ FUNCTIONS,
23
+ ALTITUDES,
24
+ } from "./enroll-from-cohort.mjs";
25
+ import { readAgentJson } from "./sot.mjs";
26
+
27
+ function tmpRoot() {
28
+ const root = mkdtempSync(join(tmpdir(), "maestro-enroll-"));
29
+ mkdirSync(join(root, "config"), { recursive: true });
30
+ return root;
31
+ }
32
+ function writeAgent(root, obj) {
33
+ writeFileSync(join(root, "config", "agent.json"), JSON.stringify(obj, null, 2) + "\n");
34
+ }
35
+
36
+ // A representative MemberProfileDTO from the org-data API.
37
+ const PROFILE = {
38
+ slug: "robin",
39
+ kind: "agent",
40
+ displayName: "Robin Vega",
41
+ firstName: "Robin",
42
+ lastName: "Vega",
43
+ title: "VP of Engineering",
44
+ team: "Platform Engineering",
45
+ role: "engineering-lead",
46
+ agentEmail: "robin@example.com",
47
+ supervisor: { displayName: "Layla Al-Masri", title: "CTO", email: "layla@example.com" },
48
+ profileSections: [
49
+ { id: "operating-principles", content: "- Ship small\n- Default to autonomy\n- Escalate blockers fast" },
50
+ { id: "responsibilities", items: ["Own CI/CD", "Run platform on-call"] },
51
+ { id: "communication", content: "Direct, technical, low-ceremony." },
52
+ ],
53
+ };
54
+
55
+ test("mapProfileToAgentConfig maps the authoritative fields", () => {
56
+ const patch = mapProfileToAgentConfig(PROFILE);
57
+ assert.equal(patch.firstName, "Robin");
58
+ assert.equal(patch.lastName, "Vega");
59
+ assert.equal(patch.fullName, "Robin Vega");
60
+ assert.equal(patch.title, "VP of Engineering");
61
+ assert.equal(patch.email, "robin@example.com");
62
+ assert.equal(patch.function, "technical-leader");
63
+ assert.equal(patch.altitude, "vp");
64
+ assert.equal(patch.principal.fullName, "Layla Al-Masri");
65
+ assert.equal(patch.principal.title, "CTO");
66
+ assert.equal(patch.principal.email, "layla@example.com");
67
+ assert.deepEqual(patch.operatingPrinciples, ["Ship small", "Default to autonomy", "Escalate blockers fast"]);
68
+ assert.deepEqual(patch.responsibilities, ["Own CI/CD", "Run platform on-call"]);
69
+ assert.equal(patch.communication.defaultTone, "Direct, technical, low-ceremony.");
70
+ });
71
+
72
+ test("mapProfileToAgentConfig is fail-open on a null/empty profile", () => {
73
+ assert.deepEqual(mapProfileToAgentConfig(null), {});
74
+ assert.deepEqual(mapProfileToAgentConfig({}), {});
75
+ // A bare role signal IS enough to derive an axis.
76
+ assert.equal(mapProfileToAgentConfig({ role: "engineer" }).function, "technical-leader");
77
+ });
78
+
79
+ // A MemberProfileDTO carrying the Cohort-authored charter + the authoritative
80
+ // operating fields the mapper previously DROPPED.
81
+ const PROFILE_FULL = {
82
+ ...PROFILE,
83
+ autonomyLevel: "high",
84
+ escalationPath: "CTO, then CEO for irreversible decisions",
85
+ operatingChannel: "#platform",
86
+ skills: ["ci-cd", "incident-response"],
87
+ reports: [{ member: { slug: "kai", displayName: "Kai Tan" }, style: "SOLID" }],
88
+ governance: [{ type: "ADVISES", direction: "out", counterpart: { slug: "secops" }, theme: "security" }],
89
+ charter: {
90
+ approvedByLine: "Approved by Layla Al-Masri, CTO — 2026-06-01",
91
+ gatesText: "All irreversible spend > $10k escalates.",
92
+ sections: [
93
+ { kind: "purpose", order: 1, title: "Purpose & Mandate", body: "Own platform reliability and developer velocity." },
94
+ { kind: "decision_rights", order: 2, title: null, body: "Owns deploy cadence; recommends headcount." },
95
+ ],
96
+ },
97
+ };
98
+
99
+ test("mapProfileToAgentConfig captures the Cohort-authored charter (SoT)", () => {
100
+ const patch = mapProfileToAgentConfig(PROFILE_FULL);
101
+ assert.ok(patch.charter, "charter present in patch");
102
+ assert.equal(patch.charter.approvedByLine, "Approved by Layla Al-Masri, CTO — 2026-06-01");
103
+ assert.equal(patch.charter.gatesText, "All irreversible spend > $10k escalates.");
104
+ assert.equal(patch.charter.sections.length, 2);
105
+ assert.equal(patch.charter.sections[0].title, "Purpose & Mandate");
106
+ assert.equal(patch.charter.sections[1].title, null);
107
+ assert.match(patch.charter.sections[1].body, /Owns deploy cadence/);
108
+ });
109
+
110
+ test("mapProfileToAgentConfig persists the authoritative org operating fields", () => {
111
+ const patch = mapProfileToAgentConfig(PROFILE_FULL);
112
+ assert.ok(patch.orgProfile, "orgProfile present in patch");
113
+ assert.equal(patch.orgProfile.autonomyLevel, "high");
114
+ assert.match(patch.orgProfile.escalationPath, /CTO/);
115
+ assert.equal(patch.orgProfile.operatingChannel, "#platform");
116
+ assert.deepEqual(patch.orgProfile.skills, ["ci-cd", "incident-response"]);
117
+ assert.equal(patch.orgProfile.reports[0].member.slug, "kai");
118
+ assert.equal(patch.orgProfile.governance[0].type, "ADVISES");
119
+ });
120
+
121
+ test("charter/orgProfile absent → no charter/orgProfile keys (empty patch stays empty)", () => {
122
+ const patch = mapProfileToAgentConfig(PROFILE); // base fixture has neither
123
+ assert.equal(patch.charter, undefined);
124
+ assert.equal(patch.orgProfile, undefined);
125
+ // A charter with no real content is also dropped.
126
+ const noContent = mapProfileToAgentConfig({ charter: { approvedByLine: "", gatesText: "", sections: [] } });
127
+ assert.equal(noContent.charter, undefined);
128
+ });
129
+
130
+ test("deriveFunction + deriveAltitude resolve to valid axes (safe default)", () => {
131
+ assert.ok(FUNCTIONS.includes(deriveFunction({ role: "sales lead", team: "Revenue" })));
132
+ assert.equal(deriveFunction({ role: "sales lead", team: "Revenue" }), "commercial-leader");
133
+ assert.equal(deriveFunction({ role: "general counsel", team: "Legal" }), "compliance-leader");
134
+ assert.equal(deriveFunction({ role: "weird-unknown-role" }), "executive-operator");
135
+ assert.ok(ALTITUDES.includes(deriveAltitude({ title: "Chief Technology Officer" })));
136
+ assert.equal(deriveAltitude({ title: "Chief Technology Officer" }), "c-suite");
137
+ assert.equal(deriveAltitude({ title: "Senior Manager, Ops" }), "senior-manager");
138
+ assert.equal(deriveAltitude({ title: "Mystery" }), "c-suite");
139
+ });
140
+
141
+ test("mergeProfilePatch protects a locally-customised value unless --force", () => {
142
+ const current = { firstName: "Robin", title: "Custom Title The Operator Set", email: "" };
143
+ const patch = mapProfileToAgentConfig(PROFILE);
144
+ const reduced = mergeProfilePatch(current, patch, { force: false });
145
+ // title is locally set → dropped; email is empty → kept; firstName equals → still set is fine
146
+ assert.equal(reduced.title, undefined, "local title is NOT clobbered");
147
+ assert.equal(reduced.email, "robin@example.com", "empty local value gets filled");
148
+ // With --force, the org value wins.
149
+ const forced = mergeProfilePatch(current, patch, { force: true });
150
+ assert.equal(forced.title, "VP of Engineering");
151
+ });
152
+
153
+ test("mergeProfilePatch reduces nested objects leaf-by-leaf", () => {
154
+ const current = { principal: { fullName: "Existing Boss", title: "" } };
155
+ const patch = mapProfileToAgentConfig(PROFILE);
156
+ const reduced = mergeProfilePatch(current, patch, { force: false });
157
+ assert.equal(reduced.principal.fullName, undefined, "local principal.fullName kept");
158
+ assert.equal(reduced.principal.title, "CTO", "empty principal.title filled");
159
+ });
160
+
161
+ test("mergeProfilePatch treats a placeholder string as unset", () => {
162
+ const current = { title: "UNCONFIGURED" };
163
+ const reduced = mergeProfilePatch(current, { title: "VP of Engineering" }, { force: false });
164
+ assert.equal(reduced.title, "VP of Engineering");
165
+ });
166
+
167
+ test("diffPatch reports leaf-level changes including nested + array", () => {
168
+ const before = { title: "Old", principal: { title: "Old VP" } };
169
+ const patch = { title: "VP of Engineering", principal: { title: "CTO" }, responsibilities: ["a", "b"] };
170
+ const diff = diffPatch(before, patch);
171
+ const byPath = Object.fromEntries(diff.map((d) => [d.path, d]));
172
+ assert.equal(byPath["title"].from, "Old");
173
+ assert.equal(byPath["title"].to, "VP of Engineering");
174
+ assert.equal(byPath["principal.title"].to, "CTO");
175
+ assert.ok(byPath["responsibilities"], "array change is reported");
176
+ });
177
+
178
+ test("cohortPullEnv gates on api key + org id; agent id optional", () => {
179
+ assert.equal(cohortPullEnv({}).enabled, false);
180
+ assert.equal(cohortPullEnv({ COHORT_API_KEY: "k" }).enabled, false);
181
+ const e = cohortPullEnv({ COHORT_API_KEY: "k", COHORT_ORG_ID: "acme", COHORT_AGENT_ID: "robin" });
182
+ assert.equal(e.enabled, true);
183
+ assert.equal(e.apiKey, "k");
184
+ assert.equal(e.orgId, "acme");
185
+ assert.equal(e.agentId, "robin");
186
+ });
187
+
188
+ test("pullAndPlan: with env + mocked profile, plans a patch and a diff", async () => {
189
+ const root = tmpRoot();
190
+ writeAgent(root, { firstName: "UNCONFIGURED" });
191
+ const plan = await pullAndPlan({
192
+ agentRoot: root,
193
+ env: { COHORT_API_KEY: "k", COHORT_ORG_ID: "acme", COHORT_AGENT_ID: "robin" },
194
+ fetchSelfProfileImpl: async () => ({ ok: true, result: PROFILE }),
195
+ });
196
+ assert.equal(plan.pulled, true);
197
+ assert.equal(plan.patch.firstName, "Robin", "placeholder firstName overwritten");
198
+ assert.equal(plan.patch.email, "robin@example.com");
199
+ assert.ok(plan.changes.length > 0);
200
+ });
201
+
202
+ test("pullAndPlan: absent env → not pulled (manual path stands)", async () => {
203
+ const root = tmpRoot();
204
+ const plan = await pullAndPlan({
205
+ agentRoot: root,
206
+ env: {},
207
+ fetchSelfProfileImpl: async () => ({ ok: true, result: PROFILE }),
208
+ });
209
+ assert.equal(plan.pulled, false);
210
+ assert.deepEqual(plan.patch, {});
211
+ assert.deepEqual(plan.changes, []);
212
+ });
213
+
214
+ test("pullAndPlan: fetch failure → fail-open, not pulled", async () => {
215
+ const root = tmpRoot();
216
+ const plan = await pullAndPlan({
217
+ agentRoot: root,
218
+ env: { COHORT_API_KEY: "k", COHORT_ORG_ID: "acme" },
219
+ fetchSelfProfileImpl: async () => ({ ok: false, error: { code: "INTERNAL", message: "down" } }),
220
+ });
221
+ assert.equal(plan.pulled, false);
222
+ assert.deepEqual(plan.patch, {});
223
+ });
224
+
225
+ test("pullAndPlan: a local override is NOT clobbered without --force", async () => {
226
+ const root = tmpRoot();
227
+ writeAgent(root, { title: "My Custom Title" });
228
+ const env = { COHORT_API_KEY: "k", COHORT_ORG_ID: "acme", COHORT_AGENT_ID: "robin" };
229
+ const noForce = await pullAndPlan({ agentRoot: root, env, fetchSelfProfileImpl: async () => ({ ok: true, result: PROFILE }) });
230
+ assert.equal(noForce.patch.title, undefined, "custom title protected");
231
+ const forced = await pullAndPlan({ agentRoot: root, env, force: true, fetchSelfProfileImpl: async () => ({ ok: true, result: PROFILE }) });
232
+ assert.equal(forced.patch.title, "VP of Engineering", "--force overwrites");
233
+ });