@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.
Files changed (26) hide show
  1. package/package.json +4 -3
  2. package/src/__tests__/e2e/pipeline-e2e.test.ts +6 -0
  3. package/src/main.ts +10 -2
  4. package/src/modules/cli/__tests__/interactive/auth-step.test.ts +24 -0
  5. package/src/modules/cli/__tests__/interactive/interactive.test.ts +14 -0
  6. package/src/modules/cli/__tests__/interactive/process-supervisor.test.ts +45 -0
  7. package/src/modules/cli/__tests__/interactive/prompts.test.ts +75 -0
  8. package/src/modules/cli/__tests__/update.test.ts +20 -0
  9. package/src/modules/cli/commands/interactive/auth-step.ts +61 -0
  10. package/src/modules/cli/commands/interactive/launch-step.ts +76 -0
  11. package/src/modules/cli/commands/interactive/process-supervisor.ts +105 -0
  12. package/src/modules/cli/commands/interactive/project-step.ts +157 -0
  13. package/src/modules/cli/commands/interactive/prompts.ts +75 -0
  14. package/src/modules/cli/commands/interactive.ts +38 -0
  15. package/src/modules/cli/commands/resume.ts +16 -0
  16. package/src/modules/cli/commands/run.ts +28 -7
  17. package/src/modules/cli/commands/task-dispatch-handler.ts +11 -3
  18. package/src/modules/cli/commands/update.ts +119 -0
  19. package/src/modules/cli/index.ts +1 -0
  20. package/src/modules/git-workspace/__tests__/worktree-cleanup.test.ts +9 -5
  21. package/src/modules/git-workspace/__tests__/worktree-manager.test.ts +19 -4
  22. package/src/modules/git-workspace/branch-manager.ts +10 -3
  23. package/src/modules/git-workspace/worktree-manager.ts +43 -27
  24. package/src/modules/planning/__tests__/feature-name-generator.test.ts +124 -0
  25. package/src/modules/planning/feature-name-generator.ts +103 -0
  26. 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. Create parent worktree for merging task branches
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
- parentWorktreePath = await worktreeManager.createParentWorktree(runId, parentBranch);
151
- logger.info({ parentWorktreePath }, "Parent worktree created for merging");
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) // Check branch name for 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
- const deletedBranches = await branchManager.cleanupOrphanBranches(runId, true);
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
- const branchName = `aad/${runId}/${taskId}`;
61
- const worktreePath = await worktreeManager.createTaskWorktree(taskId, branchName);
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
+ }
@@ -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
- stdout: " aad/run1/task-001\n aad/run1/task-002\n",
39
- stderr: "",
40
- exitCode: 0,
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 on existing branch", async () => {
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 worktreePath = await worktreeManager.createParentWorktree(runId, parentBranch);
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 both aad/* (slash) and aad-* (hyphen) patterns for backward compatibility
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
- ? [`aad/${runId as string}/*`, `aad-*-${runId as string}*`]
143
- : ["aad/*", "aad-*"];
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, "HEAD"],
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, logger: this.logger }
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, logger: this.logger }
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(runId: RunId, parentBranch: string): Promise<string> {
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 on existing branch (no -b flag)
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 branch of branches) {
298
+ for (const prefix of branchPrefixes) {
298
299
  try {
299
- await gitOps.gitExec(["branch", "-D", branch], { cwd: repoRoot, logger });
300
- deletedBranches++;
301
- logger.debug({ branch }, "Deleted orphaned branch");
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
- logger.debug({ branch }, "Failed to delete orphaned branch (ignored)");
319
+ // Pattern didn't match any branches
304
320
  }
305
321
  }
306
322
  } catch (error) {