@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,199 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { mkdir, rm } from "node:fs/promises";
|
|
3
|
+
import type { Logger } from "pino";
|
|
4
|
+
import type { TaskId, RunId } from "@aad/shared/types";
|
|
5
|
+
import { GitWorkspaceError } from "@aad/shared/errors";
|
|
6
|
+
import { defaultGitOps, type GitOps } from "./git-exec";
|
|
7
|
+
|
|
8
|
+
export interface WorktreeManagerFsOps {
|
|
9
|
+
mkdir: typeof mkdir;
|
|
10
|
+
rm: typeof rm;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const defaultFsOps: WorktreeManagerFsOps = { mkdir, rm };
|
|
14
|
+
|
|
15
|
+
export interface WorktreeManagerOptions {
|
|
16
|
+
repoRoot: string;
|
|
17
|
+
worktreeBase: string;
|
|
18
|
+
logger?: Logger;
|
|
19
|
+
gitOps?: GitOps;
|
|
20
|
+
fsOps?: WorktreeManagerFsOps;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Manage git worktrees for parallel task execution
|
|
25
|
+
*/
|
|
26
|
+
export class WorktreeManager {
|
|
27
|
+
private repoRoot: string;
|
|
28
|
+
private worktreeBase: string;
|
|
29
|
+
private logger?: Logger;
|
|
30
|
+
private gitOps: GitOps;
|
|
31
|
+
private fsOps: WorktreeManagerFsOps;
|
|
32
|
+
|
|
33
|
+
constructor(options: WorktreeManagerOptions) {
|
|
34
|
+
this.repoRoot = options.repoRoot;
|
|
35
|
+
this.worktreeBase = options.worktreeBase;
|
|
36
|
+
this.logger = options.logger;
|
|
37
|
+
this.gitOps = options.gitOps ?? defaultGitOps;
|
|
38
|
+
this.fsOps = options.fsOps ?? defaultFsOps;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Create worktree for a task
|
|
43
|
+
*/
|
|
44
|
+
async createTaskWorktree(taskId: TaskId, branch: string): Promise<string> {
|
|
45
|
+
const worktreePath = join(this.worktreeBase, taskId as string);
|
|
46
|
+
|
|
47
|
+
this.logger?.info({ taskId, branch, worktreePath }, "Creating task worktree");
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
// Ensure worktree base directory exists
|
|
51
|
+
await this.fsOps.mkdir(this.worktreeBase, { recursive: true });
|
|
52
|
+
|
|
53
|
+
// Create worktree with new branch
|
|
54
|
+
await this.gitOps.gitExec(
|
|
55
|
+
["worktree", "add", "-b", branch, worktreePath, "HEAD"],
|
|
56
|
+
{ cwd: this.repoRoot, logger: this.logger }
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
return worktreePath;
|
|
60
|
+
} catch (error) {
|
|
61
|
+
throw new GitWorkspaceError("Failed to create task worktree", {
|
|
62
|
+
taskId: taskId as string,
|
|
63
|
+
branch,
|
|
64
|
+
worktreePath,
|
|
65
|
+
error: error instanceof Error ? error.message : String(error),
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Create worktree for parent branch (used for merging)
|
|
72
|
+
*/
|
|
73
|
+
async createParentWorktree(runId: RunId, parentBranch: string): Promise<string> {
|
|
74
|
+
const worktreePath = join(this.worktreeBase, `parent-${runId as string}`);
|
|
75
|
+
|
|
76
|
+
this.logger?.info({ runId, parentBranch, worktreePath }, "Creating parent worktree");
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
await this.fsOps.mkdir(this.worktreeBase, { recursive: true });
|
|
80
|
+
|
|
81
|
+
// Create worktree on existing branch (no -b flag)
|
|
82
|
+
await this.gitOps.gitExec(
|
|
83
|
+
["worktree", "add", worktreePath, parentBranch],
|
|
84
|
+
{ cwd: this.repoRoot, logger: this.logger }
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
return worktreePath;
|
|
88
|
+
} catch (error) {
|
|
89
|
+
throw new GitWorkspaceError("Failed to create parent worktree", {
|
|
90
|
+
runId: runId as string,
|
|
91
|
+
parentBranch,
|
|
92
|
+
worktreePath,
|
|
93
|
+
error: error instanceof Error ? error.message : String(error),
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Remove worktree
|
|
100
|
+
*/
|
|
101
|
+
async removeWorktree(worktreePath: string, force = false): Promise<void> {
|
|
102
|
+
this.logger?.info({ worktreePath, force }, "Removing worktree");
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
const args = ["worktree", "remove", worktreePath];
|
|
106
|
+
if (force) {
|
|
107
|
+
args.push("--force");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
await this.gitOps.gitExec(args, { cwd: this.repoRoot, logger: this.logger });
|
|
111
|
+
} catch (error) {
|
|
112
|
+
// If worktree already removed, ignore error
|
|
113
|
+
if (error instanceof GitWorkspaceError) {
|
|
114
|
+
const stderr = error.context.stderr;
|
|
115
|
+
if (typeof stderr === "string" && stderr.includes("not a working tree")) {
|
|
116
|
+
this.logger?.debug({ worktreePath }, "Worktree already removed");
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Try to force remove directory as fallback
|
|
122
|
+
try {
|
|
123
|
+
await this.fsOps.rm(worktreePath, { recursive: true, force: true });
|
|
124
|
+
await this.gitOps.gitExec(["worktree", "prune"], { cwd: this.repoRoot, logger: this.logger });
|
|
125
|
+
} catch (cleanupError) {
|
|
126
|
+
this.logger?.debug({ cleanupError, worktreePath }, "Worktree cleanup fallback failed (ignored)");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
throw new GitWorkspaceError("Failed to remove worktree", {
|
|
130
|
+
worktreePath,
|
|
131
|
+
error: error instanceof Error ? error.message : String(error),
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* List all worktrees
|
|
138
|
+
*/
|
|
139
|
+
async listWorktrees(): Promise<Array<{ path: string; branch: string; head: string }>> {
|
|
140
|
+
const result = await this.gitOps.gitExec(
|
|
141
|
+
["worktree", "list", "--porcelain"],
|
|
142
|
+
{ cwd: this.repoRoot, logger: this.logger }
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
const worktrees: Array<{ path: string; branch: string; head: string }> = [];
|
|
146
|
+
const lines = result.stdout.split("\n");
|
|
147
|
+
|
|
148
|
+
let current: Partial<{ path: string; branch: string; head: string }> = {};
|
|
149
|
+
|
|
150
|
+
for (const line of lines) {
|
|
151
|
+
if (line.startsWith("worktree ")) {
|
|
152
|
+
current.path = line.substring("worktree ".length);
|
|
153
|
+
} else if (line.startsWith("branch ")) {
|
|
154
|
+
current.branch = line.substring("branch ".length);
|
|
155
|
+
} else if (line.startsWith("HEAD ")) {
|
|
156
|
+
current.head = line.substring("HEAD ".length);
|
|
157
|
+
} else if (line === "") {
|
|
158
|
+
if (current.path && current.head) {
|
|
159
|
+
worktrees.push({
|
|
160
|
+
path: current.path,
|
|
161
|
+
branch: current.branch ?? "(detached)",
|
|
162
|
+
head: current.head,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
current = {};
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return worktrees;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Verify worktree exists and is valid
|
|
174
|
+
*/
|
|
175
|
+
async verifyWorktree(worktreePath: string): Promise<boolean> {
|
|
176
|
+
try {
|
|
177
|
+
await this.gitOps.gitExec(
|
|
178
|
+
["rev-parse", "--git-dir"],
|
|
179
|
+
{ cwd: worktreePath, logger: this.logger }
|
|
180
|
+
);
|
|
181
|
+
return true;
|
|
182
|
+
} catch (error) {
|
|
183
|
+
this.logger?.debug({ error, worktreePath }, "Worktree verification failed");
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Cleanup orphaned worktrees
|
|
190
|
+
*/
|
|
191
|
+
async pruneWorktrees(): Promise<void> {
|
|
192
|
+
this.logger?.info("Pruning orphaned worktrees");
|
|
193
|
+
|
|
194
|
+
await this.gitOps.gitExec(
|
|
195
|
+
["worktree", "prune"],
|
|
196
|
+
{ cwd: this.repoRoot, logger: this.logger }
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach } from "bun:test";
|
|
2
|
+
import { LogStore, type LogEntry, type LogFilter } from "../log-store";
|
|
3
|
+
|
|
4
|
+
describe("LogStore", () => {
|
|
5
|
+
let store: LogStore;
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
store = new LogStore();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test("adds log entry", () => {
|
|
12
|
+
const entry: LogEntry = {
|
|
13
|
+
level: "info",
|
|
14
|
+
service: "test-service",
|
|
15
|
+
message: "test message",
|
|
16
|
+
timestamp: Date.now(),
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
store.add(entry);
|
|
20
|
+
|
|
21
|
+
const all = store.getAll();
|
|
22
|
+
expect(all).toHaveLength(1);
|
|
23
|
+
expect(all[0]).toBeDefined();
|
|
24
|
+
expect(all[0]).toEqual(entry);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("adds multiple log entries", () => {
|
|
28
|
+
store.add({
|
|
29
|
+
level: "info",
|
|
30
|
+
service: "test",
|
|
31
|
+
message: "msg1",
|
|
32
|
+
timestamp: Date.now(),
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
store.add({
|
|
36
|
+
level: "error",
|
|
37
|
+
service: "test",
|
|
38
|
+
message: "msg2",
|
|
39
|
+
timestamp: Date.now(),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
expect(store.getAll()).toHaveLength(2);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("respects capacity limit", () => {
|
|
46
|
+
const smallStore = new LogStore(3);
|
|
47
|
+
|
|
48
|
+
smallStore.add({
|
|
49
|
+
level: "info",
|
|
50
|
+
service: "test",
|
|
51
|
+
message: "msg1",
|
|
52
|
+
timestamp: Date.now(),
|
|
53
|
+
});
|
|
54
|
+
smallStore.add({
|
|
55
|
+
level: "info",
|
|
56
|
+
service: "test",
|
|
57
|
+
message: "msg2",
|
|
58
|
+
timestamp: Date.now(),
|
|
59
|
+
});
|
|
60
|
+
smallStore.add({
|
|
61
|
+
level: "info",
|
|
62
|
+
service: "test",
|
|
63
|
+
message: "msg3",
|
|
64
|
+
timestamp: Date.now(),
|
|
65
|
+
});
|
|
66
|
+
smallStore.add({
|
|
67
|
+
level: "info",
|
|
68
|
+
service: "test",
|
|
69
|
+
message: "msg4",
|
|
70
|
+
timestamp: Date.now(),
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const all = smallStore.getAll();
|
|
74
|
+
expect(all).toHaveLength(3);
|
|
75
|
+
expect(all[0]?.message).toBe("msg2");
|
|
76
|
+
expect(all[2]?.message).toBe("msg4");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("clears all entries", () => {
|
|
80
|
+
store.add({
|
|
81
|
+
level: "info",
|
|
82
|
+
service: "test",
|
|
83
|
+
message: "msg1",
|
|
84
|
+
timestamp: Date.now(),
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
expect(store.getAll()).toHaveLength(1);
|
|
88
|
+
|
|
89
|
+
store.clear();
|
|
90
|
+
|
|
91
|
+
expect(store.getAll()).toHaveLength(0);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("queries by level", () => {
|
|
95
|
+
const now = Date.now();
|
|
96
|
+
|
|
97
|
+
store.add({ level: "info", service: "test", message: "info msg", timestamp: now });
|
|
98
|
+
store.add({ level: "error", service: "test", message: "error msg", timestamp: now });
|
|
99
|
+
store.add({ level: "warn", service: "test", message: "warn msg", timestamp: now });
|
|
100
|
+
|
|
101
|
+
const filter: LogFilter = { level: "error" };
|
|
102
|
+
const results = store.query(filter);
|
|
103
|
+
|
|
104
|
+
expect(results).toHaveLength(1);
|
|
105
|
+
expect(results[0]?.level).toBe("error");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("queries by service", () => {
|
|
109
|
+
const now = Date.now();
|
|
110
|
+
|
|
111
|
+
store.add({ level: "info", service: "service-a", message: "msg1", timestamp: now });
|
|
112
|
+
store.add({ level: "info", service: "service-b", message: "msg2", timestamp: now });
|
|
113
|
+
store.add({ level: "info", service: "service-a", message: "msg3", timestamp: now });
|
|
114
|
+
|
|
115
|
+
const filter: LogFilter = { service: "service-a" };
|
|
116
|
+
const results = store.query(filter);
|
|
117
|
+
|
|
118
|
+
expect(results).toHaveLength(2);
|
|
119
|
+
results.forEach((entry) => {
|
|
120
|
+
expect(entry.service).toBe("service-a");
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("queries by taskId", () => {
|
|
125
|
+
const now = Date.now();
|
|
126
|
+
|
|
127
|
+
store.add({
|
|
128
|
+
level: "info",
|
|
129
|
+
service: "test",
|
|
130
|
+
message: "msg1",
|
|
131
|
+
timestamp: now,
|
|
132
|
+
taskId: "t1",
|
|
133
|
+
});
|
|
134
|
+
store.add({
|
|
135
|
+
level: "info",
|
|
136
|
+
service: "test",
|
|
137
|
+
message: "msg2",
|
|
138
|
+
timestamp: now,
|
|
139
|
+
taskId: "t2",
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
const filter: LogFilter = { taskId: "t1" };
|
|
143
|
+
const results = store.query(filter);
|
|
144
|
+
|
|
145
|
+
expect(results).toHaveLength(1);
|
|
146
|
+
expect(results[0]?.taskId).toBe("t1");
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("queries by workerId", () => {
|
|
150
|
+
const now = Date.now();
|
|
151
|
+
|
|
152
|
+
store.add({
|
|
153
|
+
level: "info",
|
|
154
|
+
service: "test",
|
|
155
|
+
message: "msg1",
|
|
156
|
+
timestamp: now,
|
|
157
|
+
workerId: "w1",
|
|
158
|
+
});
|
|
159
|
+
store.add({
|
|
160
|
+
level: "info",
|
|
161
|
+
service: "test",
|
|
162
|
+
message: "msg2",
|
|
163
|
+
timestamp: now,
|
|
164
|
+
workerId: "w2",
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const filter: LogFilter = { workerId: "w1" };
|
|
168
|
+
const results = store.query(filter);
|
|
169
|
+
|
|
170
|
+
expect(results).toHaveLength(1);
|
|
171
|
+
expect(results[0]?.workerId).toBe("w1");
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test("queries by since timestamp", () => {
|
|
175
|
+
const now = Date.now();
|
|
176
|
+
|
|
177
|
+
store.add({ level: "info", service: "test", message: "msg1", timestamp: now - 1000 });
|
|
178
|
+
store.add({ level: "info", service: "test", message: "msg2", timestamp: now });
|
|
179
|
+
store.add({ level: "info", service: "test", message: "msg3", timestamp: now + 1000 });
|
|
180
|
+
|
|
181
|
+
const filter: LogFilter = { since: now };
|
|
182
|
+
const results = store.query(filter);
|
|
183
|
+
|
|
184
|
+
expect(results).toHaveLength(2);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test("queries with limit", () => {
|
|
188
|
+
const now = Date.now();
|
|
189
|
+
|
|
190
|
+
store.add({ level: "info", service: "test", message: "msg1", timestamp: now });
|
|
191
|
+
store.add({ level: "info", service: "test", message: "msg2", timestamp: now });
|
|
192
|
+
store.add({ level: "info", service: "test", message: "msg3", timestamp: now });
|
|
193
|
+
|
|
194
|
+
const filter: LogFilter = { limit: 2 };
|
|
195
|
+
const results = store.query(filter);
|
|
196
|
+
|
|
197
|
+
expect(results).toHaveLength(2);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test("queries with multiple filters", () => {
|
|
201
|
+
const now = Date.now();
|
|
202
|
+
|
|
203
|
+
store.add({
|
|
204
|
+
level: "error",
|
|
205
|
+
service: "service-a",
|
|
206
|
+
message: "msg1",
|
|
207
|
+
timestamp: now,
|
|
208
|
+
taskId: "t1",
|
|
209
|
+
});
|
|
210
|
+
store.add({
|
|
211
|
+
level: "error",
|
|
212
|
+
service: "service-b",
|
|
213
|
+
message: "msg2",
|
|
214
|
+
timestamp: now,
|
|
215
|
+
taskId: "t1",
|
|
216
|
+
});
|
|
217
|
+
store.add({
|
|
218
|
+
level: "info",
|
|
219
|
+
service: "service-a",
|
|
220
|
+
message: "msg3",
|
|
221
|
+
timestamp: now,
|
|
222
|
+
taskId: "t1",
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
const filter: LogFilter = {
|
|
226
|
+
level: "error",
|
|
227
|
+
service: "service-a",
|
|
228
|
+
taskId: "t1",
|
|
229
|
+
};
|
|
230
|
+
const results = store.query(filter);
|
|
231
|
+
|
|
232
|
+
expect(results).toHaveLength(1);
|
|
233
|
+
expect(results[0]?.message).toBe("msg1");
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test("query returns empty array when no matches", () => {
|
|
237
|
+
const filter: LogFilter = { level: "error" };
|
|
238
|
+
const results = store.query(filter);
|
|
239
|
+
|
|
240
|
+
expect(results).toHaveLength(0);
|
|
241
|
+
});
|
|
242
|
+
});
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { describe, test, expect, afterEach } from "bun:test";
|
|
2
|
+
import { createLogger } from "../logger";
|
|
3
|
+
import { unlinkSync, existsSync } from "node:fs";
|
|
4
|
+
|
|
5
|
+
describe("createLogger", () => {
|
|
6
|
+
const testLogFile = "/tmp/aad-test-logger.log";
|
|
7
|
+
|
|
8
|
+
afterEach(() => {
|
|
9
|
+
if (existsSync(testLogFile)) {
|
|
10
|
+
unlinkSync(testLogFile);
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test("creates logger with service field", () => {
|
|
15
|
+
const logger = createLogger({ service: "test-service" });
|
|
16
|
+
|
|
17
|
+
expect(logger).toBeDefined();
|
|
18
|
+
expect(typeof logger.info).toBe("function");
|
|
19
|
+
expect(typeof logger.error).toBe("function");
|
|
20
|
+
expect(typeof logger.warn).toBe("function");
|
|
21
|
+
expect(typeof logger.debug).toBe("function");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("child logger includes service in log context", () => {
|
|
25
|
+
const logger = createLogger({ service: "test-service" });
|
|
26
|
+
|
|
27
|
+
// pino loggerは内部で bindings() を使って context を確認できる
|
|
28
|
+
const bindings = (logger as any).bindings();
|
|
29
|
+
expect(bindings.service).toBe("test-service");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("creates logger without log file in debug mode", () => {
|
|
33
|
+
const logger = createLogger({
|
|
34
|
+
service: "test-service",
|
|
35
|
+
debug: true,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
expect(logger).toBeDefined();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("creates logger with log file path", async () => {
|
|
42
|
+
const logger = createLogger({
|
|
43
|
+
service: "test-service",
|
|
44
|
+
logFilePath: testLogFile,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
logger.info("test message");
|
|
48
|
+
|
|
49
|
+
// pino transport (worker thread) の非同期書き込みをポーリングで待機
|
|
50
|
+
const maxWait = 2000;
|
|
51
|
+
const interval = 50;
|
|
52
|
+
let elapsed = 0;
|
|
53
|
+
while (!existsSync(testLogFile) && elapsed < maxWait) {
|
|
54
|
+
await new Promise((resolve) => setTimeout(resolve, interval));
|
|
55
|
+
elapsed += interval;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ファイルが作成されることを確認
|
|
59
|
+
expect(existsSync(testLogFile)).toBe(true);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("logger can log at different levels", () => {
|
|
63
|
+
const logger = createLogger({ service: "test-service" });
|
|
64
|
+
|
|
65
|
+
// ログ出力がエラーを投げないことを確認
|
|
66
|
+
expect(() => {
|
|
67
|
+
logger.info("info message");
|
|
68
|
+
logger.error("error message");
|
|
69
|
+
logger.warn("warn message");
|
|
70
|
+
logger.debug("debug message");
|
|
71
|
+
}).not.toThrow();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("logger supports structured logging", () => {
|
|
75
|
+
const logger = createLogger({ service: "test-service" });
|
|
76
|
+
|
|
77
|
+
expect(() => {
|
|
78
|
+
logger.info({ taskId: "t1", workerId: "w1" }, "task dispatched");
|
|
79
|
+
}).not.toThrow();
|
|
80
|
+
});
|
|
81
|
+
});
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { describe, test, expect, mock } from "bun:test";
|
|
2
|
+
import { createSSETransport } from "../transports/sse-transport";
|
|
3
|
+
import { LogStore, type LogEntry } from "../log-store";
|
|
4
|
+
import { EventBus } from "../../../shared/events";
|
|
5
|
+
|
|
6
|
+
describe("createSSETransport", () => {
|
|
7
|
+
test("returns object with write method", () => {
|
|
8
|
+
const logStore = new LogStore();
|
|
9
|
+
const eventBus = new EventBus();
|
|
10
|
+
|
|
11
|
+
const transport = createSSETransport({ logStore, eventBus });
|
|
12
|
+
|
|
13
|
+
expect(transport).toBeDefined();
|
|
14
|
+
expect(typeof transport.write).toBe("function");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test("write calls logStore.add with entry", () => {
|
|
18
|
+
const logStore = new LogStore();
|
|
19
|
+
const eventBus = new EventBus();
|
|
20
|
+
const addSpy = mock(() => {});
|
|
21
|
+
logStore.add = addSpy;
|
|
22
|
+
|
|
23
|
+
const transport = createSSETransport({ logStore, eventBus });
|
|
24
|
+
|
|
25
|
+
const entry: LogEntry = {
|
|
26
|
+
level: "info",
|
|
27
|
+
service: "test-service",
|
|
28
|
+
message: "Test message",
|
|
29
|
+
timestamp: Date.now(),
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
transport.write(entry);
|
|
33
|
+
|
|
34
|
+
expect(addSpy).toHaveBeenCalledTimes(1);
|
|
35
|
+
expect(addSpy).toHaveBeenCalledWith(entry);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("write emits log:entry event via EventBus", () => {
|
|
39
|
+
const logStore = new LogStore();
|
|
40
|
+
const eventBus = new EventBus();
|
|
41
|
+
const emitSpy = mock(() => {});
|
|
42
|
+
eventBus.emit = emitSpy;
|
|
43
|
+
|
|
44
|
+
const transport = createSSETransport({ logStore, eventBus });
|
|
45
|
+
|
|
46
|
+
const entry: LogEntry = {
|
|
47
|
+
level: "error",
|
|
48
|
+
service: "task-execution",
|
|
49
|
+
message: "Execution failed",
|
|
50
|
+
timestamp: Date.now(),
|
|
51
|
+
taskId: "task-1",
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
transport.write(entry);
|
|
55
|
+
|
|
56
|
+
expect(emitSpy).toHaveBeenCalledTimes(1);
|
|
57
|
+
expect(emitSpy).toHaveBeenCalledWith({
|
|
58
|
+
type: "log:entry",
|
|
59
|
+
entry,
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("write handles multiple calls correctly", () => {
|
|
64
|
+
const logStore = new LogStore();
|
|
65
|
+
const eventBus = new EventBus();
|
|
66
|
+
const addSpy = mock(() => {});
|
|
67
|
+
const emitSpy = mock(() => {});
|
|
68
|
+
logStore.add = addSpy;
|
|
69
|
+
eventBus.emit = emitSpy;
|
|
70
|
+
|
|
71
|
+
const transport = createSSETransport({ logStore, eventBus });
|
|
72
|
+
|
|
73
|
+
const entry1: LogEntry = {
|
|
74
|
+
level: "info",
|
|
75
|
+
service: "planning",
|
|
76
|
+
message: "Planning started",
|
|
77
|
+
timestamp: Date.now(),
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const entry2: LogEntry = {
|
|
81
|
+
level: "warn",
|
|
82
|
+
service: "task-queue",
|
|
83
|
+
message: "Task stale",
|
|
84
|
+
timestamp: Date.now(),
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
transport.write(entry1);
|
|
88
|
+
transport.write(entry2);
|
|
89
|
+
|
|
90
|
+
expect(addSpy).toHaveBeenCalledTimes(2);
|
|
91
|
+
expect(emitSpy).toHaveBeenCalledTimes(2);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
export interface LogEntry {
|
|
2
|
+
level: string;
|
|
3
|
+
service: string;
|
|
4
|
+
message: string;
|
|
5
|
+
timestamp: number;
|
|
6
|
+
taskId?: string;
|
|
7
|
+
workerId?: string;
|
|
8
|
+
[key: string]: unknown;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface LogFilter {
|
|
12
|
+
level?: string;
|
|
13
|
+
service?: string;
|
|
14
|
+
taskId?: string;
|
|
15
|
+
workerId?: string;
|
|
16
|
+
since?: number;
|
|
17
|
+
limit?: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class LogStore {
|
|
21
|
+
private entries: LogEntry[] = [];
|
|
22
|
+
private capacity: number;
|
|
23
|
+
|
|
24
|
+
constructor(capacity: number = 10000) {
|
|
25
|
+
this.capacity = capacity;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
add(entry: LogEntry): void {
|
|
29
|
+
this.entries.push(entry);
|
|
30
|
+
|
|
31
|
+
// リングバッファ: capacityを超えたら古いものを削除
|
|
32
|
+
if (this.entries.length > this.capacity) {
|
|
33
|
+
this.entries.shift();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
query(filter: LogFilter): LogEntry[] {
|
|
38
|
+
let results = this.entries;
|
|
39
|
+
|
|
40
|
+
// level filter
|
|
41
|
+
if (filter.level !== undefined) {
|
|
42
|
+
results = results.filter((entry) => entry.level === filter.level);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// service filter
|
|
46
|
+
if (filter.service !== undefined) {
|
|
47
|
+
results = results.filter((entry) => entry.service === filter.service);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// taskId filter
|
|
51
|
+
if (filter.taskId !== undefined) {
|
|
52
|
+
results = results.filter((entry) => entry.taskId === filter.taskId);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// workerId filter
|
|
56
|
+
if (filter.workerId !== undefined) {
|
|
57
|
+
results = results.filter((entry) => entry.workerId === filter.workerId);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// since filter
|
|
61
|
+
if (filter.since !== undefined) {
|
|
62
|
+
results = results.filter((entry) => entry.timestamp >= filter.since!);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// limit filter
|
|
66
|
+
if (filter.limit !== undefined && filter.limit > 0) {
|
|
67
|
+
results = results.slice(0, filter.limit);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return results;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
clear(): void {
|
|
74
|
+
this.entries = [];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
getAll(): LogEntry[] {
|
|
78
|
+
return [...this.entries];
|
|
79
|
+
}
|
|
80
|
+
}
|