@cohortapp/agent-sdk 2.16.0 → 2.18.4

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 (529) hide show
  1. package/.claude/settings.json +18 -0
  2. package/.env.example +23 -7
  3. package/README.md +1 -0
  4. package/bin/maestro.mjs +62 -0
  5. package/docs/guides/billing-console-keys.md +60 -0
  6. package/docs/guides/front-door-session.md +54 -9
  7. package/docs/guides/mac-mini.md +20 -25
  8. package/docs/guides/poller-daemon-setup.md +4 -1
  9. package/docs/guides/setup-wizard.md +1 -1
  10. package/docs/runbooks/fleet-rollout.md +156 -0
  11. package/docs/runbooks/mac-mini-bootstrap.md +12 -14
  12. package/lib/action-executor.js +19 -3
  13. package/lib/budget-guard.mjs +279 -3
  14. package/lib/channels/base-adapter.mjs +3 -1
  15. package/lib/channels/contract.mjs +2 -1
  16. package/lib/channels/inbox-item.mjs +8 -0
  17. package/lib/claude-bin.mjs +5 -6
  18. package/lib/cli/doctor-checks.mjs +141 -10
  19. package/lib/cli/global-setup-extras.mjs +5 -1
  20. package/lib/cli/inbox.mjs +100 -15
  21. package/lib/cli/seat-auth.mjs +463 -0
  22. package/lib/cli/session.mjs +80 -12
  23. package/lib/collective/capture.mjs +8 -6
  24. package/lib/collective/global-config.mjs +63 -1
  25. package/lib/collective/presence.mjs +142 -5
  26. package/lib/comms/send-gate.mjs +559 -1
  27. package/lib/context/budget.mjs +327 -0
  28. package/lib/context/history-scope.mjs +138 -0
  29. package/lib/diagnostics/alerts.mjs +49 -0
  30. package/lib/diagnostics/cadence-output-freshness.mjs +288 -0
  31. package/lib/engine/agents/definitions.mjs +343 -0
  32. package/lib/engine/agents/persist.mjs +275 -0
  33. package/lib/engine/agents/runtime.mjs +748 -0
  34. package/lib/engine/agents/usage.mjs +95 -0
  35. package/lib/engine/auth-status.mjs +139 -0
  36. package/lib/engine/budget.mjs +194 -0
  37. package/lib/engine/cli.mjs +1204 -0
  38. package/lib/engine/commands/index.mjs +269 -0
  39. package/lib/engine/context/budget.mjs +219 -0
  40. package/lib/engine/context/cache.mjs +125 -0
  41. package/lib/engine/context/child-env.mjs +215 -0
  42. package/lib/engine/context/compaction.mjs +342 -0
  43. package/lib/engine/context/images.mjs +90 -0
  44. package/lib/engine/context/instructions.mjs +327 -0
  45. package/lib/engine/context/lazy-instructions.mjs +169 -0
  46. package/lib/engine/context/manager.mjs +182 -0
  47. package/lib/engine/context/real-path.mjs +91 -0
  48. package/lib/engine/context/secret-values.mjs +163 -0
  49. package/lib/engine/context/settings.mjs +274 -0
  50. package/lib/engine/context/stream-input.mjs +159 -0
  51. package/lib/engine/guard.mjs +152 -0
  52. package/lib/engine/hooks.mjs +713 -0
  53. package/lib/engine/loop.mjs +560 -0
  54. package/lib/engine/mcp/client.mjs +254 -0
  55. package/lib/engine/mcp/config.mjs +301 -0
  56. package/lib/engine/mcp/http.mjs +201 -0
  57. package/lib/engine/mcp/index.mjs +146 -0
  58. package/lib/engine/mcp/jsonrpc.mjs +147 -0
  59. package/lib/engine/mcp/naming.mjs +66 -0
  60. package/lib/engine/mcp/resources.mjs +89 -0
  61. package/lib/engine/mcp/results.mjs +133 -0
  62. package/lib/engine/mcp/stdio.mjs +137 -0
  63. package/lib/engine/mcp/supervisor.mjs +116 -0
  64. package/lib/engine/messages.mjs +104 -0
  65. package/lib/engine/output/json.mjs +164 -0
  66. package/lib/engine/output/stream-json.mjs +266 -0
  67. package/lib/engine/permissions.mjs +845 -0
  68. package/lib/engine/process-identity.mjs +164 -0
  69. package/lib/engine/process-tree.mjs +551 -0
  70. package/lib/engine/prompt.mjs +60 -0
  71. package/lib/engine/session/store.mjs +299 -0
  72. package/lib/engine/session-runtime/args.mjs +97 -0
  73. package/lib/engine/session-runtime/host.mjs +143 -0
  74. package/lib/engine/session-runtime/inbox.mjs +122 -0
  75. package/lib/engine/session-runtime/notifications.mjs +129 -0
  76. package/lib/engine/session-runtime/registry.mjs +328 -0
  77. package/lib/engine/session-runtime/runner.mjs +344 -0
  78. package/lib/engine/session-runtime/socket.mjs +212 -0
  79. package/lib/engine/session-runtime/wakeup.mjs +115 -0
  80. package/lib/engine/skills/index.mjs +321 -0
  81. package/lib/engine/tools/bash-background.mjs +533 -0
  82. package/lib/engine/tools/bash.mjs +216 -0
  83. package/lib/engine/tools/edit.mjs +97 -0
  84. package/lib/engine/tools/glob.mjs +81 -0
  85. package/lib/engine/tools/grep.mjs +224 -0
  86. package/lib/engine/tools/index.mjs +84 -0
  87. package/lib/engine/tools/list-agents.mjs +32 -0
  88. package/lib/engine/tools/ls.mjs +127 -0
  89. package/lib/engine/tools/monitor.mjs +82 -0
  90. package/lib/engine/tools/notebook-edit.mjs +218 -0
  91. package/lib/engine/tools/read.mjs +103 -0
  92. package/lib/engine/tools/schedule-wakeup.mjs +45 -0
  93. package/lib/engine/tools/schema.mjs +144 -0
  94. package/lib/engine/tools/send-message.mjs +77 -0
  95. package/lib/engine/tools/session.mjs +70 -0
  96. package/lib/engine/tools/todo.mjs +144 -0
  97. package/lib/engine/tools/toolsearch.mjs +217 -0
  98. package/lib/engine/tools/walk.mjs +193 -0
  99. package/lib/engine/tools/web-switch.mjs +31 -0
  100. package/lib/engine/tools/webfetch-html.mjs +387 -0
  101. package/lib/engine/tools/webfetch-net.mjs +340 -0
  102. package/lib/engine/tools/webfetch.mjs +198 -0
  103. package/lib/engine/tools/websearch.mjs +91 -0
  104. package/lib/engine/tools/workflow.mjs +95 -0
  105. package/lib/engine/tools/write.mjs +76 -0
  106. package/lib/engine/tui/line-editor.mjs +137 -0
  107. package/lib/engine/tui/render.mjs +86 -0
  108. package/lib/engine/tui/tui.mjs +274 -0
  109. package/lib/engine/wire/anthropic-messages.mjs +263 -0
  110. package/lib/engine/wire/effort.mjs +36 -0
  111. package/lib/engine/wire/errors.mjs +496 -0
  112. package/lib/engine/wire/http.mjs +441 -0
  113. package/lib/engine/wire/index.mjs +76 -0
  114. package/lib/engine/wire/openai-chat.mjs +332 -0
  115. package/lib/engine/wire/prompt-cache.mjs +79 -0
  116. package/lib/engine/wire/search.mjs +140 -0
  117. package/lib/engine/wire/sse.mjs +114 -0
  118. package/lib/engine/wire/stall.mjs +349 -0
  119. package/lib/engine/wire/token-provider.mjs +175 -0
  120. package/lib/engine/wire/usage.mjs +192 -0
  121. package/lib/engine/workflow/host.mjs +524 -0
  122. package/lib/engine/workflow/journal.mjs +188 -0
  123. package/lib/engine/workflow/json-schema.mjs +171 -0
  124. package/lib/engine/workflow/meta.mjs +329 -0
  125. package/lib/engine/workflow/notifications.mjs +52 -0
  126. package/lib/engine/workflow/runtime.mjs +447 -0
  127. package/lib/engine/workflow/sandbox.mjs +534 -0
  128. package/lib/engine/workflow/worker.mjs +141 -0
  129. package/lib/engine/workflow/worktree.mjs +74 -0
  130. package/lib/execution/disposition.mjs +1 -1
  131. package/lib/execution/intake.mjs +10 -0
  132. package/lib/execution/surface-policy.mjs +15 -0
  133. package/lib/learning/curator.mjs +8 -6
  134. package/lib/learning/reflect.mjs +8 -6
  135. package/lib/model-router/catalog/cohort.yaml +137 -0
  136. package/lib/model-router/catalog.mjs +118 -1
  137. package/lib/model-router/economics.mjs +9 -0
  138. package/lib/model-router/failover.mjs +67 -16
  139. package/lib/model-router/llm-task.mjs +39 -3
  140. package/lib/model-router/resolve.mjs +95 -3
  141. package/lib/model-router/spawn.mjs +46 -47
  142. package/lib/model-router/taxonomy.mjs +126 -4
  143. package/lib/org/cost-sync.mjs +141 -11
  144. package/lib/org/inbound/broadcast.mjs +289 -0
  145. package/lib/org/inbound/collective.mjs +375 -0
  146. package/lib/org/inbound/directedness.mjs +96 -8
  147. package/lib/org/inbound/facts.mjs +82 -4
  148. package/lib/org/inbound/hydrate.mjs +555 -51
  149. package/lib/org/inbound/project.mjs +22 -0
  150. package/lib/org/inbound/surfaces.mjs +14 -0
  151. package/lib/org/llm-token.mjs +879 -0
  152. package/lib/org/mesh.mjs +61 -0
  153. package/lib/org/messaging.mjs +3 -1
  154. package/lib/org/protocol.checksum +1 -1
  155. package/lib/org/protocol.mjs +15 -0
  156. package/lib/org/quota.mjs +520 -0
  157. package/lib/org/tool-surface.mjs +104 -16
  158. package/lib/org/ui-parity.mjs +16 -1
  159. package/lib/org/work-ledger.mjs +37 -6
  160. package/lib/rate-guard.mjs +114 -1
  161. package/lib/resource-governor.mjs +41 -6
  162. package/lib/runtime/adapter.mjs +823 -0
  163. package/lib/runtime/child-env.mjs +191 -0
  164. package/lib/runtime/legacy-shell-guard.mjs +97 -0
  165. package/lib/runtime/seat-engine.mjs +162 -0
  166. package/lib/session/ask-ledger.mjs +271 -0
  167. package/lib/session/current-work.mjs +676 -0
  168. package/lib/session/feed-core.mjs +40 -3
  169. package/lib/session/launch-args.mjs +56 -4
  170. package/lib/session/status-summary.mjs +26 -9
  171. package/lib/session/upgrade-notice.mjs +42 -0
  172. package/lib/setup/claude-probe.mjs +117 -13
  173. package/lib/setup/enrich.mjs +13 -10
  174. package/lib/setup/sections/model.mjs +39 -13
  175. package/lib/telemetry/collect.mjs +208 -9
  176. package/lib/upgrade/ignored-drift.mjs +105 -0
  177. package/lib/voice/post-call-brief.mjs +30 -17
  178. package/package.json +15 -3
  179. package/plugins/maestro-skills/skills/board-work.md +5 -0
  180. package/plugins/maestro-skills/skills/inbound-triage.md +56 -15
  181. package/plugins/maestro-skills/skills/main-session.md +18 -7
  182. package/scripts/ci/check-tarball-fidelity.mjs +126 -2
  183. package/scripts/ci/run-tests.mjs +47 -19
  184. package/scripts/cohort-llm/api-key-helper.mjs +92 -0
  185. package/scripts/collective/hook-runner.mjs +29 -2
  186. package/scripts/continuous-monitor.sh +13 -0
  187. package/scripts/cost/track-claude-usage.mjs +15 -0
  188. package/scripts/daemon/agent-daemon.mjs +408 -20
  189. package/scripts/daemon/assurance.mjs +48 -12
  190. package/scripts/daemon/cadence-consumer.mjs +218 -68
  191. package/scripts/daemon/cadence-handlers.mjs +73 -4
  192. package/scripts/daemon/classifier.mjs +75 -26
  193. package/scripts/daemon/context-compiler.mjs +104 -59
  194. package/scripts/daemon/deliver.mjs +30 -1
  195. package/scripts/daemon/dispatcher.mjs +804 -157
  196. package/scripts/daemon/health.mjs +14 -1
  197. package/scripts/daemon/lib/session-router.mjs +310 -42
  198. package/scripts/daemon/maestro-daemon.mjs +11 -0
  199. package/scripts/daemon/prompt-builder.mjs +121 -12
  200. package/scripts/daemon/responder.mjs +315 -146
  201. package/scripts/daemon/sdk-version.mjs +98 -16
  202. package/scripts/eval/probe-gateway.mjs +635 -0
  203. package/scripts/eval/replay/extract.mjs +270 -0
  204. package/scripts/eval/replay/grade.mjs +260 -0
  205. package/scripts/eval/replay/lib/config.mjs +50 -0
  206. package/scripts/eval/replay/lib/effects.mjs +65 -0
  207. package/scripts/eval/replay/lib/fixture.mjs +188 -0
  208. package/scripts/eval/replay/lib/judge.mjs +72 -0
  209. package/scripts/eval/replay/lib/redact.mjs +136 -0
  210. package/scripts/eval/replay/lib/sandbox.mjs +170 -0
  211. package/scripts/eval/replay/lib/schema-check.mjs +63 -0
  212. package/scripts/eval/replay/lib/transcript.mjs +76 -0
  213. package/scripts/eval/replay/mcp-replay-stub.mjs +101 -0
  214. package/scripts/eval/replay/report.mjs +185 -0
  215. package/scripts/eval/replay/run.mjs +404 -0
  216. package/scripts/fleet/rollout.mjs +1094 -0
  217. package/scripts/hooks/pre-send-audit.sh +36 -245
  218. package/scripts/hooks/pre-write-yaml-validate.mjs +275 -0
  219. package/scripts/hooks/validate-state-yaml.sh +190 -0
  220. package/scripts/huddle/huddle-llm.mjs +361 -0
  221. package/scripts/huddle/huddle-server.mjs +46 -121
  222. package/scripts/local-triggers/autoupdate.sh +448 -78
  223. package/scripts/local-triggers/run-trigger.sh +13 -0
  224. package/scripts/maintenance/pin-integrity.mjs +364 -0
  225. package/scripts/poll-slack-events.sh +41 -9
  226. package/scripts/poller/slack-socket-mode.mjs +28 -3
  227. package/scripts/session/supervisor.mjs +80 -13
  228. package/scripts/spawn-session.sh +13 -0
  229. package/bin/maestro.test.mjs +0 -1574
  230. package/lib/action-executor.test.mjs +0 -871
  231. package/lib/archetype.test.mjs +0 -132
  232. package/lib/assurance/plan-note.test.mjs +0 -234
  233. package/lib/assurance/room-budget.test.mjs +0 -486
  234. package/lib/assurance/tier.test.mjs +0 -174
  235. package/lib/autonomy.test.mjs +0 -66
  236. package/lib/backlog.test.mjs +0 -302
  237. package/lib/backup/policy.test.mjs +0 -305
  238. package/lib/budget-escalate.test.mjs +0 -232
  239. package/lib/budget-guard.envelope.test.mjs +0 -476
  240. package/lib/budget-guard.test.mjs +0 -427
  241. package/lib/cadence-bus-requeue.test.mjs +0 -83
  242. package/lib/cadence-bus-schedule.test.mjs +0 -194
  243. package/lib/cadence-bus.test.mjs +0 -720
  244. package/lib/cadences.test.mjs +0 -230
  245. package/lib/capability/inventory.test.mjs +0 -232
  246. package/lib/capability.test.mjs +0 -78
  247. package/lib/channels/base-adapter.test.mjs +0 -590
  248. package/lib/channels/channels.test.mjs +0 -371
  249. package/lib/channels/contract.test.mjs +0 -162
  250. package/lib/channels/inbox-item.test.mjs +0 -368
  251. package/lib/channels/orgmail/adapter.test.mjs +0 -448
  252. package/lib/channels/pairing.test.mjs +0 -270
  253. package/lib/channels/repeat-suppressor.test.mjs +0 -134
  254. package/lib/channels/slack-adapter.test.mjs +0 -212
  255. package/lib/channels/telegram-adapter.test.mjs +0 -306
  256. package/lib/channels/voice/adapter.test.mjs +0 -278
  257. package/lib/channels/whatsapp/adapter-baileys.test.mjs +0 -359
  258. package/lib/channels/whatsapp/baileys-typing.test.mjs +0 -154
  259. package/lib/charter.test.mjs +0 -89
  260. package/lib/claude-bin.test.mjs +0 -131
  261. package/lib/cli/board.test.mjs +0 -227
  262. package/lib/cli/design.test.mjs +0 -270
  263. package/lib/cli/doctor-checks.test.mjs +0 -336
  264. package/lib/cli/global-setup-extras.test.mjs +0 -462
  265. package/lib/cli/inbox.test.mjs +0 -230
  266. package/lib/cli/session-ack.test.mjs +0 -63
  267. package/lib/cli/session.test.mjs +0 -613
  268. package/lib/collective/capture.test.mjs +0 -121
  269. package/lib/collective/cards.test.mjs +0 -114
  270. package/lib/collective/config.test.mjs +0 -123
  271. package/lib/collective/global-config.test.mjs +0 -220
  272. package/lib/collective/global-skills.test.mjs +0 -126
  273. package/lib/collective/presence.test.mjs +0 -95
  274. package/lib/collective/recall.test.mjs +0 -116
  275. package/lib/collective/vendor-skills.test.mjs +0 -306
  276. package/lib/comms/send-gate.test.mjs +0 -770
  277. package/lib/comms.test.mjs +0 -41
  278. package/lib/cost/ledger-row.test.mjs +0 -183
  279. package/lib/design/design-md.test.mjs +0 -318
  280. package/lib/design/fixtures/DESIGN.golden.md +0 -238
  281. package/lib/design/fixtures/PRODUCT.golden.md +0 -67
  282. package/lib/design/fixtures/foundation.json +0 -133
  283. package/lib/design/refresh-gate.test.mjs +0 -144
  284. package/lib/design/write.test.mjs +0 -241
  285. package/lib/diagnostics/alerts.test.mjs +0 -318
  286. package/lib/diagnostics/backup-freshness.test.mjs +0 -185
  287. package/lib/diagnostics/counters.test.mjs +0 -206
  288. package/lib/diagnostics/events.test.mjs +0 -290
  289. package/lib/diagnostics/otel.test.mjs +0 -196
  290. package/lib/diagnostics/trace.test.mjs +0 -251
  291. package/lib/env-compat.test.mjs +0 -104
  292. package/lib/execution/disposition.test.mjs +0 -553
  293. package/lib/execution/drive.test.mjs +0 -270
  294. package/lib/execution/effects.test.mjs +0 -344
  295. package/lib/execution/intake.test.mjs +0 -389
  296. package/lib/execution/journal.test.mjs +0 -261
  297. package/lib/execution/match.test.mjs +0 -235
  298. package/lib/execution/pipeline.test.mjs +0 -392
  299. package/lib/execution/route.test.mjs +0 -186
  300. package/lib/execution/surface-policy.test.mjs +0 -162
  301. package/lib/fs-atomic.test.mjs +0 -72
  302. package/lib/fs-ownership.test.mjs +0 -158
  303. package/lib/goals/admission.test.mjs +0 -164
  304. package/lib/goals/classify.test.mjs +0 -167
  305. package/lib/goals/collaborate.test.mjs +0 -336
  306. package/lib/goals/gaps.test.mjs +0 -284
  307. package/lib/goals/loop.test.mjs +0 -845
  308. package/lib/hooks/bus.test.mjs +0 -387
  309. package/lib/identity/persona.test.mjs +0 -142
  310. package/lib/kpi-sensors.test.mjs +0 -278
  311. package/lib/kpi.test.mjs +0 -244
  312. package/lib/learning/config.test.mjs +0 -75
  313. package/lib/learning/counters.test.mjs +0 -69
  314. package/lib/learning/curator-consolidate.test.mjs +0 -238
  315. package/lib/learning/curator.test.mjs +0 -106
  316. package/lib/learning/reflect.test.mjs +0 -0
  317. package/lib/learning/session-index.test.mjs +0 -125
  318. package/lib/learning/skill-writer.test.mjs +0 -210
  319. package/lib/mandate/audit.test.mjs +0 -195
  320. package/lib/mandate/contract.test.mjs +0 -185
  321. package/lib/mandate/derive.test.mjs +0 -274
  322. package/lib/mandate/model.test.mjs +0 -164
  323. package/lib/mandate/refresh.test.mjs +0 -389
  324. package/lib/mcp/server.test.mjs +0 -426
  325. package/lib/model-router/auth-profiles.test.mjs +0 -580
  326. package/lib/model-router/catalog.test.mjs +0 -385
  327. package/lib/model-router/economics.test.mjs +0 -438
  328. package/lib/model-router/failover.test.mjs +0 -439
  329. package/lib/model-router/health.test.mjs +0 -338
  330. package/lib/model-router/integration-coverage.test.mjs +0 -831
  331. package/lib/model-router/integration.test.mjs +0 -564
  332. package/lib/model-router/ledger.test.mjs +0 -415
  333. package/lib/model-router/llm-task.test.mjs +0 -392
  334. package/lib/model-router/org-credentials.test.mjs +0 -265
  335. package/lib/model-router/pricing-refresh.test.mjs +0 -286
  336. package/lib/model-router/reconcile.test.mjs +0 -316
  337. package/lib/model-router/repair.test.mjs +0 -180
  338. package/lib/model-router/spawn.test.mjs +0 -446
  339. package/lib/model-router/taxonomy.test.mjs +0 -410
  340. package/lib/model-router.test.mjs +0 -1207
  341. package/lib/org/activity.test.mjs +0 -134
  342. package/lib/org/approvals.test.mjs +0 -216
  343. package/lib/org/awareness.test.mjs +0 -159
  344. package/lib/org/board-mine-cache.test.mjs +0 -53
  345. package/lib/org/board.test.mjs +0 -187
  346. package/lib/org/bootstrap-context.test.mjs +0 -153
  347. package/lib/org/client.test.mjs +0 -1206
  348. package/lib/org/cohort-client.test.mjs +0 -126
  349. package/lib/org/cost-sync.test.mjs +0 -153
  350. package/lib/org/doctor.test.mjs +0 -346
  351. package/lib/org/engagement-ledger.test.mjs +0 -112
  352. package/lib/org/engagement.test.mjs +0 -739
  353. package/lib/org/handoff.test.mjs +0 -269
  354. package/lib/org/inbound/directedness.test.mjs +0 -668
  355. package/lib/org/inbound/facts.test.mjs +0 -471
  356. package/lib/org/inbound/hydrate.test.mjs +0 -453
  357. package/lib/org/inbound/index.test.mjs +0 -429
  358. package/lib/org/inbound/project.test.mjs +0 -287
  359. package/lib/org/integration-tools.test.mjs +0 -160
  360. package/lib/org/keys.test.mjs +0 -92
  361. package/lib/org/knowledge.test.mjs +0 -326
  362. package/lib/org/leases.test.mjs +0 -235
  363. package/lib/org/mesh-directives.test.mjs +0 -110
  364. package/lib/org/mesh-integration.test.mjs +0 -127
  365. package/lib/org/mesh.test.mjs +0 -400
  366. package/lib/org/messaging.test.mjs +0 -471
  367. package/lib/org/param-contract.test.mjs +0 -477
  368. package/lib/org/policy.test.mjs +0 -237
  369. package/lib/org/protocol.checksum.test.mjs +0 -90
  370. package/lib/org/protocol.test.mjs +0 -323
  371. package/lib/org/push.test.mjs +0 -792
  372. package/lib/org/registry.test.mjs +0 -100
  373. package/lib/org/resource-tools.test.mjs +0 -361
  374. package/lib/org/tool-access.test.mjs +0 -144
  375. package/lib/org/tool-surface-integration.test.mjs +0 -120
  376. package/lib/org/tool-surface.test.mjs +0 -1268
  377. package/lib/org/typing.test.mjs +0 -291
  378. package/lib/org/ui-parity.test.mjs +0 -560
  379. package/lib/org/verify.test.mjs +0 -194
  380. package/lib/org/work-ledger.test.mjs +0 -273
  381. package/lib/plan/adoption-e2e.test.mjs +0 -366
  382. package/lib/plan/budget-enforcement.test.mjs +0 -400
  383. package/lib/plan/compile.test.mjs +0 -382
  384. package/lib/plan/emit.test.mjs +0 -269
  385. package/lib/plan/explain.test.mjs +0 -188
  386. package/lib/prompts/parallelism.test.mjs +0 -177
  387. package/lib/rag/rag.test.mjs +0 -505
  388. package/lib/rate-guard.test.mjs +0 -272
  389. package/lib/reactive-gate.test.mjs +0 -57
  390. package/lib/render.test.mjs +0 -68
  391. package/lib/resource-governor.test.mjs +0 -488
  392. package/lib/scheduling/dynamic-jobs.test.mjs +0 -344
  393. package/lib/scheduling/jitter.test.mjs +0 -140
  394. package/lib/secrets/broker.test.mjs +0 -280
  395. package/lib/secrets/providers.test.mjs +0 -274
  396. package/lib/security/audit-engine.test.mjs +0 -424
  397. package/lib/security/coerce-args.test.mjs +0 -281
  398. package/lib/security/dangerous-tools.test.mjs +0 -68
  399. package/lib/security/external-content.test.mjs +0 -84
  400. package/lib/security/redact.test.mjs +0 -441
  401. package/lib/security/secret-equal.test.mjs +0 -55
  402. package/lib/session/config.test.mjs +0 -92
  403. package/lib/session/feed-core.test.mjs +0 -198
  404. package/lib/session/first-run.test.mjs +0 -121
  405. package/lib/session/frontdoor.test.mjs +0 -205
  406. package/lib/session/handoffs.test.mjs +0 -183
  407. package/lib/session/identity.test.mjs +0 -180
  408. package/lib/session/inbox-claims.test.mjs +0 -286
  409. package/lib/session/launch-args.test.mjs +0 -157
  410. package/lib/session/liveness.test.mjs +0 -100
  411. package/lib/session/status-summary.test.mjs +0 -118
  412. package/lib/session-permissions.test.mjs +0 -120
  413. package/lib/setup/claude-probe.test.mjs +0 -187
  414. package/lib/setup/completeness.test.mjs +0 -110
  415. package/lib/setup/context-pack.test.mjs +0 -89
  416. package/lib/setup/enrich.test.mjs +0 -115
  417. package/lib/setup/enroll-from-cohort.test.mjs +0 -300
  418. package/lib/setup/integration.test.mjs +0 -162
  419. package/lib/setup/io.test.mjs +0 -77
  420. package/lib/setup/runner.test.mjs +0 -132
  421. package/lib/setup/sections/identity.test.mjs +0 -234
  422. package/lib/setup/sections/inventory.test.mjs +0 -198
  423. package/lib/setup/sections/learning.test.mjs +0 -81
  424. package/lib/setup/sections/mandate.test.mjs +0 -388
  425. package/lib/setup/sections/messaging.test.mjs +0 -127
  426. package/lib/setup/sections/model.test.mjs +0 -240
  427. package/lib/setup/sections/org.test.mjs +0 -346
  428. package/lib/setup/sections/orgmail.test.mjs +0 -118
  429. package/lib/setup/sections/recovery.test.mjs +0 -98
  430. package/lib/setup/sections/subagents.test.mjs +0 -429
  431. package/lib/setup/sections/verify.test.mjs +0 -175
  432. package/lib/setup/sot.test.mjs +0 -81
  433. package/lib/setup/state.test.mjs +0 -115
  434. package/lib/singleton.test.mjs +0 -151
  435. package/lib/subagents/cli.test.mjs +0 -389
  436. package/lib/subagents/client.test.mjs +0 -309
  437. package/lib/subagents/gap.test.mjs +0 -234
  438. package/lib/subagents/lock.test.mjs +0 -248
  439. package/lib/subagents/manifest.test.mjs +0 -175
  440. package/lib/subagents/refs.test.mjs +0 -204
  441. package/lib/subagents/resolve.test.mjs +0 -422
  442. package/lib/subagents/schema.test.mjs +0 -328
  443. package/lib/telemetry/alerts.test.mjs +0 -109
  444. package/lib/telemetry/collect.test.mjs +0 -1274
  445. package/lib/tool-definitions-integration.test.mjs +0 -83
  446. package/lib/tool-definitions.test.mjs +0 -437
  447. package/lib/upgrade/global-refresh.test.mjs +0 -65
  448. package/lib/upgrade/launchd-reconcile.test.mjs +0 -272
  449. package/lib/upgrade/post-steps.test.mjs +0 -200
  450. package/lib/upgrade/verify.test.mjs +0 -164
  451. package/lib/util/fetch-timeout.test.mjs +0 -202
  452. package/lib/util/reconnect.test.mjs +0 -369
  453. package/lib/util/unhandled.test.mjs +0 -216
  454. package/lib/voice/outbound.test.mjs +0 -69
  455. package/lib/voice/session-rotation.test.mjs +0 -114
  456. package/lib/voice/stt.test.mjs +0 -226
  457. package/lib/voice/voice.test.mjs +0 -990
  458. package/scripts/cadence/enqueue-cadence-tick.test.mjs +0 -187
  459. package/scripts/ci/check-docs-accuracy.test.mjs +0 -409
  460. package/scripts/ci/check-durable-write-seam.test.mjs +0 -90
  461. package/scripts/ci/check-no-build-artifacts.test.mjs +0 -71
  462. package/scripts/ci/check-no-residual-identity.test.mjs +0 -202
  463. package/scripts/ci/check-skill-packs.test.mjs +0 -495
  464. package/scripts/ci/check-subagent-frontmatter.test.mjs +0 -124
  465. package/scripts/ci/check.test.mjs +0 -194
  466. package/scripts/ci/conformance-org-api.test.mjs +0 -425
  467. package/scripts/cloud-relay/voice/relay-identity.test.mjs +0 -96
  468. package/scripts/collective/hook-runner.test.mjs +0 -173
  469. package/scripts/cost/fleet-digest.test.mjs +0 -207
  470. package/scripts/cost/track-claude-usage-pricing.test.mjs +0 -183
  471. package/scripts/cost/track-claude-usage.test.mjs +0 -148
  472. package/scripts/daemon/agent-daemon-board-mine.test.mjs +0 -96
  473. package/scripts/daemon/agent-daemon-design.test.mjs +0 -238
  474. package/scripts/daemon/agent-daemon-frontdoor.test.mjs +0 -60
  475. package/scripts/daemon/agent-daemon.test.mjs +0 -995
  476. package/scripts/daemon/assurance-e2e.test.mjs +0 -613
  477. package/scripts/daemon/assurance.test.mjs +0 -1791
  478. package/scripts/daemon/board-mirror.test.mjs +0 -165
  479. package/scripts/daemon/cadence-consumer-frontdoor.test.mjs +0 -393
  480. package/scripts/daemon/cadence-consumer-governance.test.mjs +0 -276
  481. package/scripts/daemon/cadence-consumer.test.mjs +0 -776
  482. package/scripts/daemon/cadence-handlers.test.mjs +0 -837
  483. package/scripts/daemon/classifier-identity.test.mjs +0 -137
  484. package/scripts/daemon/classifier.test.mjs +0 -266
  485. package/scripts/daemon/classify-kind.test.mjs +0 -40
  486. package/scripts/daemon/context-compiler.test.mjs +0 -300
  487. package/scripts/daemon/deliver.test.mjs +0 -564
  488. package/scripts/daemon/dispatcher-cooldown.test.mjs +0 -122
  489. package/scripts/daemon/dispatcher-governance.test.mjs +0 -1013
  490. package/scripts/daemon/dispatcher-resume.test.mjs +0 -166
  491. package/scripts/daemon/execution-ladder.test.mjs +0 -470
  492. package/scripts/daemon/goal-steward-cadence.test.mjs +0 -312
  493. package/scripts/daemon/inbox-deferral-session.test.mjs +0 -49
  494. package/scripts/daemon/inbox-deferral.test.mjs +0 -336
  495. package/scripts/daemon/inbox-wake.test.mjs +0 -199
  496. package/scripts/daemon/integration.test.mjs +0 -149
  497. package/scripts/daemon/lib/self-echo.test.mjs +0 -153
  498. package/scripts/daemon/lib/session-router.test.mjs +0 -295
  499. package/scripts/daemon/prompt-builder-preamble.test.mjs +0 -210
  500. package/scripts/daemon/prompt-builder.test.mjs +0 -344
  501. package/scripts/daemon/responder-cost.test.mjs +0 -68
  502. package/scripts/daemon/responder-history.test.mjs +0 -185
  503. package/scripts/daemon/sdk-version.test.mjs +0 -31
  504. package/scripts/daemon/session-lock.test.mjs +0 -252
  505. package/scripts/daemon/session-outcomes.test.mjs +0 -533
  506. package/scripts/daemon/typing-registry.test.mjs +0 -102
  507. package/scripts/hooks/pre-send-audit.test.mjs +0 -354
  508. package/scripts/huddle/huddle-prompt.test.mjs +0 -176
  509. package/scripts/local-triggers/autoupdate.test.mjs +0 -518
  510. package/scripts/local-triggers/generate-plists.test.mjs +0 -456
  511. package/scripts/media-generation/brand-clause.test.mjs +0 -135
  512. package/scripts/org/send-orgmail.first-contact.test.mjs +0 -102
  513. package/scripts/poller/inbox-privilege-injection.test.mjs +0 -167
  514. package/scripts/poller/inbox-scan-poller.test.mjs +0 -295
  515. package/scripts/poller/lib/cloud-relay-dedup.test.mjs +0 -133
  516. package/scripts/poller/slack-socket-mode.test.mjs +0 -805
  517. package/scripts/poller-launchd/install.test.mjs +0 -243
  518. package/scripts/restore-from-backup.test.mjs +0 -181
  519. package/scripts/session/feed.test.mjs +0 -196
  520. package/scripts/session/supervisor-sh.test.mjs +0 -218
  521. package/scripts/session/supervisor.test.mjs +0 -482
  522. package/scripts/setup/configure-macos.test.mjs +0 -306
  523. package/scripts/setup/gen-subagent-manifest.test.mjs +0 -124
  524. package/scripts/setup/generate-agent-package-json.test.mjs +0 -143
  525. package/scripts/setup/generate-capability.test.mjs +0 -134
  526. package/scripts/setup/init-agent.test.mjs +0 -370
  527. package/scripts/setup/init-skill-marketplace.test.mjs +0 -193
  528. package/scripts/vendor/sync-skill-packs.test.mjs +0 -103
  529. package/scripts/watchdog/memory-watchdog.test.mjs +0 -64
@@ -1,1574 +0,0 @@
1
- /**
2
- * maestro.test.mjs — CLI tests for the bin/maestro.mjs commands.
3
- *
4
- * Covers:
5
- * • `create` scaffolds the cadence-bus directory tree, the enqueue
6
- * script, the consumer, and the cadence handlers under the new agent.
7
- * • `upgrade` migrates a legacy-style agent repo: backs up old plists,
8
- * bootstraps state/cadence-bus/, regenerates plists.
9
- * • `upgrade` preserves agent-specific files (CLAUDE.md, config/,
10
- * knowledge/, memory/, state/, logs/, outputs/, .env) untouched.
11
- * • `upgrade` is idempotent when run twice on the same repo.
12
- * • `doctor` flags legacy plists with actionable remediation.
13
- *
14
- * Note: `create` runs `npm install` at the end, which is heavyweight. To
15
- * keep test runtime reasonable we skip the npm install step by passing
16
- * an alternate npm shim on PATH that is a no-op. Same trick for git.
17
- */
18
-
19
- import { test } from "node:test";
20
- import assert from "node:assert/strict";
21
- import { promises as fsp } from "fs";
22
- import { writeFileSync, readFileSync, existsSync, mkdirSync, readdirSync } from "node:fs";
23
- import { tmpdir } from "os";
24
- import { join, resolve, dirname } from "path";
25
- import { fileURLToPath } from "node:url";
26
- import { spawnSync, execFileSync } from "node:child_process";
27
- import { chmodSync, statSync } from "node:fs";
28
-
29
- const __dirname = dirname(fileURLToPath(import.meta.url));
30
- const MAESTRO_ROOT = resolve(__dirname, "..");
31
- const MAESTRO_CLI = resolve(__dirname, "maestro.mjs");
32
-
33
- // Pure predicates exported from the CLI module (importing does not dispatch).
34
- const { evaluateCostTripwire, summariseLedgerRows, evaluateStaleness } = await import(MAESTRO_CLI);
35
-
36
- // ---------------------------------------------------------------------------
37
- // Helpers
38
- // ---------------------------------------------------------------------------
39
-
40
- async function tmpRoot(prefix = "maestro-cli-test") {
41
- const path = join(
42
- tmpdir(),
43
- `${prefix}-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
44
- );
45
- await fsp.mkdir(path, { recursive: true });
46
- return path;
47
- }
48
-
49
- async function rmRoot(path) {
50
- try { await fsp.rm(path, { recursive: true, force: true }); } catch { /* */ }
51
- }
52
-
53
- // ---------------------------------------------------------------------------
54
- // The machine the CLI must NEVER touch from a test: the developer's real
55
- // ~/Library/LaunchAgents, ~/.claude and global npm. `upgrade` now reconciles
56
- // all three (WP-M6), so every CLI run gets a throwaway HOME, a `launchctl`
57
- // stub that treats "installed" as "loaded" (in `list` AND in `print gui/<uid>`,
58
- // which is what the reconcile reads), an `npm` stub, a `claude` stub (logs and
59
- // exits 1 → global-setup's JSON fallback) and MAESTRO_SKIP_GLOBAL_INSTALL=1.
60
- // The claude stub is reached through CLAUDE_BIN, not PATH: lib/claude-bin.mjs
61
- // resolves ~/.local/bin, /opt/homebrew/bin, /usr/local/bin BEFORE PATH, so a
62
- // PATH-only stub is silently bypassed by the developer's real `claude`.
63
- // ---------------------------------------------------------------------------
64
-
65
- const TEST_SHIM = await tmpRoot("maestro-shim-global");
66
- {
67
- const stub = async (name, body) => { writeFileSync(join(TEST_SHIM, name), `#!/bin/bash\n${body}\n`); await fsp.chmod(join(TEST_SHIM, name), 0o755); };
68
- await stub("launchctl", `echo "launchctl $*" >> "$HOME/launchctl.log"
69
- if [ "$1" = "list" ]; then
70
- if [ -n "\${2:-}" ]; then [ -f "$HOME/Library/LaunchAgents/$2.plist" ] && exit 0 || exit 113; fi
71
- printf 'PID\tStatus\tLabel\n'
72
- for f in "$HOME"/Library/LaunchAgents/*.plist; do [ -f "$f" ] && printf -- '-\t0\t%s\n' "$(basename "$f" .plist)"; done
73
- exit 0
74
- fi
75
- if [ "$1" = "print" ]; then
76
- case "$2" in
77
- gui/*/*) l="\${2##*/}"; if [ -f "$HOME/Library/LaunchAgents/$l.plist" ]; then echo "$l = {}"; exit 0; fi; echo "Could not find service \"$l\" in domain for user gui: 501" >&2; exit 113 ;;
78
- gui/*) printf '%s = {\n\tservices = {\n' "$2"; for f in "$HOME"/Library/LaunchAgents/*.plist; do [ -f "$f" ] && printf '\t\t0\t0\t%s\n' "$(basename "$f" .plist)"; done; printf '\t}\n}\n'; exit 0 ;;
79
- esac
80
- fi
81
- exit 0`);
82
- await stub("npm", `echo "npm $*" >> "$HOME/npm.log"
83
- case "$1" in
84
- ls) echo '{}'; exit 1 ;;
85
- view) exit 1 ;;
86
- root) echo "$HOME/npm-global/lib/node_modules"; exit 0 ;;
87
- esac
88
- exit 0`);
89
- await stub("claude", `echo "claude $*" >> "$HOME/claude.log"
90
- exit 1`);
91
- }
92
-
93
- /** A throwaway HOME for one CLI run (or a shared one when the test does not care). */
94
- async function freshHome() {
95
- const home = await tmpRoot("maestro-home");
96
- mkdirSync(join(home, "Library", "LaunchAgents"), { recursive: true });
97
- return home;
98
- }
99
- const TEST_HOME = await freshHome();
100
-
101
- function baseEnv(extra = {}) {
102
- return {
103
- ...process.env,
104
- HOME: TEST_HOME,
105
- PATH: `${TEST_SHIM}:${process.env.PATH || ""}`,
106
- CLAUDE_BIN: join(TEST_SHIM, "claude"),
107
- MAESTRO_SKIP_GLOBAL_INSTALL: "1",
108
- ...extra,
109
- };
110
- }
111
-
112
- /**
113
- * Build a PATH that shadows `npm` (and optionally `git`) with no-op shims
114
- * so `create` doesn't actually try to install dependencies. Returns the new
115
- * env block plus the shim dir (for cleanup).
116
- */
117
- async function withShim() {
118
- const dir = await tmpRoot("maestro-shim");
119
- const npm = join(dir, "npm");
120
- writeFileSync(npm, "#!/bin/bash\necho '[shim npm]' \"$@\" >&2\nexit 0\n");
121
- await fsp.chmod(npm, 0o755);
122
- // Keep real git (we want git init to actually init).
123
- const env = baseEnv({ PATH: `${dir}:${TEST_SHIM}:${process.env.PATH || ""}` });
124
- return { env, dir };
125
- }
126
-
127
- function runCli(args, cwd, env) {
128
- return spawnSync(process.execPath, [MAESTRO_CLI, ...args], {
129
- cwd: cwd || process.cwd(),
130
- env: env || baseEnv(),
131
- encoding: "utf-8",
132
- });
133
- }
134
-
135
- /** Like runCli but pipes `input` to the child's stdin (for `secrets rotate`). */
136
- function runCliStdin(args, cwd, input, env) {
137
- return spawnSync(process.execPath, [MAESTRO_CLI, ...args], {
138
- cwd: cwd || process.cwd(),
139
- env: env || baseEnv(),
140
- input,
141
- encoding: "utf-8",
142
- });
143
- }
144
-
145
- // ---------------------------------------------------------------------------
146
- // create
147
- // ---------------------------------------------------------------------------
148
-
149
- test("create scaffolds cadence-bus dirs and enqueue/consumer/handlers", async () => {
150
- const parent = await tmpRoot("maestro-create");
151
- const { env, dir: shim } = await withShim();
152
- try {
153
- const r = runCli(["create", "test-agent"], parent, env);
154
- assert.equal(r.status, 0, r.stderr || r.stdout);
155
- const agent = join(parent, "test-agent");
156
-
157
- // Cadence bus directory tree
158
- for (const d of [
159
- "state/cadence-bus",
160
- "state/cadence-bus/inbox",
161
- "state/cadence-bus/claimed",
162
- "state/cadence-bus/processed",
163
- "state/cadence-bus/failed",
164
- "state/cadence-bus/dlq",
165
- "logs/cadence-bus",
166
- ]) {
167
- assert.ok(existsSync(join(agent, d)), `missing ${d}`);
168
- }
169
-
170
- // New framework files copied into the agent repo.
171
- for (const f of [
172
- "lib/cadence-bus.mjs",
173
- "scripts/cadence/enqueue-cadence-tick.mjs",
174
- "scripts/cadence/launchd-cadence-wrapper.sh",
175
- "scripts/cadence/cadence-status.mjs",
176
- "scripts/daemon/cadence-consumer.mjs",
177
- "scripts/daemon/cadence-handlers.mjs",
178
- ]) {
179
- assert.ok(existsSync(join(agent, f)), `missing ${f}`);
180
- }
181
-
182
- // Agent package.json has the cadence:* scripts.
183
- const pkg = JSON.parse(readFileSync(join(agent, "package.json"), "utf-8"));
184
- assert.ok(pkg.scripts["cadence:enqueue"]);
185
- assert.ok(pkg.scripts["cadence:consume"]);
186
- assert.ok(pkg.scripts["cadence:status"]);
187
- } finally {
188
- await rmRoot(parent);
189
- await rmRoot(shim);
190
- }
191
- });
192
-
193
- // ---------------------------------------------------------------------------
194
- // upgrade — migration
195
- // ---------------------------------------------------------------------------
196
-
197
- async function makeLegacyAgent() {
198
- const root = await tmpRoot("maestro-legacy");
199
- // Minimal identity so `upgrade` recognises this as a Maestro repo.
200
- mkdirSync(join(root, "config"), { recursive: true });
201
- writeFileSync(join(root, "CLAUDE.md"), "# legacy agent\n");
202
- writeFileSync(join(root, "config/agent.ts"), "export const AGENT = { firstName: 'legacy' };\n");
203
- writeFileSync(join(root, "package.json"), '{"name":"legacy-agent","version":"1.0.0"}\n');
204
- // Personal/local agent state that must NOT be touched.
205
- mkdirSync(join(root, "knowledge/decisions"), { recursive: true });
206
- mkdirSync(join(root, "memory/interactions"), { recursive: true });
207
- mkdirSync(join(root, "state/queues"), { recursive: true });
208
- mkdirSync(join(root, "outputs"), { recursive: true });
209
- mkdirSync(join(root, "logs"), { recursive: true });
210
- writeFileSync(join(root, "knowledge/decisions/local-decision.md"), "# do not touch");
211
- writeFileSync(join(root, "memory/interactions/local-memory.yaml"), "# do not touch");
212
- writeFileSync(join(root, "state/queues/action-stack.yaml"), "items:\n - id: P1\n title: local item\n");
213
- writeFileSync(join(root, "outputs/local-output.md"), "# my draft");
214
- writeFileSync(join(root, "logs/local.log"), "audit data");
215
- writeFileSync(join(root, ".env"), "ANTHROPIC_API_KEY=sk-x\n");
216
-
217
- // Legacy plist that calls run-trigger.sh directly.
218
- mkdirSync(join(root, "scripts/local-triggers/plists"), { recursive: true });
219
- writeFileSync(
220
- join(root, "scripts/local-triggers/plists/ai.adaptic.legacy-inbox-processor.plist"),
221
- `<?xml version="1.0"?><plist><dict>
222
- <key>ProgramArguments</key><array>
223
- <string>/foo/scripts/local-triggers/run-trigger.sh</string>
224
- <string>inbox-processor</string>
225
- </array></dict></plist>`
226
- );
227
-
228
- // Init git so smart-merge sees the working tree as clean.
229
- execFileSync("git", ["init", "-q"], { cwd: root });
230
- execFileSync("git", ["add", "-A"], { cwd: root });
231
- execFileSync(
232
- "git",
233
- ["-c", "user.email=test@test", "-c", "user.name=test", "commit", "-q", "-m", "init"],
234
- { cwd: root },
235
- );
236
- return root;
237
- }
238
-
239
- test("upgrade backs up legacy plists and regenerates with cadence-bus marker", async () => {
240
- const root = await makeLegacyAgent();
241
- try {
242
- const r = runCli(["upgrade"], root);
243
- assert.equal(r.status, 0, r.stderr);
244
- // Backup directory exists.
245
- const backupRoot = join(root, ".maestro/backup/plists");
246
- assert.ok(existsSync(backupRoot), "backup root must exist");
247
- const stamps = readdirSync(backupRoot);
248
- assert.ok(stamps.length >= 1);
249
- const backedUp = readdirSync(join(backupRoot, stamps[0]));
250
- assert.ok(backedUp.some((n) => /legacy-inbox-processor\.plist$/.test(n)));
251
-
252
- // After regeneration, plists carry the cadence-bus marker and none
253
- // reference run-trigger.sh.
254
- const plistDir = join(root, "scripts/local-triggers/plists");
255
- const plists = readdirSync(plistDir).filter((n) => n.endsWith(".plist"));
256
- assert.ok(plists.length >= 1);
257
- let legacy = 0;
258
- let modern = 0;
259
- for (const name of plists) {
260
- const body = readFileSync(join(plistDir, name), "utf-8");
261
- if (body.includes("run-trigger.sh")) legacy++;
262
- if (body.includes("maestro-plist-arch: cadence-bus")) modern++;
263
- }
264
- assert.equal(legacy, 0, "no legacy plists should remain post-upgrade");
265
- assert.equal(modern, plists.length, "every plist must carry the cadence-bus marker");
266
- } finally { await rmRoot(root); }
267
- });
268
-
269
- test("upgrade preserves agent-specific files untouched", async () => {
270
- const root = await makeLegacyAgent();
271
- try {
272
- const before = {
273
- claudemd: readFileSync(join(root, "CLAUDE.md"), "utf-8"),
274
- agentTs: readFileSync(join(root, "config/agent.ts"), "utf-8"),
275
- decision: readFileSync(join(root, "knowledge/decisions/local-decision.md"), "utf-8"),
276
- memory: readFileSync(join(root, "memory/interactions/local-memory.yaml"), "utf-8"),
277
- queue: readFileSync(join(root, "state/queues/action-stack.yaml"), "utf-8"),
278
- output: readFileSync(join(root, "outputs/local-output.md"), "utf-8"),
279
- log: readFileSync(join(root, "logs/local.log"), "utf-8"),
280
- env: readFileSync(join(root, ".env"), "utf-8"),
281
- };
282
- const r = runCli(["upgrade"], root);
283
- assert.equal(r.status, 0, r.stderr);
284
- for (const [k, v] of Object.entries(before)) {
285
- const path = ({
286
- claudemd: "CLAUDE.md",
287
- agentTs: "config/agent.ts",
288
- decision: "knowledge/decisions/local-decision.md",
289
- memory: "memory/interactions/local-memory.yaml",
290
- queue: "state/queues/action-stack.yaml",
291
- output: "outputs/local-output.md",
292
- log: "logs/local.log",
293
- env: ".env",
294
- })[k];
295
- assert.equal(
296
- readFileSync(join(root, path), "utf-8"),
297
- v,
298
- `${path} was modified by upgrade`,
299
- );
300
- }
301
- } finally { await rmRoot(root); }
302
- });
303
-
304
- test("upgrade is idempotent (second run is a clean no-op for plists)", async () => {
305
- const root = await makeLegacyAgent();
306
- try {
307
- const first = runCli(["upgrade"], root);
308
- assert.equal(first.status, 0);
309
- // Snapshot the regenerated plist contents.
310
- const plistDir = join(root, "scripts/local-triggers/plists");
311
- const before = {};
312
- for (const name of readdirSync(plistDir)) {
313
- before[name] = readFileSync(join(plistDir, name), "utf-8");
314
- }
315
- const second = runCli(["upgrade"], root);
316
- assert.equal(second.status, 0);
317
- // The second run should NOT introduce any new backup directory beyond
318
- // what the first run created, because there are no legacy plists left.
319
- const backups = readdirSync(join(root, ".maestro/backup/plists"));
320
- assert.equal(backups.length, 1, "no new backup directory on idempotent run");
321
- // Plist contents byte-identical.
322
- for (const [name, body] of Object.entries(before)) {
323
- assert.equal(readFileSync(join(plistDir, name), "utf-8"), body, `${name} changed`);
324
- }
325
- } finally { await rmRoot(root); }
326
- });
327
-
328
- test("upgrade --dry-run reports legacy without modifying anything", async () => {
329
- const root = await makeLegacyAgent();
330
- try {
331
- // Snapshot a known sentinel file.
332
- const sentinel = readFileSync(join(root, "scripts/local-triggers/plists/ai.adaptic.legacy-inbox-processor.plist"), "utf-8");
333
- const r = runCli(["upgrade", "--dry-run"], root);
334
- assert.equal(r.status, 0);
335
- assert.match(r.stdout, /still call run-trigger\.sh/);
336
- // Sentinel must not have changed.
337
- assert.equal(
338
- readFileSync(join(root, "scripts/local-triggers/plists/ai.adaptic.legacy-inbox-processor.plist"), "utf-8"),
339
- sentinel,
340
- );
341
- // No backup directory created.
342
- assert.ok(!existsSync(join(root, ".maestro/backup")), "dry-run must not create backups");
343
- } finally { await rmRoot(root); }
344
- });
345
-
346
- // ---------------------------------------------------------------------------
347
- // doctor
348
- // ---------------------------------------------------------------------------
349
-
350
- test("doctor flags legacy plists with actionable remediation", async () => {
351
- const root = await makeLegacyAgent();
352
- try {
353
- const r = runCli(["doctor"], root);
354
- // doctor exits 1 when issues are found; that's expected here.
355
- assert.ok(r.status >= 0);
356
- const combined = (r.stdout || "") + (r.stderr || "");
357
- assert.match(combined, /run-trigger\.sh/);
358
- assert.match(combined, /maestro upgrade/);
359
- } finally { await rmRoot(root); }
360
- });
361
-
362
- test("doctor recognises new cadence-bus architecture after upgrade", async () => {
363
- const root = await makeLegacyAgent();
364
- try {
365
- runCli(["upgrade"], root);
366
- const r = runCli(["doctor"], root);
367
- const combined = (r.stdout || "") + (r.stderr || "");
368
- assert.match(combined, /cadence-bus architecture/);
369
- // 'plist(s) still call run-trigger.sh' must NOT appear.
370
- assert.ok(!combined.includes("plist(s) still call run-trigger.sh"));
371
- } finally { await rmRoot(root); }
372
- });
373
-
374
- // ---------------------------------------------------------------------------
375
- // setup (WS1 wizard) — CLI surface
376
- // ---------------------------------------------------------------------------
377
-
378
- test("setup --help runs in a non-agent dir without throwing (exit 0)", async () => {
379
- const dir = await tmpRoot("maestro-setup-help");
380
- try {
381
- const r = runCli(["setup", "--help"], dir);
382
- assert.equal(r.status, 0, r.stderr);
383
- assert.match(r.stdout, /maestro setup/);
384
- assert.match(r.stdout, /--headless/);
385
- } finally { await rmRoot(dir); }
386
- });
387
-
388
- test("setup --status in a non-agent dir prints a graceful message (exit 0)", async () => {
389
- const dir = await tmpRoot("maestro-setup-status");
390
- try {
391
- const r = runCli(["setup", "--status"], dir);
392
- assert.equal(r.status, 0, r.stderr);
393
- const out = (r.stdout || "") + (r.stderr || "");
394
- assert.match(out, /Not a Maestro agent directory/);
395
- } finally { await rmRoot(dir); }
396
- });
397
-
398
- test("setup --status in an agent dir lists section detect() statuses", async () => {
399
- const dir = await tmpRoot("maestro-setup-agent");
400
- try {
401
- mkdirSync(join(dir, "config"), { recursive: true });
402
- // Minimal SoT so the agent-repo guard passes + sections can detect.
403
- writeFileSync(join(dir, "config/agent.json"), JSON.stringify({
404
- firstName: "UNCONFIGURED", lastName: "AGENT", title: "Unconfigured Role", email: "",
405
- function: "executive-operator", altitude: "c-suite",
406
- }, null, 2));
407
- const r = runCli(["setup", "--status"], dir);
408
- assert.equal(r.status, 0, r.stderr);
409
- const out = (r.stdout || "") + (r.stderr || "");
410
- assert.match(out, /identity/);
411
- assert.match(out, /operating-model/);
412
- assert.match(out, /verify/);
413
- } finally { await rmRoot(dir); }
414
- });
415
-
416
- // ---------------------------------------------------------------------------
417
- // doctor cost-telemetry tripwire — pure predicate (observability F2)
418
- // ---------------------------------------------------------------------------
419
-
420
- /** A measured session row in the shape the tracker now writes. */
421
- function measuredRow(over = {}) {
422
- return {
423
- ts: new Date().toISOString(), cadence: "inbox", source: "dispatcher",
424
- model: "sonnet", input_tokens: 16, output_tokens: 5000,
425
- cache_read_input_tokens: 553181, measurement: "measured",
426
- estimated_usd: 0.241, total_cost_usd: 0.533412, ...over,
427
- };
428
- }
429
- /** A session that ran but whose usage could not be read. */
430
- function unmeasuredRow(over = {}) {
431
- return {
432
- ts: new Date().toISOString(), cadence: "inbox", source: "dispatcher",
433
- model: "sonnet", input_tokens: null, output_tokens: null,
434
- measurement: "unknown", unmeasured_reason: "no-usage-field",
435
- estimated_usd: null, total_cost_usd: null, ...over,
436
- };
437
- }
438
- /** A message send: no model ran, so $0 is a fact rather than a gap. */
439
- function nonLlmRow(over = {}) {
440
- return {
441
- ts: new Date().toISOString(), source: "messaging", task_class: "messaging.send",
442
- model: null, input_tokens: 0, output_tokens: 0, estimated_usd: 0, ...over,
443
- };
444
- }
445
-
446
- test("summariseLedgerRows bills the authoritative cost, not the cache-blind estimate", () => {
447
- const s = summariseLedgerRows([measuredRow(), measuredRow({ total_cost_usd: 1 })]);
448
- assert.equal(s.rowCount, 2);
449
- assert.equal(s.totalUsd, 1.533412, "total_cost_usd wins over estimated_usd");
450
- });
451
-
452
- test("summariseLedgerRows counts sessions, not attribution rows; tolerates junk", () => {
453
- const s = summariseLedgerRows([
454
- measuredRow(), unmeasuredRow(), nonLlmRow(), null, "garbage",
455
- ]);
456
- assert.equal(s.rowCount, 2, "1 measured + 1 unmeasured; the send is not a session");
457
- assert.equal(s.measured, 1);
458
- assert.equal(s.unmeasured, 1);
459
- assert.equal(s.nonLlmRows, 1);
460
- });
461
-
462
- test("summariseLedgerRows on empty input is zeroed", () => {
463
- assert.equal(summariseLedgerRows([]).rowCount, 0);
464
- assert.equal(summariseLedgerRows([]).totalUsd, 0);
465
- assert.equal(summariseLedgerRows(undefined).rowCount, 0);
466
- });
467
-
468
- test("evaluateCostTripwire: unmeasured rows → RED (the 0-vs-unknown distinction)", () => {
469
- // The defect doctor exists to catch: a session ran and nobody priced it.
470
- const t = evaluateCostTripwire({ rows: [measuredRow(), unmeasuredRow()], sessionCount: 2 });
471
- assert.equal(t.red, true);
472
- assert.equal(t.unmeasured, 1);
473
- assert.match(t.reason, /NO token counts/);
474
- assert.match(t.fix, /unmeasured_reason/);
475
- });
476
-
477
- test("evaluateCostTripwire: a genuine zero-token session is NOT unmeasured", () => {
478
- // A model ran and emitted nothing. That is a measurement; it must stay GREEN
479
- // as long as the day bills above $0 — otherwise doctor cries wolf.
480
- const t = evaluateCostTripwire({
481
- rows: [measuredRow(), measuredRow({ input_tokens: 40, output_tokens: 0, total_cost_usd: 0 })],
482
- sessionCount: 2,
483
- });
484
- assert.equal(t.red, false);
485
- assert.equal(t.unmeasured, 0);
486
- });
487
-
488
- test("evaluateCostTripwire: sessions logged but no ledger rows → RED (unrecorded)", () => {
489
- const t = evaluateCostTripwire({ rows: [], sessionCount: 3 });
490
- assert.equal(t.red, true);
491
- assert.equal(t.unrecorded, 3);
492
- assert.match(t.reason, /only 0 dispatcher row\(s\) reached the cost ledger/);
493
- });
494
-
495
- test("evaluateCostTripwire: rows from ANOTHER writer do not mask an unrecorded dispatcher", () => {
496
- // The population bug this replaced. `sessionCount` comes from the dispatcher's
497
- // own session log; `rowCount` counted rows from every writer. With the
498
- // responder now writing rows too, a dispatcher that had stopped recording
499
- // entirely read as unrecorded:0 / GREEN — 35 real sessions unpriced, and the
500
- // one detector meant to catch exactly that said nothing.
501
- const responderRows = Array.from({ length: 40 }, () => measuredRow({ source: "responder" }));
502
- const t = evaluateCostTripwire({ rows: responderRows, sessionCount: 35 });
503
- assert.equal(t.red, true, "35 dispatcher sessions with 0 dispatcher rows is RED");
504
- assert.equal(t.unrecorded, 35);
505
- assert.equal(t.sourceRowCount, 0);
506
- assert.match(t.reason, /dispatcher logged 35/);
507
- });
508
-
509
- test("evaluateCostTripwire: measured rows billing $0 → RED (pricing broken)", () => {
510
- const t = evaluateCostTripwire({
511
- rows: [measuredRow({ total_cost_usd: 0, estimated_usd: 0 })],
512
- sessionCount: 1,
513
- });
514
- assert.equal(t.red, true);
515
- assert.match(t.reason, /bill to \$0/);
516
- assert.match(t.fix, /--cache-read-tokens/);
517
- });
518
-
519
- test("evaluateCostTripwire: no sessions and no rows → GREEN (nothing to measure)", () => {
520
- const t = evaluateCostTripwire({ rows: [], sessionCount: 0 });
521
- assert.equal(t.red, false);
522
- assert.match(t.reason, /nothing to measure/i);
523
- });
524
-
525
- test("evaluateCostTripwire: attribution-only day is quiet, not blind", () => {
526
- // Regression guard for the false alarm this replaced: 41 message-send rows on
527
- // 2026-08-11 would have armed the old tripwire all by themselves.
528
- const t = evaluateCostTripwire({ rows: [nonLlmRow(), nonLlmRow()], sessionCount: 0 });
529
- assert.equal(t.red, false);
530
- assert.match(t.reason, /nothing to measure/i);
531
- });
532
-
533
- test("evaluateCostTripwire: spend > 0 with every session measured → GREEN", () => {
534
- const t = evaluateCostTripwire({ rows: [measuredRow()], sessionCount: 1 });
535
- assert.equal(t.red, false);
536
- assert.equal(t.totalUsd, 0.533412);
537
- assert.match(t.reason, /live/i);
538
- });
539
-
540
- test("evaluateCostTripwire: defaults (no args) are GREEN, not a false alarm", () => {
541
- const t = evaluateCostTripwire();
542
- assert.equal(t.red, false);
543
- assert.equal(t.status, "green");
544
- });
545
-
546
- // ---------------------------------------------------------------------------
547
- // doctor cost-telemetry tripwire — end-to-end against a real ledger file
548
- // ---------------------------------------------------------------------------
549
-
550
- /**
551
- * Minimal agent repo that doctor can run against without exploding before our
552
- * checks. doctor exits 1 on any issue (a bare repo has many), so these tests
553
- * assert on the specific lines our checks emit rather than the exit code.
554
- */
555
- async function makeBareAgent() {
556
- const root = await tmpRoot("maestro-doctor");
557
- mkdirSync(join(root, "config"), { recursive: true });
558
- writeFileSync(join(root, "config/agent.json"), JSON.stringify({
559
- firstName: "test", function: "executive-operator", altitude: "c-suite",
560
- }, null, 2));
561
- writeFileSync(join(root, "CLAUDE.md"), "# test agent\n");
562
- return root;
563
- }
564
-
565
- function utcDate() { return new Date().toISOString().slice(0, 10); }
566
-
567
- test("doctor RED when a session ran with no token counts recorded", async () => {
568
- const root = await makeBareAgent();
569
- try {
570
- mkdirSync(join(root, "state/cost-tracking"), { recursive: true });
571
- const ledger = join(root, "state/cost-tracking", `${utcDate()}.jsonl`);
572
- writeFileSync(ledger,
573
- JSON.stringify(unmeasuredRow({ unmeasured_reason: "no-usage-field" })) + "\n" +
574
- JSON.stringify(unmeasuredRow({ cadence: "backlog", unmeasured_reason: "empty-stdout" })) + "\n"
575
- );
576
- const r = runCli(["doctor"], root);
577
- const out = (r.stdout || "") + (r.stderr || "");
578
- assert.match(out, /Cost telemetry BLIND/);
579
- assert.match(out, /NO token counts/);
580
- } finally { await rmRoot(root); }
581
- });
582
-
583
- test("doctor GREEN when today's ledger records real spend", async () => {
584
- const root = await makeBareAgent();
585
- try {
586
- mkdirSync(join(root, "state/cost-tracking"), { recursive: true });
587
- const ledger = join(root, "state/cost-tracking", `${utcDate()}.jsonl`);
588
- writeFileSync(ledger, JSON.stringify(measuredRow()) + "\n");
589
- const r = runCli(["doctor"], root);
590
- const out = (r.stdout || "") + (r.stderr || "");
591
- assert.match(out, /Cost telemetry: spend telemetry live/);
592
- assert.ok(!out.includes("Cost telemetry BLIND"), "must not flag blind when spend>0");
593
- } finally { await rmRoot(root); }
594
- });
595
-
596
- test("doctor GREEN (nothing to measure) when no ledger exists today", async () => {
597
- const root = await makeBareAgent();
598
- try {
599
- const r = runCli(["doctor"], root);
600
- const out = (r.stdout || "") + (r.stderr || "");
601
- assert.match(out, /nothing to measure/);
602
- } finally { await rmRoot(root); }
603
- });
604
-
605
- test("doctor does NOT cry wolf on an empty ledger while the daemon is live", async () => {
606
- // The regression this replaced: doctor keyed "sessions ran" off a fresh
607
- // heartbeat, so every UTC day went RED between midnight and the first session
608
- // and printed a fix that had already shipped.
609
- const root = await makeBareAgent();
610
- try {
611
- mkdirSync(join(root, "state/cadence-bus"), { recursive: true });
612
- writeFileSync(join(root, "state/cadence-bus/health.json"),
613
- JSON.stringify({ ts: new Date().toISOString() }));
614
- const r = runCli(["doctor"], root);
615
- const out = (r.stdout || "") + (r.stderr || "");
616
- assert.ok(!out.includes("Cost telemetry BLIND"), "a live daemon with no sessions yet is not blind");
617
- assert.match(out, /nothing to measure/);
618
- } finally { await rmRoot(root); }
619
- });
620
-
621
- // ---------------------------------------------------------------------------
622
- // doctor .env permission check (setup-onboarding W4)
623
- // ---------------------------------------------------------------------------
624
-
625
- test("doctor flags a group/world-readable .env with the chmod fix", { skip: typeof process.getuid !== "function" }, async () => {
626
- const root = await makeBareAgent();
627
- try {
628
- const envPath = join(root, ".env");
629
- writeFileSync(envPath, "ANTHROPIC_API_KEY=sk-secret\n");
630
- chmodSync(envPath, 0o644);
631
- const r = runCli(["doctor"], root);
632
- const out = (r.stdout || "") + (r.stderr || "");
633
- assert.match(out, /\.env is mode 644/);
634
- assert.match(out, /chmod 600 \.env/);
635
- } finally { await rmRoot(root); }
636
- });
637
-
638
- test("doctor passes a chmod-600 .env", { skip: typeof process.getuid !== "function" }, async () => {
639
- const root = await makeBareAgent();
640
- try {
641
- const envPath = join(root, ".env");
642
- writeFileSync(envPath, "ANTHROPIC_API_KEY=sk-secret\n");
643
- chmodSync(envPath, 0o600);
644
- const r = runCli(["doctor"], root);
645
- const out = (r.stdout || "") + (r.stderr || "");
646
- assert.match(out, /\.env permissions are owner-only/);
647
- assert.ok(!out.includes(".env is mode"), "must not flag a 600 .env");
648
- } finally { await rmRoot(root); }
649
- });
650
-
651
- // ---------------------------------------------------------------------------
652
- // evaluateStaleness (pure) — the router data-file staleness ladder (graft #18)
653
- // ---------------------------------------------------------------------------
654
-
655
- test("evaluateStaleness: fresh data is green", () => {
656
- const now = Date.UTC(2026, 5, 11);
657
- const v = evaluateStaleness({ fetched: "2026-06-10", now });
658
- assert.equal(v.status, "green");
659
- assert.equal(v.ageDays, 1);
660
- });
661
-
662
- test("evaluateStaleness: >7d but <30d is a WARN", () => {
663
- const now = Date.UTC(2026, 5, 30);
664
- const v = evaluateStaleness({ fetched: "2026-06-10", now });
665
- assert.equal(v.status, "warn");
666
- assert.ok(v.ageDays >= 7 && v.ageDays < 30);
667
- });
668
-
669
- test("evaluateStaleness: >=30d is a FAIL", () => {
670
- const now = Date.UTC(2026, 6, 15);
671
- const v = evaluateStaleness({ fetched: "2026-06-10", now });
672
- assert.equal(v.status, "fail");
673
- assert.ok(v.ageDays >= 30);
674
- });
675
-
676
- test("evaluateStaleness: a stale volatile catalog escalates the FAIL reason", () => {
677
- const now = Date.UTC(2026, 6, 15);
678
- const v = evaluateStaleness({ fetched: "2026-06-10", now, hasVolatile: true, label: "catalog" });
679
- assert.equal(v.status, "fail");
680
- assert.match(v.reason, /volatile/);
681
- });
682
-
683
- test("evaluateStaleness: a missing fetch date is a WARN (provenance unknown)", () => {
684
- const v = evaluateStaleness({ fetched: null });
685
- assert.equal(v.status, "warn");
686
- assert.match(v.reason, /no fetch date/);
687
- });
688
-
689
- // ---------------------------------------------------------------------------
690
- // router CLI — `maestro router why` / `maestro router validate`
691
- // ---------------------------------------------------------------------------
692
-
693
- const V2_ROUTER_YAML = `
694
- schema_version: 2
695
- aliases:
696
- frontier: anthropic/claude-opus-4-8
697
- default: anthropic/claude-sonnet-4-6
698
- fast: anthropic/claude-haiku-4-5
699
- cheap: deepseek/deepseek-v4-flash
700
- defaults:
701
- data_class: sensitive
702
- backends:
703
- anthropic: { allowed_data_classes: [public, internal, sensitive] }
704
- deepseek: { allowed_data_classes: [public] }
705
- routing_policy:
706
- - match: { task_class: classify.inbox }
707
- harness: direct
708
- chain: [fast, cheap, rules]
709
- needs_tool_use: false
710
- - default: true
711
- chain: [default, fast]
712
- fallback_to_anthropic: true
713
- `;
714
-
715
- async function makeV2RouterAgent() {
716
- const root = await makeBareAgent();
717
- writeFileSync(join(root, "config/model-routing.yaml"), V2_ROUTER_YAML);
718
- return root;
719
- }
720
-
721
- test("router why: prints chosen + explain + chain for a session request", async () => {
722
- const root = await makeV2RouterAgent();
723
- try {
724
- const r = runCli(["router", "why", "session.responder"], root);
725
- const out = (r.stdout || "") + (r.stderr || "");
726
- assert.match(out, /chosen/);
727
- assert.match(out, /anthropic\/claude-sonnet-4-6/);
728
- assert.match(out, /explain/);
729
- assert.match(out, /chain/);
730
- } finally { await rmRoot(root); }
731
- });
732
-
733
- test("router why: accepts inline request JSON and reports tried[] skips", async () => {
734
- const root = await makeV2RouterAgent();
735
- try {
736
- const r = runCli(
737
- ["router", "why", '{"task_class":"classify.inbox","data_class":"public"}'],
738
- root
739
- );
740
- const out = (r.stdout || "") + (r.stderr || "");
741
- // No keys present → cheap deepseek + anthropic haiku-direct are unreachable;
742
- // the deterministic "rules" fallback wins and the skips are listed.
743
- assert.match(out, /tried/);
744
- assert.match(out, /missing_credential/);
745
- } finally { await rmRoot(root); }
746
- });
747
-
748
- test("router validate: a clean v2 config validates (exit 0)", async () => {
749
- const root = await makeV2RouterAgent();
750
- try {
751
- const r = runCli(["router", "validate"], root);
752
- const out = (r.stdout || "") + (r.stderr || "");
753
- assert.equal(r.status, 0, out);
754
- assert.match(out, /valid/);
755
- } finally { await rmRoot(root); }
756
- });
757
-
758
- test("router validate: an unknown match key fails validation (exit 1)", async () => {
759
- const root = await makeBareAgent();
760
- try {
761
- writeFileSync(
762
- join(root, "config/model-routing.yaml"),
763
- "schema_version: 2\naliases:\n default: anthropic/claude-sonnet-4-6\nrouting_policy:\n - match: { taks_class: x }\n chain: [default]\n - default: true\n chain: [default]\n"
764
- );
765
- const r = runCli(["router", "validate"], root);
766
- const out = (r.stdout || "") + (r.stderr || "");
767
- assert.equal(r.status, 1, out);
768
- assert.match(out, /unknown match key/);
769
- } finally { await rmRoot(root); }
770
- });
771
-
772
- test("router help: bare `router` prints usage", async () => {
773
- const root = await makeBareAgent();
774
- try {
775
- const r = runCli(["router"], root);
776
- const out = (r.stdout || "") + (r.stderr || "");
777
- assert.match(out, /router why/);
778
- assert.match(out, /router validate/);
779
- } finally { await rmRoot(root); }
780
- });
781
-
782
- // ---------------------------------------------------------------------------
783
- // audit — security-posture audit + attestation (G15)
784
- // ---------------------------------------------------------------------------
785
-
786
- const GOOD_AUDIT_SETTINGS = JSON.stringify({
787
- hooks: {
788
- PreToolUse: [
789
- { matcher: "mcp__claude_ai_Slack__slack_send_message", hooks: [{ type: "command", command: "./scripts/hooks/block-mcp-slack-send.sh" }] },
790
- { matcher: "mcp__claude_ai_Gmail__gmail_create_draft", hooks: [{ type: "command", command: "./scripts/hooks/pre-send-audit.sh gmail" }] },
791
- ],
792
- PostToolUse: [{ matcher: "", hooks: [{ type: "command", command: "./scripts/hooks/post-action-log.sh" }] }],
793
- Stop: [{ matcher: "", hooks: [{ type: "command", command: "./scripts/hooks/session-end-log.sh" }] }],
794
- },
795
- }, null, 2);
796
-
797
- /** A conformant agent repo (every audit check passes when scoped-perms is set). */
798
- async function makeConformantAgent() {
799
- const root = await makeBareAgent();
800
- mkdirSync(join(root, ".claude"), { recursive: true });
801
- mkdirSync(join(root, "scripts/hooks"), { recursive: true });
802
- mkdirSync(join(root, ".maestro"), { recursive: true });
803
- mkdirSync(join(root, "policies"), { recursive: true });
804
- // config/agent.json already written by makeBareAgent.
805
- const envPath = join(root, ".env");
806
- writeFileSync(envPath, "ANTHROPIC_API_KEY=sk-test\nMAESTRO_SCOPED_PERMISSIONS=1\n");
807
- chmodSync(envPath, 0o600);
808
- writeFileSync(join(root, ".claude/settings.json"), GOOD_AUDIT_SETTINGS);
809
- for (const s of ["block-mcp-slack-send.sh", "pre-send-audit.sh", "post-action-log.sh", "session-end-log.sh"]) {
810
- writeFileSync(join(root, "scripts/hooks", s), "#!/bin/bash\nexit 0\n");
811
- }
812
- writeFileSync(join(root, ".maestro/backup-config.yaml"), "enabled: true\nprovider: s3\nbucket: b\n");
813
- writeFileSync(join(root, "config/validation-rules.yaml"), "ai_disclosure:\n enabled: true\n severity: block\n");
814
- writeFileSync(join(root, "policies/action-classification.yaml"), "classes: []\n");
815
- return root;
816
- }
817
-
818
- test("audit: a conformant agent exits 0 and reports posture conformant", async () => {
819
- const root = await makeConformantAgent();
820
- try {
821
- const r = runCli(["audit"], root);
822
- const out = (r.stdout || "") + (r.stderr || "");
823
- assert.equal(r.status, 0, out);
824
- assert.match(out, /checks passed/);
825
- assert.match(out, /attestation:/);
826
- assert.match(out, /Posture conformant/);
827
- } finally { await rmRoot(root); }
828
- });
829
-
830
- test("audit: a group-readable .env makes the run exit non-zero with the fix", async () => {
831
- const root = await makeConformantAgent();
832
- try {
833
- chmodSync(join(root, ".env"), 0o644);
834
- const r = runCli(["audit"], root);
835
- const out = (r.stdout || "") + (r.stderr || "");
836
- assert.equal(r.status, 1, out);
837
- assert.match(out, /env-mode-600/);
838
- assert.match(out, /chmod 600/);
839
- assert.match(out, /unsuppressed failure/);
840
- } finally { await rmRoot(root); }
841
- });
842
-
843
- test("audit --fix: repairs a group-readable .env and then exits 0", async () => {
844
- const root = await makeConformantAgent();
845
- try {
846
- chmodSync(join(root, ".env"), 0o644);
847
- const r = runCli(["audit", "--fix"], root);
848
- const out = (r.stdout || "") + (r.stderr || "");
849
- assert.equal(r.status, 0, out);
850
- assert.match(out, /applied 1 safe fix/);
851
- assert.equal(statSync(join(root, ".env")).mode & 0o777, 0o600);
852
- } finally { await rmRoot(root); }
853
- });
854
-
855
- test("audit: a suppressed-with-reason failure exits 0", async () => {
856
- const root = await makeConformantAgent();
857
- try {
858
- chmodSync(join(root, ".env"), 0o644); // would fail env-mode-600
859
- writeFileSync(join(root, "config/audit-suppressions.yaml"),
860
- "- id: env-mode-600\n reason: ephemeral CI runner\n");
861
- const r = runCli(["audit"], root);
862
- const out = (r.stdout || "") + (r.stderr || "");
863
- assert.equal(r.status, 0, out);
864
- assert.match(out, /SUPPRESSED/);
865
- assert.match(out, /ephemeral CI runner/);
866
- } finally { await rmRoot(root); }
867
- });
868
-
869
- test("audit --json: emits structured results + a hash-locked attestation", async () => {
870
- const root = await makeConformantAgent();
871
- try {
872
- const r = runCli(["audit", "--json"], root);
873
- assert.equal(r.status, 0, r.stderr || r.stdout);
874
- const doc = JSON.parse(r.stdout);
875
- assert.equal(doc.ok, true);
876
- assert.ok(Array.isArray(doc.results) && doc.results.length >= 8);
877
- assert.equal(typeof doc.attestation.hash, "string");
878
- assert.equal(doc.attestation.schema, "maestro.audit.attestation.v1");
879
- assert.equal(doc.attestation.identity.function, "executive-operator");
880
- } finally { await rmRoot(root); }
881
- });
882
-
883
- test("audit --json: missing scoped-permissions makes ok:false and exit 1", async () => {
884
- const root = await makeConformantAgent();
885
- try {
886
- writeFileSync(join(root, ".env"), "ANTHROPIC_API_KEY=sk-test\n"); // drop opt-in
887
- chmodSync(join(root, ".env"), 0o600);
888
- const r = runCli(["audit", "--json"], root);
889
- assert.equal(r.status, 1);
890
- const doc = JSON.parse(r.stdout);
891
- assert.equal(doc.ok, false);
892
- const sp = doc.results.find((x) => x.id === "scoped-permissions-configured");
893
- assert.equal(sp.ok, false);
894
- assert.equal(sp.severity, "critical");
895
- } finally { await rmRoot(root); }
896
- });
897
-
898
- test("audit: exits 1 with a clear message outside a Maestro agent dir", async () => {
899
- const dir = await tmpRoot("maestro-audit-nonagent");
900
- try {
901
- const r = runCli(["audit"], dir);
902
- const out = (r.stdout || "") + (r.stderr || "");
903
- assert.equal(r.status, 1);
904
- assert.match(out, /Not a Maestro agent directory/);
905
- } finally { await rmRoot(dir); }
906
- });
907
-
908
- test("audit --help: prints usage and exits 0", async () => {
909
- const dir = await tmpRoot("maestro-audit-help");
910
- try {
911
- const r = runCli(["audit", "--help"], dir);
912
- assert.equal(r.status, 0, r.stderr);
913
- assert.match(r.stdout, /maestro audit/);
914
- assert.match(r.stdout, /--fix/);
915
- } finally { await rmRoot(dir); }
916
- });
917
-
918
- // ---------------------------------------------------------------------------
919
- // secrets — list / rotate (subcommand, gaps-enterprise-ops G12)
920
- // ---------------------------------------------------------------------------
921
-
922
- test("secrets --help prints usage and exits 0", async () => {
923
- const dir = await tmpRoot("maestro-secrets-help");
924
- try {
925
- const r = runCli(["secrets", "--help"], dir);
926
- assert.equal(r.status, 0, r.stderr);
927
- assert.match(r.stdout, /maestro secrets/);
928
- assert.match(r.stdout, /names only/);
929
- } finally { await rmRoot(dir); }
930
- });
931
-
932
- test("secrets: exits 1 outside a Maestro agent dir", async () => {
933
- const dir = await tmpRoot("maestro-secrets-nonagent");
934
- try {
935
- const r = runCli(["secrets", "list"], dir);
936
- const out = (r.stdout || "") + (r.stderr || "");
937
- assert.equal(r.status, 1);
938
- assert.match(out, /Not a Maestro agent directory/);
939
- } finally { await rmRoot(dir); }
940
- });
941
-
942
- test("secrets rotate then list shows the NAME but never the value", async () => {
943
- const root = await makeBareAgent();
944
- try {
945
- // config/secrets.yaml selects the local file provider.
946
- writeFileSync(join(root, "config/secrets.yaml"), "provider: file\n");
947
-
948
- // Rotate a secret with the value supplied on stdin (never in argv).
949
- const rot = runCliStdin(["secrets", "rotate", "--name", "DB_PASSWORD"], root, "p@ss-SECRET-123");
950
- const rotOut = (rot.stdout || "") + (rot.stderr || "");
951
- assert.equal(rot.status, 0, rotOut);
952
- assert.match(rotOut, /Rotated "DB_PASSWORD"/);
953
-
954
- // list shows the name...
955
- const ls = runCli(["secrets", "list"], root);
956
- const lsOut = (ls.stdout || "") + (ls.stderr || "");
957
- assert.equal(ls.status, 0, lsOut);
958
- assert.match(lsOut, /DB_PASSWORD/);
959
- // ...but never the value, in either command's output.
960
- assert.ok(!lsOut.includes("p@ss-SECRET-123"), "list must not print the value");
961
- assert.ok(!rotOut.includes("p@ss-SECRET-123"), "rotate must not echo the value");
962
-
963
- // The on-disk store is sealed (ciphertext != plaintext).
964
- const store = readFileSync(join(root, "state/secrets/.secrets.json"), "utf-8");
965
- assert.ok(!store.includes("p@ss-SECRET-123"), "value sealed at rest");
966
-
967
- // The audit log records the name, not the value.
968
- const audit = readFileSync(join(root, "logs/audit/secrets.jsonl"), "utf-8");
969
- assert.match(audit, /DB_PASSWORD/);
970
- assert.ok(!audit.includes("p@ss-SECRET-123"), "audit is names-only");
971
- } finally { await rmRoot(root); }
972
- });
973
-
974
- test("secrets rotate without --name exits 1 with usage", async () => {
975
- const root = await makeBareAgent();
976
- try {
977
- const r = runCliStdin(["secrets", "rotate"], root, "v");
978
- const out = (r.stdout || "") + (r.stderr || "");
979
- assert.equal(r.status, 1);
980
- assert.match(out, /--name/);
981
- } finally { await rmRoot(root); }
982
- });
983
-
984
- test("secrets sync without a configured broker exits 1", async () => {
985
- const root = await makeBareAgent();
986
- try {
987
- writeFileSync(join(root, "config/secrets.yaml"), "provider: file\n");
988
- const r = runCli(["secrets", "sync"], root);
989
- const out = (r.stdout || "") + (r.stderr || "");
990
- assert.equal(r.status, 1);
991
- assert.match(out, /No broker configured/);
992
- } finally { await rmRoot(root); }
993
- });
994
-
995
- // ---------------------------------------------------------------------------
996
- // doctor — backup-config DR check (gaps-enterprise-ops G16)
997
- // ---------------------------------------------------------------------------
998
-
999
- test("doctor RED when there is no DR posture at all, and names the one command that fixes it", async () => {
1000
- const root = await makeBareAgent();
1001
- try {
1002
- const r = runCli(["doctor"], root);
1003
- const out = (r.stdout || "") + (r.stderr || "");
1004
- assert.match(out, /No DR posture at all/);
1005
- assert.match(out, /backup-config\.yaml/);
1006
- assert.match(out, /maestro init backup-replication --apply/);
1007
- // doctor exits 1 whenever any issue is found; the missing backup is one.
1008
- assert.equal(r.status, 1, out);
1009
- } finally { await rmRoot(root); }
1010
- });
1011
-
1012
- test("doctor RED when backup is enabled but has never actually run", async () => {
1013
- const root = await makeBareAgent();
1014
- try {
1015
- mkdirSync(join(root, ".maestro"), { recursive: true });
1016
- writeFileSync(join(root, ".maestro/backup-config.yaml"), "enabled: true\nprefix: t\n");
1017
- const r = runCli(["doctor"], root);
1018
- const out = (r.stdout || "") + (r.stderr || "");
1019
- assert.match(out, /NEVER completed a run/);
1020
- assert.match(out, /backup-run\.mjs/);
1021
- } finally { await rmRoot(root); }
1022
- });
1023
-
1024
- test("doctor WARNS (not GREEN) when restore points are fresh but local-only", async () => {
1025
- const root = await makeBareAgent();
1026
- try {
1027
- mkdirSync(join(root, ".maestro"), { recursive: true });
1028
- writeFileSync(join(root, ".maestro/backup-config.yaml"), "enabled: true\nprefix: t\nlocal:\n enabled: true\n");
1029
- writeFileSync(join(root, ".maestro/last-backup.json"), JSON.stringify({
1030
- completed_at: new Date().toISOString(), tier: "local", provider: "local",
1031
- }));
1032
- const r = runCli(["doctor"], root);
1033
- const out = (r.stdout || "") + (r.stderr || "");
1034
- assert.match(out, /LOCAL-ONLY/);
1035
- assert.match(out, /offsite\.provider/);
1036
- assert.ok(!out.includes("No DR posture at all"), "must not claim the config is missing");
1037
- } finally { await rmRoot(root); }
1038
- });
1039
-
1040
- test("doctor GREEN for backup only when it is fresh AND off-machine", async () => {
1041
- const root = await makeBareAgent();
1042
- try {
1043
- mkdirSync(join(root, ".maestro"), { recursive: true });
1044
- writeFileSync(
1045
- join(root, ".maestro/backup-config.yaml"),
1046
- "enabled: true\nprefix: t\noffsite:\n provider: s3\n bucket: agent-backups\n"
1047
- );
1048
- writeFileSync(join(root, ".maestro/last-backup.json"), JSON.stringify({
1049
- completed_at: new Date().toISOString(), tier: "offsite", provider: "s3", bucket: "agent-backups",
1050
- }));
1051
- const r = runCli(["doctor"], root);
1052
- const out = (r.stdout || "") + (r.stderr || "");
1053
- assert.match(out, /Backup fresh \+ off-machine/);
1054
- assert.ok(!out.includes("No DR posture at all"), "must not flag missing backup when fresh");
1055
- assert.ok(!out.includes("LOCAL-ONLY"), "offsite is configured");
1056
- } finally { await rmRoot(root); }
1057
- });
1058
-
1059
- test("doctor reports an include path that the credential deny-list drops", async () => {
1060
- const root = await makeBareAgent();
1061
- try {
1062
- mkdirSync(join(root, ".maestro"), { recursive: true });
1063
- writeFileSync(
1064
- join(root, ".maestro/backup-config.yaml"),
1065
- "enabled: true\nprefix: t\ninclude:\n - state\n - .env\n"
1066
- );
1067
- writeFileSync(join(root, ".maestro/last-backup.json"), JSON.stringify({
1068
- completed_at: new Date().toISOString(), tier: "local",
1069
- }));
1070
- const r = runCli(["doctor"], root);
1071
- const out = (r.stdout || "") + (r.stderr || "");
1072
- assert.match(out, /DROPPED by the credential deny-list/);
1073
- } finally { await rmRoot(root); }
1074
- });
1075
-
1076
- // ---------------------------------------------------------------------------
1077
- // framework-features.json — registry validity + new-system enrollment
1078
- // (registry-scaffold-exports integration lane)
1079
- // ---------------------------------------------------------------------------
1080
-
1081
- test("framework-features.json is valid JSON and registers the new systems", async () => {
1082
- const raw = readFileSync(join(MAESTRO_ROOT, "framework-features.json"), "utf-8");
1083
- const reg = JSON.parse(raw); // throws if invalid → test fails
1084
- assert.ok(reg.features && typeof reg.features === "object", "features map present");
1085
- const ids = Object.keys(reg.features);
1086
- // Every new system added by this lane must be enrolled in the registry.
1087
- for (const id of [
1088
- "org-cohort", "org-credential-broker", "secrets-lifecycle",
1089
- "diagnostics-spine", "alerting", "hook-bus", "dynamic-scheduling",
1090
- "send-gate", "knowledge-plane",
1091
- ]) {
1092
- assert.ok(ids.includes(id), `framework-features.json missing feature id: ${id}`);
1093
- }
1094
- // Pre-existing core systems still enrolled (no regression).
1095
- for (const id of ["model-router", "cadence-bus"]) {
1096
- assert.ok(ids.includes(id), `framework-features.json lost feature id: ${id}`);
1097
- }
1098
- // Schema fidelity: each new entry matches the existing feature schema.
1099
- for (const id of ["org-cohort", "secrets-lifecycle", "diagnostics-spine"]) {
1100
- const f = reg.features[id];
1101
- assert.equal(typeof f.version, "string", `${id}.version`);
1102
- assert.equal(typeof f.since, "string", `${id}.since`);
1103
- assert.equal(typeof f.title, "string", `${id}.title`);
1104
- assert.ok(f.init && typeof f.init === "object", `${id}.init`);
1105
- assert.equal(typeof f.init.auto, "boolean", `${id}.init.auto`);
1106
- assert.equal(typeof f.init.command, "string", `${id}.init.command`);
1107
- assert.equal(typeof f.init.description, "string", `${id}.init.description`);
1108
- // init.command must be a single-line string (feature-init trust-boundary guard).
1109
- assert.ok(!/[\0\r\n]/.test(f.init.command), `${id}.init.command must be single-line`);
1110
- }
1111
- // Honest enablement: org/cohort + its broker default OFF until configured.
1112
- assert.equal(reg.features["org-cohort"].init.auto, false, "org-cohort must not auto-enroll");
1113
- assert.equal(reg.features["org-credential-broker"].init.auto, false, "broker must not auto-enable");
1114
- });
1115
-
1116
- // ---------------------------------------------------------------------------
1117
- // create — ships the new disabled-by-default scaffold configs
1118
- // ---------------------------------------------------------------------------
1119
-
1120
- test("create ships disabled-by-default org.yaml + secrets.yaml templates", async () => {
1121
- const parent = await tmpRoot("maestro-create-cfg");
1122
- const { env, dir: shim } = await withShim();
1123
- try {
1124
- const r = runCli(["create", "cfg-agent"], parent, env);
1125
- assert.equal(r.status, 0, r.stderr || r.stdout);
1126
- const agent = join(parent, "cfg-agent");
1127
-
1128
- const orgYaml = join(agent, "config/org.yaml");
1129
- const secretsYaml = join(agent, "config/secrets.yaml");
1130
- assert.ok(existsSync(orgYaml), "config/org.yaml must be scaffolded");
1131
- assert.ok(existsSync(secretsYaml), "config/secrets.yaml must be scaffolded");
1132
-
1133
- // org.yaml is disabled by default (the agent runs standalone).
1134
- const orgBody = readFileSync(orgYaml, "utf-8");
1135
- assert.match(orgBody, /enabled:\s*false/, "org enrollment must default OFF");
1136
- assert.match(orgBody, /cohort/);
1137
-
1138
- // secrets.yaml defaults to the local file provider.
1139
- const secBody = readFileSync(secretsYaml, "utf-8");
1140
- assert.match(secBody, /provider:\s*file/, "secrets must default to the local file provider");
1141
- } finally {
1142
- await rmRoot(parent);
1143
- await rmRoot(shim);
1144
- }
1145
- });
1146
-
1147
- // ---------------------------------------------------------------------------
1148
- // upgrade — propagates new config templates to existing agents (add-only)
1149
- // ---------------------------------------------------------------------------
1150
-
1151
- test("upgrade adds new scaffold config templates without clobbering existing ones", async () => {
1152
- const root = await makeLegacyAgent();
1153
- try {
1154
- // Seed an operator-customised alerts.yaml that upgrade must NOT overwrite.
1155
- mkdirSync(join(root, "config"), { recursive: true });
1156
- const customAlerts = "alerts:\n webhook: https://hooks.example/custom\n";
1157
- writeFileSync(join(root, "config/alerts.yaml"), customAlerts);
1158
- execFileSync("git", ["add", "-A"], { cwd: root });
1159
- execFileSync("git", ["-c", "user.email=t@t", "-c", "user.name=t", "commit", "-q", "-m", "seed alerts"], { cwd: root });
1160
-
1161
- const r = runCli(["upgrade"], root);
1162
- assert.equal(r.status, 0, r.stderr);
1163
-
1164
- // New templates landed.
1165
- assert.ok(existsSync(join(root, "config/org.yaml")), "org.yaml must be delivered on upgrade");
1166
- assert.ok(existsSync(join(root, "config/secrets.yaml")), "secrets.yaml must be delivered on upgrade");
1167
- assert.match(readFileSync(join(root, "config/org.yaml"), "utf-8"), /enabled:\s*false/);
1168
-
1169
- // Existing operator config preserved byte-for-byte.
1170
- assert.equal(readFileSync(join(root, "config/alerts.yaml"), "utf-8"), customAlerts,
1171
- "upgrade must not clobber an existing operator config");
1172
- } finally { await rmRoot(root); }
1173
- });
1174
-
1175
- // ---------------------------------------------------------------------------
1176
- // doctor — surfaces the new framework systems
1177
- // ---------------------------------------------------------------------------
1178
-
1179
- test("doctor surfaces the new framework systems (org / secrets / diagnostics)", async () => {
1180
- // Use a freshly-created agent so lib/* + the scaffold configs are present.
1181
- const parent = await tmpRoot("maestro-doctor-systems");
1182
- const { env, dir: shim } = await withShim();
1183
- try {
1184
- const cr = runCli(["create", "doc-agent"], parent, env);
1185
- assert.equal(cr.status, 0, cr.stderr || cr.stdout);
1186
- const agent = join(parent, "doc-agent");
1187
- const r = runCli(["doctor"], agent);
1188
- const out = (r.stdout || "") + (r.stderr || "");
1189
- // 2.0: the presence-only org line became real connectivity probes; a fresh
1190
- // (unenrolled) create short-circuits on the single "not enrolled" line.
1191
- assert.match(out, /Org plane: not enrolled|Org enrollment resolved/);
1192
- assert.match(out, /Secret lifecycle/);
1193
- assert.match(out, /Diagnostics spine present/);
1194
- assert.match(out, /Alerting configured/);
1195
- // None of the new lib modules should be flagged missing in a fresh create.
1196
- assert.ok(!/module missing/.test(out), "fresh create must ship every new lib module");
1197
- } finally {
1198
- await rmRoot(parent);
1199
- await rmRoot(shim);
1200
- }
1201
- });
1202
-
1203
- // ---------------------------------------------------------------------------
1204
- // upgrade — prune (framework files deleted upstream)
1205
- //
1206
- // The copy loop only ever adds and overwrites, so before this every file the
1207
- // SDK had ever shipped stayed resident on an agent machine forever. Orphaned
1208
- // *modules* keep importing symbols their collaborators no longer export, so
1209
- // they fail permanently and drag the agent's own suite red — found in the
1210
- // field on Isla's machine, where a stale scripts/daemon/responder.test.mjs
1211
- // survived the transport refactor still importing a deleted export.
1212
- //
1213
- // Prune's contract is narrow on purpose: it removes a file only when git
1214
- // proves it is tracked-and-clean, i.e. pristine framework material the
1215
- // operator never touched. Everything else is kept and reported.
1216
- // ---------------------------------------------------------------------------
1217
-
1218
- /**
1219
- * Agent repo carrying `orphans` — paths upstream does not ship. Written before
1220
- * the commit so git sees them as tracked and clean (prune's precondition).
1221
- */
1222
- async function makeAgentWithOrphans(orphans, opts = {}) {
1223
- const root = await tmpRoot("maestro-orphan");
1224
- mkdirSync(join(root, "config"), { recursive: true });
1225
- writeFileSync(join(root, "CLAUDE.md"), "# agent\n");
1226
- writeFileSync(join(root, "config/agent.ts"), "export const AGENT = { firstName: 'orphan' };\n");
1227
- writeFileSync(join(root, "package.json"), '{"name":"orphan-agent","version":"1.0.0"}\n');
1228
- for (const [rel, body] of Object.entries(orphans)) {
1229
- mkdirSync(dirname(join(root, rel)), { recursive: true });
1230
- writeFileSync(join(root, rel), body);
1231
- }
1232
- // Provenance: prune only touches files a PREVIOUS upgrade recorded shipping.
1233
- // Without this the orphans are indistinguishable from operator-authored work
1234
- // and prune correctly refuses to act — so every prune test must seed it.
1235
- if (opts.seedManifest !== false) {
1236
- mkdirSync(join(root, ".maestro"), { recursive: true });
1237
- writeFileSync(
1238
- join(root, ".maestro/shipped-manifest.json"),
1239
- JSON.stringify({ version: 1, files: Object.keys(orphans).sort() }, null, 2) + "\n",
1240
- );
1241
- }
1242
-
1243
- execFileSync("git", ["init", "-q"], { cwd: root });
1244
- execFileSync("git", ["add", "-A"], { cwd: root });
1245
- execFileSync(
1246
- "git",
1247
- ["-c", "user.email=test@test", "-c", "user.name=test", "commit", "-q", "-m", "init"],
1248
- { cwd: root },
1249
- );
1250
- return root;
1251
- }
1252
-
1253
- const ORPHAN = "lib/zz-deleted-upstream-fixture.mjs";
1254
-
1255
- test("upgrade prunes a pristine framework file deleted upstream, with a backup", async () => {
1256
- const root = await makeAgentWithOrphans({ [ORPHAN]: "export const gone = 1;\n" });
1257
- try {
1258
- const r = runCli(["upgrade"], root);
1259
- assert.equal(r.status, 0, r.stderr);
1260
- assert.ok(!existsSync(join(root, ORPHAN)), "orphan should have been pruned");
1261
- assert.ok(
1262
- existsSync(join(root, ".maestro/backup", ORPHAN)),
1263
- "prune must be reversible — a backup is required",
1264
- );
1265
- assert.equal(
1266
- readFileSync(join(root, ".maestro/backup", ORPHAN), "utf-8"),
1267
- "export const gone = 1;\n",
1268
- "backup must hold the original bytes",
1269
- );
1270
- } finally {
1271
- await fsp.rm(root, { recursive: true, force: true });
1272
- }
1273
- });
1274
-
1275
- test("upgrade never prunes a file the operator edited", async () => {
1276
- const root = await makeAgentWithOrphans({ [ORPHAN]: "export const gone = 1;\n" });
1277
- try {
1278
- writeFileSync(join(root, ORPHAN), "export const gone = 2; // my change\n");
1279
- const r = runCli(["upgrade"], root);
1280
- assert.equal(r.status, 0, r.stderr);
1281
- assert.ok(existsSync(join(root, ORPHAN)), "an edited orphan is the operator's, not ours to delete");
1282
- assert.match(
1283
- readFileSync(join(root, ORPHAN), "utf-8"),
1284
- /my change/,
1285
- "the operator's bytes must survive verbatim",
1286
- );
1287
- assert.match(r.stdout, /kept/i, "the kept orphan must be reported, not silently left");
1288
- } finally {
1289
- await fsp.rm(root, { recursive: true, force: true });
1290
- }
1291
- });
1292
-
1293
- test("upgrade never prunes an untracked file", async () => {
1294
- const root = await makeAgentWithOrphans({});
1295
- try {
1296
- // Written after the commit → untracked → indistinguishable from operator work.
1297
- mkdirSync(join(root, "lib"), { recursive: true });
1298
- writeFileSync(join(root, ORPHAN), "export const mine = 1;\n");
1299
- const r = runCli(["upgrade"], root);
1300
- assert.equal(r.status, 0, r.stderr);
1301
- assert.ok(existsSync(join(root, ORPHAN)), "untracked files are never prune candidates");
1302
- } finally {
1303
- await fsp.rm(root, { recursive: true, force: true });
1304
- }
1305
- });
1306
-
1307
- test("upgrade --no-prune leaves upstream-deleted files in place", async () => {
1308
- const root = await makeAgentWithOrphans({ [ORPHAN]: "export const gone = 1;\n" });
1309
- try {
1310
- const r = runCli(["upgrade", "--no-prune"], root);
1311
- assert.equal(r.status, 0, r.stderr);
1312
- assert.ok(existsSync(join(root, ORPHAN)), "--no-prune must suppress removal entirely");
1313
- } finally {
1314
- await fsp.rm(root, { recursive: true, force: true });
1315
- }
1316
- });
1317
-
1318
- test("upgrade --dry-run reports a prune without performing it", async () => {
1319
- const root = await makeAgentWithOrphans({ [ORPHAN]: "export const gone = 1;\n" });
1320
- try {
1321
- const r = runCli(["upgrade", "--dry-run"], root);
1322
- assert.equal(r.status, 0, r.stderr);
1323
- assert.ok(existsSync(join(root, ORPHAN)), "dry run must not delete");
1324
- assert.ok(!existsSync(join(root, ".maestro/backup", ORPHAN)), "dry run must not write backups");
1325
- assert.match(r.stdout, /prune/i, "dry run must still tell the operator what it would remove");
1326
- } finally {
1327
- await fsp.rm(root, { recursive: true, force: true });
1328
- }
1329
- });
1330
-
1331
- test("upgrade honours .maestroignore over prune", async () => {
1332
- const root = await makeAgentWithOrphans({
1333
- [ORPHAN]: "export const gone = 1;\n",
1334
- ".maestroignore": `${ORPHAN}\n`,
1335
- });
1336
- try {
1337
- const r = runCli(["upgrade"], root);
1338
- assert.equal(r.status, 0, r.stderr);
1339
- assert.ok(existsSync(join(root, ORPHAN)), ".maestroignore must win over prune, as it does over overwrite");
1340
- } finally {
1341
- await fsp.rm(root, { recursive: true, force: true });
1342
- }
1343
- });
1344
-
1345
- test("upgrade prune leaves files that upstream still ships", async () => {
1346
- const root = await makeAgentWithOrphans({});
1347
- try {
1348
- const r = runCli(["upgrade"], root);
1349
- assert.equal(r.status, 0, r.stderr);
1350
- // A file the SDK genuinely ships must survive — the guard against a prune
1351
- // that mistakes "present upstream" for "absent" and empties the repo.
1352
- assert.ok(
1353
- existsSync(join(root, "lib/identity/persona.mjs")),
1354
- "currently-shipped framework files must never be pruned",
1355
- );
1356
- assert.ok(existsSync(join(root, "scripts/daemon/responder.mjs")));
1357
- } finally {
1358
- await fsp.rm(root, { recursive: true, force: true });
1359
- }
1360
- });
1361
-
1362
- test("upgrade prune never touches merge-mode agents/", async () => {
1363
- const custom = "agents/my-private-agent.md";
1364
- const root = await makeAgentWithOrphans({ [custom]: "# mine\n" });
1365
- try {
1366
- const r = runCli(["upgrade"], root);
1367
- assert.equal(r.status, 0, r.stderr);
1368
- assert.ok(
1369
- existsSync(join(root, custom)),
1370
- "agents/ is merge-mode: local-only files there are the point, not orphans",
1371
- );
1372
- } finally {
1373
- await fsp.rm(root, { recursive: true, force: true });
1374
- }
1375
- });
1376
-
1377
- test("upgrade prune never removes machine-generated launchd plists", async () => {
1378
- // A committed plist is tracked, clean, and absent upstream — it satisfies
1379
- // every safety check prune has, yet it is this machine's schedule, not
1380
- // framework material. Regression: an early prune deleted the very plist the
1381
- // cadence-bus migration was about to back up and regenerate.
1382
- const plist = "scripts/local-triggers/plists/ai.adaptic.some-trigger.plist";
1383
- const root = await makeAgentWithOrphans({ [plist]: "<?xml version=\"1.0\"?><plist/>" });
1384
- try {
1385
- const r = runCli(["upgrade"], root);
1386
- assert.equal(r.status, 0, r.stderr);
1387
- // Prune backs a file up under .maestro/backup/<path> before unlinking it
1388
- // and lists it as "pruned"; neither may happen here. (The file itself is
1389
- // gone after the run since WP-M6 — generate-plists.sh runs unconditionally
1390
- // as the first post-step and, as it always has, clears scripts/local-
1391
- // triggers/plists/ before rendering this machine's set. That is the
1392
- // generator owning its directory, not prune eating machine config.)
1393
- assert.ok(
1394
- !existsSync(join(root, ".maestro", "backup", plist)),
1395
- "generated plists are machine config; prune must leave them alone (no prune backup)",
1396
- );
1397
- assert.ok(!/pruned/.test(r.stdout), `prune must not report the plist:\n${r.stdout}`);
1398
- } finally {
1399
- await fsp.rm(root, { recursive: true, force: true });
1400
- }
1401
- });
1402
-
1403
- test("upgrade prune spares operator-authored files the framework never shipped", async () => {
1404
- // The hazard that forced the provenance manifest. scripts/ is a smart root,
1405
- // so an operator's own committed script there is tracked, clean and absent
1406
- // upstream — every signal git can offer says "orphan". On a real agent
1407
- // machine this set included the pre-commit hook guarding a live org
1408
- // credential. Provenance, not cleanliness, is what makes prune safe.
1409
- const mine = "scripts/pre-commit-secret-scan.sh";
1410
- const root = await makeAgentWithOrphans({
1411
- [mine]: "#!/bin/sh\nexit 0\n",
1412
- [ORPHAN]: "export const gone = 1;\n",
1413
- });
1414
- try {
1415
- // Manifest records ONLY the framework file — the operator's script was
1416
- // never delivered by the SDK, so it must not be a prune candidate.
1417
- writeFileSync(
1418
- join(root, ".maestro/shipped-manifest.json"),
1419
- JSON.stringify({ version: 1, files: [ORPHAN] }, null, 2) + "\n",
1420
- );
1421
- execFileSync("git", ["add", "-A"], { cwd: root });
1422
- execFileSync(
1423
- "git",
1424
- ["-c", "user.email=test@test", "-c", "user.name=test", "commit", "-q", "-m", "manifest"],
1425
- { cwd: root },
1426
- );
1427
-
1428
- const r = runCli(["upgrade"], root);
1429
- assert.equal(r.status, 0, r.stderr);
1430
- assert.ok(existsSync(join(root, mine)), "a file the framework never shipped is the operator's");
1431
- assert.ok(!existsSync(join(root, ORPHAN)), "the genuinely-shipped orphan should still prune");
1432
- } finally {
1433
- await fsp.rm(root, { recursive: true, force: true });
1434
- }
1435
- });
1436
-
1437
- test("upgrade prunes nothing on a machine with no shipped-manifest, then records one", async () => {
1438
- const root = await makeAgentWithOrphans({ [ORPHAN]: "export const gone = 1;\n" }, { seedManifest: false });
1439
- try {
1440
- const first = runCli(["upgrade"], root);
1441
- assert.equal(first.status, 0, first.stderr);
1442
- assert.ok(
1443
- existsSync(join(root, ORPHAN)),
1444
- "with no provenance record, prune must do nothing rather than guess",
1445
- );
1446
- assert.ok(
1447
- existsSync(join(root, ".maestro/shipped-manifest.json")),
1448
- "the first run must record provenance so the next one can prune",
1449
- );
1450
- const manifest = JSON.parse(readFileSync(join(root, ".maestro/shipped-manifest.json"), "utf-8"));
1451
- assert.ok(manifest.files.includes("scripts/daemon/responder.mjs"), "manifest lists shipped files");
1452
- assert.ok(!manifest.files.includes(ORPHAN), "manifest must not list what upstream doesn't ship");
1453
- } finally {
1454
- await fsp.rm(root, { recursive: true, force: true });
1455
- }
1456
- });
1457
-
1458
- test("upgrade --dry-run writes no shipped-manifest", async () => {
1459
- const root = await makeAgentWithOrphans({}, { seedManifest: false });
1460
- try {
1461
- const r = runCli(["upgrade", "--dry-run"], root);
1462
- assert.equal(r.status, 0, r.stderr);
1463
- assert.ok(
1464
- !existsSync(join(root, ".maestro/shipped-manifest.json")),
1465
- "--dry-run must stay side-effect-free, including provenance",
1466
- );
1467
- } finally {
1468
- await fsp.rm(root, { recursive: true, force: true });
1469
- }
1470
- });
1471
-
1472
- // ---------------------------------------------------------------------------
1473
- // upgrade — seat reconcile post-steps (WP-M6): the first hop from an old
1474
- // version must leave the machine wholly on the new architecture by itself.
1475
- // ---------------------------------------------------------------------------
1476
-
1477
- /** makeLegacyAgent + a NAMED config/agent.json, so plist labels and the identity block use "legacy". */
1478
- async function makeNamedLegacyAgent() {
1479
- const root = await makeLegacyAgent();
1480
- writeFileSync(join(root, "config/agent.json"), JSON.stringify({ firstName: "Legacy", lastName: "Tester", fullName: "Legacy Tester" }, null, 2));
1481
- execFileSync("git", ["add", "-A"], { cwd: root });
1482
- execFileSync("git", ["-c", "user.email=test@test", "-c", "user.name=test", "commit", "-q", "-m", "name"], { cwd: root });
1483
- return root;
1484
- }
1485
-
1486
- test("upgrade installs the -session plist into $HOME/Library/LaunchAgents (600, bootstrapped), never kickstarts daemon/session, records sdkVersion in the manifest and writes .maestro/upgrade-result.json", async () => {
1487
- const root = await makeNamedLegacyAgent();
1488
- const home = await freshHome();
1489
- try {
1490
- // An existing seat: the daemon job is installed (old content) and loaded.
1491
- writeFileSync(join(home, "Library", "LaunchAgents", "ai.maestro.legacy-daemon.plist"), "<plist>old daemon</plist>");
1492
- const r = runCli(["upgrade"], root, baseEnv({ HOME: home }));
1493
- assert.equal(r.status, 0, r.stderr + r.stdout);
1494
-
1495
- const la = join(home, "Library", "LaunchAgents");
1496
- const sessionPlist = join(la, "ai.maestro.legacy-session.plist");
1497
- assert.ok(existsSync(sessionPlist), `session plist must be installed; stdout:\n${r.stdout}`);
1498
- assert.equal(statSync(sessionPlist).mode & 0o777, 0o600);
1499
- assert.match(readFileSync(sessionPlist, "utf-8"), /scripts\/session\/supervisor\.sh/);
1500
- assert.match(readFileSync(join(la, "ai.maestro.legacy-daemon.plist"), "utf-8"), /launchd-wrapper\.sh/, "daemon plist rewritten to the generated one");
1501
-
1502
- const calls = readFileSync(join(home, "launchctl.log"), "utf-8");
1503
- assert.match(calls, new RegExp(`^launchctl bootstrap gui/\\d+ ${sessionPlist.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`, "m"), calls);
1504
- assert.ok(!/kickstart/.test(calls), `nothing is kickstarted by upgrade (the -autoupdate job is the one it runs inside):\n${calls}`);
1505
- assert.ok(!/bootout|unload/.test(calls), calls);
1506
- assert.match(calls, /^launchctl print gui\/\d+$/m, "loaded-ness is read from the gui domain");
1507
-
1508
- // The claude STUB handled `mcp add-json` — not the developer's real CLI.
1509
- // lib/claude-bin.mjs would otherwise pick /usr/local/bin/claude over PATH;
1510
- // CLAUDE_BIN in baseEnv is what makes this isolation real.
1511
- assert.ok(existsSync(join(home, "claude.log")), "the claude stub must be the binary global-setup reached");
1512
- assert.match(readFileSync(join(home, "claude.log"), "utf-8"), /^claude mcp add-json cohort .* --scope user$/m);
1513
- const claudeJson = JSON.parse(readFileSync(join(home, ".claude.json"), "utf-8"));
1514
- assert.equal(claudeJson.mcpServers.cohort.env.COHORT_AGENT_ROOT.replace(/^\/private/, ""), root.replace(/^\/private/, ""), "stub exit 1 → the JSON merge fallback registered the server");
1515
- assert.ok(!("firstStartVersion" in claudeJson), "a real `claude` would have stamped its own keys into ~/.claude.json");
1516
- assert.ok(!existsSync(join(home, "npm.log")) || !/npm install -g/.test(readFileSync(join(home, "npm.log"), "utf-8")), "no global install under MAESTRO_SKIP_GLOBAL_INSTALL=1");
1517
-
1518
- const manifest = JSON.parse(readFileSync(join(root, ".maestro/shipped-manifest.json"), "utf-8"));
1519
- const sdkVersion = JSON.parse(readFileSync(join(MAESTRO_ROOT, "package.json"), "utf-8")).version;
1520
- assert.equal(manifest.sdkVersion, sdkVersion);
1521
- assert.equal(manifest.version, 1, "the manifest FORMAT version is untouched");
1522
-
1523
- const result = JSON.parse(readFileSync(join(root, ".maestro/upgrade-result.json"), "utf-8"));
1524
- assert.equal(result.to, sdkVersion);
1525
- assert.equal(result.from, null, "no prior manifest → from is unknown");
1526
- assert.match(result.at, /^\d{4}-\d{2}-\d{2}T/);
1527
- assert.deepEqual(Object.keys(result.steps), ["plists", "launchd", "globalSetup", "globalInstall", "verify"]);
1528
- assert.equal(result.steps.plists.ok, true, result.steps.plists.detail);
1529
- assert.equal(result.steps.launchd.ok, true, result.steps.launchd.detail);
1530
- assert.equal(result.steps.globalSetup.ok, true, result.steps.globalSetup.detail);
1531
- assert.match(result.steps.globalInstall.detail, /MAESTRO_SKIP_GLOBAL_INSTALL/);
1532
- assert.match(r.stdout, /Seat reconcile/);
1533
-
1534
- // global-setup ran for real against the throwaway HOME.
1535
- assert.match(readFileSync(join(home, ".claude", "CLAUDE.md"), "utf-8"), /BEGIN maestro:identity/);
1536
- assert.ok(existsSync(join(home, ".claude", "skills", "maestro-main-session", "SKILL.md")));
1537
-
1538
- // Second run: from is the recorded version, the session job is unchanged.
1539
- const again = runCli(["upgrade"], root, baseEnv({ HOME: home }));
1540
- assert.equal(again.status, 0, again.stderr);
1541
- const result2 = JSON.parse(readFileSync(join(root, ".maestro/upgrade-result.json"), "utf-8"));
1542
- assert.equal(result2.from, sdkVersion);
1543
- assert.match(result2.steps.launchd.detail, /0 installed, 0 replaced/);
1544
- assert.match(result.steps.launchd.detail, /1 replaced/, "the pre-installed daemon plist is rewritten in place");
1545
- } finally { await rmRoot(root); await rmRoot(home); }
1546
- });
1547
-
1548
- test("upgrade --verify exits 0 on a reconciled seat and 1 once a generated plist is missing; doctor prints the same rows; --dry-run runs no post-steps", async () => {
1549
- const root = await makeNamedLegacyAgent();
1550
- const home = await freshHome();
1551
- try {
1552
- const dry = runCli(["upgrade", "--dry-run"], root, baseEnv({ HOME: home }));
1553
- assert.equal(dry.status, 0, dry.stderr);
1554
- assert.ok(!existsSync(join(root, ".maestro/upgrade-result.json")), "--dry-run writes no result");
1555
- assert.ok(!existsSync(join(home, "Library", "LaunchAgents", "ai.maestro.legacy-session.plist")), "--dry-run installs nothing");
1556
-
1557
- assert.equal(runCli(["upgrade"], root, baseEnv({ HOME: home })).status, 0);
1558
- const v = runCli(["upgrade", "--verify"], root, baseEnv({ HOME: home }));
1559
- assert.equal(v.status, 0, `verify should pass on a reconciled seat:\n${v.stdout}${v.stderr}`);
1560
- assert.match(v.stdout, /generated plist\(s\) installed and loaded/);
1561
- assert.match(v.stdout, /identity, MCP, settings, pointer and skills present/);
1562
- assert.match(v.stdout, /verify: seat is on/);
1563
-
1564
- execFileSync("rm", [join(home, "Library", "LaunchAgents", "ai.maestro.legacy-session.plist")]);
1565
- const bad = runCli(["upgrade", "--verify"], root, baseEnv({ HOME: home }));
1566
- assert.equal(bad.status, 1);
1567
- assert.match(bad.stdout, /not installed: ai\.maestro\.legacy-session/);
1568
- assert.match(bad.stdout, /verify: MISMATCH \(plists\)/);
1569
-
1570
- const d = runCli(["doctor"], root, baseEnv({ HOME: home }));
1571
- // doctor's fail() rows go to stderr; ok/warn to stdout.
1572
- assert.match(d.stdout + d.stderr, /not installed: ai\.maestro\.legacy-session/, `doctor carries the verify rows:\n${d.stdout.slice(-1500)}\n${d.stderr.slice(-1500)}`);
1573
- } finally { await rmRoot(root); await rmRoot(home); }
1574
- });