@ronkovic/aad 0.3.5 → 0.3.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -3
- package/src/__tests__/e2e/pipeline-e2e.test.ts +6 -0
- package/src/main.ts +10 -2
- package/src/modules/cli/__tests__/interactive/auth-step.test.ts +24 -0
- package/src/modules/cli/__tests__/interactive/interactive.test.ts +14 -0
- package/src/modules/cli/__tests__/interactive/process-supervisor.test.ts +45 -0
- package/src/modules/cli/__tests__/interactive/prompts.test.ts +75 -0
- package/src/modules/cli/__tests__/update.test.ts +20 -0
- package/src/modules/cli/commands/interactive/auth-step.ts +61 -0
- package/src/modules/cli/commands/interactive/launch-step.ts +76 -0
- package/src/modules/cli/commands/interactive/process-supervisor.ts +105 -0
- package/src/modules/cli/commands/interactive/project-step.ts +157 -0
- package/src/modules/cli/commands/interactive/prompts.ts +75 -0
- package/src/modules/cli/commands/interactive.ts +38 -0
- package/src/modules/cli/commands/resume.ts +16 -0
- package/src/modules/cli/commands/run.ts +28 -7
- package/src/modules/cli/commands/task-dispatch-handler.ts +11 -3
- package/src/modules/cli/commands/update.ts +119 -0
- package/src/modules/cli/index.ts +1 -0
- package/src/modules/git-workspace/__tests__/worktree-cleanup.test.ts +9 -5
- package/src/modules/git-workspace/__tests__/worktree-manager.test.ts +19 -4
- package/src/modules/git-workspace/branch-manager.ts +10 -3
- package/src/modules/git-workspace/worktree-manager.ts +43 -27
- package/src/modules/planning/__tests__/feature-name-generator.test.ts +124 -0
- package/src/modules/planning/feature-name-generator.ts +103 -0
- package/src/modules/task-queue/dispatcher.ts +6 -4
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive UI utility wrappers around @inquirer/prompts
|
|
3
|
+
*/
|
|
4
|
+
import { confirm, input, select, password } from "@inquirer/prompts";
|
|
5
|
+
|
|
6
|
+
export interface Choice<T> {
|
|
7
|
+
name: string;
|
|
8
|
+
value: T;
|
|
9
|
+
description?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function askConfirm(
|
|
13
|
+
message: string,
|
|
14
|
+
defaultValue = true,
|
|
15
|
+
): Promise<boolean> {
|
|
16
|
+
return confirm({ message, default: defaultValue });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function askInput(
|
|
20
|
+
message: string,
|
|
21
|
+
opts?: { default?: string; validate?: (v: string) => string | true },
|
|
22
|
+
): Promise<string> {
|
|
23
|
+
return input({
|
|
24
|
+
message,
|
|
25
|
+
default: opts?.default,
|
|
26
|
+
validate: opts?.validate,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function askSelect<T>(
|
|
31
|
+
message: string,
|
|
32
|
+
choices: Array<Choice<T>>,
|
|
33
|
+
): Promise<T> {
|
|
34
|
+
return select({ message, choices });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function askPassword(message: string): Promise<string> {
|
|
38
|
+
return password({ message, mask: "•" });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function printBox(lines: string[], title?: string): void {
|
|
42
|
+
const maxLen = Math.max(
|
|
43
|
+
title?.length ?? 0,
|
|
44
|
+
...lines.map((l) => stripAnsi(l).length),
|
|
45
|
+
);
|
|
46
|
+
const width = maxLen + 2;
|
|
47
|
+
const border = "─".repeat(width);
|
|
48
|
+
|
|
49
|
+
console.log(`┌${border}┐`);
|
|
50
|
+
if (title) {
|
|
51
|
+
console.log(`│ ${title.padEnd(width - 1)}│`);
|
|
52
|
+
console.log(`├${border}┤`);
|
|
53
|
+
}
|
|
54
|
+
for (const line of lines) {
|
|
55
|
+
const pad = width - 1 - stripAnsi(line).length;
|
|
56
|
+
console.log(`│ ${line}${" ".repeat(Math.max(0, pad))}│`);
|
|
57
|
+
}
|
|
58
|
+
console.log(`└${border}┘`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function stripAnsi(str: string): string {
|
|
62
|
+
return str.replace(/\x1b\[[0-9;]*m/g, "");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function success(msg: string): void {
|
|
66
|
+
console.log(` \x1b[32m✓\x1b[0m ${msg}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function warn(msg: string): void {
|
|
70
|
+
console.log(` \x1b[33m⚠\x1b[0m ${msg}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function error(msg: string): void {
|
|
74
|
+
console.log(` \x1b[31m✗\x1b[0m ${msg}`);
|
|
75
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive mode — main entry point
|
|
3
|
+
* Launched when `aad` is run with no arguments
|
|
4
|
+
*/
|
|
5
|
+
import type { Command } from "commander";
|
|
6
|
+
import { runAuthStep } from "./interactive/auth-step.js";
|
|
7
|
+
import { runProjectStep } from "./interactive/project-step.js";
|
|
8
|
+
import { runLaunchStep } from "./interactive/launch-step.js";
|
|
9
|
+
|
|
10
|
+
export function registerInteractiveCommand(program: Command): void {
|
|
11
|
+
program
|
|
12
|
+
.command("interactive")
|
|
13
|
+
.description("Interactive setup wizard for multi-project AAD execution")
|
|
14
|
+
.action(async () => {
|
|
15
|
+
console.log();
|
|
16
|
+
console.log(" ┌─────────────────────────────────────┐");
|
|
17
|
+
console.log(" │ AAD Interactive Mode │");
|
|
18
|
+
console.log(" └─────────────────────────────────────┘");
|
|
19
|
+
console.log();
|
|
20
|
+
|
|
21
|
+
// Phase 1: Auth
|
|
22
|
+
console.log(" ── Step 1: 認証設定 ──\n");
|
|
23
|
+
await runAuthStep();
|
|
24
|
+
|
|
25
|
+
// Phase 2: Project setup
|
|
26
|
+
console.log("\n ── Step 2: プロジェクト設定 ──");
|
|
27
|
+
const projects = await runProjectStep();
|
|
28
|
+
|
|
29
|
+
if (projects.length === 0) {
|
|
30
|
+
console.log("\n プロジェクトが設定されていません。終了します。\n");
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Phase 3: Launch
|
|
35
|
+
console.log("\n ── Step 3: 実行 ──\n");
|
|
36
|
+
await runLaunchStep(projects);
|
|
37
|
+
});
|
|
38
|
+
}
|
|
@@ -125,11 +125,27 @@ export async function resumeRun(app: App, runIdStr: string): Promise<void> {
|
|
|
125
125
|
// Check if parent worktree exists from a previous run
|
|
126
126
|
const parentWorktreePath = `${process.cwd()}/.aad/worktrees/parent-${runId}`;
|
|
127
127
|
const parentWorktreeExists = await Bun.file(`${parentWorktreePath}/.git`).exists();
|
|
128
|
+
// Detect feature branch name from parent worktree
|
|
129
|
+
let featureBranchName: string | undefined;
|
|
130
|
+
if (parentWorktreeExists) {
|
|
131
|
+
try {
|
|
132
|
+
const branchResult = await import("../../git-workspace").then(
|
|
133
|
+
(m) => m.getCurrentBranch(parentWorktreePath),
|
|
134
|
+
);
|
|
135
|
+
if (branchResult && /^(feat|fix|docs|style|refactor|perf|test|chore)\//.test(branchResult)) {
|
|
136
|
+
featureBranchName = branchResult;
|
|
137
|
+
}
|
|
138
|
+
} catch {
|
|
139
|
+
// Fallback: no feature branch detection
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
128
143
|
registerTaskDispatchHandler({
|
|
129
144
|
app,
|
|
130
145
|
runId,
|
|
131
146
|
parentBranch: runState.parentBranch,
|
|
132
147
|
parentWorktreePath: parentWorktreeExists ? parentWorktreePath : undefined,
|
|
148
|
+
featureBranchName,
|
|
133
149
|
});
|
|
134
150
|
|
|
135
151
|
// 6. Dispatcher.start()
|
|
@@ -12,6 +12,7 @@ import { getCurrentBranch, cleanupOrphanedFromPreviousRuns } from "../../git-wor
|
|
|
12
12
|
import { checkMemoryAndWarn } from "../../../shared/memory-check";
|
|
13
13
|
import { installShutdownHandler } from "../../../shared/shutdown-handler";
|
|
14
14
|
import { registerTaskDispatchHandler } from "./task-dispatch-handler";
|
|
15
|
+
import { generateFeatureName } from "../../planning/feature-name-generator";
|
|
15
16
|
|
|
16
17
|
export function createRunCommand(getApp: () => App): Command {
|
|
17
18
|
const command = new Command("run")
|
|
@@ -44,7 +45,7 @@ export function createRunCommand(getApp: () => App): Command {
|
|
|
44
45
|
* メインパイプライン実行
|
|
45
46
|
*/
|
|
46
47
|
export async function runPipeline(app: App, requirementsPath: string, keepWorktrees = false): Promise<void> {
|
|
47
|
-
const { config, logger, eventBus, planningService, dispatcher, processManager, worktreeManager, stores, branchManager } = app;
|
|
48
|
+
const { config, logger, eventBus, planningService, dispatcher, processManager, worktreeManager, stores, branchManager, providerRegistry } = app;
|
|
48
49
|
|
|
49
50
|
// Validate requirements file exists
|
|
50
51
|
const reqFile = Bun.file(requirementsPath);
|
|
@@ -144,17 +145,27 @@ export async function runPipeline(app: App, requirementsPath: string, keepWorktr
|
|
|
144
145
|
logger,
|
|
145
146
|
});
|
|
146
147
|
|
|
147
|
-
// 5.
|
|
148
|
+
// 5. Generate feature name and create parent worktree with real branch
|
|
148
149
|
let parentWorktreePath: string | undefined;
|
|
150
|
+
let featureBranchName: string | undefined;
|
|
149
151
|
try {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
+
const { prefix, name: featureName } = await generateFeatureName(
|
|
153
|
+
requirementsPath,
|
|
154
|
+
providerRegistry.getProvider("implementer"),
|
|
155
|
+
logger,
|
|
156
|
+
);
|
|
157
|
+
featureBranchName = `${prefix}/${featureName}/parent`;
|
|
158
|
+
const result = await worktreeManager.createParentWorktree(runId, parentBranch, featureBranchName);
|
|
159
|
+
parentWorktreePath = result.worktreePath;
|
|
160
|
+
featureBranchName = result.branch;
|
|
161
|
+
logger.info({ parentWorktreePath, featureBranchName }, "Parent worktree created for merging");
|
|
162
|
+
console.log(`🌿 Feature Branch: ${featureBranchName}`);
|
|
152
163
|
} catch (error) {
|
|
153
164
|
logger.warn({ error }, "Failed to create parent worktree, using repo root as fallback");
|
|
154
165
|
}
|
|
155
166
|
|
|
156
167
|
// 5.5. Register task dispatch handler (shared with resume)
|
|
157
|
-
registerTaskDispatchHandler({ app, runId, parentBranch, parentWorktreePath });
|
|
168
|
+
registerTaskDispatchHandler({ app, runId, parentBranch, parentWorktreePath, featureBranchName });
|
|
158
169
|
|
|
159
170
|
// 6. 進捗表示
|
|
160
171
|
const progressSpinner = createSpinner("Executing tasks...");
|
|
@@ -219,7 +230,7 @@ export async function runPipeline(app: App, requirementsPath: string, keepWorktr
|
|
|
219
230
|
const worktreeBase = `${process.cwd()}/.aad/worktrees`;
|
|
220
231
|
const runWorktrees = worktrees.filter(
|
|
221
232
|
(wt) => wt.path.startsWith(worktreeBase)
|
|
222
|
-
&& wt.branch.includes(runId) //
|
|
233
|
+
&& (wt.branch.includes(runId) || (featureBranchName && wt.branch.startsWith(featureBranchName.replace(/\/parent$/, "")))) // Match by runId or feature name
|
|
223
234
|
&& !wt.path.includes(`parent-${runId}`) // Keep parent worktree (contains merge results)
|
|
224
235
|
);
|
|
225
236
|
|
|
@@ -239,7 +250,17 @@ export async function runPipeline(app: App, requirementsPath: string, keepWorktr
|
|
|
239
250
|
await worktreeManager.pruneWorktrees();
|
|
240
251
|
|
|
241
252
|
// Cleanup orphaned branches for this run (force delete merged branches)
|
|
242
|
-
|
|
253
|
+
let deletedBranches = await branchManager.cleanupOrphanBranches(runId, true);
|
|
254
|
+
|
|
255
|
+
// Also cleanup feature-named branches if applicable
|
|
256
|
+
if (featureBranchName) {
|
|
257
|
+
const featurePrefix = featureBranchName.replace(/\/parent$/, "");
|
|
258
|
+
const featureBranches = await branchManager.cleanupOrphanBranches(
|
|
259
|
+
createRunId(featurePrefix.replace(/^[^/]+\//, "")),
|
|
260
|
+
true,
|
|
261
|
+
);
|
|
262
|
+
deletedBranches = [...deletedBranches, ...featureBranches];
|
|
263
|
+
}
|
|
243
264
|
|
|
244
265
|
console.log(` Removed ${removed} worktree(s)`);
|
|
245
266
|
console.log(` Deleted ${deletedBranches.length} branch(es)`);
|
|
@@ -28,6 +28,9 @@ export interface TaskDispatchContext {
|
|
|
28
28
|
/** Path to the parent worktree where task branches are merged into.
|
|
29
29
|
* If not provided, falls back to process.cwd() (repo root). */
|
|
30
30
|
parentWorktreePath?: string;
|
|
31
|
+
/** Feature branch name (e.g. feat/auth-feature/parent).
|
|
32
|
+
* Task branches are created from this branch. */
|
|
33
|
+
featureBranchName?: string;
|
|
31
34
|
}
|
|
32
35
|
|
|
33
36
|
/**
|
|
@@ -35,7 +38,7 @@ export interface TaskDispatchContext {
|
|
|
35
38
|
* Handles memory gate, worker state, worktree creation, and TDD pipeline execution.
|
|
36
39
|
*/
|
|
37
40
|
export function registerTaskDispatchHandler(ctx: TaskDispatchContext): void {
|
|
38
|
-
const { app, runId, parentBranch, parentWorktreePath } = ctx;
|
|
41
|
+
const { app, runId, parentBranch, parentWorktreePath, featureBranchName } = ctx;
|
|
39
42
|
const mergeTarget = parentWorktreePath ?? process.cwd();
|
|
40
43
|
const { eventBus, logger, config, processManager, worktreeManager, providerRegistry, stores } = app;
|
|
41
44
|
|
|
@@ -57,8 +60,13 @@ export function registerTaskDispatchHandler(ctx: TaskDispatchContext): void {
|
|
|
57
60
|
throw new Error(`Task not found: ${taskId}`);
|
|
58
61
|
}
|
|
59
62
|
|
|
60
|
-
|
|
61
|
-
|
|
63
|
+
// Derive task branch name from feature branch or runId
|
|
64
|
+
// e.g. feat/auth-feature/task-001 (from feat/auth-feature/parent)
|
|
65
|
+
const branchPrefix = featureBranchName
|
|
66
|
+
? featureBranchName.replace(/\/parent$/, "")
|
|
67
|
+
: `feat/${runId}`;
|
|
68
|
+
const branchName = `${branchPrefix}/${taskId}`;
|
|
69
|
+
const worktreePath = await worktreeManager.createTaskWorktree(taskId, branchName, featureBranchName);
|
|
62
70
|
logger.info({ taskId, worktreePath, branchName }, "Worktree created");
|
|
63
71
|
|
|
64
72
|
const workspace = await detectWorkspace(worktreePath, logger);
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `aad update` — Self-update AAD to the latest version
|
|
3
|
+
*/
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import packageJson from "../../../../package.json" with { type: "json" };
|
|
6
|
+
|
|
7
|
+
interface UpdateOptions {
|
|
8
|
+
check: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async function exec(
|
|
12
|
+
cmd: string[],
|
|
13
|
+
): Promise<{ stdout: string; stderr: string; exitCode: number }> {
|
|
14
|
+
const proc = Bun.spawn(cmd, {
|
|
15
|
+
stdout: "pipe",
|
|
16
|
+
stderr: "pipe",
|
|
17
|
+
});
|
|
18
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
19
|
+
new Response(proc.stdout).text(),
|
|
20
|
+
new Response(proc.stderr).text(),
|
|
21
|
+
proc.exited,
|
|
22
|
+
]);
|
|
23
|
+
return { stdout: stdout.trim(), stderr: stderr.trim(), exitCode };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function getLatestVersion(): Promise<string | null> {
|
|
27
|
+
try {
|
|
28
|
+
const result = await exec(["npm", "view", "@ronkovic/aad", "version"]);
|
|
29
|
+
if (result.exitCode === 0 && result.stdout) {
|
|
30
|
+
return result.stdout;
|
|
31
|
+
}
|
|
32
|
+
} catch {
|
|
33
|
+
// ignore
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function compareVersions(current: string, latest: string): number {
|
|
39
|
+
const c = current.split(".").map(Number);
|
|
40
|
+
const l = latest.split(".").map(Number);
|
|
41
|
+
for (let i = 0; i < 3; i++) {
|
|
42
|
+
if ((c[i] ?? 0) < (l[i] ?? 0)) return -1;
|
|
43
|
+
if ((c[i] ?? 0) > (l[i] ?? 0)) return 1;
|
|
44
|
+
}
|
|
45
|
+
return 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function detectPackageManager(): "bun" | "npm" {
|
|
49
|
+
if (process.env.BUN_INSTALL || process.argv[0]?.includes("bun")) {
|
|
50
|
+
return "bun";
|
|
51
|
+
}
|
|
52
|
+
return "npm";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function clearBunxCache(): Promise<boolean> {
|
|
56
|
+
const cacheDir = `/private/tmp/bunx-${process.getuid?.() ?? 502}-@ronkovic/`;
|
|
57
|
+
try {
|
|
58
|
+
const result = await exec(["rm", "-rf", cacheDir]);
|
|
59
|
+
return result.exitCode === 0;
|
|
60
|
+
} catch {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function createUpdateCommand(): Command {
|
|
66
|
+
return new Command("update")
|
|
67
|
+
.description("Update AAD to the latest version")
|
|
68
|
+
.option("--check", "Check for updates without installing", false)
|
|
69
|
+
.action(async (opts: UpdateOptions) => {
|
|
70
|
+
const currentVersion = packageJson.version;
|
|
71
|
+
console.log(`\n 現在のバージョン: v${currentVersion}`);
|
|
72
|
+
|
|
73
|
+
console.log(" 最新バージョンを確認中...");
|
|
74
|
+
const latestVersion = await getLatestVersion();
|
|
75
|
+
|
|
76
|
+
if (!latestVersion) {
|
|
77
|
+
console.log(" \x1b[31m✗\x1b[0m レジストリからバージョン情報を取得できませんでした\n");
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
console.log(` 最新バージョン: v${latestVersion}`);
|
|
82
|
+
|
|
83
|
+
const cmp = compareVersions(currentVersion, latestVersion);
|
|
84
|
+
if (cmp >= 0) {
|
|
85
|
+
console.log(" \x1b[32m✓\x1b[0m 最新バージョンです\n");
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (opts.check) {
|
|
90
|
+
console.log(` \x1b[33m⚠\x1b[0m 更新可能: v${currentVersion} → v${latestVersion}\n`);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Perform update
|
|
95
|
+
const pm = await detectPackageManager();
|
|
96
|
+
console.log(`\n パッケージマネージャ: ${pm}`);
|
|
97
|
+
|
|
98
|
+
if (pm === "bun") {
|
|
99
|
+
console.log(" bunxキャッシュをクリア中...");
|
|
100
|
+
await clearBunxCache();
|
|
101
|
+
console.log(" \x1b[32m✓\x1b[0m キャッシュクリア完了");
|
|
102
|
+
console.log(`\n 次回 \`bunx @ronkovic/aad\` 実行時にv${latestVersion}が使用されます\n`);
|
|
103
|
+
} else {
|
|
104
|
+
console.log(" npm install中...");
|
|
105
|
+
const result = await exec([
|
|
106
|
+
"npm",
|
|
107
|
+
"install",
|
|
108
|
+
"-g",
|
|
109
|
+
`@ronkovic/aad@${latestVersion}`,
|
|
110
|
+
]);
|
|
111
|
+
if (result.exitCode === 0) {
|
|
112
|
+
console.log(` \x1b[32m✓\x1b[0m v${latestVersion}にアップデートしました\n`);
|
|
113
|
+
} else {
|
|
114
|
+
console.log(` \x1b[31m✗\x1b[0m アップデート失敗: ${result.stderr}\n`);
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
}
|
package/src/modules/cli/index.ts
CHANGED
|
@@ -21,6 +21,7 @@ export { createRunCommand, runPipeline } from "./commands/run";
|
|
|
21
21
|
export { createResumeCommand, resumeRun } from "./commands/resume";
|
|
22
22
|
export { createStatusCommand, displayStatus } from "./commands/status";
|
|
23
23
|
export { createCleanupCommand, cleanupWorktrees } from "./commands/cleanup";
|
|
24
|
+
export { createUpdateCommand } from "./commands/update";
|
|
24
25
|
|
|
25
26
|
export {
|
|
26
27
|
createShutdownManager,
|
|
@@ -34,11 +34,15 @@ describe("cleanupOrphanedFromPreviousRuns", () => {
|
|
|
34
34
|
};
|
|
35
35
|
}
|
|
36
36
|
if (args[0] === "branch" && args[1] === "--list") {
|
|
37
|
-
return
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
37
|
+
// Only return branches for matching prefix pattern
|
|
38
|
+
if (args[2] === "aad/*") {
|
|
39
|
+
return {
|
|
40
|
+
stdout: " aad/run1/task-001\n aad/run1/task-002\n",
|
|
41
|
+
stderr: "",
|
|
42
|
+
exitCode: 0,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
return { stdout: "", stderr: "", exitCode: 0 };
|
|
42
46
|
}
|
|
43
47
|
return { stdout: "", stderr: "", exitCode: 0 };
|
|
44
48
|
}),
|
|
@@ -71,15 +71,30 @@ describe("WorktreeManager", () => {
|
|
|
71
71
|
});
|
|
72
72
|
|
|
73
73
|
describe("createParentWorktree", () => {
|
|
74
|
-
test("creates worktree
|
|
74
|
+
test("creates worktree with feature branch", async () => {
|
|
75
75
|
const runId = createRunId("run-001");
|
|
76
76
|
const parentBranch = "main";
|
|
77
|
+
const featureBranch = "feat/auth-feature/parent";
|
|
77
78
|
|
|
78
|
-
const
|
|
79
|
+
const result = await worktreeManager.createParentWorktree(runId, parentBranch, featureBranch);
|
|
79
80
|
|
|
80
|
-
expect(worktreePath).toBe("/test/worktrees/parent-run-001");
|
|
81
|
+
expect(result.worktreePath).toBe("/test/worktrees/parent-run-001");
|
|
82
|
+
expect(result.branch).toBe(featureBranch);
|
|
81
83
|
expect(mockGitOps.gitExec).toHaveBeenCalledWith(
|
|
82
|
-
["worktree", "add", "/test/worktrees/parent-run-001", parentBranch],
|
|
84
|
+
["worktree", "add", "-b", featureBranch, "/test/worktrees/parent-run-001", parentBranch],
|
|
85
|
+
expect.objectContaining({ cwd: repoRoot })
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("uses default branch name when featureBranch not provided", async () => {
|
|
90
|
+
const runId = createRunId("run-001");
|
|
91
|
+
const parentBranch = "main";
|
|
92
|
+
|
|
93
|
+
const result = await worktreeManager.createParentWorktree(runId, parentBranch);
|
|
94
|
+
|
|
95
|
+
expect(result.branch).toBe("feat/run-001/parent");
|
|
96
|
+
expect(mockGitOps.gitExec).toHaveBeenCalledWith(
|
|
97
|
+
["worktree", "add", "-b", "feat/run-001/parent", "/test/worktrees/parent-run-001", parentBranch],
|
|
83
98
|
expect.objectContaining({ cwd: repoRoot })
|
|
84
99
|
);
|
|
85
100
|
});
|
|
@@ -137,10 +137,17 @@ export class BranchManager {
|
|
|
137
137
|
* Cleanup orphaned AAD branches (branches without worktrees)
|
|
138
138
|
*/
|
|
139
139
|
async cleanupOrphanBranches(runId?: RunId, force = false): Promise<string[]> {
|
|
140
|
-
// Support
|
|
140
|
+
// Support aad/*, conventional commit prefixes (feat/*, fix/*, etc.), and legacy aad-* patterns
|
|
141
|
+
const conventionalPrefixes = ["aad", "feat", "fix", "docs", "style", "refactor", "perf", "test", "chore"];
|
|
141
142
|
const patterns = runId
|
|
142
|
-
? [
|
|
143
|
-
|
|
143
|
+
? [
|
|
144
|
+
...conventionalPrefixes.map((p) => `${p}/${runId as string}/*`),
|
|
145
|
+
`aad-*-${runId as string}*`,
|
|
146
|
+
]
|
|
147
|
+
: [
|
|
148
|
+
...conventionalPrefixes.map((p) => `${p}/*`),
|
|
149
|
+
"aad-*",
|
|
150
|
+
];
|
|
144
151
|
|
|
145
152
|
this.logger?.info({ patterns, force }, "Cleaning up orphan branches");
|
|
146
153
|
|
|
@@ -41,10 +41,11 @@ export class WorktreeManager {
|
|
|
41
41
|
/**
|
|
42
42
|
* Create worktree for a task
|
|
43
43
|
*/
|
|
44
|
-
async createTaskWorktree(taskId: TaskId, branch: string): Promise<string> {
|
|
44
|
+
async createTaskWorktree(taskId: TaskId, branch: string, baseBranch?: string): Promise<string> {
|
|
45
45
|
const worktreePath = join(this.worktreeBase, taskId as string);
|
|
46
|
+
const startPoint = baseBranch ?? "HEAD";
|
|
46
47
|
|
|
47
|
-
this.logger?.info({ taskId, branch, worktreePath }, "Creating task worktree");
|
|
48
|
+
this.logger?.info({ taskId, branch, baseBranch: startPoint, worktreePath }, "Creating task worktree");
|
|
48
49
|
|
|
49
50
|
try {
|
|
50
51
|
// Ensure worktree base directory exists
|
|
@@ -53,9 +54,9 @@ export class WorktreeManager {
|
|
|
53
54
|
// Clean up stale worktree/branch if they already exist
|
|
54
55
|
await this.cleanupStaleWorktree(worktreePath, branch);
|
|
55
56
|
|
|
56
|
-
// Create worktree with new branch
|
|
57
|
+
// Create worktree with new branch from baseBranch (parent feature branch)
|
|
57
58
|
await this.gitOps.gitExec(
|
|
58
|
-
["worktree", "add", "-b", branch, worktreePath,
|
|
59
|
+
["worktree", "add", "-b", branch, worktreePath, startPoint],
|
|
59
60
|
{ cwd: this.repoRoot, logger: this.logger }
|
|
60
61
|
);
|
|
61
62
|
|
|
@@ -75,11 +76,11 @@ export class WorktreeManager {
|
|
|
75
76
|
* Handles cases where a previous run left behind artifacts.
|
|
76
77
|
*/
|
|
77
78
|
private async cleanupStaleWorktree(worktreePath: string, branch: string): Promise<void> {
|
|
78
|
-
// 1. Remove existing worktree directory if it exists
|
|
79
|
+
// 1. Remove existing worktree directory if it exists (no logger to suppress error-level output)
|
|
79
80
|
try {
|
|
80
81
|
await this.gitOps.gitExec(
|
|
81
82
|
["worktree", "remove", worktreePath, "--force"],
|
|
82
|
-
{ cwd: this.repoRoot
|
|
83
|
+
{ cwd: this.repoRoot }
|
|
83
84
|
);
|
|
84
85
|
this.logger?.info({ worktreePath }, "Removed stale worktree");
|
|
85
86
|
} catch {
|
|
@@ -98,11 +99,11 @@ export class WorktreeManager {
|
|
|
98
99
|
// Non-critical
|
|
99
100
|
}
|
|
100
101
|
|
|
101
|
-
// 3. Delete stale branch if it exists
|
|
102
|
+
// 3. Delete stale branch if it exists (no logger to suppress error-level git output)
|
|
102
103
|
try {
|
|
103
104
|
await this.gitOps.gitExec(
|
|
104
105
|
["branch", "-D", branch],
|
|
105
|
-
{ cwd: this.repoRoot
|
|
106
|
+
{ cwd: this.repoRoot }
|
|
106
107
|
);
|
|
107
108
|
this.logger?.info({ branch }, "Deleted stale branch");
|
|
108
109
|
} catch {
|
|
@@ -113,25 +114,32 @@ export class WorktreeManager {
|
|
|
113
114
|
/**
|
|
114
115
|
* Create worktree for parent branch (used for merging)
|
|
115
116
|
*/
|
|
116
|
-
async createParentWorktree(
|
|
117
|
+
async createParentWorktree(
|
|
118
|
+
runId: RunId,
|
|
119
|
+
parentBranch: string,
|
|
120
|
+
featureBranch?: string,
|
|
121
|
+
): Promise<{ worktreePath: string; branch: string }> {
|
|
117
122
|
const worktreePath = join(this.worktreeBase, `parent-${runId as string}`);
|
|
123
|
+
const branch = featureBranch ?? `feat/${runId as string}/parent`;
|
|
118
124
|
|
|
119
|
-
this.logger?.info({ runId, parentBranch, worktreePath }, "Creating parent worktree");
|
|
125
|
+
this.logger?.info({ runId, parentBranch, branch, worktreePath }, "Creating parent worktree");
|
|
120
126
|
|
|
121
127
|
try {
|
|
122
128
|
await this.fsOps.mkdir(this.worktreeBase, { recursive: true });
|
|
123
129
|
|
|
124
|
-
// Create worktree
|
|
130
|
+
// Create worktree with a new branch based on parentBranch
|
|
131
|
+
// e.g. git worktree add -b feat/auth-feature/parent <path> main
|
|
125
132
|
await this.gitOps.gitExec(
|
|
126
|
-
["worktree", "add", worktreePath, parentBranch],
|
|
133
|
+
["worktree", "add", "-b", branch, worktreePath, parentBranch],
|
|
127
134
|
{ cwd: this.repoRoot, logger: this.logger }
|
|
128
135
|
);
|
|
129
136
|
|
|
130
|
-
return worktreePath;
|
|
137
|
+
return { worktreePath, branch };
|
|
131
138
|
} catch (error) {
|
|
132
139
|
throw new GitWorkspaceError("Failed to create parent worktree", {
|
|
133
140
|
runId: runId as string,
|
|
134
141
|
parentBranch,
|
|
142
|
+
branch,
|
|
135
143
|
worktreePath,
|
|
136
144
|
error: error instanceof Error ? error.message : String(error),
|
|
137
145
|
});
|
|
@@ -279,28 +287,36 @@ export async function cleanupOrphanedFromPreviousRuns(
|
|
|
279
287
|
// non-critical
|
|
280
288
|
}
|
|
281
289
|
|
|
282
|
-
// 3. Delete all branches matching aad
|
|
290
|
+
// 3. Delete all branches matching aad/*, feat/*, fix/*, etc.
|
|
291
|
+
const branchPrefixes = ["aad", "feat", "fix", "docs", "style", "refactor", "perf", "test", "chore"];
|
|
283
292
|
try {
|
|
284
293
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
285
294
|
const gitOps = (worktreeManager as any).gitOps as GitOps;
|
|
286
295
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
287
296
|
const repoRoot = (worktreeManager as any).repoRoot as string;
|
|
288
|
-
const result = await gitOps.gitExec(
|
|
289
|
-
["branch", "--list", "aad/*"],
|
|
290
|
-
{ cwd: repoRoot, logger },
|
|
291
|
-
);
|
|
292
|
-
const branches = result.stdout
|
|
293
|
-
.split("\n")
|
|
294
|
-
.map((b: string) => b.trim().replace(/^\* /, ""))
|
|
295
|
-
.filter((b: string) => b.length > 0);
|
|
296
297
|
|
|
297
|
-
for (const
|
|
298
|
+
for (const prefix of branchPrefixes) {
|
|
298
299
|
try {
|
|
299
|
-
await gitOps.gitExec(
|
|
300
|
-
|
|
301
|
-
|
|
300
|
+
const result = await gitOps.gitExec(
|
|
301
|
+
["branch", "--list", `${prefix}/*`],
|
|
302
|
+
{ cwd: repoRoot },
|
|
303
|
+
);
|
|
304
|
+
const branches = result.stdout
|
|
305
|
+
.split("\n")
|
|
306
|
+
.map((b: string) => b.trim().replace(/^[*+]\s*/, ""))
|
|
307
|
+
.filter((b: string) => b.length > 0);
|
|
308
|
+
|
|
309
|
+
for (const branch of branches) {
|
|
310
|
+
try {
|
|
311
|
+
await gitOps.gitExec(["branch", "-D", branch], { cwd: repoRoot });
|
|
312
|
+
deletedBranches++;
|
|
313
|
+
logger.debug({ branch }, "Deleted orphaned branch");
|
|
314
|
+
} catch {
|
|
315
|
+
logger.debug({ branch }, "Failed to delete orphaned branch (ignored)");
|
|
316
|
+
}
|
|
317
|
+
}
|
|
302
318
|
} catch {
|
|
303
|
-
|
|
319
|
+
// Pattern didn't match any branches
|
|
304
320
|
}
|
|
305
321
|
}
|
|
306
322
|
} catch (error) {
|