@adhdev/daemon-core 0.9.82-rc.5 → 0.9.82-rc.500

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 (479) hide show
  1. package/dist/boot/daemon-lifecycle.d.ts +11 -0
  2. package/dist/boot/process-hardening.d.ts +50 -0
  3. package/dist/build-info.d.ts +37 -0
  4. package/dist/chat/source-machine.d.ts +166 -0
  5. package/dist/chat/source-resolver.d.ts +104 -0
  6. package/dist/chat/subscription-updates.d.ts +1 -0
  7. package/dist/cli-adapter-types.d.ts +85 -2
  8. package/dist/cli-adapters/cli-script-runner.d.ts +117 -0
  9. package/dist/cli-adapters/cli-state-engine.d.ts +246 -0
  10. package/dist/cli-adapters/provider-cli-adapter.d.ts +139 -64
  11. package/dist/cli-adapters/provider-cli-parse.d.ts +5 -0
  12. package/dist/cli-adapters/provider-cli-runtime.d.ts +34 -0
  13. package/dist/cli-adapters/provider-cli-shared.d.ts +74 -0
  14. package/dist/cli-adapters/pty-write-chunking.d.ts +34 -0
  15. package/dist/cli-adapters/raw-terminal-io.d.ts +37 -0
  16. package/dist/cli-adapters/resolve-executable.d.ts +69 -0
  17. package/dist/cli-adapters/terminal-backends/ghostty-vt-backend.d.ts +6 -3
  18. package/dist/cli-adapters/terminal-backends/types.d.ts +10 -2
  19. package/dist/cli-adapters/terminal-screen.d.ts +4 -11
  20. package/dist/commands/chat-commands-debug-bundle.d.ts +14 -0
  21. package/dist/commands/chat-commands-read.d.ts +23 -0
  22. package/dist/commands/chat-commands-scope.d.ts +39 -0
  23. package/dist/commands/chat-commands-shared.d.ts +33 -0
  24. package/dist/commands/chat-commands-write.d.ts +14 -0
  25. package/dist/commands/chat-commands.d.ts +10 -23
  26. package/dist/commands/cli-manager.d.ts +71 -1
  27. package/dist/commands/handler.d.ts +125 -0
  28. package/dist/commands/high-family/index.d.ts +3 -0
  29. package/dist/commands/high-family/mesh-coordinator-launch.d.ts +2 -0
  30. package/dist/commands/high-family/mesh-events.d.ts +2 -0
  31. package/dist/commands/high-family/mesh-status.d.ts +2 -0
  32. package/dist/commands/high-family/types.d.ts +64 -0
  33. package/dist/commands/low-family/coordinator-prompt.d.ts +9 -0
  34. package/dist/commands/low-family/daemon-lifecycle.d.ts +2 -0
  35. package/dist/commands/low-family/diagnostics.d.ts +2 -0
  36. package/dist/commands/low-family/index.d.ts +3 -0
  37. package/dist/commands/low-family/mesh-ledger.d.ts +10 -0
  38. package/dist/commands/low-family/mesh-node-logs.d.ts +2 -0
  39. package/dist/commands/low-family/notification.d.ts +2 -0
  40. package/dist/commands/low-family/refine-config.d.ts +2 -0
  41. package/dist/commands/low-family/session-host.d.ts +2 -0
  42. package/dist/commands/low-family/spec-providerdev.d.ts +11 -0
  43. package/dist/commands/low-family/status-meta.d.ts +2 -0
  44. package/dist/commands/low-family/types.d.ts +33 -0
  45. package/dist/commands/med-family/cli-agent.d.ts +2 -0
  46. package/dist/commands/med-family/fast-forward.d.ts +2 -0
  47. package/dist/commands/med-family/ide.d.ts +10 -0
  48. package/dist/commands/med-family/index.d.ts +3 -0
  49. package/dist/commands/med-family/mesh-crud.d.ts +48 -0
  50. package/dist/commands/med-family/mesh-host-pairing.d.ts +2 -0
  51. package/dist/commands/med-family/mesh-queue.d.ts +2 -0
  52. package/dist/commands/med-family/mesh-restart.d.ts +2 -0
  53. package/dist/commands/med-family/types.d.ts +152 -0
  54. package/dist/commands/mesh-coordinator.d.ts +85 -1
  55. package/dist/commands/router-aggregate-status.d.ts +24 -0
  56. package/dist/commands/router-mesh-session-owner.d.ts +59 -0
  57. package/dist/commands/router-refine.d.ts +151 -0
  58. package/dist/commands/router-worktree-cleanup.d.ts +133 -0
  59. package/dist/commands/router.d.ts +212 -11
  60. package/dist/commands/upgrade-helper.d.ts +41 -1
  61. package/dist/config/chat-history.d.ts +11 -0
  62. package/dist/config/config.d.ts +19 -0
  63. package/dist/config/mesh-config.d.ts +132 -1
  64. package/dist/config/mesh-json-config.d.ts +199 -0
  65. package/dist/config/registry-resolver.d.ts +54 -0
  66. package/dist/config/repo-settings.d.ts +77 -0
  67. package/dist/config/state-store.d.ts +30 -0
  68. package/dist/daemon/dev-server.d.ts +0 -2
  69. package/dist/detection/cli-detector.d.ts +17 -0
  70. package/dist/detection/ide-detector.d.ts +13 -0
  71. package/dist/detection/win32-ide-version.d.ts +37 -0
  72. package/dist/git/change-impact-config.d.ts +159 -0
  73. package/dist/git/git-commands.d.ts +27 -2
  74. package/dist/git/git-diff.d.ts +6 -0
  75. package/dist/git/git-executor.d.ts +11 -0
  76. package/dist/git/git-status.d.ts +85 -0
  77. package/dist/git/git-types.d.ts +5 -43
  78. package/dist/git/git-worktree.d.ts +71 -1
  79. package/dist/git/index.d.ts +3 -1
  80. package/dist/index.d.ts +74 -17
  81. package/dist/index.js +61082 -24164
  82. package/dist/index.js.map +1 -1
  83. package/dist/index.mjs +57034 -20299
  84. package/dist/index.mjs.map +1 -1
  85. package/dist/installer.d.ts +1 -4
  86. package/dist/ipc/local-ipc-server.d.ts +91 -0
  87. package/dist/launch.d.ts +1 -1
  88. package/dist/logging/async-batch-writer.d.ts +10 -0
  89. package/dist/logging/debug-config.d.ts +16 -0
  90. package/dist/logging/log-redactor.d.ts +24 -0
  91. package/dist/logging/log-tail-reader.d.ts +81 -0
  92. package/dist/logging/logger.d.ts +1 -1
  93. package/dist/mesh/contracts.d.ts +256 -0
  94. package/dist/mesh/coordinator-prompt.d.ts +76 -0
  95. package/dist/mesh/coordinator-registry.d.ts +59 -0
  96. package/dist/mesh/mesh-active-work.d.ts +203 -0
  97. package/dist/mesh/mesh-clone-grace.d.ts +8 -0
  98. package/dist/mesh/mesh-completion-synthesis.d.ts +4 -0
  99. package/dist/mesh/mesh-coordinator-config.d.ts +21 -0
  100. package/dist/mesh/mesh-delivery-policy.d.ts +104 -0
  101. package/dist/mesh/mesh-event-classify.d.ts +7 -0
  102. package/dist/mesh/mesh-event-forwarding.d.ts +58 -0
  103. package/dist/mesh/mesh-event-trace.d.ts +21 -0
  104. package/dist/mesh/mesh-events-coordinator.d.ts +4 -0
  105. package/dist/mesh/mesh-events-pending.d.ts +258 -0
  106. package/dist/mesh/mesh-events-stale.d.ts +61 -0
  107. package/dist/mesh/mesh-events-utils.d.ts +75 -0
  108. package/dist/mesh/mesh-events.d.ts +6 -37
  109. package/dist/mesh/mesh-fast-forward.d.ts +81 -0
  110. package/dist/mesh/mesh-host-ownership.d.ts +29 -0
  111. package/dist/mesh/mesh-idle-reminder.d.ts +68 -0
  112. package/dist/mesh/mesh-init.d.ts +128 -0
  113. package/dist/mesh/mesh-ledger-reconciliation.d.ts +23 -4
  114. package/dist/mesh/mesh-ledger.d.ts +139 -1
  115. package/dist/mesh/mesh-magi-status.d.ts +63 -0
  116. package/dist/mesh/mesh-missions.d.ts +281 -0
  117. package/dist/mesh/mesh-node-identity.d.ts +310 -0
  118. package/dist/mesh/mesh-queue-assignment.d.ts +162 -0
  119. package/dist/mesh/mesh-reconcile-acked-hold.d.ts +17 -0
  120. package/dist/mesh/mesh-reconcile-config.d.ts +6 -0
  121. package/dist/mesh/mesh-reconcile-identity.d.ts +6 -0
  122. package/dist/mesh/mesh-reconcile-loop.d.ts +84 -0
  123. package/dist/mesh/mesh-reconcile-v2-backstop.d.ts +16 -0
  124. package/dist/mesh/mesh-refine-batch.d.ts +68 -0
  125. package/dist/mesh/mesh-refine-gates.d.ts +457 -0
  126. package/dist/mesh/mesh-refine-status.d.ts +62 -0
  127. package/dist/mesh/mesh-remote-event-pull.d.ts +15 -0
  128. package/dist/mesh/mesh-review-inbox.d.ts +90 -0
  129. package/dist/mesh/mesh-routing.d.ts +70 -0
  130. package/dist/mesh/mesh-runtime-store.d.ts +584 -0
  131. package/dist/mesh/mesh-scheduling-runtime.d.ts +78 -0
  132. package/dist/mesh/mesh-task-inflight.d.ts +46 -0
  133. package/dist/mesh/mesh-task-stats.d.ts +49 -0
  134. package/dist/mesh/mesh-unresolved-forward-outbox.d.ts +34 -0
  135. package/dist/mesh/mesh-warmup-deadline.d.ts +68 -0
  136. package/dist/mesh/mesh-work-queue.d.ts +403 -6
  137. package/dist/mesh/preview-freshness.d.ts +28 -0
  138. package/dist/mesh/refine-config.d.ts +216 -0
  139. package/dist/mesh/worktree-bootstrap-config.d.ts +219 -0
  140. package/dist/providers/acp-provider-instance.d.ts +5 -0
  141. package/dist/providers/approval-utils.d.ts +46 -0
  142. package/dist/providers/chat-message-normalization.d.ts +58 -0
  143. package/dist/providers/cli-provider-effect-format.d.ts +30 -0
  144. package/dist/providers/cli-provider-history-dedup.d.ts +17 -0
  145. package/dist/providers/cli-provider-input-prompt.d.ts +12 -0
  146. package/dist/providers/cli-provider-instance-types.d.ts +47 -0
  147. package/dist/providers/cli-provider-instance.d.ts +461 -30
  148. package/dist/providers/cli-provider-status-helpers.d.ts +46 -0
  149. package/dist/providers/cli-provider-transcript-merge.d.ts +7 -0
  150. package/dist/providers/contracts.d.ts +196 -6
  151. package/dist/providers/external-sources.d.ts +71 -0
  152. package/dist/providers/manual-attendance.d.ts +79 -0
  153. package/dist/providers/native-history/antigravity-claim-registry.d.ts +39 -0
  154. package/dist/providers/native-history/antigravity-cli-transcript.d.ts +137 -0
  155. package/dist/providers/native-history/claude-cli-transcript.d.ts +70 -0
  156. package/dist/providers/native-history/codex-cli-transcript.d.ts +73 -0
  157. package/dist/providers/native-history/constants.d.ts +12 -0
  158. package/dist/providers/native-history/dispatcher.d.ts +44 -0
  159. package/dist/providers/native-history/hermes-cli-transcript.d.ts +30 -0
  160. package/dist/providers/native-history/index.d.ts +13 -0
  161. package/dist/providers/provider-instance-manager.d.ts +30 -0
  162. package/dist/providers/provider-instance.d.ts +31 -1
  163. package/dist/providers/provider-loader.d.ts +42 -5
  164. package/dist/providers/provider-trust.d.ts +31 -0
  165. package/dist/providers/read-chat-contract.d.ts +29 -0
  166. package/dist/providers/sdk/v1/builders/acp/detect-status.d.ts +68 -0
  167. package/dist/providers/sdk/v1/builders/cli/detect-status.d.ts +86 -0
  168. package/dist/providers/sdk/v1/builders/cli/parse-approval-squash.d.ts +59 -0
  169. package/dist/providers/sdk/v1/builders/cli/parse-approval.d.ts +65 -0
  170. package/dist/providers/sdk/v1/builders/cli/parse-session.d.ts +91 -0
  171. package/dist/providers/sdk/v1/builders/cli/visible-region.d.ts +42 -0
  172. package/dist/providers/sdk/v1/fixture-tooling/format.d.ts +126 -0
  173. package/dist/providers/sdk/v1/fixture-tooling/index.d.ts +8 -0
  174. package/dist/providers/sdk/v1/fixture-tooling/replay.d.ts +38 -0
  175. package/dist/providers/sdk/v1/index.d.ts +30 -0
  176. package/dist/providers/sdk/v1/sandbox/README-design.d.ts +193 -0
  177. package/dist/providers/sdk/v1/sandbox/require-whitelist.d.ts +74 -0
  178. package/dist/providers/sdk/v1/sandbox/script-runner.d.ts +98 -0
  179. package/dist/providers/sdk/v1/types/cli/index.d.ts +277 -0
  180. package/dist/providers/sdk/v1/types/common/index.d.ts +203 -0
  181. package/dist/providers/sdk/v1/validators/index.d.ts +5 -0
  182. package/dist/providers/sdk/v1/validators/manifest.d.ts +40 -0
  183. package/dist/providers/sdk/v1/validators/taint.d.ts +52 -0
  184. package/dist/providers/spec/adapter.d.ts +98 -0
  185. package/dist/providers/spec/background-task-detector.d.ts +29 -0
  186. package/dist/providers/spec/cli-adapter.d.ts +224 -0
  187. package/dist/providers/spec/evaluator.d.ts +45 -0
  188. package/dist/providers/spec/fsm-driver.d.ts +449 -0
  189. package/dist/providers/spec/fsm-evaluator.d.ts +93 -0
  190. package/dist/providers/spec/fsm-loader.d.ts +14 -0
  191. package/dist/providers/spec/fsm-types.d.ts +202 -0
  192. package/dist/providers/spec/native-history-executor.d.ts +50 -0
  193. package/dist/providers/spec/pre-launch-trust.d.ts +16 -0
  194. package/dist/providers/spec/route.d.ts +4 -0
  195. package/dist/providers/spec/types.d.ts +244 -0
  196. package/dist/providers/status-monitor.d.ts +7 -7
  197. package/dist/providers/transcript-v2.d.ts +176 -0
  198. package/dist/providers/types/interactive-prompt.d.ts +84 -0
  199. package/dist/providers/working-dir.d.ts +17 -0
  200. package/dist/repo-mesh-types.d.ts +778 -3
  201. package/dist/runtime-defaults.d.ts +2 -0
  202. package/dist/session-host/managed-host.d.ts +64 -0
  203. package/dist/session-host/runtime-surface.d.ts +10 -16
  204. package/dist/sessions/registry.d.ts +26 -0
  205. package/dist/shared-types-extra.d.ts +2 -4
  206. package/dist/shared-types.d.ts +110 -55
  207. package/dist/status/chat-tail-hot-sessions.d.ts +40 -0
  208. package/dist/status/normalize.d.ts +1 -1
  209. package/dist/status/normalize.js +1 -0
  210. package/dist/status/normalize.js.map +1 -1
  211. package/dist/status/normalize.mjs +1 -0
  212. package/dist/status/normalize.mjs.map +1 -1
  213. package/dist/status/reporter.d.ts +2 -0
  214. package/dist/status/snapshot.d.ts +27 -0
  215. package/dist/system/hash.d.ts +8 -0
  216. package/dist/system/load-better-sqlite3.d.ts +21 -0
  217. package/dist/types.d.ts +20 -6
  218. package/package.json +11 -4
  219. package/src/agent-stream/poller.ts +2 -3
  220. package/src/agent-stream/provider-adapter.ts +1 -1
  221. package/src/boot/daemon-lifecycle.ts +83 -14
  222. package/src/boot/process-hardening.ts +89 -0
  223. package/src/build-info.ts +73 -0
  224. package/src/chat/source-machine.ts +534 -0
  225. package/src/chat/source-resolver.ts +0 -0
  226. package/src/chat/subscription-updates.ts +20 -1
  227. package/src/cli-adapter-types.d.ts +3 -1
  228. package/src/cli-adapter-types.ts +85 -2
  229. package/src/cli-adapters/cli-script-runner.ts +421 -0
  230. package/src/cli-adapters/cli-state-engine.ts +1379 -0
  231. package/src/cli-adapters/provider-cli-adapter.d.ts +1 -1
  232. package/src/cli-adapters/provider-cli-adapter.ts +1059 -1143
  233. package/src/cli-adapters/provider-cli-parse.d.ts +2 -0
  234. package/src/cli-adapters/provider-cli-parse.ts +19 -0
  235. package/src/cli-adapters/provider-cli-runtime.ts +56 -7
  236. package/src/cli-adapters/provider-cli-shared.d.ts +4 -0
  237. package/src/cli-adapters/provider-cli-shared.ts +121 -11
  238. package/src/cli-adapters/pty-transport.ts +2 -1
  239. package/src/cli-adapters/pty-write-chunking.ts +106 -0
  240. package/src/cli-adapters/raw-terminal-io.ts +252 -0
  241. package/src/cli-adapters/resolve-executable.ts +204 -0
  242. package/src/cli-adapters/session-host-transport.ts +2 -1
  243. package/src/cli-adapters/terminal-backends/ghostty-vt-backend.ts +63 -29
  244. package/src/cli-adapters/terminal-backends/types.ts +10 -3
  245. package/src/cli-adapters/terminal-screen.ts +16 -81
  246. package/src/commands/chat-commands-debug-bundle.ts +398 -0
  247. package/src/commands/chat-commands-read.ts +2788 -0
  248. package/src/commands/chat-commands-scope.ts +54 -0
  249. package/src/commands/chat-commands-shared.ts +114 -0
  250. package/src/commands/chat-commands-write.ts +891 -0
  251. package/src/commands/chat-commands.ts +19 -1841
  252. package/src/commands/cli-manager.d.ts +2 -0
  253. package/src/commands/cli-manager.ts +724 -27
  254. package/src/commands/handler.ts +865 -2
  255. package/src/commands/high-family/index.ts +28 -0
  256. package/src/commands/high-family/mesh-coordinator-launch.ts +696 -0
  257. package/src/commands/high-family/mesh-events.ts +102 -0
  258. package/src/commands/high-family/mesh-status.ts +857 -0
  259. package/src/commands/high-family/types.ts +80 -0
  260. package/src/commands/low-family/coordinator-prompt.ts +125 -0
  261. package/src/commands/low-family/daemon-lifecycle.ts +120 -0
  262. package/src/commands/low-family/diagnostics.ts +57 -0
  263. package/src/commands/low-family/index.ts +37 -0
  264. package/src/commands/low-family/mesh-ledger.ts +62 -0
  265. package/src/commands/low-family/mesh-node-logs.ts +87 -0
  266. package/src/commands/low-family/notification.ts +116 -0
  267. package/src/commands/low-family/refine-config.ts +106 -0
  268. package/src/commands/low-family/session-host.ts +282 -0
  269. package/src/commands/low-family/spec-providerdev.ts +217 -0
  270. package/src/commands/low-family/status-meta.ts +112 -0
  271. package/src/commands/low-family/types.ts +39 -0
  272. package/src/commands/med-family/cli-agent.ts +270 -0
  273. package/src/commands/med-family/fast-forward.ts +230 -0
  274. package/src/commands/med-family/ide.ts +163 -0
  275. package/src/commands/med-family/index.ts +37 -0
  276. package/src/commands/med-family/mesh-crud.ts +1309 -0
  277. package/src/commands/med-family/mesh-host-pairing.ts +234 -0
  278. package/src/commands/med-family/mesh-queue.ts +167 -0
  279. package/src/commands/med-family/mesh-restart.ts +92 -0
  280. package/src/commands/med-family/types.ts +153 -0
  281. package/src/commands/mesh-coordinator.ts +334 -124
  282. package/src/commands/router-aggregate-status.ts +209 -0
  283. package/src/commands/router-mesh-session-owner.ts +114 -0
  284. package/src/commands/router-refine.ts +1794 -0
  285. package/src/commands/router-worktree-cleanup.ts +870 -0
  286. package/src/commands/router.ts +657 -2704
  287. package/src/commands/stream-commands.ts +8 -0
  288. package/src/commands/upgrade-helper.ts +310 -45
  289. package/src/config/chat-history.ts +517 -24
  290. package/src/config/config.ts +30 -0
  291. package/src/config/mesh-config.ts +542 -23
  292. package/src/config/mesh-json-config.ts +376 -0
  293. package/src/config/recent-activity.ts +8 -2
  294. package/src/config/registry-resolver.ts +100 -0
  295. package/src/config/repo-settings.ts +111 -0
  296. package/src/config/state-store.ts +55 -0
  297. package/src/daemon/dev-auto-implement.ts +3 -2
  298. package/src/daemon/dev-cli-debug.ts +10 -1
  299. package/src/daemon/dev-server.ts +0 -541
  300. package/src/detection/cli-detector.ts +94 -9
  301. package/src/detection/ide-detector.ts +55 -16
  302. package/src/detection/win32-ide-version.ts +106 -0
  303. package/src/git/change-impact-config.ts +354 -0
  304. package/src/git/git-commands.ts +113 -17
  305. package/src/git/git-diff.ts +81 -11
  306. package/src/git/git-executor.ts +12 -0
  307. package/src/git/git-status.ts +916 -51
  308. package/src/git/git-summary.ts +3 -0
  309. package/src/git/git-types.ts +17 -54
  310. package/src/git/git-worktree.ts +261 -4
  311. package/src/git/index.ts +17 -0
  312. package/src/index.ts +221 -13
  313. package/src/installer.d.ts +1 -1
  314. package/src/installer.ts +8 -6
  315. package/src/ipc/local-ipc-server.ts +278 -0
  316. package/src/launch.d.ts +1 -1
  317. package/src/launch.ts +37 -28
  318. package/src/logging/async-batch-writer.ts +55 -0
  319. package/src/logging/command-log.ts +7 -5
  320. package/src/logging/debug-config.ts +25 -0
  321. package/src/logging/debug-trace.ts +7 -2
  322. package/src/logging/log-redactor.ts +100 -0
  323. package/src/logging/log-tail-reader.ts +341 -0
  324. package/src/logging/logger.ts +14 -7
  325. package/src/mesh/contracts.ts +510 -0
  326. package/src/mesh/coordinator-prompt.ts +626 -34
  327. package/src/mesh/coordinator-registry.ts +121 -0
  328. package/src/mesh/mesh-active-work.ts +706 -0
  329. package/src/mesh/mesh-clone-grace.ts +68 -0
  330. package/src/mesh/mesh-completion-synthesis.ts +416 -0
  331. package/src/mesh/mesh-coordinator-config.ts +97 -0
  332. package/src/mesh/mesh-delivery-policy.ts +284 -0
  333. package/src/mesh/mesh-event-classify.ts +78 -0
  334. package/src/mesh/mesh-event-forwarding.ts +2042 -0
  335. package/src/mesh/mesh-event-trace.ts +67 -0
  336. package/src/mesh/mesh-events-coordinator.ts +32 -0
  337. package/src/mesh/mesh-events-pending.ts +1548 -0
  338. package/src/mesh/mesh-events-stale.ts +452 -0
  339. package/src/mesh/mesh-events-utils.ts +443 -0
  340. package/src/mesh/mesh-events.ts +43 -897
  341. package/src/mesh/mesh-fast-forward.ts +856 -0
  342. package/src/mesh/mesh-host-ownership.ts +111 -0
  343. package/src/mesh/mesh-idle-reminder.ts +153 -0
  344. package/src/mesh/mesh-init.ts +350 -0
  345. package/src/mesh/mesh-ledger-reconciliation.ts +12 -7
  346. package/src/mesh/mesh-ledger.ts +898 -102
  347. package/src/mesh/mesh-magi-status.ts +223 -0
  348. package/src/mesh/mesh-missions.ts +695 -0
  349. package/src/mesh/mesh-node-identity.ts +2065 -0
  350. package/src/mesh/mesh-queue-assignment.ts +2498 -0
  351. package/src/mesh/mesh-reconcile-acked-hold.ts +230 -0
  352. package/src/mesh/mesh-reconcile-config.ts +66 -0
  353. package/src/mesh/mesh-reconcile-identity.ts +103 -0
  354. package/src/mesh/mesh-reconcile-loop.ts +1709 -0
  355. package/src/mesh/mesh-reconcile-v2-backstop.ts +62 -0
  356. package/src/mesh/mesh-refine-batch.ts +205 -0
  357. package/src/mesh/mesh-refine-gates.ts +1792 -0
  358. package/src/mesh/mesh-refine-status.ts +231 -0
  359. package/src/mesh/mesh-remote-event-pull.ts +286 -0
  360. package/src/mesh/mesh-review-inbox.ts +308 -0
  361. package/src/mesh/mesh-routing.ts +291 -0
  362. package/src/mesh/mesh-runtime-store.ts +2318 -0
  363. package/src/mesh/mesh-scheduling-runtime.ts +198 -0
  364. package/src/mesh/mesh-task-inflight.ts +70 -0
  365. package/src/mesh/mesh-task-stats.ts +161 -0
  366. package/src/mesh/mesh-unresolved-forward-outbox.ts +215 -0
  367. package/src/mesh/mesh-warmup-deadline.ts +152 -0
  368. package/src/mesh/mesh-work-queue.ts +1532 -161
  369. package/src/mesh/p2p-relay-failure.ts +2 -2
  370. package/src/mesh/preview-freshness.ts +163 -0
  371. package/src/mesh/refine-config.ts +423 -0
  372. package/src/mesh/worktree-bootstrap-config.ts +599 -0
  373. package/src/providers/acp-provider-instance.ts +43 -10
  374. package/src/providers/approval-utils.d.ts +5 -0
  375. package/src/providers/approval-utils.ts +100 -6
  376. package/src/providers/chat-message-normalization.ts +128 -4
  377. package/src/providers/cli-provider-effect-format.ts +53 -0
  378. package/src/providers/cli-provider-history-dedup.ts +75 -0
  379. package/src/providers/cli-provider-input-prompt.ts +133 -0
  380. package/src/providers/cli-provider-instance-types.ts +150 -0
  381. package/src/providers/cli-provider-instance.ts +2914 -498
  382. package/src/providers/cli-provider-status-helpers.ts +123 -0
  383. package/src/providers/cli-provider-transcript-merge.ts +114 -0
  384. package/src/providers/contracts.d.ts +55 -0
  385. package/src/providers/contracts.ts +207 -6
  386. package/src/providers/extension-provider-instance.ts +12 -7
  387. package/src/providers/external-sources.ts +218 -0
  388. package/src/providers/ide-provider-instance.ts +35 -12
  389. package/src/providers/manual-attendance.ts +105 -0
  390. package/src/providers/native-history/antigravity-claim-registry.ts +138 -0
  391. package/src/providers/native-history/antigravity-cli-transcript.ts +1275 -0
  392. package/src/providers/native-history/claude-cli-transcript.ts +396 -0
  393. package/src/providers/native-history/codex-cli-transcript.ts +479 -0
  394. package/src/providers/native-history/constants.ts +19 -0
  395. package/src/providers/native-history/dispatcher.ts +617 -0
  396. package/src/providers/native-history/hermes-cli-transcript.ts +312 -0
  397. package/src/providers/native-history/index.ts +30 -0
  398. package/src/providers/provider-instance-manager.ts +71 -0
  399. package/src/providers/provider-instance.ts +24 -1
  400. package/src/providers/provider-loader.ts +700 -55
  401. package/src/providers/provider-schema.ts +93 -14
  402. package/src/providers/provider-trust.ts +114 -0
  403. package/src/providers/read-chat-contract.ts +76 -16
  404. package/src/providers/sdk/README.md +49 -0
  405. package/src/providers/sdk/v1/builders/acp/detect-status.ts +144 -0
  406. package/src/providers/sdk/v1/builders/cli/detect-status.ts +313 -0
  407. package/src/providers/sdk/v1/builders/cli/parse-approval-squash.ts +158 -0
  408. package/src/providers/sdk/v1/builders/cli/parse-approval.ts +261 -0
  409. package/src/providers/sdk/v1/builders/cli/parse-session.ts +276 -0
  410. package/src/providers/sdk/v1/builders/cli/visible-region.ts +143 -0
  411. package/src/providers/sdk/v1/fixture-tooling/format.ts +130 -0
  412. package/src/providers/sdk/v1/fixture-tooling/index.ts +22 -0
  413. package/src/providers/sdk/v1/fixture-tooling/replay.ts +352 -0
  414. package/src/providers/sdk/v1/index.ts +152 -0
  415. package/src/providers/sdk/v1/sandbox/README-design.ts +195 -0
  416. package/src/providers/sdk/v1/sandbox/require-whitelist.ts +472 -0
  417. package/src/providers/sdk/v1/sandbox/script-runner.ts +150 -0
  418. package/src/providers/sdk/v1/schemas/cli/provider.schema.json +538 -0
  419. package/src/providers/sdk/v1/schemas/primitives/acp-session-protocol-v1.json +131 -0
  420. package/src/providers/sdk/v1/schemas/primitives/native-history-codex-rollout-v1.json +66 -0
  421. package/src/providers/sdk/v1/schemas/primitives/tui-approval-squash-v1.json +91 -0
  422. package/src/providers/sdk/v1/schemas/primitives/tui-assistant-block-v1.json +91 -0
  423. package/src/providers/sdk/v1/schemas/primitives/tui-cue-ordering-v1.json +47 -0
  424. package/src/providers/sdk/v1/schemas/primitives/tui-dispatch-order-v1.json +32 -0
  425. package/src/providers/sdk/v1/schemas/primitives/tui-footer-chrome-v1.json +42 -0
  426. package/src/providers/sdk/v1/schemas/primitives/tui-index-finder-v1.json +27 -0
  427. package/src/providers/sdk/v1/schemas/primitives/tui-modal-v1.json +125 -0
  428. package/src/providers/sdk/v1/schemas/primitives/tui-prompt-marker-v1.json +45 -0
  429. package/src/providers/sdk/v1/schemas/primitives/tui-session-id-extraction-v1.json +46 -0
  430. package/src/providers/sdk/v1/schemas/primitives/tui-settled-prompt-v1.json +71 -0
  431. package/src/providers/sdk/v1/schemas/primitives/tui-spinner-v1.json +83 -0
  432. package/src/providers/sdk/v1/schemas/primitives/tui-transcript-pty-v1.json +83 -0
  433. package/src/providers/sdk/v1/schemas/primitives/tui-visible-region-v1.json +57 -0
  434. package/src/providers/sdk/v1/schemas/primitives/tui-welcome-screen-v1.json +35 -0
  435. package/src/providers/sdk/v1/types/cli/index.ts +365 -0
  436. package/src/providers/sdk/v1/types/common/index.ts +229 -0
  437. package/src/providers/sdk/v1/validators/index.ts +19 -0
  438. package/src/providers/sdk/v1/validators/manifest.ts +110 -0
  439. package/src/providers/sdk/v1/validators/taint.ts +309 -0
  440. package/src/providers/spec/adapter.ts +246 -0
  441. package/src/providers/spec/background-task-detector.ts +178 -0
  442. package/src/providers/spec/cli-adapter.ts +1090 -0
  443. package/src/providers/spec/evaluator.ts +407 -0
  444. package/src/providers/spec/fsm-driver.ts +1528 -0
  445. package/src/providers/spec/fsm-evaluator.ts +290 -0
  446. package/src/providers/spec/fsm-loader.ts +128 -0
  447. package/src/providers/spec/fsm-types.ts +301 -0
  448. package/src/providers/spec/native-history-executor.ts +1256 -0
  449. package/src/providers/spec/pre-launch-trust.ts +104 -0
  450. package/src/providers/spec/route.ts +51 -0
  451. package/src/providers/spec/types.ts +284 -0
  452. package/src/providers/status-monitor.d.ts +7 -7
  453. package/src/providers/status-monitor.ts +37 -22
  454. package/src/providers/transcript-v2.ts +567 -0
  455. package/src/providers/types/interactive-prompt.ts +536 -0
  456. package/src/providers/version-archive.ts +64 -24
  457. package/src/providers/working-dir.ts +23 -0
  458. package/src/repo-mesh-types.ts +1040 -5
  459. package/src/runtime-defaults.ts +39 -0
  460. package/src/session-host/managed-host.ts +218 -0
  461. package/src/session-host/runtime-surface.ts +20 -80
  462. package/src/sessions/registry.ts +44 -0
  463. package/src/shared-types-extra.ts +2 -4
  464. package/src/shared-types.d.ts +8 -0
  465. package/src/shared-types.ts +148 -55
  466. package/src/status/builders.ts +69 -6
  467. package/src/status/chat-tail-hot-sessions.ts +117 -2
  468. package/src/status/normalize.ts +2 -0
  469. package/src/status/reporter.ts +19 -1
  470. package/src/status/snapshot.ts +100 -27
  471. package/src/system/hash.ts +23 -0
  472. package/src/system/host-memory.ts +29 -12
  473. package/src/system/load-better-sqlite3.ts +68 -0
  474. package/src/types.ts +26 -6
  475. package/dist/cli-adapters/terminal-backends/xterm-backend.d.ts +0 -17
  476. package/dist/mesh/mesh-sync.d.ts +0 -53
  477. package/src/cli-adapters/terminal-backends/ghostty-vt-backend.d.ts +0 -16
  478. package/src/cli-adapters/terminal-backends/xterm-backend.ts +0 -97
  479. package/src/mesh/mesh-sync.ts +0 -111
@@ -9,615 +9,106 @@
9
9
  * 3. Everything else → delegated to commandHandler.handle()
10
10
  */
11
11
 
12
+
12
13
  import { DaemonCdpManager } from '../cdp/manager.js';
13
- import { registerExtensionProviders } from '../cdp/setup.js';
14
14
  import { DaemonCommandHandler } from './handler.js';
15
+ import { lowFamilyRegistry } from './low-family/index.js';
16
+ import { medFamilyRegistry } from './med-family/index.js';
17
+ import { launchIde } from './med-family/ide.js';
18
+ import type { MedFamilyContext } from './med-family/index.js';
19
+ import { highFamilyRegistry } from './high-family/index.js';
20
+ import type { HighFamilyContext } from './high-family/index.js';
15
21
  import { DaemonCliManager } from './cli-manager.js';
16
- import { supportsExplicitSessionResume } from './cli-manager.js';
17
- import type { HostedCliRuntimeDescriptor } from './cli-manager.js';
18
22
  import type { ProviderLoader } from '../providers/provider-loader.js';
19
23
  import type { ProviderInstanceManager } from '../providers/provider-instance-manager.js';
20
- import { launchWithCdp, killIdeProcess, isIdeRunning } from '../launch.js';
21
- import { loadConfig, saveConfig, updateConfig } from '../config/config.js';
22
- import { loadState, saveState } from '../config/state-store.js';
23
- import { resolveIdeLaunchWorkspace } from '../config/workspaces.js';
24
- import { appendRecentActivity, getRecentActivity, markSessionSeen, dismissSessionNotification, markSessionNotificationUnread } from '../config/recent-activity.js';
25
- import { getSavedProviderSessions } from '../config/saved-sessions.js';
26
- import { listProviderHistorySessions } from '../config/chat-history.js';
27
- import { detectIDEs } from '../detection/ide-detector.js';
28
- import { detectCLI } from '../detection/cli-detector.js';
29
- import { getGitRepoStatus } from '../git/git-status.js';
30
- import type { GitSubmoduleStatus } from '../git/git-types.js';
24
+ import { killIdeProcess, isIdeRunning } from '../launch.js';
25
+ import { normalizeMeshNodeId, meshNodeIdMatches } from '@adhdev/mesh-shared';
31
26
  import { SessionRegistry } from '../sessions/registry.js';
32
27
  import { LOG } from '../logging/logger.js';
33
28
  import { logCommand } from '../logging/command-log.js';
34
29
  import type { CommandLogEntry } from '../logging/command-log.js';
35
- import * as yaml from 'js-yaml';
36
- import { getRecentLogs, LOG_PATH } from '../logging/logger.js';
37
- import { createInteractionId, getRecentDebugTrace, recordDebugTrace } from '../logging/debug-trace.js';
38
- import { getSessionHostSurfaceKind, partitionSessionHostRecords } from '../session-host/runtime-surface.js';
39
- import { createHermesManualMeshCoordinatorSetup, resolveMeshCoordinatorSetup } from './mesh-coordinator.js';
40
- import { buildSessionEntries } from '../status/builders.js';
41
- import { handleMeshForwardEvent, drainPendingMeshCoordinatorEvents } from '../mesh/mesh-events.js';
42
- import { buildMachineInfo, buildStatusSnapshot } from '../status/snapshot.js';
43
- import { getSessionCompletionMarker } from '../status/snapshot.js';
44
- import { execNpmCommandSync, resolveCurrentGlobalInstallSurface, spawnDetachedDaemonUpgradeHelper } from './upgrade-helper.js';
30
+ import { createInteractionId, recordDebugTrace } from '../logging/debug-trace.js';
31
+ import { getSessionHostSurfaceKind } from '../session-host/runtime-surface.js';
32
+ import { handleMeshForwardEvent, queuePendingMeshCoordinatorEvent } from '../mesh/mesh-events.js';
33
+ import { buildMeshHostRequiredFailure, resolveMeshHostStatus } from '../mesh/mesh-host-ownership.js';
34
+ import { analyzeMeshRefineNodeChangeArea, orderMeshRefineBatchNodes } from '../mesh/mesh-refine-batch.js';
35
+ import type { WorktreeBootstrapState } from '../mesh/worktree-bootstrap-config.js';
36
+ import { getMeshQueueRevision } from '../mesh/mesh-work-queue.js';
45
37
  import type { RepoMeshSessionCleanupMode } from '../repo-mesh-types.js';
46
- import { homedir } from 'os';
47
- import { join as pathJoin, resolve as pathResolve } from 'path';
38
+ import { DEFAULT_MESH_POLICY, magiAutoLaunchedSessionCleanupDecision } from '../repo-mesh-types.js';
39
+ import { resolve as pathResolve } from 'path';
48
40
  import * as fs from 'fs';
49
-
50
- type ReleaseChannel = 'stable' | 'preview';
51
- const CHANNEL_NPM_TAG: Record<ReleaseChannel, 'latest' | 'next'> = { stable: 'latest', preview: 'next' };
52
- const CHANNEL_SERVER_URL: Record<ReleaseChannel, string> = {
53
- stable: 'https://api.adhf.dev',
54
- preview: 'https://api-preview.adhf.dev',
55
- };
56
-
57
- function normalizeReleaseChannel(value: unknown): ReleaseChannel | null {
58
- if (typeof value !== 'string') return null;
59
- const normalized = value.trim().toLowerCase();
60
- if (normalized === 'stable' || normalized === 'latest') return 'stable';
61
- if (normalized === 'preview' || normalized === 'next') return 'preview';
62
- return null;
63
- }
64
-
65
- function resolveUpgradeChannel(args: any): ReleaseChannel {
66
- return normalizeReleaseChannel(args?.channel)
67
- || normalizeReleaseChannel(args?.updatePolicy?.channel)
68
- || normalizeReleaseChannel(args?.npmTag)
69
- || normalizeReleaseChannel(loadConfig().updateChannel)
70
- || 'stable';
71
- }
72
-
73
- function readProviderPriorityFromPolicy(policy: unknown): string[] {
74
- const record = policy && typeof policy === 'object' && !Array.isArray(policy)
75
- ? policy as Record<string, unknown>
76
- : {};
77
- const raw = record.providerPriority;
78
- if (!Array.isArray(raw)) return [];
79
- const seen = new Set<string>();
80
- return raw
81
- .map(type => typeof type === 'string' ? type.trim() : '')
82
- .filter(Boolean)
83
- .filter(type => {
84
- if (seen.has(type)) return false;
85
- seen.add(type);
86
- return true;
87
- });
88
- }
89
-
90
- function readObjectRecord(value: unknown): Record<string, any> {
91
- return value && typeof value === 'object' && !Array.isArray(value)
92
- ? value as Record<string, any>
93
- : {};
94
- }
95
-
96
- function readStringValue(...values: unknown[]): string | undefined {
97
- for (const value of values) {
98
- if (typeof value === 'string' && value.trim()) return value.trim();
99
- }
100
- return undefined;
101
- }
102
-
103
- function readNumberValue(...values: unknown[]): number | undefined {
104
- for (const value of values) {
105
- if (typeof value === 'number' && Number.isFinite(value)) return value;
106
- }
107
- return undefined;
108
- }
109
-
110
- function readBooleanValue(...values: unknown[]): boolean | undefined {
111
- for (const value of values) {
112
- if (typeof value === 'boolean') return value;
113
- }
114
- return undefined;
115
- }
116
-
117
- function readGitSubmodules(value: unknown): GitSubmoduleStatus[] | undefined {
118
- if (!Array.isArray(value)) return undefined;
119
- const submodules = value
120
- .map(entry => {
121
- const submodule = readObjectRecord(entry);
122
- const path = readStringValue(submodule.path);
123
- const commit = readStringValue(submodule.commit);
124
- const repoPath = readStringValue(submodule.repoPath, submodule.repo_root);
125
- if (!path || !commit || !repoPath) return null;
126
- return {
127
- path,
128
- commit,
129
- repoPath,
130
- dirty: readBooleanValue(submodule.dirty) ?? false,
131
- outOfSync: readBooleanValue(submodule.outOfSync, submodule.out_of_sync) ?? false,
132
- lastCheckedAt: readNumberValue(submodule.lastCheckedAt, submodule.last_checked_at) ?? Date.now(),
133
- ...(readStringValue(submodule.error) ? { error: readStringValue(submodule.error) } : {}),
134
- };
135
- })
136
- .filter((entry): entry is GitSubmoduleStatus => entry !== null);
137
- return submodules.length > 0 ? submodules : undefined;
138
- }
139
-
140
- function buildCachedInlineMeshGitStatus(node: any): Record<string, unknown> | undefined {
141
- const cachedStatus = readObjectRecord(node?.cachedStatus);
142
- const cachedGit = readObjectRecord(cachedStatus.git);
143
- if (Object.keys(cachedGit).length) {
144
- const conflictFiles = Array.isArray(cachedGit.conflictFiles)
145
- ? cachedGit.conflictFiles.filter((value: unknown): value is string => typeof value === 'string')
146
- : [];
147
- const conflictCount = readNumberValue(cachedGit.conflicts) ?? conflictFiles.length;
148
- const hasConflicts = readBooleanValue(cachedGit.hasConflicts) ?? conflictCount > 0;
149
- const isGitRepo = readBooleanValue(cachedGit.isGitRepo);
150
- if (isGitRepo !== undefined) {
151
- const submodules = readGitSubmodules(cachedGit.submodules);
152
- return {
153
- workspace: readStringValue(cachedGit.workspace, node?.workspace) || '',
154
- repoRoot: readStringValue(cachedGit.repoRoot, node?.repoRoot, node?.workspace) || null,
155
- isGitRepo,
156
- branch: readStringValue(cachedGit.branch) ?? null,
157
- headCommit: readStringValue(cachedGit.headCommit) ?? null,
158
- headMessage: readStringValue(cachedGit.headMessage) ?? null,
159
- upstream: readStringValue(cachedGit.upstream) ?? null,
160
- ahead: readNumberValue(cachedGit.ahead) ?? 0,
161
- behind: readNumberValue(cachedGit.behind) ?? 0,
162
- staged: readNumberValue(cachedGit.staged) ?? 0,
163
- modified: readNumberValue(cachedGit.modified) ?? 0,
164
- untracked: readNumberValue(cachedGit.untracked) ?? 0,
165
- deleted: readNumberValue(cachedGit.deleted) ?? 0,
166
- renamed: readNumberValue(cachedGit.renamed) ?? 0,
167
- hasConflicts,
168
- conflictFiles,
169
- stashCount: readNumberValue(cachedGit.stashCount) ?? 0,
170
- lastCheckedAt: readNumberValue(cachedGit.lastCheckedAt) ?? Date.now(),
171
- ...(submodules ? { submodules } : {}),
172
- };
173
- }
174
- }
175
-
176
- const rawGit = readObjectRecord(node?.lastGit ?? node?.last_git);
177
- const gitResult = readObjectRecord(rawGit.result);
178
- const directStatus = readObjectRecord(rawGit.status);
179
- const nestedStatus = readObjectRecord(gitResult.status);
180
- const rawProbe = readObjectRecord(node?.lastProbe ?? node?.last_probe);
181
- const probeGit = readObjectRecord(rawProbe.git);
182
- const probeGitResult = readObjectRecord(probeGit.result);
183
- const probeDirectStatus = readObjectRecord(probeGit.status);
184
- const probeNestedStatus = readObjectRecord(probeGitResult.status);
185
- const status = Object.keys(directStatus).length
186
- ? directStatus
187
- : Object.keys(nestedStatus).length
188
- ? nestedStatus
189
- : Object.keys(probeDirectStatus).length
190
- ? probeDirectStatus
191
- : Object.keys(probeNestedStatus).length
192
- ? probeNestedStatus
193
- : {};
194
- const isGitRepo = readBooleanValue(status.isGitRepo);
195
- if (!Object.keys(status).length || isGitRepo === undefined) return undefined;
196
- const conflictFiles = Array.isArray(status.conflictFiles)
197
- ? status.conflictFiles.filter((value: unknown): value is string => typeof value === 'string')
198
- : [];
199
- const conflictCount = readNumberValue(status.conflicts) ?? conflictFiles.length;
200
- const hasConflicts = readBooleanValue(status.hasConflicts) ?? conflictCount > 0;
201
- const submodules = readGitSubmodules(status.submodules);
202
- return {
203
- workspace: readStringValue(status.workspace, node?.workspace) || '',
204
- repoRoot: readStringValue(status.repoRoot, node?.repoRoot, node?.workspace) || null,
205
- isGitRepo,
206
- branch: readStringValue(status.branch) ?? null,
207
- headCommit: readStringValue(status.headCommit) ?? null,
208
- headMessage: readStringValue(status.headMessage) ?? null,
209
- upstream: readStringValue(status.upstream) ?? null,
210
- ahead: readNumberValue(status.ahead) ?? 0,
211
- behind: readNumberValue(status.behind) ?? 0,
212
- staged: readNumberValue(status.staged) ?? 0,
213
- modified: readNumberValue(status.modified) ?? 0,
214
- untracked: readNumberValue(status.untracked) ?? 0,
215
- deleted: readNumberValue(status.deleted) ?? 0,
216
- renamed: readNumberValue(status.renamed) ?? 0,
217
- hasConflicts,
218
- conflictFiles,
219
- stashCount: readNumberValue(status.stashCount) ?? 0,
220
- lastCheckedAt: Date.now(),
221
- ...(submodules ? { submodules } : {}),
222
- };
223
- }
224
-
225
- function applyCachedInlineMeshNodeStatus(status: Record<string, unknown>, node: any): boolean {
226
- const cachedStatus = readObjectRecord(node?.cachedStatus);
227
- const git = buildCachedInlineMeshGitStatus(node);
228
- const error = readStringValue(cachedStatus.error, node?.error);
229
- const health = readStringValue(cachedStatus.health, node?.health);
230
- const machineStatus = readStringValue(cachedStatus.machineStatus, node?.machineStatus);
231
- if (!git && !error && !health) return false;
232
- if (!machineStatus && !git && !error) return false;
233
- if (git) status.git = git;
234
- if (error) status.error = error;
235
- if (health) {
236
- status.health = health;
237
- return true;
238
- }
239
- if (git) {
240
- const dirty = Number(git.staged || 0) + Number(git.modified || 0) + Number(git.untracked || 0) + Number(git.deleted || 0) + Number(git.renamed || 0) > 0;
241
- status.health = git.isGitRepo === false ? 'degraded' : dirty ? 'dirty' : 'online';
242
- return true;
243
- }
244
- return false;
245
- }
246
-
247
- async function resolveProviderTypeFromPriority(args: {
248
- nodeId: string;
249
- providerPriority: string[];
250
- providerLoader: ProviderLoader;
251
- onStatusChange?: () => void;
252
- }): Promise<{ providerType?: string; error?: string }> {
253
- if (!args.providerPriority.length) {
254
- return { error: `Node '${args.nodeId}' has no providerPriority policy; pass cliType explicitly or configure node.policy.providerPriority` };
255
- }
256
-
257
- const failed: string[] = [];
258
- for (const requestedType of args.providerPriority) {
259
- const normalizedType = args.providerLoader.resolveAlias(requestedType);
260
- if (!args.providerLoader.isMachineProviderEnabled(normalizedType)) {
261
- failed.push(`${requestedType}: disabled`);
262
- continue;
263
- }
264
- const detected = await detectCLI(normalizedType, args.providerLoader, { includeVersion: false });
265
- args.providerLoader.setCliDetectionResults([{
266
- id: normalizedType,
267
- installed: !!detected,
268
- path: detected?.path,
269
- }], false);
270
- args.onStatusChange?.();
271
- if (detected) return { providerType: normalizedType };
272
- failed.push(`${requestedType}: not detected`);
273
- }
274
-
275
- return { error: `No usable provider detected for node '${args.nodeId}' from providerPriority: ${failed.join('; ')}` };
276
- }
277
- type MeshCoordinatorConfigFormat = 'claude_mcp_json' | 'hermes_config_yaml';
278
- type MeshRefineValidationStatus = 'passed' | 'failed' | 'skipped';
279
- type MeshRefineValidationCommand = {
280
- command: string;
281
- args: string[];
282
- displayCommand: string;
283
- category: string;
284
- source: string;
285
- };
286
-
287
- type MeshRefineValidationSummary = {
288
- status: MeshRefineValidationStatus;
289
- required: true;
290
- commandsRun: Array<Record<string, unknown>>;
291
- rejectedCommands: Array<Record<string, unknown>>;
292
- skippedReason?: string;
293
- timeoutMs: number;
294
- outputLimitBytes: number;
295
- };
296
-
297
- const REFINE_VALIDATION_CATEGORIES = ['typecheck', 'test', 'lint', 'build'] as const;
298
- const REFINE_VALIDATION_TIMEOUT_MS = 120_000;
299
- const REFINE_VALIDATION_OUTPUT_LIMIT_BYTES = 128 * 1024;
300
- const REFINE_VALIDATION_SUMMARY_CHARS = 2_000;
301
- const REFINE_VALIDATION_MAX_COMMANDS = 4;
302
-
303
- function truncateValidationOutput(value: unknown): string {
304
- const text = typeof value === 'string' ? value : value == null ? '' : String(value);
305
- if (text.length <= REFINE_VALIDATION_SUMMARY_CHARS) return text;
306
- return `${text.slice(0, REFINE_VALIDATION_SUMMARY_CHARS)}\n[truncated ${text.length - REFINE_VALIDATION_SUMMARY_CHARS} chars]`;
307
- }
308
-
309
- function readPackageScripts(workspace: string): Record<string, string> {
310
- try {
311
- const packageJsonPath = pathJoin(workspace, 'package.json');
312
- const parsed = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
313
- return parsed?.scripts && typeof parsed.scripts === 'object' && !Array.isArray(parsed.scripts)
314
- ? parsed.scripts as Record<string, string>
315
- : {};
316
- } catch {
317
- return {};
318
- }
319
- }
320
-
321
- function tokenizeValidationCommand(command: string): string[] | null {
322
- const trimmed = command.trim();
323
- if (!trimmed) return null;
324
- // Fail closed: the gate never hands shell syntax to a shell. Package-manager
325
- // scripts are invoked via execFile(binary, args), and metacharacters/quotes are
326
- // rejected before tokenization so `npm run test && rm -rf` cannot be smuggled in.
327
- if (/[;&|<>`$\\\n\r'\"]/.test(trimmed)) return null;
328
- const tokens = trimmed.split(/\s+/).filter(Boolean);
329
- if (!tokens.length) return null;
330
- if (tokens.some(token => !/^[A-Za-z0-9_@./:=+-]+$/.test(token))) return null;
331
- return tokens;
332
- }
333
-
334
- function scriptMatchesValidationCategory(scriptName: string, category: string): boolean {
335
- return scriptName === category || scriptName.startsWith(`${category}:`);
336
- }
337
-
338
- function parsePackageManagerValidationCommand(
339
- rawCommand: string,
340
- category: string,
341
- scripts: Record<string, string>,
342
- source: string,
343
- ): { command?: MeshRefineValidationCommand; rejected?: Record<string, unknown> } {
344
- const tokens = tokenizeValidationCommand(rawCommand);
345
- if (!tokens) {
346
- return { rejected: { command: rawCommand, category, source, reason: 'unsafe command string is not allowlisted' } };
347
- }
348
-
349
- const [binary, second, third, ...rest] = tokens;
350
- let scriptName = '';
351
- let command = binary;
352
- let args: string[] = [];
353
-
354
- if ((binary === 'npm' || binary === 'pnpm' || binary === 'bun') && second === 'run' && third) {
355
- scriptName = third;
356
- args = ['run', scriptName, ...rest];
357
- } else if (binary === 'npm' && second === 'test' && !third) {
358
- scriptName = 'test';
359
- args = ['test'];
360
- } else if (binary === 'yarn' && second === 'run' && third) {
361
- scriptName = third;
362
- args = ['run', scriptName, ...rest];
363
- } else if (binary === 'yarn' && second && !third) {
364
- scriptName = second;
365
- args = [scriptName];
366
- } else {
367
- return { rejected: { command: rawCommand, category, source, reason: 'command is not a supported package-manager script invocation' } };
368
- }
369
-
370
- if (!scriptName || !Object.prototype.hasOwnProperty.call(scripts, scriptName)) {
371
- return { rejected: { command: rawCommand, category, source, script: scriptName, reason: 'script is not declared in package.json' } };
372
- }
373
- if (!scriptMatchesValidationCategory(scriptName, category)) {
374
- return { rejected: { command: rawCommand, category, source, script: scriptName, reason: 'script name is outside the validation category allowlist' } };
375
- }
376
-
377
- return {
378
- command: {
379
- command,
380
- args,
381
- displayCommand: [command, ...args].join(' '),
382
- category,
383
- source,
384
- },
385
- };
386
- }
387
-
388
- function collectProjectContextValidationCandidates(mesh: any): Array<{ command: string; category: string; source: string; confidence?: string }> {
389
- const commands = mesh?.projectContext?.commands;
390
- if (!commands || typeof commands !== 'object' || Array.isArray(commands)) return [];
391
- const candidates: Array<{ command: string; category: string; source: string; confidence?: string }> = [];
392
- for (const category of REFINE_VALIDATION_CATEGORIES) {
393
- const entries = Array.isArray(commands[category]) ? commands[category] : [];
394
- for (const entry of entries) {
395
- if (typeof entry?.command !== 'string') continue;
396
- candidates.push({
397
- command: entry.command,
398
- category,
399
- source: typeof entry.sourcePath === 'string' ? entry.sourcePath : 'projectContext.commands',
400
- confidence: typeof entry.confidence === 'string' ? entry.confidence : undefined,
401
- });
402
- }
403
- }
404
- return candidates.sort((a, b) => {
405
- const rank = (value?: string) => value === 'high' ? 0 : value === 'medium' ? 1 : 2;
406
- return rank(a.confidence) - rank(b.confidence);
407
- });
408
- }
409
-
410
- function collectPolicyValidationCandidates(mesh: any): Array<{ command: string; category: string; source: string }> {
411
- const policy = mesh?.policy && typeof mesh.policy === 'object' && !Array.isArray(mesh.policy) ? mesh.policy : {};
412
- const configured = Array.isArray(policy.validationCommands)
413
- ? policy.validationCommands
414
- : Array.isArray(policy.validationGate?.commands)
415
- ? policy.validationGate.commands
416
- : [];
417
- return configured
418
- .map((entry: any) => typeof entry === 'string' ? { command: entry, category: '', source: 'mesh.policy.validationCommands' } : entry)
419
- .filter((entry: any) => entry && typeof entry.command === 'string')
420
- .map((entry: any) => {
421
- const commandText = entry.command.trim();
422
- const category = REFINE_VALIDATION_CATEGORIES.find(cat => commandText.includes(` ${cat}`)) ?? '';
423
- return { command: commandText, category, source: 'mesh.policy.validationCommands' };
424
- })
425
- .filter((entry: any) => !!entry.category);
426
- }
427
-
428
- function selectMeshRefineValidationCommands(mesh: any, workspace: string): { commands: MeshRefineValidationCommand[]; rejectedCommands: Array<Record<string, unknown>>; source: string } {
429
- const scripts = readPackageScripts(workspace);
430
- const rejectedCommands: Array<Record<string, unknown>> = [];
431
- const selected: MeshRefineValidationCommand[] = [];
432
- const seen = new Set<string>();
433
- const candidates = [
434
- ...collectPolicyValidationCandidates(mesh),
435
- ...collectProjectContextValidationCandidates(mesh),
436
- ];
437
-
438
- for (const candidate of candidates) {
439
- const parsed = parsePackageManagerValidationCommand(candidate.command, candidate.category, scripts, candidate.source);
440
- if (parsed.rejected) {
441
- rejectedCommands.push(parsed.rejected);
442
- continue;
443
- }
444
- if (!parsed.command || seen.has(parsed.command.displayCommand)) continue;
445
- selected.push(parsed.command);
446
- seen.add(parsed.command.displayCommand);
447
- if (selected.length >= REFINE_VALIDATION_MAX_COMMANDS) break;
448
- }
449
-
450
- if (!selected.length && candidates.length === 0) {
451
- for (const category of REFINE_VALIDATION_CATEGORIES) {
452
- if (!Object.prototype.hasOwnProperty.call(scripts, category)) continue;
453
- const fallback = parsePackageManagerValidationCommand(`npm run ${category}`, category, scripts, 'package.json:scripts');
454
- if (fallback.command && !seen.has(fallback.command.displayCommand)) {
455
- selected.push(fallback.command);
456
- seen.add(fallback.command.displayCommand);
457
- } else if (fallback.rejected) {
458
- rejectedCommands.push(fallback.rejected);
459
- }
460
- if (selected.length >= 2) break;
461
- }
462
- }
463
-
464
- return {
465
- commands: selected,
466
- rejectedCommands,
467
- source: selected.some(command => command.source === 'mesh.policy.validationCommands')
468
- ? 'mesh_policy'
469
- : selected.some(command => command.source !== 'package.json:scripts')
470
- ? 'project_context'
471
- : selected.length
472
- ? 'package_json_scripts'
473
- : 'unavailable',
474
- };
475
- }
476
-
477
- async function runMeshRefineValidationGate(mesh: any, workspace: string): Promise<MeshRefineValidationSummary> {
478
- const { execFile } = await import('node:child_process');
479
- const { promisify } = await import('node:util');
480
- const execFileAsync = promisify(execFile);
481
- const selection = selectMeshRefineValidationCommands(mesh, workspace);
482
- const summary: MeshRefineValidationSummary = {
483
- status: 'skipped',
484
- required: true,
485
- commandsRun: [],
486
- rejectedCommands: selection.rejectedCommands,
487
- skippedReason: undefined,
488
- timeoutMs: REFINE_VALIDATION_TIMEOUT_MS,
489
- outputLimitBytes: REFINE_VALIDATION_OUTPUT_LIMIT_BYTES,
490
- };
491
-
492
- if (!selection.commands.length) {
493
- summary.skippedReason = 'validation_unavailable: no allowlisted projectContext, mesh policy, or package.json build/test/typecheck/lint command was available';
494
- return summary;
495
- }
496
-
497
- for (const candidate of selection.commands) {
498
- const startedAt = Date.now();
499
- try {
500
- const result = await execFileAsync(candidate.command, candidate.args, {
501
- cwd: workspace,
502
- encoding: 'utf8',
503
- timeout: REFINE_VALIDATION_TIMEOUT_MS,
504
- maxBuffer: REFINE_VALIDATION_OUTPUT_LIMIT_BYTES,
505
- env: { ...process.env, CI: process.env.CI || '1' },
506
- });
507
- summary.commandsRun.push({
508
- command: candidate.command,
509
- args: candidate.args,
510
- displayCommand: candidate.displayCommand,
511
- category: candidate.category,
512
- source: candidate.source,
513
- passed: true,
514
- exitCode: 0,
515
- durationMs: Date.now() - startedAt,
516
- stdout: truncateValidationOutput(result.stdout),
517
- stderr: truncateValidationOutput(result.stderr),
518
- });
519
- } catch (error: any) {
520
- summary.commandsRun.push({
521
- command: candidate.command,
522
- args: candidate.args,
523
- displayCommand: candidate.displayCommand,
524
- category: candidate.category,
525
- source: candidate.source,
526
- passed: false,
527
- exitCode: typeof error?.code === 'number' ? error.code : null,
528
- signal: typeof error?.signal === 'string' ? error.signal : null,
529
- timedOut: error?.killed === true || /timed out/i.test(String(error?.message || '')),
530
- durationMs: Date.now() - startedAt,
531
- stdout: truncateValidationOutput(error?.stdout),
532
- stderr: truncateValidationOutput(error?.stderr || error?.message),
533
- });
534
- summary.status = 'failed';
535
- return summary;
536
- }
537
- }
538
-
539
- summary.status = 'passed';
540
- return summary;
541
- }
542
-
543
- function loadYamlModule(): { load: (input: string) => any; dump: (input: any, options?: Record<string, any>) => string } {
544
- return yaml as { load: (input: string) => any; dump: (input: any, options?: Record<string, any>) => string };
545
- }
546
-
547
- function getMcpServersKey(format: MeshCoordinatorConfigFormat): 'mcpServers' | 'mcp_servers' {
548
- return format === 'hermes_config_yaml' ? 'mcp_servers' : 'mcpServers';
549
- }
550
-
551
- function parseMeshCoordinatorMcpConfig(text: string, format: MeshCoordinatorConfigFormat): Record<string, any> {
552
- if (!text.trim()) return {};
553
- if (format === 'claude_mcp_json') return JSON.parse(text);
554
- const parsed = loadYamlModule().load(text);
555
- return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
556
- }
557
-
558
- function serializeMeshCoordinatorMcpConfig(config: Record<string, any>, format: MeshCoordinatorConfigFormat): string {
559
- if (format === 'claude_mcp_json') return JSON.stringify(config, null, 2);
560
- return loadYamlModule().dump(config, { noRefs: true, lineWidth: 120 });
561
- }
562
-
563
- function resolveHermesUserHome(): string {
564
- const explicitHome = process.env.HERMES_HOME?.trim();
565
- return explicitHome || pathJoin(homedir(), '.hermes');
566
- }
567
-
568
- function loadHermesCoordinatorBaseConfig(targetConfigPath: string): { config: Record<string, any>; sourceHome: string; sourceConfigPath: string } {
569
- const sourceHome = resolveHermesUserHome();
570
- const sourceConfigPath = pathJoin(sourceHome, 'config.yaml');
571
- if (!fs.existsSync(sourceConfigPath)) return { config: {}, sourceHome, sourceConfigPath };
572
- if (pathResolve(sourceConfigPath) === pathResolve(targetConfigPath)) return { config: {}, sourceHome, sourceConfigPath };
573
-
574
- const parsed = parseMeshCoordinatorMcpConfig(fs.readFileSync(sourceConfigPath, 'utf-8'), 'hermes_config_yaml');
575
- const { mcp_servers: _mcpServers, ...baseConfig } = parsed;
576
- return { config: baseConfig, sourceHome, sourceConfigPath };
577
- }
578
-
579
- function stripHermesCoordinatorTempModelProviderOverrides(config: Record<string, any>): Record<string, any> {
580
- const {
581
- model: _model,
582
- provider: _provider,
583
- default_model: _defaultModel,
584
- defaultProvider: _defaultProvider,
585
- default_provider: _defaultProviderSnake,
586
- modelProvider: _modelProvider,
587
- model_provider: _modelProviderSnake,
588
- ...sanitized
589
- } = config;
590
- const delegation = sanitized.delegation;
591
- if (delegation && typeof delegation === 'object' && !Array.isArray(delegation)) {
592
- const {
593
- model: _delegationModel,
594
- provider: _delegationProvider,
595
- modelProvider: _delegationModelProvider,
596
- model_provider: _delegationModelProviderSnake,
597
- ...delegationRest
598
- } = delegation;
599
- if (Object.keys(delegationRest).length > 0) {
600
- sanitized.delegation = delegationRest;
601
- } else {
602
- delete sanitized.delegation;
603
- }
604
- }
605
- return sanitized;
606
- }
607
-
608
- function copyHermesCoordinatorCredentialFiles(sourceHome: string, targetHome: string) {
609
- if (pathResolve(sourceHome) === pathResolve(targetHome)) return;
610
- for (const fileName of ['.env', 'auth.json']) {
611
- const sourcePath = pathJoin(sourceHome, fileName);
612
- const targetPath = pathJoin(targetHome, fileName);
613
- if (!fs.existsSync(sourcePath)) continue;
614
- try {
615
- fs.copyFileSync(sourcePath, targetPath);
616
- } catch (error: any) {
617
- LOG.warn('MeshCoordinator', `Could not copy Hermes ${fileName} into isolated coordinator home: ${error?.message || error}`);
618
- }
619
- }
620
- }
41
+ import { execFileSync } from 'node:child_process';
42
+
43
+ // ─── Extracted-module imports (symbols the dispatch class consumes) ───
44
+ import {
45
+ foldMeshNodeIdentityToCanonical,
46
+ inlineMeshCarriesTransientNodeTruth,
47
+ MESH_DIRECT_PROBE_REUSE_MS,
48
+ MeshGitProbeCache,
49
+ normalizeInlineMeshNodeIdentity,
50
+ readInlineMeshNodeId,
51
+ readObjectRecord,
52
+ readStringValue,
53
+ reconcileInlineMeshCache,
54
+ sanitizeInlineMesh,
55
+ } from '../mesh/mesh-node-identity.js';
56
+ import {
57
+ alignRefinerySubmodulesAfterMerge,
58
+ buildMeshRefineValidationPlan,
59
+ buildSubmodulePublishRequiredNextStep,
60
+ checkWorktreeChangesPatchEquivalentInRef,
61
+ MeshRefineAsyncJobStatus,
62
+ MeshRefineBatchJobHandle,
63
+ MeshRefineBatchJobStatus,
64
+ MeshRefineBatchTerminalJob,
65
+ MeshRefineJobHandle,
66
+ MeshRefineTerminalJob,
67
+ MeshWorktreePatchContainmentSummary,
68
+ recordMeshRefineStage,
69
+ RefineContext,
70
+ RefineExecFileAsync,
71
+ RefineStageOutcome,
72
+ resolveRefineryAutoPublishSubmoduleMainCommits,
73
+ runMeshRefineEffectiveDiffGate,
74
+ runMeshRefinePatchEquivalenceGate,
75
+ runMeshRefineSubmoduleReachabilityGate,
76
+ runMeshRefineValidationGate,
77
+ truncateValidationOutput,
78
+ } from '../mesh/mesh-refine-gates.js';
79
+ // ─── Refinery job orchestration (bodies extracted from this file) ───
80
+ import {
81
+ batchRefineMeshNodes,
82
+ resumePendingRefineJobsOnStartup,
83
+ startMeshRefineBatchJob,
84
+ startMeshRefineJob,
85
+ } from './router-refine.js';
86
+ // ─── Worktree / mesh-session cleanup (bodies extracted from this file) ───
87
+ import {
88
+ bestEffortRemoveWorktreeDir,
89
+ cleanupLocalWorktreeNode,
90
+ cleanupMeshSessions,
91
+ getWorktreeForceCleanupConvergence,
92
+ isCompletedHostedSession,
93
+ precheckLocalWorktreeRemovable,
94
+ recordIntentionalMeshSessionStop,
95
+ sessionMatchesMeshNode,
96
+ } from './router-worktree-cleanup.js';
97
+ // ─── Aggregate mesh-status cache (bodies extracted from this file) ───
98
+ import {
99
+ getCachedAggregateMeshStatus,
100
+ hydrateCachedAggregateMeshStatusFromInline,
101
+ rememberAggregateMeshStatus,
102
+ } from './router-aggregate-status.js';
103
+ // ─── Remote mesh-session owner resolution (bodies extracted from this file) ───
104
+ import { resolveRemoteMeshSessionOwnerDaemonId } from './router-mesh-session-owner.js';
105
+
106
+ // ─── Barrel re-exports: node-identity / git-freshness, refine gates, coordinator config ───
107
+ // These modules were split out of router.ts. Re-export their public surface so the
108
+ // many existing `from '.../commands/router.js'` named imports keep resolving here.
109
+ export * from '../mesh/mesh-node-identity.js';
110
+ export * from '../mesh/mesh-refine-gates.js';
111
+ export * from '../mesh/mesh-coordinator-config.js';
621
112
 
622
113
  // ─── Types ───
623
114
 
@@ -644,12 +135,14 @@ export interface CommandRouterDeps {
644
135
  /** Reference to detected IDEs array (mutable — router updates it) */
645
136
  detectedIdes: { value: any[] };
646
137
  sessionRegistry: SessionRegistry;
647
- /** Callback for CDP manager creation after launch_ide */
138
+ /** Callback after CDP manager created (transport-specific extras) */
648
139
  onCdpManagerCreated?: (ideType: string, manager: DaemonCdpManager) => void;
649
140
  /** Callback after IDE connected (e.g., startAgentStreamPolling) */
650
141
  onIdeConnected?: () => void;
651
142
  /** Callback after status change (stop_ide, restart) */
652
143
  onStatusChange?: () => void;
144
+ /** Callback when a mesh state is invalidated */
145
+ onMeshStateChange?: (meshId: string) => void;
653
146
  /** Callback after chat-related commands */
654
147
  onPostChatCommand?: () => void;
655
148
  /** Get a connected CDP manager (for agent stream reset check) */
@@ -661,6 +154,10 @@ export interface CommandRouterDeps {
661
154
  statusVersion?: string;
662
155
  /** Session host control plane */
663
156
  sessionHostControl?: SessionHostControlPlane | null;
157
+ /** Selected-coordinator mesh peer telemetry surface for target daemons, when supported by the runtime. */
158
+ getMeshPeerConnectionStatus?: (daemonId: string) => Record<string, unknown> | null;
159
+ /** Dispatch a command to a remote mesh node via P2P/relay. Injected by cloud runtime; absent in standalone. */
160
+ dispatchMeshCommand?: (daemonId: string, cmd: string, args: Record<string, unknown>) => Promise<unknown>;
664
161
  }
665
162
 
666
163
  export interface CommandRouterResult {
@@ -673,7 +170,30 @@ const CHAT_COMMANDS = [
673
170
  'send_chat', 'new_chat', 'switch_chat', 'set_mode',
674
171
  'change_model',
675
172
  ];
676
- const READ_DEBUG_ENABLED = process.argv.includes('--dev') || process.env.ADHDEV_READ_DEBUG === '1';
173
+
174
+ // [Z] Session-scoped commands that must be forwarded to the owning REMOTE worker daemon
175
+ // when their target session is not hosted on this coordinator. These are the interactive
176
+ // controlbar / modal mutations the dashboard issues against a specific session:
177
+ // - invoke_provider_script: controlbar Model/Mode selectors run a provider script
178
+ // - resolve_action: approve/reject a modal prompt
179
+ // - set_mode / change_model / set_thought_level: direct control mutations
180
+ // send_chat is intentionally NOT here — it already reaches the worker by its own route and
181
+ // double-forwarding would be redundant. read_chat is also excluded: it can serve historical
182
+ // transcript data locally and has its own inactive-session fallback in the CommandHandler.
183
+ const MESH_FORWARDABLE_SESSION_COMMANDS = new Set([
184
+ 'invoke_provider_script',
185
+ 'resolve_action',
186
+ 'set_mode',
187
+ 'change_model',
188
+ 'set_thought_level',
189
+ // agent_command (send_chat / clear_history / stop) is session-scoped too: a command
190
+ // explicitly naming a targetSessionId MUST reach that session wherever it lives, never a
191
+ // different local session. Without forwarding, a misrouted/relayed send_chat for a REMOTE
192
+ // worker session that reaches the wrong daemon used to fuzzy-inject the task body into that
193
+ // daemon's own CLI session (TASKECHO coordinator self-echo). Forwarding to the owning daemon
194
+ // delivers it to the real worker instead. (findAdapter is also fail-closed as the backstop.)
195
+ 'agent_command',
196
+ ]);
677
197
 
678
198
  function normalizeCommandSource(source: string): CommandLogEntry['source'] {
679
199
  switch (source) {
@@ -696,140 +216,458 @@ function normalizeCommandArgsWithInteractionId(args: any): Record<string, unknow
696
216
  return base;
697
217
  }
698
218
 
699
- function toHostedCliRuntimeDescriptor(record: any): HostedCliRuntimeDescriptor | null {
700
- if (!record || typeof record !== 'object') return null;
701
- const runtimeId = typeof record.sessionId === 'string' ? record.sessionId : '';
702
- const cliType = typeof record.providerType === 'string' ? record.providerType : '';
703
- const workspace = typeof record.workspace === 'string' ? record.workspace : '';
704
- if (!runtimeId || !cliType || !workspace) return null;
705
- return {
706
- runtimeId,
707
- runtimeKey: typeof record.runtimeKey === 'string' ? record.runtimeKey : undefined,
708
- displayName: typeof record.displayName === 'string' ? record.displayName : undefined,
709
- workspaceLabel: typeof record.workspaceLabel === 'string' ? record.workspaceLabel : undefined,
710
- lifecycle: typeof record.lifecycle === 'string' ? record.lifecycle as HostedCliRuntimeDescriptor['lifecycle'] : undefined,
711
- recoveryState: typeof record.meta?.runtimeRecoveryState === 'string'
712
- ? String(record.meta.runtimeRecoveryState)
713
- : null,
714
- cliType,
715
- workspace,
716
- cliArgs: Array.isArray(record.meta?.cliArgs) ? record.meta.cliArgs as string[] : [],
717
- providerSessionId: typeof record.meta?.providerSessionId === 'string'
718
- ? String(record.meta.providerSessionId)
719
- : undefined,
720
- };
721
- }
722
-
723
- function getWriteConflictOwnerClientId(error: unknown): string | undefined {
724
- const message = typeof error === 'string'
725
- ? error
726
- : error instanceof Error
727
- ? error.message
728
- : '';
729
- const match = /^Write owned by\s+(.+)$/.exec(message.trim());
730
- return match?.[1]?.trim() || undefined;
731
- }
732
-
733
- function summarizeSessionHostRecord(result: unknown): Record<string, unknown> {
734
- if (!result || typeof result !== 'object') return {};
735
- const record = result as Record<string, any>;
736
- return {
737
- runtimeKey: typeof record.runtimeKey === 'string' ? record.runtimeKey : undefined,
738
- lifecycle: typeof record.lifecycle === 'string' ? record.lifecycle : undefined,
739
- surfaceKind: getSessionHostSurfaceKind(record as any),
740
- attachedClientCount: Array.isArray(record.attachedClients) ? record.attachedClients.length : undefined,
741
- hasWriteOwner: !!record.writeOwner,
742
- writeOwnerClientId: typeof record.writeOwner?.clientId === 'string' ? record.writeOwner.clientId : undefined,
743
- };
744
- }
745
-
746
- function summarizeSessionHostRecords(result: unknown): Record<string, unknown> {
747
- const records = Array.isArray(result) ? result : [];
748
- const groups = partitionSessionHostRecords(records as any[]);
749
- return {
750
- sessionCount: records.length,
751
- liveRuntimeCount: groups.liveRuntimes.length,
752
- recoverySnapshotCount: groups.recoverySnapshots.length,
753
- inactiveRecordCount: groups.inactiveRecords.length,
754
- };
755
- }
756
-
757
- function summarizeSessionHostDiagnostics(result: unknown): Record<string, unknown> {
758
- const diagnostics = result && typeof result === 'object' ? result as Record<string, any> : {};
759
- const sessions = Array.isArray(diagnostics.sessions) ? diagnostics.sessions : [];
760
- return {
761
- runtimeCount: typeof diagnostics.runtimeCount === 'number' ? diagnostics.runtimeCount : undefined,
762
- ...summarizeSessionHostRecords(sessions),
219
+ /**
220
+ * Confine a spec path to ~/.adhdev/providers, defeating both prefix-bypass
221
+ * (e.g. ".../providers-evil") and symlink escape. Resolves the real path of
222
+ * the *parent* directory (the file may not exist yet for writes), requires the
223
+ * basename to be a literal `*.json`, and re-joins under the verified parent so
224
+ * the returned path can't point outside the tree. Used by get/write_spec_source.
225
+ */
226
+ export function normalizeStandaloneHostCommandUrl(hostAddress: string): string {
227
+ const raw = hostAddress.trim();
228
+ if (!raw) throw new Error('hostAddress required');
229
+ const url = new URL(raw.replace(/^ws:/, 'http:').replace(/^wss:/, 'https:'));
230
+ url.pathname = '/api/v1/command';
231
+ url.search = '';
232
+ url.hash = '';
233
+ return url.toString();
234
+ }
235
+
236
+ export function buildMemberJoinNode(mesh: any, args: any, fallbackDaemonId?: string): Record<string, unknown> | null {
237
+ const requestedNodeId = typeof args?.memberNodeId === 'string' ? args.memberNodeId.trim() : '';
238
+ const explicit = args?.memberNode && typeof args.memberNode === 'object' && !Array.isArray(args.memberNode)
239
+ ? args.memberNode as Record<string, any>
240
+ : null;
241
+ const configured = Array.isArray(mesh?.nodes)
242
+ ? (requestedNodeId
243
+ ? mesh.nodes.find((node: any) => meshNodeIdMatches(node, requestedNodeId))
244
+ : mesh.nodes[0])
245
+ : null;
246
+ const source = explicit || configured;
247
+ const workspace = typeof source?.workspace === 'string' && source.workspace.trim()
248
+ ? source.workspace.trim()
249
+ : typeof args?.workspace === 'string' && args.workspace.trim()
250
+ ? args.workspace.trim()
251
+ : process.cwd();
252
+ if (!workspace) return null;
253
+ const nodeId = typeof source?.id === 'string' && source.id.trim()
254
+ ? source.id.trim()
255
+ : typeof source?.nodeId === 'string' && source.nodeId.trim()
256
+ ? source.nodeId.trim()
257
+ : undefined;
258
+ const baseOverrides = source?.userOverrides && typeof source.userOverrides === 'object' && !Array.isArray(source.userOverrides)
259
+ ? source.userOverrides as Record<string, unknown>
260
+ : {};
261
+ // This payload is built ON THE MEMBER DAEMON, so process.platform/process.arch
262
+ // are the member's OWN machine. Stamp them into userOverrides so the host
263
+ // stores the member's real platform/arch on the node record — the coordinator's
264
+ // buildMeshNodeCapabilityTags then advertises os=<member-os> instead of the
265
+ // coordinator's own platform. Only fill values the operator hasn't already set.
266
+ const userOverrides: Record<string, unknown> = {
267
+ ...baseOverrides,
268
+ ...(typeof baseOverrides.platform === 'string' && baseOverrides.platform.trim() ? {} : { platform: process.platform }),
269
+ ...(typeof baseOverrides.arch === 'string' && baseOverrides.arch.trim() ? {} : { arch: process.arch }),
763
270
  };
764
- }
765
-
766
- function summarizeSessionHostPruneResult(result: unknown): Record<string, unknown> {
767
- const value = result && typeof result === 'object' ? result as Record<string, any> : {};
768
271
  return {
769
- duplicateGroupCount: typeof value.duplicateGroupCount === 'number' ? value.duplicateGroupCount : undefined,
770
- prunedCount: Array.isArray(value.prunedSessionIds) ? value.prunedSessionIds.length : undefined,
771
- keptCount: Array.isArray(value.keptSessionIds) ? value.keptSessionIds.length : undefined,
272
+ ...(nodeId ? { id: nodeId } : {}),
273
+ workspace,
274
+ ...(typeof source?.repoRoot === 'string' && source.repoRoot.trim() ? { repoRoot: source.repoRoot.trim() } : {}),
275
+ ...(typeof source?.daemonId === 'string' && source.daemonId.trim() ? { daemonId: source.daemonId.trim() } : fallbackDaemonId ? { daemonId: fallbackDaemonId } : {}),
276
+ ...(typeof source?.machineId === 'string' && source.machineId.trim() ? { machineId: source.machineId.trim() } : {}),
277
+ userOverrides,
278
+ policy: source?.policy && typeof source.policy === 'object' && !Array.isArray(source.policy) ? source.policy : {},
279
+ role: 'member',
772
280
  };
773
281
  }
774
282
 
775
283
  export class DaemonCommandRouter {
776
- private deps: CommandRouterDeps;
284
+ /** Public (not private) so the extracted ./router-refine.ts orchestration can reach it via `self`. */
285
+ deps: CommandRouterDeps;
777
286
  /** In-memory cache for cloud-originating meshes passed via inlineMesh.
778
287
  * Allows the MCP server to query mesh data via get_mesh even when
779
288
  * the mesh doesn't exist in the local meshes.json file. */
780
289
  private inlineMeshCache = new Map<string, any>();
290
+ /** Tombstones for inline mesh nodes removed via remove_mesh_node, keyed by
291
+ * meshId → set of removed nodeIds. The dashboard keeps echoing the removed
292
+ * node in the inlineMesh it attaches to every command; without a tombstone,
293
+ * reconcileInlineMeshCache MERGEs it straight back (resurrection). A
294
+ * tombstoned node is skipped during reconcile only while its workspace is
295
+ * absent from disk — a genuine re-registration (same nodeId, workspace back
296
+ * on disk) clears the tombstone and merges normally, preserving clone
297
+ * worktree visibility and legitimate node re-creation. */
298
+ private removedInlineMeshNodeIds = new Map<string, Set<string>>();
299
+ /** Coordinator-owned whole-mesh aggregate status snapshots. Browser callers read this by default.
300
+ * Public (not private) so the extracted ./router-aggregate-status.ts orchestration can reach it via `self`. */
301
+ aggregateMeshStatusCache = new Map<string, { builtAt: number; snapshot: any; queueRevision: string }>();
302
+ /** Shared per-peer git_status probe dedup + recently-probed reuse gate.
303
+ * Spans separate mesh_status/get_mesh calls so the dashboard auto-retry
304
+ * loop cannot storm a slow peer with back-to-back refreshUpstream probes. */
305
+ private meshGitProbeCache = new MeshGitProbeCache(MESH_DIRECT_PROBE_REUSE_MS);
306
+ /** Meshes with a background SWR freshen (async mesh_status refresh) already in
307
+ * flight — so a burst of interactive detail-opens serves the cached snapshot
308
+ * and coalesces onto ONE background refresh instead of storming the peers. */
309
+ private swrRefreshInFlight = new Set<string>();
310
+ /** In-memory async Refinery jobs keyed by meshId:nodeId to reject/return duplicate in-flight requests.
311
+ * Public (not private) so the extracted ./router-refine.ts orchestration can reach it via `self`. */
312
+ runningRefineJobs = new Map<string, MeshRefineJobHandle>();
313
+ /** Terminal async Refinery jobs preserve a clear answer after the worktree node has been removed. */
314
+ terminalRefineJobs = new Map<string, MeshRefineTerminalJob>();
315
+ /** In-memory async batch Refinery jobs keyed by meshId (one batch convergence per mesh at a time). */
316
+ runningRefineBatchJobs = new Map<string, MeshRefineBatchJobHandle>();
317
+ /** Terminal async batch Refinery jobs preserve the last batch outcome for late readers. */
318
+ terminalRefineBatchJobs = new Map<string, MeshRefineBatchTerminalJob>();
781
319
 
782
320
  constructor(deps: CommandRouterDeps) {
783
321
  this.deps = deps;
784
322
  }
785
323
 
324
+ // ─── Aggregate mesh-status cache ────────────────────────────────────
325
+ // Implementation lives in ./router-aggregate-status.ts (behavior-preserving
326
+ // code move). Kept here as thin delegators: getCachedAggregateMeshStatus /
327
+ // rememberAggregateMeshStatus are bound into HighFamilyContext, so callers
328
+ // reach these via `self.` for correct instance dispatch.
329
+
330
+ private hydrateCachedAggregateMeshStatusFromInline(snapshot: any, mesh: any, options?: { requireDirectPeerTruth?: boolean }): any {
331
+ return hydrateCachedAggregateMeshStatusFromInline(this, snapshot, mesh, options);
332
+ }
333
+
334
+ private getCachedAggregateMeshStatus(
335
+ meshId: string,
336
+ mesh?: any,
337
+ options?: { requireDirectPeerTruth?: boolean; allowStalePending?: boolean },
338
+ ): any | null {
339
+ return getCachedAggregateMeshStatus(this, meshId, mesh, options);
340
+ }
341
+
342
+ private rememberAggregateMeshStatus(meshId: string, snapshot: any, refreshReason: string): any {
343
+ return rememberAggregateMeshStatus(this, meshId, snapshot, refreshReason);
344
+ }
345
+
346
+ public getCachedInlineMeshNodes(): any[] {
347
+ const nodes: any[] = [];
348
+ for (const mesh of this.inlineMeshCache.values()) {
349
+ if (Array.isArray(mesh?.nodes)) {
350
+ nodes.push(...mesh.nodes);
351
+ }
352
+ }
353
+ return nodes;
354
+ }
355
+
356
+ // ─── Remote mesh-session owner resolution ───────────────────────────
357
+ // Implementation lives in ./router-mesh-session-owner.ts (behavior-preserving
358
+ // code move). resolveRemoteMeshSessionOwnerDaemonId stays public (the [Z]
359
+ // session-scoped forward in executeDaemonCommand and a unit test call it), so
360
+ // it's kept here as a thin delegator.
361
+
362
+ public resolveRemoteMeshSessionOwnerDaemonId(sessionId: string, ownerNodeIdHint?: string): string | undefined {
363
+ return resolveRemoteMeshSessionOwnerDaemonId(this, sessionId, ownerNodeIdHint);
364
+ }
365
+
786
366
  public getCachedInlineMesh(meshId: string, inlineMesh?: unknown): any | undefined {
787
367
  if (inlineMesh && typeof inlineMesh === 'object') {
788
- this.inlineMeshCache.set(meshId, inlineMesh as any);
789
- return inlineMesh as any;
368
+ return this.warmInlineMeshCache(meshId, inlineMesh);
790
369
  }
791
370
  return this.inlineMeshCache.get(meshId);
792
371
  }
793
372
 
794
- private async getMeshForCommand(
373
+ private warmInlineMeshCache(meshId: string, inlineMesh?: unknown): any | undefined {
374
+ if (!inlineMesh || typeof inlineMesh !== 'object') return undefined;
375
+ // Save-boundary node-id normalization: reconcile each node's identity so
376
+ // `id` and `nodeId` agree before it enters the cache, so reconcile keys
377
+ // and the round-trip through the status serializer stay form-stable.
378
+ const sanitizedInlineMesh = this.applyInlineMeshNodeTombstones(
379
+ meshId,
380
+ sanitizeInlineMesh(normalizeInlineMeshNodeIdentity(inlineMesh as any)),
381
+ );
382
+ const cached = this.inlineMeshCache.get(meshId);
383
+ if (cached) {
384
+ const merged = reconcileInlineMeshCache(cached, sanitizedInlineMesh);
385
+ this.inlineMeshCache.set(meshId, merged);
386
+ return merged;
387
+ }
388
+ this.inlineMeshCache.set(meshId, sanitizedInlineMesh as any);
389
+ return sanitizedInlineMesh as any;
390
+ }
391
+
392
+ async getMeshForCommand(
795
393
  meshId: string,
796
394
  inlineMesh?: unknown,
797
395
  options?: { preferInline?: boolean },
798
- ): Promise<{ mesh: any; inline: boolean } | null> {
799
- const preferInline = options?.preferInline === true;
396
+ ): Promise<{ mesh: any; inline: boolean; source: 'inline_cache' | 'inline_bootstrap' | 'local_config' } | null> {
397
+ // Default to inline-cache-preferred: a caller that omits the flag still sees
398
+ // inline-cache-only (worktree clone) nodes in the resolved mesh view, closing
399
+ // the CLAIMSTALL gap where a missed `preferInline: true` silently dropped them.
400
+ // An explicit `preferInline: false` is still honored for any local-config-only
401
+ // read that deliberately bypasses the inline cache.
402
+ const preferInline = options?.preferInline !== false;
800
403
  if (preferInline) {
801
- const cached = this.getCachedInlineMesh(meshId, inlineMesh);
802
- if (cached) return { mesh: cached, inline: true };
404
+ const cached = this.getCachedInlineMesh(meshId);
405
+ if (cached) {
406
+ if (inlineMeshCarriesTransientNodeTruth(inlineMesh)) {
407
+ const merged = reconcileInlineMeshCache(
408
+ cached,
409
+ this.applyInlineMeshNodeTombstones(meshId, inlineMesh as any),
410
+ );
411
+ this.inlineMeshCache.set(meshId, sanitizeInlineMesh(normalizeInlineMeshNodeIdentity(merged)));
412
+ return { mesh: merged, inline: true, source: 'inline_cache' };
413
+ }
414
+ return { mesh: cached, inline: true, source: 'inline_cache' };
415
+ }
416
+ if (inlineMeshCarriesTransientNodeTruth(inlineMesh)) {
417
+ this.warmInlineMeshCache(meshId, inlineMesh);
418
+ return { mesh: inlineMesh, inline: true, source: 'inline_bootstrap' };
419
+ }
803
420
  }
804
421
  try {
805
422
  const { getMesh } = await import('../config/mesh-config.js');
806
423
  const mesh = getMesh(meshId);
807
- if (mesh) return { mesh, inline: false };
424
+ if (mesh) return { mesh, inline: false, source: 'local_config' };
808
425
  } catch { /* fall through to inline cache */ }
809
- const cached = this.getCachedInlineMesh(meshId, inlineMesh);
810
- return cached ? { mesh: cached, inline: true } : null;
426
+ const cached = this.getCachedInlineMesh(meshId);
427
+ if (cached) return { mesh: cached, inline: true, source: 'inline_cache' };
428
+ const warmedInline = this.warmInlineMeshCache(meshId, inlineMesh);
429
+ return warmedInline ? { mesh: warmedInline, inline: true, source: 'inline_bootstrap' } : null;
430
+ }
431
+
432
+ invalidateAggregateMeshStatus(meshId: string): void {
433
+ this.aggregateMeshStatusCache.delete(meshId);
434
+ this.deps.onMeshStateChange?.(meshId);
435
+ }
436
+
437
+ /**
438
+ * Build the MedFamilyContext handed to RF-ROUTER MED family handlers. Binds the
439
+ * router-private collaborators those handlers need (mesh resolution, owner
440
+ * gating, inline-cache mutation, worktree / session cleanup, refine job
441
+ * starters, IDE stop/launch) plus the inline-mesh and git-probe caches. The
442
+ * `launchIde` field closes over the freshly-built context so restart_session /
443
+ * restart_ide invoke the IDE launch directly instead of recursing through
444
+ * executeDaemonCommand('launch_ide').
445
+ */
446
+ private buildMedFamilyContext(): MedFamilyContext {
447
+ const ctx: MedFamilyContext = {
448
+ deps: this.deps,
449
+ getMeshForCommand: this.getMeshForCommand.bind(this),
450
+ getCachedInlineMesh: this.getCachedInlineMesh.bind(this),
451
+ requireMeshHostMutationOwner: this.requireMeshHostMutationOwner.bind(this),
452
+ invalidateAggregateMeshStatus: this.invalidateAggregateMeshStatus.bind(this),
453
+ updateInlineMeshNode: this.updateInlineMeshNode.bind(this),
454
+ removeInlineMeshNode: this.removeInlineMeshNode.bind(this),
455
+ normalizeMeshSessionCleanupMode: this.normalizeMeshSessionCleanupMode.bind(this),
456
+ cleanupMeshSessions: this.cleanupMeshSessions.bind(this),
457
+ cleanupLocalWorktreeNode: this.cleanupLocalWorktreeNode.bind(this),
458
+ precheckLocalWorktreeRemovable: this.precheckLocalWorktreeRemovable.bind(this),
459
+ startMeshRefineJob: this.startMeshRefineJob.bind(this),
460
+ batchRefineMeshNodes: this.batchRefineMeshNodes.bind(this),
461
+ startMeshRefineBatchJob: this.startMeshRefineBatchJob.bind(this),
462
+ stopIde: this.stopIde.bind(this),
463
+ launchIde: (args: any) => launchIde(ctx, args),
464
+ inlineMeshCache: this.inlineMeshCache,
465
+ meshGitProbeCache: this.meshGitProbeCache,
466
+ };
467
+ return ctx;
468
+ }
469
+
470
+ /**
471
+ * Build the HighFamilyContext handed to RF-ROUTER HIGH family handlers. Binds
472
+ * the router-private collaborators those handlers need (mesh resolution, the
473
+ * aggregate-status memory cache + its bound read/write helpers, the
474
+ * running-refine-job table, inline-mesh + git-probe caches, and the router's
475
+ * own `execute` for the get_mesh_review_inbox mesh_status re-entry). HIGH
476
+ * handlers reach more router-owned state than MED, but the binding shape is
477
+ * the same: bound methods + direct field references, none reachable from
478
+ * `deps`.
479
+ */
480
+ private buildHighFamilyContext(): HighFamilyContext {
481
+ return {
482
+ deps: this.deps,
483
+ getMeshForCommand: this.getMeshForCommand.bind(this),
484
+ getCachedAggregateMeshStatus: this.getCachedAggregateMeshStatus.bind(this),
485
+ rememberAggregateMeshStatus: this.rememberAggregateMeshStatus.bind(this),
486
+ execute: this.execute.bind(this),
487
+ aggregateMeshStatusCache: this.aggregateMeshStatusCache,
488
+ swrRefreshInFlight: this.swrRefreshInFlight,
489
+ runningRefineJobs: this.runningRefineJobs,
490
+ inlineMeshCache: this.inlineMeshCache,
491
+ meshGitProbeCache: this.meshGitProbeCache,
492
+ };
493
+ }
494
+
495
+
496
+ private async requireMeshHostMutationOwner(meshId: string, inlineMesh: unknown, operation: string): Promise<CommandRouterResult | null> {
497
+ const meshRecord = await this.getMeshForCommand(meshId, inlineMesh, { preferInline: true });
498
+ const mesh = meshRecord?.mesh;
499
+ if (!mesh) return { success: false, error: 'Mesh not found' };
500
+ const meshHost = resolveMeshHostStatus(mesh);
501
+ if (!meshHost.canOwnCoordinator || !meshHost.canOwnQueue) {
502
+ return { ...buildMeshHostRequiredFailure(mesh, operation), success: false, meshId };
503
+ }
504
+ return null;
811
505
  }
812
506
 
813
507
  private updateInlineMeshNode(meshId: string, mesh: any, node: any): void {
814
- if (!mesh || !Array.isArray(mesh.nodes) || !node?.id) return;
815
- const idx = mesh.nodes.findIndex((entry: any) => entry?.id === node.id || entry?.nodeId === node.id);
508
+ const incomingId = normalizeMeshNodeId(node);
509
+ if (!mesh || !Array.isArray(mesh.nodes) || !incomingId) return;
510
+ const idx = mesh.nodes.findIndex((entry: any) => meshNodeIdMatches(entry, incomingId));
816
511
  if (idx >= 0) mesh.nodes[idx] = node;
817
512
  else mesh.nodes.push(node);
818
513
  mesh.updatedAt = new Date().toISOString();
514
+ // Canonicalize node identity in place (id and nodeId kept equal): clone
515
+ // nodes are created with `id` and re-inserted here (bypassing
516
+ // warmInlineMeshCache), so this is a second save boundary. In-place
517
+ // folding preserves the caller's `mesh` / nodes-array references, which
518
+ // persistWorktreeSetupState reuses across subsequent calls.
519
+ for (const entry of mesh.nodes) foldMeshNodeIdentityToCanonical(entry);
819
520
  this.inlineMeshCache.set(meshId, mesh);
521
+ this.invalidateAggregateMeshStatus(meshId);
820
522
  }
821
523
 
822
524
  private removeInlineMeshNode(meshId: string, mesh: any, nodeId: string): boolean {
823
525
  if (!mesh || !Array.isArray(mesh.nodes)) return false;
824
- const idx = mesh.nodes.findIndex((entry: any) => entry?.id === nodeId || entry?.nodeId === nodeId);
526
+ const idx = mesh.nodes.findIndex((entry: any) => meshNodeIdMatches(entry, nodeId));
825
527
  if (idx === -1) return false;
528
+ const canonicalNodeId = readInlineMeshNodeId(mesh.nodes[idx]) || nodeId;
826
529
  mesh.nodes.splice(idx, 1);
827
530
  mesh.updatedAt = new Date().toISOString();
828
531
  this.inlineMeshCache.set(meshId, mesh);
532
+ // Tombstone the removed node so the dashboard's stale inlineMesh echo does
533
+ // not MERGE it back on the next command (see removedInlineMeshNodeIds).
534
+ this.tombstoneRemovedInlineMeshNode(meshId, canonicalNodeId);
535
+ if (canonicalNodeId !== nodeId) this.tombstoneRemovedInlineMeshNode(meshId, nodeId);
536
+ this.invalidateAggregateMeshStatus(meshId);
829
537
  return true;
830
538
  }
831
539
 
832
- private normalizeMeshSessionCleanupMode(value: unknown): RepoMeshSessionCleanupMode {
540
+ /**
541
+ * WORKTREE-BOOTSTRAP-COORD-STATE: mark a worktree node's bootstrap as reaching a
542
+ * terminal state (complete / failed) in THIS daemon's mesh view.
543
+ *
544
+ * Root cause this fixes: clone_mesh_node forwards the clone+bootstrap to the
545
+ * source node's daemon (the worktree's machine). persistWorktreeSetupState
546
+ * therefore flips worktreeBootstrap.status to 'complete' on the WORKER daemon's
547
+ * mesh object — never on the coordinator's. The coordinator only ever holds the
548
+ * 'running' state it stamped from the forwarded clone reply. The claim path's
549
+ * bootstrap gate (mesh-event-forwarding agent:ready / mesh-queue-assignment)
550
+ * reads the coordinator's mesh via getMeshWithCache, sees status==='running'
551
+ * forever, and DEFERS every claim — so the worktree_bootstrap_complete re-fire
552
+ * (triggerMeshQueue) loops against a gate that never opens: claim never lands,
553
+ * the idle session is re-registered each tick, and auto-launch keeps spawning
554
+ * fresh sessions (runaway worktree-session multiplication).
555
+ *
556
+ * Called from the worktree_bootstrap_complete/_failed event handler BEFORE the
557
+ * queue re-fire so the gate sees the terminal state and the deferred claim can
558
+ * finally land. Updates the inline cache (clone worktree nodes are inline-only)
559
+ * and, when the node also exists in local config, persists there too; both paths
560
+ * invalidate the aggregate status cache. Best-effort and idempotent.
561
+ */
562
+ public markWorktreeBootstrapTerminalState(meshId: string, nodeId: string, status: 'complete' | 'failed', opts?: { workspace?: string }): void {
563
+ if (!meshId || !nodeId) return;
564
+ const terminalBootstrap = (prev?: Record<string, unknown>): Record<string, unknown> => ({
565
+ ...(prev && typeof prev === 'object' ? prev : {}),
566
+ status,
567
+ completedAt: (prev as any)?.completedAt ?? new Date().toISOString(),
568
+ });
569
+ const stamp = (mesh: any): boolean => {
570
+ if (!mesh || !Array.isArray(mesh.nodes)) return false;
571
+ const node = mesh.nodes.find((entry: any) => meshNodeIdMatches(entry, nodeId));
572
+ if (!node) return false;
573
+ const prev = (node.worktreeBootstrap && typeof node.worktreeBootstrap === 'object')
574
+ ? node.worktreeBootstrap as Record<string, unknown>
575
+ : {};
576
+ if (prev.status === status) return false;
577
+ node.worktreeBootstrap = terminalBootstrap(prev);
578
+ return true;
579
+ };
580
+ let changed = false;
581
+ // Inline cache (the authoritative view for inline-only clone worktree nodes).
582
+ try {
583
+ const cached = this.getCachedInlineMesh(meshId);
584
+ if (cached && stamp(cached)) {
585
+ cached.updatedAt = new Date().toISOString();
586
+ this.inlineMeshCache.set(meshId, cached);
587
+ changed = true;
588
+ } else if (!cached || !(Array.isArray(cached.nodes) && cached.nodes.some((entry: any) => meshNodeIdMatches(entry, nodeId)))) {
589
+ // Fix (3) HYDRATE-ON-MISS: the terminal bootstrap event arrived for a node this
590
+ // coordinator's inline view does NOT hold — the clone reply that would have seeded
591
+ // the worktree node never reached this daemon (or the inline mesh for this id is
592
+ // empty). With no node entry the claim gate has nothing to open, so the deferred
593
+ // claim (mesh-event-forwarding agent:ready / mesh-queue-assignment) strands forever
594
+ // and the coordinator relaunch/stop-loops. Instead of returning early, upsert a
595
+ // minimal worktree node carrying the terminal bootstrap status from the event
596
+ // payload so the gate — which reads this same inline view — sees a non-'running'
597
+ // state and the registered idle session can finally claim. Identity is canonicalized
598
+ // by updateInlineMeshNode (id/nodeId folded via the shared 3-form normalizer), so the
599
+ // remote-clone node-id form resolves the same way every claim-path consumer matches.
600
+ const shell = (cached && typeof cached === 'object')
601
+ ? cached
602
+ : { id: meshId, nodes: [] as any[], updatedAt: new Date().toISOString() };
603
+ if (!Array.isArray(shell.nodes)) shell.nodes = [];
604
+ const hydratedNode: any = {
605
+ id: nodeId,
606
+ nodeId,
607
+ isLocalWorktree: true,
608
+ ...(opts?.workspace ? { workspace: opts.workspace } : {}),
609
+ worktreeBootstrap: terminalBootstrap(),
610
+ };
611
+ this.updateInlineMeshNode(meshId, shell, hydratedNode);
612
+ changed = true;
613
+ }
614
+ } catch { /* best-effort */ }
615
+ if (changed) this.invalidateAggregateMeshStatus(meshId);
616
+ // Local config (a worktree node registered via addNode also lives here).
617
+ // Done in a detached dynamic-import chain so the method stays sync; both the
618
+ // stamp and the persist are best-effort, and the inline-cache stamp above is
619
+ // what the coordinator's claim gate reads.
620
+ void import('../config/mesh-config.js')
621
+ .then(({ getMesh, updateNode }) => {
622
+ const local = getMesh(meshId);
623
+ if (local && stamp(local)) {
624
+ const node = local.nodes.find((entry: any) => meshNodeIdMatches(entry, nodeId));
625
+ if (node) updateNode(meshId, node.id, { worktreeBootstrap: node.worktreeBootstrap } as any);
626
+ this.invalidateAggregateMeshStatus(meshId);
627
+ }
628
+ })
629
+ .catch(() => { /* persistence is best-effort */ });
630
+ }
631
+
632
+ private tombstoneRemovedInlineMeshNode(meshId: string, nodeId: string): void {
633
+ if (!nodeId) return;
634
+ let set = this.removedInlineMeshNodeIds.get(meshId);
635
+ if (!set) {
636
+ set = new Set<string>();
637
+ this.removedInlineMeshNodeIds.set(meshId, set);
638
+ }
639
+ set.add(nodeId);
640
+ }
641
+
642
+ /** Filter an incoming inline mesh against this mesh's tombstones before it is
643
+ * reconciled into the cache. A tombstoned node is dropped only while its
644
+ * workspace is still absent from disk; if the workspace is back (genuine
645
+ * re-registration), the tombstone is cleared and the node merges normally. */
646
+ private applyInlineMeshNodeTombstones(meshId: string, incoming: any): any {
647
+ const tombstones = this.removedInlineMeshNodeIds.get(meshId);
648
+ if (!tombstones?.size || !incoming || typeof incoming !== 'object' || !Array.isArray(incoming.nodes)) {
649
+ return incoming;
650
+ }
651
+ let dropped = false;
652
+ const nodes = incoming.nodes.filter((node: any) => {
653
+ const nodeId = readInlineMeshNodeId(node);
654
+ if (!nodeId || !tombstones.has(nodeId)) return true;
655
+ const workspace = readStringValue(node?.workspace);
656
+ // Genuine re-registration: same nodeId, workspace back on disk →
657
+ // clear the tombstone and let the node merge normally.
658
+ if (workspace && fs.existsSync(workspace)) {
659
+ tombstones.delete(nodeId);
660
+ return true;
661
+ }
662
+ dropped = true;
663
+ return false;
664
+ });
665
+ if (tombstones.size === 0) this.removedInlineMeshNodeIds.delete(meshId);
666
+ if (!dropped) return incoming;
667
+ return { ...incoming, nodes };
668
+ }
669
+
670
+ normalizeMeshSessionCleanupMode(value: unknown): RepoMeshSessionCleanupMode {
833
671
  return value === 'stop'
834
672
  || value === 'delete_stopped'
835
673
  || value === 'stop_and_delete'
@@ -838,445 +676,77 @@ export class DaemonCommandRouter {
838
676
  : 'preserve';
839
677
  }
840
678
 
841
- private sessionMatchesMeshNode(record: any, node: any, nodeId: string, sessionIds?: Set<string>): boolean {
842
- const sessionId = typeof record?.sessionId === 'string' ? record.sessionId : '';
843
- if (!sessionId) return false;
844
- if (sessionIds?.size) return sessionIds.has(sessionId);
845
- const workspace = typeof node?.workspace === 'string' ? node.workspace : '';
846
- if (workspace && record?.workspace === workspace) return true;
847
- if (record?.meta?.meshNodeId === nodeId) return true;
848
- return false;
679
+ // ─── Worktree / mesh-session cleanup ────────────────────────────────
680
+ // Implementation lives in ./router-worktree-cleanup.ts (behavior-preserving
681
+ // code move). Kept here as thin delegators: cleanupMeshSessions /
682
+ // cleanupLocalWorktreeNode / precheckLocalWorktreeRemovable are bound into
683
+ // MedFamilyContext; bestEffortRemoveWorktreeDir is overridden on the instance
684
+ // by a unit test, so callers reach these via `self.` for correct dispatch.
685
+
686
+ sessionMatchesMeshNode(record: any, node: any, nodeId: string, sessionIds?: Set<string>): boolean {
687
+ return sessionMatchesMeshNode(this, record, node, nodeId, sessionIds);
688
+ }
689
+
690
+ async bestEffortRemoveWorktreeDir(dir: string): Promise<{ removed: boolean; residue: boolean; error?: string }> {
691
+ return bestEffortRemoveWorktreeDir(this, dir);
849
692
  }
850
693
 
851
- private async cleanupLocalWorktreeNode(args: {
694
+ async precheckLocalWorktreeRemovable(args: {
852
695
  mesh: any;
853
696
  node: any;
854
697
  nodeId: string;
855
- }): Promise<{ success: true; skipped?: boolean; removedPath?: string; repoRoot?: string; reason?: string; fallback?: string; forced?: boolean; convergence?: Record<string, unknown> } | { success: false; code: string; error: string; recoveryHint: string; convergence?: Record<string, unknown> }> {
856
- const workspace = typeof args.node?.workspace === 'string' ? args.node.workspace.trim() : '';
857
- if (!workspace) {
858
- return {
859
- success: false,
860
- code: 'mesh_worktree_cleanup_missing_workspace',
861
- error: `Worktree node '${args.nodeId}' is missing workspace metadata`,
862
- recoveryHint: 'Inspect the mesh node record before removing it, or remove stale metadata manually only after confirming no managed worktree remains.',
863
- };
864
- }
865
-
866
- const worktreeExists = fs.existsSync(workspace);
867
- const sourceNode = args.node?.clonedFromNodeId
868
- ? args.mesh?.nodes?.find((n: any) => n.id === args.node.clonedFromNodeId || n.nodeId === args.node.clonedFromNodeId)
869
- : args.mesh?.nodes?.find((n: any) => !n.isLocalWorktree);
870
- const repoRoot = typeof sourceNode?.repoRoot === 'string' && sourceNode.repoRoot.trim()
871
- ? sourceNode.repoRoot.trim()
872
- : typeof sourceNode?.workspace === 'string' && sourceNode.workspace.trim()
873
- ? sourceNode.workspace.trim()
874
- : '';
875
-
876
- if (!worktreeExists) {
877
- return { success: true, skipped: true, removedPath: workspace, repoRoot: repoRoot || undefined, reason: 'worktree_path_missing' };
878
- }
879
- if (!repoRoot || !fs.existsSync(repoRoot)) {
880
- return {
881
- success: false,
882
- code: 'mesh_worktree_cleanup_missing_source_repo',
883
- error: `Refusing to remove worktree '${workspace}' because the source repo root is unavailable`,
884
- recoveryHint: 'Run mesh_remove_node from the machine that owns the source repo, or verify the source node metadata before retrying.',
885
- };
886
- }
887
- if (typeof args.node?.worktreeBranch !== 'string' || !args.node.worktreeBranch.trim()) {
888
- return {
889
- success: false,
890
- code: 'mesh_worktree_cleanup_missing_branch',
891
- error: `Refusing to remove worktree '${workspace}' because worktreeBranch metadata is missing`,
892
- recoveryHint: 'Confirm this is an ADHDev-managed worktree before removing it manually; managed worktree nodes include worktreeBranch metadata.',
893
- };
894
- }
895
-
896
- const { resolveWorktreePath, listWorktrees, removeWorktree } = await import('../git/git-worktree.js');
897
- const normalizePath = (value: string) => {
898
- const resolved = pathResolve(value);
899
- try { return fs.realpathSync(resolved); } catch { return resolved; }
900
- };
901
- const expectedPath = normalizePath(resolveWorktreePath(repoRoot, String(args.mesh?.name || args.mesh?.id || 'mesh'), args.node.worktreeBranch));
902
- const actualPath = normalizePath(workspace);
903
- if (actualPath !== expectedPath) {
904
- return {
905
- success: false,
906
- code: 'mesh_worktree_cleanup_unexpected_path',
907
- error: `Refusing to remove worktree '${workspace}' because it is not at the expected managed path '${expectedPath}'`,
908
- recoveryHint: 'Use git worktree list/status to inspect the path. Retry only after confirming the mesh node metadata points to an ADHDev-managed worktree.',
909
- };
910
- }
911
-
912
- const entries = await listWorktrees(repoRoot);
913
- const managedEntry = entries.find(entry => normalizePath(entry.path) === actualPath);
914
- if (!managedEntry) {
915
- return {
916
- success: false,
917
- code: 'mesh_worktree_cleanup_not_registered',
918
- error: `Refusing to remove '${workspace}' because it is not registered in git worktree list for '${repoRoot}'`,
919
- recoveryHint: 'Inspect git worktree list --porcelain from the source repo. If the path was already removed, prune git worktrees before retrying.',
920
- };
921
- }
922
- if (managedEntry.branch && managedEntry.branch !== args.node.worktreeBranch) {
923
- return {
924
- success: false,
925
- code: 'mesh_worktree_cleanup_branch_mismatch',
926
- error: `Refusing to remove '${workspace}' because git reports branch '${managedEntry.branch}', expected '${args.node.worktreeBranch}'`,
927
- recoveryHint: 'Inspect the worktree branch and mesh metadata before retrying cleanup.',
928
- };
929
- }
930
-
931
- const forceFallbackConvergence = await this.getWorktreeForceCleanupConvergence({
932
- repoRoot,
933
- workspace,
934
- node: args.node,
935
- });
698
+ force?: boolean;
699
+ }): Promise<{ ok: true } | { ok: false; code: string; error: string; recoveryHint: string }> {
700
+ return precheckLocalWorktreeRemovable(this, args);
701
+ }
936
702
 
937
- try {
938
- const result = await removeWorktree(repoRoot, workspace, {
939
- requireClean: true,
940
- allowSubmoduleForceFallback: forceFallbackConvergence.allow,
941
- });
942
- return {
943
- success: true,
944
- removedPath: result.removedPath,
945
- repoRoot,
946
- ...(result.fallback ? {
947
- fallback: result.fallback,
948
- forced: result.forced,
949
- reason: result.reason,
950
- convergence: forceFallbackConvergence,
951
- } : {}),
952
- };
953
- } catch (e: any) {
954
- const message = String(e?.message || e || 'worktree cleanup failed');
955
- const dirty = message.includes('dirty worktree') || message.includes('local changes');
956
- const submoduleForceBlocked = /working trees containing submodules cannot be moved or removed/i.test(message) && !forceFallbackConvergence.allow;
957
- return {
958
- success: false,
959
- code: dirty
960
- ? 'mesh_worktree_cleanup_dirty'
961
- : submoduleForceBlocked
962
- ? 'mesh_worktree_cleanup_force_fallback_blocked'
963
- : 'mesh_worktree_cleanup_failed',
964
- error: submoduleForceBlocked
965
- ? `${message}; refusing --force fallback because convergence could not be verified: ${forceFallbackConvergence.error || 'unknown convergence state'}`
966
- : message,
967
- recoveryHint: dirty
968
- ? 'Commit, stash, or intentionally discard the worktree changes before retrying mesh_remove_node. The mesh registry entry is preserved until cleanup is safe.'
969
- : submoduleForceBlocked
970
- ? 'Verify the worktree branch is merged/contained in the source default branch (for example origin/main) or mark the node with a safe branchConvergence final state before retrying. The mesh registry entry is preserved.'
971
- : 'Inspect git worktree status/list from the source repo and retry after resolving the reported cleanup failure.',
972
- ...(submoduleForceBlocked ? { convergence: forceFallbackConvergence } : {}),
973
- };
974
- }
703
+ async cleanupLocalWorktreeNode(args: {
704
+ mesh: any;
705
+ node: any;
706
+ nodeId: string;
707
+ force?: boolean;
708
+ }): Promise<{ success: true; skipped?: boolean; removedPath?: string; repoRoot?: string; reason?: string; fallback?: string; forced?: boolean; convergence?: Record<string, unknown>; recovered?: boolean; residue?: boolean; residueWarning?: string; residueError?: string; branchRefDeleted?: boolean; branchRefReason?: string; branchRefForced?: boolean; branchRefWarning?: string } | { success: false; code: string; error: string; recoveryHint: string; convergence?: Record<string, unknown> }> {
709
+ return cleanupLocalWorktreeNode(this, args);
975
710
  }
976
711
 
977
- private async getWorktreeForceCleanupConvergence(args: {
712
+ async getWorktreeForceCleanupConvergence(args: {
978
713
  repoRoot: string;
979
714
  workspace: string;
980
715
  node: any;
981
716
  }): Promise<{ allow: boolean; status?: string; source?: string; ref?: string; error?: string }> {
982
- const metadataStatus = typeof args.node?.branchConvergence?.status === 'string'
983
- ? args.node.branchConvergence.status
984
- : '';
985
- if (metadataStatus === 'merged_to_main' || metadataStatus === 'cleanup_candidate') {
986
- return { allow: true, status: metadataStatus, source: 'node_branch_convergence' };
987
- }
988
-
989
- const { execFile } = await import('node:child_process');
990
- const { promisify } = await import('node:util');
991
- const execFileAsync = promisify(execFile);
992
- const runGit = async (gitArgs: string[], cwd: string): Promise<string> => {
993
- const { stdout } = await execFileAsync('git', gitArgs, {
994
- cwd,
995
- encoding: 'utf8',
996
- timeout: 30_000,
997
- maxBuffer: 4 * 1024 * 1024,
998
- windowsHide: true,
999
- });
1000
- return String(stdout || '').trim();
1001
- };
1002
-
1003
- let head = '';
1004
- try {
1005
- head = await runGit(['rev-parse', 'HEAD'], args.workspace);
1006
- } catch (e: any) {
1007
- return { allow: false, error: `could not resolve worktree HEAD: ${e?.message || e}` };
1008
- }
1009
- if (!head) return { allow: false, error: 'worktree HEAD is empty' };
1010
-
1011
- const candidateRefs: string[] = [];
1012
- try {
1013
- const defaultBranch = await runGit(['branch', '--show-current'], args.repoRoot);
1014
- if (defaultBranch) {
1015
- candidateRefs.push(defaultBranch, `origin/${defaultBranch}`);
1016
- }
1017
- } catch { /* fall through to common refs */ }
1018
- candidateRefs.push('origin/main', 'origin/master', 'main', 'master');
1019
-
1020
- const seen = new Set<string>();
1021
- const checkedRefs: string[] = [];
1022
- for (const ref of candidateRefs) {
1023
- if (!ref || seen.has(ref)) continue;
1024
- seen.add(ref);
1025
- let commit = '';
1026
- try {
1027
- commit = await runGit(['rev-parse', '--verify', `${ref}^{commit}`], args.repoRoot);
1028
- } catch {
1029
- continue;
1030
- }
1031
- checkedRefs.push(ref);
1032
- try {
1033
- await runGit(['merge-base', '--is-ancestor', head, commit], args.repoRoot);
1034
- return { allow: true, status: 'merged_to_default_ref', source: 'git_merge_base', ref };
1035
- } catch {
1036
- // Not contained in this candidate ref; keep checking other safe refs.
1037
- }
1038
- }
1039
-
1040
- return {
1041
- allow: false,
1042
- status: metadataStatus || undefined,
1043
- error: checkedRefs.length
1044
- ? `worktree HEAD is not contained in checked refs: ${checkedRefs.join(', ')}`
1045
- : 'no default/main refs were available for convergence verification',
1046
- };
717
+ return getWorktreeForceCleanupConvergence(this, args);
1047
718
  }
1048
719
 
1049
- private isCompletedHostedSession(record: any): boolean {
1050
- return record?.lifecycle === 'stopped' || record?.lifecycle === 'failed' || record?.lifecycle === 'interrupted';
720
+ isCompletedHostedSession(record: any): boolean {
721
+ return isCompletedHostedSession(this, record);
1051
722
  }
1052
723
 
1053
- private async recordIntentionalMeshSessionStop(args: {
724
+ async recordIntentionalMeshSessionStop(args: {
1054
725
  meshId: string;
1055
726
  nodeId: string;
1056
727
  node: any;
1057
728
  sessionId: string;
1058
729
  mode: RepoMeshSessionCleanupMode;
1059
- source: 'mesh_cleanup_sessions' | 'mesh_remove_node';
730
+ source: 'mesh_cleanup_sessions' | 'mesh_remove_node' | 'magi_session_cleanup';
1060
731
  action: 'stop_session' | 'delete_session_force';
1061
732
  }): Promise<void> {
1062
- try {
1063
- const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
1064
- appendLedgerEntry(args.meshId, {
1065
- kind: 'session_stopped',
1066
- nodeId: args.nodeId,
1067
- sessionId: args.sessionId,
1068
- payload: {
1069
- intentional: true,
1070
- reason: 'operator_cleanup',
1071
- intentionalStopReason: 'operator_cleanup',
1072
- source: args.source,
1073
- cleanupMode: args.mode,
1074
- action: args.action,
1075
- workspace: typeof args.node?.workspace === 'string' ? args.node.workspace : undefined,
1076
- },
1077
- });
1078
- } catch (e: any) {
1079
- LOG.warn('MeshCleanup', `Failed to record intentional cleanup stop for ${args.sessionId}: ${e?.message || e}`);
1080
- }
733
+ return recordIntentionalMeshSessionStop(this, args);
1081
734
  }
1082
735
 
1083
- private async cleanupMeshSessions(args: {
736
+ async cleanupMeshSessions(args: {
1084
737
  meshId: string;
1085
738
  nodeId: string;
1086
739
  node: any;
1087
740
  mode: RepoMeshSessionCleanupMode;
1088
741
  sessionIds?: string[];
1089
742
  dryRun?: boolean;
1090
- source?: 'mesh_cleanup_sessions' | 'mesh_remove_node';
743
+ source?: 'mesh_cleanup_sessions' | 'mesh_remove_node' | 'magi_session_cleanup';
744
+ requireAutoLaunchedForTaskIds?: Record<string, string>;
745
+ reclaimOrphans?: boolean;
746
+ liveMeshNodeIds?: string[];
1091
747
  }): Promise<{ success: boolean; [key: string]: unknown }> {
1092
- if (args.mode === 'preserve') {
1093
- return { success: true, mode: 'preserve', matchedCount: 0, stoppedSessionIds: [], deletedSessionIds: [], skippedSessionIds: [] };
1094
- }
1095
- if (!this.deps.sessionHostControl) return { success: false, error: 'Session host control unavailable' };
1096
-
1097
- const requestedSessionIds = Array.isArray(args.sessionIds)
1098
- ? new Set(args.sessionIds.map(id => typeof id === 'string' ? id.trim() : '').filter(Boolean))
1099
- : undefined;
1100
- const sessions = await this.deps.sessionHostControl.listSessions();
1101
- const matched = sessions.filter(record => this.sessionMatchesMeshNode(record, args.node, args.nodeId, requestedSessionIds));
1102
- const hasExplicitSessionIds = !!requestedSessionIds?.size;
1103
- const stoppedSessionIds: string[] = [];
1104
- const deletedSessionIds: string[] = [];
1105
- const skippedSessionIds: string[] = [];
1106
- const skippedLiveSessionIds: string[] = [];
1107
- const deleteUnsupportedSessionIds: string[] = [];
1108
- const recordsRemainSessionIds: string[] = [];
1109
- const errors: Array<{ sessionId: string; error: string }> = [];
1110
- const cleanupSource = args.source || 'mesh_cleanup_sessions';
1111
- const markedIntentionalStopSessionIds = new Set<string>();
1112
- const markIntentionalStop = async (sessionId: string, action: 'stop_session' | 'delete_session_force') => {
1113
- if (args.dryRun || markedIntentionalStopSessionIds.has(sessionId)) return;
1114
- markedIntentionalStopSessionIds.add(sessionId);
1115
- await this.recordIntentionalMeshSessionStop({
1116
- meshId: args.meshId,
1117
- nodeId: args.nodeId,
1118
- node: args.node,
1119
- sessionId,
1120
- mode: args.mode,
1121
- source: cleanupSource,
1122
- action,
1123
- });
1124
- };
1125
- const matchedBySurfaceKind = {
1126
- live_runtime: 0,
1127
- recovery_snapshot: 0,
1128
- inactive_record: 0,
1129
- };
1130
-
1131
- for (const record of matched) {
1132
- const surfaceKind = getSessionHostSurfaceKind(record);
1133
- matchedBySurfaceKind[surfaceKind] += 1;
1134
- }
1135
-
1136
- for (const record of matched) {
1137
- const sessionId = String(record.sessionId);
1138
- const completed = this.isCompletedHostedSession(record);
1139
- const surfaceKind = getSessionHostSurfaceKind(record);
1140
- const liveRuntime = surfaceKind === 'live_runtime';
1141
- if (!hasExplicitSessionIds && liveRuntime) {
1142
- skippedSessionIds.push(sessionId);
1143
- skippedLiveSessionIds.push(sessionId);
1144
- continue;
1145
- }
1146
- try {
1147
- if (args.mode === 'stop') {
1148
- if (!completed) {
1149
- if (!args.dryRun) {
1150
- await markIntentionalStop(sessionId, 'stop_session');
1151
- await this.deps.sessionHostControl.stopSession(sessionId);
1152
- }
1153
- stoppedSessionIds.push(sessionId);
1154
- } else {
1155
- skippedSessionIds.push(sessionId);
1156
- }
1157
- continue;
1158
- }
1159
-
1160
- if (args.mode === 'delete_stopped') {
1161
- if (completed) {
1162
- if (!args.dryRun) await this.deps.sessionHostControl.deleteSession(sessionId, { force: false });
1163
- deletedSessionIds.push(sessionId);
1164
- } else {
1165
- skippedSessionIds.push(sessionId);
1166
- }
1167
- continue;
1168
- }
1169
-
1170
- if (args.mode === 'stop_and_delete') {
1171
- if (!completed) await markIntentionalStop(sessionId, 'delete_session_force');
1172
- if (!args.dryRun) await this.deps.sessionHostControl.deleteSession(sessionId, { force: true });
1173
- deletedSessionIds.push(sessionId);
1174
- continue;
1175
- }
1176
- } catch (e: any) {
1177
- const message = e?.message || String(e);
1178
- if (message.includes('Unsupported session host request: delete_session')
1179
- && (args.mode === 'delete_stopped' || args.mode === 'stop_and_delete')) {
1180
- deleteUnsupportedSessionIds.push(sessionId);
1181
- recordsRemainSessionIds.push(sessionId);
1182
- if (args.mode === 'stop_and_delete' && !completed) {
1183
- try {
1184
- await markIntentionalStop(sessionId, 'stop_session');
1185
- await this.deps.sessionHostControl.stopSession(sessionId);
1186
- stoppedSessionIds.push(sessionId);
1187
- } catch (stopError: any) {
1188
- errors.push({ sessionId, error: stopError?.message || String(stopError) });
1189
- continue;
1190
- }
1191
- }
1192
- skippedSessionIds.push(sessionId);
1193
- continue;
1194
- }
1195
- errors.push({ sessionId, error: message });
1196
- }
1197
- }
1198
-
1199
- const deleteUnsupported = deleteUnsupportedSessionIds.length > 0;
1200
- return {
1201
- success: errors.length === 0,
1202
- mode: args.mode,
1203
- dryRun: args.dryRun === true,
1204
- matchedCount: matched.length,
1205
- matchedBySurfaceKind,
1206
- stoppedSessionIds,
1207
- deletedSessionIds,
1208
- skippedSessionIds,
1209
- skippedLiveSessionIds,
1210
- ...(deleteUnsupported ? {
1211
- deleteUnsupported: true,
1212
- effectiveCleanup: args.mode === 'stop_and_delete'
1213
- ? 'stopped_only_records_remain'
1214
- : 'delete_unsupported_records_remain',
1215
- deleteUnsupportedSessionIds,
1216
- recordsRemainSessionIds,
1217
- } : {}),
1218
- ...(errors.length ? { errors } : {}),
1219
- };
1220
- }
1221
-
1222
- private async traceSessionHostAction<T>(
1223
- action: string,
1224
- args: any,
1225
- run: () => Promise<T>,
1226
- summarizeResult?: (result: T) => Record<string, unknown>,
1227
- ): Promise<T> {
1228
- const interactionId = typeof args?._interactionId === 'string' ? args._interactionId : undefined;
1229
- const sessionId = typeof args?.sessionId === 'string' ? args.sessionId : undefined;
1230
- const requestedPayload: Record<string, unknown> = { action };
1231
- if (sessionId) requestedPayload.sessionId = sessionId;
1232
- if (typeof args?.clientId === 'string') requestedPayload.clientId = args.clientId;
1233
- if (typeof args?.signal === 'string') requestedPayload.signal = args.signal;
1234
- if (typeof args?.providerType === 'string') requestedPayload.providerType = args.providerType;
1235
- if (typeof args?.workspace === 'string') requestedPayload.workspace = args.workspace;
1236
- if (typeof args?.dryRun === 'boolean') requestedPayload.dryRun = args.dryRun;
1237
-
1238
- recordDebugTrace({
1239
- interactionId,
1240
- category: 'session_host',
1241
- stage: 'action_requested',
1242
- level: 'info',
1243
- sessionId,
1244
- payload: requestedPayload,
1245
- });
1246
-
1247
- try {
1248
- const result = await run();
1249
- recordDebugTrace({
1250
- interactionId,
1251
- category: 'session_host',
1252
- stage: 'action_result',
1253
- level: 'info',
1254
- sessionId,
1255
- payload: {
1256
- ...requestedPayload,
1257
- success: true,
1258
- ...(summarizeResult ? summarizeResult(result) : {}),
1259
- },
1260
- });
1261
- return result;
1262
- } catch (error: any) {
1263
- recordDebugTrace({
1264
- interactionId,
1265
- category: 'session_host',
1266
- stage: 'action_failed',
1267
- level: 'error',
1268
- sessionId,
1269
- payload: {
1270
- ...requestedPayload,
1271
- error: error?.message || String(error),
1272
- failureKind: getWriteConflictOwnerClientId(error) ? 'write_conflict' : 'request_failed',
1273
- conflictOwnerClientId: getWriteConflictOwnerClientId(error),
1274
- },
1275
- });
1276
- throw error;
1277
- }
748
+ return cleanupMeshSessions(this, args);
1278
749
  }
1279
-
1280
750
  /**
1281
751
  * Unified command routing.
1282
752
  * Returns result for all commands:
@@ -1347,1631 +817,114 @@ export class DaemonCommandRouter {
1347
817
  }
1348
818
  }
1349
819
 
1350
- // ─── Daemon-level command core ───────────────────
1351
-
1352
- /**
1353
- * Daemon-level command execution (IDE start/stop/restart, CLI, detect, logs).
1354
- * Returns null if not handled at this level → caller delegates to CommandHandler.
1355
- */
1356
- private async executeDaemonCommand(cmd: string, args: any): Promise<CommandRouterResult | null> {
1357
- switch (cmd) {
1358
- // ─── CLI / ACP commands ───
1359
- case 'mesh_forward_event': {
1360
- return handleMeshForwardEvent({ instanceManager: this.deps.instanceManager } as any, args as Record<string, unknown>);
1361
- }
1362
-
1363
- case 'get_pending_mesh_events': {
1364
- const events = drainPendingMeshCoordinatorEvents();
1365
- return { success: true, events };
1366
- }
1367
-
1368
- case 'launch_cli':
1369
- case 'stop_cli':
1370
- case 'set_cli_view_mode':
1371
- case 'agent_command': {
1372
- return this.deps.cliManager.handleCliCommand(cmd, args);
1373
- }
1374
-
1375
- // ─── Logs ───
1376
- case 'get_logs': {
1377
- const count = parseInt(args?.count) || parseInt(args?.lines) || 100;
1378
- const minLevel = args?.minLevel || 'info';
1379
- const sinceTs = args?.since || 0;
1380
-
1381
- try {
1382
- // Priority 1: ring buffer (fast and structured)
1383
- let logs = getRecentLogs(count, minLevel);
1384
- if (sinceTs > 0) {
1385
- logs = logs.filter((l: any) => l.ts > sinceTs);
1386
- }
1387
- if (logs.length > 0) {
1388
- return { success: true, logs, totalBuffered: logs.length };
1389
- }
1390
- // Incremental polling must not fall back to unfiltered file text: the file
1391
- // format is not timestamp-filterable, and returning its tail makes the UI
1392
- // replace structured logs with old raw fallback lines when nothing new exists.
1393
- if (sinceTs > 0) {
1394
- return { success: true, logs: [], totalBuffered: 0 };
1395
- }
1396
- // Priority 2: file fallback
1397
- if (fs.existsSync(LOG_PATH)) {
1398
- const content = fs.readFileSync(LOG_PATH, 'utf-8');
1399
- const allLines = content.split('\n');
1400
- const recent = allLines.slice(-count).join('\n');
1401
- return { success: true, logs: recent, totalLines: allLines.length };
1402
- }
1403
- return { success: true, logs: [], totalBuffered: 0 };
1404
- } catch (e: any) {
1405
- return { success: false, error: e.message };
1406
- }
1407
- }
1408
-
1409
- case 'get_debug_trace': {
1410
- const count = parseInt(args?.count) || parseInt(args?.limit) || 100;
1411
- const sinceTs = Number(args?.since) || 0;
1412
- const interactionId = typeof args?.interactionId === 'string' ? args.interactionId : undefined;
1413
- const category = typeof args?.category === 'string' ? args.category : undefined;
1414
- const trace = getRecentDebugTrace({ interactionId, category, limit: count })
1415
- .filter((entry) => !sinceTs || entry.ts > sinceTs);
1416
- return { success: true, trace, count: trace.length };
1417
- }
1418
-
1419
- case 'session_host_get_diagnostics': {
1420
- if (!this.deps.sessionHostControl) return { success: false, error: 'Session host control unavailable' };
1421
- const diagnostics = await this.traceSessionHostAction('session_host_get_diagnostics', args, () => this.deps.sessionHostControl!.getDiagnostics({
1422
- includeSessions: args?.includeSessions !== false,
1423
- limit: Number(args?.limit) || undefined,
1424
- }), (result) => ({
1425
- includeSessions: args?.includeSessions !== false,
1426
- limit: Number(args?.limit) || undefined,
1427
- ...summarizeSessionHostDiagnostics(result),
1428
- }));
1429
- return { success: true, diagnostics };
1430
- }
1431
-
1432
- case 'session_host_list_sessions': {
1433
- if (!this.deps.sessionHostControl) return { success: false, error: 'Session host control unavailable' };
1434
- const sessions = await this.traceSessionHostAction('session_host_list_sessions', args, () => this.deps.sessionHostControl!.listSessions(), (records) => summarizeSessionHostRecords(records));
1435
- return { success: true, sessions };
1436
- }
1437
-
1438
- case 'session_host_stop_session': {
1439
- if (!this.deps.sessionHostControl) return { success: false, error: 'Session host control unavailable' };
1440
- const sessionId = typeof args?.sessionId === 'string' ? args.sessionId : '';
1441
- if (!sessionId) return { success: false, error: 'sessionId required' };
1442
- const record = await this.traceSessionHostAction('session_host_stop_session', args, () => this.deps.sessionHostControl!.stopSession(sessionId), (result) => summarizeSessionHostRecord(result));
1443
- return { success: true, record };
1444
- }
1445
-
1446
- case 'session_host_resume_session': {
1447
- if (!this.deps.sessionHostControl) return { success: false, error: 'Session host control unavailable' };
1448
- const sessionId = typeof args?.sessionId === 'string' ? args.sessionId : '';
1449
- if (!sessionId) return { success: false, error: 'sessionId required' };
1450
- const record = await this.traceSessionHostAction('session_host_resume_session', args, async () => {
1451
- const nextRecord = await this.deps.sessionHostControl!.resumeSession(sessionId);
1452
- const hosted = toHostedCliRuntimeDescriptor(nextRecord);
1453
- if (hosted) {
1454
- await this.deps.cliManager.restoreHostedSessions([hosted]);
1455
- }
1456
- return nextRecord;
1457
- }, (result) => ({
1458
- ...summarizeSessionHostRecord(result),
1459
- restoredHostedSession: !!toHostedCliRuntimeDescriptor(result),
1460
- }));
1461
- return { success: true, record };
1462
- }
1463
-
1464
- case 'session_host_restart_session': {
1465
- if (!this.deps.sessionHostControl) return { success: false, error: 'Session host control unavailable' };
1466
- const sessionId = typeof args?.sessionId === 'string' ? args.sessionId : '';
1467
- if (!sessionId) return { success: false, error: 'sessionId required' };
1468
- const record = await this.traceSessionHostAction('session_host_restart_session', args, async () => {
1469
- const nextRecord = await this.deps.sessionHostControl!.restartSession(sessionId);
1470
- const hosted = toHostedCliRuntimeDescriptor(nextRecord);
1471
- if (hosted) {
1472
- await this.deps.cliManager.restoreHostedSessions([hosted]);
1473
- }
1474
- return nextRecord;
1475
- }, (result) => ({
1476
- ...summarizeSessionHostRecord(result),
1477
- restoredHostedSession: !!toHostedCliRuntimeDescriptor(result),
1478
- }));
1479
- return { success: true, record };
1480
- }
1481
-
1482
- case 'session_host_send_signal': {
1483
- if (!this.deps.sessionHostControl) return { success: false, error: 'Session host control unavailable' };
1484
- const sessionId = typeof args?.sessionId === 'string' ? args.sessionId : '';
1485
- const signal = typeof args?.signal === 'string' ? args.signal : '';
1486
- if (!sessionId) return { success: false, error: 'sessionId required' };
1487
- if (!signal) return { success: false, error: 'signal required' };
1488
- const record = await this.traceSessionHostAction('session_host_send_signal', args, () => this.deps.sessionHostControl!.sendSignal(sessionId, signal), (result) => summarizeSessionHostRecord(result));
1489
- return { success: true, record };
1490
- }
1491
820
 
1492
- case 'session_host_force_detach_client': {
1493
- if (!this.deps.sessionHostControl) return { success: false, error: 'Session host control unavailable' };
1494
- const sessionId = typeof args?.sessionId === 'string' ? args.sessionId : '';
1495
- const clientId = typeof args?.clientId === 'string' ? args.clientId : '';
1496
- if (!sessionId) return { success: false, error: 'sessionId required' };
1497
- if (!clientId) return { success: false, error: 'clientId required' };
1498
- const record = await this.traceSessionHostAction('session_host_force_detach_client', args, () => this.deps.sessionHostControl!.forceDetachClient(sessionId, clientId), (result) => summarizeSessionHostRecord(result));
1499
- return { success: true, record };
1500
- }
821
+ // ─── Refinery job orchestration ─────────────────────────────────────
822
+ // Implementation lives in ./router-refine.ts (behavior-preserving code move).
823
+ // Only the externally-referenced entry points remain here as thin delegators.
1501
824
 
1502
- case 'session_host_prune_duplicate_sessions': {
1503
- if (!this.deps.sessionHostControl) return { success: false, error: 'Session host control unavailable' };
1504
- const result = await this.traceSessionHostAction('session_host_prune_duplicate_sessions', args, () => this.deps.sessionHostControl!.pruneDuplicateSessions({
1505
- providerType: typeof args?.providerType === 'string' ? args.providerType : undefined,
1506
- workspace: typeof args?.workspace === 'string' ? args.workspace : undefined,
1507
- dryRun: args?.dryRun === true,
1508
- }), (value) => summarizeSessionHostPruneResult(value));
1509
- return { success: true, result };
1510
- }
1511
-
1512
- case 'session_host_acquire_write': {
1513
- if (!this.deps.sessionHostControl) return { success: false, error: 'Session host control unavailable' };
1514
- const sessionId = typeof args?.sessionId === 'string' ? args.sessionId : '';
1515
- const clientId = typeof args?.clientId === 'string' ? args.clientId : '';
1516
- const ownerType = args?.ownerType === 'agent' ? 'agent' : 'user';
1517
- if (!sessionId) return { success: false, error: 'sessionId required' };
1518
- if (!clientId) return { success: false, error: 'clientId required' };
1519
- const record = await this.traceSessionHostAction('session_host_acquire_write', args, () => this.deps.sessionHostControl!.acquireWrite({
1520
- sessionId,
1521
- clientId,
1522
- ownerType,
1523
- force: args?.force !== false,
1524
- }), (result) => ({
1525
- ...summarizeSessionHostRecord(result),
1526
- ownerType,
1527
- }));
1528
- return { success: true, record };
1529
- }
1530
-
1531
- case 'session_host_release_write': {
1532
- if (!this.deps.sessionHostControl) return { success: false, error: 'Session host control unavailable' };
1533
- const sessionId = typeof args?.sessionId === 'string' ? args.sessionId : '';
1534
- const clientId = typeof args?.clientId === 'string' ? args.clientId : '';
1535
- if (!sessionId) return { success: false, error: 'sessionId required' };
1536
- if (!clientId) return { success: false, error: 'clientId required' };
1537
- const record = await this.traceSessionHostAction('session_host_release_write', args, () => this.deps.sessionHostControl!.releaseWrite({
1538
- sessionId,
1539
- clientId,
1540
- }), (result) => summarizeSessionHostRecord(result));
1541
- return { success: true, record };
1542
- }
1543
-
1544
- case 'list_saved_sessions': {
1545
- const providerType = typeof args?.providerType === 'string'
1546
- ? args.providerType.trim()
1547
- : typeof args?.agentType === 'string'
1548
- ? args.agentType.trim()
1549
- : '';
1550
- const kind = args?.kind === 'acp' ? 'acp' : 'cli';
1551
- if (!providerType) {
1552
- return { success: false, error: 'providerType required' };
1553
- }
1554
-
1555
- const wantsAll = args?.all === true;
1556
- const offset = wantsAll ? 0 : Math.max(0, Number(args?.offset) || 0);
1557
- const limit = wantsAll ? Number.MAX_SAFE_INTEGER : Math.max(1, Math.min(100, Number(args?.limit) || 30));
1558
- const requestedWorkspace = typeof args?.workspace === 'string' ? args.workspace.trim() : '';
1559
- const requestedProviderSessionId = typeof args?.providerSessionId === 'string'
1560
- ? args.providerSessionId.trim()
1561
- : typeof args?.activeProviderSessionId === 'string'
1562
- ? args.activeProviderSessionId.trim()
1563
- : '';
1564
- const providerMeta = this.deps.providerLoader.resolve?.(providerType) || this.deps.providerLoader.getMeta(providerType);
1565
- const { sessions: historySessions, hasMore, source } = listProviderHistorySessions(providerType, {
1566
- canonicalHistory: providerMeta?.canonicalHistory,
1567
- offset,
1568
- limit,
1569
- historyBehavior: providerMeta?.historyBehavior,
1570
- scripts: providerMeta?.scripts as any,
1571
- });
1572
- const state = loadState();
1573
- const savedSessions = getSavedProviderSessions(state, { providerType, kind });
1574
- const recentSessions = getRecentActivity(state, 200)
1575
- .filter(entry => entry.providerType === providerType && entry.kind === kind && entry.providerSessionId);
1576
- const savedSessionById = new Map(savedSessions.map(entry => [entry.providerSessionId, entry]));
1577
- const recentSessionById = new Map(recentSessions.map(entry => [entry.providerSessionId!, entry]));
1578
- const canResumeById = supportsExplicitSessionResume(providerMeta?.resume);
1579
-
1580
- return {
1581
- success: true,
1582
- sessions: historySessions.map(session => {
1583
- const saved = savedSessionById.get(session.historySessionId);
1584
- const recent = recentSessionById.get(session.historySessionId);
1585
- const workspace = saved?.workspace
1586
- || recent?.workspace
1587
- || session.workspace
1588
- || (requestedWorkspace && requestedProviderSessionId === session.historySessionId ? requestedWorkspace : undefined);
1589
- return {
1590
- id: session.historySessionId,
1591
- providerSessionId: session.historySessionId,
1592
- providerType,
1593
- providerName: saved?.providerName || recent?.providerName || providerType,
1594
- kind: saved?.kind || recent?.kind || kind,
1595
- title: saved?.title || recent?.title || session.sessionTitle || session.preview || providerType,
1596
- workspace,
1597
- summaryMetadata: saved?.summaryMetadata || recent?.summaryMetadata,
1598
- preview: session.preview,
1599
- messageCount: session.messageCount,
1600
- firstMessageAt: session.firstMessageAt,
1601
- lastMessageAt: session.lastMessageAt,
1602
- canResume: !!workspace && canResumeById,
1603
- historySource: session.source,
1604
- sourcePath: session.sourcePath,
1605
- sourceMtimeMs: session.sourceMtimeMs,
1606
- };
1607
- }),
1608
- hasMore,
1609
- source,
1610
- };
1611
- }
1612
-
1613
- // ─── restart_session: IDE / CLI / ACP unified ───
1614
- case 'restart_session': {
1615
- const targetType = args?.cliType || args?.agentType || args?.ideType;
1616
- if (!targetType) throw new Error('cliType or ideType required');
1617
-
1618
- // Check if IDE (in cdpManagers or provider category is ide)
1619
- const isIde = this.deps.cdpManagers.has(targetType) ||
1620
- this.deps.providerLoader.getMeta(targetType)?.category === 'ide';
1621
-
1622
- if (isIde) {
1623
- // IDE restart: stop (with process kill) → launch
1624
- await this.stopIde(targetType, true);
1625
- const launchResult = await this.executeDaemonCommand('launch_ide', { ideType: targetType, enableCdp: true, workspace: args?.workspace });
1626
- return { success: true, restarted: true, ideType: targetType, launch: launchResult };
1627
- }
1628
-
1629
- // CLI/ACP restart: delegate to CliManager
1630
- return this.deps.cliManager.handleCliCommand(cmd, args);
1631
- }
1632
-
1633
- // ─── IDE stop ───
1634
- case 'stop_ide': {
1635
- const ideType = args?.ideType;
1636
- if (!ideType) throw new Error('ideType required');
1637
- const killProcess = args?.killProcess !== false; // default true
1638
- await this.stopIde(ideType, killProcess);
1639
- try {
1640
- const results = await detectIDEs(this.deps.providerLoader);
1641
- this.deps.detectedIdes.value = results;
1642
- this.deps.providerLoader.setIdeDetectionResults(results, true);
1643
- } catch { /* ignore detection refresh errors */ }
1644
- return { success: true, ideType, stopped: true, processKilled: killProcess };
1645
- }
1646
-
1647
- // ─── IDE restart ───
1648
- case 'restart_ide': {
1649
- const ideType = args?.ideType;
1650
- if (!ideType) throw new Error('ideType required');
1651
- await this.stopIde(ideType, true); // always kill process on restart
1652
- const launchResult = await this.executeDaemonCommand('launch_ide', { ideType, enableCdp: true, workspace: args?.workspace });
1653
- return { success: true, ideType, restarted: true, launch: launchResult };
1654
- }
1655
-
1656
- // ─── IDE launch + CDP connect ───
1657
- case 'launch_ide': {
1658
- const ideKey = args?.ideId || args?.ideType;
1659
- const resolvedWorkspace = resolveIdeLaunchWorkspace(
1660
- {
1661
- workspace: args?.workspace,
1662
- workspaceId: args?.workspaceId,
1663
- useDefaultWorkspace: args?.useDefaultWorkspace,
1664
- },
1665
- loadConfig(),
1666
- );
1667
- const launchArgs = {
1668
- ideId: ideKey,
1669
- workspace: resolvedWorkspace,
1670
- newWindow: args?.newWindow,
1671
- };
1672
- LOG.info('LaunchIDE', `target=${ideKey || 'auto'}`);
1673
- const result = await launchWithCdp(launchArgs);
1674
-
1675
- if (result.success && result.port && result.ideId && !this.deps.cdpManagers.has(result.ideId)) {
1676
- const logFn = this.deps.getCdpLogFn
1677
- ? this.deps.getCdpLogFn(result.ideId)
1678
- : LOG.forComponent(`CDP:${result.ideId}`).asLogFn();
1679
- const provider = this.deps.providerLoader.getMeta(result.ideId);
1680
- const manager = new DaemonCdpManager(result.port, logFn, undefined, provider?.targetFilter);
1681
- const connected = await manager.connect();
1682
- if (connected) {
1683
- // Register active extension providers for this IDE in CDP manager
1684
- registerExtensionProviders(this.deps.providerLoader, manager, result.ideId);
1685
- this.deps.cdpManagers.set(result.ideId, manager);
1686
- LOG.info('CDP', `Connected: ${result.ideId} (port ${result.port})`);
1687
- LOG.info('CDP', `${this.deps.cdpManagers.size} IDE(s) connected`);
1688
-
1689
- // Notify consumer (e.g. setupIdeInstance)
1690
- this.deps.onCdpManagerCreated?.(result.ideId, manager);
1691
- }
1692
- }
1693
- this.deps.onIdeConnected?.();
1694
- try {
1695
- const results = await detectIDEs(this.deps.providerLoader);
1696
- this.deps.detectedIdes.value = results;
1697
- this.deps.providerLoader.setIdeDetectionResults(results, true);
1698
- } catch { /* ignore detection refresh errors */ }
1699
- if (result.success && resolvedWorkspace) {
1700
- try {
1701
- const next = appendRecentActivity(loadState(), {
1702
- kind: 'ide',
1703
- providerType: result.ideId || ideKey,
1704
- providerName: result.ideId || ideKey,
1705
- workspace: resolvedWorkspace,
1706
- title: result.ideId || ideKey,
1707
- });
1708
- saveState(next);
1709
- } catch { /* ignore activity persist errors */ }
1710
- } else if (result.success && (result.ideId || ideKey)) {
1711
- try {
1712
- saveState(appendRecentActivity(loadState(), {
1713
- kind: 'ide',
1714
- providerType: result.ideId || ideKey,
1715
- providerName: result.ideId || ideKey,
1716
- title: result.ideId || ideKey,
1717
- }));
1718
- } catch { /* ignore activity persist errors */ }
1719
- }
1720
- return { ...result };
1721
- }
1722
-
1723
- // ─── Detect providers ───
1724
- case 'detect_provider': {
1725
- const providerType = typeof args?.providerType === 'string' ? args.providerType.trim() : '';
1726
- if (!providerType) return { success: false, error: 'providerType is required' };
1727
- const normalizedType = this.deps.providerLoader.resolveAlias(providerType);
1728
- const provider = this.deps.providerLoader.getByAlias(providerType);
1729
- if (!provider) return { success: false, error: `Provider not found: ${providerType}` };
1730
- if (provider.category !== 'cli' && provider.category !== 'acp') {
1731
- return { success: false, error: `Provider detection is only supported for CLI/ACP providers: ${providerType}` };
1732
- }
1733
- if (!this.deps.providerLoader.isMachineProviderEnabled(normalizedType)) {
1734
- return { success: false, error: `Provider is disabled on this machine: ${providerType}` };
1735
- }
1736
- const detected = await detectCLI(normalizedType, this.deps.providerLoader, { includeVersion: false });
1737
- this.deps.providerLoader.setCliDetectionResults([{
1738
- id: normalizedType,
1739
- installed: !!detected,
1740
- path: detected?.path,
1741
- }], false);
1742
- this.deps.onStatusChange?.();
1743
- return {
1744
- success: true,
1745
- providerType: normalizedType,
1746
- detected: !!detected,
1747
- path: detected?.path || null,
1748
- };
1749
- }
1750
-
1751
- // ─── Detect IDEs ───
1752
- case 'detect_ides': {
1753
- const results = await detectIDEs(this.deps.providerLoader);
1754
- this.deps.detectedIdes.value = results;
1755
- this.deps.providerLoader.setIdeDetectionResults(results, true);
1756
- return { success: true, detectedInfo: results };
1757
- }
1758
-
1759
- // ─── Set User Name ───
1760
- case 'set_user_name': {
1761
- const name = args?.userName;
1762
- if (!name || typeof name !== 'string') throw new Error('userName required');
1763
- updateConfig({ userName: name });
1764
- return { success: true, userName: name };
1765
- }
1766
-
1767
- case 'get_status_metadata': {
1768
- const snapshot = buildStatusSnapshot({
1769
- allStates: this.deps.instanceManager.collectAllStates(),
1770
- cdpManagers: this.deps.cdpManagers,
1771
- providerLoader: this.deps.providerLoader,
1772
- detectedIdes: this.deps.detectedIdes.value,
1773
- instanceId: this.deps.statusInstanceId || loadConfig().machineId || 'daemon',
1774
- version: this.deps.statusVersion || 'unknown',
1775
- profile: 'metadata',
1776
- });
1777
- return { success: true, status: snapshot };
1778
- }
1779
-
1780
- case 'get_machine_runtime_stats': {
1781
- return {
1782
- success: true,
1783
- machine: buildMachineInfo('full'),
1784
- timestamp: Date.now(),
1785
- };
1786
- }
1787
-
1788
- case 'mark_session_seen': {
1789
- const sessionId = args?.sessionId;
1790
- if (!sessionId || typeof sessionId !== 'string') {
1791
- return { success: false, error: 'sessionId is required' };
1792
- }
1793
- const currentState = loadState();
1794
- const prevSeenAt = currentState.sessionReads?.[sessionId] || 0;
1795
- const sessionEntries = buildSessionEntries(
1796
- this.deps.instanceManager.collectAllStates(),
1797
- this.deps.cdpManagers,
1798
- );
1799
- const targetSession = sessionEntries.find((entry) => entry.id === sessionId);
1800
- const requestedCompletionMarker = typeof args?.completionMarker === 'string'
1801
- ? args.completionMarker.trim()
1802
- : '';
1803
- const completionMarker = requestedCompletionMarker || (targetSession ? getSessionCompletionMarker(targetSession) : '');
1804
- const requestedProviderSessionId = typeof args?.providerSessionId === 'string'
1805
- ? args.providerSessionId.trim()
1806
- : '';
1807
- const providerSessionId = requestedProviderSessionId || targetSession?.providerSessionId;
1808
- const next = markSessionSeen(
1809
- currentState,
1810
- sessionId,
1811
- typeof args?.seenAt === 'number' ? args.seenAt : Date.now(),
1812
- completionMarker,
1813
- providerSessionId,
1814
- );
1815
- if (READ_DEBUG_ENABLED) {
1816
- LOG.info('RecentRead', `mark_session_seen sessionId=${sessionId} seenAt=${String(args?.seenAt || '')} prevSeenAt=${String(prevSeenAt)} nextSeenAt=${String(next.sessionReads?.[sessionId] || 0)} marker=${completionMarker || '-'}`);
1817
- }
1818
- saveState(next);
1819
- this.deps.onStatusChange?.();
1820
- return {
1821
- success: true,
1822
- sessionId,
1823
- seenAt: next.sessionReads?.[sessionId] || Date.now(),
1824
- completionMarker,
1825
- };
1826
- }
1827
-
1828
- case 'delete_notification': {
1829
- const sessionId = args?.sessionId;
1830
- const notificationId = typeof args?.notificationId === 'string' ? args.notificationId.trim() : '';
1831
- if (!sessionId || typeof sessionId !== 'string') {
1832
- return { success: false, error: 'sessionId is required' };
1833
- }
1834
- if (!notificationId) {
1835
- return { success: false, error: 'notificationId is required' };
1836
- }
1837
- const sessionEntries = buildSessionEntries(
1838
- this.deps.instanceManager.collectAllStates(),
1839
- this.deps.cdpManagers,
1840
- );
1841
- const targetSession = sessionEntries.find((entry) => entry.id === sessionId);
1842
- const next = dismissSessionNotification(
1843
- loadState(),
1844
- sessionId,
1845
- notificationId,
1846
- targetSession?.providerSessionId,
1847
- );
1848
- saveState(next);
1849
- this.deps.onStatusChange?.();
1850
- return {
1851
- success: true,
1852
- sessionId,
1853
- notificationId,
1854
- };
1855
- }
1856
-
1857
- case 'mark_notification_unread': {
1858
- const sessionId = args?.sessionId;
1859
- const notificationId = typeof args?.notificationId === 'string' ? args.notificationId.trim() : '';
1860
- if (!sessionId || typeof sessionId !== 'string') {
1861
- return { success: false, error: 'sessionId is required' };
1862
- }
1863
- if (!notificationId) {
1864
- return { success: false, error: 'notificationId is required' };
1865
- }
1866
- const sessionEntries = buildSessionEntries(
1867
- this.deps.instanceManager.collectAllStates(),
1868
- this.deps.cdpManagers,
1869
- );
1870
- const targetSession = sessionEntries.find((entry) => entry.id === sessionId);
1871
- const next = markSessionNotificationUnread(
1872
- loadState(),
1873
- sessionId,
1874
- notificationId,
1875
- targetSession?.providerSessionId,
1876
- );
1877
- saveState(next);
1878
- this.deps.onStatusChange?.();
1879
- return {
1880
- success: true,
1881
- sessionId,
1882
- notificationId,
1883
- };
1884
- }
1885
-
1886
- // ─── Daemon Self-Upgrade ───
1887
- case 'daemon_upgrade': {
1888
- LOG.info('Upgrade', 'Remote upgrade requested from dashboard');
1889
- try {
1890
- // Detect package name for upgrade
1891
- const isStandalone = this.deps.packageName === '@adhdev/daemon-standalone'
1892
- || process.argv[1]?.includes('daemon-standalone');
1893
- const pkgName = isStandalone ? '@adhdev/daemon-standalone' : 'adhdev';
1894
- const npmSurface = resolveCurrentGlobalInstallSurface({ packageName: pkgName });
1895
- const channel = resolveUpgradeChannel(args);
1896
- const npmTag = CHANNEL_NPM_TAG[channel];
1897
-
1898
- // Check channel-pinned dist-tag and resolve it to a concrete install version.
1899
- const latest = String(execNpmCommandSync(['view', `${pkgName}@${npmTag}`, 'version'], { encoding: 'utf-8', timeout: 10000 }, npmSurface)).trim();
1900
- LOG.info('Upgrade', `Latest ${pkgName}@${npmTag}: v${latest}`);
1901
- updateConfig({ updateChannel: channel, serverUrl: CHANNEL_SERVER_URL[channel] } as any);
1902
- let currentInstalled: string | null = null;
1903
- try {
1904
- const currentJson = String(execNpmCommandSync(['ls', '-g', pkgName, '--depth=0', '--json'], {
1905
- encoding: 'utf-8',
1906
- timeout: 10000,
1907
- stdio: ['pipe', 'pipe', 'pipe'],
1908
- }, npmSurface)).trim();
1909
- const parsed = JSON.parse(currentJson);
1910
- currentInstalled = parsed?.dependencies?.[pkgName]?.version || null;
1911
- } catch {
1912
- // ignore ls failures; upgrade can still proceed
1913
- }
1914
-
1915
- const runningVersion = typeof this.deps.statusVersion === 'string'
1916
- ? this.deps.statusVersion.trim().replace(/^v/, '')
1917
- : null;
1918
- if (currentInstalled === latest && runningVersion === latest) {
1919
- LOG.info('Upgrade', `Already on ${channel} channel version v${latest}; skipping install`);
1920
- return { success: true, upgraded: false, alreadyLatest: true, version: latest, channel, npmTag };
1921
- }
1922
- if (currentInstalled === latest && runningVersion && runningVersion !== latest) {
1923
- LOG.info('Upgrade', `Installed package is v${latest}, but running daemon is v${runningVersion}; scheduling restart`);
1924
- }
1925
-
1926
- spawnDetachedDaemonUpgradeHelper({
1927
- packageName: pkgName,
1928
- targetVersion: latest,
1929
- parentPid: process.pid,
1930
- restartArgv: process.argv.slice(1),
1931
- cwd: process.cwd(),
1932
- sessionHostAppName: process.env.ADHDEV_SESSION_HOST_NAME || 'adhdev',
1933
- });
1934
- LOG.info('Upgrade', `Scheduled detached ${channel} upgrade to v${latest}`);
1935
-
1936
- // Exit after the command response has been sent so the helper can replace the package cleanly.
1937
- setTimeout(() => {
1938
- LOG.info('Upgrade', 'Exiting daemon so detached upgrader can continue...');
1939
- process.exit(0);
1940
- }, 3000);
1941
-
1942
- return { success: true, upgraded: true, version: latest, restarting: true, channel, npmTag };
1943
- } catch (e: any) {
1944
- LOG.error('Upgrade', `Failed: ${e.message}`);
1945
- return { success: false, error: e.message };
1946
- }
1947
- }
1948
-
1949
- // ─── Machine Settings ───
1950
- case 'set_machine_nickname': {
1951
- const nickname = args?.nickname;
1952
- updateConfig({ machineNickname: nickname || null });
1953
- return { success: true };
1954
- }
1955
-
1956
- // ─── Mesh CRUD (local meshes.json) ───
1957
- case 'list_meshes': {
1958
- try {
1959
- const { listMeshes } = await import('../config/mesh-config.js');
1960
- return { success: true, meshes: listMeshes() };
1961
- } catch (e: any) {
1962
- return { success: false, error: e.message };
1963
- }
1964
- }
1965
-
1966
- case 'get_mesh': {
1967
- const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
1968
- if (!meshId) return { success: false, error: 'meshId required' };
1969
- try {
1970
- const { getMesh } = await import('../config/mesh-config.js');
1971
- const mesh = getMesh(meshId);
1972
- if (mesh) return { success: true, mesh };
1973
- } catch { /* fall through to inline cache */ }
1974
- // Fallback: check in-memory cache for cloud-originating meshes
1975
- const cached = this.inlineMeshCache.get(meshId);
1976
- if (cached) return { success: true, mesh: cached };
1977
- return { success: false, error: 'Mesh not found' };
1978
- }
1979
-
1980
- case 'create_mesh': {
1981
- const name = typeof args?.name === 'string' ? args.name.trim() : '';
1982
- const repoIdentity = typeof args?.repoIdentity === 'string' ? args.repoIdentity.trim() : '';
1983
- const repoRemoteUrl = typeof args?.repoRemoteUrl === 'string' ? args.repoRemoteUrl.trim() : undefined;
1984
- const defaultBranch = typeof args?.defaultBranch === 'string' ? args.defaultBranch.trim() : undefined;
1985
- if (!name) return { success: false, error: 'name required' };
1986
- try {
1987
- const { createMesh } = await import('../config/mesh-config.js');
1988
- const mesh = createMesh({ name, repoIdentity, repoRemoteUrl, defaultBranch, policy: args?.policy });
1989
- return { success: true, mesh };
1990
- } catch (e: any) {
1991
- return { success: false, error: e.message };
1992
- }
1993
- }
1994
-
1995
- case 'update_mesh': {
1996
- const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
1997
- if (!meshId) return { success: false, error: 'meshId required' };
1998
- try {
1999
- const { updateMesh } = await import('../config/mesh-config.js');
2000
- const patch: Record<string, unknown> = {};
2001
- if (typeof args?.name === 'string') patch.name = args.name;
2002
- if (typeof args?.defaultBranch === 'string') patch.defaultBranch = args.defaultBranch;
2003
- if (args?.policy && typeof args.policy === 'object' && !Array.isArray(args.policy)) patch.policy = args.policy;
2004
- if (args?.coordinator && typeof args.coordinator === 'object' && !Array.isArray(args.coordinator)) patch.coordinator = args.coordinator;
2005
- if (!Object.keys(patch).length) return { success: false, error: 'No updates provided' };
2006
- const mesh = updateMesh(meshId, patch as any);
2007
- if (!mesh) return { success: false, error: 'Mesh not found' };
2008
- this.inlineMeshCache.set(meshId, mesh);
2009
- return { success: true, mesh };
2010
- } catch (e: any) {
2011
- return { success: false, error: e.message };
2012
- }
2013
- }
2014
-
2015
- case 'delete_mesh': {
2016
- const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
2017
- if (!meshId) return { success: false, error: 'meshId required' };
2018
- try {
2019
- const { deleteMesh } = await import('../config/mesh-config.js');
2020
- const deleted = deleteMesh(meshId);
2021
- return { success: true, deleted };
2022
- } catch (e: any) {
2023
- return { success: false, error: e.message };
2024
- }
2025
- }
2026
-
2027
- case 'get_mesh_ledger': {
2028
- const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
2029
- if (!meshId) return { success: false, error: 'meshId required' };
2030
- try {
2031
- const { readLedgerEntries, getLedgerSummary } = await import('../mesh/mesh-ledger.js');
2032
- const tail = typeof args?.tail === 'number' ? args.tail : 20;
2033
- const since = typeof args?.since === 'string' ? args.since : undefined;
2034
- const kind = Array.isArray(args?.kind) ? args.kind.filter((k: any) => typeof k === 'string') : undefined;
2035
- const entries = readLedgerEntries(meshId, { tail, since, kind });
2036
- const summary = getLedgerSummary(meshId);
2037
- return { success: true, entries, summary };
2038
- } catch (e: any) {
2039
- return { success: false, error: e.message };
2040
- }
2041
- }
2042
-
2043
- case 'get_mesh_ledger_slice': {
2044
- const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
2045
- if (!meshId) return { success: false, error: 'meshId required' };
2046
- try {
2047
- const { readLedgerSlice } = await import('../mesh/mesh-ledger.js');
2048
- const kind = Array.isArray(args?.kind) ? args.kind.filter((k: any) => typeof k === 'string') : undefined;
2049
- const slice = readLedgerSlice(meshId, {
2050
- afterId: typeof args?.afterId === 'string' ? args.afterId : undefined,
2051
- since: typeof args?.since === 'string' ? args.since : undefined,
2052
- kind,
2053
- limit: typeof args?.limit === 'number' ? args.limit : undefined,
2054
- });
2055
- return { success: true, slice };
2056
- } catch (e: any) {
2057
- return { success: false, error: e.message };
2058
- }
2059
- }
2060
-
2061
- case 'import_mesh_ledger_slice': {
2062
- const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
2063
- if (!meshId) return { success: false, error: 'meshId required' };
2064
- try {
2065
- const { appendRemoteLedgerEntries, getLedgerSummary } = await import('../mesh/mesh-ledger.js');
2066
- const entries = Array.isArray(args?.entries)
2067
- ? args.entries as any[]
2068
- : Array.isArray(args?.slice?.entries)
2069
- ? args.slice.entries as any[]
2070
- : [];
2071
- const result = appendRemoteLedgerEntries(meshId, entries as any);
2072
- return { success: true, result, summary: getLedgerSummary(meshId) };
2073
- } catch (e: any) {
2074
- return { success: false, error: e.message };
2075
- }
2076
- }
2077
-
2078
- case 'get_mesh_queue': {
2079
- const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
2080
- if (!meshId) return { success: false, error: 'meshId required' };
2081
- try {
2082
- const { getMeshQueueStats, getQueue } = await import('../mesh/mesh-work-queue.js');
2083
- const status = Array.isArray(args?.status)
2084
- ? args.status.map((s: any) => typeof s === 'string' ? s.trim() : '').filter(Boolean)
2085
- : undefined;
2086
- const queue = getQueue(meshId, { status: status as any });
2087
- const summary = getMeshQueueStats(meshId);
2088
- return {
2089
- success: true,
2090
- queue,
2091
- summary,
2092
- sourceOfTruth: {
2093
- kind: 'mesh_work_queue_file',
2094
- activeStatuses: ['pending', 'assigned'],
2095
- historicalStatuses: ['completed', 'failed', 'cancelled'],
2096
- notes: 'pending/assigned are active work; completed/failed/cancelled are historical records.',
2097
- },
2098
- };
2099
- } catch (e: any) {
2100
- return { success: false, error: e.message };
2101
- }
2102
- }
2103
-
2104
- case 'cancel_mesh_queue_task': {
2105
- const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
2106
- const taskId = typeof args?.taskId === 'string' ? args.taskId.trim() : '';
2107
- if (!meshId || !taskId) return { success: false, error: 'meshId and taskId required' };
2108
- try {
2109
- const { cancelTask } = await import('../mesh/mesh-work-queue.js');
2110
- const reason = typeof args?.reason === 'string' ? args.reason : undefined;
2111
- const task = cancelTask(meshId, taskId, { reason });
2112
- if (!task) return { success: false, error: `Queue task '${taskId}' not found` };
2113
- return { success: true, task };
2114
- } catch (e: any) {
2115
- return { success: false, error: e.message };
2116
- }
2117
- }
2118
-
2119
- case 'requeue_mesh_queue_task': {
2120
- const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
2121
- const taskId = typeof args?.taskId === 'string' ? args.taskId.trim() : '';
2122
- if (!meshId || !taskId) return { success: false, error: 'meshId and taskId required' };
2123
- try {
2124
- const { requeueTask } = await import('../mesh/mesh-work-queue.js');
2125
- const task = requeueTask(meshId, taskId, {
2126
- reason: typeof args?.reason === 'string' ? args.reason : undefined,
2127
- targetNodeId: typeof args?.targetNodeId === 'string' ? args.targetNodeId.trim() : undefined,
2128
- targetSessionId: typeof args?.targetSessionId === 'string' ? args.targetSessionId.trim() : undefined,
2129
- clearTargetNode: args?.clearTargetNode === true,
2130
- clearTargetSession: args?.clearTargetSession !== false,
2131
- });
2132
- if (!task) return { success: false, error: `Queue task '${taskId}' not found` };
2133
- return { success: true, task };
2134
- } catch (e: any) {
2135
- return { success: false, error: e.message };
2136
- }
2137
- }
2138
-
2139
- case 'add_mesh_node': {
2140
- const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
2141
- const workspace = typeof args?.workspace === 'string' ? args.workspace.trim() : '';
2142
- if (!meshId) return { success: false, error: 'meshId required' };
2143
- if (!workspace) return { success: false, error: 'workspace required' };
2144
- try {
2145
- const { addNode } = await import('../config/mesh-config.js');
2146
- const providerPriority = Array.isArray(args?.providerPriority)
2147
- ? args.providerPriority.map((type: any) => typeof type === 'string' ? type.trim() : '').filter(Boolean)
2148
- : [];
2149
- const readOnly = args?.readOnly === true;
2150
- const policy = {
2151
- ...(readOnly ? { readOnly: true } : {}),
2152
- ...(providerPriority.length ? { providerPriority } : {}),
2153
- };
2154
- const node = addNode(meshId, { workspace, ...(policy ? { policy } : {}) });
2155
- if (!node) return { success: false, error: 'Mesh not found' };
2156
- return { success: true, node };
2157
- } catch (e: any) {
2158
- return { success: false, error: e.message };
2159
- }
2160
- }
2161
-
2162
- case 'update_mesh_node': {
2163
- const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
2164
- const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
2165
- if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
2166
- try {
2167
- const { updateNode } = await import('../config/mesh-config.js');
2168
- const policy = args?.policy && typeof args.policy === 'object' && !Array.isArray(args.policy)
2169
- ? { ...(args.policy as Record<string, unknown>) }
2170
- : {};
2171
- if (Array.isArray(args?.providerPriority)) {
2172
- const providerPriority = args.providerPriority
2173
- .map((type: any) => typeof type === 'string' ? type.trim() : '')
2174
- .filter(Boolean);
2175
- delete (policy as any).provider_priority;
2176
- if (providerPriority.length) {
2177
- (policy as any).providerPriority = providerPriority;
2178
- } else {
2179
- delete (policy as any).providerPriority;
2180
- }
2181
- }
2182
- const node = updateNode(meshId, nodeId, { policy: policy as any });
2183
- if (!node) return { success: false, error: 'Mesh node not found' };
2184
- return { success: true, node };
2185
- } catch (e: any) {
2186
- return { success: false, error: e.message };
2187
- }
2188
- }
2189
-
2190
- case 'cleanup_mesh_sessions': {
2191
- const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
2192
- const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
2193
- if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
2194
- try {
2195
- const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
2196
- const mesh = meshRecord?.mesh;
2197
- if (!mesh) return { success: false, error: 'Mesh not found' };
2198
- const node = mesh?.nodes?.find((n: any) => n.id === nodeId || n.nodeId === nodeId);
2199
- if (!node) return { success: false, error: `Node '${nodeId}' not found in mesh` };
2200
- const mode = this.normalizeMeshSessionCleanupMode(args?.mode ?? mesh?.policy?.sessionCleanupOnNodeRemove);
2201
- const sessionIds = Array.isArray(args?.sessionIds)
2202
- ? args.sessionIds.map((id: any) => typeof id === 'string' ? id.trim() : '').filter(Boolean)
2203
- : undefined;
2204
- const result = await this.cleanupMeshSessions({
2205
- meshId,
2206
- nodeId,
2207
- node,
2208
- mode,
2209
- sessionIds,
2210
- dryRun: args?.dryRun === true,
2211
- source: 'mesh_cleanup_sessions',
2212
- });
2213
- return result;
2214
- } catch (e: any) {
2215
- return { success: false, error: e.message };
2216
- }
2217
- }
2218
-
2219
- case 'refine_mesh_node': {
2220
- const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
2221
- const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
2222
- if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
2223
- try {
2224
- const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
2225
- const mesh = meshRecord?.mesh;
2226
- const node = mesh?.nodes?.find((n: any) => n.id === nodeId || n.nodeId === nodeId);
2227
- if (!node) return { success: false, error: `Node '${nodeId}' not found in mesh` };
2228
-
2229
- if (!node.isLocalWorktree || !node.workspace) {
2230
- return { success: false, error: `Refinery requires a local worktree node` };
2231
- }
2232
-
2233
- const sourceNode = node.clonedFromNodeId
2234
- ? mesh?.nodes.find((n: any) => n.id === node.clonedFromNodeId || n.nodeId === node.clonedFromNodeId)
2235
- : mesh?.nodes.find((n: any) => !n.isLocalWorktree);
2236
- const repoRoot = sourceNode?.repoRoot || sourceNode?.workspace;
2237
- if (!repoRoot) return { success: false, error: 'Source node repoRoot not found' };
2238
-
2239
- const { execFile } = await import('node:child_process');
2240
- const { promisify } = await import('node:util');
2241
- const execFileAsync = promisify(execFile);
2242
-
2243
- const { stdout: branchStdout } = await execFileAsync('git', ['branch', '--show-current'], { cwd: node.workspace, encoding: 'utf8' });
2244
- const branch = branchStdout.trim();
2245
- if (!branch) return { success: false, error: 'Could not determine branch of the worktree node' };
2246
-
2247
- const { stdout: baseBranchStdout } = await execFileAsync('git', ['branch', '--show-current'], { cwd: repoRoot, encoding: 'utf8' });
2248
- const baseBranch = baseBranchStdout.trim();
2249
-
2250
- const validationSummary = await runMeshRefineValidationGate(mesh, node.workspace);
2251
- if (validationSummary.status === 'failed') {
2252
- return {
2253
- success: false,
2254
- code: 'validation_failed',
2255
- convergenceStatus: 'blocked_review',
2256
- error: 'Refinery validation gate failed; merge/refine was not attempted.',
2257
- branch,
2258
- into: baseBranch,
2259
- validationSummary,
2260
- finalBranchConvergenceState: {
2261
- branch,
2262
- baseBranch,
2263
- merged: false,
2264
- removed: false,
2265
- validation: 'failed',
2266
- status: 'blocked_review',
2267
- },
2268
- };
2269
- }
2270
- if (validationSummary.status === 'skipped') {
2271
- return {
2272
- success: false,
2273
- code: 'validation_unavailable',
2274
- convergenceStatus: 'blocked_review',
2275
- error: 'Refinery validation gate is required but no allowlisted validation command was available; merge/refine was not attempted.',
2276
- branch,
2277
- into: baseBranch,
2278
- validationSummary,
2279
- finalBranchConvergenceState: {
2280
- branch,
2281
- baseBranch,
2282
- merged: false,
2283
- removed: false,
2284
- validation: 'unavailable',
2285
- status: 'blocked_review',
2286
- },
2287
- };
2288
- }
2289
-
2290
- try {
2291
- await execFileAsync('git', ['merge', '--no-ff', branch, '-m', `Auto-merge branch '${branch}' via Refinery`], { cwd: repoRoot, encoding: 'utf8' });
2292
- } catch (e: any) {
2293
- return {
2294
- success: false,
2295
- error: `Merge failed (conflicts?): ${e.message}`,
2296
- validationSummary,
2297
- finalBranchConvergenceState: {
2298
- branch,
2299
- baseBranch,
2300
- merged: false,
2301
- removed: false,
2302
- validation: 'passed',
2303
- status: 'not_mergeable',
2304
- },
2305
- };
2306
- }
2307
-
2308
- const removeResult = await this.execute('remove_mesh_node', {
2309
- meshId,
2310
- nodeId,
2311
- sessionCleanupMode: 'kill',
2312
- inlineMesh: args?.inlineMesh,
2313
- });
2314
-
2315
- try {
2316
- const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
2317
- appendLedgerEntry(meshId, {
2318
- kind: 'node_removed',
2319
- nodeId,
2320
- payload: { refined: true, mergedBranch: branch, into: baseBranch, validationSummary },
2321
- });
2322
- } catch {}
2323
-
2324
- return {
2325
- success: true,
2326
- merged: true,
2327
- branch,
2328
- into: baseBranch,
2329
- removeResult,
2330
- validationSummary,
2331
- finalBranchConvergenceState: {
2332
- branch: baseBranch,
2333
- mergedBranch: branch,
2334
- baseBranch,
2335
- merged: true,
2336
- removed: removeResult?.success !== false,
2337
- validation: 'passed',
2338
- status: removeResult?.success === false ? 'merged_cleanup_failed' : 'merged',
2339
- },
2340
- };
2341
- } catch (e: any) {
2342
- return { success: false, error: e.message };
2343
- }
2344
- }
2345
-
2346
- case 'remove_mesh_node': {
2347
- const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
2348
- const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
2349
- if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
2350
- try {
2351
- const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
2352
- const mesh = meshRecord?.mesh;
2353
- const node = mesh?.nodes?.find((n: any) => n.id === nodeId || n.nodeId === nodeId);
2354
-
2355
- const sessionCleanupMode = this.normalizeMeshSessionCleanupMode(
2356
- args?.sessionCleanupMode ?? args?.session_cleanup_mode ?? mesh?.policy?.sessionCleanupOnNodeRemove,
2357
- );
2358
- let sessionCleanup: Record<string, unknown> | undefined;
2359
- if (node && sessionCleanupMode !== 'preserve') {
2360
- sessionCleanup = await this.cleanupMeshSessions({ meshId, nodeId, node, mode: sessionCleanupMode, source: 'mesh_remove_node' });
2361
- if (sessionCleanup.success === false) return { success: false, removed: false, sessionCleanup };
2362
- }
2363
-
2364
- let worktreeCleanup: Record<string, unknown> | undefined;
2365
- if (node?.isLocalWorktree) {
2366
- const cleanupResult = await this.cleanupLocalWorktreeNode({ mesh, node, nodeId });
2367
- if (cleanupResult.success === false) {
2368
- return {
2369
- success: false,
2370
- removed: false,
2371
- code: cleanupResult.code,
2372
- error: cleanupResult.error,
2373
- recoveryHint: cleanupResult.recoveryHint,
2374
- ...(sessionCleanup ? { sessionCleanup } : {}),
2375
- worktreeCleanup: cleanupResult,
2376
- };
2377
- }
2378
- worktreeCleanup = cleanupResult;
2379
- }
2380
-
2381
- let removed = false;
2382
- if (meshRecord?.inline) {
2383
- removed = this.removeInlineMeshNode(meshId, mesh, nodeId);
2384
- } else {
2385
- const { removeNode } = await import('../config/mesh-config.js');
2386
- removed = removeNode(meshId, nodeId);
2387
- }
2388
-
2389
- // Record in task ledger
2390
- if (removed) {
2391
- try {
2392
- const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
2393
- appendLedgerEntry(meshId, {
2394
- kind: 'node_removed',
2395
- nodeId,
2396
- payload: {
2397
- worktree: !!node?.isLocalWorktree,
2398
- sessionCleanupMode,
2399
- workspace: typeof node?.workspace === 'string' ? node.workspace : undefined,
2400
- daemonId: typeof node?.daemonId === 'string' ? node.daemonId : undefined,
2401
- worktreeBranch: typeof node?.worktreeBranch === 'string' ? node.worktreeBranch : undefined,
2402
- worktreeCleanupFallback: typeof worktreeCleanup?.fallback === 'string' ? worktreeCleanup.fallback : undefined,
2403
- forced: worktreeCleanup?.forced === true ? true : undefined,
2404
- forceFallbackReason: typeof worktreeCleanup?.reason === 'string' ? worktreeCleanup.reason : undefined,
2405
- },
2406
- });
2407
- } catch { /* ledger append is best-effort */ }
2408
- }
2409
-
2410
- return { success: true, removed, ...(sessionCleanup ? { sessionCleanup } : {}), ...(worktreeCleanup ? { worktreeCleanup } : {}) };
2411
- } catch (e: any) {
2412
- return { success: false, error: e.message };
2413
- }
2414
- }
2415
-
2416
- case 'clone_mesh_node': {
2417
- const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
2418
- const sourceNodeId = typeof args?.sourceNodeId === 'string' ? args.sourceNodeId.trim() : '';
2419
- const branch = typeof args?.branch === 'string' ? args.branch.trim() : '';
2420
- const baseBranch = typeof args?.baseBranch === 'string' ? args.baseBranch.trim() : undefined;
2421
- if (!meshId) return { success: false, error: 'meshId required' };
2422
- if (!sourceNodeId) return { success: false, error: 'sourceNodeId required' };
2423
- if (!branch) return { success: false, error: 'branch required' };
2424
-
2425
- try {
2426
- const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
2427
- const mesh = meshRecord?.mesh;
2428
- if (!mesh) return { success: false, error: 'Mesh not found' };
2429
-
2430
- const sourceNode = mesh.nodes?.find((n: any) => n.id === sourceNodeId || n.nodeId === sourceNodeId);
2431
- if (!sourceNode) return { success: false, error: `Source node '${sourceNodeId}' not found in mesh` };
2432
-
2433
- const repoRoot = sourceNode.repoRoot || sourceNode.workspace;
2434
- const { createWorktree } = await import('../git/git-worktree.js');
2435
- const result = await createWorktree({
2436
- repoRoot,
2437
- branch,
2438
- baseBranch,
2439
- meshName: mesh.name,
2440
- });
2441
-
2442
- let node: any;
2443
- if (meshRecord.inline) {
2444
- const { randomUUID } = await import('crypto');
2445
- node = {
2446
- id: `node_${randomUUID().replace(/-/g, '')}`,
2447
- workspace: result.worktreePath,
2448
- repoRoot: result.worktreePath,
2449
- daemonId: sourceNode.daemonId,
2450
- machineId: sourceNode.machineId ?? (sourceNode as any).machine_id,
2451
- userOverrides: { ...(sourceNode.userOverrides || {}) },
2452
- policy: { ...(sourceNode.policy || {}) },
2453
- isLocalWorktree: true,
2454
- worktreeBranch: result.branch,
2455
- clonedFromNodeId: sourceNodeId,
2456
- };
2457
- this.updateInlineMeshNode(meshId, mesh, node);
2458
- } else {
2459
- const { addNode } = await import('../config/mesh-config.js');
2460
- node = addNode(meshId, {
2461
- workspace: result.worktreePath,
2462
- repoRoot: result.worktreePath,
2463
- daemonId: sourceNode.daemonId,
2464
- machineId: sourceNode.machineId ?? (sourceNode as any).machine_id,
2465
- userOverrides: { ...(sourceNode.userOverrides || {}) },
2466
- isLocalWorktree: true,
2467
- worktreeBranch: result.branch,
2468
- clonedFromNodeId: sourceNodeId,
2469
- policy: { ...(sourceNode.policy || {}) },
2470
- });
2471
- if (!node) return { success: false, error: 'Failed to register worktree node' };
2472
- }
825
+ async resumePendingRefineJobsOnStartup(): Promise<void> {
826
+ return resumePendingRefineJobsOnStartup(this);
827
+ }
2473
828
 
2474
- // Initialize submodules if policy allows (default: true)
2475
- const initSubmodules = (sourceNode.policy as any)?.initSubmodulesOnClone !== false;
2476
- if (initSubmodules) {
2477
- try {
2478
- const { runGit } = await import('../git/git-executor.js');
2479
- await runGit(
2480
- { workspace: result.worktreePath, repoRoot: result.worktreePath, isGitRepo: true },
2481
- ['submodule', 'update', '--init', '--recursive'],
2482
- { timeoutMs: 120000 },
2483
- );
2484
- } catch (subErr: any) {
2485
- // Submodule init is best-effort; don't fail the clone
2486
- console.warn('[mesh] Submodule init failed for worktree:', subErr.message);
2487
- }
2488
- }
829
+ private async batchRefineMeshNodes(meshId: string, requestedNodeIds: string[] | undefined, args: any): Promise<CommandRouterResult> {
830
+ return batchRefineMeshNodes(this, meshId, requestedNodeIds, args);
831
+ }
2489
832
 
2490
- // Record in task ledger
2491
- try {
2492
- const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
2493
- appendLedgerEntry(meshId, {
2494
- kind: 'node_cloned',
2495
- nodeId: node.id,
2496
- payload: { sourceNodeId, branch: result.branch, worktreePath: result.worktreePath, submodulesInitialized: initSubmodules },
2497
- });
2498
- } catch { /* ledger append is best-effort */ }
2499
-
2500
- return {
2501
- success: true,
2502
- node,
2503
- worktreePath: result.worktreePath,
2504
- branch: result.branch,
2505
- };
2506
- } catch (e: any) {
2507
- return { success: false, error: e.message };
2508
- }
2509
- }
2510
- case 'trigger_mesh_queue': {
2511
- const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
2512
- if (!meshId) return { success: false, error: 'meshId required' };
2513
- try {
2514
- const { triggerMeshQueue } = await import('../mesh/mesh-events.js');
2515
- if (meshId) {
2516
- triggerMeshQueue(this.deps as any, meshId);
2517
- }
2518
- return { success: true };
2519
- } catch (e: any) {
2520
- return { success: false, error: e.message };
2521
- }
2522
- }
833
+ private async startMeshRefineBatchJob(meshId: string, requestedNodeIds: string[] | undefined, args: any): Promise<CommandRouterResult> {
834
+ return startMeshRefineBatchJob(this, meshId, requestedNodeIds, args);
835
+ }
2523
836
 
2524
- // ─── Mesh Coordinator Launch ───
2525
- case 'launch_mesh_coordinator': {
2526
- const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
2527
- let cliType = typeof args?.cliType === 'string' ? args.cliType.trim() : '';
2528
- if (!meshId) return { success: false, error: 'meshId required' };
2529
-
2530
- try {
2531
- const { buildCoordinatorSystemPrompt } = await import('../mesh/coordinator-prompt.js');
2532
-
2533
- // Support inline mesh data from cloud (bypasses local meshes.json lookup)
2534
- let mesh: any;
2535
- if (args?.inlineMesh && typeof args.inlineMesh === 'object') {
2536
- mesh = args.inlineMesh;
2537
- // Cache cloud mesh so the MCP server can retrieve it via get_mesh
2538
- this.inlineMeshCache.set(meshId, mesh);
2539
- } else {
2540
- const { getMesh } = await import('../config/mesh-config.js');
2541
- mesh = getMesh(meshId);
2542
- }
2543
- if (!mesh) return { success: false, error: 'Mesh not found' };
2544
- if (!Array.isArray(mesh.nodes) || mesh.nodes.length === 0) return { success: false, error: 'No nodes in mesh' };
2545
-
2546
- const requestedCoordinatorNodeId = typeof args?.coordinatorNodeId === 'string'
2547
- ? args.coordinatorNodeId.trim()
2548
- : '';
2549
- const preferredCoordinatorNodeId = requestedCoordinatorNodeId
2550
- || (typeof mesh.coordinator?.preferredNodeId === 'string' ? mesh.coordinator.preferredNodeId.trim() : '');
2551
- const coordinatorNode = preferredCoordinatorNodeId
2552
- ? mesh.nodes.find((node: any) => node?.id === preferredCoordinatorNodeId || node?.nodeId === preferredCoordinatorNodeId)
2553
- : mesh.nodes[0];
2554
- if (!coordinatorNode) {
2555
- return {
2556
- success: false,
2557
- code: 'mesh_coordinator_node_not_found',
2558
- error: `Coordinator node ${preferredCoordinatorNodeId} was not found in mesh`,
2559
- meshId,
2560
- cliType,
2561
- };
2562
- }
2563
- const workspace = typeof coordinatorNode.workspace === 'string' ? coordinatorNode.workspace.trim() : '';
2564
- if (!workspace) return { success: false, error: 'Coordinator node workspace required', meshId, cliType };
2565
- if (!cliType) {
2566
- const resolved = await resolveProviderTypeFromPriority({
2567
- nodeId: String(coordinatorNode.id || coordinatorNode.nodeId || preferredCoordinatorNodeId || 'coordinator'),
2568
- providerPriority: readProviderPriorityFromPolicy(coordinatorNode.policy),
2569
- providerLoader: this.deps.providerLoader,
2570
- onStatusChange: this.deps.onStatusChange,
2571
- });
2572
- if (!resolved.providerType) {
2573
- return {
2574
- success: false,
2575
- code: 'mesh_coordinator_provider_priority_unusable',
2576
- error: resolved.error || 'No usable provider found from node providerPriority',
2577
- meshId,
2578
- cliType,
2579
- workspace,
2580
- };
2581
- }
2582
- cliType = resolved.providerType;
2583
- }
2584
- const providerMeta = this.deps.providerLoader.resolve?.(cliType) || this.deps.providerLoader.getMeta(cliType);
2585
- const coordinatorSetup = resolveMeshCoordinatorSetup({
2586
- provider: providerMeta,
2587
- cliType,
2588
- meshId,
2589
- workspace,
2590
- });
2591
-
2592
- if (coordinatorSetup.kind === 'unsupported') {
2593
- return {
2594
- success: false,
2595
- code: 'mesh_coordinator_unsupported',
2596
- error: coordinatorSetup.reason,
2597
- meshId,
2598
- cliType,
2599
- workspace,
2600
- };
2601
- }
837
+ private async startMeshRefineJob(meshId: string, nodeId: string, args: any): Promise<CommandRouterResult> {
838
+ return startMeshRefineJob(this, meshId, nodeId, args);
839
+ }
2602
840
 
2603
- if (coordinatorSetup.kind === 'manual') {
2604
- return {
2605
- success: false,
2606
- code: 'mesh_coordinator_manual_mcp_setup_required',
2607
- error: coordinatorSetup.instructions,
2608
- meshId,
2609
- cliType,
2610
- workspace,
2611
- meshCoordinatorSetup: coordinatorSetup,
2612
- };
2613
- }
841
+ // ─── Daemon-level command core ───────────────────
2614
842
 
2615
- // ─── CLI-command MCP registration (Codex, Gemini CLI) ───────────
2616
- if (coordinatorSetup.kind === 'cli_command') {
2617
- // Build coordinator prompt first fail closed on errors.
2618
- let cliCmdSystemPrompt = '';
2619
- try {
2620
- cliCmdSystemPrompt = buildCoordinatorSystemPrompt({ mesh, coordinatorCliType: cliType });
2621
- } catch (error: any) {
2622
- const message = error?.message || String(error);
2623
- LOG.error('MeshCoordinator', `Failed to build coordinator prompt: ${message}`);
2624
- return {
2625
- success: false,
2626
- code: 'mesh_coordinator_prompt_failed',
2627
- error: `Failed to build Repo Mesh coordinator prompt: ${message}`,
2628
- meshId, cliType, workspace,
2629
- };
2630
- }
2631
-
2632
- // Run the provider's MCP registration command.
2633
- try {
2634
- const { execFileSync: execCmdSync } = await import('node:child_process');
2635
- const cmdParts = coordinatorSetup.command.trim().split(/\s+/);
2636
- const [regCmd, ...regArgs] = cmdParts;
2637
- LOG.info('MeshCoordinator', `Running MCP registration: ${coordinatorSetup.command}`);
2638
- execCmdSync(regCmd, regArgs, { stdio: 'pipe', timeout: 15_000 });
2639
- } catch (error: any) {
2640
- // Non-fatal server may already be registered (providers return exit 1 on duplicate).
2641
- LOG.warn('MeshCoordinator', `MCP registration command failed (may be pre-registered): ${error?.message || error}`);
2642
- }
2643
-
2644
- // Inject system prompt using provider-native methods.
2645
- // Codex: -c 'instructions="..."' CLI config override
2646
- // Gemini: write GEMINI.md to workspace (auto-loaded as context)
2647
- const cliCmdArgs: string[] = [];
2648
- const cliCmdEnv: Record<string, string> = {};
2649
- if (cliCmdSystemPrompt) {
2650
- if (cliType === 'codex-cli') {
2651
- // Codex reads `developer_instructions` from config.toml as system instructions.
2652
- // The -c flag overrides a config key for this session only.
2653
- cliCmdArgs.push('-c', `developer_instructions=${JSON.stringify(cliCmdSystemPrompt)}`);
2654
- } else if (cliType === 'gemini-cli') {
2655
- // Gemini CLI auto-loads GEMINI.md from CWD as project context.
2656
- // Write a temporary GEMINI.md to the workspace before launch.
2657
- try {
2658
- const { writeFileSync: wfs, existsSync: efs, readFileSync: rfs } = await import('node:fs');
2659
- const geminiMdPath = `${workspace}/GEMINI.md`;
2660
- const marker = '<!-- adhdev-mesh-coordinator-prompt -->';
2661
- const markerEnd = '<!-- /adhdev-mesh-coordinator-prompt -->';
2662
- const block = `${marker}\n${cliCmdSystemPrompt}\n${markerEnd}`;
2663
- if (efs(geminiMdPath)) {
2664
- const existing = rfs(geminiMdPath, 'utf-8');
2665
- // Replace existing block or append
2666
- const replaced = existing.replace(
2667
- new RegExp(`${marker}[\\s\\S]*?${markerEnd}`, 'g'),
2668
- block,
2669
- );
2670
- wfs(geminiMdPath, replaced.includes(marker) ? replaced : `${existing}\n\n${block}`);
2671
- } else {
2672
- wfs(geminiMdPath, block);
2673
- }
2674
- LOG.info('MeshCoordinator', `Wrote coordinator prompt to ${workspace}/GEMINI.md`);
2675
- } catch (e: any) {
2676
- LOG.warn('MeshCoordinator', `Could not write GEMINI.md: ${e?.message || e}`);
2677
- }
2678
- }
2679
- }
2680
-
2681
- const cliCmdLaunch: any = await this.deps.cliManager.handleCliCommand('launch_cli', {
2682
- cliType,
2683
- dir: workspace,
2684
- cliArgs: cliCmdArgs.length > 0 ? cliCmdArgs : undefined,
2685
- env: Object.keys(cliCmdEnv).length > 0 ? cliCmdEnv : undefined,
2686
- settings: { meshCoordinatorFor: meshId },
843
+ /**
844
+ * Daemon-level command execution (IDE start/stop/restart, CLI, detect, logs).
845
+ * Returns null if not handled at this level → caller delegates to CommandHandler.
846
+ */
847
+ private async executeDaemonCommand(cmd: string, args: any): Promise<CommandRouterResult | null> {
848
+ // [Z] Remote mesh worker session-scoped command forward.
849
+ //
850
+ // Session-scoped commands issued from the dashboard (the controlbar Model/Mode
851
+ // selectors → invoke_provider_script, and modal approval resolve_action, plus the
852
+ // direct set_mode/change_model/set_thought_level mutations) target a session by
853
+ // targetSessionId. agent_command (send_chat / clear_history / stop) is included for the
854
+ // same reason: a command naming a session must reach THAT session, never a different
855
+ // local one. When that session is a mesh worker hosted on a REMOTE daemon, this
856
+ // coordinator never holds its live instance, so the CommandHandler delegation would
857
+ // fail with "Live session not found" — or, for agent_command, findAdapter would have
858
+ // fuzzy-injected the message into the coordinator's own CLI session (TASKECHO). Forward
859
+ // to the owning worker daemon — the same daemon that already executes send_chat for that
860
+ // session — so the command acts on the real worker. _meshDirectDispatch prevents
861
+ // re-forwarding once the call lands on the owning daemon (it then handles the session
862
+ // locally). A locally-hosted worker (or any session this coordinator owns) resolves to
863
+ // undefined below and falls through to normal local handling — no regression.
864
+ if (MESH_FORWARDABLE_SESSION_COMMANDS.has(cmd) && this.deps.dispatchMeshCommand && !args?._meshDirectDispatch) {
865
+ const targetSessionId = readStringValue(args?.targetSessionId, args?.sessionId, args?.instanceId);
866
+ if (targetSessionId) {
867
+ const localInstance = this.deps.instanceManager?.getInstance(targetSessionId);
868
+ const localRegistry = this.deps.sessionRegistry?.get?.(targetSessionId);
869
+ if (!localInstance && !localRegistry) {
870
+ // CANCEL-STOP-RELAY: pass the authoritative owning nodeId (when the caller
871
+ // shipped one in meshContext, e.g. mesh_queue_cancel's assignedNodeId) as the
872
+ // deterministic owner-resolution fallback. The session-id cache scan stays the
873
+ // primary path; the hint only kicks in when that scan misses (worktree-clone
874
+ // worker session not yet in / form-mismatched against the cached snapshot).
875
+ const meshContext = readObjectRecord(args?.meshContext);
876
+ const ownerNodeIdHint = readStringValue(meshContext.nodeId);
877
+ const ownerDaemonId = this.resolveRemoteMeshSessionOwnerDaemonId(targetSessionId, ownerNodeIdHint);
878
+ if (ownerDaemonId) {
879
+ LOG.info('Mesh', `[Mesh] Forwarding session-scoped '${cmd}' for remote worker session ${targetSessionId.split('_')[0]} daemon ${ownerDaemonId.slice(0, 12)}`);
880
+ const forwarded = await this.deps.dispatchMeshCommand(ownerDaemonId, cmd, {
881
+ ...(typeof args === 'object' && args !== null ? args as Record<string, unknown> : {}),
882
+ _meshDirectDispatch: true,
2687
883
  });
2688
-
2689
- if (!cliCmdLaunch?.success) {
2690
- return { success: false, error: cliCmdLaunch?.error || 'Failed to launch CLI session' };
2691
- }
2692
-
2693
- LOG.info('MeshCoordinator', `Launched ${cliType} coordinator (cli_command) for mesh ${meshId}`);
2694
- try {
2695
- const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
2696
- appendLedgerEntry(meshId, {
2697
- kind: 'coordinator_started',
2698
- sessionId: cliCmdLaunch.sessionId || cliCmdLaunch.id,
2699
- providerType: cliType,
2700
- payload: { workspace },
2701
- });
2702
- } catch { /* best-effort */ }
2703
-
2704
- return {
2705
- success: true,
2706
- meshId,
2707
- cliType,
2708
- workspace,
2709
- sessionId: cliCmdLaunch.sessionId || cliCmdLaunch.id,
2710
- mcpRegistered: true,
2711
- };
2712
- }
2713
-
2714
- const configFormat = coordinatorSetup.configFormat as MeshCoordinatorConfigFormat;
2715
- if (configFormat !== 'claude_mcp_json' && configFormat !== 'hermes_config_yaml') {
2716
- return {
2717
- success: false,
2718
- code: 'mesh_coordinator_unsupported',
2719
- error: `Unsupported auto-import MCP config format: ${String(coordinatorSetup.configFormat)}`,
2720
- meshId,
2721
- cliType,
2722
- workspace,
2723
- };
2724
- }
2725
-
2726
- // Build the coordinator prompt before mutating workspace config or launching.
2727
- // Prompt generation failures are configuration/data-shape errors; fail closed so
2728
- // broken mesh state is visible instead of silently launching with weaker rules.
2729
- let systemPrompt = '';
2730
- try {
2731
- systemPrompt = buildCoordinatorSystemPrompt({ mesh, coordinatorCliType: cliType });
2732
- } catch (error: any) {
2733
- const message = error?.message || String(error);
2734
- LOG.error('MeshCoordinator', `Failed to build coordinator prompt: ${message}`);
2735
- return {
2736
- success: false,
2737
- code: 'mesh_coordinator_prompt_failed',
2738
- error: `Failed to build Repo Mesh coordinator prompt: ${message}`,
2739
- meshId,
2740
- cliType,
2741
- workspace,
2742
- };
2743
- }
2744
-
2745
- // 1. Write provider-declared MCP config for CLIs that auto-import it.
2746
- const { existsSync, readFileSync, writeFileSync, copyFileSync, mkdirSync } = await import('fs');
2747
- const { dirname } = await import('path');
2748
- const mcpConfigPath = coordinatorSetup.configPath;
2749
- const hermesManualFallback = cliType === 'hermes-cli' && configFormat === 'hermes_config_yaml'
2750
- ? createHermesManualMeshCoordinatorSetup(meshId, workspace)
2751
- : null;
2752
- let hermesBaseConfig: { config: Record<string, any>; sourceHome: string; sourceConfigPath: string } | null = null;
2753
- if (hermesManualFallback) {
2754
- try {
2755
- hermesBaseConfig = loadHermesCoordinatorBaseConfig(mcpConfigPath);
2756
- } catch (error: any) {
2757
- const message = `Failed to parse Hermes base config for automatic coordinator setup: ${error?.message || error}`;
2758
- LOG.error('MeshCoordinator', message);
2759
- return { success: false, code: 'mesh_coordinator_config_parse_failed', error: message, meshId, cliType, workspace };
2760
- }
2761
- }
2762
- const returnManualFallback = (message: string) => ({
2763
- success: false,
2764
- code: 'mesh_coordinator_manual_mcp_setup_required',
2765
- error: message,
2766
- meshId,
2767
- cliType,
2768
- workspace,
2769
- meshCoordinatorSetup: hermesManualFallback,
2770
- });
2771
-
2772
- // Merge ADHDev mesh server into existing config.
2773
- // Pass full mesh data as env var so the MCP server can bootstrap
2774
- // without depending on meshes.json or a running daemon.
2775
- const mcpServerEntry: Record<string, any> = {
2776
- command: coordinatorSetup.mcpServer.command,
2777
- args: coordinatorSetup.mcpServer.args,
2778
- };
2779
- if (args?.inlineMesh) {
2780
- const modeArgIndex = coordinatorSetup.mcpServer.args.findIndex((value: string) => value === '--mode');
2781
- const mcpTransport = modeArgIndex >= 0 ? coordinatorSetup.mcpServer.args[modeArgIndex + 1] : 'ipc';
2782
- mcpServerEntry.env = {
2783
- ADHDEV_INLINE_MESH: JSON.stringify(mesh),
2784
- ADHDEV_MCP_TRANSPORT: mcpTransport === 'local' ? 'local' : 'ipc',
2785
- };
2786
- }
2787
-
2788
- try {
2789
- mkdirSync(dirname(mcpConfigPath), { recursive: true });
2790
- } catch (error: any) {
2791
- const message = `Could not prepare MCP config path for automatic setup: ${error?.message || error}`;
2792
- LOG.error('MeshCoordinator', message);
2793
- if (hermesManualFallback) return returnManualFallback(message);
2794
- return { success: false, code: 'mesh_coordinator_config_write_failed', error: message, meshId, cliType, workspace };
2795
- }
2796
-
2797
- // Backup existing MCP config if present.
2798
- const hadExistingMcpConfig = existsSync(mcpConfigPath);
2799
- let existingMcpConfig: Record<string, any> = hermesBaseConfig?.config || {};
2800
- if (hermesBaseConfig) {
2801
- copyHermesCoordinatorCredentialFiles(hermesBaseConfig.sourceHome, dirname(mcpConfigPath));
2802
- }
2803
- if (hadExistingMcpConfig) {
2804
- try {
2805
- const parsedExistingMcpConfig = parseMeshCoordinatorMcpConfig(readFileSync(mcpConfigPath, 'utf-8'), configFormat);
2806
- const existingCoordinatorConfig = hermesManualFallback
2807
- ? stripHermesCoordinatorTempModelProviderOverrides(parsedExistingMcpConfig)
2808
- : parsedExistingMcpConfig;
2809
- existingMcpConfig = { ...existingMcpConfig, ...existingCoordinatorConfig };
2810
- copyFileSync(mcpConfigPath, mcpConfigPath + '.backup');
2811
- } catch (error: any) {
2812
- LOG.error('MeshCoordinator', `Failed to parse existing MCP config ${mcpConfigPath}: ${error?.message || error}`);
2813
- return {
2814
- success: false,
2815
- code: 'mesh_coordinator_config_parse_failed',
2816
- error: `Failed to parse existing MCP config at ${mcpConfigPath}`,
2817
- };
2818
- }
2819
- }
2820
-
2821
- const mcpServersKey = getMcpServersKey(configFormat);
2822
- const existingServers = existingMcpConfig[mcpServersKey];
2823
- const mcpConfig = {
2824
- ...existingMcpConfig,
2825
- [mcpServersKey]: {
2826
- ...(existingServers && typeof existingServers === 'object' && !Array.isArray(existingServers) ? existingServers : {}),
2827
- [coordinatorSetup.serverName]: mcpServerEntry,
2828
- },
2829
- };
2830
- try {
2831
- writeFileSync(mcpConfigPath, serializeMeshCoordinatorMcpConfig(mcpConfig, configFormat), 'utf-8');
2832
- } catch (error: any) {
2833
- const message = `Could not write MCP config for automatic setup: ${error?.message || error}`;
2834
- LOG.error('MeshCoordinator', message);
2835
- if (hermesManualFallback) return returnManualFallback(message);
2836
- return { success: false, code: 'mesh_coordinator_config_write_failed', error: message, meshId, cliType, workspace };
884
+ return (forwarded ?? { success: false, error: 'no response from remote worker daemon' }) as CommandRouterResult;
2837
885
  }
2838
- LOG.info('MeshCoordinator', `Wrote ${mcpConfigPath} with ${coordinatorSetup.serverName} server`);
2839
-
2840
- const cliArgs: string[] = [];
2841
- const launchEnv: Record<string, string> = {};
2842
- if (configFormat === 'hermes_config_yaml') {
2843
- launchEnv.HERMES_HOME = dirname(mcpConfigPath);
2844
- launchEnv.HERMES_IGNORE_USER_CONFIG = '';
2845
- }
2846
- if (systemPrompt) {
2847
- if (configFormat === 'hermes_config_yaml') {
2848
- launchEnv.HERMES_EPHEMERAL_SYSTEM_PROMPT = systemPrompt;
2849
- } else {
2850
- cliArgs.push('--append-system-prompt', systemPrompt);
2851
- }
2852
- }
2853
- if (cliType === 'claude-cli') {
2854
- cliArgs.push('--mcp-config', coordinatorSetup.configPath);
2855
- }
2856
-
2857
- // 3. Launch CLI session via existing cliManager.
2858
- // Provider-specific prompt injection remains fail-closed: Claude gets
2859
- // explicit CLI args, while Hermes reads HERMES_EPHEMERAL_SYSTEM_PROMPT.
2860
- const launchResult: any = await this.deps.cliManager.handleCliCommand('launch_cli', {
2861
- cliType,
2862
- dir: workspace,
2863
- cliArgs: cliArgs.length > 0 ? cliArgs : undefined,
2864
- env: Object.keys(launchEnv).length > 0 ? launchEnv : undefined,
2865
- settings: {
2866
- meshCoordinatorFor: meshId
2867
- }
2868
- });
2869
-
2870
- if (!launchResult?.success) {
2871
- return { success: false, error: launchResult?.error || 'Failed to launch CLI session' };
2872
- }
2873
-
2874
- LOG.info('MeshCoordinator', `Launched ${cliType} coordinator for mesh ${meshId} in ${workspace}`);
2875
-
2876
- // Record coordinator launch in task ledger
2877
- try {
2878
- const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
2879
- appendLedgerEntry(meshId, {
2880
- kind: 'coordinator_started',
2881
- sessionId: launchResult.sessionId || launchResult.id,
2882
- providerType: cliType,
2883
- payload: { workspace },
2884
- });
2885
- } catch { /* ledger append is best-effort */ }
2886
-
2887
- return {
2888
- success: true,
2889
- meshId,
2890
- cliType,
2891
- workspace,
2892
- sessionId: launchResult.sessionId || launchResult.id,
2893
- mcpConfigWritten: true,
2894
- };
2895
- } catch (e: any) {
2896
- LOG.error('MeshCoordinator', `Failed: ${e.message}`);
2897
- return { success: false, error: e.message };
2898
886
  }
2899
887
  }
888
+ }
2900
889
 
2901
- case 'mesh_status': {
2902
- const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
2903
- if (!meshId) return { success: false, error: 'meshId required' };
2904
- try {
2905
- const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
2906
- const mesh = meshRecord?.mesh;
2907
- if (!mesh) return { success: false, error: 'Mesh not found' };
2908
-
2909
- const { getMeshQueueStats, getQueue } = await import('../mesh/mesh-work-queue.js');
2910
- const queue = getQueue(meshId);
2911
- const queueSummary = getMeshQueueStats(meshId);
2912
-
2913
- const { readLedgerEntries, getLedgerSummary } = await import('../mesh/mesh-ledger.js');
2914
- const ledgerEntries = readLedgerEntries(meshId, { tail: 20 });
2915
- const ledgerSummary = getLedgerSummary(meshId);
2916
-
2917
- const nodeStatuses = [];
2918
- for (const node of mesh.nodes || []) {
2919
- const status: Record<string, unknown> = {
2920
- nodeId: node.id || node.nodeId,
2921
- machineLabel: node.machineLabel || node.id || node.nodeId,
2922
- workspace: node.workspace,
2923
- repoRoot: node.repoRoot,
2924
- isLocalWorktree: node.isLocalWorktree,
2925
- worktreeBranch: node.worktreeBranch,
2926
- daemonId: node.daemonId,
2927
- machineId: node.machineId,
2928
- health: 'unknown',
2929
- providers: node.providers || [],
2930
- activeSessions: [],
2931
- };
2932
- if (node.workspace && typeof node.workspace === 'string') {
2933
- if (!fs.existsSync(node.workspace as string) && applyCachedInlineMeshNodeStatus(status, node)) {
2934
- nodeStatuses.push(status);
2935
- continue;
2936
- }
2937
- try {
2938
- const gitStatus = await getGitRepoStatus(node.workspace as string, { timeoutMs: 10_000 });
2939
- status.git = gitStatus;
2940
- if (gitStatus.isGitRepo) {
2941
- const dirty = (gitStatus.staged + gitStatus.modified + gitStatus.untracked + gitStatus.deleted + gitStatus.renamed) > 0;
2942
- status.health = gitStatus.branch ? (dirty ? 'dirty' : 'online') : 'degraded';
2943
- } else {
2944
- status.health = 'degraded';
2945
- if (gitStatus.error && !status.error) status.error = gitStatus.error;
2946
- }
2947
- } catch {
2948
- if (!applyCachedInlineMeshNodeStatus(status, node)) {
2949
- status.health = 'degraded';
2950
- }
2951
- }
2952
- } else {
2953
- applyCachedInlineMeshNodeStatus(status, node);
2954
- }
2955
- nodeStatuses.push(status);
2956
- }
890
+ // RF-ROUTER LOW family: low-coupling commands (session-host control, spec
891
+ // provider-dev, refine/change-impact config) are handled by the registry
892
+ // before the switch. A hit returns the same CommandRouterResult the inlined
893
+ // case used to; a miss falls through to the switch unchanged.
894
+ const lowFamilyHandler = lowFamilyRegistry.get(cmd);
895
+ if (lowFamilyHandler) {
896
+ return await lowFamilyHandler({
897
+ deps: this.deps,
898
+ getMeshForCommand: this.getMeshForCommand.bind(this),
899
+ }, args);
900
+ }
2957
901
 
2958
- return {
2959
- success: true,
2960
- meshId: mesh.id,
2961
- meshName: mesh.name,
2962
- repoIdentity: mesh.repoIdentity,
2963
- defaultBranch: mesh.defaultBranch,
2964
- nodes: nodeStatuses,
2965
- queue: { tasks: queue, summary: queueSummary },
2966
- ledger: { entries: ledgerEntries, summary: ledgerSummary },
2967
- };
2968
- } catch (e: any) {
2969
- return { success: false, error: e.message };
2970
- }
2971
- }
902
+ // RF-ROUTER MED family: medium-coupling commands (CLI/ACP agent, IDE
903
+ // lifecycle, mesh CRUD, mesh queue, mesh host pairing, fast-forward /
904
+ // refine convergence) are handled by the registry after the LOW family and
905
+ // before the switch. Unlike LOW handlers, MED handlers need router-private
906
+ // collaborators, so the context carries bound methods + the inline-mesh /
907
+ // git-probe caches + the launchIde helper (which breaks the original
908
+ // launch_ide ↔ restart_* self-recursion). A hit returns the same
909
+ // CommandRouterResult the inlined case used to; a miss falls through.
910
+ const medFamilyHandler = medFamilyRegistry.get(cmd);
911
+ if (medFamilyHandler) {
912
+ return await medFamilyHandler(this.buildMedFamilyContext(), args);
913
+ }
2972
914
 
2973
- default:
2974
- break;
915
+ // RF-ROUTER HIGH family: high-coupling commands (mesh coordinator-event
916
+ // relay + interactive prompt, mesh coordinator launch, mesh aggregate
917
+ // status + review inbox) are handled by the registry after the LOW and
918
+ // MED families and before the (now empty) switch. HIGH handlers reach the
919
+ // most router-owned state — the aggregate-status memory cache and the
920
+ // running-refine-job table — so the context carries those plus bound
921
+ // read/write helpers and the router's own `execute` (the
922
+ // get_mesh_review_inbox mesh_status re-entry). A hit returns the same
923
+ // CommandRouterResult the inlined case used to; a miss falls through to
924
+ // CommandHandler delegation.
925
+ const highFamilyHandler = highFamilyRegistry.get(cmd);
926
+ if (highFamilyHandler) {
927
+ return await highFamilyHandler(this.buildHighFamilyContext(), args);
2975
928
  }
2976
929
 
2977
930
  return null; // Not handled at this level → delegate to CommandHandler
@@ -3022,7 +975,7 @@ export class DaemonCommandRouter {
3022
975
 
3023
976
  // 3. Kill OS process if requested
3024
977
  if (killProcess) {
3025
- const running = isIdeRunning(ideType);
978
+ const running = await isIdeRunning(ideType);
3026
979
  if (running) {
3027
980
  LOG.info('StopIDE', `Killing IDE process: ${ideType}`);
3028
981
  const killed = await killIdeProcess(ideType);