Files
fusion/packages/engine/src/worktree-paths.ts
gsxdsm 283adce4d0 FN-6295: move AI merge clean rooms into worktrees
Move AI-merge clean-room worktrees into the configured worktrees directory while preserving legacy cleanup.

- Resolve AI-merge roots through the configured worktrees directory using a hidden .ai-merge container.
- Keep local git excludes and stale cleanup compatible with legacy .fusion/ai-merge and tmpdir locations.
- Exclude the .ai-merge container from worktree pool and self-healing one-level sweeps.
- Add regression coverage for path resolution, cleanup, orphan reap, and cap enforcement behavior.
- Update architecture and agent guidance for the relocated clean-room root.

Files changed:
 AGENTS.md                                          |  2 +-
 docs/architecture.md                               |  4 +-
 .../engine/src/__tests__/merger-ai-cleanup.test.ts | 50 +++++++++----
 .../__tests__/self-healing-tempdir-sweep.test.ts   | 87 +++++++++++++++++++---
 .../engine/src/__tests__/worktree-paths.test.ts    | 24 ++++++
 .../engine/src/__tests__/worktree-pool.test.ts     | 55 ++++++++++++++
 packages/engine/src/merger-ai.ts                   | 27 +++++--
 packages/engine/src/self-healing.ts                | 25 +++----
 packages/engine/src/worktree-paths.ts              | 17 +++++
 packages/engine/src/worktree-pool.ts               | 10 +--
 10 files changed, 247 insertions(+), 54 deletions(-)

Fusion-Task-Id: FN-6295
Fusion-Task-Lineage: d95d32c2-3253-4964-b68a-252b4cb65109
2026-06-12 12:49:34 -07:00

73 lines
2.4 KiB
TypeScript

import { homedir } from "node:os";
import { basename, isAbsolute, join, relative, resolve } from "node:path";
import type { Settings } from "@fusion/core";
import type { WorktreeBackendKind } from "./worktree-backend.js";
import { canonicalizePath } from "./worktree-pool.js";
export const AI_MERGE_DIRNAME = ".ai-merge";
export function isAiMergeContainerDir(name: string): boolean {
return name === AI_MERGE_DIRNAME;
}
export function resolveAiMergeRootPath(
rootDir: string,
settings: Pick<Settings, "worktreesDir"> | undefined,
): string {
return join(resolveWorktreesDir(rootDir, settings), AI_MERGE_DIRNAME);
}
export function resolveLegacyAiMergeRootPath(rootDir: string): string {
return join(rootDir, ".fusion", "ai-merge");
}
export function resolveWorktreesDir(
rootDir: string,
settings: Pick<Settings, "worktreesDir"> | undefined,
): string {
const configured = settings?.worktreesDir;
if (!configured) {
return join(rootDir, ".worktrees");
}
const expandedHome = configured.replace(/^~(?=$|[\\/])/, homedir());
const expandedRepo = expandedHome.replaceAll("{repo}", basename(rootDir));
return resolve(rootDir, expandedRepo);
}
export function resolveTaskWorktreePath(
rootDir: string,
settings: Pick<Settings, "worktreesDir"> | undefined,
worktreeName: string,
): string {
return join(resolveWorktreesDir(rootDir, settings), worktreeName);
}
// Structural backend input avoids importing the full WorktreeBackend interface here.
export async function resolveTaskWorktreePathForBackend(
rootDir: string,
worktreeName: string,
settings: Pick<Settings, "worktreesDir"> | undefined,
backend: {
kind: WorktreeBackendKind;
resolveWorktreePath?: (input: { rootDir: string; worktreeName: string; branch: string }) => Promise<string>;
},
branch: string,
): Promise<string> {
if (backend.kind === "worktrunk" && backend.resolveWorktreePath) {
return backend.resolveWorktreePath({ rootDir, worktreeName, branch });
}
return resolveTaskWorktreePath(rootDir, settings, worktreeName);
}
export function isInsideConfiguredWorktreesDir(
rootDir: string,
settings: Pick<Settings, "worktreesDir"> | undefined,
candidate: string,
): boolean {
const worktreesDir = canonicalizePath(resolveWorktreesDir(rootDir, settings));
const target = canonicalizePath(candidate);
const rel = relative(worktreesDir, target);
return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
}