@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.
- package/README.md +292 -12
- package/package.json +6 -1
- package/src/__tests__/e2e/pipeline-e2e.test.ts +1 -0
- package/src/__tests__/e2e/resume-e2e.test.ts +2 -0
- package/src/__tests__/integration/pipeline.test.ts +1 -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 +2 -0
- package/src/modules/claude-provider/__tests__/provider-registry.test.ts +1 -0
- package/src/modules/cli/__tests__/cleanup.test.ts +72 -0
- package/src/modules/cli/__tests__/resume.test.ts +1 -0
- package/src/modules/cli/__tests__/run.test.ts +1 -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 +3 -2
- package/src/modules/cli/commands/run.ts +57 -7
- 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 +143 -18
- 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 +1 -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 +1 -0
- package/src/modules/task-execution/__tests__/executor.test.ts +86 -0
- package/src/modules/task-execution/__tests__/tester-verify.test.ts +4 -3
- package/src/modules/task-execution/executor.ts +87 -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 +50 -1
- package/src/shared/__tests__/prerequisites.test.ts +176 -0
- package/src/shared/config.ts +6 -0
- package/src/shared/prerequisites.ts +190 -0
- package/src/shared/types.ts +13 -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,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";
|
|
@@ -43,6 +43,7 @@ describe("executeTddPipeline", () => {
|
|
|
43
43
|
teams: { splitter: false, reviewer: false },
|
|
44
44
|
memorySync: true,
|
|
45
45
|
dashboard: { enabled: true, port: 7333, host: "localhost" },
|
|
46
|
+
git: { autoPush: false },
|
|
46
47
|
};
|
|
47
48
|
|
|
48
49
|
const mockProvider: ClaudeProvider = {
|
|
@@ -139,6 +140,7 @@ describe("executeTddPipeline", () => {
|
|
|
139
140
|
teams: { splitter: false, reviewer: false },
|
|
140
141
|
memorySync: true,
|
|
141
142
|
dashboard: { enabled: true, port: 7333, host: "localhost" },
|
|
143
|
+
git: { autoPush: false },
|
|
142
144
|
};
|
|
143
145
|
|
|
144
146
|
let callCount = 0;
|
|
@@ -225,6 +227,7 @@ describe("executeTddPipeline", () => {
|
|
|
225
227
|
teams: { splitter: false, reviewer: false },
|
|
226
228
|
memorySync: true,
|
|
227
229
|
dashboard: { enabled: true, port: 7333, host: "localhost" },
|
|
230
|
+
git: { autoPush: false },
|
|
228
231
|
};
|
|
229
232
|
|
|
230
233
|
const mockProvider: ClaudeProvider = {
|
|
@@ -315,6 +318,7 @@ describe("executeTddPipeline", () => {
|
|
|
315
318
|
teams: { splitter: false, reviewer: false },
|
|
316
319
|
memorySync: true,
|
|
317
320
|
dashboard: { enabled: true, port: 7333, host: "localhost" },
|
|
321
|
+
git: { autoPush: false },
|
|
318
322
|
};
|
|
319
323
|
|
|
320
324
|
let phaseCount = 0;
|
|
@@ -416,6 +420,7 @@ describe("executeTddPipeline", () => {
|
|
|
416
420
|
teams: { splitter: false, reviewer: false },
|
|
417
421
|
memorySync: true,
|
|
418
422
|
dashboard: { enabled: true, port: 7333, host: "localhost" },
|
|
423
|
+
git: { autoPush: false },
|
|
419
424
|
};
|
|
420
425
|
|
|
421
426
|
const mockProvider: ClaudeProvider = {
|
|
@@ -512,6 +517,7 @@ describe("executeTddPipeline", () => {
|
|
|
512
517
|
teams: { splitter: false, reviewer: false },
|
|
513
518
|
memorySync: true,
|
|
514
519
|
dashboard: { enabled: true, port: 7333, host: "localhost" },
|
|
520
|
+
git: { autoPush: false },
|
|
515
521
|
};
|
|
516
522
|
|
|
517
523
|
let callCount = 0;
|
|
@@ -572,6 +578,7 @@ describe("executeTddPipeline", () => {
|
|
|
572
578
|
teams: { splitter: false, reviewer: false },
|
|
573
579
|
memorySync: true,
|
|
574
580
|
dashboard: { enabled: true, port: 7333, host: "localhost" },
|
|
581
|
+
git: { autoPush: false },
|
|
575
582
|
};
|
|
576
583
|
|
|
577
584
|
const mockProvider: ClaudeProvider = {
|
|
@@ -629,6 +636,7 @@ describe("executeTddPipeline", () => {
|
|
|
629
636
|
teams: { splitter: false, reviewer: false },
|
|
630
637
|
memorySync: true,
|
|
631
638
|
dashboard: { enabled: true, port: 7333, host: "localhost" },
|
|
639
|
+
git: { autoPush: false },
|
|
632
640
|
};
|
|
633
641
|
|
|
634
642
|
const mockProvider: ClaudeProvider = {
|
|
@@ -687,6 +695,7 @@ describe("executeTddPipeline", () => {
|
|
|
687
695
|
teams: { splitter: false, reviewer: false },
|
|
688
696
|
memorySync: true,
|
|
689
697
|
dashboard: { enabled: true, port: 7333, host: "localhost" },
|
|
698
|
+
git: { autoPush: false },
|
|
690
699
|
};
|
|
691
700
|
|
|
692
701
|
const mockProvider: ClaudeProvider = {
|
|
@@ -738,6 +747,7 @@ describe("executeTddPipeline", () => {
|
|
|
738
747
|
teams: { splitter: false, reviewer: false },
|
|
739
748
|
memorySync: true,
|
|
740
749
|
dashboard: { enabled: true, port: 7333, host: "localhost" },
|
|
750
|
+
git: { autoPush: false },
|
|
741
751
|
};
|
|
742
752
|
|
|
743
753
|
const mockProvider: ClaudeProvider = {
|
|
@@ -757,4 +767,80 @@ describe("executeTddPipeline", () => {
|
|
|
757
767
|
expect(result.status).toBe("failed");
|
|
758
768
|
expect(result.error).toBe("Unknown error");
|
|
759
769
|
});
|
|
770
|
+
|
|
771
|
+
test("emits warn log events when commit fails (invalid workspace)", async () => {
|
|
772
|
+
const task: Task = {
|
|
773
|
+
taskId: createTaskId("task-commit-warn"),
|
|
774
|
+
title: "Commit warning test",
|
|
775
|
+
description: "Test warn logs on commit failure",
|
|
776
|
+
filesToModify: [],
|
|
777
|
+
dependsOn: [],
|
|
778
|
+
priority: 1,
|
|
779
|
+
status: "running",
|
|
780
|
+
retryCount: 0,
|
|
781
|
+
};
|
|
782
|
+
|
|
783
|
+
// workspace.path が無効 → git add/commit は全て失敗する想定
|
|
784
|
+
const workspace: WorkspaceInfo = {
|
|
785
|
+
path: "/nonexistent-invalid-workspace",
|
|
786
|
+
language: "typescript",
|
|
787
|
+
packageManager: "bun",
|
|
788
|
+
framework: "hono",
|
|
789
|
+
testFramework: "bun-test",
|
|
790
|
+
};
|
|
791
|
+
|
|
792
|
+
const config: Config = {
|
|
793
|
+
workers: { num: 2, max: 8 },
|
|
794
|
+
models: {},
|
|
795
|
+
timeouts: { claude: 1200, test: 600, staleTask: 5400 },
|
|
796
|
+
retry: { maxRetries: 2 },
|
|
797
|
+
debug: false,
|
|
798
|
+
adaptiveEffort: false,
|
|
799
|
+
teams: { splitter: false, reviewer: false },
|
|
800
|
+
memorySync: true,
|
|
801
|
+
dashboard: { enabled: true, port: 7333, host: "localhost" },
|
|
802
|
+
git: { autoPush: false },
|
|
803
|
+
};
|
|
804
|
+
|
|
805
|
+
const mockProvider: ClaudeProvider = {
|
|
806
|
+
async call(): Promise<ClaudeResponse> {
|
|
807
|
+
return { result: "OK", exitCode: 0, model: "claude-sonnet-4-5", effortLevel: "medium", duration: 1000 };
|
|
808
|
+
},
|
|
809
|
+
};
|
|
810
|
+
|
|
811
|
+
const mockMergeService = {
|
|
812
|
+
async mergeToParent(): Promise<MergeResult> {
|
|
813
|
+
return { success: true, message: "Merged" };
|
|
814
|
+
},
|
|
815
|
+
} as unknown as MergeService;
|
|
816
|
+
|
|
817
|
+
const events: Array<{ type: string; entry?: { level?: string; message?: string } }> = [];
|
|
818
|
+
const mockEventBus = {
|
|
819
|
+
on() {},
|
|
820
|
+
off() {},
|
|
821
|
+
emit(event: any) {
|
|
822
|
+
events.push(event);
|
|
823
|
+
},
|
|
824
|
+
} as unknown as EventBus;
|
|
825
|
+
|
|
826
|
+
const mockSpawner = {
|
|
827
|
+
async spawn() {
|
|
828
|
+
return { exitCode: 0, stdout: "OK", stderr: "" };
|
|
829
|
+
},
|
|
830
|
+
};
|
|
831
|
+
|
|
832
|
+
await executeTddPipeline(
|
|
833
|
+
task, workspace, "branch", "main", "/parent", createRunId("run-cw"), config,
|
|
834
|
+
mockProvider, mockMergeService, mockEventBus, mockSpawner
|
|
835
|
+
);
|
|
836
|
+
|
|
837
|
+
const warnEvents = events.filter(
|
|
838
|
+
(e) => e.type === "log:entry" && e.entry?.level === "warn"
|
|
839
|
+
);
|
|
840
|
+
|
|
841
|
+
// Red/Green 両フェーズのコミット失敗 warn が発火
|
|
842
|
+
expect(warnEvents.length).toBeGreaterThanOrEqual(2);
|
|
843
|
+
expect(warnEvents.some((e) => e.entry?.message?.includes("Red phase"))).toBe(true);
|
|
844
|
+
expect(warnEvents.some((e) => e.entry?.message?.includes("Green phase"))).toBe(true);
|
|
845
|
+
});
|
|
760
846
|
});
|
|
@@ -200,16 +200,17 @@ describe("buildTestCommand", () => {
|
|
|
200
200
|
expect(buildTestCommand(workspace)).toEqual(["./gradlew", "test"]);
|
|
201
201
|
});
|
|
202
202
|
|
|
203
|
-
test("
|
|
203
|
+
test("returns fallback for unknown test framework", () => {
|
|
204
204
|
const workspace: WorkspaceInfo = {
|
|
205
205
|
path: "/path/to/workspace",
|
|
206
206
|
language: "unknown",
|
|
207
|
-
packageManager: "
|
|
207
|
+
packageManager: "npm",
|
|
208
208
|
framework: "unknown",
|
|
209
209
|
testFramework: "unknown",
|
|
210
210
|
};
|
|
211
211
|
|
|
212
|
-
|
|
212
|
+
// After fallback implementation, unknown should return npm test
|
|
213
|
+
expect(buildTestCommand(workspace)).toEqual(["npm", "test"]);
|
|
213
214
|
});
|
|
214
215
|
});
|
|
215
216
|
|
|
@@ -3,6 +3,7 @@ import type {
|
|
|
3
3
|
WorkspaceInfo,
|
|
4
4
|
RunId,
|
|
5
5
|
TaskExecutionResult,
|
|
6
|
+
PreviousFailure,
|
|
6
7
|
} from "@aad/shared/types";
|
|
7
8
|
import type { Config } from "@aad/shared/config";
|
|
8
9
|
import type { ClaudeProvider } from "@aad/claude-provider";
|
|
@@ -19,6 +20,14 @@ import { runReviewer } from "./phases/reviewer";
|
|
|
19
20
|
import { runMergePhase } from "./phases/merge";
|
|
20
21
|
import { PhaseError } from "@aad/shared/errors";
|
|
21
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Retry context for TDD pipeline execution
|
|
25
|
+
*/
|
|
26
|
+
export interface RetryContext {
|
|
27
|
+
retryCount: number;
|
|
28
|
+
previousFailure?: PreviousFailure;
|
|
29
|
+
}
|
|
30
|
+
|
|
22
31
|
/**
|
|
23
32
|
* Execute full TDD pipeline for a task
|
|
24
33
|
* Phases: Red → Green → Verify → Review → Merge
|
|
@@ -34,7 +43,8 @@ export async function executeTddPipeline(
|
|
|
34
43
|
provider: ClaudeProvider,
|
|
35
44
|
mergeService: MergeService,
|
|
36
45
|
eventBus: EventBus,
|
|
37
|
-
testSpawner?: ProcessSpawner
|
|
46
|
+
testSpawner?: ProcessSpawner,
|
|
47
|
+
retryContext?: RetryContext
|
|
38
48
|
): Promise<TaskExecutionResult> {
|
|
39
49
|
const startTime = Date.now();
|
|
40
50
|
|
|
@@ -74,7 +84,7 @@ export async function executeTddPipeline(
|
|
|
74
84
|
effortLevel: testerEffort,
|
|
75
85
|
model: config.models.tester,
|
|
76
86
|
timeout: config.timeouts.claude * 1000,
|
|
77
|
-
});
|
|
87
|
+
}, retryContext);
|
|
78
88
|
|
|
79
89
|
if (!redResult.success) {
|
|
80
90
|
eventBus.emit({
|
|
@@ -98,6 +108,33 @@ export async function executeTddPipeline(
|
|
|
98
108
|
duration: Date.now() - redStart,
|
|
99
109
|
});
|
|
100
110
|
|
|
111
|
+
// ===== Commit failing tests =====
|
|
112
|
+
try {
|
|
113
|
+
await gitExec(["add", "-A"], { cwd: workspace.path });
|
|
114
|
+
try {
|
|
115
|
+
await gitExec(["reset", "HEAD", "--", ".claude/"], { cwd: workspace.path });
|
|
116
|
+
} catch {
|
|
117
|
+
// .claude/ がない場合は無視
|
|
118
|
+
}
|
|
119
|
+
await gitExec(
|
|
120
|
+
["commit", "--no-gpg-sign", "-m", `test: Add failing tests for ${task.title}`],
|
|
121
|
+
{ cwd: workspace.path }
|
|
122
|
+
);
|
|
123
|
+
} catch (commitError) {
|
|
124
|
+
// If commit fails (e.g., no changes), log but don't fail the pipeline
|
|
125
|
+
eventBus.emit({
|
|
126
|
+
type: "log:entry",
|
|
127
|
+
entry: {
|
|
128
|
+
level: "warn",
|
|
129
|
+
service: "task-execution",
|
|
130
|
+
message: "Commit after Red phase failed (no changes?)",
|
|
131
|
+
timestamp: Date.now(),
|
|
132
|
+
taskId: task.taskId as string,
|
|
133
|
+
error: String(commitError),
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
101
138
|
// ===== Phase 2: Green - Implement minimal code =====
|
|
102
139
|
eventBus.emit({
|
|
103
140
|
type: "execution:phase:started",
|
|
@@ -110,7 +147,7 @@ export async function executeTddPipeline(
|
|
|
110
147
|
effortLevel: implementerEffort,
|
|
111
148
|
model: config.models.implementer,
|
|
112
149
|
timeout: config.timeouts.claude * 1000,
|
|
113
|
-
});
|
|
150
|
+
}, retryContext);
|
|
114
151
|
|
|
115
152
|
if (!greenResult.success) {
|
|
116
153
|
eventBus.emit({
|
|
@@ -138,13 +175,29 @@ export async function executeTddPipeline(
|
|
|
138
175
|
// Commit changes after Green phase so they can be merged later
|
|
139
176
|
try {
|
|
140
177
|
await gitExec(["add", "-A"], { cwd: workspace.path });
|
|
178
|
+
try {
|
|
179
|
+
await gitExec(["reset", "HEAD", "--", ".claude/"], { cwd: workspace.path });
|
|
180
|
+
} catch {
|
|
181
|
+
// .claude/ がない場合は無視
|
|
182
|
+
}
|
|
141
183
|
await gitExec(
|
|
142
184
|
["commit", "--no-gpg-sign", "-m", `feat: Implement ${task.title}`],
|
|
143
185
|
{ cwd: workspace.path }
|
|
144
186
|
);
|
|
145
|
-
} catch (
|
|
187
|
+
} catch (commitError) {
|
|
146
188
|
// If commit fails (e.g., no changes), log but don't fail the pipeline
|
|
147
189
|
// This can happen if Claude didn't generate any new files
|
|
190
|
+
eventBus.emit({
|
|
191
|
+
type: "log:entry",
|
|
192
|
+
entry: {
|
|
193
|
+
level: "warn",
|
|
194
|
+
service: "task-execution",
|
|
195
|
+
message: "Commit after Green phase failed (no changes?)",
|
|
196
|
+
timestamp: Date.now(),
|
|
197
|
+
taskId: task.taskId as string,
|
|
198
|
+
error: String(commitError),
|
|
199
|
+
},
|
|
200
|
+
});
|
|
148
201
|
}
|
|
149
202
|
|
|
150
203
|
// ===== Phase 3: Verify - Run tests =====
|
|
@@ -221,6 +274,36 @@ export async function executeTddPipeline(
|
|
|
221
274
|
});
|
|
222
275
|
}
|
|
223
276
|
|
|
277
|
+
// ===== Commit review changes (if any) =====
|
|
278
|
+
try {
|
|
279
|
+
const statusResult = await gitExec(["status", "--porcelain"], { cwd: workspace.path });
|
|
280
|
+
if (statusResult.stdout.trim() !== "") {
|
|
281
|
+
await gitExec(["add", "-A"], { cwd: workspace.path });
|
|
282
|
+
try {
|
|
283
|
+
await gitExec(["reset", "HEAD", "--", ".claude/"], { cwd: workspace.path });
|
|
284
|
+
} catch {
|
|
285
|
+
// .claude/ がない場合は無視
|
|
286
|
+
}
|
|
287
|
+
await gitExec(
|
|
288
|
+
["commit", "--no-gpg-sign", "-m", `refactor: Apply review feedback for ${task.title}`],
|
|
289
|
+
{ cwd: workspace.path }
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
} catch (commitError) {
|
|
293
|
+
// If commit fails, log but don't fail the pipeline
|
|
294
|
+
eventBus.emit({
|
|
295
|
+
type: "log:entry",
|
|
296
|
+
entry: {
|
|
297
|
+
level: "warn",
|
|
298
|
+
service: "task-execution",
|
|
299
|
+
message: "Commit after Review phase failed",
|
|
300
|
+
timestamp: Date.now(),
|
|
301
|
+
taskId: task.taskId as string,
|
|
302
|
+
error: String(commitError),
|
|
303
|
+
},
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
224
307
|
// ===== Phase 5: Merge - Merge to parent branch =====
|
|
225
308
|
eventBus.emit({
|
|
226
309
|
type: "execution:phase:started",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Task, WorkspaceInfo, EffortLevel, PhaseResult } from "@aad/shared/types";
|
|
2
2
|
import type { ClaudeProvider } from "@aad/claude-provider";
|
|
3
|
+
import type { RetryContext } from "../executor";
|
|
3
4
|
|
|
4
5
|
export interface ImplementerGreenOptions {
|
|
5
6
|
effortLevel?: EffortLevel;
|
|
@@ -10,10 +11,10 @@ export interface ImplementerGreenOptions {
|
|
|
10
11
|
/**
|
|
11
12
|
* Build TDD Green phase prompt for implementer agent
|
|
12
13
|
*/
|
|
13
|
-
export function buildGreenPhasePrompt(task: Task, workspace: WorkspaceInfo): string {
|
|
14
|
+
export function buildGreenPhasePrompt(task: Task, workspace: WorkspaceInfo, retryContext?: RetryContext): string {
|
|
14
15
|
const codingConventions = getCodingConventions(workspace.language);
|
|
15
16
|
|
|
16
|
-
|
|
17
|
+
let prompt = `implementerエージェントとして、TDD Green フェーズを実行してください。
|
|
17
18
|
|
|
18
19
|
Task ID: ${task.taskId as string}
|
|
19
20
|
Task Title: ${task.title}
|
|
@@ -28,10 +29,25 @@ Task Description: ${task.description}
|
|
|
28
29
|
|
|
29
30
|
実行内容:
|
|
30
31
|
1. 作成されたテストを確認する
|
|
31
|
-
2.
|
|
32
|
+
2. テストをパスするための最小限の実装を書く(${codingConventions})
|
|
32
33
|
3. テストを実行してパスすることを確認する
|
|
33
34
|
|
|
34
35
|
注意: 過度な最適化やリファクタリングは行わず、テストをパスするための最小限のコードを書いてください。`;
|
|
36
|
+
|
|
37
|
+
if (retryContext?.previousFailure) {
|
|
38
|
+
prompt += `\n\n⚠️ リトライ情報 (${retryContext.retryCount}回目):
|
|
39
|
+
前回のフェーズ「${retryContext.previousFailure.phase}」で失敗しました。
|
|
40
|
+
エラー: ${retryContext.previousFailure.error}`;
|
|
41
|
+
|
|
42
|
+
if (retryContext.previousFailure.testOutput) {
|
|
43
|
+
prompt += `\n\n前回のテスト出力:
|
|
44
|
+
${retryContext.previousFailure.testOutput}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
prompt += `\n\n前回の失敗パターンを特に注意して実装してください。`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return prompt;
|
|
35
51
|
}
|
|
36
52
|
|
|
37
53
|
/**
|
|
@@ -72,9 +88,10 @@ export async function runImplementerGreen(
|
|
|
72
88
|
task: Task,
|
|
73
89
|
workspace: WorkspaceInfo,
|
|
74
90
|
provider: ClaudeProvider,
|
|
75
|
-
options: ImplementerGreenOptions = {}
|
|
91
|
+
options: ImplementerGreenOptions = {},
|
|
92
|
+
retryContext?: RetryContext
|
|
76
93
|
): Promise<PhaseResult> {
|
|
77
|
-
const prompt = buildGreenPhasePrompt(task, workspace);
|
|
94
|
+
const prompt = buildGreenPhasePrompt(task, workspace, retryContext);
|
|
78
95
|
|
|
79
96
|
const response = await provider.call({
|
|
80
97
|
prompt,
|
|
@@ -78,7 +78,7 @@ export async function runMergePhase(
|
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
// Merge failed with conflicts
|
|
81
|
-
|
|
81
|
+
let conflicts = mergeResult.conflicts ?? [];
|
|
82
82
|
|
|
83
83
|
if (conflicts.length === 0) {
|
|
84
84
|
// Merge failed but no conflicts detected (unexpected error)
|
|
@@ -90,7 +90,49 @@ export async function runMergePhase(
|
|
|
90
90
|
};
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
//
|
|
93
|
+
// Auto-resolve generated files (lockfiles, snapshots)
|
|
94
|
+
const AUTO_RESOLVE_PATTERNS = [
|
|
95
|
+
"package-lock.json",
|
|
96
|
+
"yarn.lock",
|
|
97
|
+
"bun.lockb",
|
|
98
|
+
"pnpm-lock.yaml",
|
|
99
|
+
"go.sum",
|
|
100
|
+
"Cargo.lock",
|
|
101
|
+
".snap",
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
const autoResolved: string[] = [];
|
|
105
|
+
for (const conflict of conflicts) {
|
|
106
|
+
if (AUTO_RESOLVE_PATTERNS.some((pattern) => conflict.includes(pattern))) {
|
|
107
|
+
try {
|
|
108
|
+
await gitExec(["checkout", "--theirs", conflict], { cwd: parentWorktree });
|
|
109
|
+
await gitExec(["add", conflict], { cwd: parentWorktree });
|
|
110
|
+
autoResolved.push(conflict);
|
|
111
|
+
} catch (_resolveError) {
|
|
112
|
+
// If auto-resolve fails, leave it for Claude
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Remove auto-resolved conflicts from list
|
|
118
|
+
conflicts = conflicts.filter((c) => !autoResolved.includes(c));
|
|
119
|
+
|
|
120
|
+
// If all conflicts are auto-resolved, complete merge
|
|
121
|
+
if (conflicts.length === 0) {
|
|
122
|
+
try {
|
|
123
|
+
await gitExec(["commit", "--no-gpg-sign", "--no-edit"], { cwd: parentWorktree });
|
|
124
|
+
return {
|
|
125
|
+
success: true,
|
|
126
|
+
output: `Auto-resolved conflicts: ${autoResolved.join(", ")}`,
|
|
127
|
+
hadConflict: true,
|
|
128
|
+
duration: Date.now() - startTime,
|
|
129
|
+
};
|
|
130
|
+
} catch (_commitError) {
|
|
131
|
+
// Commit failed, fall through to Claude resolution
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Resolve remaining conflicts with Claude
|
|
94
136
|
const prompt = buildConflictResolutionPrompt(task, conflicts);
|
|
95
137
|
|
|
96
138
|
const response = await provider.call({
|