@ronkovic/aad 0.3.9 → 0.5.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 (132) hide show
  1. package/README.md +332 -14
  2. package/package.json +6 -1
  3. package/src/__tests__/e2e/cleanup-e2e.test.ts +186 -0
  4. package/src/__tests__/e2e/dashboard-api-e2e.test.ts +87 -0
  5. package/src/__tests__/e2e/pipeline-e2e.test.ts +10 -68
  6. package/src/__tests__/e2e/resume-e2e.test.ts +9 -11
  7. package/src/__tests__/e2e/retry-e2e.test.ts +285 -0
  8. package/src/__tests__/e2e/status-e2e.test.ts +227 -0
  9. package/src/__tests__/e2e/tdd-pipeline-e2e.test.ts +360 -0
  10. package/src/__tests__/helpers/index.ts +6 -0
  11. package/src/__tests__/helpers/mock-claude-provider.ts +53 -0
  12. package/src/__tests__/helpers/mock-logger.ts +36 -0
  13. package/src/__tests__/helpers/wait-helpers.ts +34 -0
  14. package/src/__tests__/integration/pipeline.test.ts +3 -0
  15. package/src/modules/claude-provider/__tests__/claude-sdk-real-env.test.ts +1 -1
  16. package/src/modules/claude-provider/__tests__/claude-sdk.adapter.test.ts +6 -0
  17. package/src/modules/claude-provider/__tests__/provider-registry.test.ts +3 -0
  18. package/src/modules/cli/__tests__/cleanup.test.ts +73 -0
  19. package/src/modules/cli/__tests__/resume.test.ts +4 -0
  20. package/src/modules/cli/__tests__/run.test.ts +37 -0
  21. package/src/modules/cli/__tests__/status.test.ts +1 -0
  22. package/src/modules/cli/app.ts +2 -0
  23. package/src/modules/cli/commands/__tests__/task-dispatch-handler.test.ts +145 -0
  24. package/src/modules/cli/commands/cleanup.ts +26 -11
  25. package/src/modules/cli/commands/resume.ts +14 -8
  26. package/src/modules/cli/commands/run.ts +70 -8
  27. package/src/modules/cli/commands/task-dispatch-handler.ts +73 -3
  28. package/src/modules/dashboard/__tests__/api-graph.test.ts +332 -0
  29. package/src/modules/dashboard/__tests__/api-timeline.test.ts +461 -0
  30. package/src/modules/dashboard/routes/sse.ts +3 -2
  31. package/src/modules/dashboard/server.ts +1 -0
  32. package/src/modules/dashboard/services/sse-broadcaster.ts +29 -0
  33. package/src/modules/dashboard/ui/dashboard.html +640 -349
  34. package/src/modules/git-workspace/__tests__/branch-manager.test.ts +52 -0
  35. package/src/modules/git-workspace/__tests__/dependency-installer.test.ts +77 -0
  36. package/src/modules/git-workspace/__tests__/git-exec.test.ts +26 -0
  37. package/src/modules/git-workspace/__tests__/merge-service.test.ts +19 -0
  38. package/src/modules/git-workspace/__tests__/pr-manager.test.ts +80 -0
  39. package/src/modules/git-workspace/__tests__/template-copy.test.ts +189 -0
  40. package/src/modules/git-workspace/__tests__/worktree-cleanup.test.ts +29 -2
  41. package/src/modules/git-workspace/__tests__/worktree-manager.test.ts +64 -4
  42. package/src/modules/git-workspace/branch-manager.ts +24 -3
  43. package/src/modules/git-workspace/dependency-installer.ts +113 -0
  44. package/src/modules/git-workspace/git-exec.ts +3 -2
  45. package/src/modules/git-workspace/index.ts +10 -1
  46. package/src/modules/git-workspace/merge-service.ts +36 -2
  47. package/src/modules/git-workspace/pr-manager.ts +278 -0
  48. package/src/modules/git-workspace/template-copy.ts +302 -0
  49. package/src/modules/git-workspace/worktree-manager.ts +37 -11
  50. package/src/modules/planning/__tests__/planning-service.test.ts +3 -0
  51. package/src/modules/planning/__tests__/planning.service.test.ts +149 -0
  52. package/src/modules/planning/__tests__/project-detection.test.ts +7 -1
  53. package/src/modules/planning/planning.service.ts +16 -2
  54. package/src/modules/planning/project-detection.ts +4 -1
  55. package/src/modules/process-manager/__tests__/process-manager.test.ts +3 -0
  56. package/src/modules/process-manager/process-manager.ts +2 -1
  57. package/src/modules/task-execution/__tests__/executor.test.ts +496 -0
  58. package/src/modules/task-execution/__tests__/tester-verify.test.ts +4 -3
  59. package/src/modules/task-execution/executor.ts +163 -4
  60. package/src/modules/task-execution/phases/implementer-green.ts +22 -5
  61. package/src/modules/task-execution/phases/merge.ts +44 -2
  62. package/src/modules/task-execution/phases/tester-red.ts +22 -5
  63. package/src/modules/task-execution/phases/tester-verify.ts +22 -6
  64. package/src/modules/task-queue/dispatcher.ts +96 -3
  65. package/src/shared/__tests__/config.test.ts +30 -0
  66. package/src/shared/__tests__/events.test.ts +42 -16
  67. package/src/shared/__tests__/prerequisites.test.ts +176 -0
  68. package/src/shared/__tests__/shutdown-handler.test.ts +96 -0
  69. package/src/shared/config.ts +10 -0
  70. package/src/shared/events.ts +5 -0
  71. package/src/shared/memory-check.ts +2 -2
  72. package/src/shared/prerequisites.ts +190 -0
  73. package/src/shared/shutdown-handler.ts +12 -5
  74. package/src/shared/types.ts +25 -0
  75. package/templates/CLAUDE.md +122 -0
  76. package/templates/settings.json +117 -0
  77. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/progress.json +0 -10
  78. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/queue/completed/task-getall-2.json +0 -10
  79. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/queue/pending/task-1.json +0 -13
  80. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/queue/pending/task-getall-1.json +0 -10
  81. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/queue/pending/task-status-change.json +0 -10
  82. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/workers/worker-1.json +0 -5
  83. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/workers/worker-2.json +0 -5
  84. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/progress.json +0 -10
  85. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/queue/completed/task-getall-2.json +0 -10
  86. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/queue/pending/task-1.json +0 -13
  87. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/queue/pending/task-getall-1.json +0 -10
  88. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/queue/pending/task-status-change.json +0 -10
  89. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/workers/worker-1.json +0 -5
  90. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/workers/worker-2.json +0 -5
  91. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/progress.json +0 -10
  92. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/queue/completed/task-getall-2.json +0 -10
  93. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/queue/pending/task-1.json +0 -13
  94. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/queue/pending/task-getall-1.json +0 -10
  95. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/queue/pending/task-status-change.json +0 -10
  96. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/workers/worker-1.json +0 -5
  97. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/workers/worker-2.json +0 -5
  98. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/progress.json +0 -10
  99. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/queue/completed/task-getall-2.json +0 -10
  100. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/queue/pending/task-1.json +0 -13
  101. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/queue/pending/task-getall-1.json +0 -10
  102. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/queue/pending/task-status-change.json +0 -10
  103. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/workers/worker-1.json +0 -5
  104. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/workers/worker-2.json +0 -5
  105. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/progress.json +0 -10
  106. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/queue/completed/task-getall-2.json +0 -10
  107. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/queue/pending/task-1.json +0 -13
  108. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/queue/pending/task-getall-1.json +0 -10
  109. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/queue/pending/task-status-change.json +0 -10
  110. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/workers/worker-1.json +0 -5
  111. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/workers/worker-2.json +0 -5
  112. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/progress.json +0 -10
  113. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/queue/completed/task-getall-2.json +0 -10
  114. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/queue/pending/task-1.json +0 -13
  115. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/queue/pending/task-getall-1.json +0 -10
  116. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/queue/pending/task-status-change.json +0 -10
  117. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/workers/worker-1.json +0 -5
  118. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/workers/worker-2.json +0 -5
  119. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/progress.json +0 -10
  120. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/queue/completed/task-getall-2.json +0 -10
  121. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/queue/pending/task-1.json +0 -13
  122. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/queue/pending/task-getall-1.json +0 -10
  123. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/queue/pending/task-status-change.json +0 -10
  124. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/workers/worker-1.json +0 -5
  125. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/workers/worker-2.json +0 -5
  126. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/progress.json +0 -10
  127. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/queue/completed/task-getall-2.json +0 -10
  128. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/queue/pending/task-1.json +0 -13
  129. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/queue/pending/task-getall-1.json +0 -10
  130. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/queue/pending/task-status-change.json +0 -10
  131. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/workers/worker-1.json +0 -5
  132. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/workers/worker-2.json +0 -5
@@ -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,9 @@ function createMockConfig(teamsEnabled: boolean = false): Config {
54
54
  port: 7333,
55
55
  host: "localhost",
56
56
  },
57
+ git: { autoPush: false },
58
+ skipCompleted: true,
59
+ strictTdd: false,
57
60
  };
58
61
  }
59
62
 
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Planning Service Tests
3
+ * Verifies task planning orchestration and task_plan.json persistence
4
+ */
5
+
6
+ import { describe, test, expect, beforeEach, afterEach, mock } from "bun:test";
7
+ import { PlanningService } from "../planning.service";
8
+ import { EventBus } from "@aad/shared/events";
9
+ import { loadConfig } from "@aad/shared/config";
10
+ import { pino } from "pino";
11
+ import { createRunId } from "@aad/shared/types";
12
+ import { mkdir, readFile, writeFile, rm } from "node:fs/promises";
13
+ import { join } from "node:path";
14
+ import { tmpdir } from "node:os";
15
+ import type { ClaudeProvider } from "../../claude-provider";
16
+
17
+ describe("PlanningService - task_plan.json persistence", () => {
18
+ let service: PlanningService;
19
+ let mockProvider: ClaudeProvider;
20
+ let eventBus: EventBus;
21
+ let logger: import("pino").Logger;
22
+ let testDocsDir: string;
23
+
24
+ beforeEach(async () => {
25
+ eventBus = new EventBus();
26
+ logger = pino({ level: "silent" });
27
+ const config = loadConfig({ AAD_NUM_WORKERS: "2" });
28
+
29
+ // Create temp docs directory for testing
30
+ testDocsDir = join(tmpdir(), `aad-test-docs-${Date.now()}`);
31
+ await mkdir(testDocsDir, { recursive: true });
32
+
33
+ // Mock Claude provider that writes valid task_plan.json
34
+ mockProvider = {
35
+ call: mock(async (request) => {
36
+ // Extract taskPlanFilePath from prompt
37
+ const match = request.prompt.match(/Write.*to the following path:\s*([^\n]+)/);
38
+ if (!match?.[1]) {
39
+ throw new Error("Could not find task_plan.json path in prompt");
40
+ }
41
+ const taskPlanFilePath = match[1].trim();
42
+
43
+ // Write valid task_plan.json
44
+ const taskPlan = {
45
+ run_id: "test-run-123",
46
+ parent_branch: "main",
47
+ tasks: [
48
+ {
49
+ task_id: "task-001",
50
+ title: "Test Task 1",
51
+ description: "Test description",
52
+ files_to_modify: ["file1.ts"],
53
+ depends_on: [],
54
+ priority: 1,
55
+ },
56
+ ],
57
+ };
58
+
59
+ await writeFile(taskPlanFilePath, JSON.stringify(taskPlan, null, 2));
60
+
61
+ return {
62
+ exitCode: 0,
63
+ result: "Task plan written successfully",
64
+ };
65
+ }) as any,
66
+ };
67
+
68
+ service = new PlanningService(mockProvider, eventBus, config, logger);
69
+ });
70
+
71
+ afterEach(async () => {
72
+ // Cleanup test docs directory
73
+ await rm(testDocsDir, { recursive: true, force: true });
74
+ });
75
+
76
+ test("persists task_plan.json to docs directory on successful planning", async () => {
77
+ const runId = createRunId("test-run-123");
78
+ const requirementsPath = join(testDocsDir, "requirements.md");
79
+ await writeFile(requirementsPath, "# Test Requirements\n\nTest content");
80
+
81
+ const taskPlan = await service.planTasks({
82
+ runId,
83
+ parentBranch: "main",
84
+ requirementsPath,
85
+ targetDocsDir: testDocsDir,
86
+ projectRoot: process.cwd(),
87
+ });
88
+
89
+ expect(taskPlan).toBeDefined();
90
+ expect(taskPlan.tasks.length).toBe(1);
91
+
92
+ // Verify task_plan.json was persisted to docs directory
93
+ const persistedPath = join(testDocsDir, "task_plan.json");
94
+ const persistedContent = await readFile(persistedPath, "utf-8");
95
+ const persistedPlan = JSON.parse(persistedContent);
96
+
97
+ expect(persistedPlan.run_id).toBe("test-run-123");
98
+ expect(persistedPlan.parent_branch).toBe("main");
99
+ expect(persistedPlan.tasks.length).toBe(1);
100
+ expect(persistedPlan.tasks[0].task_id).toBe("task-001");
101
+ });
102
+
103
+ test("handles persistence error gracefully (non-critical)", async () => {
104
+ const runId = createRunId("test-run-456");
105
+ const requirementsPath = join(testDocsDir, "requirements.md");
106
+ await writeFile(requirementsPath, "# Test Requirements\n\nTest content");
107
+
108
+ // Use invalid docs directory (permission denied scenario)
109
+ const invalidDocsDir = "/invalid/path/that/does/not/exist";
110
+
111
+ // Planning should succeed even if persistence fails
112
+ const taskPlan = await service.planTasks({
113
+ runId,
114
+ parentBranch: "main",
115
+ requirementsPath,
116
+ targetDocsDir: invalidDocsDir,
117
+ projectRoot: process.cwd(),
118
+ });
119
+
120
+ expect(taskPlan).toBeDefined();
121
+ expect(taskPlan.tasks.length).toBe(1);
122
+ });
123
+
124
+ test("creates target directory if it does not exist", async () => {
125
+ const runId = createRunId("test-run-789");
126
+ const requirementsPath = join(testDocsDir, "requirements.md");
127
+ await writeFile(requirementsPath, "# Test Requirements\n\nTest content");
128
+
129
+ // Use nested directory that doesn't exist yet
130
+ const nestedDocsDir = join(testDocsDir, "nested", "docs", "dir");
131
+
132
+ const taskPlan = await service.planTasks({
133
+ runId,
134
+ parentBranch: "main",
135
+ requirementsPath,
136
+ targetDocsDir: nestedDocsDir,
137
+ projectRoot: process.cwd(),
138
+ });
139
+
140
+ expect(taskPlan).toBeDefined();
141
+
142
+ // Verify directory was created and file was persisted
143
+ const persistedPath = join(nestedDocsDir, "task_plan.json");
144
+ const persistedContent = await readFile(persistedPath, "utf-8");
145
+ const persistedPlan = JSON.parse(persistedContent);
146
+
147
+ expect(persistedPlan.tasks.length).toBe(1);
148
+ });
149
+ });
@@ -162,7 +162,13 @@ describe("detectPackageManager", () => {
162
162
  expect(result).toBe("npm");
163
163
  });
164
164
 
165
- test("detects bun", async () => {
165
+ test("detects bun (text lockfile)", async () => {
166
+ const checker = createMockFileChecker({ "bun.lock": "" });
167
+ const result = await detectPackageManager("/test", checker);
168
+ expect(result).toBe("bun");
169
+ });
170
+
171
+ test("detects bun (binary lockfile)", async () => {
166
172
  const checker = createMockFileChecker({ "bun.lockb": "" });
167
173
  const result = await detectPackageManager("/test", checker);
168
174
  expect(result).toBe("bun");
@@ -11,7 +11,7 @@ import { parseTaskPlan, validateTaskPlan } from "../task-queue";
11
11
  import { validateFileConflicts, formatConflictErrors } from "./file-conflict-validator";
12
12
  import { analyzeProject, createBunFileChecker, type FileChecker } from "./project-detection";
13
13
  import { join } from "node:path";
14
- import { mkdtemp, readFile, rm } from "node:fs/promises";
14
+ import { mkdtemp, readFile, rm, mkdir } from "node:fs/promises";
15
15
  import { tmpdir } from "node:os";
16
16
 
17
17
  export interface PlanTasksParams {
@@ -141,6 +141,15 @@ export class PlanningService {
141
141
  taskCount: taskPlan.tasks.length,
142
142
  });
143
143
 
144
+ // Save task_plan.json to targetDocsDir
145
+ try {
146
+ await mkdir(targetDocsDir, { recursive: true });
147
+ await Bun.write(join(targetDocsDir, "task_plan.json"), taskPlanJsonStr);
148
+ this.logger.debug({ targetDocsDir }, "Persisted task_plan.json");
149
+ } catch (error) {
150
+ this.logger.warn({ error, targetDocsDir }, "Failed to persist task_plan.json (non-critical)");
151
+ }
152
+
144
153
  return taskPlan;
145
154
  } catch (error) {
146
155
  const errorMessage = error instanceof Error ? error.message : String(error);
@@ -202,7 +211,12 @@ Parent branch: ${parentBranch}
202
211
  Instructions:
203
212
  1. Read the requirements file/directory
204
213
  2. Split into independent tasks with clear file assignments
205
- 3. Set depends_on for tasks that modify the same files (later task depends on earlier)
214
+ 3. Set depends_on based on:
215
+ - File overlap: tasks that modify the same files (later task depends on earlier)
216
+ - Logical dependency: tasks whose output is consumed by another task
217
+ (e.g., DB schema → API that queries the DB → Frontend that calls the API)
218
+ - Import dependency: tasks that create modules imported by other tasks
219
+ If no dependency exists, use an empty array []
206
220
  4. Set priority (1 = highest)
207
221
  5. CRITICAL: Write the result as a JSON file to the following path:
208
222
  ${taskPlanFilePath}
@@ -36,6 +36,7 @@ export type TestFramework =
36
36
  | "jest"
37
37
  | "bun:test"
38
38
  | "mocha"
39
+ | "playwright"
39
40
  | "go-test"
40
41
  | "cargo-test"
41
42
  | "terraform-validate"
@@ -204,7 +205,8 @@ export async function detectPackageManager(
204
205
  if (await checker.exists(path.join(projectRoot, "pnpm-lock.yaml"))) return "pnpm";
205
206
  if (await checker.exists(path.join(projectRoot, "yarn.lock"))) return "yarn";
206
207
  if (await checker.exists(path.join(projectRoot, "package-lock.json"))) return "npm";
207
- if (await checker.exists(path.join(projectRoot, "bun.lockb"))) return "bun";
208
+ if (await checker.exists(path.join(projectRoot, "bun.lock"))) return "bun"; // text format (v1.2+)
209
+ if (await checker.exists(path.join(projectRoot, "bun.lockb"))) return "bun"; // binary format (legacy)
208
210
 
209
211
  // Rust/Go
210
212
  if (await checker.exists(path.join(projectRoot, "Cargo.lock"))) return "cargo";
@@ -290,6 +292,7 @@ export async function detectTestFramework(
290
292
  if (content.includes('"bun-types"') || content.includes('"@types/bun"')) {
291
293
  return "bun:test";
292
294
  }
295
+ if (content.includes('"@playwright/test"')) return "playwright";
293
296
  if (content.includes('"vitest"')) return "vitest";
294
297
  if (content.includes('"jest"')) return "jest";
295
298
  if (content.includes('"mocha"')) return "mocha";
@@ -24,6 +24,9 @@ describe("ProcessManager", () => {
24
24
  teams: { splitter: false, reviewer: false },
25
25
  memorySync: false,
26
26
  dashboard: { enabled: false, port: 7333, host: "localhost" },
27
+ git: { autoPush: false },
28
+ skipCompleted: true,
29
+ strictTdd: false,
27
30
  },
28
31
  logger,
29
32
  });
@@ -29,7 +29,7 @@ export class ProcessManager {
29
29
  /**
30
30
  * Initialize worker pool
31
31
  */
32
- async initializePool(numWorkers: number): Promise<void> {
32
+ initializePool(numWorkers: number): Promise<void> {
33
33
  if (this.initialized) {
34
34
  throw new ProcessManagerError("ProcessManager already initialized");
35
35
  }
@@ -49,6 +49,7 @@ export class ProcessManager {
49
49
 
50
50
  this.initialized = true;
51
51
  this.deps.logger.info({ numWorkers }, "Worker pool initialized");
52
+ return Promise.resolve();
52
53
  }
53
54
 
54
55
  /**