@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,322 @@
1
+ /**
2
+ * Run Command
3
+ * メインパイプライン: plan → dispatch → execute
4
+ */
5
+
6
+ import { Command } from "commander";
7
+ import { randomUUID } from "node:crypto";
8
+ import type { App } from "../app";
9
+ import { formatProgress, formatTaskTable, createSpinner } from "../output";
10
+ import { createRunId, createWorkerId } from "../../../shared/types";
11
+ import type { WorkspaceInfo } from "../../../shared/types";
12
+ import { getCurrentBranch } from "../../git-workspace";
13
+ import { executeTddPipeline } from "../../task-execution";
14
+ import {
15
+ detectProjectType,
16
+ detectPackageManager,
17
+ detectTestFramework,
18
+ detectFramework,
19
+ detectOrm,
20
+ detectArchitecturePattern,
21
+ createBunFileChecker,
22
+ } from "../../planning";
23
+
24
+ export function createRunCommand(getApp: () => App): Command {
25
+ const command = new Command("run")
26
+ .description("Run AAD pipeline with requirements file")
27
+ .argument("<requirements>", "Path to requirements.md file")
28
+ .option("-w, --workers <number>", "Number of workers", parseInt)
29
+ .option("--persist <mode>", "Persistence mode: memory or fs", "memory")
30
+ .option("--debug", "Enable debug logging")
31
+ .option("--no-dashboard", "Disable dashboard server")
32
+ .option("--provider <type>", "Default provider: cli or sdk", "cli")
33
+ .option("--repos <paths>", "Comma-separated repository paths for multi-repo mode")
34
+ .option("--strategy <type>", "Multi-repo strategy: independent or coordinated", "independent")
35
+ .option("--plugins <paths>", "Comma-separated plugin paths to load")
36
+ .action(async (requirementsPath: string) => {
37
+ try {
38
+ const app = getApp();
39
+ await runPipeline(app, requirementsPath);
40
+ } catch (error) {
41
+ const message = error instanceof Error ? error.message : String(error);
42
+ console.error("Run pipeline failed:", message);
43
+ process.exit(1);
44
+ }
45
+ });
46
+
47
+ return command;
48
+ }
49
+
50
+ /**
51
+ * メインパイプライン実行
52
+ */
53
+ export async function runPipeline(app: App, requirementsPath: string): Promise<void> {
54
+ const { config, logger, eventBus, planningService, dispatcher, processManager, worktreeManager, providerRegistry, stores } = app;
55
+
56
+ // Validate requirements file exists
57
+ const reqFile = Bun.file(requirementsPath);
58
+ if (!(await reqFile.exists())) {
59
+ throw new Error(`Requirements file not found: ${requirementsPath}`);
60
+ }
61
+
62
+ logger.info({ requirementsPath }, "Starting AAD pipeline");
63
+
64
+ // 1. RunId生成 + 現在ブランチ取得
65
+ const runId = createRunId(randomUUID());
66
+ const parentBranch = await getCurrentBranch(process.cwd());
67
+
68
+ logger.info({ runId, parentBranch }, "Initialized run");
69
+ console.log(`\n🚀 AAD Run: ${runId}`);
70
+ console.log(`📍 Parent Branch: ${parentBranch}\n`);
71
+
72
+ // 2. PlanningService.planTasks() → TaskPlan
73
+ const planSpinner = createSpinner("Planning tasks...");
74
+ planSpinner.start();
75
+
76
+ // Run before:planning hook
77
+ await app.pluginManager.runHook("before:planning", {
78
+ runId,
79
+ parentBranch,
80
+ requirementsPath,
81
+ });
82
+
83
+ const targetDocsDir = `${process.cwd()}/.aad/docs/${runId}`;
84
+ const taskPlan = await planningService.planTasks({
85
+ runId,
86
+ parentBranch,
87
+ requirementsPath,
88
+ targetDocsDir,
89
+ projectRoot: process.cwd(),
90
+ });
91
+
92
+ planSpinner.stop();
93
+
94
+ // Run after:planning hook
95
+ await app.pluginManager.runHook("after:planning", { runId, taskPlan });
96
+
97
+ logger.info({ runId, taskCount: taskPlan.tasks.length }, "Task plan generated");
98
+ console.log(`✅ Task plan generated: ${taskPlan.tasks.length} tasks\n`);
99
+
100
+ if (taskPlan.tasks.length === 0) {
101
+ console.log("No tasks to execute.");
102
+ return;
103
+ }
104
+
105
+ // タスク一覧表示
106
+ console.log(formatTaskTable(taskPlan.tasks));
107
+ console.log();
108
+
109
+ // 3. Dispatcher.initialize(taskPlan) + ProcessManager.initializePool()
110
+ await dispatcher.initialize(taskPlan);
111
+ await processManager.initializePool(config.workers.num);
112
+
113
+ logger.info({ runId, workerCount: app.config.workers.num }, "Dispatcher and ProcessManager initialized");
114
+
115
+ // 4. WorkerStore + RunStoreにworker/run state保存
116
+ await stores.runStore.save({
117
+ runId,
118
+ parentBranch,
119
+ totalTasks: taskPlan.tasks.length,
120
+ pending: taskPlan.tasks.length,
121
+ running: 0,
122
+ completed: 0,
123
+ failed: 0,
124
+ startTime: new Date().toISOString(),
125
+ });
126
+
127
+ // Worker state初期化
128
+ for (let i = 0; i < app.config.workers.num; i++) {
129
+ const workerId = createWorkerId(`worker-${i}`);
130
+ await stores.workerStore.save({
131
+ workerId,
132
+ status: "idle",
133
+ currentTask: null,
134
+ });
135
+ }
136
+
137
+ // 5. EventBus: task:dispatched → TDDパイプライン起動
138
+ eventBus.on<Extract<import("../../../shared/events").AADEvent, { type: "task:dispatched" }>>(
139
+ "task:dispatched",
140
+ (event) => {
141
+ const taskId = event.taskId;
142
+ const workerId = event.workerId;
143
+ logger.info({ taskId, workerId }, "Task dispatched, starting TDD pipeline");
144
+
145
+ void (async () => {
146
+ try {
147
+ const task = await stores.taskStore.get(taskId);
148
+ if (!task) {
149
+ throw new Error(`Task not found: ${taskId}`);
150
+ }
151
+
152
+ // Worktree作成
153
+ const branchName = `aad/${runId}/${taskId}`;
154
+ const worktreePath = await worktreeManager.createTaskWorktree(taskId, branchName);
155
+ logger.info({ taskId, worktreePath, branchName }, "Worktree created");
156
+
157
+ // Workspace情報 (project detectionから自動検出)
158
+ const workspace = await detectWorkspace(worktreePath, logger);
159
+
160
+ // Run before:execution hook
161
+ await app.pluginManager.runHook("before:execution", { taskId, task, worktreePath });
162
+
163
+ // TDDパイプライン実行
164
+ const provider = providerRegistry.getProvider("implementer");
165
+ const result = await executeTddPipeline(
166
+ task,
167
+ workspace,
168
+ branchName,
169
+ parentBranch,
170
+ process.cwd(),
171
+ runId,
172
+ app.config,
173
+ provider,
174
+ app.mergeService,
175
+ eventBus,
176
+ undefined // testSpawner: use default
177
+ );
178
+
179
+ // Run after:execution hook
180
+ await app.pluginManager.runHook("after:execution", { taskId, result });
181
+
182
+ // 結果をEventBusに通知
183
+ eventBus.emit({ type: "task:completed", taskId, result });
184
+ } catch (error) {
185
+ logger.error({ taskId, workerId, error }, "TDD pipeline failed");
186
+ eventBus.emit({ type: "task:failed", taskId, error: String(error) });
187
+ }
188
+ })();
189
+ }
190
+ );
191
+
192
+ // 6. 進捗表示
193
+ const progressSpinner = createSpinner("Executing tasks...");
194
+ progressSpinner.start();
195
+
196
+ let lastProgress = { total: 0, pending: 0, running: 0, completed: 0, failed: 0 };
197
+
198
+ eventBus.on<Extract<import("../../../shared/events").AADEvent, { type: "progress:updated" }>>(
199
+ "progress:updated",
200
+ (event) => {
201
+ lastProgress = event.state;
202
+ // Note: Spinner doesn't have update() method - just log progress
203
+ console.log(formatProgress(event.state));
204
+ }
205
+ );
206
+
207
+ // 7. Dispatcher.start() (イベントドリブン)
208
+ void dispatcher.start();
209
+
210
+ // 8. run:completed待機 → 結果表示
211
+ await new Promise<void>((resolve) => {
212
+ const handler: import("../../../shared/events").EventListener<
213
+ Extract<import("../../../shared/events").AADEvent, { type: "run:completed" }>
214
+ > = () => {
215
+ // runIdの一致をチェック (テスト時はany runIdでも許可)
216
+ progressSpinner.stop();
217
+ eventBus.off("run:completed", handler);
218
+ resolve();
219
+ };
220
+ eventBus.on("run:completed", handler);
221
+ });
222
+
223
+ // 最終結果表示
224
+ console.log("\n" + "=".repeat(60));
225
+ console.log(formatProgress(lastProgress));
226
+ console.log("=".repeat(60) + "\n");
227
+
228
+ if (lastProgress.failed > 0) {
229
+ logger.error({ runId, failedCount: lastProgress.failed }, "Pipeline completed with failures");
230
+ console.error(`❌ Pipeline completed with ${lastProgress.failed} failed tasks`);
231
+ process.exitCode = 1;
232
+ } else {
233
+ logger.info({ runId }, "Pipeline completed successfully");
234
+ console.log("✅ Pipeline completed successfully!");
235
+ }
236
+ }
237
+
238
+ /**
239
+ * Detect workspace information from worktree path
240
+ */
241
+ async function detectWorkspace(
242
+ worktreePath: string,
243
+ logger: import("pino").Logger
244
+ ): Promise<WorkspaceInfo> {
245
+ try {
246
+ const fileChecker = createBunFileChecker();
247
+ const projectType = await detectProjectType(worktreePath, fileChecker);
248
+ const packageManager = await detectPackageManager(worktreePath, fileChecker);
249
+ const testFramework = await detectTestFramework(worktreePath, projectType, fileChecker);
250
+ const framework = await detectFramework(worktreePath, projectType, fileChecker);
251
+ const orm = await detectOrm(worktreePath, projectType, fileChecker);
252
+ const architecture = await detectArchitecturePattern(worktreePath, fileChecker);
253
+
254
+ // Map planning module types → shared types
255
+ const workspace: WorkspaceInfo = {
256
+ path: worktreePath,
257
+ language: mapProjectTypeToLanguage(projectType),
258
+ packageManager,
259
+ framework: framework !== "unknown" ? framework : "none",
260
+ testFramework: mapTestFramework(testFramework),
261
+ orm: orm !== "unknown" ? orm : undefined,
262
+ architecturePattern: architecture !== "custom" && architecture !== "unknown" ? architecture : undefined,
263
+ };
264
+
265
+ logger.debug({ workspace }, "Workspace detected");
266
+ return workspace;
267
+ } catch (error) {
268
+ // Fallback to defaults on detection failure
269
+ logger.warn({ error, worktreePath }, "Project detection failed, using fallback");
270
+ return {
271
+ path: worktreePath,
272
+ language: "typescript",
273
+ packageManager: "bun",
274
+ framework: "none",
275
+ testFramework: "bun-test",
276
+ };
277
+ }
278
+ }
279
+
280
+ /**
281
+ * Map ProjectType → language string
282
+ */
283
+ function mapProjectTypeToLanguage(projectType: import("../../planning").ProjectType): string {
284
+ const mapping: Record<import("../../planning").ProjectType, string> = {
285
+ go: "go",
286
+ "go-workspace": "go",
287
+ rust: "rust",
288
+ python: "python",
289
+ nodejs: "typescript",
290
+ nextjs: "typescript",
291
+ express: "typescript",
292
+ react: "typescript",
293
+ terraform: "hcl",
294
+ unknown: "typescript",
295
+ };
296
+ return mapping[projectType];
297
+ }
298
+
299
+ /**
300
+ * Map planning TestFramework → shared TestFramework
301
+ */
302
+ function mapTestFramework(tf: import("../../planning").TestFramework): import("../../../shared/types").TestFramework {
303
+ // "bun:test" → "bun-test"
304
+ if (tf === "bun:test") return "bun-test";
305
+ // "unittest" は shared types にないので "pytest" にフォールバック
306
+ if (tf === "unittest") return "pytest";
307
+ // "terraform-validate" は shared types にないので "unknown" にフォールバック
308
+ if (tf === "terraform-validate") return "unknown";
309
+
310
+ // その他は1:1マッピング ("pytest", "vitest", "jest", "mocha", "go-test", "cargo-test", "unknown")
311
+ const mapping: Partial<Record<import("../../planning").TestFramework, import("../../../shared/types").TestFramework>> = {
312
+ pytest: "pytest",
313
+ vitest: "vitest",
314
+ jest: "jest",
315
+ mocha: "mocha",
316
+ "go-test": "go-test",
317
+ "cargo-test": "cargo",
318
+ unknown: "unknown",
319
+ };
320
+
321
+ return mapping[tf] ?? "unknown";
322
+ }
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Status Command
3
+ * Display current run status and task progress
4
+ */
5
+
6
+ import { Command } from "commander";
7
+ import type { App } from "../app";
8
+ import type { RunId } from "../../../shared/types";
9
+ import { createRunId } from "../../../shared/types";
10
+ import { formatRunStatus, formatTaskTable } from "../output";
11
+
12
+ export interface StatusCommandOptions {
13
+ runId?: string;
14
+ }
15
+
16
+ /**
17
+ * Create status command
18
+ */
19
+ export function createStatusCommand(getApp: () => App): Command {
20
+ const command = new Command("status");
21
+
22
+ command
23
+ .description("Display run status and task progress")
24
+ .argument("[run-id]", "Run ID to check status (defaults to latest run)")
25
+ .action(async (runIdArg?: string) => {
26
+ let app: App | null = null;
27
+
28
+ try {
29
+ app = getApp();
30
+ await displayStatus(app, runIdArg);
31
+ await app.shutdown();
32
+ process.exit(0);
33
+ } catch (error) {
34
+ if (error instanceof Error) {
35
+ console.error(`Error: ${error.message}`);
36
+ } else {
37
+ console.error("Unknown error:", error);
38
+ }
39
+
40
+ if (app) {
41
+ try {
42
+ await app.shutdown();
43
+ } catch {
44
+ // Ignore shutdown errors
45
+ }
46
+ }
47
+
48
+ process.exit(1);
49
+ }
50
+ });
51
+
52
+ return command;
53
+ }
54
+
55
+ /**
56
+ * Display status for a run
57
+ */
58
+ export async function displayStatus(app: App, runIdArg?: string): Promise<void> {
59
+ const { stores, logger } = app;
60
+
61
+ let runId: RunId;
62
+
63
+ if (runIdArg) {
64
+ runId = createRunId(runIdArg);
65
+ } else {
66
+ // Auto-detect latest run
67
+ const latestRun = await stores.runStore.getLatest();
68
+ if (!latestRun) {
69
+ throw new Error(
70
+ "No runs found. Please provide a run ID or create a new run with 'aad run'."
71
+ );
72
+ }
73
+ runId = latestRun.runId;
74
+ logger.info({ runId }, "Auto-detected latest run");
75
+ }
76
+
77
+ logger.info({ runId }, "Fetching run status");
78
+
79
+ // 1. Fetch run state
80
+ const runState = await stores.runStore.get(runId);
81
+ if (!runState) {
82
+ throw new Error(`Run not found: ${runId}`);
83
+ }
84
+
85
+ // 2. Fetch all tasks
86
+ const tasks = await stores.taskStore.getAll();
87
+
88
+ // 3. Display run status
89
+ console.log("\n" + "=".repeat(60));
90
+ console.log(formatRunStatus(runState));
91
+ console.log("=".repeat(60));
92
+
93
+ // 4. Display task table
94
+ if (tasks.length > 0) {
95
+ console.log("\n" + formatTaskTable(tasks));
96
+ } else {
97
+ console.log("\nNo tasks found.");
98
+ }
99
+
100
+ console.log();
101
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * CLI Module
3
+ *
4
+ * Command-line interface for AAD.
5
+ * Provides progress display, table formatting, and error output helpers.
6
+ */
7
+
8
+ export {
9
+ formatProgress,
10
+ formatTaskTable,
11
+ formatRunStatus,
12
+ formatError,
13
+ formatDuration,
14
+ createSpinner,
15
+ type Spinner,
16
+ } from "./output";
17
+
18
+ export { createApp, type App, type AppOptions } from "./app";
19
+
20
+ export { createRunCommand, runPipeline } from "./commands/run";
21
+ export { createResumeCommand, resumeRun } from "./commands/resume";
22
+ export { createStatusCommand, displayStatus } from "./commands/status";
23
+ export { createCleanupCommand, cleanupWorktrees } from "./commands/cleanup";
24
+
25
+ export {
26
+ createShutdownManager,
27
+ type ShutdownManager,
28
+ type ShutdownHandler,
29
+ } from "./shutdown";
@@ -0,0 +1,256 @@
1
+ import type { ProgressState, Task, RunState } from "@aad/shared/types";
2
+
3
+ // ANSI Color Codes
4
+ const RESET = "\x1b[0m";
5
+ const BOLD = "\x1b[1m";
6
+
7
+ const RED = "\x1b[31m";
8
+ const GREEN = "\x1b[32m";
9
+ const BLUE = "\x1b[34m";
10
+ const CYAN = "\x1b[36m";
11
+ const GRAY = "\x1b[90m";
12
+
13
+ // Spinner Implementation
14
+ export interface Spinner {
15
+ start(): void;
16
+ stop(): void;
17
+ succeed(message?: string): void;
18
+ fail(message?: string): void;
19
+ }
20
+
21
+ const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
22
+
23
+ export function createSpinner(message: string): Spinner {
24
+ let frameIndex = 0;
25
+ let intervalId: Timer | null = null;
26
+
27
+ return {
28
+ start() {
29
+ process.stdout.write(`${SPINNER_FRAMES[0]} ${message}`);
30
+ intervalId = setInterval(() => {
31
+ frameIndex = (frameIndex + 1) % SPINNER_FRAMES.length;
32
+ process.stdout.write(`\r${SPINNER_FRAMES[frameIndex]} ${message}`);
33
+ }, 80);
34
+ },
35
+
36
+ stop() {
37
+ if (intervalId) {
38
+ clearInterval(intervalId);
39
+ intervalId = null;
40
+ process.stdout.write("\r\x1b[K"); // Clear line
41
+ }
42
+ },
43
+
44
+ succeed(msg?: string) {
45
+ if (intervalId) {
46
+ clearInterval(intervalId);
47
+ intervalId = null;
48
+ }
49
+ const finalMsg = msg ?? message;
50
+ process.stdout.write(`\r${GREEN}✓${RESET} ${finalMsg}\n`);
51
+ },
52
+
53
+ fail(msg?: string) {
54
+ if (intervalId) {
55
+ clearInterval(intervalId);
56
+ intervalId = null;
57
+ }
58
+ const finalMsg = msg ?? message;
59
+ process.stdout.write(`\r${RED}✗${RESET} ${finalMsg}\n`);
60
+ },
61
+ };
62
+ }
63
+
64
+ /**
65
+ * Format progress state as colored summary
66
+ */
67
+ export function formatProgress(state: ProgressState): string {
68
+ const { total, pending, running, completed, failed } = state;
69
+ const percentage = total > 0 ? Math.round((completed / total) * 100) : 0;
70
+
71
+ const parts = [
72
+ `${BOLD}Progress:${RESET} ${percentage}%`,
73
+ `${BLUE}${pending} pending${RESET}`,
74
+ `${CYAN}${running} running${RESET}`,
75
+ `${GREEN}${completed} completed${RESET}`,
76
+ ];
77
+
78
+ if (failed > 0) {
79
+ parts.push(`${RED}${failed} failed${RESET}`);
80
+ }
81
+
82
+ return parts.join(" | ");
83
+ }
84
+
85
+ /**
86
+ * Format task list as ASCII table
87
+ */
88
+ export function formatTaskTable(tasks: Task[]): string {
89
+ if (tasks.length === 0) {
90
+ return `${GRAY}No tasks found.${RESET}`;
91
+ }
92
+
93
+ const rows: string[][] = [
94
+ ["ID", "Title", "Status", "Worker", "Duration"],
95
+ ];
96
+
97
+ for (const task of tasks) {
98
+ const id = task.taskId.substring(0, 8);
99
+ const title =
100
+ task.title.length > 40 ? task.title.substring(0, 37) + "..." : task.title;
101
+ const status = colorizeStatus(task.status);
102
+ const worker = task.workerId
103
+ ? task.workerId.substring(0, 8)
104
+ : GRAY + "-" + RESET;
105
+ const duration = task.startTime
106
+ ? formatDuration(
107
+ task.endTime
108
+ ? new Date(task.endTime).getTime() -
109
+ new Date(task.startTime).getTime()
110
+ : Date.now() - new Date(task.startTime).getTime()
111
+ )
112
+ : GRAY + "-" + RESET;
113
+
114
+ rows.push([id, title, status, worker, duration]);
115
+ }
116
+
117
+ return formatTable(rows);
118
+ }
119
+
120
+ /**
121
+ * Format run state as summary box
122
+ */
123
+ export function formatRunStatus(state: RunState): string {
124
+ const lines: string[] = [];
125
+ const duration = state.endTime
126
+ ? formatDuration(
127
+ new Date(state.endTime).getTime() - new Date(state.startTime).getTime()
128
+ )
129
+ : formatDuration(Date.now() - new Date(state.startTime).getTime());
130
+
131
+ lines.push(`${BOLD}Run ID:${RESET} ${state.runId}`);
132
+ lines.push(`${BOLD}Branch:${RESET} ${state.parentBranch}`);
133
+ lines.push(`${BOLD}Duration:${RESET} ${duration}`);
134
+ lines.push("");
135
+ lines.push(
136
+ formatProgress({
137
+ total: state.totalTasks,
138
+ pending: state.pending,
139
+ running: state.running,
140
+ completed: state.completed,
141
+ failed: state.failed,
142
+ })
143
+ );
144
+
145
+ return lines.join("\n");
146
+ }
147
+
148
+ /**
149
+ * Format error with stack trace
150
+ */
151
+ export function formatError(error: Error): string {
152
+ const lines: string[] = [];
153
+ lines.push(`${RED}${BOLD}Error:${RESET} ${error.message}`);
154
+
155
+ if (error.stack) {
156
+ const stackLines = error.stack.split("\n").slice(1, 4); // First 3 stack frames
157
+ for (const line of stackLines) {
158
+ lines.push(`${GRAY} ${line.trim()}${RESET}`);
159
+ }
160
+ }
161
+
162
+ return lines.join("\n");
163
+ }
164
+
165
+ /**
166
+ * Format duration in human-readable format
167
+ */
168
+ export function formatDuration(ms: number): string {
169
+ if (ms < 0) return "0s";
170
+
171
+ const seconds = Math.floor(ms / 1000);
172
+ const minutes = Math.floor(seconds / 60);
173
+ const hours = Math.floor(minutes / 60);
174
+
175
+ if (hours > 0) {
176
+ return `${hours}h ${minutes % 60}m`;
177
+ } else if (minutes > 0) {
178
+ return `${minutes}m ${seconds % 60}s`;
179
+ } else {
180
+ return `${seconds}s`;
181
+ }
182
+ }
183
+
184
+ // Helper Functions
185
+
186
+ function colorizeStatus(status: string): string {
187
+ switch (status) {
188
+ case "pending":
189
+ return `${BLUE}${status}${RESET}`;
190
+ case "running":
191
+ return `${CYAN}${status}${RESET}`;
192
+ case "completed":
193
+ return `${GREEN}${status}${RESET}`;
194
+ case "failed":
195
+ return `${RED}${status}${RESET}`;
196
+ default:
197
+ return `${GRAY}${status}${RESET}`;
198
+ }
199
+ }
200
+
201
+ function formatTable(rows: string[][]): string {
202
+ if (rows.length === 0) return "";
203
+
204
+ const headerRow = rows[0];
205
+ if (!headerRow) return "";
206
+
207
+ // Calculate column widths (without ANSI codes)
208
+ const colWidths = headerRow.map((_, colIndex) => {
209
+ return Math.max(
210
+ ...rows.map((row) => stripAnsi(row[colIndex] ?? "").length)
211
+ );
212
+ });
213
+
214
+ const lines: string[] = [];
215
+
216
+ // Header
217
+ const header = headerRow
218
+ .map((cell, i) => {
219
+ const width = colWidths[i];
220
+ return width !== undefined
221
+ ? cell.padEnd(width + getAnsiLength(cell))
222
+ : cell;
223
+ })
224
+ .join(" | ");
225
+ lines.push(BOLD + header + RESET);
226
+
227
+ // Separator
228
+ const separator = colWidths.map((w) => "-".repeat(w)).join("-+-");
229
+ lines.push(separator);
230
+
231
+ // Rows
232
+ for (let i = 1; i < rows.length; i++) {
233
+ const currentRow = rows[i];
234
+ if (!currentRow) continue;
235
+
236
+ const row = currentRow
237
+ .map((cell, j) => {
238
+ const width = colWidths[j];
239
+ return width !== undefined
240
+ ? cell.padEnd(width + getAnsiLength(cell || ""))
241
+ : cell;
242
+ })
243
+ .join(" | ");
244
+ lines.push(row);
245
+ }
246
+
247
+ return lines.join("\n");
248
+ }
249
+
250
+ function stripAnsi(str: string): string {
251
+ return str.replace(/\x1b\[[0-9;]*m/g, "");
252
+ }
253
+
254
+ function getAnsiLength(str: string): number {
255
+ return str.length - stripAnsi(str).length;
256
+ }