@ronkovic/aad 0.3.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 (195) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +312 -0
  3. package/bin/aad.js +2 -0
  4. package/package.json +78 -0
  5. package/src/__tests__/e2e/pipeline-e2e.test.ts +279 -0
  6. package/src/__tests__/e2e/resume-e2e.test.ts +200 -0
  7. package/src/__tests__/integration/cli-smoke.test.ts +175 -0
  8. package/src/__tests__/integration/pipeline.test.ts +346 -0
  9. package/src/bun-imports.d.ts +14 -0
  10. package/src/main.ts +52 -0
  11. package/src/modules/claude-provider/__tests__/claude-cli.adapter.test.ts +277 -0
  12. package/src/modules/claude-provider/__tests__/claude-sdk-real-env.test.ts +127 -0
  13. package/src/modules/claude-provider/__tests__/claude-sdk.adapter.test.ts +347 -0
  14. package/src/modules/claude-provider/__tests__/effort-strategy.test.ts +212 -0
  15. package/src/modules/claude-provider/__tests__/provider-registry.test.ts +251 -0
  16. package/src/modules/claude-provider/__tests__/retry.test.ts +201 -0
  17. package/src/modules/claude-provider/claude-cli.adapter.ts +156 -0
  18. package/src/modules/claude-provider/claude-provider.port.ts +35 -0
  19. package/src/modules/claude-provider/claude-sdk.adapter.ts +217 -0
  20. package/src/modules/claude-provider/effort-strategy.ts +94 -0
  21. package/src/modules/claude-provider/index.ts +32 -0
  22. package/src/modules/claude-provider/provider-registry.ts +92 -0
  23. package/src/modules/claude-provider/retry.ts +81 -0
  24. package/src/modules/cli/__tests__/app.test.ts +160 -0
  25. package/src/modules/cli/__tests__/cleanup.test.ts +111 -0
  26. package/src/modules/cli/__tests__/commands.test.ts +186 -0
  27. package/src/modules/cli/__tests__/output.test.ts +329 -0
  28. package/src/modules/cli/__tests__/resume.test.ts +324 -0
  29. package/src/modules/cli/__tests__/run.test.ts +168 -0
  30. package/src/modules/cli/__tests__/shutdown.test.ts +168 -0
  31. package/src/modules/cli/__tests__/status.test.ts +144 -0
  32. package/src/modules/cli/app.ts +241 -0
  33. package/src/modules/cli/commands/cleanup.ts +120 -0
  34. package/src/modules/cli/commands/resume.ts +156 -0
  35. package/src/modules/cli/commands/run.ts +322 -0
  36. package/src/modules/cli/commands/status.ts +101 -0
  37. package/src/modules/cli/index.ts +29 -0
  38. package/src/modules/cli/output.ts +256 -0
  39. package/src/modules/cli/shutdown.ts +122 -0
  40. package/src/modules/dashboard/__tests__/api-routes.test.ts +204 -0
  41. package/src/modules/dashboard/__tests__/file-watcher.test.ts +34 -0
  42. package/src/modules/dashboard/__tests__/server.test.ts +120 -0
  43. package/src/modules/dashboard/__tests__/sse-broadcaster.test.ts +163 -0
  44. package/src/modules/dashboard/__tests__/sse-routes.test.ts +58 -0
  45. package/src/modules/dashboard/__tests__/state-aggregator.test.ts +330 -0
  46. package/src/modules/dashboard/index.ts +8 -0
  47. package/src/modules/dashboard/routes/api.ts +84 -0
  48. package/src/modules/dashboard/routes/sse.ts +37 -0
  49. package/src/modules/dashboard/server.ts +111 -0
  50. package/src/modules/dashboard/services/file-watcher.ts +36 -0
  51. package/src/modules/dashboard/services/sse-broadcaster.ts +81 -0
  52. package/src/modules/dashboard/services/state-aggregator.ts +132 -0
  53. package/src/modules/dashboard/ui/dashboard.html +405 -0
  54. package/src/modules/git-workspace/__tests__/branch-manager.test.ts +335 -0
  55. package/src/modules/git-workspace/__tests__/git-exec.test.ts +91 -0
  56. package/src/modules/git-workspace/__tests__/memory-sync.test.ts +273 -0
  57. package/src/modules/git-workspace/__tests__/merge-service.test.ts +286 -0
  58. package/src/modules/git-workspace/__tests__/settings-merge.test.ts +163 -0
  59. package/src/modules/git-workspace/__tests__/worktree-manager.test.ts +247 -0
  60. package/src/modules/git-workspace/branch-manager.ts +191 -0
  61. package/src/modules/git-workspace/git-exec.ts +124 -0
  62. package/src/modules/git-workspace/index.ts +17 -0
  63. package/src/modules/git-workspace/memory-sync.ts +89 -0
  64. package/src/modules/git-workspace/merge-service.ts +156 -0
  65. package/src/modules/git-workspace/settings-merge.ts +95 -0
  66. package/src/modules/git-workspace/worktree-manager.ts +199 -0
  67. package/src/modules/logging/__tests__/log-store.test.ts +242 -0
  68. package/src/modules/logging/__tests__/logger.test.ts +81 -0
  69. package/src/modules/logging/__tests__/sse-transport.test.ts +93 -0
  70. package/src/modules/logging/index.ts +7 -0
  71. package/src/modules/logging/log-store.ts +80 -0
  72. package/src/modules/logging/logger.ts +55 -0
  73. package/src/modules/logging/transports/sse-transport.ts +28 -0
  74. package/src/modules/multi-repo/__tests__/multi-repo-planner.test.ts +93 -0
  75. package/src/modules/multi-repo/__tests__/repo-context.test.ts +79 -0
  76. package/src/modules/multi-repo/index.ts +12 -0
  77. package/src/modules/multi-repo/multi-repo-planner.ts +112 -0
  78. package/src/modules/multi-repo/repo-context.ts +71 -0
  79. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/progress.json +10 -0
  80. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/queue/completed/task-getall-2.json +10 -0
  81. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/queue/pending/task-1.json +13 -0
  82. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/queue/pending/task-getall-1.json +10 -0
  83. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/queue/pending/task-status-change.json +10 -0
  84. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/workers/worker-1.json +5 -0
  85. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/workers/worker-2.json +5 -0
  86. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/progress.json +10 -0
  87. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/queue/completed/task-getall-2.json +10 -0
  88. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/queue/pending/task-1.json +13 -0
  89. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/queue/pending/task-getall-1.json +10 -0
  90. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/queue/pending/task-status-change.json +10 -0
  91. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/workers/worker-1.json +5 -0
  92. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/workers/worker-2.json +5 -0
  93. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/progress.json +10 -0
  94. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/queue/completed/task-getall-2.json +10 -0
  95. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/queue/pending/task-1.json +13 -0
  96. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/queue/pending/task-getall-1.json +10 -0
  97. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/queue/pending/task-status-change.json +10 -0
  98. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/workers/worker-1.json +5 -0
  99. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/workers/worker-2.json +5 -0
  100. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/progress.json +10 -0
  101. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/queue/completed/task-getall-2.json +10 -0
  102. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/queue/pending/task-1.json +13 -0
  103. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/queue/pending/task-getall-1.json +10 -0
  104. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/queue/pending/task-status-change.json +10 -0
  105. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/workers/worker-1.json +5 -0
  106. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/workers/worker-2.json +5 -0
  107. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/progress.json +10 -0
  108. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/queue/completed/task-getall-2.json +10 -0
  109. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/queue/pending/task-1.json +13 -0
  110. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/queue/pending/task-getall-1.json +10 -0
  111. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/queue/pending/task-status-change.json +10 -0
  112. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/workers/worker-1.json +5 -0
  113. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/workers/worker-2.json +5 -0
  114. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/progress.json +10 -0
  115. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/queue/completed/task-getall-2.json +10 -0
  116. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/queue/pending/task-1.json +13 -0
  117. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/queue/pending/task-getall-1.json +10 -0
  118. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/queue/pending/task-status-change.json +10 -0
  119. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/workers/worker-1.json +5 -0
  120. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/workers/worker-2.json +5 -0
  121. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/progress.json +10 -0
  122. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/queue/completed/task-getall-2.json +10 -0
  123. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/queue/pending/task-1.json +13 -0
  124. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/queue/pending/task-getall-1.json +10 -0
  125. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/queue/pending/task-status-change.json +10 -0
  126. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/workers/worker-1.json +5 -0
  127. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/workers/worker-2.json +5 -0
  128. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/progress.json +10 -0
  129. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/queue/completed/task-getall-2.json +10 -0
  130. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/queue/pending/task-1.json +13 -0
  131. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/queue/pending/task-getall-1.json +10 -0
  132. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/queue/pending/task-status-change.json +10 -0
  133. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/workers/worker-1.json +5 -0
  134. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/workers/worker-2.json +5 -0
  135. package/src/modules/persistence/__tests__/file-lock.test.ts +141 -0
  136. package/src/modules/persistence/__tests__/index.test.ts +38 -0
  137. package/src/modules/persistence/__tests__/stores.test.ts +594 -0
  138. package/src/modules/persistence/file-lock.ts +158 -0
  139. package/src/modules/persistence/fs-run-store.ts +73 -0
  140. package/src/modules/persistence/fs-task-store.ts +152 -0
  141. package/src/modules/persistence/fs-worker-store.ts +116 -0
  142. package/src/modules/persistence/in-memory-stores.ts +98 -0
  143. package/src/modules/persistence/index.ts +60 -0
  144. package/src/modules/persistence/stores.port.ts +60 -0
  145. package/src/modules/planning/__tests__/file-conflict-validator.test.ts +256 -0
  146. package/src/modules/planning/__tests__/planning-service.test.ts +366 -0
  147. package/src/modules/planning/__tests__/project-detection.test.ts +707 -0
  148. package/src/modules/planning/file-conflict-validator.ts +135 -0
  149. package/src/modules/planning/index.ts +40 -0
  150. package/src/modules/planning/planning.service.ts +262 -0
  151. package/src/modules/planning/project-detection.ts +525 -0
  152. package/src/modules/plugin/__tests__/plugin-loader.test.ts +83 -0
  153. package/src/modules/plugin/__tests__/plugin-manager.test.ts +187 -0
  154. package/src/modules/plugin/index.ts +3 -0
  155. package/src/modules/plugin/plugin-loader.ts +46 -0
  156. package/src/modules/plugin/plugin-manager.ts +90 -0
  157. package/src/modules/plugin/plugin.types.ts +37 -0
  158. package/src/modules/process-manager/__tests__/process-manager.test.ts +210 -0
  159. package/src/modules/process-manager/__tests__/worker.test.ts +89 -0
  160. package/src/modules/process-manager/index.ts +5 -0
  161. package/src/modules/process-manager/process-manager.ts +193 -0
  162. package/src/modules/process-manager/worker.ts +106 -0
  163. package/src/modules/task-execution/__tests__/default-spawner.test.ts +154 -0
  164. package/src/modules/task-execution/__tests__/executor.test.ts +760 -0
  165. package/src/modules/task-execution/__tests__/implementer-green.test.ts +286 -0
  166. package/src/modules/task-execution/__tests__/merge-phase.test.ts +368 -0
  167. package/src/modules/task-execution/__tests__/reviewer.test.ts +302 -0
  168. package/src/modules/task-execution/__tests__/tester-red.test.ts +281 -0
  169. package/src/modules/task-execution/__tests__/tester-verify.test.ts +313 -0
  170. package/src/modules/task-execution/executor.ts +303 -0
  171. package/src/modules/task-execution/index.ts +45 -0
  172. package/src/modules/task-execution/phases/default-spawner.ts +49 -0
  173. package/src/modules/task-execution/phases/implementer-green.ts +100 -0
  174. package/src/modules/task-execution/phases/merge.ts +122 -0
  175. package/src/modules/task-execution/phases/reviewer.ts +160 -0
  176. package/src/modules/task-execution/phases/tester-red.ts +100 -0
  177. package/src/modules/task-execution/phases/tester-verify.ts +120 -0
  178. package/src/modules/task-queue/__tests__/dependency-resolver.test.ts +456 -0
  179. package/src/modules/task-queue/__tests__/dispatcher.test.ts +824 -0
  180. package/src/modules/task-queue/__tests__/task-plan.test.ts +122 -0
  181. package/src/modules/task-queue/__tests__/task.test.ts +130 -0
  182. package/src/modules/task-queue/dependency-resolver.ts +171 -0
  183. package/src/modules/task-queue/dispatcher.ts +372 -0
  184. package/src/modules/task-queue/index.ts +16 -0
  185. package/src/modules/task-queue/task-plan.ts +40 -0
  186. package/src/modules/task-queue/task.ts +67 -0
  187. package/src/shared/__tests__/config.test.ts +204 -0
  188. package/src/shared/__tests__/errors.test.ts +285 -0
  189. package/src/shared/__tests__/events.test.ts +496 -0
  190. package/src/shared/__tests__/types.test.ts +360 -0
  191. package/src/shared/config.ts +133 -0
  192. package/src/shared/errors.ts +128 -0
  193. package/src/shared/events.ts +171 -0
  194. package/src/shared/types.ts +143 -0
  195. package/tsconfig.json +30 -0
@@ -0,0 +1,60 @@
1
+ import { z } from "zod";
2
+ import type { Task, TaskId, TaskStatus, Worker, WorkerId, RunState, RunId } from "@aad/shared/types";
3
+
4
+ // Zod Schemas for validation (string-based, not branded)
5
+ export const TaskSchema = z.object({
6
+ taskId: z.string(),
7
+ title: z.string(),
8
+ description: z.string(),
9
+ filesToModify: z.array(z.string()),
10
+ dependsOn: z.array(z.string()),
11
+ priority: z.number(),
12
+ status: z.enum(["pending", "running", "completed", "failed"]),
13
+ workerId: z.string().optional(),
14
+ startTime: z.string().optional(),
15
+ endTime: z.string().optional(),
16
+ retryCount: z.number(),
17
+ failureReason: z.string().optional(),
18
+ });
19
+
20
+ export const WorkerSchema = z.object({
21
+ workerId: z.string(),
22
+ status: z.enum(["idle", "busy", "stopped"]),
23
+ currentTask: z.string().nullable(),
24
+ pid: z.number().optional(),
25
+ });
26
+
27
+ export const RunStateSchema = z.object({
28
+ runId: z.string(),
29
+ parentBranch: z.string(),
30
+ totalTasks: z.number(),
31
+ pending: z.number(),
32
+ running: z.number(),
33
+ completed: z.number(),
34
+ failed: z.number(),
35
+ startTime: z.string(),
36
+ endTime: z.string().optional(),
37
+ });
38
+
39
+ // Port Interfaces
40
+ export interface TaskStore {
41
+ get(taskId: TaskId): Promise<Task | null>;
42
+ getAll(): Promise<Task[]>;
43
+ getByStatus(status: TaskStatus): Promise<Task[]>;
44
+ save(task: Task): Promise<void>;
45
+ delete(taskId: TaskId): Promise<void>;
46
+ }
47
+
48
+ export interface WorkerStore {
49
+ get(workerId: WorkerId): Promise<Worker | null>;
50
+ getAll(): Promise<Worker[]>;
51
+ getIdle(): Promise<Worker[]>;
52
+ save(worker: Worker): Promise<void>;
53
+ delete(workerId: WorkerId): Promise<void>;
54
+ }
55
+
56
+ export interface RunStore {
57
+ get(runId: RunId): Promise<RunState | null>;
58
+ getLatest(): Promise<RunState | null>;
59
+ save(state: RunState): Promise<void>;
60
+ }
@@ -0,0 +1,256 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import type { Task } from "../../../shared/types";
3
+ import { createTaskId } from "../../../shared/types";
4
+ import {
5
+ validateFileConflicts,
6
+ formatConflictErrors,
7
+ } from "../file-conflict-validator";
8
+
9
+ function createTestTask(
10
+ taskId: string,
11
+ filesToModify: string[],
12
+ dependsOn: string[] = [],
13
+ priority: number = 1
14
+ ): Task {
15
+ return {
16
+ taskId: createTaskId(taskId),
17
+ title: `Task ${taskId}`,
18
+ description: `Test task ${taskId}`,
19
+ filesToModify,
20
+ dependsOn: dependsOn.map(createTaskId),
21
+ priority,
22
+ status: "pending",
23
+ retryCount: 0,
24
+ };
25
+ }
26
+
27
+ describe("validateFileConflicts", () => {
28
+ test("passes when no file conflicts exist", () => {
29
+ const tasks = [
30
+ createTestTask("task-1", ["file1.ts"], [], 1),
31
+ createTestTask("task-2", ["file2.ts"], [], 2),
32
+ createTestTask("task-3", ["file3.ts"], [], 3),
33
+ ];
34
+
35
+ const result = validateFileConflicts(tasks);
36
+
37
+ expect(result.valid).toBe(true);
38
+ expect(result.conflicts).toHaveLength(0);
39
+ });
40
+
41
+ test("passes when file conflict has direct dependency", () => {
42
+ const tasks = [
43
+ createTestTask("task-1", ["shared.ts"], [], 1),
44
+ createTestTask("task-2", ["shared.ts"], ["task-1"], 2),
45
+ ];
46
+
47
+ const result = validateFileConflicts(tasks);
48
+
49
+ expect(result.valid).toBe(true);
50
+ expect(result.conflicts).toHaveLength(0);
51
+ });
52
+
53
+ test("passes when file conflict has transitive dependency", () => {
54
+ const tasks = [
55
+ createTestTask("task-1", ["shared.ts"], [], 1),
56
+ createTestTask("task-2", ["other.ts"], ["task-1"], 2),
57
+ createTestTask("task-3", ["shared.ts"], ["task-2"], 3),
58
+ ];
59
+
60
+ const result = validateFileConflicts(tasks);
61
+
62
+ expect(result.valid).toBe(true);
63
+ expect(result.conflicts).toHaveLength(0);
64
+ });
65
+
66
+ test("fails when file conflict has no dependency", () => {
67
+ const tasks = [
68
+ createTestTask("task-1", ["shared.ts"], [], 1),
69
+ createTestTask("task-2", ["shared.ts"], [], 2),
70
+ ];
71
+
72
+ const result = validateFileConflicts(tasks);
73
+
74
+ expect(result.valid).toBe(false);
75
+ expect(result.conflicts).toHaveLength(1);
76
+ expect(result.conflicts[0]).toMatchObject({
77
+ filePath: "shared.ts",
78
+ ownerTask: createTaskId("task-1"),
79
+ conflictingTask: createTaskId("task-2"),
80
+ hasDependency: false,
81
+ });
82
+ });
83
+
84
+ test("detects multiple file conflicts", () => {
85
+ const tasks = [
86
+ createTestTask("task-1", ["file1.ts", "file2.ts"], [], 1),
87
+ createTestTask("task-2", ["file1.ts", "file3.ts"], [], 2),
88
+ createTestTask("task-3", ["file2.ts", "file4.ts"], [], 3),
89
+ ];
90
+
91
+ const result = validateFileConflicts(tasks);
92
+
93
+ expect(result.valid).toBe(false);
94
+ expect(result.conflicts).toHaveLength(2);
95
+
96
+ const filePathsInConflicts = result.conflicts.map((c) => c.filePath);
97
+ expect(filePathsInConflicts).toContain("file1.ts");
98
+ expect(filePathsInConflicts).toContain("file2.ts");
99
+ });
100
+
101
+ test("ignores empty filesToModify arrays", () => {
102
+ const tasks = [
103
+ createTestTask("task-1", [], [], 1),
104
+ createTestTask("task-2", ["file1.ts"], [], 2),
105
+ ];
106
+
107
+ const result = validateFileConflicts(tasks);
108
+
109
+ expect(result.valid).toBe(true);
110
+ expect(result.conflicts).toHaveLength(0);
111
+ });
112
+
113
+ test("handles circular dependency detection", () => {
114
+ const tasks = [
115
+ createTestTask("task-1", ["shared.ts"], ["task-2"], 1),
116
+ createTestTask("task-2", ["shared.ts"], ["task-1"], 2),
117
+ ];
118
+
119
+ const result = validateFileConflicts(tasks);
120
+
121
+ // Circular dependency: both tasks think they depend on each other
122
+ // Our visited set prevents infinite loop, so this passes validation
123
+ // (The circular dependency itself is a separate validation concern)
124
+ expect(result.valid).toBe(true);
125
+ });
126
+
127
+ test("processes tasks in priority order", () => {
128
+ const tasks = [
129
+ createTestTask("task-high", ["shared.ts"], [], 1), // Highest priority
130
+ createTestTask("task-low", ["shared.ts"], [], 10), // Lower priority
131
+ ];
132
+
133
+ const result = validateFileConflicts(tasks);
134
+
135
+ expect(result.valid).toBe(false);
136
+ expect(result.conflicts[0]?.ownerTask).toBe(createTaskId("task-high"));
137
+ expect(result.conflicts[0]?.conflictingTask).toBe(createTaskId("task-low"));
138
+ });
139
+
140
+ test("validates complex dependency chain", () => {
141
+ const tasks = [
142
+ createTestTask("task-1", ["base.ts"], [], 1),
143
+ createTestTask("task-2", ["utils.ts"], ["task-1"], 2),
144
+ createTestTask("task-3", ["service.ts"], ["task-2"], 3),
145
+ createTestTask("task-4", ["base.ts"], ["task-3"], 4), // Depends on task-3, which transitively depends on task-1
146
+ ];
147
+
148
+ const result = validateFileConflicts(tasks);
149
+
150
+ expect(result.valid).toBe(true);
151
+ expect(result.conflicts).toHaveLength(0);
152
+ });
153
+
154
+ test("detects missing dependency in complex scenario", () => {
155
+ const tasks = [
156
+ createTestTask("task-1", ["shared.ts"], [], 1),
157
+ createTestTask("task-2", ["other.ts"], ["task-1"], 2),
158
+ createTestTask("task-3", ["shared.ts"], [], 3), // Missing dependency on task-1
159
+ ];
160
+
161
+ const result = validateFileConflicts(tasks);
162
+
163
+ expect(result.valid).toBe(false);
164
+ expect(result.conflicts).toHaveLength(1);
165
+ expect(result.conflicts[0]).toMatchObject({
166
+ filePath: "shared.ts",
167
+ ownerTask: createTaskId("task-1"),
168
+ conflictingTask: createTaskId("task-3"),
169
+ hasDependency: false,
170
+ });
171
+ });
172
+
173
+ test("handles same file modified by multiple tasks with proper dependencies", () => {
174
+ const tasks = [
175
+ createTestTask("task-1", ["config.ts"], [], 1),
176
+ createTestTask("task-2", ["config.ts"], ["task-1"], 2),
177
+ createTestTask("task-3", ["config.ts"], ["task-2"], 3),
178
+ createTestTask("task-4", ["config.ts"], ["task-3"], 4),
179
+ ];
180
+
181
+ const result = validateFileConflicts(tasks);
182
+
183
+ expect(result.valid).toBe(true);
184
+ expect(result.conflicts).toHaveLength(0);
185
+ });
186
+
187
+ test("skips empty file paths", () => {
188
+ const tasks = [
189
+ createTestTask("task-1", ["valid.ts", "", " "], [], 1),
190
+ createTestTask("task-2", ["valid.ts"], [], 2),
191
+ ];
192
+
193
+ const result = validateFileConflicts(tasks);
194
+
195
+ // Only valid.ts should cause a conflict, not the empty strings
196
+ expect(result.valid).toBe(false);
197
+ expect(result.conflicts).toHaveLength(1);
198
+ expect(result.conflicts[0]?.filePath).toBe("valid.ts");
199
+ });
200
+
201
+ test("handles task not found in dependency lookup", () => {
202
+ // task-2 depends on "task-nonexistent" which isn't in the tasks list
203
+ const tasks = [
204
+ createTestTask("task-1", ["shared.ts"], [], 1),
205
+ createTestTask("task-2", ["shared.ts"], ["task-nonexistent"], 2),
206
+ ];
207
+
208
+ const result = validateFileConflicts(tasks);
209
+
210
+ // task-2 can't prove dependency on task-1 via task-nonexistent, so conflict
211
+ expect(result.valid).toBe(false);
212
+ expect(result.conflicts).toHaveLength(1);
213
+ });
214
+ });
215
+
216
+ describe("formatConflictErrors", () => {
217
+ test("returns empty array for valid result", () => {
218
+ const tasks = [
219
+ createTestTask("task-1", ["file1.ts"], [], 1),
220
+ createTestTask("task-2", ["file2.ts"], [], 2),
221
+ ];
222
+ const result = validateFileConflicts(tasks);
223
+ const errors = formatConflictErrors(result);
224
+ expect(errors).toHaveLength(0);
225
+ });
226
+
227
+ test("formats conflict errors as human-readable messages", () => {
228
+ const tasks = [
229
+ createTestTask("task-1", ["shared.ts"], [], 1),
230
+ createTestTask("task-2", ["shared.ts"], [], 2),
231
+ ];
232
+
233
+ const result = validateFileConflicts(tasks);
234
+ const errors = formatConflictErrors(result);
235
+
236
+ expect(errors).toHaveLength(1);
237
+ expect(errors[0]).toContain("File conflict detected");
238
+ expect(errors[0]).toContain("shared.ts");
239
+ expect(errors[0]).toContain("task-1");
240
+ expect(errors[0]).toContain("task-2");
241
+ expect(errors[0]).toContain("ERROR");
242
+ });
243
+
244
+ test("formats multiple conflicts", () => {
245
+ const tasks = [
246
+ createTestTask("task-1", ["file1.ts", "file2.ts"], [], 1),
247
+ createTestTask("task-2", ["file1.ts"], [], 2),
248
+ createTestTask("task-3", ["file2.ts"], [], 3),
249
+ ];
250
+
251
+ const result = validateFileConflicts(tasks);
252
+ const errors = formatConflictErrors(result);
253
+
254
+ expect(errors).toHaveLength(2);
255
+ });
256
+ });
@@ -0,0 +1,366 @@
1
+ import { describe, test, expect, beforeEach } from "bun:test";
2
+ import type { Config } from "../../../shared/config";
3
+ import type { ClaudeProvider, ClaudeRequest, ClaudeResponse } from "../../claude-provider";
4
+ import type pino from "pino";
5
+ import { EventBus, type AADEvent } from "../../../shared/events";
6
+ import { PlanningService } from "../planning.service";
7
+ import { createRunId } from "../../../shared/types";
8
+ import type { FileChecker } from "../project-detection";
9
+
10
+ // Mock ClaudeProvider
11
+ class MockClaudeProvider implements ClaudeProvider {
12
+ public lastRequest: ClaudeRequest | null = null;
13
+ public mockResponse: ClaudeResponse = {
14
+ result: "Mock response",
15
+ exitCode: 0,
16
+ model: "claude-sonnet-4-5",
17
+ effortLevel: "medium",
18
+ duration: 1000,
19
+ };
20
+
21
+ async call(request: ClaudeRequest): Promise<ClaudeResponse> {
22
+ this.lastRequest = request;
23
+ return this.mockResponse;
24
+ }
25
+ }
26
+
27
+ // Mock Logger
28
+ function createMockLogger(): pino.Logger {
29
+ return {
30
+ info: () => {},
31
+ warn: () => {},
32
+ error: () => {},
33
+ debug: () => {},
34
+ child: () => createMockLogger(),
35
+ } as unknown as pino.Logger;
36
+ }
37
+
38
+ // Mock Config
39
+ function createMockConfig(teamsEnabled: boolean = false): Config {
40
+ return {
41
+ workers: { num: 2, max: 8 },
42
+ models: {},
43
+ timeouts: { claude: 1200, test: 600, staleTask: 5400 },
44
+ retry: { maxRetries: 2 },
45
+ debug: false,
46
+ adaptiveEffort: false,
47
+ teams: {
48
+ splitter: teamsEnabled,
49
+ reviewer: false,
50
+ },
51
+ memorySync: true,
52
+ dashboard: {
53
+ enabled: true,
54
+ port: 7333,
55
+ host: "localhost",
56
+ },
57
+ };
58
+ }
59
+
60
+ // Mock FileChecker to avoid real filesystem I/O in tests
61
+ const mockFileChecker: FileChecker = {
62
+ exists: async () => false,
63
+ readText: async () => "",
64
+ glob: async () => [],
65
+ };
66
+
67
+ describe("PlanningService", () => {
68
+ let eventBus: EventBus;
69
+ let mockProvider: MockClaudeProvider;
70
+ let mockLogger: pino.Logger;
71
+
72
+ beforeEach(() => {
73
+ eventBus = new EventBus();
74
+ mockProvider = new MockClaudeProvider();
75
+ mockLogger = createMockLogger();
76
+ });
77
+
78
+ test("creates planning service instance", () => {
79
+ const config = createMockConfig();
80
+ const service = new PlanningService(mockProvider, eventBus, config, mockLogger, { fileChecker: mockFileChecker });
81
+ expect(service).toBeDefined();
82
+ });
83
+
84
+ test("planTasks emits planning:started event", async () => {
85
+ const config = createMockConfig();
86
+ const service = new PlanningService(mockProvider, eventBus, config, mockLogger, { fileChecker: mockFileChecker });
87
+
88
+ let eventEmitted = false;
89
+ type PlanningStartedEvent = Extract<AADEvent, { type: "planning:started" }>;
90
+ eventBus.on<PlanningStartedEvent>("planning:started", (event) => {
91
+ eventEmitted = true;
92
+ expect(event.requirementsPath).toBe("/test/requirements.md");
93
+ });
94
+
95
+ // Mock successful task plan generation
96
+ mockProvider.mockResponse = {
97
+ result: JSON.stringify({
98
+ run_id: "test-run",
99
+ parent_branch: "feature/test-run",
100
+ tasks: [
101
+ {
102
+ task_id: "task-1",
103
+ title: "Test task",
104
+ description: "Test description",
105
+ files_to_modify: ["file1.ts"],
106
+ depends_on: [],
107
+ priority: 1,
108
+ },
109
+ ],
110
+ }),
111
+ exitCode: 0,
112
+ model: "claude-sonnet-4-5",
113
+ effortLevel: "medium" as const,
114
+ duration: 1000,
115
+ };
116
+
117
+ try {
118
+ await service.planTasks({
119
+ runId: createRunId("test-run"),
120
+ parentBranch: "feature/test-run",
121
+ requirementsPath: "/test/requirements.md",
122
+ targetDocsDir: "/test/docs",
123
+ });
124
+ } catch {
125
+ // Ignore errors for this test
126
+ }
127
+
128
+ expect(eventEmitted).toBe(true);
129
+ });
130
+
131
+ test("planTasks calls ClaudeProvider with solo mode prompt", async () => {
132
+ const config = createMockConfig(false); // teams disabled
133
+ const service = new PlanningService(mockProvider, eventBus, config, mockLogger, { fileChecker: mockFileChecker });
134
+
135
+ mockProvider.mockResponse = {
136
+ result: JSON.stringify({
137
+ run_id: "test-run",
138
+ parent_branch: "feature/test-run",
139
+ tasks: [
140
+ {
141
+ task_id: "task-1",
142
+ title: "Test task",
143
+ description: "Test",
144
+ files_to_modify: ["file1.ts"],
145
+ depends_on: [],
146
+ priority: 1,
147
+ },
148
+ ],
149
+ }),
150
+ exitCode: 0,
151
+ model: "claude-sonnet-4-5",
152
+ effortLevel: "medium" as const,
153
+ duration: 1000,
154
+ };
155
+
156
+ try {
157
+ await service.planTasks({
158
+ runId: createRunId("test-run"),
159
+ parentBranch: "feature/test-run",
160
+ requirementsPath: "/test/requirements.md",
161
+ targetDocsDir: "/test/docs",
162
+ });
163
+ } catch {
164
+ // Ignore errors
165
+ }
166
+
167
+ expect(mockProvider.lastRequest).toBeDefined();
168
+ expect(mockProvider.lastRequest?.prompt).toContain("splitterエージェントとして");
169
+ expect(mockProvider.lastRequest?.prompt).not.toContain("サブエージェント");
170
+ }, 15_000);
171
+
172
+ test("planTasks calls ClaudeProvider with teams mode prompt", async () => {
173
+ const config = createMockConfig(true); // teams enabled
174
+ const service = new PlanningService(mockProvider, eventBus, config, mockLogger, { fileChecker: mockFileChecker });
175
+
176
+ mockProvider.mockResponse = {
177
+ result: JSON.stringify({
178
+ run_id: "test-run",
179
+ parent_branch: "feature/test-run",
180
+ tasks: [],
181
+ }),
182
+ exitCode: 0,
183
+ model: "claude-sonnet-4-5",
184
+ effortLevel: "medium" as const,
185
+ duration: 1000,
186
+ };
187
+
188
+ try {
189
+ await service.planTasks({
190
+ runId: createRunId("test-run"),
191
+ parentBranch: "feature/test-run",
192
+ requirementsPath: "/test/requirements.md",
193
+ targetDocsDir: "/test/docs",
194
+ });
195
+ } catch {
196
+ // Ignore errors
197
+ }
198
+
199
+ expect(mockProvider.lastRequest).toBeDefined();
200
+ expect(mockProvider.lastRequest?.prompt).toContain("サブエージェント");
201
+ // Teams mode now uses subagents instead of prompt-only instructions
202
+ expect(mockProvider.lastRequest?.subagents).toBeDefined();
203
+ expect(mockProvider.lastRequest?.subagents).toHaveLength(3);
204
+ expect(mockProvider.lastRequest?.subagents?.map(s => s.name)).toEqual([
205
+ "codebase-analyzer",
206
+ "requirement-analyzer",
207
+ "dependency-mapper",
208
+ ]);
209
+ // Each subagent should have a non-empty prompt
210
+ for (const sa of mockProvider.lastRequest?.subagents ?? []) {
211
+ expect(sa.prompt.length).toBeGreaterThan(0);
212
+ }
213
+ });
214
+
215
+ test("planTasks does not include subagents in solo mode", async () => {
216
+ const config = createMockConfig(false);
217
+ const service = new PlanningService(mockProvider, eventBus, config, mockLogger, { fileChecker: mockFileChecker });
218
+
219
+ mockProvider.mockResponse = {
220
+ result: JSON.stringify({
221
+ run_id: "test-run",
222
+ parent_branch: "feature/test-run",
223
+ tasks: [
224
+ {
225
+ task_id: "task-1",
226
+ title: "Test task",
227
+ description: "Test",
228
+ files_to_modify: ["file1.ts"],
229
+ depends_on: [],
230
+ priority: 1,
231
+ },
232
+ ],
233
+ }),
234
+ exitCode: 0,
235
+ model: "claude-sonnet-4-5",
236
+ effortLevel: "medium" as const,
237
+ duration: 1000,
238
+ };
239
+
240
+ await service.planTasks({
241
+ runId: createRunId("test-run"),
242
+ parentBranch: "feature/test-run",
243
+ requirementsPath: "/test/requirements.md",
244
+ targetDocsDir: "/test/docs",
245
+ });
246
+
247
+ expect(mockProvider.lastRequest?.subagents).toBeUndefined();
248
+ });
249
+
250
+ test("planTasks emits planning:completed on success", async () => {
251
+ const config = createMockConfig();
252
+ const service = new PlanningService(mockProvider, eventBus, config, mockLogger, { fileChecker: mockFileChecker });
253
+
254
+ let completedEvent: Extract<AADEvent, { type: "planning:completed" }> | null = null;
255
+ type PlanningCompletedEvent = Extract<AADEvent, { type: "planning:completed" }>;
256
+ eventBus.on<PlanningCompletedEvent>("planning:completed", (event) => {
257
+ completedEvent = event;
258
+ });
259
+
260
+ mockProvider.mockResponse = {
261
+ result: JSON.stringify({
262
+ run_id: "test-run",
263
+ parent_branch: "feature/test-run",
264
+ tasks: [
265
+ {
266
+ task_id: "task-1",
267
+ title: "Test",
268
+ description: "Test",
269
+ files_to_modify: ["file1.ts"],
270
+ depends_on: [],
271
+ priority: 1,
272
+ },
273
+ {
274
+ task_id: "task-2",
275
+ title: "Test 2",
276
+ description: "Test 2",
277
+ files_to_modify: ["file2.ts"],
278
+ depends_on: [],
279
+ priority: 2,
280
+ },
281
+ ],
282
+ }),
283
+ exitCode: 0,
284
+ model: "claude-sonnet-4-5",
285
+ effortLevel: "medium" as const,
286
+ duration: 1000,
287
+ };
288
+
289
+ const result = await service.planTasks({
290
+ runId: createRunId("test-run"),
291
+ parentBranch: "feature/test-run",
292
+ requirementsPath: "/test/requirements.md",
293
+ targetDocsDir: "/test/docs",
294
+ });
295
+
296
+ expect(result).toBeDefined();
297
+ expect(result.tasks).toHaveLength(2);
298
+ expect(completedEvent).toBeDefined();
299
+ expect(completedEvent!.taskCount).toBe(2);
300
+ });
301
+
302
+ test("planTasks emits planning:failed on Claude error", async () => {
303
+ const config = createMockConfig();
304
+ const service = new PlanningService(mockProvider, eventBus, config, mockLogger, { fileChecker: mockFileChecker });
305
+
306
+ let failedEvent: Extract<AADEvent, { type: "planning:failed" }> | null = null;
307
+ type PlanningFailedEvent = Extract<AADEvent, { type: "planning:failed" }>;
308
+ eventBus.on<PlanningFailedEvent>("planning:failed", (event) => {
309
+ failedEvent = event;
310
+ });
311
+
312
+ mockProvider.mockResponse = {
313
+ result: "Claude API error",
314
+ exitCode: 1,
315
+ model: "claude-sonnet-4-5",
316
+ effortLevel: "medium" as const,
317
+ duration: 1000,
318
+ };
319
+
320
+ await expect(
321
+ service.planTasks({
322
+ runId: createRunId("test-run"),
323
+ parentBranch: "feature/test-run",
324
+ requirementsPath: "/test/requirements.md",
325
+ targetDocsDir: "/test/docs",
326
+ })
327
+ ).rejects.toThrow();
328
+
329
+ expect(failedEvent).toBeDefined();
330
+ expect(failedEvent!.error).toContain("exit code 1");
331
+ });
332
+
333
+ test("planTasks emits planning:failed on invalid task plan", async () => {
334
+ const config = createMockConfig();
335
+ const service = new PlanningService(mockProvider, eventBus, config, mockLogger, { fileChecker: mockFileChecker });
336
+
337
+ let failedEvent: Extract<AADEvent, { type: "planning:failed" }> | null = null;
338
+ type PlanningFailedEvent = Extract<AADEvent, { type: "planning:failed" }>;
339
+ eventBus.on<PlanningFailedEvent>("planning:failed", (event) => {
340
+ failedEvent = event;
341
+ });
342
+
343
+ mockProvider.mockResponse = {
344
+ result: JSON.stringify({
345
+ run_id: "test-run",
346
+ parent_branch: "feature/test-run",
347
+ tasks: "invalid", // Should be array
348
+ }),
349
+ exitCode: 0,
350
+ model: "claude-sonnet-4-5",
351
+ effortLevel: "medium" as const,
352
+ duration: 1000,
353
+ };
354
+
355
+ await expect(
356
+ service.planTasks({
357
+ runId: createRunId("test-run"),
358
+ parentBranch: "feature/test-run",
359
+ requirementsPath: "/test/requirements.md",
360
+ targetDocsDir: "/test/docs",
361
+ })
362
+ ).rejects.toThrow();
363
+
364
+ expect(failedEvent).toBeDefined();
365
+ });
366
+ });