@cohortapp/agent-sdk 2.11.15 → 2.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (280) hide show
  1. package/.env.example +37 -22
  2. package/README.md +2 -0
  3. package/bin/maestro.mjs +117 -39
  4. package/bin/maestro.test.mjs +175 -5
  5. package/docs/guides/front-door-session.md +313 -0
  6. package/docs/guides/mac-mini.md +100 -28
  7. package/docs/guides/org-onboarding.md +1 -1
  8. package/docs/guides/setup-wizard.md +9 -5
  9. package/docs/runbooks/cohort-cutover.md +11 -1
  10. package/docs/runbooks/mac-mini-bootstrap.md +38 -63
  11. package/lib/cadence-bus-requeue.test.mjs +83 -0
  12. package/lib/cadence-bus.mjs +43 -7
  13. package/lib/channels/inbox-item.mjs +59 -2
  14. package/lib/cli/board.mjs +285 -0
  15. package/lib/cli/board.test.mjs +227 -0
  16. package/lib/cli/design.mjs +185 -0
  17. package/lib/cli/design.test.mjs +270 -0
  18. package/lib/cli/doctor-checks.mjs +441 -0
  19. package/lib/cli/doctor-checks.test.mjs +336 -0
  20. package/lib/cli/global-setup-extras.mjs +454 -0
  21. package/lib/cli/global-setup-extras.test.mjs +462 -0
  22. package/lib/cli/inbox.mjs +304 -0
  23. package/lib/cli/inbox.test.mjs +230 -0
  24. package/lib/cli/session-ack.mjs +63 -0
  25. package/lib/cli/session-ack.test.mjs +63 -0
  26. package/lib/cli/session.mjs +760 -0
  27. package/lib/cli/session.test.mjs +613 -0
  28. package/lib/collective/global-config.mjs +209 -6
  29. package/lib/collective/global-config.test.mjs +145 -0
  30. package/lib/collective/global-skills.mjs +145 -0
  31. package/lib/collective/global-skills.test.mjs +126 -0
  32. package/lib/collective/presence.mjs +4 -3
  33. package/lib/collective/vendor-skills.mjs +305 -0
  34. package/lib/collective/vendor-skills.test.mjs +306 -0
  35. package/lib/comms/send-gate.mjs +115 -0
  36. package/lib/comms/send-gate.test.mjs +113 -0
  37. package/lib/design/design-md.mjs +793 -0
  38. package/lib/design/design-md.test.mjs +318 -0
  39. package/lib/design/fixtures/DESIGN.golden.md +238 -0
  40. package/lib/design/fixtures/PRODUCT.golden.md +67 -0
  41. package/lib/design/fixtures/foundation.json +133 -0
  42. package/lib/design/refresh-gate.mjs +154 -0
  43. package/lib/design/refresh-gate.test.mjs +144 -0
  44. package/lib/design/write.mjs +275 -0
  45. package/lib/design/write.test.mjs +241 -0
  46. package/lib/feature-init.mjs +2 -2
  47. package/lib/mcp/server.test.mjs +9 -4
  48. package/lib/model-router/spawn.test.mjs +21 -0
  49. package/lib/org/board-mine-cache.mjs +99 -0
  50. package/lib/org/board-mine-cache.test.mjs +53 -0
  51. package/lib/org/board.mjs +11 -0
  52. package/lib/org/board.test.mjs +11 -1
  53. package/lib/org/client.mjs +36 -0
  54. package/lib/org/client.test.mjs +46 -0
  55. package/lib/org/inbound/directedness.mjs +18 -2
  56. package/lib/org/inbound/directedness.test.mjs +58 -0
  57. package/lib/org/inbound/index.mjs +8 -1
  58. package/lib/org/inbound/index.test.mjs +22 -0
  59. package/lib/org/mesh-directives.test.mjs +110 -0
  60. package/lib/org/mesh.mjs +61 -1
  61. package/lib/org/protocol.checksum +1 -1
  62. package/lib/org/protocol.mjs +52 -0
  63. package/lib/org/protocol.test.mjs +12 -1
  64. package/lib/org/registry.mjs +3 -2
  65. package/lib/org/tool-surface.mjs +120 -0
  66. package/lib/org/tool-surface.test.mjs +118 -5
  67. package/lib/prompts/parallelism.mjs +79 -0
  68. package/lib/prompts/parallelism.test.mjs +177 -0
  69. package/lib/security/external-content.mjs +1 -1
  70. package/lib/security/external-content.test.mjs +17 -0
  71. package/lib/session/config.mjs +137 -0
  72. package/lib/session/config.test.mjs +92 -0
  73. package/lib/session/feed-core.mjs +229 -0
  74. package/lib/session/feed-core.test.mjs +198 -0
  75. package/lib/session/first-run.mjs +126 -0
  76. package/lib/session/first-run.test.mjs +121 -0
  77. package/lib/session/frontdoor.mjs +266 -0
  78. package/lib/session/frontdoor.test.mjs +205 -0
  79. package/lib/session/handoffs.mjs +295 -0
  80. package/lib/session/handoffs.test.mjs +183 -0
  81. package/lib/session/identity.mjs +220 -0
  82. package/lib/session/identity.test.mjs +180 -0
  83. package/lib/session/inbox-claims.mjs +434 -0
  84. package/lib/session/inbox-claims.test.mjs +286 -0
  85. package/lib/session/launch-args.mjs +161 -0
  86. package/lib/session/launch-args.test.mjs +157 -0
  87. package/lib/session/liveness.mjs +174 -0
  88. package/lib/session/liveness.test.mjs +100 -0
  89. package/lib/session/status-summary.mjs +172 -0
  90. package/lib/session/status-summary.test.mjs +118 -0
  91. package/lib/session-permissions.mjs +39 -3
  92. package/lib/session-permissions.test.mjs +20 -0
  93. package/lib/setup/claude-probe.mjs +161 -24
  94. package/lib/setup/claude-probe.test.mjs +187 -0
  95. package/lib/setup/sections/learning.mjs +2 -1
  96. package/lib/setup/sections/model.mjs +104 -24
  97. package/lib/setup/sections/model.test.mjs +240 -0
  98. package/lib/setup/sections/org.mjs +27 -2
  99. package/lib/setup/sections/org.test.mjs +35 -2
  100. package/lib/setup/sections/verify.mjs +5 -0
  101. package/lib/setup/state.mjs +30 -10
  102. package/lib/setup/state.test.mjs +24 -1
  103. package/lib/singleton.js +11 -3
  104. package/lib/singleton.test.mjs +16 -0
  105. package/lib/subagents/lock.mjs +1 -1
  106. package/lib/telemetry/collect.mjs +270 -6
  107. package/lib/telemetry/collect.test.mjs +196 -1
  108. package/lib/upgrade/global-refresh.mjs +108 -0
  109. package/lib/upgrade/global-refresh.test.mjs +65 -0
  110. package/lib/upgrade/launchd-reconcile.mjs +327 -0
  111. package/lib/upgrade/launchd-reconcile.test.mjs +272 -0
  112. package/lib/upgrade/post-steps.mjs +151 -0
  113. package/lib/upgrade/post-steps.test.mjs +200 -0
  114. package/lib/upgrade/verify.mjs +215 -0
  115. package/lib/upgrade/verify.test.mjs +164 -0
  116. package/lib/voice/outbound.mjs +3 -2
  117. package/lib/voice/post-call-brief.mjs +2 -1
  118. package/lib/voice/session-rotation.mjs +6 -1
  119. package/lib/voice/session-rotation.test.mjs +114 -0
  120. package/package.json +3 -3
  121. package/plugins/maestro-skills/plugin.json +25 -1
  122. package/plugins/maestro-skills/skills/board-work.md +63 -0
  123. package/plugins/maestro-skills/skills/cohort-design.md +153 -0
  124. package/plugins/maestro-skills/skills/inbound-triage.md +80 -0
  125. package/plugins/maestro-skills/skills/main-session.md +102 -0
  126. package/plugins/maestro-skills/skills/peer-sessions.md +65 -0
  127. package/plugins/maestro-skills/skills/persona-discipline.md +75 -0
  128. package/plugins/maestro-skills/vendor/emilkowalski/LICENSE +21 -0
  129. package/plugins/maestro-skills/vendor/emilkowalski/UPSTREAM.json +70 -0
  130. package/plugins/maestro-skills/vendor/emilkowalski/skills/animate/RECIPES.md +324 -0
  131. package/plugins/maestro-skills/vendor/emilkowalski/skills/animate/SKILL.md +199 -0
  132. package/plugins/maestro-skills/vendor/emilkowalski/skills/animation-vocabulary/SKILL.md +173 -0
  133. package/plugins/maestro-skills/vendor/emilkowalski/skills/apple-design/SKILL.md +282 -0
  134. package/plugins/maestro-skills/vendor/emilkowalski/skills/emil-design-eng/SKILL.md +674 -0
  135. package/plugins/maestro-skills/vendor/emilkowalski/skills/find-animation-opportunities/SKILL.md +132 -0
  136. package/plugins/maestro-skills/vendor/emilkowalski/skills/improve-animations/AUDIT.md +115 -0
  137. package/plugins/maestro-skills/vendor/emilkowalski/skills/improve-animations/PLAN-TEMPLATE.md +73 -0
  138. package/plugins/maestro-skills/vendor/emilkowalski/skills/improve-animations/SKILL.md +101 -0
  139. package/plugins/maestro-skills/vendor/emilkowalski/skills/prototype/PICKER.md +197 -0
  140. package/plugins/maestro-skills/vendor/emilkowalski/skills/prototype/SKILL.md +90 -0
  141. package/plugins/maestro-skills/vendor/emilkowalski/skills/review-animations/SKILL.md +112 -0
  142. package/plugins/maestro-skills/vendor/emilkowalski/skills/review-animations/STANDARDS.md +187 -0
  143. package/plugins/maestro-skills/vendor/impeccable/LICENSE +191 -0
  144. package/plugins/maestro-skills/vendor/impeccable/NOTICE.md +11 -0
  145. package/plugins/maestro-skills/vendor/impeccable/SKILL.md +86 -0
  146. package/plugins/maestro-skills/vendor/impeccable/UPSTREAM.json +201 -0
  147. package/plugins/maestro-skills/vendor/impeccable/agents/impeccable-asset-producer.md +42 -0
  148. package/plugins/maestro-skills/vendor/impeccable/agents/impeccable-documenter.md +29 -0
  149. package/plugins/maestro-skills/vendor/impeccable/agents/impeccable-finish-reviewer.md +43 -0
  150. package/plugins/maestro-skills/vendor/impeccable/agents/impeccable-manual-edit-applier.md +97 -0
  151. package/plugins/maestro-skills/vendor/impeccable/reference/adapt.md +312 -0
  152. package/plugins/maestro-skills/vendor/impeccable/reference/adapt.native.md +58 -0
  153. package/plugins/maestro-skills/vendor/impeccable/reference/android.md +46 -0
  154. package/plugins/maestro-skills/vendor/impeccable/reference/animate.md +89 -0
  155. package/plugins/maestro-skills/vendor/impeccable/reference/audit.md +136 -0
  156. package/plugins/maestro-skills/vendor/impeccable/reference/audit.native.md +139 -0
  157. package/plugins/maestro-skills/vendor/impeccable/reference/bolder.md +33 -0
  158. package/plugins/maestro-skills/vendor/impeccable/reference/clarify.md +94 -0
  159. package/plugins/maestro-skills/vendor/impeccable/reference/colorize.md +86 -0
  160. package/plugins/maestro-skills/vendor/impeccable/reference/craft-floor.md +44 -0
  161. package/plugins/maestro-skills/vendor/impeccable/reference/craft.md +5 -0
  162. package/plugins/maestro-skills/vendor/impeccable/reference/critique.md +806 -0
  163. package/plugins/maestro-skills/vendor/impeccable/reference/degraded/asset-producer.md +37 -0
  164. package/plugins/maestro-skills/vendor/impeccable/reference/degraded/documenter.md +24 -0
  165. package/plugins/maestro-skills/vendor/impeccable/reference/degraded/finish-reviewer.md +38 -0
  166. package/plugins/maestro-skills/vendor/impeccable/reference/degraded/manual-edit-applier.md +92 -0
  167. package/plugins/maestro-skills/vendor/impeccable/reference/delight.md +70 -0
  168. package/plugins/maestro-skills/vendor/impeccable/reference/distill.md +111 -0
  169. package/plugins/maestro-skills/vendor/impeccable/reference/doctor.md +54 -0
  170. package/plugins/maestro-skills/vendor/impeccable/reference/document.md +416 -0
  171. package/plugins/maestro-skills/vendor/impeccable/reference/extract.md +69 -0
  172. package/plugins/maestro-skills/vendor/impeccable/reference/harden.md +336 -0
  173. package/plugins/maestro-skills/vendor/impeccable/reference/hooks.md +111 -0
  174. package/plugins/maestro-skills/vendor/impeccable/reference/init.md +131 -0
  175. package/plugins/maestro-skills/vendor/impeccable/reference/ios.md +51 -0
  176. package/plugins/maestro-skills/vendor/impeccable/reference/layout.md +84 -0
  177. package/plugins/maestro-skills/vendor/impeccable/reference/live-setup.md +104 -0
  178. package/plugins/maestro-skills/vendor/impeccable/reference/live.md +325 -0
  179. package/plugins/maestro-skills/vendor/impeccable/reference/new-work.md +147 -0
  180. package/plugins/maestro-skills/vendor/impeccable/reference/onboard.md +234 -0
  181. package/plugins/maestro-skills/vendor/impeccable/reference/operate.md +61 -0
  182. package/plugins/maestro-skills/vendor/impeccable/reference/optimize.md +258 -0
  183. package/plugins/maestro-skills/vendor/impeccable/reference/overdrive.md +127 -0
  184. package/plugins/maestro-skills/vendor/impeccable/reference/polish.md +105 -0
  185. package/plugins/maestro-skills/vendor/impeccable/reference/quieter.md +99 -0
  186. package/plugins/maestro-skills/vendor/impeccable/reference/routing.md +24 -0
  187. package/plugins/maestro-skills/vendor/impeccable/reference/shape.md +59 -0
  188. package/plugins/maestro-skills/vendor/impeccable/reference/typeset.md +80 -0
  189. package/plugins/maestro-skills/vendor/impeccable/reference/visualize.md +46 -0
  190. package/plugins/maestro-skills/vendor/taste-skill/LICENSE +21 -0
  191. package/plugins/maestro-skills/vendor/taste-skill/UPSTREAM.json +37 -0
  192. package/plugins/maestro-skills/vendor/taste-skill/skills/minimalist-skill/SKILL.md +85 -0
  193. package/plugins/maestro-skills/vendor/taste-skill/skills/redesign-skill/SKILL.md +178 -0
  194. package/plugins/maestro-skills/vendor/taste-skill/skills/soft-skill/SKILL.md +98 -0
  195. package/plugins/maestro-skills/vendor/taste-skill/skills/taste-skill/SKILL.md +1206 -0
  196. package/plugins/maestro-skills/vendor/unlazy/LICENSE +21 -0
  197. package/plugins/maestro-skills/vendor/unlazy/SECURITY.md +72 -0
  198. package/plugins/maestro-skills/vendor/unlazy/SKILL.md +104 -0
  199. package/plugins/maestro-skills/vendor/unlazy/UPSTREAM.json +94 -0
  200. package/plugins/maestro-skills/vendor/unlazy/references/dispatch.md +82 -0
  201. package/plugins/maestro-skills/vendor/unlazy/references/gates.md +149 -0
  202. package/plugins/maestro-skills/vendor/unlazy/references/method.md +49 -0
  203. package/plugins/maestro-skills/vendor/unlazy/references/orchestration.md +107 -0
  204. package/plugins/maestro-skills/vendor/unlazy/references/parallel.md +133 -0
  205. package/plugins/maestro-skills/vendor/unlazy/references/token-economy.md +48 -0
  206. package/plugins/maestro-skills/vendor/unlazy/scripts/dispatch-check.mjs +139 -0
  207. package/plugins/maestro-skills/vendor/unlazy/scripts/gate-check.mjs +960 -0
  208. package/plugins/maestro-skills/vendor/unlazy/scripts/gate-lint.mjs +245 -0
  209. package/plugins/maestro-skills/vendor/unlazy/scripts/lib/check-supervisor.mjs +46 -0
  210. package/plugins/maestro-skills/vendor/unlazy/scripts/lib/dispatch.mjs +293 -0
  211. package/plugins/maestro-skills/vendor/unlazy/scripts/lib/gates.mjs +953 -0
  212. package/plugins/maestro-skills/vendor/unlazy/scripts/lib/process-tree.mjs +161 -0
  213. package/plugins/maestro-skills/vendor/unlazy/scripts/lib/regex-worker.mjs +9 -0
  214. package/plugins/maestro-skills/vendor/unlazy/templates/PLAN.md +116 -0
  215. package/plugins/maestro-skills/vendor/unlazy/templates/gates-leaf.md +51 -0
  216. package/plugins/maestro-skills/vendor/unlazy/templates/gates-node.md +51 -0
  217. package/scaffold/CLAUDE.md +24 -0
  218. package/scripts/ci/check-durable-write-seam.mjs +147 -0
  219. package/scripts/ci/check-durable-write-seam.test.mjs +90 -0
  220. package/scripts/ci/check-skill-packs.mjs +388 -0
  221. package/scripts/ci/check-skill-packs.test.mjs +495 -0
  222. package/scripts/ci/check.mjs +6 -0
  223. package/scripts/collective/hook-runner.mjs +39 -4
  224. package/scripts/collective/hook-runner.test.mjs +85 -2
  225. package/scripts/daemon/agent-daemon-board-mine.test.mjs +96 -0
  226. package/scripts/daemon/agent-daemon-design.test.mjs +238 -0
  227. package/scripts/daemon/agent-daemon-frontdoor.test.mjs +60 -0
  228. package/scripts/daemon/agent-daemon.mjs +249 -10
  229. package/scripts/daemon/agent-daemon.test.mjs +73 -0
  230. package/scripts/daemon/assurance-e2e.test.mjs +141 -6
  231. package/scripts/daemon/assurance.mjs +461 -37
  232. package/scripts/daemon/assurance.test.mjs +408 -43
  233. package/scripts/daemon/cadence-consumer-frontdoor.test.mjs +393 -0
  234. package/scripts/daemon/cadence-consumer.mjs +289 -89
  235. package/scripts/daemon/cadence-handlers.mjs +53 -0
  236. package/scripts/daemon/classifier.mjs +1 -1
  237. package/scripts/daemon/dispatcher-resume.test.mjs +166 -0
  238. package/scripts/daemon/dispatcher.mjs +127 -19
  239. package/scripts/daemon/health.mjs +12 -1
  240. package/scripts/daemon/inbox-deferral-session.test.mjs +49 -0
  241. package/scripts/daemon/inbox-deferral.mjs +6 -0
  242. package/scripts/daemon/lib/self-echo.mjs +201 -0
  243. package/scripts/daemon/lib/self-echo.test.mjs +153 -0
  244. package/scripts/daemon/maestro-daemon.mjs +3 -0
  245. package/scripts/daemon/prompt-builder.mjs +19 -3
  246. package/scripts/daemon/responder.mjs +51 -40
  247. package/scripts/daemon/sdk-version.mjs +51 -0
  248. package/scripts/daemon/sdk-version.test.mjs +31 -0
  249. package/scripts/hooks/pre-send-audit.sh +97 -4
  250. package/scripts/hooks/pre-send-audit.test.mjs +140 -1
  251. package/scripts/local-triggers/autoupdate.sh +243 -19
  252. package/scripts/local-triggers/autoupdate.test.mjs +518 -0
  253. package/scripts/local-triggers/generate-plists.sh +24 -1
  254. package/scripts/local-triggers/generate-plists.test.mjs +49 -11
  255. package/scripts/org/send-orgmail.first-contact.test.mjs +102 -0
  256. package/scripts/org/send-orgmail.mjs +27 -3
  257. package/scripts/poller/inbox-privilege-injection.test.mjs +167 -0
  258. package/scripts/poller/slack-poller.mjs +13 -1
  259. package/scripts/poller/utils.mjs +46 -1
  260. package/scripts/poller-launchd/install.sh +19 -11
  261. package/scripts/poller-launchd/install.test.mjs +243 -0
  262. package/scripts/poller-launchd/launchd-poller-wrapper.sh +92 -0
  263. package/scripts/poller-launchd/migrate.sh +66 -0
  264. package/scripts/poller-launchd/poller.plist.template +4 -2
  265. package/scripts/session/feed.mjs +237 -0
  266. package/scripts/session/feed.test.mjs +196 -0
  267. package/scripts/session/supervisor-sh.test.mjs +218 -0
  268. package/scripts/session/supervisor.mjs +328 -0
  269. package/scripts/session/supervisor.sh +141 -0
  270. package/scripts/session/supervisor.test.mjs +482 -0
  271. package/scripts/setup/configure-macos.sh +250 -55
  272. package/scripts/setup/configure-macos.test.mjs +306 -0
  273. package/scripts/setup/init-agent.sh +112 -7
  274. package/scripts/setup/init-agent.test.mjs +220 -1
  275. package/scripts/vendor/skill-packs.mjs +354 -0
  276. package/scripts/vendor/sync-skill-packs.mjs +242 -0
  277. package/scripts/vendor/sync-skill-packs.test.mjs +103 -0
  278. package/scripts/watchdog/memory-watchdog.sh +37 -1
  279. package/scripts/watchdog/memory-watchdog.test.mjs +64 -0
  280. package/scripts/setup/boot-claude-session.sh +0 -94
package/.env.example CHANGED
@@ -36,7 +36,11 @@ COHORT_BASE=https://os.cohortapp.com
36
36
  # agent's workforce member (aliases: COHORT_API_KEY, COHORT_TOKEN).
37
37
  COHORT_API_TOKEN=
38
38
 
39
- # Org slug — sent as the x-org-id pin header + used by pull-enrollment.
39
+ # The org's ID — NOT its slug. It has the form org_default_<slug> (Adaptic:
40
+ # org_default_adaptic, slug adaptic) and is sent as the x-org-id pin header,
41
+ # which hq compares with strict equality against the key's Org.id: a slug here
42
+ # 401s every call. `maestro doctor` checks this value against the server and
43
+ # says "you set the slug" when that is what happened. Also used by pull-enrollment.
40
44
  COHORT_ORG_ID=
41
45
 
42
46
  # OPTIONAL — this agent's member id/slug. Used by pull-enrollment, org_whoami,
@@ -53,29 +57,40 @@ COHORT_AGENT_EMAIL=
53
57
  # The agent's reasoning engines. At minimum you need Anthropic (Claude).
54
58
  #
55
59
 
56
- # REQUIRED — Primary reasoning engine. Two ways to authenticate:
57
- #
58
- # Option A — API key (pay-per-token)
59
- # Set ANTHROPIC_API_KEY below to a valid sk-ant-api03-... key.
60
- # Get one: https://console.anthropic.com/settings/keys
61
- #
62
- # Option B — Claude Code subscription (Pro/Max, OAuth via Keychain)
63
- # LEAVE ANTHROPIC_API_KEY EMPTY *and* set MAESTRO_PREFER_SUBSCRIPTION_AUTH=1.
64
- # This tells the cadence consumer to strip ANTHROPIC_API_KEY from every
65
- # sub-session spawn so claude --print falls back to the keychain OAuth
66
- # token. Most agents on a Mac mini with a Claude Code subscription
67
- # should use this option — routine cadence ticks cost zero API credits.
68
- #
69
- # Doctor validates the key against api.anthropic.com on every run; an
70
- # invalid key here will cascade 401s through every sub-session spawn.
71
- ANTHROPIC_API_KEY=
72
-
73
- # OPTIONAL — When set to 1, the cadence consumer strips ANTHROPIC_API_KEY
74
- # from every claude --print sub-session env so claude falls back to
75
- # Claude Code subscription auth (Keychain OAuth). Use this when the
76
- # agent's Mac has a Claude Code Pro/Max subscription.
60
+ # REQUIRED — Primary reasoning engine. ONE auth story, three ways to satisfy it,
61
+ # in order of preference. `maestro setup` (model section, order 30) writes the
62
+ # chosen one here and chmods this file 600; `maestro doctor` reports which mode
63
+ # is in effect and what `claude auth status` reports (presence of a credential,
64
+ # not its validity — a bad token surfaces on the first real spawn).
65
+ #
66
+ # Option A — subscription OAuth token ← THE FLEET DEFAULT (seat machines)
67
+ # On ANY machine where you are logged in to Claude Code, run
68
+ # claude setup-token
69
+ # and paste the long-lived token below, together with
70
+ # MAESTRO_PREFER_SUBSCRIPTION_AUTH=1. Every spawn — the daemon's --print
71
+ # lane, the main session, cadence sub-sessions — carries it; the interactive
72
+ # keychain login is NOT relied on because it expires headlessly. The token is
73
+ # per seat; mint a fresh one with the same command when it is rejected.
74
+ # Headless setup reads it from the environment: CLAUDE_CODE_OAUTH_TOKEN=… maestro setup --headless
75
+ CLAUDE_CODE_OAUTH_TOKEN=
76
+
77
+ # Option B — keychain login on this machine (interactive / dev boxes only)
78
+ # `claude login` here, leave CLAUDE_CODE_OAUTH_TOKEN and ANTHROPIC_API_KEY
79
+ # empty, set MAESTRO_PREFER_SUBSCRIPTION_AUTH=1. Doctor warns: the login
80
+ # expires headlessly and a seat machine should hold the token instead.
81
+ #
82
+ # Set to 1 for Option A and Option B: every claude spawn strips
83
+ # ANTHROPIC_API_KEY from its env so claude rides the subscription (token or
84
+ # keychain) — routine cadence ticks then cost zero API credits.
77
85
  MAESTRO_PREFER_SUBSCRIPTION_AUTH=
78
86
 
87
+ # Option C — API key (pay-per-token)
88
+ # Set ANTHROPIC_API_KEY to a valid sk-ant-api03-... key and leave the two
89
+ # above empty. Get one: https://console.anthropic.com/settings/keys
90
+ # Doctor validates it against api.anthropic.com on every run; an invalid
91
+ # key cascades 401s through every sub-session spawn.
92
+ ANTHROPIC_API_KEY=
93
+
79
94
  # OPTIONAL — Supplemental model access (GPT-4, embeddings)
80
95
  # Get your key: https://platform.openai.com/api-keys
81
96
  # Subscription: OpenAI API plan (pay-per-token)
package/README.md CHANGED
@@ -568,6 +568,8 @@ Autonomous work is designed to survive reboot and power loss, and the agent degr
568
568
 
569
569
  - [Setup Wizard](docs/guides/setup-wizard.md) -- `maestro setup`: sections, resume/checkpoint, source of truth, enrichment, verify probe, all flags
570
570
  - [Agent Persona Setup](docs/guides/agent-persona-setup.md) -- Identity, operating charter, and behavioural policies
571
+ - [Front-door Session](docs/guides/front-door-session.md) -- The always-on main session: what runs, how to attach, stop, restart, and read its status
572
+ - [Mac mini bring-up](docs/guides/mac-mini.md) -- Boxed mini to enrolled seat: token, create, setup, session install, Tailscale SSH, doctor
571
573
  - [Email Setup](docs/guides/email-setup.md) -- Gmail IMAP polling, SMTP sending, thread dedup, signatures
572
574
  - [Slack Setup](docs/guides/slack-setup.md) -- Slack app, tokens, sending, typing indicators, events, CDP
573
575
  - [WhatsApp Setup](docs/guides/whatsapp-setup.md) -- Twilio WhatsApp sandbox/production, webhook handler
package/bin/maestro.mjs CHANGED
@@ -816,12 +816,24 @@ function loadShippedManifest(cwd) {
816
816
  function writeShippedManifest(cwd, files) {
817
817
  const dst = join(cwd, SHIPPED_MANIFEST_REL);
818
818
  mkdirSync(dirname(dst), { recursive: true });
819
+ // `sdkVersion` (2.12+) is what `upgrade --verify` compares against the
820
+ // running package; `version` is the manifest FORMAT and stays 1.
819
821
  writeFileSync(
820
822
  dst,
821
- JSON.stringify({ version: 1, files: [...files].sort() }, null, 2) + "\n",
823
+ JSON.stringify({ version: 1, sdkVersion: readFrameworkVersion(), files: [...files].sort() }, null, 2) + "\n",
822
824
  );
823
825
  }
824
826
 
827
+ /** The SDK version the previous upgrade recorded here, or null (pre-2.12 manifest / none). */
828
+ function readShippedManifestVersion(cwd) {
829
+ try {
830
+ const raw = JSON.parse(readFileSync(join(cwd, SHIPPED_MANIFEST_REL), "utf-8"));
831
+ return typeof raw?.sdkVersion === "string" ? raw.sdkVersion : null;
832
+ } catch {
833
+ return null; // absent or unreadable — "from" is simply unknown
834
+ }
835
+ }
836
+
825
837
  function sha256File(p) {
826
838
  return createHash("sha256").update(readFileSync(p)).digest("hex");
827
839
  }
@@ -1130,10 +1142,12 @@ function parseUpgradeFlags(args) {
1130
1142
  noIncoming: false,
1131
1143
  verbose: false,
1132
1144
  noPrune: false,
1145
+ verify: false,
1133
1146
  };
1134
1147
  for (const a of args) {
1135
1148
  if (a === "--dry-run" || a === "-n") flags.dryRun = true;
1136
1149
  else if (a === "--force-overwrite" || a === "--force") flags.forceOverwrite = true;
1150
+ else if (a === "--verify") flags.verify = true;
1137
1151
  else if (a === "--no-incoming") flags.noIncoming = true;
1138
1152
  else if (a === "--no-prune") flags.noPrune = true;
1139
1153
  else if (a === "--verbose" || a === "-v") flags.verbose = true;
@@ -1154,9 +1168,25 @@ Flags:
1154
1168
  --force-overwrite Overwrite even locally-modified files (backs them up)
1155
1169
  --no-incoming Don't write .maestro/incoming/ shadows for preserved files
1156
1170
  --no-prune Keep framework files that upstream has deleted
1171
+ --verify Report only: is this seat wholly on this SDK version
1172
+ (global bin, local package, shipped manifest, running
1173
+ daemon, plists installed + loaded, global-setup)?
1174
+ Exit 1 on any mismatch. doctor prints the same rows.
1157
1175
  --verbose, -v Print classification for every file
1158
1176
  --help, -h Show this help
1159
1177
 
1178
+ After the file merge (every non-dry run, each step fail-open, reported here
1179
+ and in .maestro/upgrade-result.json {from,to,at,steps}):
1180
+ plists scripts/local-triggers/generate-plists.sh
1181
+ launchd install / replace / load every generated plist under
1182
+ ~/Library/LaunchAgents (mode 600, launchctl bootstrap);
1183
+ -daemon and -session are rewritten, never restarted
1184
+ globalSetup maestro global-setup (identity block, cohort MCP, settings, skills,
1185
+ vendored design skill packs)
1186
+ globalInstall npm i -g @cohortapp/agent-sdk@<this version>
1187
+ (MAESTRO_SKIP_GLOBAL_INSTALL=1 skips)
1188
+ verify the --verify report
1189
+
1160
1190
  Per-file behaviour:
1161
1191
  added — new upstream file → copy
1162
1192
  updated — existed, byte-equal to your committed copy → safe overwrite
@@ -1190,6 +1220,16 @@ Per-file behaviour:
1190
1220
  process.exit(1);
1191
1221
  }
1192
1222
 
1223
+ if (flags.verify) {
1224
+ // Report only — nothing is written. lib/upgrade/verify.mjs is the core;
1225
+ // the same rows appear in `maestro doctor`.
1226
+ const { runVerify, formatVerifyReport } = await import("../lib/upgrade/verify.mjs");
1227
+ const report = runVerify({ cwd, expected: readFrameworkVersion() });
1228
+ for (const line of formatVerifyReport(report)) console.log(line);
1229
+ process.exitCode = report.ok ? 0 : 1;
1230
+ return;
1231
+ }
1232
+
1193
1233
  const inGit = isGitRepo(cwd);
1194
1234
  if (!inGit) {
1195
1235
  warn("Not in a git repo — cannot detect local modifications.");
@@ -1330,6 +1370,8 @@ Per-file behaviour:
1330
1370
  // Removals are backed up under .maestro/backup/ exactly like a forced
1331
1371
  // overwrite, so a prune is always reversible.
1332
1372
  const priorManifest = loadShippedManifest(cwd);
1373
+ // What this machine ran before this upgrade — the `from` of upgrade-result.json.
1374
+ const priorSdkVersion = readShippedManifestVersion(cwd);
1333
1375
 
1334
1376
  if (flags.noPrune) {
1335
1377
  log("Prune skipped (--no-prune): upstream-deleted files left in place.");
@@ -1614,6 +1656,31 @@ Per-file behaviour:
1614
1656
  }
1615
1657
  }
1616
1658
 
1659
+ // Post-steps (design 2026-09-08, WP-M6): plists → launchd reconcile →
1660
+ // global-setup → global npm install → verify. This is what puts an existing
1661
+ // seat wholly on the new architecture on the first hop from ANY old version,
1662
+ // because the old autoupdate.sh runs THIS upgrade and nothing else. Each
1663
+ // step is fail-open; the daemon is restarted by autoupdate, never here.
1664
+ if (!flags.dryRun) {
1665
+ console.log();
1666
+ console.log(`${C.bold}Seat reconcile${C.reset}`);
1667
+ try {
1668
+ const { runUpgradePostSteps } = await import("../lib/upgrade/post-steps.mjs");
1669
+ await runUpgradePostSteps({
1670
+ cwd,
1671
+ sdkVersion: readFrameworkVersion(),
1672
+ from: priorSdkVersion,
1673
+ say: { log: (m) => console.log(` ${m}`), ok, warn },
1674
+ // global-setup needs config/agent.json + the collective hook runner;
1675
+ // a repo without them is reported as a skipped step, not a crash.
1676
+ globalSetup: isAgentRepo(cwd) ? () => globalSetup([cwd]) : null,
1677
+ });
1678
+ log("Result: .maestro/upgrade-result.json · re-check any time: maestro upgrade --verify");
1679
+ } catch (e) {
1680
+ warn(`seat reconcile skipped: ${e && e.message ? e.message : e}`);
1681
+ }
1682
+ }
1683
+
1617
1684
  console.log();
1618
1685
  log("Agent-specific paths (config/, CLAUDE.md, knowledge/, memory/, state/, outputs/, logs/, .env) were NOT touched.");
1619
1686
  }
@@ -2301,6 +2368,35 @@ async function doctor() {
2301
2368
  } catch { /* pmset unavailable (non-mac / sandbox) — skip */ }
2302
2369
  }
2303
2370
 
2371
+ // Claude auth, the COHORT_ORG_ID id-vs-slug trap, the effective source of
2372
+ // every Cohort value, the global SDK install, Tailscale SSH and the main
2373
+ // session job — mode-aware, so a seat on the subscription token is no
2374
+ // longer told "ANTHROPIC_API_KEY not set". Pure checks + one injected edge
2375
+ // live in lib/cli/doctor-checks.mjs (design 2026-09-08 §3.9).
2376
+ try {
2377
+ const { runDoctorChecks } = await import("../lib/cli/doctor-checks.mjs");
2378
+ for (const r of await runDoctorChecks({ cwd })) {
2379
+ if (r.level === "ok") ok(r.msg);
2380
+ else if (r.level === "warn") warn(r.msg);
2381
+ else { fail(r.msg); issues++; }
2382
+ }
2383
+ } catch (err) {
2384
+ warn(`auth / front-door checks unavailable: ${err && err.message ? err.message : err}`);
2385
+ }
2386
+
2387
+ // `maestro upgrade --verify` rows: one SDK version end to end (global bin,
2388
+ // local package, shipped manifest, running daemon, plists, global-setup).
2389
+ try {
2390
+ const { runVerify } = await import("../lib/upgrade/verify.mjs");
2391
+ for (const r of runVerify({ cwd, expected: readFrameworkVersion() }).rows) {
2392
+ if (r.level === "ok") ok(r.msg);
2393
+ else if (r.level === "warn") warn(r.msg);
2394
+ else { fail(r.msg); issues++; }
2395
+ }
2396
+ } catch (err) {
2397
+ warn(`upgrade verify unavailable: ${err && err.message ? err.message : err}`);
2398
+ }
2399
+
2304
2400
  // ── .env ────────────────────────────────────────────────────────────────
2305
2401
  if (existsSync(join(cwd, ".env"))) {
2306
2402
  // .env permission hygiene (setup-onboarding W4): every credential — Slack,
@@ -2327,46 +2423,9 @@ async function doctor() {
2327
2423
  else if (required) { warn(`${key} not set`); issues++; }
2328
2424
  else warn(`${key} not set (optional)`);
2329
2425
  };
2330
- check("ANTHROPIC_API_KEY", true);
2331
2426
  check("SLACK_USER_TOKEN", false);
2332
2427
  check("GMAIL_APP_PASSWORD", false);
2333
2428
 
2334
- // Auth validity: if ANTHROPIC_API_KEY is set, ping the API to
2335
- // verify it works. An invalid key in .env will silently be sent
2336
- // to every `claude --print` sub-session and cause cascading 401s
2337
- // (exactly the ravi-ai inbox-processor runaway). Better to catch
2338
- // it here. Skips the check if the user opted out via
2339
- // MAESTRO_PREFER_SUBSCRIPTION_AUTH=1 (subscription wins).
2340
- const keyMatch = env.match(/^ANTHROPIC_API_KEY=(.+)$/m);
2341
- const preferSubsMatch = env.match(/^MAESTRO_PREFER_SUBSCRIPTION_AUTH=(.+)$/m);
2342
- const preferSubs = preferSubsMatch && /^1|true|yes$/i.test(preferSubsMatch[1].trim());
2343
- if (keyMatch && !preferSubs) {
2344
- const key = keyMatch[1].trim().replace(/^"|"$/g, "");
2345
- try {
2346
- const result = spawnSync("curl", [
2347
- "-s", "-o", "/dev/null", "-w", "%{http_code}",
2348
- "-X", "POST",
2349
- "-H", `x-api-key: ${key}`,
2350
- "-H", "anthropic-version: 2023-06-01",
2351
- "-H", "content-type: application/json",
2352
- "--max-time", "8",
2353
- "https://api.anthropic.com/v1/messages",
2354
- "-d", JSON.stringify({ model: "claude-haiku-4-5", max_tokens: 5, messages: [{ role: "user", content: "ping" }] }),
2355
- ], { encoding: "utf-8" });
2356
- const code = (result.stdout || "").trim();
2357
- if (code === "200") ok("ANTHROPIC_API_KEY validated against api.anthropic.com");
2358
- else if (code === "401") {
2359
- warn(`ANTHROPIC_API_KEY is INVALID (HTTP 401 from api.anthropic.com).`);
2360
- warn(` This will cause every sub-session spawn to fail. Either:`);
2361
- warn(` 1. Replace the key in .env with a valid one, OR`);
2362
- warn(` 2. Set MAESTRO_PREFER_SUBSCRIPTION_AUTH=1 in .env to use Claude Code subscription auth.`);
2363
- issues++;
2364
- } else if (code) warn(`ANTHROPIC_API_KEY check returned HTTP ${code} (expected 200)`);
2365
- else warn(`ANTHROPIC_API_KEY check skipped (no network / curl missing)`);
2366
- } catch { warn("ANTHROPIC_API_KEY check failed (curl error)"); }
2367
- } else if (preferSubs) {
2368
- ok("MAESTRO_PREFER_SUBSCRIPTION_AUTH=1 — using Claude Code subscription (Keychain OAuth)");
2369
- }
2370
2429
 
2371
2430
  // ── Slack Socket Mode ────────────────────────────────────────────────
2372
2431
  // When SLACK_APP_LEVEL_TOKEN is set, verify the launchd job is loaded
@@ -3297,6 +3356,14 @@ async function globalSetup(args = []) {
3297
3356
  if (!dryRun) writeFileSync(pointerFile, JSON.stringify(gc.buildPointer(cwd, agentName), null, 2));
3298
3357
  ok(`${dryRun ? "[dry-run] " : ""}machine pointer → ${pointerFile}`);
3299
3358
 
3359
+ // 4) Front-door session (2026-09 §3.7): identity block, cohort MCP server,
3360
+ // settings additions, global skills and the vendored design skill packs
3361
+ // (WP-M7) — lib/cli/global-setup-extras.mjs.
3362
+ try {
3363
+ const extras = await import("../lib/cli/global-setup-extras.mjs");
3364
+ await extras.applyFrontDoorSetup({ agentRoot: cwd, claudeDir, dryRun, log, ok, warn });
3365
+ } catch (err) { warn(`front-door setup skipped: ${err && err.message}`); }
3366
+
3300
3367
  console.log();
3301
3368
  if (dryRun) {
3302
3369
  log("Dry run complete — no files written.");
@@ -3737,6 +3804,11 @@ switch (command) {
3737
3804
  case "router": await routerCmd(args); break;
3738
3805
  case "secrets": await secretsCmd(args); break;
3739
3806
  case "who-owns": case "who": await whoOwnsCmd(args); break;
3807
+ case "inbox": process.exitCode = await (await import("../lib/cli/inbox.mjs")).runInbox(args); break;
3808
+ case "session-ack": process.exitCode = await (await import("../lib/cli/session-ack.mjs")).runSessionAck(args); break;
3809
+ case "session": { const r = await (await import("../lib/cli/session.mjs")).run(args); process.exitCode = r.code; break; }
3810
+ case "board": await (await import("../lib/cli/board.mjs")).boardCmd(args); break;
3811
+ case "design": await (await import("../lib/cli/design.mjs")).designCmd(args); break;
3740
3812
  case "init":
3741
3813
  case "update-init":
3742
3814
  case "init-update":
@@ -3750,7 +3822,8 @@ Usage:
3750
3822
  npx @cohortapp/agent-sdk setup [section] [flags] Configure this agent (deterministic, resumable wizard)
3751
3823
  npx @cohortapp/agent-sdk pair <code> Open a pre-auth pairing handshake (org admin approves it)
3752
3824
  npx @cohortapp/agent-sdk sync [--force] [--dry-run] Re-pull this agent's record from Cohort into config/agent.json
3753
- npx @cohortapp/agent-sdk upgrade [--dry-run] Update framework files
3825
+ npx @cohortapp/agent-sdk upgrade [--dry-run] Update framework files (+ plists, launchd, global-setup, global install)
3826
+ npx @cohortapp/agent-sdk upgrade --verify Is this seat wholly on this SDK version? (exit 1 on mismatch)
3754
3827
  npx @cohortapp/agent-sdk init [feature] [--apply] Run pending feature-init steps
3755
3828
  npx @cohortapp/agent-sdk global-setup [--dry-run] Wire collective memory into ~/.claude (machine-wide)
3756
3829
  npx @cohortapp/agent-sdk doctor Verify installation
@@ -3761,6 +3834,11 @@ Usage:
3761
3834
  npx @cohortapp/agent-sdk secrets sync [--name X] Pull org secrets from the broker into the local store
3762
3835
  npx @cohortapp/agent-sdk secrets rotate --name X Rotate a secret (new value read from stdin)
3763
3836
  npx @cohortapp/agent-sdk who-owns <scope> Who owns a scope / escalate-to / reports (org context, zero-LLM)
3837
+ npx @cohortapp/agent-sdk inbox list|show|claim|reply|done|defer The main session's inbox (JSON; shares the daemon's markers)
3838
+ npx @cohortapp/agent-sdk session-ack <tickId> Ack a cadence tick handed to the main session
3839
+ npx @cohortapp/agent-sdk session <cmd> Front-door session: status|attach|start|stop|restart|spawn|peers|handoffs|ack
3840
+ npx @cohortapp/agent-sdk board mine|track|claim|complete Your work across every board (maestro board --help)
3841
+ npx @cohortapp/agent-sdk design sync [--out <dir>] Pull the brand foundation → DESIGN.md + PRODUCT.md
3764
3842
 
3765
3843
  Upgrade flags:
3766
3844
  --dry-run, -n Preview changes without writing
@@ -50,6 +50,65 @@ async function rmRoot(path) {
50
50
  try { await fsp.rm(path, { recursive: true, force: true }); } catch { /* */ }
51
51
  }
52
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
+
53
112
  /**
54
113
  * Build a PATH that shadows `npm` (and optionally `git`) with no-op shims
55
114
  * so `create` doesn't actually try to install dependencies. Returns the new
@@ -61,14 +120,14 @@ async function withShim() {
61
120
  writeFileSync(npm, "#!/bin/bash\necho '[shim npm]' \"$@\" >&2\nexit 0\n");
62
121
  await fsp.chmod(npm, 0o755);
63
122
  // Keep real git (we want git init to actually init).
64
- const env = { ...process.env, PATH: `${dir}:${process.env.PATH || ""}` };
123
+ const env = baseEnv({ PATH: `${dir}:${TEST_SHIM}:${process.env.PATH || ""}` });
65
124
  return { env, dir };
66
125
  }
67
126
 
68
127
  function runCli(args, cwd, env) {
69
128
  return spawnSync(process.execPath, [MAESTRO_CLI, ...args], {
70
129
  cwd: cwd || process.cwd(),
71
- env: env || process.env,
130
+ env: env || baseEnv(),
72
131
  encoding: "utf-8",
73
132
  });
74
133
  }
@@ -77,7 +136,7 @@ function runCli(args, cwd, env) {
77
136
  function runCliStdin(args, cwd, input, env) {
78
137
  return spawnSync(process.execPath, [MAESTRO_CLI, ...args], {
79
138
  cwd: cwd || process.cwd(),
80
- env: env || process.env,
139
+ env: env || baseEnv(),
81
140
  input,
82
141
  encoding: "utf-8",
83
142
  });
@@ -1325,10 +1384,17 @@ test("upgrade prune never removes machine-generated launchd plists", async () =>
1325
1384
  try {
1326
1385
  const r = runCli(["upgrade"], root);
1327
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.)
1328
1393
  assert.ok(
1329
- existsSync(join(root, plist)),
1330
- "generated plists are machine config; prune must leave them alone",
1394
+ !existsSync(join(root, ".maestro", "backup", plist)),
1395
+ "generated plists are machine config; prune must leave them alone (no prune backup)",
1331
1396
  );
1397
+ assert.ok(!/pruned/.test(r.stdout), `prune must not report the plist:\n${r.stdout}`);
1332
1398
  } finally {
1333
1399
  await fsp.rm(root, { recursive: true, force: true });
1334
1400
  }
@@ -1402,3 +1468,107 @@ test("upgrade --dry-run writes no shipped-manifest", async () => {
1402
1468
  await fsp.rm(root, { recursive: true, force: true });
1403
1469
  }
1404
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
+ });