@ronkovic/aad 0.3.1 → 0.3.3

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 (40) hide show
  1. package/README.md +16 -0
  2. package/package.json +1 -1
  3. package/src/main.ts +1 -1
  4. package/src/modules/claude-provider/claude-provider.port.ts +1 -0
  5. package/src/modules/claude-provider/claude-sdk.adapter.ts +61 -4
  6. package/src/modules/cli/__tests__/commands.test.ts +8 -0
  7. package/src/modules/cli/__tests__/resume.test.ts +1 -1
  8. package/src/modules/cli/__tests__/run.test.ts +11 -4
  9. package/src/modules/cli/app.ts +4 -3
  10. package/src/modules/cli/commands/resume.ts +21 -4
  11. package/src/modules/cli/commands/run.ts +82 -158
  12. package/src/modules/cli/commands/task-dispatch-handler.ts +206 -0
  13. package/src/modules/cli/shutdown.ts +4 -1
  14. package/src/modules/git-workspace/__tests__/worktree-cleanup.test.ts +103 -0
  15. package/src/modules/git-workspace/__tests__/worktree-manager.test.ts +83 -0
  16. package/src/modules/git-workspace/branch-manager.ts +8 -8
  17. package/src/modules/git-workspace/index.ts +1 -1
  18. package/src/modules/git-workspace/merge-service.ts +3 -7
  19. package/src/modules/git-workspace/worktree-manager.ts +113 -0
  20. package/src/modules/planning/__tests__/planning-service.test.ts +156 -3
  21. package/src/modules/planning/planning.service.ts +105 -51
  22. package/src/modules/process-manager/process-manager.ts +1 -1
  23. package/src/modules/task-execution/executor.ts +7 -4
  24. package/src/modules/task-execution/index.ts +5 -6
  25. package/src/modules/task-execution/phases/implementer-green.ts +1 -7
  26. package/src/modules/task-execution/phases/merge.ts +24 -0
  27. package/src/modules/task-execution/phases/reviewer.ts +1 -7
  28. package/src/modules/task-execution/phases/tester-red.ts +1 -7
  29. package/src/modules/task-queue/dispatcher.ts +6 -2
  30. package/src/shared/__tests__/memory-check.test.ts +91 -0
  31. package/src/shared/__tests__/memory-monitor.test.ts +120 -0
  32. package/src/shared/__tests__/shutdown-handler.test.ts +138 -0
  33. package/src/shared/__tests__/utils.test.ts +294 -0
  34. package/src/shared/errors.ts +7 -0
  35. package/src/shared/events.ts +12 -0
  36. package/src/shared/memory-check.ts +151 -0
  37. package/src/shared/memory-monitor.ts +69 -0
  38. package/src/shared/shutdown-handler.ts +119 -0
  39. package/src/shared/types.ts +9 -0
  40. package/src/shared/utils.ts +49 -0
package/README.md CHANGED
@@ -91,6 +91,21 @@ bun install
91
91
  bun run dev
92
92
  ```
93
93
 
94
+ ## システム要件
95
+
96
+ | | RAM | 推奨ワーカー数 |
97
+ |---|---|---|
98
+ | **最小** | 8GB | 1(シングルワーカー) |
99
+ | **推奨** | 16GB | 2(並列ワーカー) |
100
+
101
+ **メモリ安全機能:**
102
+ - **自動メモリゲート** — 空きメモリが閾値を下回ると新規ワーカーの起動を抑制
103
+ - **シーケンシャルフォールバック** — メモリ不足時に並列実行から逐次実行へ自動切替
104
+ - **グレースフルシャットダウン** — メモリ圧迫時に安全にプロセスを終了
105
+ - **ランタイム監視** — 実行中のメモリ使用量をリアルタイムで監視
106
+
107
+ > **macOS:** メモリ監視は `vm_stat` ベースで実装されています。Linux環境では `/proc/meminfo` を使用します。
108
+
94
109
  ## クイックスタート
95
110
 
96
111
  ### 1. 要件ファイルの作成
@@ -303,6 +318,7 @@ aad cleanup --run <run_id> --force
303
318
  ## リンク
304
319
 
305
320
  - [Architecture Documentation](docs/architecture.md)
321
+ - [Case Studies](docs/case-studies/) - Real-world execution examples
306
322
  - [Rewrite Plan (Internal)](docs/rewrite-plan/README.md)
307
323
  - [Project Guidelines](CLAUDE.md)
308
324
  - [Issue Tracker](https://github.com/ronkovic/aad/issues)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ronkovic/aad",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "description": "Autonomous Agent Development Orchestrator - Multi-agent TDD pipeline powered by Claude",
5
5
  "module": "src/main.ts",
6
6
  "type": "module",
package/src/main.ts CHANGED
@@ -27,7 +27,7 @@ program
27
27
  .option("--persist <mode>", "Persistence mode: memory or fs", "memory")
28
28
  .option("--debug", "Enable debug logging", false)
29
29
  .option("--no-dashboard", "Disable dashboard server")
30
- .option("--provider <type>", "Default provider: cli or sdk", "cli");
30
+ .option("--provider <type>", "Default provider: cli or sdk", "sdk");
31
31
 
32
32
  // App factory with global options
33
33
  const getApp = (): App => {
@@ -15,6 +15,7 @@ export interface ClaudeRequest {
15
15
  maxRetries?: number;
16
16
  allowedTools?: string[];
17
17
  outputFormat?: "text" | "json";
18
+ jsonSchema?: Record<string, unknown>; // JSON schema for structured output (when outputFormat="json")
18
19
  permissionMode?: "acceptEdits" | "bypassPermissions" | "default" | "delegate" | "dontAsk" | "plan";
19
20
  cwd?: string;
20
21
  appendSystemPrompt?: string;
@@ -2,6 +2,7 @@ import type { ClaudeProvider, ClaudeRequest, ClaudeResponse } from "./claude-pro
2
2
  import type { Config } from "../../shared/config";
3
3
  import type pino from "pino";
4
4
  import { ClaudeProviderError } from "../../shared/errors";
5
+ import { createMemoryMonitor } from "../../shared/memory-monitor";
5
6
  import {
6
7
  query,
7
8
  type Options,
@@ -26,10 +27,32 @@ export class ClaudeSdkAdapter implements ClaudeProvider {
26
27
  const startTime = Date.now();
27
28
  const childLogger = this.logger.child({ adapter: "sdk", prompt: request.prompt.slice(0, 50) });
28
29
 
30
+ const memMonitor = createMemoryMonitor({
31
+ logger: childLogger,
32
+ checkIntervalMs: 2000,
33
+ criticalFreeGB: 0.8,
34
+ });
35
+
29
36
  try {
30
37
  childLogger.info("Starting SDK query");
31
38
 
32
39
  const options = this.buildOptions(request);
40
+
41
+ // Memory monitor: abort query if memory becomes critical
42
+ const memoryAbortController = new AbortController();
43
+ memMonitor.onCritical(() => {
44
+ childLogger.error("Critical memory: aborting SDK query");
45
+ memoryAbortController.abort();
46
+ });
47
+
48
+ // Chain abort controllers if request already has a timeout
49
+ if (options.abortController) {
50
+ const originalController = options.abortController;
51
+ originalController.signal.addEventListener("abort", () => memoryAbortController.abort());
52
+ }
53
+ options.abortController = memoryAbortController;
54
+
55
+ memMonitor.start();
33
56
  const q = query({ prompt: request.prompt, options });
34
57
 
35
58
  // AsyncGeneratorを消費してClaudeResponseにマッピング
@@ -48,10 +71,21 @@ export class ClaudeSdkAdapter implements ClaudeProvider {
48
71
  const textBlocks = assistantMsg.message.content.filter(
49
72
  (block: { type: string }) => block.type === "text"
50
73
  );
74
+
75
+ childLogger.debug({
76
+ contentLength: assistantMsg.message.content.length,
77
+ textBlockCount: textBlocks.length,
78
+ blockTypes: assistantMsg.message.content.map((b: { type: string }) => b.type)
79
+ }, "Assistant message content");
80
+
51
81
  if (textBlocks.length > 0) {
52
82
  lastAssistantMessage = textBlocks
53
83
  .map((block: { text?: string }) => block.text ?? "")
54
84
  .join("\n");
85
+ childLogger.debug({
86
+ textLength: lastAssistantMessage.length,
87
+ textPreview: lastAssistantMessage.slice(0, 300)
88
+ }, "Extracted text from assistant message");
55
89
  }
56
90
  actualModel = assistantMsg.message.model ?? actualModel;
57
91
 
@@ -64,8 +98,22 @@ export class ClaudeSdkAdapter implements ClaudeProvider {
64
98
  resultMessage = message as SDKResultMessage;
65
99
  if (resultMessage.subtype === "success") {
66
100
  const successMsg = resultMessage as SDKResultSuccess;
67
- lastAssistantMessage = successMsg.result;
101
+ childLogger.debug({
102
+ sdkResultLength: successMsg.result?.length ?? 0,
103
+ sdkResultPreview: successMsg.result?.slice(0, 200) ?? "",
104
+ hasResult: !!successMsg.result,
105
+ willOverwrite: !!lastAssistantMessage
106
+ }, "SDK result message received");
107
+
108
+ // Result message contains the final output; use it if available
109
+ if (successMsg.result) {
110
+ lastAssistantMessage = successMsg.result;
111
+ }
68
112
  exitCode = 0;
113
+ childLogger.debug({
114
+ finalLength: lastAssistantMessage.length,
115
+ finalPreview: lastAssistantMessage.slice(0, 200)
116
+ }, "Result success");
69
117
  } else {
70
118
  const errorMsg = resultMessage as SDKResultError;
71
119
  error = errorMsg.errors.join("; ");
@@ -74,6 +122,7 @@ export class ClaudeSdkAdapter implements ClaudeProvider {
74
122
  }
75
123
  }
76
124
 
125
+ memMonitor.stop();
77
126
  const duration = Date.now() - startTime;
78
127
 
79
128
  if (exitCode !== 0) {
@@ -85,14 +134,22 @@ export class ClaudeSdkAdapter implements ClaudeProvider {
85
134
 
86
135
  childLogger.info({ duration, model: actualModel }, "SDK query completed");
87
136
 
88
- return {
137
+ const response: ClaudeResponse = {
89
138
  result: lastAssistantMessage,
90
139
  exitCode,
91
140
  model: actualModel,
92
141
  effortLevel: request.effortLevel ?? "medium",
93
142
  duration,
94
143
  };
144
+
145
+ childLogger.debug({
146
+ responseLength: response.result.length,
147
+ responsePreview: response.result.slice(0, 300)
148
+ }, "Returning response");
149
+
150
+ return response;
95
151
  } catch (err) {
152
+ memMonitor.stop();
96
153
  const duration = Date.now() - startTime;
97
154
  childLogger.error({ err, duration }, "SDK query error");
98
155
 
@@ -121,10 +178,10 @@ export class ClaudeSdkAdapter implements ClaudeProvider {
121
178
  // Output Format
122
179
  if (request.outputFormat === "json") {
123
180
  // JSON出力を要求する場合、schema指定が必要
124
- // ここでは汎用的なschemaを使用
181
+ // jsonSchemaが指定されていればそれを使用、なければ汎用的なschemaを使用
125
182
  options.outputFormat = {
126
183
  type: "json_schema",
127
- schema: {
184
+ schema: request.jsonSchema ?? {
128
185
  type: "object",
129
186
  properties: {
130
187
  result: { type: "string" },
@@ -46,12 +46,20 @@ describe("Resume Command", () => {
46
46
  stores: {
47
47
  runStore: {
48
48
  get: mock(async () => mockRunState),
49
+ save: mock(async () => {}),
49
50
  } as any,
50
51
  taskStore: {
51
52
  list: mock(async () => [mockTask]),
52
53
  getAll: mock(async () => [mockTask]),
53
54
  save: mock(async () => {}),
54
55
  } as any,
56
+ workerStore: {
57
+ save: mock(async () => {}),
58
+ getAll: mock(async () => []),
59
+ getIdle: mock(async () => []),
60
+ get: mock(async () => null),
61
+ delete: mock(async () => {}),
62
+ } as any,
55
63
  } as any,
56
64
  dispatcher: {
57
65
  initialize: mock(async () => {}),
@@ -83,7 +83,7 @@ describe("resumeRun", () => {
83
83
  getAll: mock(async () => mockTasks),
84
84
  save: mock(async () => {}),
85
85
  } as any,
86
- workerStore: {} as any,
86
+ workerStore: { save: async () => {}, getAll: async () => [], getIdle: async () => [], get: async () => null, delete: async () => {} } as any,
87
87
  },
88
88
  dispatcher: {
89
89
  initialize: mock(async () => {}),
@@ -39,9 +39,13 @@ describe("runPipeline", () => {
39
39
  logger: {
40
40
  info: mock(() => {}),
41
41
  error: mock(() => {}),
42
+ warn: mock(() => {}),
43
+ debug: mock(() => {}),
42
44
  child: mock(() => ({
43
45
  info: mock(() => {}),
44
46
  error: mock(() => {}),
47
+ warn: mock(() => {}),
48
+ debug: mock(() => {}),
45
49
  })),
46
50
  } as any,
47
51
  stores: {
@@ -113,12 +117,15 @@ describe("runPipeline", () => {
113
117
  });
114
118
 
115
119
  test("runs pipeline with valid requirements", async () => {
116
- // Mock dispatcher.start to emit run:completed immediately
120
+ // Mock dispatcher.start to emit run:completed with the actual runId
117
121
  mockApp.dispatcher.start = mock(async () => {
118
- // Emit run:completed after a short delay to allow setup
119
122
  setTimeout(() => {
120
- // Use any runId that matches the pattern
121
- eventBus.emit({ type: "run:completed", runId: createRunId("any-run-id") });
123
+ // Capture runId from the queue:initialized event or use wildcard listener
124
+ // The run:completed handler checks runId, so we need to match it.
125
+ // Listen for queue:initialized to get the actual runId from dispatcher.initialize call.
126
+ const initCall = (mockApp.dispatcher.initialize as any).mock?.calls?.[0]?.[0];
127
+ const actualRunId = initCall?.runId ?? createRunId("fallback-run-id");
128
+ eventBus.emit({ type: "run:completed", runId: actualRunId });
122
129
  }, 50);
123
130
  });
124
131
 
@@ -86,13 +86,13 @@ export function createApp(options: AppOptions = {}): App {
86
86
 
87
87
  // 4. LogStore + SSE Transport初期化
88
88
  const logStore = new LogStore();
89
- const sseTransport = createSSETransport({ logStore, eventBus });
90
- void sseTransport;
89
+ // SSE transport registers EventBus listeners internally; no direct reference needed
90
+ createSSETransport({ logStore, eventBus });
91
91
 
92
92
  // 5. ProviderRegistry初期化
93
93
  const providerRegistry = createProviderRegistry(
94
94
  {
95
- default: options.providerDefault ?? "cli",
95
+ default: options.providerDefault ?? "sdk",
96
96
  overrides: options.providerOverrides,
97
97
  },
98
98
  config,
@@ -128,6 +128,7 @@ export function createApp(options: AppOptions = {}): App {
128
128
  staleTaskThreshold: config.timeouts.staleTask * 1000, // Convert seconds to milliseconds
129
129
  },
130
130
  logger: logger.child({ service: "task-queue" }),
131
+ getIdleWorkerIds: () => processManager.getIdleWorkers(),
131
132
  });
132
133
  logger.debug("Dispatcher initialized");
133
134
 
@@ -5,7 +5,9 @@
5
5
 
6
6
  import { Command } from "commander";
7
7
  import type { App } from "../app";
8
- import { createRunId } from "../../../shared/types";
8
+ import { createRunId, createWorkerId } from "../../../shared/types";
9
+ import { registerTaskDispatchHandler } from "./task-dispatch-handler";
10
+ import { installShutdownHandler } from "../../../shared/shutdown-handler";
9
11
 
10
12
  export function createResumeCommand(getApp: () => App): Command {
11
13
  const command = new Command("resume")
@@ -45,9 +47,10 @@ export async function resumeRun(app: App, runIdStr: string): Promise<void> {
45
47
 
46
48
  // 2. タスク一覧を読み込み
47
49
  const allTasks = await stores.taskStore.getAll();
48
- const runTasks = allTasks.filter((task) => {
49
- return task.taskId.includes(runIdStr);
50
- });
50
+ // In-memory store contains only one run's tasks (no cross-run contamination).
51
+ // For fs persistence, tasks are scoped to the run directory.
52
+ // Use all tasks — the dispatcher was initialized with this run's task plan.
53
+ const runTasks = allTasks;
51
54
 
52
55
  logger.info({ taskCount: runTasks.length }, "Tasks loaded");
53
56
 
@@ -104,9 +107,23 @@ export async function resumeRun(app: App, runIdStr: string): Promise<void> {
104
107
  await dispatcher.initialize(taskPlan);
105
108
  await processManager.initializePool(app.config.workers.num);
106
109
 
110
+ // Worker state初期化 (ProcessManagerと同じ1-indexedに合わせる)
111
+ for (let i = 0; i < app.config.workers.num; i++) {
112
+ const workerId = createWorkerId(`worker-${i + 1}`);
113
+ await stores.workerStore.save({ workerId, status: "idle", currentTask: null });
114
+ }
115
+
107
116
  logger.info("Dispatcher and ProcessManager re-initialized");
108
117
  console.log("✅ Dispatcher and ProcessManager re-initialized\n");
109
118
 
119
+ // 5.5. Install shutdown handler + task dispatch handler
120
+ installShutdownHandler({
121
+ runId,
122
+ stores: { runStore: stores.runStore, taskStore: stores.taskStore },
123
+ logger,
124
+ });
125
+ registerTaskDispatchHandler({ app, runId, parentBranch: runState.parentBranch });
126
+
110
127
  // 6. Dispatcher.start()
111
128
  dispatcher.start();
112
129
  logger.info({ runId }, "Run resumed");
@@ -8,18 +8,10 @@ import { randomUUID } from "node:crypto";
8
8
  import type { App } from "../app";
9
9
  import { formatProgress, formatTaskTable, createSpinner } from "../output";
10
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";
11
+ import { getCurrentBranch, cleanupOrphanedFromPreviousRuns } from "../../git-workspace";
12
+ import { checkMemoryAndWarn } from "../../../shared/memory-check";
13
+ import { installShutdownHandler } from "../../../shared/shutdown-handler";
14
+ import { registerTaskDispatchHandler } from "./task-dispatch-handler";
23
15
 
24
16
  export function createRunCommand(getApp: () => App): Command {
25
17
  const command = new Command("run")
@@ -29,14 +21,15 @@ export function createRunCommand(getApp: () => App): Command {
29
21
  .option("--persist <mode>", "Persistence mode: memory or fs", "memory")
30
22
  .option("--debug", "Enable debug logging")
31
23
  .option("--no-dashboard", "Disable dashboard server")
32
- .option("--provider <type>", "Default provider: cli or sdk", "cli")
24
+ .option("--provider <type>", "Default provider: cli or sdk", "sdk")
33
25
  .option("--repos <paths>", "Comma-separated repository paths for multi-repo mode")
34
26
  .option("--strategy <type>", "Multi-repo strategy: independent or coordinated", "independent")
35
27
  .option("--plugins <paths>", "Comma-separated plugin paths to load")
36
- .action(async (requirementsPath: string) => {
28
+ .option("--keep-worktrees", "Keep worktrees and branches after completion (default: auto-cleanup)", false)
29
+ .action(async (requirementsPath: string, cmdOptions: { keepWorktrees?: boolean }) => {
37
30
  try {
38
31
  const app = getApp();
39
- await runPipeline(app, requirementsPath);
32
+ await runPipeline(app, requirementsPath, cmdOptions.keepWorktrees ?? false);
40
33
  } catch (error) {
41
34
  const message = error instanceof Error ? error.message : String(error);
42
35
  console.error("Run pipeline failed:", message);
@@ -50,8 +43,8 @@ export function createRunCommand(getApp: () => App): Command {
50
43
  /**
51
44
  * メインパイプライン実行
52
45
  */
53
- export async function runPipeline(app: App, requirementsPath: string): Promise<void> {
54
- const { config, logger, eventBus, planningService, dispatcher, processManager, worktreeManager, providerRegistry, stores } = app;
46
+ export async function runPipeline(app: App, requirementsPath: string, keepWorktrees = false): Promise<void> {
47
+ const { config, logger, eventBus, planningService, dispatcher, processManager, worktreeManager, stores, branchManager } = app;
55
48
 
56
49
  // Validate requirements file exists
57
50
  const reqFile = Bun.file(requirementsPath);
@@ -61,6 +54,16 @@ export async function runPipeline(app: App, requirementsPath: string): Promise<v
61
54
 
62
55
  logger.info({ requirementsPath }, "Starting AAD pipeline");
63
56
 
57
+ // 0. Memory check
58
+ await checkMemoryAndWarn(logger);
59
+
60
+ // 0.5. Cleanup orphaned worktrees/branches from previous runs
61
+ try {
62
+ await cleanupOrphanedFromPreviousRuns(worktreeManager, logger);
63
+ } catch (error) {
64
+ logger.warn({ error }, "Orphaned worktree cleanup failed (non-critical)");
65
+ }
66
+
64
67
  // 1. RunId生成 + 現在ブランチ取得
65
68
  const runId = createRunId(randomUUID());
66
69
  const parentBranch = await getCurrentBranch(process.cwd());
@@ -124,9 +127,9 @@ export async function runPipeline(app: App, requirementsPath: string): Promise<v
124
127
  startTime: new Date().toISOString(),
125
128
  });
126
129
 
127
- // Worker state初期化
130
+ // Worker state初期化 (ProcessManagerと同じ1-indexedに合わせる)
128
131
  for (let i = 0; i < app.config.workers.num; i++) {
129
- const workerId = createWorkerId(`worker-${i}`);
132
+ const workerId = createWorkerId(`worker-${i + 1}`);
130
133
  await stores.workerStore.save({
131
134
  workerId,
132
135
  status: "idle",
@@ -134,60 +137,15 @@ export async function runPipeline(app: App, requirementsPath: string): Promise<v
134
137
  });
135
138
  }
136
139
 
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");
140
+ // 4.5. Install graceful shutdown handler
141
+ installShutdownHandler({
142
+ runId,
143
+ stores: { runStore: stores.runStore, taskStore: stores.taskStore },
144
+ logger,
145
+ });
144
146
 
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
- );
147
+ // 5. Register task dispatch handler (shared with resume)
148
+ registerTaskDispatchHandler({ app, runId, parentBranch });
191
149
 
192
150
  // 6. 進捗表示
193
151
  const progressSpinner = createSpinner("Executing tasks...");
@@ -207,16 +165,23 @@ export async function runPipeline(app: App, requirementsPath: string): Promise<v
207
165
  // 7. Dispatcher.start() (イベントドリブン)
208
166
  void dispatcher.start();
209
167
 
210
- // 8. run:completed待機 → 結果表示
211
- await new Promise<void>((resolve) => {
168
+ // 8. run:completed待機 → 結果表示 (with timeout)
169
+ const pipelineTimeoutMs = (config.timeouts.staleTask || 5400) * taskPlan.tasks.length * 1000;
170
+ await new Promise<void>((resolve, reject) => {
212
171
  const handler: import("../../../shared/events").EventListener<
213
172
  Extract<import("../../../shared/events").AADEvent, { type: "run:completed" }>
214
- > = () => {
215
- // runIdの一致をチェック (テスト時はany runIdでも許可)
173
+ > = (event) => {
174
+ if (event.runId !== runId) return;
175
+ clearTimeout(timer);
216
176
  progressSpinner.stop();
217
177
  eventBus.off("run:completed", handler);
218
178
  resolve();
219
179
  };
180
+ const timer = setTimeout(() => {
181
+ progressSpinner.stop();
182
+ eventBus.off("run:completed", handler);
183
+ reject(new Error(`Pipeline timed out after ${Math.round(pipelineTimeoutMs / 1000)}s`));
184
+ }, pipelineTimeoutMs);
220
185
  eventBus.on("run:completed", handler);
221
186
  });
222
187
 
@@ -233,90 +198,49 @@ export async function runPipeline(app: App, requirementsPath: string): Promise<v
233
198
  logger.info({ runId }, "Pipeline completed successfully");
234
199
  console.log("✅ Pipeline completed successfully!");
235
200
  }
236
- }
237
201
 
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
- };
202
+ // 9. Auto-cleanup worktrees and branches (unless --keep-worktrees)
203
+ if (!keepWorktrees) {
204
+ console.log("\n🧹 Cleaning up worktrees and branches...");
205
+ logger.info({ runId }, "Auto-cleanup started");
206
+
207
+ try {
208
+ // List all worktrees
209
+ const worktrees = await worktreeManager.listWorktrees();
210
+ const worktreeBase = `${process.cwd()}/.aad/worktrees`;
211
+ const runWorktrees = worktrees.filter(
212
+ (wt) => wt.path.startsWith(worktreeBase)
213
+ && wt.branch.includes(runId) // Check branch name for runId
214
+ && !wt.path.includes(`parent-${runId}`) // Keep parent worktree (contains merge results)
215
+ );
216
+
217
+ // Remove worktrees for this run
218
+ let removed = 0;
219
+ for (const worktree of runWorktrees) {
220
+ try {
221
+ await worktreeManager.removeWorktree(worktree.path, false);
222
+ removed++;
223
+ logger.debug({ path: worktree.path }, "Worktree removed");
224
+ } catch (error) {
225
+ logger.error({ path: worktree.path, error }, "Failed to remove worktree");
226
+ }
227
+ }
264
228
 
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
- }
229
+ // Prune orphaned worktrees
230
+ await worktreeManager.pruneWorktrees();
279
231
 
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
- }
232
+ // Cleanup orphaned branches for this run (force delete merged branches)
233
+ const deletedBranches = await branchManager.cleanupOrphanBranches(runId, true);
298
234
 
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";
235
+ console.log(` Removed ${removed} worktree(s)`);
236
+ console.log(` Deleted ${deletedBranches.length} branch(es)`);
237
+ logger.info({ runId, removedWorktrees: removed, deletedBranches: deletedBranches.length }, "Auto-cleanup completed");
238
+ } catch (error) {
239
+ logger.error({ runId, error }, "Auto-cleanup failed");
240
+ console.error(" ⚠️ Cleanup failed (non-critical)");
241
+ }
242
+ } else {
243
+ logger.info({ runId }, "Auto-cleanup skipped (--keep-worktrees)");
244
+ }
322
245
  }
246
+