@cohortapp/agent-sdk 2.17.0 → 2.18.5

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