@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,3516 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Maestro CLI — Create and manage autonomous AI agent deployments.
4
+ *
5
+ * Usage:
6
+ * npx @cohortapp/agent-sdk create <dirname> # Scaffold a new agent repo
7
+ * npx @cohortapp/agent-sdk upgrade # Update framework files in current agent repo
8
+ * npx @cohortapp/agent-sdk doctor # Verify installation and configuration
9
+ */
10
+
11
+ import { resolve, join, dirname, relative, sep, basename } from "node:path";
12
+ import { fileURLToPath, pathToFileURL } from "node:url";
13
+ import {
14
+ mkdirSync,
15
+ cpSync,
16
+ copyFileSync,
17
+ existsSync,
18
+ readFileSync,
19
+ writeFileSync,
20
+ readdirSync,
21
+ statSync,
22
+ lstatSync,
23
+ openSync,
24
+ readSync,
25
+ closeSync,
26
+ } from "node:fs";
27
+ import { execFileSync, spawnSync } from "node:child_process";
28
+ import { createHash } from "node:crypto";
29
+ import { homedir } from "node:os";
30
+ import { checkOwnershipSet } from "../lib/fs-ownership.mjs";
31
+ import { resolveArchetype, migrateLegacyArchetype } from "../lib/archetype.mjs";
32
+ import { runAudit, applyFixes, buildAttestation } from "../lib/security/audit-engine.mjs";
33
+ import { selectProvider } from "../lib/secrets/providers.mjs";
34
+ import { syncSecrets, rotateSecret, makeBroker, auditLogPath } from "../lib/secrets/broker.mjs";
35
+ import { applyBrandEnvCompat } from "../lib/env-compat.mjs";
36
+
37
+ // Fleet back-compat FIRST: bridge NEOLITH_* ⇄ COHORT_* env names before any
38
+ // env-first resolution below — installed hosts still export the legacy names.
39
+ applyBrandEnvCompat();
40
+
41
+ const __dirname = dirname(fileURLToPath(import.meta.url));
42
+ const MAESTRO_ROOT = resolve(__dirname, "..");
43
+ const SCAFFOLD_DIR = join(MAESTRO_ROOT, "scaffold");
44
+ const FRAMEWORK_FEATURES_PATH = join(MAESTRO_ROOT, "framework-features.json");
45
+
46
+ // Plist architecture marker — must match the value emitted by
47
+ // scripts/local-triggers/generate-plists.sh. Used by doctor + upgrade
48
+ // migration to distinguish cadence-bus plists from the legacy
49
+ // spawn-per-tick pattern (which invoked run-trigger.sh directly).
50
+ const PLIST_ARCH_MARKER = "maestro-plist-arch: cadence-bus";
51
+ const LEGACY_PLIST_INDICATOR = "run-trigger.sh";
52
+
53
+ // ---------------------------------------------------------------------------
54
+ // Colours
55
+ // ---------------------------------------------------------------------------
56
+
57
+ const C = {
58
+ reset: "\x1b[0m",
59
+ bold: "\x1b[1m",
60
+ red: "\x1b[31m",
61
+ green: "\x1b[32m",
62
+ yellow: "\x1b[33m",
63
+ blue: "\x1b[34m",
64
+ cyan: "\x1b[36m",
65
+ };
66
+
67
+ const log = (msg) => console.log(`${C.blue}[maestro]${C.reset} ${msg}`);
68
+ const ok = (msg) => console.log(`${C.green} ✓${C.reset} ${msg}`);
69
+ const warn = (msg) => console.log(`${C.yellow} ⚠${C.reset} ${msg}`);
70
+ const fail = (msg) => console.error(`${C.red} ✗${C.reset} ${msg}`);
71
+
72
+ // ---------------------------------------------------------------------------
73
+ // CREATE — scaffold a new agent repo
74
+ // ---------------------------------------------------------------------------
75
+
76
+ function create(targetName) {
77
+ if (!targetName) {
78
+ fail("Usage: maestro create <agent-directory-name>");
79
+ fail("Example: maestro create acme-ai");
80
+ process.exit(1);
81
+ }
82
+
83
+ const targetDir = resolve(process.cwd(), targetName);
84
+
85
+ if (existsSync(targetDir)) {
86
+ fail(`Directory already exists: ${targetDir}`);
87
+ process.exit(1);
88
+ }
89
+
90
+ console.log();
91
+ log(`Creating agent: ${targetName}`);
92
+ mkdirSync(targetDir, { recursive: true });
93
+
94
+ // ── Step 1: Copy framework files ────────────────────────────────────────
95
+
96
+ log("Copying framework files...");
97
+
98
+ const frameworkDirs = [
99
+ "scripts",
100
+ "agents",
101
+ "workflows",
102
+ "policies",
103
+ "teams",
104
+ "docs",
105
+ "public",
106
+ "plugins",
107
+ "schedules",
108
+ "desktop-control",
109
+ "ingest",
110
+ "mcp",
111
+ "services",
112
+ // lib/ holds shared framework primitives — singleton lock, cadence bus,
113
+ // action executor, tool definitions — that scripts/ imports via relative
114
+ // paths. Must travel with each agent repo so the relative imports work
115
+ // without depending on a checkout of @cohortapp/agent-sdk on the same host.
116
+ "lib",
117
+ ];
118
+
119
+ for (const dir of frameworkDirs) {
120
+ const src = join(MAESTRO_ROOT, dir);
121
+ if (existsSync(src)) {
122
+ cpSync(src, join(targetDir, dir), { recursive: true });
123
+ ok(dir);
124
+ }
125
+ }
126
+
127
+ // ── Step 2: Copy scaffold templates ─────────────────────────────────────
128
+
129
+ log("Copying agent templates...");
130
+
131
+ if (existsSync(SCAFFOLD_DIR)) {
132
+ cpSync(SCAFFOLD_DIR, targetDir, { recursive: true });
133
+ ok("scaffold templates");
134
+ }
135
+
136
+ // Copy .claude/ directory
137
+ const claudeDir = join(MAESTRO_ROOT, ".claude");
138
+ if (existsSync(claudeDir)) {
139
+ cpSync(claudeDir, join(targetDir, ".claude"), { recursive: true });
140
+ ok(".claude/ (settings, commands)");
141
+ }
142
+
143
+ // ── Step 3: Create agent-specific directories ───────────────────────────
144
+
145
+ log("Creating agent directories...");
146
+
147
+ const agentDirs = [
148
+ "config",
149
+ "knowledge/decisions",
150
+ "knowledge/decisions/archive",
151
+ "knowledge/entities",
152
+ "knowledge/memory",
153
+ "knowledge/sources",
154
+ "knowledge/syntheses",
155
+ "memory/interactions",
156
+ "memory/profiles/users",
157
+ "memory/profiles/channels",
158
+ "memory/precedents",
159
+ "memory/precedents/market-signals",
160
+ "memory/indexes",
161
+ "memory/templates",
162
+ "state/inbox/slack",
163
+ "state/inbox/gmail",
164
+ "state/inbox/calendar",
165
+ "state/inbox/sms",
166
+ "state/inbox/whatsapp",
167
+ "state/inbox/internal",
168
+ "state/inbox/attachments",
169
+ "state/inbox/processed",
170
+ "state/queues",
171
+ "state/dashboards",
172
+ "state/polling",
173
+ "state/locks/outbound",
174
+ "state/triggers/priority",
175
+ "state/handoffs",
176
+ "state/huddle",
177
+ "state/indexes",
178
+ "state/rag",
179
+ "state/sessions",
180
+ "state/slack-responded",
181
+ "state/slack-thread-tracker",
182
+ "state/tmp",
183
+ "state/cadence-bus",
184
+ "state/cadence-bus/inbox",
185
+ "state/cadence-bus/claimed",
186
+ "state/cadence-bus/processed",
187
+ "state/cadence-bus/failed",
188
+ "state/cadence-bus/dlq",
189
+ // 1.12.0 — channel bus + voice + WhatsApp (Baileys) state dirs.
190
+ // Pre-created at scaffold time so daemon startups don't race with
191
+ // first-run mkdir calls. Each feature's init wizard re-creates these
192
+ // idempotently as a safety net.
193
+ "state/channels",
194
+ "state/voice",
195
+ "state/whatsapp/auth",
196
+ "state/whatsapp/audio",
197
+ "state/whatsapp/outbound",
198
+ "outputs/briefs",
199
+ "outputs/drafts",
200
+ "outputs/memos",
201
+ "outputs/research",
202
+ "outputs/tasks",
203
+ "outputs/sessions",
204
+ "logs/polling",
205
+ "logs/workflows",
206
+ "logs/sessions",
207
+ "logs/audit",
208
+ "logs/security",
209
+ "logs/evolution",
210
+ "logs/huddle",
211
+ "logs/daemon",
212
+ "logs/cadence-bus",
213
+ "logs/infra",
214
+ "logs/monitor",
215
+ "logs/phone",
216
+ "logs/sms",
217
+ "logs/whatsapp",
218
+ "logs/email",
219
+ // 1.12.0
220
+ "logs/channels",
221
+ "logs/voice",
222
+ "logs/voice/sessions",
223
+ "self-optimization/scenarios",
224
+ "tests",
225
+ ];
226
+
227
+ for (const dir of agentDirs) {
228
+ mkdirSync(join(targetDir, dir), { recursive: true });
229
+ }
230
+ ok(`${agentDirs.length} agent directories`);
231
+
232
+ // ── Step 3b: Generate template files ───────────────────────────────────
233
+ // These are empty-but-structured templates that define the operational
234
+ // schema for queues, dashboards, knowledge, and config. Without them,
235
+ // workflows and triggers have nothing to read/write.
236
+
237
+ log("Generating operational templates...");
238
+
239
+ // Queue templates — 16 operational queues
240
+ const queueTemplates = {
241
+ "action-stack": "Active action items requiring attention or follow-through",
242
+ "decision-queue": "Key decisions awaiting principal input or closure",
243
+ "follow-ups": "Commitments to external parties with due dates",
244
+ "awaiting-reply": "Messages sent that are awaiting responses",
245
+ "awaiting-approval": "Items pending principal or stakeholder approval",
246
+ "blocked-initiatives": "Strategic initiatives blocked by dependencies",
247
+ "high-risk-items": "Items with elevated risk requiring close monitoring",
248
+ "upcoming-deadlines": "Time-sensitive items with approaching deadlines",
249
+ "relationship-health": "Relationship maintenance and engagement tracking",
250
+ "hiring-critical-path": "Critical hiring actions and pipeline blockers",
251
+ "platform-blockers": "Technical and platform issues blocking delivery",
252
+ "capital-watchpoints": "Financial and fundraising items requiring attention",
253
+ "legal-obligations": "Regulatory submissions and legal compliance deadlines",
254
+ "narrative-opportunities": "Strategic narrative and thought leadership opportunities",
255
+ "tech-debt": "Technical debt with strategic impact",
256
+ "improvement-backlog": "Self-improvement and operational enhancement candidates",
257
+ };
258
+
259
+ for (const [name, desc] of Object.entries(queueTemplates)) {
260
+ const content = `queue_name: ${name}\ndescription: ${desc}\nitems: []\n`;
261
+ writeFileSync(join(targetDir, `state/queues/${name}.yaml`), content);
262
+ }
263
+ ok(`${Object.keys(queueTemplates).length} queue templates`);
264
+
265
+ // Dashboard templates — 10 operational dashboards
266
+ const dashboardTemplates = {
267
+ "executive-summary": {
268
+ description: "Primary situational awareness — read at every session start",
269
+ fields: "generated: null\nsummary:\n total_open_items: 0\n critical_items: 0\n overdue_items: 0\n sla_breaches: 0\n items_resolved_today: 0\n items_created_today: 0\nqueue_health: {}\ntop_priorities: []\ntop_blockers: []\n",
270
+ },
271
+ "daemon-health": {
272
+ description: "System health metrics for the daemon and triggers",
273
+ fields: "status: initialising\nlast_check: null\ntrigger_health: {}\npoller_health: {}\n",
274
+ },
275
+ "strategy-scorecard": {
276
+ description: "Progress on strategic priorities and quarterly OKRs",
277
+ fields: "generated: null\npriorities: []\ninitiatives: []\n",
278
+ },
279
+ "risk-register": {
280
+ description: "Enterprise risk register with severity and mitigation",
281
+ fields: "generated: null\nrisks: []\n",
282
+ },
283
+ "engineering-health": {
284
+ description: "Per-repo engineering activity, PRs, delivery confidence",
285
+ fields: "generated: null\nrepositories: []\ndelivery_confidence: null\n",
286
+ },
287
+ "org-health": {
288
+ description: "Hiring pipeline, capability gaps, structural risks",
289
+ fields: "generated: null\nhiring_pipeline: {}\ncapability_gaps: []\n",
290
+ },
291
+ "leadership-accountability": {
292
+ description: "Per-leader commitment follow-through tracking",
293
+ fields: "generated: null\nleaders: []\n",
294
+ },
295
+ "relationship-pipeline": {
296
+ description: "Strategic relationships with stage and warmth tracking",
297
+ fields: "generated: null\nrelationships: []\n",
298
+ },
299
+ "capital-readiness": {
300
+ description: "Runway, fundraising readiness, investor pipeline",
301
+ fields: "generated: null\nrunway_months: null\nreadiness_score: null\npipeline: []\n",
302
+ },
303
+ "system-health": {
304
+ description: "Self-governance metrics — evolution, coverage, queue health",
305
+ fields: "generated: null\nevolution_changes: 0\nknowledge_coverage: null\ntrigger_reliability: null\n",
306
+ },
307
+ };
308
+
309
+ for (const [name, { description, fields }] of Object.entries(dashboardTemplates)) {
310
+ const content = `# ${name.replace(/-/g, " ").replace(/\b\w/g, c => c.toUpperCase())} Dashboard\n# ${description}\n\n${fields}`;
311
+ writeFileSync(join(targetDir, `state/dashboards/${name}.yaml`), content);
312
+ }
313
+ ok(`${Object.keys(dashboardTemplates).length} dashboard templates`);
314
+
315
+ // Config templates — operational config files
316
+ log("Generating config templates...");
317
+
318
+ const configTemplates = {
319
+ "environment.yaml": `# Environment Configuration
320
+ # All environment-specific values — actual secrets are in .env
321
+ # Agent identity is defined in config/agent.ts
322
+
323
+ system:
324
+ name: maestro
325
+ version: 1.0.0
326
+ operator_persona: "UNCONFIGURED"
327
+ operator_role: "UNCONFIGURED"
328
+ company: ""
329
+ ceo: ""
330
+ timezone: UTC
331
+
332
+ machine:
333
+ type: mac-mini
334
+ os: macOS
335
+ hostname: \${HOSTNAME}
336
+ purpose: Autonomous agent operations node
337
+
338
+ paths:
339
+ agent_home: ~/${targetName}
340
+ logs: ~/${targetName}/logs
341
+ outputs: ~/${targetName}/outputs
342
+ `,
343
+ "contacts.yaml": `# Key Contacts
344
+ # Communication classifications and relationship context
345
+ # Updated by: init-maestro wizard, agent interactions
346
+
347
+ contacts: []
348
+
349
+ # Classification levels:
350
+ # - internal: team members, direct reports
351
+ # - external-trusted: partners, advisors, board members
352
+ # - external-standard: vendors, candidates, general contacts
353
+ # - regulatory: regulators, legal counterparties
354
+ `,
355
+ "priorities.yaml": `# Active Strategic Priorities
356
+ # Updated by: weekly-priorities workflow, principal directive
357
+ # Read by: morning-brief, backlog-executor, strategy-scorecard
358
+
359
+ priorities: []
360
+
361
+ # Priority format:
362
+ # - id: P1
363
+ # title: Priority title
364
+ # status: active | paused | completed
365
+ # owner: principal | agent
366
+ # milestones: []
367
+ # deadline: null
368
+ `,
369
+ "sla-defaults.yaml": `# SLA Defaults — response time expectations by item type
370
+ # Used by: inbox-processor, meeting-action-capture, midday-sweep
371
+
372
+ sla_defaults:
373
+ principal_request: 4
374
+ leadership_request: 24
375
+ external_partner: 48
376
+ regulatory: 24
377
+ candidate: 24
378
+ board_member: 12
379
+ investor: 24
380
+ general: 72
381
+
382
+ thresholds:
383
+ at_risk_percent: 75
384
+ breach_percent: 100
385
+
386
+ drift_thresholds:
387
+ critical_priority: 7
388
+ medium_priority: 14
389
+ low_priority: 30
390
+ `,
391
+ "brand-assets.yaml": `# Brand Assets Configuration
392
+ # Official brand assets, design system, and usage rules
393
+ # Assets directory: public/assets/
394
+
395
+ asset_inventory:
396
+ icon_dark: public/assets/icon-dark.svg
397
+ icon_light: public/assets/icon-light.svg
398
+ logo_dark: public/assets/logo-dark.svg
399
+ logo_light: public/assets/logo-light.svg
400
+
401
+ usage_rules:
402
+ dark_variants: "Use on light/white backgrounds"
403
+ light_variants: "Use on dark backgrounds"
404
+ full_logo: "Cover pages, document headers, title slides"
405
+ icon_only: "Footers, watermarks, compact placements"
406
+ never: "stretch, rotate, recolour, or modify the assets"
407
+ `,
408
+ "repo-registry.yaml": `# Source Repository Registry
409
+ # Cross-repository map for codebase awareness
410
+ # Updated by: init-maestro wizard
411
+
412
+ repositories: []
413
+
414
+ # Format:
415
+ # - name: repo-name
416
+ # path: ~/repo-name
417
+ # purpose: What this repo contains
418
+ # domains: [domain1, domain2]
419
+ `,
420
+ "slack-channels.yaml": `# Slack Channel Configuration
421
+ # Which channels the agent monitors and their purpose
422
+ # Updated by: init-maestro wizard
423
+
424
+ channels: []
425
+
426
+ # Format:
427
+ # - id: C0XXXXX
428
+ # name: channel-name
429
+ # purpose: What this channel is for
430
+ # monitor: true | false
431
+ # respond: true | false
432
+ `,
433
+ "tooling-map.yaml": `# Tooling Map
434
+ # Maps responsibilities to available tools and scripts
435
+ # Generated by: init-maestro sub-agent 8
436
+
437
+ tools: []
438
+ `,
439
+ "validation-rules.yaml": `# Outbound Correspondence Validation Rules
440
+ # Used by: scripts/validate-outbound.py
441
+ # Purpose: Pre-send gate to prevent factual inaccuracies and protocol violations
442
+
443
+ relationship_claims:
444
+ enabled: true
445
+ severity: block
446
+ patterns: []
447
+
448
+ ai_disclosure:
449
+ enabled: true
450
+ severity: block
451
+ blocked_phrases:
452
+ - "I am an AI"
453
+ - "as an AI"
454
+ - "I'm an artificial intelligence"
455
+ - "language model"
456
+
457
+ tone_violations:
458
+ enabled: true
459
+ severity: warn
460
+ rules: []
461
+ `,
462
+ "mcp-integrations.yaml": `# MCP Server Integration Configuration
463
+ # Which MCP services this agent connects to
464
+ # Updated by: init-maestro wizard
465
+
466
+ integrations:
467
+ slack: { enabled: false, notes: "Configure via .env" }
468
+ gmail: { enabled: false, notes: "Configure via .env" }
469
+ calendar: { enabled: false, notes: "Configure via .env" }
470
+ `,
471
+ "mcp-servers.json": `{}`,
472
+ "caller-id-map.yaml": `# Caller ID Map
473
+ # Maps phone numbers to known contacts for SMS/voice identification
474
+ # Updated by: agent interactions
475
+
476
+ contacts: {}
477
+ `,
478
+ "hiring.yaml": `# Hiring Configuration
479
+ # Pipeline settings, screening rules, and template references
480
+ # Updated by: init-maestro wizard (for executive-operator / operations-leader archetypes)
481
+
482
+ enabled: false
483
+ roles: []
484
+ screening:
485
+ auto_advance_threshold: 75
486
+ auto_reject_threshold: 40
487
+ review_band: [40, 75]
488
+ `,
489
+ };
490
+
491
+ for (const [filename, content] of Object.entries(configTemplates)) {
492
+ const dest = join(targetDir, `config/${filename}`);
493
+ if (!existsSync(dest)) {
494
+ writeFileSync(dest, content);
495
+ }
496
+ }
497
+ ok(`${Object.keys(configTemplates).length} config templates`);
498
+
499
+ // Knowledge templates — schemas and empty indexes
500
+ log("Generating knowledge templates...");
501
+
502
+ const knowledgeTemplates = {
503
+ "decisions/decision-schema.yaml": `# Decision Record Schema
504
+ # Format: DEC-YYYY-MM-DD-NNN
505
+ # See docs for full schema documentation
506
+
507
+ schema:
508
+ version: "1.0"
509
+ fields:
510
+ id: { type: string, format: "DEC-YYYY-MM-DD-NNN", required: true }
511
+ date: { type: date, format: "YYYY-MM-DD", required: true }
512
+ title: { type: string, required: true }
513
+ domain: { type: string, required: true }
514
+ decision_maker: { type: string, required: true }
515
+ decision_text: { type: string, required: true }
516
+ context: { type: string, required: true }
517
+ rationale: { type: string, required: true }
518
+ status: { type: string, enum: [active, superseded, reversed, completed], default: active }
519
+ `,
520
+ "decisions/index.yaml": `# Decision Index
521
+ # Auto-maintained by decision-log agent
522
+
523
+ decisions: []
524
+ `,
525
+ "entities/entity-map.yaml": `# Entity Map
526
+ # All contacts, companies, regulators, advisors
527
+ # De-duplication rules and relationship mapping
528
+
529
+ entities: []
530
+ `,
531
+ "entities/org-chart.md": `# Organisation Chart
532
+
533
+ *Configure via /init-maestro*
534
+ `,
535
+ "sources/source-registry.yaml": `# Authoritative Source Registry
536
+ # Central registry of all information sources
537
+
538
+ sources: {}
539
+ `,
540
+ "sources/blocker-playbooks.yaml": `# Blocker Resolution Playbooks
541
+ # Common blocker patterns and resolution strategies
542
+
543
+ playbooks: []
544
+ `,
545
+ "syntheses/strategy-state.yaml": `# Living Strategy State
546
+ # Evolves weekly with strategic position, assumptions, risks, signals
547
+
548
+ last_updated: null
549
+ strategic_position: ""
550
+ key_assumptions: []
551
+ active_risks: []
552
+ market_signals: []
553
+ `,
554
+ "syntheses/README.md": `# Knowledge Syntheses
555
+
556
+ Strategic syntheses generated by domain agents. Updated weekly.
557
+ `,
558
+ "memory/executive-memory.yaml": `# Executive Memory
559
+ # Institutional memory for strategic context
560
+
561
+ entries: []
562
+ `,
563
+ };
564
+
565
+ for (const [path, content] of Object.entries(knowledgeTemplates)) {
566
+ writeFileSync(join(targetDir, `knowledge/${path}`), content);
567
+ }
568
+ ok(`${Object.keys(knowledgeTemplates).length} knowledge templates`);
569
+
570
+ // Self-optimization templates
571
+ const selfOptTemplates = {
572
+ "program.md": `# Self-Optimization Program
573
+
574
+ This agent evolves its own operating setup through structured experimentation.
575
+ Changes are classified by risk: low (auto-apply), medium (apply + notify), high (escalate).
576
+ `,
577
+ "experiment-log.yaml": `# Experiment Log
578
+ experiments: []
579
+ `,
580
+ "failure-patterns.yaml": `# Failure Patterns
581
+ # Recurring failure modes and mitigations
582
+ patterns: []
583
+ `,
584
+ "metric-snapshots.yaml": `# Metric Snapshots
585
+ # Periodic captures of operational metrics
586
+ snapshots: []
587
+ `,
588
+ "scoring-rubrics.yaml": `# Scoring Rubrics
589
+ # Evaluation criteria for self-assessment
590
+ rubrics: []
591
+ `,
592
+ };
593
+
594
+ for (const [filename, content] of Object.entries(selfOptTemplates)) {
595
+ writeFileSync(join(targetDir, `self-optimization/${filename}`), content);
596
+ }
597
+ ok(`${Object.keys(selfOptTemplates).length} self-optimization templates`);
598
+
599
+ // .gitkeep files for empty directories that git would otherwise ignore
600
+ const gitkeepDirs = [
601
+ "state/handoffs", "state/huddle", "state/indexes", "state/rag",
602
+ "state/sessions", "state/slack-responded", "state/slack-thread-tracker",
603
+ "state/tmp", "state/inbox/attachments", "state/inbox/processed",
604
+ "state/inbox/whatsapp", "memory/precedents/market-signals",
605
+ "knowledge/decisions/archive", "tests",
606
+ "logs/infra", "logs/monitor", "logs/phone", "logs/sms",
607
+ "logs/whatsapp", "logs/email", "self-optimization/scenarios",
608
+ ];
609
+
610
+ for (const dir of gitkeepDirs) {
611
+ const keepFile = join(targetDir, dir, ".gitkeep");
612
+ if (!existsSync(keepFile)) {
613
+ writeFileSync(keepFile, "");
614
+ }
615
+ }
616
+ ok(".gitkeep files for empty directories");
617
+
618
+ // ── Step 4: Generate package.json ───────────────────────────────────────
619
+
620
+ log("Generating package.json...");
621
+
622
+ const agentPackage = {
623
+ name: targetName,
624
+ version: "1.0.0",
625
+ description: "Maestro AI agent — configure with: maestro setup",
626
+ type: "module",
627
+ private: true,
628
+ scripts: {
629
+ "init-agent": "./scripts/setup/init-agent.sh",
630
+ "configure-macos": "sudo ./scripts/setup/configure-macos.sh",
631
+ "configure-macos:check": "./scripts/setup/configure-macos.sh --check",
632
+ daemon: "node scripts/daemon/maestro-daemon.mjs",
633
+ "cadence:enqueue": "node scripts/cadence/enqueue-cadence-tick.mjs",
634
+ "cadence:consume": "node scripts/daemon/cadence-consumer.mjs",
635
+ "cadence:status": "node scripts/cadence/cadence-status.mjs",
636
+ healthcheck: "./scripts/healthcheck.sh",
637
+ "emergency-stop": "./scripts/emergency-stop.sh",
638
+ resume: "./scripts/resume-operations.sh",
639
+ "pdf:memo": "node scripts/pdf-generation/build-document.mjs --template memo",
640
+ "pdf:board-pack": "node scripts/pdf-generation/build-document.mjs --template board-pack",
641
+ "pdf:letter": "node scripts/pdf-generation/build-document.mjs --template corporate-letter",
642
+ "pdf:investor": "node scripts/pdf-generation/build-document.mjs --template investor-letter",
643
+ "gen:illustration": "node scripts/media-generation/generate-assets.mjs --illustration",
644
+ "gen:video": "node scripts/media-generation/generate-assets.mjs --video",
645
+ "gen:all-missing": "node scripts/media-generation/generate-assets.mjs --all-missing",
646
+ "gen:list": "node scripts/media-generation/generate-assets.mjs --list",
647
+ "slack:events": "node scripts/slack-events-server.mjs",
648
+ "slack:events:start": "./scripts/slack-events-ctl.sh start",
649
+ "slack:events:stop": "./scripts/slack-events-ctl.sh stop",
650
+ "slack:events:status": "./scripts/slack-events-ctl.sh status",
651
+ upgrade: "npx @cohortapp/agent-sdk upgrade",
652
+ init: "npx @cohortapp/agent-sdk init",
653
+ doctor: "npx @cohortapp/agent-sdk doctor",
654
+ },
655
+ dependencies: {
656
+ "@google/genai": "^1.42.0",
657
+ dotenv: "^16.4.5",
658
+ execa: "^9.6.1",
659
+ imapflow: "^1.2.18",
660
+ mailparser: "^3.9.6",
661
+ openai: "^6.33.0",
662
+ },
663
+ devDependencies: {
664
+ sharp: "^0.34.5",
665
+ tsx: "^4.20.6",
666
+ },
667
+ };
668
+
669
+ writeFileSync(
670
+ join(targetDir, "package.json"),
671
+ JSON.stringify(agentPackage, null, 2) + "\n"
672
+ );
673
+ ok("package.json");
674
+
675
+ // ── Step 5: Copy .env.example and .gitignore ────────────────────────────
676
+
677
+ for (const file of [".env.example", ".gitignore"]) {
678
+ const src = join(MAESTRO_ROOT, file);
679
+ if (existsSync(src)) {
680
+ cpSync(src, join(targetDir, file));
681
+ ok(file);
682
+ }
683
+ }
684
+
685
+ // ── Step 6: Initialize git ──────────────────────────────────────────────
686
+
687
+ log("Initializing git repository...");
688
+ try {
689
+ execFileSync("git", ["init"], { cwd: targetDir, stdio: "pipe" });
690
+ ok("git init");
691
+ } catch {
692
+ warn("git init failed — initialize manually");
693
+ }
694
+
695
+ // ── Step 7: Install dependencies ────────────────────────────────────────
696
+
697
+ log("Installing dependencies...");
698
+ try {
699
+ execFileSync("npm", ["install"], { cwd: targetDir, stdio: "pipe" });
700
+ ok("npm install");
701
+ } catch {
702
+ warn("npm install failed — run manually: cd " + targetDir + " && npm install");
703
+ }
704
+
705
+ // ── Done ────────────────────────────────────────────────────────────────
706
+
707
+ console.log();
708
+ console.log("╔══════════════════════════════════════════════════════════════╗");
709
+ console.log("║ Agent created successfully! ║");
710
+ console.log("╠══════════════════════════════════════════════════════════════╣");
711
+ console.log("║ ║");
712
+ console.log("║ Next steps: ║");
713
+ console.log(`║ 1. cd ${targetName.padEnd(51)}║`);
714
+ console.log('║ 2. maestro setup ║');
715
+ console.log("║ ║");
716
+ console.log("║ The wizard handles everything: identity, company context, ║");
717
+ console.log("║ model/provider, channels, the operating model, and ║");
718
+ console.log("║ self-verification. ║");
719
+ console.log("║ ║");
720
+ console.log("╚══════════════════════════════════════════════════════════════╝");
721
+ console.log();
722
+ }
723
+
724
+ // ---------------------------------------------------------------------------
725
+ // UPGRADE — update framework files in current agent repo (smart merge)
726
+ // ---------------------------------------------------------------------------
727
+ //
728
+ // Upgrade philosophy: by default, never silently overwrite a file the user has
729
+ // modified locally. Detection uses git as the source of truth:
730
+ //
731
+ // • untracked or has-uncommitted-changes in agent repo ⇒ "locally modified"
732
+ // • tracked and clean against HEAD ⇒ "vendored, safe to overwrite"
733
+ //
734
+ // For every framework file we'd otherwise overwrite, we classify into:
735
+ //
736
+ // added — file doesn't exist in agent repo → copy
737
+ // updated — file exists, agent's copy matches HEAD (no local edits) → overwrite
738
+ // same — file exists, content already byte-identical to upstream → skip
739
+ // preserved — file exists with local edits → keep agent's copy, write the
740
+ // upstream version to .maestro/incoming/<path> for manual review
741
+ // (unless --force-overwrite is passed)
742
+ //
743
+ // Flags:
744
+ // --dry-run preview only; no files written
745
+ // --force-overwrite overwrite even locally-modified files (with backup)
746
+ // --no-incoming skip writing .maestro/incoming/ shadows
747
+ // --verbose list every file's classification
748
+
749
+ // Paths that get upgraded. Each entry is { path, mode } where mode is:
750
+ // "smart" — per-file classification described above
751
+ // "merge" — add new files only; never overwrite existing (used for agents/)
752
+ const UPGRADE_PATHS = [
753
+ { path: "scripts", mode: "smart" },
754
+ { path: "policies", mode: "smart" },
755
+ { path: "docs", mode: "smart" },
756
+ { path: "public/assets", mode: "smart" },
757
+ { path: "workflows", mode: "smart" },
758
+ { path: "schedules", mode: "smart" },
759
+ { path: "desktop-control", mode: "smart" },
760
+ { path: "ingest", mode: "smart" },
761
+ { path: "mcp", mode: "smart" },
762
+ { path: ".claude/commands", mode: "smart" },
763
+ { path: "plugins/maestro-skills", mode: "smart" },
764
+ { path: "teams", mode: "smart" },
765
+ { path: "agents", mode: "merge" },
766
+ // Framework primitives — colocated with scripts/ so relative imports
767
+ // (e.g. ../../lib/cadence-bus.mjs from scripts/cadence/*) resolve in
768
+ // every agent repo without needing @cohortapp/agent-sdk in node_modules.
769
+ { path: "lib", mode: "smart" },
770
+ ];
771
+
772
+ function sha256File(p) {
773
+ return createHash("sha256").update(readFileSync(p)).digest("hex");
774
+ }
775
+
776
+ function walkFiles(root) {
777
+ const out = [];
778
+ if (!existsSync(root)) return out;
779
+ const stack = [root];
780
+ while (stack.length) {
781
+ const dir = stack.pop();
782
+ for (const name of readdirSync(dir)) {
783
+ const full = join(dir, name);
784
+ // Use lstat first so we can detect (and skip) symlinks — particularly
785
+ // broken ones, which are common in shared scaffolds.
786
+ let lst;
787
+ try { lst = lstatSync(full); }
788
+ catch { continue; }
789
+ if (lst.isSymbolicLink()) {
790
+ // Skip symlinks entirely — agents shouldn't inherit links into paths
791
+ // that may not exist on the target machine.
792
+ continue;
793
+ }
794
+ if (lst.isDirectory()) stack.push(full);
795
+ else if (lst.isFile()) out.push(full);
796
+ }
797
+ }
798
+ return out;
799
+ }
800
+
801
+ // .maestroignore — gitignore-style allowlist of paths the upgrade must NOT
802
+ // touch. Supports comments (#), directory prefixes (trailing /), exact paths,
803
+ // simple globs (* matches a single path segment, ** matches recursively), and
804
+ // negation (! prefix to un-ignore). Match order is top-to-bottom; last match
805
+ // wins, like .gitignore.
806
+ function loadMaestroignore(cwd) {
807
+ const file = join(cwd, ".maestroignore");
808
+ if (!existsSync(file)) return null;
809
+ const patterns = [];
810
+ for (const raw of readFileSync(file, "utf-8").split(/\r?\n/)) {
811
+ const stripped = raw.replace(/^\s+|\s+$/g, "");
812
+ if (!stripped || stripped.startsWith("#")) continue;
813
+ const negate = stripped.startsWith("!");
814
+ const pat = negate ? stripped.slice(1).trim() : stripped;
815
+ if (!pat) continue;
816
+ patterns.push({ pat, negate });
817
+ }
818
+ return patterns.length ? patterns : null;
819
+ }
820
+
821
+ function patternToRegex(pat) {
822
+ // Helper: convert a glob-aware pattern to its regex source.
823
+ // ** must be substituted before single * so we don't double-rewrite it.
824
+ // * is intentionally outside the regex-escape char class so the
825
+ // glob substitution can find unescaped `*` characters afterward.
826
+ const globToRe = (p) =>
827
+ p.replace(/[.+^${}()|[\]\\]/g, "\\$&")
828
+ .replace(/\*\*/g, "<DBL>")
829
+ .replace(/\*/g, "[^/]*")
830
+ .replace(/<DBL>/g, ".*");
831
+
832
+ // Directory prefix: `scripts/daemon/` or `agents/role-*/` match
833
+ // anything under that path (recursive).
834
+ if (pat.endsWith("/")) {
835
+ return new RegExp("^" + globToRe(pat));
836
+ }
837
+ // Single-line glob: must match in full (no trailing slash).
838
+ if (pat.includes("*")) {
839
+ return new RegExp("^" + globToRe(pat) + "$");
840
+ }
841
+ // Exact: also matches anything underneath (gitignore semantics — a
842
+ // pattern without trailing slash still protects a directory if it
843
+ // resolves to one).
844
+ const escaped = pat.replace(/[.+^${}()|[\]\\]/g, "\\$&");
845
+ return new RegExp("^" + escaped + "(/|$)");
846
+ }
847
+
848
+ function matchesIgnore(repoRel, patterns) {
849
+ if (!patterns) return false;
850
+ let ignored = false;
851
+ for (const { pat, negate } of patterns) {
852
+ if (patternToRegex(pat).test(repoRel)) {
853
+ ignored = !negate;
854
+ }
855
+ }
856
+ return ignored;
857
+ }
858
+
859
+ function isGitRepo(cwd) {
860
+ try {
861
+ execFileSync("git", ["rev-parse", "--is-inside-work-tree"], { cwd, stdio: "pipe" });
862
+ return true;
863
+ } catch { return false; }
864
+ }
865
+
866
+ // Build a Set of repo-relative paths that are dirty (modified, added, untracked).
867
+ // Single git invocation is much faster than per-file checks.
868
+ function dirtyPathSet(cwd) {
869
+ const set = new Set();
870
+ try {
871
+ const out = execFileSync(
872
+ "git",
873
+ ["status", "--porcelain=v1", "-z", "--untracked-files=all"],
874
+ { cwd, encoding: "utf-8" }
875
+ );
876
+ // -z separates records with NUL; each record is "XY path" (no quoting).
877
+ // For renames (R/C) the format is "R newpath\0oldpath\0" — we only care
878
+ // about destination, but for safety we treat both as dirty.
879
+ for (const rec of out.split("\0")) {
880
+ if (!rec) continue;
881
+ const status = rec.slice(0, 2);
882
+ const path = rec.slice(3);
883
+ // Anything in porcelain output is non-clean by definition.
884
+ if (status !== " ") set.add(path);
885
+ }
886
+ } catch {
887
+ // Not a git repo or git failed — caller will fall back to caution mode.
888
+ }
889
+ return set;
890
+ }
891
+
892
+ // ---------------------------------------------------------------------------
893
+ // Cadence-bus migration (called from upgrade)
894
+ // ---------------------------------------------------------------------------
895
+ //
896
+ // As of maestro 1.8 scheduled cadence ticks no longer spawn a fresh Claude
897
+ // Code session per tick. launchd plists now invoke a lightweight Node
898
+ // enqueue script that drops a JSON event onto state/cadence-bus/; the
899
+ // persistent maestro-daemon.mjs consumes the bus.
900
+ //
901
+ // This migration runs idempotently on every upgrade:
902
+ // 1. Detect generated plists at scripts/local-triggers/plists/ that still
903
+ // reference run-trigger.sh (the legacy per-tick spawn path).
904
+ // 2. Back them up to .maestro/backup/plists/<utc-timestamp>/.
905
+ // 3. Regenerate plists via scripts/local-triggers/generate-plists.sh so
906
+ // the on-disk files match the cadence-bus architecture.
907
+ // 4. Surface installed plists at ~/Library/LaunchAgents/ that still match
908
+ // the legacy pattern, with exact launchctl unload/load commands the
909
+ // operator can run.
910
+ //
911
+ // Returns a summary object the caller uses to print user-visible output.
912
+
913
+ function plistRefersToLegacy(path) {
914
+ try {
915
+ const body = readFileSync(path, "utf-8");
916
+ return body.includes(LEGACY_PLIST_INDICATOR);
917
+ } catch {
918
+ return false;
919
+ }
920
+ }
921
+
922
+ function plistMatchesArch(path) {
923
+ try {
924
+ const body = readFileSync(path, "utf-8");
925
+ return body.includes(PLIST_ARCH_MARKER);
926
+ } catch {
927
+ return false;
928
+ }
929
+ }
930
+
931
+ function listPlists(dir) {
932
+ if (!existsSync(dir)) return [];
933
+ try {
934
+ return readdirSync(dir).filter((n) => n.endsWith(".plist")).map((n) => join(dir, n));
935
+ } catch {
936
+ return [];
937
+ }
938
+ }
939
+
940
+ function migrateCadenceBus(cwd, flags) {
941
+ const summary = {
942
+ generated_legacy: [],
943
+ generated_modern: [],
944
+ installed_legacy: [],
945
+ backed_up: [],
946
+ regenerated: false,
947
+ skipped_regen_reason: null,
948
+ bus_dir: null,
949
+ notes: [],
950
+ };
951
+
952
+ // Bootstrap the cadence-bus directory tree even when nothing else is
953
+ // moving — needed for fresh installs that upgrade through this path.
954
+ const busBase = join(cwd, "state", "cadence-bus");
955
+ summary.bus_dir = busBase;
956
+ for (const d of ["inbox", "claimed", "processed", "failed", "dlq"]) {
957
+ const full = join(busBase, d);
958
+ if (!existsSync(full) && !flags.dryRun) {
959
+ mkdirSync(full, { recursive: true });
960
+ }
961
+ }
962
+ const versionFile = join(busBase, "VERSION");
963
+ if (!existsSync(versionFile) && !flags.dryRun) {
964
+ writeFileSync(versionFile, "1\n");
965
+ }
966
+
967
+ // Inspect generated plists.
968
+ const generatedDir = join(cwd, "scripts/local-triggers/plists");
969
+ const generated = listPlists(generatedDir);
970
+ for (const p of generated) {
971
+ const rel = relative(cwd, p);
972
+ if (plistRefersToLegacy(p)) summary.generated_legacy.push(rel);
973
+ else if (plistMatchesArch(p)) summary.generated_modern.push(rel);
974
+ }
975
+
976
+ // Inspect installed plists.
977
+ const installedDir = join(process.env.HOME || "/", "Library/LaunchAgents");
978
+ for (const p of listPlists(installedDir)) {
979
+ const name = basename(p);
980
+ // Only consider plists owned by Maestro — labels start with ai.maestro.
981
+ // (or legacy ai.adaptic. for agents installed before the rename).
982
+ if (!name.startsWith("ai.maestro.") && !name.startsWith("ai.adaptic.")) continue;
983
+ if (plistRefersToLegacy(p)) summary.installed_legacy.push(p);
984
+ }
985
+
986
+ // Back up any legacy generated plists.
987
+ if (summary.generated_legacy.length > 0) {
988
+ const ts = new Date().toISOString().replace(/[:.]/g, "-");
989
+ const backupDir = join(cwd, ".maestro/backup/plists", ts);
990
+ if (!flags.dryRun) {
991
+ mkdirSync(backupDir, { recursive: true });
992
+ for (const rel of summary.generated_legacy) {
993
+ const src = join(cwd, rel);
994
+ const dst = join(backupDir, basename(src));
995
+ try {
996
+ copyFileSync(src, dst);
997
+ summary.backed_up.push(relative(cwd, dst));
998
+ } catch (err) {
999
+ summary.notes.push(`backup failed for ${rel}: ${err.message}`);
1000
+ }
1001
+ }
1002
+ } else {
1003
+ // Dry-run: still report what would be backed up.
1004
+ summary.notes.push(`would back up ${summary.generated_legacy.length} legacy plist(s) to .maestro/backup/plists/${ts}/`);
1005
+ }
1006
+
1007
+ // Regenerate via the bundled script. Soft failure — operator can rerun.
1008
+ const generator = join(cwd, "scripts/local-triggers/generate-plists.sh");
1009
+ if (existsSync(generator)) {
1010
+ if (!flags.dryRun) {
1011
+ try {
1012
+ execFileSync("/bin/bash", [generator], { cwd, stdio: "pipe" });
1013
+ summary.regenerated = true;
1014
+ } catch (err) {
1015
+ summary.skipped_regen_reason = `generate-plists.sh failed: ${err.message.split("\n")[0]}`;
1016
+ }
1017
+ } else {
1018
+ summary.notes.push("dry-run: would regenerate plists via scripts/local-triggers/generate-plists.sh");
1019
+ }
1020
+ } else {
1021
+ summary.skipped_regen_reason = "scripts/local-triggers/generate-plists.sh missing";
1022
+ }
1023
+ }
1024
+
1025
+ return summary;
1026
+ }
1027
+
1028
+ function printCadenceBusSummary(summary, flags) {
1029
+ console.log();
1030
+ console.log(`${C.bold}Cadence bus migration${C.reset}${flags.dryRun ? " (dry run)" : ""}`);
1031
+ if (summary.generated_legacy.length === 0 && summary.installed_legacy.length === 0) {
1032
+ ok("Already on cadence-bus architecture (no legacy plists found).");
1033
+ if (summary.generated_modern.length > 0) {
1034
+ ok(`${summary.generated_modern.length} generated plist(s) carry the cadence-bus marker.`);
1035
+ }
1036
+ return;
1037
+ }
1038
+ if (summary.generated_legacy.length > 0) {
1039
+ warn(`${summary.generated_legacy.length} generated plist(s) still call run-trigger.sh directly:`);
1040
+ for (const p of summary.generated_legacy.slice(0, 8)) console.log(` ${p}`);
1041
+ if (summary.generated_legacy.length > 8) {
1042
+ console.log(` … and ${summary.generated_legacy.length - 8} more`);
1043
+ }
1044
+ }
1045
+ if (summary.backed_up.length > 0) {
1046
+ ok(`Backed up ${summary.backed_up.length} legacy plist(s) under ${dirname(summary.backed_up[0])}`);
1047
+ }
1048
+ if (summary.regenerated) {
1049
+ ok("Regenerated plists with the cadence-bus architecture.");
1050
+ } else if (summary.skipped_regen_reason) {
1051
+ warn(`Skipped plist regeneration: ${summary.skipped_regen_reason}`);
1052
+ warn("Run manually: ./scripts/local-triggers/generate-plists.sh");
1053
+ }
1054
+
1055
+ if (summary.installed_legacy.length > 0) {
1056
+ console.log();
1057
+ warn(`${summary.installed_legacy.length} installed launchd plist(s) at ~/Library/LaunchAgents/ still use the legacy spawn-per-tick pattern.`);
1058
+ warn("Reload them so launchctl picks up the cadence-bus versions:");
1059
+ for (const p of summary.installed_legacy.slice(0, 8)) {
1060
+ const label = basename(p, ".plist");
1061
+ console.log(` launchctl unload "${p}" && launchctl load "${p}"`);
1062
+ // Cosmetic alias for label clarity.
1063
+ void label;
1064
+ }
1065
+ if (summary.installed_legacy.length > 8) {
1066
+ console.log(` … and ${summary.installed_legacy.length - 8} more`);
1067
+ }
1068
+ }
1069
+
1070
+ for (const note of summary.notes) console.log(` note: ${note}`);
1071
+ }
1072
+
1073
+ function parseUpgradeFlags(args) {
1074
+ const flags = {
1075
+ dryRun: false,
1076
+ forceOverwrite: false,
1077
+ noIncoming: false,
1078
+ verbose: false,
1079
+ };
1080
+ for (const a of args) {
1081
+ if (a === "--dry-run" || a === "-n") flags.dryRun = true;
1082
+ else if (a === "--force-overwrite" || a === "--force") flags.forceOverwrite = true;
1083
+ else if (a === "--no-incoming") flags.noIncoming = true;
1084
+ else if (a === "--verbose" || a === "-v") flags.verbose = true;
1085
+ else if (a === "--help" || a === "-h") return null;
1086
+ else { fail(`Unknown flag: ${a}`); process.exit(1); }
1087
+ }
1088
+ return flags;
1089
+ }
1090
+
1091
+ async function upgrade(args = []) {
1092
+ const flags = parseUpgradeFlags(args);
1093
+ if (flags === null) {
1094
+ console.log(`
1095
+ Usage: maestro upgrade [flags]
1096
+
1097
+ Flags:
1098
+ --dry-run, -n Preview changes without writing
1099
+ --force-overwrite Overwrite even locally-modified files (backs them up)
1100
+ --no-incoming Don't write .maestro/incoming/ shadows for preserved files
1101
+ --verbose, -v Print classification for every file
1102
+ --help, -h Show this help
1103
+
1104
+ Per-file behaviour:
1105
+ added — new upstream file → copy
1106
+ updated — existed, byte-equal to your committed copy → safe overwrite
1107
+ same — existed and already byte-identical to upstream → skip
1108
+ ignored — matches a pattern in .maestroignore → never touched (any state)
1109
+ preserved — has uncommitted local edits → keep yours, upstream lands at
1110
+ .maestro/incoming/<path> for manual diff
1111
+ mergeKept — under agents/ → never overwrite (custom agents preserved)
1112
+ forced — overwritten by --force-overwrite; backup at .maestro/backup/<path>
1113
+
1114
+ .maestroignore format (gitignore-style, top-down, last match wins):
1115
+ scripts/slack-send.sh exact file
1116
+ scripts/daemon/ directory and everything beneath
1117
+ scripts/send-email-*.py single-segment glob
1118
+ workflows/** recursive glob
1119
+ !workflows/quarterly/ un-ignore (force overwrite)
1120
+ `);
1121
+ return;
1122
+ }
1123
+
1124
+ const cwd = process.cwd();
1125
+
1126
+ // config/agent.json is the single source of truth (config/agent.ts is the
1127
+ // derived wrapper). Accept either as the agent-repo signal — but agent.json is
1128
+ // primary; a CLAUDE.md alone (framework checkout) still qualifies for upgrade.
1129
+ if (!existsSync(join(cwd, "config/agent.json")) && !existsSync(join(cwd, "config/agent.ts")) && !existsSync(join(cwd, "CLAUDE.md"))) {
1130
+ fail("Not a Maestro agent directory (config/agent.json not found)");
1131
+ process.exit(1);
1132
+ }
1133
+
1134
+ const inGit = isGitRepo(cwd);
1135
+ if (!inGit) {
1136
+ warn("Not in a git repo — cannot detect local modifications.");
1137
+ warn("All existing files will be treated as locally modified and preserved.");
1138
+ warn("Use --force-overwrite to override.");
1139
+ }
1140
+
1141
+ const dirty = inGit ? dirtyPathSet(cwd) : null;
1142
+ const ignorePatterns = loadMaestroignore(cwd);
1143
+ if (ignorePatterns) {
1144
+ log(`.maestroignore loaded (${ignorePatterns.length} pattern${ignorePatterns.length === 1 ? "" : "s"})`);
1145
+ }
1146
+
1147
+ const banner = flags.dryRun ? "DRY RUN — " : "";
1148
+ log(`${banner}Upgrading framework files from @cohortapp/agent-sdk...`);
1149
+
1150
+ const counts = { added: 0, updated: 0, same: 0, ignored: 0, preserved: 0, mergeKept: 0, forced: 0 };
1151
+ const preservedFiles = [];
1152
+ const ignoredFiles = [];
1153
+
1154
+ for (const { path: relRoot, mode } of UPGRADE_PATHS) {
1155
+ const srcRoot = join(MAESTRO_ROOT, relRoot);
1156
+ const dstRoot = join(cwd, relRoot);
1157
+ if (!existsSync(srcRoot)) continue;
1158
+
1159
+ const srcFiles = walkFiles(srcRoot);
1160
+ for (const srcFile of srcFiles) {
1161
+ const relFromRoot = relative(srcRoot, srcFile);
1162
+ const dstFile = join(dstRoot, relFromRoot);
1163
+ const repoRel = relative(cwd, dstFile).split(sep).join("/");
1164
+
1165
+ const isIgnored = matchesIgnore(repoRel, ignorePatterns);
1166
+
1167
+ // Case 0: .maestroignore match — never touch, regardless of state.
1168
+ // Checked BEFORE existsSync so that protected new-files-from-upstream
1169
+ // are never silently introduced into the agent repo either.
1170
+ if (isIgnored) {
1171
+ counts.ignored++;
1172
+ ignoredFiles.push(repoRel);
1173
+ if (flags.verbose) console.log(` · ${repoRel} (ignored via .maestroignore)`);
1174
+ continue;
1175
+ }
1176
+
1177
+ // Case 1: new file (doesn't exist in agent) — always copy.
1178
+ if (!existsSync(dstFile)) {
1179
+ if (!flags.dryRun) {
1180
+ mkdirSync(dirname(dstFile), { recursive: true });
1181
+ copyFileSync(srcFile, dstFile);
1182
+ }
1183
+ counts.added++;
1184
+ if (flags.verbose) ok(`+ ${repoRel}`);
1185
+ continue;
1186
+ }
1187
+
1188
+ // Case 2: byte-identical — nothing to do.
1189
+ const srcHash = sha256File(srcFile);
1190
+ const dstHash = sha256File(dstFile);
1191
+ if (srcHash === dstHash) {
1192
+ counts.same++;
1193
+ if (flags.verbose) console.log(` = ${repoRel}`);
1194
+ continue;
1195
+ }
1196
+
1197
+ // Case 3: merge-mode never overwrites existing files.
1198
+ if (mode === "merge") {
1199
+ counts.mergeKept++;
1200
+ if (flags.verbose) console.log(` ~ ${repoRel} (merge-mode, kept)`);
1201
+ continue;
1202
+ }
1203
+
1204
+ // Case 4: locally modified? Determine via git dirty set.
1205
+ const locallyModified = inGit ? dirty.has(repoRel) : true;
1206
+
1207
+ if (locallyModified && !flags.forceOverwrite) {
1208
+ counts.preserved++;
1209
+ preservedFiles.push(repoRel);
1210
+ if (!flags.dryRun && !flags.noIncoming) {
1211
+ const shadow = join(cwd, ".maestro", "incoming", repoRel);
1212
+ mkdirSync(dirname(shadow), { recursive: true });
1213
+ copyFileSync(srcFile, shadow);
1214
+ }
1215
+ if (flags.verbose) console.log(` ~ ${repoRel} (local edits, preserved)`);
1216
+ continue;
1217
+ }
1218
+
1219
+ // Case 5: overwrite (clean tracked file, or --force).
1220
+ if (locallyModified && flags.forceOverwrite) {
1221
+ // Back up the about-to-be-clobbered file.
1222
+ if (!flags.dryRun) {
1223
+ const backup = join(cwd, ".maestro", "backup", repoRel);
1224
+ mkdirSync(dirname(backup), { recursive: true });
1225
+ copyFileSync(dstFile, backup);
1226
+ }
1227
+ counts.forced++;
1228
+ if (flags.verbose) warn(`! ${repoRel} (forced; backup written)`);
1229
+ } else {
1230
+ counts.updated++;
1231
+ if (flags.verbose) console.log(` > ${repoRel}`);
1232
+ }
1233
+ if (!flags.dryRun) {
1234
+ mkdirSync(dirname(dstFile), { recursive: true });
1235
+ copyFileSync(srcFile, dstFile);
1236
+ }
1237
+ }
1238
+ }
1239
+
1240
+ // Ensure standard runtime directories exist.
1241
+ const ensureDirs = [
1242
+ "state/handoffs", "state/huddle", "state/indexes", "state/rag",
1243
+ "state/sessions", "state/slack-responded", "state/slack-thread-tracker",
1244
+ "state/tmp", "state/inbox/whatsapp", "state/inbox/attachments",
1245
+ "state/inbox/processed", "memory/precedents/market-signals",
1246
+ "knowledge/decisions/archive", "self-optimization/scenarios", "tests",
1247
+ "logs/infra", "logs/monitor", "logs/phone", "logs/sms",
1248
+ "logs/whatsapp", "logs/email",
1249
+ // Cadence bus (architecture introduced in maestro 1.8). Idempotent.
1250
+ "state/cadence-bus", "state/cadence-bus/inbox", "state/cadence-bus/claimed",
1251
+ "state/cadence-bus/processed", "state/cadence-bus/failed", "state/cadence-bus/dlq",
1252
+ "logs/cadence-bus",
1253
+ // Org/secrets/diagnostics/scheduling/hook spine (maestro 1.18+). Runtime
1254
+ // dirs for lib/{secrets,diagnostics,scheduling,hooks,org}; all fail-open if
1255
+ // absent, but pre-creating them avoids first-write races. Idempotent.
1256
+ "state/secrets", "state/scheduling", "state/scheduling/jobs",
1257
+ "logs/diagnostics", "logs/diagnostics/counters", "logs/traces",
1258
+ "logs/events", "logs/audit/hooks",
1259
+ ];
1260
+ let newDirs = 0;
1261
+ for (const dir of ensureDirs) {
1262
+ const dirPath = join(cwd, dir);
1263
+ if (!existsSync(dirPath)) {
1264
+ if (!flags.dryRun) mkdirSync(dirPath, { recursive: true });
1265
+ newDirs++;
1266
+ }
1267
+ }
1268
+
1269
+ // ── Scaffold config templates (add-only) ────────────────────────────────
1270
+ // scaffold/config/* are disabled-by-default templates a new agent gets at
1271
+ // `create` time (cpSync flattens scaffold/ over the repo root). UPGRADE_PATHS
1272
+ // deliberately does NOT include config/ (it is operator-owned, never smart-
1273
+ // merged), so existing agents would otherwise never receive NEW config
1274
+ // templates introduced by a framework rev — e.g. org.yaml / secrets.yaml /
1275
+ // alerts.yaml shipped with the org/secrets/diagnostics systems. Copy any
1276
+ // scaffold/config file that the agent does NOT already have. We NEVER
1277
+ // overwrite an existing config (operator-owned, may be customised or enabled);
1278
+ // .maestroignore and --dry-run are honoured. The `.example` companions land
1279
+ // too so the agent has the documented full form alongside the live default.
1280
+ const scaffoldConfigDir = join(SCAFFOLD_DIR, "config");
1281
+ let newConfigs = 0;
1282
+ if (existsSync(scaffoldConfigDir)) {
1283
+ for (const name of readdirSync(scaffoldConfigDir)) {
1284
+ const src = join(scaffoldConfigDir, name);
1285
+ let st;
1286
+ try { st = lstatSync(src); } catch { continue; }
1287
+ if (!st.isFile()) continue; // configs are flat files
1288
+ const dst = join(cwd, "config", name);
1289
+ const repoRel = relative(cwd, dst).split(sep).join("/");
1290
+ if (matchesIgnore(repoRel, ignorePatterns)) {
1291
+ counts.ignored++;
1292
+ ignoredFiles.push(repoRel);
1293
+ if (flags.verbose) console.log(` · ${repoRel} (ignored via .maestroignore)`);
1294
+ continue;
1295
+ }
1296
+ if (existsSync(dst)) {
1297
+ // Operator owns config/ — never clobber an existing template.
1298
+ if (flags.verbose) console.log(` ~ ${repoRel} (config exists, kept)`);
1299
+ continue;
1300
+ }
1301
+ if (!flags.dryRun) {
1302
+ mkdirSync(dirname(dst), { recursive: true });
1303
+ copyFileSync(src, dst);
1304
+ }
1305
+ counts.added++;
1306
+ newConfigs++;
1307
+ if (flags.verbose) ok(`+ ${repoRel}`);
1308
+ }
1309
+ }
1310
+
1311
+ // ── SoT self-heal: kill the agent.ts/agent.json split-brain ─────────────
1312
+ // The single source of truth is config/agent.json with config/agent.ts as a
1313
+ // thin re-export wrapper. Legacy repos carry a standalone literal agent.ts
1314
+ // (and may lack agent.json). Auto-migrate so they self-heal on upgrade.
1315
+ if (!flags.dryRun) {
1316
+ try {
1317
+ const tsPath = join(cwd, "config/agent.ts");
1318
+ const jsonPath = join(cwd, "config/agent.json");
1319
+ const tsSrc = existsSync(tsPath) ? readFileSync(tsPath, "utf-8") : "";
1320
+ const tsIsWrapper = /import\s+\w+\s+from\s+['"]\.\/agent\.json['"]/.test(tsSrc);
1321
+ const tsIsStandalone = existsSync(tsPath) && /export\s+const\s+agent\s*(?::[^=]+)?=\s*\{/.test(tsSrc) && !tsIsWrapper;
1322
+
1323
+ if (!existsSync(jsonPath) && tsIsStandalone) {
1324
+ // No SoT yet — run the full migration (extracts agent.json + writes wrapper).
1325
+ try {
1326
+ execFileSync("node", [join(MAESTRO_ROOT, "scripts/setup/migrate-agent-to-sot.mjs")], {
1327
+ cwd, env: { ...process.env, AGENT_DIR: cwd, AGENT_ROOT: cwd, MAESTRO_ROOT }, stdio: "pipe",
1328
+ });
1329
+ ok("migrated config/agent.ts → config/agent.json + thin wrapper (SoT self-heal)");
1330
+ } catch (e) {
1331
+ warn(`SoT migration could not run automatically: ${e && e.message ? e.message : e}`);
1332
+ warn(" Run manually: npx tsx scripts/setup/migrate-agent-to-sot.mjs");
1333
+ }
1334
+ } else if (existsSync(jsonPath) && tsIsStandalone) {
1335
+ // SoT exists but the .ts is still a standalone literal — just re-wrap it.
1336
+ const { ensureSotWrapper } = await import(join(MAESTRO_ROOT, "lib", "setup", "sot.mjs"));
1337
+ const r = ensureSotWrapper(cwd);
1338
+ if (r.wrote) ok("rewrote config/agent.ts as the SoT wrapper (re-exports agent.json)");
1339
+ }
1340
+ } catch (e) {
1341
+ warn(`SoT self-heal skipped: ${e && e.message ? e.message : e}`);
1342
+ }
1343
+ }
1344
+
1345
+ // Cadence-bus migration: detect legacy spawn-per-tick plists and rewrite
1346
+ // them so launchd enqueues cadence events instead of spawning Claude.
1347
+ const cadenceMigration = migrateCadenceBus(cwd, flags);
1348
+
1349
+ // Regenerate derived config from config/agent.json (the SOT).
1350
+ // Soft failures — older agents that haven't migrated to the SOT layout
1351
+ // yet don't have agent.json and that's fine.
1352
+ if (!flags.dryRun && existsSync(join(cwd, "config", "agent.json"))) {
1353
+ // config/agent.env — env-var file consumed by shell scripts.
1354
+ try {
1355
+ execFileSync(
1356
+ "node",
1357
+ [join(MAESTRO_ROOT, "scripts/setup/generate-agent-env.mjs")],
1358
+ { cwd, env: { ...process.env, AGENT_DIR: cwd }, stdio: "pipe" }
1359
+ );
1360
+ ok("regenerated config/agent.env");
1361
+ } catch (e) {
1362
+ warn(`could not regenerate config/agent.env: ${e.message}`);
1363
+ }
1364
+ // config/environment.yaml — operational config consumed by workflows.
1365
+ // Replaces the previously-static hardcoded identity fields.
1366
+ try {
1367
+ execFileSync(
1368
+ "node",
1369
+ [join(MAESTRO_ROOT, "scripts/setup/render-environment-yaml.mjs")],
1370
+ { cwd, env: { ...process.env, AGENT_DIR: cwd }, stdio: "pipe" }
1371
+ );
1372
+ ok("regenerated config/environment.yaml");
1373
+ } catch (e) {
1374
+ warn(`could not regenerate config/environment.yaml: ${e.message}`);
1375
+ }
1376
+ }
1377
+
1378
+ // Summary
1379
+ console.log();
1380
+ console.log(`${C.bold}Upgrade summary${C.reset}${flags.dryRun ? " (dry run, nothing written)" : ""}`);
1381
+ if (counts.added) ok(`${counts.added} added (new files)`);
1382
+ if (counts.updated) ok(`${counts.updated} updated (vendored, no local edits)`);
1383
+ if (counts.same) console.log(` = ${counts.same} already up-to-date`);
1384
+ if (counts.ignored) console.log(` · ${counts.ignored} ignored (.maestroignore protected)`);
1385
+ if (counts.mergeKept) console.log(` ~ ${counts.mergeKept} merge-mode kept (agents/ custom files preserved)`);
1386
+ if (counts.preserved) warn(`${counts.preserved} preserved (local edits — kept your version)`);
1387
+ if (counts.forced) warn(`${counts.forced} force-overwritten (backups in .maestro/backup/)`);
1388
+ if (newDirs) ok(`${newDirs} new directories created`);
1389
+
1390
+ if (preservedFiles.length && !flags.noIncoming && !flags.dryRun) {
1391
+ console.log();
1392
+ log("Upstream versions of your locally-modified files saved to .maestro/incoming/");
1393
+ log("Review with: diff <path> .maestro/incoming/<path>");
1394
+ for (const p of preservedFiles.slice(0, 5)) console.log(` ${p}`);
1395
+ if (preservedFiles.length > 5) console.log(` … and ${preservedFiles.length - 5} more`);
1396
+ }
1397
+
1398
+ // Surface the cadence-bus migration summary AFTER the file-by-file
1399
+ // upgrade summary so the operator sees both layers of change.
1400
+ printCadenceBusSummary(cadenceMigration, flags);
1401
+
1402
+ // Reconcile framework features — run auto-init steps, queue manual ones.
1403
+ if (!flags.dryRun && existsSync(FRAMEWORK_FEATURES_PATH)) {
1404
+ try {
1405
+ const { loadRegistry, reconcileFeatures } = await import("../lib/feature-init.mjs");
1406
+ const registry = loadRegistry(FRAMEWORK_FEATURES_PATH);
1407
+ console.log();
1408
+ log("Reconciling framework features...");
1409
+ const result = reconcileFeatures({
1410
+ maestroRoot: MAESTRO_ROOT,
1411
+ agentRoot: cwd,
1412
+ registry,
1413
+ runAuto: true,
1414
+ logger: (entry) => {
1415
+ if (entry.stage === "completed") ok(`init: ${entry.feature}`);
1416
+ else if (entry.stage === "failed") warn(`init failed: ${entry.feature} — ${entry.message}`);
1417
+ else if (entry.stage === "pending") console.log(` ~ pending: ${entry.feature} (run \`maestro init ${entry.feature} --apply\`)`);
1418
+ },
1419
+ });
1420
+ if (result.pending.length) {
1421
+ warn(`${result.pending.length} feature(s) still pending — run: maestro init`);
1422
+ } else if (result.ranAuto.length) {
1423
+ ok(`${result.ranAuto.length} feature(s) initialised this run`);
1424
+ } else {
1425
+ ok("All framework features up to date");
1426
+ }
1427
+ } catch (e) {
1428
+ warn(`feature reconciliation skipped: ${e.message}`);
1429
+ }
1430
+ }
1431
+
1432
+ console.log();
1433
+ log("Agent-specific paths (config/, CLAUDE.md, knowledge/, memory/, state/, outputs/, logs/, .env) were NOT touched.");
1434
+ }
1435
+
1436
+ // ---------------------------------------------------------------------------
1437
+ // DOCTOR — cost-telemetry tripwire (pure predicate, unit-tested)
1438
+ // ---------------------------------------------------------------------------
1439
+ //
1440
+ // Observability audit F2: the daily-budget governor sums estimated_usd across
1441
+ // today's cost ledger rows. If callers spawn sessions but record $0 (the
1442
+ // historic --input-tokens 0 --output-tokens 0 bug), the ledger fills with rows
1443
+ // whose estimated_usd is 0, the spend band is permanently 0%, and the budget
1444
+ // cap silently can never trip. Doctor surfaces that "blind" state in RED.
1445
+ //
1446
+ // The ledger lives at <agentRoot>/state/cost-tracking/<UTC-YYYY-MM-DD>.jsonl
1447
+ // and is written by scripts/cost/track-claude-usage.mjs (record subcommand).
1448
+ // Each row carries estimated_usd (the tracker computes it from token counts).
1449
+ //
1450
+ // evaluateCostTripwire is a pure function so the red/green decision is testable
1451
+ // in isolation from disk + the doctor's side-effecting ok/warn/fail output.
1452
+
1453
+ /**
1454
+ * Sum estimated_usd and count rows from already-parsed ledger rows.
1455
+ * Tolerant of missing/NaN estimated_usd (treated as 0) and ignores non-objects.
1456
+ * @param {Array<object>} rows
1457
+ * @returns {{ rowCount: number, totalUsd: number }}
1458
+ */
1459
+ function summariseLedgerRows(rows) {
1460
+ let rowCount = 0;
1461
+ let totalUsd = 0;
1462
+ for (const row of rows || []) {
1463
+ if (!row || typeof row !== "object") continue;
1464
+ rowCount++;
1465
+ const usd = Number(row.estimated_usd);
1466
+ if (Number.isFinite(usd)) totalUsd += usd;
1467
+ }
1468
+ return { rowCount, totalUsd: +totalUsd.toFixed(6) };
1469
+ }
1470
+
1471
+ /**
1472
+ * Decide the cost-telemetry posture from today's ledger rows and a count of
1473
+ * sessions known to have run today (rows themselves OR a daemon heartbeat that
1474
+ * proves activity). The contract the spec pins down:
1475
+ *
1476
+ * RED — sessions ran today (rows exist OR heartbeat active) but summed
1477
+ * estimated_usd === 0 → "spend telemetry is blind".
1478
+ * GREEN — no sessions ran (nothing to measure) OR spend > 0.
1479
+ *
1480
+ * @param {{ rows?: Array<object>, sessionCount?: number }} input
1481
+ * rows — parsed ledger rows for today (may be empty)
1482
+ * sessionCount — sessions evidenced today from outside the ledger (e.g. a
1483
+ * fresh daemon heartbeat). Activity = (rows.length>0) || (sessionCount>0).
1484
+ * @returns {{ red: boolean, status: "red"|"green", reason: string, rowCount: number, totalUsd: number, sessionCount: number }}
1485
+ */
1486
+ function evaluateCostTripwire({ rows = [], sessionCount = 0 } = {}) {
1487
+ const { rowCount, totalUsd } = summariseLedgerRows(rows);
1488
+ const sessions = Math.max(0, Number(sessionCount) || 0);
1489
+ const hadActivity = rowCount > 0 || sessions > 0;
1490
+
1491
+ if (!hadActivity) {
1492
+ return {
1493
+ red: false,
1494
+ status: "green",
1495
+ reason: "no sessions ran today — nothing to measure",
1496
+ rowCount,
1497
+ totalUsd,
1498
+ sessionCount: sessions,
1499
+ };
1500
+ }
1501
+ if (totalUsd > 0) {
1502
+ return {
1503
+ red: false,
1504
+ status: "green",
1505
+ reason: `spend telemetry live ($${totalUsd} across ${rowCount} ledger row(s))`,
1506
+ rowCount,
1507
+ totalUsd,
1508
+ sessionCount: sessions,
1509
+ };
1510
+ }
1511
+ return {
1512
+ red: true,
1513
+ status: "red",
1514
+ reason: `spend telemetry is blind — ${hadActivity ? "sessions ran today" : ""} but ledger sums $0 (${rowCount} row(s)${sessions ? `, heartbeat-active` : ""}). Callers are recording 0 tokens; budget governor can never trip on spend.`,
1515
+ rowCount,
1516
+ totalUsd,
1517
+ sessionCount: sessions,
1518
+ };
1519
+ }
1520
+
1521
+ /**
1522
+ * Staleness ladder for the router's data files (pricing cache & catalog cost
1523
+ * provenance, and the policy overlay): a freshly-fetched figure is GREEN; a
1524
+ * `fetched`/`issued_at` date older than 7 days is WARN; older than 30 days is
1525
+ * FAIL. Volatile-priced rows whose provenance is stale are the dangerous case
1526
+ * (a promo expiry silently busting a budget), so a stale catalog with any
1527
+ * volatile row escalates straight to FAIL at 30 d. Pure + testable; the doctor
1528
+ * maps the verdict onto ok/warn/fail.
1529
+ *
1530
+ * @param {{ fetched?: string|null, now?: number, hasVolatile?: boolean, label?: string }} input
1531
+ * fetched — ISO date (YYYY-MM-DD or full ISO) the data was last refreshed
1532
+ * now — epoch ms (injectable for tests; default Date.now)
1533
+ * hasVolatile — true if any priced row is volatile (escalates the FAIL case)
1534
+ * @returns {{ status: "green"|"warn"|"fail", ageDays: number|null, reason: string }}
1535
+ */
1536
+ function evaluateStaleness({ fetched, now = Date.now(), hasVolatile = false, label = "data" } = {}) {
1537
+ if (!fetched) {
1538
+ return { status: "warn", ageDays: null, reason: `${label} has no fetch date — provenance unknown` };
1539
+ }
1540
+ const t = new Date(fetched).getTime();
1541
+ if (!Number.isFinite(t)) {
1542
+ return { status: "warn", ageDays: null, reason: `${label} fetch date "${fetched}" is unparseable` };
1543
+ }
1544
+ const ageDays = Math.floor((now - t) / 86_400_000);
1545
+ if (ageDays >= 30) {
1546
+ return {
1547
+ status: "fail",
1548
+ ageDays,
1549
+ reason: `${label} is ${ageDays}d old (>30d)${hasVolatile ? " and prices volatile — a promo expiry may be silently busting the budget" : ""}`,
1550
+ };
1551
+ }
1552
+ if (ageDays >= 7) {
1553
+ return { status: "warn", ageDays, reason: `${label} is ${ageDays}d old (>7d) — refresh the pricing cache` };
1554
+ }
1555
+ return { status: "green", ageDays, reason: `${label} fresh (${ageDays}d old)` };
1556
+ }
1557
+
1558
+ // ---------------------------------------------------------------------------
1559
+ // ROUTER — `maestro router why <item>` / `maestro router validate`
1560
+ // ---------------------------------------------------------------------------
1561
+
1562
+ const ROUTER_HELP = `
1563
+ ${C.bold}maestro router${C.reset} — inspect the v2 model router.
1564
+
1565
+ Usage:
1566
+ maestro router why <request-json|task_class> Resolve a RouteRequest and print
1567
+ the decision: chain, chosen,
1568
+ explain, tried[], audit.
1569
+ maestro router validate Strict-validate config/model-routing.yaml
1570
+ against the catalog (+ overlay).
1571
+
1572
+ Examples:
1573
+ maestro router why '{"task_class":"classify.inbox","data_class":"public"}'
1574
+ maestro router why session.responder
1575
+ maestro router validate
1576
+ `;
1577
+
1578
+ /**
1579
+ * Load the v2 routing config + bundled catalog for the current agent repo.
1580
+ * Returns { config, catalog } or null with an explanation logged.
1581
+ */
1582
+ async function loadRouterContext(cwd) {
1583
+ const routerLib = join(MAESTRO_ROOT, "lib", "model-router.mjs");
1584
+ const catalogLib = join(MAESTRO_ROOT, "lib", "model-router", "catalog.mjs");
1585
+ if (!existsSync(routerLib) || !existsSync(catalogLib)) {
1586
+ fail("model-router library not reachable — run `npm run upgrade` in this agent");
1587
+ return null;
1588
+ }
1589
+ const { loadRoutingConfig } = await import(`file://${routerLib}`);
1590
+ const { loadCatalog } = await import(`file://${catalogLib}`);
1591
+ let config = null;
1592
+ try {
1593
+ config = loadRoutingConfig(cwd);
1594
+ } catch (err) {
1595
+ fail(`config/model-routing.yaml failed to load: ${err.message}`);
1596
+ return null;
1597
+ }
1598
+ const catalog = loadCatalog(cwd, {});
1599
+ return { config, catalog, routerLib };
1600
+ }
1601
+
1602
+ async function routerCmd(args = []) {
1603
+ const sub = args[0];
1604
+ if (!sub || sub === "--help" || sub === "-h") {
1605
+ console.log(ROUTER_HELP);
1606
+ return;
1607
+ }
1608
+ const cwd = process.cwd();
1609
+ const ctx = await loadRouterContext(cwd);
1610
+ if (!ctx) {
1611
+ process.exitCode = 1;
1612
+ return;
1613
+ }
1614
+ const { config, catalog, routerLib } = ctx;
1615
+
1616
+ if (sub === "validate") {
1617
+ const { validateRoutingConfig } = await import(`file://${routerLib}`);
1618
+ if (!config) {
1619
+ warn("No config/model-routing.yaml present — router is inert (Anthropic-only).");
1620
+ return;
1621
+ }
1622
+ if (config.schema_version !== 2) {
1623
+ ok("config/model-routing.yaml is a v1 config (legacy resolver path; not v2-validated).");
1624
+ return;
1625
+ }
1626
+ const { errors, warnings } = validateRoutingConfig(config, { catalog, env: process.env });
1627
+ for (const w of warnings) warn(`${w.where}: ${w.warning}`);
1628
+ for (const e of errors) fail(`${e.where}: ${e.error}`);
1629
+ if (errors.length === 0) ok(`config/model-routing.yaml valid (${warnings.length} warning(s)).`);
1630
+ process.exitCode = errors.length ? 1 : 0;
1631
+ return;
1632
+ }
1633
+
1634
+ if (sub === "why") {
1635
+ const { resolveChain } = await import(`file://${routerLib}`);
1636
+ const raw = args.slice(1).join(" ").trim();
1637
+ if (!raw) {
1638
+ fail('usage: maestro router why <request-json|task_class>');
1639
+ process.exitCode = 1;
1640
+ return;
1641
+ }
1642
+ let request;
1643
+ if (raw.startsWith("{")) {
1644
+ try {
1645
+ request = JSON.parse(raw);
1646
+ } catch (err) {
1647
+ fail(`could not parse request JSON: ${err.message}`);
1648
+ process.exitCode = 1;
1649
+ return;
1650
+ }
1651
+ } else {
1652
+ // Bare token → treat as a task_class.
1653
+ request = { task_class: raw };
1654
+ }
1655
+ const decision = resolveChain(request, { agentRoot: cwd, config, catalog, env: process.env });
1656
+ printRouteDecision(decision);
1657
+ return;
1658
+ }
1659
+
1660
+ fail(`Unknown router subcommand: ${sub}`);
1661
+ console.log(ROUTER_HELP);
1662
+ process.exitCode = 1;
1663
+ }
1664
+
1665
+ /** Pretty-print a RouteDecision for `maestro router why`. */
1666
+ function printRouteDecision(d) {
1667
+ if (!d) {
1668
+ fail("resolveChain returned nothing");
1669
+ return;
1670
+ }
1671
+ console.log();
1672
+ console.log(`${C.bold}decision${C.reset} ${d.decision_id}`);
1673
+ if (d.chosen) {
1674
+ console.log(`${C.bold}chosen${C.reset} ${C.green}${d.chosen.provider}/${d.chosen.model}${C.reset} harness=${d.chosen.harness} transport=${d.chosen.transport} wire=${d.chosen.wire}`);
1675
+ } else {
1676
+ console.log(`${C.bold}chosen${C.reset} ${C.red}<no route>${C.reset}`);
1677
+ }
1678
+ console.log(`${C.bold}explain${C.reset} ${d.explain}`);
1679
+ console.log(`${C.bold}lane${C.reset} ${d.lane} cache_ttl=${d.cache_ttl} estCostUSD=${d.estCostUSD}`);
1680
+ console.log(`${C.bold}chain${C.reset} ${(d.chain || []).map((c) => `${c.ref}(${c.harness}/${c.transport})`).join(" → ") || "(empty)"}`);
1681
+ if (d.tried && d.tried.length) {
1682
+ console.log(`${C.bold}tried${C.reset}`);
1683
+ for (const t of d.tried) console.log(` ${C.yellow}✗${C.reset} ${t.ref} — ${t.reason}`);
1684
+ }
1685
+ if (Object.keys(d.envForSpawn || {}).length) {
1686
+ console.log(`${C.bold}envForSpawn${C.reset}`);
1687
+ for (const [k, v] of Object.entries(d.envForSpawn)) {
1688
+ const shown = /TOKEN|KEY/.test(k) && v ? "***" : v;
1689
+ console.log(` ${k}=${shown}`);
1690
+ }
1691
+ }
1692
+ console.log(`${C.bold}spawnArgs${C.reset} model=${d.spawnArgs.modelFlag} maxTurns=${d.spawnArgs.maxTurns} bare=${d.spawnArgs.bare}`);
1693
+ console.log(`${C.bold}audit${C.reset} rule=${d.audit.rule} fallback=${d.audit.fallback_reason || "—"} config=${d.audit.config_source || "—"} overlay=${d.audit.overlay_version || "—"}`);
1694
+ console.log();
1695
+ }
1696
+
1697
+ // ---------------------------------------------------------------------------
1698
+ // DOCTOR — verify installation
1699
+ // ---------------------------------------------------------------------------
1700
+
1701
+ async function doctor() {
1702
+ const cwd = process.cwd();
1703
+ console.log();
1704
+ log("Running Maestro diagnostics...");
1705
+ console.log();
1706
+
1707
+ let issues = 0;
1708
+
1709
+ // ── Framework files ─────────────────────────────────────────────────────
1710
+ // config/agent.json is the single source of truth; config/agent.ts is the
1711
+ // derived thin wrapper (checked separately below as a secondary signal).
1712
+ const essentialFiles = [
1713
+ "config/agent.json", "CLAUDE.md", ".claude/settings.json",
1714
+ ".claude/commands/init-maestro.md", "package.json",
1715
+ "scripts/setup/init-agent.sh", "scripts/healthcheck.sh",
1716
+ "config/environment.yaml", "config/contacts.yaml",
1717
+ "config/priorities.yaml", "config/sla-defaults.yaml",
1718
+ "scripts/daemon/maestro-daemon.mjs",
1719
+ "scripts/local-triggers/generate-plists.sh",
1720
+ "state/dashboards/executive-summary.yaml",
1721
+ "state/queues/action-stack.yaml",
1722
+ "knowledge/decisions/decision-schema.yaml",
1723
+ // Cadence-bus architecture (maestro 1.8+)
1724
+ "lib/cadence-bus.mjs",
1725
+ "scripts/cadence/enqueue-cadence-tick.mjs",
1726
+ "scripts/cadence/launchd-cadence-wrapper.sh",
1727
+ "scripts/cadence/cadence-status.mjs",
1728
+ "scripts/daemon/cadence-consumer.mjs",
1729
+ "scripts/daemon/cadence-handlers.mjs",
1730
+ ];
1731
+
1732
+ for (const file of essentialFiles) {
1733
+ if (existsSync(join(cwd, file))) ok(file);
1734
+ else { fail(`Missing: ${file}`); issues++; }
1735
+ }
1736
+
1737
+ // ── config/agent.ts is the derived SoT wrapper (secondary signal) ───────
1738
+ // A standalone literal .ts (legacy split-brain) is a WARN, fixed by `upgrade`.
1739
+ {
1740
+ const tsPath = join(cwd, "config/agent.ts");
1741
+ if (existsSync(tsPath) && existsSync(join(cwd, "config/agent.json"))) {
1742
+ const src = readFileSync(tsPath, "utf-8");
1743
+ if (/import\s+\w+\s+from\s+['"]\.\/agent\.json['"]/.test(src)) ok("config/agent.ts is the SoT wrapper (re-exports agent.json)");
1744
+ else { warn("config/agent.ts is a standalone literal (legacy split-brain) — run: npx @cohortapp/agent-sdk upgrade"); issues++; }
1745
+ }
1746
+ }
1747
+
1748
+ // ── Archetype profile (function × altitude) ─────────────────────────────
1749
+ // Resolve the agent's operating model from the framework archetype library.
1750
+ // A freshly-created framework checkout has no config/agent.json; skip there.
1751
+ try {
1752
+ const agentJsonPath = join(cwd, "config/agent.json");
1753
+ if (existsSync(agentJsonPath)) {
1754
+ const cfg = JSON.parse(readFileSync(agentJsonPath, "utf-8"));
1755
+ const sel = cfg.function && cfg.altitude
1756
+ ? { function: cfg.function, altitude: cfg.altitude }
1757
+ : migrateLegacyArchetype(cfg.archetype);
1758
+ if (!sel) {
1759
+ warn("config/agent.json has no { function, altitude } archetype — run maestro setup");
1760
+ issues++;
1761
+ } else {
1762
+ const profile = await resolveArchetype(sel);
1763
+ ok(`Archetype resolves: ${profile.label} (${profile.function} × ${profile.altitude})`);
1764
+ }
1765
+ }
1766
+ } catch (err) {
1767
+ fail(`Archetype profile invalid: ${err && err.message ? err.message : err}`);
1768
+ issues++;
1769
+ }
1770
+
1771
+ // ── Operating model: completeness gate (shared with `maestro setup`) ─────
1772
+ // The single source of truth for "fully set up" lives in
1773
+ // lib/setup/completeness.mjs so setup and doctor never drift. A freshly-created
1774
+ // framework checkout (no config/agent.json) is tolerated — skip the gate there.
1775
+ if (existsSync(join(cwd, "config/agent.json"))) {
1776
+ try {
1777
+ const { assertComplete } = await import(join(MAESTRO_ROOT, "lib", "setup", "completeness.mjs"));
1778
+ const res = await assertComplete(cwd);
1779
+ if (res.ok) {
1780
+ ok("Operating model complete (charter, priorities, backlog≥5, roster, cadences, policies — placeholder-free)");
1781
+ } else {
1782
+ // Identity gaps are reported by the archetype check above; surface the
1783
+ // operating-model artifacts here with their remedy.
1784
+ for (const m of res.missing) {
1785
+ if (m.artifact.startsWith("identity") || m.artifact === "config/agent.json") continue;
1786
+ warn(`Operating model: ${m.artifact} — ${m.reason}`);
1787
+ issues++;
1788
+ }
1789
+ warn(" Finish setup: maestro setup (resumes from the checkpoint)");
1790
+ }
1791
+ } catch (err) {
1792
+ warn(`Operating-model gate skipped: ${err && err.message ? err.message : err}`);
1793
+ }
1794
+ }
1795
+
1796
+ // ── Cadence bus state directories ───────────────────────────────────────
1797
+ const busDirs = [
1798
+ "state/cadence-bus",
1799
+ "state/cadence-bus/inbox",
1800
+ "state/cadence-bus/claimed",
1801
+ "state/cadence-bus/processed",
1802
+ "state/cadence-bus/failed",
1803
+ "state/cadence-bus/dlq",
1804
+ "logs/cadence-bus",
1805
+ ];
1806
+ for (const d of busDirs) {
1807
+ if (existsSync(join(cwd, d))) ok(d);
1808
+ else { warn(`Missing: ${d} — run: npx @cohortapp/agent-sdk upgrade`); issues++; }
1809
+ }
1810
+
1811
+ // ── Launchd plist architecture ──────────────────────────────────────────
1812
+ const plistDir = join(cwd, "scripts/local-triggers/plists");
1813
+ if (existsSync(plistDir)) {
1814
+ const plists = readdirSync(plistDir).filter((n) => n.endsWith(".plist"));
1815
+ let legacy = 0;
1816
+ let modern = 0;
1817
+ for (const name of plists) {
1818
+ const body = readFileSync(join(plistDir, name), "utf-8");
1819
+ if (body.includes(LEGACY_PLIST_INDICATOR)) legacy++;
1820
+ else if (body.includes(PLIST_ARCH_MARKER)) modern++;
1821
+ }
1822
+ if (plists.length === 0) {
1823
+ warn("scripts/local-triggers/plists/ is empty — run: scripts/local-triggers/generate-plists.sh");
1824
+ issues++;
1825
+ } else if (legacy > 0) {
1826
+ // Legacy plists are a migration warning rather than a fatal — the
1827
+ // operator can fix them with `maestro upgrade`. Use warn() so the
1828
+ // message lands on stdout where scripted callers will see it.
1829
+ warn(`${legacy} plist(s) still call run-trigger.sh directly (legacy spawn-per-tick).`);
1830
+ warn(` Fix: npx @cohortapp/agent-sdk upgrade (will back up + regenerate)`);
1831
+ issues++;
1832
+ } else {
1833
+ ok(`${modern}/${plists.length} plist(s) use the cadence-bus architecture`);
1834
+ }
1835
+
1836
+ // Spot-check installed plists at ~/Library/LaunchAgents/.
1837
+ const installedDir = join(process.env.HOME || "/", "Library/LaunchAgents");
1838
+ if (existsSync(installedDir)) {
1839
+ let installedLegacy = 0;
1840
+ for (const name of readdirSync(installedDir)) {
1841
+ if (!name.startsWith("ai.maestro.") && !name.startsWith("ai.adaptic.")) continue;
1842
+ if (!name.endsWith(".plist")) continue;
1843
+ const body = readFileSync(join(installedDir, name), "utf-8");
1844
+ if (body.includes(LEGACY_PLIST_INDICATOR)) installedLegacy++;
1845
+ }
1846
+ if (installedLegacy > 0) {
1847
+ warn(`${installedLegacy} installed launchd plist(s) still use the legacy pattern.`);
1848
+ warn(" Fix: npx @cohortapp/agent-sdk upgrade then follow the printed launchctl commands.");
1849
+ issues++;
1850
+ } else {
1851
+ ok("Installed launchd plists (ai.maestro.*/legacy ai.adaptic.*) are on cadence-bus or absent");
1852
+ }
1853
+ }
1854
+ } else {
1855
+ warn("scripts/local-triggers/plists/ does not exist yet — run scripts/setup/init-agent.sh");
1856
+ }
1857
+
1858
+ // ── Cadence consumer heartbeat ──────────────────────────────────────────
1859
+ try {
1860
+ // Read via the bundled lib so we don't reinvent path resolution.
1861
+ const mod = readFileSync(join(cwd, "state/cadence-bus/health.json"), "utf-8");
1862
+ const health = JSON.parse(mod);
1863
+ const ageMs = Date.now() - new Date(health.ts).getTime();
1864
+ if (ageMs < 60_000) ok(`Cadence consumer heartbeat fresh (${Math.round(ageMs / 1000)}s)`);
1865
+ else if (ageMs < 5 * 60_000) warn(`Cadence consumer heartbeat stale (${Math.round(ageMs / 1000)}s) — daemon may be slow.`);
1866
+ else {
1867
+ warn(`Cadence consumer heartbeat very stale (${Math.round(ageMs / 1000)}s) — start the daemon: npm run daemon`);
1868
+ issues++;
1869
+ }
1870
+ } catch {
1871
+ warn("No cadence consumer heartbeat yet — start the daemon: npm run daemon");
1872
+ }
1873
+
1874
+ // ── Manual tick smoke test ──────────────────────────────────────────────
1875
+ // Enqueue a heartbeat tick if cadence files are in place. The bus
1876
+ // accepts the event even when the consumer isn't running; the file lands
1877
+ // on disk and is recoverable. This proves the producer side end-to-end.
1878
+ const enqueueScript = join(cwd, "scripts/cadence/enqueue-cadence-tick.mjs");
1879
+ if (existsSync(enqueueScript)) {
1880
+ const result = spawnSync(process.execPath, [
1881
+ enqueueScript, "cadence-bus-heartbeat",
1882
+ "--source=daemon", "--metadata=note=doctor-smoke", "--quiet",
1883
+ ], { cwd, encoding: "utf-8", env: { ...process.env, AGENT_ROOT: cwd } });
1884
+ if (result.status === 0) ok("Cadence enqueue smoke test passed");
1885
+ else {
1886
+ fail(`Cadence enqueue smoke test failed (exit ${result.status}): ${(result.stderr || "").trim()}`);
1887
+ issues++;
1888
+ }
1889
+ }
1890
+
1891
+ // ── WS4: recovery + resource governance ─────────────────────────────────
1892
+ // The kill switch is HUMAN-ONLY now; the watchdog self-heals (soft→hard→
1893
+ // restart→graceful-reboot) and never writes .emergency-stop on OOM. Surface
1894
+ // the live governance posture so an operator can see throttle/breaker/budget
1895
+ // state and the reboot wiring at a glance.
1896
+ {
1897
+ // .emergency-stop present → the agent is intentionally stopped by a human.
1898
+ if (existsSync(join(cwd, ".emergency-stop"))) {
1899
+ let reason = "";
1900
+ try { reason = readFileSync(join(cwd, ".emergency-stop"), "utf-8").trim().split("\n")[0]; } catch { /* */ }
1901
+ fail(`.emergency-stop present${reason ? ` ("${reason}")` : ""} — human-only kill switch is set; agent will not run. Clear with: scripts/resume-operations.sh`);
1902
+ issues++;
1903
+ } else {
1904
+ ok(".emergency-stop absent (agent not human-halted)");
1905
+ }
1906
+
1907
+ // Watchdog heartbeat freshness (<90s).
1908
+ try {
1909
+ const hbRaw = readFileSync(join(cwd, "state/heartbeat"), "utf-8").trim();
1910
+ const hbAgeMs = Date.now() - new Date(hbRaw).getTime();
1911
+ if (Number.isFinite(hbAgeMs) && hbAgeMs < 90_000) ok(`Memory watchdog heartbeat fresh (${Math.round(hbAgeMs / 1000)}s)`);
1912
+ else { warn(`Memory watchdog heartbeat stale (${Math.round(hbAgeMs / 1000)}s) — check ai.maestro.*-memory-watchdog`); issues++; }
1913
+ } catch {
1914
+ warn("No memory watchdog heartbeat (state/heartbeat) — is the watchdog plist loaded?");
1915
+ }
1916
+
1917
+ // Soft-throttle ceiling (state/throttle.json) — WARN while unexpired.
1918
+ try {
1919
+ const { readThrottle, dynamicMax, defaultDeps } = await import(join(MAESTRO_ROOT, "lib", "resource-governor.mjs"));
1920
+ const thr = readThrottle({ agentRoot: cwd });
1921
+ if (thr.ceiling !== Infinity) {
1922
+ warn(`Resource governor throttled: ceiling=${thr.ceiling}${thr.reason ? ` (${thr.reason})` : ""} — watchdog SOFT/HARD tier active; auto-clears on TTL`);
1923
+ } else {
1924
+ ok("No active soft-throttle (state/throttle.json clear/expired)");
1925
+ }
1926
+ // Computed dynamicMax vs RAM (sizing sanity).
1927
+ try {
1928
+ const deps = defaultDeps({ agentRoot: cwd });
1929
+ const dMax = dynamicMax(deps);
1930
+ ok(`Governor dynamicMax=${dMax} (RAM ${Math.round(deps.totalmem / (1024 * 1024 * 1024))}GB, ${deps.cpus} cpus)`);
1931
+ } catch { /* os read best-effort */ }
1932
+ } catch (err) {
1933
+ warn(`Resource governor check skipped: ${err && err.message ? err.message : err}`);
1934
+ }
1935
+
1936
+ // Shared 429 breaker — WARN if open.
1937
+ try {
1938
+ const { checkRateLimit } = await import(join(MAESTRO_ROOT, "lib", "rate-guard.mjs"));
1939
+ const rb = checkRateLimit(process.env.MAESTRO_RATE_PROVIDER || "anthropic", { agentRoot: cwd });
1940
+ if (!rb.allowed) {
1941
+ const mins = Math.max(0, Math.round((rb.retryAt - Date.now()) / 60_000));
1942
+ warn(`429 rate breaker OPEN — spawns gated for ~${mins}m (state/rate-limits/)`); issues++;
1943
+ } else {
1944
+ ok("429 rate breaker closed (spawns allowed)");
1945
+ }
1946
+ } catch { /* breaker read best-effort */ }
1947
+
1948
+ // Today's budget band.
1949
+ try {
1950
+ const { dailyStatus } = await import(join(MAESTRO_ROOT, "lib", "budget-guard.mjs"));
1951
+ const b = dailyStatus({ agentRoot: cwd });
1952
+ if (b.essentialOnly) { warn(`Daily budget cap reached ($${b.spentUSD}/$${b.capUSD}) — essential-only (inbox replies continue; backlog+cadence deferred)`); issues++; }
1953
+ else if (b.band >= 75) { warn(`Daily budget at ${b.band}% ($${b.spentUSD}/$${b.capUSD})`); }
1954
+ else ok(`Daily budget band ${b.band}% ($${b.spentUSD}/$${b.capUSD})`);
1955
+ } catch { /* ledger read best-effort */ }
1956
+
1957
+ // Cost-telemetry tripwire (observability F2). Read today's cost ledger and
1958
+ // cross-check against evidence that sessions actually ran (ledger rows OR a
1959
+ // fresh cadence-consumer heartbeat). If sessions ran but summed
1960
+ // estimated_usd is $0, spend telemetry is blind and the budget governor
1961
+ // can never trip — RED. No sessions, or spend>0 → GREEN.
1962
+ try {
1963
+ const ledgerDate = new Date().toISOString().slice(0, 10);
1964
+ const ledgerPath = join(cwd, "state/cost-tracking", `${ledgerDate}.jsonl`);
1965
+ const rows = [];
1966
+ if (existsSync(ledgerPath)) {
1967
+ const body = readFileSync(ledgerPath, "utf-8");
1968
+ for (const line of body.split("\n")) {
1969
+ if (!line.trim()) continue;
1970
+ try { rows.push(JSON.parse(line)); } catch { /* skip malformed row */ }
1971
+ }
1972
+ }
1973
+ // Heartbeat as out-of-ledger session evidence: a fresh consumer heartbeat
1974
+ // (<5m) means the daemon is live and spawning, so $0 spend is suspicious
1975
+ // even before the first ledger row lands.
1976
+ let heartbeatActive = 0;
1977
+ try {
1978
+ const health = JSON.parse(readFileSync(join(cwd, "state/cadence-bus/health.json"), "utf-8"));
1979
+ const ageMs = Date.now() - new Date(health.ts).getTime();
1980
+ if (Number.isFinite(ageMs) && ageMs >= 0 && ageMs < 5 * 60_000) heartbeatActive = 1;
1981
+ } catch { /* no heartbeat — rely on ledger rows alone */ }
1982
+
1983
+ const tripwire = evaluateCostTripwire({ rows, sessionCount: heartbeatActive });
1984
+ if (tripwire.red) {
1985
+ fail(`Cost telemetry BLIND — ${tripwire.reason}`);
1986
+ fail(` Fix: callers must pass real --input-tokens/--output-tokens to scripts/cost/track-claude-usage.mjs (lift usage from the claude --output-format json envelope).`);
1987
+ issues++;
1988
+ } else if (tripwire.rowCount === 0 && heartbeatActive === 0) {
1989
+ ok("Cost telemetry: no sessions recorded today (nothing to measure)");
1990
+ } else {
1991
+ ok(`Cost telemetry live ($${tripwire.totalUsd} across ${tripwire.rowCount} ledger row(s) today)`);
1992
+ }
1993
+ } catch { /* cost ledger read best-effort */ }
1994
+
1995
+ // Resume-pending markers stuck at the strike cap.
1996
+ try {
1997
+ const rpDir = join(cwd, "state/sessions/resume-pending");
1998
+ if (existsSync(rpDir)) {
1999
+ let stuck = 0, total = 0;
2000
+ for (const f of readdirSync(rpDir)) {
2001
+ if (!f.endsWith(".json")) continue;
2002
+ total++;
2003
+ try {
2004
+ const m = JSON.parse(readFileSync(join(rpDir, f), "utf-8"));
2005
+ if ((m.recoveryAttempts || 0) >= 3) stuck++;
2006
+ } catch { /* skip */ }
2007
+ }
2008
+ if (stuck > 0) { warn(`${stuck}/${total} resume-pending marker(s) hit the 3-strike cap — items were marked blocked; review state/queues/`); issues++; }
2009
+ else if (total > 0) ok(`${total} in-flight resume marker(s), none stuck`);
2010
+ else ok("No in-flight resume markers");
2011
+ }
2012
+ } catch { /* */ }
2013
+
2014
+ // pmset autorestart — the box must power back on after an outage.
2015
+ try {
2016
+ const ps = spawnSync("pmset", ["-g"], { encoding: "utf-8" });
2017
+ if (ps.status === 0) {
2018
+ const m = (ps.stdout || "").match(/\bautorestart\s+(\d)/);
2019
+ if (m && m[1] === "1") ok("pmset autorestart=1 (box reboots after power loss)");
2020
+ else { warn("pmset autorestart is not 1 — set: sudo pmset -a autorestart 1 (so the agent recovers from power loss)"); issues++; }
2021
+ }
2022
+ } catch { /* pmset unavailable (non-mac / sandbox) — skip */ }
2023
+ }
2024
+
2025
+ // ── .env ────────────────────────────────────────────────────────────────
2026
+ if (existsSync(join(cwd, ".env"))) {
2027
+ // .env permission hygiene (setup-onboarding W4): every credential — Slack,
2028
+ // Gmail, Twilio, Anthropic — lives here in plaintext. It must be chmod 600
2029
+ // (owner-only); a group/world-readable .env on a shared or stolen Mac mini
2030
+ // leaks the principal's full credential set. The framework never chmods it
2031
+ // for the operator — surface the exact fix instead. POSIX-only (mode bits).
2032
+ if (typeof process.getuid === "function") {
2033
+ try {
2034
+ const mode = statSync(join(cwd, ".env")).mode & 0o777;
2035
+ if ((mode & 0o077) !== 0) {
2036
+ fail(`.env is mode ${mode.toString(8).padStart(3, "0")} — group/world can read plaintext secrets.`);
2037
+ fail(` Fix: chmod 600 .env`);
2038
+ issues++;
2039
+ } else {
2040
+ ok(".env permissions are owner-only (chmod 600)");
2041
+ }
2042
+ } catch { /* stat failed — skip */ }
2043
+ }
2044
+ const env = readFileSync(join(cwd, ".env"), "utf-8");
2045
+ const check = (key, required) => {
2046
+ const re = new RegExp(`^${key}=.+`, "m");
2047
+ if (re.test(env)) ok(`${key} configured`);
2048
+ else if (required) { warn(`${key} not set`); issues++; }
2049
+ else warn(`${key} not set (optional)`);
2050
+ };
2051
+ check("ANTHROPIC_API_KEY", true);
2052
+ check("SLACK_USER_TOKEN", false);
2053
+ check("GMAIL_APP_PASSWORD", false);
2054
+
2055
+ // Auth validity: if ANTHROPIC_API_KEY is set, ping the API to
2056
+ // verify it works. An invalid key in .env will silently be sent
2057
+ // to every `claude --print` sub-session and cause cascading 401s
2058
+ // (exactly the ravi-ai inbox-processor runaway). Better to catch
2059
+ // it here. Skips the check if the user opted out via
2060
+ // MAESTRO_PREFER_SUBSCRIPTION_AUTH=1 (subscription wins).
2061
+ const keyMatch = env.match(/^ANTHROPIC_API_KEY=(.+)$/m);
2062
+ const preferSubsMatch = env.match(/^MAESTRO_PREFER_SUBSCRIPTION_AUTH=(.+)$/m);
2063
+ const preferSubs = preferSubsMatch && /^1|true|yes$/i.test(preferSubsMatch[1].trim());
2064
+ if (keyMatch && !preferSubs) {
2065
+ const key = keyMatch[1].trim().replace(/^"|"$/g, "");
2066
+ try {
2067
+ const result = spawnSync("curl", [
2068
+ "-s", "-o", "/dev/null", "-w", "%{http_code}",
2069
+ "-X", "POST",
2070
+ "-H", `x-api-key: ${key}`,
2071
+ "-H", "anthropic-version: 2023-06-01",
2072
+ "-H", "content-type: application/json",
2073
+ "--max-time", "8",
2074
+ "https://api.anthropic.com/v1/messages",
2075
+ "-d", JSON.stringify({ model: "claude-haiku-4-5", max_tokens: 5, messages: [{ role: "user", content: "ping" }] }),
2076
+ ], { encoding: "utf-8" });
2077
+ const code = (result.stdout || "").trim();
2078
+ if (code === "200") ok("ANTHROPIC_API_KEY validated against api.anthropic.com");
2079
+ else if (code === "401") {
2080
+ warn(`ANTHROPIC_API_KEY is INVALID (HTTP 401 from api.anthropic.com).`);
2081
+ warn(` This will cause every sub-session spawn to fail. Either:`);
2082
+ warn(` 1. Replace the key in .env with a valid one, OR`);
2083
+ warn(` 2. Set MAESTRO_PREFER_SUBSCRIPTION_AUTH=1 in .env to use Claude Code subscription auth.`);
2084
+ issues++;
2085
+ } else if (code) warn(`ANTHROPIC_API_KEY check returned HTTP ${code} (expected 200)`);
2086
+ else warn(`ANTHROPIC_API_KEY check skipped (no network / curl missing)`);
2087
+ } catch { warn("ANTHROPIC_API_KEY check failed (curl error)"); }
2088
+ } else if (preferSubs) {
2089
+ ok("MAESTRO_PREFER_SUBSCRIPTION_AUTH=1 — using Claude Code subscription (Keychain OAuth)");
2090
+ }
2091
+
2092
+ // ── Slack Socket Mode ────────────────────────────────────────────────
2093
+ // When SLACK_APP_LEVEL_TOKEN is set, verify the launchd job is loaded
2094
+ // and the listener has connected recently. The check is fully optional:
2095
+ // the framework still works with the legacy 60s poller if the operator
2096
+ // hasn't enabled Socket Mode yet.
2097
+ const socketTokenMatch = env.match(/^SLACK_APP_LEVEL_TOKEN=(.+)$/m);
2098
+ const socketToken = socketTokenMatch ? socketTokenMatch[1].trim().replace(/^["']|["']$/g, "") : "";
2099
+ if (socketToken && socketToken.startsWith("xapp-")) {
2100
+ ok("SLACK_APP_LEVEL_TOKEN configured (Socket Mode candidate)");
2101
+
2102
+ // Resolve the agent's first name the same way generate-plists.sh does
2103
+ // so the label matches whatever is installed in ~/Library/LaunchAgents/.
2104
+ let firstName = "";
2105
+ try {
2106
+ const aj = JSON.parse(readFileSync(join(cwd, "config/agent.json"), "utf-8"));
2107
+ if (aj.firstName) firstName = String(aj.firstName).toLowerCase();
2108
+ } catch { /* fall back */ }
2109
+ if (!firstName) firstName = cwd.split("/").pop().replace(/-ai$/, "").toLowerCase();
2110
+ // Is the plist installed at ~/Library/LaunchAgents/? Prefer the current
2111
+ // ai.maestro.* namespace; fall back to legacy ai.adaptic.* for agents
2112
+ // installed before the rename.
2113
+ const installedDir = join(process.env.HOME || "/", "Library/LaunchAgents");
2114
+ const socketCandidates = [`ai.maestro.${firstName}-slack-socket`, `ai.adaptic.${firstName}-slack-socket`];
2115
+ const socketLabel = socketCandidates.find((l) => existsSync(join(installedDir, `${l}.plist`))) || socketCandidates[0];
2116
+ const installedPath = join(installedDir, `${socketLabel}.plist`);
2117
+ if (existsSync(installedPath)) {
2118
+ // Is it actually loaded? launchctl list returns 0 when the label exists.
2119
+ const listRes = spawnSync("launchctl", ["list", socketLabel], { encoding: "utf-8" });
2120
+ if (listRes.status === 0) ok(`launchd job loaded: ${socketLabel}`);
2121
+ else {
2122
+ warn(`Plist installed but launchd job not loaded: ${socketLabel}`);
2123
+ warn(` Fix: launchctl load ${installedPath}`);
2124
+ issues++;
2125
+ }
2126
+ } else {
2127
+ warn(`SLACK_APP_LEVEL_TOKEN set but ${installedPath} missing.`);
2128
+ warn(" Run: node scripts/setup/init-slack-socket-mode.mjs");
2129
+ issues++;
2130
+ }
2131
+
2132
+ // Did the listener log a `hello` envelope recently? The log lives at
2133
+ // logs/polling/<today>-slack-socket.jsonl — scan the last few lines
2134
+ // for a fresh "hello" or "inbox-item-written" entry.
2135
+ const today = new Date().toISOString().slice(0, 10);
2136
+ const yesterday = new Date(Date.now() - 86400000).toISOString().slice(0, 10);
2137
+ let foundRecent = false;
2138
+ let latestTs = null;
2139
+ for (const day of [today, yesterday]) {
2140
+ const logPath = join(cwd, "logs/polling", `${day}-slack-socket.jsonl`);
2141
+ if (!existsSync(logPath)) continue;
2142
+ // Read the tail (last 50KB) to avoid loading huge files.
2143
+ try {
2144
+ const stat = statSync(logPath);
2145
+ const buf = Buffer.alloc(Math.min(stat.size, 50_000));
2146
+ const fd = openSync(logPath, "r");
2147
+ try {
2148
+ const start = Math.max(0, stat.size - buf.length);
2149
+ readSync(fd, buf, 0, buf.length, start);
2150
+ } finally { closeSync(fd); }
2151
+ const tail = buf.toString("utf-8");
2152
+ for (const line of tail.split("\n").reverse()) {
2153
+ if (!line) continue;
2154
+ try {
2155
+ const entry = JSON.parse(line);
2156
+ if (entry.message === "hello — connected to Slack Socket Mode" ||
2157
+ entry.message === "inbox-item-written" ||
2158
+ entry.message === "starting Socket Mode listener") {
2159
+ latestTs = entry.ts;
2160
+ const ageMs = Date.now() - new Date(entry.ts).getTime();
2161
+ if (ageMs >= 0 && ageMs < 60 * 60_000) foundRecent = true;
2162
+ break;
2163
+ }
2164
+ } catch { /* malformed line — skip */ }
2165
+ }
2166
+ if (latestTs) break;
2167
+ } catch { /* unreadable log — skip */ }
2168
+ }
2169
+ if (foundRecent) {
2170
+ const ageMin = Math.round((Date.now() - new Date(latestTs).getTime()) / 60_000);
2171
+ ok(`Socket Mode listener was active ${ageMin}m ago`);
2172
+ } else if (latestTs) {
2173
+ const ageMin = Math.round((Date.now() - new Date(latestTs).getTime()) / 60_000);
2174
+ warn(`Socket Mode last connect ${ageMin}m ago — may be stalled`);
2175
+ issues++;
2176
+ } else {
2177
+ warn("Socket Mode listener has not produced a connect log yet.");
2178
+ warn(" Check: tail -f logs/polling/$(date +%Y-%m-%d)-slack-socket.jsonl");
2179
+ issues++;
2180
+ }
2181
+ }
2182
+ } else {
2183
+ fail(".env file not found — copy from .env.example");
2184
+ issues++;
2185
+ }
2186
+
2187
+ // ── Dependencies ────────────────────────────────────────────────────────
2188
+ if (existsSync(join(cwd, "node_modules"))) ok("node_modules installed");
2189
+ else { fail("node_modules not found — run: npm install"); issues++; }
2190
+
2191
+ try {
2192
+ execFileSync("which", ["claude"], { stdio: "pipe" });
2193
+ ok("Claude CLI installed");
2194
+ } catch {
2195
+ fail("Claude CLI not found — install: npm install -g @anthropic-ai/claude-code");
2196
+ issues++;
2197
+ }
2198
+
2199
+ try {
2200
+ execFileSync("which", ["jq"], { stdio: "pipe" });
2201
+ ok("jq installed");
2202
+ } catch {
2203
+ warn("jq not found — install: brew install jq (used by various shell helpers)");
2204
+ }
2205
+
2206
+ // ── Emergency-stop wiring ───────────────────────────────────────────────
2207
+ const stopScript = join(cwd, "scripts/emergency-stop.sh");
2208
+ if (existsSync(stopScript)) {
2209
+ const body = readFileSync(stopScript, "utf-8");
2210
+ if (body.includes("$AGENT_DIR/schedules") || body.includes("$SOPHIE_AI_DIR/schedules")) {
2211
+ warn("scripts/emergency-stop.sh still looks in $REPO/schedules/ (legacy path bug). Run: maestro upgrade");
2212
+ issues++;
2213
+ } else if (!body.includes("Library/LaunchAgents")) {
2214
+ warn("scripts/emergency-stop.sh doesn't reference ~/Library/LaunchAgents/ — kill-switch may be ineffective.");
2215
+ issues++;
2216
+ } else {
2217
+ ok("scripts/emergency-stop.sh present (uses ~/Library/LaunchAgents/)");
2218
+ }
2219
+ } else { warn("scripts/emergency-stop.sh missing — kill-switch unavailable"); issues++; }
2220
+
2221
+ // ── Feature-init state ──────────────────────────────────────────────────
2222
+ const featureState = join(cwd, ".maestro/features.json");
2223
+ if (existsSync(featureState)) {
2224
+ try {
2225
+ const s = JSON.parse(readFileSync(featureState, "utf-8"));
2226
+ const pending = (s.pending || []).length;
2227
+ const initialised = Object.keys(s.initialized || {}).length;
2228
+ if (pending > 0) {
2229
+ warn(`${pending} framework feature(s) pending init — run: maestro init`);
2230
+ issues++;
2231
+ } else {
2232
+ ok(`${initialised} framework feature(s) initialised`);
2233
+ }
2234
+ } catch {
2235
+ warn(".maestro/features.json is malformed — run: maestro upgrade");
2236
+ issues++;
2237
+ }
2238
+ } else {
2239
+ warn(".maestro/features.json not found — run: maestro upgrade");
2240
+ issues++;
2241
+ }
2242
+
2243
+ // ── Workflow / agent / skill cross-references ──────────────────────────
2244
+ // Scan workflows/ for `agent:` and `skill:` references and verify each
2245
+ // exists. Catches the "morning-brief references agent: foo that doesn't
2246
+ // exist" class of integration drift.
2247
+ const workflowsDir = join(cwd, "workflows");
2248
+ const agentsDir = join(cwd, "agents");
2249
+ const skillsDir = join(cwd, "plugins/maestro-skills/skills");
2250
+ if (existsSync(workflowsDir)) {
2251
+ const missingAgents = new Set();
2252
+ const missingSkills = new Set();
2253
+ const walkYaml = (dir) => {
2254
+ for (const name of readdirSync(dir)) {
2255
+ const full = join(dir, name);
2256
+ const st = statSync(full);
2257
+ if (st.isDirectory()) walkYaml(full);
2258
+ else if (st.isFile() && /\.ya?ml$/.test(name)) {
2259
+ const body = readFileSync(full, "utf-8");
2260
+ for (const m of body.matchAll(/^\s+agent:\s*([\w-]+)\b/gm)) {
2261
+ const a = m[1];
2262
+ if (a === "agent-self" || a === "agent_self") continue;
2263
+ if (!existsSync(join(agentsDir, a))) missingAgents.add(a);
2264
+ }
2265
+ for (const m of body.matchAll(/^\s+skill:\s*([\w-]+)\b/gm)) {
2266
+ const s = m[1];
2267
+ if (!existsSync(join(skillsDir, `${s}.md`))) missingSkills.add(s);
2268
+ }
2269
+ }
2270
+ }
2271
+ };
2272
+ try { walkYaml(workflowsDir); }
2273
+ catch (err) { warn(`workflow scan failed: ${err.message}`); }
2274
+ if (missingAgents.size === 0 && missingSkills.size === 0) {
2275
+ ok("All workflow agent/skill references resolved");
2276
+ } else {
2277
+ if (missingAgents.size) { warn(`Workflows reference ${missingAgents.size} missing agent(s): ${[...missingAgents].slice(0,5).join(", ")}${missingAgents.size > 5 ? "…" : ""}`); issues++; }
2278
+ if (missingSkills.size) { warn(`Workflows reference ${missingSkills.size} missing skill(s): ${[...missingSkills].slice(0,5).join(", ")}${missingSkills.size > 5 ? "…" : ""}`); issues++; }
2279
+ }
2280
+ }
2281
+
2282
+ // ── state/ + logs/ ownership & permission hygiene (audit M3) ─────────────
2283
+ // The framework runs as the unprivileged user. If state/ or logs/ were
2284
+ // created/chown'd root (e.g. an installer step run under sudo) the daemon's
2285
+ // writes fail silently; a 0777 chmod "fix" then leaves private state
2286
+ // world-writable. Verify ownership + perms here and tell the operator the
2287
+ // exact command to run. We never chown/chmod automatically.
2288
+ if (typeof process.getuid === "function") {
2289
+ const ownershipPaths = [
2290
+ "state", "logs",
2291
+ "state/queues", "state/dashboards",
2292
+ "state/cadence-bus",
2293
+ "state/cadence-bus/inbox", "state/cadence-bus/claimed",
2294
+ "state/cadence-bus/processed", "state/cadence-bus/failed",
2295
+ "state/cadence-bus/dlq",
2296
+ "logs/cadence-bus",
2297
+ ];
2298
+ const entries = ownershipPaths.map((rel) => {
2299
+ const full = join(cwd, rel);
2300
+ try {
2301
+ const st = statSync(full);
2302
+ return { path: rel, mode: st.mode, uid: st.uid, exists: true };
2303
+ } catch {
2304
+ return { path: rel, exists: false };
2305
+ }
2306
+ });
2307
+ const ownership = checkOwnershipSet(entries, { runtimeUid: process.getuid() });
2308
+ if (ownership.ok) {
2309
+ ok("state/ and logs/ owned by runtime uid, not group/world-writable");
2310
+ } else {
2311
+ for (const v of ownership.violations) {
2312
+ fail(`${v.path}: ${v.reasons.join("; ")}`);
2313
+ }
2314
+ fail(` Fix: ${ownership.fixCommand}`);
2315
+ issues += ownership.violations.length;
2316
+ }
2317
+ }
2318
+
2319
+ // ── Cadence-bus DLQ depth ───────────────────────────────────────────────
2320
+ const dlqDir = join(cwd, "state/cadence-bus/dlq");
2321
+ if (existsSync(dlqDir)) {
2322
+ const dlqFiles = readdirSync(dlqDir).filter((n) => n.endsWith(".json"));
2323
+ if (dlqFiles.length === 0) ok("Cadence-bus DLQ is empty");
2324
+ else if (dlqFiles.length < 5) warn(`Cadence-bus DLQ: ${dlqFiles.length} event(s). Review: state/cadence-bus/dlq/`);
2325
+ else {
2326
+ fail(`Cadence-bus DLQ: ${dlqFiles.length} event(s) — investigate before clearing.`);
2327
+ issues++;
2328
+ }
2329
+ }
2330
+
2331
+ // ── Backup configured + fresh (DR-config, gaps-enterprise-ops G16) ──────
2332
+ // RED when backup is unconfigured: at fleet scale a machine with no DR target
2333
+ // is a silent data-loss risk. Previously doctor exited 0 on a missing backup
2334
+ // target; now it FAILs so the gap surfaces. GREEN only when a target is
2335
+ // configured AND a recent (<25h) successful run is recorded.
2336
+ const backupCfg = join(cwd, ".maestro/backup-config.yaml");
2337
+ if (existsSync(backupCfg)) {
2338
+ const cfg = readFileSync(backupCfg, "utf-8");
2339
+ if (/enabled:\s*true/.test(cfg)) {
2340
+ const lastBackup = join(cwd, ".maestro/last-backup.json");
2341
+ if (existsSync(lastBackup)) {
2342
+ try {
2343
+ const lb = JSON.parse(readFileSync(lastBackup, "utf-8"));
2344
+ const age = Date.now() - new Date(lb.completed_at).getTime();
2345
+ if (age < 25 * 60 * 60 * 1000) ok(`Backup configured + fresh: last run ${Math.round(age / 3600000)}h ago (${lb.provider}://${lb.bucket})`);
2346
+ else { warn(`Last backup ${Math.round(age / 3600000)}h ago — investigate scripts/maintenance/backup-to-cloud.sh`); issues++; }
2347
+ } catch { warn("last-backup.json is malformed"); }
2348
+ } else {
2349
+ warn("Backup enabled but no successful run yet — schedule scripts/maintenance/backup-to-cloud.sh");
2350
+ issues++;
2351
+ }
2352
+ } else {
2353
+ fail("Backup target present but disabled (.maestro/backup-config.yaml: enabled is not true).");
2354
+ console.log(" fix: set enabled: true and schedule scripts/maintenance/backup-to-cloud.sh");
2355
+ issues++;
2356
+ }
2357
+ } else {
2358
+ fail("No backup target configured — fleet machines need a DR backup (.maestro/backup-config.yaml missing).");
2359
+ console.log(" fix: create .maestro/backup-config.yaml (enabled: true, provider, bucket) and schedule scripts/maintenance/backup-to-cloud.sh");
2360
+ issues++;
2361
+ }
2362
+
2363
+ // ── Cross-agent registry ────────────────────────────────────────────────
2364
+ const knownAgents = join(cwd, "config/known-agents.json");
2365
+ if (existsSync(knownAgents)) {
2366
+ try {
2367
+ const reg = JSON.parse(readFileSync(knownAgents, "utf-8"));
2368
+ const count = Array.isArray(reg.agents) ? reg.agents.length : 0;
2369
+ if (count > 0) ok(`Cross-agent registry: ${count} peer agent(s)`);
2370
+ else warn("config/known-agents.json present but empty — peer-agent detection won't work");
2371
+ } catch {
2372
+ warn("config/known-agents.json is malformed");
2373
+ issues++;
2374
+ }
2375
+ } else {
2376
+ warn("config/known-agents.json missing — run: maestro init known-agents-registry --apply");
2377
+ issues++;
2378
+ }
2379
+
2380
+ // ── Model router (1.12.0+) ─────────────────────────────────────────────
2381
+ // Disabled-but-present is the normal "ready to opt in" state. Active is
2382
+ // verified by loading the config and checking each backend's auth env var.
2383
+ const routerActive = join(cwd, "config/model-routing.yaml");
2384
+ const routerDisabled = join(cwd, "config/model-routing.yaml.disabled");
2385
+ if (existsSync(routerActive)) {
2386
+ try {
2387
+ // Use the framework's loader so we get the same parser as the daemon.
2388
+ const routerPath = process.env.HOME
2389
+ ? join(process.env.HOME, "maestro/lib/model-router.mjs")
2390
+ : null;
2391
+ if (routerPath && existsSync(routerPath)) {
2392
+ const { loadRoutingConfig } = await import(`file://${routerPath}`);
2393
+ const cfg = loadRoutingConfig(cwd);
2394
+ if (!cfg) {
2395
+ warn("config/model-routing.yaml present but loader returned null — check syntax");
2396
+ issues++;
2397
+ } else if (cfg.schema_version === 2) {
2398
+ // ── Policy v2 path: strict-validate + staleness ladder ──────────
2399
+ ok(`Model router active (policy v2, ${Object.keys(cfg.aliases || {}).length} alias(es))`);
2400
+ try {
2401
+ const { validateRoutingConfig } = await import(`file://${routerPath}`);
2402
+ const catalogLib = join(process.env.HOME, "maestro/lib/model-router/catalog.mjs");
2403
+ const { loadCatalog } = await import(`file://${catalogLib}`);
2404
+ const catalog = loadCatalog(cwd, {});
2405
+ const { errors, warnings } = validateRoutingConfig(cfg, { catalog, env: process.env });
2406
+ for (const w of warnings) warn(` router config: ${w.where} — ${w.warning}`);
2407
+ for (const e of errors) { fail(` router config: ${e.where} — ${e.error}`); issues++; }
2408
+ if (errors.length === 0) ok(" config strict-validation: passed");
2409
+
2410
+ // Staleness ladder: catalog cost provenance + pricing cache.
2411
+ // WARN @7d, FAIL @30d (graft #18). Volatile rows escalate the FAIL.
2412
+ const models = catalog.models || [];
2413
+ let oldest = null;
2414
+ let anyVolatile = false;
2415
+ for (const r of models) {
2416
+ const p = r.cost_provenance || {};
2417
+ if (p.volatile) anyVolatile = true;
2418
+ if (p.fetched && (oldest == null || p.fetched < oldest)) oldest = p.fetched;
2419
+ }
2420
+ const catStale = evaluateStaleness({ fetched: oldest, hasVolatile: anyVolatile, label: "catalog cost provenance" });
2421
+ if (catStale.status === "fail") { fail(` ${catStale.reason}`); issues++; }
2422
+ else if (catStale.status === "warn") { warn(` ${catStale.reason}`); issues++; }
2423
+ else ok(` ${catStale.reason}`);
2424
+
2425
+ // Pricing cache freshness (cost fields refresh out of band).
2426
+ const priceCachePath = join(cwd, "state/model-router/pricing-cache.json");
2427
+ if (existsSync(priceCachePath)) {
2428
+ let fetched = null;
2429
+ try {
2430
+ const pc = JSON.parse(readFileSync(priceCachePath, "utf-8"));
2431
+ const entries = pc.models && typeof pc.models === "object" ? pc.models : pc;
2432
+ for (const v of Object.values(entries)) {
2433
+ const f = v && v.provenance && v.provenance.fetched;
2434
+ if (f && (fetched == null || f < fetched)) fetched = f;
2435
+ }
2436
+ if (!fetched && pc.fetched) fetched = pc.fetched;
2437
+ } catch { /* */ }
2438
+ const pcStale = evaluateStaleness({ fetched, hasVolatile: anyVolatile, label: "pricing cache" });
2439
+ if (pcStale.status === "fail") { fail(` ${pcStale.reason}`); issues++; }
2440
+ else if (pcStale.status === "warn") { warn(` ${pcStale.reason}`); issues++; }
2441
+ else ok(` ${pcStale.reason}`);
2442
+ } else {
2443
+ ok(" pricing cache absent — using bundled catalog prices (provenance above governs)");
2444
+ }
2445
+ } catch (verr) {
2446
+ warn(` router v2 checks skipped: ${verr.message}`);
2447
+ }
2448
+ } else {
2449
+ const names = Object.keys(cfg.backends);
2450
+ ok(`Model router active (${names.length} backend${names.length === 1 ? "" : "s"}: ${names.join(", ")})`);
2451
+ // Verify auth env for each backend that needs one. We check .env
2452
+ // on disk (not process.env) because doctor doesn't load dotenv;
2453
+ // the daemon does that at spawn time, so a key present in .env is
2454
+ // effectively configured even if process.env doesn't show it.
2455
+ let envBody = "";
2456
+ try { envBody = readFileSync(join(cwd, ".env"), "utf-8"); } catch { /* */ }
2457
+ for (const [name, b] of Object.entries(cfg.backends)) {
2458
+ if (!b.auth_env) continue;
2459
+ const re = new RegExp(`^${b.auth_env}=.+`, "m");
2460
+ const hasInEnvFile = re.test(envBody);
2461
+ const hasInProcEnv = !!process.env[b.auth_env];
2462
+ if (!hasInEnvFile && !hasInProcEnv) {
2463
+ warn(`Model router backend "${name}" needs ${b.auth_env} — not set in .env or process env`);
2464
+ issues++;
2465
+ } else {
2466
+ ok(` ${name}: ${b.auth_env} configured`);
2467
+ }
2468
+ }
2469
+ }
2470
+ } else {
2471
+ warn("model-router config present but lib/model-router.mjs not reachable — `npm run upgrade` in agent");
2472
+ }
2473
+ } catch (err) {
2474
+ fail(`Model router config failed to load: ${err.message}`);
2475
+ issues++;
2476
+ }
2477
+ } else if (existsSync(routerDisabled)) {
2478
+ ok("Model router available (config/model-routing.yaml.disabled — rename to activate)");
2479
+ }
2480
+
2481
+ // ── Voice realtime (1.12.0+) ───────────────────────────────────────────
2482
+ const voiceActive = join(cwd, "config/voice.yaml");
2483
+ const voiceDisabled = join(cwd, "config/voice.yaml.disabled");
2484
+ if (existsSync(voiceActive)) {
2485
+ ok("Voice channel active (config/voice.yaml)");
2486
+ if (existsSync(join(cwd, ".env"))) {
2487
+ const env = readFileSync(join(cwd, ".env"), "utf-8");
2488
+ if (/^OPENAI_API_KEY=.+/m.test(env)) ok(" OPENAI_API_KEY configured");
2489
+ else { warn(" OPENAI_API_KEY missing — voice channel will not connect"); issues++; }
2490
+ if (/^TWILIO_ACCOUNT_SID=.+/m.test(env) && /^TWILIO_AUTH_TOKEN=.+/m.test(env)) ok(" Twilio credentials configured");
2491
+ else warn(" Twilio creds missing (only required for SIP inbound mode)");
2492
+ }
2493
+ if (!existsSync(join(cwd, "logs/voice/sessions"))) warn(" logs/voice/sessions/ missing — run: maestro init voice-realtime --apply");
2494
+ } else if (existsSync(voiceDisabled)) {
2495
+ ok("Voice channel available (config/voice.yaml.disabled — rename to activate)");
2496
+ }
2497
+
2498
+ // ── Channel bus (1.12.0+) ──────────────────────────────────────────────
2499
+ const channelStateDir = join(cwd, "state/channels");
2500
+ if (existsSync(channelStateDir)) ok("Channel bus state dir present (state/channels/)");
2501
+ else warn("state/channels/ missing — run: maestro init channel-bus --apply");
2502
+
2503
+ // ── WhatsApp transport (1.12.0+) ───────────────────────────────────────
2504
+ const waActive = join(cwd, "config/whatsapp.yaml");
2505
+ const waDisabled = join(cwd, "config/whatsapp.yaml.disabled");
2506
+ if (existsSync(waActive)) {
2507
+ try {
2508
+ const body = readFileSync(waActive, "utf-8");
2509
+ const isBaileys = /^transport:\s*baileys\b/m.test(body);
2510
+ const isTwilio = /^transport:\s*twilio\b/m.test(body);
2511
+ if (isBaileys) {
2512
+ ok("WhatsApp channel active (transport: baileys)");
2513
+ const pkgA = join(cwd, "node_modules/baileys/package.json");
2514
+ const pkgB = join(cwd, "node_modules/@whiskeysockets/baileys/package.json");
2515
+ if (existsSync(pkgA) || existsSync(pkgB)) ok(" baileys npm package installed");
2516
+ else { warn(" baileys not installed — run: npm i baileys"); issues++; }
2517
+ const authDir = join(cwd, "state/whatsapp/auth");
2518
+ if (!existsSync(authDir)) { warn(" state/whatsapp/auth/ missing — run: maestro init whatsapp-baileys --apply"); issues++; }
2519
+ } else if (isTwilio) {
2520
+ ok("WhatsApp channel active (transport: twilio)");
2521
+ if (existsSync(join(cwd, ".env"))) {
2522
+ const env = readFileSync(join(cwd, ".env"), "utf-8");
2523
+ if (/^TWILIO_ACCOUNT_SID=.+/m.test(env)) ok(" TWILIO_ACCOUNT_SID configured");
2524
+ else { warn(" TWILIO_ACCOUNT_SID missing"); issues++; }
2525
+ }
2526
+ } else {
2527
+ warn("config/whatsapp.yaml present but transport is neither 'twilio' nor 'baileys'");
2528
+ issues++;
2529
+ }
2530
+ } catch (err) {
2531
+ warn(`config/whatsapp.yaml unreadable: ${err.message}`);
2532
+ issues++;
2533
+ }
2534
+ } else if (existsSync(waDisabled)) {
2535
+ ok("WhatsApp channel available (config/whatsapp.yaml.disabled — rename to activate)");
2536
+ }
2537
+
2538
+ // ── Collective memory & org mesh ─────────────────────────────────────────
2539
+ if (existsSync(join(cwd, "scripts/collective/hook-runner.mjs"))) {
2540
+ const pointer = join(homedir(), ".claude", "maestro-agent.json");
2541
+ const globalSettings = join(homedir(), ".claude", "settings.json");
2542
+ let wired = false;
2543
+ if (existsSync(globalSettings)) {
2544
+ try {
2545
+ const gs = JSON.parse(readFileSync(globalSettings, "utf-8"));
2546
+ const cmds = Object.values(gs.hooks || {}).flat().flatMap((g) => (g && g.hooks ? g.hooks : [])).map((h) => h && h.command).filter(Boolean);
2547
+ wired = cmds.some((c) => c.includes("scripts/collective/hook-runner.mjs"));
2548
+ } catch { /* */ }
2549
+ }
2550
+ if (wired && existsSync(pointer)) {
2551
+ ok("Collective memory wired into global Claude config (~/.claude)");
2552
+ } else {
2553
+ warn("Collective memory not wired into global Claude config.");
2554
+ warn(" Fix: npx @cohortapp/agent-sdk global-setup");
2555
+ issues++;
2556
+ }
2557
+ }
2558
+
2559
+ // ── New framework systems (maestro 1.18+) ───────────────────────────────
2560
+ // One-line presence check per system. The lib/* modules are delivered by
2561
+ // `maestro upgrade` (UPGRADE_PATHS includes lib/); a missing module means an
2562
+ // upgrade is overdue (WARN, not FAIL — these systems are additive + fail-open).
2563
+ // The configs are disabled-by-default templates, so "available" is the healthy
2564
+ // resting state until the operator opts in.
2565
+ {
2566
+ const libPresent = (rel) => existsSync(join(cwd, rel));
2567
+ const needsUpgrade = (rel, label) => {
2568
+ if (libPresent(rel)) return true;
2569
+ warn(`${label} module missing (${rel}) — run: npx @cohortapp/agent-sdk upgrade`);
2570
+ issues++;
2571
+ return false;
2572
+ };
2573
+
2574
+ // Org / Cohort plane — REAL connectivity probes (2.0 §5), not presence
2575
+ // checks: enrollment resolution, directory reachability + latency, the
2576
+ // x-org-protocol drift header, the messaging.channels pairing gate, and
2577
+ // the workspace-mailbox probe when config/orgmail.yaml exists. `fail`
2578
+ // counts toward the issues total; `warn` lines are advisory (protocol
2579
+ // drift / servers predating the phase-1 header).
2580
+ if (needsUpgrade("lib/org/client.mjs", "Org client")) {
2581
+ try {
2582
+ const { checkOrgConnectivity } = await import("../lib/org/doctor.mjs");
2583
+ for (const r of await checkOrgConnectivity({ agentRoot: cwd })) {
2584
+ if (r.level === "ok") ok(r.msg);
2585
+ else if (r.level === "warn") warn(r.msg);
2586
+ else { fail(r.msg); issues++; }
2587
+ }
2588
+ } catch (err) {
2589
+ warn(`Org connectivity probes unavailable: ${err && err.message ? err.message : err}`);
2590
+ }
2591
+ }
2592
+
2593
+ // Secret lifecycle (local sealed store by default; broker optional).
2594
+ if (needsUpgrade("lib/secrets/broker.mjs", "Secrets broker")) {
2595
+ if (existsSync(join(cwd, "config/secrets.yaml"))) ok("Secret lifecycle configured (config/secrets.yaml — `maestro secrets list`)");
2596
+ else ok("Secret lifecycle available (config/secrets.yaml absent — `maestro upgrade` ships the template)");
2597
+ }
2598
+
2599
+ // Diagnostics / observability spine.
2600
+ needsUpgrade("lib/diagnostics/trace.mjs", "Diagnostics trace");
2601
+ if (libPresent("lib/diagnostics/counters.mjs")) ok("Diagnostics spine present (trace/events/counters)");
2602
+
2603
+ // Behavioral alerting (thresholds + webhook sink).
2604
+ if (existsSync(join(cwd, "config/alerts.yaml"))) ok("Alerting configured (config/alerts.yaml)");
2605
+ else warn("config/alerts.yaml absent — run: npx @cohortapp/agent-sdk upgrade");
2606
+
2607
+ // Hook bus, dynamic scheduling, send-gate — additive lib spine.
2608
+ needsUpgrade("lib/hooks/bus.mjs", "Hook bus");
2609
+ if (libPresent("lib/scheduling/dynamic-jobs.mjs")) ok("Dynamic scheduling present (lib/scheduling/)");
2610
+ needsUpgrade("lib/comms/send-gate.mjs", "Send-gate");
2611
+ }
2612
+
2613
+ console.log();
2614
+ if (issues === 0) ok("All checks passed.");
2615
+ else warn(`${issues} issue(s) found.`);
2616
+ process.exitCode = issues === 0 ? 0 : 1;
2617
+ }
2618
+
2619
+ // ---------------------------------------------------------------------------
2620
+ // SETUP — the deterministic, resumable, self-verifying wizard (WS1)
2621
+ // ---------------------------------------------------------------------------
2622
+
2623
+ const SETUP_HELP = `
2624
+ ${C.bold}maestro setup${C.reset} — configure this agent (deterministic, resumable, self-verifying).
2625
+
2626
+ Usage:
2627
+ maestro setup [section] [flags]
2628
+
2629
+ Sections (run in order): identity company model comms tools operating-model enrich verify
2630
+
2631
+ Flags:
2632
+ --resume Resume from the checkpoint (default when one exists)
2633
+ --from <id> Start at section <id> and continue
2634
+ --only <id,id,...> Run only these sections (forced, even if complete)
2635
+ --status Print each section's detect() status and exit
2636
+ --headless Never prompt; use --answers / env / defaults
2637
+ --answers <file.json> Pre-supplied answers for headless / scripted runs
2638
+ --no-enrich Skip LLM enrichment (leave deterministic skeletons)
2639
+ --dry-run Print what would run; write nothing
2640
+ --verbose Print generator output as sections apply
2641
+ --help, -h Show this help
2642
+
2643
+ Run from inside an agent repo (one with config/agent.json). After
2644
+ \`maestro create <dir>\`, do: cd <dir> && maestro setup
2645
+ `;
2646
+
2647
+ /**
2648
+ * `maestro pair <code>` — open a pre-auth pairing handshake with the org.
2649
+ *
2650
+ * The operator (or an org admin acting on the agent's behalf) picks a `code`,
2651
+ * runs this to POST pairing.request (UNAUTHENTICATED — the agent has no key
2652
+ * yet), and shares the code with an org admin who approves it via
2653
+ * pairing.approve. The device keypair is materialised on first use and its
2654
+ * public key rides in the handshake. Enablement of the knowledge plane still
2655
+ * waits on the admin issuing a Bearer key (paste it into config/org.yaml or
2656
+ * re-run `maestro setup --only org`). Complements the admin-API-key lane; it
2657
+ * does not replace it.
2658
+ */
2659
+ async function pairCmd(args = []) {
2660
+ if (args.includes("--help") || args.includes("-h")) {
2661
+ log("Usage: maestro pair <code>");
2662
+ log(" Opens a pre-auth pairing handshake; an org admin approves it (pairing.approve).");
2663
+ log(" Resolves the Cohort base from COHORT_BASE or config/org.yaml.");
2664
+ return;
2665
+ }
2666
+ const cwd = process.cwd();
2667
+ const isAgentRepo = existsSync(join(cwd, "config/agent.json")) || existsSync(join(cwd, "config/agent.ts"));
2668
+ if (!isAgentRepo) {
2669
+ warn("Not a Maestro agent directory (no config/agent.json here).");
2670
+ log("Run this from an agent repo (create one with `maestro create <dirname>`).");
2671
+ process.exitCode = 1;
2672
+ return;
2673
+ }
2674
+
2675
+ let orgMod, clientMod;
2676
+ try {
2677
+ orgMod = await import(pathToFileURL(join(MAESTRO_ROOT, "lib", "setup", "sections", "org.mjs")).href);
2678
+ clientMod = await import(pathToFileURL(join(MAESTRO_ROOT, "lib", "org", "client.mjs")).href);
2679
+ } catch (err) {
2680
+ fail(`could not load the org client: ${err && err.message ? err.message : err}`);
2681
+ process.exitCode = 1;
2682
+ return;
2683
+ }
2684
+
2685
+ const cfg = await orgMod.readOrgConfig(cwd);
2686
+ const c = clientMod.configFromAgent(cfg);
2687
+ const base = (process.env.COHORT_BASE || c.base || "").trim();
2688
+ const code = (args.find((a) => a && !a.startsWith("-")) || process.env.COHORT_PAIRING_CODE || "").trim();
2689
+
2690
+ if (!base) {
2691
+ fail("no Cohort base URL — set COHORT_BASE or run `maestro setup --only org` first.");
2692
+ process.exitCode = 1;
2693
+ return;
2694
+ }
2695
+ if (!code || code.length < 4) {
2696
+ fail("usage: maestro pair <code> (4+ chars; share it with your org admin to approve).");
2697
+ process.exitCode = 1;
2698
+ return;
2699
+ }
2700
+
2701
+ log(`Requesting pairing with ${base} …`);
2702
+ const res = await orgMod.requestPairing(cwd, { base, code, orgId: c.orgId });
2703
+ if (res.ok) {
2704
+ ok(`Pairing requested — code ${res.code}, agent ${res.agentId}, status ${res.status}.`);
2705
+ log("Ask an org admin to approve it (pairing.approve), then set org.cohort.token to the issued key");
2706
+ log("(or re-run `maestro setup --only org`). config/org.yaml records the outstanding handshake.");
2707
+ } else {
2708
+ fail(`Pairing failed: ${res.error}`);
2709
+ process.exitCode = 1;
2710
+ }
2711
+ }
2712
+
2713
+ async function setupCmd(args = []) {
2714
+ if (args.includes("--help") || args.includes("-h")) {
2715
+ console.log(SETUP_HELP);
2716
+ return;
2717
+ }
2718
+ const cwd = process.cwd();
2719
+ const isAgentRepo = existsSync(join(cwd, "config/agent.json")) || existsSync(join(cwd, "config/agent.ts"));
2720
+ const wantStatus = args.includes("--status");
2721
+
2722
+ if (!isAgentRepo) {
2723
+ // Graceful, non-throwing message (the done-bar requires --help / --status to
2724
+ // run cleanly in a non-agent dir).
2725
+ warn("Not a Maestro agent directory (no config/agent.json here).");
2726
+ log("Create one first: npx @cohortapp/agent-sdk create <dirname>");
2727
+ log("Then: cd <dirname> && maestro setup");
2728
+ if (!wantStatus) process.exitCode = 1;
2729
+ return;
2730
+ }
2731
+
2732
+ let runner;
2733
+ try {
2734
+ runner = await import(join(MAESTRO_ROOT, "lib", "setup", "runner.mjs"));
2735
+ } catch (err) {
2736
+ fail(`could not load the setup runner: ${err && err.message ? err.message : err}`);
2737
+ process.exitCode = 1;
2738
+ return;
2739
+ }
2740
+
2741
+ try {
2742
+ const parsed = runner.parseFlags(args);
2743
+ const res = await runner.runSetup({
2744
+ agentRoot: cwd,
2745
+ maestroRoot: MAESTRO_ROOT,
2746
+ flags: parsed.flags,
2747
+ section: parsed.section,
2748
+ });
2749
+ if (res && res.status) return; // --status already printed
2750
+ console.log();
2751
+ const gateGreen = res && res.verify && res.verify.complete && res.verify.complete.ok;
2752
+ if (res && res.ok && gateGreen) {
2753
+ ok("Setup complete — full operating model + a green completeness gate.");
2754
+ } else if (res && res.ok) {
2755
+ // No hard failures (identity/archetype/Claude all OK), but the completeness
2756
+ // gate isn't fully green — usually un-enriched prose slots (e.g. --no-enrich).
2757
+ ok("Setup ran — identity, model, and the operating model are in place.");
2758
+ warn("Some company-specific slots are still skeletons (run without --no-enrich to fill them).");
2759
+ warn("Re-run any time: maestro setup (resumes from the checkpoint)");
2760
+ } else {
2761
+ warn("Setup finished with hard failures — see the capability summary above.");
2762
+ warn("Re-run after fixing: maestro setup (resumes from the checkpoint)");
2763
+ process.exitCode = 1;
2764
+ }
2765
+ } catch (err) {
2766
+ fail(`setup failed: ${err && err.message ? err.message : err}`);
2767
+ process.exitCode = 1;
2768
+ }
2769
+ }
2770
+
2771
+ // ---------------------------------------------------------------------------
2772
+ // INIT — run pending feature-init steps
2773
+ // ---------------------------------------------------------------------------
2774
+
2775
+ async function initCmd(args) {
2776
+ const cwd = process.cwd();
2777
+ const help = args.includes("--help") || args.includes("-h");
2778
+ const apply = args.includes("--apply") || args.includes("-y") || args.includes("--yes");
2779
+ const featureArg = args.find((a) => !a.startsWith("-"));
2780
+
2781
+ if (help) {
2782
+ console.log(`
2783
+ Usage: maestro init [feature-name] [--apply]
2784
+
2785
+ Reconciles this agent's .maestro/features.json against the framework
2786
+ feature registry (framework-features.json shipped with @cohortapp/agent-sdk).
2787
+ Auto-runnable features are executed directly; features requiring user
2788
+ input are listed and can be opted into individually with their name as
2789
+ the first argument.
2790
+
2791
+ maestro init list pending features (dry-run)
2792
+ maestro init --apply run every auto-runnable pending step
2793
+ maestro init <feature> --apply run only the named feature's init step
2794
+ maestro init --help this help
2795
+
2796
+ After init, run \`maestro doctor\` to confirm everything is in place.
2797
+ `);
2798
+ return;
2799
+ }
2800
+
2801
+ const { loadRegistry, loadAgentState, reconcileFeatures, listPending, markInitialised, diffFeatures } = await import("../lib/feature-init.mjs");
2802
+ let registry;
2803
+ try { registry = loadRegistry(FRAMEWORK_FEATURES_PATH); }
2804
+ catch (err) { fail(err.message); process.exit(1); }
2805
+
2806
+ if (featureArg) {
2807
+ // Targeted init — run just one feature's command.
2808
+ const def = registry.features?.[featureArg];
2809
+ if (!def) { fail(`unknown feature: ${featureArg}`); process.exit(1); }
2810
+ log(`Running init for ${featureArg}: ${def.init?.description || ""}`);
2811
+ if (!apply) { warn("dry-run — pass --apply to actually run."); return; }
2812
+ const r = await runFeatureInit({ name: featureArg, def, cwd });
2813
+ if (r.ok) {
2814
+ markInitialised(cwd, featureArg, def.version);
2815
+ ok(`initialised: ${featureArg} v${def.version}`);
2816
+ } else {
2817
+ fail(`init failed: ${r.error}`);
2818
+ process.exit(1);
2819
+ }
2820
+ return;
2821
+ }
2822
+
2823
+ // Reconcile + list pending.
2824
+ const state = loadAgentState(cwd);
2825
+ const diff = diffFeatures(registry, state);
2826
+ if (diff.length === 0 && (state.pending || []).length === 0) {
2827
+ ok("All framework features are initialised.");
2828
+ return;
2829
+ }
2830
+
2831
+ // Show what's pending before doing anything.
2832
+ if (!apply) {
2833
+ log(`${diff.length} feature(s) need initialisation:`);
2834
+ for (const item of diff) {
2835
+ const auto = item.definition.init?.auto;
2836
+ console.log(` ${auto ? "•" : "?"} ${item.definition.title || item.name} (v${item.version}) — ${auto ? "auto" : "needs --apply"}`);
2837
+ if (item.definition.init?.description) console.log(` ${item.definition.init.description}`);
2838
+ }
2839
+ console.log();
2840
+ log("Run `maestro init --apply` to execute every auto-runnable step.");
2841
+ return;
2842
+ }
2843
+
2844
+ // --apply: run auto features now.
2845
+ log("Running auto-init steps...");
2846
+ const result = reconcileFeatures({
2847
+ maestroRoot: MAESTRO_ROOT,
2848
+ agentRoot: cwd,
2849
+ registry,
2850
+ runAuto: true,
2851
+ logger: (entry) => {
2852
+ const sym = entry.stage === "completed" ? `${C.green}✓${C.reset}` : entry.stage === "failed" ? `${C.red}✗${C.reset}` : entry.stage === "pending" ? `${C.yellow}~${C.reset}` : `${C.cyan}…${C.reset}`;
2853
+ console.log(` ${sym} ${entry.feature}${entry.message ? ` — ${entry.message}` : ""}`);
2854
+ },
2855
+ });
2856
+
2857
+ console.log();
2858
+ if (result.ranAuto.length) ok(`${result.ranAuto.length} feature(s) initialised: ${result.ranAuto.join(", ")}`);
2859
+ if (result.pending.length) warn(`${result.pending.length} feature(s) still pending (need manual decision): ${result.pending.join(", ")}`);
2860
+ if (result.failed.length) {
2861
+ fail(`${result.failed.length} feature(s) failed during init:`);
2862
+ for (const f of result.failed) console.log(` ✗ ${f.name}: ${f.error}`);
2863
+ process.exit(1);
2864
+ }
2865
+ if (result.pending.length) {
2866
+ console.log();
2867
+ const pendingList = listPending(cwd);
2868
+ for (const p of pendingList) {
2869
+ console.log(` To initialise ${p.feature}: maestro init ${p.feature} --apply`);
2870
+ }
2871
+ }
2872
+ }
2873
+
2874
+ async function runFeatureInit({ name, def, cwd }) {
2875
+ const { execFileSync } = await import("node:child_process");
2876
+ const cmd = def.init?.command;
2877
+ if (!cmd || cmd === "true") return { ok: true };
2878
+ const parts = cmd.split(/\s+/);
2879
+ try {
2880
+ if (parts[0] === "node" && parts[1]) {
2881
+ const script = join(cwd, parts[1]);
2882
+ const fallback = join(MAESTRO_ROOT, parts[1]);
2883
+ const scriptPath = existsSync(script) ? script : (existsSync(fallback) ? fallback : null);
2884
+ if (!scriptPath) return { ok: false, error: `script not found: ${parts[1]}` };
2885
+ execFileSync(process.execPath, [scriptPath, ...parts.slice(2)], {
2886
+ cwd,
2887
+ env: { ...process.env, AGENT_ROOT: cwd, AGENT_DIR: cwd, MAESTRO_ROOT },
2888
+ stdio: "inherit",
2889
+ });
2890
+ return { ok: true };
2891
+ }
2892
+ execFileSync("/bin/bash", ["-lc", cmd], {
2893
+ cwd,
2894
+ env: { ...process.env, AGENT_ROOT: cwd, AGENT_DIR: cwd, MAESTRO_ROOT },
2895
+ stdio: "inherit",
2896
+ });
2897
+ return { ok: true };
2898
+ } catch (err) {
2899
+ return { ok: false, error: err.message };
2900
+ }
2901
+ }
2902
+
2903
+ // ---------------------------------------------------------------------------
2904
+ // MAIN
2905
+ // ---------------------------------------------------------------------------
2906
+
2907
+ /** Timestamp suffix for config backups. */
2908
+ function tsStamp() {
2909
+ return new Date().toISOString().replace(/[:.]/g, "-");
2910
+ }
2911
+
2912
+ /** Does `dir` look like a collective-capable agent repo? */
2913
+ function isAgentRepo(dir) {
2914
+ return existsSync(join(dir, "config", "agent.json")) &&
2915
+ existsSync(join(dir, "scripts", "collective", "hook-runner.mjs"));
2916
+ }
2917
+
2918
+ /** Scan the home dir (depth 1) for collective-capable agent repos. */
2919
+ function discoverAgentRepos(home) {
2920
+ const out = [];
2921
+ let entries;
2922
+ try { entries = readdirSync(home, { withFileTypes: true }); } catch { return out; }
2923
+ for (const e of entries) {
2924
+ if (!e.isDirectory() || e.name.startsWith(".")) continue;
2925
+ const dir = join(home, e.name);
2926
+ if (isAgentRepo(dir)) out.push(dir);
2927
+ }
2928
+ return out;
2929
+ }
2930
+
2931
+ /**
2932
+ * Resolve the agent repo global-setup should wire. Order: explicit path arg →
2933
+ * cwd → resolveAgentRoot (env/walk/pointer) → unique home-dir scan. Returns the
2934
+ * path, or null if none / ambiguous (caller reports).
2935
+ */
2936
+ async function resolveSetupRepo(args) {
2937
+ const argPath = args.find((a) => !a.startsWith("-"));
2938
+ if (argPath) { const p = resolve(argPath); return isAgentRepo(p) ? p : null; }
2939
+ if (existsSync(join(process.cwd(), "scripts/collective/hook-runner.mjs"))) return process.cwd();
2940
+ try {
2941
+ const { resolveAgentRoot } = await import("../lib/collective/config.mjs");
2942
+ const r = resolveAgentRoot(process.cwd());
2943
+ if (r && isAgentRepo(r)) return r;
2944
+ } catch { /* */ }
2945
+ const found = discoverAgentRepos(homedir());
2946
+ if (found.length === 1) return found[0];
2947
+ if (found.length > 1) { fail(`Multiple agent repos found: ${found.join(", ")}. Pass one: maestro global-setup <repo>`); return null; }
2948
+ return null;
2949
+ }
2950
+
2951
+ /**
2952
+ * global-setup — wire the Collective Memory & Org Mesh layer into the GLOBAL
2953
+ * Claude Code config (~/.claude) so EVERY session on this machine contributes to
2954
+ * and recalls from this agent's long-term memory, and is aware of concurrent
2955
+ * sessions. Idempotent + additive (never clobbers existing hooks/plugins/config;
2956
+ * backs up settings.json before writing). Run by init-agent.sh, by `wundr
2957
+ * install` (which defers to maestro), and standalone. See
2958
+ * docs/architecture/collective-memory-and-org-mesh.md.
2959
+ */
2960
+ async function globalSetup(args = []) {
2961
+ const dryRun = args.includes("--dry-run") || args.includes("-n");
2962
+ console.log();
2963
+ log("Wiring collective memory into the global Claude Code config...");
2964
+ const cwd = await resolveSetupRepo(args);
2965
+ if (!cwd) {
2966
+ fail("Could not locate a Maestro agent repo to wire.");
2967
+ fail("Run from your agent repo, pass it explicitly (`maestro global-setup <repo>`),");
2968
+ fail("or run `npx @cohortapp/agent-sdk upgrade` in the agent repo first.");
2969
+ process.exit(1);
2970
+ }
2971
+ log(`Agent repo: ${cwd}`);
2972
+ const gc = await import("../lib/collective/global-config.mjs");
2973
+ const claudeDir = join(homedir(), ".claude");
2974
+ const settingsPath = join(claudeDir, "settings.json");
2975
+ const claudeMdPath = join(claudeDir, "CLAUDE.md");
2976
+ const pointerFile = join(claudeDir, "maestro-agent.json");
2977
+ if (!dryRun) mkdirSync(claudeDir, { recursive: true });
2978
+
2979
+ let agentName = "";
2980
+ try {
2981
+ const aj = JSON.parse(readFileSync(join(cwd, "config/agent.json"), "utf-8"));
2982
+ agentName = aj.firstName || aj.fullName || "";
2983
+ } catch { /* unconfigured is fine */ }
2984
+
2985
+ // 1) settings.json hooks (deep-merged, idempotent, backed up before write).
2986
+ let settings = {};
2987
+ if (existsSync(settingsPath)) {
2988
+ try { settings = JSON.parse(readFileSync(settingsPath, "utf-8")); } catch { settings = {}; }
2989
+ }
2990
+ const { hooks, added } = gc.mergeCollectiveHooks(settings.hooks || {}, cwd, process.execPath);
2991
+ if (added.length > 0) {
2992
+ settings.hooks = hooks;
2993
+ if (!dryRun) {
2994
+ if (existsSync(settingsPath)) {
2995
+ try { copyFileSync(settingsPath, `${settingsPath}.backup.${tsStamp()}`); } catch { /* */ }
2996
+ }
2997
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
2998
+ }
2999
+ ok(`${dryRun ? "[dry-run] " : ""}global hooks added: ${added.join(", ")}`);
3000
+ } else {
3001
+ ok("global hooks already present (no change)");
3002
+ }
3003
+
3004
+ // 2) global CLAUDE.md directive (only our delimited block is touched).
3005
+ let mdContent = "";
3006
+ if (existsSync(claudeMdPath)) { try { mdContent = readFileSync(claudeMdPath, "utf-8"); } catch { /* */ } }
3007
+ const md = gc.upsertClaudeMdBlock(mdContent, gc.buildCollectiveClaudeMd(cwd));
3008
+ if (md.changed) {
3009
+ if (!dryRun) writeFileSync(claudeMdPath, md.content);
3010
+ ok(`${dryRun ? "[dry-run] " : ""}global CLAUDE.md collective block written`);
3011
+ } else {
3012
+ ok("global CLAUDE.md collective block up to date");
3013
+ }
3014
+
3015
+ // 3) machine pointer (locates the agent for sessions in any repo).
3016
+ if (!dryRun) writeFileSync(pointerFile, JSON.stringify(gc.buildPointer(cwd, agentName), null, 2));
3017
+ ok(`${dryRun ? "[dry-run] " : ""}machine pointer → ${pointerFile}`);
3018
+
3019
+ console.log();
3020
+ if (dryRun) {
3021
+ log("Dry run complete — no files written.");
3022
+ } else {
3023
+ log("Collective memory wired. New Claude Code sessions on this machine now");
3024
+ log("contribute to + recall from this agent, and see concurrent sessions.");
3025
+ if (added.length > 0) log("Restart running interactive sessions to pick up the new hooks.");
3026
+ }
3027
+ }
3028
+
3029
+ // ---------------------------------------------------------------------------
3030
+ // AUDIT — registry-driven security-posture audit + attestation (G15)
3031
+ // ---------------------------------------------------------------------------
3032
+ //
3033
+ // `maestro audit` runs lib/security/audit-engine.mjs against the current agent
3034
+ // repo and prints a one-line-per-check summary (or JSON). It exits non-zero on
3035
+ // any unsuppressed failure, so it's CI-usable. `--fix` applies the safe,
3036
+ // declared auto-fixes first, then re-audits. `--attest` (implied by --json)
3037
+ // emits the hash-locked conformance artifact the org server aggregates.
3038
+
3039
+ async function auditCmd(args = []) {
3040
+ const flags = { json: false, fix: false };
3041
+ for (const a of args) {
3042
+ if (a === "--json") flags.json = true;
3043
+ else if (a === "--fix") flags.fix = true;
3044
+ else if (a === "--help" || a === "-h") {
3045
+ console.log(`
3046
+ Usage: maestro audit [--json] [--fix]
3047
+
3048
+ Runs the registry-driven security-posture audit (lib/security/audit-engine.mjs)
3049
+ against this agent repo. Exits non-zero on any unsuppressed failure.
3050
+
3051
+ Flags:
3052
+ --json Emit machine-readable JSON (results + hash-locked attestation)
3053
+ --fix Apply safe auto-fixes (e.g. chmod 600 .env, remove tracked .pyc),
3054
+ then re-audit. Non-fixable findings are left for you to resolve.
3055
+
3056
+ Suppressions: config/audit-suppressions.yaml — a list of { id, reason, until }.
3057
+ A suppression silences one check id, requires a reason, and expires at 'until'.
3058
+ `.trim());
3059
+ return;
3060
+ } else { fail(`Unknown flag: ${a}`); process.exit(1); }
3061
+ }
3062
+
3063
+ const cwd = process.cwd();
3064
+ if (!existsSync(join(cwd, "config/agent.json")) && !existsSync(join(cwd, "config/agent.ts")) && !existsSync(join(cwd, "CLAUDE.md"))) {
3065
+ if (flags.json) { console.log(JSON.stringify({ ok: false, error: "not a Maestro agent directory" })); }
3066
+ else { fail("Not a Maestro agent directory (config/agent.json not found)"); }
3067
+ process.exit(1);
3068
+ }
3069
+
3070
+ let audit;
3071
+ let fixed = [];
3072
+ if (flags.fix) {
3073
+ const res = await applyFixes(cwd);
3074
+ fixed = res.fixed;
3075
+ audit = res.audit;
3076
+ } else {
3077
+ audit = await runAudit(cwd);
3078
+ }
3079
+ const attestation = buildAttestation(audit, cwd);
3080
+
3081
+ if (flags.json) {
3082
+ console.log(JSON.stringify({ ok: audit.ok, summary: audit.summary, results: audit.results, suppressed: audit.suppressed, fixed, attestation }, null, 2));
3083
+ process.exit(audit.ok ? 0 : 1);
3084
+ }
3085
+
3086
+ console.log();
3087
+ console.log(`${C.bold}Security posture audit${C.reset}`);
3088
+ if (fixed.length) ok(`applied ${fixed.length} safe fix(es): ${fixed.join(", ")}`);
3089
+ for (const r of audit.results) {
3090
+ if (r.ok) { ok(`[${r.severity}] ${r.id} — ${r.detail}`); continue; }
3091
+ if (r.suppressed) {
3092
+ warn(`[${r.severity}] ${r.id} — SUPPRESSED (${r.suppressionReason}${r.suppressionUntil ? `, until ${r.suppressionUntil}` : ""})`);
3093
+ continue;
3094
+ }
3095
+ fail(`[${r.severity}] ${r.id} — ${r.detail}`);
3096
+ if (r.fix) console.log(` fix: ${r.fix}${r.fixable ? " (or: maestro audit --fix)" : ""}`);
3097
+ }
3098
+ console.log();
3099
+ const s = audit.summary;
3100
+ console.log(`=== ${s.passed}/${s.total} checks passed${s.suppressed ? `, ${s.suppressed} suppressed` : ""}${s.failed ? `, ${s.failed} FAILED` : ""} ===`);
3101
+ console.log(`attestation: ${attestation.hash.slice(0, 16)}… (archetype: ${attestation.identity.function || "?"}/${attestation.identity.altitude || "?"})`);
3102
+ if (audit.ok) ok("Posture conformant — no unsuppressed failures.");
3103
+ else fail(`Posture has ${s.failed} unsuppressed failure(s)${s.worstSeverity ? ` (worst: ${s.worstSeverity})` : ""}.`);
3104
+ process.exit(audit.ok ? 0 : 1);
3105
+ }
3106
+
3107
+ // ---------------------------------------------------------------------------
3108
+ // SECRETS — secret lifecycle (gaps-enterprise-ops G12)
3109
+ // ---------------------------------------------------------------------------
3110
+ //
3111
+ // `maestro secrets sync` Pull org secrets from the broker into the
3112
+ // local encrypted store (cached, offline).
3113
+ // `maestro secrets list` Print secret NAMES only (never values).
3114
+ // `maestro secrets rotate --name X` Rotate X; the new value is read from stdin
3115
+ // so it never lands in shell history / argv.
3116
+ //
3117
+ // Config lives in config/secrets.yaml. The broker fetch is the runtime global
3118
+ // (real network) here — tests exercise lib/secrets directly with an injected
3119
+ // fetch, never through this command.
3120
+
3121
+ const SECRETS_HELP = `
3122
+ Usage: maestro secrets <list|sync|rotate> [flags]
3123
+
3124
+ list Print secret names in the local store (names only)
3125
+ sync [--name X ...] Pull org secrets from the broker into the local store
3126
+ (no --name → pull every name the broker lists)
3127
+ rotate --name X Rotate secret X; the new value is read from stdin
3128
+
3129
+ Config: config/secrets.yaml (provider: file|env|keychain, broker: { base_url, token_env })
3130
+ Audit: logs/audit/secrets.jsonl (names only — values are never logged)
3131
+ `.trim();
3132
+
3133
+ /** Load + parse config/secrets.yaml (YAML or JSON), or null when absent. */
3134
+ async function loadSecretsConfig(cwd) {
3135
+ const path = join(cwd, "config/secrets.yaml");
3136
+ if (!existsSync(path)) return null;
3137
+ const text = readFileSync(path, "utf-8");
3138
+ try {
3139
+ const y = await import("js-yaml");
3140
+ return y.load(text) || null;
3141
+ } catch {
3142
+ // js-yaml unavailable — accept a JSON config as a graceful fallback.
3143
+ try { return JSON.parse(text); } catch { return null; }
3144
+ }
3145
+ }
3146
+
3147
+ /** Read a value from stdin (for `rotate`, so secrets never touch argv). */
3148
+ function readStdin() {
3149
+ try {
3150
+ // 1 MiB cap is plenty for a secret; read the whole fd 0.
3151
+ const buf = readFileSync(0, "utf-8");
3152
+ return buf.replace(/\r?\n$/, "");
3153
+ } catch {
3154
+ return "";
3155
+ }
3156
+ }
3157
+
3158
+ async function secretsCmd(args = []) {
3159
+ const sub = args[0];
3160
+ if (!sub || sub === "--help" || sub === "-h") {
3161
+ console.log(SECRETS_HELP);
3162
+ return;
3163
+ }
3164
+ const cwd = process.cwd();
3165
+ if (!existsSync(join(cwd, "config/agent.json")) && !existsSync(join(cwd, "config/agent.ts")) && !existsSync(join(cwd, "CLAUDE.md"))) {
3166
+ fail("Not a Maestro agent directory (config/agent.json not found)");
3167
+ process.exit(1);
3168
+ }
3169
+
3170
+ // Parse --name (repeatable) for sync/rotate.
3171
+ const names = [];
3172
+ for (let i = 1; i < args.length; i++) {
3173
+ if (args[i] === "--name" || args[i] === "-n") { names.push(args[++i]); }
3174
+ }
3175
+
3176
+ const cfg = await loadSecretsConfig(cwd);
3177
+
3178
+ if (sub === "list") {
3179
+ const provider = selectProvider(cfg, { agentRoot: cwd, env: process.env });
3180
+ const list = await provider.list();
3181
+ if (list.length === 0) {
3182
+ log(`No secrets in the local store (provider: ${provider.name}).`);
3183
+ return;
3184
+ }
3185
+ log(`Secrets in the local store (provider: ${provider.name}) — names only:`);
3186
+ for (const n of list) console.log(` ${n}`);
3187
+ return;
3188
+ }
3189
+
3190
+ if (sub === "sync") {
3191
+ const broker = makeBroker(cfg, { fetch: globalThis.fetch });
3192
+ if (!broker) {
3193
+ fail("No broker configured — set config/secrets.yaml: broker.base_url + token_env.");
3194
+ process.exitCode = 1;
3195
+ return;
3196
+ }
3197
+ let toPull = names.filter(Boolean);
3198
+ if (toPull.length === 0) {
3199
+ // No explicit names → pull everything the broker advertises.
3200
+ toPull = await broker.list();
3201
+ if (toPull.length === 0) {
3202
+ warn("Broker returned no secret names to sync.");
3203
+ return;
3204
+ }
3205
+ }
3206
+ let res;
3207
+ try {
3208
+ res = await syncSecrets({ cfg, agentRoot: cwd, env: process.env, fetch: globalThis.fetch, names: toPull });
3209
+ } catch (err) {
3210
+ fail(`secrets sync failed: ${err.message}`);
3211
+ process.exitCode = 1;
3212
+ return;
3213
+ }
3214
+ if (res.synced.length) ok(`Synced ${res.synced.length} secret(s): ${res.synced.join(", ")}`);
3215
+ if (res.missing.length) warn(`Broker had no value for: ${res.missing.join(", ")}`);
3216
+ for (const f of res.failed) fail(`Failed to store ${f.name}: ${f.error}`);
3217
+ process.exitCode = res.failed.length ? 1 : 0;
3218
+ return;
3219
+ }
3220
+
3221
+ if (sub === "rotate") {
3222
+ const name = names.filter(Boolean)[0];
3223
+ if (!name) {
3224
+ fail("usage: maestro secrets rotate --name <SECRET_NAME> (new value on stdin)");
3225
+ process.exitCode = 1;
3226
+ return;
3227
+ }
3228
+ const value = readStdin();
3229
+ if (!value) {
3230
+ fail("No value on stdin. Pipe the new value: echo -n 'newval' | maestro secrets rotate --name X");
3231
+ process.exitCode = 1;
3232
+ return;
3233
+ }
3234
+ try {
3235
+ const res = await rotateSecret(name, value, { cfg, agentRoot: cwd, env: process.env, fetch: globalThis.fetch });
3236
+ ok(`Rotated "${res.name}" on ${res.provider}${res.mirrored ? " (mirrored locally)" : ""}.`);
3237
+ log(`Audit: ${relative(cwd, auditLogPath(cwd))} (names only)`);
3238
+ } catch (err) {
3239
+ fail(`Rotation failed: ${err.message}`);
3240
+ process.exitCode = 1;
3241
+ return;
3242
+ }
3243
+ return;
3244
+ }
3245
+
3246
+ fail(`Unknown secrets subcommand: ${sub}`);
3247
+ console.log(SECRETS_HELP);
3248
+ process.exitCode = 1;
3249
+ }
3250
+
3251
+ // ---------------------------------------------------------------------------
3252
+ // SYNC — re-pull this agent's record from Cohort and update config/agent.json
3253
+ // ---------------------------------------------------------------------------
3254
+ //
3255
+ // The seamless-UPDATE path that mirrors the Cohort-driven setup pull: when
3256
+ // COHORT_API_KEY + COHORT_ORG_ID (optionally COHORT_AGENT_ID) are present it
3257
+ // re-pulls the MemberProfileDTO, maps it to the config/agent.json subset, merges
3258
+ // it (never clobbering a locally-customised value unless --force), regenerates
3259
+ // the derived artefacts, and prints a diff of what changed. Fail-open + idempotent.
3260
+
3261
+ const SYNC_HELP = `
3262
+ Usage: maestro sync [flags]
3263
+
3264
+ Re-pull this agent's record from Cohort (the org single source of truth) and
3265
+ update config/agent.json. Requires COHORT_API_KEY + COHORT_ORG_ID in the env
3266
+ (optionally COHORT_AGENT_ID to select a specific member; otherwise whoami).
3267
+
3268
+ Flags:
3269
+ --force Overwrite locally-customised values too (default: protect them)
3270
+ --dry-run Show the diff without writing config/agent.json
3271
+ --help, -h Show this help
3272
+ `;
3273
+
3274
+ function parseSyncFlags(args) {
3275
+ const flags = { force: false, dryRun: false };
3276
+ for (const a of args) {
3277
+ if (a === "--force") flags.force = true;
3278
+ else if (a === "--dry-run" || a === "-n") flags.dryRun = true;
3279
+ else if (a === "--help" || a === "-h") return null;
3280
+ else { fail(`Unknown sync flag: ${a}`); process.exit(1); }
3281
+ }
3282
+ return flags;
3283
+ }
3284
+
3285
+ /** Render one diff row's value compactly for the CLI report. */
3286
+ function fmtSyncVal(v) {
3287
+ if (v === undefined) return "(unset)";
3288
+ if (Array.isArray(v)) return `[${v.length} item${v.length === 1 ? "" : "s"}]`;
3289
+ if (v && typeof v === "object") return JSON.stringify(v);
3290
+ const s = String(v);
3291
+ return s.length > 60 ? `${s.slice(0, 57)}…` : s;
3292
+ }
3293
+
3294
+ async function syncCmd(args = []) {
3295
+ const flags = parseSyncFlags(args);
3296
+ if (flags === null) { console.log(SYNC_HELP); return; }
3297
+ const cwd = process.cwd();
3298
+
3299
+ if (!existsSync(join(cwd, "config/agent.json")) && !existsSync(join(cwd, "config/agent.ts"))) {
3300
+ fail("Not a Maestro agent directory (config/agent.json not found)");
3301
+ process.exit(1);
3302
+ }
3303
+
3304
+ const { cohortPullEnv, pullAndPlan } = await import(join(MAESTRO_ROOT, "lib", "setup", "enroll-from-cohort.mjs"));
3305
+ const { fetchSelfProfile } = await import(join(MAESTRO_ROOT, "lib", "org", "client.mjs"));
3306
+
3307
+ const e = cohortPullEnv();
3308
+ if (!e.enabled) {
3309
+ fail("Cohort pull not configured — set COHORT_API_KEY and COHORT_ORG_ID.");
3310
+ process.exit(1);
3311
+ }
3312
+
3313
+ // Test seam (sandbox/offline): MAESTRO_SYNC_FETCH_SELF_PROFILE points at an ESM
3314
+ // module default-exporting a fetchSelfProfile-shaped fn, so the sync path can be
3315
+ // exercised end-to-end without a network round-trip. Never used in production.
3316
+ let fetchSelf = fetchSelfProfile;
3317
+ if (process.env.MAESTRO_SYNC_FETCH_SELF_PROFILE) {
3318
+ try {
3319
+ const mod = await import(pathToFileURL(process.env.MAESTRO_SYNC_FETCH_SELF_PROFILE).href);
3320
+ if (typeof (mod.default || mod.fetchSelfProfile) === "function") fetchSelf = mod.default || mod.fetchSelfProfile;
3321
+ } catch { /* fall back to the real client */ }
3322
+ }
3323
+
3324
+ log(`Re-pulling profile from Cohort (${e.agentId ? `member ${e.agentId}` : "whoami"})…`);
3325
+ const plan = await pullAndPlan({ agentRoot: cwd, force: flags.force, fetchSelfProfileImpl: fetchSelf });
3326
+ if (!plan.pulled) {
3327
+ warn(`No profile pulled (${plan.reason || "unknown"}). config/agent.json unchanged.`);
3328
+ return;
3329
+ }
3330
+
3331
+ if (!plan.changes.length) {
3332
+ ok("Already in sync with Cohort — no changes.");
3333
+ return;
3334
+ }
3335
+
3336
+ console.log();
3337
+ console.log(`${C.bold}Changes from Cohort${flags.dryRun ? " (dry run)" : ""}:${C.reset}`);
3338
+ for (const ch of plan.changes) {
3339
+ console.log(` ${ch.path}: ${C.yellow}${fmtSyncVal(ch.from)}${C.reset} → ${C.green}${fmtSyncVal(ch.to)}${C.reset}`);
3340
+ }
3341
+ console.log();
3342
+
3343
+ if (flags.dryRun) {
3344
+ log("Dry run — config/agent.json not modified. Re-run without --dry-run to apply.");
3345
+ return;
3346
+ }
3347
+
3348
+ const { mergeAgentJson, ensureSotWrapper } = await import(join(MAESTRO_ROOT, "lib", "setup", "sot.mjs"));
3349
+ mergeAgentJson(cwd, plan.patch);
3350
+ ok(`Updated config/agent.json (${plan.changes.length} field${plan.changes.length === 1 ? "" : "s"}).`);
3351
+
3352
+ // Regenerate the derived artefacts so the archetype/wrapper/env reflect the pull.
3353
+ try {
3354
+ const { runGenerator } = await import(join(MAESTRO_ROOT, "lib", "setup", "run-generator.mjs"));
3355
+ runGenerator("init-archetype.mjs", cwd, { maestroRoot: MAESTRO_ROOT });
3356
+ ensureSotWrapper(cwd);
3357
+ runGenerator("generate-agent-env.mjs", cwd, { maestroRoot: MAESTRO_ROOT });
3358
+ ok("Regenerated archetype, agent.ts wrapper, and agent.env.");
3359
+ } catch (err) {
3360
+ warn(`Derived-artefact regeneration skipped: ${err && err.message ? err.message : err}`);
3361
+ }
3362
+
3363
+ // Refresh the authoritative org context cache (config/org-context.json) so the
3364
+ // agent's situational awareness tracks the org. Fail-open + non-blocking.
3365
+ try {
3366
+ const { bootstrapOrgContextInto } = await import(join(MAESTRO_ROOT, "lib", "setup", "sections", "org.mjs"));
3367
+ const res = await bootstrapOrgContextInto(cwd, {});
3368
+ if (res && res.path) ok("Refreshed config/org-context.json from Cohort.");
3369
+ } catch { /* fail-open — local model stands */ }
3370
+ }
3371
+
3372
+ // ---------------------------------------------------------------------------
3373
+ // WHO-OWNS — situational awareness over the cached org context
3374
+ // ---------------------------------------------------------------------------
3375
+ //
3376
+ // The runtime consumer of lib/org/awareness.mjs: deterministic, ZERO-LLM answers
3377
+ // drawn from config/org-context.json (the Cohort bootstrap cache) — "who owns
3378
+ // this scope?", "who do I escalate to?", "who reports to me?", "which adopted
3379
+ // decisions touch this topic?". Fail-open: an un-enrolled agent (no cache) prints
3380
+ // a clear hint instead of erroring.
3381
+
3382
+ /**
3383
+ * Render the awareness answers for the current agent to stdout.
3384
+ * @param {string[]} args - [scope?] plus flags (--escalate, --reports, --decisions, --json)
3385
+ */
3386
+ async function whoOwnsCmd(args = []) {
3387
+ const cwd = process.cwd();
3388
+ const flags = new Set(args.filter((a) => a.startsWith("--")));
3389
+ const positional = args.filter((a) => !a.startsWith("--"));
3390
+ const scope = positional.join(" ").trim();
3391
+ const asJson = flags.has("--json");
3392
+
3393
+ const { loadOrgContext } = await import(join(MAESTRO_ROOT, "lib", "org", "bootstrap-context.mjs"));
3394
+ const ctx = loadOrgContext(cwd);
3395
+ if (!ctx) {
3396
+ if (asJson) { console.log(JSON.stringify({ error: "no-org-context" })); return; }
3397
+ warn("No cached org context (config/org-context.json). Enroll + sync first:");
3398
+ warn(" maestro setup --only org # then maestro sync");
3399
+ return;
3400
+ }
3401
+
3402
+ const aw = await import(join(MAESTRO_ROOT, "lib", "org", "awareness.mjs"));
3403
+ const wantEscalate = flags.has("--escalate");
3404
+ const wantReports = flags.has("--reports");
3405
+ const wantDecisions = flags.has("--decisions");
3406
+ const wantOwner = !!scope || (!wantEscalate && !wantReports && !wantDecisions);
3407
+
3408
+ const result = {};
3409
+ if (wantOwner && scope) result.owner = aw.ownerOf(ctx, scope);
3410
+ if (wantEscalate || (!scope && !wantReports && !wantDecisions)) result.escalateTo = aw.escalateTo(ctx);
3411
+ if (wantReports) result.reports = aw.whoReportsToMe(ctx, ctx.profile && ctx.profile.slug);
3412
+ if (wantDecisions) result.decisions = aw.relevantDecisions(ctx, scope);
3413
+
3414
+ if (asJson) { console.log(JSON.stringify(result, null, 2)); return; }
3415
+
3416
+ console.log();
3417
+ if (scope && "owner" in result) {
3418
+ const o = result.owner;
3419
+ if (o) log(`Owner of "${scope}": ${C.bold}${o.owner.displayName || o.owner.slug}${C.reset} (${o.role || "assigned"}) — stream ${o.stream.name || o.stream.code || "?"}`);
3420
+ else warn(`No accountable owner found for "${scope}" in the org strategy.`);
3421
+ }
3422
+ if ("escalateTo" in result) {
3423
+ const e = result.escalateTo;
3424
+ if (e) log(`Escalate to: ${e.manager ? (e.manager.displayName || e.manager.slug) : "(no manager)"}${e.escalationPath ? ` — ${e.escalationPath}` : ""}`);
3425
+ else warn("No escalation target recorded in the org context.");
3426
+ }
3427
+ if (wantReports) {
3428
+ const rs = result.reports || [];
3429
+ if (rs.length) { log(`Reports (${rs.length}):`); for (const r of rs) ok(`${r.displayName || r.slug}${r.style ? ` [${r.style}]` : ""}`); }
3430
+ else warn("No direct reports recorded in the org context.");
3431
+ }
3432
+ if (wantDecisions) {
3433
+ const ds = result.decisions || [];
3434
+ if (ds.length) { log(`Adopted decisions${scope ? ` touching "${scope}"` : ""} (${ds.length}):`); for (const d of ds.slice(0, 12)) ok(d.title || d.summary || d.id || "(untitled)"); }
3435
+ else warn(`No adopted decisions${scope ? ` touching "${scope}"` : ""}.`);
3436
+ }
3437
+ }
3438
+
3439
+ // Pure, side-effect-free helpers exported for unit tests (bin/maestro.test.mjs).
3440
+ // They carry no I/O so importing this module to test them is safe; the CLI
3441
+ // dispatch below only runs when the file is the process entrypoint.
3442
+ export { evaluateCostTripwire, summariseLedgerRows, evaluateStaleness, parseSyncFlags, fmtSyncVal };
3443
+
3444
+ async function main() {
3445
+ const [, , command, ...args] = process.argv;
3446
+
3447
+ switch (command) {
3448
+ case "create": create(args[0]); break;
3449
+ case "setup": await setupCmd(args); break;
3450
+ case "pair": await pairCmd(args); break;
3451
+ case "sync": await syncCmd(args); break;
3452
+ case "upgrade": await upgrade(args); break;
3453
+ case "global-setup": await globalSetup(args); break;
3454
+ case "doctor": await doctor(); break;
3455
+ case "audit": await auditCmd(args); break;
3456
+ case "router": await routerCmd(args); break;
3457
+ case "secrets": await secretsCmd(args); break;
3458
+ case "who-owns": case "who": await whoOwnsCmd(args); break;
3459
+ case "init":
3460
+ case "update-init":
3461
+ case "init-update":
3462
+ await initCmd(args); break;
3463
+ case "--help": case "-h": case undefined:
3464
+ console.log(`
3465
+ Maestro — Autonomous AI Agent Operating System
3466
+
3467
+ Usage:
3468
+ npx @cohortapp/agent-sdk create <dirname> Create a new agent repo
3469
+ npx @cohortapp/agent-sdk setup [section] [flags] Configure this agent (deterministic, resumable wizard)
3470
+ npx @cohortapp/agent-sdk pair <code> Open a pre-auth pairing handshake (org admin approves it)
3471
+ npx @cohortapp/agent-sdk sync [--force] [--dry-run] Re-pull this agent's record from Cohort into config/agent.json
3472
+ npx @cohortapp/agent-sdk upgrade [--dry-run] Update framework files
3473
+ npx @cohortapp/agent-sdk init [feature] [--apply] Run pending feature-init steps
3474
+ npx @cohortapp/agent-sdk global-setup [--dry-run] Wire collective memory into ~/.claude (machine-wide)
3475
+ npx @cohortapp/agent-sdk doctor Verify installation
3476
+ npx @cohortapp/agent-sdk audit [--json] [--fix] Security-posture audit + hash-locked attestation
3477
+ npx @cohortapp/agent-sdk router why <item> Explain a routing decision (chain + tried + explain)
3478
+ npx @cohortapp/agent-sdk router validate Strict-validate config/model-routing.yaml
3479
+ npx @cohortapp/agent-sdk secrets list List local secret names (names only)
3480
+ npx @cohortapp/agent-sdk secrets sync [--name X] Pull org secrets from the broker into the local store
3481
+ npx @cohortapp/agent-sdk secrets rotate --name X Rotate a secret (new value read from stdin)
3482
+ npx @cohortapp/agent-sdk who-owns <scope> Who owns a scope / escalate-to / reports (org context, zero-LLM)
3483
+
3484
+ Upgrade flags:
3485
+ --dry-run, -n Preview changes without writing
3486
+ --force-overwrite Overwrite even locally-modified files (backs them up)
3487
+ --no-incoming Don't write .maestro/incoming/ shadows
3488
+ --verbose, -v List classification for every file
3489
+
3490
+ Init flags:
3491
+ (no args) List pending features (dry-run)
3492
+ --apply, -y Run every auto-runnable pending step
3493
+ <feature> --apply Run only the named feature's init step
3494
+
3495
+ Workflow:
3496
+ 1. npx @cohortapp/agent-sdk create my-agent
3497
+ 2. cd my-agent
3498
+ 3. maestro setup
3499
+
3500
+ Upgrade workflow (existing agents):
3501
+ 1. npm update @cohortapp/agent-sdk
3502
+ 2. npm run upgrade (runs maestro upgrade + auto-init)
3503
+ 3. maestro init (run any deferred init steps)
3504
+ `);
3505
+ break;
3506
+ default:
3507
+ fail(`Unknown command: ${command}`);
3508
+ process.exit(1);
3509
+ }
3510
+ }
3511
+
3512
+ // Dispatch only when invoked as the CLI entrypoint — importing for tests must
3513
+ // not run any command (process.argv would be the test runner's argv).
3514
+ if (import.meta.url === `file://${process.argv[1]}`) {
3515
+ await main();
3516
+ }