@ronkovic/aad 0.3.2 → 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/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 +4 -8
- package/src/shared/shutdown-handler.ts +5 -1
- package/src/shared/types.ts +9 -0
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>
|
|
@@ -19,14 +19,10 @@ export interface MemoryStatus {
|
|
|
19
19
|
* Get current system memory status (macOS only)
|
|
20
20
|
*/
|
|
21
21
|
export async function getMemoryStatus(): Promise<MemoryStatus> {
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
return {
|
|
27
|
-
stdout: await new Response(Bun.spawn(["vm_stat"], { stdout: "pipe" }).stdout).text(),
|
|
28
|
-
};
|
|
29
|
-
});
|
|
22
|
+
const proc = Bun.spawn(["vm_stat"], { stdout: "pipe" });
|
|
23
|
+
const stdout = await new Response(proc.stdout).text();
|
|
24
|
+
const code = await proc.exited;
|
|
25
|
+
if (code !== 0) throw new Error("vm_stat failed");
|
|
30
26
|
|
|
31
27
|
// Parse vm_stat output
|
|
32
28
|
const pageSizeMatch = stdout.match(/page size of (\d+) bytes/);
|
|
@@ -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
|
+
}
|