Add configurable worktree target path and session run diagnostics

This commit is contained in:
2026-02-23 20:38:05 -05:00
parent e7dbc9870f
commit 83bbf1a9ce
13 changed files with 434 additions and 7 deletions

View File

@@ -1,6 +1,6 @@
import { execFile } from "node:child_process";
import { createHash } from "node:crypto";
import { mkdir, open, unlink, writeFile } from "node:fs/promises";
import { mkdir, open, stat, unlink, writeFile } from "node:fs/promises";
import { dirname, isAbsolute, resolve } from "node:path";
import { promisify } from "node:util";
@@ -272,6 +272,7 @@ export class ResourceProvisioningOrchestrator {
export type GitWorktreeProviderConfig = {
rootDirectory: string;
baseRef: string;
targetPath?: string;
};
export type PortRangeProviderConfig = {
@@ -313,6 +314,10 @@ export function createGitWorktreeProvider(
provision: async ({ sessionId, workspaceRoot, options }) => {
const rootDirectory = readOptionalString(options, "rootDirectory", config.rootDirectory);
const baseRef = readOptionalString(options, "baseRef", config.baseRef);
const targetPath = normalizeWorktreeTargetPath(
readOptionalStringOrUndefined(options, "targetPath") ?? config.targetPath,
"targetPath",
);
const repoRoot = await runGit(["-C", workspaceRoot, "rev-parse", "--show-toplevel"]);
const worktreeRoot = resolvePath(repoRoot, rootDirectory);
@@ -321,6 +326,18 @@ export function createGitWorktreeProvider(
const worktreeName = buildScopedName(sessionId);
const worktreePath = resolve(worktreeRoot, worktreeName);
await runGit(["-C", repoRoot, "worktree", "add", "--detach", worktreePath, baseRef]);
if (targetPath) {
await runGit(["-C", worktreePath, "sparse-checkout", "init", "--cone"]);
await runGit(["-C", worktreePath, "sparse-checkout", "set", targetPath]);
}
const preferredWorkingDirectory = targetPath ? resolve(worktreePath, targetPath) : worktreePath;
await assertDirectoryExists(
preferredWorkingDirectory,
targetPath
? `Configured worktree target path "${targetPath}" is not a directory in ref "${baseRef}".`
: `Provisioned worktree path "${preferredWorkingDirectory}" does not exist.`,
);
return {
kind: "git-worktree",
@@ -329,6 +346,7 @@ export function createGitWorktreeProvider(
worktreeRoot,
worktreePath,
baseRef,
...(targetPath ? { targetPath } : {}),
},
soft: {
env: {
@@ -339,12 +357,14 @@ export function createGitWorktreeProvider(
promptSections: [
`Git worktree: ${worktreePath}`,
`Worktree base ref: ${baseRef}`,
...(targetPath ? [`Worktree target path: ${targetPath} (sparse-checkout enabled)`] : []),
],
metadata: {
git_worktree_path: worktreePath,
git_worktree_base_ref: baseRef,
...(targetPath ? { git_worktree_target_path: targetPath } : {}),
},
preferredWorkingDirectory: worktreePath,
preferredWorkingDirectory,
},
release: async () => {
await runGit(["-C", repoRoot, "worktree", "remove", "--force", worktreePath]);
@@ -576,6 +596,21 @@ function readOptionalString(
return value.trim();
}
function readOptionalStringOrUndefined(
options: Record<string, unknown>,
key: string,
): string | undefined {
const value = options[key];
if (value === undefined) {
return undefined;
}
if (typeof value !== "string") {
throw new Error(`Option "${key}" must be a string when provided.`);
}
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : undefined;
}
function readOptionalInteger(
options: Record<string, unknown>,
key: string,
@@ -595,6 +630,46 @@ function readOptionalInteger(
return value;
}
function normalizeWorktreeTargetPath(value: string | undefined, key: string): string | undefined {
if (!value) {
return undefined;
}
const slashNormalized = value.replace(/\\/g, "/");
if (isAbsolute(slashNormalized) || /^[a-zA-Z]:\//.test(slashNormalized)) {
throw new Error(`Option "${key}" must be a relative path within the repository worktree.`);
}
const normalizedSegments = slashNormalized
.split("/")
.map((segment) => segment.trim())
.filter((segment) => segment.length > 0 && segment !== ".");
if (normalizedSegments.some((segment) => segment === "..")) {
throw new Error(`Option "${key}" must not contain ".." path segments.`);
}
if (normalizedSegments.length === 0) {
return undefined;
}
return normalizedSegments.join("/");
}
async function assertDirectoryExists(path: string, errorMessage: string): Promise<void> {
try {
const stats = await stat(path);
if (!stats.isDirectory()) {
throw new Error(errorMessage);
}
} catch (error) {
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
throw new Error(errorMessage);
}
throw error;
}
}
function readNumberFromAllocation(allocation: Record<string, JsonValue>, key: string): number {
const value = allocation[key];
if (typeof value !== "number" || !Number.isInteger(value)) {
@@ -642,6 +717,8 @@ export function buildChildResourceRequests(input: ChildResourceSuballocationInpu
const parentWorktreePath = readStringFromAllocation(parentGit, "worktreePath");
const baseRefRaw = parentGit.baseRef;
const baseRef = typeof baseRefRaw === "string" && baseRefRaw.trim().length > 0 ? baseRefRaw : "HEAD";
const targetPathRaw = parentGit.targetPath;
const targetPath = typeof targetPathRaw === "string" ? targetPathRaw.trim() : "";
requests.push({
kind: "git-worktree",
@@ -652,6 +729,7 @@ export function buildChildResourceRequests(input: ChildResourceSuballocationInpu
buildScopedName(input.parentSnapshot.sessionId),
),
baseRef,
...(targetPath ? { targetPath } : {}),
},
});
}

View File

@@ -11,6 +11,7 @@ function toProvisioningConfig(input: Readonly<AppConfig>): BuiltInProvisioningCo
gitWorktree: {
rootDirectory: input.provisioning.gitWorktree.rootDirectory,
baseRef: input.provisioning.gitWorktree.baseRef,
targetPath: input.provisioning.gitWorktree.targetPath,
},
portRange: {
basePort: input.provisioning.portRange.basePort,