@cohortapp/agent-sdk 2.17.0 → 2.18.5

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