feat(FN-4904): complete Step 1 — promote worktree root resolver

Fusion-Task-Id: FN-4904
Fusion-Task-Lineage: 5da52c8c-0ed6-4e3a-920e-aa1b56d0b719
This commit is contained in:
Fusion (runfusion.ai)
2026-05-17 10:28:51 -07:00
committed by gsxdsm
parent 3d729c7a50
commit 87d259b366
4 changed files with 63 additions and 37 deletions

View File

@@ -0,0 +1,28 @@
import { describe, expect, it } from "vitest";
import { getProjectRootFromWorktree } from "../pi-extensions.js";
describe("getProjectRootFromWorktree", () => {
it("detects POSIX worktree paths", () => {
expect(getProjectRootFromWorktree("/repo/.worktrees/fn-001")).toBe("/repo");
expect(getProjectRootFromWorktree("/repo/.worktrees/fn-001/src/file.ts")).toBe("/repo");
});
it("detects Windows worktree paths", () => {
expect(getProjectRootFromWorktree("C:\\repo\\.worktrees\\fn-001")).toBe("C:\\repo");
expect(getProjectRootFromWorktree("C:\\repo\\.worktrees\\fn-001\\src\\file.ts")).toBe("C:\\repo");
});
it("supports configured candidate worktrees dir paths", () => {
expect(
getProjectRootFromWorktree("/tmp/.fn-worktrees/repo/fn-001/src", {
worktreesDirCandidates: ["/tmp/.fn-worktrees/repo"],
}),
).toBe("/tmp/.fn-worktrees");
expect(
getProjectRootFromWorktree("/tmp/repo.worktrees/fn-001", {
worktreesDirCandidates: ["/tmp/repo.worktrees"],
}),
).toBe("/tmp");
});
});

View File

@@ -194,7 +194,7 @@ export {
MasterKeyPermissionError,
MasterKeyCorruptError,
} from "./master-key.js";
export { discoverPiExtensions, formatPiExtensionSource, getEnabledPiExtensionPaths, getFusionAgentDir, getFusionAgentSettingsPath, getLegacyPiAgentDir, getPiExtensionDiscoveryDirs, reconcileClaudeCliPaths, reconcileDroidCliPaths, resolvePiExtensionProjectRoot, updatePiExtensionDisabledIds } from "./pi-extensions.js";
export { discoverPiExtensions, formatPiExtensionSource, getEnabledPiExtensionPaths, getFusionAgentDir, getFusionAgentSettingsPath, getLegacyPiAgentDir, getPiExtensionDiscoveryDirs, getProjectRootFromWorktree, reconcileClaudeCliPaths, reconcileDroidCliPaths, resolvePiExtensionProjectRoot, updatePiExtensionDisabledIds } from "./pi-extensions.js";
export type { PiExtensionEntry, PiExtensionSettings, PiExtensionSource } from "./pi-extensions.js";
export { canTransition, getValidTransitions, resolveDependencyOrder } from "./board.js";
export { computeBlockerFanoutMap, BLOCKER_ESCALATION_COLUMNS, isStaleBlockedByBlocker } from "./blocker-fanout.js";

View File

@@ -36,7 +36,39 @@ export function getFusionAgentSettingsPath(home?: string): string {
return join(getFusionAgentDir(home), "settings.json");
}
export function getProjectRootFromWorktree(
cwd: string,
opts?: { worktreesDirCandidates?: string[] },
): string | null {
const legacyMatch = cwd.match(/^(.+?)[\\/]\.worktrees[\\/][^\\/]+(?:[\\/]|$)/);
if (legacyMatch) {
return legacyMatch[1]!;
}
for (const candidate of opts?.worktreesDirCandidates ?? []) {
const normalizedCandidate = resolve(candidate);
const normalizedCwd = resolve(cwd);
const rel = relative(normalizedCandidate, normalizedCwd);
if (rel !== "" && !rel.startsWith("..") && !isAbsolute(rel)) {
const firstSegment = rel.split(/[\\/]/).filter(Boolean)[0];
if (firstSegment) {
const parent = normalizedCandidate.split(/[\\/]/).slice(0, -1).join("/");
if (parent) {
return parent;
}
}
}
}
return null;
}
export function resolvePiExtensionProjectRoot(cwd: string): string {
const worktreeProjectRoot = getProjectRootFromWorktree(cwd);
if (worktreeProjectRoot && existsSync(join(worktreeProjectRoot, ".fusion"))) {
return worktreeProjectRoot;
}
let current = resolve(cwd);
while (true) {
if (existsSync(join(current, ".fusion"))) {

View File

@@ -34,7 +34,7 @@ import {
type AgentSession,
type ToolDefinition,
} from "@mariozechner/pi-coding-agent";
import { getEnabledPiExtensionPaths, getFusionAgentDir, getLegacyPiAgentDir, reconcileClaudeCliPaths, reconcileDroidCliPaths, resolvePiExtensionProjectRoot } from "@fusion/core";
import { getEnabledPiExtensionPaths, getFusionAgentDir, getLegacyPiAgentDir, getProjectRootFromWorktree, reconcileClaudeCliPaths, reconcileDroidCliPaths, resolvePiExtensionProjectRoot } from "@fusion/core";
import type {
AgentPermissionPolicyActionCategory,
PermanentAgentActionCategory,
@@ -1225,41 +1225,7 @@ async function registerExtensionProviders(cwd: string, modelRegistry: ModelRegis
// ── Worktree Path Boundary Helpers ──────────────────────────────────────────
/**
* Detect if a path is a task worktree under `.worktrees/`.
* Returns the project root if the path is a worktree, otherwise null.
*
* Examples:
* `/project/.worktrees/fn-001` → `/project`
* `/project/.worktrees/fn-001/src/file.ts` → `/project`
* `/project` → null (not a worktree)
*/
export function getProjectRootFromWorktree(
cwd: string,
opts?: { worktreesDirCandidates?: string[] },
): string | null {
const legacyMatch = cwd.match(/^(.+?)[\\/]\.worktrees[\\/][^\\/]+(?:[\\/]|$)/);
if (legacyMatch) {
return legacyMatch[1]!;
}
for (const candidate of opts?.worktreesDirCandidates ?? []) {
const normalizedCandidate = resolve(candidate);
const normalizedCwd = resolve(cwd);
const rel = relative(normalizedCandidate, normalizedCwd);
if (rel !== "" && !rel.startsWith("..") && !isAbsolute(rel)) {
const firstSegment = rel.split(/[\\/]/).filter(Boolean)[0];
if (firstSegment) {
const parent = normalizedCandidate.split(/[\\/]/).slice(0, -1).join("/");
if (parent) {
return parent;
}
}
}
}
return null;
}
export { getProjectRootFromWorktree };
async function isRegisteredGitWorktree(projectRoot: string, worktreePath: string): Promise<boolean> {
try {