@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
@@ -1,8 +1,31 @@
1
- import type { GitRepoStatus, GitSubmoduleStatus, GitUpstreamFreshness } from './git-types.js';
2
- import { GitCommandError, resolveGitRepository, runGit } from './git-executor.js';
1
+ import type { DaemonBuildBehind, GitRepoStatus, GitSubmoduleStatus, GitUpstreamFreshness } from './git-types.js';
2
+ import { GIT_STATUS_TIMEOUT_MS, GitCommandError, resolveGitRepository, runGit } from './git-executor.js';
3
+ import { getDaemonBuildInfo, type DaemonBuildInfo } from '../build-info.js';
3
4
 
4
5
  type ResolvedGitRepo = { workspace: string; repoRoot: string | null; isGitRepo: boolean };
5
6
 
7
+ /**
8
+ * Last successfully-collected status per workspace, used to survive a transient git
9
+ * failure (timeout, slow Windows spawn under load, a momentary lock) WITHOUT dropping
10
+ * the node out of the mesh graph. A genuine "not a git repository" answer is NOT a
11
+ * transient failure — it never populates this cache and always reports isGitRepo:false.
12
+ */
13
+ const lastKnownGoodStatus = new Map<string, GitRepoStatus>();
14
+
15
+ /** Test seam: clear the last-known-good status cache between cases. */
16
+ export function __resetGitStatusCacheForTests(): void {
17
+ lastKnownGoodStatus.clear();
18
+ }
19
+
20
+ /**
21
+ * git failure reasons that are transient/environmental rather than a real statement
22
+ * that the workspace is not a repo. On these we prefer the last-known-good status so a
23
+ * single slow git call cannot make a healthy node vanish from the graph.
24
+ */
25
+ function isTransientGitFailure(error: GitCommandError): boolean {
26
+ return error.reason === 'timeout' || error.reason === 'git_command_failed';
27
+ }
28
+
6
29
  export interface GitStatusOptions {
7
30
  timeoutMs?: number;
8
31
  /** When true, include submodule status in the result. Defaults to true. */
@@ -14,6 +37,12 @@ export interface GitStatusOptions {
14
37
  * Callers should opt into this only for convergence-critical surfaces.
15
38
  */
16
39
  refreshUpstream?: boolean;
40
+ /**
41
+ * Test/override seam for the daemon build stamp used by the stale-build
42
+ * detector. Production callers omit this so the real baked-in build commit
43
+ * (getDaemonBuildInfo) is used.
44
+ */
45
+ daemonBuildInfo?: DaemonBuildInfo;
17
46
  }
18
47
 
19
48
  interface GitUpstreamProbe {
@@ -28,9 +57,50 @@ export async function getGitRepoStatus(
28
57
  ): Promise<GitRepoStatus> {
29
58
  const lastCheckedAt = Date.now();
30
59
  const includeSubmodules = options.includeSubmodules !== false;
60
+ // Status collection fans out into several git subprocesses (status, head, stash,
61
+ // submodule, optionally fetch). On Windows the per-spawn cost alone can exceed the
62
+ // 5s default, so unless the caller pinned a timeout, give the whole collection path
63
+ // the larger status budget. A caller that explicitly sets timeoutMs (e.g. a test
64
+ // injecting 1ms to exercise the transient-failure path) is respected.
65
+ const effectiveOptions: GitStatusOptions =
66
+ options.timeoutMs === undefined ? { ...options, timeoutMs: GIT_STATUS_TIMEOUT_MS } : options;
31
67
 
32
68
  try {
33
- const repo = await resolveGitRepository(workspace, options);
69
+ const repo = await resolveGitRepository(workspace, effectiveOptions);
70
+ const status = await collectGitRepoStatus(repo, includeSubmodules, lastCheckedAt, effectiveOptions);
71
+ lastKnownGoodStatus.set(workspace, status);
72
+ return status;
73
+ } catch (error) {
74
+ const gitError = error instanceof GitCommandError
75
+ ? error
76
+ : new GitCommandError('git_command_failed', 'Failed to read Git status', { cause: error });
77
+
78
+ // A transient/environmental failure (timeout, slow-spawn-under-load) must NOT make
79
+ // a healthy node lose its repo identity and drop out of the mesh graph. Prefer the
80
+ // last status we successfully collected for this workspace, re-stamped as stale.
81
+ if (isTransientGitFailure(gitError)) {
82
+ const cached = lastKnownGoodStatus.get(workspace);
83
+ if (cached) {
84
+ return {
85
+ ...cached,
86
+ lastCheckedAt,
87
+ upstreamStatus: 'unavailable',
88
+ error: gitError.stderr || gitError.message,
89
+ reason: gitError.reason,
90
+ };
91
+ }
92
+ }
93
+
94
+ return emptyStatus(workspace, lastCheckedAt, gitError);
95
+ }
96
+ }
97
+
98
+ async function collectGitRepoStatus(
99
+ repo: ResolvedGitRepo,
100
+ includeSubmodules: boolean,
101
+ lastCheckedAt: number,
102
+ options: GitStatusOptions,
103
+ ): Promise<GitRepoStatus> {
34
104
  let parsed = await readPorcelainStatus(repo, options);
35
105
  let upstreamProbe: GitUpstreamProbe = getInitialUpstreamProbe(parsed);
36
106
 
@@ -48,6 +118,13 @@ export async function getGitRepoStatus(
48
118
  if (includeSubmodules) {
49
119
  submodules = await getSubmoduleStatuses(repo, options);
50
120
  }
121
+ const submoduleDirty = (submodules || []).some(submodule => submodule.dirty || submodule.outOfSync || !!submodule.error);
122
+ const dirty = parsed.staged + parsed.modified + parsed.untracked + parsed.deleted + parsed.renamed > 0
123
+ || parsed.conflictFiles.length > 0
124
+ || stashCount > 0
125
+ || submoduleDirty;
126
+
127
+ const daemonBuildBehind = await detectDaemonBuildBehind(repo, submodules, options);
51
128
 
52
129
  return {
53
130
  workspace: repo.workspace,
@@ -67,22 +144,201 @@ export async function getGitRepoStatus(
67
144
  untracked: parsed.untracked,
68
145
  deleted: parsed.deleted,
69
146
  renamed: parsed.renamed,
147
+ dirty,
70
148
  hasConflicts: parsed.conflictFiles.length > 0,
71
149
  conflictFiles: parsed.conflictFiles,
72
150
  stashCount,
73
151
  lastCheckedAt,
74
152
  submodules,
153
+ ...(daemonBuildBehind ? { daemonBuildBehind } : {}),
75
154
  };
76
- } catch (error) {
77
- if (error instanceof GitCommandError) {
78
- return emptyStatus(workspace, lastCheckedAt, error);
155
+ }
156
+
157
+ /**
158
+ * Detect whether the running daemon's build commit is a STRICT ancestor of this
159
+ * workspace's HEAD (root) or any of its submodules' HEAD. This surfaces the
160
+ * "merged a fix to main but the live daemon still ships the old bundle" gap:
161
+ * once the fix is committed, the workspace HEAD advances past the daemon's
162
+ * baked-in build commit, but the daemon keeps the old behavior until it is
163
+ * rebuilt/redeployed and restarted.
164
+ *
165
+ * Conservative by construction — returns undefined unless ancestry is provable:
166
+ * - build commit unknown → undefined
167
+ * - build commit not an object in this repo/submodule (different repo) → skip
168
+ * - build commit === HEAD (daemon is current) → undefined
169
+ * - build commit NOT an ancestor of HEAD (daemon ahead / diverged) → undefined
170
+ * Any git error is swallowed (no warning) so a flaky probe never over-warns.
171
+ */
172
+ /**
173
+ * Package names that, when changed, mean the daemon runtime is stale and must be
174
+ * rebuilt/redeployed + restarted. Everything NOT in this set (web-core,
175
+ * web-standalone, web-devconsole, terminal-render-web) is web-only — a daemon
176
+ * restart is not required for those, only a web redeploy. mcp-server runs in the
177
+ * same process surface as the daemon tooling, so it is classified as
178
+ * daemon-affecting (conservative). Unknown package → daemon-affecting.
179
+ */
180
+ const DAEMON_RUNTIME_PACKAGES = new Set([
181
+ 'daemon-core',
182
+ 'daemon-standalone',
183
+ 'session-host-core',
184
+ 'session-host-daemon',
185
+ 'terminal-mux-core',
186
+ 'terminal-mux-control',
187
+ 'terminal-mux-cli',
188
+ 'ghostty-vt-node',
189
+ 'mcp-server',
190
+ ]);
191
+
192
+ const WEB_ONLY_PACKAGES = new Set([
193
+ 'web-core',
194
+ 'web-standalone',
195
+ 'web-devconsole',
196
+ 'terminal-render-web',
197
+ ]);
198
+
199
+ /**
200
+ * Root-level (non-package) files that demonstrably cannot change what the daemon
201
+ * runtime executes: convergence/verify markers, documentation, and license/notice
202
+ * text. A root commit that moves the oss gitlink while the oss commit only touched
203
+ * one of these is NOT a reason to rebuild/restart the daemon — flagging it produces
204
+ * the staleDaemonBuild false-positive this guard exists to suppress.
205
+ *
206
+ * Deliberately conservative: anything NOT matched here (root config like
207
+ * package.json / tsconfig / build scripts, `.txt` fixtures, lockfiles, unknown
208
+ * dotfiles) stays daemon-affecting, because a false-negative — staying silent when
209
+ * the daemon really IS stale — is worse than an over-warn. Markers are the common
210
+ * real-world case (e.g. `.verify-patch-equiv-rc292`), so they are matched broadly;
211
+ * docs are matched by the `docs/` prefix or a markdown/text-doc extension at a
212
+ * filename we recognize as documentation (README/CHANGELOG/LICENSE/NOTICE).
213
+ */
214
+ function isNonRuntimeRootFile(file: string): boolean {
215
+ const base = file.slice(file.lastIndexOf('/') + 1);
216
+ // Verify/convergence markers: dotfiles whose name signals a transient marker.
217
+ if (/^\.(?:verify|marker|converge|ff-verify|patch-equiv|live-verify)\b/i.test(base)) return true;
218
+ // Documentation living under a docs/ tree (any depth, root or nested).
219
+ if (/(?:^|\/)docs\//i.test(file)) return true;
220
+ // Recognized top-level documentation / license files.
221
+ if (/^(?:README|CHANGELOG|LICENSE|NOTICE|AUTHORS|CONTRIBUTING|CODEOWNERS)(?:\.[A-Za-z0-9]+)?$/i.test(base)) {
222
+ return true;
223
+ }
224
+ return false;
225
+ }
226
+
227
+ /**
228
+ * Determine whether the changes between buildCommit..HEAD touch any daemon-runtime
229
+ * package. Returns isDaemonAffecting:true conservatively when the changed-file set
230
+ * can't be obtained or any changed path is outside the known web-only package set
231
+ * AND is not a recognized non-runtime root file (marker/doc/license).
232
+ */
233
+ async function classifyDaemonBuildChange(
234
+ repoPath: string,
235
+ buildCommit: string,
236
+ options: GitStatusOptions,
237
+ ): Promise<{ isDaemonAffecting: boolean; affectedPackages: string[] }> {
238
+ try {
239
+ const diff = await runGit(repoPath, ['diff', '--name-only', `${buildCommit}..HEAD`], options);
240
+ const files = diff.stdout
241
+ .split('\n')
242
+ .map((line) => line.trim())
243
+ .filter(Boolean);
244
+ if (files.length === 0) {
245
+ // No file diff (e.g. only merge metadata) — nothing actionable, but stay
246
+ // conservative and treat as daemon-affecting so we don't suppress a real warning.
247
+ return { isDaemonAffecting: true, affectedPackages: [] };
248
+ }
249
+ const pkgs = new Set<string>();
250
+ // A non-package path that is NOT a recognized benign root file (marker/doc).
251
+ // Only these force daemon-affecting; benign markers/docs are ignored so a
252
+ // gitlink-moving root commit over a marker-only oss commit no longer over-warns.
253
+ let sawRuntimeAmbiguousNonPackage = false;
254
+ for (const file of files) {
255
+ const match = file.match(/(?:^|\/)packages\/([^/]+)\//);
256
+ if (!match) {
257
+ if (!isNonRuntimeRootFile(file)) sawRuntimeAmbiguousNonPackage = true;
258
+ continue;
259
+ }
260
+ pkgs.add(match[1]);
261
+ }
262
+ const affectedPackages = [...pkgs].sort();
263
+ // Daemon-affecting if: any runtime-ambiguous non-package file changed, any
264
+ // unknown package changed, or any explicit daemon-runtime package changed.
265
+ // The daemon is unaffected only when every changed file is either a known
266
+ // web-only package or a recognized benign root file (and at least one such
267
+ // file changed) — i.e. nothing runtime-ambiguous remains.
268
+ const allBenign =
269
+ !sawRuntimeAmbiguousNonPackage &&
270
+ affectedPackages.every((p) => WEB_ONLY_PACKAGES.has(p) && !DAEMON_RUNTIME_PACKAGES.has(p));
271
+ return { isDaemonAffecting: !allBenign, affectedPackages };
272
+ } catch {
273
+ // diff probe failed → can't prove web-only; stay conservative.
274
+ return { isDaemonAffecting: true, affectedPackages: [] };
275
+ }
276
+ }
277
+
278
+ async function detectDaemonBuildBehind(
279
+ repo: ResolvedGitRepo,
280
+ submodules: GitSubmoduleStatus[] | undefined,
281
+ options: GitStatusOptions,
282
+ ): Promise<DaemonBuildBehind | undefined> {
283
+ const build = options.daemonBuildInfo ?? getDaemonBuildInfo();
284
+ if (!build.commit || build.commit === 'unknown') return undefined;
285
+
286
+ // Check the root repo first, then each submodule. The daemon build commit is
287
+ // baked from the daemon-core (oss submodule) HEAD, so on an adhdev
288
+ // superproject worktree the match is expected on the `oss` submodule, not the
289
+ // root — checking both keeps the helper repo-agnostic.
290
+ const scopes: Array<{ scope: string; repoPath: string }> = [
291
+ { scope: 'root', repoPath: repo.repoRoot || repo.workspace },
292
+ ];
293
+ for (const sub of submodules || []) {
294
+ if (sub.repoPath && !sub.error) scopes.push({ scope: sub.path, repoPath: sub.repoPath });
295
+ }
296
+
297
+ for (const { scope, repoPath } of scopes) {
298
+ try {
299
+ // Build commit must be a real object in THIS repo, else it's a different repo.
300
+ await runGit(repoPath, ['cat-file', '-e', `${build.commit}^{commit}`], options);
301
+ const headResult = await runGit(repoPath, ['rev-parse', 'HEAD'], options);
302
+ const head = headResult.stdout.trim();
303
+ if (!head || head === build.commit) continue;
304
+ // Strict ancestor: build commit is reachable from HEAD but is not HEAD.
305
+ await runGit(repoPath, ['merge-base', '--is-ancestor', build.commit, 'HEAD'], options);
306
+ // No throw → build commit IS an ancestor of HEAD → daemon is behind.
307
+ // Inspect WHICH packages changed in buildCommit..HEAD. A daemon rebuild/restart
308
+ // is only actually required when a daemon-runtime package changed; if only web /
309
+ // render packages changed, the daemon is unaffected and just the web deploy is
310
+ // pending. Conservative: any probe failure → treat as daemon-affecting.
311
+ const { isDaemonAffecting, affectedPackages } = await classifyDaemonBuildChange(
312
+ repoPath,
313
+ build.commit,
314
+ options,
315
+ );
316
+ const scopeLabel = scope === 'root' ? 'workspace' : scope;
317
+ const benignDetail = affectedPackages.length > 0
318
+ ? `only web packages changed (${affectedPackages.join(', ')})`
319
+ : 'only non-runtime files changed (markers/docs)';
320
+ const warning = isDaemonAffecting
321
+ ? `Live daemon was built from ${build.commitShort} which is behind ${scopeLabel} HEAD ${head.slice(0, 7)}. ` +
322
+ `Merged code is NOT live until the daemon is rebuilt/redeployed and restarted — a local dist rebuild alone does not update a cloud daemon.`
323
+ : `Live daemon was built from ${build.commitShort} which is behind ${scopeLabel} HEAD ${head.slice(0, 7)}, ` +
324
+ `but ${benignDetail}. ` +
325
+ `Daemon restart NOT required — redeploy the web app to reflect the change.`;
326
+ return {
327
+ buildCommit: build.commit,
328
+ buildCommitShort: build.commitShort,
329
+ head,
330
+ scope,
331
+ isDaemonAffecting,
332
+ ...(affectedPackages && affectedPackages.length > 0 ? { affectedPackages } : {}),
333
+ warning,
334
+ };
335
+ } catch {
336
+ // cat-file / merge-base non-zero exit (commit absent or not an ancestor)
337
+ // or any git error → not a provable staleness for this scope; try next.
338
+ continue;
79
339
  }
80
- return emptyStatus(
81
- workspace,
82
- lastCheckedAt,
83
- new GitCommandError('git_command_failed', 'Failed to read Git status', { cause: error }),
84
- );
85
340
  }
341
+ return undefined;
86
342
  }
87
343
 
88
344
  interface ParsedPorcelainStatus {
@@ -285,6 +541,7 @@ function emptyStatus(workspace: string, lastCheckedAt: number, error: GitCommand
285
541
  untracked: 0,
286
542
  deleted: 0,
287
543
  renamed: 0,
544
+ dirty: false,
288
545
  hasConflicts: false,
289
546
  conflictFiles: [],
290
547
  stashCount: 0,
@@ -303,44 +560,152 @@ async function getSubmoduleStatuses(
303
560
  if (!repo.repoRoot) return [];
304
561
 
305
562
  try {
306
- const result = await runGit(repo, ['submodule', 'status', '--recursive'], options);
307
- return parseSubmoduleStatusOutput(result.stdout, repo.repoRoot, options.submoduleIgnorePaths);
563
+ // Do NOT shell out to `git submodule status`. That porcelain wrapper is a
564
+ // shell script (`git-submodule`) that, per submodule, spawns several child
565
+ // `git` processes; on Windows the wrapper + per-spawn cost alone measured
566
+ // 6.9–62.4s under AV, which dominated the whole collectGitRepoStatus budget
567
+ // and stalled the mesh graph cold-open. The information it gives us — the
568
+ // gitlink sync state (path / recorded SHA / +/-/U prefix) — is fully
569
+ // derivable from plumbing commands that don't go through the shell wrapper:
570
+ // • paths ← `.gitmodules` (git config --file, plumbing)
571
+ // • expected SHA ← `git ls-tree HEAD <path>` (the gitlink the super-
572
+ // project's HEAD tree records)
573
+ // • actual SHA ← `git -C <sub> rev-parse HEAD` (already paid below by
574
+ // enrichSubmoduleWorktreeStatus for the dirty check)
575
+ // Comparing expected vs actual reproduces `+` (out of sync); a checked-out
576
+ // submodule whose worktree is absent/uninitialized reproduces `-`. The `U`
577
+ // (conflict) prefix is surfaced separately via the superproject porcelain
578
+ // status that the caller already parses, and a conflicted submodule's own
579
+ // status read here also flags it dirty — so no row is lost.
580
+ const submodules = await deriveSubmoduleGitlinkStatuses(repo, options);
581
+ await Promise.all(submodules.map(submodule => enrichSubmoduleWorktreeStatus(repo, submodule, options)));
582
+ return submodules;
308
583
  } catch {
309
584
  return [];
310
585
  }
311
586
  }
312
587
 
313
- function parseSubmoduleStatusOutput(
314
- output: string,
315
- repoRoot: string,
316
- ignorePaths?: string[],
317
- ): GitSubmoduleStatus[] {
318
- const submodules: GitSubmoduleStatus[] = [];
319
- const ignoreSet = new Set(ignorePaths || []);
588
+ /**
589
+ * Enumerate the superproject's submodules and their gitlink sync state without the
590
+ * slow `git submodule status` shell wrapper. Pure plumbing: read paths from
591
+ * `.gitmodules`, the expected (recorded) gitlink SHA from `ls-tree HEAD`, and the
592
+ * actual checked-out SHA from the submodule's own `rev-parse HEAD`.
593
+ */
594
+ async function deriveSubmoduleGitlinkStatuses(
595
+ repo: ResolvedGitRepo,
596
+ options: GitStatusOptions,
597
+ ): Promise<GitSubmoduleStatus[]> {
598
+ if (!repo.repoRoot) return [];
599
+ const paths = await readSubmodulePaths(repo, options);
600
+ const ignoreSet = new Set(options.submoduleIgnorePaths || []);
601
+ const lastCheckedAt = Date.now();
320
602
 
321
- for (const line of output.split('\n')) {
322
- if (!line.trim()) continue;
323
-
324
- // Format: [+- ]<commit> <path> (<branch>)
325
- // - = out of sync, + = dirty, ' ' = clean
326
- const match = line.match(/^([\-+\s])([0-9a-f]{40})\s+(\S+)(?:\s+\(([^)]+)\))?/);
327
- if (!match) continue;
328
-
329
- const prefix = match[1];
330
- const commit = match[2];
331
- const path = match[3];
332
-
333
- if (ignoreSet.has(path)) continue;
334
-
335
- submodules.push({
336
- path,
337
- commit,
338
- repoPath: repoRoot + '/' + path,
339
- dirty: prefix === '+',
340
- outOfSync: prefix === '-',
341
- lastCheckedAt: Date.now(),
342
- });
603
+ const entries = await Promise.all(
604
+ paths
605
+ .filter(path => !ignoreSet.has(path))
606
+ .map(async (path): Promise<GitSubmoduleStatus> => {
607
+ const repoPath = repo.repoRoot + '/' + path;
608
+ const expected = await readGitlinkExpectedSha(repo, path, options);
609
+ const actual = await readSubmoduleHeadSha(repo, repoPath, options);
610
+ // Uninitialized / no checked-out HEAD reproduces `git submodule status`'s
611
+ // `-` prefix; a present-but-divergent HEAD reproduces the `+` prefix.
612
+ const outOfSync = actual === null
613
+ ? true
614
+ : expected !== null && expected !== actual;
615
+ return {
616
+ path,
617
+ // Prefer the recorded gitlink SHA (matches the legacy column); fall back
618
+ // to the checked-out SHA so the field is never empty when both are known.
619
+ commit: expected ?? actual ?? '',
620
+ repoPath,
621
+ dirty: false,
622
+ outOfSync,
623
+ lastCheckedAt,
624
+ };
625
+ }),
626
+ );
627
+ return entries;
628
+ }
629
+
630
+ /** Read submodule paths from `.gitmodules` via plumbing (no shell wrapper). */
631
+ async function readSubmodulePaths(repo: ResolvedGitRepo, options: GitStatusOptions): Promise<string[]> {
632
+ if (!repo.repoRoot) return [];
633
+ const gitmodulesPath = repo.repoRoot + '/.gitmodules';
634
+ try {
635
+ const result = await runGit(
636
+ repo,
637
+ ['config', '--file', gitmodulesPath, '--get-regexp', '^submodule\\..*\\.path$'],
638
+ options,
639
+ );
640
+ const paths: string[] = [];
641
+ for (const line of result.stdout.split('\n')) {
642
+ // Each line: `submodule.<name>.path <path>`
643
+ const spaceIdx = line.indexOf(' ');
644
+ if (spaceIdx < 0) continue;
645
+ const value = line.slice(spaceIdx + 1).trim();
646
+ if (value) paths.push(value);
647
+ }
648
+ return paths;
649
+ } catch {
650
+ // No .gitmodules (not a superproject) or unreadable → no submodules.
651
+ return [];
343
652
  }
653
+ }
344
654
 
345
- return submodules;
655
+ /** Expected gitlink SHA recorded in the superproject HEAD tree for this submodule path. */
656
+ async function readGitlinkExpectedSha(
657
+ repo: ResolvedGitRepo,
658
+ submodulePath: string,
659
+ options: GitStatusOptions,
660
+ ): Promise<string | null> {
661
+ try {
662
+ // `ls-tree HEAD <path>` prints: `<mode> commit <sha>\t<path>` for a gitlink.
663
+ const result = await runGit(repo, ['ls-tree', 'HEAD', submodulePath], options);
664
+ const line = result.stdout.split('\n').find(l => l.trim().length > 0);
665
+ if (!line) return null;
666
+ const match = line.match(/^\s*\d+\s+commit\s+([0-9a-f]{40})\b/);
667
+ return match ? match[1] : null;
668
+ } catch {
669
+ return null;
670
+ }
671
+ }
672
+
673
+ /** Actual checked-out HEAD SHA of a submodule, or null if uninitialized/unreadable. */
674
+ async function readSubmoduleHeadSha(
675
+ repo: ResolvedGitRepo,
676
+ repoPath: string,
677
+ options: GitStatusOptions,
678
+ ): Promise<string | null> {
679
+ try {
680
+ // Run in the submodule worktree via cwd (inside the superproject root, so the
681
+ // executor's path-inside-repo guard is satisfied) rather than resolving the
682
+ // submodule as a fresh repo — that would cost an extra `rev-parse --show-toplevel`
683
+ // spawn per submodule, which is exactly the Windows spawn cost this fix removes.
684
+ const result = await runGit(repo, ['rev-parse', 'HEAD'], { ...options, cwd: repoPath });
685
+ const sha = result.stdout.trim();
686
+ return /^[0-9a-f]{40}$/.test(sha) ? sha : null;
687
+ } catch {
688
+ return null;
689
+ }
346
690
  }
691
+
692
+ async function enrichSubmoduleWorktreeStatus(
693
+ repo: ResolvedGitRepo,
694
+ submodule: GitSubmoduleStatus,
695
+ options: GitStatusOptions,
696
+ ): Promise<void> {
697
+ try {
698
+ const result = await runGit(repo, ['status', '--porcelain=v2', '--branch'], {
699
+ ...options,
700
+ cwd: submodule.repoPath,
701
+ });
702
+ const parsed = parsePorcelainV2Status(result.stdout);
703
+ const dirty = parsed.staged + parsed.modified + parsed.untracked + parsed.deleted + parsed.renamed > 0
704
+ || parsed.conflictFiles.length > 0;
705
+ submodule.dirty = submodule.dirty || dirty;
706
+ } catch (error) {
707
+ submodule.dirty = true;
708
+ submodule.error = formatGitError(error);
709
+ }
710
+ }
711
+
@@ -6,69 +6,21 @@
6
6
  * infer these values from agent transcripts in frontend code.
7
7
  */
8
8
 
9
- export type GitFailureReason =
10
- | 'not_git_repo'
11
- | 'git_not_installed'
12
- | 'timeout'
13
- | 'path_outside_repo'
14
- | 'dirty_index_required'
15
- | 'conflict'
16
- | 'invalid_args'
17
- | 'nothing_to_commit'
18
- | 'git_command_failed';
9
+ // Pure shape types now live in @adhdev/mesh-shared so the cloud (web-core) and
10
+ // standalone (daemon-core) transports share ONE definition instead of hand-syncing
11
+ // two. Re-exported here so existing `from './git-types'` / package imports keep
12
+ // working unchanged. Daemon-specific types (GitSnapshot, GitCommandName, …) stay
13
+ // local below and consume these re-exports.
14
+ export type {
15
+ GitFailureReason,
16
+ GitRepoIdentity,
17
+ GitSubmoduleStatus,
18
+ GitUpstreamFreshness,
19
+ GitRepoStatus,
20
+ DaemonBuildBehind,
21
+ } from '@adhdev/mesh-shared';
19
22
 
20
- export interface GitRepoIdentity {
21
- workspace: string;
22
- repoRoot: string | null;
23
- isGitRepo: boolean;
24
- }
25
-
26
- export interface GitSubmoduleStatus {
27
- /** Submodule path relative to repo root */
28
- path: string;
29
- /** Current commit SHA the submodule is at */
30
- commit: string;
31
- /** Path to the submodule repo (absolute) */
32
- repoPath: string;
33
- /** Whether the submodule has uncommitted changes */
34
- dirty: boolean;
35
- /** Whether the submodule commit differs from what the parent repo expects */
36
- outOfSync: boolean;
37
- /** Last checked timestamp */
38
- lastCheckedAt: number;
39
- /** Error message if submodule status could not be read */
40
- error?: string;
41
- }
42
-
43
- export type GitUpstreamFreshness = 'fresh' | 'unchecked' | 'stale' | 'no_upstream' | 'unavailable';
44
-
45
- export interface GitRepoStatus extends GitRepoIdentity {
46
- branch: string | null;
47
- headCommit: string | null;
48
- headMessage: string | null;
49
- upstream: string | null;
50
- /** Whether ahead/behind was verified against a freshly fetched upstream ref. */
51
- upstreamStatus: GitUpstreamFreshness;
52
- /** Timestamp for the fetch that refreshed upstream refs when upstreamStatus === 'fresh'. */
53
- upstreamFetchedAt?: number;
54
- /** Error from the last refresh attempt when upstreamStatus === 'stale'. */
55
- upstreamFetchError?: string;
56
- ahead: number;
57
- behind: number;
58
- staged: number;
59
- modified: number;
60
- untracked: number;
61
- deleted: number;
62
- renamed: number;
63
- hasConflicts: boolean;
64
- conflictFiles: string[];
65
- stashCount: number;
66
- lastCheckedAt: number;
67
- /** Submodule statuses when auto-discover is enabled */
68
- submodules?: GitSubmoduleStatus[];
69
- error?: string;
70
- reason?: GitFailureReason;
71
- }
23
+ import type { GitRepoIdentity, GitRepoStatus, GitFailureReason, GitUpstreamFreshness } from '@adhdev/mesh-shared';
72
24
 
73
25
  export type GitFileChangeStatus =
74
26
  | 'added'
@@ -116,8 +116,11 @@ export async function createWorktree(opts: WorktreeCreateOptions): Promise<Workt
116
116
  });
117
117
  } catch (error: any) {
118
118
  const stderr = typeof error.stderr === 'string' ? error.stderr : '';
119
- // Clean error messages for common failures
120
119
  if (/already exists/i.test(stderr)) {
120
+ // Distinguish directory-collision (TOCTOU race) from branch-already-exists
121
+ if (existsSync(targetDir)) {
122
+ throw new Error(`Worktree target directory was created concurrently: ${targetDir}`);
123
+ }
121
124
  throw new Error(`Branch '${branch}' already exists or is checked out in another worktree`);
122
125
  }
123
126
  throw new Error(`git worktree add failed: ${stderr.trim() || error.message}`);
@@ -170,6 +173,10 @@ export async function removeWorktree(repoRoot: string, worktreePath: string, opt
170
173
  const stdout = typeof error.stdout === 'string' ? error.stdout : '';
171
174
  const detail = `${stderr}\n${stdout}\n${error.message || ''}`;
172
175
  if (opts.allowSubmoduleForceFallback && SUBMODULE_WORKTREE_REMOVE_RE.test(detail)) {
176
+ process.stderr.write(
177
+ `[adhdev-mesh] WARNING: git worktree remove --force fallback for submodule worktree '${worktreePath}'. `
178
+ + `Any uncommitted changes inside submodules will be lost.\n`,
179
+ );
173
180
  try {
174
181
  await execFileAsync('git', ['worktree', 'remove', '--force', worktreePath], {
175
182
  cwd: repoRoot,