@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.
- package/README.md +16 -0
- package/package.json +1 -1
- package/src/main.ts +1 -1
- package/src/modules/claude-provider/claude-provider.port.ts +1 -0
- package/src/modules/claude-provider/claude-sdk.adapter.ts +61 -4
- package/src/modules/cli/__tests__/commands.test.ts +8 -0
- package/src/modules/cli/__tests__/resume.test.ts +1 -1
- package/src/modules/cli/__tests__/run.test.ts +11 -4
- package/src/modules/cli/app.ts +4 -3
- package/src/modules/cli/commands/resume.ts +21 -4
- package/src/modules/cli/commands/run.ts +82 -158
- package/src/modules/cli/commands/task-dispatch-handler.ts +206 -0
- package/src/modules/cli/shutdown.ts +4 -1
- package/src/modules/git-workspace/__tests__/worktree-cleanup.test.ts +103 -0
- package/src/modules/git-workspace/__tests__/worktree-manager.test.ts +83 -0
- package/src/modules/git-workspace/branch-manager.ts +8 -8
- package/src/modules/git-workspace/index.ts +1 -1
- package/src/modules/git-workspace/merge-service.ts +3 -7
- package/src/modules/git-workspace/worktree-manager.ts +113 -0
- package/src/modules/planning/__tests__/planning-service.test.ts +156 -3
- package/src/modules/planning/planning.service.ts +105 -51
- package/src/modules/process-manager/process-manager.ts +1 -1
- package/src/modules/task-execution/executor.ts +7 -4
- package/src/modules/task-execution/index.ts +5 -6
- package/src/modules/task-execution/phases/implementer-green.ts +1 -7
- package/src/modules/task-execution/phases/merge.ts +24 -0
- package/src/modules/task-execution/phases/reviewer.ts +1 -7
- package/src/modules/task-execution/phases/tester-red.ts +1 -7
- package/src/modules/task-queue/dispatcher.ts +6 -2
- package/src/shared/__tests__/memory-check.test.ts +91 -0
- package/src/shared/__tests__/memory-monitor.test.ts +120 -0
- package/src/shared/__tests__/shutdown-handler.test.ts +138 -0
- package/src/shared/__tests__/utils.test.ts +294 -0
- package/src/shared/errors.ts +7 -0
- package/src/shared/events.ts +12 -0
- package/src/shared/memory-check.ts +151 -0
- package/src/shared/memory-monitor.ts +69 -0
- package/src/shared/shutdown-handler.ts +119 -0
- package/src/shared/types.ts +9 -0
- 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
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", "
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
|
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
|
-
//
|
|
121
|
-
|
|
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
|
|
package/src/modules/cli/app.ts
CHANGED
|
@@ -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
|
-
|
|
90
|
-
|
|
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 ?? "
|
|
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
|
-
|
|
49
|
-
|
|
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
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
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", "
|
|
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
|
-
.
|
|
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,
|
|
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.
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
146
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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
|
-
|
|
266
|
-
|
|
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
|
-
|
|
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
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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
|
+
|