@adhdev/daemon-core 0.9.82-rc.32 → 0.9.82-rc.321

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 (306) hide show
  1. package/dist/boot/daemon-lifecycle.d.ts +8 -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 +46 -2
  8. package/dist/cli-adapters/cli-script-runner.d.ts +117 -0
  9. package/dist/cli-adapters/cli-state-engine.d.ts +198 -0
  10. package/dist/cli-adapters/provider-cli-adapter.d.ts +115 -64
  11. package/dist/cli-adapters/provider-cli-parse.d.ts +4 -0
  12. package/dist/cli-adapters/provider-cli-runtime.d.ts +34 -0
  13. package/dist/cli-adapters/provider-cli-shared.d.ts +38 -0
  14. package/dist/cli-adapters/raw-terminal-io.d.ts +37 -0
  15. package/dist/cli-adapters/resolve-executable.d.ts +14 -0
  16. package/dist/cli-adapters/terminal-backends/ghostty-vt-backend.d.ts +6 -3
  17. package/dist/cli-adapters/terminal-backends/types.d.ts +10 -2
  18. package/dist/cli-adapters/terminal-screen.d.ts +4 -11
  19. package/dist/commands/cli-manager.d.ts +2 -1
  20. package/dist/commands/handler.d.ts +110 -0
  21. package/dist/commands/mesh-coordinator.d.ts +85 -1
  22. package/dist/commands/router.d.ts +229 -1
  23. package/dist/commands/upgrade-helper.d.ts +12 -0
  24. package/dist/config/chat-history.d.ts +9 -0
  25. package/dist/config/config.d.ts +5 -0
  26. package/dist/config/mesh-config.d.ts +72 -1
  27. package/dist/detection/ide-detector.d.ts +13 -0
  28. package/dist/detection/win32-ide-version.d.ts +37 -0
  29. package/dist/git/git-commands.d.ts +9 -1
  30. package/dist/git/git-diff.d.ts +6 -0
  31. package/dist/git/git-executor.d.ts +11 -0
  32. package/dist/git/git-status.d.ts +9 -0
  33. package/dist/git/git-types.d.ts +2 -50
  34. package/dist/index.d.ts +51 -12
  35. package/dist/index.js +35378 -11873
  36. package/dist/index.js.map +1 -1
  37. package/dist/index.mjs +38854 -15461
  38. package/dist/index.mjs.map +1 -1
  39. package/dist/installer.d.ts +1 -4
  40. package/dist/ipc/local-ipc-server.d.ts +91 -0
  41. package/dist/launch.d.ts +1 -1
  42. package/dist/logging/async-batch-writer.d.ts +10 -0
  43. package/dist/logging/log-redactor.d.ts +24 -0
  44. package/dist/logging/log-tail-reader.d.ts +46 -0
  45. package/dist/mesh/contracts.d.ts +164 -0
  46. package/dist/mesh/coordinator-prompt.d.ts +36 -0
  47. package/dist/mesh/coordinator-registry.d.ts +59 -0
  48. package/dist/mesh/mesh-active-work.d.ts +108 -0
  49. package/dist/mesh/mesh-delivery-policy.d.ts +131 -0
  50. package/dist/mesh/mesh-events-coordinator.d.ts +169 -0
  51. package/dist/mesh/mesh-events-pending.d.ts +45 -0
  52. package/dist/mesh/mesh-events-stale.d.ts +40 -0
  53. package/dist/mesh/mesh-events-utils.d.ts +47 -0
  54. package/dist/mesh/mesh-events.d.ts +6 -49
  55. package/dist/mesh/mesh-fast-forward.d.ts +81 -0
  56. package/dist/mesh/mesh-host-ownership.d.ts +9 -0
  57. package/dist/mesh/mesh-init.d.ts +86 -0
  58. package/dist/mesh/mesh-ledger-reconciliation.d.ts +23 -3
  59. package/dist/mesh/mesh-ledger.d.ts +77 -1
  60. package/dist/mesh/mesh-missions.d.ts +116 -0
  61. package/dist/mesh/mesh-reconcile-loop.d.ts +7 -0
  62. package/dist/mesh/mesh-refine-batch.d.ts +68 -0
  63. package/dist/mesh/mesh-refine-status.d.ts +62 -0
  64. package/dist/mesh/mesh-review-inbox.d.ts +90 -0
  65. package/dist/mesh/mesh-routing.d.ts +70 -0
  66. package/dist/mesh/mesh-runtime-store.d.ts +401 -0
  67. package/dist/mesh/mesh-task-stats.d.ts +49 -0
  68. package/dist/mesh/mesh-unresolved-forward-outbox.d.ts +30 -0
  69. package/dist/mesh/mesh-work-queue.d.ts +204 -5
  70. package/dist/mesh/preview-freshness.d.ts +18 -0
  71. package/dist/mesh/refine-config.d.ts +215 -0
  72. package/dist/mesh/worktree-bootstrap-config.d.ts +135 -0
  73. package/dist/providers/acp-provider-instance.d.ts +2 -0
  74. package/dist/providers/approval-utils.d.ts +13 -0
  75. package/dist/providers/cli-provider-instance.d.ts +61 -3
  76. package/dist/providers/contracts.d.ts +130 -6
  77. package/dist/providers/external-sources.d.ts +71 -0
  78. package/dist/providers/native-history/antigravity-cli-transcript.d.ts +100 -0
  79. package/dist/providers/native-history/claude-cli-transcript.d.ts +70 -0
  80. package/dist/providers/native-history/codex-cli-transcript.d.ts +73 -0
  81. package/dist/providers/native-history/dispatcher.d.ts +27 -0
  82. package/dist/providers/native-history/hermes-cli-transcript.d.ts +30 -0
  83. package/dist/providers/native-history/index.d.ts +13 -0
  84. package/dist/providers/provider-instance-manager.d.ts +13 -0
  85. package/dist/providers/provider-instance.d.ts +13 -1
  86. package/dist/providers/provider-loader.d.ts +26 -4
  87. package/dist/providers/provider-trust.d.ts +31 -0
  88. package/dist/providers/read-chat-contract.d.ts +29 -0
  89. package/dist/providers/sdk/v1/builders/acp/detect-status.d.ts +68 -0
  90. package/dist/providers/sdk/v1/builders/cli/detect-status.d.ts +85 -0
  91. package/dist/providers/sdk/v1/builders/cli/parse-approval-squash.d.ts +59 -0
  92. package/dist/providers/sdk/v1/builders/cli/parse-approval.d.ts +65 -0
  93. package/dist/providers/sdk/v1/builders/cli/parse-session.d.ts +91 -0
  94. package/dist/providers/sdk/v1/builders/cli/visible-region.d.ts +42 -0
  95. package/dist/providers/sdk/v1/fixture-tooling/format.d.ts +126 -0
  96. package/dist/providers/sdk/v1/fixture-tooling/index.d.ts +8 -0
  97. package/dist/providers/sdk/v1/fixture-tooling/replay.d.ts +38 -0
  98. package/dist/providers/sdk/v1/index.d.ts +30 -0
  99. package/dist/providers/sdk/v1/sandbox/README-design.d.ts +193 -0
  100. package/dist/providers/sdk/v1/sandbox/require-whitelist.d.ts +74 -0
  101. package/dist/providers/sdk/v1/sandbox/script-runner.d.ts +98 -0
  102. package/dist/providers/sdk/v1/types/cli/index.d.ts +277 -0
  103. package/dist/providers/sdk/v1/types/common/index.d.ts +203 -0
  104. package/dist/providers/sdk/v1/validators/index.d.ts +5 -0
  105. package/dist/providers/sdk/v1/validators/manifest.d.ts +40 -0
  106. package/dist/providers/sdk/v1/validators/taint.d.ts +52 -0
  107. package/dist/providers/spec/adapter.d.ts +69 -0
  108. package/dist/providers/spec/cli-adapter.d.ts +217 -0
  109. package/dist/providers/spec/evaluator.d.ts +23 -0
  110. package/dist/providers/spec/fsm-driver.d.ts +278 -0
  111. package/dist/providers/spec/fsm-evaluator.d.ts +72 -0
  112. package/dist/providers/spec/fsm-loader.d.ts +14 -0
  113. package/dist/providers/spec/fsm-types.d.ts +128 -0
  114. package/dist/providers/spec/native-history-executor.d.ts +41 -0
  115. package/dist/providers/spec/pre-launch-trust.d.ts +16 -0
  116. package/dist/providers/spec/route.d.ts +4 -0
  117. package/dist/providers/spec/types.d.ts +180 -0
  118. package/dist/providers/transcript-v2.d.ts +176 -0
  119. package/dist/providers/types/interactive-prompt.d.ts +84 -0
  120. package/dist/providers/working-dir.d.ts +17 -0
  121. package/dist/repo-mesh-types.d.ts +277 -13
  122. package/dist/sessions/registry.d.ts +6 -0
  123. package/dist/shared-types-extra.d.ts +2 -4
  124. package/dist/shared-types.d.ts +42 -1
  125. package/dist/status/normalize.d.ts +1 -1
  126. package/dist/status/normalize.js +1 -0
  127. package/dist/status/normalize.js.map +1 -1
  128. package/dist/status/normalize.mjs +1 -0
  129. package/dist/status/normalize.mjs.map +1 -1
  130. package/dist/status/reporter.d.ts +2 -0
  131. package/dist/status/snapshot.d.ts +1 -0
  132. package/dist/types.d.ts +5 -0
  133. package/package.json +7 -3
  134. package/src/agent-stream/poller.ts +2 -3
  135. package/src/boot/daemon-lifecycle.ts +55 -12
  136. package/src/boot/process-hardening.ts +89 -0
  137. package/src/build-info.ts +73 -0
  138. package/src/chat/source-machine.ts +534 -0
  139. package/src/chat/source-resolver.ts +0 -0
  140. package/src/chat/subscription-updates.ts +20 -1
  141. package/src/cli-adapter-types.d.ts +1 -0
  142. package/src/cli-adapter-types.ts +47 -2
  143. package/src/cli-adapters/cli-script-runner.ts +421 -0
  144. package/src/cli-adapters/cli-state-engine.ts +1215 -0
  145. package/src/cli-adapters/provider-cli-adapter.d.ts +1 -1
  146. package/src/cli-adapters/provider-cli-adapter.ts +710 -1140
  147. package/src/cli-adapters/provider-cli-parse.d.ts +1 -0
  148. package/src/cli-adapters/provider-cli-parse.ts +13 -0
  149. package/src/cli-adapters/provider-cli-runtime.ts +56 -7
  150. package/src/cli-adapters/provider-cli-shared.d.ts +2 -0
  151. package/src/cli-adapters/provider-cli-shared.ts +85 -11
  152. package/src/cli-adapters/pty-transport.ts +2 -1
  153. package/src/cli-adapters/raw-terminal-io.ts +252 -0
  154. package/src/cli-adapters/resolve-executable.ts +90 -0
  155. package/src/cli-adapters/session-host-transport.ts +2 -1
  156. package/src/cli-adapters/terminal-backends/ghostty-vt-backend.ts +53 -29
  157. package/src/cli-adapters/terminal-backends/types.ts +10 -3
  158. package/src/cli-adapters/terminal-screen.ts +16 -81
  159. package/src/commands/chat-commands.ts +1822 -61
  160. package/src/commands/cli-manager.ts +286 -14
  161. package/src/commands/handler.ts +809 -2
  162. package/src/commands/mesh-coordinator.ts +332 -122
  163. package/src/commands/router.ts +6369 -704
  164. package/src/commands/stream-commands.ts +8 -0
  165. package/src/commands/upgrade-helper.ts +81 -30
  166. package/src/config/chat-history.ts +483 -24
  167. package/src/config/config.ts +12 -0
  168. package/src/config/mesh-config.ts +320 -4
  169. package/src/config/recent-activity.ts +8 -2
  170. package/src/daemon/dev-auto-implement.ts +3 -2
  171. package/src/daemon/dev-cli-debug.ts +10 -1
  172. package/src/detection/cli-detector.ts +28 -9
  173. package/src/detection/ide-detector.ts +55 -16
  174. package/src/detection/win32-ide-version.ts +106 -0
  175. package/src/git/git-commands.ts +42 -12
  176. package/src/git/git-diff.ts +53 -0
  177. package/src/git/git-executor.ts +12 -0
  178. package/src/git/git-status.ts +408 -43
  179. package/src/git/git-types.ts +14 -62
  180. package/src/git/git-worktree.ts +8 -1
  181. package/src/index.ts +145 -11
  182. package/src/installer.d.ts +1 -1
  183. package/src/installer.ts +8 -6
  184. package/src/ipc/local-ipc-server.ts +278 -0
  185. package/src/launch.d.ts +1 -1
  186. package/src/launch.ts +37 -28
  187. package/src/logging/async-batch-writer.ts +55 -0
  188. package/src/logging/log-redactor.ts +100 -0
  189. package/src/logging/log-tail-reader.ts +220 -0
  190. package/src/logging/logger.ts +2 -1
  191. package/src/mesh/contracts.ts +329 -0
  192. package/src/mesh/coordinator-prompt.ts +234 -31
  193. package/src/mesh/coordinator-registry.ts +121 -0
  194. package/src/mesh/mesh-active-work.ts +469 -0
  195. package/src/mesh/mesh-delivery-policy.ts +315 -0
  196. package/src/mesh/mesh-events-coordinator.ts +2102 -0
  197. package/src/mesh/mesh-events-pending.ts +508 -0
  198. package/src/mesh/mesh-events-stale.ts +290 -0
  199. package/src/mesh/mesh-events-utils.ts +254 -0
  200. package/src/mesh/mesh-events.ts +30 -1035
  201. package/src/mesh/mesh-fast-forward.ts +839 -0
  202. package/src/mesh/mesh-host-ownership.ts +73 -0
  203. package/src/mesh/mesh-init.ts +260 -0
  204. package/src/mesh/mesh-ledger-reconciliation.ts +12 -5
  205. package/src/mesh/mesh-ledger.ts +589 -102
  206. package/src/mesh/mesh-missions.ts +248 -0
  207. package/src/mesh/mesh-reconcile-loop.ts +496 -0
  208. package/src/mesh/mesh-refine-batch.ts +197 -0
  209. package/src/mesh/mesh-refine-status.ts +231 -0
  210. package/src/mesh/mesh-review-inbox.ts +307 -0
  211. package/src/mesh/mesh-routing.ts +291 -0
  212. package/src/mesh/mesh-runtime-store.ts +1578 -0
  213. package/src/mesh/mesh-task-stats.ts +154 -0
  214. package/src/mesh/mesh-unresolved-forward-outbox.ts +185 -0
  215. package/src/mesh/mesh-work-queue.ts +663 -142
  216. package/src/mesh/preview-freshness.ts +118 -0
  217. package/src/mesh/refine-config.ts +403 -0
  218. package/src/mesh/worktree-bootstrap-config.ts +326 -0
  219. package/src/providers/acp-provider-instance.ts +19 -4
  220. package/src/providers/approval-utils.d.ts +4 -0
  221. package/src/providers/approval-utils.ts +47 -5
  222. package/src/providers/chat-message-normalization.ts +4 -11
  223. package/src/providers/cli-provider-instance.ts +958 -91
  224. package/src/providers/contracts.d.ts +55 -0
  225. package/src/providers/contracts.ts +141 -6
  226. package/src/providers/extension-provider-instance.ts +5 -1
  227. package/src/providers/external-sources.ts +218 -0
  228. package/src/providers/ide-provider-instance.ts +25 -6
  229. package/src/providers/native-history/antigravity-cli-transcript.ts +643 -0
  230. package/src/providers/native-history/claude-cli-transcript.ts +396 -0
  231. package/src/providers/native-history/codex-cli-transcript.ts +479 -0
  232. package/src/providers/native-history/dispatcher.ts +340 -0
  233. package/src/providers/native-history/hermes-cli-transcript.ts +230 -0
  234. package/src/providers/native-history/index.ts +30 -0
  235. package/src/providers/provider-instance-manager.ts +30 -0
  236. package/src/providers/provider-instance.ts +10 -1
  237. package/src/providers/provider-loader.ts +662 -50
  238. package/src/providers/provider-schema.ts +87 -14
  239. package/src/providers/provider-trust.ts +114 -0
  240. package/src/providers/read-chat-contract.ts +76 -16
  241. package/src/providers/sdk/README.md +49 -0
  242. package/src/providers/sdk/v1/builders/acp/detect-status.ts +144 -0
  243. package/src/providers/sdk/v1/builders/cli/detect-status.ts +262 -0
  244. package/src/providers/sdk/v1/builders/cli/parse-approval-squash.ts +158 -0
  245. package/src/providers/sdk/v1/builders/cli/parse-approval.ts +250 -0
  246. package/src/providers/sdk/v1/builders/cli/parse-session.ts +276 -0
  247. package/src/providers/sdk/v1/builders/cli/visible-region.ts +143 -0
  248. package/src/providers/sdk/v1/fixture-tooling/format.ts +130 -0
  249. package/src/providers/sdk/v1/fixture-tooling/index.ts +22 -0
  250. package/src/providers/sdk/v1/fixture-tooling/replay.ts +352 -0
  251. package/src/providers/sdk/v1/index.ts +152 -0
  252. package/src/providers/sdk/v1/sandbox/README-design.ts +195 -0
  253. package/src/providers/sdk/v1/sandbox/require-whitelist.ts +472 -0
  254. package/src/providers/sdk/v1/sandbox/script-runner.ts +150 -0
  255. package/src/providers/sdk/v1/schemas/cli/provider.schema.json +504 -0
  256. package/src/providers/sdk/v1/schemas/primitives/acp-session-protocol-v1.json +131 -0
  257. package/src/providers/sdk/v1/schemas/primitives/native-history-codex-rollout-v1.json +66 -0
  258. package/src/providers/sdk/v1/schemas/primitives/tui-approval-squash-v1.json +91 -0
  259. package/src/providers/sdk/v1/schemas/primitives/tui-assistant-block-v1.json +91 -0
  260. package/src/providers/sdk/v1/schemas/primitives/tui-cue-ordering-v1.json +47 -0
  261. package/src/providers/sdk/v1/schemas/primitives/tui-dispatch-order-v1.json +32 -0
  262. package/src/providers/sdk/v1/schemas/primitives/tui-footer-chrome-v1.json +42 -0
  263. package/src/providers/sdk/v1/schemas/primitives/tui-index-finder-v1.json +27 -0
  264. package/src/providers/sdk/v1/schemas/primitives/tui-modal-v1.json +125 -0
  265. package/src/providers/sdk/v1/schemas/primitives/tui-prompt-marker-v1.json +45 -0
  266. package/src/providers/sdk/v1/schemas/primitives/tui-session-id-extraction-v1.json +46 -0
  267. package/src/providers/sdk/v1/schemas/primitives/tui-settled-prompt-v1.json +71 -0
  268. package/src/providers/sdk/v1/schemas/primitives/tui-spinner-v1.json +83 -0
  269. package/src/providers/sdk/v1/schemas/primitives/tui-transcript-pty-v1.json +83 -0
  270. package/src/providers/sdk/v1/schemas/primitives/tui-visible-region-v1.json +57 -0
  271. package/src/providers/sdk/v1/schemas/primitives/tui-welcome-screen-v1.json +35 -0
  272. package/src/providers/sdk/v1/types/cli/index.ts +365 -0
  273. package/src/providers/sdk/v1/types/common/index.ts +229 -0
  274. package/src/providers/sdk/v1/validators/index.ts +19 -0
  275. package/src/providers/sdk/v1/validators/manifest.ts +110 -0
  276. package/src/providers/sdk/v1/validators/taint.ts +309 -0
  277. package/src/providers/spec/adapter.ts +168 -0
  278. package/src/providers/spec/cli-adapter.ts +986 -0
  279. package/src/providers/spec/evaluator.ts +345 -0
  280. package/src/providers/spec/fsm-driver.ts +909 -0
  281. package/src/providers/spec/fsm-evaluator.ts +255 -0
  282. package/src/providers/spec/fsm-loader.ts +112 -0
  283. package/src/providers/spec/fsm-types.ts +222 -0
  284. package/src/providers/spec/native-history-executor.ts +1136 -0
  285. package/src/providers/spec/pre-launch-trust.ts +104 -0
  286. package/src/providers/spec/route.ts +51 -0
  287. package/src/providers/spec/types.ts +226 -0
  288. package/src/providers/transcript-v2.ts +567 -0
  289. package/src/providers/types/interactive-prompt.ts +536 -0
  290. package/src/providers/version-archive.ts +64 -24
  291. package/src/providers/working-dir.ts +23 -0
  292. package/src/repo-mesh-types.ts +356 -13
  293. package/src/sessions/registry.ts +6 -0
  294. package/src/shared-types-extra.ts +2 -4
  295. package/src/shared-types.ts +45 -1
  296. package/src/status/builders.ts +26 -6
  297. package/src/status/normalize.ts +2 -0
  298. package/src/status/reporter.ts +15 -0
  299. package/src/status/snapshot.ts +84 -25
  300. package/src/system/host-memory.ts +29 -12
  301. package/src/types.ts +5 -0
  302. package/dist/cli-adapters/terminal-backends/xterm-backend.d.ts +0 -17
  303. package/dist/mesh/mesh-sync.d.ts +0 -53
  304. package/src/cli-adapters/terminal-backends/ghostty-vt-backend.d.ts +0 -16
  305. package/src/cli-adapters/terminal-backends/xterm-backend.ts +0 -97
  306. package/src/mesh/mesh-sync.ts +0 -111
@@ -8,7 +8,7 @@
8
8
 
9
9
  import { existsSync, readFileSync, writeFileSync } from 'fs';
10
10
  import { join } from 'path';
11
- import { randomUUID } from 'crypto';
11
+ import { createHash, randomBytes, randomUUID } from 'crypto';
12
12
  import { getConfigDir } from './config.js';
13
13
  import type {
14
14
  LocalMeshConfig,
@@ -18,8 +18,11 @@ import type {
18
18
  RepoMeshNodePolicy,
19
19
  RepoMeshNodeCapabilities,
20
20
  RepoMeshCoordinatorConfig,
21
+ RepoMeshHostMetadata,
22
+ RepoMeshDaemonRole,
21
23
  } from '../repo-mesh-types.js';
22
- import { DEFAULT_MESH_POLICY } from '../repo-mesh-types.js';
24
+ import { DEFAULT_MESH_POLICY, normalizeMeshSchedulingStrategy } from '../repo-mesh-types.js';
25
+ import { createDefaultMeshHostMetadata } from '../mesh/mesh-host-ownership.js';
23
26
 
24
27
  // ─── Persistence ────────────────────────────────
25
28
 
@@ -39,6 +42,20 @@ function loadMeshConfig(): LocalMeshConfig {
39
42
  }
40
43
  }
41
44
 
45
+ function normalizeCapabilityTags(value: unknown): string[] | undefined {
46
+ if (!Array.isArray(value)) return undefined;
47
+ const seen = new Set<string>();
48
+ const tags = value
49
+ .map(tag => typeof tag === 'string' ? tag.trim() : '')
50
+ .filter(Boolean)
51
+ .filter(tag => {
52
+ if (seen.has(tag)) return false;
53
+ seen.add(tag);
54
+ return true;
55
+ });
56
+ return tags.length ? tags : undefined;
57
+ }
58
+
42
59
  function saveMeshConfig(config: LocalMeshConfig): void {
43
60
  const path = getMeshConfigPath();
44
61
  writeFileSync(path, JSON.stringify(config, null, 2), { encoding: 'utf-8', mode: 0o600 });
@@ -78,21 +95,60 @@ const SESSION_CLEANUP_MODES = new Set(['preserve', 'stop', 'delete_stopped', 'st
78
95
  const SPAWNED_SESSION_VISIBILITY_MODES = new Set(['visible', 'hidden']);
79
96
 
80
97
  function mergeMeshPolicy(base: RepoMeshPolicy | undefined, patch: Partial<RepoMeshPolicy> | undefined): RepoMeshPolicy {
81
- const policy: RepoMeshPolicy = { ...DEFAULT_MESH_POLICY, ...(base || {}), ...(patch || {}) };
98
+ const autoFastForward = normalizeAutoFastForwardPolicy({
99
+ ...DEFAULT_MESH_POLICY.autoFastForward,
100
+ ...((base?.autoFastForward && typeof base.autoFastForward === 'object') ? base.autoFastForward : {}),
101
+ ...((patch?.autoFastForward && typeof patch.autoFastForward === 'object') ? patch.autoFastForward : {}),
102
+ });
103
+ const policy: RepoMeshPolicy = {
104
+ ...DEFAULT_MESH_POLICY,
105
+ ...(base || {}),
106
+ ...(patch || {}),
107
+ autoFastForward,
108
+ };
82
109
  if (!['block', 'warn', 'checkpoint_then_continue'].includes(policy.dirtyWorkspaceBehavior)) {
83
110
  policy.dirtyWorkspaceBehavior = 'warn';
84
111
  }
85
112
  const maxParallelTasks = Number(policy.maxParallelTasks);
86
113
  policy.maxParallelTasks = Number.isFinite(maxParallelTasks) ? Math.max(1, Math.min(8, Math.floor(maxParallelTasks))) : 2;
114
+ policy.allowAutoPublishSubmoduleMainCommits = policy.allowAutoPublishSubmoduleMainCommits === true;
87
115
  if (!SESSION_CLEANUP_MODES.has(String(policy.sessionCleanupOnNodeRemove))) {
88
116
  policy.sessionCleanupOnNodeRemove = 'preserve';
89
117
  }
90
118
  if (!SPAWNED_SESSION_VISIBILITY_MODES.has(String(policy.spawnedSessionVisibility))) {
91
119
  policy.spawnedSessionVisibility = 'visible';
92
120
  }
121
+ // Load-balancing: normalize the scheduling strategy so an invalid/blank value
122
+ // falls back to 'first_eligible' (strict no-change). Only persist the field when
123
+ // it is explicitly a non-default value to keep existing meshes.json untouched.
124
+ const normalizedStrategy = normalizeMeshSchedulingStrategy(policy.schedulingStrategy);
125
+ if (normalizedStrategy === 'first_eligible') {
126
+ delete policy.schedulingStrategy;
127
+ } else {
128
+ policy.schedulingStrategy = normalizedStrategy;
129
+ }
130
+ // Convergence routing: strict opt-in (default false). Only persist when explicitly
131
+ // enabled so existing meshes.json stays byte-for-byte untouched.
132
+ if (policy.autoConvergeCodeChange === true) {
133
+ policy.autoConvergeCodeChange = true;
134
+ } else {
135
+ delete policy.autoConvergeCodeChange;
136
+ }
93
137
  return policy;
94
138
  }
95
139
 
140
+ function normalizeAutoFastForwardPolicy(value: unknown): NonNullable<RepoMeshPolicy['autoFastForward']> {
141
+ const record = value && typeof value === 'object' && !Array.isArray(value)
142
+ ? value as Record<string, unknown>
143
+ : {};
144
+ const maxBehind = Number(record.maxBehind);
145
+ return {
146
+ enabled: record.enabled !== false,
147
+ ...(Number.isFinite(maxBehind) && maxBehind >= 0 ? { maxBehind: Math.floor(maxBehind) } : {}),
148
+ requireCleanSubmodules: record.requireCleanSubmodules !== false,
149
+ };
150
+ }
151
+
96
152
  export function listMeshes(): LocalMeshEntry[] {
97
153
  return loadMeshConfig().meshes;
98
154
  }
@@ -112,6 +168,7 @@ export interface CreateMeshOptions {
112
168
  defaultBranch?: string;
113
169
  policy?: Partial<RepoMeshPolicy>;
114
170
  coordinator?: RepoMeshCoordinatorConfig;
171
+ meshHost?: RepoMeshHostMetadata;
115
172
  }
116
173
 
117
174
  export function createMesh(opts: CreateMeshOptions): LocalMeshEntry {
@@ -133,6 +190,7 @@ export function createMesh(opts: CreateMeshOptions): LocalMeshEntry {
133
190
  defaultBranch: opts.defaultBranch,
134
191
  policy: mergeMeshPolicy(undefined, opts.policy),
135
192
  coordinator: opts.coordinator || {},
193
+ meshHost: opts.meshHost || createDefaultMeshHostMetadata(),
136
194
  nodes: [],
137
195
  createdAt: now,
138
196
  updatedAt: now,
@@ -148,6 +206,7 @@ export interface UpdateMeshOptions {
148
206
  defaultBranch?: string;
149
207
  policy?: Partial<RepoMeshPolicy>;
150
208
  coordinator?: RepoMeshCoordinatorConfig;
209
+ meshHost?: RepoMeshHostMetadata;
151
210
  }
152
211
 
153
212
  export function updateMesh(meshId: string, opts: UpdateMeshOptions): LocalMeshEntry | undefined {
@@ -159,6 +218,7 @@ export function updateMesh(meshId: string, opts: UpdateMeshOptions): LocalMeshEn
159
218
  if (opts.defaultBranch !== undefined) mesh.defaultBranch = opts.defaultBranch;
160
219
  if (opts.policy) mesh.policy = mergeMeshPolicy(mesh.policy, opts.policy);
161
220
  if (opts.coordinator) mesh.coordinator = opts.coordinator;
221
+ if (opts.meshHost) mesh.meshHost = opts.meshHost;
162
222
  mesh.updatedAt = new Date().toISOString();
163
223
 
164
224
  saveMeshConfig(config);
@@ -174,6 +234,240 @@ export function deleteMesh(meshId: string): boolean {
174
234
  return true;
175
235
  }
176
236
 
237
+ function normalizeManualHostAddress(hostAddress: string): string {
238
+ const normalized = hostAddress.trim().replace(/\/+$/, '');
239
+ if (!normalized) throw new Error('hostAddress required');
240
+ let parsed: URL;
241
+ try {
242
+ parsed = new URL(normalized);
243
+ } catch {
244
+ throw new Error('hostAddress must be a valid http(s) or ws(s) URL');
245
+ }
246
+ if (!['http:', 'https:', 'ws:', 'wss:'].includes(parsed.protocol)) {
247
+ throw new Error('hostAddress must use http, https, ws, or wss');
248
+ }
249
+ return normalized;
250
+ }
251
+
252
+ export function tokenIdForManualPairing(token: string): string {
253
+ return `tok_${createHash('sha256').update(token).digest('hex').slice(0, 16)}`;
254
+ }
255
+
256
+ function normalizeTokenExpiry(value: unknown): string | undefined {
257
+ if (typeof value !== 'string' || !value.trim()) return undefined;
258
+ const date = new Date(value);
259
+ if (Number.isNaN(date.getTime())) throw new Error('expiresAt must be a valid ISO date');
260
+ return date.toISOString();
261
+ }
262
+
263
+ function assertPairingTokenValid(pairing: RepoMeshHostMetadata['pairing'], rawToken: string, nowIso: string): { ok: true; tokenId: string } | { ok: false; reason: string; expectedTokenId?: string; presentedTokenId?: string } {
264
+ const token = rawToken.trim();
265
+ if (!token) return { ok: false, reason: 'token required' };
266
+ const presentedTokenId = tokenIdForManualPairing(token);
267
+ const expectedTokenId = pairing?.tokenId;
268
+ if (!expectedTokenId || pairing?.status === 'not_configured' || pairing?.status === 'revoked') {
269
+ return { ok: false, reason: 'host pairing token is not configured', presentedTokenId };
270
+ }
271
+ if (pairing.expiresAt && new Date(pairing.expiresAt).getTime() <= new Date(nowIso).getTime()) {
272
+ return { ok: false, reason: 'host pairing token expired', expectedTokenId, presentedTokenId };
273
+ }
274
+ if (presentedTokenId !== expectedTokenId) {
275
+ return { ok: false, reason: 'invalid pairing token', expectedTokenId, presentedTokenId };
276
+ }
277
+ return { ok: true, tokenId: presentedTokenId };
278
+ }
279
+
280
+ export interface ConfigureMeshHostPairingOptions {
281
+ hostAddress: string;
282
+ token: string;
283
+ now?: string;
284
+ }
285
+
286
+ export function configureMeshHostPairing(
287
+ meshId: string,
288
+ opts: ConfigureMeshHostPairingOptions,
289
+ ): { mesh: LocalMeshEntry; meshHost: RepoMeshHostMetadata; hostAddress: string } | undefined {
290
+ const hostAddress = normalizeManualHostAddress(opts.hostAddress);
291
+ const token = opts.token.trim();
292
+ if (!token) throw new Error('token required');
293
+
294
+ const config = loadMeshConfig();
295
+ const mesh = config.meshes.find(m => m.id === meshId);
296
+ if (!mesh) return undefined;
297
+
298
+ const now = opts.now || new Date().toISOString();
299
+ const previous = mesh.meshHost || createDefaultMeshHostMetadata();
300
+ const meshHost: RepoMeshHostMetadata = {
301
+ ...previous,
302
+ role: 'member',
303
+ hostAddress,
304
+ pairing: {
305
+ status: 'pairing',
306
+ tokenId: tokenIdForManualPairing(token),
307
+ lastPairedAt: now,
308
+ },
309
+ };
310
+
311
+ mesh.meshHost = meshHost;
312
+ mesh.updatedAt = now;
313
+ saveMeshConfig(config);
314
+ return { mesh, meshHost, hostAddress };
315
+ }
316
+
317
+ export interface CreateMeshHostPairingTokenOptions {
318
+ token?: string;
319
+ expiresAt?: string;
320
+ now?: string;
321
+ }
322
+
323
+ export function createMeshHostPairingToken(
324
+ meshId: string,
325
+ opts: CreateMeshHostPairingTokenOptions = {},
326
+ ): { mesh: LocalMeshEntry; meshHost: RepoMeshHostMetadata; token: string; tokenId: string; expiresAt?: string } | undefined {
327
+ const config = loadMeshConfig();
328
+ const mesh = config.meshes.find(m => m.id === meshId);
329
+ if (!mesh) return undefined;
330
+ const now = opts.now || new Date().toISOString();
331
+ const token = (opts.token || `mhj_${randomBytes(24).toString('base64url')}`).trim();
332
+ if (!token) throw new Error('token required');
333
+ const tokenId = tokenIdForManualPairing(token);
334
+ const expiresAt = normalizeTokenExpiry(opts.expiresAt);
335
+ const previous = mesh.meshHost || createDefaultMeshHostMetadata();
336
+ if (previous.role === 'member') {
337
+ throw new Error('Mesh Host daemon required to create host pairing tokens; member daemons cannot mint host join tokens.');
338
+ }
339
+ const meshHost: RepoMeshHostMetadata = {
340
+ ...previous,
341
+ role: 'host',
342
+ pairing: {
343
+ status: 'pairing',
344
+ tokenId,
345
+ lastPairedAt: now,
346
+ ...(expiresAt ? { expiresAt } : {}),
347
+ },
348
+ };
349
+ mesh.meshHost = meshHost;
350
+ mesh.updatedAt = now;
351
+ saveMeshConfig(config);
352
+ return { mesh, meshHost, token, tokenId, ...(expiresAt ? { expiresAt } : {}) };
353
+ }
354
+
355
+ export interface MeshHostJoinMemberNodeInput {
356
+ id?: string;
357
+ workspace: string;
358
+ repoRoot?: string;
359
+ daemonId?: string;
360
+ machineId?: string;
361
+ userOverrides?: Partial<RepoMeshNodeCapabilities>;
362
+ policy?: RepoMeshNodePolicy;
363
+ role?: RepoMeshDaemonRole;
364
+ }
365
+
366
+ export interface ApplyMeshHostJoinOptions {
367
+ token: string;
368
+ memberNode: MeshHostJoinMemberNodeInput;
369
+ memberMeshId?: string;
370
+ now?: string;
371
+ }
372
+
373
+ export function applyMeshHostJoinRequest(
374
+ meshId: string,
375
+ opts: ApplyMeshHostJoinOptions,
376
+ ): { accepted: true; mesh: LocalMeshEntry; meshHost: RepoMeshHostMetadata; node: LocalMeshNodeEntry; tokenId: string } | { accepted: false; mesh?: LocalMeshEntry; meshHost?: RepoMeshHostMetadata; tokenId?: string; reason: string } | undefined {
377
+ const config = loadMeshConfig();
378
+ const mesh = config.meshes.find(m => m.id === meshId);
379
+ if (!mesh) return undefined;
380
+ const now = opts.now || new Date().toISOString();
381
+ const previous = mesh.meshHost || createDefaultMeshHostMetadata();
382
+ if (previous.role === 'member') {
383
+ return { accepted: false, mesh, meshHost: previous, reason: 'Mesh Host daemon required to accept join requests' };
384
+ }
385
+ const meshHost: RepoMeshHostMetadata = { ...previous, role: 'host' };
386
+ const validation = assertPairingTokenValid(meshHost.pairing, opts.token, now);
387
+ if (!validation.ok) {
388
+ mesh.meshHost = {
389
+ ...meshHost,
390
+ pairing: {
391
+ ...(meshHost.pairing || { status: 'not_configured' as const }),
392
+ status: 'rejected',
393
+ lastRejectedAt: now,
394
+ },
395
+ };
396
+ mesh.updatedAt = now;
397
+ saveMeshConfig(config);
398
+ return { accepted: false, mesh, meshHost: mesh.meshHost, tokenId: validation.presentedTokenId, reason: validation.reason };
399
+ }
400
+
401
+ const workspace = opts.memberNode.workspace.trim();
402
+ if (!workspace) throw new Error('memberNode.workspace required');
403
+ const memberId = opts.memberNode.id?.trim();
404
+ let node = mesh.nodes.find(n => (memberId && n.id === memberId) || n.workspace === workspace);
405
+ if (node) {
406
+ node.workspace = workspace;
407
+ node.repoRoot = opts.memberNode.repoRoot;
408
+ node.daemonId = opts.memberNode.daemonId;
409
+ node.machineId = opts.memberNode.machineId;
410
+ node.userOverrides = opts.memberNode.userOverrides || node.userOverrides || {};
411
+ node.policy = { ...(node.policy || {}), ...(opts.memberNode.policy || {}) };
412
+ node.role = 'member';
413
+ } else {
414
+ if (mesh.nodes.length >= 10) throw new Error('Maximum 10 nodes per mesh');
415
+ node = {
416
+ id: memberId || `node_${randomUUID().replace(/-/g, '')}`,
417
+ workspace,
418
+ repoRoot: opts.memberNode.repoRoot,
419
+ daemonId: opts.memberNode.daemonId,
420
+ machineId: opts.memberNode.machineId,
421
+ userOverrides: opts.memberNode.userOverrides || {},
422
+ policy: opts.memberNode.policy || {},
423
+ role: 'member',
424
+ };
425
+ mesh.nodes.push(node);
426
+ }
427
+ mesh.meshHost = {
428
+ ...meshHost,
429
+ pairing: {
430
+ ...(meshHost.pairing || {}),
431
+ status: 'paired',
432
+ tokenId: validation.tokenId,
433
+ joinedAt: now,
434
+ lastPairedAt: meshHost.pairing?.lastPairedAt || now,
435
+ ...(meshHost.pairing?.expiresAt ? { expiresAt: meshHost.pairing.expiresAt } : {}),
436
+ },
437
+ };
438
+ mesh.updatedAt = now;
439
+ saveMeshConfig(config);
440
+ return { accepted: true, mesh, meshHost: mesh.meshHost, node, tokenId: validation.tokenId };
441
+ }
442
+
443
+ export function markMeshHostPairingJoined(
444
+ meshId: string,
445
+ opts: { hostDaemonId?: string; hostNodeId?: string; joinedAt?: string; token?: string; tokenId?: string },
446
+ ): { mesh: LocalMeshEntry; meshHost: RepoMeshHostMetadata } | undefined {
447
+ const config = loadMeshConfig();
448
+ const mesh = config.meshes.find(m => m.id === meshId);
449
+ if (!mesh) return undefined;
450
+ const now = opts.joinedAt || new Date().toISOString();
451
+ const previous = mesh.meshHost || createDefaultMeshHostMetadata();
452
+ const tokenId = opts.tokenId || (opts.token ? tokenIdForManualPairing(opts.token) : previous.pairing?.tokenId);
453
+ mesh.meshHost = {
454
+ ...previous,
455
+ role: 'member',
456
+ ...(opts.hostDaemonId ? { hostDaemonId: opts.hostDaemonId } : {}),
457
+ ...(opts.hostNodeId ? { hostNodeId: opts.hostNodeId } : {}),
458
+ pairing: {
459
+ ...(previous.pairing || {}),
460
+ status: 'paired',
461
+ ...(tokenId ? { tokenId } : {}),
462
+ joinedAt: now,
463
+ lastPairedAt: previous.pairing?.lastPairedAt || now,
464
+ },
465
+ };
466
+ mesh.updatedAt = now;
467
+ saveMeshConfig(config);
468
+ return { mesh, meshHost: mesh.meshHost };
469
+ }
470
+
177
471
  // ─── Node Operations ────────────────────────────
178
472
 
179
473
  export interface AddNodeOptions {
@@ -181,11 +475,14 @@ export interface AddNodeOptions {
181
475
  repoRoot?: string;
182
476
  daemonId?: string;
183
477
  machineId?: string;
478
+ capabilities?: string[];
184
479
  userOverrides?: Partial<RepoMeshNodeCapabilities>;
185
480
  policy?: RepoMeshNodePolicy;
186
481
  isLocalWorktree?: boolean;
187
482
  worktreeBranch?: string;
188
483
  clonedFromNodeId?: string;
484
+ worktreeBootstrap?: LocalMeshNodeEntry['worktreeBootstrap'];
485
+ role?: RepoMeshDaemonRole;
189
486
  }
190
487
 
191
488
  export function addNode(meshId: string, opts: AddNodeOptions): LocalMeshNodeEntry | undefined {
@@ -208,11 +505,14 @@ export function addNode(meshId: string, opts: AddNodeOptions): LocalMeshNodeEntr
208
505
  repoRoot: opts.repoRoot,
209
506
  daemonId: opts.daemonId,
210
507
  machineId: opts.machineId,
508
+ capabilities: normalizeCapabilityTags(opts.capabilities),
211
509
  userOverrides: opts.userOverrides || {},
212
510
  policy: opts.policy || {},
213
511
  isLocalWorktree: opts.isLocalWorktree,
214
512
  worktreeBranch: opts.worktreeBranch,
215
513
  clonedFromNodeId: opts.clonedFromNodeId,
514
+ worktreeBootstrap: opts.worktreeBootstrap,
515
+ role: opts.role,
216
516
  };
217
517
 
218
518
  mesh.nodes.push(node);
@@ -238,7 +538,14 @@ export function removeNode(meshId: string, nodeId: string): boolean {
238
538
  export function updateNode(
239
539
  meshId: string,
240
540
  nodeId: string,
241
- opts: { userOverrides?: Partial<RepoMeshNodeCapabilities>; policy?: RepoMeshNodePolicy },
541
+ opts: {
542
+ userOverrides?: Partial<RepoMeshNodeCapabilities>;
543
+ policy?: RepoMeshNodePolicy;
544
+ worktreeBootstrap?: LocalMeshNodeEntry['worktreeBootstrap'];
545
+ /** Per-node instruction surfaced in the coordinator prompt. Pass an
546
+ * empty string or undefined to clear it. */
547
+ systemPrompt?: string;
548
+ },
242
549
  ): LocalMeshNodeEntry | undefined {
243
550
  const config = loadMeshConfig();
244
551
  const mesh = config.meshes.find(m => m.id === meshId);
@@ -249,6 +556,15 @@ export function updateNode(
249
556
 
250
557
  if (opts.userOverrides) node.userOverrides = { ...node.userOverrides, ...opts.userOverrides };
251
558
  if (opts.policy) node.policy = { ...node.policy, ...opts.policy };
559
+ if (opts.worktreeBootstrap) node.worktreeBootstrap = opts.worktreeBootstrap;
560
+ if (Object.prototype.hasOwnProperty.call(opts, 'systemPrompt')) {
561
+ // Honor explicit clears: { systemPrompt: undefined } drops the field.
562
+ if (opts.systemPrompt && opts.systemPrompt.trim()) {
563
+ node.systemPrompt = opts.systemPrompt;
564
+ } else {
565
+ delete node.systemPrompt;
566
+ }
567
+ }
252
568
  mesh.updatedAt = new Date().toISOString();
253
569
  saveMeshConfig(config);
254
570
  return node;
@@ -92,12 +92,18 @@ export function buildSessionReadStateKey(sessionId: string, providerSessionId?:
92
92
 
93
93
  export function getSessionSeenAt(state: DaemonState, sessionId: string, providerSessionId?: string | null): number {
94
94
  const providerKey = buildSessionReadStateKey(sessionId, providerSessionId);
95
- return state.sessionReads?.[providerKey] || state.sessionReads?.[sessionId] || 0;
95
+ return Math.max(state.sessionReads?.[providerKey] || 0, state.sessionReads?.[sessionId] || 0);
96
96
  }
97
97
 
98
98
  export function getSessionSeenMarker(state: DaemonState, sessionId: string, providerSessionId?: string | null): string {
99
99
  const providerKey = buildSessionReadStateKey(sessionId, providerSessionId);
100
- return state.sessionReadMarkers?.[providerKey] || state.sessionReadMarkers?.[sessionId] || '';
100
+ const providerSeenAt = state.sessionReads?.[providerKey] || 0;
101
+ const sessionSeenAt = state.sessionReads?.[sessionId] || 0;
102
+ const providerMarker = state.sessionReadMarkers?.[providerKey] || '';
103
+ const sessionMarker = state.sessionReadMarkers?.[sessionId] || '';
104
+ if (sessionSeenAt > providerSeenAt && sessionMarker) return sessionMarker;
105
+ if (providerSeenAt > sessionSeenAt && providerMarker) return providerMarker;
106
+ return providerMarker || sessionMarker;
101
107
  }
102
108
 
103
109
  export function getSessionNotificationDismissal(state: DaemonState, sessionId: string, providerSessionId?: string | null): string {
@@ -9,6 +9,7 @@
9
9
  import * as fs from 'fs';
10
10
  import * as path from 'path';
11
11
  import * as os from 'os';
12
+ import { DEFAULT_SESSION_HOST_COLS, DEFAULT_SESSION_HOST_ROWS } from '@adhdev/session-host-core';
12
13
  import type * as http from 'http';
13
14
  import type { ChildProcess } from 'child_process';
14
15
  import type { DevServerContext, ProviderCategory } from './dev-server-types.js';
@@ -482,8 +483,8 @@ export async function handleAutoImplement(ctx: DevServerContext, type: string, r
482
483
  const isWin = os.platform() === 'win32';
483
484
  child = pty.spawn(isWin ? 'cmd.exe' : (process.env.SHELL || '/bin/zsh'), [isWin ? '/c' : '-c', shellCmd], {
484
485
  name: 'xterm-256color',
485
- cols: 120,
486
- rows: 40,
486
+ cols: DEFAULT_SESSION_HOST_COLS,
487
+ rows: DEFAULT_SESSION_HOST_ROWS,
487
488
  cwd: providerDir,
488
489
  env: { ...process.env, ...(spawn.env || {}) },
489
490
  });
@@ -802,7 +802,16 @@ export async function handleCliSend(ctx: DevServerContext, req: http.IncomingMes
802
802
  }
803
803
 
804
804
  try {
805
- ctx.instanceManager!.sendEvent(target.instanceId, 'send_message', { text });
805
+ if (target.category === 'cli') {
806
+ const bundle = getCliTargetBundle(ctx, type, instanceId);
807
+ if (!bundle) {
808
+ ctx.json(res, 404, { error: `No running CLI adapter found for: ${type || instanceId}` });
809
+ return;
810
+ }
811
+ await bundle.adapter.sendMessage(text);
812
+ } else {
813
+ ctx.instanceManager!.sendEvent(target.instanceId, 'send_message', { text });
814
+ }
806
815
  ctx.json(res, 200, { sent: true, type: target.type, instanceId: target.instanceId });
807
816
  } catch (e: any) {
808
817
  ctx.json(res, 500, { error: `Send failed: ${e.message}` });
@@ -12,6 +12,7 @@ import * as os from 'os';
12
12
  import * as path from 'path';
13
13
  import { existsSync } from 'fs';
14
14
  import type { ProviderLoader } from '../providers/provider-loader.js';
15
+ import { findBinary } from '../cli-adapters/provider-cli-shared.js';
15
16
 
16
17
  export interface CLIInfo {
17
18
  id: string;
@@ -57,6 +58,29 @@ function resolveCommandPath(command: string): string | null {
57
58
  return null;
58
59
  }
59
60
 
61
+ /**
62
+ * Resolve a CLI command to an absolute path for install detection.
63
+ * Order: explicit path → PATH (`where`/`which`) → well-known global-bin dirs
64
+ * (e.g. %APPDATA%\npm) via the shared spawn-layer findBinary.
65
+ *
66
+ * The final step keeps detection consistent with the spawn layer: findBinary
67
+ * already searches npm's default Windows prefix at %APPDATA%\npm (where
68
+ * `npm i -g @openai/codex` lands), so a CLI installed under an npm prefix that
69
+ * is NOT on the daemon's inherited PATH is detected as installed instead of
70
+ * being blocked at the launch gate. findBinary returns a bare "<name>.cmd"
71
+ * (non-absolute) when nothing is found, so we only accept an absolute path
72
+ * that actually exists.
73
+ */
74
+ async function resolveDetectionPath(command: string, whichCmd: string): Promise<string | null> {
75
+ const explicitPath = resolveCommandPath(command);
76
+ if (explicitPath) return explicitPath;
77
+ const whichResult = await execAsync(`${whichCmd} ${shellQuote(command)}`);
78
+ if (whichResult) return whichResult.split('\n')[0];
79
+ const resolved = findBinary(command);
80
+ if (path.isAbsolute(resolved) && existsSync(resolved)) return resolved;
81
+ return null;
82
+ }
83
+
60
84
  /** Run a shell command with timeout, returning stdout or null on failure */
61
85
  function execAsync(cmd: string, timeoutMs = 5000): Promise<string | null> {
62
86
  return new Promise((resolve) => {
@@ -97,11 +121,8 @@ export async function detectCLIs(
97
121
  const results = await Promise.all(
98
122
  cliList.map(async (cli): Promise<CLIInfo> => {
99
123
  try {
100
- const explicitPath = resolveCommandPath(cli.command);
101
- const pathResult = explicitPath || await execAsync(`${whichCmd} ${shellQuote(cli.command)}`);
102
- if (!pathResult) return { ...cli, installed: false };
103
-
104
- const firstPath = explicitPath || pathResult.split('\n')[0];
124
+ const firstPath = await resolveDetectionPath(cli.command, whichCmd);
125
+ if (!firstPath) return { ...cli, installed: false };
105
126
 
106
127
  // Get version (parallel with other checks)
107
128
  let version: string | undefined;
@@ -148,10 +169,8 @@ export async function detectCLI(
148
169
  const platform = os.platform();
149
170
  const whichCmd = platform === 'win32' ? 'where' : 'which';
150
171
  try {
151
- const explicitPath = resolveCommandPath(target.command);
152
- const pathResult = explicitPath || await execAsync(`${whichCmd} ${shellQuote(target.command)}`);
153
- if (!pathResult) return null;
154
- const firstPath = explicitPath || pathResult.split('\n')[0];
172
+ const firstPath = await resolveDetectionPath(target.command, whichCmd);
173
+ if (!firstPath) return null;
155
174
  let version: string | undefined;
156
175
  if (options?.includeVersion !== false) {
157
176
  const versionCommands = [
@@ -7,11 +7,14 @@
7
7
  * Migrated from @adhdev/core — this is now the single source of truth.
8
8
  */
9
9
 
10
- import { execSync } from 'child_process';
11
- import { existsSync } from 'fs';
10
+ import { exec } from 'child_process';
11
+ import { promisify } from 'util';
12
+ const execAsync = promisify(exec);
13
+ import { existsSync, statSync } from 'fs';
12
14
  import { platform, homedir } from 'os';
13
15
  import * as path from 'path';
14
16
  import type { ProviderLoader } from '../providers/provider-loader.js';
17
+ import { isKnownWin32GuiExe, readWin32IdeVersionFromDisk } from './win32-ide-version.js';
15
18
 
16
19
  // ─── Types ──────────────────────────────────────
17
20
 
@@ -73,25 +76,54 @@ function findCliCommand(command: string): string | null {
73
76
  const resolved = path.isAbsolute(candidate) ? candidate : path.resolve(candidate);
74
77
  return existsSync(resolved) ? resolved : null;
75
78
  }
76
- try {
77
- const result = execSync(
78
- platform() === 'win32' ? `where ${trimmed}` : `which ${trimmed}`,
79
- { encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] }
80
- ).trim();
81
- return result.split('\n')[0] || null;
82
- } catch {
83
- return null;
79
+ const isWin = platform() === 'win32';
80
+ const paths = (process.env.PATH || '').split(isWin ? ';' : ':');
81
+ const exes = isWin ? ['.exe', '.cmd', '.bat', ''] : [''];
82
+ for (const p of paths) {
83
+ if (!p) continue;
84
+ for (const ext of exes) {
85
+ const fullPath = path.join(p, trimmed + ext);
86
+ try {
87
+ if (existsSync(fullPath)) {
88
+ const stat = statSync(fullPath);
89
+ if (stat.isFile() && (isWin || (stat.mode & 0o111))) {
90
+ return fullPath;
91
+ }
92
+ }
93
+ } catch { }
94
+ }
84
95
  }
96
+ return null;
85
97
  }
86
98
 
87
- function getIdeVersion(cliCommand: string): string | null {
99
+ /**
100
+ * Resolve an IDE version on demand (NOT at boot).
101
+ *
102
+ * Order of preference, all spawn-free where possible:
103
+ * 1. win32: read the bundled product.json/package.json next to the exe.
104
+ * 2. Otherwise spawn `<cli> --version` — but ONLY when the binary is not a
105
+ * known GUI executable (the #4 guard), so we never boot an IDE window.
106
+ *
107
+ * `win32ProcessNames` is provider.json `processNames.win32` (type → exe names),
108
+ * used to recognise GUI executables. Callers that have a ProviderLoader can
109
+ * pass `providerLoader.getWinProcessNames()`.
110
+ */
111
+ export async function getIdeVersion(
112
+ cliCommand: string,
113
+ win32ProcessNames: Record<string, string[]> = {},
114
+ ): Promise<string | null> {
115
+ if (platform() === 'win32') {
116
+ const fromDisk = readWin32IdeVersionFromDisk(cliCommand);
117
+ if (fromDisk) return fromDisk;
118
+ // Refuse to spawn a known GUI exe — it would launch the IDE window.
119
+ if (isKnownWin32GuiExe(cliCommand, win32ProcessNames)) return null;
120
+ }
88
121
  try {
89
- const result = execSync(`"${cliCommand}" --version`, {
122
+ const { stdout } = await execAsync(`"${cliCommand}" --version`, {
90
123
  encoding: 'utf-8',
91
124
  timeout: 10000,
92
- stdio: ['pipe', 'pipe', 'pipe'],
93
- }).trim();
94
- return result.split('\n')[0] || null;
125
+ });
126
+ return stdout.trim().split('\n')[0] || null;
95
127
  } catch {
96
128
  return null;
97
129
  }
@@ -152,7 +184,14 @@ export async function detectIDEs(providerLoader?: ProviderLoader): Promise<IDEIn
152
184
  const installed = os === 'darwin'
153
185
  ? !!(resolvedCli || appPath)
154
186
  : !!resolvedCli;
155
- const version = resolvedCli ? getIdeVersion(resolvedCli) : null;
187
+ // Boot-time IDE detection must NOT spawn `<cli> --version`. On Windows
188
+ // the resolved "CLI" is frequently the GUI Electron exe (case-insensitive
189
+ // FS matches `...\cursor\cursor.exe` against `Cursor.exe`), and running it
190
+ // with `--version` boots the IDE window. `installed` is decided purely by
191
+ // existsSync above, and the dashboard drops `version` anyway — the only
192
+ // real version consumer is provider-loader.resolve() at CDP attach time,
193
+ // which fetches the version lazily. So leave it null here.
194
+ const version: string | null = null;
156
195
 
157
196
  results.push({
158
197
  id: def.id,