| import { execFile } from 'node:child_process'; |
| import { mkdir, realpath, stat } from 'node:fs/promises'; |
| import { isAbsolute, join, normalize } from 'node:path'; |
| import { promisify } from 'node:util'; |
| import { |
| SUBAGENT_WORKSPACE_BINDING_SCHEMA_VERSION, |
| isSubagentWorkspaceBinding, |
| type ProvisionSubagentWorktreeInput, |
| type SubagentWorkspaceBinding, |
| type SubagentWorktreeExecutor, |
| } from '@maka/core'; |
| import { resolveProjectLocation } from './project-catalog.js'; |
| |
| const execFileAsync = promisify(execFile); |
| const LEASE_PATTERN = /^subagent_worktree_([a-f0-9]{32})$/; |
| const GIT_TIMEOUT_MS = 2 * 60 * 1_000; |
| |
| export interface CreateGitWorktreeChildExecutorInput { |
| storageRoot: string; |
| } |
| |
| /** |
| * Host-owned Git worktree allocator for linked child Sessions. |
| * |
| * Lease identity, branch, and path are deterministic. A retry therefore |
| * adopts the same worktree instead of creating a second filesystem side |
| * effect. Worktrees intentionally survive terminal child runs so Session |
| * resume/follow-up keeps the exact workspace. |
| */ |
| export function createGitWorktreeChildExecutor( |
| input: CreateGitWorktreeChildExecutorInput, |
| ): SubagentWorktreeExecutor { |
| return new GitWorktreeChildExecutor(join(input.storageRoot, 'subagent-worktrees')); |
| } |
| |
| class GitWorktreeChildExecutor implements SubagentWorktreeExecutor { |
| private readonly inFlight = new Map<string, Promise<SubagentWorkspaceBinding>>(); |
| private readonly repositoryTails = new Map<string, Promise<void>>(); |
| |
| constructor(private readonly worktreeRoot: string) {} |
| |
| async provision(input: ProvisionSubagentWorktreeInput): Promise<SubagentWorkspaceBinding> { |
| const suffix = leaseSuffix(input.leaseId); |
| const existing = this.inFlight.get(input.leaseId); |
| if (existing) return existing; |
| const task = this.provisionOnce(input, suffix).finally(() => { |
| if (this.inFlight.get(input.leaseId) === task) this.inFlight.delete(input.leaseId); |
| }); |
| this.inFlight.set(input.leaseId, task); |
| return task; |
| } |
| |
| async ensure(binding: SubagentWorkspaceBinding): Promise<void> { |
| if (!isSubagentWorkspaceBinding(binding)) { |
| throw new Error('Invalid subagent worktree binding'); |
| } |
| const inspected = await this.inspectOwnedWorktree(binding.worktreePath, binding.branch); |
| if (!inspected) { |
| throw new Error(`Subagent worktree is unavailable: ${binding.worktreePath}`); |
| } |
| if ( |
| inspected.gitCommonDir !== normalize(binding.gitCommonDir) || |
| inspected.baseCommit !== binding.baseCommit |
| ) { |
| throw new Error(`Subagent worktree binding changed: ${binding.worktreePath}`); |
| } |
| const lease = await gitConfigGet(inspected.worktreePath, branchLeaseConfigKey(binding.branch)); |
| if (lease !== binding.leaseId) { |
| throw new Error(`Subagent worktree lease changed: ${binding.worktreePath}`); |
| } |
| } |
| |
| private async provisionOnce( |
| input: ProvisionSubagentWorktreeInput, |
| suffix: string, |
| ): Promise<SubagentWorkspaceBinding> { |
| if (!input.sourceSessionId) throw new Error('Subagent worktree source Session is required'); |
| const source = await resolveProjectLocation({ path: input.sourceCwd }); |
| if (source.kind !== 'git' || !source.git) { |
| throw new Error('Worktree child execution requires a Git project'); |
| } |
| const root = await ensureDirectory(this.worktreeRoot); |
| const worktreePath = join(root, suffix); |
| const branch = `maka/subagent/${suffix}`; |
| const gitCommonDir = normalize(source.git.commonDir); |
| return this.withRepositoryAllocation(gitCommonDir, () => |
| this.provisionResolved(input.leaseId, source.git!.worktreeRoot, { |
| worktreePath, |
| branch, |
| gitCommonDir, |
| }), |
| ); |
| } |
| |
| private async provisionResolved( |
| leaseId: string, |
| sourceWorktreeRoot: string, |
| target: { |
| worktreePath: string; |
| branch: string; |
| gitCommonDir: string; |
| }, |
| ): Promise<SubagentWorkspaceBinding> { |
| const { worktreePath, branch, gitCommonDir } = target; |
| const adopted = await this.inspectOwnedWorktree(worktreePath, branch); |
| if (adopted) { |
| if (adopted.gitCommonDir !== gitCommonDir) { |
| throw new Error(`Subagent worktree belongs to another Git repository: ${worktreePath}`); |
| } |
| return this.finalizeBinding(leaseId, adopted); |
| } |
| |
| const branchCommit = await gitRevParseOptional(sourceWorktreeRoot, branch); |
| const storedLease = await gitConfigGet(sourceWorktreeRoot, branchLeaseConfigKey(branch)); |
| if (branchCommit && storedLease !== leaseId) { |
| throw new Error(`Subagent worktree branch is already owned: ${branch}`); |
| } |
| |
| let baseCommit: string; |
| if (branchCommit) { |
| baseCommit = |
| (await gitConfigGet(sourceWorktreeRoot, branchBaseConfigKey(branch))) ?? branchCommit; |
| await runGit(sourceWorktreeRoot, ['worktree', 'add', '--quiet', worktreePath, branch]); |
| } else { |
| await assertCleanGitWorktree(sourceWorktreeRoot); |
| baseCommit = await gitRevParse(sourceWorktreeRoot, 'HEAD'); |
| await runGit(sourceWorktreeRoot, [ |
| 'worktree', |
| 'add', |
| '--quiet', |
| '-b', |
| branch, |
| worktreePath, |
| baseCommit, |
| ]); |
| } |
| |
| const inspected = await this.inspectOwnedWorktree(worktreePath, branch); |
| if (!inspected || inspected.gitCommonDir !== gitCommonDir) { |
| throw new Error(`Git did not create the expected subagent worktree: ${worktreePath}`); |
| } |
| await setBranchLease(worktreePath, branch, leaseId, baseCommit); |
| return { |
| schemaVersion: SUBAGENT_WORKSPACE_BINDING_SCHEMA_VERSION, |
| kind: 'git_worktree', |
| leaseId, |
| gitCommonDir, |
| worktreePath: inspected.worktreePath, |
| branch, |
| baseCommit, |
| }; |
| } |
| |
| private async withRepositoryAllocation<T>( |
| gitCommonDir: string, |
| operation: () => Promise<T>, |
| ): Promise<T> { |
| const previous = this.repositoryTails.get(gitCommonDir) ?? Promise.resolve(); |
| let release!: () => void; |
| const current = new Promise<void>((resolve) => { |
| release = resolve; |
| }); |
| const tail = previous.then(() => current); |
| this.repositoryTails.set(gitCommonDir, tail); |
| await previous; |
| try { |
| return await operation(); |
| } finally { |
| release(); |
| if (this.repositoryTails.get(gitCommonDir) === tail) { |
| this.repositoryTails.delete(gitCommonDir); |
| } |
| } |
| } |
| |
| private async finalizeBinding( |
| leaseId: string, |
| inspected: InspectedWorktree, |
| ): Promise<SubagentWorkspaceBinding> { |
| const lease = await gitConfigGet( |
| inspected.worktreePath, |
| branchLeaseConfigKey(inspected.branch), |
| ); |
| if (lease && lease !== leaseId) { |
| throw new Error(`Subagent worktree lease changed: ${inspected.worktreePath}`); |
| } |
| const baseCommit = |
| (await gitConfigGet(inspected.worktreePath, branchBaseConfigKey(inspected.branch))) ?? |
| (await gitRevParse(inspected.worktreePath, inspected.branch)); |
| await setBranchLease(inspected.worktreePath, inspected.branch, leaseId, baseCommit); |
| return { |
| schemaVersion: SUBAGENT_WORKSPACE_BINDING_SCHEMA_VERSION, |
| kind: 'git_worktree', |
| leaseId, |
| gitCommonDir: inspected.gitCommonDir, |
| worktreePath: inspected.worktreePath, |
| branch: inspected.branch, |
| baseCommit, |
| }; |
| } |
| |
| private async inspectOwnedWorktree( |
| path: string, |
| expectedBranch: string, |
| ): Promise<InspectedWorktree | undefined> { |
| if (!(await isDirectory(path))) return undefined; |
| const location = await resolveProjectLocation({ path }); |
| if (location.kind !== 'git' || !location.git?.isWorktree) { |
| throw new Error(`Subagent workspace is not a linked Git worktree: ${path}`); |
| } |
| const branch = ( |
| await runGit(location.git.worktreeRoot, ['symbolic-ref', '--quiet', '--short', 'HEAD']) |
| ).trim(); |
| if (branch !== expectedBranch) { |
| throw new Error(`Subagent worktree branch changed from ${expectedBranch} to ${branch}`); |
| } |
| const baseCommit = |
| (await gitConfigGet(location.git.worktreeRoot, branchBaseConfigKey(branch))) ?? |
| (await gitRevParse(location.git.worktreeRoot, branch)); |
| return { |
| worktreePath: normalize(await realpath(location.git.worktreeRoot)), |
| gitCommonDir: normalize(location.git.commonDir), |
| branch, |
| baseCommit, |
| }; |
| } |
| } |
| |
| interface InspectedWorktree { |
| worktreePath: string; |
| gitCommonDir: string; |
| branch: string; |
| baseCommit: string; |
| } |
| |
| async function assertCleanGitWorktree(path: string): Promise<void> { |
| const status = await runGit(path, [ |
| 'status', |
| '--porcelain=v1', |
| '--untracked-files=normal', |
| '--ignore-submodules=none', |
| ]); |
| if (status.trim()) { |
| throw new Error( |
| 'Worktree child execution requires the source Git worktree to have no uncommitted changes', |
| ); |
| } |
| } |
| |
| async function setBranchLease( |
| cwd: string, |
| branch: string, |
| leaseId: string, |
| baseCommit: string, |
| ): Promise<void> { |
| await runGit(cwd, ['config', '--local', branchLeaseConfigKey(branch), leaseId]); |
| await runGit(cwd, ['config', '--local', branchBaseConfigKey(branch), baseCommit]); |
| } |
| |
| function branchLeaseConfigKey(branch: string): string { |
| return `branch.${branch}.maka-worktree-lease`; |
| } |
| |
| function branchBaseConfigKey(branch: string): string { |
| return `branch.${branch}.maka-worktree-base`; |
| } |
| |
| async function gitConfigGet(cwd: string, key: string): Promise<string | undefined> { |
| try { |
| const output = await runGit(cwd, ['config', '--local', '--get', key]); |
| return output.trim() || undefined; |
| } catch (error) { |
| if (gitExitCode(error) === 1) return undefined; |
| throw error; |
| } |
| } |
| |
| async function gitRevParse(cwd: string, ref: string): Promise<string> { |
| return (await runGit(cwd, ['rev-parse', '--verify', ref])).trim(); |
| } |
| |
| async function gitRevParseOptional(cwd: string, ref: string): Promise<string | undefined> { |
| try { |
| return await gitRevParse(cwd, ref); |
| } catch (error) { |
| if (gitExitCode(error) === 128) return undefined; |
| throw error; |
| } |
| } |
| |
| async function runGit(cwd: string, args: readonly string[]): Promise<string> { |
| const env: NodeJS.ProcessEnv = { ...process.env, GIT_OPTIONAL_LOCKS: '0' }; |
| delete env.GIT_DIR; |
| delete env.GIT_WORK_TREE; |
| delete env.GIT_INDEX_FILE; |
| delete env.GIT_COMMON_DIR; |
| const { stdout } = await execFileAsync('git', ['-C', cwd, ...args], { |
| env, |
| encoding: 'utf8', |
| maxBuffer: 1024 * 1024, |
| timeout: GIT_TIMEOUT_MS, |
| windowsHide: true, |
| }); |
| return stdout; |
| } |
| |
| function gitExitCode(error: unknown): number | undefined { |
| if (!error || typeof error !== 'object' || !('code' in error)) return undefined; |
| return typeof error.code === 'number' ? error.code : undefined; |
| } |
| |
| function leaseSuffix(leaseId: string): string { |
| const match = LEASE_PATTERN.exec(leaseId); |
| if (!match?.[1]) throw new Error(`Invalid subagent worktree lease id: ${leaseId}`); |
| return match[1]; |
| } |
| |
| async function ensureDirectory(path: string): Promise<string> { |
| await mkdir(path, { recursive: true }); |
| const canonical = normalize(await realpath(path)); |
| if (!isAbsolute(canonical) || !(await stat(canonical)).isDirectory()) { |
| throw new Error(`Invalid subagent worktree root: ${path}`); |
| } |
| return canonical; |
| } |
| |
| async function isDirectory(path: string): Promise<boolean> { |
| try { |
| return (await stat(path)).isDirectory(); |
| } catch (error) { |
| if ((error as NodeJS.ErrnoException).code === 'ENOENT') return false; |
| throw error; |
| } |
| } |