@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,55 @@
|
|
|
1
|
+
import pino from "pino";
|
|
2
|
+
|
|
3
|
+
export interface LoggerOptions {
|
|
4
|
+
service: string;
|
|
5
|
+
debug?: boolean;
|
|
6
|
+
logFilePath?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function createLogger(options: LoggerOptions): pino.Logger {
|
|
10
|
+
const { service, debug = false, logFilePath } = options;
|
|
11
|
+
|
|
12
|
+
const targets: pino.TransportTargetOptions[] = [];
|
|
13
|
+
|
|
14
|
+
// Debug mode: pino-pretty for console
|
|
15
|
+
if (debug) {
|
|
16
|
+
targets.push({
|
|
17
|
+
target: "pino-pretty",
|
|
18
|
+
level: "debug",
|
|
19
|
+
options: {
|
|
20
|
+
colorize: true,
|
|
21
|
+
translateTime: "HH:MM:ss",
|
|
22
|
+
ignore: "pid,hostname",
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Log file: JSON Lines format
|
|
28
|
+
if (logFilePath) {
|
|
29
|
+
targets.push({
|
|
30
|
+
target: "pino/file",
|
|
31
|
+
level: "info",
|
|
32
|
+
options: {
|
|
33
|
+
destination: logFilePath,
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// If no targets, use default console (warn level, stderr)
|
|
39
|
+
if (targets.length === 0) {
|
|
40
|
+
targets.push({
|
|
41
|
+
target: "pino/file",
|
|
42
|
+
level: "warn",
|
|
43
|
+
options: {
|
|
44
|
+
destination: 2, // stderr
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const logger = pino({
|
|
50
|
+
level: debug ? "debug" : "warn",
|
|
51
|
+
transport: targets.length === 1 ? targets[0] : { targets },
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
return logger.child({ service });
|
|
55
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { LogStore, LogEntry } from "../log-store";
|
|
2
|
+
import type { EventBus } from "../../../shared/events";
|
|
3
|
+
|
|
4
|
+
export interface SSETransportOptions {
|
|
5
|
+
logStore: LogStore;
|
|
6
|
+
eventBus: EventBus;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface SSETransport {
|
|
10
|
+
write(entry: LogEntry): void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function createSSETransport(options: SSETransportOptions): SSETransport {
|
|
14
|
+
const { logStore, eventBus } = options;
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
write(entry: LogEntry): void {
|
|
18
|
+
// LogStoreに追加(リングバッファ永続化)
|
|
19
|
+
logStore.add(entry);
|
|
20
|
+
|
|
21
|
+
// EventBus経由でSSE配信用にブロードキャスト
|
|
22
|
+
eventBus.emit({
|
|
23
|
+
type: "log:entry",
|
|
24
|
+
entry,
|
|
25
|
+
});
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { MultiRepoPlanner } from "../multi-repo-planner";
|
|
3
|
+
import type { RepoContext } from "../repo-context";
|
|
4
|
+
import { createRunId, createTaskId, createRepoName, type TaskPlan } from "../../../shared/types";
|
|
5
|
+
import type pino from "pino";
|
|
6
|
+
|
|
7
|
+
function createMockLogger(): pino.Logger {
|
|
8
|
+
return { info: () => {}, warn: () => {}, error: () => {}, debug: () => {} } as unknown as pino.Logger;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function createMockPlanningService(plan: TaskPlan) {
|
|
12
|
+
return {
|
|
13
|
+
planTasks: async () => plan,
|
|
14
|
+
} as any;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const mockRunId = createRunId("test-run-001");
|
|
18
|
+
|
|
19
|
+
function makePlan(tasks: { id: string; deps?: string[] }[]): TaskPlan {
|
|
20
|
+
return {
|
|
21
|
+
runId: mockRunId,
|
|
22
|
+
parentBranch: "main",
|
|
23
|
+
tasks: tasks.map((t) => ({
|
|
24
|
+
taskId: createTaskId(t.id),
|
|
25
|
+
title: `Task ${t.id}`,
|
|
26
|
+
description: "",
|
|
27
|
+
filesToModify: [],
|
|
28
|
+
dependsOn: (t.deps ?? []).map(createTaskId),
|
|
29
|
+
priority: 1,
|
|
30
|
+
status: "pending" as const,
|
|
31
|
+
retryCount: 0,
|
|
32
|
+
})),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function makeRepo(name: string): RepoContext {
|
|
37
|
+
return {
|
|
38
|
+
name: createRepoName(name),
|
|
39
|
+
path: `/tmp/${name}`,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
describe("MultiRepoPlanner", () => {
|
|
44
|
+
test("independent strategy prefixes task IDs with repo name", async () => {
|
|
45
|
+
const plan = makePlan([{ id: "task-1" }, { id: "task-2", deps: ["task-1"] }]);
|
|
46
|
+
const planner = new MultiRepoPlanner({
|
|
47
|
+
planningService: createMockPlanningService(plan),
|
|
48
|
+
logger: createMockLogger(),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const result = await planner.plan({
|
|
52
|
+
repos: [makeRepo("frontend"), makeRepo("backend")],
|
|
53
|
+
runId: mockRunId,
|
|
54
|
+
parentBranch: "main",
|
|
55
|
+
requirementsPath: "req.md",
|
|
56
|
+
targetDocsDir: "/tmp/docs",
|
|
57
|
+
strategy: "independent",
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
expect(result.strategy).toBe("independent");
|
|
61
|
+
expect(result.plans.size).toBe(2);
|
|
62
|
+
|
|
63
|
+
const frontendPlan = result.plans.get(createRepoName("frontend"))!;
|
|
64
|
+
expect(frontendPlan.tasks[0]!.taskId as string).toBe("frontend/task-1");
|
|
65
|
+
expect(frontendPlan.tasks[1]!.dependsOn[0] as string).toBe("frontend/task-1");
|
|
66
|
+
expect(frontendPlan.tasks[0]!.repoName as string).toBe("frontend");
|
|
67
|
+
|
|
68
|
+
const backendPlan = result.plans.get(createRepoName("backend"))!;
|
|
69
|
+
expect(backendPlan.tasks[0]!.taskId as string).toBe("backend/task-1");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("coordinated strategy produces single plan under primary repo", async () => {
|
|
73
|
+
const plan = makePlan([{ id: "task-1" }]);
|
|
74
|
+
const planner = new MultiRepoPlanner({
|
|
75
|
+
planningService: createMockPlanningService(plan),
|
|
76
|
+
logger: createMockLogger(),
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const result = await planner.plan({
|
|
80
|
+
repos: [makeRepo("primary"), makeRepo("secondary")],
|
|
81
|
+
runId: mockRunId,
|
|
82
|
+
parentBranch: "main",
|
|
83
|
+
requirementsPath: "req.md",
|
|
84
|
+
targetDocsDir: "/tmp/docs",
|
|
85
|
+
strategy: "coordinated",
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
expect(result.strategy).toBe("coordinated");
|
|
89
|
+
expect(result.plans.size).toBe(1);
|
|
90
|
+
expect(result.plans.has(createRepoName("primary"))).toBe(true);
|
|
91
|
+
expect(result.plans.get(createRepoName("primary"))!.tasks[0]!.repoName as string).toBe("primary");
|
|
92
|
+
});
|
|
93
|
+
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { createRepoContext, createRepoContexts } from "../repo-context";
|
|
3
|
+
import type { FileChecker } from "../../planning/project-detection";
|
|
4
|
+
|
|
5
|
+
function createMockFileChecker(existingPaths: Set<string>): FileChecker {
|
|
6
|
+
return {
|
|
7
|
+
async exists(path: string) {
|
|
8
|
+
return existingPaths.has(path);
|
|
9
|
+
},
|
|
10
|
+
async readText(_path: string) {
|
|
11
|
+
return "";
|
|
12
|
+
},
|
|
13
|
+
async glob(_pattern: string, _cwd: string) {
|
|
14
|
+
return [];
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe("createRepoContext", () => {
|
|
20
|
+
test("creates context for valid git repo", async () => {
|
|
21
|
+
const checker = createMockFileChecker(new Set(["/tmp/my-repo", "/tmp/my-repo/.git"]));
|
|
22
|
+
const ctx = await createRepoContext({ path: "/tmp/my-repo" }, checker);
|
|
23
|
+
|
|
24
|
+
expect(ctx.name as string).toBe("my-repo");
|
|
25
|
+
expect(ctx.path).toBe("/tmp/my-repo");
|
|
26
|
+
expect(ctx.ref).toBeUndefined();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("uses provided name over directory name", async () => {
|
|
30
|
+
const checker = createMockFileChecker(new Set(["/tmp/my-repo", "/tmp/my-repo/.git"]));
|
|
31
|
+
const ctx = await createRepoContext({ path: "/tmp/my-repo", name: "custom-name" }, checker);
|
|
32
|
+
|
|
33
|
+
expect(ctx.name as string).toBe("custom-name");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("includes ref when provided", async () => {
|
|
37
|
+
const checker = createMockFileChecker(new Set(["/tmp/my-repo", "/tmp/my-repo/.git"]));
|
|
38
|
+
const ctx = await createRepoContext({ path: "/tmp/my-repo", ref: "main" }, checker);
|
|
39
|
+
|
|
40
|
+
expect(ctx.ref).toBe("main");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("throws when path does not exist", async () => {
|
|
44
|
+
const checker = createMockFileChecker(new Set());
|
|
45
|
+
await expect(createRepoContext({ path: "/nonexistent" }, checker)).rejects.toThrow(
|
|
46
|
+
"does not exist",
|
|
47
|
+
);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("throws when path is not a git repo", async () => {
|
|
51
|
+
const checker = createMockFileChecker(new Set(["/tmp/not-git"]));
|
|
52
|
+
await expect(createRepoContext({ path: "/tmp/not-git" }, checker)).rejects.toThrow(
|
|
53
|
+
"not a git repository",
|
|
54
|
+
);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe("createRepoContexts", () => {
|
|
59
|
+
test("creates multiple contexts in parallel", async () => {
|
|
60
|
+
const checker = createMockFileChecker(
|
|
61
|
+
new Set(["/tmp/repo-a", "/tmp/repo-a/.git", "/tmp/repo-b", "/tmp/repo-b/.git"]),
|
|
62
|
+
);
|
|
63
|
+
const contexts = await createRepoContexts(
|
|
64
|
+
[{ path: "/tmp/repo-a" }, { path: "/tmp/repo-b" }],
|
|
65
|
+
checker,
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
expect(contexts).toHaveLength(2);
|
|
69
|
+
expect(contexts[0]!.name as string).toBe("repo-a");
|
|
70
|
+
expect(contexts[1]!.name as string).toBe("repo-b");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("rejects if any repo is invalid", async () => {
|
|
74
|
+
const checker = createMockFileChecker(new Set(["/tmp/repo-a", "/tmp/repo-a/.git"]));
|
|
75
|
+
await expect(
|
|
76
|
+
createRepoContexts([{ path: "/tmp/repo-a" }, { path: "/tmp/bad" }], checker),
|
|
77
|
+
).rejects.toThrow("does not exist");
|
|
78
|
+
});
|
|
79
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Multi-Repo Module - Barrel Export
|
|
2
|
+
|
|
3
|
+
export { createRepoContext, createRepoContexts } from "./repo-context";
|
|
4
|
+
export type { RepoContext, RepoContextInput } from "./repo-context";
|
|
5
|
+
|
|
6
|
+
export { MultiRepoPlanner } from "./multi-repo-planner";
|
|
7
|
+
export type {
|
|
8
|
+
MultiRepoStrategy,
|
|
9
|
+
MultiRepoPlan,
|
|
10
|
+
MultiRepoPlannerDeps,
|
|
11
|
+
MultiRepoPlanParams,
|
|
12
|
+
} from "./multi-repo-planner";
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// MultiRepoPlanner - orchestrates planning across multiple repositories
|
|
2
|
+
|
|
3
|
+
import type { RepoContext } from "./repo-context";
|
|
4
|
+
import type { PlanningService } from "../planning/planning.service";
|
|
5
|
+
import type { TaskPlan, RunId, RepoName } from "../../shared/types";
|
|
6
|
+
import { createTaskId } from "../../shared/types";
|
|
7
|
+
import type pino from "pino";
|
|
8
|
+
|
|
9
|
+
export type MultiRepoStrategy = "independent" | "coordinated";
|
|
10
|
+
|
|
11
|
+
export interface MultiRepoPlan {
|
|
12
|
+
strategy: MultiRepoStrategy;
|
|
13
|
+
plans: Map<RepoName, TaskPlan>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface MultiRepoPlannerDeps {
|
|
17
|
+
planningService: PlanningService;
|
|
18
|
+
logger: pino.Logger;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface MultiRepoPlanParams {
|
|
22
|
+
repos: RepoContext[];
|
|
23
|
+
runId: RunId;
|
|
24
|
+
parentBranch: string;
|
|
25
|
+
requirementsPath: string;
|
|
26
|
+
targetDocsDir: string;
|
|
27
|
+
strategy: MultiRepoStrategy;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class MultiRepoPlanner {
|
|
31
|
+
constructor(private deps: MultiRepoPlannerDeps) {}
|
|
32
|
+
|
|
33
|
+
async plan(params: MultiRepoPlanParams): Promise<MultiRepoPlan> {
|
|
34
|
+
const { strategy } = params;
|
|
35
|
+
|
|
36
|
+
this.deps.logger.info(
|
|
37
|
+
{ strategy, repoCount: params.repos.length },
|
|
38
|
+
"Starting multi-repo planning",
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
if (strategy === "independent") {
|
|
42
|
+
return this.planIndependent(params);
|
|
43
|
+
}
|
|
44
|
+
return this.planCoordinated(params);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Independent strategy: plan each repo separately, prefix task IDs with repo name.
|
|
49
|
+
*/
|
|
50
|
+
private async planIndependent(params: MultiRepoPlanParams): Promise<MultiRepoPlan> {
|
|
51
|
+
const plans = new Map<RepoName, TaskPlan>();
|
|
52
|
+
|
|
53
|
+
const results = await Promise.all(
|
|
54
|
+
params.repos.map(async (repo) => {
|
|
55
|
+
const taskPlan = await this.deps.planningService.planTasks({
|
|
56
|
+
runId: params.runId,
|
|
57
|
+
parentBranch: params.parentBranch,
|
|
58
|
+
requirementsPath: params.requirementsPath,
|
|
59
|
+
targetDocsDir: params.targetDocsDir,
|
|
60
|
+
projectRoot: repo.path,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Prefix task IDs with repo name
|
|
64
|
+
const prefixedTasks = taskPlan.tasks.map((task) => ({
|
|
65
|
+
...task,
|
|
66
|
+
taskId: createTaskId(`${repo.name}/${task.taskId}`),
|
|
67
|
+
repoName: repo.name,
|
|
68
|
+
dependsOn: task.dependsOn.map((dep) => createTaskId(`${repo.name}/${dep}`)),
|
|
69
|
+
}));
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
repoName: repo.name,
|
|
73
|
+
plan: { ...taskPlan, tasks: prefixedTasks },
|
|
74
|
+
};
|
|
75
|
+
}),
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
for (const { repoName, plan } of results) {
|
|
79
|
+
plans.set(repoName, plan);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return { strategy: "independent", plans };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Coordinated strategy: build a combined context and make a single planning call.
|
|
87
|
+
* Uses the first repo as the primary project root.
|
|
88
|
+
*/
|
|
89
|
+
private async planCoordinated(params: MultiRepoPlanParams): Promise<MultiRepoPlan> {
|
|
90
|
+
const plans = new Map<RepoName, TaskPlan>();
|
|
91
|
+
|
|
92
|
+
// Use first repo as primary project root for coordinated planning
|
|
93
|
+
const primaryRepo = params.repos[0]!;
|
|
94
|
+
const taskPlan = await this.deps.planningService.planTasks({
|
|
95
|
+
runId: params.runId,
|
|
96
|
+
parentBranch: params.parentBranch,
|
|
97
|
+
requirementsPath: params.requirementsPath,
|
|
98
|
+
targetDocsDir: params.targetDocsDir,
|
|
99
|
+
projectRoot: primaryRepo.path,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Assign tasks to primary repo (coordinated produces a single unified plan)
|
|
103
|
+
const tasksWithRepo = taskPlan.tasks.map((task) => ({
|
|
104
|
+
...task,
|
|
105
|
+
repoName: primaryRepo.name,
|
|
106
|
+
}));
|
|
107
|
+
|
|
108
|
+
plans.set(primaryRepo.name, { ...taskPlan, tasks: tasksWithRepo });
|
|
109
|
+
|
|
110
|
+
return { strategy: "coordinated", plans };
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// RepoContext - validates and enriches repository context for multi-repo planning
|
|
2
|
+
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import type { ProjectAnalysis, FileChecker } from "../planning/project-detection";
|
|
5
|
+
import { analyzeProject } from "../planning/project-detection";
|
|
6
|
+
import { createRepoName, type RepoName } from "../../shared/types";
|
|
7
|
+
|
|
8
|
+
export interface RepoContext {
|
|
9
|
+
name: RepoName;
|
|
10
|
+
path: string;
|
|
11
|
+
ref?: string;
|
|
12
|
+
workspace?: ProjectAnalysis;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface RepoContextInput {
|
|
16
|
+
name?: string;
|
|
17
|
+
path: string;
|
|
18
|
+
ref?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Create a validated RepoContext from input.
|
|
23
|
+
* Validates path exists and is a git repo, then detects workspace info.
|
|
24
|
+
*/
|
|
25
|
+
export async function createRepoContext(
|
|
26
|
+
input: RepoContextInput,
|
|
27
|
+
fileChecker: FileChecker,
|
|
28
|
+
): Promise<RepoContext> {
|
|
29
|
+
const resolvedPath = path.resolve(input.path);
|
|
30
|
+
|
|
31
|
+
// Validate path exists
|
|
32
|
+
const exists = await fileChecker.exists(resolvedPath);
|
|
33
|
+
if (!exists) {
|
|
34
|
+
throw new Error(`Repository path does not exist: ${resolvedPath}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Validate is a git repo
|
|
38
|
+
const gitDir = path.join(resolvedPath, ".git");
|
|
39
|
+
const isGitRepo = await fileChecker.exists(gitDir);
|
|
40
|
+
if (!isGitRepo) {
|
|
41
|
+
throw new Error(`Path is not a git repository: ${resolvedPath}`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Derive name from directory if not provided
|
|
45
|
+
const name = createRepoName(input.name ?? path.basename(resolvedPath));
|
|
46
|
+
|
|
47
|
+
// Detect workspace info
|
|
48
|
+
let workspace: ProjectAnalysis | undefined;
|
|
49
|
+
try {
|
|
50
|
+
workspace = await analyzeProject(resolvedPath, fileChecker);
|
|
51
|
+
} catch {
|
|
52
|
+
// Non-fatal: workspace detection is best-effort
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
name,
|
|
57
|
+
path: resolvedPath,
|
|
58
|
+
ref: input.ref,
|
|
59
|
+
workspace,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Create multiple RepoContexts in parallel.
|
|
65
|
+
*/
|
|
66
|
+
export async function createRepoContexts(
|
|
67
|
+
inputs: RepoContextInput[],
|
|
68
|
+
fileChecker: FileChecker,
|
|
69
|
+
): Promise<RepoContext[]> {
|
|
70
|
+
return Promise.all(inputs.map((input) => createRepoContext(input, fileChecker)));
|
|
71
|
+
}
|