@ronkovic/aad 0.3.0

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 (195) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +312 -0
  3. package/bin/aad.js +2 -0
  4. package/package.json +78 -0
  5. package/src/__tests__/e2e/pipeline-e2e.test.ts +279 -0
  6. package/src/__tests__/e2e/resume-e2e.test.ts +200 -0
  7. package/src/__tests__/integration/cli-smoke.test.ts +175 -0
  8. package/src/__tests__/integration/pipeline.test.ts +346 -0
  9. package/src/bun-imports.d.ts +14 -0
  10. package/src/main.ts +52 -0
  11. package/src/modules/claude-provider/__tests__/claude-cli.adapter.test.ts +277 -0
  12. package/src/modules/claude-provider/__tests__/claude-sdk-real-env.test.ts +127 -0
  13. package/src/modules/claude-provider/__tests__/claude-sdk.adapter.test.ts +347 -0
  14. package/src/modules/claude-provider/__tests__/effort-strategy.test.ts +212 -0
  15. package/src/modules/claude-provider/__tests__/provider-registry.test.ts +251 -0
  16. package/src/modules/claude-provider/__tests__/retry.test.ts +201 -0
  17. package/src/modules/claude-provider/claude-cli.adapter.ts +156 -0
  18. package/src/modules/claude-provider/claude-provider.port.ts +35 -0
  19. package/src/modules/claude-provider/claude-sdk.adapter.ts +217 -0
  20. package/src/modules/claude-provider/effort-strategy.ts +94 -0
  21. package/src/modules/claude-provider/index.ts +32 -0
  22. package/src/modules/claude-provider/provider-registry.ts +92 -0
  23. package/src/modules/claude-provider/retry.ts +81 -0
  24. package/src/modules/cli/__tests__/app.test.ts +160 -0
  25. package/src/modules/cli/__tests__/cleanup.test.ts +111 -0
  26. package/src/modules/cli/__tests__/commands.test.ts +186 -0
  27. package/src/modules/cli/__tests__/output.test.ts +329 -0
  28. package/src/modules/cli/__tests__/resume.test.ts +324 -0
  29. package/src/modules/cli/__tests__/run.test.ts +168 -0
  30. package/src/modules/cli/__tests__/shutdown.test.ts +168 -0
  31. package/src/modules/cli/__tests__/status.test.ts +144 -0
  32. package/src/modules/cli/app.ts +241 -0
  33. package/src/modules/cli/commands/cleanup.ts +120 -0
  34. package/src/modules/cli/commands/resume.ts +156 -0
  35. package/src/modules/cli/commands/run.ts +322 -0
  36. package/src/modules/cli/commands/status.ts +101 -0
  37. package/src/modules/cli/index.ts +29 -0
  38. package/src/modules/cli/output.ts +256 -0
  39. package/src/modules/cli/shutdown.ts +122 -0
  40. package/src/modules/dashboard/__tests__/api-routes.test.ts +204 -0
  41. package/src/modules/dashboard/__tests__/file-watcher.test.ts +34 -0
  42. package/src/modules/dashboard/__tests__/server.test.ts +120 -0
  43. package/src/modules/dashboard/__tests__/sse-broadcaster.test.ts +163 -0
  44. package/src/modules/dashboard/__tests__/sse-routes.test.ts +58 -0
  45. package/src/modules/dashboard/__tests__/state-aggregator.test.ts +330 -0
  46. package/src/modules/dashboard/index.ts +8 -0
  47. package/src/modules/dashboard/routes/api.ts +84 -0
  48. package/src/modules/dashboard/routes/sse.ts +37 -0
  49. package/src/modules/dashboard/server.ts +111 -0
  50. package/src/modules/dashboard/services/file-watcher.ts +36 -0
  51. package/src/modules/dashboard/services/sse-broadcaster.ts +81 -0
  52. package/src/modules/dashboard/services/state-aggregator.ts +132 -0
  53. package/src/modules/dashboard/ui/dashboard.html +405 -0
  54. package/src/modules/git-workspace/__tests__/branch-manager.test.ts +335 -0
  55. package/src/modules/git-workspace/__tests__/git-exec.test.ts +91 -0
  56. package/src/modules/git-workspace/__tests__/memory-sync.test.ts +273 -0
  57. package/src/modules/git-workspace/__tests__/merge-service.test.ts +286 -0
  58. package/src/modules/git-workspace/__tests__/settings-merge.test.ts +163 -0
  59. package/src/modules/git-workspace/__tests__/worktree-manager.test.ts +247 -0
  60. package/src/modules/git-workspace/branch-manager.ts +191 -0
  61. package/src/modules/git-workspace/git-exec.ts +124 -0
  62. package/src/modules/git-workspace/index.ts +17 -0
  63. package/src/modules/git-workspace/memory-sync.ts +89 -0
  64. package/src/modules/git-workspace/merge-service.ts +156 -0
  65. package/src/modules/git-workspace/settings-merge.ts +95 -0
  66. package/src/modules/git-workspace/worktree-manager.ts +199 -0
  67. package/src/modules/logging/__tests__/log-store.test.ts +242 -0
  68. package/src/modules/logging/__tests__/logger.test.ts +81 -0
  69. package/src/modules/logging/__tests__/sse-transport.test.ts +93 -0
  70. package/src/modules/logging/index.ts +7 -0
  71. package/src/modules/logging/log-store.ts +80 -0
  72. package/src/modules/logging/logger.ts +55 -0
  73. package/src/modules/logging/transports/sse-transport.ts +28 -0
  74. package/src/modules/multi-repo/__tests__/multi-repo-planner.test.ts +93 -0
  75. package/src/modules/multi-repo/__tests__/repo-context.test.ts +79 -0
  76. package/src/modules/multi-repo/index.ts +12 -0
  77. package/src/modules/multi-repo/multi-repo-planner.ts +112 -0
  78. package/src/modules/multi-repo/repo-context.ts +71 -0
  79. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/progress.json +10 -0
  80. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/queue/completed/task-getall-2.json +10 -0
  81. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/queue/pending/task-1.json +13 -0
  82. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/queue/pending/task-getall-1.json +10 -0
  83. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/queue/pending/task-status-change.json +10 -0
  84. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/workers/worker-1.json +5 -0
  85. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/workers/worker-2.json +5 -0
  86. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/progress.json +10 -0
  87. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/queue/completed/task-getall-2.json +10 -0
  88. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/queue/pending/task-1.json +13 -0
  89. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/queue/pending/task-getall-1.json +10 -0
  90. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/queue/pending/task-status-change.json +10 -0
  91. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/workers/worker-1.json +5 -0
  92. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/workers/worker-2.json +5 -0
  93. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/progress.json +10 -0
  94. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/queue/completed/task-getall-2.json +10 -0
  95. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/queue/pending/task-1.json +13 -0
  96. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/queue/pending/task-getall-1.json +10 -0
  97. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/queue/pending/task-status-change.json +10 -0
  98. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/workers/worker-1.json +5 -0
  99. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/workers/worker-2.json +5 -0
  100. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/progress.json +10 -0
  101. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/queue/completed/task-getall-2.json +10 -0
  102. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/queue/pending/task-1.json +13 -0
  103. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/queue/pending/task-getall-1.json +10 -0
  104. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/queue/pending/task-status-change.json +10 -0
  105. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/workers/worker-1.json +5 -0
  106. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/workers/worker-2.json +5 -0
  107. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/progress.json +10 -0
  108. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/queue/completed/task-getall-2.json +10 -0
  109. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/queue/pending/task-1.json +13 -0
  110. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/queue/pending/task-getall-1.json +10 -0
  111. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/queue/pending/task-status-change.json +10 -0
  112. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/workers/worker-1.json +5 -0
  113. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/workers/worker-2.json +5 -0
  114. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/progress.json +10 -0
  115. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/queue/completed/task-getall-2.json +10 -0
  116. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/queue/pending/task-1.json +13 -0
  117. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/queue/pending/task-getall-1.json +10 -0
  118. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/queue/pending/task-status-change.json +10 -0
  119. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/workers/worker-1.json +5 -0
  120. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/workers/worker-2.json +5 -0
  121. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/progress.json +10 -0
  122. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/queue/completed/task-getall-2.json +10 -0
  123. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/queue/pending/task-1.json +13 -0
  124. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/queue/pending/task-getall-1.json +10 -0
  125. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/queue/pending/task-status-change.json +10 -0
  126. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/workers/worker-1.json +5 -0
  127. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/workers/worker-2.json +5 -0
  128. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/progress.json +10 -0
  129. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/queue/completed/task-getall-2.json +10 -0
  130. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/queue/pending/task-1.json +13 -0
  131. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/queue/pending/task-getall-1.json +10 -0
  132. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/queue/pending/task-status-change.json +10 -0
  133. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/workers/worker-1.json +5 -0
  134. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/workers/worker-2.json +5 -0
  135. package/src/modules/persistence/__tests__/file-lock.test.ts +141 -0
  136. package/src/modules/persistence/__tests__/index.test.ts +38 -0
  137. package/src/modules/persistence/__tests__/stores.test.ts +594 -0
  138. package/src/modules/persistence/file-lock.ts +158 -0
  139. package/src/modules/persistence/fs-run-store.ts +73 -0
  140. package/src/modules/persistence/fs-task-store.ts +152 -0
  141. package/src/modules/persistence/fs-worker-store.ts +116 -0
  142. package/src/modules/persistence/in-memory-stores.ts +98 -0
  143. package/src/modules/persistence/index.ts +60 -0
  144. package/src/modules/persistence/stores.port.ts +60 -0
  145. package/src/modules/planning/__tests__/file-conflict-validator.test.ts +256 -0
  146. package/src/modules/planning/__tests__/planning-service.test.ts +366 -0
  147. package/src/modules/planning/__tests__/project-detection.test.ts +707 -0
  148. package/src/modules/planning/file-conflict-validator.ts +135 -0
  149. package/src/modules/planning/index.ts +40 -0
  150. package/src/modules/planning/planning.service.ts +262 -0
  151. package/src/modules/planning/project-detection.ts +525 -0
  152. package/src/modules/plugin/__tests__/plugin-loader.test.ts +83 -0
  153. package/src/modules/plugin/__tests__/plugin-manager.test.ts +187 -0
  154. package/src/modules/plugin/index.ts +3 -0
  155. package/src/modules/plugin/plugin-loader.ts +46 -0
  156. package/src/modules/plugin/plugin-manager.ts +90 -0
  157. package/src/modules/plugin/plugin.types.ts +37 -0
  158. package/src/modules/process-manager/__tests__/process-manager.test.ts +210 -0
  159. package/src/modules/process-manager/__tests__/worker.test.ts +89 -0
  160. package/src/modules/process-manager/index.ts +5 -0
  161. package/src/modules/process-manager/process-manager.ts +193 -0
  162. package/src/modules/process-manager/worker.ts +106 -0
  163. package/src/modules/task-execution/__tests__/default-spawner.test.ts +154 -0
  164. package/src/modules/task-execution/__tests__/executor.test.ts +760 -0
  165. package/src/modules/task-execution/__tests__/implementer-green.test.ts +286 -0
  166. package/src/modules/task-execution/__tests__/merge-phase.test.ts +368 -0
  167. package/src/modules/task-execution/__tests__/reviewer.test.ts +302 -0
  168. package/src/modules/task-execution/__tests__/tester-red.test.ts +281 -0
  169. package/src/modules/task-execution/__tests__/tester-verify.test.ts +313 -0
  170. package/src/modules/task-execution/executor.ts +303 -0
  171. package/src/modules/task-execution/index.ts +45 -0
  172. package/src/modules/task-execution/phases/default-spawner.ts +49 -0
  173. package/src/modules/task-execution/phases/implementer-green.ts +100 -0
  174. package/src/modules/task-execution/phases/merge.ts +122 -0
  175. package/src/modules/task-execution/phases/reviewer.ts +160 -0
  176. package/src/modules/task-execution/phases/tester-red.ts +100 -0
  177. package/src/modules/task-execution/phases/tester-verify.ts +120 -0
  178. package/src/modules/task-queue/__tests__/dependency-resolver.test.ts +456 -0
  179. package/src/modules/task-queue/__tests__/dispatcher.test.ts +824 -0
  180. package/src/modules/task-queue/__tests__/task-plan.test.ts +122 -0
  181. package/src/modules/task-queue/__tests__/task.test.ts +130 -0
  182. package/src/modules/task-queue/dependency-resolver.ts +171 -0
  183. package/src/modules/task-queue/dispatcher.ts +372 -0
  184. package/src/modules/task-queue/index.ts +16 -0
  185. package/src/modules/task-queue/task-plan.ts +40 -0
  186. package/src/modules/task-queue/task.ts +67 -0
  187. package/src/shared/__tests__/config.test.ts +204 -0
  188. package/src/shared/__tests__/errors.test.ts +285 -0
  189. package/src/shared/__tests__/events.test.ts +496 -0
  190. package/src/shared/__tests__/types.test.ts +360 -0
  191. package/src/shared/config.ts +133 -0
  192. package/src/shared/errors.ts +128 -0
  193. package/src/shared/events.ts +171 -0
  194. package/src/shared/types.ts +143 -0
  195. package/tsconfig.json +30 -0
@@ -0,0 +1,191 @@
1
+ import type { Logger } from "pino";
2
+ import type { TaskId, RunId } from "@aad/shared/types";
3
+ import { GitWorkspaceError } from "@aad/shared/errors";
4
+ import { defaultGitOps, type GitOps } from "./git-exec";
5
+
6
+ export interface BranchManagerOptions {
7
+ repoRoot: string;
8
+ logger?: Logger;
9
+ gitOps?: GitOps;
10
+ }
11
+
12
+ /**
13
+ * Manage git branches for parallel task execution
14
+ */
15
+ export class BranchManager {
16
+ private repoRoot: string;
17
+ private logger?: Logger;
18
+ private gitOps: GitOps;
19
+
20
+ constructor(options: BranchManagerOptions) {
21
+ this.repoRoot = options.repoRoot;
22
+ this.logger = options.logger;
23
+ this.gitOps = options.gitOps ?? defaultGitOps;
24
+ }
25
+
26
+ /**
27
+ * Create parent branch for a run
28
+ */
29
+ async createParentBranch(runId: RunId, baseBranch: string): Promise<string> {
30
+ const branchName = `aad-run-${runId as string}`;
31
+
32
+ this.logger?.info({ runId, baseBranch, branchName }, "Creating parent branch");
33
+
34
+ try {
35
+ // Check if branch already exists
36
+ if (await this.gitOps.branchExists(branchName, this.repoRoot, this.logger)) {
37
+ throw new GitWorkspaceError("Parent branch already exists", {
38
+ branchName,
39
+ runId: runId as string,
40
+ });
41
+ }
42
+
43
+ // Create branch from base
44
+ await this.gitOps.gitExec(
45
+ ["checkout", "-b", branchName, baseBranch],
46
+ { cwd: this.repoRoot, logger: this.logger }
47
+ );
48
+
49
+ return branchName;
50
+ } catch (error) {
51
+ if (error instanceof GitWorkspaceError) {
52
+ throw error;
53
+ }
54
+
55
+ throw new GitWorkspaceError("Failed to create parent branch", {
56
+ runId: runId as string,
57
+ baseBranch,
58
+ branchName,
59
+ error: error instanceof Error ? error.message : String(error),
60
+ });
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Create task branch
66
+ */
67
+ async createTaskBranch(taskId: TaskId, parentBranch: string): Promise<string> {
68
+ const branchName = `aad-task-${taskId as string}`;
69
+
70
+ this.logger?.info({ taskId, parentBranch, branchName }, "Creating task branch");
71
+
72
+ try {
73
+ // Check if branch already exists
74
+ if (await this.gitOps.branchExists(branchName, this.repoRoot, this.logger)) {
75
+ this.logger?.warn({ branchName }, "Task branch already exists, using existing");
76
+ return branchName;
77
+ }
78
+
79
+ // Create branch from parent (without checking out)
80
+ await this.gitOps.gitExec(
81
+ ["branch", branchName, parentBranch],
82
+ { cwd: this.repoRoot, logger: this.logger }
83
+ );
84
+
85
+ return branchName;
86
+ } catch (error) {
87
+ throw new GitWorkspaceError("Failed to create task branch", {
88
+ taskId: taskId as string,
89
+ parentBranch,
90
+ branchName,
91
+ error: error instanceof Error ? error.message : String(error),
92
+ });
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Delete branch (local)
98
+ */
99
+ async deleteBranch(branchName: string, force = false): Promise<void> {
100
+ this.logger?.info({ branchName, force }, "Deleting branch");
101
+
102
+ try {
103
+ const args = ["branch", force ? "-D" : "-d", branchName];
104
+ await this.gitOps.gitExec(args, { cwd: this.repoRoot, logger: this.logger });
105
+ } catch (error) {
106
+ throw new GitWorkspaceError("Failed to delete branch", {
107
+ branchName,
108
+ force,
109
+ error: error instanceof Error ? error.message : String(error),
110
+ });
111
+ }
112
+ }
113
+
114
+ /**
115
+ * List branches matching pattern
116
+ */
117
+ async listBranches(pattern: string): Promise<string[]> {
118
+ try {
119
+ const result = await this.gitOps.gitExec(
120
+ ["branch", "--list", pattern],
121
+ { cwd: this.repoRoot, logger: this.logger }
122
+ );
123
+
124
+ return result.stdout
125
+ .split("\n")
126
+ .map((line) => line.trim().replace(/^\* /, ""))
127
+ .filter((line) => line.length > 0);
128
+ } catch (error) {
129
+ throw new GitWorkspaceError("Failed to list branches", {
130
+ pattern,
131
+ error: error instanceof Error ? error.message : String(error),
132
+ });
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Cleanup orphaned AAD branches (branches without worktrees)
138
+ */
139
+ async cleanupOrphanBranches(runId?: RunId): Promise<string[]> {
140
+ // Support both aad/* (slash) and aad-* (hyphen) patterns for backward compatibility
141
+ const patterns = runId
142
+ ? [`aad/${runId as string}/*`, `aad-*-${runId as string}*`]
143
+ : ["aad/*", "aad-*"];
144
+
145
+ this.logger?.info({ patterns }, "Cleaning up orphan branches");
146
+
147
+ const deleted: string[] = [];
148
+
149
+ for (const pattern of patterns) {
150
+ const branches = await this.listBranches(pattern);
151
+
152
+ for (const branch of branches) {
153
+ try {
154
+ // Try to delete branch (non-force)
155
+ await this.deleteBranch(branch, false);
156
+ deleted.push(branch);
157
+ this.logger?.info({ branch }, "Deleted orphan branch");
158
+ } catch (_error) {
159
+ // Branch still has commits or is checked out, skip
160
+ this.logger?.debug({ branch }, "Skipping branch (in use or has commits)");
161
+ }
162
+ }
163
+ }
164
+
165
+ return deleted;
166
+ }
167
+
168
+ /**
169
+ * Force cleanup all AAD branches for a run
170
+ */
171
+ async forceCleanupRun(runId: RunId): Promise<string[]> {
172
+ // Support both aad/* (slash) and aad-* (hyphen) patterns
173
+ const patterns = [`aad/${runId as string}/*`, `aad-*-${runId as string}*`];
174
+ const deleted: string[] = [];
175
+
176
+ for (const pattern of patterns) {
177
+ const branches = await this.listBranches(pattern);
178
+
179
+ for (const branch of branches) {
180
+ try {
181
+ await this.deleteBranch(branch, true);
182
+ deleted.push(branch);
183
+ } catch (error) {
184
+ this.logger?.warn({ branch, error }, "Failed to force delete branch");
185
+ }
186
+ }
187
+ }
188
+
189
+ return deleted;
190
+ }
191
+ }
@@ -0,0 +1,124 @@
1
+ import type { Logger } from "pino";
2
+ import { GitWorkspaceError } from "@aad/shared/errors";
3
+
4
+ export interface GitExecOptions {
5
+ cwd?: string;
6
+ timeout?: number;
7
+ logger?: Logger;
8
+ }
9
+
10
+ export interface GitExecResult {
11
+ stdout: string;
12
+ stderr: string;
13
+ exitCode: number;
14
+ }
15
+
16
+ /**
17
+ * Execute git command using Bun.spawn
18
+ * Throws GitWorkspaceError on non-zero exit code
19
+ */
20
+ export async function gitExec(
21
+ args: string[],
22
+ options: GitExecOptions = {}
23
+ ): Promise<GitExecResult> {
24
+ const { cwd = process.cwd(), timeout = 30000, logger } = options;
25
+
26
+ logger?.debug({ args, cwd }, "Executing git command");
27
+
28
+ try {
29
+ const proc = Bun.spawn(["git", ...args], {
30
+ cwd,
31
+ stdout: "pipe",
32
+ stderr: "pipe",
33
+ });
34
+
35
+ // Set timeout
36
+ const timeoutId = setTimeout(() => {
37
+ proc.kill();
38
+ }, timeout);
39
+
40
+ const [stdout, stderr, exitCode] = await Promise.all([
41
+ new Response(proc.stdout).text(),
42
+ new Response(proc.stderr).text(),
43
+ proc.exited,
44
+ ]);
45
+
46
+ clearTimeout(timeoutId);
47
+
48
+ if (exitCode !== 0) {
49
+ logger?.error({ args, exitCode, stderr }, "Git command failed");
50
+ throw new GitWorkspaceError("Git command failed", {
51
+ args,
52
+ exitCode,
53
+ stdout: stdout.trim(),
54
+ stderr: stderr.trim(),
55
+ cwd,
56
+ });
57
+ }
58
+
59
+ logger?.debug({ args, exitCode }, "Git command succeeded");
60
+
61
+ return {
62
+ stdout: stdout.trim(),
63
+ stderr: stderr.trim(),
64
+ exitCode,
65
+ };
66
+ } catch (error) {
67
+ if (error instanceof GitWorkspaceError) {
68
+ throw error;
69
+ }
70
+
71
+ const err = error as Error;
72
+ throw new GitWorkspaceError("Failed to execute git command", {
73
+ args,
74
+ error: err.message,
75
+ cwd,
76
+ });
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Check if a directory is a git repository
82
+ */
83
+ export async function isGitRepo(dir: string, logger?: Logger): Promise<boolean> {
84
+ try {
85
+ await gitExec(["rev-parse", "--git-dir"], { cwd: dir, logger });
86
+ return true;
87
+ } catch {
88
+ return false;
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Get current branch name
94
+ */
95
+ export async function getCurrentBranch(cwd: string, logger?: Logger): Promise<string> {
96
+ const result = await gitExec(["rev-parse", "--abbrev-ref", "HEAD"], { cwd, logger });
97
+ return result.stdout;
98
+ }
99
+
100
+ /**
101
+ * Check if branch exists (local or remote)
102
+ */
103
+ export async function branchExists(
104
+ branch: string,
105
+ cwd: string,
106
+ logger?: Logger
107
+ ): Promise<boolean> {
108
+ try {
109
+ await gitExec(["rev-parse", "--verify", branch], { cwd, logger });
110
+ return true;
111
+ } catch {
112
+ return false;
113
+ }
114
+ }
115
+
116
+ /**
117
+ * GitOps interface for dependency injection in tests
118
+ */
119
+ export interface GitOps {
120
+ gitExec: typeof gitExec;
121
+ branchExists: typeof branchExists;
122
+ }
123
+
124
+ export const defaultGitOps: GitOps = { gitExec, branchExists };
@@ -0,0 +1,17 @@
1
+ export { gitExec, isGitRepo, getCurrentBranch, branchExists, defaultGitOps } from "./git-exec";
2
+ export type { GitExecOptions, GitExecResult, GitOps } from "./git-exec";
3
+
4
+ export { WorktreeManager } from "./worktree-manager";
5
+ export type { WorktreeManagerOptions, WorktreeManagerFsOps } from "./worktree-manager";
6
+
7
+ export { BranchManager } from "./branch-manager";
8
+ export type { BranchManagerOptions } from "./branch-manager";
9
+
10
+ export { MergeService } from "./merge-service";
11
+ export type { MergeServiceOptions, MergeResult } from "./merge-service";
12
+
13
+ export { mergeSettings, loadAndMergeSettings, saveSettings } from "./settings-merge";
14
+ export type { ClaudeSettings } from "./settings-merge";
15
+
16
+ export { harvestMemory } from "./memory-sync";
17
+ export type { MemorySyncOptions, MemorySyncFsOps } from "./memory-sync";
@@ -0,0 +1,89 @@
1
+ import { readdir, readFile, writeFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import type { Logger } from "pino";
4
+
5
+ export interface MemorySyncFsOps {
6
+ readdir: typeof readdir;
7
+ readFile: typeof readFile;
8
+ writeFile: typeof writeFile;
9
+ }
10
+
11
+ const defaultFsOps: MemorySyncFsOps = { readdir, readFile, writeFile };
12
+
13
+ export interface MemorySyncOptions {
14
+ sourceDir: string;
15
+ targetDir: string;
16
+ maxLines?: number;
17
+ logger?: Logger;
18
+ fsOps?: MemorySyncFsOps;
19
+ }
20
+
21
+ /**
22
+ * Harvest Agent Memory with line-based deduplication
23
+ */
24
+ export async function harvestMemory(options: MemorySyncOptions): Promise<void> {
25
+ const { sourceDir, targetDir, maxLines = 200, logger, fsOps = defaultFsOps } = options;
26
+
27
+ logger?.info({ sourceDir, targetDir }, "Harvesting agent memory");
28
+
29
+ try {
30
+ const agentDirs = await fsOps.readdir(sourceDir, { withFileTypes: true });
31
+
32
+ for (const dirent of agentDirs) {
33
+ if (!dirent.isDirectory()) {
34
+ continue;
35
+ }
36
+
37
+ const agentName = dirent.name;
38
+ const sourceMemoryFile = join(sourceDir, agentName, "MEMORY.md");
39
+ const targetMemoryFile = join(targetDir, agentName, "MEMORY.md");
40
+
41
+ try {
42
+ // Read source memory
43
+ const sourceContent = await fsOps.readFile(sourceMemoryFile, "utf-8");
44
+ const sourceLines = sourceContent.split("\n").filter((line) => line.trim().length > 0);
45
+
46
+ // Read target memory (if exists)
47
+ let targetLines: string[] = [];
48
+ try {
49
+ const targetContent = await fsOps.readFile(targetMemoryFile, "utf-8");
50
+ targetLines = targetContent.split("\n").filter((line) => line.trim().length > 0);
51
+ } catch {
52
+ // Target doesn't exist, that's fine
53
+ }
54
+
55
+ // Deduplicate lines (preserve order, keep newest)
56
+ const seenLines = new Set<string>();
57
+ const mergedLines: string[] = [];
58
+
59
+ // Add target lines first (older)
60
+ for (const line of targetLines) {
61
+ if (!seenLines.has(line)) {
62
+ seenLines.add(line);
63
+ mergedLines.push(line);
64
+ }
65
+ }
66
+
67
+ // Add source lines (newer, may override)
68
+ for (const line of sourceLines) {
69
+ if (!seenLines.has(line)) {
70
+ seenLines.add(line);
71
+ mergedLines.push(line);
72
+ }
73
+ }
74
+
75
+ // Limit to maxLines (keep most recent)
76
+ const limitedLines = mergedLines.slice(-maxLines);
77
+
78
+ // Write back to target
79
+ await fsOps.writeFile(targetMemoryFile, limitedLines.join("\n") + "\n");
80
+
81
+ logger?.debug({ agentName, lines: limitedLines.length }, "Harvested agent memory");
82
+ } catch (error) {
83
+ logger?.debug({ agentName, error }, "Failed to harvest agent memory (skipping)");
84
+ }
85
+ }
86
+ } catch (error) {
87
+ logger?.warn({ sourceDir, error }, "Failed to harvest memories");
88
+ }
89
+ }
@@ -0,0 +1,156 @@
1
+ import type { Logger } from "pino";
2
+ import type { TaskId } from "@aad/shared/types";
3
+ import { gitExec } from "./git-exec";
4
+ import { FileLock } from "../persistence";
5
+
6
+ export interface MergeServiceOptions {
7
+ repoRoot: string;
8
+ logger?: Logger;
9
+ }
10
+
11
+ export interface MergeResult {
12
+ success: boolean;
13
+ conflicts?: string[];
14
+ message?: string;
15
+ }
16
+
17
+ /**
18
+ * Merge task branches into parent branch with conflict detection
19
+ */
20
+ export class MergeService {
21
+ private repoRoot: string;
22
+ private logger?: Logger;
23
+
24
+ constructor(options: MergeServiceOptions) {
25
+ this.repoRoot = options.repoRoot;
26
+ this.logger = options.logger;
27
+ }
28
+
29
+ /**
30
+ * Merge task branch into parent branch (with file lock)
31
+ */
32
+ async mergeToParent(
33
+ taskId: TaskId,
34
+ taskBranch: string,
35
+ parentBranch: string,
36
+ parentWorktree: string
37
+ ): Promise<MergeResult> {
38
+ const lockDir = `${parentWorktree}/.git/aad-merge.lock`;
39
+ const lock = new FileLock({ lockDir, timeout: 60000, logger: this.logger });
40
+
41
+ try {
42
+ await lock.acquire();
43
+
44
+ this.logger?.info({ taskId, taskBranch, parentBranch }, "Merging task to parent");
45
+
46
+ // Fetch latest from task branch
47
+ await gitExec(
48
+ ["fetch", this.repoRoot, taskBranch],
49
+ { cwd: parentWorktree, logger: this.logger }
50
+ );
51
+
52
+ // Try merge
53
+ try {
54
+ await gitExec(
55
+ ["merge", "--no-ff", "-m", `Merge task ${taskId as string}: ${taskBranch}`, "FETCH_HEAD"],
56
+ { cwd: parentWorktree, logger: this.logger }
57
+ );
58
+
59
+ this.logger?.info({ taskId, taskBranch }, "Merge succeeded");
60
+
61
+ return {
62
+ success: true,
63
+ message: `Successfully merged ${taskBranch} into ${parentBranch}`,
64
+ };
65
+ } catch (error) {
66
+ // Check if merge conflict
67
+ const conflicts = await this.detectConflicts(parentWorktree);
68
+
69
+ if (conflicts.length > 0) {
70
+ // Abort merge
71
+ await gitExec(
72
+ ["merge", "--abort"],
73
+ { cwd: parentWorktree, logger: this.logger }
74
+ );
75
+
76
+ this.logger?.warn({ taskId, conflicts }, "Merge conflict detected");
77
+
78
+ return {
79
+ success: false,
80
+ conflicts,
81
+ message: `Merge conflict: ${conflicts.join(", ")}`,
82
+ };
83
+ }
84
+
85
+ throw error;
86
+ }
87
+ } finally {
88
+ await lock.release();
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Detect merge conflicts
94
+ */
95
+ async detectConflicts(worktreePath: string): Promise<string[]> {
96
+ try {
97
+ const result = await gitExec(
98
+ ["diff", "--name-only", "--diff-filter=U"],
99
+ { cwd: worktreePath, logger: this.logger }
100
+ );
101
+
102
+ return result.stdout.split("\n").filter((line) => line.length > 0);
103
+ } catch (error) {
104
+ this.logger?.warn({ error, worktreePath }, "Failed to detect merge conflicts");
105
+ return [];
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Fetch dependency changes from another task branch
111
+ */
112
+ async fetchDependencyChanges(
113
+ depTaskBranch: string,
114
+ targetWorktree: string
115
+ ): Promise<void> {
116
+ this.logger?.info({ depTaskBranch, targetWorktree }, "Fetching dependency changes");
117
+
118
+ await gitExec(
119
+ ["fetch", this.repoRoot, depTaskBranch],
120
+ { cwd: targetWorktree, logger: this.logger }
121
+ );
122
+
123
+ await gitExec(
124
+ ["merge", "--no-ff", "-m", `Merge dependency: ${depTaskBranch}`, "FETCH_HEAD"],
125
+ { cwd: targetWorktree, logger: this.logger }
126
+ );
127
+ }
128
+
129
+ /**
130
+ * Get list of changed files in a branch compared to base
131
+ */
132
+ async getChangedFiles(branch: string, baseBranch: string): Promise<string[]> {
133
+ const result = await gitExec(
134
+ ["diff", "--name-only", `${baseBranch}...${branch}`],
135
+ { cwd: this.repoRoot, logger: this.logger }
136
+ );
137
+
138
+ return result.stdout.split("\n").filter((line) => line.length > 0);
139
+ }
140
+
141
+ /**
142
+ * Check if branches have conflicting changes
143
+ */
144
+ async hasConflictingChanges(
145
+ branch1: string,
146
+ branch2: string,
147
+ baseBranch: string
148
+ ): Promise<boolean> {
149
+ const files1 = await this.getChangedFiles(branch1, baseBranch);
150
+ const files2 = await this.getChangedFiles(branch2, baseBranch);
151
+
152
+ // Simple check: do they modify the same files?
153
+ const files1Set = new Set(files1);
154
+ return files2.some((file) => files1Set.has(file));
155
+ }
156
+ }
@@ -0,0 +1,95 @@
1
+ import type { Logger } from "pino";
2
+
3
+ export interface ClaudeSettings {
4
+ permissions?: Record<string, unknown>;
5
+ hooks?: Record<string, unknown>;
6
+ env?: Record<string, string>;
7
+ [key: string]: unknown;
8
+ }
9
+
10
+ /**
11
+ * Deep merge Claude settings with union semantics for arrays
12
+ */
13
+ export function mergeSettings(
14
+ base: ClaudeSettings,
15
+ override: ClaudeSettings,
16
+ logger?: Logger
17
+ ): ClaudeSettings {
18
+ const merged: ClaudeSettings = { ...base };
19
+
20
+ for (const [key, value] of Object.entries(override)) {
21
+ if (value === undefined || value === null) {
22
+ continue;
23
+ }
24
+
25
+ if (!merged[key]) {
26
+ merged[key] = value;
27
+ continue;
28
+ }
29
+
30
+ // Array union (for permissions, hooks)
31
+ if (Array.isArray(merged[key]) && Array.isArray(value)) {
32
+ const baseArray = merged[key] as unknown[];
33
+ const overrideArray = value as unknown[];
34
+ merged[key] = [...new Set([...baseArray, ...overrideArray])];
35
+ continue;
36
+ }
37
+
38
+ // Object deep merge
39
+ if (
40
+ typeof merged[key] === "object" &&
41
+ !Array.isArray(merged[key]) &&
42
+ typeof value === "object" &&
43
+ !Array.isArray(value)
44
+ ) {
45
+ merged[key] = mergeSettings(
46
+ merged[key] as ClaudeSettings,
47
+ value as ClaudeSettings,
48
+ logger
49
+ );
50
+ continue;
51
+ }
52
+
53
+ // Primitive override
54
+ merged[key] = value;
55
+ }
56
+
57
+ logger?.debug({ merged }, "Settings merged");
58
+
59
+ return merged;
60
+ }
61
+
62
+ /**
63
+ * Load and merge settings from multiple sources
64
+ */
65
+ export async function loadAndMergeSettings(
66
+ paths: string[],
67
+ logger?: Logger
68
+ ): Promise<ClaudeSettings> {
69
+ let merged: ClaudeSettings = {};
70
+
71
+ for (const path of paths) {
72
+ try {
73
+ const content = await Bun.file(path).text();
74
+ const settings = JSON.parse(content) as ClaudeSettings;
75
+ merged = mergeSettings(merged, settings, logger);
76
+ logger?.debug({ path }, "Loaded settings");
77
+ } catch (error) {
78
+ logger?.debug({ path, error }, "Failed to load settings (skipping)");
79
+ }
80
+ }
81
+
82
+ return merged;
83
+ }
84
+
85
+ /**
86
+ * Save merged settings
87
+ */
88
+ export async function saveSettings(
89
+ path: string,
90
+ settings: ClaudeSettings,
91
+ logger?: Logger
92
+ ): Promise<void> {
93
+ await Bun.write(path, JSON.stringify(settings, null, 2));
94
+ logger?.debug({ path }, "Saved settings");
95
+ }