@cohortapp/agent-sdk 2.17.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 (526) hide show
  1. package/.claude/settings.json +18 -0
  2. package/.env.example +18 -5
  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/setup-wizard.md +1 -1
  9. package/docs/runbooks/fleet-rollout.md +156 -0
  10. package/docs/runbooks/mac-mini-bootstrap.md +12 -14
  11. package/lib/action-executor.js +19 -3
  12. package/lib/budget-guard.mjs +279 -3
  13. package/lib/channels/base-adapter.mjs +3 -1
  14. package/lib/channels/contract.mjs +2 -1
  15. package/lib/channels/inbox-item.mjs +8 -0
  16. package/lib/claude-bin.mjs +5 -6
  17. package/lib/cli/doctor-checks.mjs +141 -10
  18. package/lib/cli/global-setup-extras.mjs +5 -1
  19. package/lib/cli/inbox.mjs +100 -15
  20. package/lib/cli/seat-auth.mjs +463 -0
  21. package/lib/cli/session.mjs +80 -12
  22. package/lib/collective/capture.mjs +8 -6
  23. package/lib/collective/global-config.mjs +63 -1
  24. package/lib/collective/presence.mjs +142 -5
  25. package/lib/comms/send-gate.mjs +559 -1
  26. package/lib/diagnostics/alerts.mjs +49 -0
  27. package/lib/diagnostics/cadence-output-freshness.mjs +288 -0
  28. package/lib/engine/agents/definitions.mjs +343 -0
  29. package/lib/engine/agents/persist.mjs +275 -0
  30. package/lib/engine/agents/runtime.mjs +748 -0
  31. package/lib/engine/agents/usage.mjs +95 -0
  32. package/lib/engine/auth-status.mjs +139 -0
  33. package/lib/engine/budget.mjs +194 -0
  34. package/lib/engine/cli.mjs +1204 -0
  35. package/lib/engine/commands/index.mjs +269 -0
  36. package/lib/engine/context/budget.mjs +219 -0
  37. package/lib/engine/context/cache.mjs +125 -0
  38. package/lib/engine/context/child-env.mjs +215 -0
  39. package/lib/engine/context/compaction.mjs +342 -0
  40. package/lib/engine/context/images.mjs +90 -0
  41. package/lib/engine/context/instructions.mjs +327 -0
  42. package/lib/engine/context/lazy-instructions.mjs +169 -0
  43. package/lib/engine/context/manager.mjs +182 -0
  44. package/lib/engine/context/real-path.mjs +91 -0
  45. package/lib/engine/context/secret-values.mjs +163 -0
  46. package/lib/engine/context/settings.mjs +274 -0
  47. package/lib/engine/context/stream-input.mjs +159 -0
  48. package/lib/engine/guard.mjs +152 -0
  49. package/lib/engine/hooks.mjs +713 -0
  50. package/lib/engine/loop.mjs +560 -0
  51. package/lib/engine/mcp/client.mjs +254 -0
  52. package/lib/engine/mcp/config.mjs +301 -0
  53. package/lib/engine/mcp/http.mjs +201 -0
  54. package/lib/engine/mcp/index.mjs +146 -0
  55. package/lib/engine/mcp/jsonrpc.mjs +147 -0
  56. package/lib/engine/mcp/naming.mjs +66 -0
  57. package/lib/engine/mcp/resources.mjs +89 -0
  58. package/lib/engine/mcp/results.mjs +133 -0
  59. package/lib/engine/mcp/stdio.mjs +137 -0
  60. package/lib/engine/mcp/supervisor.mjs +116 -0
  61. package/lib/engine/messages.mjs +104 -0
  62. package/lib/engine/output/json.mjs +164 -0
  63. package/lib/engine/output/stream-json.mjs +266 -0
  64. package/lib/engine/permissions.mjs +845 -0
  65. package/lib/engine/process-identity.mjs +164 -0
  66. package/lib/engine/process-tree.mjs +551 -0
  67. package/lib/engine/prompt.mjs +60 -0
  68. package/lib/engine/session/store.mjs +299 -0
  69. package/lib/engine/session-runtime/args.mjs +97 -0
  70. package/lib/engine/session-runtime/host.mjs +143 -0
  71. package/lib/engine/session-runtime/inbox.mjs +122 -0
  72. package/lib/engine/session-runtime/notifications.mjs +129 -0
  73. package/lib/engine/session-runtime/registry.mjs +328 -0
  74. package/lib/engine/session-runtime/runner.mjs +344 -0
  75. package/lib/engine/session-runtime/socket.mjs +212 -0
  76. package/lib/engine/session-runtime/wakeup.mjs +115 -0
  77. package/lib/engine/skills/index.mjs +321 -0
  78. package/lib/engine/tools/bash-background.mjs +533 -0
  79. package/lib/engine/tools/bash.mjs +216 -0
  80. package/lib/engine/tools/edit.mjs +97 -0
  81. package/lib/engine/tools/glob.mjs +81 -0
  82. package/lib/engine/tools/grep.mjs +224 -0
  83. package/lib/engine/tools/index.mjs +84 -0
  84. package/lib/engine/tools/list-agents.mjs +32 -0
  85. package/lib/engine/tools/ls.mjs +127 -0
  86. package/lib/engine/tools/monitor.mjs +82 -0
  87. package/lib/engine/tools/notebook-edit.mjs +218 -0
  88. package/lib/engine/tools/read.mjs +103 -0
  89. package/lib/engine/tools/schedule-wakeup.mjs +45 -0
  90. package/lib/engine/tools/schema.mjs +144 -0
  91. package/lib/engine/tools/send-message.mjs +77 -0
  92. package/lib/engine/tools/session.mjs +70 -0
  93. package/lib/engine/tools/todo.mjs +144 -0
  94. package/lib/engine/tools/toolsearch.mjs +217 -0
  95. package/lib/engine/tools/walk.mjs +193 -0
  96. package/lib/engine/tools/web-switch.mjs +31 -0
  97. package/lib/engine/tools/webfetch-html.mjs +387 -0
  98. package/lib/engine/tools/webfetch-net.mjs +340 -0
  99. package/lib/engine/tools/webfetch.mjs +198 -0
  100. package/lib/engine/tools/websearch.mjs +91 -0
  101. package/lib/engine/tools/workflow.mjs +95 -0
  102. package/lib/engine/tools/write.mjs +76 -0
  103. package/lib/engine/tui/line-editor.mjs +137 -0
  104. package/lib/engine/tui/render.mjs +86 -0
  105. package/lib/engine/tui/tui.mjs +274 -0
  106. package/lib/engine/wire/anthropic-messages.mjs +263 -0
  107. package/lib/engine/wire/effort.mjs +36 -0
  108. package/lib/engine/wire/errors.mjs +496 -0
  109. package/lib/engine/wire/http.mjs +441 -0
  110. package/lib/engine/wire/index.mjs +76 -0
  111. package/lib/engine/wire/openai-chat.mjs +332 -0
  112. package/lib/engine/wire/prompt-cache.mjs +79 -0
  113. package/lib/engine/wire/search.mjs +140 -0
  114. package/lib/engine/wire/sse.mjs +114 -0
  115. package/lib/engine/wire/stall.mjs +349 -0
  116. package/lib/engine/wire/token-provider.mjs +175 -0
  117. package/lib/engine/wire/usage.mjs +192 -0
  118. package/lib/engine/workflow/host.mjs +524 -0
  119. package/lib/engine/workflow/journal.mjs +188 -0
  120. package/lib/engine/workflow/json-schema.mjs +171 -0
  121. package/lib/engine/workflow/meta.mjs +329 -0
  122. package/lib/engine/workflow/notifications.mjs +52 -0
  123. package/lib/engine/workflow/runtime.mjs +447 -0
  124. package/lib/engine/workflow/sandbox.mjs +534 -0
  125. package/lib/engine/workflow/worker.mjs +141 -0
  126. package/lib/engine/workflow/worktree.mjs +74 -0
  127. package/lib/execution/disposition.mjs +1 -1
  128. package/lib/execution/intake.mjs +10 -0
  129. package/lib/execution/surface-policy.mjs +15 -0
  130. package/lib/learning/curator.mjs +8 -6
  131. package/lib/learning/reflect.mjs +8 -6
  132. package/lib/model-router/catalog/cohort.yaml +137 -0
  133. package/lib/model-router/catalog.mjs +118 -1
  134. package/lib/model-router/failover.mjs +67 -16
  135. package/lib/model-router/llm-task.mjs +39 -3
  136. package/lib/model-router/resolve.mjs +89 -3
  137. package/lib/model-router/spawn.mjs +46 -47
  138. package/lib/model-router/taxonomy.mjs +126 -4
  139. package/lib/org/cost-sync.mjs +141 -11
  140. package/lib/org/inbound/broadcast.mjs +289 -0
  141. package/lib/org/inbound/collective.mjs +375 -0
  142. package/lib/org/inbound/directedness.mjs +96 -8
  143. package/lib/org/inbound/facts.mjs +78 -2
  144. package/lib/org/inbound/project.mjs +22 -0
  145. package/lib/org/inbound/surfaces.mjs +14 -0
  146. package/lib/org/llm-token.mjs +879 -0
  147. package/lib/org/mesh.mjs +61 -0
  148. package/lib/org/messaging.mjs +3 -1
  149. package/lib/org/protocol.checksum +1 -1
  150. package/lib/org/protocol.mjs +15 -0
  151. package/lib/org/quota.mjs +520 -0
  152. package/lib/org/tool-surface.mjs +104 -16
  153. package/lib/org/ui-parity.mjs +16 -1
  154. package/lib/org/work-ledger.mjs +37 -6
  155. package/lib/rate-guard.mjs +114 -1
  156. package/lib/resource-governor.mjs +41 -6
  157. package/lib/runtime/adapter.mjs +823 -0
  158. package/lib/runtime/child-env.mjs +191 -0
  159. package/lib/runtime/legacy-shell-guard.mjs +97 -0
  160. package/lib/runtime/seat-engine.mjs +162 -0
  161. package/lib/session/ask-ledger.mjs +271 -0
  162. package/lib/session/current-work.mjs +676 -0
  163. package/lib/session/feed-core.mjs +40 -3
  164. package/lib/session/launch-args.mjs +56 -4
  165. package/lib/session/status-summary.mjs +26 -9
  166. package/lib/session/upgrade-notice.mjs +42 -0
  167. package/lib/setup/claude-probe.mjs +117 -13
  168. package/lib/setup/enrich.mjs +13 -10
  169. package/lib/setup/sections/model.mjs +39 -13
  170. package/lib/telemetry/collect.mjs +208 -9
  171. package/lib/upgrade/ignored-drift.mjs +105 -0
  172. package/lib/voice/post-call-brief.mjs +30 -17
  173. package/package.json +13 -3
  174. package/plugins/maestro-skills/skills/board-work.md +5 -0
  175. package/plugins/maestro-skills/skills/inbound-triage.md +56 -15
  176. package/plugins/maestro-skills/skills/main-session.md +18 -7
  177. package/scripts/ci/check-tarball-fidelity.mjs +126 -2
  178. package/scripts/ci/run-tests.mjs +47 -19
  179. package/scripts/cohort-llm/api-key-helper.mjs +92 -0
  180. package/scripts/collective/hook-runner.mjs +29 -2
  181. package/scripts/continuous-monitor.sh +13 -0
  182. package/scripts/cost/track-claude-usage.mjs +15 -0
  183. package/scripts/daemon/agent-daemon.mjs +408 -20
  184. package/scripts/daemon/assurance.mjs +48 -12
  185. package/scripts/daemon/cadence-consumer.mjs +218 -68
  186. package/scripts/daemon/cadence-handlers.mjs +73 -4
  187. package/scripts/daemon/classifier.mjs +75 -26
  188. package/scripts/daemon/context-compiler.mjs +51 -37
  189. package/scripts/daemon/deliver.mjs +30 -1
  190. package/scripts/daemon/dispatcher.mjs +595 -149
  191. package/scripts/daemon/health.mjs +14 -1
  192. package/scripts/daemon/maestro-daemon.mjs +11 -0
  193. package/scripts/daemon/prompt-builder.mjs +24 -0
  194. package/scripts/daemon/responder.mjs +246 -79
  195. package/scripts/daemon/sdk-version.mjs +98 -16
  196. package/scripts/eval/probe-gateway.mjs +635 -0
  197. package/scripts/eval/replay/extract.mjs +270 -0
  198. package/scripts/eval/replay/grade.mjs +260 -0
  199. package/scripts/eval/replay/lib/config.mjs +50 -0
  200. package/scripts/eval/replay/lib/effects.mjs +65 -0
  201. package/scripts/eval/replay/lib/fixture.mjs +188 -0
  202. package/scripts/eval/replay/lib/judge.mjs +72 -0
  203. package/scripts/eval/replay/lib/redact.mjs +136 -0
  204. package/scripts/eval/replay/lib/sandbox.mjs +170 -0
  205. package/scripts/eval/replay/lib/schema-check.mjs +63 -0
  206. package/scripts/eval/replay/lib/transcript.mjs +76 -0
  207. package/scripts/eval/replay/mcp-replay-stub.mjs +101 -0
  208. package/scripts/eval/replay/report.mjs +185 -0
  209. package/scripts/eval/replay/run.mjs +404 -0
  210. package/scripts/fleet/rollout.mjs +1094 -0
  211. package/scripts/hooks/pre-send-audit.sh +36 -245
  212. package/scripts/hooks/pre-write-yaml-validate.mjs +275 -0
  213. package/scripts/hooks/validate-state-yaml.sh +190 -0
  214. package/scripts/huddle/huddle-llm.mjs +361 -0
  215. package/scripts/huddle/huddle-server.mjs +46 -121
  216. package/scripts/local-triggers/autoupdate.sh +448 -78
  217. package/scripts/local-triggers/run-trigger.sh +13 -0
  218. package/scripts/maintenance/pin-integrity.mjs +364 -0
  219. package/scripts/poll-slack-events.sh +41 -9
  220. package/scripts/poller/slack-socket-mode.mjs +28 -3
  221. package/scripts/session/supervisor.mjs +80 -13
  222. package/scripts/spawn-session.sh +13 -0
  223. package/bin/maestro.test.mjs +0 -1574
  224. package/lib/action-executor.test.mjs +0 -871
  225. package/lib/archetype.test.mjs +0 -132
  226. package/lib/assurance/plan-note.test.mjs +0 -234
  227. package/lib/assurance/room-budget.test.mjs +0 -486
  228. package/lib/assurance/tier.test.mjs +0 -174
  229. package/lib/autonomy.test.mjs +0 -66
  230. package/lib/backlog.test.mjs +0 -302
  231. package/lib/backup/policy.test.mjs +0 -305
  232. package/lib/budget-escalate.test.mjs +0 -232
  233. package/lib/budget-guard.envelope.test.mjs +0 -476
  234. package/lib/budget-guard.test.mjs +0 -427
  235. package/lib/cadence-bus-requeue.test.mjs +0 -83
  236. package/lib/cadence-bus-schedule.test.mjs +0 -194
  237. package/lib/cadence-bus.test.mjs +0 -720
  238. package/lib/cadences.test.mjs +0 -230
  239. package/lib/capability/inventory.test.mjs +0 -232
  240. package/lib/capability.test.mjs +0 -78
  241. package/lib/channels/base-adapter.test.mjs +0 -590
  242. package/lib/channels/channels.test.mjs +0 -371
  243. package/lib/channels/contract.test.mjs +0 -162
  244. package/lib/channels/inbox-item.test.mjs +0 -368
  245. package/lib/channels/orgmail/adapter.test.mjs +0 -448
  246. package/lib/channels/pairing.test.mjs +0 -270
  247. package/lib/channels/repeat-suppressor.test.mjs +0 -134
  248. package/lib/channels/slack-adapter.test.mjs +0 -212
  249. package/lib/channels/telegram-adapter.test.mjs +0 -306
  250. package/lib/channels/voice/adapter.test.mjs +0 -278
  251. package/lib/channels/whatsapp/adapter-baileys.test.mjs +0 -359
  252. package/lib/channels/whatsapp/baileys-typing.test.mjs +0 -154
  253. package/lib/charter.test.mjs +0 -89
  254. package/lib/claude-bin.test.mjs +0 -131
  255. package/lib/cli/board.test.mjs +0 -227
  256. package/lib/cli/design.test.mjs +0 -270
  257. package/lib/cli/doctor-checks.test.mjs +0 -336
  258. package/lib/cli/global-setup-extras.test.mjs +0 -462
  259. package/lib/cli/inbox.test.mjs +0 -230
  260. package/lib/cli/session-ack.test.mjs +0 -63
  261. package/lib/cli/session.test.mjs +0 -613
  262. package/lib/collective/capture.test.mjs +0 -121
  263. package/lib/collective/cards.test.mjs +0 -114
  264. package/lib/collective/config.test.mjs +0 -123
  265. package/lib/collective/global-config.test.mjs +0 -220
  266. package/lib/collective/global-skills.test.mjs +0 -126
  267. package/lib/collective/presence.test.mjs +0 -95
  268. package/lib/collective/recall.test.mjs +0 -116
  269. package/lib/collective/vendor-skills.test.mjs +0 -306
  270. package/lib/comms/send-gate.test.mjs +0 -770
  271. package/lib/comms.test.mjs +0 -41
  272. package/lib/context/budget.test.mjs +0 -252
  273. package/lib/context/history-scope.test.mjs +0 -79
  274. package/lib/cost/ledger-row.test.mjs +0 -183
  275. package/lib/design/design-md.test.mjs +0 -318
  276. package/lib/design/fixtures/DESIGN.golden.md +0 -238
  277. package/lib/design/fixtures/PRODUCT.golden.md +0 -67
  278. package/lib/design/fixtures/foundation.json +0 -133
  279. package/lib/design/refresh-gate.test.mjs +0 -144
  280. package/lib/design/write.test.mjs +0 -241
  281. package/lib/diagnostics/alerts.test.mjs +0 -318
  282. package/lib/diagnostics/backup-freshness.test.mjs +0 -185
  283. package/lib/diagnostics/counters.test.mjs +0 -206
  284. package/lib/diagnostics/events.test.mjs +0 -290
  285. package/lib/diagnostics/otel.test.mjs +0 -196
  286. package/lib/diagnostics/trace.test.mjs +0 -251
  287. package/lib/env-compat.test.mjs +0 -104
  288. package/lib/execution/disposition.test.mjs +0 -553
  289. package/lib/execution/drive.test.mjs +0 -270
  290. package/lib/execution/effects.test.mjs +0 -344
  291. package/lib/execution/intake.test.mjs +0 -389
  292. package/lib/execution/journal.test.mjs +0 -261
  293. package/lib/execution/match.test.mjs +0 -235
  294. package/lib/execution/pipeline.test.mjs +0 -392
  295. package/lib/execution/route.test.mjs +0 -186
  296. package/lib/execution/surface-policy.test.mjs +0 -162
  297. package/lib/fs-atomic.test.mjs +0 -72
  298. package/lib/fs-ownership.test.mjs +0 -158
  299. package/lib/goals/admission.test.mjs +0 -164
  300. package/lib/goals/classify.test.mjs +0 -167
  301. package/lib/goals/collaborate.test.mjs +0 -336
  302. package/lib/goals/gaps.test.mjs +0 -284
  303. package/lib/goals/loop.test.mjs +0 -845
  304. package/lib/hooks/bus.test.mjs +0 -387
  305. package/lib/identity/persona.test.mjs +0 -142
  306. package/lib/kpi-sensors.test.mjs +0 -278
  307. package/lib/kpi.test.mjs +0 -244
  308. package/lib/learning/config.test.mjs +0 -75
  309. package/lib/learning/counters.test.mjs +0 -69
  310. package/lib/learning/curator-consolidate.test.mjs +0 -238
  311. package/lib/learning/curator.test.mjs +0 -106
  312. package/lib/learning/reflect.test.mjs +0 -0
  313. package/lib/learning/session-index.test.mjs +0 -125
  314. package/lib/learning/skill-writer.test.mjs +0 -210
  315. package/lib/mandate/audit.test.mjs +0 -195
  316. package/lib/mandate/contract.test.mjs +0 -185
  317. package/lib/mandate/derive.test.mjs +0 -274
  318. package/lib/mandate/model.test.mjs +0 -164
  319. package/lib/mandate/refresh.test.mjs +0 -389
  320. package/lib/mcp/server.test.mjs +0 -426
  321. package/lib/model-router/auth-profiles.test.mjs +0 -580
  322. package/lib/model-router/catalog.test.mjs +0 -385
  323. package/lib/model-router/economics.test.mjs +0 -438
  324. package/lib/model-router/failover.test.mjs +0 -439
  325. package/lib/model-router/health.test.mjs +0 -338
  326. package/lib/model-router/integration-coverage.test.mjs +0 -831
  327. package/lib/model-router/integration.test.mjs +0 -564
  328. package/lib/model-router/ledger.test.mjs +0 -415
  329. package/lib/model-router/llm-task.test.mjs +0 -392
  330. package/lib/model-router/org-credentials.test.mjs +0 -265
  331. package/lib/model-router/pricing-refresh.test.mjs +0 -286
  332. package/lib/model-router/reconcile.test.mjs +0 -316
  333. package/lib/model-router/repair.test.mjs +0 -180
  334. package/lib/model-router/spawn.test.mjs +0 -446
  335. package/lib/model-router/taxonomy.test.mjs +0 -410
  336. package/lib/model-router.test.mjs +0 -1207
  337. package/lib/org/activity.test.mjs +0 -134
  338. package/lib/org/approvals.test.mjs +0 -216
  339. package/lib/org/awareness.test.mjs +0 -159
  340. package/lib/org/board-mine-cache.test.mjs +0 -53
  341. package/lib/org/board.test.mjs +0 -187
  342. package/lib/org/bootstrap-context.test.mjs +0 -153
  343. package/lib/org/client.test.mjs +0 -1206
  344. package/lib/org/cohort-client.test.mjs +0 -126
  345. package/lib/org/cost-sync.test.mjs +0 -153
  346. package/lib/org/doctor.test.mjs +0 -346
  347. package/lib/org/engagement-ledger.test.mjs +0 -112
  348. package/lib/org/engagement.test.mjs +0 -739
  349. package/lib/org/handoff.test.mjs +0 -269
  350. package/lib/org/inbound/directedness.test.mjs +0 -668
  351. package/lib/org/inbound/facts.test.mjs +0 -471
  352. package/lib/org/inbound/hydrate.test.mjs +0 -908
  353. package/lib/org/inbound/index.test.mjs +0 -429
  354. package/lib/org/inbound/project.test.mjs +0 -287
  355. package/lib/org/integration-tools.test.mjs +0 -160
  356. package/lib/org/keys.test.mjs +0 -92
  357. package/lib/org/knowledge.test.mjs +0 -326
  358. package/lib/org/leases.test.mjs +0 -235
  359. package/lib/org/mesh-directives.test.mjs +0 -110
  360. package/lib/org/mesh-integration.test.mjs +0 -127
  361. package/lib/org/mesh.test.mjs +0 -400
  362. package/lib/org/messaging.test.mjs +0 -471
  363. package/lib/org/param-contract.test.mjs +0 -477
  364. package/lib/org/policy.test.mjs +0 -237
  365. package/lib/org/protocol.checksum.test.mjs +0 -90
  366. package/lib/org/protocol.test.mjs +0 -323
  367. package/lib/org/push.test.mjs +0 -792
  368. package/lib/org/registry.test.mjs +0 -100
  369. package/lib/org/resource-tools.test.mjs +0 -361
  370. package/lib/org/tool-access.test.mjs +0 -144
  371. package/lib/org/tool-surface-integration.test.mjs +0 -120
  372. package/lib/org/tool-surface.test.mjs +0 -1268
  373. package/lib/org/typing.test.mjs +0 -291
  374. package/lib/org/ui-parity.test.mjs +0 -560
  375. package/lib/org/verify.test.mjs +0 -194
  376. package/lib/org/work-ledger.test.mjs +0 -273
  377. package/lib/plan/adoption-e2e.test.mjs +0 -366
  378. package/lib/plan/budget-enforcement.test.mjs +0 -400
  379. package/lib/plan/compile.test.mjs +0 -382
  380. package/lib/plan/emit.test.mjs +0 -269
  381. package/lib/plan/explain.test.mjs +0 -188
  382. package/lib/prompts/parallelism.test.mjs +0 -177
  383. package/lib/rag/rag.test.mjs +0 -505
  384. package/lib/rate-guard.test.mjs +0 -272
  385. package/lib/reactive-gate.test.mjs +0 -57
  386. package/lib/render.test.mjs +0 -68
  387. package/lib/resource-governor.test.mjs +0 -488
  388. package/lib/scheduling/dynamic-jobs.test.mjs +0 -344
  389. package/lib/scheduling/jitter.test.mjs +0 -140
  390. package/lib/secrets/broker.test.mjs +0 -280
  391. package/lib/secrets/providers.test.mjs +0 -274
  392. package/lib/security/audit-engine.test.mjs +0 -424
  393. package/lib/security/coerce-args.test.mjs +0 -281
  394. package/lib/security/dangerous-tools.test.mjs +0 -68
  395. package/lib/security/external-content.test.mjs +0 -84
  396. package/lib/security/redact.test.mjs +0 -441
  397. package/lib/security/secret-equal.test.mjs +0 -55
  398. package/lib/session/config.test.mjs +0 -92
  399. package/lib/session/feed-core.test.mjs +0 -198
  400. package/lib/session/first-run.test.mjs +0 -121
  401. package/lib/session/frontdoor.test.mjs +0 -205
  402. package/lib/session/handoffs.test.mjs +0 -183
  403. package/lib/session/identity.test.mjs +0 -180
  404. package/lib/session/inbox-claims.test.mjs +0 -286
  405. package/lib/session/launch-args.test.mjs +0 -157
  406. package/lib/session/liveness.test.mjs +0 -100
  407. package/lib/session/status-summary.test.mjs +0 -118
  408. package/lib/session-permissions.test.mjs +0 -120
  409. package/lib/setup/claude-probe.test.mjs +0 -187
  410. package/lib/setup/completeness.test.mjs +0 -110
  411. package/lib/setup/context-pack.test.mjs +0 -89
  412. package/lib/setup/enrich.test.mjs +0 -115
  413. package/lib/setup/enroll-from-cohort.test.mjs +0 -300
  414. package/lib/setup/integration.test.mjs +0 -162
  415. package/lib/setup/io.test.mjs +0 -77
  416. package/lib/setup/runner.test.mjs +0 -132
  417. package/lib/setup/sections/identity.test.mjs +0 -234
  418. package/lib/setup/sections/inventory.test.mjs +0 -198
  419. package/lib/setup/sections/learning.test.mjs +0 -81
  420. package/lib/setup/sections/mandate.test.mjs +0 -388
  421. package/lib/setup/sections/messaging.test.mjs +0 -127
  422. package/lib/setup/sections/model.test.mjs +0 -240
  423. package/lib/setup/sections/org.test.mjs +0 -346
  424. package/lib/setup/sections/orgmail.test.mjs +0 -118
  425. package/lib/setup/sections/recovery.test.mjs +0 -98
  426. package/lib/setup/sections/subagents.test.mjs +0 -429
  427. package/lib/setup/sections/verify.test.mjs +0 -175
  428. package/lib/setup/sot.test.mjs +0 -81
  429. package/lib/setup/state.test.mjs +0 -115
  430. package/lib/singleton.test.mjs +0 -151
  431. package/lib/subagents/cli.test.mjs +0 -389
  432. package/lib/subagents/client.test.mjs +0 -309
  433. package/lib/subagents/gap.test.mjs +0 -234
  434. package/lib/subagents/lock.test.mjs +0 -248
  435. package/lib/subagents/manifest.test.mjs +0 -175
  436. package/lib/subagents/refs.test.mjs +0 -204
  437. package/lib/subagents/resolve.test.mjs +0 -422
  438. package/lib/subagents/schema.test.mjs +0 -328
  439. package/lib/telemetry/alerts.test.mjs +0 -109
  440. package/lib/telemetry/collect.test.mjs +0 -1274
  441. package/lib/tool-definitions-integration.test.mjs +0 -83
  442. package/lib/tool-definitions.test.mjs +0 -437
  443. package/lib/upgrade/global-refresh.test.mjs +0 -65
  444. package/lib/upgrade/launchd-reconcile.test.mjs +0 -272
  445. package/lib/upgrade/post-steps.test.mjs +0 -200
  446. package/lib/upgrade/verify.test.mjs +0 -164
  447. package/lib/util/fetch-timeout.test.mjs +0 -202
  448. package/lib/util/reconnect.test.mjs +0 -369
  449. package/lib/util/unhandled.test.mjs +0 -216
  450. package/lib/voice/outbound.test.mjs +0 -69
  451. package/lib/voice/session-rotation.test.mjs +0 -114
  452. package/lib/voice/stt.test.mjs +0 -226
  453. package/lib/voice/voice.test.mjs +0 -990
  454. package/scripts/cadence/enqueue-cadence-tick.test.mjs +0 -187
  455. package/scripts/ci/check-docs-accuracy.test.mjs +0 -409
  456. package/scripts/ci/check-durable-write-seam.test.mjs +0 -90
  457. package/scripts/ci/check-no-build-artifacts.test.mjs +0 -71
  458. package/scripts/ci/check-no-residual-identity.test.mjs +0 -202
  459. package/scripts/ci/check-skill-packs.test.mjs +0 -495
  460. package/scripts/ci/check-subagent-frontmatter.test.mjs +0 -124
  461. package/scripts/ci/check.test.mjs +0 -194
  462. package/scripts/ci/conformance-org-api.test.mjs +0 -425
  463. package/scripts/cloud-relay/voice/relay-identity.test.mjs +0 -96
  464. package/scripts/collective/hook-runner.test.mjs +0 -173
  465. package/scripts/cost/fleet-digest.test.mjs +0 -207
  466. package/scripts/cost/track-claude-usage-pricing.test.mjs +0 -183
  467. package/scripts/cost/track-claude-usage.test.mjs +0 -148
  468. package/scripts/daemon/agent-daemon-board-mine.test.mjs +0 -96
  469. package/scripts/daemon/agent-daemon-design.test.mjs +0 -238
  470. package/scripts/daemon/agent-daemon-frontdoor.test.mjs +0 -60
  471. package/scripts/daemon/agent-daemon.test.mjs +0 -995
  472. package/scripts/daemon/assurance-e2e.test.mjs +0 -613
  473. package/scripts/daemon/assurance.test.mjs +0 -1791
  474. package/scripts/daemon/board-mirror.test.mjs +0 -165
  475. package/scripts/daemon/cadence-consumer-frontdoor.test.mjs +0 -393
  476. package/scripts/daemon/cadence-consumer-governance.test.mjs +0 -276
  477. package/scripts/daemon/cadence-consumer.test.mjs +0 -776
  478. package/scripts/daemon/cadence-handlers.test.mjs +0 -837
  479. package/scripts/daemon/classifier-identity.test.mjs +0 -137
  480. package/scripts/daemon/classifier.test.mjs +0 -266
  481. package/scripts/daemon/classify-kind.test.mjs +0 -40
  482. package/scripts/daemon/context-compiler.test.mjs +0 -406
  483. package/scripts/daemon/deliver.test.mjs +0 -564
  484. package/scripts/daemon/dispatcher-cooldown.test.mjs +0 -122
  485. package/scripts/daemon/dispatcher-governance.test.mjs +0 -1013
  486. package/scripts/daemon/dispatcher-resume.test.mjs +0 -166
  487. package/scripts/daemon/dispatcher-session-continuity.test.mjs +0 -365
  488. package/scripts/daemon/execution-ladder.test.mjs +0 -470
  489. package/scripts/daemon/goal-steward-cadence.test.mjs +0 -312
  490. package/scripts/daemon/inbox-deferral-session.test.mjs +0 -49
  491. package/scripts/daemon/inbox-deferral.test.mjs +0 -336
  492. package/scripts/daemon/inbox-wake.test.mjs +0 -199
  493. package/scripts/daemon/integration.test.mjs +0 -149
  494. package/scripts/daemon/lib/self-echo.test.mjs +0 -153
  495. package/scripts/daemon/lib/session-router.test.mjs +0 -554
  496. package/scripts/daemon/prompt-builder-preamble.test.mjs +0 -210
  497. package/scripts/daemon/prompt-builder.test.mjs +0 -556
  498. package/scripts/daemon/responder-cost.test.mjs +0 -68
  499. package/scripts/daemon/responder-history.test.mjs +0 -221
  500. package/scripts/daemon/sdk-version.test.mjs +0 -31
  501. package/scripts/daemon/session-lock.test.mjs +0 -252
  502. package/scripts/daemon/session-outcomes.test.mjs +0 -533
  503. package/scripts/daemon/typing-registry.test.mjs +0 -102
  504. package/scripts/hooks/pre-send-audit.test.mjs +0 -354
  505. package/scripts/huddle/huddle-prompt.test.mjs +0 -176
  506. package/scripts/local-triggers/autoupdate.test.mjs +0 -518
  507. package/scripts/local-triggers/generate-plists.test.mjs +0 -456
  508. package/scripts/media-generation/brand-clause.test.mjs +0 -135
  509. package/scripts/org/send-orgmail.first-contact.test.mjs +0 -102
  510. package/scripts/poller/inbox-privilege-injection.test.mjs +0 -167
  511. package/scripts/poller/inbox-scan-poller.test.mjs +0 -295
  512. package/scripts/poller/lib/cloud-relay-dedup.test.mjs +0 -133
  513. package/scripts/poller/slack-socket-mode.test.mjs +0 -805
  514. package/scripts/poller-launchd/install.test.mjs +0 -243
  515. package/scripts/restore-from-backup.test.mjs +0 -181
  516. package/scripts/session/feed.test.mjs +0 -196
  517. package/scripts/session/supervisor-sh.test.mjs +0 -218
  518. package/scripts/session/supervisor.test.mjs +0 -482
  519. package/scripts/setup/configure-macos.test.mjs +0 -306
  520. package/scripts/setup/gen-subagent-manifest.test.mjs +0 -124
  521. package/scripts/setup/generate-agent-package-json.test.mjs +0 -143
  522. package/scripts/setup/generate-capability.test.mjs +0 -134
  523. package/scripts/setup/init-agent.test.mjs +0 -370
  524. package/scripts/setup/init-skill-marketplace.test.mjs +0 -193
  525. package/scripts/vendor/sync-skill-packs.test.mjs +0 -103
  526. 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
- });