feat: errand review & request changes
Add errand.requestChanges procedure that re-spawns an agent in the existing worktree with user feedback. Replace raw <pre> diff blocks with syntax-highlighted ErrandDiffView using FileCard components. Add Output/Changes tabs to the active errand view.
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Errand Router
|
||||
*
|
||||
* All 9 errand procedures: create, list, get, diff, complete, merge, delete, sendMessage, abandon.
|
||||
* All 10 errand procedures: create, list, get, diff, complete, merge, delete, sendMessage, abandon, requestChanges.
|
||||
* Errands are small isolated changes that spawn a dedicated agent in a git worktree.
|
||||
*/
|
||||
|
||||
@@ -17,8 +17,9 @@ import {
|
||||
requireBranchManager,
|
||||
} from './_helpers.js';
|
||||
import { writeErrandManifest } from '../../agent/file-io.js';
|
||||
import { buildErrandPrompt } from '../../agent/prompts/index.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';
|
||||
@@ -441,6 +442,96 @@ export function errandProcedures(publicProcedure: ProcedureBuilder) {
|
||||
const updated = await repo.update(input.id, { status: 'abandoned' });
|
||||
return updated;
|
||||
}),
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// errand.requestChanges
|
||||
// -----------------------------------------------------------------------
|
||||
requestChanges: publicProcedure
|
||||
.input(z.object({
|
||||
id: z.string().min(1),
|
||||
feedback: z.string().min(1),
|
||||
}))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const repo = requireErrandRepository(ctx);
|
||||
const errand = await repo.findById(input.id);
|
||||
if (!errand) {
|
||||
throw new TRPCError({ code: 'NOT_FOUND', message: 'Errand not found' });
|
||||
}
|
||||
|
||||
if (errand.status !== 'pending_review' && errand.status !== 'conflict') {
|
||||
throw new TRPCError({
|
||||
code: 'BAD_REQUEST',
|
||||
message: `Cannot request changes on an errand with status '${errand.status}'`,
|
||||
});
|
||||
}
|
||||
|
||||
if (!errand.projectId) {
|
||||
throw new TRPCError({ code: 'NOT_FOUND', message: 'Errand has no project' });
|
||||
}
|
||||
const project = await requireProjectRepository(ctx).findById(errand.projectId);
|
||||
if (!project) {
|
||||
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);
|
||||
let agent;
|
||||
try {
|
||||
agent = await agentManager.spawn({
|
||||
prompt,
|
||||
mode: 'errand',
|
||||
cwd: worktree.path,
|
||||
provider: undefined,
|
||||
});
|
||||
} catch (err) {
|
||||
throw new TRPCError({
|
||||
code: 'INTERNAL_SERVER_ERROR',
|
||||
message: err instanceof Error ? err.message : String(err),
|
||||
});
|
||||
}
|
||||
|
||||
// Update manifest files
|
||||
await writeErrandManifest({
|
||||
agentWorkdir: worktree.path,
|
||||
errandId: errand.id,
|
||||
description: errand.description,
|
||||
branch: errand.branch,
|
||||
projectName: project.name,
|
||||
agentId: agent.id,
|
||||
agentName: agent.name,
|
||||
});
|
||||
|
||||
// Transition back to active with new agent
|
||||
await repo.update(errand.id, { status: 'active', agentId: agent.id });
|
||||
|
||||
return { id: errand.id, agentId: agent.id };
|
||||
}),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user