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:
@@ -19,8 +19,6 @@ import {
|
||||
import { writeErrandManifest } from '../../agent/file-io.js';
|
||||
import { buildErrandPrompt, buildErrandRevisionPrompt } from '../../agent/prompts/index.js';
|
||||
import { join } from 'node:path';
|
||||
import { existsSync, rmSync } from 'node:fs';
|
||||
import { SimpleGitWorktreeManager } from '../../git/manager.js';
|
||||
import { ensureProjectClone, getProjectCloneDir } from '../../git/project-clones.js';
|
||||
import type { TRPCContext } from '../context.js';
|
||||
|
||||
@@ -98,7 +96,7 @@ export function errandProcedures(publicProcedure: ProcedureBuilder) {
|
||||
});
|
||||
}
|
||||
|
||||
// 7.5. Create DB record early (agentId null) to get a stable ID for the worktree
|
||||
// 7.5. Create DB record early (agentId null)
|
||||
const repo = requireErrandRepository(ctx);
|
||||
let errand;
|
||||
try {
|
||||
@@ -121,37 +119,22 @@ export function errandProcedures(publicProcedure: ProcedureBuilder) {
|
||||
|
||||
const errandId = errand.id;
|
||||
|
||||
// 8. Create worktree using the DB-assigned errand ID
|
||||
const worktreeManager = new SimpleGitWorktreeManager(clonePath);
|
||||
let worktree;
|
||||
try {
|
||||
worktree = await worktreeManager.create(errandId, branchName, baseBranch);
|
||||
} catch (err) {
|
||||
// Clean up DB record and branch on worktree failure
|
||||
try { await repo.delete(errandId); } catch { /* no-op */ }
|
||||
try { await branchManager.deleteBranch(clonePath, branchName); } catch { /* no-op */ }
|
||||
throw new TRPCError({
|
||||
code: 'INTERNAL_SERVER_ERROR',
|
||||
message: err instanceof Error ? err.message : String(err),
|
||||
});
|
||||
}
|
||||
|
||||
// 9. Build prompt
|
||||
// 8. Build prompt
|
||||
const prompt = buildErrandPrompt(input.description);
|
||||
|
||||
// 10. Spawn agent
|
||||
// 9. Spawn agent — worktree created via projectId in agent-workdirs/<alias>/<project.name>/
|
||||
const agentManager = requireAgentManager(ctx);
|
||||
let agent;
|
||||
try {
|
||||
agent = await agentManager.spawn({
|
||||
prompt,
|
||||
mode: 'errand',
|
||||
cwd: worktree.path,
|
||||
provider: undefined,
|
||||
projectId: input.projectId,
|
||||
branchName,
|
||||
baseBranch,
|
||||
});
|
||||
} catch (err) {
|
||||
// Clean up worktree, DB record, and branch on spawn failure
|
||||
try { await worktreeManager.remove(errandId); } catch { /* no-op */ }
|
||||
// Clean up DB record and branch on spawn failure
|
||||
try { await repo.delete(errandId); } catch { /* no-op */ }
|
||||
try { await branchManager.deleteBranch(clonePath, branchName); } catch { /* no-op */ }
|
||||
throw new TRPCError({
|
||||
@@ -160,9 +143,10 @@ export function errandProcedures(publicProcedure: ProcedureBuilder) {
|
||||
});
|
||||
}
|
||||
|
||||
// 11. Write errand manifest files
|
||||
// 10. Write errand manifest files
|
||||
const agentWorkdir = join(ctx.workspaceRoot!, 'agent-workdirs', agent.name, project.name);
|
||||
await writeErrandManifest({
|
||||
agentWorkdir: worktree.path,
|
||||
agentWorkdir,
|
||||
errandId,
|
||||
description: input.description,
|
||||
branch: branchName,
|
||||
@@ -171,10 +155,10 @@ export function errandProcedures(publicProcedure: ProcedureBuilder) {
|
||||
agentName: agent.name,
|
||||
});
|
||||
|
||||
// 12. Update DB record with agent ID
|
||||
// 11. Update DB record with agent ID
|
||||
await repo.update(errandId, { agentId: agent.id });
|
||||
|
||||
// 13. Return result
|
||||
// 12. Return result
|
||||
return { id: errandId, branch: branchName, agentId: agent.id };
|
||||
}),
|
||||
|
||||
@@ -314,9 +298,11 @@ export function errandProcedures(publicProcedure: ProcedureBuilder) {
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
// Clean merge — remove worktree and mark merged
|
||||
const worktreeManager = new SimpleGitWorktreeManager(clonePath);
|
||||
try { await worktreeManager.remove(errand.id); } catch { /* no-op */ }
|
||||
// Clean merge — delete agent (removes worktree) and mark merged
|
||||
const agentManager = requireAgentManager(ctx);
|
||||
if (errand.agentId) {
|
||||
try { await agentManager.delete(errand.agentId); } catch { /* no-op */ }
|
||||
}
|
||||
await repo.update(input.id, { status: 'merged' });
|
||||
return { status: 'merged' };
|
||||
} else {
|
||||
@@ -345,18 +331,17 @@ export function errandProcedures(publicProcedure: ProcedureBuilder) {
|
||||
|
||||
const agentManager = requireAgentManager(ctx);
|
||||
|
||||
// Stop agent if active
|
||||
if (errand.status === 'active' && errand.agentId) {
|
||||
// Stop and delete agent (removes worktree + logs)
|
||||
if (errand.agentId) {
|
||||
try { await agentManager.stop(errand.agentId); } catch { /* no-op */ }
|
||||
try { await agentManager.delete(errand.agentId); } catch { /* no-op */ }
|
||||
}
|
||||
|
||||
// Remove worktree and branch (best-effort)
|
||||
// Delete branch (agent doesn't own the branch name)
|
||||
if (errand.projectId) {
|
||||
const project = await requireProjectRepository(ctx).findById(errand.projectId);
|
||||
if (project) {
|
||||
const clonePath = await resolveClonePath(project, ctx);
|
||||
const worktreeManager = new SimpleGitWorktreeManager(clonePath);
|
||||
try { await worktreeManager.remove(errand.id); } catch { /* no-op if already gone */ }
|
||||
try { await requireBranchManager(ctx).deleteBranch(clonePath, errand.branch); } catch { /* no-op */ }
|
||||
}
|
||||
}
|
||||
@@ -397,23 +382,7 @@ export function errandProcedures(publicProcedure: ProcedureBuilder) {
|
||||
});
|
||||
}
|
||||
|
||||
// Resolve errand worktree path — errand agents don't use agent-workdirs/
|
||||
let worktreePath: string | undefined;
|
||||
if (errand.projectId) {
|
||||
const project = await requireProjectRepository(ctx).findById(errand.projectId);
|
||||
if (project) {
|
||||
try {
|
||||
const clonePath = await resolveClonePath(project, ctx);
|
||||
const wm = new SimpleGitWorktreeManager(clonePath);
|
||||
const wt = await wm.get(errand.id);
|
||||
if (wt) worktreePath = wt.path;
|
||||
} catch {
|
||||
// Fall through — sendUserMessage will use default path
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await agentManager.sendUserMessage(errand.agentId, input.message, worktreePath);
|
||||
await agentManager.sendUserMessage(errand.agentId, input.message);
|
||||
return { success: true };
|
||||
}),
|
||||
|
||||
@@ -439,18 +408,17 @@ export function errandProcedures(publicProcedure: ProcedureBuilder) {
|
||||
const agentManager = requireAgentManager(ctx);
|
||||
const branchManager = requireBranchManager(ctx);
|
||||
|
||||
// Stop agent if active
|
||||
if (errand.status === 'active' && errand.agentId) {
|
||||
// Stop and delete agent (removes worktree + logs)
|
||||
if (errand.agentId) {
|
||||
try { await agentManager.stop(errand.agentId); } catch { /* no-op */ }
|
||||
try { await agentManager.delete(errand.agentId); } catch { /* no-op */ }
|
||||
}
|
||||
|
||||
// Remove worktree and branch (best-effort)
|
||||
// Delete branch
|
||||
if (errand.projectId) {
|
||||
const project = await requireProjectRepository(ctx).findById(errand.projectId);
|
||||
if (project) {
|
||||
const clonePath = await resolveClonePath(project, ctx);
|
||||
const worktreeManager = new SimpleGitWorktreeManager(clonePath);
|
||||
try { await worktreeManager.remove(errand.id); } catch { /* no-op if already gone */ }
|
||||
try { await branchManager.deleteBranch(clonePath, errand.branch); } catch { /* no-op */ }
|
||||
}
|
||||
}
|
||||
@@ -489,41 +457,24 @@ export function errandProcedures(publicProcedure: ProcedureBuilder) {
|
||||
throw new TRPCError({ code: 'NOT_FOUND', message: 'Project not found' });
|
||||
}
|
||||
|
||||
// Resolve clone path and verify worktree still exists
|
||||
const clonePath = await resolveClonePath(project, ctx);
|
||||
const worktreeManager = new SimpleGitWorktreeManager(clonePath);
|
||||
let worktree;
|
||||
try {
|
||||
worktree = await worktreeManager.get(errand.id);
|
||||
} catch {
|
||||
throw new TRPCError({
|
||||
code: 'BAD_REQUEST',
|
||||
message: 'Worktree no longer exists — cannot request changes. Delete and re-create the errand.',
|
||||
});
|
||||
}
|
||||
if (!worktree) {
|
||||
throw new TRPCError({
|
||||
code: 'BAD_REQUEST',
|
||||
message: 'Worktree no longer exists — cannot request changes. Delete and re-create the errand.',
|
||||
});
|
||||
}
|
||||
|
||||
// Clean up stale signal.json to prevent false completion detection
|
||||
const signalPath = join(worktree.path, '.cw', 'output', 'signal.json');
|
||||
if (existsSync(signalPath)) {
|
||||
rmSync(signalPath);
|
||||
}
|
||||
|
||||
// Build revision prompt and spawn new agent in existing worktree
|
||||
const prompt = buildErrandRevisionPrompt(errand.description, input.feedback);
|
||||
const agentManager = requireAgentManager(ctx);
|
||||
|
||||
// Stop and delete old agent — committed work is preserved on the branch
|
||||
if (errand.agentId) {
|
||||
try { await agentManager.stop(errand.agentId); } catch { /* no-op */ }
|
||||
try { await agentManager.delete(errand.agentId); } catch { /* no-op */ }
|
||||
}
|
||||
|
||||
// Spawn fresh agent on the same branch (all committed work is preserved)
|
||||
const prompt = buildErrandRevisionPrompt(errand.description, input.feedback);
|
||||
let agent;
|
||||
try {
|
||||
agent = await agentManager.spawn({
|
||||
prompt,
|
||||
mode: 'errand',
|
||||
cwd: worktree.path,
|
||||
provider: undefined,
|
||||
projectId: errand.projectId!,
|
||||
branchName: errand.branch,
|
||||
baseBranch: errand.baseBranch,
|
||||
});
|
||||
} catch (err) {
|
||||
throw new TRPCError({
|
||||
@@ -533,8 +484,9 @@ export function errandProcedures(publicProcedure: ProcedureBuilder) {
|
||||
}
|
||||
|
||||
// Update manifest files
|
||||
const agentWorkdir = join(ctx.workspaceRoot!, 'agent-workdirs', agent.name, project.name);
|
||||
await writeErrandManifest({
|
||||
agentWorkdir: worktree.path,
|
||||
agentWorkdir,
|
||||
errandId: errand.id,
|
||||
description: errand.description,
|
||||
branch: errand.branch,
|
||||
|
||||
Reference in New Issue
Block a user