@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.
- package/LICENSE +21 -0
- package/README.md +312 -0
- package/bin/aad.js +2 -0
- package/package.json +78 -0
- package/src/__tests__/e2e/pipeline-e2e.test.ts +279 -0
- package/src/__tests__/e2e/resume-e2e.test.ts +200 -0
- package/src/__tests__/integration/cli-smoke.test.ts +175 -0
- package/src/__tests__/integration/pipeline.test.ts +346 -0
- package/src/bun-imports.d.ts +14 -0
- package/src/main.ts +52 -0
- package/src/modules/claude-provider/__tests__/claude-cli.adapter.test.ts +277 -0
- package/src/modules/claude-provider/__tests__/claude-sdk-real-env.test.ts +127 -0
- package/src/modules/claude-provider/__tests__/claude-sdk.adapter.test.ts +347 -0
- package/src/modules/claude-provider/__tests__/effort-strategy.test.ts +212 -0
- package/src/modules/claude-provider/__tests__/provider-registry.test.ts +251 -0
- package/src/modules/claude-provider/__tests__/retry.test.ts +201 -0
- package/src/modules/claude-provider/claude-cli.adapter.ts +156 -0
- package/src/modules/claude-provider/claude-provider.port.ts +35 -0
- package/src/modules/claude-provider/claude-sdk.adapter.ts +217 -0
- package/src/modules/claude-provider/effort-strategy.ts +94 -0
- package/src/modules/claude-provider/index.ts +32 -0
- package/src/modules/claude-provider/provider-registry.ts +92 -0
- package/src/modules/claude-provider/retry.ts +81 -0
- package/src/modules/cli/__tests__/app.test.ts +160 -0
- package/src/modules/cli/__tests__/cleanup.test.ts +111 -0
- package/src/modules/cli/__tests__/commands.test.ts +186 -0
- package/src/modules/cli/__tests__/output.test.ts +329 -0
- package/src/modules/cli/__tests__/resume.test.ts +324 -0
- package/src/modules/cli/__tests__/run.test.ts +168 -0
- package/src/modules/cli/__tests__/shutdown.test.ts +168 -0
- package/src/modules/cli/__tests__/status.test.ts +144 -0
- package/src/modules/cli/app.ts +241 -0
- package/src/modules/cli/commands/cleanup.ts +120 -0
- package/src/modules/cli/commands/resume.ts +156 -0
- package/src/modules/cli/commands/run.ts +322 -0
- package/src/modules/cli/commands/status.ts +101 -0
- package/src/modules/cli/index.ts +29 -0
- package/src/modules/cli/output.ts +256 -0
- package/src/modules/cli/shutdown.ts +122 -0
- package/src/modules/dashboard/__tests__/api-routes.test.ts +204 -0
- package/src/modules/dashboard/__tests__/file-watcher.test.ts +34 -0
- package/src/modules/dashboard/__tests__/server.test.ts +120 -0
- package/src/modules/dashboard/__tests__/sse-broadcaster.test.ts +163 -0
- package/src/modules/dashboard/__tests__/sse-routes.test.ts +58 -0
- package/src/modules/dashboard/__tests__/state-aggregator.test.ts +330 -0
- package/src/modules/dashboard/index.ts +8 -0
- package/src/modules/dashboard/routes/api.ts +84 -0
- package/src/modules/dashboard/routes/sse.ts +37 -0
- package/src/modules/dashboard/server.ts +111 -0
- package/src/modules/dashboard/services/file-watcher.ts +36 -0
- package/src/modules/dashboard/services/sse-broadcaster.ts +81 -0
- package/src/modules/dashboard/services/state-aggregator.ts +132 -0
- package/src/modules/dashboard/ui/dashboard.html +405 -0
- package/src/modules/git-workspace/__tests__/branch-manager.test.ts +335 -0
- package/src/modules/git-workspace/__tests__/git-exec.test.ts +91 -0
- package/src/modules/git-workspace/__tests__/memory-sync.test.ts +273 -0
- package/src/modules/git-workspace/__tests__/merge-service.test.ts +286 -0
- package/src/modules/git-workspace/__tests__/settings-merge.test.ts +163 -0
- package/src/modules/git-workspace/__tests__/worktree-manager.test.ts +247 -0
- package/src/modules/git-workspace/branch-manager.ts +191 -0
- package/src/modules/git-workspace/git-exec.ts +124 -0
- package/src/modules/git-workspace/index.ts +17 -0
- package/src/modules/git-workspace/memory-sync.ts +89 -0
- package/src/modules/git-workspace/merge-service.ts +156 -0
- package/src/modules/git-workspace/settings-merge.ts +95 -0
- package/src/modules/git-workspace/worktree-manager.ts +199 -0
- package/src/modules/logging/__tests__/log-store.test.ts +242 -0
- package/src/modules/logging/__tests__/logger.test.ts +81 -0
- package/src/modules/logging/__tests__/sse-transport.test.ts +93 -0
- package/src/modules/logging/index.ts +7 -0
- package/src/modules/logging/log-store.ts +80 -0
- package/src/modules/logging/logger.ts +55 -0
- package/src/modules/logging/transports/sse-transport.ts +28 -0
- package/src/modules/multi-repo/__tests__/multi-repo-planner.test.ts +93 -0
- package/src/modules/multi-repo/__tests__/repo-context.test.ts +79 -0
- package/src/modules/multi-repo/index.ts +12 -0
- package/src/modules/multi-repo/multi-repo-planner.ts +112 -0
- package/src/modules/multi-repo/repo-context.ts +71 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-81991/progress.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-81991/queue/completed/task-getall-2.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-81991/queue/pending/task-1.json +13 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-81991/queue/pending/task-getall-1.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-81991/queue/pending/task-status-change.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-81991/workers/worker-1.json +5 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-81991/workers/worker-2.json +5 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-82469/progress.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-82469/queue/completed/task-getall-2.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-82469/queue/pending/task-1.json +13 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-82469/queue/pending/task-getall-1.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-82469/queue/pending/task-status-change.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-82469/workers/worker-1.json +5 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-82469/workers/worker-2.json +5 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-85301/progress.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-85301/queue/completed/task-getall-2.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-85301/queue/pending/task-1.json +13 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-85301/queue/pending/task-getall-1.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-85301/queue/pending/task-status-change.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-85301/workers/worker-1.json +5 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-85301/workers/worker-2.json +5 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-85759/progress.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-85759/queue/completed/task-getall-2.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-85759/queue/pending/task-1.json +13 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-85759/queue/pending/task-getall-1.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-85759/queue/pending/task-status-change.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-85759/workers/worker-1.json +5 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-85759/workers/worker-2.json +5 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-86184/progress.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-86184/queue/completed/task-getall-2.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-86184/queue/pending/task-1.json +13 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-86184/queue/pending/task-getall-1.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-86184/queue/pending/task-status-change.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-86184/workers/worker-1.json +5 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-86184/workers/worker-2.json +5 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-88026/progress.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-88026/queue/completed/task-getall-2.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-88026/queue/pending/task-1.json +13 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-88026/queue/pending/task-getall-1.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-88026/queue/pending/task-status-change.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-88026/workers/worker-1.json +5 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-88026/workers/worker-2.json +5 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-89475/progress.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-89475/queue/completed/task-getall-2.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-89475/queue/pending/task-1.json +13 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-89475/queue/pending/task-getall-1.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-89475/queue/pending/task-status-change.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-89475/workers/worker-1.json +5 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-89475/workers/worker-2.json +5 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-89924/progress.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-89924/queue/completed/task-getall-2.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-89924/queue/pending/task-1.json +13 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-89924/queue/pending/task-getall-1.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-89924/queue/pending/task-status-change.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-89924/workers/worker-1.json +5 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-89924/workers/worker-2.json +5 -0
- package/src/modules/persistence/__tests__/file-lock.test.ts +141 -0
- package/src/modules/persistence/__tests__/index.test.ts +38 -0
- package/src/modules/persistence/__tests__/stores.test.ts +594 -0
- package/src/modules/persistence/file-lock.ts +158 -0
- package/src/modules/persistence/fs-run-store.ts +73 -0
- package/src/modules/persistence/fs-task-store.ts +152 -0
- package/src/modules/persistence/fs-worker-store.ts +116 -0
- package/src/modules/persistence/in-memory-stores.ts +98 -0
- package/src/modules/persistence/index.ts +60 -0
- package/src/modules/persistence/stores.port.ts +60 -0
- package/src/modules/planning/__tests__/file-conflict-validator.test.ts +256 -0
- package/src/modules/planning/__tests__/planning-service.test.ts +366 -0
- package/src/modules/planning/__tests__/project-detection.test.ts +707 -0
- package/src/modules/planning/file-conflict-validator.ts +135 -0
- package/src/modules/planning/index.ts +40 -0
- package/src/modules/planning/planning.service.ts +262 -0
- package/src/modules/planning/project-detection.ts +525 -0
- package/src/modules/plugin/__tests__/plugin-loader.test.ts +83 -0
- package/src/modules/plugin/__tests__/plugin-manager.test.ts +187 -0
- package/src/modules/plugin/index.ts +3 -0
- package/src/modules/plugin/plugin-loader.ts +46 -0
- package/src/modules/plugin/plugin-manager.ts +90 -0
- package/src/modules/plugin/plugin.types.ts +37 -0
- package/src/modules/process-manager/__tests__/process-manager.test.ts +210 -0
- package/src/modules/process-manager/__tests__/worker.test.ts +89 -0
- package/src/modules/process-manager/index.ts +5 -0
- package/src/modules/process-manager/process-manager.ts +193 -0
- package/src/modules/process-manager/worker.ts +106 -0
- package/src/modules/task-execution/__tests__/default-spawner.test.ts +154 -0
- package/src/modules/task-execution/__tests__/executor.test.ts +760 -0
- package/src/modules/task-execution/__tests__/implementer-green.test.ts +286 -0
- package/src/modules/task-execution/__tests__/merge-phase.test.ts +368 -0
- package/src/modules/task-execution/__tests__/reviewer.test.ts +302 -0
- package/src/modules/task-execution/__tests__/tester-red.test.ts +281 -0
- package/src/modules/task-execution/__tests__/tester-verify.test.ts +313 -0
- package/src/modules/task-execution/executor.ts +303 -0
- package/src/modules/task-execution/index.ts +45 -0
- package/src/modules/task-execution/phases/default-spawner.ts +49 -0
- package/src/modules/task-execution/phases/implementer-green.ts +100 -0
- package/src/modules/task-execution/phases/merge.ts +122 -0
- package/src/modules/task-execution/phases/reviewer.ts +160 -0
- package/src/modules/task-execution/phases/tester-red.ts +100 -0
- package/src/modules/task-execution/phases/tester-verify.ts +120 -0
- package/src/modules/task-queue/__tests__/dependency-resolver.test.ts +456 -0
- package/src/modules/task-queue/__tests__/dispatcher.test.ts +824 -0
- package/src/modules/task-queue/__tests__/task-plan.test.ts +122 -0
- package/src/modules/task-queue/__tests__/task.test.ts +130 -0
- package/src/modules/task-queue/dependency-resolver.ts +171 -0
- package/src/modules/task-queue/dispatcher.ts +372 -0
- package/src/modules/task-queue/index.ts +16 -0
- package/src/modules/task-queue/task-plan.ts +40 -0
- package/src/modules/task-queue/task.ts +67 -0
- package/src/shared/__tests__/config.test.ts +204 -0
- package/src/shared/__tests__/errors.test.ts +285 -0
- package/src/shared/__tests__/events.test.ts +496 -0
- package/src/shared/__tests__/types.test.ts +360 -0
- package/src/shared/config.ts +133 -0
- package/src/shared/errors.ts +128 -0
- package/src/shared/events.ts +171 -0
- package/src/shared/types.ts +143 -0
- 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
|
+
}
|