@adhdev/daemon-core 0.9.82-rc.31 → 0.9.82-rc.310

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 (293) 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 +23 -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 +106 -63
  11. package/dist/cli-adapters/provider-cli-parse.d.ts +4 -0
  12. package/dist/cli-adapters/provider-cli-shared.d.ts +38 -0
  13. package/dist/cli-adapters/raw-terminal-io.d.ts +37 -0
  14. package/dist/cli-adapters/resolve-executable.d.ts +14 -0
  15. package/dist/cli-adapters/terminal-backends/ghostty-vt-backend.d.ts +6 -3
  16. package/dist/cli-adapters/terminal-backends/types.d.ts +10 -2
  17. package/dist/cli-adapters/terminal-screen.d.ts +4 -11
  18. package/dist/commands/cli-manager.d.ts +2 -1
  19. package/dist/commands/handler.d.ts +110 -0
  20. package/dist/commands/mesh-coordinator.d.ts +85 -1
  21. package/dist/commands/router.d.ts +210 -1
  22. package/dist/config/chat-history.d.ts +9 -0
  23. package/dist/config/config.d.ts +5 -0
  24. package/dist/config/mesh-config.d.ts +72 -1
  25. package/dist/git/git-commands.d.ts +9 -1
  26. package/dist/git/git-diff.d.ts +6 -0
  27. package/dist/git/git-executor.d.ts +11 -0
  28. package/dist/git/git-status.d.ts +9 -0
  29. package/dist/git/git-types.d.ts +2 -50
  30. package/dist/index.d.ts +51 -12
  31. package/dist/index.js +38320 -16024
  32. package/dist/index.js.map +1 -1
  33. package/dist/index.mjs +38009 -15817
  34. package/dist/index.mjs.map +1 -1
  35. package/dist/installer.d.ts +1 -4
  36. package/dist/ipc/local-ipc-server.d.ts +91 -0
  37. package/dist/launch.d.ts +1 -1
  38. package/dist/logging/async-batch-writer.d.ts +10 -0
  39. package/dist/mesh/contracts.d.ts +164 -0
  40. package/dist/mesh/coordinator-prompt.d.ts +36 -0
  41. package/dist/mesh/coordinator-registry.d.ts +59 -0
  42. package/dist/mesh/mesh-active-work.d.ts +108 -0
  43. package/dist/mesh/mesh-delivery-policy.d.ts +131 -0
  44. package/dist/mesh/mesh-events-coordinator.d.ts +138 -0
  45. package/dist/mesh/mesh-events-pending.d.ts +45 -0
  46. package/dist/mesh/mesh-events-stale.d.ts +40 -0
  47. package/dist/mesh/mesh-events-utils.d.ts +47 -0
  48. package/dist/mesh/mesh-events.d.ts +6 -49
  49. package/dist/mesh/mesh-fast-forward.d.ts +81 -0
  50. package/dist/mesh/mesh-host-ownership.d.ts +9 -0
  51. package/dist/mesh/mesh-init.d.ts +86 -0
  52. package/dist/mesh/mesh-ledger-reconciliation.d.ts +23 -3
  53. package/dist/mesh/mesh-ledger.d.ts +77 -1
  54. package/dist/mesh/mesh-missions.d.ts +116 -0
  55. package/dist/mesh/mesh-reconcile-loop.d.ts +7 -0
  56. package/dist/mesh/mesh-refine-batch.d.ts +68 -0
  57. package/dist/mesh/mesh-refine-status.d.ts +62 -0
  58. package/dist/mesh/mesh-review-inbox.d.ts +90 -0
  59. package/dist/mesh/mesh-routing.d.ts +70 -0
  60. package/dist/mesh/mesh-runtime-store.d.ts +377 -0
  61. package/dist/mesh/mesh-task-stats.d.ts +49 -0
  62. package/dist/mesh/mesh-unresolved-forward-outbox.d.ts +30 -0
  63. package/dist/mesh/mesh-work-queue.d.ts +181 -5
  64. package/dist/mesh/preview-freshness.d.ts +18 -0
  65. package/dist/mesh/refine-config.d.ts +215 -0
  66. package/dist/mesh/worktree-bootstrap-config.d.ts +135 -0
  67. package/dist/providers/acp-provider-instance.d.ts +2 -0
  68. package/dist/providers/approval-utils.d.ts +13 -0
  69. package/dist/providers/cli-provider-instance.d.ts +61 -3
  70. package/dist/providers/contracts.d.ts +130 -6
  71. package/dist/providers/external-sources.d.ts +71 -0
  72. package/dist/providers/native-history/antigravity-cli-transcript.d.ts +100 -0
  73. package/dist/providers/native-history/claude-cli-transcript.d.ts +70 -0
  74. package/dist/providers/native-history/codex-cli-transcript.d.ts +73 -0
  75. package/dist/providers/native-history/dispatcher.d.ts +27 -0
  76. package/dist/providers/native-history/hermes-cli-transcript.d.ts +30 -0
  77. package/dist/providers/native-history/index.d.ts +13 -0
  78. package/dist/providers/provider-instance-manager.d.ts +13 -0
  79. package/dist/providers/provider-instance.d.ts +13 -1
  80. package/dist/providers/provider-loader.d.ts +26 -4
  81. package/dist/providers/provider-trust.d.ts +31 -0
  82. package/dist/providers/read-chat-contract.d.ts +29 -0
  83. package/dist/providers/sdk/v1/builders/acp/detect-status.d.ts +68 -0
  84. package/dist/providers/sdk/v1/builders/cli/detect-status.d.ts +85 -0
  85. package/dist/providers/sdk/v1/builders/cli/parse-approval-squash.d.ts +59 -0
  86. package/dist/providers/sdk/v1/builders/cli/parse-approval.d.ts +65 -0
  87. package/dist/providers/sdk/v1/builders/cli/parse-session.d.ts +91 -0
  88. package/dist/providers/sdk/v1/builders/cli/visible-region.d.ts +42 -0
  89. package/dist/providers/sdk/v1/fixture-tooling/format.d.ts +126 -0
  90. package/dist/providers/sdk/v1/fixture-tooling/index.d.ts +8 -0
  91. package/dist/providers/sdk/v1/fixture-tooling/replay.d.ts +38 -0
  92. package/dist/providers/sdk/v1/index.d.ts +30 -0
  93. package/dist/providers/sdk/v1/sandbox/README-design.d.ts +193 -0
  94. package/dist/providers/sdk/v1/sandbox/require-whitelist.d.ts +74 -0
  95. package/dist/providers/sdk/v1/sandbox/script-runner.d.ts +98 -0
  96. package/dist/providers/sdk/v1/types/cli/index.d.ts +277 -0
  97. package/dist/providers/sdk/v1/types/common/index.d.ts +203 -0
  98. package/dist/providers/sdk/v1/validators/index.d.ts +5 -0
  99. package/dist/providers/sdk/v1/validators/manifest.d.ts +40 -0
  100. package/dist/providers/sdk/v1/validators/taint.d.ts +52 -0
  101. package/dist/providers/spec/adapter.d.ts +59 -0
  102. package/dist/providers/spec/cli-adapter.d.ts +186 -0
  103. package/dist/providers/spec/evaluator.d.ts +23 -0
  104. package/dist/providers/spec/fsm-driver.d.ts +278 -0
  105. package/dist/providers/spec/fsm-evaluator.d.ts +72 -0
  106. package/dist/providers/spec/fsm-loader.d.ts +14 -0
  107. package/dist/providers/spec/fsm-types.d.ts +128 -0
  108. package/dist/providers/spec/native-history-executor.d.ts +41 -0
  109. package/dist/providers/spec/pre-launch-trust.d.ts +16 -0
  110. package/dist/providers/spec/route.d.ts +4 -0
  111. package/dist/providers/spec/types.d.ts +144 -0
  112. package/dist/providers/transcript-v2.d.ts +176 -0
  113. package/dist/providers/types/interactive-prompt.d.ts +84 -0
  114. package/dist/repo-mesh-types.d.ts +203 -13
  115. package/dist/sessions/registry.d.ts +6 -0
  116. package/dist/shared-types-extra.d.ts +2 -4
  117. package/dist/shared-types.d.ts +42 -1
  118. package/dist/status/normalize.d.ts +1 -1
  119. package/dist/status/normalize.js +1 -0
  120. package/dist/status/normalize.js.map +1 -1
  121. package/dist/status/normalize.mjs +1 -0
  122. package/dist/status/normalize.mjs.map +1 -1
  123. package/dist/status/reporter.d.ts +2 -0
  124. package/dist/status/snapshot.d.ts +1 -0
  125. package/dist/types.d.ts +5 -0
  126. package/package.json +7 -3
  127. package/src/agent-stream/poller.ts +2 -3
  128. package/src/boot/daemon-lifecycle.ts +55 -12
  129. package/src/boot/process-hardening.ts +89 -0
  130. package/src/build-info.ts +73 -0
  131. package/src/chat/source-machine.ts +534 -0
  132. package/src/chat/source-resolver.ts +0 -0
  133. package/src/chat/subscription-updates.ts +20 -1
  134. package/src/cli-adapter-types.d.ts +1 -0
  135. package/src/cli-adapter-types.ts +22 -2
  136. package/src/cli-adapters/cli-script-runner.ts +421 -0
  137. package/src/cli-adapters/cli-state-engine.ts +1215 -0
  138. package/src/cli-adapters/provider-cli-adapter.d.ts +1 -1
  139. package/src/cli-adapters/provider-cli-adapter.ts +674 -1139
  140. package/src/cli-adapters/provider-cli-parse.d.ts +1 -0
  141. package/src/cli-adapters/provider-cli-parse.ts +13 -0
  142. package/src/cli-adapters/provider-cli-runtime.ts +3 -1
  143. package/src/cli-adapters/provider-cli-shared.d.ts +2 -0
  144. package/src/cli-adapters/provider-cli-shared.ts +68 -11
  145. package/src/cli-adapters/pty-transport.ts +2 -1
  146. package/src/cli-adapters/raw-terminal-io.ts +252 -0
  147. package/src/cli-adapters/resolve-executable.ts +45 -0
  148. package/src/cli-adapters/session-host-transport.ts +2 -1
  149. package/src/cli-adapters/terminal-backends/ghostty-vt-backend.ts +53 -29
  150. package/src/cli-adapters/terminal-backends/types.ts +10 -3
  151. package/src/cli-adapters/terminal-screen.ts +16 -81
  152. package/src/commands/chat-commands.ts +1813 -60
  153. package/src/commands/cli-manager.ts +286 -14
  154. package/src/commands/handler.ts +809 -2
  155. package/src/commands/mesh-coordinator.ts +332 -122
  156. package/src/commands/router.ts +5911 -595
  157. package/src/commands/upgrade-helper.ts +29 -0
  158. package/src/config/chat-history.ts +474 -24
  159. package/src/config/config.ts +12 -0
  160. package/src/config/mesh-config.ts +303 -3
  161. package/src/config/recent-activity.ts +8 -2
  162. package/src/daemon/dev-auto-implement.ts +3 -2
  163. package/src/daemon/dev-cli-debug.ts +10 -1
  164. package/src/detection/ide-detector.ts +26 -16
  165. package/src/git/git-commands.ts +42 -12
  166. package/src/git/git-diff.ts +53 -0
  167. package/src/git/git-executor.ts +12 -0
  168. package/src/git/git-status.ts +302 -18
  169. package/src/git/git-types.ts +14 -62
  170. package/src/git/git-worktree.ts +8 -1
  171. package/src/index.ts +131 -11
  172. package/src/installer.d.ts +1 -1
  173. package/src/installer.ts +8 -6
  174. package/src/ipc/local-ipc-server.ts +278 -0
  175. package/src/launch.d.ts +1 -1
  176. package/src/launch.ts +37 -28
  177. package/src/logging/async-batch-writer.ts +55 -0
  178. package/src/logging/logger.ts +2 -1
  179. package/src/mesh/contracts.ts +329 -0
  180. package/src/mesh/coordinator-prompt.ts +234 -31
  181. package/src/mesh/coordinator-registry.ts +121 -0
  182. package/src/mesh/mesh-active-work.ts +469 -0
  183. package/src/mesh/mesh-delivery-policy.ts +315 -0
  184. package/src/mesh/mesh-events-coordinator.ts +1786 -0
  185. package/src/mesh/mesh-events-pending.ts +508 -0
  186. package/src/mesh/mesh-events-stale.ts +290 -0
  187. package/src/mesh/mesh-events-utils.ts +254 -0
  188. package/src/mesh/mesh-events.ts +30 -1035
  189. package/src/mesh/mesh-fast-forward.ts +839 -0
  190. package/src/mesh/mesh-host-ownership.ts +73 -0
  191. package/src/mesh/mesh-init.ts +260 -0
  192. package/src/mesh/mesh-ledger-reconciliation.ts +12 -5
  193. package/src/mesh/mesh-ledger.ts +589 -102
  194. package/src/mesh/mesh-missions.ts +248 -0
  195. package/src/mesh/mesh-reconcile-loop.ts +466 -0
  196. package/src/mesh/mesh-refine-batch.ts +197 -0
  197. package/src/mesh/mesh-refine-status.ts +231 -0
  198. package/src/mesh/mesh-review-inbox.ts +307 -0
  199. package/src/mesh/mesh-routing.ts +291 -0
  200. package/src/mesh/mesh-runtime-store.ts +1513 -0
  201. package/src/mesh/mesh-task-stats.ts +154 -0
  202. package/src/mesh/mesh-unresolved-forward-outbox.ts +185 -0
  203. package/src/mesh/mesh-work-queue.ts +604 -145
  204. package/src/mesh/preview-freshness.ts +118 -0
  205. package/src/mesh/refine-config.ts +403 -0
  206. package/src/mesh/worktree-bootstrap-config.ts +326 -0
  207. package/src/providers/acp-provider-instance.ts +15 -1
  208. package/src/providers/approval-utils.d.ts +4 -0
  209. package/src/providers/approval-utils.ts +47 -5
  210. package/src/providers/chat-message-normalization.ts +4 -11
  211. package/src/providers/cli-provider-instance.ts +953 -87
  212. package/src/providers/contracts.d.ts +55 -0
  213. package/src/providers/contracts.ts +141 -6
  214. package/src/providers/extension-provider-instance.ts +5 -1
  215. package/src/providers/external-sources.ts +218 -0
  216. package/src/providers/ide-provider-instance.ts +25 -6
  217. package/src/providers/native-history/antigravity-cli-transcript.ts +643 -0
  218. package/src/providers/native-history/claude-cli-transcript.ts +396 -0
  219. package/src/providers/native-history/codex-cli-transcript.ts +479 -0
  220. package/src/providers/native-history/dispatcher.ts +340 -0
  221. package/src/providers/native-history/hermes-cli-transcript.ts +230 -0
  222. package/src/providers/native-history/index.ts +30 -0
  223. package/src/providers/provider-instance-manager.ts +30 -0
  224. package/src/providers/provider-instance.ts +10 -1
  225. package/src/providers/provider-loader.ts +584 -50
  226. package/src/providers/provider-schema.ts +87 -14
  227. package/src/providers/provider-trust.ts +114 -0
  228. package/src/providers/read-chat-contract.ts +76 -16
  229. package/src/providers/sdk/README.md +49 -0
  230. package/src/providers/sdk/v1/builders/acp/detect-status.ts +144 -0
  231. package/src/providers/sdk/v1/builders/cli/detect-status.ts +262 -0
  232. package/src/providers/sdk/v1/builders/cli/parse-approval-squash.ts +158 -0
  233. package/src/providers/sdk/v1/builders/cli/parse-approval.ts +250 -0
  234. package/src/providers/sdk/v1/builders/cli/parse-session.ts +276 -0
  235. package/src/providers/sdk/v1/builders/cli/visible-region.ts +143 -0
  236. package/src/providers/sdk/v1/fixture-tooling/format.ts +130 -0
  237. package/src/providers/sdk/v1/fixture-tooling/index.ts +22 -0
  238. package/src/providers/sdk/v1/fixture-tooling/replay.ts +352 -0
  239. package/src/providers/sdk/v1/index.ts +152 -0
  240. package/src/providers/sdk/v1/sandbox/README-design.ts +195 -0
  241. package/src/providers/sdk/v1/sandbox/require-whitelist.ts +472 -0
  242. package/src/providers/sdk/v1/sandbox/script-runner.ts +150 -0
  243. package/src/providers/sdk/v1/schemas/cli/provider.schema.json +504 -0
  244. package/src/providers/sdk/v1/schemas/primitives/acp-session-protocol-v1.json +131 -0
  245. package/src/providers/sdk/v1/schemas/primitives/native-history-codex-rollout-v1.json +66 -0
  246. package/src/providers/sdk/v1/schemas/primitives/tui-approval-squash-v1.json +91 -0
  247. package/src/providers/sdk/v1/schemas/primitives/tui-assistant-block-v1.json +91 -0
  248. package/src/providers/sdk/v1/schemas/primitives/tui-cue-ordering-v1.json +47 -0
  249. package/src/providers/sdk/v1/schemas/primitives/tui-dispatch-order-v1.json +32 -0
  250. package/src/providers/sdk/v1/schemas/primitives/tui-footer-chrome-v1.json +42 -0
  251. package/src/providers/sdk/v1/schemas/primitives/tui-index-finder-v1.json +27 -0
  252. package/src/providers/sdk/v1/schemas/primitives/tui-modal-v1.json +125 -0
  253. package/src/providers/sdk/v1/schemas/primitives/tui-prompt-marker-v1.json +45 -0
  254. package/src/providers/sdk/v1/schemas/primitives/tui-session-id-extraction-v1.json +46 -0
  255. package/src/providers/sdk/v1/schemas/primitives/tui-settled-prompt-v1.json +71 -0
  256. package/src/providers/sdk/v1/schemas/primitives/tui-spinner-v1.json +83 -0
  257. package/src/providers/sdk/v1/schemas/primitives/tui-transcript-pty-v1.json +83 -0
  258. package/src/providers/sdk/v1/schemas/primitives/tui-visible-region-v1.json +57 -0
  259. package/src/providers/sdk/v1/schemas/primitives/tui-welcome-screen-v1.json +35 -0
  260. package/src/providers/sdk/v1/types/cli/index.ts +365 -0
  261. package/src/providers/sdk/v1/types/common/index.ts +229 -0
  262. package/src/providers/sdk/v1/validators/index.ts +19 -0
  263. package/src/providers/sdk/v1/validators/manifest.ts +110 -0
  264. package/src/providers/sdk/v1/validators/taint.ts +309 -0
  265. package/src/providers/spec/adapter.ts +155 -0
  266. package/src/providers/spec/cli-adapter.ts +838 -0
  267. package/src/providers/spec/evaluator.ts +345 -0
  268. package/src/providers/spec/fsm-driver.ts +875 -0
  269. package/src/providers/spec/fsm-evaluator.ts +255 -0
  270. package/src/providers/spec/fsm-loader.ts +112 -0
  271. package/src/providers/spec/fsm-types.ts +222 -0
  272. package/src/providers/spec/native-history-executor.ts +943 -0
  273. package/src/providers/spec/pre-launch-trust.ts +104 -0
  274. package/src/providers/spec/route.ts +51 -0
  275. package/src/providers/spec/types.ts +189 -0
  276. package/src/providers/transcript-v2.ts +567 -0
  277. package/src/providers/types/interactive-prompt.ts +536 -0
  278. package/src/providers/version-archive.ts +38 -20
  279. package/src/repo-mesh-types.ts +257 -13
  280. package/src/sessions/registry.ts +6 -0
  281. package/src/shared-types-extra.ts +2 -4
  282. package/src/shared-types.ts +45 -1
  283. package/src/status/builders.ts +26 -6
  284. package/src/status/normalize.ts +2 -0
  285. package/src/status/reporter.ts +15 -0
  286. package/src/status/snapshot.ts +84 -25
  287. package/src/system/host-memory.ts +29 -12
  288. package/src/types.ts +5 -0
  289. package/dist/cli-adapters/terminal-backends/xterm-backend.d.ts +0 -17
  290. package/dist/mesh/mesh-sync.d.ts +0 -53
  291. package/src/cli-adapters/terminal-backends/ghostty-vt-backend.d.ts +0 -16
  292. package/src/cli-adapters/terminal-backends/xterm-backend.ts +0 -97
  293. 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
24
  import { DEFAULT_MESH_POLICY } 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,12 +95,23 @@ 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
  }
@@ -93,6 +121,18 @@ function mergeMeshPolicy(base: RepoMeshPolicy | undefined, patch: Partial<RepoMe
93
121
  return policy;
94
122
  }
95
123
 
124
+ function normalizeAutoFastForwardPolicy(value: unknown): NonNullable<RepoMeshPolicy['autoFastForward']> {
125
+ const record = value && typeof value === 'object' && !Array.isArray(value)
126
+ ? value as Record<string, unknown>
127
+ : {};
128
+ const maxBehind = Number(record.maxBehind);
129
+ return {
130
+ enabled: record.enabled !== false,
131
+ ...(Number.isFinite(maxBehind) && maxBehind >= 0 ? { maxBehind: Math.floor(maxBehind) } : {}),
132
+ requireCleanSubmodules: record.requireCleanSubmodules !== false,
133
+ };
134
+ }
135
+
96
136
  export function listMeshes(): LocalMeshEntry[] {
97
137
  return loadMeshConfig().meshes;
98
138
  }
@@ -112,6 +152,7 @@ export interface CreateMeshOptions {
112
152
  defaultBranch?: string;
113
153
  policy?: Partial<RepoMeshPolicy>;
114
154
  coordinator?: RepoMeshCoordinatorConfig;
155
+ meshHost?: RepoMeshHostMetadata;
115
156
  }
116
157
 
117
158
  export function createMesh(opts: CreateMeshOptions): LocalMeshEntry {
@@ -133,6 +174,7 @@ export function createMesh(opts: CreateMeshOptions): LocalMeshEntry {
133
174
  defaultBranch: opts.defaultBranch,
134
175
  policy: mergeMeshPolicy(undefined, opts.policy),
135
176
  coordinator: opts.coordinator || {},
177
+ meshHost: opts.meshHost || createDefaultMeshHostMetadata(),
136
178
  nodes: [],
137
179
  createdAt: now,
138
180
  updatedAt: now,
@@ -148,6 +190,7 @@ export interface UpdateMeshOptions {
148
190
  defaultBranch?: string;
149
191
  policy?: Partial<RepoMeshPolicy>;
150
192
  coordinator?: RepoMeshCoordinatorConfig;
193
+ meshHost?: RepoMeshHostMetadata;
151
194
  }
152
195
 
153
196
  export function updateMesh(meshId: string, opts: UpdateMeshOptions): LocalMeshEntry | undefined {
@@ -159,6 +202,7 @@ export function updateMesh(meshId: string, opts: UpdateMeshOptions): LocalMeshEn
159
202
  if (opts.defaultBranch !== undefined) mesh.defaultBranch = opts.defaultBranch;
160
203
  if (opts.policy) mesh.policy = mergeMeshPolicy(mesh.policy, opts.policy);
161
204
  if (opts.coordinator) mesh.coordinator = opts.coordinator;
205
+ if (opts.meshHost) mesh.meshHost = opts.meshHost;
162
206
  mesh.updatedAt = new Date().toISOString();
163
207
 
164
208
  saveMeshConfig(config);
@@ -174,6 +218,240 @@ export function deleteMesh(meshId: string): boolean {
174
218
  return true;
175
219
  }
176
220
 
221
+ function normalizeManualHostAddress(hostAddress: string): string {
222
+ const normalized = hostAddress.trim().replace(/\/+$/, '');
223
+ if (!normalized) throw new Error('hostAddress required');
224
+ let parsed: URL;
225
+ try {
226
+ parsed = new URL(normalized);
227
+ } catch {
228
+ throw new Error('hostAddress must be a valid http(s) or ws(s) URL');
229
+ }
230
+ if (!['http:', 'https:', 'ws:', 'wss:'].includes(parsed.protocol)) {
231
+ throw new Error('hostAddress must use http, https, ws, or wss');
232
+ }
233
+ return normalized;
234
+ }
235
+
236
+ export function tokenIdForManualPairing(token: string): string {
237
+ return `tok_${createHash('sha256').update(token).digest('hex').slice(0, 16)}`;
238
+ }
239
+
240
+ function normalizeTokenExpiry(value: unknown): string | undefined {
241
+ if (typeof value !== 'string' || !value.trim()) return undefined;
242
+ const date = new Date(value);
243
+ if (Number.isNaN(date.getTime())) throw new Error('expiresAt must be a valid ISO date');
244
+ return date.toISOString();
245
+ }
246
+
247
+ function assertPairingTokenValid(pairing: RepoMeshHostMetadata['pairing'], rawToken: string, nowIso: string): { ok: true; tokenId: string } | { ok: false; reason: string; expectedTokenId?: string; presentedTokenId?: string } {
248
+ const token = rawToken.trim();
249
+ if (!token) return { ok: false, reason: 'token required' };
250
+ const presentedTokenId = tokenIdForManualPairing(token);
251
+ const expectedTokenId = pairing?.tokenId;
252
+ if (!expectedTokenId || pairing?.status === 'not_configured' || pairing?.status === 'revoked') {
253
+ return { ok: false, reason: 'host pairing token is not configured', presentedTokenId };
254
+ }
255
+ if (pairing.expiresAt && new Date(pairing.expiresAt).getTime() <= new Date(nowIso).getTime()) {
256
+ return { ok: false, reason: 'host pairing token expired', expectedTokenId, presentedTokenId };
257
+ }
258
+ if (presentedTokenId !== expectedTokenId) {
259
+ return { ok: false, reason: 'invalid pairing token', expectedTokenId, presentedTokenId };
260
+ }
261
+ return { ok: true, tokenId: presentedTokenId };
262
+ }
263
+
264
+ export interface ConfigureMeshHostPairingOptions {
265
+ hostAddress: string;
266
+ token: string;
267
+ now?: string;
268
+ }
269
+
270
+ export function configureMeshHostPairing(
271
+ meshId: string,
272
+ opts: ConfigureMeshHostPairingOptions,
273
+ ): { mesh: LocalMeshEntry; meshHost: RepoMeshHostMetadata; hostAddress: string } | undefined {
274
+ const hostAddress = normalizeManualHostAddress(opts.hostAddress);
275
+ const token = opts.token.trim();
276
+ if (!token) throw new Error('token required');
277
+
278
+ const config = loadMeshConfig();
279
+ const mesh = config.meshes.find(m => m.id === meshId);
280
+ if (!mesh) return undefined;
281
+
282
+ const now = opts.now || new Date().toISOString();
283
+ const previous = mesh.meshHost || createDefaultMeshHostMetadata();
284
+ const meshHost: RepoMeshHostMetadata = {
285
+ ...previous,
286
+ role: 'member',
287
+ hostAddress,
288
+ pairing: {
289
+ status: 'pairing',
290
+ tokenId: tokenIdForManualPairing(token),
291
+ lastPairedAt: now,
292
+ },
293
+ };
294
+
295
+ mesh.meshHost = meshHost;
296
+ mesh.updatedAt = now;
297
+ saveMeshConfig(config);
298
+ return { mesh, meshHost, hostAddress };
299
+ }
300
+
301
+ export interface CreateMeshHostPairingTokenOptions {
302
+ token?: string;
303
+ expiresAt?: string;
304
+ now?: string;
305
+ }
306
+
307
+ export function createMeshHostPairingToken(
308
+ meshId: string,
309
+ opts: CreateMeshHostPairingTokenOptions = {},
310
+ ): { mesh: LocalMeshEntry; meshHost: RepoMeshHostMetadata; token: string; tokenId: string; expiresAt?: string } | undefined {
311
+ const config = loadMeshConfig();
312
+ const mesh = config.meshes.find(m => m.id === meshId);
313
+ if (!mesh) return undefined;
314
+ const now = opts.now || new Date().toISOString();
315
+ const token = (opts.token || `mhj_${randomBytes(24).toString('base64url')}`).trim();
316
+ if (!token) throw new Error('token required');
317
+ const tokenId = tokenIdForManualPairing(token);
318
+ const expiresAt = normalizeTokenExpiry(opts.expiresAt);
319
+ const previous = mesh.meshHost || createDefaultMeshHostMetadata();
320
+ if (previous.role === 'member') {
321
+ throw new Error('Mesh Host daemon required to create host pairing tokens; member daemons cannot mint host join tokens.');
322
+ }
323
+ const meshHost: RepoMeshHostMetadata = {
324
+ ...previous,
325
+ role: 'host',
326
+ pairing: {
327
+ status: 'pairing',
328
+ tokenId,
329
+ lastPairedAt: now,
330
+ ...(expiresAt ? { expiresAt } : {}),
331
+ },
332
+ };
333
+ mesh.meshHost = meshHost;
334
+ mesh.updatedAt = now;
335
+ saveMeshConfig(config);
336
+ return { mesh, meshHost, token, tokenId, ...(expiresAt ? { expiresAt } : {}) };
337
+ }
338
+
339
+ export interface MeshHostJoinMemberNodeInput {
340
+ id?: string;
341
+ workspace: string;
342
+ repoRoot?: string;
343
+ daemonId?: string;
344
+ machineId?: string;
345
+ userOverrides?: Partial<RepoMeshNodeCapabilities>;
346
+ policy?: RepoMeshNodePolicy;
347
+ role?: RepoMeshDaemonRole;
348
+ }
349
+
350
+ export interface ApplyMeshHostJoinOptions {
351
+ token: string;
352
+ memberNode: MeshHostJoinMemberNodeInput;
353
+ memberMeshId?: string;
354
+ now?: string;
355
+ }
356
+
357
+ export function applyMeshHostJoinRequest(
358
+ meshId: string,
359
+ opts: ApplyMeshHostJoinOptions,
360
+ ): { accepted: true; mesh: LocalMeshEntry; meshHost: RepoMeshHostMetadata; node: LocalMeshNodeEntry; tokenId: string } | { accepted: false; mesh?: LocalMeshEntry; meshHost?: RepoMeshHostMetadata; tokenId?: string; reason: string } | undefined {
361
+ const config = loadMeshConfig();
362
+ const mesh = config.meshes.find(m => m.id === meshId);
363
+ if (!mesh) return undefined;
364
+ const now = opts.now || new Date().toISOString();
365
+ const previous = mesh.meshHost || createDefaultMeshHostMetadata();
366
+ if (previous.role === 'member') {
367
+ return { accepted: false, mesh, meshHost: previous, reason: 'Mesh Host daemon required to accept join requests' };
368
+ }
369
+ const meshHost: RepoMeshHostMetadata = { ...previous, role: 'host' };
370
+ const validation = assertPairingTokenValid(meshHost.pairing, opts.token, now);
371
+ if (!validation.ok) {
372
+ mesh.meshHost = {
373
+ ...meshHost,
374
+ pairing: {
375
+ ...(meshHost.pairing || { status: 'not_configured' as const }),
376
+ status: 'rejected',
377
+ lastRejectedAt: now,
378
+ },
379
+ };
380
+ mesh.updatedAt = now;
381
+ saveMeshConfig(config);
382
+ return { accepted: false, mesh, meshHost: mesh.meshHost, tokenId: validation.presentedTokenId, reason: validation.reason };
383
+ }
384
+
385
+ const workspace = opts.memberNode.workspace.trim();
386
+ if (!workspace) throw new Error('memberNode.workspace required');
387
+ const memberId = opts.memberNode.id?.trim();
388
+ let node = mesh.nodes.find(n => (memberId && n.id === memberId) || n.workspace === workspace);
389
+ if (node) {
390
+ node.workspace = workspace;
391
+ node.repoRoot = opts.memberNode.repoRoot;
392
+ node.daemonId = opts.memberNode.daemonId;
393
+ node.machineId = opts.memberNode.machineId;
394
+ node.userOverrides = opts.memberNode.userOverrides || node.userOverrides || {};
395
+ node.policy = { ...(node.policy || {}), ...(opts.memberNode.policy || {}) };
396
+ node.role = 'member';
397
+ } else {
398
+ if (mesh.nodes.length >= 10) throw new Error('Maximum 10 nodes per mesh');
399
+ node = {
400
+ id: memberId || `node_${randomUUID().replace(/-/g, '')}`,
401
+ workspace,
402
+ repoRoot: opts.memberNode.repoRoot,
403
+ daemonId: opts.memberNode.daemonId,
404
+ machineId: opts.memberNode.machineId,
405
+ userOverrides: opts.memberNode.userOverrides || {},
406
+ policy: opts.memberNode.policy || {},
407
+ role: 'member',
408
+ };
409
+ mesh.nodes.push(node);
410
+ }
411
+ mesh.meshHost = {
412
+ ...meshHost,
413
+ pairing: {
414
+ ...(meshHost.pairing || {}),
415
+ status: 'paired',
416
+ tokenId: validation.tokenId,
417
+ joinedAt: now,
418
+ lastPairedAt: meshHost.pairing?.lastPairedAt || now,
419
+ ...(meshHost.pairing?.expiresAt ? { expiresAt: meshHost.pairing.expiresAt } : {}),
420
+ },
421
+ };
422
+ mesh.updatedAt = now;
423
+ saveMeshConfig(config);
424
+ return { accepted: true, mesh, meshHost: mesh.meshHost, node, tokenId: validation.tokenId };
425
+ }
426
+
427
+ export function markMeshHostPairingJoined(
428
+ meshId: string,
429
+ opts: { hostDaemonId?: string; hostNodeId?: string; joinedAt?: string; token?: string; tokenId?: string },
430
+ ): { mesh: LocalMeshEntry; meshHost: RepoMeshHostMetadata } | undefined {
431
+ const config = loadMeshConfig();
432
+ const mesh = config.meshes.find(m => m.id === meshId);
433
+ if (!mesh) return undefined;
434
+ const now = opts.joinedAt || new Date().toISOString();
435
+ const previous = mesh.meshHost || createDefaultMeshHostMetadata();
436
+ const tokenId = opts.tokenId || (opts.token ? tokenIdForManualPairing(opts.token) : previous.pairing?.tokenId);
437
+ mesh.meshHost = {
438
+ ...previous,
439
+ role: 'member',
440
+ ...(opts.hostDaemonId ? { hostDaemonId: opts.hostDaemonId } : {}),
441
+ ...(opts.hostNodeId ? { hostNodeId: opts.hostNodeId } : {}),
442
+ pairing: {
443
+ ...(previous.pairing || {}),
444
+ status: 'paired',
445
+ ...(tokenId ? { tokenId } : {}),
446
+ joinedAt: now,
447
+ lastPairedAt: previous.pairing?.lastPairedAt || now,
448
+ },
449
+ };
450
+ mesh.updatedAt = now;
451
+ saveMeshConfig(config);
452
+ return { mesh, meshHost: mesh.meshHost };
453
+ }
454
+
177
455
  // ─── Node Operations ────────────────────────────
178
456
 
179
457
  export interface AddNodeOptions {
@@ -181,11 +459,14 @@ export interface AddNodeOptions {
181
459
  repoRoot?: string;
182
460
  daemonId?: string;
183
461
  machineId?: string;
462
+ capabilities?: string[];
184
463
  userOverrides?: Partial<RepoMeshNodeCapabilities>;
185
464
  policy?: RepoMeshNodePolicy;
186
465
  isLocalWorktree?: boolean;
187
466
  worktreeBranch?: string;
188
467
  clonedFromNodeId?: string;
468
+ worktreeBootstrap?: LocalMeshNodeEntry['worktreeBootstrap'];
469
+ role?: RepoMeshDaemonRole;
189
470
  }
190
471
 
191
472
  export function addNode(meshId: string, opts: AddNodeOptions): LocalMeshNodeEntry | undefined {
@@ -208,11 +489,14 @@ export function addNode(meshId: string, opts: AddNodeOptions): LocalMeshNodeEntr
208
489
  repoRoot: opts.repoRoot,
209
490
  daemonId: opts.daemonId,
210
491
  machineId: opts.machineId,
492
+ capabilities: normalizeCapabilityTags(opts.capabilities),
211
493
  userOverrides: opts.userOverrides || {},
212
494
  policy: opts.policy || {},
213
495
  isLocalWorktree: opts.isLocalWorktree,
214
496
  worktreeBranch: opts.worktreeBranch,
215
497
  clonedFromNodeId: opts.clonedFromNodeId,
498
+ worktreeBootstrap: opts.worktreeBootstrap,
499
+ role: opts.role,
216
500
  };
217
501
 
218
502
  mesh.nodes.push(node);
@@ -238,7 +522,14 @@ export function removeNode(meshId: string, nodeId: string): boolean {
238
522
  export function updateNode(
239
523
  meshId: string,
240
524
  nodeId: string,
241
- opts: { userOverrides?: Partial<RepoMeshNodeCapabilities>; policy?: RepoMeshNodePolicy },
525
+ opts: {
526
+ userOverrides?: Partial<RepoMeshNodeCapabilities>;
527
+ policy?: RepoMeshNodePolicy;
528
+ worktreeBootstrap?: LocalMeshNodeEntry['worktreeBootstrap'];
529
+ /** Per-node instruction surfaced in the coordinator prompt. Pass an
530
+ * empty string or undefined to clear it. */
531
+ systemPrompt?: string;
532
+ },
242
533
  ): LocalMeshNodeEntry | undefined {
243
534
  const config = loadMeshConfig();
244
535
  const mesh = config.meshes.find(m => m.id === meshId);
@@ -249,6 +540,15 @@ export function updateNode(
249
540
 
250
541
  if (opts.userOverrides) node.userOverrides = { ...node.userOverrides, ...opts.userOverrides };
251
542
  if (opts.policy) node.policy = { ...node.policy, ...opts.policy };
543
+ if (opts.worktreeBootstrap) node.worktreeBootstrap = opts.worktreeBootstrap;
544
+ if (Object.prototype.hasOwnProperty.call(opts, 'systemPrompt')) {
545
+ // Honor explicit clears: { systemPrompt: undefined } drops the field.
546
+ if (opts.systemPrompt && opts.systemPrompt.trim()) {
547
+ node.systemPrompt = opts.systemPrompt;
548
+ } else {
549
+ delete node.systemPrompt;
550
+ }
551
+ }
252
552
  mesh.updatedAt = new Date().toISOString();
253
553
  saveMeshConfig(config);
254
554
  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}` });
@@ -7,8 +7,10 @@
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';
@@ -73,25 +75,33 @@ function findCliCommand(command: string): string | null {
73
75
  const resolved = path.isAbsolute(candidate) ? candidate : path.resolve(candidate);
74
76
  return existsSync(resolved) ? resolved : null;
75
77
  }
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;
78
+ const isWin = platform() === 'win32';
79
+ const paths = (process.env.PATH || '').split(isWin ? ';' : ':');
80
+ const exes = isWin ? ['.exe', '.cmd', '.bat', ''] : [''];
81
+ for (const p of paths) {
82
+ if (!p) continue;
83
+ for (const ext of exes) {
84
+ const fullPath = path.join(p, trimmed + ext);
85
+ try {
86
+ if (existsSync(fullPath)) {
87
+ const stat = statSync(fullPath);
88
+ if (stat.isFile() && (isWin || (stat.mode & 0o111))) {
89
+ return fullPath;
90
+ }
91
+ }
92
+ } catch { }
93
+ }
84
94
  }
95
+ return null;
85
96
  }
86
97
 
87
- function getIdeVersion(cliCommand: string): string | null {
98
+ async function getIdeVersion(cliCommand: string): Promise<string | null> {
88
99
  try {
89
- const result = execSync(`"${cliCommand}" --version`, {
100
+ const { stdout } = await execAsync(`"${cliCommand}" --version`, {
90
101
  encoding: 'utf-8',
91
102
  timeout: 10000,
92
- stdio: ['pipe', 'pipe', 'pipe'],
93
- }).trim();
94
- return result.split('\n')[0] || null;
103
+ });
104
+ return stdout.trim().split('\n')[0] || null;
95
105
  } catch {
96
106
  return null;
97
107
  }
@@ -152,7 +162,7 @@ export async function detectIDEs(providerLoader?: ProviderLoader): Promise<IDEIn
152
162
  const installed = os === 'darwin'
153
163
  ? !!(resolvedCli || appPath)
154
164
  : !!resolvedCli;
155
- const version = resolvedCli ? getIdeVersion(resolvedCli) : null;
165
+ const version = resolvedCli ? await getIdeVersion(resolvedCli) : null;
156
166
 
157
167
  results.push({
158
168
  id: def.id,
@@ -42,8 +42,12 @@ export interface GitLogResult extends GitRepoIdentity {
42
42
  }
43
43
 
44
44
  export interface GitCheckpointResult extends GitRepoIdentity {
45
- commit: string;
45
+ commit?: string;
46
46
  message: string;
47
+ status?: 'created' | 'skipped';
48
+ skipped?: boolean;
49
+ noop?: boolean;
50
+ reason?: 'nothing_to_commit';
47
51
  lastCheckedAt: number;
48
52
  }
49
53
 
@@ -62,9 +66,9 @@ export interface GitPushResult extends GitRepoIdentity {
62
66
  }
63
67
 
64
68
  export interface GitCommandServices {
65
- getStatus?: (params: { workspace: string; refreshUpstream?: boolean }) => Promise<GitRepoStatus> | GitRepoStatus;
66
- getDiffSummary?: (params: { workspace: string; staged?: boolean }) => Promise<GitDiffSummary> | GitDiffSummary;
67
- getDiffFile?: (params: { workspace: string; path: string; staged?: boolean }) => Promise<GitFileDiff> | GitFileDiff;
69
+ getStatus?: (params: { workspace: string; refreshUpstream?: boolean; includeSubmodules?: boolean; submoduleIgnorePaths?: string[] }) => Promise<GitRepoStatus> | GitRepoStatus;
70
+ getDiffSummary?: (params: { workspace: string; staged?: boolean; base?: string }) => Promise<GitDiffSummary> | GitDiffSummary;
71
+ getDiffFile?: (params: { workspace: string; path: string; staged?: boolean; base?: string }) => Promise<GitFileDiff> | GitFileDiff;
68
72
  createSnapshot?: (params: {
69
73
  workspace: string;
70
74
  reason: GitSnapshotReason;
@@ -172,8 +176,8 @@ const defaultSnapshotStore = createGitSnapshotStore({
172
176
  export function createDefaultGitCommandServices(): GitCommandServices {
173
177
  return {
174
178
  getStatus: ({ workspace, refreshUpstream }) => getGitRepoStatus(workspace, { refreshUpstream }),
175
- getDiffSummary: ({ workspace }) => getGitDiffSummary(workspace),
176
- getDiffFile: ({ workspace, path: filePath }) => getGitFileDiff(workspace, filePath),
179
+ getDiffSummary: ({ workspace, base }) => getGitDiffSummary(workspace, base ? { baseRef: base } : {}),
180
+ getDiffFile: ({ workspace, path: filePath, base }) => getGitFileDiff(workspace, filePath, base ? { baseRef: base } : {}),
177
181
  createSnapshot: ({ workspace, reason, sessionId, turnId }) => defaultSnapshotStore.create({
178
182
  workspace,
179
183
  reason,
@@ -290,13 +294,22 @@ export async function handleGitCommand(
290
294
  switch (command) {
291
295
  case 'git_status': {
292
296
  if (!services.getStatus) return serviceNotImplemented(command);
293
- const status = await runService(() => services.getStatus!({ workspace, refreshUpstream: optionalBoolean(args?.refreshUpstream) }));
297
+ const submoduleIgnorePaths = Array.isArray(args?.submoduleIgnorePaths)
298
+ ? args.submoduleIgnorePaths.filter((value: unknown): value is string => typeof value === 'string' && value.trim().length > 0)
299
+ : undefined;
300
+ const statusParams: { workspace: string; refreshUpstream?: boolean; includeSubmodules?: boolean; submoduleIgnorePaths?: string[] } = { workspace };
301
+ const refreshUpstream = optionalBoolean(args?.refreshUpstream);
302
+ const includeSubmodules = optionalBoolean(args?.includeSubmodules);
303
+ if (refreshUpstream !== undefined) statusParams.refreshUpstream = refreshUpstream;
304
+ if (includeSubmodules !== undefined) statusParams.includeSubmodules = includeSubmodules;
305
+ if (submoduleIgnorePaths && submoduleIgnorePaths.length > 0) statusParams.submoduleIgnorePaths = submoduleIgnorePaths;
306
+ const status = await runService(() => services.getStatus!(statusParams));
294
307
  return 'success' in status ? status : { success: true, status };
295
308
  }
296
309
 
297
310
  case 'git_diff_summary': {
298
311
  if (!services.getDiffSummary) return serviceNotImplemented(command);
299
- const diffSummary = await runService(() => services.getDiffSummary!({ workspace, staged: optionalBoolean(args?.staged) }));
312
+ const diffSummary = await runService(() => services.getDiffSummary!({ workspace, staged: optionalBoolean(args?.staged), base: optionalString(args?.base) }));
300
313
  return 'success' in diffSummary ? diffSummary : { success: true, diffSummary };
301
314
  }
302
315
 
@@ -442,6 +455,15 @@ async function gitCheckpoint(
442
455
  if (statusResult.hasConflicts) {
443
456
  throw new GitCommandError('conflict', 'Repository has conflicts — resolve before checkpointing');
444
457
  }
458
+ const dirtySubmodules = (statusResult.submodules || []).filter(submodule => submodule.dirty);
459
+ if (dirtySubmodules.length > 0) {
460
+ const paths = dirtySubmodules.map(submodule => submodule.path).join(', ');
461
+ throw new GitCommandError(
462
+ 'dirty_index_required',
463
+ `Repository has dirty submodules that must be checkpointed first: ${paths}. ` +
464
+ 'Checkpoint or commit each dirty submodule, then checkpoint this repository to record gitlink changes.',
465
+ );
466
+ }
445
467
 
446
468
  const addArgs = includeUntracked ? ['-A'] : ['-u'];
447
469
  await runGit(repo, ['add', ...addArgs], { cwd: repoRoot });
@@ -455,10 +477,17 @@ async function gitCheckpoint(
455
477
  } catch (err: any) {
456
478
  const output = (err?.stdout || '') + (err?.stderr || '');
457
479
  if (/nothing to commit/i.test(output)) {
458
- throw new GitCommandError('nothing_to_commit', 'Nothing to commit — working tree is clean.', {
459
- stdout: err?.stdout,
460
- stderr: err?.stderr,
461
- });
480
+ return {
481
+ workspace: repo.workspace,
482
+ repoRoot,
483
+ isGitRepo: true,
484
+ message: fullMsg,
485
+ status: 'skipped',
486
+ skipped: true,
487
+ noop: true,
488
+ reason: 'nothing_to_commit',
489
+ lastCheckedAt: Date.now(),
490
+ };
462
491
  }
463
492
  throw err;
464
493
  }
@@ -469,6 +498,7 @@ async function gitCheckpoint(
469
498
  isGitRepo: true,
470
499
  commit: commitSha,
471
500
  message: fullMsg,
501
+ status: 'created',
472
502
  lastCheckedAt: Date.now(),
473
503
  };
474
504
  }