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