/** * Codewalk District CLI * * Commander-based CLI with help system and version display. * Supports server mode via --server flag. */ import { Command } from 'commander'; import { VERSION } from '../index.js'; import { CoordinationServer } from '../server/index.js'; import { GracefulShutdown } from '../server/shutdown.js'; import { ProcessManager, ProcessRegistry } from '../process/index.js'; import { LogManager } from '../logging/index.js'; import { createDefaultTrpcClient } from './trpc-client.js'; /** Environment variable for custom port */ const CW_PORT_ENV = 'CW_PORT'; /** * Starts the coordination server in foreground mode. * Server runs until terminated via SIGTERM/SIGINT. */ async function startServer(port?: number): Promise { // Get port from option, env var, or default const serverPort = port ?? (process.env[CW_PORT_ENV] ? parseInt(process.env[CW_PORT_ENV], 10) : undefined); // Create dependencies const registry = new ProcessRegistry(); const processManager = new ProcessManager(registry); const logManager = new LogManager(); // Create and start server const server = new CoordinationServer( { port: serverPort }, processManager, logManager ); try { await server.start(); } catch (error) { console.error('Failed to start server:', (error as Error).message); process.exit(1); } // Install graceful shutdown handlers const shutdown = new GracefulShutdown(server, processManager, logManager); shutdown.install(); } /** * Creates and configures the CLI program. * * @param serverHandler - Optional handler to be called for server mode * @returns Configured Commander program ready for parsing */ export function createCli(serverHandler?: (port?: number) => Promise): Command { const program = new Command(); program .name('cw') .description('Multi-agent workspace for orchestrating multiple Claude Code agents') .version(VERSION, '-v, --version', 'Display version number'); // Server mode option (global flag) program .option('-s, --server', 'Start the coordination server') .option('-p, --port ', 'Port for the server (default: 3847, env: CW_PORT)', parseInt); // Handle the case where --server is provided without a command // This makes --server work as a standalone action program.hook('preAction', async (_thisCommand, _actionCommand) => { const opts = program.opts(); if (opts.server && serverHandler) { await serverHandler(opts.port); process.exit(0); } }); // Status command - shows workspace status via tRPC program .command('status') .description('Show workspace status') .action(async () => { try { const client = createDefaultTrpcClient(); const health = await client.health.query(); console.log('Coordination Server Status'); console.log('=========================='); console.log(`Status: ${health.status}`); console.log(`Uptime: ${health.uptime}s`); console.log(`Processes: ${health.processCount}`); } catch { console.log('Server not running or unreachable. Start with: cw --server'); } }); // Agent command group const agentCommand = program .command('agent') .description('Manage agents'); // cw agent spawn --name --task agentCommand .command('spawn ') .description('Spawn a new agent to work on a task') .requiredOption('--name ', 'Human-readable name for the agent (e.g., gastown)') .requiredOption('--task ', 'Task ID to assign to agent') .option('--cwd ', 'Working directory for agent') .action(async (prompt: string, options: { name: string; task: string; cwd?: string }) => { try { const client = createDefaultTrpcClient(); 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 as Error).message); process.exit(1); } }); // cw agent stop agentCommand .command('stop ') .description('Stop a running agent by name') .action(async (name: string) => { try { const client = createDefaultTrpcClient(); const result = await client.stopAgent.mutate({ name }); console.log(`Agent '${result.name}' stopped`); } catch (error) { console.error('Failed to stop agent:', (error as Error).message); process.exit(1); } }); // cw agent list agentCommand .command('list') .description('List all agents') .action(async () => { try { const client = createDefaultTrpcClient(); 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 as Error).message); process.exit(1); } }); // cw agent get agentCommand .command('get ') .description('Get agent details by name') .action(async (name: string) => { try { const client = createDefaultTrpcClient(); 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 as Error).message); process.exit(1); } }); // cw agent resume agentCommand .command('resume ') .description('Resume an agent that is waiting for input') .action(async (name: string, response: string) => { try { const client = createDefaultTrpcClient(); const result = await client.resumeAgent.mutate({ name, prompt: response }); console.log(`Agent '${result.name}' resumed`); } catch (error) { console.error('Failed to resume agent:', (error as Error).message); process.exit(1); } }); // cw agent result agentCommand .command('result ') .description('Get agent execution result') .action(async (name: string) => { try { const client = createDefaultTrpcClient(); 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 as Error).message); process.exit(1); } }); // Task command group const taskCommand = program .command('task') .description('Manage tasks'); // cw task list --plan taskCommand .command('list') .description('List tasks for a plan') .requiredOption('--plan ', 'Plan ID to list tasks for') .action(async (options: { plan: string }) => { try { const client = createDefaultTrpcClient(); const tasks = await client.listTasks.query({ planId: options.plan }); if (tasks.length === 0) { console.log('No tasks found for this plan'); return; } // Count by status const pending = tasks.filter(t => t.status === 'pending' || t.status === 'pending_approval').length; const inProgress = tasks.filter(t => t.status === 'in_progress').length; const completed = tasks.filter(t => t.status === 'completed').length; const blocked = tasks.filter(t => t.status === 'blocked').length; console.log('Tasks:'); console.log(''); for (const task of tasks) { const statusLabel = task.status === 'in_progress' ? 'IN_PROGRESS' : task.status.toUpperCase(); const priorityLabel = task.priority === 'high' ? '[HIGH]' : task.priority === 'low' ? '[low]' : ''; console.log(` ${task.order}. ${task.name} [${statusLabel}] ${task.type} ${priorityLabel}`); } console.log(''); console.log(`Summary: ${pending} pending, ${inProgress} in progress, ${completed} completed, ${blocked} blocked`); } catch (error) { console.error('Failed to list tasks:', (error as Error).message); process.exit(1); } }); // cw task get taskCommand .command('get ') .description('Get task details by ID') .action(async (taskId: string) => { try { const client = createDefaultTrpcClient(); const task = await client.getTask.query({ id: taskId }); console.log(`Task: ${task.name}`); console.log(` ID: ${task.id}`); console.log(` Plan: ${task.planId}`); console.log(` Description: ${task.description ?? '(none)'}`); console.log(` Type: ${task.type}`); console.log(` Priority: ${task.priority}`); console.log(` Status: ${task.status}`); console.log(` Order: ${task.order}`); console.log(` Created: ${task.createdAt}`); console.log(` Updated: ${task.updatedAt}`); } catch (error) { console.error('Failed to get task:', (error as Error).message); process.exit(1); } }); // cw task status taskCommand .command('status ') .description('Update task status (pending, in_progress, completed, blocked)') .action(async (taskId: string, status: string) => { const validStatuses = ['pending', 'in_progress', 'completed', 'blocked']; if (!validStatuses.includes(status)) { console.error(`Invalid status: ${status}`); console.error(`Valid statuses: ${validStatuses.join(', ')}`); process.exit(1); } try { const client = createDefaultTrpcClient(); const task = await client.updateTaskStatus.mutate({ id: taskId, status: status as 'pending' | 'in_progress' | 'completed' | 'blocked', }); console.log(`Task '${task.name}' status updated to: ${task.status}`); } catch (error) { console.error('Failed to update task status:', (error as Error).message); process.exit(1); } }); // Message command group const messageCommand = program .command('message') .description('View agent messages and questions'); // cw message list [--agent ] [--status ] messageCommand .command('list') .description('List messages from agents') .option('--agent ', 'Filter by agent ID') .option('--status ', 'Filter by status (pending, read, responded)') .action(async (options: { agent?: string; status?: string }) => { // Validate status if provided if (options.status && !['pending', 'read', 'responded'].includes(options.status)) { console.error(`Invalid status: ${options.status}`); console.error('Valid statuses: pending, read, responded'); process.exit(1); } try { const client = createDefaultTrpcClient(); const messages = await client.listMessages.query({ agentId: options.agent, status: options.status as 'pending' | 'read' | 'responded' | undefined, }); if (messages.length === 0) { console.log('No messages found'); return; } // Count pending const pendingCount = messages.filter(m => m.status === 'pending').length; if (pendingCount > 0) { console.log(`(${pendingCount} pending)\n`); } console.log('Messages:'); console.log(''); for (const msg of messages) { const idShort = msg.id.slice(0, 8); const agentId = msg.senderId?.slice(0, 8) ?? 'user'; const content = msg.content.length > 50 ? msg.content.slice(0, 47) + '...' : msg.content; const statusLabel = msg.status.toUpperCase(); const createdAt = new Date(msg.createdAt).toLocaleString(); console.log(` ${idShort} ${agentId} ${msg.type} [${statusLabel}] ${createdAt}`); console.log(` ${content}`); } } catch (error) { console.error('Failed to list messages:', (error as Error).message); process.exit(1); } }); // cw message read messageCommand .command('read ') .description('Read full message content') .action(async (messageId: string) => { try { const client = createDefaultTrpcClient(); const message = await client.getMessage.query({ id: messageId }); console.log(`Message: ${message.id}`); console.log(` From: ${message.senderType} (${message.senderId ?? 'user'})`); console.log(` To: ${message.recipientType} (${message.recipientId ?? 'user'})`); console.log(` Type: ${message.type}`); console.log(` Status: ${message.status}`); console.log(` Created: ${new Date(message.createdAt).toLocaleString()}`); console.log(''); console.log('Content:'); console.log(message.content); if (message.status === 'pending' && message.requiresResponse) { console.log(''); console.log('---'); console.log('This message requires a response.'); console.log(`Use: cw message respond ${message.id} ""`); } } catch (error) { console.error('Failed to read message:', (error as Error).message); process.exit(1); } }); // cw message respond messageCommand .command('respond ') .description('Respond to a message') .action(async (messageId: string, response: string) => { try { const client = createDefaultTrpcClient(); const responseMessage = await client.respondToMessage.mutate({ id: messageId, response, }); console.log(`Response recorded (${responseMessage.id.slice(0, 8)})`); console.log(''); console.log('If the agent is waiting, you may want to resume it:'); console.log(' cw agent list (to see waiting agents)'); console.log(' cw agent resume ""'); } catch (error) { console.error('Failed to respond to message:', (error as Error).message); process.exit(1); } }); // Dispatch command group const dispatchCommand = program .command('dispatch') .description('Control task dispatch queue'); // cw dispatch queue dispatchCommand .command('queue ') .description('Queue a task for dispatch') .action(async (taskId: string) => { try { const client = createDefaultTrpcClient(); await client.queueTask.mutate({ taskId }); console.log(`Task '${taskId}' queued for dispatch`); } catch (error) { console.error('Failed to queue task:', (error as Error).message); process.exit(1); } }); // cw dispatch next dispatchCommand .command('next') .description('Dispatch next available task to an agent') .action(async () => { try { const client = createDefaultTrpcClient(); const result = await client.dispatchNext.mutate(); if (result.success) { console.log('Task dispatched successfully'); console.log(` Task: ${result.taskId}`); console.log(` Agent: ${result.agentId}`); } else { console.log('Dispatch failed'); console.log(` Task: ${result.taskId || '(none)'}`); console.log(` Reason: ${result.reason}`); } } catch (error) { console.error('Failed to dispatch:', (error as Error).message); process.exit(1); } }); // cw dispatch status dispatchCommand .command('status') .description('Show dispatch queue status') .action(async () => { try { const client = createDefaultTrpcClient(); const state = await client.getQueueState.query(); console.log('Dispatch Queue Status'); console.log('====================='); console.log(`Queued: ${state.queued.length}`); console.log(`Ready: ${state.ready.length}`); console.log(`Blocked: ${state.blocked.length}`); if (state.ready.length > 0) { console.log(''); console.log('Ready tasks:'); for (const task of state.ready) { const priority = task.priority === 'high' ? '[HIGH]' : task.priority === 'low' ? '[low]' : ''; console.log(` ${task.taskId} ${priority}`); } } if (state.blocked.length > 0) { console.log(''); console.log('Blocked tasks:'); for (const bt of state.blocked) { console.log(` ${bt.taskId}: ${bt.reason}`); } } } catch (error) { console.error('Failed to get queue status:', (error as Error).message); process.exit(1); } }); // cw dispatch complete dispatchCommand .command('complete ') .description('Mark a task as complete') .action(async (taskId: string) => { try { const client = createDefaultTrpcClient(); await client.completeTask.mutate({ taskId }); console.log(`Task '${taskId}' marked as complete`); } catch (error) { console.error('Failed to complete task:', (error as Error).message); process.exit(1); } }); // Merge command group const mergeCommand = program .command('merge') .description('Manage merge queue'); // cw merge queue mergeCommand .command('queue ') .description('Queue a completed task for merge') .action(async (taskId: string) => { try { const client = createDefaultTrpcClient(); await client.queueMerge.mutate({ taskId }); console.log(`Task '${taskId}' queued for merge`); } catch (error) { console.error('Failed to queue for merge:', (error as Error).message); process.exit(1); } }); // cw merge status mergeCommand .command('status') .description('Show merge queue status') .action(async () => { try { const client = createDefaultTrpcClient(); const state = await client.getMergeQueueStatus.query(); console.log('Merge Queue Status'); console.log('=================='); console.log(`Queued: ${state.queued.length}`); console.log(`In Progress: ${state.inProgress.length}`); console.log(`Merged: ${state.merged.length}`); console.log(`Conflicted: ${state.conflicted.length}`); if (state.conflicted.length > 0) { console.log(''); console.log('Conflicts:'); for (const c of state.conflicted) { console.log(` ${c.taskId}: ${c.conflicts.join(', ')}`); } } } catch (error) { console.error('Failed to get merge queue status:', (error as Error).message); process.exit(1); } }); // cw merge next mergeCommand .command('next') .description('Show next task ready to merge') .action(async () => { try { const client = createDefaultTrpcClient(); const next = await client.getNextMergeable.query(); if (next) { console.log(`Next mergeable: ${next.taskId} (priority: ${next.priority})`); } else { console.log('No tasks ready to merge'); } } catch (error) { console.error('Failed to get next mergeable:', (error as Error).message); process.exit(1); } }); // Coordinate command - process all ready merges program .command('coordinate') .description('Process all ready merges in dependency order') .option('-t, --target ', 'Target branch for merges', 'main') .action(async (options: { target: string }) => { try { const client = createDefaultTrpcClient(); console.log(`Processing merges to ${options.target}...`); const { results } = await client.processMerges.mutate({ targetBranch: options.target, }); const succeeded = results.filter(r => r.success).length; const conflicted = results.filter(r => !r.success).length; console.log(''); console.log('Results:'); console.log(` Merged: ${succeeded}`); console.log(` Conflicts: ${conflicted}`); if (conflicted > 0) { console.log(''); console.log('Conflicted tasks (bounce-back tasks created):'); for (const r of results.filter(r => !r.success)) { console.log(` ${r.taskId}: ${r.conflicts?.join(', ')}`); } } } catch (error) { console.error('Failed to process merges:', (error as Error).message); process.exit(1); } }); return program; } /** * Runs the CLI, handling server mode and commands. */ export async function runCli(): Promise { // Check for server flag early, before Commander processes const hasServerFlag = process.argv.includes('--server') || process.argv.includes('-s'); if (hasServerFlag) { // Get port from args if present const portIndex = process.argv.findIndex(arg => arg === '-p' || arg === '--port'); const port = portIndex !== -1 && process.argv[portIndex + 1] ? parseInt(process.argv[portIndex + 1], 10) : undefined; await startServer(port); // Server runs indefinitely until signal return; } // Normal CLI processing const program = createCli(); program.parse(process.argv); }