@ronkovic/aad 0.3.8 → 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.
Files changed (114) hide show
  1. package/README.md +292 -12
  2. package/package.json +6 -1
  3. package/src/__tests__/e2e/pipeline-e2e.test.ts +1 -0
  4. package/src/__tests__/e2e/resume-e2e.test.ts +2 -0
  5. package/src/__tests__/integration/pipeline.test.ts +1 -0
  6. package/src/modules/claude-provider/__tests__/claude-sdk-real-env.test.ts +1 -1
  7. package/src/modules/claude-provider/__tests__/claude-sdk.adapter.test.ts +2 -0
  8. package/src/modules/claude-provider/__tests__/provider-registry.test.ts +1 -0
  9. package/src/modules/cli/__tests__/cleanup.test.ts +72 -0
  10. package/src/modules/cli/__tests__/resume.test.ts +1 -0
  11. package/src/modules/cli/__tests__/run.test.ts +1 -0
  12. package/src/modules/cli/commands/__tests__/task-dispatch-handler.test.ts +145 -0
  13. package/src/modules/cli/commands/cleanup.ts +26 -11
  14. package/src/modules/cli/commands/resume.ts +3 -2
  15. package/src/modules/cli/commands/run.ts +57 -7
  16. package/src/modules/cli/commands/task-dispatch-handler.ts +73 -3
  17. package/src/modules/dashboard/__tests__/api-graph.test.ts +332 -0
  18. package/src/modules/dashboard/__tests__/api-timeline.test.ts +461 -0
  19. package/src/modules/dashboard/routes/sse.ts +3 -2
  20. package/src/modules/dashboard/server.ts +1 -0
  21. package/src/modules/dashboard/services/sse-broadcaster.ts +29 -0
  22. package/src/modules/dashboard/ui/dashboard.html +143 -18
  23. package/src/modules/git-workspace/__tests__/branch-manager.test.ts +52 -0
  24. package/src/modules/git-workspace/__tests__/dependency-installer.test.ts +77 -0
  25. package/src/modules/git-workspace/__tests__/git-exec.test.ts +26 -0
  26. package/src/modules/git-workspace/__tests__/merge-service.test.ts +19 -0
  27. package/src/modules/git-workspace/__tests__/pr-manager.test.ts +80 -0
  28. package/src/modules/git-workspace/__tests__/template-copy.test.ts +189 -0
  29. package/src/modules/git-workspace/__tests__/worktree-cleanup.test.ts +29 -2
  30. package/src/modules/git-workspace/__tests__/worktree-manager.test.ts +64 -4
  31. package/src/modules/git-workspace/branch-manager.ts +24 -3
  32. package/src/modules/git-workspace/dependency-installer.ts +113 -0
  33. package/src/modules/git-workspace/git-exec.ts +3 -2
  34. package/src/modules/git-workspace/index.ts +10 -1
  35. package/src/modules/git-workspace/merge-service.ts +40 -10
  36. package/src/modules/git-workspace/pr-manager.ts +278 -0
  37. package/src/modules/git-workspace/template-copy.ts +302 -0
  38. package/src/modules/git-workspace/worktree-manager.ts +37 -11
  39. package/src/modules/planning/__tests__/planning-service.test.ts +1 -0
  40. package/src/modules/planning/__tests__/planning.service.test.ts +149 -0
  41. package/src/modules/planning/__tests__/project-detection.test.ts +7 -1
  42. package/src/modules/planning/planning.service.ts +16 -2
  43. package/src/modules/planning/project-detection.ts +4 -1
  44. package/src/modules/process-manager/__tests__/process-manager.test.ts +1 -0
  45. package/src/modules/task-execution/__tests__/executor.test.ts +86 -0
  46. package/src/modules/task-execution/__tests__/tester-verify.test.ts +4 -3
  47. package/src/modules/task-execution/executor.ts +87 -4
  48. package/src/modules/task-execution/phases/implementer-green.ts +22 -5
  49. package/src/modules/task-execution/phases/merge.ts +44 -2
  50. package/src/modules/task-execution/phases/tester-red.ts +22 -5
  51. package/src/modules/task-execution/phases/tester-verify.ts +22 -6
  52. package/src/modules/task-queue/dispatcher.ts +50 -1
  53. package/src/shared/__tests__/prerequisites.test.ts +176 -0
  54. package/src/shared/config.ts +6 -0
  55. package/src/shared/prerequisites.ts +190 -0
  56. package/src/shared/types.ts +13 -0
  57. package/templates/CLAUDE.md +122 -0
  58. package/templates/settings.json +117 -0
  59. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/progress.json +0 -10
  60. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/queue/completed/task-getall-2.json +0 -10
  61. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/queue/pending/task-1.json +0 -13
  62. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/queue/pending/task-getall-1.json +0 -10
  63. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/queue/pending/task-status-change.json +0 -10
  64. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/workers/worker-1.json +0 -5
  65. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/workers/worker-2.json +0 -5
  66. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/progress.json +0 -10
  67. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/queue/completed/task-getall-2.json +0 -10
  68. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/queue/pending/task-1.json +0 -13
  69. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/queue/pending/task-getall-1.json +0 -10
  70. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/queue/pending/task-status-change.json +0 -10
  71. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/workers/worker-1.json +0 -5
  72. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/workers/worker-2.json +0 -5
  73. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/progress.json +0 -10
  74. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/queue/completed/task-getall-2.json +0 -10
  75. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/queue/pending/task-1.json +0 -13
  76. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/queue/pending/task-getall-1.json +0 -10
  77. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/queue/pending/task-status-change.json +0 -10
  78. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/workers/worker-1.json +0 -5
  79. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/workers/worker-2.json +0 -5
  80. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/progress.json +0 -10
  81. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/queue/completed/task-getall-2.json +0 -10
  82. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/queue/pending/task-1.json +0 -13
  83. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/queue/pending/task-getall-1.json +0 -10
  84. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/queue/pending/task-status-change.json +0 -10
  85. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/workers/worker-1.json +0 -5
  86. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/workers/worker-2.json +0 -5
  87. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/progress.json +0 -10
  88. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/queue/completed/task-getall-2.json +0 -10
  89. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/queue/pending/task-1.json +0 -13
  90. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/queue/pending/task-getall-1.json +0 -10
  91. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/queue/pending/task-status-change.json +0 -10
  92. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/workers/worker-1.json +0 -5
  93. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/workers/worker-2.json +0 -5
  94. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/progress.json +0 -10
  95. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/queue/completed/task-getall-2.json +0 -10
  96. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/queue/pending/task-1.json +0 -13
  97. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/queue/pending/task-getall-1.json +0 -10
  98. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/queue/pending/task-status-change.json +0 -10
  99. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/workers/worker-1.json +0 -5
  100. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/workers/worker-2.json +0 -5
  101. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/progress.json +0 -10
  102. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/queue/completed/task-getall-2.json +0 -10
  103. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/queue/pending/task-1.json +0 -13
  104. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/queue/pending/task-getall-1.json +0 -10
  105. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/queue/pending/task-status-change.json +0 -10
  106. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/workers/worker-1.json +0 -5
  107. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/workers/worker-2.json +0 -5
  108. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/progress.json +0 -10
  109. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/queue/completed/task-getall-2.json +0 -10
  110. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/queue/pending/task-1.json +0 -13
  111. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/queue/pending/task-getall-1.json +0 -10
  112. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/queue/pending/task-status-change.json +0 -10
  113. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/workers/worker-1.json +0 -5
  114. 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 for tasks that modify the same files (later task depends on earlier)
214
+ 3. Set depends_on based on:
215
+ - File overlap: tasks that modify the same files (later task depends on earlier)
216
+ - Logical dependency: tasks whose output is consumed by another task
217
+ (e.g., DB schema → API that queries the DB → Frontend that calls the API)
218
+ - Import dependency: tasks that create modules imported by other tasks
219
+ If no dependency exists, use an empty array []
206
220
  4. Set priority (1 = highest)
207
221
  5. CRITICAL: Write the result as a JSON file to the following path:
208
222
  ${taskPlanFilePath}
@@ -36,6 +36,7 @@ export type TestFramework =
36
36
  | "jest"
37
37
  | "bun:test"
38
38
  | "mocha"
39
+ | "playwright"
39
40
  | "go-test"
40
41
  | "cargo-test"
41
42
  | "terraform-validate"
@@ -204,7 +205,8 @@ export async function detectPackageManager(
204
205
  if (await checker.exists(path.join(projectRoot, "pnpm-lock.yaml"))) return "pnpm";
205
206
  if (await checker.exists(path.join(projectRoot, "yarn.lock"))) return "yarn";
206
207
  if (await checker.exists(path.join(projectRoot, "package-lock.json"))) return "npm";
207
- if (await checker.exists(path.join(projectRoot, "bun.lockb"))) return "bun";
208
+ if (await checker.exists(path.join(projectRoot, "bun.lock"))) return "bun"; // text format (v1.2+)
209
+ if (await checker.exists(path.join(projectRoot, "bun.lockb"))) return "bun"; // binary format (legacy)
208
210
 
209
211
  // Rust/Go
210
212
  if (await checker.exists(path.join(projectRoot, "Cargo.lock"))) return "cargo";
@@ -290,6 +292,7 @@ export async function detectTestFramework(
290
292
  if (content.includes('"bun-types"') || content.includes('"@types/bun"')) {
291
293
  return "bun:test";
292
294
  }
295
+ if (content.includes('"@playwright/test"')) return "playwright";
293
296
  if (content.includes('"vitest"')) return "vitest";
294
297
  if (content.includes('"jest"')) return "jest";
295
298
  if (content.includes('"mocha"')) return "mocha";
@@ -24,6 +24,7 @@ describe("ProcessManager", () => {
24
24
  teams: { splitter: false, reviewer: false },
25
25
  memorySync: false,
26
26
  dashboard: { enabled: false, port: 7333, host: "localhost" },
27
+ git: { autoPush: false },
27
28
  },
28
29
  logger,
29
30
  });
@@ -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("throws error for unknown test framework", () => {
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: "unknown",
207
+ packageManager: "npm",
208
208
  framework: "unknown",
209
209
  testFramework: "unknown",
210
210
  };
211
211
 
212
- expect(() => buildTestCommand(workspace)).toThrow("Unsupported test framework");
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 (_error) {
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
- 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({