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

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 +36031 -12365
  36. package/dist/index.js.map +1 -1
  37. package/dist/index.mjs +40382 -16833
  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 +226 -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 +365 -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 +6370 -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 +391 -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 +151 -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 +235 -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 +780 -139
  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 +494 -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,12 @@ import type {
18
18
  RepoMeshNodePolicy,
19
19
  RepoMeshNodeCapabilities,
20
20
  RepoMeshCoordinatorConfig,
21
+ RepoMeshHostMetadata,
22
+ RepoMeshDaemonRole,
23
+ RepoMeshTaskAffinityPolicy,
21
24
  } from '../repo-mesh-types.js';
22
- import { DEFAULT_MESH_POLICY } from '../repo-mesh-types.js';
25
+ import { DEFAULT_MESH_POLICY, normalizeMeshSchedulingStrategy, STANDARD_MESH_ROLES } from '../repo-mesh-types.js';
26
+ import { createDefaultMeshHostMetadata } from '../mesh/mesh-host-ownership.js';
23
27
 
24
28
  // ─── Persistence ────────────────────────────────
25
29
 
@@ -39,6 +43,20 @@ function loadMeshConfig(): LocalMeshConfig {
39
43
  }
40
44
  }
41
45
 
46
+ function normalizeCapabilityTags(value: unknown): string[] | undefined {
47
+ if (!Array.isArray(value)) return undefined;
48
+ const seen = new Set<string>();
49
+ const tags = value
50
+ .map(tag => typeof tag === 'string' ? tag.trim() : '')
51
+ .filter(Boolean)
52
+ .filter(tag => {
53
+ if (seen.has(tag)) return false;
54
+ seen.add(tag);
55
+ return true;
56
+ });
57
+ return tags.length ? tags : undefined;
58
+ }
59
+
42
60
  function saveMeshConfig(config: LocalMeshConfig): void {
43
61
  const path = getMeshConfigPath();
44
62
  writeFileSync(path, JSON.stringify(config, null, 2), { encoding: 'utf-8', mode: 0o600 });
@@ -78,21 +96,130 @@ const SESSION_CLEANUP_MODES = new Set(['preserve', 'stop', 'delete_stopped', 'st
78
96
  const SPAWNED_SESSION_VISIBILITY_MODES = new Set(['visible', 'hidden']);
79
97
 
80
98
  function mergeMeshPolicy(base: RepoMeshPolicy | undefined, patch: Partial<RepoMeshPolicy> | undefined): RepoMeshPolicy {
81
- const policy: RepoMeshPolicy = { ...DEFAULT_MESH_POLICY, ...(base || {}), ...(patch || {}) };
99
+ const autoFastForward = normalizeAutoFastForwardPolicy({
100
+ ...DEFAULT_MESH_POLICY.autoFastForward,
101
+ ...((base?.autoFastForward && typeof base.autoFastForward === 'object') ? base.autoFastForward : {}),
102
+ ...((patch?.autoFastForward && typeof patch.autoFastForward === 'object') ? patch.autoFastForward : {}),
103
+ });
104
+ const policy: RepoMeshPolicy = {
105
+ ...DEFAULT_MESH_POLICY,
106
+ ...(base || {}),
107
+ ...(patch || {}),
108
+ autoFastForward,
109
+ };
82
110
  if (!['block', 'warn', 'checkpoint_then_continue'].includes(policy.dirtyWorkspaceBehavior)) {
83
111
  policy.dirtyWorkspaceBehavior = 'warn';
84
112
  }
85
113
  const maxParallelTasks = Number(policy.maxParallelTasks);
86
114
  policy.maxParallelTasks = Number.isFinite(maxParallelTasks) ? Math.max(1, Math.min(8, Math.floor(maxParallelTasks))) : 2;
115
+ policy.allowAutoPublishSubmoduleMainCommits = policy.allowAutoPublishSubmoduleMainCommits === true;
87
116
  if (!SESSION_CLEANUP_MODES.has(String(policy.sessionCleanupOnNodeRemove))) {
88
117
  policy.sessionCleanupOnNodeRemove = 'preserve';
89
118
  }
90
119
  if (!SPAWNED_SESSION_VISIBILITY_MODES.has(String(policy.spawnedSessionVisibility))) {
91
120
  policy.spawnedSessionVisibility = 'visible';
92
121
  }
122
+ // Load-balancing: normalize the scheduling strategy so an invalid/blank value
123
+ // falls back to 'first_eligible' (strict no-change). Only persist the field when
124
+ // it is explicitly a non-default value to keep existing meshes.json untouched.
125
+ const normalizedStrategy = normalizeMeshSchedulingStrategy(policy.schedulingStrategy);
126
+ if (normalizedStrategy === 'first_eligible') {
127
+ delete policy.schedulingStrategy;
128
+ } else {
129
+ policy.schedulingStrategy = normalizedStrategy;
130
+ }
131
+ // Convergence routing: strict opt-in (default false). Only persist when explicitly
132
+ // enabled so existing meshes.json stays byte-for-byte untouched.
133
+ if (policy.autoConvergeCodeChange === true) {
134
+ policy.autoConvergeCodeChange = true;
135
+ } else {
136
+ delete policy.autoConvergeCodeChange;
137
+ }
138
+ // Task-mode → role affinity: format-only validation (drop blanks). Unknown/custom
139
+ // roles are NOT blocked — only a warning is logged. Only persist when the block
140
+ // carries meaningful content so existing meshes.json stays untouched.
141
+ const normalizedAffinity = normalizeTaskAffinityPolicy(policy.taskAffinity);
142
+ if (normalizedAffinity) {
143
+ policy.taskAffinity = normalizedAffinity;
144
+ } else {
145
+ delete policy.taskAffinity;
146
+ }
93
147
  return policy;
94
148
  }
95
149
 
150
+ /**
151
+ * Format-only normalization of a task-affinity policy. Trims/lowercases role strings,
152
+ * drops blank custom roles and blank byTaskMode keys, and warns (never blocks) when a
153
+ * declared role is not one of the standard four. Returns undefined when the result is
154
+ * empty/no-op so the field stays absent from persisted config.
155
+ */
156
+ function normalizeTaskAffinityPolicy(
157
+ value: unknown,
158
+ ): RepoMeshTaskAffinityPolicy | undefined {
159
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined;
160
+ const record = value as Record<string, unknown>;
161
+ const out: RepoMeshTaskAffinityPolicy = {};
162
+ let hasContent = false;
163
+
164
+ if (typeof record.enabled === 'boolean') {
165
+ out.enabled = record.enabled;
166
+ // `enabled:true` is the default — only persist the explicit `false`.
167
+ if (record.enabled === false) hasContent = true;
168
+ else delete out.enabled;
169
+ }
170
+
171
+ if (record.byTaskMode && typeof record.byTaskMode === 'object' && !Array.isArray(record.byTaskMode)) {
172
+ const byTaskMode: Record<string, string> = {};
173
+ for (const [mode, raw] of Object.entries(record.byTaskMode as Record<string, unknown>)) {
174
+ const modeKey = mode.trim();
175
+ if (!modeKey) continue;
176
+ if (typeof raw !== 'string') continue;
177
+ // A blank value is a deliberate opt-out for that mode — preserve it as ''.
178
+ const role = raw.trim().toLowerCase();
179
+ byTaskMode[modeKey] = role;
180
+ if (role && !STANDARD_MESH_ROLES.includes(role as any)) {
181
+ try {
182
+ console.warn(`[mesh] task_affinity.byTaskMode.${modeKey}: custom role "${role}" is not a standard mesh role (${STANDARD_MESH_ROLES.join('/')}); routing still applies`);
183
+ } catch { /* best-effort */ }
184
+ }
185
+ }
186
+ if (Object.keys(byTaskMode).length > 0) {
187
+ out.byTaskMode = byTaskMode;
188
+ hasContent = true;
189
+ }
190
+ }
191
+
192
+ if (Array.isArray(record.customRoles)) {
193
+ const seen = new Set<string>();
194
+ const customRoles: string[] = [];
195
+ for (const raw of record.customRoles) {
196
+ if (typeof raw !== 'string') continue;
197
+ const role = raw.trim().toLowerCase();
198
+ if (!role || seen.has(role)) continue;
199
+ seen.add(role);
200
+ customRoles.push(role);
201
+ }
202
+ if (customRoles.length > 0) {
203
+ out.customRoles = customRoles;
204
+ hasContent = true;
205
+ }
206
+ }
207
+
208
+ return hasContent ? out : undefined;
209
+ }
210
+
211
+ function normalizeAutoFastForwardPolicy(value: unknown): NonNullable<RepoMeshPolicy['autoFastForward']> {
212
+ const record = value && typeof value === 'object' && !Array.isArray(value)
213
+ ? value as Record<string, unknown>
214
+ : {};
215
+ const maxBehind = Number(record.maxBehind);
216
+ return {
217
+ enabled: record.enabled !== false,
218
+ ...(Number.isFinite(maxBehind) && maxBehind >= 0 ? { maxBehind: Math.floor(maxBehind) } : {}),
219
+ requireCleanSubmodules: record.requireCleanSubmodules !== false,
220
+ };
221
+ }
222
+
96
223
  export function listMeshes(): LocalMeshEntry[] {
97
224
  return loadMeshConfig().meshes;
98
225
  }
@@ -112,6 +239,7 @@ export interface CreateMeshOptions {
112
239
  defaultBranch?: string;
113
240
  policy?: Partial<RepoMeshPolicy>;
114
241
  coordinator?: RepoMeshCoordinatorConfig;
242
+ meshHost?: RepoMeshHostMetadata;
115
243
  }
116
244
 
117
245
  export function createMesh(opts: CreateMeshOptions): LocalMeshEntry {
@@ -133,6 +261,7 @@ export function createMesh(opts: CreateMeshOptions): LocalMeshEntry {
133
261
  defaultBranch: opts.defaultBranch,
134
262
  policy: mergeMeshPolicy(undefined, opts.policy),
135
263
  coordinator: opts.coordinator || {},
264
+ meshHost: opts.meshHost || createDefaultMeshHostMetadata(),
136
265
  nodes: [],
137
266
  createdAt: now,
138
267
  updatedAt: now,
@@ -148,6 +277,7 @@ export interface UpdateMeshOptions {
148
277
  defaultBranch?: string;
149
278
  policy?: Partial<RepoMeshPolicy>;
150
279
  coordinator?: RepoMeshCoordinatorConfig;
280
+ meshHost?: RepoMeshHostMetadata;
151
281
  }
152
282
 
153
283
  export function updateMesh(meshId: string, opts: UpdateMeshOptions): LocalMeshEntry | undefined {
@@ -159,6 +289,7 @@ export function updateMesh(meshId: string, opts: UpdateMeshOptions): LocalMeshEn
159
289
  if (opts.defaultBranch !== undefined) mesh.defaultBranch = opts.defaultBranch;
160
290
  if (opts.policy) mesh.policy = mergeMeshPolicy(mesh.policy, opts.policy);
161
291
  if (opts.coordinator) mesh.coordinator = opts.coordinator;
292
+ if (opts.meshHost) mesh.meshHost = opts.meshHost;
162
293
  mesh.updatedAt = new Date().toISOString();
163
294
 
164
295
  saveMeshConfig(config);
@@ -174,6 +305,240 @@ export function deleteMesh(meshId: string): boolean {
174
305
  return true;
175
306
  }
176
307
 
308
+ function normalizeManualHostAddress(hostAddress: string): string {
309
+ const normalized = hostAddress.trim().replace(/\/+$/, '');
310
+ if (!normalized) throw new Error('hostAddress required');
311
+ let parsed: URL;
312
+ try {
313
+ parsed = new URL(normalized);
314
+ } catch {
315
+ throw new Error('hostAddress must be a valid http(s) or ws(s) URL');
316
+ }
317
+ if (!['http:', 'https:', 'ws:', 'wss:'].includes(parsed.protocol)) {
318
+ throw new Error('hostAddress must use http, https, ws, or wss');
319
+ }
320
+ return normalized;
321
+ }
322
+
323
+ export function tokenIdForManualPairing(token: string): string {
324
+ return `tok_${createHash('sha256').update(token).digest('hex').slice(0, 16)}`;
325
+ }
326
+
327
+ function normalizeTokenExpiry(value: unknown): string | undefined {
328
+ if (typeof value !== 'string' || !value.trim()) return undefined;
329
+ const date = new Date(value);
330
+ if (Number.isNaN(date.getTime())) throw new Error('expiresAt must be a valid ISO date');
331
+ return date.toISOString();
332
+ }
333
+
334
+ function assertPairingTokenValid(pairing: RepoMeshHostMetadata['pairing'], rawToken: string, nowIso: string): { ok: true; tokenId: string } | { ok: false; reason: string; expectedTokenId?: string; presentedTokenId?: string } {
335
+ const token = rawToken.trim();
336
+ if (!token) return { ok: false, reason: 'token required' };
337
+ const presentedTokenId = tokenIdForManualPairing(token);
338
+ const expectedTokenId = pairing?.tokenId;
339
+ if (!expectedTokenId || pairing?.status === 'not_configured' || pairing?.status === 'revoked') {
340
+ return { ok: false, reason: 'host pairing token is not configured', presentedTokenId };
341
+ }
342
+ if (pairing.expiresAt && new Date(pairing.expiresAt).getTime() <= new Date(nowIso).getTime()) {
343
+ return { ok: false, reason: 'host pairing token expired', expectedTokenId, presentedTokenId };
344
+ }
345
+ if (presentedTokenId !== expectedTokenId) {
346
+ return { ok: false, reason: 'invalid pairing token', expectedTokenId, presentedTokenId };
347
+ }
348
+ return { ok: true, tokenId: presentedTokenId };
349
+ }
350
+
351
+ export interface ConfigureMeshHostPairingOptions {
352
+ hostAddress: string;
353
+ token: string;
354
+ now?: string;
355
+ }
356
+
357
+ export function configureMeshHostPairing(
358
+ meshId: string,
359
+ opts: ConfigureMeshHostPairingOptions,
360
+ ): { mesh: LocalMeshEntry; meshHost: RepoMeshHostMetadata; hostAddress: string } | undefined {
361
+ const hostAddress = normalizeManualHostAddress(opts.hostAddress);
362
+ const token = opts.token.trim();
363
+ if (!token) throw new Error('token required');
364
+
365
+ const config = loadMeshConfig();
366
+ const mesh = config.meshes.find(m => m.id === meshId);
367
+ if (!mesh) return undefined;
368
+
369
+ const now = opts.now || new Date().toISOString();
370
+ const previous = mesh.meshHost || createDefaultMeshHostMetadata();
371
+ const meshHost: RepoMeshHostMetadata = {
372
+ ...previous,
373
+ role: 'member',
374
+ hostAddress,
375
+ pairing: {
376
+ status: 'pairing',
377
+ tokenId: tokenIdForManualPairing(token),
378
+ lastPairedAt: now,
379
+ },
380
+ };
381
+
382
+ mesh.meshHost = meshHost;
383
+ mesh.updatedAt = now;
384
+ saveMeshConfig(config);
385
+ return { mesh, meshHost, hostAddress };
386
+ }
387
+
388
+ export interface CreateMeshHostPairingTokenOptions {
389
+ token?: string;
390
+ expiresAt?: string;
391
+ now?: string;
392
+ }
393
+
394
+ export function createMeshHostPairingToken(
395
+ meshId: string,
396
+ opts: CreateMeshHostPairingTokenOptions = {},
397
+ ): { mesh: LocalMeshEntry; meshHost: RepoMeshHostMetadata; token: string; tokenId: string; expiresAt?: string } | undefined {
398
+ const config = loadMeshConfig();
399
+ const mesh = config.meshes.find(m => m.id === meshId);
400
+ if (!mesh) return undefined;
401
+ const now = opts.now || new Date().toISOString();
402
+ const token = (opts.token || `mhj_${randomBytes(24).toString('base64url')}`).trim();
403
+ if (!token) throw new Error('token required');
404
+ const tokenId = tokenIdForManualPairing(token);
405
+ const expiresAt = normalizeTokenExpiry(opts.expiresAt);
406
+ const previous = mesh.meshHost || createDefaultMeshHostMetadata();
407
+ if (previous.role === 'member') {
408
+ throw new Error('Mesh Host daemon required to create host pairing tokens; member daemons cannot mint host join tokens.');
409
+ }
410
+ const meshHost: RepoMeshHostMetadata = {
411
+ ...previous,
412
+ role: 'host',
413
+ pairing: {
414
+ status: 'pairing',
415
+ tokenId,
416
+ lastPairedAt: now,
417
+ ...(expiresAt ? { expiresAt } : {}),
418
+ },
419
+ };
420
+ mesh.meshHost = meshHost;
421
+ mesh.updatedAt = now;
422
+ saveMeshConfig(config);
423
+ return { mesh, meshHost, token, tokenId, ...(expiresAt ? { expiresAt } : {}) };
424
+ }
425
+
426
+ export interface MeshHostJoinMemberNodeInput {
427
+ id?: string;
428
+ workspace: string;
429
+ repoRoot?: string;
430
+ daemonId?: string;
431
+ machineId?: string;
432
+ userOverrides?: Partial<RepoMeshNodeCapabilities>;
433
+ policy?: RepoMeshNodePolicy;
434
+ role?: RepoMeshDaemonRole;
435
+ }
436
+
437
+ export interface ApplyMeshHostJoinOptions {
438
+ token: string;
439
+ memberNode: MeshHostJoinMemberNodeInput;
440
+ memberMeshId?: string;
441
+ now?: string;
442
+ }
443
+
444
+ export function applyMeshHostJoinRequest(
445
+ meshId: string,
446
+ opts: ApplyMeshHostJoinOptions,
447
+ ): { accepted: true; mesh: LocalMeshEntry; meshHost: RepoMeshHostMetadata; node: LocalMeshNodeEntry; tokenId: string } | { accepted: false; mesh?: LocalMeshEntry; meshHost?: RepoMeshHostMetadata; tokenId?: string; reason: string } | undefined {
448
+ const config = loadMeshConfig();
449
+ const mesh = config.meshes.find(m => m.id === meshId);
450
+ if (!mesh) return undefined;
451
+ const now = opts.now || new Date().toISOString();
452
+ const previous = mesh.meshHost || createDefaultMeshHostMetadata();
453
+ if (previous.role === 'member') {
454
+ return { accepted: false, mesh, meshHost: previous, reason: 'Mesh Host daemon required to accept join requests' };
455
+ }
456
+ const meshHost: RepoMeshHostMetadata = { ...previous, role: 'host' };
457
+ const validation = assertPairingTokenValid(meshHost.pairing, opts.token, now);
458
+ if (!validation.ok) {
459
+ mesh.meshHost = {
460
+ ...meshHost,
461
+ pairing: {
462
+ ...(meshHost.pairing || { status: 'not_configured' as const }),
463
+ status: 'rejected',
464
+ lastRejectedAt: now,
465
+ },
466
+ };
467
+ mesh.updatedAt = now;
468
+ saveMeshConfig(config);
469
+ return { accepted: false, mesh, meshHost: mesh.meshHost, tokenId: validation.presentedTokenId, reason: validation.reason };
470
+ }
471
+
472
+ const workspace = opts.memberNode.workspace.trim();
473
+ if (!workspace) throw new Error('memberNode.workspace required');
474
+ const memberId = opts.memberNode.id?.trim();
475
+ let node = mesh.nodes.find(n => (memberId && n.id === memberId) || n.workspace === workspace);
476
+ if (node) {
477
+ node.workspace = workspace;
478
+ node.repoRoot = opts.memberNode.repoRoot;
479
+ node.daemonId = opts.memberNode.daemonId;
480
+ node.machineId = opts.memberNode.machineId;
481
+ node.userOverrides = opts.memberNode.userOverrides || node.userOverrides || {};
482
+ node.policy = { ...(node.policy || {}), ...(opts.memberNode.policy || {}) };
483
+ node.role = 'member';
484
+ } else {
485
+ if (mesh.nodes.length >= 10) throw new Error('Maximum 10 nodes per mesh');
486
+ node = {
487
+ id: memberId || `node_${randomUUID().replace(/-/g, '')}`,
488
+ workspace,
489
+ repoRoot: opts.memberNode.repoRoot,
490
+ daemonId: opts.memberNode.daemonId,
491
+ machineId: opts.memberNode.machineId,
492
+ userOverrides: opts.memberNode.userOverrides || {},
493
+ policy: opts.memberNode.policy || {},
494
+ role: 'member',
495
+ };
496
+ mesh.nodes.push(node);
497
+ }
498
+ mesh.meshHost = {
499
+ ...meshHost,
500
+ pairing: {
501
+ ...(meshHost.pairing || {}),
502
+ status: 'paired',
503
+ tokenId: validation.tokenId,
504
+ joinedAt: now,
505
+ lastPairedAt: meshHost.pairing?.lastPairedAt || now,
506
+ ...(meshHost.pairing?.expiresAt ? { expiresAt: meshHost.pairing.expiresAt } : {}),
507
+ },
508
+ };
509
+ mesh.updatedAt = now;
510
+ saveMeshConfig(config);
511
+ return { accepted: true, mesh, meshHost: mesh.meshHost, node, tokenId: validation.tokenId };
512
+ }
513
+
514
+ export function markMeshHostPairingJoined(
515
+ meshId: string,
516
+ opts: { hostDaemonId?: string; hostNodeId?: string; joinedAt?: string; token?: string; tokenId?: string },
517
+ ): { mesh: LocalMeshEntry; meshHost: RepoMeshHostMetadata } | undefined {
518
+ const config = loadMeshConfig();
519
+ const mesh = config.meshes.find(m => m.id === meshId);
520
+ if (!mesh) return undefined;
521
+ const now = opts.joinedAt || new Date().toISOString();
522
+ const previous = mesh.meshHost || createDefaultMeshHostMetadata();
523
+ const tokenId = opts.tokenId || (opts.token ? tokenIdForManualPairing(opts.token) : previous.pairing?.tokenId);
524
+ mesh.meshHost = {
525
+ ...previous,
526
+ role: 'member',
527
+ ...(opts.hostDaemonId ? { hostDaemonId: opts.hostDaemonId } : {}),
528
+ ...(opts.hostNodeId ? { hostNodeId: opts.hostNodeId } : {}),
529
+ pairing: {
530
+ ...(previous.pairing || {}),
531
+ status: 'paired',
532
+ ...(tokenId ? { tokenId } : {}),
533
+ joinedAt: now,
534
+ lastPairedAt: previous.pairing?.lastPairedAt || now,
535
+ },
536
+ };
537
+ mesh.updatedAt = now;
538
+ saveMeshConfig(config);
539
+ return { mesh, meshHost: mesh.meshHost };
540
+ }
541
+
177
542
  // ─── Node Operations ────────────────────────────
178
543
 
179
544
  export interface AddNodeOptions {
@@ -181,11 +546,14 @@ export interface AddNodeOptions {
181
546
  repoRoot?: string;
182
547
  daemonId?: string;
183
548
  machineId?: string;
549
+ capabilities?: string[];
184
550
  userOverrides?: Partial<RepoMeshNodeCapabilities>;
185
551
  policy?: RepoMeshNodePolicy;
186
552
  isLocalWorktree?: boolean;
187
553
  worktreeBranch?: string;
188
554
  clonedFromNodeId?: string;
555
+ worktreeBootstrap?: LocalMeshNodeEntry['worktreeBootstrap'];
556
+ role?: RepoMeshDaemonRole;
189
557
  }
190
558
 
191
559
  export function addNode(meshId: string, opts: AddNodeOptions): LocalMeshNodeEntry | undefined {
@@ -208,11 +576,14 @@ export function addNode(meshId: string, opts: AddNodeOptions): LocalMeshNodeEntr
208
576
  repoRoot: opts.repoRoot,
209
577
  daemonId: opts.daemonId,
210
578
  machineId: opts.machineId,
579
+ capabilities: normalizeCapabilityTags(opts.capabilities),
211
580
  userOverrides: opts.userOverrides || {},
212
581
  policy: opts.policy || {},
213
582
  isLocalWorktree: opts.isLocalWorktree,
214
583
  worktreeBranch: opts.worktreeBranch,
215
584
  clonedFromNodeId: opts.clonedFromNodeId,
585
+ worktreeBootstrap: opts.worktreeBootstrap,
586
+ role: opts.role,
216
587
  };
217
588
 
218
589
  mesh.nodes.push(node);
@@ -238,7 +609,14 @@ export function removeNode(meshId: string, nodeId: string): boolean {
238
609
  export function updateNode(
239
610
  meshId: string,
240
611
  nodeId: string,
241
- opts: { userOverrides?: Partial<RepoMeshNodeCapabilities>; policy?: RepoMeshNodePolicy },
612
+ opts: {
613
+ userOverrides?: Partial<RepoMeshNodeCapabilities>;
614
+ policy?: RepoMeshNodePolicy;
615
+ worktreeBootstrap?: LocalMeshNodeEntry['worktreeBootstrap'];
616
+ /** Per-node instruction surfaced in the coordinator prompt. Pass an
617
+ * empty string or undefined to clear it. */
618
+ systemPrompt?: string;
619
+ },
242
620
  ): LocalMeshNodeEntry | undefined {
243
621
  const config = loadMeshConfig();
244
622
  const mesh = config.meshes.find(m => m.id === meshId);
@@ -249,6 +627,15 @@ export function updateNode(
249
627
 
250
628
  if (opts.userOverrides) node.userOverrides = { ...node.userOverrides, ...opts.userOverrides };
251
629
  if (opts.policy) node.policy = { ...node.policy, ...opts.policy };
630
+ if (opts.worktreeBootstrap) node.worktreeBootstrap = opts.worktreeBootstrap;
631
+ if (Object.prototype.hasOwnProperty.call(opts, 'systemPrompt')) {
632
+ // Honor explicit clears: { systemPrompt: undefined } drops the field.
633
+ if (opts.systemPrompt && opts.systemPrompt.trim()) {
634
+ node.systemPrompt = opts.systemPrompt;
635
+ } else {
636
+ delete node.systemPrompt;
637
+ }
638
+ }
252
639
  mesh.updatedAt = new Date().toISOString();
253
640
  saveMeshConfig(config);
254
641
  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 = [