@sudocode-ai/local-server 0.1.7 → 0.1.9

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 (282) hide show
  1. package/README.md +6 -0
  2. package/dist/errors/agent-errors.d.ts +43 -0
  3. package/dist/errors/agent-errors.d.ts.map +1 -0
  4. package/dist/errors/agent-errors.js +69 -0
  5. package/dist/errors/agent-errors.js.map +1 -0
  6. package/dist/execution/adapters/claude-adapter.d.ts +63 -0
  7. package/dist/execution/adapters/claude-adapter.d.ts.map +1 -0
  8. package/dist/execution/adapters/claude-adapter.js +82 -0
  9. package/dist/execution/adapters/claude-adapter.js.map +1 -0
  10. package/dist/execution/adapters/codex-adapter.d.ts +67 -0
  11. package/dist/execution/adapters/codex-adapter.d.ts.map +1 -0
  12. package/dist/execution/adapters/codex-adapter.js +183 -0
  13. package/dist/execution/adapters/codex-adapter.js.map +1 -0
  14. package/dist/execution/adapters/codex-config-builder.d.ts +30 -0
  15. package/dist/execution/adapters/codex-config-builder.d.ts.map +1 -0
  16. package/dist/execution/adapters/codex-config-builder.js +110 -0
  17. package/dist/execution/adapters/codex-config-builder.js.map +1 -0
  18. package/dist/execution/adapters/copilot-adapter.d.ts +94 -0
  19. package/dist/execution/adapters/copilot-adapter.d.ts.map +1 -0
  20. package/dist/execution/adapters/copilot-adapter.js +163 -0
  21. package/dist/execution/adapters/copilot-adapter.js.map +1 -0
  22. package/dist/execution/adapters/copilot-config-builder.d.ts +48 -0
  23. package/dist/execution/adapters/copilot-config-builder.d.ts.map +1 -0
  24. package/dist/execution/adapters/copilot-config-builder.js +125 -0
  25. package/dist/execution/adapters/copilot-config-builder.js.map +1 -0
  26. package/dist/execution/adapters/cursor-adapter.d.ts +66 -0
  27. package/dist/execution/adapters/cursor-adapter.d.ts.map +1 -0
  28. package/dist/execution/adapters/cursor-adapter.js +121 -0
  29. package/dist/execution/adapters/cursor-adapter.js.map +1 -0
  30. package/dist/execution/adapters/cursor-config-builder.d.ts +29 -0
  31. package/dist/execution/adapters/cursor-config-builder.d.ts.map +1 -0
  32. package/dist/execution/adapters/cursor-config-builder.js +49 -0
  33. package/dist/execution/adapters/cursor-config-builder.js.map +1 -0
  34. package/dist/execution/adapters/shared/config-presets.d.ts +102 -0
  35. package/dist/execution/adapters/shared/config-presets.d.ts.map +1 -0
  36. package/dist/execution/adapters/shared/config-presets.js +205 -0
  37. package/dist/execution/adapters/shared/config-presets.js.map +1 -0
  38. package/dist/execution/adapters/shared/config-utils.d.ts +95 -0
  39. package/dist/execution/adapters/shared/config-utils.d.ts.map +1 -0
  40. package/dist/execution/adapters/shared/config-utils.js +163 -0
  41. package/dist/execution/adapters/shared/config-utils.js.map +1 -0
  42. package/dist/execution/adapters/shared/index.d.ts +8 -0
  43. package/dist/execution/adapters/shared/index.d.ts.map +1 -0
  44. package/dist/execution/adapters/shared/index.js +8 -0
  45. package/dist/execution/adapters/shared/index.js.map +1 -0
  46. package/dist/execution/executors/agent-executor-wrapper.d.ts +153 -0
  47. package/dist/execution/executors/agent-executor-wrapper.d.ts.map +1 -0
  48. package/dist/execution/executors/agent-executor-wrapper.js +652 -0
  49. package/dist/execution/executors/agent-executor-wrapper.js.map +1 -0
  50. package/dist/execution/executors/executor-factory.d.ts +95 -0
  51. package/dist/execution/executors/executor-factory.d.ts.map +1 -0
  52. package/dist/execution/executors/executor-factory.js +120 -0
  53. package/dist/execution/executors/executor-factory.js.map +1 -0
  54. package/dist/execution/output/ag-ui-adapter.d.ts +0 -2
  55. package/dist/execution/output/ag-ui-adapter.d.ts.map +1 -1
  56. package/dist/execution/output/ag-ui-adapter.js +0 -2
  57. package/dist/execution/output/ag-ui-adapter.js.map +1 -1
  58. package/dist/execution/output/index.d.ts +0 -3
  59. package/dist/execution/output/index.d.ts.map +1 -1
  60. package/dist/execution/output/index.js +0 -2
  61. package/dist/execution/output/index.js.map +1 -1
  62. package/dist/execution/output/normalized-to-ag-ui-adapter.d.ts +108 -0
  63. package/dist/execution/output/normalized-to-ag-ui-adapter.d.ts.map +1 -0
  64. package/dist/execution/output/normalized-to-ag-ui-adapter.js +321 -0
  65. package/dist/execution/output/normalized-to-ag-ui-adapter.js.map +1 -0
  66. package/dist/execution/process/builders/claude.d.ts +24 -57
  67. package/dist/execution/process/builders/claude.d.ts.map +1 -1
  68. package/dist/execution/process/builders/claude.js +153 -19
  69. package/dist/execution/process/builders/claude.js.map +1 -1
  70. package/dist/execution/transport/ipc-transport-manager.d.ts +74 -0
  71. package/dist/execution/transport/ipc-transport-manager.d.ts.map +1 -0
  72. package/dist/execution/transport/ipc-transport-manager.js +104 -0
  73. package/dist/execution/transport/ipc-transport-manager.js.map +1 -0
  74. package/dist/execution/transport/transport-manager.d.ts.map +1 -1
  75. package/dist/execution/transport/transport-manager.js +3 -0
  76. package/dist/execution/transport/transport-manager.js.map +1 -1
  77. package/dist/execution/worktree/conflict-detector.d.ts +85 -0
  78. package/dist/execution/worktree/conflict-detector.d.ts.map +1 -0
  79. package/dist/execution/worktree/conflict-detector.js +129 -0
  80. package/dist/execution/worktree/conflict-detector.js.map +1 -0
  81. package/dist/execution/worktree/git-cli.d.ts +9 -0
  82. package/dist/execution/worktree/git-cli.d.ts.map +1 -1
  83. package/dist/execution/worktree/git-cli.js +10 -0
  84. package/dist/execution/worktree/git-cli.js.map +1 -1
  85. package/dist/execution/worktree/git-sync-cli.d.ts +198 -0
  86. package/dist/execution/worktree/git-sync-cli.d.ts.map +1 -0
  87. package/dist/execution/worktree/git-sync-cli.js +401 -0
  88. package/dist/execution/worktree/git-sync-cli.js.map +1 -0
  89. package/dist/execution/worktree/manager.d.ts +18 -0
  90. package/dist/execution/worktree/manager.d.ts.map +1 -1
  91. package/dist/execution/worktree/manager.js +9 -3
  92. package/dist/execution/worktree/manager.js.map +1 -1
  93. package/dist/index.d.ts +1 -3
  94. package/dist/index.d.ts.map +1 -1
  95. package/dist/index.js +124 -229
  96. package/dist/index.js.map +1 -1
  97. package/dist/middleware/project-context.d.ts +37 -0
  98. package/dist/middleware/project-context.d.ts.map +1 -0
  99. package/dist/middleware/project-context.js +91 -0
  100. package/dist/middleware/project-context.js.map +1 -0
  101. package/dist/public/assets/index-DV9Tbujb.css +1 -0
  102. package/dist/public/assets/index-DcDX9-Ad.js +740 -0
  103. package/dist/public/assets/index-DcDX9-Ad.js.map +1 -0
  104. package/dist/public/assets/{react-vendor-ByUx1V_q.js → react-vendor-DiL5hC7l.js} +2 -2
  105. package/dist/public/assets/{react-vendor-ByUx1V_q.js.map → react-vendor-DiL5hC7l.js.map} +1 -1
  106. package/dist/public/assets/ui-vendor-B4WMPEfa.js +54 -0
  107. package/dist/public/assets/ui-vendor-B4WMPEfa.js.map +1 -0
  108. package/dist/public/index.html +4 -4
  109. package/dist/routes/agents.d.ts +3 -0
  110. package/dist/routes/agents.d.ts.map +1 -0
  111. package/dist/routes/agents.js +62 -0
  112. package/dist/routes/agents.js.map +1 -0
  113. package/dist/routes/config.d.ts +3 -0
  114. package/dist/routes/config.d.ts.map +1 -0
  115. package/dist/routes/config.js +25 -0
  116. package/dist/routes/config.js.map +1 -0
  117. package/dist/routes/editors.d.ts +15 -0
  118. package/dist/routes/editors.d.ts.map +1 -0
  119. package/dist/routes/editors.js +98 -0
  120. package/dist/routes/editors.js.map +1 -0
  121. package/dist/routes/executions-stream.d.ts +8 -5
  122. package/dist/routes/executions-stream.d.ts.map +1 -1
  123. package/dist/routes/executions-stream.js +10 -6
  124. package/dist/routes/executions-stream.js.map +1 -1
  125. package/dist/routes/executions.d.ts +6 -10
  126. package/dist/routes/executions.d.ts.map +1 -1
  127. package/dist/routes/executions.js +792 -37
  128. package/dist/routes/executions.js.map +1 -1
  129. package/dist/routes/feedback.d.ts +3 -2
  130. package/dist/routes/feedback.d.ts.map +1 -1
  131. package/dist/routes/feedback.js +12 -10
  132. package/dist/routes/feedback.js.map +1 -1
  133. package/dist/routes/files.d.ts +18 -0
  134. package/dist/routes/files.d.ts.map +1 -0
  135. package/dist/routes/files.js +89 -0
  136. package/dist/routes/files.js.map +1 -0
  137. package/dist/routes/issues.d.ts +3 -2
  138. package/dist/routes/issues.d.ts.map +1 -1
  139. package/dist/routes/issues.js +19 -18
  140. package/dist/routes/issues.js.map +1 -1
  141. package/dist/routes/projects.d.ts +11 -0
  142. package/dist/routes/projects.d.ts.map +1 -0
  143. package/dist/routes/projects.js +447 -0
  144. package/dist/routes/projects.js.map +1 -0
  145. package/dist/routes/relationships.d.ts +3 -2
  146. package/dist/routes/relationships.d.ts.map +1 -1
  147. package/dist/routes/relationships.js +12 -10
  148. package/dist/routes/relationships.js.map +1 -1
  149. package/dist/routes/repo-info.d.ts +3 -0
  150. package/dist/routes/repo-info.d.ts.map +1 -0
  151. package/dist/routes/repo-info.js +203 -0
  152. package/dist/routes/repo-info.js.map +1 -0
  153. package/dist/routes/specs.d.ts +3 -2
  154. package/dist/routes/specs.d.ts.map +1 -1
  155. package/dist/routes/specs.js +19 -18
  156. package/dist/routes/specs.js.map +1 -1
  157. package/dist/routes/version.d.ts +3 -0
  158. package/dist/routes/version.d.ts.map +1 -0
  159. package/dist/routes/version.js +25 -0
  160. package/dist/routes/version.js.map +1 -0
  161. package/dist/services/agent-registry.d.ts +140 -0
  162. package/dist/services/agent-registry.d.ts.map +1 -0
  163. package/dist/services/agent-registry.js +272 -0
  164. package/dist/services/agent-registry.js.map +1 -0
  165. package/dist/services/editor-service.d.ts +57 -0
  166. package/dist/services/editor-service.d.ts.map +1 -0
  167. package/dist/services/editor-service.js +204 -0
  168. package/dist/services/editor-service.js.map +1 -0
  169. package/dist/services/execution-changes-service.d.ts +110 -0
  170. package/dist/services/execution-changes-service.d.ts.map +1 -0
  171. package/dist/services/execution-changes-service.js +700 -0
  172. package/dist/services/execution-changes-service.js.map +1 -0
  173. package/dist/services/execution-lifecycle.d.ts +1 -0
  174. package/dist/services/execution-lifecycle.d.ts.map +1 -1
  175. package/dist/services/execution-lifecycle.js +37 -7
  176. package/dist/services/execution-lifecycle.js.map +1 -1
  177. package/dist/services/execution-logs-store.d.ts +75 -0
  178. package/dist/services/execution-logs-store.d.ts.map +1 -1
  179. package/dist/services/execution-logs-store.js +142 -2
  180. package/dist/services/execution-logs-store.js.map +1 -1
  181. package/dist/services/execution-service.d.ts +82 -59
  182. package/dist/services/execution-service.d.ts.map +1 -1
  183. package/dist/services/execution-service.js +514 -469
  184. package/dist/services/execution-service.js.map +1 -1
  185. package/dist/services/execution-worker-pool.d.ts +116 -0
  186. package/dist/services/execution-worker-pool.d.ts.map +1 -0
  187. package/dist/services/execution-worker-pool.js +326 -0
  188. package/dist/services/execution-worker-pool.js.map +1 -0
  189. package/dist/services/executions.d.ts +3 -0
  190. package/dist/services/executions.d.ts.map +1 -1
  191. package/dist/services/executions.js +11 -17
  192. package/dist/services/executions.js.map +1 -1
  193. package/dist/services/export.d.ts +8 -2
  194. package/dist/services/export.d.ts.map +1 -1
  195. package/dist/services/export.js +29 -23
  196. package/dist/services/export.js.map +1 -1
  197. package/dist/services/file-search/git-ls-files-strategy.d.ts +72 -0
  198. package/dist/services/file-search/git-ls-files-strategy.d.ts.map +1 -0
  199. package/dist/services/file-search/git-ls-files-strategy.js +176 -0
  200. package/dist/services/file-search/git-ls-files-strategy.js.map +1 -0
  201. package/dist/services/file-search/index.d.ts +9 -0
  202. package/dist/services/file-search/index.d.ts.map +1 -0
  203. package/dist/services/file-search/index.js +10 -0
  204. package/dist/services/file-search/index.js.map +1 -0
  205. package/dist/services/file-search/registry.d.ts +97 -0
  206. package/dist/services/file-search/registry.d.ts.map +1 -0
  207. package/dist/services/file-search/registry.js +140 -0
  208. package/dist/services/file-search/registry.js.map +1 -0
  209. package/dist/services/file-search/strategy.d.ts +58 -0
  210. package/dist/services/file-search/strategy.d.ts.map +1 -0
  211. package/dist/services/file-search/strategy.js +8 -0
  212. package/dist/services/file-search/strategy.js.map +1 -0
  213. package/dist/services/project-context.d.ts +69 -0
  214. package/dist/services/project-context.d.ts.map +1 -0
  215. package/dist/services/project-context.js +113 -0
  216. package/dist/services/project-context.js.map +1 -0
  217. package/dist/services/project-manager.d.ts +95 -0
  218. package/dist/services/project-manager.d.ts.map +1 -0
  219. package/dist/services/project-manager.js +388 -0
  220. package/dist/services/project-manager.js.map +1 -0
  221. package/dist/services/project-registry.d.ts +98 -0
  222. package/dist/services/project-registry.d.ts.map +1 -0
  223. package/dist/services/project-registry.js +289 -0
  224. package/dist/services/project-registry.js.map +1 -0
  225. package/dist/services/prompt-resolver.d.ts +97 -0
  226. package/dist/services/prompt-resolver.d.ts.map +1 -0
  227. package/dist/services/prompt-resolver.js +377 -0
  228. package/dist/services/prompt-resolver.js.map +1 -0
  229. package/dist/services/repo-info.d.ts +12 -0
  230. package/dist/services/repo-info.d.ts.map +1 -1
  231. package/dist/services/repo-info.js +46 -0
  232. package/dist/services/repo-info.js.map +1 -1
  233. package/dist/services/version-service.d.ts +14 -0
  234. package/dist/services/version-service.d.ts.map +1 -0
  235. package/dist/services/version-service.js +57 -0
  236. package/dist/services/version-service.js.map +1 -0
  237. package/dist/services/watcher.d.ts +3 -4
  238. package/dist/services/watcher.d.ts.map +1 -1
  239. package/dist/services/watcher.js +18 -35
  240. package/dist/services/watcher.js.map +1 -1
  241. package/dist/services/websocket.d.ts +30 -16
  242. package/dist/services/websocket.d.ts.map +1 -1
  243. package/dist/services/websocket.js +102 -37
  244. package/dist/services/websocket.js.map +1 -1
  245. package/dist/services/worktree-sync-service.d.ts +326 -0
  246. package/dist/services/worktree-sync-service.d.ts.map +1 -0
  247. package/dist/services/worktree-sync-service.js +1091 -0
  248. package/dist/services/worktree-sync-service.js.map +1 -0
  249. package/dist/types/editor.d.ts +49 -0
  250. package/dist/types/editor.d.ts.map +1 -0
  251. package/dist/types/editor.js +50 -0
  252. package/dist/types/editor.js.map +1 -0
  253. package/dist/types/project.d.ts +58 -0
  254. package/dist/types/project.d.ts.map +1 -0
  255. package/dist/types/project.js +10 -0
  256. package/dist/types/project.js.map +1 -0
  257. package/dist/utils/executable-check.d.ts +36 -0
  258. package/dist/utils/executable-check.d.ts.map +1 -0
  259. package/dist/utils/executable-check.js +79 -0
  260. package/dist/utils/executable-check.js.map +1 -0
  261. package/dist/workers/execution-worker.d.ts +18 -0
  262. package/dist/workers/execution-worker.d.ts.map +1 -0
  263. package/dist/workers/execution-worker.js +340 -0
  264. package/dist/workers/execution-worker.js.map +1 -0
  265. package/dist/workers/worker-ipc.d.ts +84 -0
  266. package/dist/workers/worker-ipc.d.ts.map +1 -0
  267. package/dist/workers/worker-ipc.js +29 -0
  268. package/dist/workers/worker-ipc.js.map +1 -0
  269. package/package.json +6 -5
  270. package/dist/execution/output/ag-ui-integration.d.ts +0 -96
  271. package/dist/execution/output/ag-ui-integration.d.ts.map +0 -1
  272. package/dist/execution/output/ag-ui-integration.js +0 -96
  273. package/dist/execution/output/ag-ui-integration.js.map +0 -1
  274. package/dist/execution/output/claude-code-output-processor.d.ts +0 -321
  275. package/dist/execution/output/claude-code-output-processor.d.ts.map +0 -1
  276. package/dist/execution/output/claude-code-output-processor.js +0 -769
  277. package/dist/execution/output/claude-code-output-processor.js.map +0 -1
  278. package/dist/public/assets/index-B3SEMufD.js +0 -580
  279. package/dist/public/assets/index-B3SEMufD.js.map +0 -1
  280. package/dist/public/assets/index-D2YGL3gX.css +0 -1
  281. package/dist/public/assets/ui-vendor-CotR6bx9.js +0 -54
  282. package/dist/public/assets/ui-vendor-CotR6bx9.js.map +0 -1
@@ -0,0 +1,1091 @@
1
+ /**
2
+ * Worktree Sync Service
3
+ *
4
+ * Orchestrates worktree sync operations including conflict detection,
5
+ * JSONL resolution, git operations, and database updates.
6
+ *
7
+ * @module services/worktree-sync-service
8
+ */
9
+ import * as fs from "fs";
10
+ import * as path from "path";
11
+ import { execSync } from "child_process";
12
+ import { GitSyncCli, } from "../execution/worktree/git-sync-cli.js";
13
+ import { ConflictDetector, } from "../execution/worktree/conflict-detector.js";
14
+ import { mergeThreeWay, hasGitConflictMarkers, parseMergeConflictFile, resolveEntities, } from "@sudocode-ai/cli/dist/merge-resolver.js";
15
+ import { writeJSONL } from "@sudocode-ai/cli/dist/jsonl.js";
16
+ /**
17
+ * Worktree sync error codes
18
+ */
19
+ export var WorktreeSyncErrorCode;
20
+ (function (WorktreeSyncErrorCode) {
21
+ WorktreeSyncErrorCode["NO_WORKTREE"] = "NO_WORKTREE";
22
+ WorktreeSyncErrorCode["WORKTREE_MISSING"] = "WORKTREE_MISSING";
23
+ WorktreeSyncErrorCode["BRANCH_MISSING"] = "BRANCH_MISSING";
24
+ WorktreeSyncErrorCode["DIRTY_WORKING_TREE"] = "DIRTY_WORKING_TREE";
25
+ WorktreeSyncErrorCode["TARGET_BRANCH_MISSING"] = "TARGET_BRANCH_MISSING";
26
+ WorktreeSyncErrorCode["NO_COMMON_BASE"] = "NO_COMMON_BASE";
27
+ WorktreeSyncErrorCode["CODE_CONFLICTS"] = "CODE_CONFLICTS";
28
+ WorktreeSyncErrorCode["MERGE_FAILED"] = "MERGE_FAILED";
29
+ WorktreeSyncErrorCode["JSONL_RESOLUTION_FAILED"] = "JSONL_RESOLUTION_FAILED";
30
+ WorktreeSyncErrorCode["DATABASE_SYNC_FAILED"] = "DATABASE_SYNC_FAILED";
31
+ WorktreeSyncErrorCode["EXECUTION_NOT_FOUND"] = "EXECUTION_NOT_FOUND";
32
+ })(WorktreeSyncErrorCode || (WorktreeSyncErrorCode = {}));
33
+ /**
34
+ * Worktree sync error class
35
+ */
36
+ export class WorktreeSyncError extends Error {
37
+ code;
38
+ cause;
39
+ constructor(message, code, cause) {
40
+ super(message);
41
+ this.code = code;
42
+ this.cause = cause;
43
+ this.name = "WorktreeSyncError";
44
+ }
45
+ }
46
+ /**
47
+ * WorktreeSyncService
48
+ *
49
+ * Main service class for orchestrating worktree sync operations
50
+ */
51
+ export class WorktreeSyncService {
52
+ db;
53
+ repoPath;
54
+ gitSync;
55
+ constructor(db, repoPath) {
56
+ this.db = db;
57
+ this.repoPath = repoPath;
58
+ this.gitSync = new GitSyncCli(repoPath);
59
+ }
60
+ /**
61
+ * Preview sync without making changes
62
+ *
63
+ * @param executionId - Execution ID to preview sync for
64
+ * @returns Preview result with conflicts, diff, and warnings
65
+ */
66
+ async previewSync(executionId) {
67
+ // 1. Load execution and validate
68
+ const execution = await this._loadAndValidateExecution(executionId);
69
+ // 2. Validate critical preconditions (ones that prevent us from getting any info)
70
+ // These are "hard" failures - we can't get diff/commits if these fail
71
+ const criticalPreconditionError = await this._validateCriticalPreconditions(execution);
72
+ if (criticalPreconditionError) {
73
+ return {
74
+ canSync: false,
75
+ conflicts: {
76
+ hasConflicts: false,
77
+ codeConflicts: [],
78
+ jsonlConflicts: [],
79
+ totalFiles: 0,
80
+ summary: "",
81
+ },
82
+ diff: { files: [], additions: 0, deletions: 0 },
83
+ commits: [],
84
+ mergeBase: "",
85
+ uncommittedJSONLChanges: [],
86
+ uncommittedChanges: { files: [], additions: 0, deletions: 0 },
87
+ executionStatus: execution.status,
88
+ warnings: [criticalPreconditionError],
89
+ };
90
+ }
91
+ // 3. Create ConflictDetector instance for worktree context
92
+ const worktreeConflictDetector = new ConflictDetector(execution.worktree_path);
93
+ // 4. Find merge base (use main repo since it has both branches)
94
+ const mergeBase = this.gitSync.getMergeBase(execution.branch_name, execution.target_branch);
95
+ // 4. Get commit list
96
+ const commits = this.gitSync.getCommitList(mergeBase, execution.branch_name);
97
+ // 6. Get diff summary (use main repo to see all changes)
98
+ const diff = this.gitSync.getDiff(mergeBase, execution.branch_name);
99
+ // 7. Detect conflicts (use worktree for conflict detection)
100
+ const conflicts = worktreeConflictDetector.detectConflicts(execution.branch_name, execution.target_branch);
101
+ // 7. Check for uncommitted changes in worktree (not included by default)
102
+ const uncommittedFiles = this._getUncommittedFiles(execution.worktree_path);
103
+ const uncommittedJSONL = uncommittedFiles.filter((file) => file.endsWith(".jsonl") &&
104
+ (file.includes(".sudocode/") || file.startsWith(".sudocode/")));
105
+ const uncommittedChanges = this._getUncommittedFileStats(execution.worktree_path);
106
+ // 8. Generate warnings and check "soft" preconditions
107
+ const warnings = [];
108
+ let canSync = true;
109
+ // Check if local working tree is clean (soft precondition - we can still show preview)
110
+ if (!this.gitSync.isWorkingTreeClean()) {
111
+ warnings.push("Local working tree has uncommitted changes. Stash or commit them first.");
112
+ canSync = false;
113
+ }
114
+ // Warn if execution is running/paused
115
+ if (execution.status === "running" || execution.status === "paused") {
116
+ warnings.push("Execution is currently active. Synced state may not reflect final execution result.");
117
+ }
118
+ // Warn about code conflicts
119
+ if (conflicts.codeConflicts.length > 0) {
120
+ warnings.push(`${conflicts.codeConflicts.length} code conflict(s) detected. Manual resolution may be required.`);
121
+ }
122
+ // Note about uncommitted files (not included by default)
123
+ // if (uncommittedChanges && uncommittedChanges.files.length > 0) {
124
+ // warnings.push(
125
+ // `${uncommittedChanges.files.length} uncommitted file(s) in worktree will NOT be included (only committed changes are synced).`
126
+ // );
127
+ // }
128
+ return {
129
+ canSync,
130
+ conflicts,
131
+ diff,
132
+ commits,
133
+ mergeBase,
134
+ uncommittedJSONLChanges: uncommittedJSONL,
135
+ uncommittedChanges,
136
+ executionStatus: execution.status,
137
+ warnings,
138
+ };
139
+ }
140
+ /**
141
+ * Validate critical preconditions that prevent us from getting any sync info
142
+ *
143
+ * These are "hard" failures - if these fail, we can't get diff/commits info.
144
+ * Returns an error message if validation fails, null if validation passes.
145
+ *
146
+ * @param execution - Execution to validate
147
+ * @returns Error message if validation fails, null if validation passes
148
+ */
149
+ async _validateCriticalPreconditions(execution) {
150
+ // 1. Check worktree path exists
151
+ if (!execution.worktree_path) {
152
+ return "No worktree path for execution";
153
+ }
154
+ // 2. Check worktree still exists on filesystem
155
+ if (!fs.existsSync(execution.worktree_path)) {
156
+ return "Worktree no longer exists";
157
+ }
158
+ // 3. Get list of branches
159
+ const branches = this._getBranches();
160
+ // 4. Check worktree branch exists
161
+ if (!branches.includes(execution.branch_name)) {
162
+ return `Worktree branch '${execution.branch_name}' not found`;
163
+ }
164
+ // 5. Check target branch exists
165
+ if (!branches.includes(execution.target_branch)) {
166
+ return `Target branch '${execution.target_branch}' not found`;
167
+ }
168
+ // 6. Verify branches have common base
169
+ try {
170
+ this.gitSync.getMergeBase(execution.branch_name, execution.target_branch);
171
+ }
172
+ catch (error) {
173
+ return "Worktree and target branch have diverged without common history";
174
+ }
175
+ return null;
176
+ }
177
+ /**
178
+ * Load execution from database and validate it exists
179
+ *
180
+ * Used by previewSync() and will be used in i-9gz4 (squash sync)
181
+ *
182
+ * @param executionId - Execution ID to load
183
+ * @returns Execution record
184
+ * @throws WorktreeSyncError if execution not found
185
+ */
186
+ async _loadAndValidateExecution(executionId) {
187
+ const stmt = this.db.prepare("SELECT * FROM executions WHERE id = ?");
188
+ const execution = stmt.get(executionId);
189
+ if (!execution) {
190
+ throw new WorktreeSyncError(`Execution ${executionId} not found`, WorktreeSyncErrorCode.EXECUTION_NOT_FOUND);
191
+ }
192
+ return execution;
193
+ }
194
+ /**
195
+ * Validate preconditions for sync
196
+ *
197
+ * Used by previewSync() and will be used in i-9gz4 (squash sync)
198
+ *
199
+ * Checks:
200
+ * - Worktree exists
201
+ * - Worktree branch exists
202
+ * - Local working tree is clean
203
+ * - Target branch exists
204
+ * - Branches have common base
205
+ *
206
+ * @param execution - Execution to validate
207
+ * @throws WorktreeSyncError if any precondition fails
208
+ */
209
+ async _validateSyncPreconditions(execution, options) {
210
+ const { skipDirtyWorkingTreeCheck = false } = options || {};
211
+ // 1. Check worktree path exists
212
+ if (!execution.worktree_path) {
213
+ throw new WorktreeSyncError("No worktree path for execution", WorktreeSyncErrorCode.NO_WORKTREE);
214
+ }
215
+ // 2. Check worktree still exists on filesystem
216
+ if (!fs.existsSync(execution.worktree_path)) {
217
+ throw new WorktreeSyncError("Worktree no longer exists", WorktreeSyncErrorCode.WORKTREE_MISSING);
218
+ }
219
+ // 3. Get list of branches
220
+ const branches = this._getBranches();
221
+ // 4. Check worktree branch exists
222
+ if (!branches.includes(execution.branch_name)) {
223
+ throw new WorktreeSyncError(`Worktree branch '${execution.branch_name}' not found`, WorktreeSyncErrorCode.BRANCH_MISSING);
224
+ }
225
+ // 5. Check target branch exists
226
+ if (!branches.includes(execution.target_branch)) {
227
+ throw new WorktreeSyncError(`Target branch '${execution.target_branch}' not found`, WorktreeSyncErrorCode.TARGET_BRANCH_MISSING);
228
+ }
229
+ // 6. Check local working tree is clean (skip for stage mode since it doesn't commit)
230
+ if (!skipDirtyWorkingTreeCheck && !this.gitSync.isWorkingTreeClean()) {
231
+ throw new WorktreeSyncError("Local working tree has uncommitted changes. Stash or commit them first.", WorktreeSyncErrorCode.DIRTY_WORKING_TREE);
232
+ }
233
+ // 7. Verify branches have common base
234
+ try {
235
+ this.gitSync.getMergeBase(execution.branch_name, execution.target_branch);
236
+ }
237
+ catch (error) {
238
+ throw new WorktreeSyncError("Worktree and target branch have diverged without common history", WorktreeSyncErrorCode.NO_COMMON_BASE, error);
239
+ }
240
+ }
241
+ /**
242
+ * Create safety snapshot before sync
243
+ *
244
+ * Creates a git tag pointing to current target branch HEAD
245
+ * for rollback capability
246
+ *
247
+ * @param executionId - Execution ID
248
+ * @param targetBranch - Target branch name
249
+ * @returns Tag name created
250
+ */
251
+ async _createSafetySnapshot(executionId, targetBranch) {
252
+ const tagName = `sudocode-sync-before-${executionId}`;
253
+ // Get current commit of target branch
254
+ const currentCommit = this._getCurrentCommit(targetBranch);
255
+ // Create annotated tag
256
+ this.gitSync.createSafetyTag(tagName, currentCommit);
257
+ return tagName;
258
+ }
259
+ /**
260
+ * Get all uncommitted files from worktree
261
+ *
262
+ * @param worktreePath - Path to worktree
263
+ * @returns Array of all uncommitted file paths
264
+ */
265
+ _getUncommittedFiles(worktreePath) {
266
+ const gitSyncWorktree = new GitSyncCli(worktreePath);
267
+ return gitSyncWorktree.getUncommittedFiles();
268
+ }
269
+ /**
270
+ * Get uncommitted file stats from worktree
271
+ *
272
+ * Returns list of files and aggregate additions/deletions stats
273
+ * for uncommitted changes in the worktree.
274
+ *
275
+ * @param worktreePath - Path to worktree
276
+ * @returns Uncommitted file stats
277
+ */
278
+ _getUncommittedFileStats(worktreePath) {
279
+ try {
280
+ // Get modified files
281
+ const modifiedOutput = execSync("git diff --numstat", {
282
+ cwd: worktreePath,
283
+ encoding: "utf8",
284
+ stdio: "pipe",
285
+ });
286
+ // Get untracked files
287
+ const untrackedFiles = execSync("git ls-files --others --exclude-standard", {
288
+ cwd: worktreePath,
289
+ encoding: "utf8",
290
+ stdio: "pipe",
291
+ })
292
+ .split("\n")
293
+ .filter((line) => line.trim().length > 0);
294
+ // Parse modified file stats
295
+ let additions = 0;
296
+ let deletions = 0;
297
+ const modifiedFiles = [];
298
+ for (const line of modifiedOutput.split("\n")) {
299
+ if (!line.trim())
300
+ continue;
301
+ const parts = line.split("\t");
302
+ if (parts.length >= 3) {
303
+ const add = parts[0] === "-" ? 0 : parseInt(parts[0], 10) || 0;
304
+ const del = parts[1] === "-" ? 0 : parseInt(parts[1], 10) || 0;
305
+ additions += add;
306
+ deletions += del;
307
+ modifiedFiles.push(parts[2]);
308
+ }
309
+ }
310
+ // Count lines in untracked files as additions
311
+ for (const filePath of untrackedFiles) {
312
+ try {
313
+ const fullPath = path.join(worktreePath, filePath);
314
+ if (fs.existsSync(fullPath) && fs.statSync(fullPath).isFile()) {
315
+ const content = fs.readFileSync(fullPath, "utf-8");
316
+ additions += content.split("\n").length;
317
+ }
318
+ }
319
+ catch (e) {
320
+ // Skip files we can't read
321
+ }
322
+ }
323
+ // Combine all files
324
+ const allFiles = [...new Set([...modifiedFiles, ...untrackedFiles])];
325
+ return {
326
+ files: allFiles,
327
+ additions,
328
+ deletions,
329
+ };
330
+ }
331
+ catch (error) {
332
+ // Return empty stats on error
333
+ return {
334
+ files: [],
335
+ additions: 0,
336
+ deletions: 0,
337
+ };
338
+ }
339
+ }
340
+ /**
341
+ * Check if local working tree is clean
342
+ *
343
+ * TODO: Will be used in i-7ya6 (sync preview)
344
+ *
345
+ * @returns true if clean, false if dirty
346
+ */
347
+ // @ts-expect-error - Foundation method, will be used in i-7ya6
348
+ _isLocalTreeClean() {
349
+ return this.gitSync.isWorkingTreeClean();
350
+ }
351
+ /**
352
+ * Get list of branches in repository
353
+ *
354
+ * @returns Array of branch names
355
+ */
356
+ _getBranches() {
357
+ try {
358
+ const output = execSync("git branch --format='%(refname:short)'", {
359
+ cwd: this.repoPath,
360
+ encoding: "utf8",
361
+ stdio: "pipe",
362
+ shell: "/bin/bash",
363
+ });
364
+ return output
365
+ .split("\n")
366
+ .map((line) => line.trim())
367
+ .filter((line) => line.length > 0);
368
+ }
369
+ catch (error) {
370
+ throw new WorktreeSyncError(`Failed to get branch list: ${error.message}`, WorktreeSyncErrorCode.BRANCH_MISSING, error);
371
+ }
372
+ }
373
+ /**
374
+ * Get current commit SHA for a branch
375
+ *
376
+ * @param branchName - Branch name
377
+ * @returns Commit SHA
378
+ */
379
+ _getCurrentCommit(branchName) {
380
+ try {
381
+ const output = execSync(`git rev-parse ${this._escapeShellArg(branchName)}`, {
382
+ cwd: this.repoPath,
383
+ encoding: "utf8",
384
+ stdio: "pipe",
385
+ });
386
+ return output.trim();
387
+ }
388
+ catch (error) {
389
+ throw new WorktreeSyncError(`Failed to get commit for branch ${branchName}: ${error.message}`, WorktreeSyncErrorCode.BRANCH_MISSING, error);
390
+ }
391
+ }
392
+ /**
393
+ * Escape shell argument for safe command execution
394
+ *
395
+ * @param arg - Argument to escape
396
+ * @returns Escaped argument
397
+ */
398
+ _escapeShellArg(arg) {
399
+ // Escape single quotes and wrap in single quotes
400
+ return `'${arg.replace(/'/g, "'\\''")}'`;
401
+ }
402
+ /**
403
+ * Resolve JSONL conflicts using three-way merge
404
+ *
405
+ * Used in squash sync to auto-resolve JSONL conflicts
406
+ *
407
+ * @param execution - Execution record
408
+ * @param jsonlConflicts - List of JSONL conflicts to resolve
409
+ * @throws WorktreeSyncError if resolution fails
410
+ */
411
+ async resolveJSONLConflicts(execution, jsonlConflicts) {
412
+ if (jsonlConflicts.length === 0) {
413
+ return;
414
+ }
415
+ const mergeBase = this.gitSync.getMergeBase(execution.branch_name, execution.target_branch);
416
+ for (const conflict of jsonlConflicts) {
417
+ try {
418
+ // Read three versions of the file
419
+ const baseVersion = await this._readJSONLVersion(conflict.filePath, mergeBase);
420
+ const ourVersion = await this._readJSONLVersion(conflict.filePath, execution.target_branch);
421
+ const theirVersion = await this._readJSONLVersion(conflict.filePath, execution.branch_name);
422
+ // Perform three-way merge
423
+ const { entities: merged } = mergeThreeWay(baseVersion, ourVersion, theirVersion);
424
+ // Write resolved version to local repo
425
+ const resolvedPath = path.join(this.repoPath, conflict.filePath);
426
+ await writeJSONL(resolvedPath, merged);
427
+ // Stage the resolved file
428
+ execSync(`git add ${this._escapeShellArg(conflict.filePath)}`, {
429
+ cwd: this.repoPath,
430
+ stdio: "pipe",
431
+ });
432
+ }
433
+ catch (error) {
434
+ throw new WorktreeSyncError(`Failed to resolve JSONL conflict in ${conflict.filePath}: ${error.message}`, WorktreeSyncErrorCode.JSONL_RESOLUTION_FAILED, error);
435
+ }
436
+ }
437
+ }
438
+ /**
439
+ * Read JSONL file at a specific git revision
440
+ *
441
+ * @param filePath - Relative path to JSONL file
442
+ * @param revision - Git revision (commit SHA or branch name)
443
+ * @returns Array of JSONL entities
444
+ */
445
+ async _readJSONLVersion(filePath, revision) {
446
+ try {
447
+ // Get file content at revision using git show
448
+ const content = execSync(`git show ${this._escapeShellArg(revision)}:${this._escapeShellArg(filePath)}`, {
449
+ cwd: this.repoPath,
450
+ encoding: "utf8",
451
+ stdio: "pipe",
452
+ });
453
+ // Parse JSONL content
454
+ const lines = content
455
+ .split("\n")
456
+ .filter((line) => line.trim().length > 0);
457
+ return lines.map((line) => JSON.parse(line));
458
+ }
459
+ catch (error) {
460
+ // File might not exist at this revision (new file)
461
+ if (error.status === 128) {
462
+ return [];
463
+ }
464
+ throw error;
465
+ }
466
+ }
467
+ /**
468
+ * Commit uncommitted JSONL files in worktree
469
+ *
470
+ * Used during sync to include uncommitted JSONL changes
471
+ *
472
+ * @param worktreePath - Path to worktree
473
+ * @param uncommittedFiles - List of uncommitted JSONL file paths
474
+ * @throws WorktreeSyncError if commit fails
475
+ */
476
+ async commitUncommittedJSONL(worktreePath, uncommittedFiles) {
477
+ if (uncommittedFiles.length === 0) {
478
+ return;
479
+ }
480
+ try {
481
+ // Stage all uncommitted JSONL files
482
+ for (const file of uncommittedFiles) {
483
+ execSync(`git add ${this._escapeShellArg(file)}`, {
484
+ cwd: worktreePath,
485
+ stdio: "pipe",
486
+ });
487
+ }
488
+ // Create commit with descriptive message
489
+ const fileList = uncommittedFiles.join(", ");
490
+ const message = `Auto-commit uncommitted JSONL changes before sync\n\nFiles: ${fileList}`;
491
+ execSync(`git commit -m ${this._escapeShellArg(message)}`, {
492
+ cwd: worktreePath,
493
+ stdio: "pipe",
494
+ });
495
+ }
496
+ catch (error) {
497
+ throw new WorktreeSyncError(`Failed to commit uncommitted JSONL files: ${error.message}`, WorktreeSyncErrorCode.DATABASE_SYNC_FAILED, error);
498
+ }
499
+ }
500
+ /**
501
+ * Perform git merge --squash operation, allowing conflicts
502
+ *
503
+ * This method doesn't throw on conflicts.
504
+ * Instead, it returns information about whether conflicts occurred.
505
+ *
506
+ * @param sourceBranch - Branch to merge from (worktree branch)
507
+ * @param targetBranch - Branch to merge into
508
+ * @returns Object with filesChanged count and hasConflicts flag
509
+ */
510
+ _performSquashMergeAllowConflicts(sourceBranch, targetBranch) {
511
+ // Checkout target branch
512
+ execSync(`git checkout ${this._escapeShellArg(targetBranch)}`, {
513
+ cwd: this.repoPath,
514
+ stdio: "pipe",
515
+ });
516
+ // Perform squash merge - may fail with conflicts
517
+ let hasConflicts = false;
518
+ try {
519
+ execSync(`git merge --squash ${this._escapeShellArg(sourceBranch)}`, {
520
+ cwd: this.repoPath,
521
+ stdio: "pipe",
522
+ });
523
+ }
524
+ catch (error) {
525
+ // Check if this is a conflict situation (exit code 1) or a real error
526
+ // git merge --squash returns 1 on conflicts but stages what it can
527
+ hasConflicts = true;
528
+ }
529
+ // Count staged files (including conflicted ones)
530
+ const statusOutput = execSync("git diff --cached --name-only", {
531
+ cwd: this.repoPath,
532
+ encoding: "utf8",
533
+ stdio: "pipe",
534
+ });
535
+ const filesChanged = statusOutput
536
+ .split("\n")
537
+ .filter((line) => line.trim().length > 0).length;
538
+ // Check for actual conflicts in the working tree
539
+ try {
540
+ const conflictCheck = execSync("git diff --name-only --diff-filter=U", {
541
+ cwd: this.repoPath,
542
+ encoding: "utf8",
543
+ stdio: "pipe",
544
+ });
545
+ hasConflicts = conflictCheck.trim().length > 0;
546
+ }
547
+ catch (e) {
548
+ // If this fails, assume no conflicts
549
+ }
550
+ return { filesChanged, hasConflicts };
551
+ }
552
+ /**
553
+ * Copy uncommitted files from worktree to local repo
554
+ *
555
+ * Copies files that are modified or untracked in the worktree
556
+ * to the local repository working directory.
557
+ *
558
+ * @param worktreePath - Path to the worktree
559
+ * @returns Number of files copied
560
+ */
561
+ async _copyUncommittedFiles(worktreePath) {
562
+ // Get list of uncommitted/untracked files in worktree
563
+ const modifiedOutput = execSync("git diff --name-only", {
564
+ cwd: worktreePath,
565
+ encoding: "utf8",
566
+ stdio: "pipe",
567
+ });
568
+ const untrackedOutput = execSync("git ls-files --others --exclude-standard", {
569
+ cwd: worktreePath,
570
+ encoding: "utf8",
571
+ stdio: "pipe",
572
+ });
573
+ const modifiedFiles = modifiedOutput
574
+ .split("\n")
575
+ .filter((line) => line.trim().length > 0);
576
+ const untrackedFiles = untrackedOutput
577
+ .split("\n")
578
+ .filter((line) => line.trim().length > 0);
579
+ const allFiles = [...new Set([...modifiedFiles, ...untrackedFiles])];
580
+ if (allFiles.length === 0) {
581
+ return 0;
582
+ }
583
+ // Copy each file from worktree to local repo
584
+ let filesCopied = 0;
585
+ for (const filePath of allFiles) {
586
+ const srcPath = path.join(worktreePath, filePath);
587
+ const destPath = path.join(this.repoPath, filePath);
588
+ // Check if source file exists
589
+ if (!fs.existsSync(srcPath)) {
590
+ continue;
591
+ }
592
+ // Create destination directory if needed
593
+ const destDir = path.dirname(destPath);
594
+ if (!fs.existsSync(destDir)) {
595
+ fs.mkdirSync(destDir, { recursive: true });
596
+ }
597
+ // Copy file
598
+ fs.copyFileSync(srcPath, destPath);
599
+ // Stage the file
600
+ execSync(`git add ${this._escapeShellArg(filePath)}`, {
601
+ cwd: this.repoPath,
602
+ stdio: "pipe",
603
+ });
604
+ filesCopied++;
605
+ }
606
+ return filesCopied;
607
+ }
608
+ /**
609
+ * Resolve JSONL merge conflicts in the local repository
610
+ *
611
+ * Checks for git conflict markers in issues.jsonl and specs.jsonl,
612
+ * and resolves them using the merge-resolver logic.
613
+ *
614
+ * @returns Number of files resolved
615
+ */
616
+ async _resolveJSONLConflicts() {
617
+ const sudocodePath = path.join(this.repoPath, ".sudocode");
618
+ const issuesPath = path.join(sudocodePath, "issues.jsonl");
619
+ const specsPath = path.join(sudocodePath, "specs.jsonl");
620
+ let filesResolved = 0;
621
+ // Check and resolve issues.jsonl
622
+ if (fs.existsSync(issuesPath) && hasGitConflictMarkers(issuesPath)) {
623
+ await this._resolveJSONLFile(issuesPath);
624
+ filesResolved++;
625
+ }
626
+ // Check and resolve specs.jsonl
627
+ if (fs.existsSync(specsPath) && hasGitConflictMarkers(specsPath)) {
628
+ await this._resolveJSONLFile(specsPath);
629
+ filesResolved++;
630
+ }
631
+ return filesResolved;
632
+ }
633
+ /**
634
+ * Resolve conflicts in a single JSONL file
635
+ *
636
+ * @param filePath - Path to the JSONL file with conflicts
637
+ */
638
+ async _resolveJSONLFile(filePath) {
639
+ // Read file with conflict markers
640
+ const content = fs.readFileSync(filePath, "utf8");
641
+ // Parse conflicts
642
+ const sections = parseMergeConflictFile(content);
643
+ // Extract all entities (from both clean and conflict sections)
644
+ const allEntities = [];
645
+ for (const section of sections) {
646
+ if (section.type === "clean") {
647
+ for (const line of section.lines) {
648
+ if (line.trim()) {
649
+ try {
650
+ allEntities.push(JSON.parse(line));
651
+ }
652
+ catch {
653
+ // Skip malformed lines
654
+ }
655
+ }
656
+ }
657
+ }
658
+ else {
659
+ // Conflict section - include both ours and theirs
660
+ for (const line of [
661
+ ...(section.ours || []),
662
+ ...(section.theirs || []),
663
+ ]) {
664
+ if (line.trim()) {
665
+ try {
666
+ allEntities.push(JSON.parse(line));
667
+ }
668
+ catch {
669
+ // Skip malformed lines
670
+ }
671
+ }
672
+ }
673
+ }
674
+ }
675
+ // Resolve conflicts
676
+ const { entities: resolved } = resolveEntities(allEntities, {
677
+ verbose: false,
678
+ });
679
+ // Write back resolved entities
680
+ await writeJSONL(filePath, resolved);
681
+ // Stage the resolved file
682
+ const relativePath = path.relative(this.repoPath, filePath);
683
+ execSync(`git add ${this._escapeShellArg(relativePath)}`, {
684
+ cwd: this.repoPath,
685
+ stdio: "pipe",
686
+ });
687
+ }
688
+ /**
689
+ * Generate commit message for squash sync
690
+ *
691
+ * @param execution - Execution record
692
+ * @param commitCount - Number of commits being squashed
693
+ * @returns Generated commit message
694
+ */
695
+ _generateCommitMessage(execution, commitCount) {
696
+ const issueId = execution.issue_id || "unknown";
697
+ const branchName = execution.branch_name;
698
+ return `Squash merge from ${branchName} (${commitCount} commit${commitCount !== 1 ? "s" : ""})
699
+
700
+ Issue: ${issueId}
701
+ Execution: ${execution.id}
702
+
703
+ Synced changes from worktree execution.`;
704
+ }
705
+ /**
706
+ * Create commit with staged changes
707
+ *
708
+ * @param message - Commit message
709
+ * @returns Commit SHA
710
+ * @throws WorktreeSyncError if commit fails
711
+ */
712
+ _createCommit(message) {
713
+ try {
714
+ execSync(`git commit -m ${this._escapeShellArg(message)}`, {
715
+ cwd: this.repoPath,
716
+ stdio: "pipe",
717
+ });
718
+ // Get the commit SHA
719
+ const sha = execSync("git rev-parse HEAD", {
720
+ cwd: this.repoPath,
721
+ encoding: "utf8",
722
+ stdio: "pipe",
723
+ }).trim();
724
+ return sha;
725
+ }
726
+ catch (error) {
727
+ throw new WorktreeSyncError(`Failed to create commit: ${error.message}`, WorktreeSyncErrorCode.MERGE_FAILED, error);
728
+ }
729
+ }
730
+ /**
731
+ * Rollback to safety snapshot
732
+ *
733
+ * @param targetBranch - Target branch to reset
734
+ * @param tagName - Safety tag to rollback to
735
+ * @throws WorktreeSyncError if rollback fails
736
+ */
737
+ async _rollbackToSnapshot(targetBranch, tagName) {
738
+ try {
739
+ // Checkout target branch
740
+ execSync(`git checkout ${this._escapeShellArg(targetBranch)}`, {
741
+ cwd: this.repoPath,
742
+ stdio: "pipe",
743
+ });
744
+ // Reset to tag
745
+ execSync(`git reset --hard ${this._escapeShellArg(tagName)}`, {
746
+ cwd: this.repoPath,
747
+ stdio: "pipe",
748
+ });
749
+ console.log(`Rolled back ${targetBranch} to ${tagName}`);
750
+ }
751
+ catch (error) {
752
+ throw new WorktreeSyncError(`Failed to rollback to snapshot ${tagName}: ${error.message}`, WorktreeSyncErrorCode.MERGE_FAILED, error);
753
+ }
754
+ }
755
+ /**
756
+ * Perform squash sync operation
757
+ *
758
+ * Squashes all committed worktree changes into a single commit on the target branch.
759
+ * Only includes committed changes - uncommitted changes are excluded.
760
+ * If merge conflicts occur, they are left for the user to resolve manually.
761
+ *
762
+ * @param executionId - Execution ID to sync
763
+ * @param customCommitMessage - Optional custom commit message
764
+ * @returns Sync result with details
765
+ * @throws WorktreeSyncError if sync fails
766
+ */
767
+ async squashSync(executionId, customCommitMessage) {
768
+ // 1. Load and validate execution
769
+ const execution = await this._loadAndValidateExecution(executionId);
770
+ // 2. Validate preconditions
771
+ await this._validateSyncPreconditions(execution);
772
+ // 3. Preview sync to get info (we'll proceed even with conflicts)
773
+ const preview = await this.previewSync(executionId);
774
+ // 4. Check if there are any commits to merge
775
+ if (preview.commits.length === 0) {
776
+ return {
777
+ success: false,
778
+ filesChanged: 0,
779
+ error: "No commits to merge. Only committed changes are included in sync.",
780
+ };
781
+ }
782
+ // 5. Check if worktree branch is already merged into target
783
+ if (this._isAncestor(execution.branch_name, execution.target_branch)) {
784
+ return {
785
+ success: false,
786
+ filesChanged: 0,
787
+ error: "Target branch is already up to date with worktree changes. Nothing to merge.",
788
+ };
789
+ }
790
+ let safetyTag;
791
+ try {
792
+ // 6. Create safety snapshot (before any changes)
793
+ safetyTag = await this._createSafetySnapshot(executionId, execution.target_branch);
794
+ // 7. Perform git merge --squash (may have conflicts)
795
+ const mergeResult = this._performSquashMergeAllowConflicts(execution.branch_name, execution.target_branch);
796
+ // 8. Check if there are unresolved conflicts
797
+ if (mergeResult.hasConflicts) {
798
+ // Return with conflicts info - user must resolve manually
799
+ return {
800
+ success: false,
801
+ filesChanged: mergeResult.filesChanged,
802
+ hasConflicts: true,
803
+ error: "Merge conflicts detected. Please resolve them manually and commit.",
804
+ cleanupOffered: false,
805
+ };
806
+ }
807
+ // 9. Generate commit message
808
+ const commitMessage = customCommitMessage ||
809
+ this._generateCommitMessage(execution, preview.commits.length);
810
+ // 10. Create commit
811
+ const finalCommit = this._createCommit(commitMessage);
812
+ // 11. Return success result
813
+ return {
814
+ success: true,
815
+ finalCommit,
816
+ filesChanged: mergeResult.filesChanged,
817
+ cleanupOffered: true,
818
+ };
819
+ }
820
+ catch (error) {
821
+ // Rollback to safety snapshot on failure
822
+ if (safetyTag) {
823
+ try {
824
+ await this._rollbackToSnapshot(execution.target_branch, safetyTag);
825
+ }
826
+ catch (rollbackError) {
827
+ console.error(`Failed to rollback to snapshot ${safetyTag}:`, rollbackError);
828
+ }
829
+ }
830
+ throw new WorktreeSyncError(`Squash sync failed: ${error.message}`, WorktreeSyncErrorCode.MERGE_FAILED, error);
831
+ }
832
+ }
833
+ /**
834
+ * Perform stage sync operation
835
+ *
836
+ * Applies committed worktree changes to the working directory without committing.
837
+ * Changes are left staged, ready for the user to commit manually.
838
+ * Only includes committed changes by default - uncommitted changes are excluded
839
+ * unless includeUncommitted is true.
840
+ *
841
+ * @param executionId - Execution ID to sync
842
+ * @param options - Optional settings
843
+ * @param options.includeUncommitted - If true, also copy uncommitted files from worktree
844
+ * @returns Sync result with details
845
+ * @throws WorktreeSyncError if sync fails
846
+ */
847
+ async stageSync(executionId, options) {
848
+ const { includeUncommitted = false } = options || {};
849
+ // 1. Load and validate execution
850
+ const execution = await this._loadAndValidateExecution(executionId);
851
+ // 2. Validate preconditions (skip dirty working tree check - stage mode doesn't commit)
852
+ await this._validateSyncPreconditions(execution, {
853
+ skipDirtyWorkingTreeCheck: true,
854
+ });
855
+ // 3. Preview sync to get info
856
+ const preview = await this.previewSync(executionId);
857
+ // 4. Check if there's anything to sync
858
+ const hasCommits = preview.commits.length > 0;
859
+ if (!hasCommits && !includeUncommitted) {
860
+ return {
861
+ success: false,
862
+ filesChanged: 0,
863
+ error: "No commits to merge. Only committed changes are included in sync.",
864
+ };
865
+ }
866
+ let safetyTag;
867
+ try {
868
+ // 5. Create safety snapshot (before any changes)
869
+ safetyTag = await this._createSafetySnapshot(executionId, execution.target_branch);
870
+ let filesChanged = 0;
871
+ let hasConflicts = false;
872
+ // 6. Perform git merge --squash for committed changes (if any)
873
+ if (hasCommits) {
874
+ const mergeResult = this._performSquashMergeAllowConflicts(execution.branch_name, execution.target_branch);
875
+ filesChanged = mergeResult.filesChanged;
876
+ hasConflicts = mergeResult.hasConflicts;
877
+ }
878
+ // 7. Copy uncommitted files from worktree if requested
879
+ let uncommittedFilesCopied = 0;
880
+ if (includeUncommitted && execution.worktree_path) {
881
+ uncommittedFilesCopied = await this._copyUncommittedFiles(execution.worktree_path);
882
+ filesChanged += uncommittedFilesCopied;
883
+ }
884
+ // 8. Auto-resolve JSONL conflicts if any
885
+ const jsonlFilesResolved = await this._resolveJSONLConflicts();
886
+ if (jsonlFilesResolved > 0) {
887
+ // Re-check for remaining conflicts after JSONL resolution
888
+ try {
889
+ const conflictCheck = execSync("git diff --name-only --diff-filter=U", {
890
+ cwd: this.repoPath,
891
+ encoding: "utf8",
892
+ stdio: "pipe",
893
+ });
894
+ hasConflicts = conflictCheck.trim().length > 0;
895
+ }
896
+ catch {
897
+ // If command fails, assume no conflicts
898
+ hasConflicts = false;
899
+ }
900
+ }
901
+ // 9. Check if there are unresolved (non-JSONL) conflicts
902
+ if (hasConflicts) {
903
+ return {
904
+ success: false,
905
+ filesChanged,
906
+ hasConflicts: true,
907
+ uncommittedFilesIncluded: uncommittedFilesCopied,
908
+ error: "Merge conflicts detected. Please resolve them manually.",
909
+ cleanupOffered: false,
910
+ };
911
+ }
912
+ // 10. Return success result WITHOUT creating a commit
913
+ // Changes remain staged for user to commit manually
914
+ return {
915
+ success: true,
916
+ filesChanged,
917
+ uncommittedFilesIncluded: uncommittedFilesCopied,
918
+ cleanupOffered: true,
919
+ };
920
+ }
921
+ catch (error) {
922
+ // Rollback to safety snapshot on failure
923
+ if (safetyTag) {
924
+ try {
925
+ await this._rollbackToSnapshot(execution.target_branch, safetyTag);
926
+ }
927
+ catch (rollbackError) {
928
+ console.error(`Failed to rollback to snapshot ${safetyTag}:`, rollbackError);
929
+ }
930
+ }
931
+ throw new WorktreeSyncError(`Stage sync failed: ${error.message}`, WorktreeSyncErrorCode.MERGE_FAILED, error);
932
+ }
933
+ }
934
+ /**
935
+ * Check if worktree branch is an ancestor of target branch
936
+ *
937
+ * If the worktree branch is an ancestor, it means target already has all
938
+ * the commits from the worktree (e.g., via a previous sync).
939
+ *
940
+ * @param worktreeBranch - Worktree branch name
941
+ * @param targetBranch - Target branch name
942
+ * @returns true if worktree branch is an ancestor of target branch
943
+ */
944
+ _isAncestor(worktreeBranch, targetBranch) {
945
+ try {
946
+ execSync(`git merge-base --is-ancestor ${this._escapeShellArg(worktreeBranch)} ${this._escapeShellArg(targetBranch)}`, {
947
+ cwd: this.repoPath,
948
+ stdio: "pipe",
949
+ });
950
+ // Exit code 0 means worktreeBranch IS an ancestor of targetBranch
951
+ return true;
952
+ }
953
+ catch {
954
+ // Exit code 1 means NOT an ancestor, which is what we want for merging
955
+ return false;
956
+ }
957
+ }
958
+ /**
959
+ * Perform preserve sync operation
960
+ *
961
+ * Merges all commits from worktree branch to target branch, preserving commit history.
962
+ * Only includes committed changes - uncommitted changes are excluded.
963
+ * If merge conflicts occur, they are left for the user to resolve manually.
964
+ *
965
+ * @param executionId - Execution ID to sync
966
+ * @returns Sync result with details
967
+ * @throws WorktreeSyncError if sync fails
968
+ */
969
+ async preserveSync(executionId) {
970
+ // 1. Load and validate execution
971
+ const execution = await this._loadAndValidateExecution(executionId);
972
+ // 2. Validate preconditions
973
+ await this._validateSyncPreconditions(execution);
974
+ // 3. Preview sync to get info
975
+ const preview = await this.previewSync(executionId);
976
+ // 4. Check if there are any commits to merge
977
+ if (preview.commits.length === 0) {
978
+ return {
979
+ success: false,
980
+ filesChanged: 0,
981
+ error: "No commits to merge. Only committed changes are included in sync.",
982
+ };
983
+ }
984
+ // 5. Check if worktree branch is already merged into target
985
+ // This happens if a previous sync (squash or preserve) already merged these commits
986
+ if (this._isAncestor(execution.branch_name, execution.target_branch)) {
987
+ return {
988
+ success: false,
989
+ filesChanged: 0,
990
+ error: "Target branch is already up to date with worktree changes. Nothing to merge.",
991
+ };
992
+ }
993
+ let safetyTag;
994
+ try {
995
+ // 6. Create safety snapshot (before any changes)
996
+ safetyTag = await this._createSafetySnapshot(executionId, execution.target_branch);
997
+ // 7. Checkout target branch
998
+ execSync(`git checkout ${this._escapeShellArg(execution.target_branch)}`, {
999
+ cwd: this.repoPath,
1000
+ stdio: "pipe",
1001
+ });
1002
+ // 8. Perform regular merge (preserves commit history)
1003
+ let hasConflicts = false;
1004
+ let filesChanged = 0;
1005
+ try {
1006
+ execSync(`git merge ${this._escapeShellArg(execution.branch_name)}`, {
1007
+ cwd: this.repoPath,
1008
+ stdio: "pipe",
1009
+ });
1010
+ }
1011
+ catch (error) {
1012
+ // Merge may have failed due to conflicts
1013
+ hasConflicts = true;
1014
+ }
1015
+ // 9. Count files changed
1016
+ try {
1017
+ const diffOutput = execSync(`git diff --name-only ${this._escapeShellArg(safetyTag)}..HEAD`, {
1018
+ cwd: this.repoPath,
1019
+ encoding: "utf8",
1020
+ stdio: "pipe",
1021
+ });
1022
+ filesChanged = diffOutput
1023
+ .split("\n")
1024
+ .filter((line) => line.trim().length > 0).length;
1025
+ }
1026
+ catch {
1027
+ // If merge is in progress, count staged/conflicted files
1028
+ const statusOutput = execSync("git diff --name-only --cached", {
1029
+ cwd: this.repoPath,
1030
+ encoding: "utf8",
1031
+ stdio: "pipe",
1032
+ });
1033
+ filesChanged = statusOutput
1034
+ .split("\n")
1035
+ .filter((line) => line.trim().length > 0).length;
1036
+ }
1037
+ // 10. Auto-resolve JSONL conflicts if any
1038
+ const jsonlFilesResolved = await this._resolveJSONLConflicts();
1039
+ if (jsonlFilesResolved > 0) {
1040
+ // Re-check for remaining conflicts
1041
+ try {
1042
+ const conflictCheck = execSync("git diff --name-only --diff-filter=U", {
1043
+ cwd: this.repoPath,
1044
+ encoding: "utf8",
1045
+ stdio: "pipe",
1046
+ });
1047
+ hasConflicts = conflictCheck.trim().length > 0;
1048
+ }
1049
+ catch {
1050
+ hasConflicts = false;
1051
+ }
1052
+ }
1053
+ // 11. Check if there are unresolved conflicts
1054
+ if (hasConflicts) {
1055
+ return {
1056
+ success: false,
1057
+ filesChanged,
1058
+ hasConflicts: true,
1059
+ error: "Merge conflicts detected. Please resolve them manually and commit.",
1060
+ cleanupOffered: false,
1061
+ };
1062
+ }
1063
+ // 12. Get the final commit SHA
1064
+ const finalCommit = execSync("git rev-parse HEAD", {
1065
+ cwd: this.repoPath,
1066
+ encoding: "utf8",
1067
+ stdio: "pipe",
1068
+ }).trim();
1069
+ // 13. Return success result
1070
+ return {
1071
+ success: true,
1072
+ finalCommit,
1073
+ filesChanged,
1074
+ cleanupOffered: true,
1075
+ };
1076
+ }
1077
+ catch (error) {
1078
+ // Rollback to safety snapshot on failure
1079
+ if (safetyTag) {
1080
+ try {
1081
+ await this._rollbackToSnapshot(execution.target_branch, safetyTag);
1082
+ }
1083
+ catch (rollbackError) {
1084
+ console.error(`Failed to rollback to snapshot ${safetyTag}:`, rollbackError);
1085
+ }
1086
+ }
1087
+ throw new WorktreeSyncError(`Preserve sync failed: ${error.message}`, WorktreeSyncErrorCode.MERGE_FAILED, error);
1088
+ }
1089
+ }
1090
+ }
1091
+ //# sourceMappingURL=worktree-sync-service.js.map