@cohortapp/agent-sdk 2.16.0 → 2.18.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (529) hide show
  1. package/.claude/settings.json +18 -0
  2. package/.env.example +23 -7
  3. package/README.md +1 -0
  4. package/bin/maestro.mjs +62 -0
  5. package/docs/guides/billing-console-keys.md +60 -0
  6. package/docs/guides/front-door-session.md +54 -9
  7. package/docs/guides/mac-mini.md +20 -25
  8. package/docs/guides/poller-daemon-setup.md +4 -1
  9. package/docs/guides/setup-wizard.md +1 -1
  10. package/docs/runbooks/fleet-rollout.md +156 -0
  11. package/docs/runbooks/mac-mini-bootstrap.md +12 -14
  12. package/lib/action-executor.js +19 -3
  13. package/lib/budget-guard.mjs +279 -3
  14. package/lib/channels/base-adapter.mjs +3 -1
  15. package/lib/channels/contract.mjs +2 -1
  16. package/lib/channels/inbox-item.mjs +8 -0
  17. package/lib/claude-bin.mjs +5 -6
  18. package/lib/cli/doctor-checks.mjs +141 -10
  19. package/lib/cli/global-setup-extras.mjs +5 -1
  20. package/lib/cli/inbox.mjs +100 -15
  21. package/lib/cli/seat-auth.mjs +463 -0
  22. package/lib/cli/session.mjs +80 -12
  23. package/lib/collective/capture.mjs +8 -6
  24. package/lib/collective/global-config.mjs +63 -1
  25. package/lib/collective/presence.mjs +142 -5
  26. package/lib/comms/send-gate.mjs +559 -1
  27. package/lib/context/budget.mjs +327 -0
  28. package/lib/context/history-scope.mjs +138 -0
  29. package/lib/diagnostics/alerts.mjs +49 -0
  30. package/lib/diagnostics/cadence-output-freshness.mjs +288 -0
  31. package/lib/engine/agents/definitions.mjs +343 -0
  32. package/lib/engine/agents/persist.mjs +275 -0
  33. package/lib/engine/agents/runtime.mjs +748 -0
  34. package/lib/engine/agents/usage.mjs +95 -0
  35. package/lib/engine/auth-status.mjs +139 -0
  36. package/lib/engine/budget.mjs +194 -0
  37. package/lib/engine/cli.mjs +1204 -0
  38. package/lib/engine/commands/index.mjs +269 -0
  39. package/lib/engine/context/budget.mjs +219 -0
  40. package/lib/engine/context/cache.mjs +125 -0
  41. package/lib/engine/context/child-env.mjs +215 -0
  42. package/lib/engine/context/compaction.mjs +342 -0
  43. package/lib/engine/context/images.mjs +90 -0
  44. package/lib/engine/context/instructions.mjs +327 -0
  45. package/lib/engine/context/lazy-instructions.mjs +169 -0
  46. package/lib/engine/context/manager.mjs +182 -0
  47. package/lib/engine/context/real-path.mjs +91 -0
  48. package/lib/engine/context/secret-values.mjs +163 -0
  49. package/lib/engine/context/settings.mjs +274 -0
  50. package/lib/engine/context/stream-input.mjs +159 -0
  51. package/lib/engine/guard.mjs +152 -0
  52. package/lib/engine/hooks.mjs +713 -0
  53. package/lib/engine/loop.mjs +560 -0
  54. package/lib/engine/mcp/client.mjs +254 -0
  55. package/lib/engine/mcp/config.mjs +301 -0
  56. package/lib/engine/mcp/http.mjs +201 -0
  57. package/lib/engine/mcp/index.mjs +146 -0
  58. package/lib/engine/mcp/jsonrpc.mjs +147 -0
  59. package/lib/engine/mcp/naming.mjs +66 -0
  60. package/lib/engine/mcp/resources.mjs +89 -0
  61. package/lib/engine/mcp/results.mjs +133 -0
  62. package/lib/engine/mcp/stdio.mjs +137 -0
  63. package/lib/engine/mcp/supervisor.mjs +116 -0
  64. package/lib/engine/messages.mjs +104 -0
  65. package/lib/engine/output/json.mjs +164 -0
  66. package/lib/engine/output/stream-json.mjs +266 -0
  67. package/lib/engine/permissions.mjs +845 -0
  68. package/lib/engine/process-identity.mjs +164 -0
  69. package/lib/engine/process-tree.mjs +551 -0
  70. package/lib/engine/prompt.mjs +60 -0
  71. package/lib/engine/session/store.mjs +299 -0
  72. package/lib/engine/session-runtime/args.mjs +97 -0
  73. package/lib/engine/session-runtime/host.mjs +143 -0
  74. package/lib/engine/session-runtime/inbox.mjs +122 -0
  75. package/lib/engine/session-runtime/notifications.mjs +129 -0
  76. package/lib/engine/session-runtime/registry.mjs +328 -0
  77. package/lib/engine/session-runtime/runner.mjs +344 -0
  78. package/lib/engine/session-runtime/socket.mjs +212 -0
  79. package/lib/engine/session-runtime/wakeup.mjs +115 -0
  80. package/lib/engine/skills/index.mjs +321 -0
  81. package/lib/engine/tools/bash-background.mjs +533 -0
  82. package/lib/engine/tools/bash.mjs +216 -0
  83. package/lib/engine/tools/edit.mjs +97 -0
  84. package/lib/engine/tools/glob.mjs +81 -0
  85. package/lib/engine/tools/grep.mjs +224 -0
  86. package/lib/engine/tools/index.mjs +84 -0
  87. package/lib/engine/tools/list-agents.mjs +32 -0
  88. package/lib/engine/tools/ls.mjs +127 -0
  89. package/lib/engine/tools/monitor.mjs +82 -0
  90. package/lib/engine/tools/notebook-edit.mjs +218 -0
  91. package/lib/engine/tools/read.mjs +103 -0
  92. package/lib/engine/tools/schedule-wakeup.mjs +45 -0
  93. package/lib/engine/tools/schema.mjs +144 -0
  94. package/lib/engine/tools/send-message.mjs +77 -0
  95. package/lib/engine/tools/session.mjs +70 -0
  96. package/lib/engine/tools/todo.mjs +144 -0
  97. package/lib/engine/tools/toolsearch.mjs +217 -0
  98. package/lib/engine/tools/walk.mjs +193 -0
  99. package/lib/engine/tools/web-switch.mjs +31 -0
  100. package/lib/engine/tools/webfetch-html.mjs +387 -0
  101. package/lib/engine/tools/webfetch-net.mjs +340 -0
  102. package/lib/engine/tools/webfetch.mjs +198 -0
  103. package/lib/engine/tools/websearch.mjs +91 -0
  104. package/lib/engine/tools/workflow.mjs +95 -0
  105. package/lib/engine/tools/write.mjs +76 -0
  106. package/lib/engine/tui/line-editor.mjs +137 -0
  107. package/lib/engine/tui/render.mjs +86 -0
  108. package/lib/engine/tui/tui.mjs +274 -0
  109. package/lib/engine/wire/anthropic-messages.mjs +263 -0
  110. package/lib/engine/wire/effort.mjs +36 -0
  111. package/lib/engine/wire/errors.mjs +496 -0
  112. package/lib/engine/wire/http.mjs +441 -0
  113. package/lib/engine/wire/index.mjs +76 -0
  114. package/lib/engine/wire/openai-chat.mjs +332 -0
  115. package/lib/engine/wire/prompt-cache.mjs +79 -0
  116. package/lib/engine/wire/search.mjs +140 -0
  117. package/lib/engine/wire/sse.mjs +114 -0
  118. package/lib/engine/wire/stall.mjs +349 -0
  119. package/lib/engine/wire/token-provider.mjs +175 -0
  120. package/lib/engine/wire/usage.mjs +192 -0
  121. package/lib/engine/workflow/host.mjs +524 -0
  122. package/lib/engine/workflow/journal.mjs +188 -0
  123. package/lib/engine/workflow/json-schema.mjs +171 -0
  124. package/lib/engine/workflow/meta.mjs +329 -0
  125. package/lib/engine/workflow/notifications.mjs +52 -0
  126. package/lib/engine/workflow/runtime.mjs +447 -0
  127. package/lib/engine/workflow/sandbox.mjs +534 -0
  128. package/lib/engine/workflow/worker.mjs +141 -0
  129. package/lib/engine/workflow/worktree.mjs +74 -0
  130. package/lib/execution/disposition.mjs +1 -1
  131. package/lib/execution/intake.mjs +10 -0
  132. package/lib/execution/surface-policy.mjs +15 -0
  133. package/lib/learning/curator.mjs +8 -6
  134. package/lib/learning/reflect.mjs +8 -6
  135. package/lib/model-router/catalog/cohort.yaml +137 -0
  136. package/lib/model-router/catalog.mjs +118 -1
  137. package/lib/model-router/economics.mjs +9 -0
  138. package/lib/model-router/failover.mjs +67 -16
  139. package/lib/model-router/llm-task.mjs +39 -3
  140. package/lib/model-router/resolve.mjs +95 -3
  141. package/lib/model-router/spawn.mjs +46 -47
  142. package/lib/model-router/taxonomy.mjs +126 -4
  143. package/lib/org/cost-sync.mjs +141 -11
  144. package/lib/org/inbound/broadcast.mjs +289 -0
  145. package/lib/org/inbound/collective.mjs +375 -0
  146. package/lib/org/inbound/directedness.mjs +96 -8
  147. package/lib/org/inbound/facts.mjs +82 -4
  148. package/lib/org/inbound/hydrate.mjs +555 -51
  149. package/lib/org/inbound/project.mjs +22 -0
  150. package/lib/org/inbound/surfaces.mjs +14 -0
  151. package/lib/org/llm-token.mjs +879 -0
  152. package/lib/org/mesh.mjs +61 -0
  153. package/lib/org/messaging.mjs +3 -1
  154. package/lib/org/protocol.checksum +1 -1
  155. package/lib/org/protocol.mjs +15 -0
  156. package/lib/org/quota.mjs +520 -0
  157. package/lib/org/tool-surface.mjs +104 -16
  158. package/lib/org/ui-parity.mjs +16 -1
  159. package/lib/org/work-ledger.mjs +37 -6
  160. package/lib/rate-guard.mjs +114 -1
  161. package/lib/resource-governor.mjs +41 -6
  162. package/lib/runtime/adapter.mjs +823 -0
  163. package/lib/runtime/child-env.mjs +191 -0
  164. package/lib/runtime/legacy-shell-guard.mjs +97 -0
  165. package/lib/runtime/seat-engine.mjs +162 -0
  166. package/lib/session/ask-ledger.mjs +271 -0
  167. package/lib/session/current-work.mjs +676 -0
  168. package/lib/session/feed-core.mjs +40 -3
  169. package/lib/session/launch-args.mjs +56 -4
  170. package/lib/session/status-summary.mjs +26 -9
  171. package/lib/session/upgrade-notice.mjs +42 -0
  172. package/lib/setup/claude-probe.mjs +117 -13
  173. package/lib/setup/enrich.mjs +13 -10
  174. package/lib/setup/sections/model.mjs +39 -13
  175. package/lib/telemetry/collect.mjs +208 -9
  176. package/lib/upgrade/ignored-drift.mjs +105 -0
  177. package/lib/voice/post-call-brief.mjs +30 -17
  178. package/package.json +15 -3
  179. package/plugins/maestro-skills/skills/board-work.md +5 -0
  180. package/plugins/maestro-skills/skills/inbound-triage.md +56 -15
  181. package/plugins/maestro-skills/skills/main-session.md +18 -7
  182. package/scripts/ci/check-tarball-fidelity.mjs +126 -2
  183. package/scripts/ci/run-tests.mjs +47 -19
  184. package/scripts/cohort-llm/api-key-helper.mjs +92 -0
  185. package/scripts/collective/hook-runner.mjs +29 -2
  186. package/scripts/continuous-monitor.sh +13 -0
  187. package/scripts/cost/track-claude-usage.mjs +15 -0
  188. package/scripts/daemon/agent-daemon.mjs +408 -20
  189. package/scripts/daemon/assurance.mjs +48 -12
  190. package/scripts/daemon/cadence-consumer.mjs +218 -68
  191. package/scripts/daemon/cadence-handlers.mjs +73 -4
  192. package/scripts/daemon/classifier.mjs +75 -26
  193. package/scripts/daemon/context-compiler.mjs +104 -59
  194. package/scripts/daemon/deliver.mjs +30 -1
  195. package/scripts/daemon/dispatcher.mjs +804 -157
  196. package/scripts/daemon/health.mjs +14 -1
  197. package/scripts/daemon/lib/session-router.mjs +310 -42
  198. package/scripts/daemon/maestro-daemon.mjs +11 -0
  199. package/scripts/daemon/prompt-builder.mjs +121 -12
  200. package/scripts/daemon/responder.mjs +315 -146
  201. package/scripts/daemon/sdk-version.mjs +98 -16
  202. package/scripts/eval/probe-gateway.mjs +635 -0
  203. package/scripts/eval/replay/extract.mjs +270 -0
  204. package/scripts/eval/replay/grade.mjs +260 -0
  205. package/scripts/eval/replay/lib/config.mjs +50 -0
  206. package/scripts/eval/replay/lib/effects.mjs +65 -0
  207. package/scripts/eval/replay/lib/fixture.mjs +188 -0
  208. package/scripts/eval/replay/lib/judge.mjs +72 -0
  209. package/scripts/eval/replay/lib/redact.mjs +136 -0
  210. package/scripts/eval/replay/lib/sandbox.mjs +170 -0
  211. package/scripts/eval/replay/lib/schema-check.mjs +63 -0
  212. package/scripts/eval/replay/lib/transcript.mjs +76 -0
  213. package/scripts/eval/replay/mcp-replay-stub.mjs +101 -0
  214. package/scripts/eval/replay/report.mjs +185 -0
  215. package/scripts/eval/replay/run.mjs +404 -0
  216. package/scripts/fleet/rollout.mjs +1094 -0
  217. package/scripts/hooks/pre-send-audit.sh +36 -245
  218. package/scripts/hooks/pre-write-yaml-validate.mjs +275 -0
  219. package/scripts/hooks/validate-state-yaml.sh +190 -0
  220. package/scripts/huddle/huddle-llm.mjs +361 -0
  221. package/scripts/huddle/huddle-server.mjs +46 -121
  222. package/scripts/local-triggers/autoupdate.sh +448 -78
  223. package/scripts/local-triggers/run-trigger.sh +13 -0
  224. package/scripts/maintenance/pin-integrity.mjs +364 -0
  225. package/scripts/poll-slack-events.sh +41 -9
  226. package/scripts/poller/slack-socket-mode.mjs +28 -3
  227. package/scripts/session/supervisor.mjs +80 -13
  228. package/scripts/spawn-session.sh +13 -0
  229. package/bin/maestro.test.mjs +0 -1574
  230. package/lib/action-executor.test.mjs +0 -871
  231. package/lib/archetype.test.mjs +0 -132
  232. package/lib/assurance/plan-note.test.mjs +0 -234
  233. package/lib/assurance/room-budget.test.mjs +0 -486
  234. package/lib/assurance/tier.test.mjs +0 -174
  235. package/lib/autonomy.test.mjs +0 -66
  236. package/lib/backlog.test.mjs +0 -302
  237. package/lib/backup/policy.test.mjs +0 -305
  238. package/lib/budget-escalate.test.mjs +0 -232
  239. package/lib/budget-guard.envelope.test.mjs +0 -476
  240. package/lib/budget-guard.test.mjs +0 -427
  241. package/lib/cadence-bus-requeue.test.mjs +0 -83
  242. package/lib/cadence-bus-schedule.test.mjs +0 -194
  243. package/lib/cadence-bus.test.mjs +0 -720
  244. package/lib/cadences.test.mjs +0 -230
  245. package/lib/capability/inventory.test.mjs +0 -232
  246. package/lib/capability.test.mjs +0 -78
  247. package/lib/channels/base-adapter.test.mjs +0 -590
  248. package/lib/channels/channels.test.mjs +0 -371
  249. package/lib/channels/contract.test.mjs +0 -162
  250. package/lib/channels/inbox-item.test.mjs +0 -368
  251. package/lib/channels/orgmail/adapter.test.mjs +0 -448
  252. package/lib/channels/pairing.test.mjs +0 -270
  253. package/lib/channels/repeat-suppressor.test.mjs +0 -134
  254. package/lib/channels/slack-adapter.test.mjs +0 -212
  255. package/lib/channels/telegram-adapter.test.mjs +0 -306
  256. package/lib/channels/voice/adapter.test.mjs +0 -278
  257. package/lib/channels/whatsapp/adapter-baileys.test.mjs +0 -359
  258. package/lib/channels/whatsapp/baileys-typing.test.mjs +0 -154
  259. package/lib/charter.test.mjs +0 -89
  260. package/lib/claude-bin.test.mjs +0 -131
  261. package/lib/cli/board.test.mjs +0 -227
  262. package/lib/cli/design.test.mjs +0 -270
  263. package/lib/cli/doctor-checks.test.mjs +0 -336
  264. package/lib/cli/global-setup-extras.test.mjs +0 -462
  265. package/lib/cli/inbox.test.mjs +0 -230
  266. package/lib/cli/session-ack.test.mjs +0 -63
  267. package/lib/cli/session.test.mjs +0 -613
  268. package/lib/collective/capture.test.mjs +0 -121
  269. package/lib/collective/cards.test.mjs +0 -114
  270. package/lib/collective/config.test.mjs +0 -123
  271. package/lib/collective/global-config.test.mjs +0 -220
  272. package/lib/collective/global-skills.test.mjs +0 -126
  273. package/lib/collective/presence.test.mjs +0 -95
  274. package/lib/collective/recall.test.mjs +0 -116
  275. package/lib/collective/vendor-skills.test.mjs +0 -306
  276. package/lib/comms/send-gate.test.mjs +0 -770
  277. package/lib/comms.test.mjs +0 -41
  278. package/lib/cost/ledger-row.test.mjs +0 -183
  279. package/lib/design/design-md.test.mjs +0 -318
  280. package/lib/design/fixtures/DESIGN.golden.md +0 -238
  281. package/lib/design/fixtures/PRODUCT.golden.md +0 -67
  282. package/lib/design/fixtures/foundation.json +0 -133
  283. package/lib/design/refresh-gate.test.mjs +0 -144
  284. package/lib/design/write.test.mjs +0 -241
  285. package/lib/diagnostics/alerts.test.mjs +0 -318
  286. package/lib/diagnostics/backup-freshness.test.mjs +0 -185
  287. package/lib/diagnostics/counters.test.mjs +0 -206
  288. package/lib/diagnostics/events.test.mjs +0 -290
  289. package/lib/diagnostics/otel.test.mjs +0 -196
  290. package/lib/diagnostics/trace.test.mjs +0 -251
  291. package/lib/env-compat.test.mjs +0 -104
  292. package/lib/execution/disposition.test.mjs +0 -553
  293. package/lib/execution/drive.test.mjs +0 -270
  294. package/lib/execution/effects.test.mjs +0 -344
  295. package/lib/execution/intake.test.mjs +0 -389
  296. package/lib/execution/journal.test.mjs +0 -261
  297. package/lib/execution/match.test.mjs +0 -235
  298. package/lib/execution/pipeline.test.mjs +0 -392
  299. package/lib/execution/route.test.mjs +0 -186
  300. package/lib/execution/surface-policy.test.mjs +0 -162
  301. package/lib/fs-atomic.test.mjs +0 -72
  302. package/lib/fs-ownership.test.mjs +0 -158
  303. package/lib/goals/admission.test.mjs +0 -164
  304. package/lib/goals/classify.test.mjs +0 -167
  305. package/lib/goals/collaborate.test.mjs +0 -336
  306. package/lib/goals/gaps.test.mjs +0 -284
  307. package/lib/goals/loop.test.mjs +0 -845
  308. package/lib/hooks/bus.test.mjs +0 -387
  309. package/lib/identity/persona.test.mjs +0 -142
  310. package/lib/kpi-sensors.test.mjs +0 -278
  311. package/lib/kpi.test.mjs +0 -244
  312. package/lib/learning/config.test.mjs +0 -75
  313. package/lib/learning/counters.test.mjs +0 -69
  314. package/lib/learning/curator-consolidate.test.mjs +0 -238
  315. package/lib/learning/curator.test.mjs +0 -106
  316. package/lib/learning/reflect.test.mjs +0 -0
  317. package/lib/learning/session-index.test.mjs +0 -125
  318. package/lib/learning/skill-writer.test.mjs +0 -210
  319. package/lib/mandate/audit.test.mjs +0 -195
  320. package/lib/mandate/contract.test.mjs +0 -185
  321. package/lib/mandate/derive.test.mjs +0 -274
  322. package/lib/mandate/model.test.mjs +0 -164
  323. package/lib/mandate/refresh.test.mjs +0 -389
  324. package/lib/mcp/server.test.mjs +0 -426
  325. package/lib/model-router/auth-profiles.test.mjs +0 -580
  326. package/lib/model-router/catalog.test.mjs +0 -385
  327. package/lib/model-router/economics.test.mjs +0 -438
  328. package/lib/model-router/failover.test.mjs +0 -439
  329. package/lib/model-router/health.test.mjs +0 -338
  330. package/lib/model-router/integration-coverage.test.mjs +0 -831
  331. package/lib/model-router/integration.test.mjs +0 -564
  332. package/lib/model-router/ledger.test.mjs +0 -415
  333. package/lib/model-router/llm-task.test.mjs +0 -392
  334. package/lib/model-router/org-credentials.test.mjs +0 -265
  335. package/lib/model-router/pricing-refresh.test.mjs +0 -286
  336. package/lib/model-router/reconcile.test.mjs +0 -316
  337. package/lib/model-router/repair.test.mjs +0 -180
  338. package/lib/model-router/spawn.test.mjs +0 -446
  339. package/lib/model-router/taxonomy.test.mjs +0 -410
  340. package/lib/model-router.test.mjs +0 -1207
  341. package/lib/org/activity.test.mjs +0 -134
  342. package/lib/org/approvals.test.mjs +0 -216
  343. package/lib/org/awareness.test.mjs +0 -159
  344. package/lib/org/board-mine-cache.test.mjs +0 -53
  345. package/lib/org/board.test.mjs +0 -187
  346. package/lib/org/bootstrap-context.test.mjs +0 -153
  347. package/lib/org/client.test.mjs +0 -1206
  348. package/lib/org/cohort-client.test.mjs +0 -126
  349. package/lib/org/cost-sync.test.mjs +0 -153
  350. package/lib/org/doctor.test.mjs +0 -346
  351. package/lib/org/engagement-ledger.test.mjs +0 -112
  352. package/lib/org/engagement.test.mjs +0 -739
  353. package/lib/org/handoff.test.mjs +0 -269
  354. package/lib/org/inbound/directedness.test.mjs +0 -668
  355. package/lib/org/inbound/facts.test.mjs +0 -471
  356. package/lib/org/inbound/hydrate.test.mjs +0 -453
  357. package/lib/org/inbound/index.test.mjs +0 -429
  358. package/lib/org/inbound/project.test.mjs +0 -287
  359. package/lib/org/integration-tools.test.mjs +0 -160
  360. package/lib/org/keys.test.mjs +0 -92
  361. package/lib/org/knowledge.test.mjs +0 -326
  362. package/lib/org/leases.test.mjs +0 -235
  363. package/lib/org/mesh-directives.test.mjs +0 -110
  364. package/lib/org/mesh-integration.test.mjs +0 -127
  365. package/lib/org/mesh.test.mjs +0 -400
  366. package/lib/org/messaging.test.mjs +0 -471
  367. package/lib/org/param-contract.test.mjs +0 -477
  368. package/lib/org/policy.test.mjs +0 -237
  369. package/lib/org/protocol.checksum.test.mjs +0 -90
  370. package/lib/org/protocol.test.mjs +0 -323
  371. package/lib/org/push.test.mjs +0 -792
  372. package/lib/org/registry.test.mjs +0 -100
  373. package/lib/org/resource-tools.test.mjs +0 -361
  374. package/lib/org/tool-access.test.mjs +0 -144
  375. package/lib/org/tool-surface-integration.test.mjs +0 -120
  376. package/lib/org/tool-surface.test.mjs +0 -1268
  377. package/lib/org/typing.test.mjs +0 -291
  378. package/lib/org/ui-parity.test.mjs +0 -560
  379. package/lib/org/verify.test.mjs +0 -194
  380. package/lib/org/work-ledger.test.mjs +0 -273
  381. package/lib/plan/adoption-e2e.test.mjs +0 -366
  382. package/lib/plan/budget-enforcement.test.mjs +0 -400
  383. package/lib/plan/compile.test.mjs +0 -382
  384. package/lib/plan/emit.test.mjs +0 -269
  385. package/lib/plan/explain.test.mjs +0 -188
  386. package/lib/prompts/parallelism.test.mjs +0 -177
  387. package/lib/rag/rag.test.mjs +0 -505
  388. package/lib/rate-guard.test.mjs +0 -272
  389. package/lib/reactive-gate.test.mjs +0 -57
  390. package/lib/render.test.mjs +0 -68
  391. package/lib/resource-governor.test.mjs +0 -488
  392. package/lib/scheduling/dynamic-jobs.test.mjs +0 -344
  393. package/lib/scheduling/jitter.test.mjs +0 -140
  394. package/lib/secrets/broker.test.mjs +0 -280
  395. package/lib/secrets/providers.test.mjs +0 -274
  396. package/lib/security/audit-engine.test.mjs +0 -424
  397. package/lib/security/coerce-args.test.mjs +0 -281
  398. package/lib/security/dangerous-tools.test.mjs +0 -68
  399. package/lib/security/external-content.test.mjs +0 -84
  400. package/lib/security/redact.test.mjs +0 -441
  401. package/lib/security/secret-equal.test.mjs +0 -55
  402. package/lib/session/config.test.mjs +0 -92
  403. package/lib/session/feed-core.test.mjs +0 -198
  404. package/lib/session/first-run.test.mjs +0 -121
  405. package/lib/session/frontdoor.test.mjs +0 -205
  406. package/lib/session/handoffs.test.mjs +0 -183
  407. package/lib/session/identity.test.mjs +0 -180
  408. package/lib/session/inbox-claims.test.mjs +0 -286
  409. package/lib/session/launch-args.test.mjs +0 -157
  410. package/lib/session/liveness.test.mjs +0 -100
  411. package/lib/session/status-summary.test.mjs +0 -118
  412. package/lib/session-permissions.test.mjs +0 -120
  413. package/lib/setup/claude-probe.test.mjs +0 -187
  414. package/lib/setup/completeness.test.mjs +0 -110
  415. package/lib/setup/context-pack.test.mjs +0 -89
  416. package/lib/setup/enrich.test.mjs +0 -115
  417. package/lib/setup/enroll-from-cohort.test.mjs +0 -300
  418. package/lib/setup/integration.test.mjs +0 -162
  419. package/lib/setup/io.test.mjs +0 -77
  420. package/lib/setup/runner.test.mjs +0 -132
  421. package/lib/setup/sections/identity.test.mjs +0 -234
  422. package/lib/setup/sections/inventory.test.mjs +0 -198
  423. package/lib/setup/sections/learning.test.mjs +0 -81
  424. package/lib/setup/sections/mandate.test.mjs +0 -388
  425. package/lib/setup/sections/messaging.test.mjs +0 -127
  426. package/lib/setup/sections/model.test.mjs +0 -240
  427. package/lib/setup/sections/org.test.mjs +0 -346
  428. package/lib/setup/sections/orgmail.test.mjs +0 -118
  429. package/lib/setup/sections/recovery.test.mjs +0 -98
  430. package/lib/setup/sections/subagents.test.mjs +0 -429
  431. package/lib/setup/sections/verify.test.mjs +0 -175
  432. package/lib/setup/sot.test.mjs +0 -81
  433. package/lib/setup/state.test.mjs +0 -115
  434. package/lib/singleton.test.mjs +0 -151
  435. package/lib/subagents/cli.test.mjs +0 -389
  436. package/lib/subagents/client.test.mjs +0 -309
  437. package/lib/subagents/gap.test.mjs +0 -234
  438. package/lib/subagents/lock.test.mjs +0 -248
  439. package/lib/subagents/manifest.test.mjs +0 -175
  440. package/lib/subagents/refs.test.mjs +0 -204
  441. package/lib/subagents/resolve.test.mjs +0 -422
  442. package/lib/subagents/schema.test.mjs +0 -328
  443. package/lib/telemetry/alerts.test.mjs +0 -109
  444. package/lib/telemetry/collect.test.mjs +0 -1274
  445. package/lib/tool-definitions-integration.test.mjs +0 -83
  446. package/lib/tool-definitions.test.mjs +0 -437
  447. package/lib/upgrade/global-refresh.test.mjs +0 -65
  448. package/lib/upgrade/launchd-reconcile.test.mjs +0 -272
  449. package/lib/upgrade/post-steps.test.mjs +0 -200
  450. package/lib/upgrade/verify.test.mjs +0 -164
  451. package/lib/util/fetch-timeout.test.mjs +0 -202
  452. package/lib/util/reconnect.test.mjs +0 -369
  453. package/lib/util/unhandled.test.mjs +0 -216
  454. package/lib/voice/outbound.test.mjs +0 -69
  455. package/lib/voice/session-rotation.test.mjs +0 -114
  456. package/lib/voice/stt.test.mjs +0 -226
  457. package/lib/voice/voice.test.mjs +0 -990
  458. package/scripts/cadence/enqueue-cadence-tick.test.mjs +0 -187
  459. package/scripts/ci/check-docs-accuracy.test.mjs +0 -409
  460. package/scripts/ci/check-durable-write-seam.test.mjs +0 -90
  461. package/scripts/ci/check-no-build-artifacts.test.mjs +0 -71
  462. package/scripts/ci/check-no-residual-identity.test.mjs +0 -202
  463. package/scripts/ci/check-skill-packs.test.mjs +0 -495
  464. package/scripts/ci/check-subagent-frontmatter.test.mjs +0 -124
  465. package/scripts/ci/check.test.mjs +0 -194
  466. package/scripts/ci/conformance-org-api.test.mjs +0 -425
  467. package/scripts/cloud-relay/voice/relay-identity.test.mjs +0 -96
  468. package/scripts/collective/hook-runner.test.mjs +0 -173
  469. package/scripts/cost/fleet-digest.test.mjs +0 -207
  470. package/scripts/cost/track-claude-usage-pricing.test.mjs +0 -183
  471. package/scripts/cost/track-claude-usage.test.mjs +0 -148
  472. package/scripts/daemon/agent-daemon-board-mine.test.mjs +0 -96
  473. package/scripts/daemon/agent-daemon-design.test.mjs +0 -238
  474. package/scripts/daemon/agent-daemon-frontdoor.test.mjs +0 -60
  475. package/scripts/daemon/agent-daemon.test.mjs +0 -995
  476. package/scripts/daemon/assurance-e2e.test.mjs +0 -613
  477. package/scripts/daemon/assurance.test.mjs +0 -1791
  478. package/scripts/daemon/board-mirror.test.mjs +0 -165
  479. package/scripts/daemon/cadence-consumer-frontdoor.test.mjs +0 -393
  480. package/scripts/daemon/cadence-consumer-governance.test.mjs +0 -276
  481. package/scripts/daemon/cadence-consumer.test.mjs +0 -776
  482. package/scripts/daemon/cadence-handlers.test.mjs +0 -837
  483. package/scripts/daemon/classifier-identity.test.mjs +0 -137
  484. package/scripts/daemon/classifier.test.mjs +0 -266
  485. package/scripts/daemon/classify-kind.test.mjs +0 -40
  486. package/scripts/daemon/context-compiler.test.mjs +0 -300
  487. package/scripts/daemon/deliver.test.mjs +0 -564
  488. package/scripts/daemon/dispatcher-cooldown.test.mjs +0 -122
  489. package/scripts/daemon/dispatcher-governance.test.mjs +0 -1013
  490. package/scripts/daemon/dispatcher-resume.test.mjs +0 -166
  491. package/scripts/daemon/execution-ladder.test.mjs +0 -470
  492. package/scripts/daemon/goal-steward-cadence.test.mjs +0 -312
  493. package/scripts/daemon/inbox-deferral-session.test.mjs +0 -49
  494. package/scripts/daemon/inbox-deferral.test.mjs +0 -336
  495. package/scripts/daemon/inbox-wake.test.mjs +0 -199
  496. package/scripts/daemon/integration.test.mjs +0 -149
  497. package/scripts/daemon/lib/self-echo.test.mjs +0 -153
  498. package/scripts/daemon/lib/session-router.test.mjs +0 -295
  499. package/scripts/daemon/prompt-builder-preamble.test.mjs +0 -210
  500. package/scripts/daemon/prompt-builder.test.mjs +0 -344
  501. package/scripts/daemon/responder-cost.test.mjs +0 -68
  502. package/scripts/daemon/responder-history.test.mjs +0 -185
  503. package/scripts/daemon/sdk-version.test.mjs +0 -31
  504. package/scripts/daemon/session-lock.test.mjs +0 -252
  505. package/scripts/daemon/session-outcomes.test.mjs +0 -533
  506. package/scripts/daemon/typing-registry.test.mjs +0 -102
  507. package/scripts/hooks/pre-send-audit.test.mjs +0 -354
  508. package/scripts/huddle/huddle-prompt.test.mjs +0 -176
  509. package/scripts/local-triggers/autoupdate.test.mjs +0 -518
  510. package/scripts/local-triggers/generate-plists.test.mjs +0 -456
  511. package/scripts/media-generation/brand-clause.test.mjs +0 -135
  512. package/scripts/org/send-orgmail.first-contact.test.mjs +0 -102
  513. package/scripts/poller/inbox-privilege-injection.test.mjs +0 -167
  514. package/scripts/poller/inbox-scan-poller.test.mjs +0 -295
  515. package/scripts/poller/lib/cloud-relay-dedup.test.mjs +0 -133
  516. package/scripts/poller/slack-socket-mode.test.mjs +0 -805
  517. package/scripts/poller-launchd/install.test.mjs +0 -243
  518. package/scripts/restore-from-backup.test.mjs +0 -181
  519. package/scripts/session/feed.test.mjs +0 -196
  520. package/scripts/session/supervisor-sh.test.mjs +0 -218
  521. package/scripts/session/supervisor.test.mjs +0 -482
  522. package/scripts/setup/configure-macos.test.mjs +0 -306
  523. package/scripts/setup/gen-subagent-manifest.test.mjs +0 -124
  524. package/scripts/setup/generate-agent-package-json.test.mjs +0 -143
  525. package/scripts/setup/generate-capability.test.mjs +0 -134
  526. package/scripts/setup/init-agent.test.mjs +0 -370
  527. package/scripts/setup/init-skill-marketplace.test.mjs +0 -193
  528. package/scripts/vendor/sync-skill-packs.test.mjs +0 -103
  529. package/scripts/watchdog/memory-watchdog.test.mjs +0 -64
@@ -30,19 +30,101 @@
30
30
  # state/autoupdate/last.json remembers it and the run holds for 24 h
31
31
  # (MAESTRO_AUTOUPDATE_FAILED_HOLD_S; delete last.json to retry now).
32
32
  #
33
- # The health gate is the DAEMON: alive + a fresh "org-mesh connected" /
34
- # "[daemon] Running" line. When the generated plist set carries a -session
35
- # job, the gate ALSO checks the session label is loaded and a supervisor is
36
- # alive — but a missing session is logged as `reconcile-failed` and does NOT
37
- # roll back (the daemon's --print lane is the front door meanwhile; doctor
38
- # and `maestro upgrade --verify` say what to do).
33
+ # THE HEALTH GATE means HEALTH, not "running". It used to be "daemon pid alive
34
+ # + a fresh 'org-mesh connected' / '[daemon] Running' line in today's log" —
35
+ # and a CRASH-LOOPING daemon satisfies both: launchd respawns it, so it is
36
+ # alive at any instant, and every respawn prints the connect line again. One
37
+ # seat logged "up to date (2.17.0)" every hour for SIX DAYS (2026-09-15..21)
38
+ # while its daemon died ~25 s after every start; the org saw no presence beat
39
+ # from it for ten days and nothing on the box said a word. The gate now asks
40
+ # four questions, and the daemon must answer all of them (is_healthy):
41
+ #
42
+ # (a) PID STABILITY — the set of daemon pids is unchanged across two samples
43
+ # STABLE_S apart AND every pid's `ps etime` is ≥ STABLE_S. STABLE_S is 90 s
44
+ # (MAESTRO_AUTOUPDATE_STABLE_S): 3× the observed ~25 s crash interval, so a
45
+ # loop cannot straddle both samples by luck, and well inside the hourly
46
+ # cadence. A 25 s loop fails on the second sample and on uptime.
47
+ # (b) NO FATAL LINE in the daemon's own log since THIS daemon booted: the
48
+ # lines lib/util/unhandled.mjs and agent-daemon.mjs write on the way down
49
+ # ("[DAEMON] uncaughtException:", "[daemon] Fatal:"), the wrapper's
50
+ # "[wrapper] FATAL:", and Node's ERR_MODULE_NOT_FOUND (a bad release's
51
+ # import failure). The log carries no per-line timestamps, the wrapper
52
+ # names the file for the day the process STARTED and APPENDS to it, so
53
+ # one file holds every incarnation that started that day. The scan
54
+ # starts at the later of: the line count captured before a kickstart,
55
+ # and the running daemon's own boot marker — "[DAEMON] boot pid=<pid>"
56
+ # (maestro-daemon.mjs, from the release that added this gate), else the last "[daemon] Running" /
57
+ # "org-mesh connected" line. A crash earlier the same day that launchd
58
+ # (or an operator) already recovered from is history, not health. A
59
+ # missing log file has no fatal lines (fail-open here — (a) and (c)
60
+ # carry the gate).
61
+ # (c) A SERVER-ACKNOWLEDGED BEAT — state/org/last-beat.json {at, ok, code?,
62
+ # sdkVersion?}, which lib/org/mesh.mjs overwrites on EVERY presence beat
63
+ # (ok:true only when the org answered 2xx; a rejected or unreachable beat
64
+ # writes ok:false). The record must say ok:true, be dated after the
65
+ # daemon's start, and be younger than BEAT_FRESH_S (300 s,
66
+ # MAESTRO_AUTOUPDATE_BEAT_FRESH_S — the same 5 min after which hq's
67
+ # presence view calls a seat offline). When the record carries
68
+ # `sdkVersion` — the version that beat TOLD the org, which hq persists to
69
+ # AgentStatus.machine.sdkVersion — it must equal the installed version:
70
+ # an accepted beat that reported the previous version proves the old
71
+ # process is alive, not that the org sees the new one. A record without
72
+ # the field (a mesh older than this check, or a beat whose status
73
+ # collector failed) is liveness proof only and is accepted as before.
74
+ # Skipped only when config/org.yaml does not enroll this seat (an
75
+ # un-enrolled daemon never beats).
76
+ # (d) The UP-TO-DATE path is not exempt. When the installed version is already
77
+ # @latest the run still runs (a)-(c); an unhealthy daemon is logged
78
+ # loudly, written to state/autoupdate/last.json {healthy:false, reason:
79
+ # "unhealthy-current: …"} (the beat carries it as machine.upgrade), and —
80
+ # once per unhealthy episode, on the transition — announced through
81
+ # state/session/upgrade-notice.json {from:CUR,to:CUR,healthy:false,reason}
82
+ # so the front-door session's feed surfaces it as a `daemon-unhealthy`
83
+ # directive (never `upgrade-available` — nothing to restart onto).
84
+ # Nothing is rolled back: there is no upgrade to undo. A recovery
85
+ # restores last.json.
86
+ # (f) AHEAD OF THE REGISTRY is reported, not silently read as current. When
87
+ # the installed version is strictly GREATER than npm's latest, this build
88
+ # was never published: it exists on this box and nowhere else, and every
89
+ # other seat — which follows @latest — will never receive it. The
90
+ # up-to-date branch used to swallow that case into "up to date (<CUR>)".
91
+ # That is exactly how THIS gate came to be running on zero machines: the
92
+ # seat that wrote it installed 2.18.0 locally, npm's latest stayed at
93
+ # 2.17.0, and fifteen boxes logged a reassuring hourly line while the
94
+ # crash-loop gate reached none of them. Now it is logged loudly and
95
+ # written to state/autoupdate/last.json {healthy:true, reason:
96
+ # "ahead-of-registry: npm latest is <LATEST>"} — healthy because the
97
+ # DAEMON is, reason because the FLEET is not — which the beat carries to
98
+ # the org as machine.upgrade. It never blocks, never rolls back and never
99
+ # writes a session notice: there is no upgrade to restart onto, and the
100
+ # fix (publish, or roll this seat back) is a human's.
101
+ # (e) A STALE DAEMON is reconciled on the up-to-date path. A hand-run
102
+ # `maestro upgrade` installs the package and reconciles the seat but
103
+ # never restarts the daemon (post-steps.mjs: "restarted by autoupdate,
104
+ # never here"; its verify row promises the new version "takes effect
105
+ # when autoupdate kickstarts it"). When state/dashboards/daemon-health.yaml
106
+ # says the LIVE daemon (its pid is one of ours) started on a different
107
+ # sdk_version than the installed one, this run kickstarts it, waits, and
108
+ # runs the gate exactly as after an automatic install — healthy →
109
+ # upgrade-notice {from:<old>,to:CUR} + last.json, as for any completed
110
+ # hop. Without this a manual upgrade ran old daemon code until the next
111
+ # release, and the gate misread its missing beat record as unhealthy.
112
+ #
113
+ # When the generated plist set carries a -session job, the gate ALSO checks the
114
+ # session label is loaded and a supervisor is alive — but a missing session is
115
+ # logged as `reconcile-failed` and does NOT roll back (the daemon's --print lane
116
+ # is the front door meanwhile; doctor and `maestro upgrade --verify` say what
117
+ # to do).
39
118
  #
40
119
  # The front-door session (ai.maestro.<first>-session) is NEVER restarted or
41
120
  # killed here: after a HEALTHY upgrade this writes
42
- # state/session/upgrade-notice.json {from,to,at}; the session's feed surfaces
43
- # it as an `upgrade-available` directive and the session restarts itself at an
44
- # idle moment via `maestro session restart` (the supervisor relaunches on the
45
- # new code). A rollback leaves no notice. Every attempt also writes
121
+ # state/session/upgrade-notice.json {from,to,at}; the session's feed
122
+ # (lib/session/feed-core.mjs) surfaces it as an `upgrade-available` directive
123
+ # and the session restarts itself at an idle moment via `maestro session
124
+ # restart` (the supervisor relaunches on the new code). The same file with
125
+ # {healthy:false, reason} is a REPORT — the feed emits `daemon-unhealthy`
126
+ # for it, a distinct action, so no session restarts on a health report. A
127
+ # rollback leaves no notice. Every attempt also writes
46
128
  # state/autoupdate/last.json {from,to,at,ok,healthy,reason}, which the beat
47
129
  # reports as machine.upgrade. See
48
130
  # docs/superpowers/specs/2026-09-08-front-door-session-design.md §3.10 + WP-M6.
@@ -53,14 +135,18 @@
53
135
  #
54
136
  # Test knobs (scripts/local-triggers/autoupdate.test.mjs): MAESTRO_AUTOUPDATE_JITTER_MAX
55
137
  # (default 600) and MAESTRO_AUTOUPDATE_HEALTH_WAIT (default 30), both seconds;
56
- # MAESTRO_AUTOUPDATE_RETRY_SLEEP (default 20 s, ×attempt); MAESTRO_AUTOUPDATE_LOCK_STALE_S
57
- # (default 7200); MAESTRO_AUTOUPDATE_FAILED_HOLD_S (default 86400);
58
- # MAESTRO_AUTOUPDATE_PATH_PREFIX (a stub-binary dir that wins over PATH);
138
+ # MAESTRO_AUTOUPDATE_STABLE_S (default 90; MAESTRO_AUTOUPDATE_STABLE_GAP_S is the
139
+ # sleep between the two pid samples, default = STABLE_S, zeroed by the test) and
140
+ # MAESTRO_AUTOUPDATE_BEAT_FRESH_S (default 300); MAESTRO_AUTOUPDATE_RETRY_SLEEP
141
+ # (default 20 s, ×attempt);
142
+ # MAESTRO_AUTOUPDATE_LOCK_STALE_S (default 7200); MAESTRO_AUTOUPDATE_FAILED_HOLD_S
143
+ # (default 86400); MAESTRO_AUTOUPDATE_PATH_PREFIX (a stub-binary dir that wins
144
+ # over PATH — the test stubs npm, launchctl, pgrep AND ps there);
59
145
  # MAESTRO_SKIP_GLOBAL_INSTALL=1 (never touch the global install).
60
146
  # =============================================================================
61
147
  set -uo pipefail
62
148
  # MAESTRO_AUTOUPDATE_PATH_PREFIX: a dir that wins over the standard prepend
63
- # (the test puts its npm/launchctl/pgrep stubs there so a run never reaches
149
+ # (the test puts its npm/launchctl/pgrep/ps stubs there so a run never reaches
64
150
  # the real registry or launchd).
65
151
  export PATH="${MAESTRO_AUTOUPDATE_PATH_PREFIX:+$MAESTRO_AUTOUPDATE_PATH_PREFIX:}/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:${PATH:-}"
66
152
 
@@ -68,6 +154,11 @@ export PATH="${MAESTRO_AUTOUPDATE_PATH_PREFIX:+$MAESTRO_AUTOUPDATE_PATH_PREFIX:}
68
154
  # The copy removes itself on exit (REEXEC_COPY is only ever a mktemp'd
69
155
  # maestro-autoupdate.* file — never the agent dir's script).
70
156
  if [ "${MAESTRO_AUTOUPDATE_REEXEC:-}" != "1" ]; then
157
+ # Pin the agent dir BEFORE re-exec'ing: the copy lives in $TMPDIR, so its own
158
+ # location says nothing about the agent. launchd's plist sets AGENT_ROOT; a
159
+ # hand-run `bash scripts/local-triggers/autoupdate.sh` did not, resolved
160
+ # AGENT_DIR to $TMPDIR/../.., and exited 0 having done nothing.
161
+ export AGENT_ROOT="${AGENT_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
71
162
  _tmp="$(mktemp "${TMPDIR:-/tmp}/maestro-autoupdate.XXXXXX")" || exit 0
72
163
  cp "${BASH_SOURCE[0]}" "$_tmp" 2>/dev/null || exit 0
73
164
  MAESTRO_AUTOUPDATE_REEXEC=1 exec /bin/bash "$_tmp" "$@"
@@ -144,46 +235,150 @@ npm_view_latest(){ # prints the latest version, or nothing after RETRIES attempt
144
235
  done
145
236
  }
146
237
 
147
- # ── version check ────────────────────────────────────────────────────────────
148
- CUR="$(node -p "require('$AGENT_DIR/node_modules/$PKG/package.json').version" 2>/dev/null || echo 0.0.0)"
149
- LATEST="$(npm_view_latest)" || LATEST=""
150
- [ -z "$LATEST" ] && { log "npm view failed after $RETRIES attempts; skip (still $CUR)"; exit 0; }
151
- if [ "$CUR" = "$LATEST" ] || [ "$(printf '%s\n%s\n' "$CUR" "$LATEST" | sort -V | tail -1)" != "$LATEST" ]; then
152
- log "up to date ($CUR)"; exit 0
153
- fi
154
-
155
- # ── failed-target hold ───────────────────────────────────────────────────────
156
- # last.json is the memory of the health gate: a LATEST that was installed here
157
- # and rolled back (or could not even be rolled back) is not retried every hour
158
- # — that would be two installs, two global refreshes and two daemon restarts an
159
- # hour until someone touched the kill-switch. An install-failed (network) entry
160
- # does not hold. Fail-open: an unreadable last.json holds nothing.
161
- FAILED_HOLD_S="${MAESTRO_AUTOUPDATE_FAILED_HOLD_S:-86400}"
162
- failed_hold_reason(){ # prints "<reason> at <at>" when LATEST failed health within FAILED_HOLD_S
163
- node -e '
164
- const [file, latest, holdS] = process.argv.slice(1);
165
- try {
166
- const j = JSON.parse(require("fs").readFileSync(file, "utf8"));
167
- const at = Date.parse(j.at);
168
- if (j.to === latest && j.ok === false && /unhealthy/.test(String(j.reason)) && Number.isFinite(at) && Date.now() - at < Number(holdS) * 1000) process.stdout.write(`${j.reason} at ${j.at}`);
169
- } catch {}
170
- ' "$AGENT_DIR/state/autoupdate/last.json" "$LATEST" "$FAILED_HOLD_S" 2>/dev/null
171
- }
172
- HOLD="$(failed_hold_reason)"
173
- if [ -n "$HOLD" ]; then
174
- log "SKIP: $LATEST already failed the health gate here ($HOLD); holding ${FAILED_HOLD_S}s (still $CUR) — rm state/autoupdate/last.json to retry now"
175
- exit 0
176
- fi
177
- log "UPDATE $CUR -> $LATEST"
178
-
238
+ # ── this agent, its daemon, and the health gate ──────────────────────────────
239
+ # Defined BEFORE the version check: the up-to-date path runs the gate too (d).
179
240
  UID_="$(id -u)"
180
241
  AGENT_FIRST="$(node -p "String(require('$AGENT_DIR/config/agent.json').firstName || '').toLowerCase()" 2>/dev/null)"
181
242
  [ -n "$AGENT_FIRST" ] && log "agent: $AGENT_FIRST (launchd/pgrep questions are scoped to ai.maestro.$AGENT_FIRST-*)"
182
- DLOG="$AGENT_DIR/logs/daemon/daemon-$(date +%Y-%m-%d).log"
243
+ DAEMON_PATTERN="$AGENT_DIR/scripts/daemon/maestro-daemon.mjs"
244
+ # The daemon's argv is exactly `<node> <AGENT_DIR>/scripts/daemon/maestro-daemon.mjs`
245
+ # (launchd-wrapper.sh execs it that way), so the pgrep pattern is anchored to
246
+ # that whole argument list. `pgrep -f <path>` alone matches ANY process whose
247
+ # argv merely contains the path — an editor, a grep, a `node --inspect` by an
248
+ # operator — and one of those appearing between the two pid samples reads as
249
+ # a pid-set change, i.e. "crash loop".
250
+ DAEMON_ARGV_RE="^[^ ]*/node $(printf '%s' "$DAEMON_PATTERN" | sed 's/[][\.*^$]/\\&/g')\$"
251
+ DLOG_DIR="$AGENT_DIR/logs/daemon"
183
252
  HEALTH_WAIT="${MAESTRO_AUTOUPDATE_HEALTH_WAIT:-30}"
253
+ STABLE_S="${MAESTRO_AUTOUPDATE_STABLE_S:-90}"
254
+ STABLE_GAP_S="${MAESTRO_AUTOUPDATE_STABLE_GAP_S:-$STABLE_S}" # the gap between the two pid samples; the test zeroes it (its stub changes per CALL)
255
+ BEAT_FRESH_S="${MAESTRO_AUTOUPDATE_BEAT_FRESH_S:-300}"
256
+ LAST_BEAT="$AGENT_DIR/state/org/last-beat.json"
257
+ MESH_FILE="$AGENT_DIR/lib/org/mesh.mjs"
258
+ mesh_records_beats(){ grep -q "LAST_BEAT_FILE" "$MESH_FILE" 2>/dev/null; } # the installed mesh writes state/org/last-beat.json (the release that added this gate)
259
+ # The lines the daemon writes on its way down. Tight on purpose: the daemon log
260
+ # is free text (LLM output included), so every pattern here is a prefix the
261
+ # code emits — lib/util/unhandled.mjs via maestro-daemon.mjs's onEvent, the
262
+ # main() catch in agent-daemon.mjs, launchd-wrapper.sh, and Node's own code
263
+ # for an import that cannot resolve (how a bad release most often dies).
264
+ FATAL_RE='\[DAEMON\] uncaughtException:|\[daemon\] Fatal:|\[wrapper\] FATAL:|ERR_MODULE_NOT_FOUND'
265
+ HEALTH_REASON="" # set by is_healthy on failure; a phrase, no quotes
184
266
  DAEMON_LABEL=""
185
267
  SESSION_LABEL=""
186
268
 
269
+ daemon_pids(){ # every pid whose argv IS this agent's daemon entry (see DAEMON_ARGV_RE), sorted, space-joined ("" when none)
270
+ pgrep -f "$DAEMON_ARGV_RE" 2>/dev/null | sort -n | tr '\n' ' ' | sed 's/ *$//'
271
+ }
272
+ etime_seconds(){ # $1 = ps etime ([[dd-]hh:]mm:ss) → seconds; "" when unparseable
273
+ local s="$1" d=0 h=0 m=0 sec=0 a b c
274
+ [ -n "$s" ] || return 0
275
+ case "$s" in *-*) d="${s%%-*}"; s="${s#*-}" ;; esac
276
+ IFS=: read -r a b c <<< "$s"
277
+ if [ -n "${c:-}" ]; then h="$a"; m="$b"; sec="$c"; else m="$a"; sec="${b:-0}"; fi
278
+ case "$d$h$m$sec" in *[!0-9]*) return 0 ;; esac
279
+ echo $(( 10#$d * 86400 + 10#$h * 3600 + 10#$m * 60 + 10#$sec ))
280
+ }
281
+ pid_uptime_s(){ # $1 = pid → seconds since it started, "" when ps cannot say
282
+ etime_seconds "$(ps -o etime= -p "$1" 2>/dev/null | tr -d ' ')"
283
+ }
284
+ day_of_epoch(){ # $1 = epoch seconds → YYYY-MM-DD in local time (the wrapper's `date +%Y-%m-%d` basis)
285
+ date -r "$1" +%Y-%m-%d 2>/dev/null || date -d "@$1" +%Y-%m-%d 2>/dev/null
286
+ }
287
+ daemon_log_for(){ # $1 = uptime seconds → the log file the wrapper opened for THIS incarnation (named for its start day)
288
+ echo "$DLOG_DIR/daemon-$(day_of_epoch $(( $(date +%s) - $1 ))).log"
289
+ }
290
+ first_fatal_after(){ # $1 = log file, $2 = line offset (lines up to it predate this daemon) → the first fatal line, or nothing
291
+ [ -f "$1" ] || return 0
292
+ tail -n +"$(( $2 + 1 ))" "$1" 2>/dev/null | grep -m1 -E "$FATAL_RE"
293
+ return 0
294
+ }
295
+ # The daemon log is APPENDED to (launchd-wrapper.sh `>>`) and shared by every
296
+ # incarnation that starts on the same day, so "since this daemon" needs a
297
+ # marker. maestro-daemon.mjs prints `[DAEMON] boot pid=<pid> at=<iso>` as its
298
+ # first line (since the release that added this gate); for a daemon older than that the LAST generic boot line
299
+ # — "[daemon] Running" / "[DAEMON] org-mesh connected", which only a process
300
+ # that got through boot prints — is the best available split. Without either,
301
+ # a one-off crash at 09:00 and the healthy respawn at 09:01 share a file and
302
+ # the crash read as "fatal since restart" every hour until midnight.
303
+ BOOT_RE='\[daemon\] Running|\[DAEMON\] org-mesh connected'
304
+ boot_offset(){ # $1 = log file, $2 = pid → the line count that precedes THIS incarnation's boot marker (0 when there is none)
305
+ local n
306
+ [ -f "$1" ] || { echo 0; return 0; }
307
+ n="$(grep -n -F "[DAEMON] boot pid=$2 " "$1" 2>/dev/null | tail -1 | cut -d: -f1)"
308
+ [ -n "$n" ] || n="$(grep -n -E "$BOOT_RE" "$1" 2>/dev/null | tail -1 | cut -d: -f1)"
309
+ echo $(( ${n:-1} - 1 ))
310
+ }
311
+ org_enrolled(){ # the SAME answer the daemon gives: lib/org/client.mjs loadOrgConfig + isEnabled — org.cohort.enabled AND a base (or apiUrl+orgId). A seat that fails that test never beats, so the gate does not ask it to.
312
+ node --input-type=module -e '
313
+ const [root] = process.argv.slice(1);
314
+ const { loadOrgConfig, isEnabled } = await import(root + "/lib/org/client.mjs");
315
+ process.exit(isEnabled(loadOrgConfig(root)) ? 0 : 3);
316
+ ' "$AGENT_DIR" 2>/dev/null
317
+ case $? in 0) return 0 ;; 3) return 1 ;; esac
318
+ log "WARN: lib/org/client.mjs could not answer whether this seat is enrolled — treating it as not enrolled (the daemon would too)"
319
+ return 1
320
+ }
321
+ beat_not_acknowledged(){ # $1 = epoch seconds the daemon started, $2 = installed version → prints WHY the org has not acknowledged this daemon ON THIS VERSION, or nothing when it has
322
+ node -e '
323
+ const [file, sinceS, freshS, meshRecords, installed] = process.argv.slice(1);
324
+ const out = (s) => process.stdout.write(s);
325
+ let j;
326
+ try { j = JSON.parse(require("fs").readFileSync(file, "utf8")); }
327
+ catch (e) { out(`no beat recorded (${e && e.code === "ENOENT" ? (meshRecords === "1" ? "state/org/last-beat.json absent — this daemon process has never completed a beat on this code" : "state/org/last-beat.json absent, and the installed lib/org/mesh.mjs does not record beats at all — a preserved local copy older than this gate? maestro upgrade --verify") : "state/org/last-beat.json unreadable"})`); process.exit(0); }
328
+ const at = Date.parse(j && j.at);
329
+ if (!Number.isFinite(at)) { out("last-beat.json carries no parseable at"); process.exit(0); }
330
+ const ageS = Math.round((Date.now() - at) / 1000);
331
+ if (j.ok !== true) out(`last beat NOT accepted by the org (${j.code || "?"}${j.message ? ": " + String(j.message).replace(/["\\\\]/g, "") : ""}) at ${j.at}`);
332
+ else if (at < Number(sinceS) * 1000) out(`last accepted beat (${j.at}) predates this daemon process`);
333
+ else if (ageS > Number(freshS)) out(`last accepted beat is ${ageS}s old (> ${freshS}s; the org already calls this seat offline)`);
334
+ else if (installed && typeof j.sdkVersion === "string" && j.sdkVersion && j.sdkVersion !== installed) out(`last accepted beat (${j.at}) told the org sdkVersion ${j.sdkVersion}, but ${installed} is installed — the org sees the old version`);
335
+ ' "$LAST_BEAT" "$1" "$BEAT_FRESH_S" "$(mesh_records_beats && echo 1 || echo 0)" "${2:-}" 2>/dev/null
336
+ }
337
+ # The daemon log offset "since the restart" — captured right before a kickstart
338
+ # (see restart_daemon). is_healthy takes the LATER of this and the boot marker
339
+ # (boot_offset), so with no restart the scan still starts at the running
340
+ # daemon's own boot line, never at the top of a shared start-day file.
341
+ PRE_LOG=""; PRE_LOG_LINES=0
342
+ installed_version(){ # the version in node_modules RIGHT NOW — re-read per call, because is_healthy runs after an install and again after a rollback
343
+ node -p "require('$AGENT_DIR/node_modules/$PKG/package.json').version" 2>/dev/null || true
344
+ }
345
+ sibling_job_audit(){ # log-only: every OTHER ai.maestro.<first>-* job whose LAST exit was non-zero. Never a gate reason — a broken relay poller is not a reason to roll the SDK back — but the daemon log is not where those jobs fail, and nothing else reads `launchctl list` for them.
346
+ [ -n "${AGENT_FIRST:-}" ] || return 0
347
+ local bad
348
+ # The -session job's 75 is EX_TEMPFAIL: the supervisor's DESIGNED "the session ended, relaunch me" exit (scripts/session/supervisor.mjs), the signature of every clean relaunch — not a failure.
349
+ bad="$(launchctl list 2>/dev/null | awk -v p="ai.maestro.${AGENT_FIRST}-" -v sess="ai.maestro.${AGENT_FIRST}-session" 'NR>1 && index($3, p)==1 && $2 != "0" && $2 != "-" && !($3 == sess && $2 == "75") { printf "%s%s(%s)", (n++ ? ", " : ""), $3, $2 }')"
350
+ [ -n "$bad" ] && log "sibling jobs with a non-zero last exit (not a gate reason; see logs/launchd and logs/polling): $bad"
351
+ return 0
352
+ }
353
+ is_healthy(){ # (a)+(b)+(c) above. Sets HEALTH_REASON on failure.
354
+ local p1 p2 pid up oldest oldest_pid dlog fatal why offset boot
355
+ HEALTH_REASON=""
356
+ p1="$(daemon_pids)"
357
+ [ -n "$p1" ] || { HEALTH_REASON="no daemon process for $DAEMON_PATTERN"; return 1; }
358
+ [ "$STABLE_GAP_S" -gt 0 ] && sleep "$STABLE_GAP_S"
359
+ p2="$(daemon_pids)"
360
+ [ "$p1" = "$p2" ] || { HEALTH_REASON="daemon pid changed within ${STABLE_GAP_S}s (${p1} -> ${p2:-none}) — crash loop"; return 1; }
361
+ oldest=""
362
+ for pid in $p1; do
363
+ up="$(pid_uptime_s "$pid")"
364
+ [ -n "$up" ] && [ "$up" -ge "$STABLE_S" ] || { HEALTH_REASON="daemon pid $pid uptime ${up:-?}s < ${STABLE_S}s — crash loop"; return 1; }
365
+ [ -n "$oldest" ] && [ "$oldest" -ge "$up" ] || { oldest="$up"; oldest_pid="$pid"; }
366
+ done
367
+ dlog="$(daemon_log_for "$oldest")"
368
+ offset=0; [ "$dlog" = "$PRE_LOG" ] && offset="$PRE_LOG_LINES"
369
+ boot="$(boot_offset "$dlog" "$oldest_pid")"
370
+ [ "$boot" -gt "$offset" ] && offset="$boot"
371
+ fatal="$(first_fatal_after "$dlog" "$offset")"
372
+ [ -z "$fatal" ] || { HEALTH_REASON="fatal in $(basename "$dlog") since this daemon booted (scanned from line $(( offset + 1 ))): $(printf '%s' "$fatal" | tr -d '"\\' | cut -c1-160)"; return 1; }
373
+ if org_enrolled; then
374
+ why="$(beat_not_acknowledged $(( $(date +%s) - oldest - 2 )) "$(installed_version)")"
375
+ [ -z "$why" ] || { HEALTH_REASON="$why"; return 1; }
376
+ else
377
+ log "org not enrolled in config/org.yaml — beat acknowledgement not required"
378
+ fi
379
+ return 0
380
+ }
381
+
187
382
  first_plist(){ # $1 = suffix, $2 = dir — this agent's plist for that suffix, or nothing (unquoted so `*` globs when no name is known)
188
383
  ls "$2"/ai.maestro.${AGENT_FIRST:-*}-"$1".plist 2>/dev/null | head -1
189
384
  }
@@ -208,9 +403,212 @@ label_loaded(){ # $1 = label — measured in the gui domain (right from ssh too)
208
403
  }
209
404
  restart_daemon(){
210
405
  resolve_labels
406
+ # Mark "since the restart" for the fatal scan: the new process appends to
407
+ # today's file (the wrapper names it for its start day), after these lines.
408
+ PRE_LOG="$DLOG_DIR/daemon-$(date +%Y-%m-%d).log"
409
+ PRE_LOG_LINES=0
410
+ [ -f "$PRE_LOG" ] && PRE_LOG_LINES="$(wc -l < "$PRE_LOG" 2>/dev/null | tr -d ' ')"
411
+ PRE_LOG_LINES="${PRE_LOG_LINES:-0}"
211
412
  if [ -z "$DAEMON_LABEL" ]; then log "no -daemon plist under ~/Library/LaunchAgents — nothing to kickstart (maestro upgrade installs it; doctor will say)"; return 0; fi
212
413
  launchctl kickstart -k "gui/$UID_/$DAEMON_LABEL" >> "$LOG" 2>&1 || log "WARN: kickstart $DAEMON_LABEL nonzero"
213
414
  }
415
+ session_reconciled(){ # only asked when a -session plist was generated; never a rollback reason
416
+ label_loaded "$SESSION_LABEL" || { log "reconcile-failed: session job $SESSION_LABEL is not loaded (maestro session start)"; return 1; }
417
+ pgrep -f "$AGENT_DIR/scripts/session/supervisor" >/dev/null 2>&1 || { log "reconcile-failed: $SESSION_LABEL is loaded but no supervisor process is alive for $AGENT_DIR (maestro session status)"; return 1; }
418
+ return 0
419
+ }
420
+ json_str(){ printf '%s' "$1" | tr -d '"\\' | tr '\n' ' '; } # a value safe inside "…" in the JSON files below
421
+ write_notice(){ # $1 = from, $2 = to, [$3 = reason → healthy:false] — tell the front-door session; never restart it here
422
+ local dir="$AGENT_DIR/state/session" tmp
423
+ mkdir -p "$dir" 2>/dev/null || return 0
424
+ tmp="$(mktemp "$dir/.upgrade-notice.XXXXXX" 2>/dev/null)" || return 0
425
+ if [ -n "${3:-}" ]; then
426
+ printf '{\n "from": "%s",\n "to": "%s",\n "at": "%s",\n "healthy": false,\n "reason": "%s"\n}\n' "$1" "$2" "$(date -u +%FT%TZ)" "$(json_str "$3")" > "$tmp" \
427
+ && mv -f "$tmp" "$dir/upgrade-notice.json" \
428
+ && log "session notified: upgrade-notice.json healthy:false on $2 (no restart — the session surfaces it)"
429
+ else
430
+ printf '{\n "from": "%s",\n "to": "%s",\n "at": "%s"\n}\n' "$1" "$2" "$(date -u +%FT%TZ)" > "$tmp" \
431
+ && mv -f "$tmp" "$dir/upgrade-notice.json" \
432
+ && log "session notified: upgrade-notice.json $1 -> $2 (session restarts itself when idle)"
433
+ fi
434
+ rm -f "$tmp" 2>/dev/null
435
+ return 0
436
+ }
437
+ write_upgrade_notice(){ write_notice "$1" "$2"; }
438
+ LAST_JSON="$AGENT_DIR/state/autoupdate/last.json"
439
+ write_last(){ # $1 from, $2 to, $3 ok, $4 healthy, $5 reason — state/autoupdate/last.json (→ beat machine.upgrade)
440
+ local dir="$AGENT_DIR/state/autoupdate" tmp
441
+ mkdir -p "$dir" 2>/dev/null || return 0
442
+ tmp="$(mktemp "$dir/.last.XXXXXX" 2>/dev/null)" || return 0
443
+ printf '{\n "from": "%s",\n "to": "%s",\n "at": "%s",\n "ok": %s,\n "healthy": %s,\n "reason": "%s"\n}\n' \
444
+ "$1" "$2" "$(date -u +%FT%TZ)" "$3" "$4" "$(json_str "$5")" > "$tmp" && mv -f "$tmp" "$LAST_JSON"
445
+ rm -f "$tmp" 2>/dev/null
446
+ return 0
447
+ }
448
+ last_reason(){ # the reason field of last.json, or nothing
449
+ node -e 'try { process.stdout.write(String(JSON.parse(require("fs").readFileSync(process.argv[1], "utf8")).reason || "")); } catch {}' "$LAST_JSON" 2>/dev/null
450
+ }
451
+ # The version the RUNNING daemon started on. scripts/daemon/health.mjs resolves
452
+ # sdk_version once at load and writes it to state/dashboards/daemon-health.yaml
453
+ # with its pid, precisely so a reader can tell the installed package from the
454
+ # code a live process is executing. Printed only when the dashboard's pid is
455
+ # one of the sampled daemon pids — a dashboard left by a dead process says
456
+ # nothing about this one.
457
+ DAEMON_HEALTH="$AGENT_DIR/state/dashboards/daemon-health.yaml"
458
+ running_daemon_version(){ # → the sdk_version of the LIVE daemon (dashboard pid ∈ daemon_pids), or nothing
459
+ local v pid
460
+ [ -f "$DAEMON_HEALTH" ] || return 0
461
+ v="$(sed -nE 's/^sdk_version:[[:space:]]*([^[:space:]]+).*$/\1/p' "$DAEMON_HEALTH" 2>/dev/null | head -1)"
462
+ pid="$(sed -nE 's/^pid:[[:space:]]*([0-9]+).*$/\1/p' "$DAEMON_HEALTH" 2>/dev/null | head -1)"
463
+ [ -n "$v" ] && [ -n "$pid" ] || return 0
464
+ case " $(daemon_pids) " in *" $pid "*) printf '%s' "$v" ;; esac
465
+ }
466
+ daemon_start_epoch(){ # of the longest-running daemon pid, "" when none
467
+ local p up best=""
468
+ for p in $(daemon_pids); do up="$(pid_uptime_s "$p")"; [ -n "$up" ] || continue; [ -n "$best" ] && [ "$best" -ge "$up" ] || best="$up"; done
469
+ [ -n "$best" ] && echo $(( $(date +%s) - best ))
470
+ return 0
471
+ }
472
+ daemon_predates_mesh(){ # no beat record, and the live daemon started BEFORE the installed lib/org/mesh.mjs was written → it cannot be running that code
473
+ local start mt
474
+ [ -f "$LAST_BEAT" ] && return 1
475
+ mesh_records_beats || return 1
476
+ start="$(daemon_start_epoch)"; [ -n "$start" ] || return 1
477
+ mt="$(stat -f %m "$MESH_FILE" 2>/dev/null || stat -c %Y "$MESH_FILE" 2>/dev/null)"; [ -n "$mt" ] || return 1
478
+ [ "$start" -lt "$mt" ]
479
+ }
480
+
481
+ # ── version check ────────────────────────────────────────────────────────────
482
+ CUR="$(node -p "require('$AGENT_DIR/node_modules/$PKG/package.json').version" 2>/dev/null || echo 0.0.0)"
483
+ LATEST="$(npm_view_latest)" || LATEST=""
484
+ [ -z "$LATEST" ] && { log "npm view failed after $RETRIES attempts; skip (still $CUR)"; exit 0; }
485
+ # (f) AHEAD OF THE REGISTRY is not "up to date", and the difference is the whole
486
+ # fleet. The branch below treats `CUR >= LATEST` as one outcome, so a seat
487
+ # carrying a build that was never published logs the same reassuring
488
+ # "up to date (<CUR>)" as a seat that is genuinely current — which is how the
489
+ # health gate written for the six-day crash loop came to be running on ZERO
490
+ # machines: the authoring seat installed 2.18.0 from its own tree, npm's latest
491
+ # stopped at 2.17.0, and every hourly run on every box said "up to date". A
492
+ # gate that lives only on the machine that wrote it is live nowhere, and
493
+ # nothing said so. Say it, in the log AND in last.json — which the beat carries
494
+ # to the org as machine.upgrade, so the fleet holds the fact rather than the
495
+ # one box that already has the code.
496
+ AHEAD=""
497
+ if [ "$CUR" != "$LATEST" ] && [ "$(printf '%s\n%s\n' "$CUR" "$LATEST" | sort -V | tail -1)" = "$CUR" ]; then
498
+ AHEAD=1
499
+ log "AHEAD OF REGISTRY: installed $CUR, npm latest is $LATEST — this build was never published. Every OTHER seat follows @latest and is pinned at $LATEST, so nothing in $CUR has reached the fleet. Publish it (npm publish) or roll this seat back; do not read the line below as the fleet being current."
500
+ fi
501
+ if [ "$CUR" = "$LATEST" ] || [ "$(printf '%s\n%s\n' "$CUR" "$LATEST" | sort -V | tail -1)" != "$LATEST" ]; then
502
+ # (d) up to date is not the same as well. Run the gate on the CURRENT
503
+ # version; there is nothing to roll back, so the outcome is evidence —
504
+ # loud in this log, in last.json (→ the beat's machine.upgrade), and in the
505
+ # session's feed on the transition into an unhealthy episode.
506
+ log "up to date ($CUR) — checking daemon health: pid stability ${STABLE_S}s, fatal scan, org-acknowledged beat (< ${BEAT_FRESH_S}s)"
507
+ PREV_REASON="$(last_reason)"
508
+ # (e) A daemon still running an OLDER version than the one installed. That
509
+ # is what a hand-run `maestro upgrade` leaves behind: its post-steps never
510
+ # restart the daemon (lib/upgrade/post-steps.mjs — "the daemon is restarted
511
+ # by autoupdate, never here") and its verify row says the new version "takes
512
+ # effect when autoupdate kickstarts it". Until this branch existed that was
513
+ # a promise nobody kept: the installed version was already @latest, so this
514
+ # path never kickstarted anything, the daemon ran the old code until the
515
+ # NEXT release, and — because an earlier mesh.mjs never wrote
516
+ # state/org/last-beat.json — the gate below read the missing record as
517
+ # "no beat recorded" and declared a stable, beating daemon unhealthy. The
518
+ # kickstart is the upgrade's missing last step; the gate then runs as after
519
+ # any restart, and a healthy outcome completes the hop the same way an
520
+ # automatic one does (notice → the session restarts itself when idle).
521
+ # Two signals, either is enough: the dashboard's sdk_version differs from
522
+ # the installed one (a version hop), or — same version — there is no beat
523
+ # record and the daemon started before the installed lib/org/mesh.mjs was
524
+ # written (a file change without a version bump: it cannot be running that
525
+ # code, and that code is the only thing that writes the record).
526
+ RUNNING_V="$(running_daemon_version)"
527
+ STALE_FROM=""; STALE_HOP=""
528
+ if [ -n "$RUNNING_V" ] && [ "$RUNNING_V" != "$CUR" ]; then
529
+ STALE_FROM="$RUNNING_V"; STALE_HOP=1
530
+ log "stale daemon: running $RUNNING_V, installed $CUR — a hand-run maestro upgrade left the daemon restart to this job; kickstarting"
531
+ elif daemon_predates_mesh; then
532
+ STALE_FROM="${RUNNING_V:-$CUR}"
533
+ log "stale daemon: started $(daemon_start_epoch | xargs -I{} date -u -r {} +%FT%TZ 2>/dev/null) — before the installed lib/org/mesh.mjs was written — so it runs code that never records a beat; kickstarting onto the installed code"
534
+ fi
535
+ if [ -n "$STALE_FROM" ]; then
536
+ restart_daemon
537
+ [ "$HEALTH_WAIT" -gt 0 ] && sleep "$HEALTH_WAIT"
538
+ fi
539
+ if is_healthy; then
540
+ log "healthy on $CUR"
541
+ sibling_job_audit
542
+ if [ -n "$STALE_HOP" ]; then
543
+ # A real version hop: complete it the way an automatic one completes —
544
+ # the notice tells the session to restart itself onto the new code.
545
+ log "OK: daemon now on $CUR (was $STALE_FROM) — the manual upgrade is complete"
546
+ write_upgrade_notice "$STALE_FROM" "$CUR"
547
+ write_last "$STALE_FROM" "$CUR" true true ""
548
+ elif [ -n "$STALE_FROM" ]; then
549
+ # Same version, newer files: the daemon is reconciled; the session is
550
+ # not told to restart (a touched or restored file must not cost it its
551
+ # context — the version did not change).
552
+ log "OK: daemon restarted onto the installed $CUR code; no session notice (same version)"
553
+ write_last "$CUR" "$CUR" true true ""
554
+ elif [ -n "$AHEAD" ]; then
555
+ # The DAEMON is well; the FLEET is not. Recorded as ok/healthy — because
556
+ # it is — with the finding in `reason`, so the org learns that this seat
557
+ # executes code no other seat can install. Written every run, not once
558
+ # per episode: unlike an unhealthy daemon this does not clear itself, and
559
+ # a fact that ages out of one log file is how six days went by.
560
+ log "OK: healthy on $CUR, but AHEAD of the registry ($LATEST) — recorded for the org as ahead-of-registry"
561
+ write_last "$CUR" "$CUR" true true "ahead-of-registry: npm latest is $LATEST"
562
+ else
563
+ case "$PREV_REASON" in unhealthy-current*)
564
+ log "recovered: last.json no longer reports unhealthy-current"
565
+ write_last "$CUR" "$CUR" true true "" ;;
566
+ ahead-of-registry*)
567
+ log "recovered: $CUR is on the registry now (latest $LATEST) — last.json no longer reports ahead-of-registry"
568
+ write_last "$CUR" "$CUR" true true "" ;;
569
+ esac
570
+ fi
571
+ else
572
+ log "UNHEALTHY on $CUR${STALE_FROM:+ (kickstarted from $STALE_FROM)}: $HEALTH_REASON — no upgrade to roll back; the org cannot see this seat. maestro doctor"
573
+ # The `unhealthy-current:` PREFIX is load-bearing (the episode check below
574
+ # and failed_hold_reason both match on it), so the ahead-of-registry fact
575
+ # is appended, never prepended — an unpublished build that is also sick is
576
+ # the worst case and must say both things.
577
+ write_last "$CUR" "$CUR" false false "unhealthy-current: $HEALTH_REASON${AHEAD:+ (and ahead-of-registry: npm latest is $LATEST)}"
578
+ case "$PREV_REASON" in
579
+ unhealthy-current*) log "session already notified this episode (last.json was unhealthy-current); notice not rewritten" ;;
580
+ *) write_notice "$CUR" "$CUR" "unhealthy-current: $HEALTH_REASON" ;;
581
+ esac
582
+ fi
583
+ exit 0
584
+ fi
585
+
586
+ # ── failed-target hold ───────────────────────────────────────────────────────
587
+ # last.json is the memory of the health gate: a LATEST that was installed here
588
+ # and rolled back (or could not even be rolled back) is not retried every hour
589
+ # — that would be two installs, two global refreshes and two daemon restarts an
590
+ # hour until someone touched the kill-switch. An install-failed (network) entry
591
+ # does not hold, and neither does an unhealthy-current entry (its `to` is the
592
+ # installed version, never a LATEST). Fail-open: an unreadable last.json holds
593
+ # nothing.
594
+ FAILED_HOLD_S="${MAESTRO_AUTOUPDATE_FAILED_HOLD_S:-86400}"
595
+ failed_hold_reason(){ # prints "<reason> at <at>" when LATEST failed health within FAILED_HOLD_S
596
+ node -e '
597
+ const [file, latest, holdS] = process.argv.slice(1);
598
+ try {
599
+ const j = JSON.parse(require("fs").readFileSync(file, "utf8"));
600
+ const at = Date.parse(j.at);
601
+ if (j.to === latest && j.ok === false && /unhealthy/.test(String(j.reason)) && Number.isFinite(at) && Date.now() - at < Number(holdS) * 1000) process.stdout.write(`${j.reason} at ${j.at}`);
602
+ } catch {}
603
+ ' "$LAST_JSON" "$LATEST" "$FAILED_HOLD_S" 2>/dev/null
604
+ }
605
+ HOLD="$(failed_hold_reason)"
606
+ if [ -n "$HOLD" ]; then
607
+ log "SKIP: $LATEST already failed the health gate here ($HOLD); holding ${FAILED_HOLD_S}s (still $CUR) — rm state/autoupdate/last.json to retry now"
608
+ exit 0
609
+ fi
610
+ log "UPDATE $CUR -> $LATEST"
611
+
214
612
  # What -session plists THIS agent had installed BEFORE this run. A rollback
215
613
  # reverts only a session job this run's upgrade installed — never one that
216
614
  # was already there (that one is the operator's, and `maestro session stop`
@@ -240,35 +638,6 @@ refresh_global(){ # $1 = version — the rollback's counterpart of upgrade() ste
240
638
  retry "npm install -g $PKG@$1" npm install -g "$PKG@$1" --no-audit --no-fund || log "WARN: global refresh to $1 failed — run: npm install -g $PKG@$1"
241
639
  return 0
242
640
  }
243
- is_healthy(){ # THIS agent's daemon process alive AND a fresh boot/connect line
244
- pgrep -f "$AGENT_DIR/scripts/daemon/maestro-daemon.mjs" >/dev/null 2>&1 || return 1
245
- tail -80 "$DLOG" 2>/dev/null | grep -qE "org-mesh connected|daemon\] Running" || return 1
246
- return 0
247
- }
248
- session_reconciled(){ # only asked when a -session plist was generated; never a rollback reason
249
- label_loaded "$SESSION_LABEL" || { log "reconcile-failed: session job $SESSION_LABEL is not loaded (maestro session start)"; return 1; }
250
- pgrep -f "$AGENT_DIR/scripts/session/supervisor" >/dev/null 2>&1 || { log "reconcile-failed: $SESSION_LABEL is loaded but no supervisor process is alive for $AGENT_DIR (maestro session status)"; return 1; }
251
- return 0
252
- }
253
- write_upgrade_notice(){ # $1 = from, $2 = to — tell the front-door session; never restart it here
254
- local dir="$AGENT_DIR/state/session" tmp
255
- mkdir -p "$dir" 2>/dev/null || return 0
256
- tmp="$(mktemp "$dir/.upgrade-notice.XXXXXX" 2>/dev/null)" || return 0
257
- printf '{\n "from": "%s",\n "to": "%s",\n "at": "%s"\n}\n' "$1" "$2" "$(date -u +%FT%TZ)" > "$tmp" \
258
- && mv -f "$tmp" "$dir/upgrade-notice.json" \
259
- && log "session notified: upgrade-notice.json $1 -> $2 (session restarts itself when idle)"
260
- rm -f "$tmp" 2>/dev/null
261
- return 0
262
- }
263
- write_last(){ # $1 from, $2 to, $3 ok, $4 healthy, $5 reason — state/autoupdate/last.json (→ beat machine.upgrade)
264
- local dir="$AGENT_DIR/state/autoupdate" tmp
265
- mkdir -p "$dir" 2>/dev/null || return 0
266
- tmp="$(mktemp "$dir/.last.XXXXXX" 2>/dev/null)" || return 0
267
- printf '{\n "from": "%s",\n "to": "%s",\n "at": "%s",\n "ok": %s,\n "healthy": %s,\n "reason": "%s"\n}\n' \
268
- "$1" "$2" "$(date -u +%FT%TZ)" "$3" "$4" "$5" > "$tmp" && mv -f "$tmp" "$dir/last.json"
269
- rm -f "$tmp" 2>/dev/null
270
- return 0
271
- }
272
641
  migrate_poller_plist(){ # audit F6 follow-through: an older install left SLACK_USER_TOKEN in a 0644 poller plist; nothing else rewrites it
273
642
  [ -f "$AGENT_DIR/scripts/poller-launchd/migrate.sh" ] || return 0
274
643
  bash "$AGENT_DIR/scripts/poller-launchd/migrate.sh" >> "$LOG" 2>&1 || log "WARN: poller plist migration nonzero (see above)"
@@ -287,11 +656,12 @@ if is_healthy; then
287
656
  REASON=""
288
657
  if [ -n "$SESSION_LABEL" ] && ! session_reconciled; then REASON="session-reconcile-failed"; fi
289
658
  log "OK: healthy on $LATEST${REASON:+ ($REASON)}"
659
+ sibling_job_audit
290
660
  write_upgrade_notice "$CUR" "$LATEST"
291
661
  write_last "$CUR" "$LATEST" true true "$REASON"
292
662
  migrate_poller_plist
293
663
  else
294
- log "UNHEALTHY on $LATEST -> ROLLBACK to $CUR"
664
+ log "UNHEALTHY on $LATEST ($HEALTH_REASON) -> ROLLBACK to $CUR"
295
665
  apply_version "$CUR" || log "WARN: rollback install nonzero"
296
666
  refresh_global "$CUR"
297
667
  revert_session_install
@@ -301,7 +671,7 @@ else
301
671
  log "rolled back to $CUR (healthy)"
302
672
  write_last "$CUR" "$LATEST" false true "unhealthy-rolled-back"
303
673
  else
304
- log "ROLLBACK health still failing on $CUR — needs operator"
674
+ log "ROLLBACK health still failing on $CUR ($HEALTH_REASON) — needs operator"
305
675
  write_last "$CUR" "$LATEST" false false "rollback-unhealthy"
306
676
  fi
307
677
  fi
@@ -35,6 +35,19 @@ TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
35
35
  DATE=$(date +%Y-%m-%d)
36
36
  LOG_FILE="$LOG_DIR/$DATE-$TRIGGER_NAME.jsonl"
37
37
 
38
+ # Retired on engine-cohort seats (W4-E1): this lane runs claude outside the
39
+ # runtime adapter. The guard exits 3 with the reason when runtime.engine is cohort.
40
+ # Fail closed: when the guard cannot run (no node on PATH — launchd's
41
+ # /usr/bin:/bin — or the guard file is missing), config/agent.json is checked
42
+ # here and an engine-cohort seat is refused all the same.
43
+ LEGACY_GUARD="$AGENT_REPO_DIR/lib/runtime/legacy-shell-guard.mjs"
44
+ if [ -f "$LEGACY_GUARD" ] && command -v node >/dev/null 2>&1; then
45
+ node "$LEGACY_GUARD" run-trigger.sh "$AGENT_REPO_DIR" || exit $?
46
+ elif [ -f "$AGENT_REPO_DIR/config/agent.json" ] && grep -Eq '"engine"[[:space:]]*:[[:space:]]*"cohort"' "$AGENT_REPO_DIR/config/agent.json"; then
47
+ echo "run-trigger.sh is retired on this seat: runtime.engine is \"cohort\", and this legacy lane runs the claude binary directly, outside the runtime adapter (the engine guard could not run: node or $LEGACY_GUARD is unavailable)." >&2
48
+ exit 3
49
+ fi
50
+
38
51
  # Check emergency stop
39
52
  if [ -f "$AGENT_REPO_DIR/.emergency-stop" ]; then
40
53
  echo "{\"timestamp\":\"$TIMESTAMP\",\"trigger\":\"$TRIGGER_NAME\",\"status\":\"blocked\",\"reason\":\"emergency-stop\"}" >> "$LOG_FILE"