@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.
- package/README.md +332 -14
- package/package.json +6 -1
- package/src/__tests__/e2e/cleanup-e2e.test.ts +186 -0
- package/src/__tests__/e2e/dashboard-api-e2e.test.ts +87 -0
- package/src/__tests__/e2e/pipeline-e2e.test.ts +10 -68
- package/src/__tests__/e2e/resume-e2e.test.ts +9 -11
- package/src/__tests__/e2e/retry-e2e.test.ts +285 -0
- package/src/__tests__/e2e/status-e2e.test.ts +227 -0
- package/src/__tests__/e2e/tdd-pipeline-e2e.test.ts +360 -0
- package/src/__tests__/helpers/index.ts +6 -0
- package/src/__tests__/helpers/mock-claude-provider.ts +53 -0
- package/src/__tests__/helpers/mock-logger.ts +36 -0
- package/src/__tests__/helpers/wait-helpers.ts +34 -0
- package/src/__tests__/integration/pipeline.test.ts +3 -0
- package/src/modules/claude-provider/__tests__/claude-sdk-real-env.test.ts +1 -1
- package/src/modules/claude-provider/__tests__/claude-sdk.adapter.test.ts +6 -0
- package/src/modules/claude-provider/__tests__/provider-registry.test.ts +3 -0
- package/src/modules/cli/__tests__/cleanup.test.ts +73 -0
- package/src/modules/cli/__tests__/resume.test.ts +4 -0
- package/src/modules/cli/__tests__/run.test.ts +37 -0
- package/src/modules/cli/__tests__/status.test.ts +1 -0
- package/src/modules/cli/app.ts +2 -0
- package/src/modules/cli/commands/__tests__/task-dispatch-handler.test.ts +145 -0
- package/src/modules/cli/commands/cleanup.ts +26 -11
- package/src/modules/cli/commands/resume.ts +14 -8
- package/src/modules/cli/commands/run.ts +70 -8
- package/src/modules/cli/commands/task-dispatch-handler.ts +73 -3
- package/src/modules/dashboard/__tests__/api-graph.test.ts +332 -0
- package/src/modules/dashboard/__tests__/api-timeline.test.ts +461 -0
- package/src/modules/dashboard/routes/sse.ts +3 -2
- package/src/modules/dashboard/server.ts +1 -0
- package/src/modules/dashboard/services/sse-broadcaster.ts +29 -0
- package/src/modules/dashboard/ui/dashboard.html +640 -349
- package/src/modules/git-workspace/__tests__/branch-manager.test.ts +52 -0
- package/src/modules/git-workspace/__tests__/dependency-installer.test.ts +77 -0
- package/src/modules/git-workspace/__tests__/git-exec.test.ts +26 -0
- package/src/modules/git-workspace/__tests__/merge-service.test.ts +19 -0
- package/src/modules/git-workspace/__tests__/pr-manager.test.ts +80 -0
- package/src/modules/git-workspace/__tests__/template-copy.test.ts +189 -0
- package/src/modules/git-workspace/__tests__/worktree-cleanup.test.ts +29 -2
- package/src/modules/git-workspace/__tests__/worktree-manager.test.ts +64 -4
- package/src/modules/git-workspace/branch-manager.ts +24 -3
- package/src/modules/git-workspace/dependency-installer.ts +113 -0
- package/src/modules/git-workspace/git-exec.ts +3 -2
- package/src/modules/git-workspace/index.ts +10 -1
- package/src/modules/git-workspace/merge-service.ts +36 -2
- package/src/modules/git-workspace/pr-manager.ts +278 -0
- package/src/modules/git-workspace/template-copy.ts +302 -0
- package/src/modules/git-workspace/worktree-manager.ts +37 -11
- package/src/modules/planning/__tests__/planning-service.test.ts +3 -0
- package/src/modules/planning/__tests__/planning.service.test.ts +149 -0
- package/src/modules/planning/__tests__/project-detection.test.ts +7 -1
- package/src/modules/planning/planning.service.ts +16 -2
- package/src/modules/planning/project-detection.ts +4 -1
- package/src/modules/process-manager/__tests__/process-manager.test.ts +3 -0
- package/src/modules/process-manager/process-manager.ts +2 -1
- package/src/modules/task-execution/__tests__/executor.test.ts +496 -0
- package/src/modules/task-execution/__tests__/tester-verify.test.ts +4 -3
- package/src/modules/task-execution/executor.ts +163 -4
- package/src/modules/task-execution/phases/implementer-green.ts +22 -5
- package/src/modules/task-execution/phases/merge.ts +44 -2
- package/src/modules/task-execution/phases/tester-red.ts +22 -5
- package/src/modules/task-execution/phases/tester-verify.ts +22 -6
- package/src/modules/task-queue/dispatcher.ts +96 -3
- package/src/shared/__tests__/config.test.ts +30 -0
- package/src/shared/__tests__/events.test.ts +42 -16
- package/src/shared/__tests__/prerequisites.test.ts +176 -0
- package/src/shared/__tests__/shutdown-handler.test.ts +96 -0
- package/src/shared/config.ts +10 -0
- package/src/shared/events.ts +5 -0
- package/src/shared/memory-check.ts +2 -2
- package/src/shared/prerequisites.ts +190 -0
- package/src/shared/shutdown-handler.ts +12 -5
- package/src/shared/types.ts +25 -0
- package/templates/CLAUDE.md +122 -0
- package/templates/settings.json +117 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-81991/progress.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-81991/queue/completed/task-getall-2.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-81991/queue/pending/task-1.json +0 -13
- package/src/modules/persistence/__tests__/.tmp-stores-test-81991/queue/pending/task-getall-1.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-81991/queue/pending/task-status-change.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-81991/workers/worker-1.json +0 -5
- package/src/modules/persistence/__tests__/.tmp-stores-test-81991/workers/worker-2.json +0 -5
- package/src/modules/persistence/__tests__/.tmp-stores-test-82469/progress.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-82469/queue/completed/task-getall-2.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-82469/queue/pending/task-1.json +0 -13
- package/src/modules/persistence/__tests__/.tmp-stores-test-82469/queue/pending/task-getall-1.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-82469/queue/pending/task-status-change.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-82469/workers/worker-1.json +0 -5
- package/src/modules/persistence/__tests__/.tmp-stores-test-82469/workers/worker-2.json +0 -5
- package/src/modules/persistence/__tests__/.tmp-stores-test-85301/progress.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-85301/queue/completed/task-getall-2.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-85301/queue/pending/task-1.json +0 -13
- package/src/modules/persistence/__tests__/.tmp-stores-test-85301/queue/pending/task-getall-1.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-85301/queue/pending/task-status-change.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-85301/workers/worker-1.json +0 -5
- package/src/modules/persistence/__tests__/.tmp-stores-test-85301/workers/worker-2.json +0 -5
- package/src/modules/persistence/__tests__/.tmp-stores-test-85759/progress.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-85759/queue/completed/task-getall-2.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-85759/queue/pending/task-1.json +0 -13
- package/src/modules/persistence/__tests__/.tmp-stores-test-85759/queue/pending/task-getall-1.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-85759/queue/pending/task-status-change.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-85759/workers/worker-1.json +0 -5
- package/src/modules/persistence/__tests__/.tmp-stores-test-85759/workers/worker-2.json +0 -5
- package/src/modules/persistence/__tests__/.tmp-stores-test-86184/progress.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-86184/queue/completed/task-getall-2.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-86184/queue/pending/task-1.json +0 -13
- package/src/modules/persistence/__tests__/.tmp-stores-test-86184/queue/pending/task-getall-1.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-86184/queue/pending/task-status-change.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-86184/workers/worker-1.json +0 -5
- package/src/modules/persistence/__tests__/.tmp-stores-test-86184/workers/worker-2.json +0 -5
- package/src/modules/persistence/__tests__/.tmp-stores-test-88026/progress.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-88026/queue/completed/task-getall-2.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-88026/queue/pending/task-1.json +0 -13
- package/src/modules/persistence/__tests__/.tmp-stores-test-88026/queue/pending/task-getall-1.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-88026/queue/pending/task-status-change.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-88026/workers/worker-1.json +0 -5
- package/src/modules/persistence/__tests__/.tmp-stores-test-88026/workers/worker-2.json +0 -5
- package/src/modules/persistence/__tests__/.tmp-stores-test-89475/progress.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-89475/queue/completed/task-getall-2.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-89475/queue/pending/task-1.json +0 -13
- package/src/modules/persistence/__tests__/.tmp-stores-test-89475/queue/pending/task-getall-1.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-89475/queue/pending/task-status-change.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-89475/workers/worker-1.json +0 -5
- package/src/modules/persistence/__tests__/.tmp-stores-test-89475/workers/worker-2.json +0 -5
- package/src/modules/persistence/__tests__/.tmp-stores-test-89924/progress.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-89924/queue/completed/task-getall-2.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-89924/queue/pending/task-1.json +0 -13
- package/src/modules/persistence/__tests__/.tmp-stores-test-89924/queue/pending/task-getall-1.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-89924/queue/pending/task-status-change.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-89924/workers/worker-1.json +0 -5
- 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
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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) =>
|
|
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
|
}
|
|
@@ -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
|
|
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.
|
|
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
|
-
|
|
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
|
/**
|