@ronkovic/aad 0.3.9 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/README.md +332 -14
  2. package/package.json +6 -1
  3. package/src/__tests__/e2e/cleanup-e2e.test.ts +186 -0
  4. package/src/__tests__/e2e/dashboard-api-e2e.test.ts +87 -0
  5. package/src/__tests__/e2e/pipeline-e2e.test.ts +10 -68
  6. package/src/__tests__/e2e/resume-e2e.test.ts +9 -11
  7. package/src/__tests__/e2e/retry-e2e.test.ts +285 -0
  8. package/src/__tests__/e2e/status-e2e.test.ts +227 -0
  9. package/src/__tests__/e2e/tdd-pipeline-e2e.test.ts +360 -0
  10. package/src/__tests__/helpers/index.ts +6 -0
  11. package/src/__tests__/helpers/mock-claude-provider.ts +53 -0
  12. package/src/__tests__/helpers/mock-logger.ts +36 -0
  13. package/src/__tests__/helpers/wait-helpers.ts +34 -0
  14. package/src/__tests__/integration/pipeline.test.ts +3 -0
  15. package/src/modules/claude-provider/__tests__/claude-sdk-real-env.test.ts +1 -1
  16. package/src/modules/claude-provider/__tests__/claude-sdk.adapter.test.ts +6 -0
  17. package/src/modules/claude-provider/__tests__/provider-registry.test.ts +3 -0
  18. package/src/modules/cli/__tests__/cleanup.test.ts +73 -0
  19. package/src/modules/cli/__tests__/resume.test.ts +4 -0
  20. package/src/modules/cli/__tests__/run.test.ts +37 -0
  21. package/src/modules/cli/__tests__/status.test.ts +1 -0
  22. package/src/modules/cli/app.ts +2 -0
  23. package/src/modules/cli/commands/__tests__/task-dispatch-handler.test.ts +145 -0
  24. package/src/modules/cli/commands/cleanup.ts +26 -11
  25. package/src/modules/cli/commands/resume.ts +14 -8
  26. package/src/modules/cli/commands/run.ts +70 -8
  27. package/src/modules/cli/commands/task-dispatch-handler.ts +73 -3
  28. package/src/modules/dashboard/__tests__/api-graph.test.ts +332 -0
  29. package/src/modules/dashboard/__tests__/api-timeline.test.ts +461 -0
  30. package/src/modules/dashboard/routes/sse.ts +3 -2
  31. package/src/modules/dashboard/server.ts +1 -0
  32. package/src/modules/dashboard/services/sse-broadcaster.ts +29 -0
  33. package/src/modules/dashboard/ui/dashboard.html +640 -349
  34. package/src/modules/git-workspace/__tests__/branch-manager.test.ts +52 -0
  35. package/src/modules/git-workspace/__tests__/dependency-installer.test.ts +77 -0
  36. package/src/modules/git-workspace/__tests__/git-exec.test.ts +26 -0
  37. package/src/modules/git-workspace/__tests__/merge-service.test.ts +19 -0
  38. package/src/modules/git-workspace/__tests__/pr-manager.test.ts +80 -0
  39. package/src/modules/git-workspace/__tests__/template-copy.test.ts +189 -0
  40. package/src/modules/git-workspace/__tests__/worktree-cleanup.test.ts +29 -2
  41. package/src/modules/git-workspace/__tests__/worktree-manager.test.ts +64 -4
  42. package/src/modules/git-workspace/branch-manager.ts +24 -3
  43. package/src/modules/git-workspace/dependency-installer.ts +113 -0
  44. package/src/modules/git-workspace/git-exec.ts +3 -2
  45. package/src/modules/git-workspace/index.ts +10 -1
  46. package/src/modules/git-workspace/merge-service.ts +36 -2
  47. package/src/modules/git-workspace/pr-manager.ts +278 -0
  48. package/src/modules/git-workspace/template-copy.ts +302 -0
  49. package/src/modules/git-workspace/worktree-manager.ts +37 -11
  50. package/src/modules/planning/__tests__/planning-service.test.ts +3 -0
  51. package/src/modules/planning/__tests__/planning.service.test.ts +149 -0
  52. package/src/modules/planning/__tests__/project-detection.test.ts +7 -1
  53. package/src/modules/planning/planning.service.ts +16 -2
  54. package/src/modules/planning/project-detection.ts +4 -1
  55. package/src/modules/process-manager/__tests__/process-manager.test.ts +3 -0
  56. package/src/modules/process-manager/process-manager.ts +2 -1
  57. package/src/modules/task-execution/__tests__/executor.test.ts +496 -0
  58. package/src/modules/task-execution/__tests__/tester-verify.test.ts +4 -3
  59. package/src/modules/task-execution/executor.ts +163 -4
  60. package/src/modules/task-execution/phases/implementer-green.ts +22 -5
  61. package/src/modules/task-execution/phases/merge.ts +44 -2
  62. package/src/modules/task-execution/phases/tester-red.ts +22 -5
  63. package/src/modules/task-execution/phases/tester-verify.ts +22 -6
  64. package/src/modules/task-queue/dispatcher.ts +96 -3
  65. package/src/shared/__tests__/config.test.ts +30 -0
  66. package/src/shared/__tests__/events.test.ts +42 -16
  67. package/src/shared/__tests__/prerequisites.test.ts +176 -0
  68. package/src/shared/__tests__/shutdown-handler.test.ts +96 -0
  69. package/src/shared/config.ts +10 -0
  70. package/src/shared/events.ts +5 -0
  71. package/src/shared/memory-check.ts +2 -2
  72. package/src/shared/prerequisites.ts +190 -0
  73. package/src/shared/shutdown-handler.ts +12 -5
  74. package/src/shared/types.ts +25 -0
  75. package/templates/CLAUDE.md +122 -0
  76. package/templates/settings.json +117 -0
  77. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/progress.json +0 -10
  78. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/queue/completed/task-getall-2.json +0 -10
  79. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/queue/pending/task-1.json +0 -13
  80. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/queue/pending/task-getall-1.json +0 -10
  81. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/queue/pending/task-status-change.json +0 -10
  82. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/workers/worker-1.json +0 -5
  83. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/workers/worker-2.json +0 -5
  84. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/progress.json +0 -10
  85. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/queue/completed/task-getall-2.json +0 -10
  86. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/queue/pending/task-1.json +0 -13
  87. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/queue/pending/task-getall-1.json +0 -10
  88. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/queue/pending/task-status-change.json +0 -10
  89. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/workers/worker-1.json +0 -5
  90. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/workers/worker-2.json +0 -5
  91. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/progress.json +0 -10
  92. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/queue/completed/task-getall-2.json +0 -10
  93. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/queue/pending/task-1.json +0 -13
  94. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/queue/pending/task-getall-1.json +0 -10
  95. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/queue/pending/task-status-change.json +0 -10
  96. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/workers/worker-1.json +0 -5
  97. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/workers/worker-2.json +0 -5
  98. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/progress.json +0 -10
  99. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/queue/completed/task-getall-2.json +0 -10
  100. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/queue/pending/task-1.json +0 -13
  101. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/queue/pending/task-getall-1.json +0 -10
  102. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/queue/pending/task-status-change.json +0 -10
  103. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/workers/worker-1.json +0 -5
  104. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/workers/worker-2.json +0 -5
  105. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/progress.json +0 -10
  106. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/queue/completed/task-getall-2.json +0 -10
  107. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/queue/pending/task-1.json +0 -13
  108. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/queue/pending/task-getall-1.json +0 -10
  109. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/queue/pending/task-status-change.json +0 -10
  110. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/workers/worker-1.json +0 -5
  111. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/workers/worker-2.json +0 -5
  112. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/progress.json +0 -10
  113. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/queue/completed/task-getall-2.json +0 -10
  114. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/queue/pending/task-1.json +0 -13
  115. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/queue/pending/task-getall-1.json +0 -10
  116. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/queue/pending/task-status-change.json +0 -10
  117. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/workers/worker-1.json +0 -5
  118. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/workers/worker-2.json +0 -5
  119. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/progress.json +0 -10
  120. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/queue/completed/task-getall-2.json +0 -10
  121. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/queue/pending/task-1.json +0 -13
  122. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/queue/pending/task-getall-1.json +0 -10
  123. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/queue/pending/task-status-change.json +0 -10
  124. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/workers/worker-1.json +0 -5
  125. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/workers/worker-2.json +0 -5
  126. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/progress.json +0 -10
  127. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/queue/completed/task-getall-2.json +0 -10
  128. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/queue/pending/task-1.json +0 -13
  129. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/queue/pending/task-getall-1.json +0 -10
  130. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/queue/pending/task-status-change.json +0 -10
  131. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/workers/worker-1.json +0 -5
  132. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/workers/worker-2.json +0 -5
@@ -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 (_error) {
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
- return `implementerエージェントとして、TDD Green フェーズを実行してください。
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. テストをパスするための最小限の実装を書く(${codingConventions}
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
- const conflicts = mergeResult.conflicts ?? [];
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
- // Resolve conflicts with Claude
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
- return `testerエージェントとして、TDD Red フェーズを実行してください。
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 "unknown":
67
- throw new TestRunnerError(
68
- "Unsupported test framework: unknown",
69
- { testFramework }
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
- { runId: this.runId, completed: progress.completed, failed: progress.failed },
340
- "Run completed"
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
  });