@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
@@ -0,0 +1,328 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * scripts/session/supervisor.mjs — keeps exactly one main session alive.
4
+ *
5
+ * Launched by launchd (`ai.maestro.<first>-session`, KeepAlive
6
+ * {SuccessfulExit:false}, ThrottleInterval 30) through supervisor.sh. One run
7
+ * of this process is ONE lifetime of the main session:
8
+ *
9
+ * 1. Take the `session` singleton lock (lib/singleton.js). A second
10
+ * supervisor exits 0 — clean — so launchd does not thrash.
11
+ * 2. Pick the multiplexer (tmux if installed, else /usr/bin/screen).
12
+ * 3. If a `maestro-<first>` mux session already exists with no lock holder
13
+ * (an orphan from a killed supervisor), ADOPT it: block until it is gone
14
+ * rather than start a duplicate.
15
+ * 4. Otherwise pre-answer the first-run dialogs an unattended session
16
+ * cannot (lib/session/first-run.mjs seeds ~/.claude.json), then launch
17
+ * `claude --name <first>-main` with the stable session id
18
+ * (`--session-id` first time, `--resume` after) IN THE FOREGROUND of
19
+ * the mux, so this process lives exactly as long as the session. A
20
+ * watchdog flags a session that is alive but has not beaten after
21
+ * HEARTBEAT_GRACE_MS (log + state/session/attention.json for doctor).
22
+ * 5. When the session ends: if a resume died non-zero inside 20 s, rotate
23
+ * to a fresh id (max 3/hour, then sleep 10 min); then exit 75 so launchd
24
+ * relaunches. No cron, no periodic kill — relaunch happens only on death.
25
+ *
26
+ * SIGTERM/SIGINT (launchd stopping the job: `kickstart -k`, `bootout`) are
27
+ * owned HERE, not by the singleton's default handlers: the mux session is
28
+ * told to quit (`screen -X quit` / `tmux kill-session`), the run then ends
29
+ * through the normal path with rotation suppressed (a signal-induced death
30
+ * inside the 20 s window is not a broken transcript), and the exit is 75.
31
+ * Exiting 0 at once — the singleton default — would orphan screen/claude in
32
+ * the supervisor's process group, which launchd then SIGKILLs, leaving the
33
+ * dead socket that step 3 has to wipe.
34
+ *
35
+ * Exit codes: 0 = another supervisor holds the lock (or nothing to supervise);
36
+ * 75 (EX_TEMPFAIL) = the session ended, relaunch me.
37
+ *
38
+ * All effects are injected (`execFile`, fs, clock, sleep, lock) so the whole
39
+ * lifecycle is unit-tested without a terminal, a mux or a `claude` binary.
40
+ * The decisions live in lib/session/*; this file sequences them.
41
+ *
42
+ * @module scripts/session/supervisor
43
+ */
44
+
45
+ import { existsSync as fsExistsSync, readFileSync as fsReadFileSync, mkdirSync as fsMkdirSync, unlinkSync as fsUnlinkSync } from "node:fs";
46
+ import { spawn } from "node:child_process";
47
+ import { homedir } from "node:os";
48
+ import { pathToFileURL } from "node:url";
49
+
50
+ import { resolveAgentRoot } from "../../lib/agent-root.mjs";
51
+ import { writeJsonAtomic as fsWriteJsonAtomic, writeFileAtomic as fsWriteFileAtomic } from "../../lib/fs-atomic.mjs";
52
+ import { parseHeartbeat } from "../../lib/session/liveness.mjs";
53
+ import { ensureClaudeConfig, heartbeatSilence, attentionRecord, HEARTBEAT_GRACE_MS } from "../../lib/session/first-run.mjs";
54
+ import { acquireLock as singletonAcquireLock } from "../../lib/singleton.js";
55
+ import { resolveClaudeBin } from "../../lib/claude-bin.mjs";
56
+ import { sessionPermissionArgs, MAIN_SESSION_SOURCE } from "../../lib/session-permissions.mjs";
57
+ import { loadSessionConfig, sessionPaths } from "../../lib/session/config.mjs";
58
+ import {
59
+ loadMainSession, saveMainSession, newMainSession, agentFirstName, muxSessionName,
60
+ launchMode, recordLaunch, rotationDecision, rotateMainSession, BACKOFF_MS,
61
+ } from "../../lib/session/identity.mjs";
62
+ import { buildClaudeArgs, detectMux, chooseMux, buildMuxCommands, parseScreenList, parseExitFile } from "../../lib/session/launch-args.mjs";
63
+
64
+ /** Another supervisor holds the lock (or the front door is not the session). Clean. */
65
+ export const EXIT_ANOTHER_HOLDS_LOCK = 0;
66
+ /** The session ended; launchd should relaunch (EX_TEMPFAIL). */
67
+ export const EXIT_RELAUNCH = 75;
68
+ /** Name of the singleton lock (state/locks/process/session.pid). */
69
+ export const LOCK_NAME = "session";
70
+ /** How often the tmux `has-session` probe (and the orphan probe) is polled. */
71
+ export const DEFAULT_POLL_MS = 2_000;
72
+ /** The signals the supervisor owns (launchd sends SIGTERM to stop a job). */
73
+ export const OWNED_SIGNALS = Object.freeze(["SIGTERM", "SIGINT"]);
74
+
75
+ /** Real signal wiring: `fn(signal)` on each owned signal. */
76
+ function installSignalHandlers(fn) {
77
+ for (const sig of OWNED_SIGNALS) process.on(sig, () => fn(sig));
78
+ }
79
+
80
+ /** Real one-shot timer (unref'd: it must never keep the process alive). Returns cancel. */
81
+ function defaultAfter(ms, fn) {
82
+ const t = setTimeout(fn, ms);
83
+ if (t && typeof t.unref === "function") t.unref();
84
+ return () => clearTimeout(t);
85
+ }
86
+
87
+ /**
88
+ * Real `execFile`: spawn without a shell, capture stdout, resolve with the
89
+ * exit code (never reject on a non-zero code — that is a result, not a defect).
90
+ * stderr is inherited so mux/claude diagnostics reach the supervisor log.
91
+ * @returns {Promise<{code:number|null, stdout:string, stderr:string}>}
92
+ */
93
+ export function execFileResult(file, args, opts = {}) {
94
+ return new Promise((resolve, reject) => {
95
+ let child;
96
+ try {
97
+ child = spawn(file, args, { cwd: opts.cwd, env: opts.env || process.env, stdio: ["ignore", "pipe", "inherit"] });
98
+ } catch (err) { reject(err); return; }
99
+ let stdout = "";
100
+ child.stdout.on("data", (d) => { stdout += d; });
101
+ child.on("error", reject);
102
+ child.on("close", (code) => resolve({ code, stdout, stderr: "" }));
103
+ });
104
+ }
105
+
106
+ const defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
107
+
108
+ /**
109
+ * Block while `probe` exits 0 (tmux `has-session`); return once it does not.
110
+ * @param {{file:string, args:string[]}} probe
111
+ * @param {{execFile:Function, sleep:Function, pollMs:number}} d
112
+ */
113
+ export async function waitForProbe(probe, d) {
114
+ for (;;) {
115
+ let r;
116
+ try { r = await d.execFile(probe.file, probe.args); } catch { return; /* probe binary gone → treat as ended */ }
117
+ if (!r || r.code !== 0) return;
118
+ await d.sleep(d.pollMs);
119
+ }
120
+ }
121
+
122
+ /** Does the mux session exist right now? */
123
+ async function muxSessionExists(mux, cmds, muxName, d) {
124
+ let r;
125
+ try { r = await d.execFile(cmds.exists.file, cmds.exists.args); } catch { return false; }
126
+ if (mux.kind === "screen") return parseScreenList(r && r.stdout, muxName);
127
+ return !!r && r.code === 0;
128
+ }
129
+
130
+ /** Block until the mux session is gone. */
131
+ async function waitForMuxSession(mux, cmds, muxName, d) {
132
+ if (mux.kind === "tmux") return waitForProbe(cmds.waitProbe, d);
133
+ while (await muxSessionExists(mux, cmds, muxName, d)) await d.sleep(d.pollMs);
134
+ }
135
+
136
+ /**
137
+ * One main-session lifetime. See the module docblock.
138
+ * @param {object} [deps] every effect, injectable; defaults are the real ones
139
+ * @returns {Promise<number>} exit code
140
+ */
141
+ export async function runSupervisor(deps = {}) {
142
+ const d = {
143
+ agentRoot: deps.agentRoot || resolveAgentRoot(),
144
+ env: deps.env || process.env,
145
+ acquireLock: deps.acquireLock || singletonAcquireLock,
146
+ existsSync: deps.existsSync || fsExistsSync,
147
+ readFileSync: deps.readFileSync || fsReadFileSync,
148
+ writeJsonAtomic: deps.writeJsonAtomic || fsWriteJsonAtomic,
149
+ writeFileAtomic: deps.writeFileAtomic || fsWriteFileAtomic,
150
+ homeDir: deps.homeDir === undefined ? homedir() : deps.homeDir,
151
+ after: deps.after || defaultAfter,
152
+ heartbeatGraceMs: deps.heartbeatGraceMs ?? HEARTBEAT_GRACE_MS,
153
+ mkdirSync: deps.mkdirSync || fsMkdirSync,
154
+ unlinkSync: deps.unlinkSync || fsUnlinkSync,
155
+ now: deps.now || Date.now,
156
+ sleep: deps.sleep || defaultSleep,
157
+ log: deps.log || ((line) => process.stderr.write(`[session-supervisor] ${line}\n`)),
158
+ uuid: deps.uuid,
159
+ execFile: deps.execFile || execFileResult,
160
+ loadConfig: deps.loadConfig || ((root) => loadSessionConfig(root)),
161
+ claudeBin: deps.claudeBin || null,
162
+ permissionArgs: deps.permissionArgs || null,
163
+ pollMs: deps.pollMs ?? DEFAULT_POLL_MS,
164
+ onSignal: deps.onSignal || installSignalHandlers,
165
+ };
166
+ const fsDeps = { readFileSync: d.readFileSync, writeJsonAtomic: d.writeJsonAtomic };
167
+ const agentRoot = d.agentRoot;
168
+ const paths = sessionPaths(agentRoot);
169
+
170
+ // 1. Exactly one. The lock's own signal handlers are declined: SIGTERM is
171
+ // handled below so the mux session is stopped before this process ends.
172
+ if (!d.acquireLock(LOCK_NAME, { signals: false })) {
173
+ d.log("another supervisor holds the session lock — exiting clean");
174
+ return EXIT_ANOTHER_HOLDS_LOCK;
175
+ }
176
+
177
+ // Shutdown on request. `stopCmd` is set once a mux session is ours to stop
178
+ // (adopted or launched); a signal before that only marks the run as
179
+ // stopping so nothing new is launched.
180
+ let stopping = null;
181
+ let stopCmd = null;
182
+ let stopIssued = null;
183
+ const requestStop = () => {
184
+ if (stopIssued || !stopCmd) return;
185
+ stopIssued = Promise.resolve()
186
+ .then(() => d.execFile(stopCmd.file, stopCmd.args))
187
+ .catch((err) => { d.log(`mux stop failed: ${err && err.message ? err.message : err}`); });
188
+ };
189
+ d.onSignal((sig) => {
190
+ if (stopping) return;
191
+ stopping = sig;
192
+ d.log(`${sig} received — stopping the session and exiting ${EXIT_RELAUNCH}`);
193
+ requestStop();
194
+ });
195
+
196
+ const cfg = await d.loadConfig(agentRoot);
197
+ if (cfg.frontDoor !== "session") {
198
+ d.log(`frontDoor is "${cfg.frontDoor}" — nothing to supervise, exiting clean`);
199
+ return EXIT_ANOTHER_HOLDS_LOCK;
200
+ }
201
+
202
+ const first = agentFirstName(agentRoot, fsDeps);
203
+ const muxName = muxSessionName(first);
204
+ try { d.mkdirSync(paths.stateDir, { recursive: true }); } catch { /* the atomic writers create it too */ }
205
+
206
+ // 2. Multiplexer.
207
+ const mux = chooseMux(cfg.mux, detectMux({ existsSync: d.existsSync }));
208
+ if (!mux) {
209
+ d.log(`no terminal multiplexer found (tmux or /usr/bin/screen) — backing off ${BACKOFF_MS / 60000} min`);
210
+ await d.sleep(BACKOFF_MS);
211
+ return EXIT_RELAUNCH;
212
+ }
213
+
214
+ // 3. Orphan adoption. We hold the lock, so an existing mux session has no
215
+ // supervisor: attach to its lifetime instead of starting a second one.
216
+ // First clear DEAD sockets (a SIGKILLed screen leaves `(Dead ???)` in
217
+ // SCREENDIR, which `-ls` still lists): `screen -wipe` is hygiene and
218
+ // parseScreenList ignores dead lines regardless, so a failing wipe costs
219
+ // nothing.
220
+ const probeCmds = buildMuxCommands({ mux, muxName, cwd: agentRoot, claudeBin: "claude", claudeArgs: [], exitFile: paths.lastExitFile });
221
+ if (probeCmds.wipe) {
222
+ try { await d.execFile(probeCmds.wipe.file, probeCmds.wipe.args); } catch { /* nothing to wipe, or no permission — the probe decides */ }
223
+ }
224
+ if (await muxSessionExists(mux, probeCmds, muxName, d)) {
225
+ d.log(`adopting orphaned ${mux.kind} session ${muxName} — waiting for it to end`);
226
+ stopCmd = probeCmds.stop;
227
+ if (stopping) requestStop();
228
+ await waitForMuxSession(mux, probeCmds, muxName, d);
229
+ await stopIssued;
230
+ d.log(`adopted session ${muxName} ended — exit ${EXIT_RELAUNCH} for relaunch`);
231
+ return EXIT_RELAUNCH;
232
+ }
233
+ if (stopping) return EXIT_RELAUNCH; // asked to stop before anything was launched
234
+
235
+ // 4. Identity + argv.
236
+ let record = loadMainSession(agentRoot, fsDeps);
237
+ if (!record || cfg.resume === false) record = newMainSession({ now: d.now, uuid: d.uuid });
238
+ const mode = launchMode(record, cfg);
239
+ let permissionArgs = d.permissionArgs;
240
+ if (!permissionArgs) {
241
+ permissionArgs = sessionPermissionArgs({ source: MAIN_SESSION_SOURCE, allowedTools: cfg.allowedTools });
242
+ if (d.env.MAESTRO_SCOPED_PERMISSIONS === "1" && !permissionArgs.includes("--allowedTools")) {
243
+ d.log("MAESTRO_SCOPED_PERMISSIONS=1 but config/session.yaml has no allowedTools — the interactive main session runs with bypass (an unattended session cannot answer permission prompts)");
244
+ }
245
+ }
246
+ const claudeArgs = buildClaudeArgs({ first, sessionId: record.sessionId, mode, permissionArgs });
247
+ const cmds = buildMuxCommands({ mux, muxName, cwd: agentRoot, claudeBin: d.claudeBin || resolveClaudeBin(), claudeArgs, exitFile: paths.lastExitFile });
248
+
249
+ // 4b. First-run dialogs. Nobody is attached to press a key, so the two
250
+ // one-time acknowledgements are recorded up front. Fail-open: a seat
251
+ // still launches when this cannot be done — the watchdog below then
252
+ // reports the blocked session instead of nothing at all.
253
+ const seeded = ensureClaudeConfig({
254
+ homeDir: d.homeDir, agentRoot, bypass: permissionArgs.includes("--dangerously-skip-permissions"),
255
+ readFileSync: d.readFileSync, writeFileAtomic: d.writeFileAtomic,
256
+ });
257
+ if (seeded.changed) d.log(`${seeded.path} seeded for an unattended launch: ${seeded.applied.join(", ")}`);
258
+ else if (!seeded.ok) d.log(`${seeded.path || "~/.claude.json"} not seeded (${seeded.error}) — a first-run dialog may block the session; the watchdog will report it`);
259
+
260
+ try { d.unlinkSync(paths.lastExitFile); } catch { /* none from a previous run */ }
261
+ try { d.unlinkSync(paths.attentionFile); } catch { /* none outstanding */ }
262
+ const launched = recordLaunch(record, { now: d.now });
263
+ const saved = saveMainSession(agentRoot, launched, fsDeps);
264
+ if (!saved.ok) d.log(`main-session.json not saved: ${saved.error}`);
265
+ d.log(`launching ${mux.kind} session ${muxName}: claude ${mode === "resume" ? "--resume" : "--session-id"} ${record.sessionId} (resumes=${launched.resumes})`);
266
+
267
+ // 5. Run, in the foreground of this process. The watchdog fires once,
268
+ // after the grace period: a session that has not beaten by then is
269
+ // almost certainly waiting on a dialog or a prompt nobody can see.
270
+ const startedAt = Number(d.now());
271
+ let launchFailed = false;
272
+ stopCmd = cmds.stop;
273
+ const cancelWatchdog = d.after(d.heartbeatGraceMs, () => {
274
+ let hb = null;
275
+ try { hb = parseHeartbeat(d.readFileSync(paths.heartbeatFile, "utf8")); } catch { hb = null; }
276
+ const silence = heartbeatSilence({ startedAt, heartbeat: hb, now: d.now(), graceMs: d.heartbeatGraceMs });
277
+ if (!silence.silent) return;
278
+ const rec = attentionRecord({ reason: silence.reason, mux, muxName, since: startedAt, runMs: silence.runMs });
279
+ try { d.writeJsonAtomic(paths.attentionFile, rec); } catch { /* the log line still says it */ }
280
+ d.log(`session ${muxName} has been up ${Math.round(silence.runMs / 1000)} s with no heartbeat — probably blocked on a first-run dialog or a permission prompt; attach with \`${rec.attach}\` and answer it`);
281
+ });
282
+ try {
283
+ const r = await d.execFile(cmds.start.file, cmds.start.args, { cwd: agentRoot, env: d.env });
284
+ if (r && r.code !== 0) { launchFailed = true; d.log(`${mux.kind} start exited ${r.code}${r.stderr ? `: ${String(r.stderr).trim()}` : ""}`); }
285
+ else if (!cmds.blocking) await waitForProbe(cmds.waitProbe, d);
286
+ } catch (err) {
287
+ launchFailed = true;
288
+ d.log(`${mux.kind} start failed: ${err && err.message ? err.message : err}`);
289
+ }
290
+ const endedAt = Number(d.now());
291
+ try { cancelWatchdog(); } catch { /* already fired */ }
292
+ await stopIssued;
293
+
294
+ let exitCode = null;
295
+ try { exitCode = parseExitFile(d.readFileSync(paths.lastExitFile, "utf8")); } catch { exitCode = null; /* never written */ }
296
+ if (launchFailed && exitCode === null) exitCode = 1;
297
+ d.log(`session ${muxName} ended after ${Math.round((endedAt - startedAt) / 1000)} s (exit ${exitCode === null ? "unknown" : exitCode})`);
298
+
299
+ // 6. Rotation — never for a death we caused.
300
+ if (stopping) {
301
+ d.log(`session ${muxName} stopped on ${stopping} — id kept, exit ${EXIT_RELAUNCH}`);
302
+ return EXIT_RELAUNCH;
303
+ }
304
+ const decision = rotationDecision({ record: launched, mode, exitCode, startedAt, endedAt, now: d.now });
305
+ if (decision.action === "rotate") {
306
+ const rotated = rotateMainSession(launched, { now: d.now, uuid: d.uuid });
307
+ const res = saveMainSession(agentRoot, rotated, fsDeps);
308
+ d.log(`resume of ${launched.sessionId} failed inside the failure window — rotated to ${rotated.sessionId}${res.ok ? "" : ` (not saved: ${res.error})`}`);
309
+ } else if (decision.action === "backoff") {
310
+ d.log(`rotation budget spent for this hour — sleeping ${decision.sleepMs / 60000} min before relaunch`);
311
+ await d.sleep(decision.sleepMs);
312
+ }
313
+ return EXIT_RELAUNCH;
314
+ }
315
+
316
+ const isMain = (() => {
317
+ try { return process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href; } catch { return false; }
318
+ })();
319
+
320
+ if (isMain) {
321
+ runSupervisor()
322
+ .then((code) => process.exit(code))
323
+ .catch((err) => {
324
+ // A defect in the supervisor itself: log it and ask launchd to try again.
325
+ process.stderr.write(`[session-supervisor] fatal: ${err && err.stack ? err.stack : err}\n`);
326
+ process.exit(EXIT_RELAUNCH);
327
+ });
328
+ }
@@ -0,0 +1,141 @@
1
+ #!/bin/bash
2
+ # supervisor.sh — launchd wrapper for the main-session supervisor.
3
+ #
4
+ # Same idiom as scripts/daemon/launchd-wrapper.sh: launchd's environment is
5
+ # bare (no HOME, PATH, AGENT_ROOT), so we set them here and exec node, rather
6
+ # than putting them in the plist (EX_CONFIG failures seen with symlinked paths).
7
+ #
8
+ # What differs from the daemon wrapper: the SESSION needs the seat's `.env`
9
+ # (CLAUDE_CODE_OAUTH_TOKEN, MAESTRO_PREFER_SUBSCRIPTION_AUTH, MAESTRO_SCOPED_
10
+ # PERMISSIONS, …) in its environment, because `claude` is launched by the
11
+ # supervisor inside a multiplexer and reads its auth from env. The daemon
12
+ # loads .env in JS; the supervisor gets it from here so the same values reach
13
+ # screen/tmux → sh → claude without any of them being written to a plist.
14
+ #
15
+ # .env handling mirrors what dotenv (the daemon's loader) does, so the same
16
+ # file yields the same token here and there: only `KEY=value` lines
17
+ # (optionally `export KEY=value`) are read; comments and malformed lines are
18
+ # skipped; a trailing CR (CRLF file) is dropped; one layer of matching quotes
19
+ # is stripped and a quoted `#` is data; an UNQUOTED value ends at the first
20
+ # `#` and is trimmed; a variable ALREADY in the environment wins (so an
21
+ # operator can override from a shell, and a launchd-provided value is never
22
+ # clobbered). Values are never echoed.
23
+ #
24
+ # Exit codes are the supervisor's: 0 = another supervisor holds the lock,
25
+ # 75 = the session ended, relaunch (KeepAlive SuccessfulExit:false does that).
26
+ #
27
+ # AGENT_ROOT: a value already in the environment (plist EnvironmentVariables,
28
+ # an operator shell) wins; otherwise the agent dir is derived from this
29
+ # script's own location, which is right for the copy `maestro upgrade` places
30
+ # at <agent>/scripts/session/. Either way the root must BE an agent directory
31
+ # (config/agent.json present) — running from the global npm package dir would
32
+ # put locks, state and logs inside node_modules and start claude in the wrong
33
+ # cwd, so that exits 78 (EX_CONFIG) with a message instead.
34
+ #
35
+ # This wrapper is exec'd by ai.maestro.{firstname}-session.plist.
36
+
37
+ set -e
38
+
39
+ if [ -z "${AGENT_ROOT:-}" ] || [ ! -d "${AGENT_ROOT:-}" ]; then
40
+ AGENT_ROOT="$(cd "$(dirname "$0")/../.." && pwd -P)"
41
+ else
42
+ AGENT_ROOT="$(cd "$AGENT_ROOT" && pwd -P)"
43
+ fi
44
+ export AGENT_ROOT
45
+ export AGENT_DIR="$AGENT_ROOT"
46
+
47
+ if [ ! -f "$AGENT_ROOT/config/agent.json" ]; then
48
+ echo "[session-wrapper] FATAL: $AGENT_ROOT is not an agent directory (no config/agent.json) — set AGENT_ROOT or run the copy under <agent>/scripts/session/" >&2
49
+ exit 78
50
+ fi
51
+
52
+ # ── emergency stop ──────────────────────────────────────────────────────────
53
+ # scripts/emergency-stop.sh drops .emergency-stop and unloads every job; the
54
+ # daemon honours the marker and exits. The session must too — otherwise a
55
+ # by-hand `maestro upgrade` (whose launchd step is held under the marker, but
56
+ # a login re-bootstraps LaunchAgents regardless) would start the front-door
57
+ # claude session under an active stop. Exit 0: KeepAlive {SuccessfulExit:
58
+ # false} does NOT relaunch a clean exit, so launchd stays quiet until the
59
+ # marker is gone and the job is started again (maestro session start).
60
+ if [ -e "$AGENT_ROOT/.emergency-stop" ] || [ -e "$AGENT_ROOT/state/EMERGENCY_STOP" ]; then
61
+ echo "[session-wrapper] emergency stop is active ($AGENT_ROOT/.emergency-stop or state/EMERGENCY_STOP) — not starting the session; remove the marker and run: maestro session start" >&2
62
+ exit 0
63
+ fi
64
+ export HOME="${HOME:-/Users/$(whoami)}"
65
+ export USER="${USER:-$(whoami)}"
66
+ export PATH="$HOME/.local/bin:/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:$PATH"
67
+
68
+ # ── .env → environment ──────────────────────────────────────────────────────
69
+ maestro_source_env() {
70
+ local file="$1" line key value
71
+ [ -f "$file" ] || return 0
72
+ while IFS= read -r line || [ -n "$line" ]; do
73
+ line="${line%$'\r'}"
74
+ line="${line#export }"
75
+ case "$line" in ''|\#*) continue ;; esac
76
+ case "$line" in *=*) ;; *) continue ;; esac
77
+ key="${line%%=*}"
78
+ value="${line#*=}"
79
+ case "$key" in ''|*[!A-Za-z0-9_]*) continue ;; esac
80
+ case "$value" in
81
+ \"*\") value="${value#\"}"; value="${value%\"}" ;;
82
+ \'*\') value="${value#\'}"; value="${value%\'}" ;;
83
+ *)
84
+ value="${value%%#*}"
85
+ value="${value%"${value##*[![:space:]]}"}"
86
+ value="${value#"${value%%[![:space:]]*}"}"
87
+ ;;
88
+ esac
89
+ if [ -z "${!key+x}" ]; then
90
+ export "$key=$value"
91
+ fi
92
+ done < "$file"
93
+ }
94
+ maestro_source_env "$AGENT_ROOT/.env"
95
+
96
+ cd "$AGENT_ROOT"
97
+
98
+ # ── node ────────────────────────────────────────────────────────────────────
99
+ # NODE_BIN may be pre-set (tests, operators); otherwise prefer nvm, then
100
+ # homebrew, then system — the daemon wrapper's order. MAESTRO_NODE_CANDIDATES
101
+ # (colon-separated) replaces the fixed list so the search is testable and an
102
+ # unusual install can be pointed at without editing this file.
103
+ NODE_BIN="${NODE_BIN:-}"
104
+ if [ -z "$NODE_BIN" ] || [ ! -x "$NODE_BIN" ]; then
105
+ NODE_BIN=""
106
+ if [ -n "${MAESTRO_NODE_CANDIDATES:-}" ]; then
107
+ IFS=':' read -r -a node_candidates <<< "$MAESTRO_NODE_CANDIDATES"
108
+ else
109
+ node_candidates=(
110
+ "$HOME/.nvm/versions/node/v24.11.1/bin/node"
111
+ "$HOME/.nvm/versions/node/v24/bin/node"
112
+ "$HOME/.nvm/versions/node/v22/bin/node"
113
+ "$HOME/.nvm/versions/node/v20/bin/node"
114
+ /opt/homebrew/bin/node
115
+ /usr/local/bin/node
116
+ /usr/bin/node
117
+ )
118
+ fi
119
+ for candidate in "${node_candidates[@]}"; do
120
+ if [ -n "$candidate" ] && [ -x "$candidate" ]; then
121
+ NODE_BIN="$candidate"
122
+ break
123
+ fi
124
+ done
125
+ fi
126
+ if [ -z "$NODE_BIN" ] && [ -d "$HOME/.nvm/versions/node" ]; then
127
+ NODE_BIN=$(ls -1d "$HOME/.nvm/versions/node"/v*/bin/node 2>/dev/null | sort -V | tail -1)
128
+ fi
129
+ if [ -z "$NODE_BIN" ] || [ ! -x "$NODE_BIN" ]; then
130
+ echo "[session-wrapper] FATAL: could not find node binary" >&2
131
+ exit 127
132
+ fi
133
+ export NODE_BIN
134
+
135
+ # ── exec ────────────────────────────────────────────────────────────────────
136
+ # The supervisor's own diagnostics go to logs/session/; the session itself
137
+ # lives inside the multiplexer and is not captured here.
138
+ SESSION_LOG_DIR="${MAESTRO_SESSION_LOG_DIR:-$AGENT_ROOT/logs/session}"
139
+ mkdir -p "$SESSION_LOG_DIR" 2>/dev/null || true
140
+ SESSION_LOG="$SESSION_LOG_DIR/supervisor-$(date +%Y-%m-%d).log"
141
+ exec "$NODE_BIN" "$AGENT_ROOT/scripts/session/supervisor.mjs" "$@" >> "$SESSION_LOG" 2>&1