refactor: unify errand worktree paths to use agent-workdirs/<alias>/

Errands now create worktrees via ProcessManager.createWorktreesForProjects()
into agent-workdirs/<alias>/<project.name>/ instead of repos/<project>/.cw-worktrees/<errandId>.
This makes getAgentWorkdir + resolveAgentCwd work correctly for all agent types.

Key changes:
- Extract createWorktreesForProjects() from createProjectWorktrees() in ProcessManager
- Add resolveAgentCwd() to ProcessManager (probes for .cw/output in subdirs)
- Add projectId to SpawnAgentOptions for single-project agents (errands)
- Skip auto-cleanup for errand agents (worktrees persist for merge/abandon)
- Errand router uses agentManager.delete() for cleanup instead of SimpleGitWorktreeManager
- Remove cwd parameter from sendUserMessage (resolves via worktreeId)
- Add pruneProjectRepos() to CleanupManager for errand worktree refs
This commit is contained in:
Lukas May
2026-03-07 00:02:27 +01:00
parent b17c0a2b4f
commit c52fa86542
7 changed files with 133 additions and 101 deletions

View File

@@ -7,7 +7,7 @@
*/
import { spawn } from 'node:child_process';
import { openSync, closeSync, existsSync } from 'node:fs';
import { openSync, closeSync, existsSync, readdirSync } from 'node:fs';
import { mkdir, writeFile } from 'node:fs/promises';
import { join } from 'node:path';
import type { ProjectRepository } from '../db/repositories/project-repository.js';
@@ -46,6 +46,36 @@ export class ProcessManager {
return join(this.workspaceRoot, 'agent-workdirs', alias);
}
/**
* Resolve the actual working directory for an agent by probing
* for the subdirectory that contains .cw/output/.
*/
resolveAgentCwd(alias: string): string {
const base = this.getAgentWorkdir(alias);
// Fast path: .cw/output exists at the base level
if (existsSync(join(base, '.cw', 'output'))) return base;
// Standalone agents use a workspace/ subdirectory
const workspaceSub = join(base, 'workspace');
if (existsSync(join(workspaceSub, '.cw'))) return workspaceSub;
// Initiative/errand agents may have written .cw/ inside a project
// subdirectory (e.g. agent-workdirs/<name>/<project-name>/.cw/).
try {
for (const entry of readdirSync(base, { withFileTypes: true })) {
if (entry.isDirectory() && entry.name !== '.cw') {
const sub = join(base, entry.name);
if (existsSync(join(sub, '.cw', 'output'))) return sub;
}
}
} catch {
// base dir may not exist
}
return base;
}
/**
* Create worktrees for all projects linked to an initiative.
* Returns the base agent workdir path.
@@ -57,13 +87,11 @@ export class ProcessManager {
branchName?: string,
): Promise<string> {
const projects = await this.projectRepository.findProjectsByInitiativeId(initiativeId);
const agentWorkdir = this.getAgentWorkdir(alias);
log.debug({
alias,
initiativeId,
projectCount: projects.length,
agentWorkdir,
baseBranch
}, 'creating project worktrees');
@@ -74,6 +102,22 @@ export class ProcessManager {
return this.createStandaloneWorktree(alias);
}
return this.createWorktreesForProjects(alias, projects, baseBranch, branchName);
}
/**
* Create worktrees for a given list of projects under agent-workdirs/<alias>/.
* Used by both initiative-based and single-project (errand) agents.
* Returns the base agent workdir path.
*/
async createWorktreesForProjects(
alias: string,
projects: Array<{ name: string; url: string; id: string; defaultBranch: string }>,
baseBranch?: string,
branchName?: string,
): Promise<string> {
const agentWorkdir = this.getAgentWorkdir(alias);
for (const project of projects) {
const clonePath = await ensureProjectClone(project, this.workspaceRoot);
const worktreeManager = new SimpleGitWorktreeManager(clonePath, undefined, agentWorkdir);