feat: Add errand tRPC router with all 9 procedures and comprehensive tests
Implements the errand workflow for small isolated changes that spawn a dedicated agent in a git worktree: - errand.create: branch + worktree + DB record + agent spawn - errand.list / errand.get / errand.diff: read procedures - errand.complete: transitions active→pending_review, stops agent - errand.merge: merges branch, handles conflicts with conflictFiles - errand.delete / errand.abandon: cleanup worktree, branch, agent - errand.sendMessage: delivers user message directly to running agent Supporting changes: - Add 'errand' to AgentMode union and agents.mode enum - Add sendUserMessage() to AgentManager interface and MockAgentManager - MockAgentManager now accepts optional agentRepository to persist agents to the DB (required for FK constraint satisfaction in tests) - Add ORDER BY createdAt DESC, id DESC to errand findAll - Fix dispatch/manager.test.ts missing sendUserMessage mock Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -26,6 +26,7 @@ import type {
|
||||
AgentDeletedEvent,
|
||||
AgentWaitingEvent,
|
||||
} from '../events/index.js';
|
||||
import type { AgentRepository } from '../db/repositories/agent-repository.js';
|
||||
|
||||
/**
|
||||
* Scenario configuration for mock agent behavior.
|
||||
@@ -83,10 +84,12 @@ export class MockAgentManager implements AgentManager {
|
||||
private scenarioOverrides: Map<string, MockAgentScenario> = new Map();
|
||||
private defaultScenario: MockAgentScenario;
|
||||
private eventBus?: EventBus;
|
||||
private agentRepository?: AgentRepository;
|
||||
|
||||
constructor(options?: { eventBus?: EventBus; defaultScenario?: MockAgentScenario }) {
|
||||
constructor(options?: { eventBus?: EventBus; defaultScenario?: MockAgentScenario; agentRepository?: AgentRepository }) {
|
||||
this.eventBus = options?.eventBus;
|
||||
this.defaultScenario = options?.defaultScenario ?? DEFAULT_SCENARIO;
|
||||
this.agentRepository = options?.agentRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -111,7 +114,7 @@ export class MockAgentManager implements AgentManager {
|
||||
* Completion happens async via setTimeout (even if delay=0).
|
||||
*/
|
||||
async spawn(options: SpawnAgentOptions): Promise<AgentInfo> {
|
||||
const { taskId, prompt } = options;
|
||||
const { taskId } = options;
|
||||
const name = options.name ?? `agent-${taskId?.slice(0, 6) ?? 'noTask'}`;
|
||||
|
||||
// Check name uniqueness
|
||||
@@ -121,11 +124,29 @@ export class MockAgentManager implements AgentManager {
|
||||
}
|
||||
}
|
||||
|
||||
const agentId = randomUUID();
|
||||
const sessionId = randomUUID();
|
||||
const worktreeId = randomUUID();
|
||||
const now = new Date();
|
||||
|
||||
// Persist to agentRepository when provided (required for FK constraints in tests)
|
||||
let agentId: string;
|
||||
if (this.agentRepository) {
|
||||
const dbAgent = await this.agentRepository.create({
|
||||
name,
|
||||
worktreeId,
|
||||
taskId: taskId ?? null,
|
||||
initiativeId: options.initiativeId ?? null,
|
||||
sessionId,
|
||||
status: 'running',
|
||||
mode: options.mode ?? 'execute',
|
||||
provider: options.provider ?? 'claude',
|
||||
accountId: null,
|
||||
});
|
||||
agentId = dbAgent.id;
|
||||
} else {
|
||||
agentId = randomUUID();
|
||||
}
|
||||
|
||||
// Determine scenario (override takes precedence — use original name or generated)
|
||||
const scenario = this.scenarioOverrides.get(name) ?? this.defaultScenario;
|
||||
|
||||
@@ -507,6 +528,18 @@ export class MockAgentManager implements AgentManager {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deliver a user message to a running errand agent.
|
||||
* Mock implementation: no-op (simulates message delivery without actual process interaction).
|
||||
*/
|
||||
async sendUserMessage(agentId: string, _message: string): Promise<void> {
|
||||
const record = this.agents.get(agentId);
|
||||
if (!record) {
|
||||
throw new Error(`Agent '${agentId}' not found`);
|
||||
}
|
||||
// Mock: succeed silently — message delivery is a no-op in tests
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all agents and pending timers.
|
||||
* Useful for test cleanup.
|
||||
|
||||
@@ -15,7 +15,7 @@ export type AgentStatus = 'idle' | 'running' | 'waiting_for_input' | 'stopped' |
|
||||
* - plan: Plan initiative into phases
|
||||
* - detail: Detail phase into individual tasks
|
||||
*/
|
||||
export type AgentMode = 'execute' | 'discuss' | 'plan' | 'detail' | 'refine' | 'chat';
|
||||
export type AgentMode = 'execute' | 'discuss' | 'plan' | 'detail' | 'refine' | 'chat' | 'errand';
|
||||
|
||||
/**
|
||||
* Context data written as input files in agent workdir before spawn.
|
||||
@@ -257,4 +257,14 @@ export interface AgentManager {
|
||||
question: string,
|
||||
fromAgentId: string,
|
||||
): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Deliver a user message to a running errand agent.
|
||||
* Does not use the conversations table — the message is injected directly
|
||||
* into the agent's Claude Code session as a resume prompt.
|
||||
*
|
||||
* @param agentId - The errand agent to message
|
||||
* @param message - The user's message text
|
||||
*/
|
||||
sendUserMessage(agentId: string, message: string): Promise<void>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user