@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,251 @@
|
|
|
1
|
+
import { test, expect, describe, beforeEach } from "bun:test";
|
|
2
|
+
import type pino from "pino";
|
|
3
|
+
import type { ClaudeProvider } from "../claude-provider.port";
|
|
4
|
+
import type { PhaseName } from "../../../shared/types";
|
|
5
|
+
import type { ProviderType } from "../index";
|
|
6
|
+
import { ProviderRegistry, createProviderRegistry, type ProviderConfig } from "../provider-registry";
|
|
7
|
+
import type { Config } from "../../../shared/config";
|
|
8
|
+
|
|
9
|
+
// Mock provider implementation
|
|
10
|
+
class MockClaudeProvider implements ClaudeProvider {
|
|
11
|
+
constructor(public readonly name: string) {}
|
|
12
|
+
|
|
13
|
+
async call() {
|
|
14
|
+
return {
|
|
15
|
+
result: `Mock response from ${this.name}`,
|
|
16
|
+
exitCode: 0,
|
|
17
|
+
model: "mock-model",
|
|
18
|
+
effortLevel: "medium" as const,
|
|
19
|
+
duration: 100,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Mock logger
|
|
25
|
+
const mockLogger: pino.Logger = {
|
|
26
|
+
debug: () => {},
|
|
27
|
+
info: () => {},
|
|
28
|
+
warn: () => {},
|
|
29
|
+
error: () => {},
|
|
30
|
+
} as unknown as pino.Logger;
|
|
31
|
+
|
|
32
|
+
// Mock app config
|
|
33
|
+
const mockAppConfig: Config = {
|
|
34
|
+
workers: {
|
|
35
|
+
num: 1,
|
|
36
|
+
max: 4,
|
|
37
|
+
},
|
|
38
|
+
models: {
|
|
39
|
+
default: "claude-sonnet-4",
|
|
40
|
+
},
|
|
41
|
+
timeouts: {
|
|
42
|
+
claude: 1200,
|
|
43
|
+
test: 600,
|
|
44
|
+
staleTask: 5400,
|
|
45
|
+
},
|
|
46
|
+
retry: {
|
|
47
|
+
maxRetries: 2,
|
|
48
|
+
},
|
|
49
|
+
debug: false,
|
|
50
|
+
adaptiveEffort: false,
|
|
51
|
+
teams: {
|
|
52
|
+
splitter: false,
|
|
53
|
+
reviewer: false,
|
|
54
|
+
},
|
|
55
|
+
memorySync: false,
|
|
56
|
+
dashboard: {
|
|
57
|
+
enabled: false,
|
|
58
|
+
port: 7333,
|
|
59
|
+
host: "localhost",
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
describe("ProviderRegistry", () => {
|
|
64
|
+
let cliProvider: ClaudeProvider;
|
|
65
|
+
let sdkProvider: ClaudeProvider;
|
|
66
|
+
let providersMap: Map<ProviderType, ClaudeProvider>;
|
|
67
|
+
|
|
68
|
+
beforeEach(() => {
|
|
69
|
+
cliProvider = new MockClaudeProvider("CLI");
|
|
70
|
+
sdkProvider = new MockClaudeProvider("SDK");
|
|
71
|
+
providersMap = new Map([
|
|
72
|
+
["cli", cliProvider],
|
|
73
|
+
["sdk", sdkProvider],
|
|
74
|
+
]);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe("constructor", () => {
|
|
78
|
+
test("should accept valid config with default provider", () => {
|
|
79
|
+
const config: ProviderConfig = { default: "cli" };
|
|
80
|
+
const registry = new ProviderRegistry(providersMap, config, mockLogger);
|
|
81
|
+
expect(registry).toBeDefined();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("should throw error if default provider not in map", () => {
|
|
85
|
+
const config: ProviderConfig = { default: "invalid" as ProviderType };
|
|
86
|
+
expect(() => {
|
|
87
|
+
new ProviderRegistry(providersMap, config, mockLogger);
|
|
88
|
+
}).toThrow("Default provider 'invalid' not found in providers map");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("should throw error if override provider not in map", () => {
|
|
92
|
+
const config: ProviderConfig = {
|
|
93
|
+
default: "cli",
|
|
94
|
+
overrides: {
|
|
95
|
+
tester: "invalid" as ProviderType,
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
expect(() => {
|
|
99
|
+
new ProviderRegistry(providersMap, config, mockLogger);
|
|
100
|
+
}).toThrow("Override provider 'invalid' for phase 'tester' not found in providers map");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("should accept valid config with overrides", () => {
|
|
104
|
+
const config: ProviderConfig = {
|
|
105
|
+
default: "cli",
|
|
106
|
+
overrides: {
|
|
107
|
+
reviewer: "sdk",
|
|
108
|
+
splitter: "sdk",
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
const registry = new ProviderRegistry(providersMap, config, mockLogger);
|
|
112
|
+
expect(registry).toBeDefined();
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe("getProviderType", () => {
|
|
117
|
+
test("should return default provider type when phase is not specified", () => {
|
|
118
|
+
const config: ProviderConfig = { default: "cli" };
|
|
119
|
+
const registry = new ProviderRegistry(providersMap, config, mockLogger);
|
|
120
|
+
|
|
121
|
+
expect(registry.getProviderType()).toBe("cli");
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("should return default provider type for phase without override", () => {
|
|
125
|
+
const config: ProviderConfig = {
|
|
126
|
+
default: "cli",
|
|
127
|
+
overrides: { reviewer: "sdk" },
|
|
128
|
+
};
|
|
129
|
+
const registry = new ProviderRegistry(providersMap, config, mockLogger);
|
|
130
|
+
|
|
131
|
+
expect(registry.getProviderType("tester")).toBe("cli");
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("should return override provider type for configured phase", () => {
|
|
135
|
+
const config: ProviderConfig = {
|
|
136
|
+
default: "cli",
|
|
137
|
+
overrides: {
|
|
138
|
+
reviewer: "sdk",
|
|
139
|
+
splitter: "sdk",
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
const registry = new ProviderRegistry(providersMap, config, mockLogger);
|
|
143
|
+
|
|
144
|
+
expect(registry.getProviderType("reviewer")).toBe("sdk");
|
|
145
|
+
expect(registry.getProviderType("splitter")).toBe("sdk");
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("should handle all phase names correctly", () => {
|
|
149
|
+
const config: ProviderConfig = {
|
|
150
|
+
default: "cli",
|
|
151
|
+
overrides: {
|
|
152
|
+
splitter: "sdk",
|
|
153
|
+
tester: "cli",
|
|
154
|
+
implementer: "cli",
|
|
155
|
+
reviewer: "sdk",
|
|
156
|
+
"merge-resolver": "cli",
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
const registry = new ProviderRegistry(providersMap, config, mockLogger);
|
|
160
|
+
|
|
161
|
+
const phases: PhaseName[] = ["splitter", "tester", "implementer", "reviewer", "merge-resolver"];
|
|
162
|
+
|
|
163
|
+
for (const phase of phases) {
|
|
164
|
+
const expected = config.overrides![phase]!;
|
|
165
|
+
expect(registry.getProviderType(phase)).toBe(expected);
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
describe("getProvider", () => {
|
|
171
|
+
test("should return default provider when phase is not specified", () => {
|
|
172
|
+
const config: ProviderConfig = { default: "cli" };
|
|
173
|
+
const registry = new ProviderRegistry(providersMap, config, mockLogger);
|
|
174
|
+
|
|
175
|
+
expect(registry.getProvider()).toBe(cliProvider);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("should return default provider for phase without override", () => {
|
|
179
|
+
const config: ProviderConfig = {
|
|
180
|
+
default: "cli",
|
|
181
|
+
overrides: { reviewer: "sdk" },
|
|
182
|
+
};
|
|
183
|
+
const registry = new ProviderRegistry(providersMap, config, mockLogger);
|
|
184
|
+
|
|
185
|
+
expect(registry.getProvider("tester")).toBe(cliProvider);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test("should return override provider for configured phase", () => {
|
|
189
|
+
const config: ProviderConfig = {
|
|
190
|
+
default: "cli",
|
|
191
|
+
overrides: {
|
|
192
|
+
reviewer: "sdk",
|
|
193
|
+
splitter: "sdk",
|
|
194
|
+
},
|
|
195
|
+
};
|
|
196
|
+
const registry = new ProviderRegistry(providersMap, config, mockLogger);
|
|
197
|
+
|
|
198
|
+
expect(registry.getProvider("reviewer")).toBe(sdkProvider);
|
|
199
|
+
expect(registry.getProvider("splitter")).toBe(sdkProvider);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test("should fallback to default for unspecified phases", () => {
|
|
203
|
+
const config: ProviderConfig = {
|
|
204
|
+
default: "sdk",
|
|
205
|
+
overrides: {
|
|
206
|
+
tester: "cli",
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
const registry = new ProviderRegistry(providersMap, config, mockLogger);
|
|
210
|
+
|
|
211
|
+
expect(registry.getProvider("reviewer")).toBe(sdkProvider);
|
|
212
|
+
expect(registry.getProvider("tester")).toBe(cliProvider);
|
|
213
|
+
expect(registry.getProvider("implementer")).toBe(sdkProvider);
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
describe("createProviderRegistry", () => {
|
|
218
|
+
test("should create registry with default provider only", () => {
|
|
219
|
+
const config: ProviderConfig = { default: "cli" };
|
|
220
|
+
const registry = createProviderRegistry(config, mockAppConfig, mockLogger);
|
|
221
|
+
|
|
222
|
+
expect(registry).toBeInstanceOf(ProviderRegistry);
|
|
223
|
+
expect(registry.getProviderType()).toBe("cli");
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test("should create registry with default and override providers", () => {
|
|
227
|
+
const config: ProviderConfig = {
|
|
228
|
+
default: "cli",
|
|
229
|
+
overrides: {
|
|
230
|
+
reviewer: "sdk",
|
|
231
|
+
splitter: "sdk",
|
|
232
|
+
},
|
|
233
|
+
};
|
|
234
|
+
const registry = createProviderRegistry(config, mockAppConfig, mockLogger);
|
|
235
|
+
|
|
236
|
+
expect(registry).toBeInstanceOf(ProviderRegistry);
|
|
237
|
+
expect(registry.getProviderType()).toBe("cli");
|
|
238
|
+
expect(registry.getProviderType("reviewer")).toBe("sdk");
|
|
239
|
+
expect(registry.getProviderType("splitter")).toBe("sdk");
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test("should create minimal set of providers based on config", () => {
|
|
243
|
+
const config: ProviderConfig = { default: "cli" };
|
|
244
|
+
const registry = createProviderRegistry(config, mockAppConfig, mockLogger);
|
|
245
|
+
|
|
246
|
+
// Should only have CLI provider
|
|
247
|
+
expect(registry.getProvider()).toBeDefined();
|
|
248
|
+
expect(registry.getProviderType()).toBe("cli");
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
});
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { withRetry } from "../retry";
|
|
3
|
+
import type { EffortLevel } from "../../../shared/types";
|
|
4
|
+
import { ClaudeProviderError, TimeoutError } from "../../../shared/errors";
|
|
5
|
+
|
|
6
|
+
describe("withRetry", () => {
|
|
7
|
+
test("returns success on first attempt", async () => {
|
|
8
|
+
const fn = async () => ({ success: true, data: "result" });
|
|
9
|
+
|
|
10
|
+
const result = await withRetry(fn, {
|
|
11
|
+
maxRetries: 2,
|
|
12
|
+
initialEffort: "medium",
|
|
13
|
+
backoffMs: 10,
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
expect(result.success).toBe(true);
|
|
17
|
+
expect(result.attempts).toBe(1);
|
|
18
|
+
expect(result.finalEffort).toBe("medium");
|
|
19
|
+
if (result.success) {
|
|
20
|
+
expect(result.data.data).toBe("result");
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("retries on failure and succeeds", async () => {
|
|
25
|
+
let attemptCount = 0;
|
|
26
|
+
|
|
27
|
+
const fn = async () => {
|
|
28
|
+
attemptCount++;
|
|
29
|
+
if (attemptCount < 2) {
|
|
30
|
+
throw new ClaudeProviderError("Temporary failure");
|
|
31
|
+
}
|
|
32
|
+
return { success: true, data: "result" };
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const result = await withRetry(fn, {
|
|
36
|
+
maxRetries: 2,
|
|
37
|
+
initialEffort: "medium",
|
|
38
|
+
backoffMs: 10,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
expect(result.success).toBe(true);
|
|
42
|
+
expect(result.attempts).toBe(2);
|
|
43
|
+
expect(attemptCount).toBe(2);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("escalates effort level on first failure (low -> medium)", async () => {
|
|
47
|
+
let attemptCount = 0;
|
|
48
|
+
let lastEffort: EffortLevel = "low";
|
|
49
|
+
|
|
50
|
+
const fn = async (effort: EffortLevel) => {
|
|
51
|
+
attemptCount++;
|
|
52
|
+
lastEffort = effort;
|
|
53
|
+
if (attemptCount < 2) {
|
|
54
|
+
throw new ClaudeProviderError("Temporary failure");
|
|
55
|
+
}
|
|
56
|
+
return { success: true, data: "result" };
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const result = await withRetry(fn, {
|
|
60
|
+
maxRetries: 2,
|
|
61
|
+
initialEffort: "low",
|
|
62
|
+
backoffMs: 10,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
expect(result.success).toBe(true);
|
|
66
|
+
expect(result.attempts).toBe(2);
|
|
67
|
+
expect(result.finalEffort).toBe("medium");
|
|
68
|
+
expect(lastEffort as string).toBe("medium");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("escalates effort level on second failure (medium -> high)", async () => {
|
|
72
|
+
let attemptCount = 0;
|
|
73
|
+
let lastEffort: EffortLevel = "medium";
|
|
74
|
+
|
|
75
|
+
const fn = async (effort: EffortLevel) => {
|
|
76
|
+
attemptCount++;
|
|
77
|
+
lastEffort = effort;
|
|
78
|
+
if (attemptCount < 3) {
|
|
79
|
+
throw new ClaudeProviderError("Temporary failure");
|
|
80
|
+
}
|
|
81
|
+
return { success: true, data: "result" };
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const result = await withRetry(fn, {
|
|
85
|
+
maxRetries: 3,
|
|
86
|
+
initialEffort: "medium",
|
|
87
|
+
backoffMs: 10,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
expect(result.success).toBe(true);
|
|
91
|
+
expect(result.attempts).toBe(3);
|
|
92
|
+
expect(result.finalEffort).toBe("high");
|
|
93
|
+
expect(lastEffort as string).toBe("high");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("does not escalate beyond high", async () => {
|
|
97
|
+
let attemptCount = 0;
|
|
98
|
+
let lastEffort: EffortLevel = "high";
|
|
99
|
+
|
|
100
|
+
const fn = async (effort: EffortLevel) => {
|
|
101
|
+
attemptCount++;
|
|
102
|
+
lastEffort = effort;
|
|
103
|
+
if (attemptCount < 3) {
|
|
104
|
+
throw new ClaudeProviderError("Temporary failure");
|
|
105
|
+
}
|
|
106
|
+
return { success: true, data: "result" };
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const result = await withRetry(fn, {
|
|
110
|
+
maxRetries: 3,
|
|
111
|
+
initialEffort: "high",
|
|
112
|
+
backoffMs: 10,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
expect(result.success).toBe(true);
|
|
116
|
+
expect(result.finalEffort).toBe("high");
|
|
117
|
+
expect(lastEffort).toBe("high");
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("returns failure after exhausting retries", async () => {
|
|
121
|
+
const fn = async () => {
|
|
122
|
+
throw new ClaudeProviderError("Persistent failure");
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const result = await withRetry(fn, {
|
|
126
|
+
maxRetries: 2,
|
|
127
|
+
initialEffort: "medium",
|
|
128
|
+
backoffMs: 10,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
expect(result.success).toBe(false);
|
|
132
|
+
expect(result.attempts).toBe(3); // initial + 2 retries
|
|
133
|
+
if (!result.success) {
|
|
134
|
+
expect(result.error).toBeInstanceOf(ClaudeProviderError);
|
|
135
|
+
expect(result.error.message).toContain("Persistent failure");
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("applies exponential backoff between retries", async () => {
|
|
140
|
+
const timestamps: number[] = [];
|
|
141
|
+
|
|
142
|
+
const fn = async () => {
|
|
143
|
+
timestamps.push(Date.now());
|
|
144
|
+
throw new ClaudeProviderError("Failure");
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
await withRetry(fn, {
|
|
148
|
+
maxRetries: 2,
|
|
149
|
+
initialEffort: "medium",
|
|
150
|
+
backoffMs: 100, // 100ms base for testing
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
expect(timestamps).toHaveLength(3);
|
|
154
|
+
|
|
155
|
+
// 1回目と2回目の間隔 (attempt 1 * 100ms)
|
|
156
|
+
const delay1 = timestamps[1]! - timestamps[0]!;
|
|
157
|
+
expect(delay1).toBeGreaterThanOrEqual(90); // 多少の誤差を許容
|
|
158
|
+
|
|
159
|
+
// 2回目と3回目の間隔 (attempt 2 * 100ms)
|
|
160
|
+
const delay2 = timestamps[2]! - timestamps[1]!;
|
|
161
|
+
expect(delay2).toBeGreaterThanOrEqual(180); // 多少の誤差を許容
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("handles TimeoutError specifically", async () => {
|
|
165
|
+
const fn = async () => {
|
|
166
|
+
throw new TimeoutError("Operation timed out", { timeout: 30000 });
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const result = await withRetry(fn, {
|
|
170
|
+
maxRetries: 1,
|
|
171
|
+
initialEffort: "medium",
|
|
172
|
+
backoffMs: 10,
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
expect(result.success).toBe(false);
|
|
176
|
+
if (!result.success) {
|
|
177
|
+
expect(result.error).toBeInstanceOf(TimeoutError);
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test("preserves error context through retries", async () => {
|
|
182
|
+
const fn = async () => {
|
|
183
|
+
throw new ClaudeProviderError("Error with context", {
|
|
184
|
+
phase: "tester",
|
|
185
|
+
taskId: "t1",
|
|
186
|
+
});
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const result = await withRetry(fn, {
|
|
190
|
+
maxRetries: 1,
|
|
191
|
+
initialEffort: "medium",
|
|
192
|
+
backoffMs: 10,
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
expect(result.success).toBe(false);
|
|
196
|
+
if (!result.success) {
|
|
197
|
+
expect(result.error.context.phase).toBe("tester");
|
|
198
|
+
expect(result.error.context.taskId).toBe("t1");
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
});
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import type { ClaudeProvider, ClaudeRequest, ClaudeResponse } from "./claude-provider.port";
|
|
2
|
+
import type { Config } from "../../shared/config";
|
|
3
|
+
import type pino from "pino";
|
|
4
|
+
import { ClaudeProviderError, TimeoutError } from "../../shared/errors";
|
|
5
|
+
import { withRetry } from "./retry";
|
|
6
|
+
import type { EffortLevel } from "../../shared/types";
|
|
7
|
+
|
|
8
|
+
export class ClaudeCliAdapter implements ClaudeProvider {
|
|
9
|
+
constructor(
|
|
10
|
+
private config: Config,
|
|
11
|
+
private logger: pino.Logger,
|
|
12
|
+
private backoffMs: number = 5000
|
|
13
|
+
) {}
|
|
14
|
+
|
|
15
|
+
async call(request: ClaudeRequest): Promise<ClaudeResponse> {
|
|
16
|
+
const startTime = Date.now();
|
|
17
|
+
|
|
18
|
+
// Effort level決定
|
|
19
|
+
const initialEffort = request.effortLevel ?? "medium";
|
|
20
|
+
|
|
21
|
+
// Retry logic適用
|
|
22
|
+
const result = await withRetry(
|
|
23
|
+
async (effort: EffortLevel) => this.executeCli(request, effort),
|
|
24
|
+
{
|
|
25
|
+
maxRetries: request.maxRetries ?? this.config.retry.maxRetries,
|
|
26
|
+
initialEffort,
|
|
27
|
+
backoffMs: this.backoffMs,
|
|
28
|
+
}
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
if (!result.success) {
|
|
32
|
+
throw result.error;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const duration = Date.now() - startTime;
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
result: result.data.stdout,
|
|
39
|
+
exitCode: result.data.exitCode,
|
|
40
|
+
model: request.model ?? this.config.models.default ?? "sonnet",
|
|
41
|
+
effortLevel: result.finalEffort,
|
|
42
|
+
duration,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private async executeCli(
|
|
47
|
+
request: ClaudeRequest,
|
|
48
|
+
effort: EffortLevel
|
|
49
|
+
): Promise<{ stdout: string; exitCode: number }> {
|
|
50
|
+
const args = this.buildCliArgs(request, effort);
|
|
51
|
+
|
|
52
|
+
this.logger.debug({ args }, "Executing Claude CLI");
|
|
53
|
+
|
|
54
|
+
const timeout = request.timeout ?? this.config.timeouts.claude;
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
const proc = Bun.spawn(args, {
|
|
58
|
+
cwd: request.cwd,
|
|
59
|
+
env: {
|
|
60
|
+
...process.env,
|
|
61
|
+
CLAUDE_CODE_EFFORT_LEVEL: effort,
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// タイムアウト処理
|
|
66
|
+
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
67
|
+
setTimeout(() => {
|
|
68
|
+
proc.kill();
|
|
69
|
+
reject(
|
|
70
|
+
new TimeoutError(`Claude CLI timed out after ${timeout}s`, {
|
|
71
|
+
timeout,
|
|
72
|
+
})
|
|
73
|
+
);
|
|
74
|
+
}, timeout * 1000);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const exitedPromise = proc.exited;
|
|
78
|
+
|
|
79
|
+
await Promise.race([exitedPromise, timeoutPromise]);
|
|
80
|
+
|
|
81
|
+
const stdout = await (proc.stdout as { text: () => Promise<string> }).text();
|
|
82
|
+
const stderr = proc.stderr ? await (proc.stderr as { text: () => Promise<string> }).text() : "";
|
|
83
|
+
const exited = await proc.exited;
|
|
84
|
+
const exitCode = typeof exited === "number" ? exited : (exited as { exitCode: number }).exitCode;
|
|
85
|
+
|
|
86
|
+
if (exitCode !== 0) {
|
|
87
|
+
throw new ClaudeProviderError(
|
|
88
|
+
`Claude CLI failed with exit code ${exitCode}`,
|
|
89
|
+
{
|
|
90
|
+
exitCode,
|
|
91
|
+
stdout,
|
|
92
|
+
stderr,
|
|
93
|
+
effort,
|
|
94
|
+
}
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return { stdout, exitCode };
|
|
99
|
+
} catch (error) {
|
|
100
|
+
if (error instanceof TimeoutError || error instanceof ClaudeProviderError) {
|
|
101
|
+
throw error;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
throw new ClaudeProviderError(
|
|
105
|
+
`Failed to execute Claude CLI: ${error instanceof Error ? error.message : String(error)}`,
|
|
106
|
+
{ originalError: error }
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private buildCliArgs(request: ClaudeRequest, _effort: EffortLevel): string[] {
|
|
112
|
+
const args: string[] = ["claude", "-p"]; // print mode (非対話)
|
|
113
|
+
|
|
114
|
+
// System prompt
|
|
115
|
+
if (request.systemPrompt || request.appendSystemPrompt) {
|
|
116
|
+
args.push("--append-system-prompt", request.systemPrompt ?? request.appendSystemPrompt!);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Allowed tools
|
|
120
|
+
if (request.allowedTools && request.allowedTools.length > 0) {
|
|
121
|
+
args.push("--allowed-tools", request.allowedTools.join(","));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Output format
|
|
125
|
+
if (request.outputFormat) {
|
|
126
|
+
args.push("--output-format", request.outputFormat);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Permission mode
|
|
130
|
+
if (request.permissionMode) {
|
|
131
|
+
args.push("--permission-mode", request.permissionMode);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Model
|
|
135
|
+
if (request.model) {
|
|
136
|
+
args.push("--model", request.model);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Resume
|
|
140
|
+
if (request.resume) {
|
|
141
|
+
args.push("--resume");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Prompt(subagentsがある場合はプロンプトに注入)
|
|
145
|
+
let prompt = request.prompt;
|
|
146
|
+
if (request.subagents && request.subagents.length > 0) {
|
|
147
|
+
const subagentDescriptions = request.subagents
|
|
148
|
+
.map(sa => `- ${sa.name}: ${sa.prompt}`)
|
|
149
|
+
.join("\n");
|
|
150
|
+
prompt = `${prompt}\n\n## Available Subagents (simulate as internal analysis threads)\n${subagentDescriptions}\n\nPlease coordinate with these roles internally and integrate their perspectives into your response.`;
|
|
151
|
+
}
|
|
152
|
+
args.push(prompt);
|
|
153
|
+
|
|
154
|
+
return args;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { EffortLevel } from "../../shared/types";
|
|
2
|
+
|
|
3
|
+
export interface SubagentConfig {
|
|
4
|
+
name: string;
|
|
5
|
+
prompt: string;
|
|
6
|
+
model?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface ClaudeRequest {
|
|
10
|
+
prompt: string;
|
|
11
|
+
systemPrompt?: string;
|
|
12
|
+
model?: string;
|
|
13
|
+
effortLevel?: EffortLevel;
|
|
14
|
+
timeout?: number;
|
|
15
|
+
maxRetries?: number;
|
|
16
|
+
allowedTools?: string[];
|
|
17
|
+
outputFormat?: "text" | "json";
|
|
18
|
+
permissionMode?: "acceptEdits" | "bypassPermissions" | "default" | "delegate" | "dontAsk" | "plan";
|
|
19
|
+
cwd?: string;
|
|
20
|
+
appendSystemPrompt?: string;
|
|
21
|
+
resume?: string;
|
|
22
|
+
subagents?: SubagentConfig[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface ClaudeResponse {
|
|
26
|
+
result: string;
|
|
27
|
+
exitCode: number;
|
|
28
|
+
model: string;
|
|
29
|
+
effortLevel: EffortLevel;
|
|
30
|
+
duration: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ClaudeProvider {
|
|
34
|
+
call(request: ClaudeRequest): Promise<ClaudeResponse>;
|
|
35
|
+
}
|