Key changes: - Add agent names (human-readable like 'gastown') instead of UUID-only - Use Claude CLI with --output-format json instead of SDK streaming - Session ID extracted from CLI JSON output, not SDK init message - Add waiting_for_input status for AskUserQuestion scenarios - Resume flow is for answering agent questions, not general resumption - CLI commands use names as primary identifier
10 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous
| phase | plan | type | wave | depends_on | files_modified | autonomous | |||||
|---|---|---|---|---|---|---|---|---|---|---|---|
| 04-agent-lifecycle | 04 | execute | 3 |
|
|
true |
Purpose: Enable users to spawn, stop, list, and manage agents via CLI (AGENT-01, 02, 03). Output: tRPC procedures and CLI commands for full agent lifecycle management.
<execution_context>
@/.claude/get-shit-done/workflows/execute-plan.md
@/.claude/get-shit-done/templates/summary.md
</execution_context>
@src/trpc/router.ts @src/trpc/context.ts @src/cli/index.ts @src/cli/trpc-client.ts
Task 1: Add AgentManager to tRPC context src/trpc/context.ts Update tRPC context to include AgentManager:- Import AgentManager and ClaudeAgentManager
- Add agentManager to context type
- Create agentManager in context factory (requires repository and worktreeManager from context)
The context should wire up:
- AgentRepository (from database)
- WorktreeManager (from git module)
- EventBus (optional, for event emission)
Example pattern from existing context:
export interface Context {
// ... existing
agentManager: AgentManager;
}
export function createContext(): Context {
// ... existing setup
const agentRepository = new DrizzleAgentRepository(db);
const agentManager = new ClaudeAgentManager(
agentRepository,
worktreeManager,
eventBus
);
return {
// ... existing
agentManager,
};
}
Note: Context may need to be async if database/worktree setup is async. npm run build passes AgentManager available in tRPC context
Task 2: Add agent procedures to tRPC router src/trpc/router.ts Add agent procedures following existing patterns:// Add to router.ts
import { z } from 'zod';
// Input schemas - support lookup by name OR id
const spawnAgentInput = z.object({
name: z.string(), // Human-readable name (required)
taskId: z.string(),
prompt: z.string(),
cwd: z.string().optional(),
});
const agentIdentifier = z.object({
name: z.string().optional(), // Lookup by name (preferred)
id: z.string().optional(), // Or by ID
}).refine(data => data.name || data.id, {
message: 'Either name or id must be provided',
});
const resumeAgentInput = z.object({
name: z.string().optional(),
id: z.string().optional(),
prompt: z.string(),
}).refine(data => data.name || data.id, {
message: 'Either name or id must be provided',
});
// Helper to resolve agent by name or id
async function resolveAgent(ctx: Context, input: { name?: string; id?: string }) {
if (input.name) {
return ctx.agentManager.getByName(input.name);
}
return ctx.agentManager.get(input.id!);
}
// Add to router
export const appRouter = router({
// ... existing procedures
// Agent procedures
spawnAgent: procedure
.input(spawnAgentInput)
.mutation(async ({ ctx, input }) => {
const agent = await ctx.agentManager.spawn({
name: input.name,
taskId: input.taskId,
prompt: input.prompt,
cwd: input.cwd,
});
return agent;
}),
stopAgent: procedure
.input(agentIdentifier)
.mutation(async ({ ctx, input }) => {
const agent = await resolveAgent(ctx, input);
if (!agent) throw new Error('Agent not found');
await ctx.agentManager.stop(agent.id);
return { success: true, name: agent.name };
}),
listAgents: procedure
.query(async ({ ctx }) => {
return ctx.agentManager.list();
}),
getAgent: procedure
.input(agentIdentifier)
.query(async ({ ctx, input }) => {
return resolveAgent(ctx, input);
}),
getAgentByName: procedure
.input(z.object({ name: z.string() }))
.query(async ({ ctx, input }) => {
return ctx.agentManager.getByName(input.name);
}),
resumeAgent: procedure
.input(resumeAgentInput)
.mutation(async ({ ctx, input }) => {
const agent = await resolveAgent(ctx, input);
if (!agent) throw new Error('Agent not found');
await ctx.agentManager.resume(agent.id, input.prompt);
return { success: true, name: agent.name };
}),
getAgentResult: procedure
.input(agentIdentifier)
.query(async ({ ctx, input }) => {
const agent = await resolveAgent(ctx, input);
if (!agent) return null;
return ctx.agentManager.getResult(agent.id);
}),
});
Export updated AppRouter type for client. npm run build passes Agent tRPC procedures added: spawn, stop, list, get, resume, getResult
Task 3: Add agent CLI commands src/cli/index.ts Add CLI commands for agent management using existing tRPC client pattern:// Add commands to CLI - use NAMES as primary identifier (like gastown)
// cw agent spawn --name <name> --task <taskId> <prompt>
// Example: cw agent spawn --name gastown --task task-123 "Fix the auth bug"
program
.command('agent spawn <prompt>')
.description('Spawn a new agent to work on a task')
.requiredOption('--name <name>', 'Human-readable name for the agent (e.g., gastown)')
.requiredOption('--task <taskId>', 'Task ID to assign to agent')
.option('--cwd <path>', 'Working directory for agent')
.action(async (prompt: string, options: { name: string; task: string; cwd?: string }) => {
const client = await getTrpcClient();
try {
const agent = await client.spawnAgent.mutate({
name: options.name,
taskId: options.task,
prompt,
cwd: options.cwd,
});
console.log(`Agent '${agent.name}' spawned`);
console.log(` ID: ${agent.id}`);
console.log(` Task: ${agent.taskId}`);
console.log(` Status: ${agent.status}`);
console.log(` Worktree: ${agent.worktreeId}`);
} catch (error) {
console.error('Failed to spawn agent:', error);
process.exit(1);
}
});
// cw agent stop <name>
// Example: cw agent stop gastown
program
.command('agent stop <name>')
.description('Stop a running agent by name')
.action(async (name: string) => {
const client = await getTrpcClient();
try {
const result = await client.stopAgent.mutate({ name });
console.log(`Agent '${result.name}' stopped`);
} catch (error) {
console.error('Failed to stop agent:', error);
process.exit(1);
}
});
// cw agent list
program
.command('agent list')
.description('List all agents')
.action(async () => {
const client = await getTrpcClient();
try {
const agents = await client.listAgents.query();
if (agents.length === 0) {
console.log('No agents found');
return;
}
console.log('Agents:');
for (const agent of agents) {
const status = agent.status === 'waiting_for_input' ? 'WAITING' : agent.status.toUpperCase();
console.log(` ${agent.name} [${status}] - ${agent.taskId}`);
}
} catch (error) {
console.error('Failed to list agents:', error);
process.exit(1);
}
});
// cw agent get <name>
// Example: cw agent get gastown
program
.command('agent get <name>')
.description('Get agent details by name')
.action(async (name: string) => {
const client = await getTrpcClient();
try {
const agent = await client.getAgent.query({ name });
if (!agent) {
console.log(`Agent '${name}' not found`);
return;
}
console.log(`Agent: ${agent.name}`);
console.log(` ID: ${agent.id}`);
console.log(` Task: ${agent.taskId}`);
console.log(` Session: ${agent.sessionId ?? '(none)'}`);
console.log(` Worktree: ${agent.worktreeId}`);
console.log(` Status: ${agent.status}`);
console.log(` Created: ${agent.createdAt}`);
console.log(` Updated: ${agent.updatedAt}`);
} catch (error) {
console.error('Failed to get agent:', error);
process.exit(1);
}
});
// cw agent resume <name> <response>
// Example: cw agent resume gastown "Use option A"
program
.command('agent resume <name> <response>')
.description('Resume an agent that is waiting for input')
.action(async (name: string, response: string) => {
const client = await getTrpcClient();
try {
const result = await client.resumeAgent.mutate({ name, prompt: response });
console.log(`Agent '${result.name}' resumed`);
} catch (error) {
console.error('Failed to resume agent:', error);
process.exit(1);
}
});
// cw agent result <name>
// Example: cw agent result gastown
program
.command('agent result <name>')
.description('Get agent execution result')
.action(async (name: string) => {
const client = await getTrpcClient();
try {
const result = await client.getAgentResult.query({ name });
if (!result) {
console.log('No result available (agent may still be running)');
return;
}
console.log(`Result: ${result.success ? 'SUCCESS' : 'FAILED'}`);
console.log(` Message: ${result.message}`);
if (result.filesModified?.length) {
console.log(` Files modified: ${result.filesModified.join(', ')}`);
}
} catch (error) {
console.error('Failed to get result:', error);
process.exit(1);
}
});
Commands use commander.js pattern from existing CLI. npm run build passes, cw agent --help shows commands CLI commands added: agent spawn, stop, list, get, resume, result
Before declaring plan complete: - [ ] npm run build succeeds without errors - [ ] cw agent --help shows all agent commands - [ ] Agent procedures accessible via tRPC client - [ ] All 6 requirements satisfied (AGENT-01 through AGENT-07 except AGENT-06)<success_criteria>
- All tasks completed
- All verification checks pass
- No errors or warnings introduced
- Users can manage agents via CLI </success_criteria>