@cohortapp/agent-sdk 2.16.0 → 2.18.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (529) hide show
  1. package/.claude/settings.json +18 -0
  2. package/.env.example +23 -7
  3. package/README.md +1 -0
  4. package/bin/maestro.mjs +62 -0
  5. package/docs/guides/billing-console-keys.md +60 -0
  6. package/docs/guides/front-door-session.md +54 -9
  7. package/docs/guides/mac-mini.md +20 -25
  8. package/docs/guides/poller-daemon-setup.md +4 -1
  9. package/docs/guides/setup-wizard.md +1 -1
  10. package/docs/runbooks/fleet-rollout.md +156 -0
  11. package/docs/runbooks/mac-mini-bootstrap.md +12 -14
  12. package/lib/action-executor.js +19 -3
  13. package/lib/budget-guard.mjs +279 -3
  14. package/lib/channels/base-adapter.mjs +3 -1
  15. package/lib/channels/contract.mjs +2 -1
  16. package/lib/channels/inbox-item.mjs +8 -0
  17. package/lib/claude-bin.mjs +5 -6
  18. package/lib/cli/doctor-checks.mjs +141 -10
  19. package/lib/cli/global-setup-extras.mjs +5 -1
  20. package/lib/cli/inbox.mjs +100 -15
  21. package/lib/cli/seat-auth.mjs +463 -0
  22. package/lib/cli/session.mjs +80 -12
  23. package/lib/collective/capture.mjs +8 -6
  24. package/lib/collective/global-config.mjs +63 -1
  25. package/lib/collective/presence.mjs +142 -5
  26. package/lib/comms/send-gate.mjs +559 -1
  27. package/lib/context/budget.mjs +327 -0
  28. package/lib/context/history-scope.mjs +138 -0
  29. package/lib/diagnostics/alerts.mjs +49 -0
  30. package/lib/diagnostics/cadence-output-freshness.mjs +288 -0
  31. package/lib/engine/agents/definitions.mjs +343 -0
  32. package/lib/engine/agents/persist.mjs +275 -0
  33. package/lib/engine/agents/runtime.mjs +748 -0
  34. package/lib/engine/agents/usage.mjs +95 -0
  35. package/lib/engine/auth-status.mjs +139 -0
  36. package/lib/engine/budget.mjs +194 -0
  37. package/lib/engine/cli.mjs +1204 -0
  38. package/lib/engine/commands/index.mjs +269 -0
  39. package/lib/engine/context/budget.mjs +219 -0
  40. package/lib/engine/context/cache.mjs +125 -0
  41. package/lib/engine/context/child-env.mjs +215 -0
  42. package/lib/engine/context/compaction.mjs +342 -0
  43. package/lib/engine/context/images.mjs +90 -0
  44. package/lib/engine/context/instructions.mjs +327 -0
  45. package/lib/engine/context/lazy-instructions.mjs +169 -0
  46. package/lib/engine/context/manager.mjs +182 -0
  47. package/lib/engine/context/real-path.mjs +91 -0
  48. package/lib/engine/context/secret-values.mjs +163 -0
  49. package/lib/engine/context/settings.mjs +274 -0
  50. package/lib/engine/context/stream-input.mjs +159 -0
  51. package/lib/engine/guard.mjs +152 -0
  52. package/lib/engine/hooks.mjs +713 -0
  53. package/lib/engine/loop.mjs +560 -0
  54. package/lib/engine/mcp/client.mjs +254 -0
  55. package/lib/engine/mcp/config.mjs +301 -0
  56. package/lib/engine/mcp/http.mjs +201 -0
  57. package/lib/engine/mcp/index.mjs +146 -0
  58. package/lib/engine/mcp/jsonrpc.mjs +147 -0
  59. package/lib/engine/mcp/naming.mjs +66 -0
  60. package/lib/engine/mcp/resources.mjs +89 -0
  61. package/lib/engine/mcp/results.mjs +133 -0
  62. package/lib/engine/mcp/stdio.mjs +137 -0
  63. package/lib/engine/mcp/supervisor.mjs +116 -0
  64. package/lib/engine/messages.mjs +104 -0
  65. package/lib/engine/output/json.mjs +164 -0
  66. package/lib/engine/output/stream-json.mjs +266 -0
  67. package/lib/engine/permissions.mjs +845 -0
  68. package/lib/engine/process-identity.mjs +164 -0
  69. package/lib/engine/process-tree.mjs +551 -0
  70. package/lib/engine/prompt.mjs +60 -0
  71. package/lib/engine/session/store.mjs +299 -0
  72. package/lib/engine/session-runtime/args.mjs +97 -0
  73. package/lib/engine/session-runtime/host.mjs +143 -0
  74. package/lib/engine/session-runtime/inbox.mjs +122 -0
  75. package/lib/engine/session-runtime/notifications.mjs +129 -0
  76. package/lib/engine/session-runtime/registry.mjs +328 -0
  77. package/lib/engine/session-runtime/runner.mjs +344 -0
  78. package/lib/engine/session-runtime/socket.mjs +212 -0
  79. package/lib/engine/session-runtime/wakeup.mjs +115 -0
  80. package/lib/engine/skills/index.mjs +321 -0
  81. package/lib/engine/tools/bash-background.mjs +533 -0
  82. package/lib/engine/tools/bash.mjs +216 -0
  83. package/lib/engine/tools/edit.mjs +97 -0
  84. package/lib/engine/tools/glob.mjs +81 -0
  85. package/lib/engine/tools/grep.mjs +224 -0
  86. package/lib/engine/tools/index.mjs +84 -0
  87. package/lib/engine/tools/list-agents.mjs +32 -0
  88. package/lib/engine/tools/ls.mjs +127 -0
  89. package/lib/engine/tools/monitor.mjs +82 -0
  90. package/lib/engine/tools/notebook-edit.mjs +218 -0
  91. package/lib/engine/tools/read.mjs +103 -0
  92. package/lib/engine/tools/schedule-wakeup.mjs +45 -0
  93. package/lib/engine/tools/schema.mjs +144 -0
  94. package/lib/engine/tools/send-message.mjs +77 -0
  95. package/lib/engine/tools/session.mjs +70 -0
  96. package/lib/engine/tools/todo.mjs +144 -0
  97. package/lib/engine/tools/toolsearch.mjs +217 -0
  98. package/lib/engine/tools/walk.mjs +193 -0
  99. package/lib/engine/tools/web-switch.mjs +31 -0
  100. package/lib/engine/tools/webfetch-html.mjs +387 -0
  101. package/lib/engine/tools/webfetch-net.mjs +340 -0
  102. package/lib/engine/tools/webfetch.mjs +198 -0
  103. package/lib/engine/tools/websearch.mjs +91 -0
  104. package/lib/engine/tools/workflow.mjs +95 -0
  105. package/lib/engine/tools/write.mjs +76 -0
  106. package/lib/engine/tui/line-editor.mjs +137 -0
  107. package/lib/engine/tui/render.mjs +86 -0
  108. package/lib/engine/tui/tui.mjs +274 -0
  109. package/lib/engine/wire/anthropic-messages.mjs +263 -0
  110. package/lib/engine/wire/effort.mjs +36 -0
  111. package/lib/engine/wire/errors.mjs +496 -0
  112. package/lib/engine/wire/http.mjs +441 -0
  113. package/lib/engine/wire/index.mjs +76 -0
  114. package/lib/engine/wire/openai-chat.mjs +332 -0
  115. package/lib/engine/wire/prompt-cache.mjs +79 -0
  116. package/lib/engine/wire/search.mjs +140 -0
  117. package/lib/engine/wire/sse.mjs +114 -0
  118. package/lib/engine/wire/stall.mjs +349 -0
  119. package/lib/engine/wire/token-provider.mjs +175 -0
  120. package/lib/engine/wire/usage.mjs +192 -0
  121. package/lib/engine/workflow/host.mjs +524 -0
  122. package/lib/engine/workflow/journal.mjs +188 -0
  123. package/lib/engine/workflow/json-schema.mjs +171 -0
  124. package/lib/engine/workflow/meta.mjs +329 -0
  125. package/lib/engine/workflow/notifications.mjs +52 -0
  126. package/lib/engine/workflow/runtime.mjs +447 -0
  127. package/lib/engine/workflow/sandbox.mjs +534 -0
  128. package/lib/engine/workflow/worker.mjs +141 -0
  129. package/lib/engine/workflow/worktree.mjs +74 -0
  130. package/lib/execution/disposition.mjs +1 -1
  131. package/lib/execution/intake.mjs +10 -0
  132. package/lib/execution/surface-policy.mjs +15 -0
  133. package/lib/learning/curator.mjs +8 -6
  134. package/lib/learning/reflect.mjs +8 -6
  135. package/lib/model-router/catalog/cohort.yaml +137 -0
  136. package/lib/model-router/catalog.mjs +118 -1
  137. package/lib/model-router/economics.mjs +9 -0
  138. package/lib/model-router/failover.mjs +67 -16
  139. package/lib/model-router/llm-task.mjs +39 -3
  140. package/lib/model-router/resolve.mjs +95 -3
  141. package/lib/model-router/spawn.mjs +46 -47
  142. package/lib/model-router/taxonomy.mjs +126 -4
  143. package/lib/org/cost-sync.mjs +141 -11
  144. package/lib/org/inbound/broadcast.mjs +289 -0
  145. package/lib/org/inbound/collective.mjs +375 -0
  146. package/lib/org/inbound/directedness.mjs +96 -8
  147. package/lib/org/inbound/facts.mjs +82 -4
  148. package/lib/org/inbound/hydrate.mjs +555 -51
  149. package/lib/org/inbound/project.mjs +22 -0
  150. package/lib/org/inbound/surfaces.mjs +14 -0
  151. package/lib/org/llm-token.mjs +879 -0
  152. package/lib/org/mesh.mjs +61 -0
  153. package/lib/org/messaging.mjs +3 -1
  154. package/lib/org/protocol.checksum +1 -1
  155. package/lib/org/protocol.mjs +15 -0
  156. package/lib/org/quota.mjs +520 -0
  157. package/lib/org/tool-surface.mjs +104 -16
  158. package/lib/org/ui-parity.mjs +16 -1
  159. package/lib/org/work-ledger.mjs +37 -6
  160. package/lib/rate-guard.mjs +114 -1
  161. package/lib/resource-governor.mjs +41 -6
  162. package/lib/runtime/adapter.mjs +823 -0
  163. package/lib/runtime/child-env.mjs +191 -0
  164. package/lib/runtime/legacy-shell-guard.mjs +97 -0
  165. package/lib/runtime/seat-engine.mjs +162 -0
  166. package/lib/session/ask-ledger.mjs +271 -0
  167. package/lib/session/current-work.mjs +676 -0
  168. package/lib/session/feed-core.mjs +40 -3
  169. package/lib/session/launch-args.mjs +56 -4
  170. package/lib/session/status-summary.mjs +26 -9
  171. package/lib/session/upgrade-notice.mjs +42 -0
  172. package/lib/setup/claude-probe.mjs +117 -13
  173. package/lib/setup/enrich.mjs +13 -10
  174. package/lib/setup/sections/model.mjs +39 -13
  175. package/lib/telemetry/collect.mjs +208 -9
  176. package/lib/upgrade/ignored-drift.mjs +105 -0
  177. package/lib/voice/post-call-brief.mjs +30 -17
  178. package/package.json +15 -3
  179. package/plugins/maestro-skills/skills/board-work.md +5 -0
  180. package/plugins/maestro-skills/skills/inbound-triage.md +56 -15
  181. package/plugins/maestro-skills/skills/main-session.md +18 -7
  182. package/scripts/ci/check-tarball-fidelity.mjs +126 -2
  183. package/scripts/ci/run-tests.mjs +47 -19
  184. package/scripts/cohort-llm/api-key-helper.mjs +92 -0
  185. package/scripts/collective/hook-runner.mjs +29 -2
  186. package/scripts/continuous-monitor.sh +13 -0
  187. package/scripts/cost/track-claude-usage.mjs +15 -0
  188. package/scripts/daemon/agent-daemon.mjs +408 -20
  189. package/scripts/daemon/assurance.mjs +48 -12
  190. package/scripts/daemon/cadence-consumer.mjs +218 -68
  191. package/scripts/daemon/cadence-handlers.mjs +73 -4
  192. package/scripts/daemon/classifier.mjs +75 -26
  193. package/scripts/daemon/context-compiler.mjs +104 -59
  194. package/scripts/daemon/deliver.mjs +30 -1
  195. package/scripts/daemon/dispatcher.mjs +804 -157
  196. package/scripts/daemon/health.mjs +14 -1
  197. package/scripts/daemon/lib/session-router.mjs +310 -42
  198. package/scripts/daemon/maestro-daemon.mjs +11 -0
  199. package/scripts/daemon/prompt-builder.mjs +121 -12
  200. package/scripts/daemon/responder.mjs +315 -146
  201. package/scripts/daemon/sdk-version.mjs +98 -16
  202. package/scripts/eval/probe-gateway.mjs +635 -0
  203. package/scripts/eval/replay/extract.mjs +270 -0
  204. package/scripts/eval/replay/grade.mjs +260 -0
  205. package/scripts/eval/replay/lib/config.mjs +50 -0
  206. package/scripts/eval/replay/lib/effects.mjs +65 -0
  207. package/scripts/eval/replay/lib/fixture.mjs +188 -0
  208. package/scripts/eval/replay/lib/judge.mjs +72 -0
  209. package/scripts/eval/replay/lib/redact.mjs +136 -0
  210. package/scripts/eval/replay/lib/sandbox.mjs +170 -0
  211. package/scripts/eval/replay/lib/schema-check.mjs +63 -0
  212. package/scripts/eval/replay/lib/transcript.mjs +76 -0
  213. package/scripts/eval/replay/mcp-replay-stub.mjs +101 -0
  214. package/scripts/eval/replay/report.mjs +185 -0
  215. package/scripts/eval/replay/run.mjs +404 -0
  216. package/scripts/fleet/rollout.mjs +1094 -0
  217. package/scripts/hooks/pre-send-audit.sh +36 -245
  218. package/scripts/hooks/pre-write-yaml-validate.mjs +275 -0
  219. package/scripts/hooks/validate-state-yaml.sh +190 -0
  220. package/scripts/huddle/huddle-llm.mjs +361 -0
  221. package/scripts/huddle/huddle-server.mjs +46 -121
  222. package/scripts/local-triggers/autoupdate.sh +448 -78
  223. package/scripts/local-triggers/run-trigger.sh +13 -0
  224. package/scripts/maintenance/pin-integrity.mjs +364 -0
  225. package/scripts/poll-slack-events.sh +41 -9
  226. package/scripts/poller/slack-socket-mode.mjs +28 -3
  227. package/scripts/session/supervisor.mjs +80 -13
  228. package/scripts/spawn-session.sh +13 -0
  229. package/bin/maestro.test.mjs +0 -1574
  230. package/lib/action-executor.test.mjs +0 -871
  231. package/lib/archetype.test.mjs +0 -132
  232. package/lib/assurance/plan-note.test.mjs +0 -234
  233. package/lib/assurance/room-budget.test.mjs +0 -486
  234. package/lib/assurance/tier.test.mjs +0 -174
  235. package/lib/autonomy.test.mjs +0 -66
  236. package/lib/backlog.test.mjs +0 -302
  237. package/lib/backup/policy.test.mjs +0 -305
  238. package/lib/budget-escalate.test.mjs +0 -232
  239. package/lib/budget-guard.envelope.test.mjs +0 -476
  240. package/lib/budget-guard.test.mjs +0 -427
  241. package/lib/cadence-bus-requeue.test.mjs +0 -83
  242. package/lib/cadence-bus-schedule.test.mjs +0 -194
  243. package/lib/cadence-bus.test.mjs +0 -720
  244. package/lib/cadences.test.mjs +0 -230
  245. package/lib/capability/inventory.test.mjs +0 -232
  246. package/lib/capability.test.mjs +0 -78
  247. package/lib/channels/base-adapter.test.mjs +0 -590
  248. package/lib/channels/channels.test.mjs +0 -371
  249. package/lib/channels/contract.test.mjs +0 -162
  250. package/lib/channels/inbox-item.test.mjs +0 -368
  251. package/lib/channels/orgmail/adapter.test.mjs +0 -448
  252. package/lib/channels/pairing.test.mjs +0 -270
  253. package/lib/channels/repeat-suppressor.test.mjs +0 -134
  254. package/lib/channels/slack-adapter.test.mjs +0 -212
  255. package/lib/channels/telegram-adapter.test.mjs +0 -306
  256. package/lib/channels/voice/adapter.test.mjs +0 -278
  257. package/lib/channels/whatsapp/adapter-baileys.test.mjs +0 -359
  258. package/lib/channels/whatsapp/baileys-typing.test.mjs +0 -154
  259. package/lib/charter.test.mjs +0 -89
  260. package/lib/claude-bin.test.mjs +0 -131
  261. package/lib/cli/board.test.mjs +0 -227
  262. package/lib/cli/design.test.mjs +0 -270
  263. package/lib/cli/doctor-checks.test.mjs +0 -336
  264. package/lib/cli/global-setup-extras.test.mjs +0 -462
  265. package/lib/cli/inbox.test.mjs +0 -230
  266. package/lib/cli/session-ack.test.mjs +0 -63
  267. package/lib/cli/session.test.mjs +0 -613
  268. package/lib/collective/capture.test.mjs +0 -121
  269. package/lib/collective/cards.test.mjs +0 -114
  270. package/lib/collective/config.test.mjs +0 -123
  271. package/lib/collective/global-config.test.mjs +0 -220
  272. package/lib/collective/global-skills.test.mjs +0 -126
  273. package/lib/collective/presence.test.mjs +0 -95
  274. package/lib/collective/recall.test.mjs +0 -116
  275. package/lib/collective/vendor-skills.test.mjs +0 -306
  276. package/lib/comms/send-gate.test.mjs +0 -770
  277. package/lib/comms.test.mjs +0 -41
  278. package/lib/cost/ledger-row.test.mjs +0 -183
  279. package/lib/design/design-md.test.mjs +0 -318
  280. package/lib/design/fixtures/DESIGN.golden.md +0 -238
  281. package/lib/design/fixtures/PRODUCT.golden.md +0 -67
  282. package/lib/design/fixtures/foundation.json +0 -133
  283. package/lib/design/refresh-gate.test.mjs +0 -144
  284. package/lib/design/write.test.mjs +0 -241
  285. package/lib/diagnostics/alerts.test.mjs +0 -318
  286. package/lib/diagnostics/backup-freshness.test.mjs +0 -185
  287. package/lib/diagnostics/counters.test.mjs +0 -206
  288. package/lib/diagnostics/events.test.mjs +0 -290
  289. package/lib/diagnostics/otel.test.mjs +0 -196
  290. package/lib/diagnostics/trace.test.mjs +0 -251
  291. package/lib/env-compat.test.mjs +0 -104
  292. package/lib/execution/disposition.test.mjs +0 -553
  293. package/lib/execution/drive.test.mjs +0 -270
  294. package/lib/execution/effects.test.mjs +0 -344
  295. package/lib/execution/intake.test.mjs +0 -389
  296. package/lib/execution/journal.test.mjs +0 -261
  297. package/lib/execution/match.test.mjs +0 -235
  298. package/lib/execution/pipeline.test.mjs +0 -392
  299. package/lib/execution/route.test.mjs +0 -186
  300. package/lib/execution/surface-policy.test.mjs +0 -162
  301. package/lib/fs-atomic.test.mjs +0 -72
  302. package/lib/fs-ownership.test.mjs +0 -158
  303. package/lib/goals/admission.test.mjs +0 -164
  304. package/lib/goals/classify.test.mjs +0 -167
  305. package/lib/goals/collaborate.test.mjs +0 -336
  306. package/lib/goals/gaps.test.mjs +0 -284
  307. package/lib/goals/loop.test.mjs +0 -845
  308. package/lib/hooks/bus.test.mjs +0 -387
  309. package/lib/identity/persona.test.mjs +0 -142
  310. package/lib/kpi-sensors.test.mjs +0 -278
  311. package/lib/kpi.test.mjs +0 -244
  312. package/lib/learning/config.test.mjs +0 -75
  313. package/lib/learning/counters.test.mjs +0 -69
  314. package/lib/learning/curator-consolidate.test.mjs +0 -238
  315. package/lib/learning/curator.test.mjs +0 -106
  316. package/lib/learning/reflect.test.mjs +0 -0
  317. package/lib/learning/session-index.test.mjs +0 -125
  318. package/lib/learning/skill-writer.test.mjs +0 -210
  319. package/lib/mandate/audit.test.mjs +0 -195
  320. package/lib/mandate/contract.test.mjs +0 -185
  321. package/lib/mandate/derive.test.mjs +0 -274
  322. package/lib/mandate/model.test.mjs +0 -164
  323. package/lib/mandate/refresh.test.mjs +0 -389
  324. package/lib/mcp/server.test.mjs +0 -426
  325. package/lib/model-router/auth-profiles.test.mjs +0 -580
  326. package/lib/model-router/catalog.test.mjs +0 -385
  327. package/lib/model-router/economics.test.mjs +0 -438
  328. package/lib/model-router/failover.test.mjs +0 -439
  329. package/lib/model-router/health.test.mjs +0 -338
  330. package/lib/model-router/integration-coverage.test.mjs +0 -831
  331. package/lib/model-router/integration.test.mjs +0 -564
  332. package/lib/model-router/ledger.test.mjs +0 -415
  333. package/lib/model-router/llm-task.test.mjs +0 -392
  334. package/lib/model-router/org-credentials.test.mjs +0 -265
  335. package/lib/model-router/pricing-refresh.test.mjs +0 -286
  336. package/lib/model-router/reconcile.test.mjs +0 -316
  337. package/lib/model-router/repair.test.mjs +0 -180
  338. package/lib/model-router/spawn.test.mjs +0 -446
  339. package/lib/model-router/taxonomy.test.mjs +0 -410
  340. package/lib/model-router.test.mjs +0 -1207
  341. package/lib/org/activity.test.mjs +0 -134
  342. package/lib/org/approvals.test.mjs +0 -216
  343. package/lib/org/awareness.test.mjs +0 -159
  344. package/lib/org/board-mine-cache.test.mjs +0 -53
  345. package/lib/org/board.test.mjs +0 -187
  346. package/lib/org/bootstrap-context.test.mjs +0 -153
  347. package/lib/org/client.test.mjs +0 -1206
  348. package/lib/org/cohort-client.test.mjs +0 -126
  349. package/lib/org/cost-sync.test.mjs +0 -153
  350. package/lib/org/doctor.test.mjs +0 -346
  351. package/lib/org/engagement-ledger.test.mjs +0 -112
  352. package/lib/org/engagement.test.mjs +0 -739
  353. package/lib/org/handoff.test.mjs +0 -269
  354. package/lib/org/inbound/directedness.test.mjs +0 -668
  355. package/lib/org/inbound/facts.test.mjs +0 -471
  356. package/lib/org/inbound/hydrate.test.mjs +0 -453
  357. package/lib/org/inbound/index.test.mjs +0 -429
  358. package/lib/org/inbound/project.test.mjs +0 -287
  359. package/lib/org/integration-tools.test.mjs +0 -160
  360. package/lib/org/keys.test.mjs +0 -92
  361. package/lib/org/knowledge.test.mjs +0 -326
  362. package/lib/org/leases.test.mjs +0 -235
  363. package/lib/org/mesh-directives.test.mjs +0 -110
  364. package/lib/org/mesh-integration.test.mjs +0 -127
  365. package/lib/org/mesh.test.mjs +0 -400
  366. package/lib/org/messaging.test.mjs +0 -471
  367. package/lib/org/param-contract.test.mjs +0 -477
  368. package/lib/org/policy.test.mjs +0 -237
  369. package/lib/org/protocol.checksum.test.mjs +0 -90
  370. package/lib/org/protocol.test.mjs +0 -323
  371. package/lib/org/push.test.mjs +0 -792
  372. package/lib/org/registry.test.mjs +0 -100
  373. package/lib/org/resource-tools.test.mjs +0 -361
  374. package/lib/org/tool-access.test.mjs +0 -144
  375. package/lib/org/tool-surface-integration.test.mjs +0 -120
  376. package/lib/org/tool-surface.test.mjs +0 -1268
  377. package/lib/org/typing.test.mjs +0 -291
  378. package/lib/org/ui-parity.test.mjs +0 -560
  379. package/lib/org/verify.test.mjs +0 -194
  380. package/lib/org/work-ledger.test.mjs +0 -273
  381. package/lib/plan/adoption-e2e.test.mjs +0 -366
  382. package/lib/plan/budget-enforcement.test.mjs +0 -400
  383. package/lib/plan/compile.test.mjs +0 -382
  384. package/lib/plan/emit.test.mjs +0 -269
  385. package/lib/plan/explain.test.mjs +0 -188
  386. package/lib/prompts/parallelism.test.mjs +0 -177
  387. package/lib/rag/rag.test.mjs +0 -505
  388. package/lib/rate-guard.test.mjs +0 -272
  389. package/lib/reactive-gate.test.mjs +0 -57
  390. package/lib/render.test.mjs +0 -68
  391. package/lib/resource-governor.test.mjs +0 -488
  392. package/lib/scheduling/dynamic-jobs.test.mjs +0 -344
  393. package/lib/scheduling/jitter.test.mjs +0 -140
  394. package/lib/secrets/broker.test.mjs +0 -280
  395. package/lib/secrets/providers.test.mjs +0 -274
  396. package/lib/security/audit-engine.test.mjs +0 -424
  397. package/lib/security/coerce-args.test.mjs +0 -281
  398. package/lib/security/dangerous-tools.test.mjs +0 -68
  399. package/lib/security/external-content.test.mjs +0 -84
  400. package/lib/security/redact.test.mjs +0 -441
  401. package/lib/security/secret-equal.test.mjs +0 -55
  402. package/lib/session/config.test.mjs +0 -92
  403. package/lib/session/feed-core.test.mjs +0 -198
  404. package/lib/session/first-run.test.mjs +0 -121
  405. package/lib/session/frontdoor.test.mjs +0 -205
  406. package/lib/session/handoffs.test.mjs +0 -183
  407. package/lib/session/identity.test.mjs +0 -180
  408. package/lib/session/inbox-claims.test.mjs +0 -286
  409. package/lib/session/launch-args.test.mjs +0 -157
  410. package/lib/session/liveness.test.mjs +0 -100
  411. package/lib/session/status-summary.test.mjs +0 -118
  412. package/lib/session-permissions.test.mjs +0 -120
  413. package/lib/setup/claude-probe.test.mjs +0 -187
  414. package/lib/setup/completeness.test.mjs +0 -110
  415. package/lib/setup/context-pack.test.mjs +0 -89
  416. package/lib/setup/enrich.test.mjs +0 -115
  417. package/lib/setup/enroll-from-cohort.test.mjs +0 -300
  418. package/lib/setup/integration.test.mjs +0 -162
  419. package/lib/setup/io.test.mjs +0 -77
  420. package/lib/setup/runner.test.mjs +0 -132
  421. package/lib/setup/sections/identity.test.mjs +0 -234
  422. package/lib/setup/sections/inventory.test.mjs +0 -198
  423. package/lib/setup/sections/learning.test.mjs +0 -81
  424. package/lib/setup/sections/mandate.test.mjs +0 -388
  425. package/lib/setup/sections/messaging.test.mjs +0 -127
  426. package/lib/setup/sections/model.test.mjs +0 -240
  427. package/lib/setup/sections/org.test.mjs +0 -346
  428. package/lib/setup/sections/orgmail.test.mjs +0 -118
  429. package/lib/setup/sections/recovery.test.mjs +0 -98
  430. package/lib/setup/sections/subagents.test.mjs +0 -429
  431. package/lib/setup/sections/verify.test.mjs +0 -175
  432. package/lib/setup/sot.test.mjs +0 -81
  433. package/lib/setup/state.test.mjs +0 -115
  434. package/lib/singleton.test.mjs +0 -151
  435. package/lib/subagents/cli.test.mjs +0 -389
  436. package/lib/subagents/client.test.mjs +0 -309
  437. package/lib/subagents/gap.test.mjs +0 -234
  438. package/lib/subagents/lock.test.mjs +0 -248
  439. package/lib/subagents/manifest.test.mjs +0 -175
  440. package/lib/subagents/refs.test.mjs +0 -204
  441. package/lib/subagents/resolve.test.mjs +0 -422
  442. package/lib/subagents/schema.test.mjs +0 -328
  443. package/lib/telemetry/alerts.test.mjs +0 -109
  444. package/lib/telemetry/collect.test.mjs +0 -1274
  445. package/lib/tool-definitions-integration.test.mjs +0 -83
  446. package/lib/tool-definitions.test.mjs +0 -437
  447. package/lib/upgrade/global-refresh.test.mjs +0 -65
  448. package/lib/upgrade/launchd-reconcile.test.mjs +0 -272
  449. package/lib/upgrade/post-steps.test.mjs +0 -200
  450. package/lib/upgrade/verify.test.mjs +0 -164
  451. package/lib/util/fetch-timeout.test.mjs +0 -202
  452. package/lib/util/reconnect.test.mjs +0 -369
  453. package/lib/util/unhandled.test.mjs +0 -216
  454. package/lib/voice/outbound.test.mjs +0 -69
  455. package/lib/voice/session-rotation.test.mjs +0 -114
  456. package/lib/voice/stt.test.mjs +0 -226
  457. package/lib/voice/voice.test.mjs +0 -990
  458. package/scripts/cadence/enqueue-cadence-tick.test.mjs +0 -187
  459. package/scripts/ci/check-docs-accuracy.test.mjs +0 -409
  460. package/scripts/ci/check-durable-write-seam.test.mjs +0 -90
  461. package/scripts/ci/check-no-build-artifacts.test.mjs +0 -71
  462. package/scripts/ci/check-no-residual-identity.test.mjs +0 -202
  463. package/scripts/ci/check-skill-packs.test.mjs +0 -495
  464. package/scripts/ci/check-subagent-frontmatter.test.mjs +0 -124
  465. package/scripts/ci/check.test.mjs +0 -194
  466. package/scripts/ci/conformance-org-api.test.mjs +0 -425
  467. package/scripts/cloud-relay/voice/relay-identity.test.mjs +0 -96
  468. package/scripts/collective/hook-runner.test.mjs +0 -173
  469. package/scripts/cost/fleet-digest.test.mjs +0 -207
  470. package/scripts/cost/track-claude-usage-pricing.test.mjs +0 -183
  471. package/scripts/cost/track-claude-usage.test.mjs +0 -148
  472. package/scripts/daemon/agent-daemon-board-mine.test.mjs +0 -96
  473. package/scripts/daemon/agent-daemon-design.test.mjs +0 -238
  474. package/scripts/daemon/agent-daemon-frontdoor.test.mjs +0 -60
  475. package/scripts/daemon/agent-daemon.test.mjs +0 -995
  476. package/scripts/daemon/assurance-e2e.test.mjs +0 -613
  477. package/scripts/daemon/assurance.test.mjs +0 -1791
  478. package/scripts/daemon/board-mirror.test.mjs +0 -165
  479. package/scripts/daemon/cadence-consumer-frontdoor.test.mjs +0 -393
  480. package/scripts/daemon/cadence-consumer-governance.test.mjs +0 -276
  481. package/scripts/daemon/cadence-consumer.test.mjs +0 -776
  482. package/scripts/daemon/cadence-handlers.test.mjs +0 -837
  483. package/scripts/daemon/classifier-identity.test.mjs +0 -137
  484. package/scripts/daemon/classifier.test.mjs +0 -266
  485. package/scripts/daemon/classify-kind.test.mjs +0 -40
  486. package/scripts/daemon/context-compiler.test.mjs +0 -300
  487. package/scripts/daemon/deliver.test.mjs +0 -564
  488. package/scripts/daemon/dispatcher-cooldown.test.mjs +0 -122
  489. package/scripts/daemon/dispatcher-governance.test.mjs +0 -1013
  490. package/scripts/daemon/dispatcher-resume.test.mjs +0 -166
  491. package/scripts/daemon/execution-ladder.test.mjs +0 -470
  492. package/scripts/daemon/goal-steward-cadence.test.mjs +0 -312
  493. package/scripts/daemon/inbox-deferral-session.test.mjs +0 -49
  494. package/scripts/daemon/inbox-deferral.test.mjs +0 -336
  495. package/scripts/daemon/inbox-wake.test.mjs +0 -199
  496. package/scripts/daemon/integration.test.mjs +0 -149
  497. package/scripts/daemon/lib/self-echo.test.mjs +0 -153
  498. package/scripts/daemon/lib/session-router.test.mjs +0 -295
  499. package/scripts/daemon/prompt-builder-preamble.test.mjs +0 -210
  500. package/scripts/daemon/prompt-builder.test.mjs +0 -344
  501. package/scripts/daemon/responder-cost.test.mjs +0 -68
  502. package/scripts/daemon/responder-history.test.mjs +0 -185
  503. package/scripts/daemon/sdk-version.test.mjs +0 -31
  504. package/scripts/daemon/session-lock.test.mjs +0 -252
  505. package/scripts/daemon/session-outcomes.test.mjs +0 -533
  506. package/scripts/daemon/typing-registry.test.mjs +0 -102
  507. package/scripts/hooks/pre-send-audit.test.mjs +0 -354
  508. package/scripts/huddle/huddle-prompt.test.mjs +0 -176
  509. package/scripts/local-triggers/autoupdate.test.mjs +0 -518
  510. package/scripts/local-triggers/generate-plists.test.mjs +0 -456
  511. package/scripts/media-generation/brand-clause.test.mjs +0 -135
  512. package/scripts/org/send-orgmail.first-contact.test.mjs +0 -102
  513. package/scripts/poller/inbox-privilege-injection.test.mjs +0 -167
  514. package/scripts/poller/inbox-scan-poller.test.mjs +0 -295
  515. package/scripts/poller/lib/cloud-relay-dedup.test.mjs +0 -133
  516. package/scripts/poller/slack-socket-mode.test.mjs +0 -805
  517. package/scripts/poller-launchd/install.test.mjs +0 -243
  518. package/scripts/restore-from-backup.test.mjs +0 -181
  519. package/scripts/session/feed.test.mjs +0 -196
  520. package/scripts/session/supervisor-sh.test.mjs +0 -218
  521. package/scripts/session/supervisor.test.mjs +0 -482
  522. package/scripts/setup/configure-macos.test.mjs +0 -306
  523. package/scripts/setup/gen-subagent-manifest.test.mjs +0 -124
  524. package/scripts/setup/generate-agent-package-json.test.mjs +0 -143
  525. package/scripts/setup/generate-capability.test.mjs +0 -134
  526. package/scripts/setup/init-agent.test.mjs +0 -370
  527. package/scripts/setup/init-skill-marketplace.test.mjs +0 -193
  528. package/scripts/vendor/sync-skill-packs.test.mjs +0 -103
  529. package/scripts/watchdog/memory-watchdog.test.mjs +0 -64
@@ -14,7 +14,16 @@ const AGENT_REPO_DIR = process.env.AGENT_DIR || join(new URL(".", import.meta.ur
14
14
  // --verify` reads `sdk_version` off the dashboard to tell the two apart. The
15
15
  // order of trust (sdk-version.mjs): the manifest of the copied tree, then
16
16
  // node_modules, then the package this file ships in.
17
- const DAEMON_SDK_VERSION = resolveRunningSdkVersion({ agentDir: AGENT_REPO_DIR, ownPackagePath: join(new URL(".", import.meta.url).pathname, "../../package.json") }).version;
17
+ //
18
+ // The dashboard carries the INSTALLED version and the verdict beside it,
19
+ // because the number on its own cannot be checked. A seat reported 2.18.0 to
20
+ // the org off its manifest while node_modules already held 2.18.1: both
21
+ // numbers true, the disagreement the only interesting fact, and nothing on the
22
+ // seat or in the org said it. `sdk_version_agrees` is a real tri-state —
23
+ // `unknown` when the running version was INFERRED from the installed one, so
24
+ // equality by fallback can never be read as a clean hop.
25
+ const SDK = resolveRunningSdkVersion({ agentDir: AGENT_REPO_DIR, ownPackagePath: join(new URL(".", import.meta.url).pathname, "../../package.json") });
26
+ const DAEMON_SDK_VERSION = SDK.version;
18
27
 
19
28
  let stats = {
20
29
  daemon_start: new Date().toISOString(),
@@ -58,6 +67,10 @@ export function writeHealthDashboard() {
58
67
  uptime_s: Math.floor((Date.now() - new Date(stats.daemon_start).getTime()) / 1000),
59
68
  pid: process.pid,
60
69
  sdk_version: DAEMON_SDK_VERSION,
70
+ sdk_version_source: SDK.runningSource,
71
+ sdk_version_installed: SDK.installed,
72
+ sdk_version_agrees: SDK.agrees === null ? "unknown" : String(SDK.agrees),
73
+ sdk_version_drift: SDK.drift,
61
74
  ...stats,
62
75
  dispatcher,
63
76
  };
@@ -19,7 +19,7 @@
19
19
  * explicitly rejects flock — "no stale lock files if the daemon dies".
20
20
  */
21
21
 
22
- import { promises as fsp } from "fs";
22
+ import { promises as fsp, mkdirSync, readFileSync, renameSync, writeFileSync } from "fs";
23
23
  import { dirname } from "path";
24
24
 
25
25
  // canonicalizeSlackChannel exists in some installs of scripts/daemon/session-lock.mjs.
@@ -41,6 +41,122 @@ try {
41
41
 
42
42
  const EMPTY_REGISTRY = () => ({ sessions: {}, lru: [] });
43
43
 
44
+ /**
45
+ * Routing keys with a CLI process live on them right now, in THIS daemon.
46
+ *
47
+ * A registry row only becomes "live" when a session CLOSES cleanly, so the
48
+ * registry alone cannot answer "is something already running on this key". It
49
+ * did not have to before: every dispatch minted its own `randomUUID()`, so two
50
+ * concurrent turns in one room were two unrelated sessions. Now they share a
51
+ * key, and `#capital-formation` — 9,187 messages in 21 days, ~38 an hour,
52
+ * TOP-LEVEL, so `agent-daemon.mjs`'s thread lock (which needs a thread id, or a
53
+ * DM) does not cover it — delivers bursts of three and four at a time. Without
54
+ * this set, all of them would resume the same id: N `claude --print
55
+ * --session-id <same>` processes appending one transcript.
56
+ *
57
+ * So: a key that is in flight never RESUMEs. The later turns spawn cold, which
58
+ * is exactly the behaviour being replaced, and continuity resumes on the next
59
+ * turn after the room goes quiet. Process-local by construction — the lease is
60
+ * about OUR child processes, and a stale entry cannot outlive the process that
61
+ * holds it.
62
+ */
63
+ const inFlightKeys = new Set();
64
+
65
+ /** Claim a key for a spawn. Returns false when something already holds it. */
66
+ export function claimSession(key) {
67
+ const k = String(key || "");
68
+ if (!k || inFlightKeys.has(k)) return false;
69
+ inFlightKeys.add(k);
70
+ return true;
71
+ }
72
+
73
+ /** Release a claimed key. Safe to call twice, and for a key never claimed. */
74
+ export function releaseSession(key) {
75
+ inFlightKeys.delete(String(key || ""));
76
+ }
77
+
78
+ /** Is a CLI process live on this key in this daemon? */
79
+ export function isSessionInFlight(key) {
80
+ return inFlightKeys.has(String(key || ""));
81
+ }
82
+
83
+ /** For tests: forget every claim. */
84
+ export function _resetInFlightForTests() {
85
+ inFlightKeys.clear();
86
+ }
87
+
88
+ /**
89
+ * Services whose routing key is `<source>:<channel>[:<thread>]`.
90
+ *
91
+ * Extend by adding a name here, not by editing the key function — a new channel
92
+ * adapter with the same room+thread shape needs nothing else.
93
+ */
94
+ export const CONVERSATION_SOURCES = new Set(["cohort", "telegram", "whatsapp", "orgmail"]);
95
+
96
+ /**
97
+ * Translate a DAEMON-shaped item into the `{source, …}` view {@link routingKey}
98
+ * expects. PURE.
99
+ *
100
+ * Daemon items are `{service, channel_id, thread_id, …}`; the router's key
101
+ * function was specced against `{source, channel, thread_ts, …}` (memo §4.2).
102
+ * This is the seam between them, and it lives HERE rather than in one caller so
103
+ * the responder and the dispatcher cannot key the same conversation two ways —
104
+ * which would give one room two sessions and defeat the whole mechanism.
105
+ *
106
+ * Returns null for an item the router cannot key (a service with no shape, or a
107
+ * missing channel/thread). The caller then mints a fresh id, exactly as before.
108
+ *
109
+ * @param {object} item
110
+ * @returns {object|null}
111
+ */
112
+ export function routerItemFromDaemonItem(item) {
113
+ if (!item || typeof item !== "object") return null;
114
+
115
+ if (item.service === "slack") {
116
+ const channel = item.channel || item.channel_id;
117
+ if (!channel) return null;
118
+ return {
119
+ source: "slack",
120
+ channel,
121
+ thread_ts: item.thread_id || item.thread_ts || null,
122
+ ts: item.ts || item.timestamp || null,
123
+ };
124
+ }
125
+
126
+ if (item.service === "gmail") {
127
+ const tid = item.thread_id || item.threadId;
128
+ if (!tid) return null;
129
+ return { source: "gmail", thread_id: tid };
130
+ }
131
+
132
+ if (item.service === "calendar") {
133
+ const eid = item.event_id || item.eventId;
134
+ if (!eid) return null;
135
+ return { source: "calendar", event_id: eid };
136
+ }
137
+
138
+ // Cohort and the other room+thread services. THIS USED TO RETURN NULL FOR
139
+ // COHORT, so every turn in the agent's own workspace fell back to a fresh
140
+ // pre-minted UUID and started cold — the session router existed and the one
141
+ // surface the owner actually uses never reached it (design §3 R10).
142
+ //
143
+ // `channel_id` ONLY, never `channel`: `item.channel` is the display label
144
+ // (`task/<title>`, `doc/<name>`), and keying a session on a label collides
145
+ // two rooms that happen to share a title. A surface with no room id simply
146
+ // has no session to continue.
147
+ if (CONVERSATION_SOURCES.has(item.service)) {
148
+ const channel = item.channel_id;
149
+ if (!channel) return null;
150
+ return {
151
+ source: item.service,
152
+ channel,
153
+ thread_root_id: item.thread_root_id || item.thread_id || null,
154
+ };
155
+ }
156
+
157
+ return null;
158
+ }
159
+
44
160
  /**
45
161
  * Pure key-derivation function (memo §4.2 table).
46
162
  *
@@ -71,6 +187,25 @@ export function routingKey(item) {
71
187
  return `gmail:${tid}`;
72
188
  }
73
189
 
190
+ // Conversation-shaped services: one ROOM, optionally one thread inside it.
191
+ //
192
+ // `cohort` used to fall through to the throw below, so every Cohort turn was
193
+ // keyed by nothing, routed by nothing, and spawned cold — the agent's own
194
+ // workspace was the one surface with no continuity at all (design §3 R10).
195
+ // telegram / whatsapp / orgmail have exactly the same shape and were in the
196
+ // same position.
197
+ //
198
+ // The room is the key when there is no thread, for the same reason a Slack DM
199
+ // is: a room with no threading IS one conversation. A thread root narrows it,
200
+ // so two threads in a busy channel do not share a session and leak each
201
+ // other's context.
202
+ if (CONVERSATION_SOURCES.has(source)) {
203
+ const channel = item.channel || item.channel_id || "";
204
+ if (!channel) throw new Error(`routingKey: ${source} item missing channel`);
205
+ const thread = item.thread_root_id || item.threadRootId || item.thread_id || item.thread_ts || "";
206
+ return thread ? `${source}:${channel}:${thread}` : `${source}:${channel}`;
207
+ }
208
+
74
209
  if (source === "calendar") {
75
210
  const eid = item.event_id || item.eventId;
76
211
  if (!eid) throw new Error("routingKey: calendar item missing event_id");
@@ -91,6 +226,51 @@ export function routingKey(item) {
91
226
  throw new Error(`routingKey: unknown source "${source}"`);
92
227
  }
93
228
 
229
+ /**
230
+ * THE routing decision, as a pure function of the entry and the clock.
231
+ *
232
+ * Extracted so the async router (responder.mjs) and the SYNCHRONOUS one
233
+ * (dispatcher.mjs, whose spawn path is not async) cannot drift: one rule, two
234
+ * IO shells. Every branch is the memo §4.4 table.
235
+ *
236
+ * @param {object|undefined} entry the registry row for this key
237
+ * @param {{now:number, ttlSeconds:number}} o
238
+ * @returns {{decision:"EPHEMERAL"|"EPHEMERAL_REPLACE"|"RESUME", resumeId:string|null}}
239
+ */
240
+ export function decideRoute(entry, { now, ttlSeconds }) {
241
+ if (!entry) return { decision: "EPHEMERAL", resumeId: null };
242
+ if (now - entry.last_used_at > ttlSeconds * 1000) return { decision: "EPHEMERAL_REPLACE", resumeId: null };
243
+ if (entry.status !== "live") return { decision: "EPHEMERAL_REPLACE", resumeId: null };
244
+ if (entry.last_exit_code !== 0) return { decision: "EPHEMERAL_REPLACE", resumeId: null };
245
+ return { decision: "RESUME", resumeId: entry.claude_session_id };
246
+ }
247
+
248
+ /**
249
+ * Build the registry row for a touched key. Pure — {@link touchEntry} is the
250
+ * only place the row's shape is decided, for both IO shells.
251
+ */
252
+ export function touchEntry(existing, { key, claudeSessionId, daemonSessionId, model, now, ttlSeconds }) {
253
+ return {
254
+ daemon_session_id: daemonSessionId || (existing && existing.daemon_session_id) || null,
255
+ claude_session_id: claudeSessionId ?? (existing && existing.claude_session_id) ?? null,
256
+ key,
257
+ model: model || (existing && existing.model) || null,
258
+ created_at: (existing && existing.created_at) || new Date(now).toISOString(),
259
+ last_used_at: now,
260
+ ttl_seconds: ttlSeconds,
261
+ status: "live",
262
+ last_exit_code: 0,
263
+ };
264
+ }
265
+
266
+ /** Coerce a parsed blob into the registry shape. Shared by both IO shells. */
267
+ function asRegistry(parsed) {
268
+ if (!parsed || typeof parsed !== "object") return EMPTY_REGISTRY();
269
+ if (!parsed.sessions || typeof parsed.sessions !== "object") parsed.sessions = {};
270
+ if (!Array.isArray(parsed.lru)) parsed.lru = [];
271
+ return parsed;
272
+ }
273
+
94
274
  /**
95
275
  * Read the registry from disk. Missing file → empty registry shape.
96
276
  * Corrupted JSON also degrades to empty (memo §6: "Read failure on the
@@ -98,15 +278,17 @@ export function routingKey(item) {
98
278
  */
99
279
  async function readRegistry(path) {
100
280
  try {
101
- const raw = await fsp.readFile(path, "utf-8");
102
- const parsed = JSON.parse(raw);
103
- // Defensive: ensure shape
104
- if (!parsed || typeof parsed !== "object") return EMPTY_REGISTRY();
105
- if (!parsed.sessions || typeof parsed.sessions !== "object") parsed.sessions = {};
106
- if (!Array.isArray(parsed.lru)) parsed.lru = [];
107
- return parsed;
108
- } catch (err) {
109
- if (err && err.code === "ENOENT") return EMPTY_REGISTRY();
281
+ return asRegistry(JSON.parse(await fsp.readFile(path, "utf-8")));
282
+ } catch {
283
+ return EMPTY_REGISTRY();
284
+ }
285
+ }
286
+
287
+ /** The same read, synchronously — {@link createRouterSync} and every re-read. */
288
+ function readRegistrySync(path) {
289
+ try {
290
+ return asRegistry(JSON.parse(readFileSync(path, "utf-8")));
291
+ } catch {
110
292
  return EMPTY_REGISTRY();
111
293
  }
112
294
  }
@@ -151,8 +333,25 @@ export async function createRouter({
151
333
  throw new Error("createRouter: registryPath is required");
152
334
  }
153
335
 
154
- // Eagerly load so first call doesn't race with concurrent writers.
336
+ // RE-READ ON EVERY OPERATION. This used to load the file once and keep the
337
+ // object for the daemon's life, which was safe for exactly as long as this
338
+ // router was the only writer. It is not any more: `dispatcher.mjs` now writes
339
+ // the SAME file through `createRouterSync`, and a cached writer here did two
340
+ // destructive things with it — `touch()` wrote its stale snapshot back, and
341
+ // the LRU sweep at the end of `touch()` DELETED every key absent from that
342
+ // snapshot, so one ordinary quick reply erased the dispatcher's live session
343
+ // rows. Worse in the other direction: this shell would answer RESUME from a
344
+ // cached entry the dispatcher had already recorded as killed.
345
+ //
346
+ // A re-read costs one small JSON parse per spawn, against a process launch.
347
+ // Continuity is not worth a correctness hole, and two shells over one file
348
+ // must agree about what is in it.
155
349
  let registry = await readRegistry(registryPath);
350
+ /** Refresh from disk, so another writer's rows are never overwritten. */
351
+ function refresh() {
352
+ registry = readRegistrySync(registryPath);
353
+ return registry;
354
+ }
156
355
 
157
356
  /**
158
357
  * Decide RESUME / EPHEMERAL / EPHEMERAL_REPLACE for a key (memo §4.4).
@@ -162,24 +361,17 @@ export async function createRouter({
162
361
  * @returns {{ decision: "EPHEMERAL"|"EPHEMERAL_REPLACE"|"RESUME", resumeId: string|null }}
163
362
  */
164
363
  function route(key) {
165
- const entry = registry.sessions[key];
166
- if (!entry) return { decision: "EPHEMERAL", resumeId: null };
167
-
168
- const ttlMs = ttlSeconds * 1000;
169
- if (now() - entry.last_used_at > ttlMs) {
170
- return { decision: "EPHEMERAL_REPLACE", resumeId: null };
171
- }
172
- if (entry.status !== "live") {
173
- return { decision: "EPHEMERAL_REPLACE", resumeId: null };
364
+ // Never hand a running session's id to a second process — see inFlightKeys.
365
+ if (isSessionInFlight(key)) return { decision: "EPHEMERAL", resumeId: null };
366
+ const reg = refresh();
367
+ const entry = reg.sessions[key];
368
+ const out = decideRoute(entry, { now: now(), ttlSeconds });
369
+ if (out.decision === "RESUME") {
370
+ // Touch in-memory; persisted on next touch()/recordExit() write.
371
+ entry.last_used_at = now();
372
+ bumpLru(reg.lru, key);
174
373
  }
175
- if (entry.last_exit_code !== 0) {
176
- return { decision: "EPHEMERAL_REPLACE", resumeId: null };
177
- }
178
-
179
- // Touch in-memory; persisted on next touch()/recordExit() write.
180
- entry.last_used_at = now();
181
- bumpLru(registry.lru, key);
182
- return { decision: "RESUME", resumeId: entry.claude_session_id };
374
+ return out;
183
375
  }
184
376
 
185
377
  /**
@@ -194,20 +386,12 @@ export async function createRouter({
194
386
  */
195
387
  async function touch(key, { claudeSessionId, daemonSessionId, model } = {}) {
196
388
  const ts = now();
197
- const isoNow = new Date(ts).toISOString();
389
+ // Merge onto what is on disk NOW, not onto a snapshot from process start.
390
+ // The LRU sweep below deletes keys, so a stale base does not merely lose an
391
+ // update — it removes another writer's live sessions.
392
+ refresh();
198
393
  const existing = registry.sessions[key];
199
-
200
- const entry = {
201
- daemon_session_id: daemonSessionId || existing?.daemon_session_id || null,
202
- claude_session_id: claudeSessionId ?? existing?.claude_session_id ?? null,
203
- key,
204
- model: model || existing?.model || null,
205
- created_at: existing?.created_at || isoNow,
206
- last_used_at: ts,
207
- ttl_seconds: ttlSeconds,
208
- status: "live",
209
- last_exit_code: 0,
210
- };
394
+ const entry = touchEntry(existing, { key, claudeSessionId, daemonSessionId, model, now: ts, ttlSeconds });
211
395
  registry.sessions[key] = entry;
212
396
  bumpLru(registry.lru, key);
213
397
 
@@ -231,6 +415,7 @@ export async function createRouter({
231
415
  * next route() returns EPHEMERAL_REPLACE.
232
416
  */
233
417
  async function recordExit(key, exitCode) {
418
+ refresh();
234
419
  const entry = registry.sessions[key];
235
420
  if (!entry) return; // No-op — key was never touched.
236
421
  entry.last_exit_code = exitCode;
@@ -245,6 +430,7 @@ export async function createRouter({
245
430
  * Returns the count evicted.
246
431
  */
247
432
  async function evictExpired() {
433
+ refresh();
248
434
  const ttlMs = ttlSeconds * 1000;
249
435
  const cutoff = now() - ttlMs;
250
436
  let evicted = 0;
@@ -267,8 +453,90 @@ export async function createRouter({
267
453
  * the live registry by accident.
268
454
  */
269
455
  function _readForTests() {
270
- return JSON.parse(JSON.stringify(registry));
456
+ return JSON.parse(JSON.stringify(refresh()));
271
457
  }
272
458
 
273
459
  return { route, touch, recordExit, evictExpired, _readForTests };
274
460
  }
461
+
462
+
463
+ /**
464
+ * The SYNCHRONOUS face of the same registry.
465
+ *
466
+ * `dispatcher.mjs#spawnSession` builds its argv and calls `spawn` in one
467
+ * synchronous pass, reached from a synchronous `dispatch()`. Making that path
468
+ * async to await the router would ripple through the queue, the eviction path
469
+ * and every caller — so the router grows a sync shell over the SAME file, the
470
+ * SAME key function and the SAME pure decision ({@link decideRoute}). Two IO
471
+ * shells, one rule; a drift between them would be a bug in one place, not two.
472
+ *
473
+ * The registry is a single small JSON file and these calls happen once per
474
+ * spawn, so the blocking read is nanoseconds against a process launch. Writes
475
+ * keep the temp-file + rename pattern, so a concurrent reader never sees a
476
+ * half-written file.
477
+ *
478
+ * Every method is FAIL-OPEN: an unreadable or corrupt registry routes
479
+ * EPHEMERAL (a cold session, which is exactly today's behaviour), and a failed
480
+ * write is logged by the caller and forgotten. Session continuity is an
481
+ * optimisation; losing it must never cost the work.
482
+ *
483
+ * @param {object} opts same shape as {@link createRouter}
484
+ */
485
+ export function createRouterSync({
486
+ registryPath,
487
+ ttlSeconds = 1800,
488
+ maxLiveSessions = 8,
489
+ now = () => Date.now(),
490
+ } = {}) {
491
+ if (!registryPath) throw new Error("createRouterSync: registryPath is required");
492
+
493
+ // Missing or corrupt: an empty registry, which routes EPHEMERAL. Same posture
494
+ // as the async shell — never throw on the spawn path.
495
+ const read = () => readRegistrySync(registryPath);
496
+
497
+ function write(registry) {
498
+ try {
499
+ mkdirSync(dirname(registryPath), { recursive: true });
500
+ const tmp = `${registryPath}.tmp.${process.pid}.${Date.now()}`;
501
+ writeFileSync(tmp, JSON.stringify(registry, null, 2), "utf-8");
502
+ renameSync(tmp, registryPath);
503
+ return true;
504
+ } catch {
505
+ // A registry we cannot persist costs continuity on the NEXT turn, nothing
506
+ // on this one. The session is already spawning.
507
+ return false;
508
+ }
509
+ }
510
+
511
+ return {
512
+ /** @returns {{decision:string, resumeId:string|null}} */
513
+ route(key) {
514
+ // Never hand a running session's id to a second process — see inFlightKeys.
515
+ if (isSessionInFlight(key)) return { decision: "EPHEMERAL", resumeId: null };
516
+ return decideRoute(read().sessions[key], { now: now(), ttlSeconds });
517
+ },
518
+ touch(key, { claudeSessionId, daemonSessionId, model } = {}) {
519
+ const registry = read();
520
+ registry.sessions[key] = touchEntry(registry.sessions[key], {
521
+ key, claudeSessionId, daemonSessionId, model, now: now(), ttlSeconds,
522
+ });
523
+ bumpLru(registry.lru, key);
524
+ while (registry.lru.length > maxLiveSessions) {
525
+ const evictKey = registry.lru.shift();
526
+ if (evictKey && evictKey !== key) delete registry.sessions[evictKey];
527
+ }
528
+ for (const k of Object.keys(registry.sessions)) {
529
+ if (!registry.lru.includes(k)) delete registry.sessions[k];
530
+ }
531
+ return write(registry);
532
+ },
533
+ recordExit(key, exitCode) {
534
+ const registry = read();
535
+ const entry = registry.sessions[key];
536
+ if (!entry) return false;
537
+ entry.last_exit_code = exitCode;
538
+ if (exitCode !== 0) entry.status = "killed";
539
+ return write(registry);
540
+ },
541
+ };
542
+ }
@@ -39,6 +39,17 @@ process.env.MAESTRO_DAEMON = "1";
39
39
  // ---------------------------------------------------------------------------
40
40
  // 0. Environment — BEFORE anything reads a credential
41
41
  // ---------------------------------------------------------------------------
42
+ // But first: the BOOT MARKER. The wrapper appends this process's stdout to
43
+ // logs/daemon/daemon-<start day>.log, which carries no per-line timestamps and
44
+ // is SHARED by every incarnation that starts on the same day — a crash at
45
+ // 09:00 and the healthy respawn at 09:01 write to the same file. This line is
46
+ // the first thing this incarnation prints, tagged with its pid, so a reader
47
+ // can split the file into "before this process" and "since this process":
48
+ // scripts/local-triggers/autoupdate.sh's health gate scans for fatal lines
49
+ // only AFTER `[DAEMON] boot pid=<its pid>`; without it a one-off crash earlier
50
+ // the same day read as "fatal since restart" every hour until midnight. The
51
+ // format is pinned by autoupdate.test.mjs — change both or neither.
52
+ console.log(`[DAEMON] boot pid=${process.pid} at=${new Date().toISOString()}`);
42
53
  // The core daemon (agent-daemon.mjs, step 4) loads .env itself, but the org
43
54
  // mesh connects at step 3b' — EARLIER in this file. Under launchd the process
44
55
  // environment is bare, so the mesh was reading COHORT_API_KEY before anything
@@ -13,6 +13,12 @@ import { isEnabled as orgEnabled } from "../../lib/org/client.mjs";
13
13
  import { recall as orgRecall } from "../../lib/org/knowledge.mjs";
14
14
  import { outcomeSourceShareable } from "./session-outcomes.mjs";
15
15
  import { withParallelism } from "../../lib/prompts/parallelism.mjs";
16
+ import { isPrivateConversation, historyDirNames } from "../../lib/context/history-scope.mjs";
17
+ // A human's roll-call to the whole room: the SAME reply-shaping block the
18
+ // quick-reply responder injects, so a roll-call that fell through to a session
19
+ // (quick reply failed transiently) is answered in the same shape.
20
+ import { isRollCallItem, collectiveInstructions } from "../../lib/org/inbound/broadcast.mjs";
21
+ import { itemCollectiveIntent } from "../../lib/org/inbound/collective.mjs";
16
22
 
17
23
  const AGENT_REPO_DIR = process.env.AGENT_DIR || join(new URL(".", import.meta.url).pathname, "../..");
18
24
 
@@ -38,6 +44,74 @@ function loadAgent() {
38
44
  // Legacy: Maximum lines of conversation history (used when DAEMON_CONTEXT_COMPILER is off)
39
45
  const MAX_HISTORY_LINES = 30;
40
46
 
47
+ /**
48
+ * Is the compiled-context path on?
49
+ *
50
+ * IT DEFAULTS ON, and that is the fix. The code read `=== "1"` while
51
+ * `.env.example:231` shipped `DAEMON_CONTEXT_COMPILER=1`, so which of two
52
+ * materially different context paths ran depended on whether the operator had
53
+ * copied the example env — an agent set up by hand got the legacy path and an
54
+ * agent set up from the template got the compiled one, with nothing anywhere
55
+ * saying so (design §3 R11).
56
+ *
57
+ * One path ships. The legacy path stays reachable for a seat that has to turn
58
+ * it off in a hurry (`DAEMON_CONTEXT_COMPILER=0`), which is why the flag is not
59
+ * simply deleted.
60
+ *
61
+ * A value that is neither a recognised on nor a recognised off is the dangerous
62
+ * case, because it selects between two materially different context paths and
63
+ * an operator who wrote `disabled` meant OFF. The word lists below are wide
64
+ * enough to cover what people actually write, and {@link contextCompilerSetting}
65
+ * reports the leftovers so the caller can say so out loud instead of resolving
66
+ * a typo in silence.
67
+ *
68
+ * @param {NodeJS.ProcessEnv} [env]
69
+ * @returns {boolean}
70
+ */
71
+ export function contextCompilerEnabled(env = process.env) {
72
+ return contextCompilerSetting(env).enabled;
73
+ }
74
+
75
+ /** Values that mean OFF. Wider than `"0"` because operators write words. */
76
+ const CONTEXT_COMPILER_OFF = Object.freeze(["0", "false", "off", "no", "n", "disabled", "disable", "none"]);
77
+
78
+ /** Values that mean ON. */
79
+ const CONTEXT_COMPILER_ON = Object.freeze(["1", "true", "on", "yes", "y", "enabled", "enable"]);
80
+
81
+ /**
82
+ * The flag, and whether the operator's word was one we know.
83
+ *
84
+ * PURE — the env arrives on the argument.
85
+ *
86
+ * @param {NodeJS.ProcessEnv} [env]
87
+ * @returns {{enabled:boolean, recognised:boolean, raw:string}}
88
+ */
89
+ export function contextCompilerSetting(env = process.env) {
90
+ const raw = String((env && env.DAEMON_CONTEXT_COMPILER) ?? "").trim().toLowerCase();
91
+ if (!raw) return { enabled: true, recognised: true, raw };
92
+ if (CONTEXT_COMPILER_OFF.includes(raw)) return { enabled: false, recognised: true, raw };
93
+ if (CONTEXT_COMPILER_ON.includes(raw)) return { enabled: true, recognised: true, raw };
94
+ return { enabled: true, recognised: false, raw };
95
+ }
96
+
97
+ /** Warn once per process about a DAEMON_CONTEXT_COMPILER value we do not know. */
98
+ let warnedAboutContextCompiler = false;
99
+ function warnIfContextCompilerUnrecognised(env = process.env) {
100
+ if (warnedAboutContextCompiler) return;
101
+ const setting = contextCompilerSetting(env);
102
+ if (setting.recognised) return;
103
+ warnedAboutContextCompiler = true;
104
+ console.warn(
105
+ `[prompt-builder] DAEMON_CONTEXT_COMPILER="${setting.raw}" is not a value this reads. ` +
106
+ `Using the compiled context path. Set it to 0 to use the legacy path.`,
107
+ );
108
+ }
109
+
110
+ /** For tests: allow the one-time warning to fire again. */
111
+ export function _resetContextCompilerWarning() {
112
+ warnedAboutContextCompiler = false;
113
+ }
114
+
41
115
  // Org shared-knowledge injection (org enrollment). Bounded so a large recall can
42
116
  // never blow up the prompt: at most this many facts, each truncated.
43
117
  const ORG_RECALL_MAX_FACTS = 6;
@@ -603,8 +677,22 @@ function today() {
603
677
 
604
678
  /**
605
679
  * Load recent conversation history for a sender/channel from interaction logs.
606
- * Searches memory/interactions/slack/{channel-or-sender}/ for recent JSONL entries.
607
- * Returns the most recent entries as formatted context text.
680
+ *
681
+ * IT READS THE ITEM'S OWN SERVICE. `responder.mjs#logInteraction` files every
682
+ * exchange under `memory/interactions/<service>/…` and has done since the
683
+ * write-half was fixed; this read looked only under `…/slack/…`, so a Cohort,
684
+ * Telegram or WhatsApp conversation was written down and then never found
685
+ * (design §3 R12). The agent had the record and could not see it.
686
+ *
687
+ * Slack keeps its legacy `<sender-slug>` directory alongside the `dm-` one —
688
+ * historical logs live there and must not go dark on this change.
689
+ *
690
+ * AND IT READS ONLY WHAT THIS ROOM MAY SEE. `logInteraction` files every
691
+ * exchange under BOTH the room and `dm-<sender-slug>`; merging all of them into
692
+ * one block put a person's private DM sentence into the prompt built for a
693
+ * public channel the moment this read stopped being Slack-only. The gate is
694
+ * `lib/context/history-scope.mjs`, shared with the writer and with the compiled
695
+ * path so the three cannot drift (design §5.5, resource scope).
608
696
  */
609
697
  function loadConversationHistory(item) {
610
698
  if (!item) return null;
@@ -613,13 +701,14 @@ function loadConversationHistory(item) {
613
701
  // Try multiple paths where interaction logs may live
614
702
  const channelId = extractChannelId(item);
615
703
  const senderSlug = item.sender ? item.sender.replace(/\s+/g, "-").toLowerCase() : null;
704
+ const service = String(item.service || "slack");
616
705
 
617
- const candidateDirs = [];
618
- if (channelId) candidateDirs.push(join(AGENT_REPO_DIR, "memory", "interactions", "slack", channelId));
619
- if (senderSlug) {
620
- candidateDirs.push(join(AGENT_REPO_DIR, "memory", "interactions", "slack", `dm-${senderSlug}`));
621
- candidateDirs.push(join(AGENT_REPO_DIR, "memory", "interactions", "slack", senderSlug));
622
- }
706
+ const candidateDirs = historyDirNames({
707
+ channelId,
708
+ senderSlug,
709
+ isPrivate: isPrivateConversation(item),
710
+ service,
711
+ }).map((name) => join(AGENT_REPO_DIR, "memory", "interactions", service, name));
623
712
 
624
713
  const entries = [];
625
714
 
@@ -681,7 +770,7 @@ function loadConversationHistory(item) {
681
770
  */
682
771
  function extractChannelId(item) {
683
772
  if (!item) return null;
684
- // Direct channel ID field
773
+ // Direct channel ID field — every non-Slack service sets this.
685
774
  if (item.channel_id) return item.channel_id;
686
775
  // From raw_ref format: "slack:D099N1JGKRQ:1775331885.690669"
687
776
  if (item.raw_ref) {
@@ -984,6 +1073,25 @@ export async function buildPrompt(item, classResult, options = {}) {
984
1073
  parts.push("");
985
1074
  }
986
1075
 
1076
+ // 1d. COLLECTIVE shaping — a human addressed the whole room and every seat
1077
+ // answers for itself. Placed with the contact blocks above because it is the
1078
+ // same kind of instruction: what may and may not be SAID, before any work.
1079
+ //
1080
+ // The intent (roll-call / work / ambient) comes from `collective.mjs`, the
1081
+ // twin of hq's `work/collective.ts`, so the same message gets the same block
1082
+ // whether the seat is driven from here or from hq. A WORK broadcast is told
1083
+ // its board row already exists — otherwise the session's first instinct is to
1084
+ // open a second one.
1085
+ if (type === "inbox" && item && isRollCallItem(item)) {
1086
+ parts.push(
1087
+ collectiveInstructions({
1088
+ intent: itemCollectiveIntent(item, isRollCallItem),
1089
+ respondents: item.respondents,
1090
+ }),
1091
+ );
1092
+ parts.push("");
1093
+ }
1094
+
987
1095
  // 2. Session context
988
1096
  parts.push(`Date: ${date}`);
989
1097
  parts.push(`Classification: priority=${classResult.priority}, action=${action}, category=${classResult.category || "unclassified"}`);
@@ -1007,7 +1115,8 @@ export async function buildPrompt(item, classResult, options = {}) {
1007
1115
  // (context-compiler.mjs → loadOrgMemory), so skip here to avoid a duplicate
1008
1116
  // recall + duplicated context in the same prompt. Backlog items pass their
1009
1117
  // queueItem into compileContext (below) so entity coverage is preserved.
1010
- if (process.env.DAEMON_CONTEXT_COMPILER !== "1") {
1118
+ warnIfContextCompilerUnrecognised();
1119
+ if (!contextCompilerEnabled()) {
1011
1120
  const orgBlock = await buildOrgKnowledgeBlock(item, classResult, queueItem);
1012
1121
  if (orgBlock) {
1013
1122
  parts.push(orgBlock);
@@ -1016,7 +1125,7 @@ export async function buildPrompt(item, classResult, options = {}) {
1016
1125
  }
1017
1126
 
1018
1127
  // 4. Compiled context (profile + history + disclosure boundaries + active sessions + sent messages)
1019
- if (process.env.DAEMON_CONTEXT_COMPILER === "1" && (type === "inbox" ? item : true)) {
1128
+ if (contextCompilerEnabled() && (type === "inbox" ? item : true)) {
1020
1129
  try {
1021
1130
  const { contextBlock } = await compileContext(
1022
1131
  item || {},
@@ -1056,7 +1165,7 @@ export async function buildPrompt(item, classResult, options = {}) {
1056
1165
  parts.push("");
1057
1166
 
1058
1167
  // 6. User profile lookup — legacy only (when feature flag is off, profile is in compiled context)
1059
- if (process.env.DAEMON_CONTEXT_COMPILER !== "1" && type === "inbox" && item && item.sender) {
1168
+ if (!contextCompilerEnabled() && type === "inbox" && item && item.sender) {
1060
1169
  const profileName = item.sender.replace(/\s+/g, "-").toLowerCase();
1061
1170
  parts.push(`Before responding, read memory/profiles/users/${profileName}.yaml for sender preferences, standing instructions, and relationship context. If the file does not exist, proceed without it.`);
1062
1171
  parts.push("");