@ronkovic/aad 0.3.2 → 0.3.4
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 +19 -16
- package/package.json +1 -1
- 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 +7 -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 +19 -201
- package/src/modules/cli/commands/task-dispatch-handler.ts +206 -0
- package/src/modules/cli/shutdown.ts +4 -1
- package/src/modules/git-workspace/merge-service.ts +3 -7
- package/src/modules/planning/planning.service.ts +3 -1
- 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/events.ts +12 -0
- package/src/shared/memory-check.ts +34 -16
- package/src/shared/shutdown-handler.ts +5 -1
- package/src/shared/types.ts +9 -0
package/README.md
CHANGED
|
@@ -63,13 +63,13 @@ cli (エントリポイント)
|
|
|
63
63
|
### インストール不要で実行 (推奨)
|
|
64
64
|
|
|
65
65
|
```bash
|
|
66
|
-
bunx aad run requirements.md
|
|
66
|
+
bunx @ronkovic/aad run requirements.md
|
|
67
67
|
```
|
|
68
68
|
|
|
69
69
|
### グローバルインストール
|
|
70
70
|
|
|
71
71
|
```bash
|
|
72
|
-
bun install -g aad
|
|
72
|
+
bun install -g @ronkovic/aad
|
|
73
73
|
aad run requirements.md
|
|
74
74
|
```
|
|
75
75
|
|
|
@@ -151,14 +151,15 @@ aad resume <run_id>
|
|
|
151
151
|
aad cleanup
|
|
152
152
|
```
|
|
153
153
|
|
|
154
|
-
### 4. Webダッシュボード
|
|
154
|
+
### 4. Webダッシュボード
|
|
155
155
|
|
|
156
|
+
ダッシュボードはデフォルトで有効です。`http://localhost:7333` でリアルタイムの進捗・ログ・依存グラフを確認できます。
|
|
157
|
+
|
|
158
|
+
無効にする場合:
|
|
156
159
|
```bash
|
|
157
|
-
aad run requirements.md --workers 4 --dashboard
|
|
160
|
+
aad run requirements.md --workers 4 --no-dashboard
|
|
158
161
|
```
|
|
159
162
|
|
|
160
|
-
ブラウザで `http://localhost:7333` を開くと、リアルタイムで進捗・ログ・依存グラフを確認できます。
|
|
161
|
-
|
|
162
163
|
## 開発ガイド
|
|
163
164
|
|
|
164
165
|
### 環境セットアップ
|
|
@@ -264,16 +265,14 @@ AADはClaudeとの通信に2つのadapterを提供:
|
|
|
264
265
|
|
|
265
266
|
サービスごとに使い分け可能:
|
|
266
267
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
splitter: 'sdk', // 構造化JSON出力
|
|
272
|
-
reviewer: 'sdk', // 構造化レビュー結果
|
|
273
|
-
}
|
|
274
|
-
};
|
|
268
|
+
デフォルトは `sdk` です。CLIに変更する場合:
|
|
269
|
+
|
|
270
|
+
```bash
|
|
271
|
+
aad run requirements.md --provider cli
|
|
275
272
|
```
|
|
276
273
|
|
|
274
|
+
サービスごとに使い分けも可能です(設定ファイルで指定)。
|
|
275
|
+
|
|
277
276
|
## トラブルシューティング
|
|
278
277
|
|
|
279
278
|
### ログの確認
|
|
@@ -286,7 +285,11 @@ cat .aad/docs/<run_id>/logs/structured.jsonl | bunx pino-pretty
|
|
|
286
285
|
### Worktreeが残っている
|
|
287
286
|
|
|
288
287
|
```bash
|
|
289
|
-
|
|
288
|
+
# 全AAD worktreeをクリーンアップ
|
|
289
|
+
aad cleanup
|
|
290
|
+
|
|
291
|
+
# 特定のrunのみ
|
|
292
|
+
aad cleanup <run_id>
|
|
290
293
|
```
|
|
291
294
|
|
|
292
295
|
### タスクがスタックした
|
|
@@ -296,7 +299,7 @@ aad cleanup --all
|
|
|
296
299
|
aad status <run_id>
|
|
297
300
|
|
|
298
301
|
# 強制クリーンアップ
|
|
299
|
-
aad cleanup
|
|
302
|
+
aad cleanup <run_id> --force
|
|
300
303
|
```
|
|
301
304
|
|
|
302
305
|
## ライセンス
|
package/package.json
CHANGED
|
@@ -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 () => {}),
|
|
@@ -117,12 +117,15 @@ describe("runPipeline", () => {
|
|
|
117
117
|
});
|
|
118
118
|
|
|
119
119
|
test("runs pipeline with valid requirements", async () => {
|
|
120
|
-
// Mock dispatcher.start to emit run:completed
|
|
120
|
+
// Mock dispatcher.start to emit run:completed with the actual runId
|
|
121
121
|
mockApp.dispatcher.start = mock(async () => {
|
|
122
|
-
// Emit run:completed after a short delay to allow setup
|
|
123
122
|
setTimeout(() => {
|
|
124
|
-
//
|
|
125
|
-
|
|
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 });
|
|
126
129
|
}, 50);
|
|
127
130
|
});
|
|
128
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,20 +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
11
|
import { getCurrentBranch, cleanupOrphanedFromPreviousRuns } from "../../git-workspace";
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
detectProjectType,
|
|
18
|
-
detectPackageManager,
|
|
19
|
-
detectTestFramework,
|
|
20
|
-
detectFramework,
|
|
21
|
-
detectOrm,
|
|
22
|
-
detectArchitecturePattern,
|
|
23
|
-
createBunFileChecker,
|
|
24
|
-
} from "../../planning";
|
|
12
|
+
import { checkMemoryAndWarn } from "../../../shared/memory-check";
|
|
13
|
+
import { installShutdownHandler } from "../../../shared/shutdown-handler";
|
|
14
|
+
import { registerTaskDispatchHandler } from "./task-dispatch-handler";
|
|
25
15
|
|
|
26
16
|
export function createRunCommand(getApp: () => App): Command {
|
|
27
17
|
const command = new Command("run")
|
|
@@ -54,7 +44,7 @@ export function createRunCommand(getApp: () => App): Command {
|
|
|
54
44
|
* メインパイプライン実行
|
|
55
45
|
*/
|
|
56
46
|
export async function runPipeline(app: App, requirementsPath: string, keepWorktrees = false): Promise<void> {
|
|
57
|
-
const { config, logger, eventBus, planningService, dispatcher, processManager, worktreeManager,
|
|
47
|
+
const { config, logger, eventBus, planningService, dispatcher, processManager, worktreeManager, stores, branchManager } = app;
|
|
58
48
|
|
|
59
49
|
// Validate requirements file exists
|
|
60
50
|
const reqFile = Bun.file(requirementsPath);
|
|
@@ -74,9 +64,6 @@ export async function runPipeline(app: App, requirementsPath: string, keepWorktr
|
|
|
74
64
|
logger.warn({ error }, "Orphaned worktree cleanup failed (non-critical)");
|
|
75
65
|
}
|
|
76
66
|
|
|
77
|
-
// Semaphore for serializing tasks under memory pressure
|
|
78
|
-
let memoryGateLock: Promise<void> = Promise.resolve();
|
|
79
|
-
|
|
80
67
|
// 1. RunId生成 + 現在ブランチ取得
|
|
81
68
|
const runId = createRunId(randomUUID());
|
|
82
69
|
const parentBranch = await getCurrentBranch(process.cwd());
|
|
@@ -140,9 +127,9 @@ export async function runPipeline(app: App, requirementsPath: string, keepWorktr
|
|
|
140
127
|
startTime: new Date().toISOString(),
|
|
141
128
|
});
|
|
142
129
|
|
|
143
|
-
// Worker state初期化
|
|
130
|
+
// Worker state初期化 (ProcessManagerと同じ1-indexedに合わせる)
|
|
144
131
|
for (let i = 0; i < app.config.workers.num; i++) {
|
|
145
|
-
const workerId = createWorkerId(`worker-${i}`);
|
|
132
|
+
const workerId = createWorkerId(`worker-${i + 1}`);
|
|
146
133
|
await stores.workerStore.save({
|
|
147
134
|
workerId,
|
|
148
135
|
status: "idle",
|
|
@@ -157,99 +144,8 @@ export async function runPipeline(app: App, requirementsPath: string, keepWorktr
|
|
|
157
144
|
logger,
|
|
158
145
|
});
|
|
159
146
|
|
|
160
|
-
// 5.
|
|
161
|
-
|
|
162
|
-
"task:dispatched",
|
|
163
|
-
(event) => {
|
|
164
|
-
const taskId = event.taskId;
|
|
165
|
-
const workerId = event.workerId;
|
|
166
|
-
|
|
167
|
-
if (isShuttingDown()) {
|
|
168
|
-
logger.warn({ taskId }, "Shutdown in progress, skipping dispatch");
|
|
169
|
-
return;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
logger.info({ taskId, workerId }, "Task dispatched, starting TDD pipeline");
|
|
173
|
-
|
|
174
|
-
void (async () => {
|
|
175
|
-
try {
|
|
176
|
-
// Memory gate: wait for sufficient memory before dispatch
|
|
177
|
-
try {
|
|
178
|
-
const memResult = await waitForMemory(logger);
|
|
179
|
-
if (memResult.shouldReduceWorkers && config.workers.num > 1) {
|
|
180
|
-
logger.warn(
|
|
181
|
-
{ freeGB: memResult.freeGB, workers: config.workers.num },
|
|
182
|
-
"Low memory: serializing task execution",
|
|
183
|
-
);
|
|
184
|
-
// Serialize by waiting for previous task's lock
|
|
185
|
-
const previousLock = memoryGateLock;
|
|
186
|
-
let releaseLock: () => void;
|
|
187
|
-
memoryGateLock = new Promise<void>((r) => { releaseLock = r; });
|
|
188
|
-
await previousLock;
|
|
189
|
-
// releaseLock will be called at the end of this task
|
|
190
|
-
try {
|
|
191
|
-
await runTask();
|
|
192
|
-
} finally {
|
|
193
|
-
releaseLock!();
|
|
194
|
-
}
|
|
195
|
-
return;
|
|
196
|
-
}
|
|
197
|
-
} catch (memError) {
|
|
198
|
-
// MemoryError → abort task gracefully
|
|
199
|
-
logger.error({ taskId, error: memError }, "Memory gate failed");
|
|
200
|
-
eventBus.emit({ type: "task:failed", taskId, error: String(memError) });
|
|
201
|
-
return;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
await runTask();
|
|
205
|
-
|
|
206
|
-
async function runTask() {
|
|
207
|
-
const task = await stores.taskStore.get(taskId);
|
|
208
|
-
if (!task) {
|
|
209
|
-
throw new Error(`Task not found: ${taskId}`);
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// Worktree作成
|
|
213
|
-
const branchName = `aad/${runId}/${taskId}`;
|
|
214
|
-
const worktreePath = await worktreeManager.createTaskWorktree(taskId, branchName);
|
|
215
|
-
logger.info({ taskId, worktreePath, branchName }, "Worktree created");
|
|
216
|
-
|
|
217
|
-
// Workspace情報 (project detectionから自動検出)
|
|
218
|
-
const workspace = await detectWorkspace(worktreePath, logger);
|
|
219
|
-
|
|
220
|
-
// Run before:execution hook
|
|
221
|
-
await app.pluginManager.runHook("before:execution", { taskId, task, worktreePath });
|
|
222
|
-
|
|
223
|
-
// TDDパイプライン実行
|
|
224
|
-
const provider = providerRegistry.getProvider("implementer");
|
|
225
|
-
const result = await executeTddPipeline(
|
|
226
|
-
task,
|
|
227
|
-
workspace,
|
|
228
|
-
branchName,
|
|
229
|
-
parentBranch,
|
|
230
|
-
process.cwd(),
|
|
231
|
-
runId,
|
|
232
|
-
app.config,
|
|
233
|
-
provider,
|
|
234
|
-
app.mergeService,
|
|
235
|
-
eventBus,
|
|
236
|
-
undefined // testSpawner: use default
|
|
237
|
-
);
|
|
238
|
-
|
|
239
|
-
// Run after:execution hook
|
|
240
|
-
await app.pluginManager.runHook("after:execution", { taskId, result });
|
|
241
|
-
|
|
242
|
-
// 結果をEventBusに通知
|
|
243
|
-
eventBus.emit({ type: "task:completed", taskId, result });
|
|
244
|
-
} // end runTask
|
|
245
|
-
|
|
246
|
-
} catch (error) {
|
|
247
|
-
logger.error({ taskId, workerId, error }, "TDD pipeline failed");
|
|
248
|
-
eventBus.emit({ type: "task:failed", taskId, error: String(error) });
|
|
249
|
-
}
|
|
250
|
-
})();
|
|
251
|
-
}
|
|
252
|
-
);
|
|
147
|
+
// 5. Register task dispatch handler (shared with resume)
|
|
148
|
+
registerTaskDispatchHandler({ app, runId, parentBranch });
|
|
253
149
|
|
|
254
150
|
// 6. 進捗表示
|
|
255
151
|
const progressSpinner = createSpinner("Executing tasks...");
|
|
@@ -269,16 +165,23 @@ export async function runPipeline(app: App, requirementsPath: string, keepWorktr
|
|
|
269
165
|
// 7. Dispatcher.start() (イベントドリブン)
|
|
270
166
|
void dispatcher.start();
|
|
271
167
|
|
|
272
|
-
// 8. run:completed待機 → 結果表示
|
|
273
|
-
|
|
168
|
+
// 8. run:completed待機 → 結果表示 (with timeout)
|
|
169
|
+
const pipelineTimeoutMs = (config.timeouts.staleTask || 5400) * taskPlan.tasks.length * 1000;
|
|
170
|
+
await new Promise<void>((resolve, reject) => {
|
|
274
171
|
const handler: import("../../../shared/events").EventListener<
|
|
275
172
|
Extract<import("../../../shared/events").AADEvent, { type: "run:completed" }>
|
|
276
|
-
> = () => {
|
|
277
|
-
|
|
173
|
+
> = (event) => {
|
|
174
|
+
if (event.runId !== runId) return;
|
|
175
|
+
clearTimeout(timer);
|
|
278
176
|
progressSpinner.stop();
|
|
279
177
|
eventBus.off("run:completed", handler);
|
|
280
178
|
resolve();
|
|
281
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);
|
|
282
185
|
eventBus.on("run:completed", handler);
|
|
283
186
|
});
|
|
284
187
|
|
|
@@ -341,88 +244,3 @@ export async function runPipeline(app: App, requirementsPath: string, keepWorktr
|
|
|
341
244
|
}
|
|
342
245
|
}
|
|
343
246
|
|
|
344
|
-
/**
|
|
345
|
-
* Detect workspace information from worktree path
|
|
346
|
-
*/
|
|
347
|
-
async function detectWorkspace(
|
|
348
|
-
worktreePath: string,
|
|
349
|
-
logger: import("pino").Logger
|
|
350
|
-
): Promise<WorkspaceInfo> {
|
|
351
|
-
try {
|
|
352
|
-
const fileChecker = createBunFileChecker();
|
|
353
|
-
const projectType = await detectProjectType(worktreePath, fileChecker);
|
|
354
|
-
const packageManager = await detectPackageManager(worktreePath, fileChecker);
|
|
355
|
-
const testFramework = await detectTestFramework(worktreePath, projectType, fileChecker);
|
|
356
|
-
const framework = await detectFramework(worktreePath, projectType, fileChecker);
|
|
357
|
-
const orm = await detectOrm(worktreePath, projectType, fileChecker);
|
|
358
|
-
const architecture = await detectArchitecturePattern(worktreePath, fileChecker);
|
|
359
|
-
|
|
360
|
-
// Map planning module types → shared types
|
|
361
|
-
const workspace: WorkspaceInfo = {
|
|
362
|
-
path: worktreePath,
|
|
363
|
-
language: mapProjectTypeToLanguage(projectType),
|
|
364
|
-
packageManager,
|
|
365
|
-
framework: framework !== "unknown" ? framework : "none",
|
|
366
|
-
testFramework: mapTestFramework(testFramework),
|
|
367
|
-
orm: orm !== "unknown" ? orm : undefined,
|
|
368
|
-
architecturePattern: architecture !== "custom" && architecture !== "unknown" ? architecture : undefined,
|
|
369
|
-
};
|
|
370
|
-
|
|
371
|
-
logger.debug({ workspace }, "Workspace detected");
|
|
372
|
-
return workspace;
|
|
373
|
-
} catch (error) {
|
|
374
|
-
// Fallback to defaults on detection failure
|
|
375
|
-
logger.warn({ error, worktreePath }, "Project detection failed, using fallback");
|
|
376
|
-
return {
|
|
377
|
-
path: worktreePath,
|
|
378
|
-
language: "typescript",
|
|
379
|
-
packageManager: "bun",
|
|
380
|
-
framework: "none",
|
|
381
|
-
testFramework: "bun-test",
|
|
382
|
-
};
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
/**
|
|
387
|
-
* Map ProjectType → language string
|
|
388
|
-
*/
|
|
389
|
-
function mapProjectTypeToLanguage(projectType: import("../../planning").ProjectType): string {
|
|
390
|
-
const mapping: Record<import("../../planning").ProjectType, string> = {
|
|
391
|
-
go: "go",
|
|
392
|
-
"go-workspace": "go",
|
|
393
|
-
rust: "rust",
|
|
394
|
-
python: "python",
|
|
395
|
-
nodejs: "typescript",
|
|
396
|
-
nextjs: "typescript",
|
|
397
|
-
express: "typescript",
|
|
398
|
-
react: "typescript",
|
|
399
|
-
terraform: "hcl",
|
|
400
|
-
unknown: "typescript",
|
|
401
|
-
};
|
|
402
|
-
return mapping[projectType];
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
/**
|
|
406
|
-
* Map planning TestFramework → shared TestFramework
|
|
407
|
-
*/
|
|
408
|
-
function mapTestFramework(tf: import("../../planning").TestFramework): import("../../../shared/types").TestFramework {
|
|
409
|
-
// "bun:test" → "bun-test"
|
|
410
|
-
if (tf === "bun:test") return "bun-test";
|
|
411
|
-
// "unittest" は shared types にないので "pytest" にフォールバック
|
|
412
|
-
if (tf === "unittest") return "pytest";
|
|
413
|
-
// "terraform-validate" は shared types にないので "unknown" にフォールバック
|
|
414
|
-
if (tf === "terraform-validate") return "unknown";
|
|
415
|
-
|
|
416
|
-
// その他は1:1マッピング ("pytest", "vitest", "jest", "mocha", "go-test", "cargo-test", "unknown")
|
|
417
|
-
const mapping: Partial<Record<import("../../planning").TestFramework, import("../../../shared/types").TestFramework>> = {
|
|
418
|
-
pytest: "pytest",
|
|
419
|
-
vitest: "vitest",
|
|
420
|
-
jest: "jest",
|
|
421
|
-
mocha: "mocha",
|
|
422
|
-
"go-test": "go-test",
|
|
423
|
-
"cargo-test": "cargo",
|
|
424
|
-
unknown: "unknown",
|
|
425
|
-
};
|
|
426
|
-
|
|
427
|
-
return mapping[tf] ?? "unknown";
|
|
428
|
-
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Task Dispatch Handler
|
|
3
|
+
* Shared logic for registering task:dispatched event handler.
|
|
4
|
+
* Used by both `run` and `resume` commands.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { App } from "../app";
|
|
8
|
+
import type { TaskId, WorkerId, WorkspaceInfo } from "../../../shared/types";
|
|
9
|
+
import type { RunId } from "../../../shared/types";
|
|
10
|
+
import type { AADEvent } from "../../../shared/events";
|
|
11
|
+
import { executeTddPipeline } from "../../task-execution";
|
|
12
|
+
import { waitForMemory } from "../../../shared/memory-check";
|
|
13
|
+
import { isShuttingDown } from "../../../shared/shutdown-handler";
|
|
14
|
+
import {
|
|
15
|
+
detectProjectType,
|
|
16
|
+
detectPackageManager,
|
|
17
|
+
detectTestFramework,
|
|
18
|
+
detectFramework,
|
|
19
|
+
detectOrm,
|
|
20
|
+
detectArchitecturePattern,
|
|
21
|
+
createBunFileChecker,
|
|
22
|
+
} from "../../planning";
|
|
23
|
+
|
|
24
|
+
export interface TaskDispatchContext {
|
|
25
|
+
app: App;
|
|
26
|
+
runId: RunId;
|
|
27
|
+
parentBranch: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Register task:dispatched event handler on the EventBus.
|
|
32
|
+
* Handles memory gate, worker state, worktree creation, and TDD pipeline execution.
|
|
33
|
+
*/
|
|
34
|
+
export function registerTaskDispatchHandler(ctx: TaskDispatchContext): void {
|
|
35
|
+
const { app, runId, parentBranch } = ctx;
|
|
36
|
+
const { eventBus, logger, config, processManager, worktreeManager, providerRegistry, stores } = app;
|
|
37
|
+
|
|
38
|
+
let memoryGateLock: Promise<void> = Promise.resolve();
|
|
39
|
+
|
|
40
|
+
async function executeTask(taskId: TaskId, workerId: WorkerId): Promise<void> {
|
|
41
|
+
try {
|
|
42
|
+
await stores.workerStore.save({ workerId, status: "busy", currentTask: taskId });
|
|
43
|
+
} catch (storeError) {
|
|
44
|
+
logger.warn({ workerId, error: storeError }, "Worker store update failed");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let releaseMemoryGate: (() => void) | null = null;
|
|
48
|
+
try {
|
|
49
|
+
releaseMemoryGate = await acquireMemoryGate();
|
|
50
|
+
|
|
51
|
+
const task = await stores.taskStore.get(taskId);
|
|
52
|
+
if (!task) {
|
|
53
|
+
throw new Error(`Task not found: ${taskId}`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const branchName = `aad/${runId}/${taskId}`;
|
|
57
|
+
const worktreePath = await worktreeManager.createTaskWorktree(taskId, branchName);
|
|
58
|
+
logger.info({ taskId, worktreePath, branchName }, "Worktree created");
|
|
59
|
+
|
|
60
|
+
const workspace = await detectWorkspace(worktreePath, logger);
|
|
61
|
+
|
|
62
|
+
await app.pluginManager.runHook("before:execution", { taskId, task, worktreePath });
|
|
63
|
+
|
|
64
|
+
const provider = providerRegistry.getProvider("implementer");
|
|
65
|
+
const result = await executeTddPipeline(
|
|
66
|
+
task, workspace, branchName, parentBranch, process.cwd(),
|
|
67
|
+
runId, app.config, provider, app.mergeService, eventBus,
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
await app.pluginManager.runHook("after:execution", { taskId, result });
|
|
71
|
+
eventBus.emit({ type: "task:completed", taskId, result });
|
|
72
|
+
} catch (error) {
|
|
73
|
+
logger.error({ taskId, workerId, error }, "TDD pipeline failed");
|
|
74
|
+
eventBus.emit({ type: "task:failed", taskId, error: String(error) });
|
|
75
|
+
} finally {
|
|
76
|
+
releaseMemoryGate?.();
|
|
77
|
+
try {
|
|
78
|
+
processManager.completeTask(workerId);
|
|
79
|
+
await stores.workerStore.save({ workerId, status: "idle", currentTask: null });
|
|
80
|
+
} catch (stateError) {
|
|
81
|
+
logger.warn({ workerId, taskId, error: stateError }, "Worker state recovery failed");
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function acquireMemoryGate(): Promise<(() => void) | null> {
|
|
87
|
+
const memResult = await waitForMemory(logger);
|
|
88
|
+
if (memResult.shouldReduceWorkers && config.workers.num > 1) {
|
|
89
|
+
logger.warn({ freeGB: memResult.freeGB }, "Low memory: serializing task execution");
|
|
90
|
+
const previousLock = memoryGateLock;
|
|
91
|
+
let releaseLock: () => void;
|
|
92
|
+
memoryGateLock = new Promise<void>((r) => { releaseLock = r; });
|
|
93
|
+
await previousLock;
|
|
94
|
+
return () => releaseLock!();
|
|
95
|
+
}
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
eventBus.on<Extract<AADEvent, { type: "task:dispatched" }>>(
|
|
100
|
+
"task:dispatched",
|
|
101
|
+
(event) => {
|
|
102
|
+
const taskId = event.taskId;
|
|
103
|
+
const workerId = event.workerId;
|
|
104
|
+
|
|
105
|
+
if (isShuttingDown()) {
|
|
106
|
+
logger.warn({ taskId }, "Shutdown in progress, skipping dispatch");
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
logger.info({ taskId, workerId }, "Task dispatched, starting TDD pipeline");
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
processManager.assignTask(workerId, taskId);
|
|
114
|
+
} catch (stateError) {
|
|
115
|
+
logger.warn({ workerId, taskId, error: stateError }, "Worker state transition failed");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
void executeTask(taskId, workerId);
|
|
119
|
+
}
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Detect workspace information from worktree path
|
|
125
|
+
*/
|
|
126
|
+
async function detectWorkspace(
|
|
127
|
+
worktreePath: string,
|
|
128
|
+
logger: import("pino").Logger
|
|
129
|
+
): Promise<WorkspaceInfo> {
|
|
130
|
+
try {
|
|
131
|
+
const fileChecker = createBunFileChecker();
|
|
132
|
+
const projectType = await detectProjectType(worktreePath, fileChecker);
|
|
133
|
+
const packageManager = await detectPackageManager(worktreePath, fileChecker);
|
|
134
|
+
const testFramework = await detectTestFramework(worktreePath, projectType, fileChecker);
|
|
135
|
+
const framework = await detectFramework(worktreePath, projectType, fileChecker);
|
|
136
|
+
const orm = await detectOrm(worktreePath, projectType, fileChecker);
|
|
137
|
+
const architecture = await detectArchitecturePattern(worktreePath, fileChecker);
|
|
138
|
+
|
|
139
|
+
const language = await detectLanguage(worktreePath, projectType);
|
|
140
|
+
|
|
141
|
+
const workspace: WorkspaceInfo = {
|
|
142
|
+
path: worktreePath,
|
|
143
|
+
language,
|
|
144
|
+
packageManager,
|
|
145
|
+
framework: framework !== "unknown" ? framework : "none",
|
|
146
|
+
testFramework: mapTestFramework(testFramework),
|
|
147
|
+
orm: orm !== "unknown" ? orm : undefined,
|
|
148
|
+
architecturePattern: architecture !== "custom" && architecture !== "unknown" ? architecture : undefined,
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
logger.debug({ workspace }, "Workspace detected");
|
|
152
|
+
return workspace;
|
|
153
|
+
} catch (error) {
|
|
154
|
+
logger.warn({ error, worktreePath }, "Project detection failed, using fallback");
|
|
155
|
+
return {
|
|
156
|
+
path: worktreePath,
|
|
157
|
+
language: "typescript",
|
|
158
|
+
packageManager: "bun",
|
|
159
|
+
framework: "none",
|
|
160
|
+
testFramework: "bun-test",
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function detectLanguage(worktreePath: string, projectType: import("../../planning").ProjectType): Promise<string> {
|
|
166
|
+
const nodeTypes = ["nodejs", "nextjs", "express", "react"];
|
|
167
|
+
if (nodeTypes.includes(projectType)) {
|
|
168
|
+
const tsConfigExists = await Bun.file(`${worktreePath}/tsconfig.json`).exists();
|
|
169
|
+
return tsConfigExists ? "typescript" : "javascript";
|
|
170
|
+
}
|
|
171
|
+
return mapProjectTypeToLanguage(projectType);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function mapProjectTypeToLanguage(projectType: import("../../planning").ProjectType): string {
|
|
175
|
+
const mapping: Record<import("../../planning").ProjectType, string> = {
|
|
176
|
+
go: "go",
|
|
177
|
+
"go-workspace": "go",
|
|
178
|
+
rust: "rust",
|
|
179
|
+
python: "python",
|
|
180
|
+
nodejs: "javascript",
|
|
181
|
+
nextjs: "typescript",
|
|
182
|
+
express: "javascript",
|
|
183
|
+
react: "javascript",
|
|
184
|
+
terraform: "hcl",
|
|
185
|
+
unknown: "typescript",
|
|
186
|
+
};
|
|
187
|
+
return mapping[projectType];
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function mapTestFramework(tf: import("../../planning").TestFramework): import("../../../shared/types").TestFramework {
|
|
191
|
+
if (tf === "bun:test") return "bun-test";
|
|
192
|
+
if (tf === "unittest") return "pytest";
|
|
193
|
+
if (tf === "terraform-validate") return "unknown";
|
|
194
|
+
|
|
195
|
+
const mapping: Partial<Record<import("../../planning").TestFramework, import("../../../shared/types").TestFramework>> = {
|
|
196
|
+
pytest: "pytest",
|
|
197
|
+
vitest: "vitest",
|
|
198
|
+
jest: "jest",
|
|
199
|
+
mocha: "mocha",
|
|
200
|
+
"go-test": "go-test",
|
|
201
|
+
"cargo-test": "cargo",
|
|
202
|
+
unknown: "unknown",
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
return mapping[tf] ?? "unknown";
|
|
206
|
+
}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Shutdown Manager
|
|
2
|
+
* Shutdown Manager (Public API)
|
|
3
3
|
* LIFO順でshutdownハンドラを実行し、二重shutdown防止、シグナルハンドリングを提供
|
|
4
|
+
*
|
|
5
|
+
* Note: Currently unused internally — run/resume use shared/shutdown-handler.ts instead.
|
|
6
|
+
* Retained as public API for plugin/external integrations.
|
|
4
7
|
*/
|
|
5
8
|
|
|
6
9
|
import type { Logger } from "pino";
|
|
@@ -67,14 +67,10 @@ export class MergeService {
|
|
|
67
67
|
const conflicts = await this.detectConflicts(parentWorktree);
|
|
68
68
|
|
|
69
69
|
if (conflicts.length > 0) {
|
|
70
|
-
|
|
71
|
-
await gitExec(
|
|
72
|
-
["merge", "--abort"],
|
|
73
|
-
{ cwd: parentWorktree, logger: this.logger }
|
|
74
|
-
);
|
|
75
|
-
|
|
76
|
-
this.logger?.warn({ taskId, conflicts }, "Merge conflict detected");
|
|
70
|
+
this.logger?.warn({ taskId, conflicts }, "Merge conflict detected (merge left in progress for resolution)");
|
|
77
71
|
|
|
72
|
+
// Do NOT abort — leave the merge in progress so the resolver can
|
|
73
|
+
// edit conflicted files, `git add` them, and then commit.
|
|
78
74
|
return {
|
|
79
75
|
success: false,
|
|
80
76
|
conflicts,
|
|
@@ -152,7 +152,9 @@ export class PlanningService {
|
|
|
152
152
|
throw error;
|
|
153
153
|
} finally {
|
|
154
154
|
// Cleanup temp directory
|
|
155
|
-
await rm(tempDir, { recursive: true, force: true }).catch(() => {
|
|
155
|
+
await rm(tempDir, { recursive: true, force: true }).catch((cleanupErr) => {
|
|
156
|
+
this.logger.warn({ tempDir, error: cleanupErr }, "Failed to cleanup planning temp directory");
|
|
157
|
+
});
|
|
156
158
|
}
|
|
157
159
|
}
|
|
158
160
|
|
|
@@ -29,7 +29,7 @@ export class ProcessManager {
|
|
|
29
29
|
/**
|
|
30
30
|
* Initialize worker pool
|
|
31
31
|
*/
|
|
32
|
-
initializePool(numWorkers: number): void {
|
|
32
|
+
async initializePool(numWorkers: number): Promise<void> {
|
|
33
33
|
if (this.initialized) {
|
|
34
34
|
throw new ProcessManagerError("ProcessManager already initialized");
|
|
35
35
|
}
|
|
@@ -11,6 +11,7 @@ import type { EventBus } from "@aad/shared/events";
|
|
|
11
11
|
import type { ProcessSpawner } from "./phases/tester-verify";
|
|
12
12
|
|
|
13
13
|
import { estimateTaskComplexity, getAdaptiveEffortLevel } from "@aad/claude-provider";
|
|
14
|
+
import { gitExec } from "@aad/git-workspace";
|
|
14
15
|
import { runTesterRed } from "./phases/tester-red";
|
|
15
16
|
import { runImplementerGreen } from "./phases/implementer-green";
|
|
16
17
|
import { runTests } from "./phases/tester-verify";
|
|
@@ -136,8 +137,11 @@ export async function executeTddPipeline(
|
|
|
136
137
|
// ===== Commit generated code =====
|
|
137
138
|
// Commit changes after Green phase so they can be merged later
|
|
138
139
|
try {
|
|
139
|
-
await
|
|
140
|
-
await
|
|
140
|
+
await gitExec(["add", "-A"], { cwd: workspace.path });
|
|
141
|
+
await gitExec(
|
|
142
|
+
["commit", "--no-gpg-sign", "-m", `feat: Implement ${task.title}`],
|
|
143
|
+
{ cwd: workspace.path }
|
|
144
|
+
);
|
|
141
145
|
} catch (_error) {
|
|
142
146
|
// If commit fails (e.g., no changes), log but don't fail the pipeline
|
|
143
147
|
// This can happen if Claude didn't generate any new files
|
|
@@ -256,11 +260,10 @@ export async function executeTddPipeline(
|
|
|
256
260
|
}
|
|
257
261
|
|
|
258
262
|
if (mergeResult.hadConflict) {
|
|
259
|
-
// Emit conflict event for monitoring
|
|
260
263
|
eventBus.emit({
|
|
261
264
|
type: "execution:merge:conflict",
|
|
262
265
|
taskId: task.taskId,
|
|
263
|
-
conflictedFiles: []
|
|
266
|
+
conflictedFiles: mergeResult.output ? [mergeResult.output] : [],
|
|
264
267
|
});
|
|
265
268
|
}
|
|
266
269
|
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
// Main executor
|
|
2
2
|
export { executeTddPipeline } from "./executor";
|
|
3
3
|
|
|
4
|
+
// Shared PhaseResult type (moved from individual phases)
|
|
5
|
+
export type { PhaseResult } from "@aad/shared/types";
|
|
6
|
+
|
|
4
7
|
// Phase functions
|
|
5
8
|
export { runTesterRed, buildRedPhasePrompt } from "./phases/tester-red";
|
|
6
|
-
export type {
|
|
9
|
+
export type { TesterRedOptions } from "./phases/tester-red";
|
|
7
10
|
|
|
8
11
|
export {
|
|
9
12
|
runImplementerGreen,
|
|
10
13
|
buildGreenPhasePrompt,
|
|
11
14
|
} from "./phases/implementer-green";
|
|
12
|
-
export type {
|
|
13
|
-
PhaseResult as ImplementerGreenResult,
|
|
14
|
-
ImplementerGreenOptions,
|
|
15
|
-
} from "./phases/implementer-green";
|
|
15
|
+
export type { ImplementerGreenOptions } from "./phases/implementer-green";
|
|
16
16
|
|
|
17
17
|
export { runTests, buildTestCommand } from "./phases/tester-verify";
|
|
18
18
|
export type {
|
|
@@ -27,7 +27,6 @@ export {
|
|
|
27
27
|
buildTeamsReviewPrompt,
|
|
28
28
|
} from "./phases/reviewer";
|
|
29
29
|
export type {
|
|
30
|
-
PhaseResult as ReviewerResult,
|
|
31
30
|
ReviewerOptions,
|
|
32
31
|
ReviewConfig,
|
|
33
32
|
} from "./phases/reviewer";
|
|
@@ -1,12 +1,6 @@
|
|
|
1
|
-
import type { Task, WorkspaceInfo, EffortLevel } from "@aad/shared/types";
|
|
1
|
+
import type { Task, WorkspaceInfo, EffortLevel, PhaseResult } from "@aad/shared/types";
|
|
2
2
|
import type { ClaudeProvider } from "@aad/claude-provider";
|
|
3
3
|
|
|
4
|
-
export interface PhaseResult {
|
|
5
|
-
success: boolean;
|
|
6
|
-
output: string;
|
|
7
|
-
duration?: number;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
4
|
export interface ImplementerGreenOptions {
|
|
11
5
|
effortLevel?: EffortLevel;
|
|
12
6
|
model?: string;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Task, WorkspaceInfo } from "@aad/shared/types";
|
|
2
2
|
import type { ClaudeProvider } from "@aad/claude-provider";
|
|
3
3
|
import type { MergeService } from "@aad/git-workspace";
|
|
4
|
+
import { gitExec } from "@aad/git-workspace";
|
|
4
5
|
|
|
5
6
|
export interface MergePhaseResult {
|
|
6
7
|
success: boolean;
|
|
@@ -105,6 +106,29 @@ export async function runMergePhase(
|
|
|
105
106
|
const duration = Date.now() - startTime;
|
|
106
107
|
|
|
107
108
|
if (response.exitCode === 0) {
|
|
109
|
+
// Finalize the merge commit after conflict resolution
|
|
110
|
+
try {
|
|
111
|
+
const statusResult = await gitExec(["diff", "--name-only", "--diff-filter=U"], { cwd: parentWorktree });
|
|
112
|
+
if (statusResult.stdout.trim() === "") {
|
|
113
|
+
// All conflicts resolved — complete the merge commit
|
|
114
|
+
await gitExec(
|
|
115
|
+
["commit", "--no-gpg-sign", "--no-edit"],
|
|
116
|
+
{ cwd: parentWorktree }
|
|
117
|
+
);
|
|
118
|
+
} else {
|
|
119
|
+
// Still unresolved — abort and report failure
|
|
120
|
+
await gitExec(["merge", "--abort"], { cwd: parentWorktree });
|
|
121
|
+
return {
|
|
122
|
+
success: false,
|
|
123
|
+
output: `Unresolved conflicts remain: ${statusResult.stdout.trim()}`,
|
|
124
|
+
hadConflict: true,
|
|
125
|
+
duration,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
} catch (_commitError) {
|
|
129
|
+
// Commit may already be done by Claude via Bash tool — not fatal
|
|
130
|
+
}
|
|
131
|
+
|
|
108
132
|
return {
|
|
109
133
|
success: true,
|
|
110
134
|
output: response.result,
|
|
@@ -1,12 +1,6 @@
|
|
|
1
|
-
import type { Task, WorkspaceInfo, RunId } from "@aad/shared/types";
|
|
1
|
+
import type { Task, WorkspaceInfo, RunId, PhaseResult } from "@aad/shared/types";
|
|
2
2
|
import type { ClaudeProvider, SubagentConfig } from "@aad/claude-provider";
|
|
3
3
|
|
|
4
|
-
export interface PhaseResult {
|
|
5
|
-
success: boolean;
|
|
6
|
-
output: string;
|
|
7
|
-
duration?: number;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
4
|
export interface ReviewerOptions {
|
|
11
5
|
model?: string;
|
|
12
6
|
timeout?: number;
|
|
@@ -1,12 +1,6 @@
|
|
|
1
|
-
import type { Task, WorkspaceInfo, EffortLevel } from "@aad/shared/types";
|
|
1
|
+
import type { Task, WorkspaceInfo, EffortLevel, PhaseResult } from "@aad/shared/types";
|
|
2
2
|
import type { ClaudeProvider } from "@aad/claude-provider";
|
|
3
3
|
|
|
4
|
-
export interface PhaseResult {
|
|
5
|
-
success: boolean;
|
|
6
|
-
output: string;
|
|
7
|
-
duration?: number;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
4
|
export interface TesterRedOptions {
|
|
11
5
|
effortLevel?: EffortLevel;
|
|
12
6
|
model?: string;
|
|
@@ -39,6 +39,8 @@ export interface DispatcherDeps {
|
|
|
39
39
|
eventBus: EventBus;
|
|
40
40
|
config: DispatcherConfig;
|
|
41
41
|
logger: Logger;
|
|
42
|
+
/** Optional synchronous idle worker provider (e.g. ProcessManager) */
|
|
43
|
+
getIdleWorkerIds?: () => WorkerId[];
|
|
42
44
|
}
|
|
43
45
|
|
|
44
46
|
/**
|
|
@@ -161,8 +163,10 @@ export class Dispatcher {
|
|
|
161
163
|
return false;
|
|
162
164
|
}
|
|
163
165
|
|
|
164
|
-
// Get idle workers
|
|
165
|
-
const idleWorkers =
|
|
166
|
+
// Get idle workers (prefer synchronous source if available)
|
|
167
|
+
const idleWorkers = this.deps.getIdleWorkerIds
|
|
168
|
+
? this.deps.getIdleWorkerIds().map(id => ({ workerId: id, status: "idle" as const, currentTask: null }))
|
|
169
|
+
: await this.deps.workerStore.getIdle();
|
|
166
170
|
|
|
167
171
|
if (idleWorkers.length === 0) {
|
|
168
172
|
this.deps.logger.debug("No idle workers available");
|
package/src/shared/events.ts
CHANGED
|
@@ -158,6 +158,18 @@ export class EventBus {
|
|
|
158
158
|
this.emitter.emit("*", event);
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
+
/**
|
|
162
|
+
* Get current listener count for monitoring
|
|
163
|
+
*/
|
|
164
|
+
getListenerCount(type?: EventType): number {
|
|
165
|
+
if (type) {
|
|
166
|
+
return this.emitter.listenerCount(type);
|
|
167
|
+
}
|
|
168
|
+
return this.emitter.eventNames().reduce(
|
|
169
|
+
(sum, name) => sum + this.emitter.listenerCount(name), 0
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
161
173
|
off<T extends AADEvent>(
|
|
162
174
|
type: T["type"] | "*",
|
|
163
175
|
listener: EventListener<T>
|
|
@@ -17,16 +17,30 @@ export interface MemoryStatus {
|
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
19
|
* Get current system memory status (macOS only)
|
|
20
|
+
*
|
|
21
|
+
* Uses vm_stat but counts inactive + free + speculative as "available" memory.
|
|
22
|
+
* macOS aggressively caches files in inactive pages which are immediately
|
|
23
|
+
* reclaimable — counting only "free" pages dramatically underreports
|
|
24
|
+
* available memory (e.g. 0.2GB "free" when 4GB+ is actually available).
|
|
25
|
+
*
|
|
26
|
+
* Environment variable AAD_SKIP_MEMORY_CHECK=1 disables all memory checks.
|
|
20
27
|
*/
|
|
21
28
|
export async function getMemoryStatus(): Promise<MemoryStatus> {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}).exited.then(async (code) => {
|
|
25
|
-
if (code !== 0) throw new Error("vm_stat failed");
|
|
29
|
+
// Allow disabling memory checks entirely
|
|
30
|
+
if (process.env.AAD_SKIP_MEMORY_CHECK === "1") {
|
|
26
31
|
return {
|
|
27
|
-
|
|
32
|
+
totalGB: 16,
|
|
33
|
+
usedGB: 0,
|
|
34
|
+
freeGB: 16,
|
|
35
|
+
usedPercent: 0,
|
|
36
|
+
isLowMemory: false,
|
|
28
37
|
};
|
|
29
|
-
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const proc = Bun.spawn(["vm_stat"], { stdout: "pipe" });
|
|
41
|
+
const stdout = await new Response(proc.stdout).text();
|
|
42
|
+
const code = await proc.exited;
|
|
43
|
+
if (code !== 0) throw new Error("vm_stat failed");
|
|
30
44
|
|
|
31
45
|
// Parse vm_stat output
|
|
32
46
|
const pageSizeMatch = stdout.match(/page size of (\d+) bytes/);
|
|
@@ -35,6 +49,7 @@ export async function getMemoryStatus(): Promise<MemoryStatus> {
|
|
|
35
49
|
const inactiveMatch = stdout.match(/Pages inactive:\s+(\d+)/);
|
|
36
50
|
const speculativeMatch = stdout.match(/Pages speculative:\s+(\d+)/);
|
|
37
51
|
const wiredMatch = stdout.match(/Pages wired down:\s+(\d+)/);
|
|
52
|
+
const purgableMatch = stdout.match(/Pages purgeable:\s+(\d+)/);
|
|
38
53
|
|
|
39
54
|
if (!pageSizeMatch || !freeMatch || !activeMatch || !inactiveMatch || !wiredMatch) {
|
|
40
55
|
throw new Error("Failed to parse vm_stat output");
|
|
@@ -46,30 +61,33 @@ export async function getMemoryStatus(): Promise<MemoryStatus> {
|
|
|
46
61
|
const inactivePages = parseInt(inactiveMatch[1]!, 10);
|
|
47
62
|
const speculativePages = parseInt(speculativeMatch?.[1] ?? "0", 10);
|
|
48
63
|
const wiredPages = parseInt(wiredMatch[1]!, 10);
|
|
64
|
+
const purgablePages = parseInt(purgableMatch?.[1] ?? "0", 10);
|
|
49
65
|
|
|
50
66
|
// Calculate memory in GB
|
|
51
67
|
const bytesToGB = (bytes: number) => bytes / (1024 * 1024 * 1024);
|
|
52
68
|
const pagesToGB = (pages: number) => bytesToGB(pages * pageSize);
|
|
53
69
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const
|
|
70
|
+
// Available = free + inactive + speculative + purgeable
|
|
71
|
+
// macOS reclaims inactive/purgeable pages on demand — they are effectively free
|
|
72
|
+
const availableGB = pagesToGB(freePages + inactivePages + speculativePages + purgablePages);
|
|
73
|
+
const usedGB = pagesToGB(activePages + wiredPages);
|
|
74
|
+
const totalGB = availableGB + usedGB;
|
|
57
75
|
const usedPercent = (usedGB / totalGB) * 100;
|
|
58
76
|
|
|
59
|
-
// Determine if memory is low
|
|
60
|
-
const isLowMemory =
|
|
77
|
+
// Determine if memory is low (using available, not just free)
|
|
78
|
+
const isLowMemory = availableGB < 2.0;
|
|
61
79
|
let recommendedAction: string | undefined;
|
|
62
80
|
|
|
63
|
-
if (
|
|
64
|
-
recommendedAction = "Critical:
|
|
65
|
-
} else if (
|
|
66
|
-
recommendedAction = "Warning:
|
|
81
|
+
if (availableGB < 1.5) {
|
|
82
|
+
recommendedAction = "Critical: Available memory < 1.5GB. Close heavy applications (Chrome, Docker) before running AAD.";
|
|
83
|
+
} else if (availableGB < 2.5) {
|
|
84
|
+
recommendedAction = "Warning: Available memory < 2.5GB. Consider closing unnecessary applications.";
|
|
67
85
|
}
|
|
68
86
|
|
|
69
87
|
return {
|
|
70
88
|
totalGB: Math.round(totalGB * 10) / 10,
|
|
71
89
|
usedGB: Math.round(usedGB * 10) / 10,
|
|
72
|
-
freeGB: Math.round(
|
|
90
|
+
freeGB: Math.round(availableGB * 10) / 10,
|
|
73
91
|
usedPercent: Math.round(usedPercent),
|
|
74
92
|
isLowMemory,
|
|
75
93
|
recommendedAction,
|
|
@@ -46,7 +46,11 @@ export function _resetShutdownState(): void {
|
|
|
46
46
|
* Install signal handlers for graceful shutdown with state persistence.
|
|
47
47
|
*/
|
|
48
48
|
export function installShutdownHandler(options: ShutdownHandlerOptions): void {
|
|
49
|
-
|
|
49
|
+
// Allow re-installation with new options (e.g. resume after run)
|
|
50
|
+
if (_installed && _cleanup) {
|
|
51
|
+
_cleanup();
|
|
52
|
+
_cleanup = null;
|
|
53
|
+
}
|
|
50
54
|
_installed = true;
|
|
51
55
|
|
|
52
56
|
const { runId, stores, logger, exitFn = (code: number) => process.exit(code) } = options;
|
package/src/shared/types.ts
CHANGED
|
@@ -141,3 +141,12 @@ export interface WorkspaceInfo {
|
|
|
141
141
|
orm?: string;
|
|
142
142
|
architecturePattern?: string;
|
|
143
143
|
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Common result type for TDD pipeline phases (Red/Green/Review)
|
|
147
|
+
*/
|
|
148
|
+
export interface PhaseResult {
|
|
149
|
+
success: boolean;
|
|
150
|
+
output: string;
|
|
151
|
+
duration?: number;
|
|
152
|
+
}
|