@ronkovic/aad 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +312 -0
- package/bin/aad.js +2 -0
- package/package.json +78 -0
- package/src/__tests__/e2e/pipeline-e2e.test.ts +279 -0
- package/src/__tests__/e2e/resume-e2e.test.ts +200 -0
- package/src/__tests__/integration/cli-smoke.test.ts +175 -0
- package/src/__tests__/integration/pipeline.test.ts +346 -0
- package/src/bun-imports.d.ts +14 -0
- package/src/main.ts +52 -0
- package/src/modules/claude-provider/__tests__/claude-cli.adapter.test.ts +277 -0
- package/src/modules/claude-provider/__tests__/claude-sdk-real-env.test.ts +127 -0
- package/src/modules/claude-provider/__tests__/claude-sdk.adapter.test.ts +347 -0
- package/src/modules/claude-provider/__tests__/effort-strategy.test.ts +212 -0
- package/src/modules/claude-provider/__tests__/provider-registry.test.ts +251 -0
- package/src/modules/claude-provider/__tests__/retry.test.ts +201 -0
- package/src/modules/claude-provider/claude-cli.adapter.ts +156 -0
- package/src/modules/claude-provider/claude-provider.port.ts +35 -0
- package/src/modules/claude-provider/claude-sdk.adapter.ts +217 -0
- package/src/modules/claude-provider/effort-strategy.ts +94 -0
- package/src/modules/claude-provider/index.ts +32 -0
- package/src/modules/claude-provider/provider-registry.ts +92 -0
- package/src/modules/claude-provider/retry.ts +81 -0
- package/src/modules/cli/__tests__/app.test.ts +160 -0
- package/src/modules/cli/__tests__/cleanup.test.ts +111 -0
- package/src/modules/cli/__tests__/commands.test.ts +186 -0
- package/src/modules/cli/__tests__/output.test.ts +329 -0
- package/src/modules/cli/__tests__/resume.test.ts +324 -0
- package/src/modules/cli/__tests__/run.test.ts +168 -0
- package/src/modules/cli/__tests__/shutdown.test.ts +168 -0
- package/src/modules/cli/__tests__/status.test.ts +144 -0
- package/src/modules/cli/app.ts +241 -0
- package/src/modules/cli/commands/cleanup.ts +120 -0
- package/src/modules/cli/commands/resume.ts +156 -0
- package/src/modules/cli/commands/run.ts +322 -0
- package/src/modules/cli/commands/status.ts +101 -0
- package/src/modules/cli/index.ts +29 -0
- package/src/modules/cli/output.ts +256 -0
- package/src/modules/cli/shutdown.ts +122 -0
- package/src/modules/dashboard/__tests__/api-routes.test.ts +204 -0
- package/src/modules/dashboard/__tests__/file-watcher.test.ts +34 -0
- package/src/modules/dashboard/__tests__/server.test.ts +120 -0
- package/src/modules/dashboard/__tests__/sse-broadcaster.test.ts +163 -0
- package/src/modules/dashboard/__tests__/sse-routes.test.ts +58 -0
- package/src/modules/dashboard/__tests__/state-aggregator.test.ts +330 -0
- package/src/modules/dashboard/index.ts +8 -0
- package/src/modules/dashboard/routes/api.ts +84 -0
- package/src/modules/dashboard/routes/sse.ts +37 -0
- package/src/modules/dashboard/server.ts +111 -0
- package/src/modules/dashboard/services/file-watcher.ts +36 -0
- package/src/modules/dashboard/services/sse-broadcaster.ts +81 -0
- package/src/modules/dashboard/services/state-aggregator.ts +132 -0
- package/src/modules/dashboard/ui/dashboard.html +405 -0
- package/src/modules/git-workspace/__tests__/branch-manager.test.ts +335 -0
- package/src/modules/git-workspace/__tests__/git-exec.test.ts +91 -0
- package/src/modules/git-workspace/__tests__/memory-sync.test.ts +273 -0
- package/src/modules/git-workspace/__tests__/merge-service.test.ts +286 -0
- package/src/modules/git-workspace/__tests__/settings-merge.test.ts +163 -0
- package/src/modules/git-workspace/__tests__/worktree-manager.test.ts +247 -0
- package/src/modules/git-workspace/branch-manager.ts +191 -0
- package/src/modules/git-workspace/git-exec.ts +124 -0
- package/src/modules/git-workspace/index.ts +17 -0
- package/src/modules/git-workspace/memory-sync.ts +89 -0
- package/src/modules/git-workspace/merge-service.ts +156 -0
- package/src/modules/git-workspace/settings-merge.ts +95 -0
- package/src/modules/git-workspace/worktree-manager.ts +199 -0
- package/src/modules/logging/__tests__/log-store.test.ts +242 -0
- package/src/modules/logging/__tests__/logger.test.ts +81 -0
- package/src/modules/logging/__tests__/sse-transport.test.ts +93 -0
- package/src/modules/logging/index.ts +7 -0
- package/src/modules/logging/log-store.ts +80 -0
- package/src/modules/logging/logger.ts +55 -0
- package/src/modules/logging/transports/sse-transport.ts +28 -0
- package/src/modules/multi-repo/__tests__/multi-repo-planner.test.ts +93 -0
- package/src/modules/multi-repo/__tests__/repo-context.test.ts +79 -0
- package/src/modules/multi-repo/index.ts +12 -0
- package/src/modules/multi-repo/multi-repo-planner.ts +112 -0
- package/src/modules/multi-repo/repo-context.ts +71 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-81991/progress.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-81991/queue/completed/task-getall-2.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-81991/queue/pending/task-1.json +13 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-81991/queue/pending/task-getall-1.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-81991/queue/pending/task-status-change.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-81991/workers/worker-1.json +5 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-81991/workers/worker-2.json +5 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-82469/progress.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-82469/queue/completed/task-getall-2.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-82469/queue/pending/task-1.json +13 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-82469/queue/pending/task-getall-1.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-82469/queue/pending/task-status-change.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-82469/workers/worker-1.json +5 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-82469/workers/worker-2.json +5 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-85301/progress.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-85301/queue/completed/task-getall-2.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-85301/queue/pending/task-1.json +13 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-85301/queue/pending/task-getall-1.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-85301/queue/pending/task-status-change.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-85301/workers/worker-1.json +5 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-85301/workers/worker-2.json +5 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-85759/progress.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-85759/queue/completed/task-getall-2.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-85759/queue/pending/task-1.json +13 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-85759/queue/pending/task-getall-1.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-85759/queue/pending/task-status-change.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-85759/workers/worker-1.json +5 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-85759/workers/worker-2.json +5 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-86184/progress.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-86184/queue/completed/task-getall-2.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-86184/queue/pending/task-1.json +13 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-86184/queue/pending/task-getall-1.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-86184/queue/pending/task-status-change.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-86184/workers/worker-1.json +5 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-86184/workers/worker-2.json +5 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-88026/progress.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-88026/queue/completed/task-getall-2.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-88026/queue/pending/task-1.json +13 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-88026/queue/pending/task-getall-1.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-88026/queue/pending/task-status-change.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-88026/workers/worker-1.json +5 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-88026/workers/worker-2.json +5 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-89475/progress.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-89475/queue/completed/task-getall-2.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-89475/queue/pending/task-1.json +13 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-89475/queue/pending/task-getall-1.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-89475/queue/pending/task-status-change.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-89475/workers/worker-1.json +5 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-89475/workers/worker-2.json +5 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-89924/progress.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-89924/queue/completed/task-getall-2.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-89924/queue/pending/task-1.json +13 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-89924/queue/pending/task-getall-1.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-89924/queue/pending/task-status-change.json +10 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-89924/workers/worker-1.json +5 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-89924/workers/worker-2.json +5 -0
- package/src/modules/persistence/__tests__/file-lock.test.ts +141 -0
- package/src/modules/persistence/__tests__/index.test.ts +38 -0
- package/src/modules/persistence/__tests__/stores.test.ts +594 -0
- package/src/modules/persistence/file-lock.ts +158 -0
- package/src/modules/persistence/fs-run-store.ts +73 -0
- package/src/modules/persistence/fs-task-store.ts +152 -0
- package/src/modules/persistence/fs-worker-store.ts +116 -0
- package/src/modules/persistence/in-memory-stores.ts +98 -0
- package/src/modules/persistence/index.ts +60 -0
- package/src/modules/persistence/stores.port.ts +60 -0
- package/src/modules/planning/__tests__/file-conflict-validator.test.ts +256 -0
- package/src/modules/planning/__tests__/planning-service.test.ts +366 -0
- package/src/modules/planning/__tests__/project-detection.test.ts +707 -0
- package/src/modules/planning/file-conflict-validator.ts +135 -0
- package/src/modules/planning/index.ts +40 -0
- package/src/modules/planning/planning.service.ts +262 -0
- package/src/modules/planning/project-detection.ts +525 -0
- package/src/modules/plugin/__tests__/plugin-loader.test.ts +83 -0
- package/src/modules/plugin/__tests__/plugin-manager.test.ts +187 -0
- package/src/modules/plugin/index.ts +3 -0
- package/src/modules/plugin/plugin-loader.ts +46 -0
- package/src/modules/plugin/plugin-manager.ts +90 -0
- package/src/modules/plugin/plugin.types.ts +37 -0
- package/src/modules/process-manager/__tests__/process-manager.test.ts +210 -0
- package/src/modules/process-manager/__tests__/worker.test.ts +89 -0
- package/src/modules/process-manager/index.ts +5 -0
- package/src/modules/process-manager/process-manager.ts +193 -0
- package/src/modules/process-manager/worker.ts +106 -0
- package/src/modules/task-execution/__tests__/default-spawner.test.ts +154 -0
- package/src/modules/task-execution/__tests__/executor.test.ts +760 -0
- package/src/modules/task-execution/__tests__/implementer-green.test.ts +286 -0
- package/src/modules/task-execution/__tests__/merge-phase.test.ts +368 -0
- package/src/modules/task-execution/__tests__/reviewer.test.ts +302 -0
- package/src/modules/task-execution/__tests__/tester-red.test.ts +281 -0
- package/src/modules/task-execution/__tests__/tester-verify.test.ts +313 -0
- package/src/modules/task-execution/executor.ts +303 -0
- package/src/modules/task-execution/index.ts +45 -0
- package/src/modules/task-execution/phases/default-spawner.ts +49 -0
- package/src/modules/task-execution/phases/implementer-green.ts +100 -0
- package/src/modules/task-execution/phases/merge.ts +122 -0
- package/src/modules/task-execution/phases/reviewer.ts +160 -0
- package/src/modules/task-execution/phases/tester-red.ts +100 -0
- package/src/modules/task-execution/phases/tester-verify.ts +120 -0
- package/src/modules/task-queue/__tests__/dependency-resolver.test.ts +456 -0
- package/src/modules/task-queue/__tests__/dispatcher.test.ts +824 -0
- package/src/modules/task-queue/__tests__/task-plan.test.ts +122 -0
- package/src/modules/task-queue/__tests__/task.test.ts +130 -0
- package/src/modules/task-queue/dependency-resolver.ts +171 -0
- package/src/modules/task-queue/dispatcher.ts +372 -0
- package/src/modules/task-queue/index.ts +16 -0
- package/src/modules/task-queue/task-plan.ts +40 -0
- package/src/modules/task-queue/task.ts +67 -0
- package/src/shared/__tests__/config.test.ts +204 -0
- package/src/shared/__tests__/errors.test.ts +285 -0
- package/src/shared/__tests__/events.test.ts +496 -0
- package/src/shared/__tests__/types.test.ts +360 -0
- package/src/shared/config.ts +133 -0
- package/src/shared/errors.ts +128 -0
- package/src/shared/events.ts +171 -0
- package/src/shared/types.ts +143 -0
- package/tsconfig.json +30 -0
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
import type { Logger } from "pino";
|
|
2
|
+
import type { TaskId, WorkerId, TaskPlan, Task, ProgressState } from "@aad/shared/types";
|
|
3
|
+
import type { EventBus } from "@aad/shared/events";
|
|
4
|
+
import { TaskQueueError } from "@aad/shared/errors";
|
|
5
|
+
import { getReadyTasks } from "./dependency-resolver";
|
|
6
|
+
|
|
7
|
+
// Import from persistence module
|
|
8
|
+
export interface TaskStore {
|
|
9
|
+
get(taskId: TaskId): Promise<Task | null>;
|
|
10
|
+
getAll(): Promise<Task[]>;
|
|
11
|
+
getByStatus(status: import("@aad/shared/types").TaskStatus): Promise<Task[]>;
|
|
12
|
+
save(task: Task): Promise<void>;
|
|
13
|
+
delete(taskId: TaskId): Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface WorkerStore {
|
|
17
|
+
get(workerId: WorkerId): Promise<import("@aad/shared/types").Worker | null>;
|
|
18
|
+
getAll(): Promise<Array<import("@aad/shared/types").Worker>>;
|
|
19
|
+
getIdle(): Promise<Array<import("@aad/shared/types").Worker>>;
|
|
20
|
+
save(worker: import("@aad/shared/types").Worker): Promise<void>;
|
|
21
|
+
delete(workerId: WorkerId): Promise<void>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface RunStore {
|
|
25
|
+
get(runId: import("@aad/shared/types").RunId): Promise<import("@aad/shared/types").RunState | null>;
|
|
26
|
+
save(state: import("@aad/shared/types").RunState): Promise<void>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface DispatcherConfig {
|
|
30
|
+
maxRetries?: number;
|
|
31
|
+
staleTaskCheckInterval?: number;
|
|
32
|
+
staleTaskThreshold?: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface DispatcherDeps {
|
|
36
|
+
taskStore: TaskStore;
|
|
37
|
+
workerStore: WorkerStore;
|
|
38
|
+
runStore: RunStore;
|
|
39
|
+
eventBus: EventBus;
|
|
40
|
+
config: DispatcherConfig;
|
|
41
|
+
logger: Logger;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Event-driven task dispatcher
|
|
46
|
+
* Manages task lifecycle and dependency resolution
|
|
47
|
+
*/
|
|
48
|
+
export class Dispatcher {
|
|
49
|
+
private deps: DispatcherDeps;
|
|
50
|
+
private staleCheckInterval?: Timer;
|
|
51
|
+
private taskMap: Map<string, Task> = new Map();
|
|
52
|
+
private initialized = false;
|
|
53
|
+
private runId?: import("@aad/shared/types").RunId;
|
|
54
|
+
|
|
55
|
+
constructor(deps: DispatcherDeps) {
|
|
56
|
+
this.deps = deps;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async initialize(taskPlan: TaskPlan): Promise<void> {
|
|
60
|
+
if (this.initialized) {
|
|
61
|
+
throw new TaskQueueError("Dispatcher already initialized");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Store runId for later use
|
|
65
|
+
this.runId = taskPlan.runId;
|
|
66
|
+
|
|
67
|
+
// Save all tasks to store
|
|
68
|
+
for (const task of taskPlan.tasks) {
|
|
69
|
+
await this.deps.taskStore.save(task);
|
|
70
|
+
this.taskMap.set(task.taskId as string, task);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Save run state
|
|
74
|
+
await this.deps.runStore.save({
|
|
75
|
+
runId: taskPlan.runId,
|
|
76
|
+
parentBranch: taskPlan.parentBranch,
|
|
77
|
+
totalTasks: taskPlan.tasks.length,
|
|
78
|
+
pending: taskPlan.tasks.length,
|
|
79
|
+
running: 0,
|
|
80
|
+
completed: 0,
|
|
81
|
+
failed: 0,
|
|
82
|
+
startTime: new Date().toISOString(),
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
this.initialized = true;
|
|
86
|
+
|
|
87
|
+
this.deps.eventBus.emit({
|
|
88
|
+
type: "queue:initialized",
|
|
89
|
+
totalTasks: taskPlan.tasks.length,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
this.deps.logger.info(
|
|
93
|
+
{ runId: taskPlan.runId, totalTasks: taskPlan.tasks.length },
|
|
94
|
+
"Dispatcher initialized"
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
start(): void {
|
|
99
|
+
if (!this.initialized) {
|
|
100
|
+
throw new TaskQueueError("Dispatcher not initialized");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Listen to worker:idle events
|
|
104
|
+
this.deps.eventBus.on("worker:idle", (event) => {
|
|
105
|
+
if (event.type === "worker:idle") {
|
|
106
|
+
void this.handleWorkerIdle(event.workerId);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// Listen to task:completed events
|
|
111
|
+
this.deps.eventBus.on("task:completed", (event) => {
|
|
112
|
+
if (event.type === "task:completed") {
|
|
113
|
+
void this.handleTaskCompleted(event.taskId);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// Listen to task:failed events
|
|
118
|
+
this.deps.eventBus.on("task:failed", (event) => {
|
|
119
|
+
if (event.type === "task:failed") {
|
|
120
|
+
void this.handleTaskFailed(event.taskId, event.error);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// Start stale task detection
|
|
125
|
+
this.startStaleDetection();
|
|
126
|
+
|
|
127
|
+
this.deps.logger.info("Dispatcher started");
|
|
128
|
+
|
|
129
|
+
// Trigger initial dispatch (CRITICAL FIX #2)
|
|
130
|
+
void this.dispatchNext();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
stop(): void {
|
|
134
|
+
if (this.staleCheckInterval) {
|
|
135
|
+
clearInterval(this.staleCheckInterval);
|
|
136
|
+
this.staleCheckInterval = undefined;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
this.deps.logger.info("Dispatcher stopped");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async dispatchNext(): Promise<boolean> {
|
|
143
|
+
// Get ready tasks
|
|
144
|
+
const taskMapReadonly = new Map(
|
|
145
|
+
Array.from(this.taskMap.entries()).map(([k, v]) => [k as unknown as TaskId, v])
|
|
146
|
+
) as ReadonlyMap<TaskId, Task>;
|
|
147
|
+
const readyTasks = getReadyTasks(taskMapReadonly);
|
|
148
|
+
|
|
149
|
+
if (readyTasks.length === 0) {
|
|
150
|
+
const pendingTasks = Array.from(this.taskMap.values()).filter(
|
|
151
|
+
(t) => t.status === "pending"
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
if (pendingTasks.length === 0) {
|
|
155
|
+
this.deps.eventBus.emit({ type: "queue:empty" });
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Tasks are waiting for dependencies
|
|
160
|
+
this.deps.logger.debug("No ready tasks, waiting for dependencies");
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Get idle workers
|
|
165
|
+
const idleWorkers = await this.deps.workerStore.getIdle();
|
|
166
|
+
|
|
167
|
+
if (idleWorkers.length === 0) {
|
|
168
|
+
this.deps.logger.debug("No idle workers available");
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Dispatch multiple tasks to available workers (FIX #2)
|
|
173
|
+
let dispatched = 0;
|
|
174
|
+
for (let i = 0; i < Math.min(readyTasks.length, idleWorkers.length); i++) {
|
|
175
|
+
const task = readyTasks[i];
|
|
176
|
+
const worker = idleWorkers[i];
|
|
177
|
+
|
|
178
|
+
if (!task || !worker) {
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Update task status
|
|
183
|
+
task.status = "running";
|
|
184
|
+
task.workerId = worker.workerId;
|
|
185
|
+
task.startTime = new Date().toISOString();
|
|
186
|
+
await this.deps.taskStore.save(task);
|
|
187
|
+
this.taskMap.set(task.taskId as string, task);
|
|
188
|
+
|
|
189
|
+
// Emit dispatch event
|
|
190
|
+
this.deps.eventBus.emit({
|
|
191
|
+
type: "task:dispatched",
|
|
192
|
+
taskId: task.taskId,
|
|
193
|
+
workerId: worker.workerId,
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
this.deps.logger.info(
|
|
197
|
+
{ taskId: task.taskId, workerId: worker.workerId },
|
|
198
|
+
"Task dispatched"
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
dispatched++;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
this.updateProgress();
|
|
205
|
+
|
|
206
|
+
return dispatched > 0;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async handleTaskCompleted(taskId: TaskId): Promise<void> {
|
|
210
|
+
const task = this.taskMap.get(taskId as string);
|
|
211
|
+
if (!task) {
|
|
212
|
+
this.deps.logger.warn({ taskId }, "Task not found for completion");
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
task.status = "completed";
|
|
217
|
+
task.endTime = new Date().toISOString();
|
|
218
|
+
await this.deps.taskStore.save(task);
|
|
219
|
+
this.taskMap.set(taskId as string, task);
|
|
220
|
+
|
|
221
|
+
this.updateProgress();
|
|
222
|
+
|
|
223
|
+
this.deps.logger.info({ taskId }, "Task completed");
|
|
224
|
+
|
|
225
|
+
// Try to dispatch next tasks
|
|
226
|
+
await this.dispatchNext();
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async handleTaskFailed(taskId: TaskId, error: string): Promise<void> {
|
|
230
|
+
const task = this.taskMap.get(taskId as string);
|
|
231
|
+
if (!task) {
|
|
232
|
+
this.deps.logger.warn({ taskId }, "Task not found for failure");
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const maxRetries = this.deps.config.maxRetries ?? 2;
|
|
237
|
+
|
|
238
|
+
if (task.retryCount < maxRetries) {
|
|
239
|
+
// Retry
|
|
240
|
+
task.status = "pending";
|
|
241
|
+
task.retryCount += 1;
|
|
242
|
+
task.workerId = undefined;
|
|
243
|
+
task.failureReason = error;
|
|
244
|
+
await this.deps.taskStore.save(task);
|
|
245
|
+
this.taskMap.set(taskId as string, task);
|
|
246
|
+
|
|
247
|
+
this.deps.eventBus.emit({
|
|
248
|
+
type: "task:retry",
|
|
249
|
+
taskId,
|
|
250
|
+
retryCount: task.retryCount,
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
this.deps.logger.warn(
|
|
254
|
+
{ taskId, retryCount: task.retryCount, error },
|
|
255
|
+
"Task failed, retrying"
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
// Try to dispatch
|
|
259
|
+
await this.dispatchNext();
|
|
260
|
+
} else {
|
|
261
|
+
// Failed permanently
|
|
262
|
+
task.status = "failed";
|
|
263
|
+
task.endTime = new Date().toISOString();
|
|
264
|
+
task.failureReason = error;
|
|
265
|
+
await this.deps.taskStore.save(task);
|
|
266
|
+
this.taskMap.set(taskId as string, task);
|
|
267
|
+
|
|
268
|
+
this.updateProgress();
|
|
269
|
+
|
|
270
|
+
this.deps.logger.error({ taskId, error }, "Task failed permanently");
|
|
271
|
+
|
|
272
|
+
// Cascade fail dependent tasks
|
|
273
|
+
await this.cascadeFailDependents(taskId);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
getProgress(): ProgressState {
|
|
278
|
+
const tasks = Array.from(this.taskMap.values());
|
|
279
|
+
return {
|
|
280
|
+
total: tasks.length,
|
|
281
|
+
pending: tasks.filter((t) => t.status === "pending").length,
|
|
282
|
+
running: tasks.filter((t) => t.status === "running").length,
|
|
283
|
+
completed: tasks.filter((t) => t.status === "completed").length,
|
|
284
|
+
failed: tasks.filter((t) => t.status === "failed").length,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
private async handleWorkerIdle(workerId: WorkerId): Promise<void> {
|
|
289
|
+
this.deps.logger.debug({ workerId: workerId as string }, "Worker idle, attempting dispatch");
|
|
290
|
+
await this.dispatchNext();
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
private async cascadeFailDependents(failedTaskId: TaskId): Promise<void> {
|
|
294
|
+
// Find all tasks that depend on the failed task
|
|
295
|
+
for (const task of this.taskMap.values()) {
|
|
296
|
+
if (task.dependsOn.includes(failedTaskId) && task.status === "pending") {
|
|
297
|
+
task.status = "failed";
|
|
298
|
+
task.failureReason = `Dependency ${failedTaskId as string} failed`;
|
|
299
|
+
task.endTime = new Date().toISOString();
|
|
300
|
+
await this.deps.taskStore.save(task);
|
|
301
|
+
this.taskMap.set(task.taskId as string, task);
|
|
302
|
+
|
|
303
|
+
this.deps.logger.warn(
|
|
304
|
+
{ taskId: task.taskId, failedDep: failedTaskId },
|
|
305
|
+
"Task failed due to dependency failure"
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
// Recursively fail dependents
|
|
309
|
+
await this.cascadeFailDependents(task.taskId);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
this.updateProgress();
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
private updateProgress(): void {
|
|
317
|
+
const progress = this.getProgress();
|
|
318
|
+
|
|
319
|
+
this.deps.eventBus.emit({
|
|
320
|
+
type: "progress:updated",
|
|
321
|
+
state: progress,
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
// Emit run:completed if all tasks are done (FIX #1)
|
|
325
|
+
if (progress.pending === 0 && progress.running === 0) {
|
|
326
|
+
if (this.runId) {
|
|
327
|
+
this.deps.eventBus.emit({
|
|
328
|
+
type: "run:completed",
|
|
329
|
+
runId: this.runId,
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
this.deps.logger.info(
|
|
333
|
+
{ runId: this.runId, completed: progress.completed, failed: progress.failed },
|
|
334
|
+
"Run completed"
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
private startStaleDetection(): void {
|
|
341
|
+
const interval = this.deps.config.staleTaskCheckInterval ?? 60000; // 60s default
|
|
342
|
+
|
|
343
|
+
this.staleCheckInterval = setInterval(() => {
|
|
344
|
+
this.detectStaleTasks();
|
|
345
|
+
}, interval);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
private detectStaleTasks(): void {
|
|
349
|
+
const staleThreshold = this.deps.config.staleTaskThreshold ?? 300000; // 5 minutes
|
|
350
|
+
const now = Date.now();
|
|
351
|
+
|
|
352
|
+
for (const task of this.taskMap.values()) {
|
|
353
|
+
if (task.status === "running" && task.startTime) {
|
|
354
|
+
const startTime = new Date(task.startTime).getTime();
|
|
355
|
+
const elapsed = now - startTime;
|
|
356
|
+
|
|
357
|
+
if (elapsed > staleThreshold) {
|
|
358
|
+
this.deps.eventBus.emit({
|
|
359
|
+
type: "task:stale",
|
|
360
|
+
taskId: task.taskId,
|
|
361
|
+
elapsed,
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
this.deps.logger.warn(
|
|
365
|
+
{ taskId: task.taskId, elapsed },
|
|
366
|
+
"Stale task detected"
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export { parseTask, serializeTask, TaskJsonSchema } from "./task";
|
|
2
|
+
export type { TaskJson } from "./task";
|
|
3
|
+
|
|
4
|
+
export { parseTaskPlan, loadTaskPlan } from "./task-plan";
|
|
5
|
+
export type { TaskPlanJson } from "./task-plan";
|
|
6
|
+
|
|
7
|
+
export {
|
|
8
|
+
checkDependencies,
|
|
9
|
+
detectCircularDependencies,
|
|
10
|
+
getReadyTasks,
|
|
11
|
+
validateTaskPlan,
|
|
12
|
+
} from "./dependency-resolver";
|
|
13
|
+
export type { DependencyStatus, DependencyResult } from "./dependency-resolver";
|
|
14
|
+
|
|
15
|
+
export { Dispatcher } from "./dispatcher";
|
|
16
|
+
export type { DispatcherDeps, TaskStore, WorkerStore, RunStore } from "./dispatcher";
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { createRunId, type TaskPlan } from "@aad/shared/types";
|
|
3
|
+
import { parseTask, TaskJsonSchema } from "./task";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* TaskPlan JSON schema (snake_case from Bash scripts)
|
|
7
|
+
*/
|
|
8
|
+
const TaskPlanJsonSchema = z.object({
|
|
9
|
+
run_id: z.string().transform((id) => createRunId(id)),
|
|
10
|
+
parent_branch: z.string(),
|
|
11
|
+
title: z.string().optional(),
|
|
12
|
+
description: z.string().optional(),
|
|
13
|
+
tasks: z.array(TaskJsonSchema),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export type TaskPlanJson = z.input<typeof TaskPlanJsonSchema>;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Parse task_plan.json to TaskPlan domain model
|
|
20
|
+
*/
|
|
21
|
+
export function parseTaskPlan(json: unknown): TaskPlan {
|
|
22
|
+
const parsed = TaskPlanJsonSchema.parse(json);
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
runId: parsed.run_id,
|
|
26
|
+
parentBranch: parsed.parent_branch,
|
|
27
|
+
title: parsed.title,
|
|
28
|
+
description: parsed.description,
|
|
29
|
+
tasks: parsed.tasks.map((taskJson) => parseTask(taskJson)),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Load and parse task_plan.json from file
|
|
35
|
+
*/
|
|
36
|
+
export async function loadTaskPlan(filePath: string): Promise<TaskPlan> {
|
|
37
|
+
const content = await Bun.file(filePath).text();
|
|
38
|
+
const json = JSON.parse(content);
|
|
39
|
+
return parseTaskPlan(json);
|
|
40
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { createTaskId, createWorkerId, type Task, type TaskStatus } from "@aad/shared/types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Task schema for JSON deserialization
|
|
6
|
+
* Handles snake_case to camelCase conversion
|
|
7
|
+
*/
|
|
8
|
+
const TaskStatusSchema = z.enum(["pending", "running", "completed", "failed"]);
|
|
9
|
+
|
|
10
|
+
export const TaskJsonSchema = z.object({
|
|
11
|
+
task_id: z.string().transform((id) => createTaskId(id)),
|
|
12
|
+
title: z.string(),
|
|
13
|
+
description: z.string(),
|
|
14
|
+
files_to_modify: z.array(z.string()),
|
|
15
|
+
depends_on: z.array(z.string()).transform((ids) => ids.map((id) => createTaskId(id))),
|
|
16
|
+
priority: z.number(),
|
|
17
|
+
status: TaskStatusSchema.optional().default("pending"),
|
|
18
|
+
worker_id: z.string().optional(),
|
|
19
|
+
start_time: z.string().optional(),
|
|
20
|
+
end_time: z.string().optional(),
|
|
21
|
+
retry_count: z.number().optional().default(0),
|
|
22
|
+
failure_reason: z.string().optional(),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
export type TaskJson = z.input<typeof TaskJsonSchema>;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Parse task from snake_case JSON to Task domain model
|
|
29
|
+
*/
|
|
30
|
+
export function parseTask(json: unknown): Task {
|
|
31
|
+
const parsed = TaskJsonSchema.parse(json);
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
taskId: parsed.task_id,
|
|
35
|
+
title: parsed.title,
|
|
36
|
+
description: parsed.description,
|
|
37
|
+
filesToModify: parsed.files_to_modify,
|
|
38
|
+
dependsOn: parsed.depends_on,
|
|
39
|
+
priority: parsed.priority,
|
|
40
|
+
status: parsed.status as TaskStatus,
|
|
41
|
+
workerId: parsed.worker_id ? createWorkerId(parsed.worker_id) : undefined,
|
|
42
|
+
startTime: parsed.start_time,
|
|
43
|
+
endTime: parsed.end_time,
|
|
44
|
+
retryCount: parsed.retry_count,
|
|
45
|
+
failureReason: parsed.failure_reason,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Serialize Task to snake_case JSON
|
|
51
|
+
*/
|
|
52
|
+
export function serializeTask(task: Task): Record<string, unknown> {
|
|
53
|
+
return {
|
|
54
|
+
task_id: task.taskId,
|
|
55
|
+
title: task.title,
|
|
56
|
+
description: task.description,
|
|
57
|
+
files_to_modify: task.filesToModify,
|
|
58
|
+
depends_on: task.dependsOn,
|
|
59
|
+
priority: task.priority,
|
|
60
|
+
status: task.status,
|
|
61
|
+
worker_id: task.workerId,
|
|
62
|
+
start_time: task.startTime,
|
|
63
|
+
end_time: task.endTime,
|
|
64
|
+
retry_count: task.retryCount,
|
|
65
|
+
failure_reason: task.failureReason,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { loadConfig } from "../config";
|
|
3
|
+
import { ConfigError } from "../errors";
|
|
4
|
+
|
|
5
|
+
describe("loadConfig", () => {
|
|
6
|
+
test("loads config with default values when no env vars set", () => {
|
|
7
|
+
const config = loadConfig({});
|
|
8
|
+
|
|
9
|
+
expect(config.workers.num).toBe(2);
|
|
10
|
+
expect(config.workers.max).toBe(8);
|
|
11
|
+
expect(config.models.default).toBeUndefined();
|
|
12
|
+
expect(config.models.splitter).toBeUndefined();
|
|
13
|
+
expect(config.models.tester).toBeUndefined();
|
|
14
|
+
expect(config.models.implementer).toBeUndefined();
|
|
15
|
+
expect(config.models.reviewer).toBeUndefined();
|
|
16
|
+
expect(config.models.mergeResolver).toBeUndefined();
|
|
17
|
+
expect(config.models.branchName).toBeUndefined();
|
|
18
|
+
expect(config.timeouts.claude).toBe(1200);
|
|
19
|
+
expect(config.timeouts.test).toBe(600);
|
|
20
|
+
expect(config.timeouts.staleTask).toBe(5400);
|
|
21
|
+
expect(config.retry.maxRetries).toBe(2);
|
|
22
|
+
expect(config.debug).toBe(false);
|
|
23
|
+
expect(config.adaptiveEffort).toBe(false);
|
|
24
|
+
expect(config.teams.splitter).toBe(false);
|
|
25
|
+
expect(config.teams.reviewer).toBe(false);
|
|
26
|
+
expect(config.memorySync).toBe(true);
|
|
27
|
+
expect(config.dashboard.enabled).toBe(true);
|
|
28
|
+
expect(config.dashboard.port).toBe(7333);
|
|
29
|
+
expect(config.dashboard.host).toBe("localhost");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("loads config from env vars", () => {
|
|
33
|
+
const config = loadConfig({
|
|
34
|
+
AAD_NUM_WORKERS: "4",
|
|
35
|
+
AAD_MAX_WORKERS: "16",
|
|
36
|
+
AAD_MODEL: "opus",
|
|
37
|
+
AAD_MODEL_SPLITTER: "sonnet",
|
|
38
|
+
AAD_MODEL_TESTER: "sonnet",
|
|
39
|
+
AAD_MODEL_IMPLEMENTER: "sonnet",
|
|
40
|
+
AAD_MODEL_REVIEWER: "opus",
|
|
41
|
+
AAD_MODEL_MERGE: "haiku",
|
|
42
|
+
AAD_MODEL_BRANCH_NAME: "haiku",
|
|
43
|
+
CLAUDE_TIMEOUT: "1800",
|
|
44
|
+
TEST_TIMEOUT: "900",
|
|
45
|
+
AAD_STALE_TASK_TIMEOUT: "7200",
|
|
46
|
+
MAX_PHASE_RETRIES: "3",
|
|
47
|
+
DEBUG: "1",
|
|
48
|
+
AAD_ADAPTIVE_EFFORT: "1",
|
|
49
|
+
AAD_USE_TEAMS_FOR_SPLITTER: "1",
|
|
50
|
+
AAD_USE_TEAMS_FOR_REVIEW: "1",
|
|
51
|
+
AAD_MEMORY_SYNC_ENABLED: "0",
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
expect(config.workers.num).toBe(4);
|
|
55
|
+
expect(config.workers.max).toBe(16);
|
|
56
|
+
expect(config.models.default).toBe("opus");
|
|
57
|
+
expect(config.models.splitter).toBe("sonnet");
|
|
58
|
+
expect(config.models.tester).toBe("sonnet");
|
|
59
|
+
expect(config.models.implementer).toBe("sonnet");
|
|
60
|
+
expect(config.models.reviewer).toBe("opus");
|
|
61
|
+
expect(config.models.mergeResolver).toBe("haiku");
|
|
62
|
+
expect(config.models.branchName).toBe("haiku");
|
|
63
|
+
expect(config.timeouts.claude).toBe(1800);
|
|
64
|
+
expect(config.timeouts.test).toBe(900);
|
|
65
|
+
expect(config.timeouts.staleTask).toBe(7200);
|
|
66
|
+
expect(config.retry.maxRetries).toBe(3);
|
|
67
|
+
expect(config.debug).toBe(true);
|
|
68
|
+
expect(config.adaptiveEffort).toBe(true);
|
|
69
|
+
expect(config.teams.splitter).toBe(true);
|
|
70
|
+
expect(config.teams.reviewer).toBe(true);
|
|
71
|
+
expect(config.memorySync).toBe(false);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("throws ConfigError when AAD_NUM_WORKERS exceeds AAD_MAX_WORKERS", () => {
|
|
75
|
+
expect(() => {
|
|
76
|
+
loadConfig({
|
|
77
|
+
AAD_NUM_WORKERS: "10",
|
|
78
|
+
AAD_MAX_WORKERS: "8",
|
|
79
|
+
});
|
|
80
|
+
}).toThrow(ConfigError);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("throws ConfigError for invalid AAD_NUM_WORKERS", () => {
|
|
84
|
+
expect(() => {
|
|
85
|
+
loadConfig({
|
|
86
|
+
AAD_NUM_WORKERS: "invalid",
|
|
87
|
+
});
|
|
88
|
+
}).toThrow(ConfigError);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("throws ConfigError for negative AAD_NUM_WORKERS", () => {
|
|
92
|
+
expect(() => {
|
|
93
|
+
loadConfig({
|
|
94
|
+
AAD_NUM_WORKERS: "-1",
|
|
95
|
+
});
|
|
96
|
+
}).toThrow(ConfigError);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("throws ConfigError for invalid CLAUDE_TIMEOUT", () => {
|
|
100
|
+
expect(() => {
|
|
101
|
+
loadConfig({
|
|
102
|
+
CLAUDE_TIMEOUT: "not-a-number",
|
|
103
|
+
});
|
|
104
|
+
}).toThrow(ConfigError);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("uses process.env when no env parameter provided", () => {
|
|
108
|
+
const originalNumWorkers = process.env.AAD_NUM_WORKERS;
|
|
109
|
+
const originalMaxWorkers = process.env.AAD_MAX_WORKERS;
|
|
110
|
+
process.env.AAD_NUM_WORKERS = "3";
|
|
111
|
+
process.env.AAD_MAX_WORKERS = "8";
|
|
112
|
+
|
|
113
|
+
const config = loadConfig();
|
|
114
|
+
expect(config.workers.num).toBe(3);
|
|
115
|
+
|
|
116
|
+
if (originalNumWorkers !== undefined) {
|
|
117
|
+
process.env.AAD_NUM_WORKERS = originalNumWorkers;
|
|
118
|
+
} else {
|
|
119
|
+
delete process.env.AAD_NUM_WORKERS;
|
|
120
|
+
}
|
|
121
|
+
if (originalMaxWorkers !== undefined) {
|
|
122
|
+
process.env.AAD_MAX_WORKERS = originalMaxWorkers;
|
|
123
|
+
} else {
|
|
124
|
+
delete process.env.AAD_MAX_WORKERS;
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("accepts valid model names", () => {
|
|
129
|
+
const config = loadConfig({
|
|
130
|
+
AAD_MODEL: "claude-sonnet-4-5-20250929",
|
|
131
|
+
AAD_MODEL_SPLITTER: "claude-opus-4-6",
|
|
132
|
+
AAD_MODEL_MERGE: "claude-haiku-4-5-20251001",
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
expect(config.models.default).toBe("claude-sonnet-4-5-20250929");
|
|
136
|
+
expect(config.models.splitter).toBe("claude-opus-4-6");
|
|
137
|
+
expect(config.models.mergeResolver).toBe("claude-haiku-4-5-20251001");
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("accepts short model names", () => {
|
|
141
|
+
const config = loadConfig({
|
|
142
|
+
AAD_MODEL: "sonnet",
|
|
143
|
+
AAD_MODEL_SPLITTER: "opus",
|
|
144
|
+
AAD_MODEL_MERGE: "haiku",
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
expect(config.models.default).toBe("sonnet");
|
|
148
|
+
expect(config.models.splitter).toBe("opus");
|
|
149
|
+
expect(config.models.mergeResolver).toBe("haiku");
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("loads dashboard config from env vars", () => {
|
|
153
|
+
const config = loadConfig({
|
|
154
|
+
AAD_DASHBOARD_PORT: "4444",
|
|
155
|
+
AAD_DASHBOARD_ENABLED: "0",
|
|
156
|
+
AAD_DASHBOARD_HOST: "0.0.0.0",
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
expect(config.dashboard.port).toBe(4444);
|
|
160
|
+
expect(config.dashboard.enabled).toBe(false);
|
|
161
|
+
expect(config.dashboard.host).toBe("0.0.0.0");
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("uses dashboard defaults when env vars not set", () => {
|
|
165
|
+
const config = loadConfig({});
|
|
166
|
+
|
|
167
|
+
expect(config.dashboard.enabled).toBe(true);
|
|
168
|
+
expect(config.dashboard.port).toBe(7333);
|
|
169
|
+
expect(config.dashboard.host).toBe("localhost");
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("loads repos from AAD_REPOS env var", () => {
|
|
173
|
+
const config = loadConfig({ AAD_REPOS: "/tmp/repo-a, /tmp/repo-b" });
|
|
174
|
+
|
|
175
|
+
expect(config.repos).toHaveLength(2);
|
|
176
|
+
expect(config.repos![0]!.path).toBe("/tmp/repo-a");
|
|
177
|
+
expect(config.repos![1]!.path).toBe("/tmp/repo-b");
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test("repos is undefined when AAD_REPOS not set", () => {
|
|
181
|
+
const config = loadConfig({});
|
|
182
|
+
expect(config.repos).toBeUndefined();
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test("loads multiRepoStrategy from env", () => {
|
|
186
|
+
const config = loadConfig({ AAD_MULTI_REPO_STRATEGY: "coordinated" });
|
|
187
|
+
expect(config.multiRepoStrategy).toBe("coordinated");
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test("multiRepoStrategy is undefined when not set", () => {
|
|
191
|
+
const config = loadConfig({});
|
|
192
|
+
expect(config.multiRepoStrategy).toBeUndefined();
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test("loads plugins from AAD_PLUGINS env var", () => {
|
|
196
|
+
const config = loadConfig({ AAD_PLUGINS: "./my-plugin.ts, /abs/plugin.ts" });
|
|
197
|
+
expect(config.plugins).toEqual(["./my-plugin.ts", "/abs/plugin.ts"]);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test("plugins is undefined when AAD_PLUGINS not set", () => {
|
|
201
|
+
const config = loadConfig({});
|
|
202
|
+
expect(config.plugins).toBeUndefined();
|
|
203
|
+
});
|
|
204
|
+
});
|