@ronkovic/aad 0.3.9 → 0.4.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 (114) hide show
  1. package/README.md +292 -12
  2. package/package.json +6 -1
  3. package/src/__tests__/e2e/pipeline-e2e.test.ts +1 -0
  4. package/src/__tests__/e2e/resume-e2e.test.ts +2 -0
  5. package/src/__tests__/integration/pipeline.test.ts +1 -0
  6. package/src/modules/claude-provider/__tests__/claude-sdk-real-env.test.ts +1 -1
  7. package/src/modules/claude-provider/__tests__/claude-sdk.adapter.test.ts +2 -0
  8. package/src/modules/claude-provider/__tests__/provider-registry.test.ts +1 -0
  9. package/src/modules/cli/__tests__/cleanup.test.ts +72 -0
  10. package/src/modules/cli/__tests__/resume.test.ts +1 -0
  11. package/src/modules/cli/__tests__/run.test.ts +1 -0
  12. package/src/modules/cli/commands/__tests__/task-dispatch-handler.test.ts +145 -0
  13. package/src/modules/cli/commands/cleanup.ts +26 -11
  14. package/src/modules/cli/commands/resume.ts +3 -2
  15. package/src/modules/cli/commands/run.ts +57 -7
  16. package/src/modules/cli/commands/task-dispatch-handler.ts +73 -3
  17. package/src/modules/dashboard/__tests__/api-graph.test.ts +332 -0
  18. package/src/modules/dashboard/__tests__/api-timeline.test.ts +461 -0
  19. package/src/modules/dashboard/routes/sse.ts +3 -2
  20. package/src/modules/dashboard/server.ts +1 -0
  21. package/src/modules/dashboard/services/sse-broadcaster.ts +29 -0
  22. package/src/modules/dashboard/ui/dashboard.html +143 -18
  23. package/src/modules/git-workspace/__tests__/branch-manager.test.ts +52 -0
  24. package/src/modules/git-workspace/__tests__/dependency-installer.test.ts +77 -0
  25. package/src/modules/git-workspace/__tests__/git-exec.test.ts +26 -0
  26. package/src/modules/git-workspace/__tests__/merge-service.test.ts +19 -0
  27. package/src/modules/git-workspace/__tests__/pr-manager.test.ts +80 -0
  28. package/src/modules/git-workspace/__tests__/template-copy.test.ts +189 -0
  29. package/src/modules/git-workspace/__tests__/worktree-cleanup.test.ts +29 -2
  30. package/src/modules/git-workspace/__tests__/worktree-manager.test.ts +64 -4
  31. package/src/modules/git-workspace/branch-manager.ts +24 -3
  32. package/src/modules/git-workspace/dependency-installer.ts +113 -0
  33. package/src/modules/git-workspace/git-exec.ts +3 -2
  34. package/src/modules/git-workspace/index.ts +10 -1
  35. package/src/modules/git-workspace/merge-service.ts +36 -2
  36. package/src/modules/git-workspace/pr-manager.ts +278 -0
  37. package/src/modules/git-workspace/template-copy.ts +302 -0
  38. package/src/modules/git-workspace/worktree-manager.ts +37 -11
  39. package/src/modules/planning/__tests__/planning-service.test.ts +1 -0
  40. package/src/modules/planning/__tests__/planning.service.test.ts +149 -0
  41. package/src/modules/planning/__tests__/project-detection.test.ts +7 -1
  42. package/src/modules/planning/planning.service.ts +16 -2
  43. package/src/modules/planning/project-detection.ts +4 -1
  44. package/src/modules/process-manager/__tests__/process-manager.test.ts +1 -0
  45. package/src/modules/task-execution/__tests__/executor.test.ts +86 -0
  46. package/src/modules/task-execution/__tests__/tester-verify.test.ts +4 -3
  47. package/src/modules/task-execution/executor.ts +87 -4
  48. package/src/modules/task-execution/phases/implementer-green.ts +22 -5
  49. package/src/modules/task-execution/phases/merge.ts +44 -2
  50. package/src/modules/task-execution/phases/tester-red.ts +22 -5
  51. package/src/modules/task-execution/phases/tester-verify.ts +22 -6
  52. package/src/modules/task-queue/dispatcher.ts +50 -1
  53. package/src/shared/__tests__/prerequisites.test.ts +176 -0
  54. package/src/shared/config.ts +6 -0
  55. package/src/shared/prerequisites.ts +190 -0
  56. package/src/shared/types.ts +13 -0
  57. package/templates/CLAUDE.md +122 -0
  58. package/templates/settings.json +117 -0
  59. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/progress.json +0 -10
  60. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/queue/completed/task-getall-2.json +0 -10
  61. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/queue/pending/task-1.json +0 -13
  62. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/queue/pending/task-getall-1.json +0 -10
  63. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/queue/pending/task-status-change.json +0 -10
  64. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/workers/worker-1.json +0 -5
  65. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/workers/worker-2.json +0 -5
  66. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/progress.json +0 -10
  67. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/queue/completed/task-getall-2.json +0 -10
  68. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/queue/pending/task-1.json +0 -13
  69. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/queue/pending/task-getall-1.json +0 -10
  70. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/queue/pending/task-status-change.json +0 -10
  71. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/workers/worker-1.json +0 -5
  72. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/workers/worker-2.json +0 -5
  73. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/progress.json +0 -10
  74. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/queue/completed/task-getall-2.json +0 -10
  75. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/queue/pending/task-1.json +0 -13
  76. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/queue/pending/task-getall-1.json +0 -10
  77. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/queue/pending/task-status-change.json +0 -10
  78. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/workers/worker-1.json +0 -5
  79. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/workers/worker-2.json +0 -5
  80. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/progress.json +0 -10
  81. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/queue/completed/task-getall-2.json +0 -10
  82. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/queue/pending/task-1.json +0 -13
  83. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/queue/pending/task-getall-1.json +0 -10
  84. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/queue/pending/task-status-change.json +0 -10
  85. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/workers/worker-1.json +0 -5
  86. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/workers/worker-2.json +0 -5
  87. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/progress.json +0 -10
  88. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/queue/completed/task-getall-2.json +0 -10
  89. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/queue/pending/task-1.json +0 -13
  90. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/queue/pending/task-getall-1.json +0 -10
  91. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/queue/pending/task-status-change.json +0 -10
  92. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/workers/worker-1.json +0 -5
  93. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/workers/worker-2.json +0 -5
  94. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/progress.json +0 -10
  95. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/queue/completed/task-getall-2.json +0 -10
  96. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/queue/pending/task-1.json +0 -13
  97. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/queue/pending/task-getall-1.json +0 -10
  98. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/queue/pending/task-status-change.json +0 -10
  99. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/workers/worker-1.json +0 -5
  100. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/workers/worker-2.json +0 -5
  101. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/progress.json +0 -10
  102. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/queue/completed/task-getall-2.json +0 -10
  103. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/queue/pending/task-1.json +0 -13
  104. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/queue/pending/task-getall-1.json +0 -10
  105. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/queue/pending/task-status-change.json +0 -10
  106. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/workers/worker-1.json +0 -5
  107. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/workers/worker-2.json +0 -5
  108. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/progress.json +0 -10
  109. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/queue/completed/task-getall-2.json +0 -10
  110. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/queue/pending/task-1.json +0 -13
  111. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/queue/pending/task-getall-1.json +0 -10
  112. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/queue/pending/task-status-change.json +0 -10
  113. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/workers/worker-1.json +0 -5
  114. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/workers/worker-2.json +0 -5
@@ -0,0 +1,278 @@
1
+ import type { Logger } from "pino";
2
+ import { GitWorkspaceError } from "@aad/shared/errors";
3
+
4
+ export interface PrManagerOptions {
5
+ repoRoot: string;
6
+ logger?: Logger;
7
+ }
8
+
9
+ export interface DraftPrOptions {
10
+ title: string;
11
+ body: string;
12
+ baseBranch: string;
13
+ headBranch: string;
14
+ }
15
+
16
+ export interface PrInfo {
17
+ number: number;
18
+ url: string;
19
+ state: "draft" | "open" | "closed";
20
+ }
21
+
22
+ /**
23
+ * Manage Pull Request lifecycle via GitHub CLI (gh)
24
+ */
25
+ export class PrManager {
26
+ private repoRoot: string;
27
+ private logger?: Logger;
28
+
29
+ constructor(options: PrManagerOptions) {
30
+ this.repoRoot = options.repoRoot;
31
+ this.logger = options.logger;
32
+ }
33
+
34
+ /**
35
+ * Check if gh CLI is installed
36
+ */
37
+ async checkGhInstalled(): Promise<boolean> {
38
+ try {
39
+ const proc = Bun.spawn(["gh", "--version"], {
40
+ cwd: this.repoRoot,
41
+ stdout: "pipe",
42
+ stderr: "pipe",
43
+ });
44
+ const exitCode = await proc.exited;
45
+ return exitCode === 0;
46
+ } catch {
47
+ return false;
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Create a draft PR
53
+ */
54
+ async createDraftPr(options: DraftPrOptions): Promise<PrInfo> {
55
+ this.logger?.info({ headBranch: options.headBranch, baseBranch: options.baseBranch }, "Creating draft PR");
56
+
57
+ if (!(await this.checkGhInstalled())) {
58
+ throw new GitWorkspaceError("GitHub CLI (gh) is not installed or not in PATH", {
59
+ suggestion: "Install gh from https://cli.github.com/",
60
+ });
61
+ }
62
+
63
+ try {
64
+ // Create draft PR using gh CLI
65
+ const proc = Bun.spawn(
66
+ [
67
+ "gh",
68
+ "pr",
69
+ "create",
70
+ "--draft",
71
+ "--title",
72
+ options.title,
73
+ "--body",
74
+ options.body,
75
+ "--base",
76
+ options.baseBranch,
77
+ "--head",
78
+ options.headBranch,
79
+ ],
80
+ {
81
+ cwd: this.repoRoot,
82
+ stdout: "pipe",
83
+ stderr: "pipe",
84
+ }
85
+ );
86
+
87
+ const exitCode = await proc.exited;
88
+ const stdout = proc.stdout ? await new Response(proc.stdout).text() : "";
89
+ const stderr = proc.stderr ? await new Response(proc.stderr).text() : "";
90
+
91
+ if (exitCode !== 0) {
92
+ throw new Error(`gh pr create failed: ${stderr}`);
93
+ }
94
+
95
+ // Parse PR URL from stdout (gh prints the PR URL)
96
+ const url = stdout.trim();
97
+ const match = url.match(/\/pull\/(\d+)$/);
98
+ const number = match?.[1] ? parseInt(match[1], 10) : 0;
99
+
100
+ this.logger?.info({ prNumber: number, url }, "Draft PR created");
101
+
102
+ return {
103
+ number,
104
+ url,
105
+ state: "draft",
106
+ };
107
+ } catch (error) {
108
+ throw new GitWorkspaceError("Failed to create draft PR", {
109
+ headBranch: options.headBranch,
110
+ baseBranch: options.baseBranch,
111
+ error: error instanceof Error ? error.message : String(error),
112
+ });
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Mark draft PR as ready for review
118
+ */
119
+ async markPrReady(prNumber: number): Promise<void> {
120
+ this.logger?.info({ prNumber }, "Marking PR as ready for review");
121
+
122
+ if (!(await this.checkGhInstalled())) {
123
+ throw new GitWorkspaceError("GitHub CLI (gh) is not installed or not in PATH", {
124
+ suggestion: "Install gh from https://cli.github.com/",
125
+ });
126
+ }
127
+
128
+ try {
129
+ const proc = Bun.spawn(["gh", "pr", "ready", String(prNumber)], {
130
+ cwd: this.repoRoot,
131
+ stdout: "pipe",
132
+ stderr: "pipe",
133
+ });
134
+
135
+ const exitCode = await proc.exited;
136
+ const stderr = proc.stderr ? await new Response(proc.stderr).text() : "";
137
+
138
+ if (exitCode !== 0) {
139
+ throw new Error(`gh pr ready failed: ${stderr}`);
140
+ }
141
+
142
+ this.logger?.info({ prNumber }, "PR marked as ready");
143
+ } catch (error) {
144
+ throw new GitWorkspaceError("Failed to mark PR as ready", {
145
+ prNumber,
146
+ error: error instanceof Error ? error.message : String(error),
147
+ });
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Get PR info
153
+ */
154
+ async getPrInfo(prNumber: number): Promise<PrInfo | null> {
155
+ if (!(await this.checkGhInstalled())) {
156
+ return null;
157
+ }
158
+
159
+ try {
160
+ const proc = Bun.spawn(
161
+ ["gh", "pr", "view", String(prNumber), "--json", "number,url,isDraft,state"],
162
+ {
163
+ cwd: this.repoRoot,
164
+ stdout: "pipe",
165
+ stderr: "pipe",
166
+ }
167
+ );
168
+
169
+ const exitCode = await proc.exited;
170
+ const stdout = proc.stdout ? await new Response(proc.stdout).text() : "";
171
+
172
+ if (exitCode !== 0) {
173
+ return null;
174
+ }
175
+
176
+ const data = JSON.parse(stdout) as {
177
+ number: number;
178
+ url: string;
179
+ isDraft: boolean;
180
+ state: string;
181
+ };
182
+
183
+ return {
184
+ number: data.number,
185
+ url: data.url,
186
+ state: data.isDraft ? "draft" : data.state === "OPEN" ? "open" : "closed",
187
+ };
188
+ } catch {
189
+ return null;
190
+ }
191
+ }
192
+
193
+ /**
194
+ * Close PR
195
+ */
196
+ async closePr(prNumber: number): Promise<void> {
197
+ this.logger?.info({ prNumber }, "Closing PR");
198
+
199
+ if (!(await this.checkGhInstalled())) {
200
+ throw new GitWorkspaceError("GitHub CLI (gh) is not installed or not in PATH", {
201
+ suggestion: "Install gh from https://cli.github.com/",
202
+ });
203
+ }
204
+
205
+ try {
206
+ const proc = Bun.spawn(["gh", "pr", "close", String(prNumber)], {
207
+ cwd: this.repoRoot,
208
+ stdout: "pipe",
209
+ stderr: "pipe",
210
+ });
211
+
212
+ const exitCode = await proc.exited;
213
+ const stderr = proc.stderr ? await new Response(proc.stderr).text() : "";
214
+
215
+ if (exitCode !== 0) {
216
+ throw new Error(`gh pr close failed: ${stderr}`);
217
+ }
218
+
219
+ this.logger?.info({ prNumber }, "PR closed");
220
+ } catch (error) {
221
+ throw new GitWorkspaceError("Failed to close PR", {
222
+ prNumber,
223
+ error: error instanceof Error ? error.message : String(error),
224
+ });
225
+ }
226
+ }
227
+
228
+ /**
229
+ * Find PR by head branch
230
+ */
231
+ async findPrByBranch(headBranch: string): Promise<PrInfo | null> {
232
+ if (!(await this.checkGhInstalled())) {
233
+ return null;
234
+ }
235
+
236
+ try {
237
+ const proc = Bun.spawn(
238
+ ["gh", "pr", "list", "--head", headBranch, "--json", "number,url,isDraft,state", "--limit", "1"],
239
+ {
240
+ cwd: this.repoRoot,
241
+ stdout: "pipe",
242
+ stderr: "pipe",
243
+ }
244
+ );
245
+
246
+ const exitCode = await proc.exited;
247
+ const stdout = proc.stdout ? await new Response(proc.stdout).text() : "";
248
+
249
+ if (exitCode !== 0) {
250
+ return null;
251
+ }
252
+
253
+ const data = JSON.parse(stdout) as Array<{
254
+ number: number;
255
+ url: string;
256
+ isDraft: boolean;
257
+ state: string;
258
+ }>;
259
+
260
+ if (data.length === 0) {
261
+ return null;
262
+ }
263
+
264
+ const pr = data[0];
265
+ if (!pr) {
266
+ return null;
267
+ }
268
+
269
+ return {
270
+ number: pr.number,
271
+ url: pr.url,
272
+ state: pr.isDraft ? "draft" : pr.state === "OPEN" ? "open" : "closed",
273
+ };
274
+ } catch {
275
+ return null;
276
+ }
277
+ }
278
+ }
@@ -0,0 +1,302 @@
1
+ import { mkdir, readdir, cp, stat } from "node:fs/promises";
2
+ import { existsSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import type { Logger } from "pino";
5
+ import { loadAndMergeSettings, saveSettings } from "./settings-merge";
6
+
7
+ export interface CopyTemplatesOptions {
8
+ targetDir: string; // worktreeパス
9
+ projectRoot: string;
10
+ templateDir?: string; // default: resolveTemplateDir(projectRoot)
11
+ logger?: Logger;
12
+ }
13
+
14
+ const PROJECT_CONTEXT_HEADER = "# プロジェクト固有コンテキスト\n\n";
15
+
16
+ /**
17
+ * Resolve template directory path:
18
+ * 1. Project local override: {projectRoot}/.aad/templates/ (if exists)
19
+ * 2. Package bundled: {packageRoot}/templates/
20
+ */
21
+ export function resolveTemplateDir(projectRoot: string): string {
22
+ const localTemplateDir = join(projectRoot, ".aad", "templates");
23
+
24
+ // Check if local override exists
25
+ if (existsSync(localTemplateDir)) {
26
+ return localTemplateDir;
27
+ }
28
+
29
+ // Fallback to package bundled templates
30
+ // src/modules/git-workspace/ -> ../../../.aad/templates
31
+ const packageRoot = join(import.meta.dir, "..", "..", "..");
32
+ return join(packageRoot, ".aad", "templates");
33
+ }
34
+
35
+ /**
36
+ * Copy templates from .aad/templates/ to target worktree
37
+ * Implements overlay merge strategy:
38
+ * - CLAUDE.md: template base + project-specific append
39
+ * - .claude/: project base + template overlay (agents, hooks, rules, skills)
40
+ * - settings.json: deep merge
41
+ * - .gitignore: copy from template
42
+ */
43
+ export async function copyTemplatesToWorktree(
44
+ options: CopyTemplatesOptions
45
+ ): Promise<void> {
46
+ const {
47
+ targetDir,
48
+ projectRoot,
49
+ templateDir = resolveTemplateDir(projectRoot),
50
+ logger,
51
+ } = options;
52
+
53
+ logger?.info({ targetDir, templateDir }, "Copying templates to worktree");
54
+
55
+ // Ensure target directory exists
56
+ await mkdir(targetDir, { recursive: true });
57
+
58
+ // Each step is non-critical, continue on errors
59
+ try {
60
+ // (a) CLAUDE.md: template base + project CLAUDE.md appended
61
+ await copyCLAUDEmd({
62
+ templateDir,
63
+ projectRoot,
64
+ targetDir,
65
+ logger,
66
+ });
67
+ } catch (error) {
68
+ logger?.debug({ error }, "CLAUDE.md copy failed (non-critical)");
69
+ }
70
+
71
+ try {
72
+ // (b) .claude/: overlay merge (agents, hooks, rules, skills)
73
+ await copyClaudeDir({
74
+ templateDir,
75
+ projectRoot,
76
+ targetDir,
77
+ logger,
78
+ });
79
+ } catch (error) {
80
+ logger?.debug({ error }, ".claude/ copy failed (non-critical)");
81
+ }
82
+
83
+ try {
84
+ // (c) settings.json: deep merge
85
+ await mergeSettingsJson({
86
+ templateDir,
87
+ projectRoot,
88
+ targetDir,
89
+ logger,
90
+ });
91
+ } catch (error) {
92
+ logger?.debug({ error }, "settings.json merge failed (non-critical)");
93
+ }
94
+
95
+ try {
96
+ // (d) .gitignore: copy from template
97
+ await copyGitignore({
98
+ templateDir,
99
+ targetDir,
100
+ logger,
101
+ });
102
+ } catch (error) {
103
+ logger?.debug({ error }, ".gitignore copy failed (non-critical)");
104
+ }
105
+
106
+ logger?.info({ targetDir }, "Template copy completed");
107
+ }
108
+
109
+ /**
110
+ * Copy CLAUDE.md: template base + project content appended
111
+ */
112
+ async function copyCLAUDEmd(opts: {
113
+ templateDir: string;
114
+ projectRoot: string;
115
+ targetDir: string;
116
+ logger?: Logger;
117
+ }): Promise<void> {
118
+ const { templateDir, projectRoot, targetDir, logger } = opts;
119
+
120
+ const templateCLAUDE = join(templateDir, "CLAUDE.md");
121
+ const projectCLAUDE = join(projectRoot, "CLAUDE.md");
122
+ const targetCLAUDE = join(targetDir, "CLAUDE.md");
123
+
124
+ let content = "";
125
+
126
+ // Start with template CLAUDE.md if exists
127
+ try {
128
+ content = await Bun.file(templateCLAUDE).text();
129
+ logger?.debug({ templateCLAUDE }, "Loaded template CLAUDE.md");
130
+ } catch {
131
+ logger?.debug({ templateCLAUDE }, "No template CLAUDE.md found (skipping)");
132
+ }
133
+
134
+ // Append project CLAUDE.md if exists
135
+ try {
136
+ const projectContent = await Bun.file(projectCLAUDE).text();
137
+ if (projectContent.trim()) {
138
+ content += "\n\n" + PROJECT_CONTEXT_HEADER + projectContent;
139
+ logger?.debug({ projectCLAUDE }, "Appended project CLAUDE.md");
140
+ }
141
+ } catch {
142
+ logger?.debug({ projectCLAUDE }, "No project CLAUDE.md found (skipping)");
143
+ }
144
+
145
+ // Write to target
146
+ if (content.trim()) {
147
+ await Bun.write(targetCLAUDE, content);
148
+ logger?.debug({ targetCLAUDE }, "Written CLAUDE.md");
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Copy .claude/ directory: project base + template overlay
154
+ * Overlay: agents, hooks, rules, skills from template
155
+ * Preserve: agent-memory from project (existing wins)
156
+ */
157
+ async function copyClaudeDir(opts: {
158
+ templateDir: string;
159
+ projectRoot: string;
160
+ targetDir: string;
161
+ logger?: Logger;
162
+ }): Promise<void> {
163
+ const { templateDir, projectRoot, targetDir, logger } = opts;
164
+
165
+ const projectClaudeDir = join(projectRoot, ".claude");
166
+ const templateClaudeDir = join(templateDir, ".claude");
167
+ const targetClaudeDir = join(targetDir, ".claude");
168
+
169
+ // Ensure target .claude/ exists
170
+ await mkdir(targetClaudeDir, { recursive: true });
171
+
172
+ // Copy project .claude/ subdirectories as base layer (if exists), excluding agent-memory
173
+ // We manually copy subdirectories to exclude agent-memory
174
+ try {
175
+ let projectClaudeExists = false;
176
+ try {
177
+ const stats = await stat(projectClaudeDir);
178
+ projectClaudeExists = stats.isDirectory();
179
+ } catch {
180
+ projectClaudeExists = false;
181
+ }
182
+
183
+ if (projectClaudeExists) {
184
+ const entries = await readdir(projectClaudeDir, { withFileTypes: true });
185
+ for (const entry of entries) {
186
+ if (entry.name === "agent-memory") {
187
+ continue; // Skip agent-memory, will handle separately
188
+ }
189
+ const src = join(projectClaudeDir, entry.name);
190
+ const dest = join(targetClaudeDir, entry.name);
191
+ if (entry.isDirectory()) {
192
+ await cp(src, dest, { recursive: true });
193
+ } else {
194
+ await cp(src, dest);
195
+ }
196
+ }
197
+ logger?.debug({ projectClaudeDir }, "Copied project .claude/ as base (excluding agent-memory)");
198
+ }
199
+ } catch (error) {
200
+ logger?.debug({ projectClaudeDir, error }, "No project .claude/ found or copy failed");
201
+ }
202
+
203
+ // Overlay template subdirectories (agents, hooks, rules, skills)
204
+ const overlayDirs = ["agents", "hooks", "rules", "skills"];
205
+
206
+ for (const subdir of overlayDirs) {
207
+ const templateSubdir = join(templateClaudeDir, subdir);
208
+ const targetSubdir = join(targetClaudeDir, subdir);
209
+
210
+ try {
211
+ // Ensure target subdir exists
212
+ await mkdir(targetSubdir, { recursive: true });
213
+
214
+ // Copy template files (overwrite)
215
+ await cp(templateSubdir, targetSubdir, { recursive: true });
216
+ logger?.debug({ templateSubdir, targetSubdir }, `Overlayed ${subdir}/`);
217
+ } catch {
218
+ logger?.debug({ templateSubdir }, `No template ${subdir}/ found (skipping)`);
219
+ }
220
+ }
221
+
222
+ // Handle agent-memory: existing (project) wins, don't overwrite
223
+ const projectMemoryDir = join(projectClaudeDir, "agent-memory");
224
+ const templateMemoryDir = join(templateClaudeDir, "agent-memory");
225
+ const targetMemoryDir = join(targetClaudeDir, "agent-memory");
226
+
227
+ try {
228
+ // Check if project has agent-memory directory
229
+ let projectMemoryExists = false;
230
+ try {
231
+ const stats = await stat(projectMemoryDir);
232
+ projectMemoryExists = stats.isDirectory();
233
+ } catch {
234
+ projectMemoryExists = false;
235
+ }
236
+
237
+ if (projectMemoryExists) {
238
+ // Copy project agent-memory
239
+ await cp(projectMemoryDir, targetMemoryDir, { recursive: true });
240
+ logger?.debug({ projectMemoryDir }, "Preserved project agent-memory/");
241
+ } else {
242
+ // No project memory, copy template if exists
243
+ try {
244
+ await cp(templateMemoryDir, targetMemoryDir, { recursive: true });
245
+ logger?.debug({ templateMemoryDir }, "Copied template agent-memory/");
246
+ } catch {
247
+ logger?.debug("No template agent-memory found");
248
+ }
249
+ }
250
+ } catch (error) {
251
+ logger?.debug({ error }, "No agent-memory to handle");
252
+ }
253
+ }
254
+
255
+ /**
256
+ * Merge settings.json: project + template deep merge
257
+ */
258
+ async function mergeSettingsJson(opts: {
259
+ templateDir: string;
260
+ projectRoot: string;
261
+ targetDir: string;
262
+ logger?: Logger;
263
+ }): Promise<void> {
264
+ const { templateDir, projectRoot, targetDir, logger } = opts;
265
+
266
+ const projectSettings = join(projectRoot, ".claude", "settings.json");
267
+ const templateSettings = join(templateDir, "settings.json");
268
+ const targetSettings = join(targetDir, ".claude", "settings.json");
269
+
270
+ // Load and merge settings
271
+ const merged = await loadAndMergeSettings(
272
+ [projectSettings, templateSettings],
273
+ logger
274
+ );
275
+
276
+ // Save to target
277
+ await mkdir(join(targetDir, ".claude"), { recursive: true });
278
+ await saveSettings(targetSettings, merged, logger);
279
+
280
+ logger?.debug({ targetSettings }, "Merged and saved settings.json");
281
+ }
282
+
283
+ /**
284
+ * Copy .gitignore from template
285
+ */
286
+ async function copyGitignore(opts: {
287
+ templateDir: string;
288
+ targetDir: string;
289
+ logger?: Logger;
290
+ }): Promise<void> {
291
+ const { templateDir, targetDir, logger } = opts;
292
+
293
+ const templateGitignore = join(templateDir, ".gitignore");
294
+ const targetGitignore = join(targetDir, ".gitignore");
295
+
296
+ try {
297
+ await cp(templateGitignore, targetGitignore, { force: true });
298
+ logger?.debug({ templateGitignore, targetGitignore }, "Copied .gitignore");
299
+ } catch {
300
+ logger?.debug({ templateGitignore }, "No template .gitignore found (skipping)");
301
+ }
302
+ }
@@ -169,16 +169,20 @@ export class WorktreeManager {
169
169
  }
170
170
  }
171
171
 
172
- // Try to force remove directory as fallback
173
- try {
174
- await this.fsOps.rm(worktreePath, { recursive: true, force: true });
175
- await this.gitOps.gitExec(["worktree", "prune"], { cwd: this.repoRoot, logger: this.logger });
176
- } catch (cleanupError) {
177
- this.logger?.debug({ cleanupError, worktreePath }, "Worktree cleanup fallback failed (ignored)");
172
+ // Try to force remove directory as fallback (only if force=true)
173
+ if (force) {
174
+ try {
175
+ await this.fsOps.rm(worktreePath, { recursive: true, force: true });
176
+ await this.gitOps.gitExec(["worktree", "prune"], { cwd: this.repoRoot, logger: this.logger });
177
+ return;
178
+ } catch (cleanupError) {
179
+ this.logger?.debug({ cleanupError, worktreePath }, "Worktree cleanup fallback failed");
180
+ }
178
181
  }
179
182
 
180
183
  throw new GitWorkspaceError("Failed to remove worktree", {
181
184
  worktreePath,
185
+ force,
182
186
  error: error instanceof Error ? error.message : String(error),
183
187
  });
184
188
  }
@@ -217,6 +221,15 @@ export class WorktreeManager {
217
221
  }
218
222
  }
219
223
 
224
+ // Push the last worktree if it hasn't been pushed yet
225
+ if (current.path && current.head) {
226
+ worktrees.push({
227
+ path: current.path,
228
+ branch: current.branch ?? "(detached)",
229
+ head: current.head,
230
+ });
231
+ }
232
+
220
233
  return worktrees;
221
234
  }
222
235
 
@@ -256,16 +269,24 @@ export class WorktreeManager {
256
269
  export async function cleanupOrphanedFromPreviousRuns(
257
270
  worktreeManager: WorktreeManager,
258
271
  logger: Logger,
259
- ): Promise<{ removedWorktrees: number; deletedBranches: number }> {
272
+ ): Promise<{ removedWorktrees: number; deletedBranches: number; preservedBranches: number }> {
260
273
  logger.info("Cleaning up orphaned worktrees/branches from previous runs");
261
274
 
262
275
  let removedWorktrees = 0;
263
276
  let deletedBranches = 0;
277
+ let preservedBranches = 0;
264
278
 
265
- // 1. Remove all worktrees under .aad/worktrees/
279
+ // 1. Remove all worktrees under .aad/worktrees/ (except parent worktrees)
266
280
  try {
267
281
  const worktrees = await worktreeManager.listWorktrees();
268
- const aadWorktrees = worktrees.filter((wt) => wt.path.includes("/.aad/worktrees/"));
282
+ const aadWorktrees = worktrees.filter((wt) => {
283
+ if (!wt.path.includes("/.aad/worktrees/")) return false;
284
+ if (/\/parent-[^/]+$/.test(wt.path)) {
285
+ logger.info({ path: wt.path }, "Preserving parent worktree (contains commit history)");
286
+ return false;
287
+ }
288
+ return true;
289
+ });
269
290
 
270
291
  for (const wt of aadWorktrees) {
271
292
  try {
@@ -307,6 +328,11 @@ export async function cleanupOrphanedFromPreviousRuns(
307
328
  .filter((b: string) => b.length > 0);
308
329
 
309
330
  for (const branch of branches) {
331
+ if (branch.endsWith("/parent")) {
332
+ logger.info({ branch }, "Preserving parent branch (contains commit history)");
333
+ preservedBranches++;
334
+ continue;
335
+ }
310
336
  try {
311
337
  await gitOps.gitExec(["branch", "-D", branch], { cwd: repoRoot });
312
338
  deletedBranches++;
@@ -323,6 +349,6 @@ export async function cleanupOrphanedFromPreviousRuns(
323
349
  logger.debug({ error }, "Failed to list branches during cleanup");
324
350
  }
325
351
 
326
- logger.info({ removedWorktrees, deletedBranches }, "Orphaned cleanup completed");
327
- return { removedWorktrees, deletedBranches };
352
+ logger.info({ removedWorktrees, deletedBranches, preservedBranches }, "Orphaned cleanup completed");
353
+ return { removedWorktrees, deletedBranches, preservedBranches };
328
354
  }
@@ -54,6 +54,7 @@ function createMockConfig(teamsEnabled: boolean = false): Config {
54
54
  port: 7333,
55
55
  host: "localhost",
56
56
  },
57
+ git: { autoPush: false },
57
58
  };
58
59
  }
59
60