@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
@@ -1,1274 +0,0 @@
1
- /**
2
- * collect.test.mjs — the SHARED STATUS SNAPSHOT collector (collectStatus).
3
- *
4
- * Hermetic: injects node:os, a child-process count, an execFile fake (so no real
5
- * powermetrics/df fork), and log text — asserts the snapshot shape, the
6
- * state/activity/session derivation, machine stats, sub-agent count, and
7
- * claudeAuth detection. Also unit-tests the pure parsers.
8
- *
9
- * Run: node --test lib/telemetry/collect.test.mjs
10
- */
11
- "use strict";
12
-
13
- import { test } from "node:test";
14
- import assert from "node:assert/strict";
15
- import { mkdtempSync, mkdirSync, writeFileSync, rmSync, utimesSync } from "node:fs";
16
- import { tmpdir } from "node:os";
17
- import { join } from "node:path";
18
-
19
- import {
20
- collectStatus,
21
- resolveActivity,
22
- detectClaudeAuth,
23
- countSubAgents,
24
- parsePowermetrics,
25
- parseDf,
26
- collectMachine,
27
- collectPowermetrics,
28
- collectThermal,
29
- parseThermalProbe,
30
- parseThermalPressure,
31
- powermetricsSamplers,
32
- parseSystemProfilerHardware,
33
- ramLabel,
34
- collectSpend24h,
35
- collectInventory,
36
- sessionLiveness,
37
- SESSION_STALE_MS,
38
- _internals,
39
- } from "./collect.mjs";
40
-
41
- const NOW = Date.parse("2026-06-12T12:00:00Z");
42
-
43
- /** A node:os fake with controllable load/mem/cpus. */
44
- function fakeOs(opts = {}) {
45
- return {
46
- loadavg: () => opts.loadavg || [1.5, 1.2, 1.0],
47
- totalmem: () => opts.totalmem ?? 16 * 1024 * 1024 * 1024,
48
- freemem: () => opts.freemem ?? 8 * 1024 * 1024 * 1024,
49
- cpus: () => new Array(opts.cpus ?? 8).fill({ model: "x" }),
50
- platform: () => opts.platform || "darwin",
51
- };
52
- }
53
-
54
- /** An execFile fake that returns canned stdout per binary. Never forks. */
55
- function fakeExecFile(map = {}) {
56
- return (cmd, args, optsOrCb, maybeCb) => {
57
- const cb = typeof optsOrCb === "function" ? optsOrCb : maybeCb;
58
- const out = map[cmd] ?? "";
59
- queueMicrotask(() => cb(null, out, ""));
60
- return { on() {} };
61
- };
62
- }
63
-
64
- // ---------------------------------------------------------------------------
65
-
66
- test("collectStatus builds a well-formed snapshot (mocked os/powermetrics/df)", async () => {
67
- const status = await collectStatus({
68
- agentRoot: "/nonexistent-root", // no logs → claudeAuth unknown
69
- now: NOW,
70
- os: fakeOs({ loadavg: [2, 1, 1], totalmem: 16e9, freemem: 4e9, cpus: 8 }),
71
- execFile: fakeExecFile({
72
- "/usr/bin/powermetrics": "CPU die temperature: 61.4 C\nCPU idle residency: 78.0 %\n",
73
- "/bin/df": "Filesystem 1024-blocks Used Available Capacity Mounted\n/dev/disk1 1000 600 400 60% /\n",
74
- }),
75
- subAgentsRunning: 2,
76
- });
77
-
78
- // shape
79
- assert.equal(typeof status.state, "string");
80
- assert.equal(typeof status.activity, "string");
81
- assert.equal(typeof status.subAgentsRunning, "number");
82
- assert.ok(Array.isArray(status.alerts));
83
- assert.ok(Array.isArray(status.machine.loadavg));
84
- assert.equal(status.machine.loadavg.length, 3);
85
- assert.match(status.ts, /^2026-06-12T12:00:00/);
86
-
87
- // machine
88
- assert.equal(status.machine.memUsedPct, 75); // 1 - 4/16 = 0.75
89
- assert.equal(status.machine.tempC, 61.4);
90
- assert.equal(status.machine.diskUsedPct, 60);
91
- assert.equal(status.machine.cpuPct, 22); // 100 - 78 idle
92
-
93
- // sub-agents + idle→busy promotion
94
- assert.equal(status.subAgentsRunning, 2);
95
- assert.equal(status.state, "busy"); // idle but children running → busy
96
- assert.equal(status.claudeAuth, "unknown");
97
- });
98
-
99
- test("collectStatus: relogin in logs → state error + claudeAuth relogin_required + critical alert", async () => {
100
- const status = await collectStatus({
101
- agentRoot: "/x",
102
- now: NOW,
103
- os: fakeOs(),
104
- execFile: fakeExecFile({ "/usr/bin/powermetrics": "", "/bin/df": "" }),
105
- subAgentsRunning: 0,
106
- logText: "ERROR authentication_error: please run /login\n",
107
- });
108
- assert.equal(status.claudeAuth, "relogin_required");
109
- assert.equal(status.state, "error");
110
- const relogin = status.alerts.find((a) => a.id === "claude_relogin_required");
111
- assert.ok(relogin, "a critical re-login alert is attached");
112
- assert.equal(relogin.severity, "critical");
113
- });
114
-
115
- test("resolveActivity: a running work session → busy + session{taskClass,model}", () => {
116
- const sessions = [
117
- { sessionId: "s1", kind: "interactive", intent: "session: inbox", model: "sonnet", lastBeat: "2026-06-12T11:59:50Z" },
118
- { sessionId: "d1", kind: "daemon", intent: "daemon", lastBeat: "2026-06-12T11:59:55Z" },
119
- ];
120
- const act = resolveActivity({ sessions, now: NOW });
121
- assert.equal(act.state, "busy");
122
- assert.equal(act.session.id, "s1");
123
- assert.equal(act.session.taskClass, "inbox");
124
- assert.equal(act.session.model, "sonnet");
125
- assert.match(act.activity, /inbox/);
126
- });
127
-
128
- test("resolveActivity: only a daemon present → idle, no session", () => {
129
- const act = resolveActivity({ sessions: [{ sessionId: "d1", kind: "daemon", intent: "daemon", lastBeat: "2026-06-12T11:59:55Z" }], now: NOW });
130
- assert.equal(act.state, "idle");
131
- assert.equal(act.session, null);
132
- assert.equal(act.activity, "idle");
133
- });
134
-
135
- test("resolveActivity: nothing running → idle", () => {
136
- const act = resolveActivity({ sessions: [], now: NOW });
137
- assert.equal(act.state, "idle");
138
- assert.equal(act.activity, "idle");
139
- assert.equal(act.session, null);
140
- });
141
-
142
- test("countSubAgents: explicit override wins; injected liveClaudeStats used otherwise", () => {
143
- assert.equal(countSubAgents({ subAgentsRunning: 3 }), 3);
144
- assert.equal(countSubAgents({ liveClaudeStats: () => ({ count: 4, rssMB: 100 }) }), 4);
145
- assert.equal(countSubAgents({ liveClaudeStats: () => { throw new Error("boom"); } }), 0);
146
- });
147
-
148
- test("detectClaudeAuth: logText override paths", () => {
149
- assert.equal(detectClaudeAuth({ logText: "all good, session complete" }), "ok");
150
- assert.equal(detectClaudeAuth({ logText: "Invalid API key provided" }), "relogin_required");
151
- assert.equal(detectClaudeAuth({ logText: "" }), "unknown");
152
- });
153
-
154
- test("detectClaudeAuth: scans recent log files under agentRoot", () => {
155
- const root = mkdtempSync(join(tmpdir(), "tel-auth-"));
156
- try {
157
- mkdirSync(join(root, "logs", "sessions"), { recursive: true });
158
- writeFileSync(join(root, "logs", "sessions", "2026-06-12-sessions.jsonl"),
159
- '{"msg":"oauth token has expired, please run claude login"}\n');
160
- assert.equal(detectClaudeAuth({ agentRoot: root, now: NOW }), "relogin_required");
161
-
162
- // A clean recent log → ok.
163
- const root2 = mkdtempSync(join(tmpdir(), "tel-auth2-"));
164
- mkdirSync(join(root2, "logs", "sessions"), { recursive: true });
165
- writeFileSync(join(root2, "logs", "sessions", "2026-06-12-sessions.jsonl"), '{"msg":"ok"}\n');
166
- assert.equal(detectClaudeAuth({ agentRoot: root2, now: NOW }), "ok");
167
- rmSync(root2, { recursive: true, force: true });
168
-
169
- // No logs at all → unknown.
170
- const root3 = mkdtempSync(join(tmpdir(), "tel-auth3-"));
171
- assert.equal(detectClaudeAuth({ agentRoot: root3, now: NOW }), "unknown");
172
- rmSync(root3, { recursive: true, force: true });
173
- } finally {
174
- rmSync(root, { recursive: true, force: true });
175
- }
176
- });
177
-
178
- test("collectMachine: derives memUsedPct + loadavg without powermetrics/df", async () => {
179
- // memUsedPct now prefers macOS's memory_pressure, so "no external tools" has
180
- // to include that one too — otherwise this test reads the REAL machine and
181
- // asserts against the fake os, which is how it started failing.
182
- const noTools = (cmd, args, opts, cb) => { cb(new Error("ENOENT")); return { on() {} }; };
183
- const m = await collectMachine({ os: fakeOs({ totalmem: 10e9, freemem: 2e9, loadavg: [4, 2, 1], cpus: 4 }), execFile: noTools, powermetrics: false, disk: false });
184
- assert.equal(m.memUsedPct, 80);
185
- assert.deepEqual(m.loadavg, [4, 2, 1]);
186
- assert.equal(m.cpuPct, 100); // load 4 / 4 cpus = 100%
187
- assert.equal(m.tempC, undefined);
188
- assert.equal(m.diskUsedPct, undefined);
189
- });
190
-
191
- test("parsePowermetrics + parseDf are tolerant pure parsers", () => {
192
- const pm = parsePowermetrics("GPU die temperature: 45 C\nCPU die temperature: 72.5 C\nCPU idle residency: 90.0 %\n");
193
- assert.equal(pm.tempC, 72.5);
194
- assert.equal(pm.cpuPct, 10);
195
- assert.deepEqual(parsePowermetrics("nonsense"), {});
196
-
197
- assert.equal(parseDf("Filesystem blocks Used Avail Capacity Mounted\n/dev/x 100 88 12 88% /\n"), 88);
198
- assert.equal(parseDf(""), undefined);
199
- });
200
-
201
- test("collectStatus never throws on a broken execFile / os", async () => {
202
- const status = await collectStatus({
203
- agentRoot: "/x",
204
- now: NOW,
205
- os: { loadavg: () => { throw new Error("x"); }, totalmem: () => { throw new Error("x"); }, freemem: () => 0, cpus: () => { throw new Error("x"); }, platform: () => "darwin" },
206
- execFile: () => { throw new Error("exec boom"); },
207
- subAgentsRunning: 0,
208
- });
209
- assert.ok(status && typeof status === "object");
210
- assert.deepEqual(status.machine.loadavg, [0, 0, 0]);
211
- assert.equal(status.machine.memUsedPct, 0);
212
- });
213
-
214
- test("execFileSafe resolves '' on a throwing exec impl (no reject)", async () => {
215
- const out = await _internals.execFileSafe(() => { throw new Error("nope"); }, "x", [], 50);
216
- assert.equal(out, "");
217
- });
218
-
219
- // ---------------------------------------------------------------------------
220
- // The Fleet telemetry vocabulary — the fields the hq Fleet row reads by name.
221
- //
222
- // These four (device / ram / uptimeDays / spend24h) were READ by
223
- // hq/src/components/fleet/machine.ts#buildFleetMachine and emitted by NOTHING,
224
- // while four others (loadavg / memUsedPct / cpuPct / diskUsedPct) were emitted
225
- // here and read by nothing. The intersection in production was EMPTY, so the
226
- // Fleet view em-dashed every hardware column on the one machine that was
227
- // genuinely alive. These tests pin the producer half of that contract.
228
- // ---------------------------------------------------------------------------
229
-
230
- const SP_HARDWARE = [
231
- "Hardware:",
232
- " Hardware Overview:",
233
- " Model Name: Mac mini",
234
- " Model Identifier: Mac16,11",
235
- " Chip: Apple M4 Pro",
236
- " Total Number of Cores: 12 (8 performance and 4 efficiency)",
237
- " Memory: 24 GB",
238
- "",
239
- ].join("\n");
240
-
241
- test("collectMachine emits the INVENTORY the Fleet row reads: device, ram, cores", async () => {
242
- const m = await collectMachine({
243
- os: fakeOs({ totalmem: 24 * 1024 ** 3, freemem: 2e9, cpus: 12 }),
244
- execFile: fakeExecFile({ "/usr/sbin/system_profiler": SP_HARDWARE }),
245
- powermetrics: false,
246
- disk: false,
247
- spend: false,
248
- });
249
- assert.equal(m.device, "Mac mini M4 Pro");
250
- assert.equal(m.ram, "24 GB");
251
- assert.equal(m.cores, 12);
252
- });
253
-
254
- test("collectMachine falls back to node:os inventory when system_profiler yields nothing", async () => {
255
- const m = await collectMachine({
256
- os: {
257
- loadavg: () => [1, 1, 1],
258
- totalmem: () => 24 * 1024 ** 3,
259
- freemem: () => 12 * 1024 ** 3,
260
- cpus: () => new Array(12).fill({ model: "Apple M4 Pro" }),
261
- platform: () => "darwin",
262
- uptime: () => 0,
263
- },
264
- execFile: fakeExecFile({}), // system_profiler returns ""
265
- powermetrics: false,
266
- disk: false,
267
- spend: false,
268
- });
269
- // Never nothing: node:os alone still proves the chip, the RAM and the cores.
270
- assert.equal(m.device, "Apple M4 Pro");
271
- assert.equal(m.ram, "24 GB");
272
- assert.equal(m.cores, 12);
273
- });
274
-
275
- test("collectMachine emits uptimeDays", async () => {
276
- const osImpl = fakeOs({});
277
- osImpl.uptime = () => 5756923; // ~66.6 days
278
- const m = await collectMachine({
279
- os: osImpl, powermetrics: false, disk: false, inventory: false, spend: false,
280
- });
281
- assert.equal(m.uptimeDays, 66.6);
282
- });
283
-
284
- test("ramLabel renders binary GB the way the spec sheet does", () => {
285
- assert.equal(ramLabel(24 * 1024 ** 3), "24 GB");
286
- assert.equal(ramLabel(192 * 1024 ** 3), "192 GB");
287
- assert.equal(ramLabel(8 * 1024 ** 3), "8 GB");
288
- assert.equal(ramLabel(0), "");
289
- assert.equal(ramLabel("nonsense"), "");
290
- });
291
-
292
- test("parseSystemProfilerHardware is a tolerant pure parser", () => {
293
- assert.deepEqual(parseSystemProfilerHardware(SP_HARDWARE), {
294
- device: "Mac mini M4 Pro",
295
- ram: "24 GB",
296
- cores: 12,
297
- });
298
- // Partial output degrades field-by-field rather than throwing.
299
- assert.deepEqual(parseSystemProfilerHardware(" Memory: 16 GB\n"), { ram: "16 GB" });
300
- assert.deepEqual(parseSystemProfilerHardware(""), {});
301
- assert.deepEqual(parseSystemProfilerHardware(null), {});
302
- });
303
-
304
- // ---------------------------------------------------------------------------
305
- // tempC — the sampler bug, and the rule that a failure is never silent
306
- // ---------------------------------------------------------------------------
307
-
308
- test("powermetricsSamplers uses the arm64 spelling — 'smc' is Intel-only", () => {
309
- // The shipped constant was "smc,cpu_power" unconditionally. On Apple Silicon
310
- // powermetrics rejects that argument list outright and exits non-zero BEFORE
311
- // the root check, so the probe was unreachable on every machine in the fleet.
312
- assert.equal(powermetricsSamplers("arm64"), "thermal,cpu_power");
313
- assert.equal(powermetricsSamplers("x64"), "smc,cpu_power");
314
- });
315
-
316
- test("collectPowermetrics passes the arch-correct sampler to the binary", async () => {
317
- const seen = [];
318
- const spy = (cmd, args, optsOrCb, maybeCb) => {
319
- const cb = typeof optsOrCb === "function" ? optsOrCb : maybeCb;
320
- seen.push({ cmd, args });
321
- queueMicrotask(() => cb(null, "CPU die temperature: 54.2 C\n", ""));
322
- return { on() {} };
323
- };
324
- const pm = await collectPowermetrics({ execFile: spy, platform: "darwin", arch: "arm64" });
325
- assert.equal(pm.tempC, 54.2);
326
- assert.equal(pm.source, "powermetrics");
327
- const samplerArg = seen[0].args[seen[0].args.indexOf("--samplers") + 1];
328
- assert.equal(samplerArg, "thermal,cpu_power");
329
- assert.ok(!samplerArg.includes("smc"), "must not send the Intel-only sampler to arm64");
330
- });
331
-
332
- test("collectPowermetrics ALWAYS says why it has no answer (fail-open, never silent)", async () => {
333
- // The unprivileged reality on every seat: the probe yields nothing.
334
- const pm = await collectPowermetrics({
335
- execFile: fakeExecFile({}), platform: "darwin", arch: "arm64", sudoPowermetrics: false,
336
- });
337
- assert.equal(pm.tempC, undefined);
338
- assert.match(pm.detail, /requires root/);
339
- // It no longer tells the operator to edit sudoers to get a temperature. That
340
- // advice was futile: powermetrics has no die temperature on Apple Silicon at
341
- // ANY privilege level, so following it to the letter still yielded nothing.
342
- assert.ok(!/TELEMETRY_SUDO_POWERMETRICS/.test(pm.detail));
343
- assert.ok(!/sudoers/.test(pm.detail));
344
-
345
- // A non-darwin host is a DIFFERENT absence and says so.
346
- const linux = await collectPowermetrics({ platform: "linux" });
347
- assert.match(linux.detail, /platform linux/);
348
- });
349
-
350
- test("collectMachine surfaces a thermal failure as a NOTE, never as the value", async () => {
351
- const m = await collectMachine({
352
- os: fakeOs({}),
353
- execFile: fakeExecFile({}),
354
- platform: "darwin",
355
- arch: "arm64",
356
- sudoPowermetrics: false,
357
- disk: false,
358
- inventory: false,
359
- spend: false,
360
- });
361
- assert.equal(m.tempC, undefined);
362
- assert.equal(m.tempSource, undefined);
363
- assert.equal(m.thermalState, undefined);
364
- assert.equal(typeof m.tempDetail, "string");
365
- // The note must not blame the operator's sudoers file for a limitation of the
366
- // hardware — that is the message this whole rewrite exists to delete.
367
- assert.ok(!/sudoers/.test(m.tempDetail));
368
- // And the cpuPct it DOES have is labelled as the estimate it is.
369
- assert.equal(m.cpuSource, "loadavg");
370
- });
371
-
372
- test("collectPowermetrics tries the opt-in sudo path only when enabled", async () => {
373
- const seen = [];
374
- const spy = (cmd, args, optsOrCb, maybeCb) => {
375
- const cb = typeof optsOrCb === "function" ? optsOrCb : maybeCb;
376
- seen.push(cmd);
377
- // Only the sudo invocation yields.
378
- const out = cmd === "/usr/bin/sudo" ? "CPU die temperature: 61.0 C\n" : "";
379
- queueMicrotask(() => cb(null, out, ""));
380
- return { on() {} };
381
- };
382
- const off = await collectPowermetrics({
383
- execFile: spy, platform: "darwin", arch: "arm64", sudoPowermetrics: false,
384
- });
385
- assert.equal(off.tempC, undefined, "must not shell out to sudo unless asked");
386
- assert.ok(!seen.includes("/usr/bin/sudo"));
387
-
388
- seen.length = 0;
389
- const on = await collectPowermetrics({
390
- execFile: spy, platform: "darwin", arch: "arm64", sudoPowermetrics: true,
391
- });
392
- assert.equal(on.tempC, 61);
393
- assert.equal(on.source, "sudo-powermetrics");
394
- assert.ok(seen.includes("/usr/bin/sudo"));
395
- // `-n` so a machine with no NOPASSWD rule fails instantly instead of hanging
396
- // the beat on a password prompt nobody will ever see.
397
- const sudoCall = seen.indexOf("/usr/bin/sudo");
398
- assert.ok(sudoCall >= 0);
399
- });
400
-
401
- // ---------------------------------------------------------------------------
402
- // Thermal — real °C without root, and a coarse state word that never fails.
403
- //
404
- // Every test here stubs the exec seam. NOTHING in this file forks `swift`,
405
- // `powermetrics` or `pmset`; the probe's real output is pinned as fixture text.
406
- // ---------------------------------------------------------------------------
407
-
408
- /** Verbatim stdout of `swift lib/telemetry/thermal-probe.swift` on a Mac16,11. */
409
- const PROBE_OK = "thermalState=nominal\ntempC=60.76\ntempSensors=42\n";
410
-
411
- /** Verbatim `sudo powermetrics --samplers thermal` — note: NO temperature line. */
412
- const PM_THERMAL = [
413
- "Machine model: Mac16,11",
414
- "",
415
- "**** Thermal pressure ****",
416
- "",
417
- "Current pressure level: Nominal",
418
- "",
419
- ].join("\n");
420
-
421
- /** Route stdout by binary, so a test can say which rung of the ladder answers. */
422
- const SWIFT = "/usr/bin/swift";
423
- const SUDO = "/usr/bin/sudo";
424
- const PMSET = "/usr/bin/pmset";
425
-
426
- test("collectThermal reads a REAL °C unprivileged, via the IOHID probe", async () => {
427
- // The headline fix. powermetrics cannot produce a die temperature on Apple
428
- // Silicon even as root; IOHIDEventSystem hands it over to any uid.
429
- const seen = [];
430
- const spy = (cmd, args, optsOrCb, maybeCb) => {
431
- const cb = typeof optsOrCb === "function" ? optsOrCb : maybeCb;
432
- seen.push(cmd);
433
- queueMicrotask(() => cb(null, cmd === SWIFT ? PROBE_OK : "", ""));
434
- return { on() {} };
435
- };
436
- const th = await collectThermal({ execFile: spy, platform: "darwin", arch: "arm64" });
437
- assert.equal(th.tempC, 60.76);
438
- assert.equal(th.tempSource, "iohid");
439
- assert.equal(th.thermalState, "nominal");
440
- assert.equal(th.thermalSource, "processinfo");
441
- // No root, no sudo, on the happy path.
442
- assert.ok(!seen.includes(SUDO), "the °C path must never need sudo");
443
- // The detail is an EXPLANATION riding alongside a real reading, not a
444
- // substitute for one — the drawer prints the number and demotes this.
445
- assert.equal(typeof th.detail, "string");
446
- assert.match(th.detail, /IOHIDEventSystem/);
447
- });
448
-
449
- test("collectThermal falls back to the coarse STATE when there is no °C", async () => {
450
- // The probe ran but the sensors gave nothing: state alone is still a useful,
451
- // honest answer, and the field semantics say so rather than faking a number.
452
- const spy = (cmd, args, optsOrCb, maybeCb) => {
453
- const cb = typeof optsOrCb === "function" ? optsOrCb : maybeCb;
454
- queueMicrotask(() => cb(null, cmd === SWIFT ? "thermalState=serious\n" : "", ""));
455
- return { on() {} };
456
- };
457
- const th = await collectThermal({ execFile: spy, platform: "darwin", arch: "arm64" });
458
- assert.equal(th.tempC, undefined);
459
- assert.equal(th.thermalState, "serious");
460
- assert.equal(th.thermalSource, "processinfo");
461
- assert.match(th.detail, /no numeric °C/);
462
- });
463
-
464
- test("collectThermal uses sudo powermetrics for PRESSURE only, and only when opted in", async () => {
465
- const seen = [];
466
- const spy = (cmd, args, optsOrCb, maybeCb) => {
467
- const cb = typeof optsOrCb === "function" ? optsOrCb : maybeCb;
468
- seen.push(cmd);
469
- // swift is absent on this host; sudo powermetrics answers.
470
- queueMicrotask(() => cb(null, cmd === SUDO ? PM_THERMAL : "", ""));
471
- return { on() {} };
472
- };
473
- const off = await collectThermal({
474
- execFile: spy, platform: "darwin", arch: "arm64", sudoPowermetrics: false,
475
- });
476
- assert.ok(!seen.includes(SUDO), "must not shell out to sudo unless asked");
477
- assert.equal(off.thermalState, undefined);
478
-
479
- seen.length = 0;
480
- const on = await collectThermal({
481
- execFile: spy, platform: "darwin", arch: "arm64", sudoPowermetrics: true,
482
- });
483
- assert.ok(seen.includes(SUDO));
484
- assert.equal(on.thermalState, "nominal");
485
- assert.equal(on.thermalSource, "sudo-powermetrics");
486
- // Root buys a pressure LEVEL and nothing more. If this ever starts asserting a
487
- // tempC from powermetrics on arm64, the fixture is lying.
488
- assert.equal(on.tempC, undefined);
489
- assert.match(on.detail, /pressure/i);
490
- });
491
-
492
- test("collectThermal will NOT dress 'no thermal events on record' up as a live reading", async () => {
493
- // `pmset -g therm` on a healthy Mac reports history, not temperature. Calling
494
- // that "nominal" would be inventing a measurement nobody took.
495
- const spy = (cmd, args, optsOrCb, maybeCb) => {
496
- const cb = typeof optsOrCb === "function" ? optsOrCb : maybeCb;
497
- const out = cmd === PMSET ? "Note: No thermal warning level has been recorded\n" : "";
498
- queueMicrotask(() => cb(null, out, ""));
499
- return { on() {} };
500
- };
501
- const th = await collectThermal({ execFile: spy, platform: "darwin", arch: "arm64" });
502
- assert.equal(th.thermalState, undefined);
503
- assert.equal(th.tempC, undefined);
504
- assert.match(th.detail, /xcode-select --install/);
505
-
506
- // But a REAL throttle event — a speed limit below 100 — is a live signal.
507
- const throttling = (cmd, args, optsOrCb, maybeCb) => {
508
- const cb = typeof optsOrCb === "function" ? optsOrCb : maybeCb;
509
- const out = cmd === PMSET ? "CPU_Speed_Limit \t= 60\n" : "";
510
- queueMicrotask(() => cb(null, out, ""));
511
- return { on() {} };
512
- };
513
- const hot = await collectThermal({ execFile: throttling, platform: "darwin", arch: "arm64" });
514
- assert.equal(hot.thermalState, "serious");
515
- assert.equal(hot.thermalSource, "pmset");
516
- });
517
-
518
- test("parseThermalProbe rejects the sensor that lies (-9201.14 °C)", () => {
519
- // `PMU tdev2` returns garbage on real hardware. The probe filters it, and the
520
- // collector band-checks AGAIN — trusting a subprocess's arithmetic is how an
521
- // absurd number reaches a dashboard.
522
- assert.deepEqual(parseThermalProbe("thermalState=nominal\ntempC=-9201.14\n"), {
523
- thermalState: "nominal",
524
- });
525
- assert.equal(parseThermalProbe("tempC=900\n").tempC, undefined);
526
- assert.equal(parseThermalProbe("tempC=61.2\n").tempC, 61.2);
527
- // Unknown keys are ignored so the probe can grow a field without breaking us.
528
- assert.equal(parseThermalProbe("tempC=61.2\nfuture=x\n").tempC, 61.2);
529
- assert.deepEqual(parseThermalProbe(""), {});
530
- assert.deepEqual(parseThermalProbe(null), {});
531
- // Only the four real levels are accepted.
532
- assert.equal(parseThermalProbe("thermalState=toasty\n").thermalState, undefined);
533
- });
534
-
535
- test("parseThermalPressure reads the only thermal fact powermetrics has on arm64", () => {
536
- assert.equal(parseThermalPressure(PM_THERMAL), "nominal");
537
- assert.equal(parseThermalPressure("Current pressure level: Critical"), "critical");
538
- assert.equal(parseThermalPressure("no such block"), undefined);
539
- assert.equal(parseThermalPressure(""), undefined);
540
- });
541
-
542
- test("parsePowermetrics computes CPU% from the PER-CLUSTER arm64 shape", () => {
543
- // The old regex wanted a bare `CPU idle residency:` line. Apple Silicon never
544
- // prints one — only per-core and per-cluster lines — so on every seat in the
545
- // fleet this silently fell through and then blamed the operator's sudoers.
546
- const arm = [
547
- "**** Processor usage ****",
548
- "E-Cluster idle residency: 78.34%",
549
- "CPU 0 idle residency: 90.00%",
550
- "CPU 1 idle residency: 80.00%",
551
- "CPU 2 idle residency: 70.00%",
552
- "CPU 3 idle residency: 60.00%",
553
- ].join("\n");
554
- const parsed = parsePowermetrics(arm);
555
- assert.equal(parsed.cpuPct, 25); // 100 - mean(90,80,70,60)
556
- // The Intel/total shape still wins outright where it exists.
557
- assert.equal(parsePowermetrics("CPU Average idle residency: 78.0 %").cpuPct, 22);
558
- });
559
-
560
- test("collectMachine ships tempC + thermalState + the note TOGETHER", async () => {
561
- // The old collector made the reading and the explanation MUTUALLY EXCLUSIVE
562
- // (`else if`), which left hq with nothing to put in the value slot — so it
563
- // printed a 118-character sudoers sentence in a column of "62°C" and "41%".
564
- const spy = (cmd, args, optsOrCb, maybeCb) => {
565
- const cb = typeof optsOrCb === "function" ? optsOrCb : maybeCb;
566
- queueMicrotask(() => cb(null, cmd === SWIFT ? PROBE_OK : "", ""));
567
- return { on() {} };
568
- };
569
- const m = await collectMachine({
570
- os: fakeOs({}),
571
- execFile: spy,
572
- platform: "darwin",
573
- arch: "arm64",
574
- sudoPowermetrics: false,
575
- disk: false,
576
- inventory: false,
577
- spend: false,
578
- });
579
- assert.equal(m.tempC, 60.8); // round1
580
- assert.equal(m.tempSource, "iohid");
581
- assert.equal(m.thermalState, "nominal");
582
- assert.equal(m.thermalSource, "processinfo");
583
- assert.equal(typeof m.tempDetail, "string");
584
- });
585
-
586
- // ---------------------------------------------------------------------------
587
- // spend24h
588
- // ---------------------------------------------------------------------------
589
-
590
- test("collectSpend24h sums BILLABLE rows in a true rolling 24h window", () => {
591
- const root = mkdtempSync(join(tmpdir(), "spend-"));
592
- try {
593
- const dir = join(root, "state", "cost-tracking");
594
- mkdirSync(dir, { recursive: true });
595
- const now = Date.parse("2026-08-13T06:00:00Z");
596
- const row = (ts, cost) => JSON.stringify({
597
- ts, model: "claude-opus-4-5", input_tokens: 10, output_tokens: 5, total_cost_usd: cost,
598
- });
599
- // Yesterday's file: one row inside the window, one outside it.
600
- writeFileSync(join(dir, "2026-08-12.jsonl"), [
601
- row("2026-08-12T05:00:00Z", 1.5), // 25h ago — OUTSIDE
602
- row("2026-08-12T07:00:00Z", 2.25), // 23h ago — inside
603
- ].join("\n") + "\n");
604
- writeFileSync(join(dir, "2026-08-13.jsonl"), [
605
- row("2026-08-13T01:00:00Z", 0.75),
606
- // A non-LLM attribution row: $0 is a FACT (no model ran), not an absence.
607
- JSON.stringify({ ts: "2026-08-13T02:00:00Z", model: null, task_class: "messaging.send", total_cost_usd: null, estimated_usd: 0 }),
608
- ].join("\n") + "\n");
609
-
610
- assert.equal(collectSpend24h({ agentRoot: root, now }), 3);
611
- } finally {
612
- rmSync(root, { recursive: true, force: true });
613
- }
614
- });
615
-
616
- test("collectSpend24h: no ledger is UNDEFINED, not a confident zero", () => {
617
- assert.equal(collectSpend24h({ agentRoot: "/nonexistent-root-xyz" }), undefined);
618
- assert.equal(collectSpend24h({}), undefined);
619
- });
620
-
621
- test("collectSpend24h does not count an UNMEASURED row as free", () => {
622
- const root = mkdtempSync(join(tmpdir(), "spend2-"));
623
- try {
624
- const dir = join(root, "state", "cost-tracking");
625
- mkdirSync(dir, { recursive: true });
626
- const now = Date.parse("2026-08-13T06:00:00Z");
627
- writeFileSync(join(dir, "2026-08-13.jsonl"), [
628
- // measurement:"unknown" → billableUsd().usd === null. Skipped, not zeroed.
629
- JSON.stringify({ ts: "2026-08-13T01:00:00Z", model: "claude-opus-4-5", measurement: "unknown", input_tokens: null, output_tokens: null, total_cost_usd: null }),
630
- JSON.stringify({ ts: "2026-08-13T02:00:00Z", model: "claude-opus-4-5", input_tokens: 1, output_tokens: 1, total_cost_usd: 4.5 }),
631
- "{ not json",
632
- ].join("\n") + "\n");
633
- assert.equal(collectSpend24h({ agentRoot: root, now }), 4.5);
634
- } finally {
635
- rmSync(root, { recursive: true, force: true });
636
- }
637
- });
638
-
639
- test("collectStatus ships the FULL machine vocabulary the Fleet row reads", async () => {
640
- const root = mkdtempSync(join(tmpdir(), "vocab-"));
641
- try {
642
- const osImpl = fakeOs({ totalmem: 24 * 1024 ** 3, freemem: 1e9, loadavg: [1.26, 1.2, 1.08], cpus: 12 });
643
- osImpl.uptime = () => 5756923;
644
- const status = await collectStatus({
645
- agentRoot: root,
646
- now: Date.parse("2026-08-13T06:00:00Z"),
647
- os: osImpl,
648
- arch: "arm64",
649
- sudoPowermetrics: false,
650
- execFile: fakeExecFile({
651
- "/usr/sbin/system_profiler": SP_HARDWARE,
652
- "/bin/df": "Filesystem 1024-blocks Used Available Capacity Mounted\n/dev/disk1 100 47 53 47% /\n",
653
- }),
654
- subAgentsRunning: 0,
655
- });
656
- const m = status.machine;
657
- // The five keys hq reads that this collector used to emit NONE of.
658
- assert.equal(m.device, "Mac mini M4 Pro");
659
- assert.equal(m.ram, "24 GB");
660
- assert.equal(m.uptimeDays, 66.6);
661
- assert.equal(typeof m.tempDetail, "string"); // tempC needs root; the REASON ships
662
- // The four it emitted that hq used to read NONE of.
663
- assert.deepEqual(m.loadavg, [1.26, 1.2, 1.08]);
664
- assert.ok(m.memUsedPct > 90);
665
- assert.equal(typeof m.cpuPct, "number");
666
- assert.equal(m.diskUsedPct, 47);
667
- assert.equal(m.cpuSource, "loadavg");
668
- } finally {
669
- rmSync(root, { recursive: true, force: true });
670
- }
671
- });
672
-
673
- // ---------------------------------------------------------------------------
674
- // memUsedPct — pressure, not occupancy
675
- //
676
- // This was `1 - os.freemem()/os.totalmem()`, which is not a pressure measure on
677
- // macOS: os.freemem() counts only entirely-free pages, and macOS keeps nearly
678
- // all RAM occupied with file cache, purgeable and compressed pages that are
679
- // reclaimable on demand. Measured on a healthy idle Mac mini M4 Pro at one
680
- // instant: os.freemem() → 99.2% "used", memory_pressure → 68% free. The seat
681
- // showed a standing CRITICAL while the watchdog, reading memory_pressure,
682
- // correctly did nothing.
683
- // ---------------------------------------------------------------------------
684
-
685
- test("memUsedPct comes from memory_pressure, not from free/total", async () => {
686
- const seen = [];
687
- const execFile = (cmd, args, opts, cb) => {
688
- seen.push(cmd);
689
- if (cmd === "/usr/bin/memory_pressure") {
690
- cb(null, "System-wide memory free percentage: 68%\n");
691
- } else {
692
- cb(new Error("not stubbed"));
693
- }
694
- return { on() {} };
695
- };
696
- // An os whose free/total ratio would say 99% used — the old, wrong answer.
697
- const osImpl = {
698
- loadavg: () => [1, 1, 1],
699
- totalmem: () => 24 * 1024 ** 3,
700
- freemem: () => 0.2 * 1024 ** 3,
701
- cpus: () => [],
702
- uptime: () => 3600,
703
- };
704
-
705
- const m = await collectMachine({ execFile, os: osImpl, platform: "darwin" });
706
-
707
- assert.equal(m.memUsedPct, 32, "100 - 68 free, i.e. what the watchdog thresholds on");
708
- assert.notEqual(m.memUsedPct, 99, "the free/total ratio must not be the answer on darwin");
709
- assert.ok(seen.includes("/usr/bin/memory_pressure"), "memory_pressure is consulted");
710
- });
711
-
712
- test("memUsedPct falls back to free/total when memory_pressure yields nothing", async () => {
713
- // Linux, or a macOS where the binary is missing/erroring. There the ratio
714
- // does mean what it says, so a fallback is right — silence is not.
715
- const execFile = (cmd, args, opts, cb) => { cb(new Error("ENOENT")); return { on() {} }; };
716
- const osImpl = {
717
- loadavg: () => [0, 0, 0],
718
- totalmem: () => 1000,
719
- freemem: () => 250,
720
- cpus: () => [],
721
- uptime: () => 1,
722
- };
723
-
724
- const m = await collectMachine({ execFile, os: osImpl, platform: "darwin" });
725
- assert.equal(m.memUsedPct, 75, "1 - 250/1000");
726
- });
727
-
728
- test("memUsedPct ignores an out-of-range memory_pressure reading", async () => {
729
- const execFile = (cmd, args, opts, cb) => {
730
- cb(null, "System-wide memory free percentage: 8000%\n");
731
- return { on() {} };
732
- };
733
- const osImpl = {
734
- loadavg: () => [0, 0, 0],
735
- totalmem: () => 1000,
736
- freemem: () => 400,
737
- cpus: () => [],
738
- uptime: () => 1,
739
- };
740
-
741
- const m = await collectMachine({ execFile, os: osImpl, platform: "darwin" });
742
- assert.equal(m.memUsedPct, 60, "a nonsense percentage falls back rather than propagating");
743
- });
744
-
745
- // ---------------------------------------------------------------------------
746
- // Fleet identity + front door (§3.8): hostname / tailnetIp / sdkVersion ride
747
- // the inventory; machine.frontDoor + machine.sessionLive come off the main
748
- // session's heartbeat file. This is how a fleet-wide upgrade is verified.
749
- //
750
- // WHY `machine` and not `session`: hq's presence.beat validator is
751
- // `session: z.object({id,taskClass,model}).strict()` — an unknown key there
752
- // rejects the WHOLE beat (BAD_REQUEST before any write) and the seat drops out
753
- // of the mesh. `machine` is `z.record(z.unknown())` by design, so a new field
754
- // lands the moment the daemon emits it. `session` stays `null` when no work
755
- // session runs — consumers read null as "no work session", and that contract
756
- // is pinned here.
757
- // ---------------------------------------------------------------------------
758
-
759
- test("collectInventory emits hostname, tailnetIp (tailscale ip -4) and sdkVersion", async () => {
760
- const osImpl = fakeOs({ cpus: 12, totalmem: 24 * 1024 ** 3 });
761
- osImpl.hostname = () => "ethans-mac-mini.local";
762
- const inv = await collectInventory({
763
- os: osImpl,
764
- execFile: fakeExecFile({ "/usr/sbin/system_profiler": SP_HARDWARE, tailscale: "100.95.116.68\n" }),
765
- sdkVersion: "2.12.0",
766
- });
767
- assert.equal(inv.hostname, "ethans-mac-mini.local");
768
- assert.equal(inv.tailnetIp, "100.95.116.68");
769
- assert.equal(inv.sdkVersion, "2.12.0");
770
- assert.equal(inv.device, "Mac mini M4 Pro");
771
- });
772
-
773
- test("collectInventory falls back to the Tailscale.app binary and is fail-open without any", async () => {
774
- const osImpl = fakeOs({});
775
- osImpl.hostname = () => "h";
776
- const appBin = "/Applications/Tailscale.app/Contents/MacOS/Tailscale";
777
- const viaApp = await collectInventory({
778
- os: osImpl, sdkVersion: "x",
779
- execFile: fakeExecFile({ [appBin]: "100.68.98.127\nfd7a::1\n" }),
780
- inventoryCache: false,
781
- });
782
- assert.equal(viaApp.tailnetIp, "100.68.98.127");
783
- const none = await collectInventory({ os: osImpl, sdkVersion: "x", execFile: fakeExecFile({}), inventoryCache: false });
784
- assert.equal(none.tailnetIp, undefined, "no tailscale → the field drops out, nothing throws");
785
- assert.equal(none.hostname, "h");
786
- });
787
-
788
- test("sdkVersion is read from the installed @cohortapp/agent-sdk package.json, not from the cache", async () => {
789
- const root = mkdtempSync(join(tmpdir(), "inv-sdk-"));
790
- try {
791
- const osImpl = fakeOs({});
792
- osImpl.hostname = () => "h";
793
- const first = await collectInventory({ agentRoot: root, os: osImpl, execFile: fakeExecFile({}), sdkVersion: "2.11.17" });
794
- assert.equal(first.sdkVersion, "2.11.17");
795
- // Same chip/RAM key → the device inventory is served from the cache, but the
796
- // version (which an autoupdate changes without touching the hardware) and
797
- // the tailnet IP are re-read every time.
798
- const second = await collectInventory({ agentRoot: root, os: osImpl, execFile: fakeExecFile({ tailscale: "100.1.1.1\n" }), sdkVersion: "2.12.0" });
799
- assert.equal(second.sdkVersion, "2.12.0");
800
- assert.equal(second.tailnetIp, "100.1.1.1");
801
- assert.equal(second.hostname, "h");
802
- // The real resolver: this checkout IS the SDK, so it reports its own version.
803
- const real = _internals.readSdkVersion();
804
- assert.match(real, /^\d+\.\d+\.\d+/);
805
- } finally { rmSync(root, { recursive: true, force: true }); }
806
- });
807
-
808
- test("sessionLiveness: a heartbeat younger than 90 s is live; older, missing or malformed is not", () => {
809
- const { sessionLiveness } = _internals;
810
- const now = Date.parse("2026-09-08T12:00:00Z");
811
- assert.deepEqual(sessionLiveness({ ts: "2026-09-08T11:59:30Z", pid: 1, sessionId: "s" }, now), { frontDoor: "session", sessionLive: true });
812
- assert.deepEqual(sessionLiveness({ ts: "2026-09-08T11:58:29Z" }, now), { frontDoor: "session", sessionLive: false });
813
- assert.deepEqual(sessionLiveness({ ts: now - 89_000 }, now), { frontDoor: "session", sessionLive: true });
814
- assert.deepEqual(sessionLiveness(null, now), { frontDoor: "daemon", sessionLive: false });
815
- assert.deepEqual(sessionLiveness({ ts: "not a date" }, now), { frontDoor: "session", sessionLive: false });
816
- assert.deepEqual(sessionLiveness({ ts: "2026-09-08T11:59:30Z" }, now, { staleMs: 10_000 }), { frontDoor: "session", sessionLive: false });
817
- });
818
-
819
- test("collectStatus: machine carries frontDoor + sessionLive from state/session/heartbeat.json; session stays null when idle", async () => {
820
- const root = mkdtempSync(join(tmpdir(), "front-door-"));
821
- try {
822
- const now = Date.parse("2026-09-08T12:00:00Z");
823
- const osImpl = fakeOs({});
824
- osImpl.hostname = () => "seat.local";
825
- const base = { agentRoot: root, now, os: osImpl, execFile: fakeExecFile({}), subAgentsRunning: 0, powermetrics: false, disk: false, spend: false };
826
- // No heartbeat file: the daemon is the front door. No work session → null.
827
- const noHb = await collectStatus(base);
828
- assert.equal(noHb.machine.frontDoor, "daemon");
829
- assert.equal(noHb.machine.sessionLive, false);
830
- assert.equal(noHb.session, null, "an idle seat beats session: null (hq's strict session schema; activity.mjs reads null as 'no work session')");
831
- // Fresh heartbeat → live main session.
832
- mkdirSync(join(root, "state", "session"), { recursive: true });
833
- writeFileSync(join(root, "state", "session", "heartbeat.json"), JSON.stringify({ pid: 4242, sessionId: "abc", name: "ethan-main", ts: "2026-09-08T11:59:50Z" }));
834
- const live = await collectStatus(base);
835
- assert.equal(live.machine.frontDoor, "session");
836
- assert.equal(live.machine.sessionLive, true);
837
- assert.equal(live.session, null);
838
- // Stale (> 90 s) → not live, but the front door is still the session job.
839
- writeFileSync(join(root, "state", "session", "heartbeat.json"), JSON.stringify({ pid: 4242, ts: "2026-09-08T11:50:00Z" }));
840
- const stale = await collectStatus(base);
841
- assert.equal(stale.machine.frontDoor, "session");
842
- assert.equal(stale.machine.sessionLive, false);
843
- // A running work session keeps EXACTLY the keys hq's strict validator knows.
844
- const busy = await collectStatus({ ...base, sessions: [{ sessionId: "s1", kind: "interactive", intent: "session: inbox", model: "sonnet", lastBeat: "2026-09-08T11:59:55Z" }] });
845
- assert.equal(busy.session.id, "s1");
846
- assert.equal(busy.session.taskClass, "inbox");
847
- const allowed = new Set(["id", "taskClass", "model"]);
848
- for (const k of Object.keys(busy.session)) assert.ok(allowed.has(k), `session.${k} would be rejected by hq's strict presence.beat schema`);
849
- assert.equal(busy.machine.frontDoor, "session");
850
- // Inventory identity rides machine.
851
- assert.equal(typeof live.machine.hostname, "string");
852
- assert.match(live.machine.sdkVersion, /^\d+\.\d+\.\d+/);
853
- } finally { rmSync(root, { recursive: true, force: true }); }
854
- });
855
-
856
- test("detectClaudeAuth: in subscription mode the shared probe decides; logs still win for relogin", () => {
857
- const root = mkdtempSync(join(tmpdir(), "auth-probe-"));
858
- try {
859
- writeFileSync(join(root, ".env"), "CLAUDE_CODE_OAUTH_TOKEN=t\nMAESTRO_PREFER_SUBSCRIPTION_AUTH=1\n");
860
- const calls = [];
861
- // An UNVERIFIED "ok" (the CLI merely reports a login — it says loggedIn for
862
- // any token value) must not flip the beat from unknown to ok: that is the
863
- // one state claudeAuth exists to catch. Only a verified ok is an ok.
864
- const probeClaude = (agentRoot) => { calls.push(agentRoot); return { ok: true, mode: "subscription", auth: "ok", verified: false, name: "x" }; };
865
- assert.equal(detectClaudeAuth({ agentRoot: root, now: NOW, probeClaude, env: {} }), "unknown", "reported-not-verified ok + no logs → unknown, as before the probe existed");
866
- assert.deepEqual(calls, [root]);
867
- assert.equal(detectClaudeAuth({ agentRoot: root, now: NOW, env: {}, probeClaude: () => ({ ok: true, auth: "ok", verified: true }) }), "ok");
868
- // The probe says relogin → relogin, even with no logs at all.
869
- assert.equal(detectClaudeAuth({ agentRoot: root, now: NOW, env: {}, probeClaude: () => ({ ok: false, auth: "relogin_required" }) }), "relogin_required");
870
- // Probe unknown → fall back to what the logs say (nothing here → unknown).
871
- assert.equal(detectClaudeAuth({ agentRoot: root, now: NOW, env: {}, probeClaude: () => ({ ok: true, auth: "unknown" }) }), "unknown");
872
- // Clean recent logs + an unverified ok → ok (the logs are the evidence, as before).
873
- mkdirSync(join(root, "logs", "sessions"), { recursive: true });
874
- const p = join(root, "logs", "sessions", "s.log");
875
- writeFileSync(p, "spawn ok, 3 turns, done");
876
- const t = new Date(NOW - 60_000);
877
- utimesSync(p, t, t);
878
- assert.equal(detectClaudeAuth({ agentRoot: root, now: NOW, probeClaude, env: {} }), "ok");
879
- // A recent relogin fingerprint in the logs wins over a probe that says ok.
880
- writeFileSync(p, "… OAuth token has expired …");
881
- utimesSync(p, t, t);
882
- assert.equal(detectClaudeAuth({ agentRoot: root, now: NOW, probeClaude, env: {} }), "relogin_required");
883
- // Not in subscription mode → the probe is never forked from the beat.
884
- writeFileSync(join(root, ".env"), "ANTHROPIC_API_KEY=sk\n");
885
- const before = calls.length;
886
- detectClaudeAuth({ agentRoot: root, now: NOW, probeClaude, env: {} });
887
- assert.equal(calls.length, before);
888
- // A token only in the daemon's ambient env (launchd) is subscription mode too
889
- // (clean logs first — a relogin fingerprint decides before any probe runs).
890
- writeFileSync(p, "spawn ok"); utimesSync(p, t, t);
891
- writeFileSync(join(root, ".env"), "");
892
- detectClaudeAuth({ agentRoot: root, now: NOW, probeClaude, env: { CLAUDE_CODE_OAUTH_TOKEN: "t" } });
893
- assert.equal(calls.length, before + 1);
894
- } finally { rmSync(root, { recursive: true, force: true }); }
895
- });
896
-
897
- // ---------------------------------------------------------------------------
898
- // Last upgrade outcome (WP-M6): machine.upgrade = {at, from, to, ok} from
899
- // state/autoupdate/last.json — fail-open, absent until a first attempt.
900
- // ---------------------------------------------------------------------------
901
-
902
- test("upgradeSummary: the four fleet-view fields from autoupdate's last.json; malformed → null", () => {
903
- const { upgradeSummary } = _internals;
904
- assert.deepEqual(
905
- upgradeSummary({ from: "2.11.17", to: "2.12.0", at: "2026-09-08T12:00:00Z", ok: true, healthy: true, reason: "" }),
906
- { at: "2026-09-08T12:00:00Z", from: "2.11.17", to: "2.12.0", ok: true },
907
- );
908
- assert.deepEqual(
909
- upgradeSummary({ from: "2.11.17", to: "2.12.0", at: "2026-09-08T12:00:00Z", ok: false, healthy: true, reason: "unhealthy-rolled-back" }),
910
- { at: "2026-09-08T12:00:00Z", from: "2.11.17", to: "2.12.0", ok: false },
911
- );
912
- assert.equal(upgradeSummary(null), null);
913
- assert.equal(upgradeSummary({ ok: true }), null, "no at/to → nothing to report");
914
- assert.equal(upgradeSummary("2.12.0"), null);
915
- });
916
-
917
- test("collectStatus: machine.upgrade rides the beat when state/autoupdate/last.json exists, and is absent (not null) when it does not or is corrupt", async () => {
918
- const root = mkdtempSync(join(tmpdir(), "tel-upgrade-"));
919
- try {
920
- const base = { agentRoot: root, os: fakeOs(), execFile: fakeExecFile({}), powermetrics: false, disk: false, spend: false, inventory: false, tailnet: false, sdkVersion: "2.12.0", withAlerts: false, now: Date.parse("2026-09-08T12:00:00Z") };
921
- const none = await collectStatus(base);
922
- assert.ok(!("upgrade" in none.machine), "absent until a first autoupdate attempt");
923
-
924
- mkdirSync(join(root, "state", "autoupdate"), { recursive: true });
925
- writeFileSync(join(root, "state", "autoupdate", "last.json"), JSON.stringify({ from: "2.11.17", to: "2.12.0", at: "2026-09-08T11:30:00Z", ok: true, healthy: true, reason: "" }));
926
- const some = await collectStatus(base);
927
- assert.deepEqual(some.machine.upgrade, { at: "2026-09-08T11:30:00Z", from: "2.11.17", to: "2.12.0", ok: true });
928
- assert.equal(some.session, null, "nothing new lands on the strictly-validated session object");
929
-
930
- writeFileSync(join(root, "state", "autoupdate", "last.json"), "{not json");
931
- const corrupt = await collectStatus(base);
932
- assert.ok(!("upgrade" in corrupt.machine));
933
-
934
- const injected = await collectStatus({ ...base, autoupdateLast: { from: "1", to: "2", at: "2026-09-08T00:00:00Z", ok: false } });
935
- assert.equal(injected.machine.upgrade.ok, false);
936
- } finally { rmSync(root, { recursive: true, force: true }); }
937
- });
938
-
939
- // ---------------------------------------------------------------------------
940
- // machine.sessionNote (WP-M8) — WHY the front door is not live
941
- // ---------------------------------------------------------------------------
942
-
943
- const M8_NOW = Date.parse("2026-09-11T12:00:00Z");
944
- /** The record scripts/session/supervisor.mjs writes via first-run#attentionRecord. */
945
- const attention = (over = {}) => ({
946
- reason: "no-heartbeat",
947
- since: "2026-09-11T11:57:00.000Z",
948
- runMs: 180_000,
949
- attach: "tmux attach -t =maestro-maya",
950
- hint: "The session has been up 180 s without a heartbeat — it is probably waiting on a first-run dialog or a permission prompt. Attach with `tmux attach -t =maestro-maya` and answer it.",
951
- ...over,
952
- });
953
-
954
- test("sessionNote: a live front door says nothing at all", () => {
955
- const { sessionNote } = _internals;
956
- assert.equal(sessionNote({ heartbeat: { ts: M8_NOW - 1_000 }, now: M8_NOW }), null);
957
- assert.equal(sessionNote({ heartbeat: { ts: M8_NOW - 89_000 }, jobInstalled: false, attention: attention(), now: M8_NOW }), null,
958
- "liveness wins over every other input — the note exists only to explain a door that is NOT open");
959
- // A shorter staleness bound makes the same beat stale, and then it does speak.
960
- assert.equal(sessionNote({ heartbeat: { ts: M8_NOW - 30_000 }, jobInstalled: true, now: M8_NOW, staleMs: 10_000 }).reason, "heartbeat-stale");
961
- });
962
-
963
- test("sessionNote: job-absent — no plist for this seat, and only ever from a hard false", () => {
964
- const { sessionNote } = _internals;
965
- const n = sessionNote({ jobInstalled: false, label: "ai.maestro.maya-session", now: M8_NOW });
966
- assert.equal(n.reason, "job-absent");
967
- assert.match(n.detail, /ai\.maestro\.maya-session/);
968
- assert.equal(n.since, undefined, "there is no moment to date — the job was never there");
969
- // It outranks a stale attention record left behind by a previous install.
970
- assert.equal(sessionNote({ jobInstalled: false, attention: attention(), heartbeat: { ts: M8_NOW - 600_000 }, now: M8_NOW }).reason, "job-absent");
971
- // UNKNOWN is not absent: a home we could not read must never accuse the seat.
972
- assert.equal(sessionNote({ jobInstalled: null, now: M8_NOW }).reason, "never-beaten");
973
- assert.equal(sessionNote({ now: M8_NOW }).reason, "never-beaten");
974
- // No label still yields a usable line.
975
- assert.match(sessionNote({ jobInstalled: false, now: M8_NOW }).detail, /main-session launchd job/);
976
- // …and it does not call a deliberate daemon-front-door seat broken: this seat
977
- // reports job-absent on EVERY beat forever, so the line states a configuration.
978
- assert.match(n.detail, /the daemon --print lane is the front door/);
979
- assert.match(n.detail, /by configuration, or the session job was never installed/);
980
- });
981
-
982
- test("sessionNote: awaiting-input passes the supervisor's own reason, since and hint through", () => {
983
- const { sessionNote } = _internals;
984
- const n = sessionNote({ jobInstalled: true, attention: attention(), now: M8_NOW });
985
- assert.equal(n.reason, "awaiting-input", "the beat's vocabulary — the record's own reason rides in detail");
986
- assert.equal(n.since, "2026-09-11T11:57:00.000Z", "attentionRecord.since, normalised to canonical ISO");
987
- assert.match(n.detail, /^up, never beaten: /, "the record's own reason survives, rendered out of the watchdog's vocabulary");
988
- assert.match(n.detail, /tmux attach -t =maestro-maya/, "the attach command is the actionable part");
989
- assert.ok(n.detail.length <= 200);
990
- // screen seats say screen.
991
- assert.match(sessionNote({ attention: attention({ attach: "screen -r maestro-isla", hint: "… Attach with `screen -r maestro-isla` and answer it." }), now: M8_NOW }).detail, /screen -r maestro-isla/);
992
- // It outranks a stale heartbeat: "waiting on a human" is the actionable fact.
993
- assert.equal(sessionNote({ jobInstalled: true, attention: attention(), heartbeat: { ts: M8_NOW - 600_000 }, now: M8_NOW }).reason, "awaiting-input");
994
- });
995
-
996
- test("sessionNote: a heartbeat LATER than the attention record supersedes it", () => {
997
- const { sessionNote } = _internals;
998
- // supervisor.mjs clears attention.json only at the START of the next launch —
999
- // never when the human answers the dialog — so the record outlives its truth.
1000
- // Answered at 11:57, beat until 3 days later, then the session died.
1001
- const beat = M8_NOW + 3 * 86_400_000;
1002
- const n = sessionNote({ jobInstalled: true, attention: attention(), heartbeat: { ts: beat }, now: beat + 86_400_000 });
1003
- assert.equal(n.reason, "heartbeat-stale", "a days-old attach command for a mux session that ended is worse than no note");
1004
- assert.equal(n.since, new Date(beat).toISOString());
1005
- // A record with no orderable `since` loses to a heartbeat that has one.
1006
- assert.equal(sessionNote({ jobInstalled: true, attention: attention({ since: "not a date" }), heartbeat: { ts: M8_NOW - 600_000 }, now: M8_NOW }).reason, "heartbeat-stale");
1007
- // But the record still wins while it is the LATER fact — the ordinary case:
1008
- // the session came up, never beat, and the watchdog flagged it.
1009
- assert.equal(sessionNote({ jobInstalled: true, attention: attention(), heartbeat: { ts: M8_NOW - 600_000 }, now: M8_NOW }).reason, "awaiting-input");
1010
- // …and with no heartbeat at all there is nothing to supersede it.
1011
- assert.equal(sessionNote({ jobInstalled: true, attention: attention(), heartbeat: { pid: 7 }, now: M8_NOW }).reason, "awaiting-input");
1012
- });
1013
-
1014
- test("sessionNote: `since` is re-formatted from a parsed instant, never passed through", () => {
1015
- const { sessionNote } = _internals;
1016
- // V8's legacy date parser accepts trailing prose, so a bare Date.parse check
1017
- // let a home path and a token-shaped run ride the one field the detail
1018
- // sanitiser does not cover. `since` is canonical ISO or it is absent.
1019
- const evil = "Thu, 01 Jan 2026 00:00:00 GMT (token sk-ant-supersecretvalue1234567890 /Users/maya/x)";
1020
- assert.ok(Number.isFinite(Date.parse(evil)), "the hostile string really does parse — that is the hazard");
1021
- const n = sessionNote({ jobInstalled: true, attention: attention({ since: evil }), now: M8_NOW });
1022
- assert.equal(n.since, "2026-01-01T00:00:00.000Z");
1023
- assert.ok(!/Users|sk-ant/.test(n.since));
1024
- // Non-ISO spellings are normalised rather than echoed.
1025
- assert.equal(sessionNote({ jobInstalled: true, attention: attention({ since: "2026-09-11T11:57:00+00:00" }), now: M8_NOW }).since, "2026-09-11T11:57:00.000Z");
1026
- });
1027
-
1028
- test("sessionNote: the attention record's own vocabulary is rendered, not collided with", () => {
1029
- const { sessionNote } = _internals;
1030
- // first-run#heartbeatSilence's "no-heartbeat" means "up, never beaten" — the
1031
- // opposite of what a beat reason of nearly that spelling would mean. Rendering
1032
- // it keeps `{reason, detail}` from reading as a contradiction side by side.
1033
- assert.match(sessionNote({ jobInstalled: true, attention: attention(), now: M8_NOW }).detail, /^up, never beaten: /);
1034
- assert.match(sessionNote({ jobInstalled: true, attention: attention({ reason: "stale-heartbeat" }), now: M8_NOW }).detail, /^no beat since this launch: /);
1035
- // A word we do not know passes through verbatim rather than being invented.
1036
- assert.match(sessionNote({ jobInstalled: true, attention: attention({ reason: "wedged-mcp" }), now: M8_NOW }).detail, /^wedged-mcp: /);
1037
- // And no beat reason collides with a watchdog reason any more.
1038
- const beatReasons = new Set(["job-absent", "awaiting-input", "heartbeat-stale", "heartbeat-unreadable", "never-beaten"]);
1039
- for (const r of ["no-heartbeat", "stale-heartbeat", "within-grace", "beating"]) assert.ok(!beatReasons.has(r), `${r} is the watchdog's word, not the beat's`);
1040
- });
1041
-
1042
- test("sessionNote: heartbeat-stale carries the last beat and its age", () => {
1043
- const { sessionNote } = _internals;
1044
- const n = sessionNote({ jobInstalled: true, heartbeat: { ts: M8_NOW - 600_000, pid: 4242 }, now: M8_NOW });
1045
- assert.deepEqual(n, { reason: "heartbeat-stale", since: "2026-09-11T11:50:00.000Z", detail: "no beat for 600 s" });
1046
- // ISO timestamps are the shape the feed actually writes.
1047
- assert.equal(sessionNote({ jobInstalled: true, heartbeat: { ts: "2026-09-11T11:50:00Z" }, now: M8_NOW }).since, "2026-09-11T11:50:00.000Z");
1048
- // A beat from the future is clock skew, not a negative age.
1049
- assert.equal(sessionNote({ jobInstalled: true, heartbeat: { ts: M8_NOW + 600_000 }, now: M8_NOW }).detail, "last beat is in the future (clock skew)");
1050
- });
1051
-
1052
- test("sessionNote: every malformed input degrades to a coarser note or to null, and nothing throws", () => {
1053
- const { sessionNote } = _internals;
1054
- // No `now` at all → no note (a note with no clock behind it would be a guess).
1055
- assert.equal(sessionNote({ heartbeat: { ts: 1 } }), null);
1056
- assert.equal(sessionNote({ now: NaN, jobInstalled: false }), null);
1057
- assert.equal(sessionNote(), null);
1058
- assert.equal(sessionNote(null), null);
1059
- assert.equal(sessionNote("nonsense"), null);
1060
- // Absent files (the reader's null) → the installed-but-silent note.
1061
- assert.deepEqual(sessionNote({ heartbeat: null, attention: null, jobInstalled: true, now: M8_NOW }),
1062
- { reason: "never-beaten", detail: "the session job is installed but has never written a heartbeat" });
1063
- // …and when job presence was never established, the note says THAT rather
1064
- // than affirming an installation nobody checked.
1065
- assert.deepEqual(sessionNote({ jobInstalled: null, now: M8_NOW }),
1066
- { reason: "never-beaten", detail: "no heartbeat from the session job, and its presence on this seat could not be checked" });
1067
- assert.equal(sessionNote({ now: M8_NOW }).detail, "no heartbeat from the session job, and its presence on this seat could not be checked");
1068
- // A parsed-but-wrong SHAPE must not throw.
1069
- assert.equal(sessionNote({ heartbeat: "{}", attention: "{}", now: M8_NOW }).reason, "never-beaten");
1070
- assert.equal(sessionNote({ heartbeat: [], attention: [], jobInstalled: true, now: M8_NOW }).reason, "never-beaten", "arrays are not records");
1071
- // A heartbeat file that is THERE but says nothing usable is its own fault —
1072
- // the job may well have beaten, so it is never called "never beaten".
1073
- assert.deepEqual(sessionNote({ heartbeat: { pid: 7 }, jobInstalled: true, now: M8_NOW }),
1074
- { reason: "heartbeat-unreadable", detail: "the heartbeat file is present but carries no readable timestamp" });
1075
- assert.equal(sessionNote({ heartbeat: { ts: "not a date" }, jobInstalled: true, now: M8_NOW }).reason, "heartbeat-unreadable");
1076
- assert.deepEqual(sessionNote({ heartbeatUnreadable: true, jobInstalled: true, now: M8_NOW }),
1077
- { reason: "heartbeat-unreadable", detail: "the heartbeat file is present but could not be parsed" });
1078
- // An attention record missing every field is still the right REASON — it is
1079
- // the file's existence that says "the supervisor flagged this seat".
1080
- assert.deepEqual(sessionNote({ attention: {}, jobInstalled: true, now: M8_NOW }), { reason: "awaiting-input" });
1081
- assert.deepEqual(sessionNote({ attention: { since: 12345, reason: 7, hint: null }, jobInstalled: true, now: M8_NOW }), { reason: "awaiting-input" });
1082
- assert.equal(sessionNote({ attention: { since: "not a date", reason: "no-heartbeat" }, jobInstalled: true, now: M8_NOW }).since, undefined);
1083
- });
1084
-
1085
- test("sanitizeNoteDetail: no home paths, no ids, no control characters, never past the cap", () => {
1086
- const { sanitizeNoteDetail } = _internals;
1087
- assert.equal(sanitizeNoteDetail("attach with `tmux attach -t =maestro-maya`"), "attach with `tmux attach -t =maestro-maya`");
1088
- // Home directories name a person and leak the seat's layout.
1089
- assert.equal(sanitizeNoteDetail("blocked in /Users/maya/maya-ai/state"), "blocked in <path>");
1090
- // A home directory with a space in it is ordinary on macOS, and its tail
1091
- // carries the person's surname — the match may not stop at the whitespace.
1092
- assert.equal(sanitizeNoteDetail("blocked in /Users/olivia chen/maya-ai/state/session"), "blocked in <path>");
1093
- assert.equal(sanitizeNoteDetail("blocked in /Users/Olivia Chen/Library/Application Support/x"), "blocked in <path>");
1094
- // …but ordinary prose after a path is not swallowed with it.
1095
- assert.equal(sanitizeNoteDetail("blocked in /Users/maya and then retry"), "blocked in <path> and then retry");
1096
- assert.equal(sanitizeNoteDetail("attach to /Users/maya/x and answer it"), "attach to <path> and answer it");
1097
- assert.equal(sanitizeNoteDetail("blocked in ~/maya-ai and /home/maya/x and /private/var/folders/t/x"), "blocked in <path> and <path> and <path>");
1098
- // Session ids and token-shaped runs.
1099
- assert.equal(sanitizeNoteDetail("resume 3f2a1c94-7b0e-4a11-9c3d-0b7e2f8a6d51 failed"), "resume <redacted> failed");
1100
- assert.equal(sanitizeNoteDetail("token sk-ant-0123456789abcdef rejected"), "token <redacted> rejected");
1101
- // Newlines and control characters collapse; the note is one line.
1102
- assert.equal(sanitizeNoteDetail("a\nb\tc\r\nd"), "a b c d");
1103
- // The cap holds, with an ellipsis so a reader knows it was cut.
1104
- const long = sanitizeNoteDetail("lorem ipsum ".repeat(50));
1105
- assert.equal(long.length, 200);
1106
- assert.ok(long.endsWith("…"));
1107
- assert.ok(sanitizeNoteDetail("ab ".repeat(20), 10).length <= 10, "the cap is a ceiling, never exceeded (a word boundary can land under it)");
1108
- // Nothing to say → "", and the caller then omits the field entirely.
1109
- assert.equal(sanitizeNoteDetail(""), "");
1110
- assert.equal(sanitizeNoteDetail(null), "");
1111
- assert.equal(sanitizeNoteDetail(undefined), "");
1112
- assert.equal(sanitizeNoteDetail(42), "");
1113
- assert.equal(sanitizeNoteDetail(" \n "), "");
1114
- // A hint long enough to overflow is truncated, not dropped.
1115
- const hint = sanitizeNoteDetail(`no-heartbeat: The session has been up 1800 s without a heartbeat — it is probably waiting on a first-run dialog or a permission prompt. Attach with \`tmux attach -t =maestro-maya\` and answer it. ${"tail ".repeat(20)}`);
1116
- assert.ok(hint.length <= 200 && hint.endsWith("…"), "an over-long hint is truncated, not dropped");
1117
- });
1118
-
1119
- test("sessionJobInstalled: a readdir of ~/Library/LaunchAgents, and 'unknown' whenever it cannot be read", () => {
1120
- const { sessionJobInstalled, sessionJobLabel } = _internals;
1121
- const label = "ai.maestro.maya-session";
1122
- assert.equal(sessionJobInstalled({ launchAgents: ["ai.maestro.maya.plist", "ai.maestro.maya-session.plist"] }, label), true);
1123
- assert.equal(sessionJobInstalled({ launchAgents: ["ai.maestro.maya-session"] }, label), true, "bare labels count too");
1124
- assert.equal(sessionJobInstalled({ launchAgents: ["ai.maestro.maya.plist"] }, label), false);
1125
- assert.equal(sessionJobInstalled({ launchAgents: [] }, label), false);
1126
- // Unknowns: no label, an unreadable home, a home that is not there.
1127
- assert.equal(sessionJobInstalled({ launchAgents: ["ai.maestro.maya-session.plist"] }, ""), null);
1128
- assert.equal(sessionJobInstalled({ os: { homedir: () => "" } }, label), null);
1129
- assert.equal(sessionJobInstalled({ os: { homedir: () => { throw new Error("nope"); } } }, label), null);
1130
- assert.equal(sessionJobInstalled({ os: { homedir: () => join(tmpdir(), "no-such-home-xyz") } }, label), null);
1131
- // An explicit override wins (the daemon may already know).
1132
- assert.equal(sessionJobInstalled({ sessionJobInstalled: true, launchAgents: [] }, label), true);
1133
- // A case difference is not an absence: agentFirstName leaves its directory
1134
- // fallback as-is (`~/Maya-ai` -> `Maya`), and the volume is case-insensitive.
1135
- assert.equal(sessionJobInstalled({ launchAgents: ["ai.maestro.Maya-session.plist"] }, "ai.maestro.maya-session"), true);
1136
- assert.equal(sessionJobInstalled({ launchAgents: ["ai.maestro.maya-session.plist"] }, "ai.maestro.Maya-session"), true);
1137
- // The label matches what generate-plists.sh and `maestro session` spell —
1138
- // including its casing, which the generator does not lower-case either.
1139
- assert.equal(sessionJobLabel("/x/maya-ai", "maya"), "ai.maestro.maya-session");
1140
- assert.equal(sessionJobLabel("/x/Maya-ai", "Maya"), "ai.maestro.Maya-session");
1141
- assert.equal(sessionJobLabel("", ""), "");
1142
- });
1143
-
1144
- test("collectStatus: machine.sessionNote appears ONLY when the front door is down, and rides machine (never session)", async () => {
1145
- const root = mkdtempSync(join(tmpdir(), "session-note-"));
1146
- try {
1147
- const now = Date.parse("2026-09-11T12:00:00Z");
1148
- const osImpl = fakeOs({});
1149
- osImpl.hostname = () => "seat.local";
1150
- const base = {
1151
- agentRoot: root, now, os: osImpl, execFile: fakeExecFile({}), subAgentsRunning: 0,
1152
- powermetrics: false, disk: false, spend: false, agentFirst: "maya", launchAgents: [],
1153
- };
1154
- const sess = join(root, "state", "session");
1155
- mkdirSync(sess, { recursive: true });
1156
-
1157
- // 1. No job on the seat — the case A010/A016 could not tell apart.
1158
- const absent = await collectStatus(base);
1159
- assert.equal(absent.machine.sessionLive, false);
1160
- assert.equal(absent.machine.sessionNote.reason, "job-absent");
1161
- assert.match(absent.machine.sessionNote.detail, /ai\.maestro\.maya-session/);
1162
- assert.equal(absent.session, null, "the note NEVER touches session — hq validates that object strictly");
1163
- assert.ok(!("sessionNote" in (absent.session || {})));
1164
-
1165
- // 2. Job installed, session up, waiting on a first-run dialog.
1166
- const installed = { ...base, launchAgents: ["ai.maestro.maya-session.plist"] };
1167
- writeFileSync(join(sess, "attention.json"), JSON.stringify(attention()));
1168
- const waiting = await collectStatus(installed);
1169
- assert.equal(waiting.machine.sessionNote.reason, "awaiting-input");
1170
- assert.equal(waiting.machine.sessionNote.since, "2026-09-11T11:57:00.000Z");
1171
- assert.match(waiting.machine.sessionNote.detail, /tmux attach -t =maestro-maya/);
1172
-
1173
- // 3. The supervisor cleared attention.json and the beat then went stale.
1174
- rmSync(join(sess, "attention.json"));
1175
- writeFileSync(join(sess, "heartbeat.json"), JSON.stringify({ pid: 4242, ts: "2026-09-11T11:50:00Z" }));
1176
- const stale = await collectStatus(installed);
1177
- assert.equal(stale.machine.frontDoor, "session");
1178
- assert.deepEqual(stale.machine.sessionNote, { reason: "heartbeat-stale", since: "2026-09-11T11:50:00.000Z", detail: "no beat for 600 s" });
1179
-
1180
- // 4. A live front door carries NO note — the key is absent, not null.
1181
- writeFileSync(join(sess, "heartbeat.json"), JSON.stringify({ pid: 4242, ts: "2026-09-11T11:59:50Z" }));
1182
- const live = await collectStatus(installed);
1183
- assert.equal(live.machine.sessionLive, true);
1184
- assert.ok(!("sessionNote" in live.machine), "a healthy seat must not pay for a field it has nothing to say in");
1185
-
1186
- // 5. Corrupt state files never break the beat — and a heartbeat file that
1187
- // is THERE and will not parse is reported as such, not as "never beaten".
1188
- writeFileSync(join(sess, "heartbeat.json"), "{not json");
1189
- writeFileSync(join(sess, "attention.json"), "{not json");
1190
- const corrupt = await collectStatus(installed);
1191
- assert.equal(corrupt.machine.sessionLive, false);
1192
- assert.deepEqual(corrupt.machine.sessionNote,
1193
- { reason: "heartbeat-unreadable", detail: "the heartbeat file is present but could not be parsed" });
1194
- assert.equal(typeof corrupt.ts, "string", "the snapshot is intact");
1195
-
1196
- // 5b. No heartbeat file at all, job installed → never-beaten.
1197
- rmSync(join(sess, "heartbeat.json"));
1198
- rmSync(join(sess, "attention.json"));
1199
- const silent = await collectStatus(installed);
1200
- assert.deepEqual(silent.machine.sessionNote,
1201
- { reason: "never-beaten", detail: "the session job is installed but has never written a heartbeat" });
1202
-
1203
- // 6. The whole note is a best-effort extra: a throwing os.homedir drops the
1204
- // launchd read, never the beat — and an unknown job presence is reported
1205
- // as neither an absence nor a presence.
1206
- const hostile = await collectStatus({ ...base, launchAgents: undefined, os: { ...osImpl, homedir: () => { throw new Error("no home"); } } });
1207
- assert.equal(hostile.machine.sessionNote.reason, "never-beaten", "unknown job presence is not an absence");
1208
- assert.equal(hostile.machine.sessionNote.detail, "no heartbeat from the session job, and its presence on this seat could not be checked",
1209
- "…and it is not reported as a presence either");
1210
- assert.equal(hostile.machine.sessionLive, false);
1211
-
1212
- // 7. The emitted shape is exactly {reason, since?, detail?} — nothing else,
1213
- // and nothing unbounded.
1214
- for (const s of [absent, waiting, stale, corrupt, silent, hostile]) {
1215
- const note = s.machine.sessionNote;
1216
- assert.equal(typeof note.reason, "string");
1217
- for (const k of Object.keys(note)) assert.ok(["reason", "since", "detail"].includes(k), `sessionNote.${k} is not in the contract`);
1218
- if (note.since !== undefined) assert.ok(Number.isFinite(Date.parse(note.since)), "since is ISO 8601");
1219
- if (note.detail !== undefined) assert.ok(note.detail.length <= 200 && !/\/Users\//.test(note.detail));
1220
- }
1221
- } finally { rmSync(root, { recursive: true, force: true }); }
1222
- });
1223
-
1224
- /**
1225
- * The front-door signal must not flicker while the session is running.
1226
- *
1227
- * This function and `lib/session/liveness.mjs#heartbeatLiveness` answer one
1228
- * question — "is the main session alive" — and they used to disagree: the
1229
- * runtime accepted a heartbeat up to `staleMs` either side of now, this one
1230
- * demanded `age >= 0`. So a heartbeat written microseconds before the wall
1231
- * clock stepped back read as future-dated and flipped the seat to DOWN, while
1232
- * the session carried on. A039 did exactly that across consecutive beats on
1233
- * 2026-09-10/11 with its session healthy and its clock within 30 s of the
1234
- * server. Symmetric now, and pinned here because the asymmetry was invisible:
1235
- * every test used a past-dated heartbeat.
1236
- */
1237
- test("sessionLiveness tolerates clock skew symmetrically, and reports a far-future beat as not live", () => {
1238
- const now = Date.parse("2026-09-11T12:00:00Z");
1239
- const at = (ms) => ({ ts: new Date(now - ms).toISOString() });
1240
-
1241
- // Fresh, and a beat from the recent past: live, as before.
1242
- assert.equal(sessionLiveness(at(0), now).sessionLive, true);
1243
- assert.equal(sessionLiveness(at(30_000), now).sessionLive, true);
1244
-
1245
- // THE REGRESSION: a beat dated slightly in the FUTURE is the clock moving,
1246
- // not the session dying. Both of these used to return false.
1247
- assert.equal(sessionLiveness(at(-1), now).sessionLive, true, "1 ms ahead is not a dead session");
1248
- assert.equal(sessionLiveness(at(-30_000), now).sessionLive, true, "30 s ahead is ordinary clock motion");
1249
-
1250
- // The window is symmetric and OPEN at both edges — the runtime's rule.
1251
- assert.equal(sessionLiveness(at(SESSION_STALE_MS - 1), now).sessionLive, true);
1252
- assert.equal(sessionLiveness(at(-(SESSION_STALE_MS - 1)), now).sessionLive, true);
1253
- assert.equal(sessionLiveness(at(SESSION_STALE_MS), now).sessionLive, false, "too old");
1254
- assert.equal(sessionLiveness(at(-SESSION_STALE_MS), now).sessionLive, false, "too far ahead to believe");
1255
-
1256
- // The front door is still named in every case a heartbeat exists.
1257
- assert.equal(sessionLiveness(at(10 * SESSION_STALE_MS), now).frontDoor, "session");
1258
- // No heartbeat at all remains the daemon's lane.
1259
- assert.deepEqual(sessionLiveness(null, now), { frontDoor: "daemon", sessionLive: false });
1260
- });
1261
-
1262
- test("sessionLiveness agrees with the session runtime's own window", async () => {
1263
- const { heartbeatLiveness, STALE_MS } = await import("../session/liveness.mjs");
1264
- assert.equal(STALE_MS, SESSION_STALE_MS, "one window, two readers");
1265
- const now = Date.parse("2026-09-11T12:00:00Z");
1266
- for (const offset of [0, 1_000, -1_000, 45_000, -45_000, STALE_MS, -STALE_MS, STALE_MS + 1, -(STALE_MS + 1)]) {
1267
- const hb = { ts: new Date(now - offset).toISOString(), pid: 0 };
1268
- assert.equal(
1269
- sessionLiveness(hb, now).sessionLive,
1270
- heartbeatLiveness(hb, { now }).live,
1271
- `the two readers disagree at offset ${offset} ms`
1272
- );
1273
- }
1274
- });