@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
|
@@ -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,9 +20,30 @@ 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
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Run pre-check to see if tests already pass
|
|
33
|
+
*/
|
|
34
|
+
async function runPreCheck(
|
|
35
|
+
workspace: WorkspaceInfo,
|
|
36
|
+
testSpawner: ProcessSpawner | undefined,
|
|
37
|
+
timeout: number
|
|
38
|
+
): Promise<boolean> {
|
|
39
|
+
const result = await runTests(workspace, testSpawner, timeout);
|
|
40
|
+
return result.success;
|
|
41
|
+
}
|
|
42
|
+
|
|
22
43
|
/**
|
|
23
44
|
* Execute full TDD pipeline for a task
|
|
24
45
|
* Phases: Red → Green → Verify → Review → Merge
|
|
46
|
+
* Optionally skips pipeline if tests already pass (pre-check)
|
|
25
47
|
*/
|
|
26
48
|
export async function executeTddPipeline(
|
|
27
49
|
task: Task,
|
|
@@ -34,11 +56,74 @@ export async function executeTddPipeline(
|
|
|
34
56
|
provider: ClaudeProvider,
|
|
35
57
|
mergeService: MergeService,
|
|
36
58
|
eventBus: EventBus,
|
|
37
|
-
testSpawner?: ProcessSpawner
|
|
59
|
+
testSpawner?: ProcessSpawner,
|
|
60
|
+
retryContext?: RetryContext
|
|
38
61
|
): Promise<TaskExecutionResult> {
|
|
39
62
|
const startTime = Date.now();
|
|
40
63
|
|
|
41
64
|
try {
|
|
65
|
+
// ===== Pre-check: Skip if tests already pass (unless strictTdd) =====
|
|
66
|
+
if (config.skipCompleted && !config.strictTdd) {
|
|
67
|
+
eventBus.emit({
|
|
68
|
+
type: "log:entry",
|
|
69
|
+
entry: {
|
|
70
|
+
level: "info",
|
|
71
|
+
service: "task-execution",
|
|
72
|
+
message: "Running pre-check to see if tests already pass",
|
|
73
|
+
timestamp: Date.now(),
|
|
74
|
+
taskId: task.taskId as string,
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const testsAlreadyPass = await runPreCheck(
|
|
79
|
+
workspace,
|
|
80
|
+
testSpawner,
|
|
81
|
+
config.timeouts.test * 1000
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
if (testsAlreadyPass) {
|
|
85
|
+
const duration = Date.now() - startTime;
|
|
86
|
+
|
|
87
|
+
eventBus.emit({
|
|
88
|
+
type: "execution:skipped",
|
|
89
|
+
taskId: task.taskId,
|
|
90
|
+
reason: "Tests already pass",
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
eventBus.emit({
|
|
94
|
+
type: "log:entry",
|
|
95
|
+
entry: {
|
|
96
|
+
level: "info",
|
|
97
|
+
service: "task-execution",
|
|
98
|
+
message: "Tests already pass, skipping TDD pipeline",
|
|
99
|
+
timestamp: Date.now(),
|
|
100
|
+
taskId: task.taskId as string,
|
|
101
|
+
duration,
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
taskId: task.taskId,
|
|
107
|
+
status: "completed",
|
|
108
|
+
duration,
|
|
109
|
+
output: "Skipped: tests already pass",
|
|
110
|
+
skipped: true,
|
|
111
|
+
phasesExecuted: ["pre-check"],
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
eventBus.emit({
|
|
116
|
+
type: "log:entry",
|
|
117
|
+
entry: {
|
|
118
|
+
level: "info",
|
|
119
|
+
service: "task-execution",
|
|
120
|
+
message: "Pre-check failed, running full TDD pipeline",
|
|
121
|
+
timestamp: Date.now(),
|
|
122
|
+
taskId: task.taskId as string,
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
42
127
|
// Estimate task complexity for adaptive effort
|
|
43
128
|
const complexity = estimateTaskComplexity(task);
|
|
44
129
|
const testerEffort = getAdaptiveEffortLevel("tester", complexity, config);
|
|
@@ -74,7 +159,7 @@ export async function executeTddPipeline(
|
|
|
74
159
|
effortLevel: testerEffort,
|
|
75
160
|
model: config.models.tester,
|
|
76
161
|
timeout: config.timeouts.claude * 1000,
|
|
77
|
-
});
|
|
162
|
+
}, retryContext);
|
|
78
163
|
|
|
79
164
|
if (!redResult.success) {
|
|
80
165
|
eventBus.emit({
|
|
@@ -98,6 +183,33 @@ export async function executeTddPipeline(
|
|
|
98
183
|
duration: Date.now() - redStart,
|
|
99
184
|
});
|
|
100
185
|
|
|
186
|
+
// ===== Commit failing tests =====
|
|
187
|
+
try {
|
|
188
|
+
await gitExec(["add", "-A"], { cwd: workspace.path });
|
|
189
|
+
try {
|
|
190
|
+
await gitExec(["reset", "HEAD", "--", ".claude/"], { cwd: workspace.path });
|
|
191
|
+
} catch {
|
|
192
|
+
// .claude/ がない場合は無視
|
|
193
|
+
}
|
|
194
|
+
await gitExec(
|
|
195
|
+
["commit", "--no-gpg-sign", "-m", `test: Add failing tests for ${task.title}`],
|
|
196
|
+
{ cwd: workspace.path }
|
|
197
|
+
);
|
|
198
|
+
} catch (commitError) {
|
|
199
|
+
// If commit fails (e.g., no changes), log but don't fail the pipeline
|
|
200
|
+
eventBus.emit({
|
|
201
|
+
type: "log:entry",
|
|
202
|
+
entry: {
|
|
203
|
+
level: "warn",
|
|
204
|
+
service: "task-execution",
|
|
205
|
+
message: "Commit after Red phase failed (no changes?)",
|
|
206
|
+
timestamp: Date.now(),
|
|
207
|
+
taskId: task.taskId as string,
|
|
208
|
+
error: String(commitError),
|
|
209
|
+
},
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
101
213
|
// ===== Phase 2: Green - Implement minimal code =====
|
|
102
214
|
eventBus.emit({
|
|
103
215
|
type: "execution:phase:started",
|
|
@@ -110,7 +222,7 @@ export async function executeTddPipeline(
|
|
|
110
222
|
effortLevel: implementerEffort,
|
|
111
223
|
model: config.models.implementer,
|
|
112
224
|
timeout: config.timeouts.claude * 1000,
|
|
113
|
-
});
|
|
225
|
+
}, retryContext);
|
|
114
226
|
|
|
115
227
|
if (!greenResult.success) {
|
|
116
228
|
eventBus.emit({
|
|
@@ -138,13 +250,29 @@ export async function executeTddPipeline(
|
|
|
138
250
|
// Commit changes after Green phase so they can be merged later
|
|
139
251
|
try {
|
|
140
252
|
await gitExec(["add", "-A"], { cwd: workspace.path });
|
|
253
|
+
try {
|
|
254
|
+
await gitExec(["reset", "HEAD", "--", ".claude/"], { cwd: workspace.path });
|
|
255
|
+
} catch {
|
|
256
|
+
// .claude/ がない場合は無視
|
|
257
|
+
}
|
|
141
258
|
await gitExec(
|
|
142
259
|
["commit", "--no-gpg-sign", "-m", `feat: Implement ${task.title}`],
|
|
143
260
|
{ cwd: workspace.path }
|
|
144
261
|
);
|
|
145
|
-
} catch (
|
|
262
|
+
} catch (commitError) {
|
|
146
263
|
// If commit fails (e.g., no changes), log but don't fail the pipeline
|
|
147
264
|
// This can happen if Claude didn't generate any new files
|
|
265
|
+
eventBus.emit({
|
|
266
|
+
type: "log:entry",
|
|
267
|
+
entry: {
|
|
268
|
+
level: "warn",
|
|
269
|
+
service: "task-execution",
|
|
270
|
+
message: "Commit after Green phase failed (no changes?)",
|
|
271
|
+
timestamp: Date.now(),
|
|
272
|
+
taskId: task.taskId as string,
|
|
273
|
+
error: String(commitError),
|
|
274
|
+
},
|
|
275
|
+
});
|
|
148
276
|
}
|
|
149
277
|
|
|
150
278
|
// ===== Phase 3: Verify - Run tests =====
|
|
@@ -221,6 +349,36 @@ export async function executeTddPipeline(
|
|
|
221
349
|
});
|
|
222
350
|
}
|
|
223
351
|
|
|
352
|
+
// ===== Commit review changes (if any) =====
|
|
353
|
+
try {
|
|
354
|
+
const statusResult = await gitExec(["status", "--porcelain"], { cwd: workspace.path });
|
|
355
|
+
if (statusResult.stdout.trim() !== "") {
|
|
356
|
+
await gitExec(["add", "-A"], { cwd: workspace.path });
|
|
357
|
+
try {
|
|
358
|
+
await gitExec(["reset", "HEAD", "--", ".claude/"], { cwd: workspace.path });
|
|
359
|
+
} catch {
|
|
360
|
+
// .claude/ がない場合は無視
|
|
361
|
+
}
|
|
362
|
+
await gitExec(
|
|
363
|
+
["commit", "--no-gpg-sign", "-m", `refactor: Apply review feedback for ${task.title}`],
|
|
364
|
+
{ cwd: workspace.path }
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
} catch (commitError) {
|
|
368
|
+
// If commit fails, log but don't fail the pipeline
|
|
369
|
+
eventBus.emit({
|
|
370
|
+
type: "log:entry",
|
|
371
|
+
entry: {
|
|
372
|
+
level: "warn",
|
|
373
|
+
service: "task-execution",
|
|
374
|
+
message: "Commit after Review phase failed",
|
|
375
|
+
timestamp: Date.now(),
|
|
376
|
+
taskId: task.taskId as string,
|
|
377
|
+
error: String(commitError),
|
|
378
|
+
},
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
|
|
224
382
|
// ===== Phase 5: Merge - Merge to parent branch =====
|
|
225
383
|
eventBus.emit({
|
|
226
384
|
type: "execution:phase:started",
|
|
@@ -282,6 +440,7 @@ export async function executeTddPipeline(
|
|
|
282
440
|
status: "completed",
|
|
283
441
|
duration,
|
|
284
442
|
output: "TDD pipeline completed successfully",
|
|
443
|
+
phasesExecuted: ["red", "green", "verify", "review", "merge"],
|
|
285
444
|
};
|
|
286
445
|
} catch (error) {
|
|
287
446
|
const duration = Date.now() - startTime;
|
|
@@ -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({
|
|
@@ -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 TesterRedOptions {
|
|
5
6
|
effortLevel?: EffortLevel;
|
|
@@ -10,10 +11,10 @@ export interface TesterRedOptions {
|
|
|
10
11
|
/**
|
|
11
12
|
* Build TDD Red phase prompt for tester agent
|
|
12
13
|
*/
|
|
13
|
-
export function buildRedPhasePrompt(task: Task, workspace: WorkspaceInfo): string {
|
|
14
|
+
export function buildRedPhasePrompt(task: Task, workspace: WorkspaceInfo, retryContext?: RetryContext): string {
|
|
14
15
|
const languagePatterns = getLanguageTestPatterns(workspace.language);
|
|
15
16
|
|
|
16
|
-
|
|
17
|
+
let prompt = `testerエージェントとして、TDD Red フェーズを実行してください。
|
|
17
18
|
|
|
18
19
|
Task ID: ${task.taskId as string}
|
|
19
20
|
Task Title: ${task.title}
|
|
@@ -28,11 +29,26 @@ Task Description: ${task.description}
|
|
|
28
29
|
|
|
29
30
|
実行内容:
|
|
30
31
|
1. タスクの要件を理解する
|
|
31
|
-
2.
|
|
32
|
+
2. 失敗するテストを作成する(言語に応じた適切なパターンを使用)
|
|
32
33
|
${languagePatterns}
|
|
33
34
|
3. テストを実行して失敗することを確認する
|
|
34
35
|
|
|
35
36
|
注意: このフェーズでは実装コードは書かないでください。テストのみ作成してください。`;
|
|
37
|
+
|
|
38
|
+
if (retryContext?.previousFailure) {
|
|
39
|
+
prompt += `\n\n⚠️ リトライ情報 (${retryContext.retryCount}回目):
|
|
40
|
+
前回のフェーズ「${retryContext.previousFailure.phase}」で失敗しました。
|
|
41
|
+
エラー: ${retryContext.previousFailure.error}`;
|
|
42
|
+
|
|
43
|
+
if (retryContext.previousFailure.testOutput) {
|
|
44
|
+
prompt += `\n\n前回のテスト出力:
|
|
45
|
+
${retryContext.previousFailure.testOutput}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
prompt += `\n\n前回の失敗を踏まえて、より堅牢なテストを作成してください。`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return prompt;
|
|
36
52
|
}
|
|
37
53
|
|
|
38
54
|
/**
|
|
@@ -72,9 +88,10 @@ export async function runTesterRed(
|
|
|
72
88
|
task: Task,
|
|
73
89
|
workspace: WorkspaceInfo,
|
|
74
90
|
provider: ClaudeProvider,
|
|
75
|
-
options: TesterRedOptions = {}
|
|
91
|
+
options: TesterRedOptions = {},
|
|
92
|
+
retryContext?: RetryContext
|
|
76
93
|
): Promise<PhaseResult> {
|
|
77
|
-
const prompt = buildRedPhasePrompt(task, workspace);
|
|
94
|
+
const prompt = buildRedPhasePrompt(task, workspace, retryContext);
|
|
78
95
|
|
|
79
96
|
const response = await provider.call({
|
|
80
97
|
prompt,
|
|
@@ -48,8 +48,12 @@ export function buildTestCommand(workspace: WorkspaceInfo): string[] {
|
|
|
48
48
|
if (packageManager === "yarn") return ["yarn", "test"];
|
|
49
49
|
return ["npx", "mocha"];
|
|
50
50
|
|
|
51
|
-
case "pytest":
|
|
51
|
+
case "pytest": {
|
|
52
|
+
const { packageManager } = workspace;
|
|
53
|
+
if (packageManager === "uv") return ["uv", "run", "pytest", "-v"];
|
|
54
|
+
if (packageManager === "poetry") return ["poetry", "run", "pytest", "-v"];
|
|
52
55
|
return ["pytest", "-v"];
|
|
56
|
+
}
|
|
53
57
|
|
|
54
58
|
case "go-test":
|
|
55
59
|
return ["go", "test", "./..."];
|
|
@@ -63,11 +67,23 @@ export function buildTestCommand(workspace: WorkspaceInfo): string[] {
|
|
|
63
67
|
case "gradle":
|
|
64
68
|
return ["./gradlew", "test"];
|
|
65
69
|
|
|
66
|
-
case "
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
70
|
+
case "playwright":
|
|
71
|
+
return ["npx", "playwright", "test"];
|
|
72
|
+
|
|
73
|
+
case "terraform":
|
|
74
|
+
return ["terraform", "validate"];
|
|
75
|
+
|
|
76
|
+
case "unknown": {
|
|
77
|
+
// Fallback: use package manager-based test command
|
|
78
|
+
const { packageManager } = workspace;
|
|
79
|
+
if (packageManager === "bun") return ["bun", "test"];
|
|
80
|
+
if (packageManager === "npm") return ["npm", "test"];
|
|
81
|
+
if (packageManager === "yarn") return ["yarn", "test"];
|
|
82
|
+
if (packageManager === "pnpm") return ["pnpm", "test"];
|
|
83
|
+
if (packageManager === "uv") return ["uv", "run", "pytest", "-v"];
|
|
84
|
+
if (packageManager === "poetry") return ["poetry", "run", "pytest", "-v"];
|
|
85
|
+
return ["npm", "test"];
|
|
86
|
+
}
|
|
71
87
|
|
|
72
88
|
default: {
|
|
73
89
|
const exhaustive: never = testFramework;
|
|
@@ -53,6 +53,7 @@ export class Dispatcher {
|
|
|
53
53
|
private taskMap: Map<string, Task> = new Map();
|
|
54
54
|
private initialized = false;
|
|
55
55
|
private runId?: import("@aad/shared/types").RunId;
|
|
56
|
+
private skippedCount = 0;
|
|
56
57
|
|
|
57
58
|
constructor(deps: DispatcherDeps) {
|
|
58
59
|
this.deps = deps;
|
|
@@ -112,6 +113,10 @@ export class Dispatcher {
|
|
|
112
113
|
// Listen to task:completed events
|
|
113
114
|
this.deps.eventBus.on("task:completed", (event) => {
|
|
114
115
|
if (event.type === "task:completed") {
|
|
116
|
+
// Track skipped tasks
|
|
117
|
+
if (event.result.skipped) {
|
|
118
|
+
this.skippedCount++;
|
|
119
|
+
}
|
|
115
120
|
void this.handleTaskCompleted(event.taskId);
|
|
116
121
|
}
|
|
117
122
|
});
|
|
@@ -248,6 +253,13 @@ export class Dispatcher {
|
|
|
248
253
|
task.retryCount += 1;
|
|
249
254
|
task.workerId = undefined;
|
|
250
255
|
task.failureReason = error;
|
|
256
|
+
// Store structured failure context for retry
|
|
257
|
+
task.previousFailure = {
|
|
258
|
+
phase: this.extractPhaseFromError(error),
|
|
259
|
+
error: error,
|
|
260
|
+
testOutput: this.extractTestOutput(error),
|
|
261
|
+
retryCount: task.retryCount,
|
|
262
|
+
};
|
|
251
263
|
await this.deps.taskStore.save(task);
|
|
252
264
|
this.taskMap.set(taskId as string, task);
|
|
253
265
|
|
|
@@ -330,14 +342,23 @@ export class Dispatcher {
|
|
|
330
342
|
// Emit run:completed if all tasks are done (FIX #1)
|
|
331
343
|
if (progress.pending === 0 && progress.running === 0) {
|
|
332
344
|
if (this.runId) {
|
|
345
|
+
const metrics = this.getRunMetrics();
|
|
346
|
+
|
|
333
347
|
this.deps.eventBus.emit({
|
|
334
348
|
type: "run:completed",
|
|
335
349
|
runId: this.runId,
|
|
336
350
|
});
|
|
337
351
|
|
|
338
352
|
this.deps.logger.info(
|
|
339
|
-
{
|
|
340
|
-
|
|
353
|
+
{
|
|
354
|
+
runId: this.runId,
|
|
355
|
+
completed: progress.completed,
|
|
356
|
+
failed: progress.failed,
|
|
357
|
+
skipped: metrics.skippedTasks,
|
|
358
|
+
totalDuration: metrics.totalDuration,
|
|
359
|
+
averageTaskDuration: Math.round(metrics.averageTaskDuration),
|
|
360
|
+
},
|
|
361
|
+
"Run completed with metrics"
|
|
341
362
|
);
|
|
342
363
|
}
|
|
343
364
|
}
|
|
@@ -369,10 +390,82 @@ export class Dispatcher {
|
|
|
369
390
|
|
|
370
391
|
this.deps.logger.warn(
|
|
371
392
|
{ taskId: task.taskId, elapsed },
|
|
372
|
-
"Stale task detected"
|
|
393
|
+
"Stale task detected, triggering retry"
|
|
394
|
+
);
|
|
395
|
+
|
|
396
|
+
// Trigger retry flow via handleTaskFailed
|
|
397
|
+
void this.handleTaskFailed(
|
|
398
|
+
task.taskId,
|
|
399
|
+
`Task stale (elapsed: ${elapsed}ms)`
|
|
373
400
|
);
|
|
374
401
|
}
|
|
375
402
|
}
|
|
376
403
|
}
|
|
377
404
|
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Extract phase name from error message
|
|
408
|
+
*/
|
|
409
|
+
private extractPhaseFromError(error: string): string {
|
|
410
|
+
const lowerError = error.toLowerCase();
|
|
411
|
+
if (lowerError.includes("red phase")) return "red";
|
|
412
|
+
if (lowerError.includes("green phase")) return "green";
|
|
413
|
+
if (lowerError.includes("verify phase")) return "verify";
|
|
414
|
+
if (lowerError.includes("review phase")) return "review";
|
|
415
|
+
if (lowerError.includes("merge phase")) return "merge";
|
|
416
|
+
return "unknown";
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Extract test output from error context (truncated to 2000 chars)
|
|
421
|
+
*/
|
|
422
|
+
private extractTestOutput(error: string): string | undefined {
|
|
423
|
+
try {
|
|
424
|
+
// Try to parse JSON context from PhaseError
|
|
425
|
+
const jsonMatch = error.match(/\{.*\}/s);
|
|
426
|
+
if (!jsonMatch) return undefined;
|
|
427
|
+
|
|
428
|
+
const context = JSON.parse(jsonMatch[0]);
|
|
429
|
+
const output = context.output ?? context.error;
|
|
430
|
+
if (!output) return undefined;
|
|
431
|
+
|
|
432
|
+
// Truncate to 2000 chars
|
|
433
|
+
return output.length > 2000
|
|
434
|
+
? output.substring(0, 2000) + "\n... (truncated)"
|
|
435
|
+
: output;
|
|
436
|
+
} catch {
|
|
437
|
+
// If parsing fails, return undefined
|
|
438
|
+
return undefined;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Calculate run metrics including skipped tasks
|
|
444
|
+
*/
|
|
445
|
+
getRunMetrics(): import("@aad/shared/types").RunMetrics {
|
|
446
|
+
const tasks = Array.from(this.taskMap.values());
|
|
447
|
+
const completedTasks = tasks.filter((t) => t.status === "completed");
|
|
448
|
+
const failedTasks = tasks.filter((t) => t.status === "failed");
|
|
449
|
+
|
|
450
|
+
const totalDuration = completedTasks.reduce((sum, task) => {
|
|
451
|
+
if (task.startTime && task.endTime) {
|
|
452
|
+
const start = new Date(task.startTime).getTime();
|
|
453
|
+
const end = new Date(task.endTime).getTime();
|
|
454
|
+
return sum + (end - start);
|
|
455
|
+
}
|
|
456
|
+
return sum;
|
|
457
|
+
}, 0);
|
|
458
|
+
|
|
459
|
+
const averageTaskDuration =
|
|
460
|
+
completedTasks.length > 0 ? totalDuration / completedTasks.length : 0;
|
|
461
|
+
|
|
462
|
+
return {
|
|
463
|
+
totalTasks: tasks.length,
|
|
464
|
+
completedTasks: completedTasks.length,
|
|
465
|
+
failedTasks: failedTasks.length,
|
|
466
|
+
skippedTasks: this.skippedCount,
|
|
467
|
+
totalDuration,
|
|
468
|
+
averageTaskDuration,
|
|
469
|
+
};
|
|
470
|
+
}
|
|
378
471
|
}
|
|
@@ -201,4 +201,34 @@ describe("loadConfig", () => {
|
|
|
201
201
|
const config = loadConfig({});
|
|
202
202
|
expect(config.plugins).toBeUndefined();
|
|
203
203
|
});
|
|
204
|
+
|
|
205
|
+
test("loads skipCompleted from env (default true)", () => {
|
|
206
|
+
const config = loadConfig({});
|
|
207
|
+
expect(config.skipCompleted).toBe(true);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test("loads skipCompleted from env (explicit false)", () => {
|
|
211
|
+
const config = loadConfig({ AAD_SKIP_COMPLETED: "0" });
|
|
212
|
+
expect(config.skipCompleted).toBe(false);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test("loads skipCompleted from env (explicit true)", () => {
|
|
216
|
+
const config = loadConfig({ AAD_SKIP_COMPLETED: "1" });
|
|
217
|
+
expect(config.skipCompleted).toBe(true);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test("loads strictTdd from env (default false)", () => {
|
|
221
|
+
const config = loadConfig({});
|
|
222
|
+
expect(config.strictTdd).toBe(false);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test("loads strictTdd from env (explicit true)", () => {
|
|
226
|
+
const config = loadConfig({ AAD_STRICT_TDD: "1" });
|
|
227
|
+
expect(config.strictTdd).toBe(true);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test("loads strictTdd from env (explicit false)", () => {
|
|
231
|
+
const config = loadConfig({ AAD_STRICT_TDD: "0" });
|
|
232
|
+
expect(config.strictTdd).toBe(false);
|
|
233
|
+
});
|
|
204
234
|
});
|