@adhdev/daemon-core 0.9.82-rc.3 → 0.9.82-rc.300

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 (288) hide show
  1. package/dist/boot/daemon-lifecycle.d.ts +10 -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/terminal-backends/ghostty-vt-backend.d.ts +6 -3
  15. package/dist/cli-adapters/terminal-backends/types.d.ts +10 -2
  16. package/dist/cli-adapters/terminal-screen.d.ts +4 -11
  17. package/dist/commands/cli-manager.d.ts +2 -1
  18. package/dist/commands/handler.d.ts +110 -0
  19. package/dist/commands/mesh-coordinator.d.ts +85 -1
  20. package/dist/commands/router.d.ts +215 -1
  21. package/dist/config/chat-history.d.ts +9 -0
  22. package/dist/config/config.d.ts +5 -0
  23. package/dist/config/mesh-config.d.ts +72 -1
  24. package/dist/git/git-commands.d.ts +10 -1
  25. package/dist/git/git-diff.d.ts +6 -0
  26. package/dist/git/git-status.d.ts +12 -0
  27. package/dist/git/git-types.d.ts +5 -43
  28. package/dist/index.d.ts +51 -12
  29. package/dist/index.js +38833 -15876
  30. package/dist/index.js.map +1 -1
  31. package/dist/index.mjs +38504 -15652
  32. package/dist/index.mjs.map +1 -1
  33. package/dist/installer.d.ts +1 -4
  34. package/dist/ipc/local-ipc-server.d.ts +91 -0
  35. package/dist/launch.d.ts +1 -1
  36. package/dist/logging/async-batch-writer.d.ts +10 -0
  37. package/dist/mesh/contracts.d.ts +164 -0
  38. package/dist/mesh/coordinator-prompt.d.ts +36 -0
  39. package/dist/mesh/coordinator-registry.d.ts +59 -0
  40. package/dist/mesh/mesh-active-work.d.ts +108 -0
  41. package/dist/mesh/mesh-delivery-policy.d.ts +131 -0
  42. package/dist/mesh/mesh-events-coordinator.d.ts +138 -0
  43. package/dist/mesh/mesh-events-pending.d.ts +45 -0
  44. package/dist/mesh/mesh-events-stale.d.ts +40 -0
  45. package/dist/mesh/mesh-events-utils.d.ts +47 -0
  46. package/dist/mesh/mesh-events.d.ts +6 -37
  47. package/dist/mesh/mesh-fast-forward.d.ts +81 -0
  48. package/dist/mesh/mesh-host-ownership.d.ts +9 -0
  49. package/dist/mesh/mesh-init.d.ts +86 -0
  50. package/dist/mesh/mesh-ledger-reconciliation.d.ts +23 -3
  51. package/dist/mesh/mesh-ledger.d.ts +77 -1
  52. package/dist/mesh/mesh-missions.d.ts +107 -0
  53. package/dist/mesh/mesh-reconcile-loop.d.ts +7 -0
  54. package/dist/mesh/mesh-refine-batch.d.ts +68 -0
  55. package/dist/mesh/mesh-refine-status.d.ts +62 -0
  56. package/dist/mesh/mesh-review-inbox.d.ts +90 -0
  57. package/dist/mesh/mesh-routing.d.ts +70 -0
  58. package/dist/mesh/mesh-runtime-store.d.ts +377 -0
  59. package/dist/mesh/mesh-task-stats.d.ts +49 -0
  60. package/dist/mesh/mesh-unresolved-forward-outbox.d.ts +30 -0
  61. package/dist/mesh/mesh-work-queue.d.ts +184 -6
  62. package/dist/mesh/preview-freshness.d.ts +18 -0
  63. package/dist/mesh/refine-config.d.ts +215 -0
  64. package/dist/mesh/worktree-bootstrap-config.d.ts +135 -0
  65. package/dist/providers/acp-provider-instance.d.ts +2 -0
  66. package/dist/providers/approval-utils.d.ts +13 -0
  67. package/dist/providers/chat-message-normalization.d.ts +1 -0
  68. package/dist/providers/cli-provider-instance.d.ts +61 -3
  69. package/dist/providers/contracts.d.ts +130 -6
  70. package/dist/providers/external-sources.d.ts +71 -0
  71. package/dist/providers/native-history/antigravity-cli-transcript.d.ts +100 -0
  72. package/dist/providers/native-history/claude-cli-transcript.d.ts +70 -0
  73. package/dist/providers/native-history/codex-cli-transcript.d.ts +73 -0
  74. package/dist/providers/native-history/dispatcher.d.ts +27 -0
  75. package/dist/providers/native-history/hermes-cli-transcript.d.ts +30 -0
  76. package/dist/providers/native-history/index.d.ts +13 -0
  77. package/dist/providers/provider-instance-manager.d.ts +13 -0
  78. package/dist/providers/provider-instance.d.ts +13 -1
  79. package/dist/providers/provider-loader.d.ts +26 -4
  80. package/dist/providers/provider-trust.d.ts +31 -0
  81. package/dist/providers/read-chat-contract.d.ts +29 -0
  82. package/dist/providers/sdk/v1/builders/acp/detect-status.d.ts +68 -0
  83. package/dist/providers/sdk/v1/builders/cli/detect-status.d.ts +85 -0
  84. package/dist/providers/sdk/v1/builders/cli/parse-approval-squash.d.ts +59 -0
  85. package/dist/providers/sdk/v1/builders/cli/parse-approval.d.ts +65 -0
  86. package/dist/providers/sdk/v1/builders/cli/parse-session.d.ts +91 -0
  87. package/dist/providers/sdk/v1/builders/cli/visible-region.d.ts +42 -0
  88. package/dist/providers/sdk/v1/fixture-tooling/format.d.ts +126 -0
  89. package/dist/providers/sdk/v1/fixture-tooling/index.d.ts +8 -0
  90. package/dist/providers/sdk/v1/fixture-tooling/replay.d.ts +38 -0
  91. package/dist/providers/sdk/v1/index.d.ts +30 -0
  92. package/dist/providers/sdk/v1/sandbox/README-design.d.ts +193 -0
  93. package/dist/providers/sdk/v1/sandbox/require-whitelist.d.ts +74 -0
  94. package/dist/providers/sdk/v1/sandbox/script-runner.d.ts +98 -0
  95. package/dist/providers/sdk/v1/types/cli/index.d.ts +277 -0
  96. package/dist/providers/sdk/v1/types/common/index.d.ts +203 -0
  97. package/dist/providers/sdk/v1/validators/index.d.ts +5 -0
  98. package/dist/providers/sdk/v1/validators/manifest.d.ts +40 -0
  99. package/dist/providers/sdk/v1/validators/taint.d.ts +52 -0
  100. package/dist/providers/spec/adapter.d.ts +59 -0
  101. package/dist/providers/spec/cli-adapter.d.ts +186 -0
  102. package/dist/providers/spec/evaluator.d.ts +23 -0
  103. package/dist/providers/spec/fsm-driver.d.ts +278 -0
  104. package/dist/providers/spec/fsm-evaluator.d.ts +72 -0
  105. package/dist/providers/spec/fsm-loader.d.ts +14 -0
  106. package/dist/providers/spec/fsm-types.d.ts +128 -0
  107. package/dist/providers/spec/native-history-executor.d.ts +41 -0
  108. package/dist/providers/spec/pre-launch-trust.d.ts +16 -0
  109. package/dist/providers/spec/route.d.ts +4 -0
  110. package/dist/providers/spec/types.d.ts +144 -0
  111. package/dist/providers/transcript-v2.d.ts +176 -0
  112. package/dist/providers/types/interactive-prompt.d.ts +84 -0
  113. package/dist/repo-mesh-types.d.ts +314 -1
  114. package/dist/sessions/registry.d.ts +6 -0
  115. package/dist/shared-types-extra.d.ts +2 -4
  116. package/dist/shared-types.d.ts +42 -1
  117. package/dist/status/normalize.d.ts +1 -1
  118. package/dist/status/normalize.js +1 -0
  119. package/dist/status/normalize.js.map +1 -1
  120. package/dist/status/normalize.mjs +1 -0
  121. package/dist/status/normalize.mjs.map +1 -1
  122. package/dist/status/reporter.d.ts +2 -0
  123. package/dist/status/snapshot.d.ts +1 -0
  124. package/dist/types.d.ts +5 -0
  125. package/package.json +7 -3
  126. package/src/agent-stream/poller.ts +2 -3
  127. package/src/boot/daemon-lifecycle.ts +58 -12
  128. package/src/boot/process-hardening.ts +89 -0
  129. package/src/build-info.ts +73 -0
  130. package/src/chat/source-machine.ts +534 -0
  131. package/src/chat/source-resolver.ts +0 -0
  132. package/src/chat/subscription-updates.ts +20 -1
  133. package/src/cli-adapter-types.d.ts +1 -0
  134. package/src/cli-adapter-types.ts +22 -2
  135. package/src/cli-adapters/cli-script-runner.ts +421 -0
  136. package/src/cli-adapters/cli-state-engine.ts +1215 -0
  137. package/src/cli-adapters/provider-cli-adapter.d.ts +1 -1
  138. package/src/cli-adapters/provider-cli-adapter.ts +674 -1139
  139. package/src/cli-adapters/provider-cli-parse.d.ts +1 -0
  140. package/src/cli-adapters/provider-cli-parse.ts +13 -0
  141. package/src/cli-adapters/provider-cli-runtime.ts +3 -1
  142. package/src/cli-adapters/provider-cli-shared.d.ts +2 -0
  143. package/src/cli-adapters/provider-cli-shared.ts +68 -11
  144. package/src/cli-adapters/raw-terminal-io.ts +252 -0
  145. package/src/cli-adapters/terminal-backends/ghostty-vt-backend.ts +53 -29
  146. package/src/cli-adapters/terminal-backends/types.ts +10 -3
  147. package/src/cli-adapters/terminal-screen.ts +16 -81
  148. package/src/commands/chat-commands.ts +1813 -60
  149. package/src/commands/cli-manager.ts +286 -14
  150. package/src/commands/handler.ts +809 -2
  151. package/src/commands/mesh-coordinator.ts +332 -122
  152. package/src/commands/router.ts +6753 -867
  153. package/src/config/chat-history.ts +474 -24
  154. package/src/config/config.ts +12 -0
  155. package/src/config/mesh-config.ts +303 -3
  156. package/src/config/recent-activity.ts +8 -2
  157. package/src/daemon/dev-auto-implement.ts +3 -2
  158. package/src/daemon/dev-cli-debug.ts +10 -1
  159. package/src/detection/ide-detector.ts +26 -16
  160. package/src/git/git-commands.ts +43 -13
  161. package/src/git/git-diff.ts +53 -0
  162. package/src/git/git-status.ts +329 -12
  163. package/src/git/git-summary.ts +3 -0
  164. package/src/git/git-types.ts +17 -54
  165. package/src/git/git-worktree.ts +8 -1
  166. package/src/index.ts +139 -11
  167. package/src/installer.d.ts +1 -1
  168. package/src/installer.ts +8 -6
  169. package/src/ipc/local-ipc-server.ts +278 -0
  170. package/src/launch.d.ts +1 -1
  171. package/src/launch.ts +37 -28
  172. package/src/logging/async-batch-writer.ts +55 -0
  173. package/src/logging/logger.ts +2 -1
  174. package/src/mesh/contracts.ts +329 -0
  175. package/src/mesh/coordinator-prompt.ts +234 -31
  176. package/src/mesh/coordinator-registry.ts +121 -0
  177. package/src/mesh/mesh-active-work.ts +469 -0
  178. package/src/mesh/mesh-delivery-policy.ts +315 -0
  179. package/src/mesh/mesh-events-coordinator.ts +1786 -0
  180. package/src/mesh/mesh-events-pending.ts +508 -0
  181. package/src/mesh/mesh-events-stale.ts +290 -0
  182. package/src/mesh/mesh-events-utils.ts +254 -0
  183. package/src/mesh/mesh-events.ts +30 -897
  184. package/src/mesh/mesh-fast-forward.ts +839 -0
  185. package/src/mesh/mesh-host-ownership.ts +73 -0
  186. package/src/mesh/mesh-init.ts +260 -0
  187. package/src/mesh/mesh-ledger-reconciliation.ts +12 -5
  188. package/src/mesh/mesh-ledger.ts +589 -102
  189. package/src/mesh/mesh-missions.ts +232 -0
  190. package/src/mesh/mesh-reconcile-loop.ts +466 -0
  191. package/src/mesh/mesh-refine-batch.ts +197 -0
  192. package/src/mesh/mesh-refine-status.ts +231 -0
  193. package/src/mesh/mesh-review-inbox.ts +307 -0
  194. package/src/mesh/mesh-routing.ts +291 -0
  195. package/src/mesh/mesh-runtime-store.ts +1513 -0
  196. package/src/mesh/mesh-task-stats.ts +154 -0
  197. package/src/mesh/mesh-unresolved-forward-outbox.ts +185 -0
  198. package/src/mesh/mesh-work-queue.ts +641 -166
  199. package/src/mesh/preview-freshness.ts +118 -0
  200. package/src/mesh/refine-config.ts +403 -0
  201. package/src/mesh/worktree-bootstrap-config.ts +326 -0
  202. package/src/providers/acp-provider-instance.ts +15 -1
  203. package/src/providers/approval-utils.d.ts +4 -0
  204. package/src/providers/approval-utils.ts +47 -5
  205. package/src/providers/chat-message-normalization.ts +7 -12
  206. package/src/providers/cli-provider-instance.ts +953 -87
  207. package/src/providers/contracts.d.ts +55 -0
  208. package/src/providers/contracts.ts +141 -6
  209. package/src/providers/extension-provider-instance.ts +5 -1
  210. package/src/providers/external-sources.ts +218 -0
  211. package/src/providers/ide-provider-instance.ts +25 -6
  212. package/src/providers/native-history/antigravity-cli-transcript.ts +643 -0
  213. package/src/providers/native-history/claude-cli-transcript.ts +396 -0
  214. package/src/providers/native-history/codex-cli-transcript.ts +479 -0
  215. package/src/providers/native-history/dispatcher.ts +340 -0
  216. package/src/providers/native-history/hermes-cli-transcript.ts +230 -0
  217. package/src/providers/native-history/index.ts +30 -0
  218. package/src/providers/provider-instance-manager.ts +30 -0
  219. package/src/providers/provider-instance.ts +10 -1
  220. package/src/providers/provider-loader.ts +584 -50
  221. package/src/providers/provider-schema.ts +87 -14
  222. package/src/providers/provider-trust.ts +114 -0
  223. package/src/providers/read-chat-contract.ts +76 -16
  224. package/src/providers/sdk/README.md +49 -0
  225. package/src/providers/sdk/v1/builders/acp/detect-status.ts +144 -0
  226. package/src/providers/sdk/v1/builders/cli/detect-status.ts +262 -0
  227. package/src/providers/sdk/v1/builders/cli/parse-approval-squash.ts +158 -0
  228. package/src/providers/sdk/v1/builders/cli/parse-approval.ts +250 -0
  229. package/src/providers/sdk/v1/builders/cli/parse-session.ts +276 -0
  230. package/src/providers/sdk/v1/builders/cli/visible-region.ts +143 -0
  231. package/src/providers/sdk/v1/fixture-tooling/format.ts +130 -0
  232. package/src/providers/sdk/v1/fixture-tooling/index.ts +22 -0
  233. package/src/providers/sdk/v1/fixture-tooling/replay.ts +352 -0
  234. package/src/providers/sdk/v1/index.ts +152 -0
  235. package/src/providers/sdk/v1/sandbox/README-design.ts +195 -0
  236. package/src/providers/sdk/v1/sandbox/require-whitelist.ts +472 -0
  237. package/src/providers/sdk/v1/sandbox/script-runner.ts +150 -0
  238. package/src/providers/sdk/v1/schemas/cli/provider.schema.json +504 -0
  239. package/src/providers/sdk/v1/schemas/primitives/acp-session-protocol-v1.json +131 -0
  240. package/src/providers/sdk/v1/schemas/primitives/native-history-codex-rollout-v1.json +66 -0
  241. package/src/providers/sdk/v1/schemas/primitives/tui-approval-squash-v1.json +91 -0
  242. package/src/providers/sdk/v1/schemas/primitives/tui-assistant-block-v1.json +91 -0
  243. package/src/providers/sdk/v1/schemas/primitives/tui-cue-ordering-v1.json +47 -0
  244. package/src/providers/sdk/v1/schemas/primitives/tui-dispatch-order-v1.json +32 -0
  245. package/src/providers/sdk/v1/schemas/primitives/tui-footer-chrome-v1.json +42 -0
  246. package/src/providers/sdk/v1/schemas/primitives/tui-index-finder-v1.json +27 -0
  247. package/src/providers/sdk/v1/schemas/primitives/tui-modal-v1.json +125 -0
  248. package/src/providers/sdk/v1/schemas/primitives/tui-prompt-marker-v1.json +45 -0
  249. package/src/providers/sdk/v1/schemas/primitives/tui-session-id-extraction-v1.json +46 -0
  250. package/src/providers/sdk/v1/schemas/primitives/tui-settled-prompt-v1.json +71 -0
  251. package/src/providers/sdk/v1/schemas/primitives/tui-spinner-v1.json +83 -0
  252. package/src/providers/sdk/v1/schemas/primitives/tui-transcript-pty-v1.json +83 -0
  253. package/src/providers/sdk/v1/schemas/primitives/tui-visible-region-v1.json +57 -0
  254. package/src/providers/sdk/v1/schemas/primitives/tui-welcome-screen-v1.json +35 -0
  255. package/src/providers/sdk/v1/types/cli/index.ts +365 -0
  256. package/src/providers/sdk/v1/types/common/index.ts +229 -0
  257. package/src/providers/sdk/v1/validators/index.ts +19 -0
  258. package/src/providers/sdk/v1/validators/manifest.ts +110 -0
  259. package/src/providers/sdk/v1/validators/taint.ts +309 -0
  260. package/src/providers/spec/adapter.ts +155 -0
  261. package/src/providers/spec/cli-adapter.ts +838 -0
  262. package/src/providers/spec/evaluator.ts +345 -0
  263. package/src/providers/spec/fsm-driver.ts +875 -0
  264. package/src/providers/spec/fsm-evaluator.ts +255 -0
  265. package/src/providers/spec/fsm-loader.ts +112 -0
  266. package/src/providers/spec/fsm-types.ts +222 -0
  267. package/src/providers/spec/native-history-executor.ts +943 -0
  268. package/src/providers/spec/pre-launch-trust.ts +104 -0
  269. package/src/providers/spec/route.ts +51 -0
  270. package/src/providers/spec/types.ts +189 -0
  271. package/src/providers/transcript-v2.ts +567 -0
  272. package/src/providers/types/interactive-prompt.ts +536 -0
  273. package/src/providers/version-archive.ts +38 -20
  274. package/src/repo-mesh-types.ts +378 -1
  275. package/src/sessions/registry.ts +6 -0
  276. package/src/shared-types-extra.ts +2 -4
  277. package/src/shared-types.ts +45 -1
  278. package/src/status/builders.ts +26 -6
  279. package/src/status/normalize.ts +2 -0
  280. package/src/status/reporter.ts +15 -0
  281. package/src/status/snapshot.ts +84 -25
  282. package/src/system/host-memory.ts +29 -12
  283. package/src/types.ts +5 -0
  284. package/dist/cli-adapters/terminal-backends/xterm-backend.d.ts +0 -17
  285. package/dist/mesh/mesh-sync.d.ts +0 -53
  286. package/src/cli-adapters/terminal-backends/ghostty-vt-backend.d.ts +0 -16
  287. package/src/cli-adapters/terminal-backends/xterm-backend.ts +0 -97
  288. package/src/mesh/mesh-sync.ts +0 -111
@@ -10,6 +10,20 @@ export interface GitDiffOptions {
10
10
  timeoutMs?: number;
11
11
  maxFiles?: number;
12
12
  maxBytes?: number;
13
+ /**
14
+ * When set, diff `<baseRef>...HEAD` (merge-base range — "what this branch
15
+ * adds on top of base") instead of the working tree. Staged/untracked
16
+ * sections do not apply in this mode.
17
+ */
18
+ baseRef?: string;
19
+ }
20
+
21
+ function validateBaseRef(ref: string): string {
22
+ const trimmed = ref.trim();
23
+ if (!trimmed || trimmed.startsWith('-') || trimmed.includes('..') || !/^[A-Za-z0-9][A-Za-z0-9._/@-]*$/.test(trimmed)) {
24
+ throw new GitCommandError('invalid_args', `Invalid base ref: ${ref}`);
25
+ }
26
+ return trimmed;
13
27
  }
14
28
 
15
29
  export interface GitFileDiffResult {
@@ -44,6 +58,30 @@ export async function getGitDiffSummary(
44
58
  try {
45
59
  const repo = await resolveGitRepository(workspace, options);
46
60
  const repoRoot = repo.repoRoot!;
61
+
62
+ if (options.baseRef) {
63
+ const range = `${validateBaseRef(options.baseRef)}...HEAD`;
64
+ const [nameStatus, numstat] = await Promise.all([
65
+ runGit(repo, ['diff', '--no-ext-diff', '--name-status', range, '--'], { ...options, cwd: repoRoot }),
66
+ runGit(repo, ['diff', '--no-ext-diff', '--numstat', range, '--'], { ...options, cwd: repoRoot }),
67
+ ]);
68
+ const outputBytes = byteLength(nameStatus.stdout + numstat.stdout);
69
+ const changes = combineDiffEntries(nameStatus.stdout, numstat.stdout, false);
70
+ const maxFiles = normalizePositiveInteger(options.maxFiles, DEFAULT_MAX_FILES);
71
+ const maxBytes = normalizePositiveInteger(options.maxBytes, DEFAULT_MAX_BYTES);
72
+ const files = changes.slice(0, maxFiles);
73
+ return {
74
+ workspace: repo.workspace,
75
+ repoRoot,
76
+ isGitRepo: true,
77
+ files,
78
+ totalInsertions: files.reduce((sum, file) => sum + file.insertions, 0),
79
+ totalDeletions: files.reduce((sum, file) => sum + file.deletions, 0),
80
+ truncated: changes.length > files.length || outputBytes > maxBytes,
81
+ lastCheckedAt,
82
+ };
83
+ }
84
+
47
85
  const [unstagedNameStatus, unstagedNumstat, stagedNameStatus, stagedNumstat, untracked] = await Promise.all([
48
86
  runGit(repo, ['diff', '--no-ext-diff', '--name-status'], { ...options, cwd: repoRoot }),
49
87
  runGit(repo, ['diff', '--no-ext-diff', '--numstat'], { ...options, cwd: repoRoot }),
@@ -106,6 +144,21 @@ export async function getGitFileDiff(
106
144
  const selected = await resolveRepoFilePath(repoRoot, filePath);
107
145
  const maxBytes = normalizePositiveInteger(options.maxBytes, DEFAULT_MAX_BYTES);
108
146
 
147
+ if (options.baseRef) {
148
+ const range = `${validateBaseRef(options.baseRef)}...HEAD`;
149
+ const result = await runGit(repo, ['diff', '--no-ext-diff', range, '--', selected.relativePath], { ...options, cwd: repoRoot });
150
+ const bounded = truncateText(result.stdout, maxBytes);
151
+ return {
152
+ workspace: repo.workspace,
153
+ repoRoot,
154
+ isGitRepo: true,
155
+ path: selected.relativePath,
156
+ diff: bounded.text,
157
+ truncated: bounded.truncated,
158
+ lastCheckedAt,
159
+ };
160
+ }
161
+
109
162
  const [unstaged, staged] = await Promise.all([
110
163
  runGit(repo, ['diff', '--no-ext-diff', '--', selected.relativePath], { ...options, cwd: repoRoot }),
111
164
  runGit(repo, ['diff', '--cached', '--no-ext-diff', '--', selected.relativePath], { ...options, cwd: repoRoot }),
@@ -1,5 +1,8 @@
1
- import type { GitRepoStatus, GitSubmoduleStatus } from './git-types.js';
1
+ import type { DaemonBuildBehind, GitRepoStatus, GitSubmoduleStatus, GitUpstreamFreshness } from './git-types.js';
2
2
  import { GitCommandError, resolveGitRepository, runGit } from './git-executor.js';
3
+ import { getDaemonBuildInfo, type DaemonBuildInfo } from '../build-info.js';
4
+
5
+ type ResolvedGitRepo = { workspace: string; repoRoot: string | null; isGitRepo: boolean };
3
6
 
4
7
  export interface GitStatusOptions {
5
8
  timeoutMs?: number;
@@ -7,6 +10,23 @@ export interface GitStatusOptions {
7
10
  includeSubmodules?: boolean;
8
11
  /** Optional filter to exclude specific submodule paths from status */
9
12
  submoduleIgnorePaths?: string[];
13
+ /**
14
+ * When true, refresh the tracked remote before trusting ahead/behind.
15
+ * Callers should opt into this only for convergence-critical surfaces.
16
+ */
17
+ refreshUpstream?: boolean;
18
+ /**
19
+ * Test/override seam for the daemon build stamp used by the stale-build
20
+ * detector. Production callers omit this so the real baked-in build commit
21
+ * (getDaemonBuildInfo) is used.
22
+ */
23
+ daemonBuildInfo?: DaemonBuildInfo;
24
+ }
25
+
26
+ interface GitUpstreamProbe {
27
+ upstreamStatus: GitUpstreamFreshness;
28
+ upstreamFetchedAt?: number;
29
+ upstreamFetchError?: string;
10
30
  }
11
31
 
12
32
  export async function getGitRepoStatus(
@@ -18,8 +38,16 @@ export async function getGitRepoStatus(
18
38
 
19
39
  try {
20
40
  const repo = await resolveGitRepository(workspace, options);
21
- const statusOutput = await runGit(repo, ['status', '--porcelain=v2', '--branch'], options);
22
- const parsed = parsePorcelainV2Status(statusOutput.stdout);
41
+ let parsed = await readPorcelainStatus(repo, options);
42
+ let upstreamProbe: GitUpstreamProbe = getInitialUpstreamProbe(parsed);
43
+
44
+ if (options.refreshUpstream) {
45
+ upstreamProbe = await refreshTrackedUpstream(repo, parsed, options);
46
+ if (upstreamProbe.upstreamStatus === 'fresh') {
47
+ parsed = await readPorcelainStatus(repo, options);
48
+ }
49
+ }
50
+
23
51
  const head = await readHead(repo, options);
24
52
  const stashCount = await readStashCount(repo, options);
25
53
 
@@ -27,6 +55,13 @@ export async function getGitRepoStatus(
27
55
  if (includeSubmodules) {
28
56
  submodules = await getSubmoduleStatuses(repo, options);
29
57
  }
58
+ const submoduleDirty = (submodules || []).some(submodule => submodule.dirty || submodule.outOfSync || !!submodule.error);
59
+ const dirty = parsed.staged + parsed.modified + parsed.untracked + parsed.deleted + parsed.renamed > 0
60
+ || parsed.conflictFiles.length > 0
61
+ || stashCount > 0
62
+ || submoduleDirty;
63
+
64
+ const daemonBuildBehind = await detectDaemonBuildBehind(repo, submodules, options);
30
65
 
31
66
  return {
32
67
  workspace: repo.workspace,
@@ -36,6 +71,9 @@ export async function getGitRepoStatus(
36
71
  headCommit: head.commit,
37
72
  headMessage: head.message,
38
73
  upstream: parsed.upstream,
74
+ upstreamStatus: parsed.upstream ? upstreamProbe.upstreamStatus : 'no_upstream',
75
+ upstreamFetchedAt: upstreamProbe.upstreamFetchedAt,
76
+ upstreamFetchError: upstreamProbe.upstreamFetchError,
39
77
  ahead: parsed.ahead,
40
78
  behind: parsed.behind,
41
79
  staged: parsed.staged,
@@ -43,11 +81,13 @@ export async function getGitRepoStatus(
43
81
  untracked: parsed.untracked,
44
82
  deleted: parsed.deleted,
45
83
  renamed: parsed.renamed,
84
+ dirty,
46
85
  hasConflicts: parsed.conflictFiles.length > 0,
47
86
  conflictFiles: parsed.conflictFiles,
48
87
  stashCount,
49
88
  lastCheckedAt,
50
89
  submodules,
90
+ ...(daemonBuildBehind ? { daemonBuildBehind } : {}),
51
91
  };
52
92
  } catch (error) {
53
93
  if (error instanceof GitCommandError) {
@@ -61,6 +101,193 @@ export async function getGitRepoStatus(
61
101
  }
62
102
  }
63
103
 
104
+ /**
105
+ * Detect whether the running daemon's build commit is a STRICT ancestor of this
106
+ * workspace's HEAD (root) or any of its submodules' HEAD. This surfaces the
107
+ * "merged a fix to main but the live daemon still ships the old bundle" gap:
108
+ * once the fix is committed, the workspace HEAD advances past the daemon's
109
+ * baked-in build commit, but the daemon keeps the old behavior until it is
110
+ * rebuilt/redeployed and restarted.
111
+ *
112
+ * Conservative by construction — returns undefined unless ancestry is provable:
113
+ * - build commit unknown → undefined
114
+ * - build commit not an object in this repo/submodule (different repo) → skip
115
+ * - build commit === HEAD (daemon is current) → undefined
116
+ * - build commit NOT an ancestor of HEAD (daemon ahead / diverged) → undefined
117
+ * Any git error is swallowed (no warning) so a flaky probe never over-warns.
118
+ */
119
+ /**
120
+ * Package names that, when changed, mean the daemon runtime is stale and must be
121
+ * rebuilt/redeployed + restarted. Everything NOT in this set (web-core,
122
+ * web-standalone, web-devconsole, terminal-render-web) is web-only — a daemon
123
+ * restart is not required for those, only a web redeploy. mcp-server runs in the
124
+ * same process surface as the daemon tooling, so it is classified as
125
+ * daemon-affecting (conservative). Unknown package → daemon-affecting.
126
+ */
127
+ const DAEMON_RUNTIME_PACKAGES = new Set([
128
+ 'daemon-core',
129
+ 'daemon-standalone',
130
+ 'session-host-core',
131
+ 'session-host-daemon',
132
+ 'terminal-mux-core',
133
+ 'terminal-mux-control',
134
+ 'terminal-mux-cli',
135
+ 'ghostty-vt-node',
136
+ 'mcp-server',
137
+ ]);
138
+
139
+ const WEB_ONLY_PACKAGES = new Set([
140
+ 'web-core',
141
+ 'web-standalone',
142
+ 'web-devconsole',
143
+ 'terminal-render-web',
144
+ ]);
145
+
146
+ /**
147
+ * Root-level (non-package) files that demonstrably cannot change what the daemon
148
+ * runtime executes: convergence/verify markers, documentation, and license/notice
149
+ * text. A root commit that moves the oss gitlink while the oss commit only touched
150
+ * one of these is NOT a reason to rebuild/restart the daemon — flagging it produces
151
+ * the staleDaemonBuild false-positive this guard exists to suppress.
152
+ *
153
+ * Deliberately conservative: anything NOT matched here (root config like
154
+ * package.json / tsconfig / build scripts, `.txt` fixtures, lockfiles, unknown
155
+ * dotfiles) stays daemon-affecting, because a false-negative — staying silent when
156
+ * the daemon really IS stale — is worse than an over-warn. Markers are the common
157
+ * real-world case (e.g. `.verify-patch-equiv-rc292`), so they are matched broadly;
158
+ * docs are matched by the `docs/` prefix or a markdown/text-doc extension at a
159
+ * filename we recognize as documentation (README/CHANGELOG/LICENSE/NOTICE).
160
+ */
161
+ function isNonRuntimeRootFile(file: string): boolean {
162
+ const base = file.slice(file.lastIndexOf('/') + 1);
163
+ // Verify/convergence markers: dotfiles whose name signals a transient marker.
164
+ if (/^\.(?:verify|marker|converge|ff-verify|patch-equiv|live-verify)\b/i.test(base)) return true;
165
+ // Documentation living under a docs/ tree (any depth, root or nested).
166
+ if (/(?:^|\/)docs\//i.test(file)) return true;
167
+ // Recognized top-level documentation / license files.
168
+ if (/^(?:README|CHANGELOG|LICENSE|NOTICE|AUTHORS|CONTRIBUTING|CODEOWNERS)(?:\.[A-Za-z0-9]+)?$/i.test(base)) {
169
+ return true;
170
+ }
171
+ return false;
172
+ }
173
+
174
+ /**
175
+ * Determine whether the changes between buildCommit..HEAD touch any daemon-runtime
176
+ * package. Returns isDaemonAffecting:true conservatively when the changed-file set
177
+ * can't be obtained or any changed path is outside the known web-only package set
178
+ * AND is not a recognized non-runtime root file (marker/doc/license).
179
+ */
180
+ async function classifyDaemonBuildChange(
181
+ repoPath: string,
182
+ buildCommit: string,
183
+ options: GitStatusOptions,
184
+ ): Promise<{ isDaemonAffecting: boolean; affectedPackages: string[] }> {
185
+ try {
186
+ const diff = await runGit(repoPath, ['diff', '--name-only', `${buildCommit}..HEAD`], options);
187
+ const files = diff.stdout
188
+ .split('\n')
189
+ .map((line) => line.trim())
190
+ .filter(Boolean);
191
+ if (files.length === 0) {
192
+ // No file diff (e.g. only merge metadata) — nothing actionable, but stay
193
+ // conservative and treat as daemon-affecting so we don't suppress a real warning.
194
+ return { isDaemonAffecting: true, affectedPackages: [] };
195
+ }
196
+ const pkgs = new Set<string>();
197
+ // A non-package path that is NOT a recognized benign root file (marker/doc).
198
+ // Only these force daemon-affecting; benign markers/docs are ignored so a
199
+ // gitlink-moving root commit over a marker-only oss commit no longer over-warns.
200
+ let sawRuntimeAmbiguousNonPackage = false;
201
+ for (const file of files) {
202
+ const match = file.match(/(?:^|\/)packages\/([^/]+)\//);
203
+ if (!match) {
204
+ if (!isNonRuntimeRootFile(file)) sawRuntimeAmbiguousNonPackage = true;
205
+ continue;
206
+ }
207
+ pkgs.add(match[1]);
208
+ }
209
+ const affectedPackages = [...pkgs].sort();
210
+ // Daemon-affecting if: any runtime-ambiguous non-package file changed, any
211
+ // unknown package changed, or any explicit daemon-runtime package changed.
212
+ // The daemon is unaffected only when every changed file is either a known
213
+ // web-only package or a recognized benign root file (and at least one such
214
+ // file changed) — i.e. nothing runtime-ambiguous remains.
215
+ const allBenign =
216
+ !sawRuntimeAmbiguousNonPackage &&
217
+ affectedPackages.every((p) => WEB_ONLY_PACKAGES.has(p) && !DAEMON_RUNTIME_PACKAGES.has(p));
218
+ return { isDaemonAffecting: !allBenign, affectedPackages };
219
+ } catch {
220
+ // diff probe failed → can't prove web-only; stay conservative.
221
+ return { isDaemonAffecting: true, affectedPackages: [] };
222
+ }
223
+ }
224
+
225
+ async function detectDaemonBuildBehind(
226
+ repo: ResolvedGitRepo,
227
+ submodules: GitSubmoduleStatus[] | undefined,
228
+ options: GitStatusOptions,
229
+ ): Promise<DaemonBuildBehind | undefined> {
230
+ const build = options.daemonBuildInfo ?? getDaemonBuildInfo();
231
+ if (!build.commit || build.commit === 'unknown') return undefined;
232
+
233
+ // Check the root repo first, then each submodule. The daemon build commit is
234
+ // baked from the daemon-core (oss submodule) HEAD, so on an adhdev
235
+ // superproject worktree the match is expected on the `oss` submodule, not the
236
+ // root — checking both keeps the helper repo-agnostic.
237
+ const scopes: Array<{ scope: string; repoPath: string }> = [
238
+ { scope: 'root', repoPath: repo.repoRoot || repo.workspace },
239
+ ];
240
+ for (const sub of submodules || []) {
241
+ if (sub.repoPath && !sub.error) scopes.push({ scope: sub.path, repoPath: sub.repoPath });
242
+ }
243
+
244
+ for (const { scope, repoPath } of scopes) {
245
+ try {
246
+ // Build commit must be a real object in THIS repo, else it's a different repo.
247
+ await runGit(repoPath, ['cat-file', '-e', `${build.commit}^{commit}`], options);
248
+ const headResult = await runGit(repoPath, ['rev-parse', 'HEAD'], options);
249
+ const head = headResult.stdout.trim();
250
+ if (!head || head === build.commit) continue;
251
+ // Strict ancestor: build commit is reachable from HEAD but is not HEAD.
252
+ await runGit(repoPath, ['merge-base', '--is-ancestor', build.commit, 'HEAD'], options);
253
+ // No throw → build commit IS an ancestor of HEAD → daemon is behind.
254
+ // Inspect WHICH packages changed in buildCommit..HEAD. A daemon rebuild/restart
255
+ // is only actually required when a daemon-runtime package changed; if only web /
256
+ // render packages changed, the daemon is unaffected and just the web deploy is
257
+ // pending. Conservative: any probe failure → treat as daemon-affecting.
258
+ const { isDaemonAffecting, affectedPackages } = await classifyDaemonBuildChange(
259
+ repoPath,
260
+ build.commit,
261
+ options,
262
+ );
263
+ const scopeLabel = scope === 'root' ? 'workspace' : scope;
264
+ const benignDetail = affectedPackages.length > 0
265
+ ? `only web packages changed (${affectedPackages.join(', ')})`
266
+ : 'only non-runtime files changed (markers/docs)';
267
+ const warning = isDaemonAffecting
268
+ ? `Live daemon was built from ${build.commitShort} which is behind ${scopeLabel} HEAD ${head.slice(0, 7)}. ` +
269
+ `Merged code is NOT live until the daemon is rebuilt/redeployed and restarted — a local dist rebuild alone does not update a cloud daemon.`
270
+ : `Live daemon was built from ${build.commitShort} which is behind ${scopeLabel} HEAD ${head.slice(0, 7)}, ` +
271
+ `but ${benignDetail}. ` +
272
+ `Daemon restart NOT required — redeploy the web app to reflect the change.`;
273
+ return {
274
+ buildCommit: build.commit,
275
+ buildCommitShort: build.commitShort,
276
+ head,
277
+ scope,
278
+ isDaemonAffecting,
279
+ ...(affectedPackages && affectedPackages.length > 0 ? { affectedPackages } : {}),
280
+ warning,
281
+ };
282
+ } catch {
283
+ // cat-file / merge-base non-zero exit (commit absent or not an ancestor)
284
+ // or any git error → not a provable staleness for this scope; try next.
285
+ continue;
286
+ }
287
+ }
288
+ return undefined;
289
+ }
290
+
64
291
  interface ParsedPorcelainStatus {
65
292
  branch: string | null;
66
293
  upstream: string | null;
@@ -74,6 +301,72 @@ interface ParsedPorcelainStatus {
74
301
  conflictFiles: string[];
75
302
  }
76
303
 
304
+ async function readPorcelainStatus(repo: ResolvedGitRepo, options: GitStatusOptions): Promise<ParsedPorcelainStatus> {
305
+ const statusOutput = await runGit(repo, ['status', '--porcelain=v2', '--branch'], options);
306
+ return parsePorcelainV2Status(statusOutput.stdout);
307
+ }
308
+
309
+ function getInitialUpstreamProbe(parsed: ParsedPorcelainStatus): GitUpstreamProbe {
310
+ return {
311
+ upstreamStatus: parsed.upstream ? 'unchecked' : 'no_upstream',
312
+ };
313
+ }
314
+
315
+ async function refreshTrackedUpstream(
316
+ repo: ResolvedGitRepo,
317
+ parsed: ParsedPorcelainStatus,
318
+ options: GitStatusOptions,
319
+ ): Promise<GitUpstreamProbe> {
320
+ if (!parsed.upstream || !parsed.branch) {
321
+ return { upstreamStatus: 'no_upstream' };
322
+ }
323
+
324
+ const remoteName = (await readBranchRemote(repo, parsed.branch, options)) ?? inferRemoteName(parsed.upstream);
325
+ if (!remoteName) {
326
+ return {
327
+ upstreamStatus: 'stale',
328
+ upstreamFetchError: `Unable to resolve remote for upstream '${parsed.upstream}'`,
329
+ };
330
+ }
331
+
332
+ try {
333
+ await runGit(repo, ['fetch', '--quiet', '--prune', '--no-tags', remoteName], options);
334
+ return {
335
+ upstreamStatus: 'fresh',
336
+ upstreamFetchedAt: Date.now(),
337
+ };
338
+ } catch (error) {
339
+ return {
340
+ upstreamStatus: 'stale',
341
+ upstreamFetchError: formatGitError(error),
342
+ };
343
+ }
344
+ }
345
+
346
+ async function readBranchRemote(repo: ResolvedGitRepo, branch: string, options: GitStatusOptions): Promise<string | null> {
347
+ try {
348
+ const result = await runGit(repo, ['config', '--get', `branch.${branch}.remote`], options);
349
+ return result.stdout.trim() || null;
350
+ } catch {
351
+ return null;
352
+ }
353
+ }
354
+
355
+ function inferRemoteName(upstream: string): string | null {
356
+ const [remoteName] = upstream.split('/');
357
+ return remoteName?.trim() || null;
358
+ }
359
+
360
+ function formatGitError(error: unknown): string {
361
+ if (error instanceof GitCommandError) {
362
+ return error.stderr || error.message;
363
+ }
364
+ if (error instanceof Error) {
365
+ return error.message;
366
+ }
367
+ return String(error);
368
+ }
369
+
77
370
  export function parsePorcelainV2Status(output: string): ParsedPorcelainStatus {
78
371
  const parsed: ParsedPorcelainStatus = {
79
372
  branch: null,
@@ -145,7 +438,7 @@ export function parsePorcelainV2Status(output: string): ParsedPorcelainStatus {
145
438
  }
146
439
 
147
440
  async function readHead(
148
- repo: { workspace: string; repoRoot: string | null; isGitRepo: boolean },
441
+ repo: ResolvedGitRepo,
149
442
  options: GitStatusOptions,
150
443
  ): Promise<{ commit: string | null; message: string | null }> {
151
444
  try {
@@ -163,7 +456,7 @@ async function readHead(
163
456
  }
164
457
 
165
458
  async function readStashCount(
166
- repo: { workspace: string; repoRoot: string | null; isGitRepo: boolean },
459
+ repo: ResolvedGitRepo,
167
460
  options: GitStatusOptions,
168
461
  ): Promise<number> {
169
462
  try {
@@ -187,6 +480,7 @@ function emptyStatus(workspace: string, lastCheckedAt: number, error: GitCommand
187
480
  headCommit: null,
188
481
  headMessage: null,
189
482
  upstream: null,
483
+ upstreamStatus: 'unavailable',
190
484
  ahead: 0,
191
485
  behind: 0,
192
486
  staged: 0,
@@ -194,6 +488,7 @@ function emptyStatus(workspace: string, lastCheckedAt: number, error: GitCommand
194
488
  untracked: 0,
195
489
  deleted: 0,
196
490
  renamed: 0,
491
+ dirty: false,
197
492
  hasConflicts: false,
198
493
  conflictFiles: [],
199
494
  stashCount: 0,
@@ -206,19 +501,41 @@ function emptyStatus(workspace: string, lastCheckedAt: number, error: GitCommand
206
501
  // ─── Submodule Status ───────────────────────────
207
502
 
208
503
  async function getSubmoduleStatuses(
209
- repo: { workspace: string; repoRoot: string | null; isGitRepo: boolean },
504
+ repo: ResolvedGitRepo,
210
505
  options: GitStatusOptions,
211
506
  ): Promise<GitSubmoduleStatus[]> {
212
507
  if (!repo.repoRoot) return [];
213
508
 
214
509
  try {
215
510
  const result = await runGit(repo, ['submodule', 'status', '--recursive'], options);
216
- return parseSubmoduleStatusOutput(result.stdout, repo.repoRoot, options.submoduleIgnorePaths);
511
+ const submodules = parseSubmoduleStatusOutput(result.stdout, repo.repoRoot, options.submoduleIgnorePaths);
512
+ await Promise.all(submodules.map(submodule => enrichSubmoduleWorktreeStatus(repo, submodule, options)));
513
+ return submodules;
217
514
  } catch {
218
515
  return [];
219
516
  }
220
517
  }
221
518
 
519
+ async function enrichSubmoduleWorktreeStatus(
520
+ repo: ResolvedGitRepo,
521
+ submodule: GitSubmoduleStatus,
522
+ options: GitStatusOptions,
523
+ ): Promise<void> {
524
+ try {
525
+ const result = await runGit(repo, ['status', '--porcelain=v2', '--branch'], {
526
+ ...options,
527
+ cwd: submodule.repoPath,
528
+ });
529
+ const parsed = parsePorcelainV2Status(result.stdout);
530
+ const dirty = parsed.staged + parsed.modified + parsed.untracked + parsed.deleted + parsed.renamed > 0
531
+ || parsed.conflictFiles.length > 0;
532
+ submodule.dirty = submodule.dirty || dirty;
533
+ } catch (error) {
534
+ submodule.dirty = true;
535
+ submodule.error = formatGitError(error);
536
+ }
537
+ }
538
+
222
539
  function parseSubmoduleStatusOutput(
223
540
  output: string,
224
541
  repoRoot: string,
@@ -230,9 +547,9 @@ function parseSubmoduleStatusOutput(
230
547
  for (const line of output.split('\n')) {
231
548
  if (!line.trim()) continue;
232
549
 
233
- // Format: [+- ]<commit> <path> (<branch>)
234
- // - = out of sync, + = dirty, ' ' = clean
235
- const match = line.match(/^([\-+\s])([0-9a-f]{40})\s+(\S+)(?:\s+\(([^)]+)\))?/);
550
+ // Format: [+-U ]<commit> <path> (<branch>)
551
+ // - = not initialized, + = gitlink out of sync, U = conflict, ' ' = aligned.
552
+ const match = line.match(/^([\-+U\s])([0-9a-f]{40})\s+(\S+)(?:\s+\(([^)]+)\))?/);
236
553
  if (!match) continue;
237
554
 
238
555
  const prefix = match[1];
@@ -245,8 +562,8 @@ function parseSubmoduleStatusOutput(
245
562
  path,
246
563
  commit,
247
564
  repoPath: repoRoot + '/' + path,
248
- dirty: prefix === '+',
249
- outOfSync: prefix === '-',
565
+ dirty: prefix === 'U',
566
+ outOfSync: prefix === '-' || prefix === '+',
250
567
  lastCheckedAt: Date.now(),
251
568
  });
252
569
  }
@@ -22,6 +22,9 @@ export function createGitCompactSummary(status: GitRepoStatus, diffSummary?: Git
22
22
  isGitRepo: status.isGitRepo,
23
23
  repoRoot: status.repoRoot,
24
24
  branch: status.branch,
25
+ upstreamStatus: status.upstreamStatus,
26
+ upstreamFetchedAt: status.upstreamFetchedAt,
27
+ upstreamFetchError: status.upstreamFetchError,
25
28
  dirty:
26
29
  status.staged > 0 ||
27
30
  status.modified > 0 ||
@@ -6,61 +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 interface GitRepoStatus extends GitRepoIdentity {
44
- branch: string | null;
45
- headCommit: string | null;
46
- headMessage: string | null;
47
- upstream: string | null;
48
- ahead: number;
49
- behind: number;
50
- staged: number;
51
- modified: number;
52
- untracked: number;
53
- deleted: number;
54
- renamed: number;
55
- hasConflicts: boolean;
56
- conflictFiles: string[];
57
- stashCount: number;
58
- lastCheckedAt: number;
59
- /** Submodule statuses when auto-discover is enabled */
60
- submodules?: GitSubmoduleStatus[];
61
- error?: string;
62
- reason?: GitFailureReason;
63
- }
23
+ import type { GitRepoIdentity, GitRepoStatus, GitFailureReason, GitUpstreamFreshness } from '@adhdev/mesh-shared';
64
24
 
65
25
  export type GitFileChangeStatus =
66
26
  | 'added'
@@ -134,6 +94,9 @@ export interface GitCompactSummary {
134
94
  isGitRepo: boolean;
135
95
  repoRoot: string | null;
136
96
  branch: string | null;
97
+ upstreamStatus: GitUpstreamFreshness;
98
+ upstreamFetchedAt?: number;
99
+ upstreamFetchError?: string;
137
100
  dirty: boolean;
138
101
  changedFiles: number;
139
102
  ahead: number;
@@ -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,