@cohortapp/agent-sdk 2.17.0 → 2.18.5

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